Программный контроллер интерфейса на 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).

hvnujauwagmc-0xcnz8bntzrruu.png

Идеологически это шина Master-Slave, инициатором обмена всегда является Master, требования ко времени реакции устройств — порядка единиц микросекунд.
Обмен всегда подразумевает «Запрос — Ответ»

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

duw_8ufrhblvyjwpmsieidwvfvu.png

Как видно, уровни напряжения отличаются почти в 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.

Итого, если требуется обеспечить совместимость с чем-то древним/нестандартным, необязательно привлекать плисовода. В зачительном количестве случаев можно обойтись и программной реализацией контроллера.

© Habrahabr.ru