Приветствую всех на нашем сайте! Выходит в свет вторая часть из обещанных трех, посвященных работе с протоколом LIN. Сегодня вооружимся микроконтроллером STM32 и реализуем связь по LIN в разных режимах работы с использованием аппаратных средств. Напоминаю, что в третьей статье будем писать свой драйвер LIN с нуля на голом UART'е.
Подготовительный этап будет протекать как обычно - открываем STM32CubeMx и настраиваем нужную периферию. Для этого проекта мы будем использовать два модуля USART, настроенных на работу в режиме LIN, соединенных между собой. Вообще, строго говоря, подключение необходимо выполнять через микросхемы LIN-трансиверов:
Но у меня сейчас, к сожалению, нет под рукой TJA1021, либо аналога, так что соединяем модули USART просто напрямую. На процесс написания программы это никак не повлияет и на работоспособности не отразится.
USART1 и USART2 настраиваем абсолютно одинаково, не забываем включить прерывания:
Кроме того, как обычно, конфигурируем тактирование микроконтроллера:
На этом взаимодействие с CubeMx закончено, генерируем проект и открываем его для дальнейшей деятельности.
Итак, в чем же заключается аппаратная поддержка LIN в STM32. На самом деле, функционал фактически минимальный. Для Master'а есть возможность аппаратно сгенерировать поле Break, используя функцию:
HAL_LIN_SendBreak(UART_HandleTypeDef *huart)
Для Slave есть возможность отловить все тот же брейк в прерывании. В общем-то вот и все...
Давайте чуть подробнее разберем механизм обнаружения поля Break подчиненным устройством. Стоит отметить, что в HAL по какой-то причине решили не делать никаких функций для этого, поэтому будем работать "руками". В общем, то нам нужно будет всего лишь включить прерывание по обнаружению брейка:
__HAL_UART_ENABLE_IT(&huart2, UART_IT_LBD);
В данном случае у меня использован USART2.
И далее в прерывании, после стандартной обработки прерывания HAL'ом, отлавливаем событие обнаружения брейка:
uint32_t isrflags = huart2.Instance->SR; if ((isrflags & USART_SR_LBD) != RESET) { CLEAR_BIT(huart2.Instance->SR, USART_SR_LBD); // ..... }
Все, на этом аппаратные возможности заканчиваются, все остальное мы сейчас сделаем самостоятельно. На USART1 у нас будет Master, соответственно, на USART2 - Slave. И начнем с более сложного, а именно с кода подчиненного устройства. Но прежде всего прочего нужно обсудить, каков будет план.
Итак, реализуем два режима работы:
- Master отправляет заголовок (header) пакета с определенным PID, после чего Slave переходит в режим приема данных.
- Master отправляет заголовок с идентификатором, который сигнализирует Slave'у о том, что надо ответить ведущему порцией данных. Соответственно, Master после отправки заголовка должен встать на прием.
Пусть значения PID для примера будут такими:
- 0x3A - соответствует первому случаю, то есть Master отправляет заголовок и данные.
- 0x3B - это уже второй случай из перечисленных.
Важное дополнение - как вы помните, байт PID помимо 6-ти битов идентификатора включает в себя еще два бита четности. Для примера же будем просто использовать вышеупомянутые значения, без учета четности.
Добавляем в наш проект эти значения и пару переменных (ссылку на полный проект я обязательно добавлю в конце статьи):
#define LIN_TX_ID 0x3A #define LIN_RX_ID 0x3B
uint8_t linTxId = LIN_TX_ID; uint8_t linRxId = LIN_RX_ID;
Кроме того, объявляем массивы для хранения данных ведущего и подчиненного:
#define LIN_DATA_BYTES_NUM 9
uint8_t linMasterData[LIN_DATA_BYTES_NUM]; uint8_t linSlaveData[LIN_DATA_BYTES_NUM];
В проекте все находится в разных файлах, здесь же для наглядности я буду комбинировать код чуть иначе. Принимать и отправлять мы будем по 8 байт, то есть максимально возможное количество байт в одном фрейме. Но обратите внимание, что в массивах по 9 байт - еще 1 байт мы выделили под прием и отправку контрольной суммы.
И раз уж об этом зашла речь, то вот функция для расчета контрольной суммы (будем использовать классический алгоритм):
uint8_t LIN_CalcCheckSum(uint8_t *data, uint8_t len) { uint16_t sum = 0; for (uint8_t i = 0; i < len; i++) { sum += data[i]; } while(sum > 0xFF) { sum -= 0xFF; } sum = 0xFF - sum; return (uint8_t)sum; }
И для систематизации работы программы добавим возможные режимы, в которых может находиться Slave:
typedef enum { LIN_RECEIVING_BREAK = 0x01, LIN_RECEIVING_SYNC = 0x02, LIN_RECEIVING_ID = 0x03, LIN_RECEIVING_DATA = 0x04, LIN_SENDING_DATA = 0x05, } LIN_State;
Суть тут ясна из названий, так что даже не будем останавливаться на этом отдельно. Итак, изначально Slave у нас готов к приему данных и находится в состоянии ожидания Break'а:
LIN_State slaveState = LIN_RECEIVING_BREAK;
Код приема брейка, как мы уже обсудили, поместим в обработчик прерывания:
/** * @brief This function handles USART2 global interrupt. */ void USART2_IRQHandler(void) { /* USER CODE BEGIN USART2_IRQn 0 */ /* USER CODE END USART2_IRQn 0 */ HAL_UART_IRQHandler(&huart2); /* USER CODE BEGIN USART2_IRQn 1 */ if (slaveState == LIN_RECEIVING_BREAK) { uint32_t isrflags = huart2.Instance->SR; if ((isrflags & USART_SR_LBD) != RESET) { CLEAR_BIT(huart2.Instance->SR, USART_SR_LBD); uint16_t temp = huart2.Instance->DR; slaveState = LIN_RECEIVING_SYNC; HAL_UART_Receive_IT(&huart2, &rxByte, 1); } } /* USER CODE END USART2_IRQn 1 */ }
Первым делом проверяем, что Slave находится в ожидании поля Break, затем проверяем, не вызвано ли прерывание как раз-таки приемом этого поля, и, если да, то переводим Slave в режим ожидания поля Sync. Тут есть небольшой нюанс в виде чтения регистра данных USART'а, это нужно для корректного приема последующих байт:
uint16_t temp = huart2.Instance->DR;
Вся остальная логика у нас будет в callback-функциях по окончанию приема и передачи. Начинаем с приема и двигаемся по всем возможным состояниям Slave устройства:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart2) { uint8_t checkSum = 0; switch(slaveState) { case LIN_RECEIVING_SYNC: if (rxByte == LIN_SYNC_BYTE) { slaveState = LIN_RECEIVING_ID; HAL_UART_Receive_IT(&huart2, &rxByte, 1); } else { slaveState = LIN_RECEIVING_BREAK; } break; default: break; } } }
Проверяем принятый байт, если значение верное - 0x55 - встаем на прием PID, иначе возвращаемся в исходную точку. Добавляем ветку для приема идентификатора:
case LIN_RECEIVING_ID: if (rxByte == LIN_RX_ID) { slaveState = LIN_SENDING_DATA; for (uint8_t i = 0; i < LIN_DATA_BYTES_NUM - 1; i++) { linSlaveData[i] = 0x30 + i; } linSlaveData[LIN_DATA_BYTES_NUM - 1] = LIN_CalcCheckSum(linSlaveData, LIN_DATA_BYTES_NUM - 1); HAL_UART_Transmit_IT(&huart2, linSlaveData, LIN_DATA_BYTES_NUM); } else { if (rxByte == LIN_TX_ID) { slaveState = LIN_RECEIVING_DATA; HAL_UART_Receive_IT(&huart2, linSlaveData, LIN_DATA_BYTES_NUM); } else { slaveState = LIN_RECEIVING_BREAK; } } break;
Здесь все чуть сложнее, но, в принципе, тоже довольно прозрачно. Проверяем PID и по его значению определяем, что нам следует делать в дальнейшем. И тут два варианта:
- Ожидать приема байт данных от Master'а
- Или отправить свою порцию данных
Для случая передачи данных Slave'ом заполняем массив тестовыми значениями и дополняем его 9-м байтом - контрольной суммой. А если PID не соответствует тем 2-м значениям, которые мы определили для этого примера, то возвращаем устройство в начальное положение, а именно в состояние ожидания брейка.
И, наконец, последняя часть callback-а по приему:
case LIN_RECEIVING_DATA: checkSum = LIN_CalcCheckSum(linSlaveData, LIN_DATA_BYTES_NUM - 1); if (linSlaveData[LIN_DATA_BYTES_NUM - 1] == checkSum) { linSlaveRxCnt++; } slaveState = LIN_RECEIVING_BREAK; break;
Как мы обсудили ранее, принимаем 9 байт, то есть 8 байт данных и байт контрольной суммы. И когда все данные приняты - рассчитываем контрольную сумму по первым 8-ми байтам и сравниваем ее значение с 9-ым байтом массива данных. В случае успеха инкрементируем переменную linSlaveRxCnt
. Этот счетчик будет для нас сигналом успешно принятых данных.
Но в зависимости от принятого идентификатора Slave мог также перейти в режим передачи данных, поэтому этот случай нам также нужно обработать. И делаем мы это в коде callback-функции по окончанию передачи данных:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart2) { switch(slaveState) { case LIN_SENDING_DATA: linSlaveTxCnt++; slaveState = LIN_RECEIVING_BREAK; break; default: break; } } }
Все, на этом логика работы подчиненного полностью готова 👍 Не мешкая переходим к реализации работы ведущего, и тут все будет на порядок проще.
При работе Master'а в режиме отправки данных Slave'у:
for (uint8_t i = 0; i < LIN_DATA_BYTES_NUM - 1; i++) { linMasterData[i] = 0x40 + i; } HAL_LIN_SendBreak(&huart1); HAL_UART_Transmit(&huart1, &linSync, 1, 10); HAL_UART_Transmit(&huart1, &linTxId, 1, 10); linMasterData[LIN_DATA_BYTES_NUM - 1] = LIN_CalcCheckSum(linMasterData, LIN_DATA_BYTES_NUM - 1); HAL_UART_Transmit(&huart1, linMasterData, LIN_DATA_BYTES_NUM, 10); linMasterTxCnt++; HAL_Delay(1000);
Заполняем данные тестовыми значениями и последовательно отправляем:
- Break
- Sync
- PID
- Данные
- Контрольную сумму
Все четко по формату пакета протокола LIN. Для примера я сделал отправку раз в секунду. В режиме, когда Master отправляет в шину заголовок и ожидает данные от подчиненного все практически идентично:
for (uint8_t i = 0; i < LIN_DATA_BYTES_NUM; i++) { linMasterData[i] = 0x00; } HAL_LIN_SendBreak(&huart1); HAL_UART_Transmit(&huart1, &linSync, 1, 10); HAL_UART_Transmit(&huart1, &linRxId, 1, 10); HAL_UART_Receive(&huart1, linMasterData, LIN_DATA_BYTES_NUM, 10); uint8_t checkSum = LIN_CalcCheckSum(linMasterData, LIN_DATA_BYTES_NUM - 1); if (linMasterData[LIN_DATA_BYTES_NUM - 1] == checkSum) { linMasterRxCnt++; } HAL_Delay(1000);
Последний аргумент функций HAL_UART_Transmit()
и HAL_UART_Receive()
- это величина таймаута в миллисекундах. Если функция не закончит свое выполнение за это время, то произойдет возврат из функции с кодом ошибки HAL_TIMEOUT
.
Разница только в том, что после отправки PID устройство встает на прием данных, а не начинает передачу, плюс по окончанию приема проверяем контрольную сумму. Для индикации работоспособности здесь также будем пользоваться обычными счетчиками пакетов linMasterRxCnt
и linMasterTxCnt
.
Переключение между режимами работы в проекте осуществляется переменной linMasterTask
. Master отправляет:
uint8_t linMasterTask = LIN_MASTER_TX;
Master принимает:
uint8_t linMasterTask = LIN_MASTER_RX;
Собираем, прошиваем, запускаем, проверяем.
Для случая, когда ведущий отправляет данные подчиненному смотрим на счетчики linMasterTxCnt
и linSlaveRxCnt
. А также на значения в массивах linMasterData
и linSlaveData
. Под отладчиком можно наблюдать как счетчики параллельно инкрементируются, то есть отправляемые пакеты успешно принимаются:
Аналогично и в режиме, когда Master получает данные от Slave:
Здесь уже вступают в игру счетчики linMasterRxCnt
и linSlaveTxCnt
. Ну и кроме того, значения в массивах принимаемых и отправляемых данных в точности совпадают.
И на этой позитивной ноте заканчиваем сегодняшнюю статью, спасибо за внимание 🤝 При возникновении любых вопросов, смело пишите в комментарии, во всем разберемся.
Ссылка на проект - MT_LIN_HW_Example.