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