Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Встала передо мной такая вот проблема — надо передавать данные между двумя микроконтроллерами STM32F407 хотя бы на скорости 100 Mbps. Можно было бы использовать Ethernet (MAC-to-MAC), но вот беда — он занят, именно из него и берутся эти данные…
Из незадействованной периферии есть разве что SPI — но он только 42 Mbps.
Как ни странно, ничего готового в сети не нашлось. И я решил реализовать параллельный тактируемый регистр на 8 бит. А что — частоту можно задать в 10 Мгц (то есть, конечно, собственно такты вдвое быстрее, но и 20 Мгц не есть что-то сложное) — так что с такой невысокой частотой не придется мучиться с разводкой платы. А скорость как раз и будет 100 Mbps.
Сказано — сделано. В общем виде система выглядит так. На передающей стороне используем таймер, один из сигналов сравнения выводим на пин — это будет тактовый сигнал, а второй будем использовать для запуска одной пересылки (burst) для DMA.
Шина у меня на частоте 82 МГц (из-за потребления тока на большей частоте :), таймер на той же частоте: так что при периоде ARR = 8 получается 10 Мгц примерно (стало быть будет около 80 Mbps, ну да и ладно).
DMA будет по такту пересылать один байт из памяти (с автоинкрементом, конечно) прямо в порт вывода регистра — в моем случае подошел PORTE — его первые 8 бит как раз и подходят как адрес приемника DMA.
На приемной стороне тактовый сигнал будем по обоим перепадам использовать для тактирования таймера, с периодом 1, а сигнал update будем использовать для запуска пересылки для DMA, который читает данные из порта (опять подошел порт PORTE) и записывает в память с автоинкрементом.
Теперь осталось правильно настроить все (код ниже) и запустить. Завершение на обеих сторонах определяется по прерыванию от DMA.
Однако, для полноты картины нужно конечно включить в код проверки на задержки передачи и обработку ошибок, но это я опускаю.
В коде ниже у таймера TIM8 использован канал CC2 для вывода сигнала — чтобы посмотреть, что получается.
Для тестов была использована одна и та же плата, просто тактовый выход PE9 был соединен с входом PC6. Главный цикл выглядел так:
По результатам: данные отлично пересылались за 30-31 микросекунду без потерь. Сигналы выглядят как-то так:
здесь белый — выход таймера TIM8, красный — тактовый сигнал (TIM1), ну а оранжевый — это младший бит данных (0-1-0-1-...).
Что не нравится при этом — ну нельзя никак запускать DMA от прерывания от входа GPIO, вот и приходится работать с таймерами. Может, кто-нибудь подскажет другой способ?
Из незадействованной периферии есть разве что SPI — но он только 42 Mbps.
Как ни странно, ничего готового в сети не нашлось. И я решил реализовать параллельный тактируемый регистр на 8 бит. А что — частоту можно задать в 10 Мгц (то есть, конечно, собственно такты вдвое быстрее, но и 20 Мгц не есть что-то сложное) — так что с такой невысокой частотой не придется мучиться с разводкой платы. А скорость как раз и будет 100 Mbps.
Сказано — сделано. В общем виде система выглядит так. На передающей стороне используем таймер, один из сигналов сравнения выводим на пин — это будет тактовый сигнал, а второй будем использовать для запуска одной пересылки (burst) для DMA.
Шина у меня на частоте 82 МГц (из-за потребления тока на большей частоте :), таймер на той же частоте: так что при периоде ARR = 8 получается 10 Мгц примерно (стало быть будет около 80 Mbps, ну да и ладно).
DMA будет по такту пересылать один байт из памяти (с автоинкрементом, конечно) прямо в порт вывода регистра — в моем случае подошел PORTE — его первые 8 бит как раз и подходят как адрес приемника DMA.
На приемной стороне тактовый сигнал будем по обоим перепадам использовать для тактирования таймера, с периодом 1, а сигнал update будем использовать для запуска пересылки для DMA, который читает данные из порта (опять подошел порт PORTE) и записывает в память с автоинкрементом.
Теперь осталось правильно настроить все (код ниже) и запустить. Завершение на обеих сторонах определяется по прерыванию от DMA.
Однако, для полноты картины нужно конечно включить в код проверки на задержки передачи и обработку ошибок, но это я опускаю.
В коде ниже у таймера TIM8 использован канал CC2 для вывода сигнала — чтобы посмотреть, что получается.
volatile int transmit_done;
volatile int receive_done;
void DMA2_Stream1_IRQHandler(void) {
TIM8->CR1 &= ~TIM_CR1_CEN;
DMA2->LIFCR |= 0b1111 << 8;
receive_done = 1;
}
void DMA2_Stream4_IRQHandler(void) {
TIM1->CR1 &= ~TIM_CR1_CEN;
TIM1->EGR |= TIM_EGR_BG;
DMA2->HIFCR |= 0b1111101;
transmit_done = 1;
}
void ii_receive(uint8_t *data, int len) {
GPIOE->MODER = (GPIOE->MODER & 0xFFFF0000) | 0x0000;
DMA2_Stream1->PAR = (uint32_t) &(GPIOE->IDR);
DMA2_Stream1->M0AR = (uint32_t) data;
DMA2_Stream1->NDTR = len;
TIM8->CNT = 0;
TIM8->BDTR |= TIM_BDTR_MOE;
receive_done = 0;
DMA2_Stream1->CR |= DMA_SxCR_EN;
TIM8->CR1 |= TIM_CR1_CEN;
}
void ii_transmit(uint8_t *data, int len) {
GPIOE->MODER = (GPIOE->MODER & 0xFFFF0000) | 0x5555;
DMA2_Stream4->PAR = (uint32_t) &(GPIOE->ODR);
DMA2_Stream4->M0AR = (uint32_t) data;
DMA2_Stream4->NDTR = len;
TIM1->CNT = 6;
transmit_done = 0;
DMA2_Stream4->CR |= DMA_SxCR_EN;
TIM1->SR |= TIM_SR_BIF;
TIM1->BDTR |= TIM_BDTR_MOE;
TIM1->CR1 |= TIM_CR1_CEN;
}
// tx: TIM1 CH4 on DMA2/stream4/channel6, CH1 on output clock in PE9
// rx: TIM8 CH2 on DMA2/stream3/channel7, CH1 on input clock in PC6
void ii_init() {
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_TIM1_CLK_ENABLE();
__HAL_RCC_TIM8_CLK_ENABLE();
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_DMA2_CLK_ENABLE();
GPIOC->MODER |= (0b10 << GPIO_MODER_MODE6_Pos)
| (0b10 << GPIO_MODER_MODE7_Pos);
GPIOC->PUPDR |= (0b10 << GPIO_PUPDR_PUPD7_Pos);
GPIOC->AFR[0] |= (GPIO_AF3_TIM8 << 24) | (GPIO_AF3_TIM8 << 28);
GPIOE->MODER |= (0b10 << GPIO_MODER_MODE9_Pos);
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9 | 0xFFFF;
GPIOE->AFR[1] |= GPIO_AF1_TIM1 << 4;
GPIOE->PUPDR |= (0b10 << GPIO_PUPDR_PUPD9_Pos);
TIM1->ARR = 8;
TIM1->CCR1 = 5;
TIM1->CCR4 = 1;
TIM1->EGR |= TIM_EGR_CC4G;
TIM1->DIER |= TIM_DIER_CC4DE;
TIM1->CCMR1 |= (0b110 << TIM_CCMR1_OC1M_Pos);
TIM1->CCER |= TIM_CCER_CC1E;
TIM1->EGR |= TIM_EGR_BG;
TIM8->ARR = 1;
TIM8->CCR2 = 1;
TIM8->EGR |= TIM_EGR_UG;
TIM8->DIER |= TIM_DIER_UDE;
TIM8->SMCR |= (0b100 << TIM_SMCR_TS_Pos) | (0b111 << TIM_SMCR_SMS_Pos);
TIM8->CCMR1 = (0b01 << TIM_CCMR1_CC1S_Pos) | (0b110 << TIM_CCMR1_OC2M_Pos);
TIM8->CCER |= (0b11 << TIM_CCER_CC1P_Pos) | TIM_CCER_CC2E;
DMA2_Stream1->CR = DMA_CHANNEL_7 | DMA_PRIORITY_VERY_HIGH | DMA_MINC_ENABLE
| (0b00 << DMA_SxCR_DIR_Pos) | DMA_SxCR_TCIE | DMA_SxCR_TEIE
| DMA_SxCR_DMEIE;
DMA2_Stream1->FCR |= DMA_FIFOMODE_ENABLE;
DMA2_Stream4->CR = DMA_CHANNEL_6 | DMA_PRIORITY_VERY_HIGH | DMA_MINC_ENABLE
| (0b01 << DMA_SxCR_DIR_Pos) | DMA_SxCR_TCIE | DMA_SxCR_TEIE
| DMA_SxCR_DMEIE;
DMA2_Stream4->FCR |= DMA_FIFOMODE_ENABLE;
HAL_NVIC_SetPriority(DMA2_Stream1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream1_IRQn);
HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream4_IRQn);
}
Для тестов была использована одна и та же плата, просто тактовый выход PE9 был соединен с входом PC6. Главный цикл выглядел так:
ii_receive(rdata, 256);
ii_transmit(tdata, 256);
while (!transmit_done);
while (!receive_done);
По результатам: данные отлично пересылались за 30-31 микросекунду без потерь. Сигналы выглядят как-то так:
здесь белый — выход таймера TIM8, красный — тактовый сигнал (TIM1), ну а оранжевый — это младший бит данных (0-1-0-1-...).
Что не нравится при этом — ну нельзя никак запускать DMA от прерывания от входа GPIO, вот и приходится работать с таймерами. Может, кто-нибудь подскажет другой способ?