Программный контроллер интерфейса на STM32

от автора

Проблема обратной совместимости, вероятнее всего, будет всегда.
В области разработки электроники порой приходится поддерживать устройства 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *