В области разработки электроники порой приходится поддерживать устройства 30-летней давности (а иногда и старше).
В таких аппаратах иногда всё собрано на логике, без каких-либо программируемых элементов.
Кроме того, в старой технике существуют доморощенные интерфейсы, которые не реализуются какими-либо серийно выпускаемыми контроллерами.
В таких случаях совместимые контроллеры приходится реализовывать на CPLD\FPGA\ASIC.
Я подумал, что можно обойтись без указанных выше средств, а именно реализовать контроллер интерфейса программно на микроконтроллере серии STM32F4.
Основная идея заключается в использовании связки TIM+DMA+GPIO:
-Таймер настраивается на требуемую частоту и генерирует запросы для DMA
-DMA по запросам таймера перекладывает данные из памяти в регистры GPIO
-В результате на линиях GPIO с нужной частотой выставляются нужные значения
Ограничения STM32:
-К регистрам GPIO имеет доступ только DMA2, а запросы к DMA2 умеют генерировать только TIM1 и TIM8.
-Транзакция DMA из памяти в регистры периферии или обратно занимает около 10-12 тактов шины (зависит от кучи условий, описанных в Application note AN4031).
Таким образом, максимум для данного решения — 16 линий с частотой порядка 12-14 МГц.
Для проверки жизнеспособности идеи был выбран интерфейс MIL-STD 1573 (известный у нас как МКО).
Интерфейс представляет собой дифференциальную пару с кодом Манчестер-2 — на каждый бит (занимающий 1 мкс) приходится переход сигнала из 0 в 1 либо обратно, то есть 2 уровня (значение бита определяется не уровнем сигнала, а направлением его перепада).
Данные передаются 16-битными словами + 1 бит чётности + 3 бита синхросигнала (1,5 + 1,5 бита на разных уровнях), итого 20 мкс.
Тактовая частота — 2 МГц, теоретическая полезная пропускная способность — чуть менее 1 Мбит/с (около 0,8).

Идеологически это шина Master-Slave, инициатором обмена всегда является Master, требования ко времени реакции устройств — порядка единиц микросекунд.
Обмен всегда подразумевает «Запрос — Ответ»
Ниже для наглядности показана осциллограмма обмена между устройством, имеющим отечественный МКО-контроллер с двухполярным питанием ±15 вольт, и получившимся в результате программным контроллером.
Это запрос Master’ом пакета и ответ Slave’а на данный запрос (видно только первое слово пакета и синхросигнал второго слова, за которым ещё 30 слов).

