Приветствую всех на нашем сайте! Выходит в свет вторая часть из обещанных трех, посвященных работе с протоколом 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.
Привет. Спасибо за статью, по Lin шине, почему то, очень мало информации в сети. Но, к сожалению, повторить проект у меня, пока что, не получилось. Хотел сделать мастера, который будет просто кидать информацию в лин шину, для проверки концепции. Подключить плату с микросхемой tja1021 к лин анализатору. В кубе сделал один юарт, в прошивке main добавил код, который мастером отправляет данные слейву (тогда же ни какие колбеки не нужны? ). Залил и стал дебагом смотреть. В результате, если не подключать к tja1021 +12, то в дебаге linMasterTxCnt++ значение растет. Если подключать +12, то все валиться с ошибками по uart. Пока застрял, дальше не знаю что делать
А что на линиях, если осцилом посмотреть? На LIN и Tx/Rx.
И на входе SLP_N?
SPL_N подтянуто резистором к +5в. На ней 4.9 вольта. На лин, скорее всего не будет ни какого сигнала, Lin анализатор ловит что то только в момент подачи напряжение на контроллер. Замеры на выводах сделаю чуть позже. Сам проект тут https://yadi.sk/d/BVPZubR2hxtQWg .
Stm32f105+tja1021 . Cube Mx v6, keil 5.
Там, насколько я помню, скорость от 1к до 20к поддерживается, но с шагом 1к.
На выводе lin нет ничего. На выводе Tx, если не подавать +12 видно что идут пакеты данных. На выводе Rx ничего нет. Если подключить +12, то данные в Tx идти перестают. Если убрать +12 данные не появляются, пока не сделать перезагрузку.
Если скорость поставить для uart 20000, вместо 19200 - ничего не меняется?
разобрался с передачей мастера. С ноля собрал проект и он заработал. https://yadi.sk/d/Et1czQsDfCLTVg
Буду разбираться с приемом. Спасибо.
Отлично!
Вопрос по приему сообщений мастером. В примере после передачи сразу становимся на прием, и этот прием не ограничен по времени. В боевых условиях, насколько я понимаю, прием должен быть ограничен по времени, что б программа не зависла в случае отсутствия приема?. есть ли какие то ограничения, сколько стоять на приеме?
Что то не разобрался в вопросе приема. На шине стоит прибор из кан хакера, который отвечает на запросы лин. Написал в main
Как я это понимал - после передачи запроса с id 33 стм будет бесконечно ждать ответа. По факту оказалось, что он ответа вообще не ждет. запросы идут один за одним с одинаковыми интервалами. В результате lin хакер показывает что идет запрос с ID 33 с интервалами в 1013 мс. На ответы lin хакера, прикидывающемся слейвом не реагирует. А, по моей задумке, должен полученное отправить мастером с id 3A.
В HAL_UART_Receive() последний аргумент - это таймаут, то есть максимальное время ожидания приема данных. По истечению функция возвращает HAL_TIMEOUT.
Еще вопрос назрел. В статье написано " Проверяем PID и по его значению определяем, что нам следует делать в дальнейшем. И тут, как вы помните, два варианта
"
Мне нужно создать лин фильтр и фильтровать одно значение. Как мне по ID понять мне надо будет отвечать на запрос мастера или ждать от него данные.
В этом примере будем мы отвечать или нет зависит от принятого ID, который мы указали ранее в прошивке.
Правильно я понмаю, что в шине слейв по одному и тому же id не может и слушать и отвечать.
То есть для того, что б отфильтровать значения надо сначала прослушать весь трафик и понят по каким ID слейвы отвечают, а по каким слушают? создавать внутри прошивки список ID которые слушают и которые отвечают и исходя из этого списка решать что делать?
Да, вроде того, обычно у слейва есть ID, по которым он принимает данные, а есть те, по которым передает. И при этом мастер тоже знает какие ID на прием и какие на передачу.
А мастером слать данные можно только из Main()? у меня затея - lin1 посылает данные в lin2. Lin2 ловит данные и эти данные кидает lin1, но уже с другим ID. Пытаюсь фильтр сделать. В коллбеке после приема данных сразу вставил код
но такого же сообщения с другим ID не приходит. Получается из коллбека по второму лину нельзя сразу же послать сообщение в первый лин?
А можете проект выслать?
https://yadi.sk/d/_fzaKzL0ycNs5A
почти полностью Ваш пример.
когда добавляю в колбеке
мк зависает. Если убираю - мк работает.
А разве идея не в том была, что lin2 принимает данные, а потом их же отправляет обратно в lin1? Здесь ведь после приема в lin2 идет отправка через lin1, а не lin2.
Но тогда вопрос в том, что lin2 слейв, а значит не может инициировать обмен данными.
Или на lin2 приходят данные извне, а потом они передаются через lin1, просто в этом проекте для теста изначально на lin2 их высылает lin1?
пока тесты идут без автомобиля и разбираюсь как это работает. А так надо будет порвать лин и корректировать некоторые данные. В колбеке приняли данные от мастера в лин2. Пока, временно, мастером выступает лин1. Позже там будет мастер автомобиля. В лин 2 приняли данные, и надо их перекинуть в лин 1 (в примере что б не путаться хотел их слать с другим ID, ну и что б не уйти в бесконечный цикл). Хотел реализовать это в колбеке сразу.
взят колбек из примера, где собираем данные, принятые в лин2. После принятия данных добавил сразу отправку брейка в первый лин. То есть взяли ваш пример и только добавил одну строчку
После этого мк зависает. Получается из колбека нельзя брейк посылать? Пробовал добавлять задержки - не помогает.
Реализовал отправку данных от мастера с помощью глобальной переменной - данные бегают как надо. Но в лин шине, которую надо порвать, есть еще и запросы от мастера. По хорошему надо в том же колбеке скопировать запрос мастера из лин 2 в лин1, дождаться ответа от слейва автомобиля и перекинуть ответ в лин2. Вот тут пока остановился на подумать, так как уперся в то, что из колбека не получается брейк отправить.
Скорее всего проблема из-за того, что прерывание uart2 начинает рекурсивно вызываться. Из USART2_IRQHandler() отправляется брейк через lin1, что снова приводит к вызову USART2_IRQHandler().
И в целом лучше не вызывать из прерывания функции типа отправки, которые долгое время занимают. Лучше флаг выставить и в main() проверять и отправлять данные.
Это опять я. Что то какая то фигня у меня с приемом данных.
HAL_UART_Receive(&huart1, linMasterData, LIN_DATA_BYTES_NUM, 10);
что то оно как то криво принимает.
Делаем запрос мастером в первую лин шину. Вторая лин шина слейвом отвечает. После этого полученные данные кидаем мастером в первую лин шину
код из майна
слейв присылает данные
40 41 42 43 44 45 46 47 crc E1
мастер их принимает и шлет с другим ID, но шлет со сдвигом
E1 40 41 42 43 44 45 46 crc 47
причем это проблема с приемом. Если в коде задать данные и их слать, то все приходит без сдвига. Я так понмаю что то не так у меня с приемом.
А почему у слейва еще байт Е1 после контрольной суммы?
это я так отметил контрольную сумму. CRC как разделитель. Я все контролирую через лин хакера, у него контральная сумма вынесена в отдельный столбец. то есть символы crc это отметка, что дальше идет байт контрольной суммы.
А, понятно
вот скриншот
На момент отправки HAL_UART_Transmit(&huart1, linMasterData, 9, 10); в буфере уже перепутанные данные?
уже после приема данные перепутанные. Оставил только однин запрос мастером данных в дебаге показало что там данные идут со сдвигом
видимо надо осциллографом или логическим анализатором смотреть, что твориться на выводе Rx, видимо там что то проскакивает. Пока сделал в функции приема сдвиг данных.
Здравствуйте, не разобрались, почему сдвигает? на F303 у меня тоже прием уже со сдвигом идет, причем всегда разным. На F103 все работало корректно
Аналогичная проблема, тоже данные идут со сдвигом, если их задать жестко в коде - то все норм отправляется
Здравствуйте! Не могу скачать архив проекта. Спасибо.
Добрый день! Загрузил на диск - https://yadi.sk/d/Vq7vPrnDorFXYA
Все нормально. Спасибо!
Здравствуйте! Статья очень хорошая.А можете заделать такую же, но с DMA? Несколько смущает, что после отправки команды на чтение/запись LIN-шины нужно ждать, пока отправится/придёт каждый байт, и в это время ничего нельзя делать. Если единственная цель устройства - это приём/передача данных по LIN, то проблем нет. Если же у устройства есть другие дела, то лучше доверить приём/передачу DMA.
Собственно в своём проекте я так и поступил. Но столкнулся с интересным поведением LIN-шины - когда Master передаёт данные на LIN-чип (использую MCP2003B) через лапку Tx, выход LIN приходит в движение, как и полагается шлёт данные, которые ему скармливает мастер. Однако выход LIN возвращает эти же данные обратно мастеру через лапку Rx. Получается, что мастер слушает себя. На вход он получает и сигнал начала передачи, и биты синхронизации и байты сообщения.
Можно ли как-то на аппаратном уровне "фильтровать" байты, отправляемые мастером, и принимать только байты от других устройств?
Добрый день!
Аппаратно, к сожалению, не получится. Поддержка ограничивается только обработкой (отправкой/обнаружением) break поля.
тоже заметил, что все, что пришло на TX, появляется на ножке RX , видимо так работает трансивер TJA1020
вот мастер запрос, видно полное дублирование
а вот мастер запрос и ответ слейва
У вас не совсем верное представление о шине LIN. Tё аппаратная суть предполагает горячую подстройку частоты приема ведомым. Скорость там понятие условное, соответствует некоторым рамкам. Но поскольку главным девизом шины является "дешево и сердито" предполагается, что ведомое устройство может даже не иметь кварца. Поэтому в т.н. заголовке, после паузы идет меандр. По сути 0х55 по которому ведомый, аппаратно замеряя тайминг перехода от 0 к 1 и усредняя результат, корректирует частоту приема. И соответственно ответа мастеру. Если таковой оговорен. Без этого возможен уход частоты обмена (например по температуре) со всеми вытекающими.
Об этом написано в теоретической части. Практическая часть без autobaud, на откуп читателю в случае необходимости.
Здравствуйте!
Спасибо за ваши статьи, очень познавательные.
Для работы с LIN я использую микроконтроллер STMG0xx. Как и во многих других микроконтроллерах от ST, в нем есть возможность автоматического определения скорости передачи данных. В режимы обычного UART (Automatic baud rate detection modes 0x55 character frame) auto baud rate работает прекрасно (пример пакета 0x55 0x31 0x32..), но при работе в режиме LIN-UART , автоматически скорость передачи данных не определяется, мое предположение, что это происходит из-за того, что перед 0х55 мастер отправляет поле BREAKE (0x0 0x55 0x3A......), микроконтроллер воспринимает breake, как ошибку кадра и в результате не происходит определение скорости (ABRF = 0 ABRE = 1). Может вы подскажете, как можно реализовать определение скорости передачи данных ведомым устройством в режиме LIN UART?
Добрый день!
Первая мысль - autobaud активировать только после того, как break принят.
К сожалению не помогает, я уже пробовал, autobaud начинает работать с самого начала кадра. Я думаю, что этот момент не был учтен инженерами ST и нужно писать, что то свое. Нигде не могу найти рабочий пример, как можно определить скорость передачи данных UART.
Теорию я понимаю, что, после получения beake, во время приема байта синхронизации, нужно запустить процедуру подсчета времени изменения уровня сигнала на ножке Rx с низкого на высокий, на основании этих измерений вычислить baud rate, но у меня не хватает опыта программно реализовать этот алгоритм. У вас случайно не было в планах написать статью по определению скорости передачи данных UART ведомым устройством?
Не, планов нет на этот счёт... Как тестовый вариант можно попробовать сам uart в целом включать только после брейка.