Недавно мы разбирались с теоретическими аспектами работы протокола 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 лучше.
Здравствуйте! Скажите пожалуйста по какой формуле высчитывается длительность одного кванта (Tq
=222.2нс)? Не очень ясно как рассчитывается это время.
Привет!
Допустим, частота тактирования для модуля CAN в целом = 36 МГц (он на APB1 обычно).
Для частоты 36 МГц период составляет 1 / 36M = 27.77 нс.
В настройках в STM32CubeMx у нас Prescaler (for Time Quantum) равен 8.
Из этого всего длительность одного кванта = 22.77 нс * 8 = 222.22 нс.
То есть по итогу отправная точка - период тактовых импульсов модуля CAN, а квант времени складывается из N (Prescaler (for Time Quantum)) этих импульсов.
Спасибо большое, теперь понятно!
Отлично)