Недавно мы разбирались с теоретическими аспектами работы протокола CAN – контролем ошибок, организацией арбитража сообщений на шине и т. д. Так вот, сегодня, как и обещал, займемся практической стороной вопроса – реализуем прием и передачу данных по CAN на микроконтроллере STM32.
Для настройки периферии будем использовать STM32CubeMx, в качестве среды разработки я, как обычно, беру IAR. Осталось упомянуть про выбранный контроллер – им сегодня будет STM32F103VE. Но, как вы помните, при работе с STM32 нет никакой проблемы в том, чтобы перейти на другой микроконтроллер или другую IDE 👍
Итак, первый шаг заключается в настройке в CubeMx всей имеющейся периферии. Не буду подробно останавливаться на том, что не касается непосредственно CAN, все можно найти в нашем курсе по этой вот ссылке.
В итоге у нас оказываются задействованы следующие пины контроллера:
Здесь мы уже активировали модуль CAN:
Но пока мы его только включили, необходимо еще выполнить настройку. И самым важным параметром является, конечно, скорость обмена данными. В случае с CAN ситуация не совсем такая, как с другими модулями STM32, для задания скорости мы должны настроить временные характеристики интерфейса, а не просто задать конкретное число:
Давайте разберемся, что это за значения и за что они отвечают. Время передачи одного бита в CAN складывается из:
Все длительности оцениваются через понятие кванта времени, для задания которого мы устанавливаем значение предделителя (в этом проекте он равен 8). Таким образом, мы получаем длительность 1 кванта:
T_{q} = 222.2\medspace нс
Первый сегмент на этой схеме – SYNC_SEG – используется для синхронизации всех узлов сети CAN. Ожидается, что фронт сигнала должен находиться внутри именно этого сегмента.
Второй сегмент - BS1 (Bit segment 1). Стандарт CAN включает в себя два сегмента - PROP_SEG и PHASE_SEG1. Оба этих сегмента в STM32 относятся к сегменту BS1. PROP_SEG нужен для компенсации физических задержек в сети, а PHASE_SEG1 используется для компенсации ошибки смещения фазы сигнала.
Сегмент BS1 определяет положение sample point (точки захвата). В этой точке модуль CAN анализирует уровень сигнала на шине, то есть определяет, принят рецессивный или доминантный бит.
И третий сегмент - BS2 (Bit segment 2). Он представляет из себя сегмент PHASE_SEG2 интерфейса CAN. Его назначение такое же как и у PHASE_SEG1.
Сегмент BS2 определяет положение transmit point (точки передачи), то есть того момента времени, когда модуль CAN выдает на линию определенный бит.
Длительности этих сегментов таковы:
- SYNC_SEG: 1 квант времени
- Bit segment 1 (BS1): 1 - 16 квантов
- Bit segment 2 (BS2): 1 – 8 квантов
Устанавливая длительности различных сегментов, мы получаем время передачи одного бита в квантах. Зная длительность самого кванта, мы легко получаем время передачи бита в секундах. А уже из этого мы рассчитываем скорость передачи данных по шине. Только при настройке CAN нужно пройти в обратном направлении.
Пусть мы хотим задать скорость обмена равной 500 Кбит/с. Тогда время передачи одного бита:
T_{bit} = \frac{1\medspace с}{500000\medspace} = 2000\medspace нс
Длительность одного кванта мы уже задали равной 222,2 нс. Таким образом, время 1-го бита в квантах:
T = \frac{T_{bit}}{T_{q}} = 9
Эти 9 квантов нужно распределить между тремя сегментами. SYNC_SEG фиксирован (1 квант), значит остается 8 квантов на сегменты BS1 и BS2. Ставим в этом примере поровну – по 4 кванта и получаем нужную нам скорость обмена данными. И, наконец, можем двигаться дальше - включаем прерывания и генерируем код:
В main()
находим функцию инициализации CAN:
/* Initialize all configured peripherals */ MX_GPIO_Init(); MX_CAN_Init();
Но на самом деле, на этом настройка не заканчивается. Дело в том, что в STM32 есть очень полезная функция фильтрации сообщений по ID. Как вы помните, ID в CAN относится не к устройству в сети, а к сообщению. То есть одно и то же устройство может рассылать сообщения с разными ID. А поскольку сеть является широковещательной, то приемник будет получать кучу сообщений, среди которых ему нужны только некоторые. И вот для этого и существует фильтрация сообщений. Благодаря аппаратной поддержке не нужно программно проверять ID всех принятых сообщений, можно всего лишь изначально настроить периферию на прием только нужных сообщений.
Настраивается этот механизм следующим образом:
CAN_FilterTypeDef canFilterConfig; canFilterConfig.FilterBank = 0; canFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; canFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; canFilterConfig.FilterIdHigh = 0x0000; canFilterConfig.FilterIdLow = 0x0000; canFilterConfig.FilterMaskIdHigh = 0x0000; canFilterConfig.FilterMaskIdLow = 0x0000; canFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; canFilterConfig.FilterActivation = ENABLE; canFilterConfig.SlaveStartFilterBank = 14; HAL_CAN_ConfigFilter(&hcan, &canFilterConfig);
Для CAN2 (при наличии 2-х модулей CAN в контроллере) будет несколько отличаться, а именно:
canFilterConfig.FilterBank = 14;
Собственно, за "пропускаемые" через фильтр ID отвечают эти поля структуры CAN_FilterTypeDef
:
canFilterConfig.FilterIdHigh = 0x0000; canFilterConfig.FilterIdLow = 0x0000; canFilterConfig.FilterMaskIdHigh = 0x0000; canFilterConfig.FilterMaskIdLow = 0x0000;
FilterIdHigh
– старшая часть IDFilterIdLow
– младшая часть IDFilterMaskIdHigh
– старшая часть маскиFilterMaskIdLow
– младшая часть маски
В данном случае все значения равны 0x0000, это означает, что абсолютно все сообщения будут проходить через фильтр.
Значения FilterIdHigh
и FilterIdLow
определяют идентификатор, с которым будет сравниваться ID принятого сообщения. А FilterMaskIdHigh
и FilterMaskIdLow
отвечают за битовую маску, которая, в свою очередь, определяет, какие биты идентификатора будут проверяться, а какие – нет. Единица в маске означает, что бит, который соответствует положению этой единицы, будет проверен. Сейчас на примере все станет окончательно понятно.
Итак, пусть мы хотим принимать только сообщения с ID из диапазона – 0x200 – 0x20F. Тогда настройка будет такой:
canFilterConfig.FilterIdHigh = 0x200 << 5; canFilterConfig.FilterIdLow = 0x0000; canFilterConfig.FilterMaskIdHigh = 0x7F0 << 5; canFilterConfig.FilterMaskIdLow = 0x0000;
Обратите внимание, что для стандартного идентификатора значения необходимо сместить влево на 5 бит. Запишем в двоичном виде значения 0x200 – 0x20F, смещенные на 5 битов:
Получается, что мы должны проверять 7 старших битов ID, одинаковые во всех идентификаторах. И, в итоге, получаем значение битов маски равным 0x7F0 << 5 (единицы соответствуют тем битам, которые должны соответствовать заданному в FilterIdHigh
значению).
После настройки фильтра мы включаем модуль CAN и разрешаем прерывания по приему данных в FIFO0:
HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
Теперь для приема сообщений CAN мы просто переопределяем соответствующую callback-функцию:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef msgHeader; uint32_t msgId = 0; uint8_t msgData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &msgHeader, msgData); if (msgHeader.IDE == CAN_ID_EXT) { msgId = msgHeader.ExtId; } else { msgId = msgHeader.StdId; } }
В самой функции мы вызываем HAL_CAN_GetRxMessage()
для получения принятых данных и, например, проверяем тип идентификатора (расширенный или стандартный). Дальше уже можно использовать принятые данные по назначению. С приемом все понятно, давайте рассмотрим и процесс передачи.
В STM32F10x реализованы 3 так называемых mailbox'а, то есть по сути "почтовых ящика", в которые мы можем складывать наши сообщения. Далее микроконтроллер аппаратно выбирает, какое сообщение отправить в линию. Можно настроить механизм аналогичный FIFO, то есть первое помещенное в mailbox сообщение и отправлено будет первым. А можно настроить периферию на отправку сообщений в соответствии с приоритетом самих сообщений.
Для каждого из mailbox'ов есть соответствующий callback, который вызывается по окончанию передачи:
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan)
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan)
Отправим тестовое сообщение:
if (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) != 0) { CAN_TxHeaderTypeDef msgHeader; uint8_t msgData[8]; msgHeader.StdId = 0x200; msgHeader.DLC = 8; msgHeader.TransmitGlobalTime = DISABLE; msgHeader.RTR = CAN_RTR_DATA; msgHeader.IDE = CAN_ID_STD; uint32_t mailBoxNum = 0; for (uint8_t i = 0; i < 8; i++) { msgData[i] = i; } HAL_CAN_AddTxMessage(&hcan, &msgHeader, msgData, &mailBoxNum); }
Первым делом вызываем HAL_CAN_GetTxMailboxesFreeLevel(&hcan)
. Функция возвращает количество свободных mailbox'ов. Если есть хоть один свободный mailbox, то начинаем формировать сообщение на передачу - устанавливаем параметры сообщения, идентификатор и, конечно же, непосредственно данные. Затем вызываем функцию HAL_CAN_AddTxMessage()
, которая поместит наше новое сообщение в один из mailbox'ов и активирует соответствующий запрос на передачу.
Обратите внимание, что помимо данных сообщения мы передаем в эту функцию адрес переменной mailBoxNum
. Здесь функция сохранит номер почтового ящика, в который отправлено сообщение, этот номер можно использовать для дальнейшей работы, например, в callback'е по окончанию передачи.
Итак, в общем-то, на этом все - CAN настроили, прием и передачу осуществили ) Проект для статьи доступен по ссылке - MT CAN Example.
До скорых встреч и спасибо за внимание 🤝
Архив поврежден
Добрый день!
Я проверил - скачалось, открылось. Попробуйте еще раз скачать.
Пишет архив поврежден
Может не докачался? Перезагрузил архив.
Я скачал. Архив не поврежден!
Отлично, спасибо!
Спасибо за статью. Помогла стартануть... Но нашёл баг в HAL_CAN_ConfigFiltr(), а конкретно, в режиме LIST, IDешники располагаются не правильно(перепутанно) т.е. если потом использовать Filter Match Index (FMI) как индекс массива данных, то данные сохраняются не ожидаемом порядке...
при нажатии на ссылку пишет: Ничего не найдено...
Хм, через Chrome почему-то не скачивается, разберусь. Пока на диск выложил - https://disk.yandex.ru/d/e5fLkW1EyXPjwg
после отправки комментария файл скачался
Подскажите как открыть проект из Архива ?
Приветствую, тут проект для IAR + Stm32CubeMx.
А можно ли как-то связать CAN и USB интерфейсы, чтобы из CANа в USB передавались данные и обратно. Я сижу, никак сообразить не могу, как это можно сделать.
Можно простейшим способом сделать - для USB класс CDC, отправляются последовательно 12 байт, контроллер их парсит - 4 байта на CAN ID, 8 байт - данные и отправляет сообщение по CAN, аналогично в обратном направлении.
На практике лучше к этим 12-ти байтам добавить признаки начала пакета, контрольную сумму, длину поля данных итд.
Там через callback реализуется или можно без него?
Прием через callback лучше.