Как видно, уровни напряжения отличаются почти в 2 раза (оба укладываются в ГОСТ), но временные характеристики сигналов одинаковые. Устройства успешно «понимают» друг друга.
Поскольку мы реализуем физический интерфейс, имеются весьма жёсткие требования к реалтайму.
Потребуется делать много вещей внутри прерываний, причём очень быстро (не более единиц микросекунд, то есть до 1000 тактов).
Кроме того, прерывания, относящиеся к физическому интерфейсу, должны иметь наивысший приоритет и вытеснять всё остальное.
Необходимо реализовать механизмы синхронизации и калибровки (на начальном этапе).
Мной была выбрана следующая конфигурация:
Передача: TIM1(2 МГц) + DMA2_Stream5_DMA_CHANNEL_6 + GPIOD->ODR (PD0 — прямой сигнал дифференциальной пары; PD1 — инверсный (можно было бы обойтись одним выходом+инвертор)).
Массив в памяти заполняется согласно протоколу МКО, затем запускается таймер+DMA, которые выпуливают массив из памяти (по одному байту) на ноги GPIO. В конце дополнительно выпуливается 0, чтобы задавить линию.
Приём: TIM8(1 Мгц) + DMA2_Stream1_DMA_CHANNEL_7 + GPIOB->IDR (PB6 — прямой сигнал дифференциальной пары; инверсный вход не используется совсем).
На ноге GPIO настраивается прерывание по изменению входного уровня (это значит, что в канале кто-то что-то начал передавать, пора начинать слушать).
По срабатыванию прерывания запускается таймер+DMA (на максимально возможную длину пакета в МКО), которые собирают с ноги GPIO уровни (по одному байту) в массив в памяти. Массив позднее анализируется.
Для приёма также введён вспомогательный таймер TIM2 (1/20 МГц — соответствует длительности одного слова в МКО).
Он используется для обработки первого принятого из канала слова (по которому принимается решение, прекращать ли приём и выходить в передачу, или же принимать пакет дальше).
Если после анализа первого слова приём продолжился, этот же таймер останавливает таймер+DMA после приёма количества слов, указанного в первом принятом слове.
Также, после срабатывания этого таймера пуляется ответное слово (или целый ответный пакет)
После приёма данных и остановки таймера+DMA выставляется packet_received_length, который означает, что есть принятые данные и их надо распарсить и отправить наверх.
На время передачи приём отключается (прерывание на ноге-уловителе отключено)
Примерно прикинув архитектуру, я взялся за реализацию.
Поскольку я буду работать с регистрами GPIO по одному байту, а полезных там 1 или 2 бита, мне нужно уметь преобразовывать полезные данные в то, что будет передано с помощью DMA в GPIO, и обратно.
Сначала мне потребовалось немножко макросов для удобной работы с форматом слов в МКО:
#define MKO_RX_GPIO_OFFSET 6 //PB6 #define MKO_RX_1 (long long)(1 << MKO_RX_GPIO_OFFSET) #define MKO_RX_BYTE_MASK ((MKO_RX_1 << 56) + (MKO_RX_1 << 48) + (MKO_RX_1 << 40) + (MKO_RX_1 << 32) + (MKO_RX_1 << 24) + (MKO_RX_1 << 16) + (MKO_RX_1 << 8) + MKO_RX_1) #define MKO_LOW (long long)(2) #define MKO_HIGH (long long)(1) #define MKO_0 ((MKO_HIGH << 8) + MKO_LOW) #define MKO_1 ((MKO_LOW << 8) + MKO_HIGH) #define MKO_0x0 ((MKO_0 << 48) + (MKO_0 << 32) + (MKO_0 << 16) + MKO_0) #define MKO_0x1 ((MKO_1 << 48) + (MKO_0 << 32) + (MKO_0 << 16) + MKO_0) . . #define MKO_0xF ((MKO_1 << 48) + (MKO_1 << 32) + (MKO_1 << 16) + MKO_1) #define CMD_ACK_WORD ((MKO_LOW << 56) + (MKO_LOW << 48) + (MKO_LOW << 40) + (MKO_HIGH << 32) + (MKO_HIGH << 24) + (MKO_HIGH << 16)) #define DATA_WORD ((MKO_HIGH << 56) + (MKO_HIGH << 48) + (MKO_HIGH << 40) + (MKO_LOW << 32) + (MKO_LOW << 24) + (MKO_LOW << 16)) const unsigned long long mko_tetrades[16] = {MKO_0x0, MKO_0x1, MKO_0x2, MKO_0x3, MKO_0x4, MKO_0x5, MKO_0x6, MKO_0x7, MKO_0x8, MKO_0x9, MKO_0xA, MKO_0xB, MKO_0xC, MKO_0xD, MKO_0xE, MKO_0xF};
Также мне нужны были функции упаковки\распаковки данных:
void short_to_mko(unsigned char* data, unsigned int start_pos, unsigned int command_word, unsigned int input_data) { if (command_word) *(long long*)&data[start_pos] |= CMD_ACK_WORD; else *(long long*)&data[start_pos] |= DATA_WORD; *(long long *)&data[start_pos + 8] = mko_tetrades[(input_data >> 12) & 0xf]; *(long long *)&data[start_pos + 16] = mko_tetrades[(input_data >> 8) & 0xf]; *(long long *)&data[start_pos + 24] = mko_tetrades[(input_data >> 4) & 0xf]; *(long long *)&data[start_pos + 32] = mko_tetrades[(input_data >> 0) & 0xf]; input_data -= (input_data >> 1) & 0x5555; input_data = ((input_data >> 2) & 0x3333) + (input_data & 0x3333); input_data = ((input_data >> 4) + input_data) & 0x0f0f; input_data = ((input_data >> 8) + input_data) & 0x00ff; if (input_data & 1) *(unsigned int*)&data[start_pos + 40] = MKO_0; else *(unsigned int*)&data[start_pos + 40] = MKO_1; } unsigned int mko_to_short(unsigned char* data, unsigned int start_pos) { unsigned int output_data; long long byte; unsigned int crc; byte = (*(long long *)&data[start_pos]) & MKO_RX_BYTE_MASK; output_data = MKO_RX_BYTE_PACK(byte); output_data <<= 8; byte = (*(long long *)&data[start_pos + 8]) & MKO_RX_BYTE_MASK; output_data |= MKO_RX_BYTE_PACK(byte) & 0xff; crc = output_data & 0xffff; crc -= (crc >> 1) & 0x5555; crc = ((crc >> 2) & 0x3333) + (crc & 0x3333); crc = ((crc >> 4) + crc) & 0x0f0f; crc = ((crc >> 8) + crc) & 0x00ff; if ((crc & 1) == (data[start_pos + 16] >> MKO_RX_GPIO_OFFSET)) packet_error = 1; return output_data & 0xffff; }
Теперь можно браться непосредственно за контроллерную часть.
Уловитель фронта принимаемого сигнала, зарядка приёмного DMA и таймера TIM2 (вспомогательного, для определения количества принятого)
void mko_start_receive() { unsigned int i; (EXTI->IMR) &= (~RX_START_INT);//выключаем прерывание (не ловим, если уже поймали (включим после окончания приёма) //заряжаем DMA на приём максимально возможной последовательности (если надо - потом на лету остановим в прерывании таймера TIM2) htim8.hdma[TIM_DMA_ID_UPDATE]->Instance->NDTR = 660; htim8.hdma[TIM_DMA_ID_UPDATE]->Instance->CR |= DMA_IT_TC; htim8.hdma[TIM_DMA_ID_UPDATE]->Instance->CR |= DMA_SxCR_EN; i = 50; while(i--);//сдвигаем запуск DMA более чем на 1 период (около 1.5 мкс) TIM8->CNT = 80;//калибровка (внутри одного периода) приёмного DMA (куда попадают отсчёты - сейчас на 250 нс (середина отсчёта)) __HAL_TIM_ENABLE_DMA(&htim8, TIM_DMA_UPDATE); __HAL_TIM_ENABLE(&htim8); TIM2->SR = 0;//дабы не генерировалось прерывание сразу после запуска таймера TIM2->ARR = 1400 - 1;//заряжаем таймер на 20 мкс (1 слово) (чтобы проанализировать первое приянтое и решить, что делать далее) TIM2->CNT = 0; HAL_TIM_Base_Start_IT(&htim2);//заряжаем на 20 мкс (1 слово) } void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) { /* EXTI line interrupt detected */ if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); if ((GPIO_Pin == GPIO_PIN_6) && ((GPIOB->IDR) & GPIO_PIN_6)) mko_start_receive(); } }
Вторая проверка состояния пина добавлена в качестве антидребезга.
Волшебные константы — результаты калибровки приёмного таймера+DMA (об этом ниже).
Далее функция запуска и настройки приёмного и передающего DMA, в том числе задание конечного адреса GPIO (сейчас GPIOB и GPIOD)
HAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length) { if((htim->State == HAL_TIM_STATE_BUSY)) return HAL_BUSY; else if((htim->State == HAL_TIM_STATE_READY)) { if((pData == 0U) && (Length > 0U)) return HAL_ERROR; else htim->State = HAL_TIM_STATE_BUSY; } htim->hdma[TIM_DMA_ID_UPDATE]->XferCpltCallback = TIM_DMAPeriodElapsedCplt; htim->hdma[TIM_DMA_ID_UPDATE]->XferErrorCallback = TIM_DMAError ; if (htim->Instance == TIM1) HAL_DMA_Start_IT(htim->hdma[TIM_DMA_ID_UPDATE], (uint32_t)pData, (uint32_t)&(GPIOD->ODR), Length); if (htim->Instance == TIM8) { #ifdef RX_CALIB HAL_DMA_Start_IT(htim->hdma[TIM_DMA_ID_UPDATE], (uint32_t)pData, (uint32_t)&(GPIOB->ODR), Length); #else HAL_DMA_Start_IT(htim->hdma[TIM_DMA_ID_UPDATE], (uint32_t)&(GPIOB->IDR), (uint32_t)pData, Length); #endif } __HAL_TIM_ENABLE_DMA(htim, TIM_DMA_UPDATE); __HAL_TIM_ENABLE(htim); return HAL_OK; }
Макрос RX_CALIB я завёл для того, чтобы откалибровать приёмный таймер+DMA, а именно для того, чтобы видеть куда приходятся выборки на входном сигнале (в идеале они должны попадать на середину бита).
Теперь основная логика МКО: анализ первого принятого слова, затем ответ либо зарядка на приём остального; ответ после приёма остального и выставление packet_received_length
void mko_slave_receive(void) { register unsigned int data; register unsigned int i; static unsigned int first = 1; static unsigned int mko_data_length; data = mko_to_short(&in_arr[0] ,0); if (first)//если это первое срабатывание таймера - анализируем первое принятое слово и решаем, что делать далее { if (!packet_error) { if (data & MASTER_DATA_REQUEST)//значит надо останавливать приём отвечать { first = 1; HAL_TIM_Base_Stop_IT(&htim2); HAL_TIM_Base_Stop_DMA(&htim8); htim8.hdma[TIM_DMA_ID_UPDATE]->Instance->CR &= ~DMA_SxCR_EN; mko_send((unsigned char*)&out_arr[i][0], out_array_data_length[i]);//после того, как отработает передающий DMA, прерывание на уловителе будет включено заново memcpy(in_arr_copy, in_arr, IN_ARRAY_LENGTH); #ifndef RX_CALIB //дабы не затереть in_arr, где при калибровке лежит выходной меандр memset(in_arr, 0, IN_ARRAY_LENGTH);//это занимает около 4 мкс, гипотетически можно убрать #endif packet_received_length = 1; } else//надо продолжать приём, зарядив таймер на нужное количество слов { first = 0; data = data & 0x1F; if (!data) data = 32; TIM2->ARR = (data * 1680) - 1; mko_data_length = data; } } else { packet_received_length = 1;//считаем, что приняли одно слово и оно с ошибкой HAL_TIM_Base_Stop_IT(&htim2); HAL_TIM_Base_Stop_DMA(&htim8); htim8.hdma[TIM_DMA_ID_UPDATE]->Instance->CR &= ~DMA_SxCR_EN; EXTI->IMR |= RX_START_INT;//enable interrupt - ловим следующий } } else//если второе срабатывание - значит приняли весь пакет, надо пулять ответное слово и анализировать данные (ставим флаг packet_received_length) { first = 1; HAL_TIM_Base_Stop_IT(&htim2); HAL_TIM_Base_Stop_DMA(&htim8); htim8.hdma[TIM_DMA_ID_UPDATE]->Instance->CR &= ~DMA_SxCR_EN; mko_send((unsigned char*)&out_arr[i][0], out_array_data_length[i]);//после того, как отработает передающий DMA, прерывание на уловителе будет включено заново memcpy(in_arr_copy, in_arr, IN_ARRAY_LENGTH); #ifndef RX_CALIB //дабы не затереть in_arr, где при калибровке лежит выходной меандр memset(in_arr, 0, IN_ARRAY_LENGTH);//это занимает около 4 мкс, гипотетически можно убрать #endif packet_received_length = mko_data_length; } } void TIM2_IRQHandler(void) { if (interface == INTERFACE_MKO_SLAVE)//в МКО ОУ если сработал этот таймер - принято слово (или целый пакет) mko_slave_receive(); else if (interface == INTERFACE_MKO_MASTER)//в МКО КШ если сработал этот таймер - принято слово (или целый пакет) mko_master_receive(); TIM2->SR = ~(TIM_IT_UPDATE); HAL_NVIC_ClearPendingIRQ(TIM2_IRQn); }
Код приведён в сжатом виде, полный проект представляет собой преобразователь MKO-Ethernet с кучей дополнительного функционала.
Однако приведённого описания и кода достаточно для понимания сути идеи.
Да, я реализовал минимальную логику, в ГОСТе на МКО описано гораздо больше.
Однако для обратной совместимости с конкретным устройством этого оказалось достаточно.
По факту проект оказался вполне успешным, контроллер полностью справляется с возложенными функциями как в режиме Master, так и Slave.
Итого, если требуется обеспечить совместимость с чем-то древним/нестандартным, необязательно привлекать плисовода. В зачительном количестве случаев можно обойтись и программной реализацией контроллера.
ссылка на оригинал статьи https://habr.com/ru/post/491434/
Добавить комментарий