CAN-шина и stm32





Здравствуйте.


У большинства читателей шина CAN ассоциируется с автомобилем. Однако если вы не автолюбитель не спешите закрывать статью так как использовать CAN можно где угодно, например в постройке «умного дома», или ещё где-то, где необходима надёжная передача данных.

Преимущества CAN-шины заключаются в том, что требуется всего два провода (витая пара), данные можно передавать на большие расстояния (вплоть до нескольких километров), устойчивость к сильным помехам, и способность автоматически восстанавливать работоспособность после сбоев.

Вся эта надёжность достигается за счёт сложного механизма обработки данных, с помощью таймингов, сегментов, и прочей хитроумной фигни, которую мы рассмотрим по ходу статьи.


Единственный неудобный момент, это необходимость в специальном CAN-трансивере (маленькая восьминогая микруха). Чтобы ничего не паять, можно купить на Али вот такие штуки


Этот трансивер (SN65HVD230, маркируется как VP230) питается от 3.3V, что несомненно является удобством при работе с stm32. Так же можно использовать другие популярные трансиверы — MCP2551 и TJA1050 (он же A1050). У TJA1050 есть небольшой недостаток — в даташите написано что он не поддерживает скорости ниже 60Кбит/с, правда у меня работал даже на меньших скоростях, но не всегда стабильно. Впрочем это не страшно так как вряд ли вы будете использовать столь низкую скорость в своих проектах.


Контакты CTX и CRX подключаются к stm32, а CANH и CANL к витой паре. «Земли» соединять не нужно, это дифференциальный сигнал. К каждой stm'ки подключается свой трансивер.


Топология сети такова…



На шине могут находиться два и более устройств (можно любое количество, однако практически оно ограничивается нагрузочной способностью передатчиков и колеблется от 100 до 200). На двух крайних устройствах должны быть установлены терминирующие резисторы (120 Ом). На платках, про которые я писал выше они уже есть. Если на шине присутствуют промежуточные устройства, то на них резисторы не устанавливаются (на платках выше, их нужно выпаять).

Не рекомендуется делать длинные ответвления для промежуточных устройств, по хорошему ответвлений вообще не должно быть, должно быть так…


Сеть похожа на RS485, но в CAN-шине не нужно программно выбирать направление передачи, нет мастера и слейвов, и нет идентификаторов устройств как у I2C. Каждый шлёт свои данные когда хочет, получают их все присутствующие на шине устройства, а разруливается всё это аппаратно самим CAN-интерфейсом. Подробнее об это ниже.


Пока вы не приобрели трансиверы, можно будет поэкспериментировать без них, для этого есть специальный режим когда одна stm'ка, может сама себе посылать пакеты на одном интерфейсе (и нет, для этого не нужно замыкать между собой ножки как это иногда делается с UART'ом, так работать не будет).


Максимальная скорость передачи 1Мбит/с. Сейчас делаются устройства с большей скоростью (CAN FD), но это нас не интересует.

Длина линии зависит от скорости передачи:

1000 Кбит/с — 40 метров.
500 Кбит/с — 100 метров.
250 Кбит/с — 200 метров.
125 Кбит/с — 500 метров.
10 Кбит/с — 6 километров.

Это наиболее распространённые скорости в промышленности, но встречаются и другие — 800 Кбит/с, 400 Кбит/с, 83.333 Кбит/с, 50 Кбит/с, 20 Кбит/с. Да и вообще можно настроить любую скорость в своих проектах, правда смысла в этом особого нет.




Теперь перейдём к более подробному рассмотрению CAN-шины.

Далее я буду называть устройства подключённые к шине узлами.

Данные в CAN-шине передаются небольшими пакетами, которые называются кадрами (frame). Существуют четыре типа кадров:


Data Frame — основной кадр, в котором передаются различные полезные данные.

Remote Frame — это кадр не содержит полезных данных, и служит для того, чтобы «попросить» какой-либо узел послать кадр. То есть, какой-то узел посылает этот кадр в сеть, а другой узел (который понимает что обращаются к нему) отправляет кадр с полезными данными. Такой способ подходит для опроса каких-то датчиков и немного снижает нагрузку на сеть. О том, как узел понимает что обращаются к нему написано ниже. А вот тут написано что не рекомендуется использовать Remote Frame.

Error Frame — кадр ошибки. Когда один из узлов обнаруживает ошибку формата кадра, он посылает в сеть Error Frame (имеет наивысший приоритет). Другие узлы получая Error Frame понимают что в сети произошла ошибка и последнее сообщение надо считать некорректным.

Overload Frame — этот кадр посылается узлом, который сильно перегружен и не успел переварить входящие сообщения. Послав в сеть кадр Overload Frame он как-бы просит другие узлы подождать немножко и повторно прислать сообщения. Это тоже происходит аппаратно. Сейчас этот кадр практически не используется так как CAN-контроллеры достаточно мощные чтоб успевать всё обработать.


Кадры Error Frame и Overload Frame передаются узлами аппаратно, без нашего участия.



Начнём с детального разбора Data Frame и потихоньку коснёмся всего.


Если мы подключимся к шине CAN-снифером работающим с программой Can Hacker и пульнём в шину кадр, то увидим в программе следующее…


Так выглядит кадр в CAN-шине глазами обывателя. Здесь есть идентификатор кадра (ID), количество полезных байт (DLC), и сами полезные байты (Data), восемь штук. Для анализа CAN-шины этого достаточно.

Все данные в CAN записываются в HEX формате. То есть в нашем случае идентификатор — это 0x0280.


К недостаткам CAN-шины можно отнести ограниченное количество передаваемых полезных байт в одном кадре, их может быть не больше восьми. Меньше можно.


А теперь посмотрим на кадр глазами программиста…



Здесь мы видим:

SOF (Start of Frame) — стартовый бит сообщающий о начале кадра. Позже мы к нему вернёмся так как он выполняет важную роль в работе всей сети.

Identifier — идентификатор кадра. Подчеркну, это не идентификатор какого-либо узла сети, это идентификатор именно кадра. Узлы CAN-шины не имеют никаких идентификаторов или любой другой адресации. Когда какой-то узел посылает кадр, его получают все имеющиеся в сети узлы, и далее каждый узел смотрит на идентификатор и решает нужно ли ему обрабатывать этот кадр или просто отбросить. То есть, при проектировании сети вы прописываете в каждом узле какие идентификаторы ему нужно принять и обработать, а какие игнорировать. При этом вам не надо проверять идентификаторы «вручную» в своей программе. Для этого у stm32 есть аппаратная фильтрация кадров, которая с помощью настраиваемых фильтров сама решает что сделать с кадром имеющим тот или иной идентификатор. То есть, например, вы настроили фильтр так, чтоб принимался только кадр с идентификатором 280, тогда кадры с другими идентификаторами будут отбрасываться аппаратно, не создавая нагрузки на ЦПУ. Фильтры можно настраивать на приём нескольких идентификаторов или группы, или нескольких групп. В общем достаточно гибкая система. Благодаря этой аппаратной фильтрации вам не нужно беспокоится о том, переварят ли вашы узлы весь трафик, даже если он очень плотный.


Идентификаторы бывают двух видов. Изначально, когда проектировалась шина CAN было решено сделать идентификатор 11-ти битным, то есть количество идентификаторов было от 0х00 до 0x07FF (0 — 2047), то есть всего 2048. Спустя некоторое время пришли к выводу что это слишком мало и придумали 29-ти битный идентификатор — от 0х00 до 1FFFFFFF (0 — 536870911). Однако для совместимости с прежней системой решили не увеличивать поле для идентификатора у старого кадра, а сделали два вида кадров. Первый остался с 11-ти битным идентификатором и его назвали стандартным. Второй получил 29-ти битный идентификатор и его назвали расширенным. Таким образом у CAN-шины появилось два вида Data Frame, стандартный и расширенный.

расширенный кадр
Расширенный кадр выглядит немного иначе…



После 11 бит идентификатора идёт бит SSR-бит (Substitute Remote Request — «заменяющий RTR-бит»), потом идёт бит IDE (см. ниже) с записанной в него единицей сообщающей что это расширенный кадр, после чего идут ещё 18 бит идентификатора, а далее всё как у стандартного кадра.


Поле Identifier на картинке обведено фигурной скобкой подписанной как Arbitration. Это говорит о том, идентификатор выполняет ещё и арбитраж на шине — задаёт приоритет кадра. Чем меньше числовое значение идентификатора кадра, тем выше у него приоритет над другими кадрами (см. ниже). Помимо этого, у расширенного кадра приоритет выше чем у стандартного.

RTR — один бит. Если этот бит равен нулю, значит передаётся Data Frame, если равен единице, значит передаётся Remote Frame.

IDE — один бит. Если этот бит равен нулю, значит передаётся стандартный кадр, если равен единице, значит передаётся расширенный кадр.

R — зарезервированный на будущее бит.

DLC — четыре бита определяющие количество полезных байт (DATA) в кадре.

DATA — полезные данные.

CRC — контрольная сумма кадра. Аппаратно вычисляется на основе передаваемых битов (от SOF до поля DATA включительно) и полинома генератора G(x), определенного в ISO 11898-1.

DEL — разделительный бит.

ACK — это бит подтверждения корректной отправки кадра. Перед отправкой кадра передатчик записывает в этот бит единицу… А вот чтоб объяснить что происходит дальше придётся кратко описать работу протокола CAN (чуть ниже).

EOF (End of Frame) — конец кадра. Семь бит высокого уровня.

ITM — три бита высокого уровня для отделения переданного кадра от следующего.


В спецификации CAN высокий уровень (логическая единица) называется рецессивным, а низкий (логический ноль) доминантным. Доминантный бит имеет приоритет над рецессивным. Короче говоря, ноль важнее единицы (ниже будет понятно к чему это сказано).



Когда никто ничего не передаёт, на обоих линях CAN_H и CAN_L устанавливается одинаковое напряжение 2.5 вольта, это значит что шина свободна, а если напряжения разные значит шина занята.

дополнительно
Напряжения логических уровней на шине могут быть разные, но суть остаётся прежняя.



Если у вас один узел работает с 3-х вольтовым трансивером, а другой с 5-ти вольтовым, то в этом нет ничего страшно.


Когда узел хочет передать кадр, он «щупает» шину и если она свободна начинает передачу. Если же шина окажется занята, тогда узел не станет ничего передавать, а будет постоянно «прощупывать» шину в ожидании когда она освободится. Как только она освободится узел тут же начнёт передачу. Такой механизм позволяет узлам не мешать друг другу.

Однако не всё так просто. Поскольку в CAN-шине происходит весьма интенсивный обмен данными, часто происходит так, что два или несколько узлов видят что шина свободна и одномоментно начинают передачу. Вот тут в дело вступает побитовый арбитраж шины основывающийся на идентификаторах кадров.

Арбитраж работает очень просто. Итак, у нас два (или более) узла начали одновременно передавать кадры в шину…


Первый узел (Device A) передаёт кадр с идентификатором 0х0647, а второй (Device В) с идентификатором 0х06C7.

Поскольку узлы ничего не знают друг о друге, то при передачи каждого бита они постоянно мониторят («прощупывают») линию. Если узел передающий в данный момент единицу обнаружит что кто-то в этот же момент передаёт ноль, тогда он тут же прекратит передачу (так как ноль имеет более высокий приоритет чем единица) и будет ждать освобождения шины. Это проиллюстрировано на картинке выше.

В самом начале линия была свободна находясь в высоком (рецессивном) состоянии, далее оба узла передали стартовый бит (низкое состояние, оно же доминантное), далее оба начали передавать идентификаторы. Сначала две единицы, далее один ноль, и наконец первый узел передал ещё один ноль, а второй передал единицу и проиграл арбитраж, так как доминантный уровень (ноль) имеет приоритет над рецессивным (единица). Теперь второй узел будет ждать когда шина освободится и попытается повторить отправку.

Таким образом идентификаторы определяют какой кадр полетит первым, а какой подождёт. Однако возникает вопрос, что произойдёт если два узла одновременно пошлют в сеть кадры с одинаковыми идентификаторами? А ответ очень прост — спецификация CAN предполагает что в сети не должно быть узлов посылающих одинаковые идентификаторы. Тем не менее вы можете сделать так, что у вас несколько узлов будут посылать одинаковые идентификаторы, однако необходимо организовать работу так, чтоб они не ломились в сеть одновременно.


Исходя из прочитанного про арбитраж, становиться ясно что функционирование всей шины, её стабильность и правильная работа, завязана на том, что все узлы должны быть чётко синхронизированы между собой. Синхронизация узлов происходит каждый раз когда кто-то начинает передачу, то есть когда на линии появляется SOF — стартовый бит. Как только он появился все узлы тут же начинают отсчёт времени. Ещё существует ресинхронизация во время передачи кадра, но об этом позже.


И наконец про ACK-бит проверки доставки кадра. Как уже говорилось выше, когда узел передаёт кадр он записывает в этот бит единицу и постоянно «щупает» линию. Когда какой-либо из узлов примет этот кадр, он проверит его на валидность (совпала CRC) и если всё нормально, тогда он изменит этот бит с единицы на ноль. Передающий узел тут же «увидит» что бит изменился, и поймёт что кадр успешно доставлен. При этом никто не знает какой именно узел перевернул бит, может тот кому «нужен» этот кадр, а может тот кто не пропустит его через фильтр и отбросит. Таким образом единственное в чём может быть уверен передающий узел, это то, что хотя бы один из узлов в сети правильно принял его кадр. По идее если линия исправна, то все узлы должны были принять этот кадр.

Если никто из узлов не «ответил», тогда в зависимости от настроек, кадр будет отправлен повторно, либо не будет (см. ниже «Automatic Retransmision»).



Далее займёмся непосредственно настройкой и продолжим изучение.




Традиционно я буду делать описание для BluePill, однако оно так же справедливо и для других камней. Настройка двух CANов на одном МК тоже будет описана.

Если у вас камень с двумя CANами, тогда настраивайте первый, второй не трогайте.


Итак, первым делом нужно посмотреть в мануале на какой шине у вас висит интерфейс CAN. У F103, F105, F205, F303, F407 и F446 это шина APB1


Желательно чтоб частота этой шины была либо 16 МГц, либо 32 МГц. Это нужно для оптимальной настройки.


Теперь активируем CAN и видим различные настройки (у меня уже настроено для скорости 500Кб)



Первый раздел (Bit Timings Parameters) самый сложный, он отвечает за настройку таймингов и скорости передачи.


Выше я писал что CAN-интерфейс тщательно мониторит линию. Так вот, мониторится не просто логические уровни, а каждый бит «раскладывается» на сегменты которые должны длиться определённое количество квантов времени. В данном примере один квант времени равен 125.0 наносекунд (Time Quantum).

Вот так выглядит один бит разложенный на сегменты SYNC_SEG, PROP_SEG, PHASE_SEG1 и PHASE_SEG2…




Теперь разберёмся что же мы будем настраивать и как оно работает…



System Clock это частота шины APB1. Предделителем (Prescaler) мы делим её на какое-то число и получаем время одного кванта (tQ). То есть один квант будет равен одному «тику» CAN System Clock.

Согласно спецификации CAN, сегмент SYNC_SEG всегда длится ровно один квант. Остальные три сегмента могут длится от 1 до 8 квантов. Их количество нужно настраивать. Время одного кванта и количество этих квантов в одном бите определяет скорость шины (см. ниже).

О том, какую функцию выполняют сегменты, вы можете почитать в мануале по ссылке ниже. Здесь я это расписывать не буду так как во-первых это муторно, а во-вторых практического смысла в этих пояснениях нет ибо всё это работает аппаратно. Если в двух словах, то они нужны для коррекции и постоянной ресинхронизации шины в процессе передачи кадра, так как из-за удалённости узлов друг от друга и не идеальности кварцевых резонаторов установленных на них, происходит расхождение времени, а шина должна работать чётко синхронно.

Sample Point — это точка в которой происходит «захват» бита.

Количество квантов в сегментах PROP_SEG, PHASE_SEG1 и PHASE_SEG2 настраивается так, чтобы Sample Point находилась в районе 87.5% длительности бита (см. ниже).

Все эти сегменты работают аппаратно.


Это очень поверхностное описание, сделанное чтоб вы понимали за что отвечают настройки в Кубе. Все подробности и нюансы можно почитать в этом мануале.



Итак, вернёмся в Куб и теперь уже более осмысленно посмотрим что у меня там настроено…



Указываем Prescaler (for Time Quantum) 4 и получаем время одного кванта Time Quantum 125 нс. Частота APB1 у нас 32МГц, делим на 4, получаем 8МГц, 1 / 8000000 = 0.000000125.

Time Quanta in Bit Segment 1 — это количество квантов из которых состоят два сегмента PROP_SEG и PHASE_SEG1 (они объединены).

Time Quanta in Bit Segment 2 — это количество квантов из которых состоит последний сегмент (PHASE_SEG2).



В результате получается что один бит у нас состоит из 16 квантов — SYNC_SEG (один квант) + Bit Segment 1 + Bit Segment 2, общей длительностью 2000 нс (Time for one Bit). Соответственно скорость передачи данных (Baud Rate) получилась 500 Кбит/с.


ReSynchronization Jamp Width — это значение от 1 до 4 квантов, на которое может аппаратно увеличиваться или уменьшаться длительность сегментов PHASE_SEG1 и PHASE_SEG2 для более точной коррекции. Цель коррекции — поместить точку «захвата» бита в более удачный момент и ресинхронизировать узлы на шине. Короче говоря, на сколько я понимаю, если линия очень длинная или вокруг неё много помех, из-за чего может проявляться неустойчивая работа, тогда это значение можно увеличить для улучшения стабильности. Однако увеличивая это значение вы снижаете скорость передачи.


Ну и на конец разберёмся откуда взялись цифры 13 (Bit Segment 1) и 2 (Bit Segment 2).

Откройте онлайн калькулятор


В выпадающем списке выберите ST Microelectronics bxCAN и в Clock Rate укажите частоту APB1 шины, в нашем случае это 32.

Как видите здесь написано что 87.5% это предпочтительное значение для захвата бита (выше я про это говорил). Больше нам ничего не нужно. Теперь нажмите кнопку Request Table и получите такую картинку…



В первой колонке показаны скорости которые вы можете настроить. Смотрим значения для 500 Кбит/с (жёлтым выделены оптимальные значения). Всё как в нашем примере — предделитель 4, общее количество квантов 16, 13 квантов для Bit Segment 1, и 2 кванта для Bit Segment 2. Sample Point получился 87.5%.

Если например захотите настроить скорость 250 Кбит/с, тогда достаточно изменить только предделитель, а если 800 Кбит/с, тогда только количество квантов для сегментов. Думаю всё понятно, и как и обещал всё просто. Вы сможете


Почему я писал что желательно чтоб частота APB1 была 32 или 16 МГц. Если указать частоту например 36 МГц, тогда мы получим вот такой результат…


Видно что Sample Point немного «уплывает», но это не критично, можно работать.




Далее нам нужно разобраться со следующим блоком настроек — Basic Parameters.




Time Triggered Communication Mode — в этом документе, описывающим работу этого пункта, написано что это сложный в понимании (прям так и написано) механизм синхронизации узлов. Суть его примерно следующая: в обычном режиме у нас синхронизация происходит по стартовому биту, а если включить этот пункт тогда узел превращается в Time Master и с определённым интервалом (есть внутренний счётчик) начинает посылать в сеть сообщения (кадры), по которым другие узлы синхронизируются. Плюс к этому, вроде как устанавливаются временные рамки для передачи сообщений узлами. Точно я не могу сказать ибо сам не стал глубоко вникать в этот вопрос. Так же мне не понятно поддерживается ли это любыми CAN-сетями и устройствами. В общем я это не включаю.

Вот ещё пара документа на эту тему, кому какой лучше «зайдёт» )))

www.can-cia.org/fileadmin/resources/documents/proceedings/2006_fredriksson.pdf

www.cs.put.poznan.pl/wswitala/download/pdf/CiA2000Paper_1.pdf

Да, далее я буду называть кадры сообщениями, и наоборот.


Automatic Bus-Off Management — это важный и полезный пункт. У модуля CAN есть два счётчика ошибок — Transmit Error Counter (счетчик ошибок передачи) и Receive Error Counter (счетчик ошибок приема). При возникновении ошибки приёма Receive Error Counter увеличивается на единицу, при ошибке передачи Transmit Error Counter увеличивается сразу на 8 (видимо это объясняется тем, что отправка считается более важным действием). При успешном приёме или передаче соответствующий счётчик уменьшается на единицу. Как только какой либо из счётчиков достигнет значения 255, модуль CAN перейдёт в состояние Bus-Off — ничего не передаёт и не отправляет в шину.

Если Automatic Bus-Off Management включён, тогда модуль CAN, будет автоматически восстанавливаться. Восстановление заключается в том, что модуль ждёт когда на шине будет «тишина» в течении времени, равное времени передачи 11 бит 128 раз подряд, и если всё окей, тогда включается в работу.

Если Automatic Bus-Off Management отключён, тогда надо «вручную» отлавливать момент перехода узла в состояние Bus-Off и переинициализировать его. В общем желательно включить этот пункт.


Automatic Wake-Up Mode — здесь всё понятно, если включено, то активность на шине разбудит спящий узел без дополнительных программностей (слово новое придумал)


Automatic Retransmission — если этот пункт включён, тогда узел будет повторять попытки отправить сообщение если не получает подтверждения приёма (как вы помните, это инвертированный ACK-бит).

Если пункт отключён, тогда узел просто пульнёт сообщение один раз и ему будет безразлично долетело оно или нет. Желательно чтоб этот пункт был включён.

Вот тут описана интересная ситуация с Bus-Off и Automatic Retransmission.

Если используется режим Time Triggered Communication (см. выше), тогда этот пункт должен быть отключён.


Receive Fifo Locked Mode — у приёмника есть два независимых буфера (RX_FIFO_0 и RX_FIFO_1), можно пользоваться одним буфером или обоими. Какие сообщения будут попадать в нулевой или в первый буфер зависит от настроек фильтров (см. ниже). Каждый из буферов разделён на три ячейки называющиеся почтовыми ящиками (см. рис. ниже). Каждый почтовый ящик может хранить одно сообщение.

Если этот режим отключён, тогда если все ящики заполнены и сообщения не вычитываются (например вы не успеваете вычитывать сообщения по причине слишком интенсивного обмена данными), тогда последнее сообщение будет перезаписываться новым.

Если этот режим включён, тогда если все ящики заполнены и сообщения не вычитываются, новые поступающие сообщения будут отбрасываться. То есть в ящиках будут оставаться старые сообщения.

Включить или отключить этот режим, решать вам, исходя из того что важнее.


Transmit Fifo Priority — у передатчика тоже есть буфер (один) разделённый на три почтовых ящика. Отправляемые сообщения помещаются в эти почтовые ящики, а оттуда уже улетают в сеть. Если слишком часто отправлять сообщения, то они будут накапливаться в этих ящиках.

Если режим включён, тогда сообщения улетают из ящиков в хронологическом порядке, то есть по принципу FIFO — первым пришёл, первым вышел.

Если режим отключён, тогда первыми улетают сообщения с более высоким приоритетом (как вы помните приоритет задаётся идентификатором). Таким образом, при очень интенсивной отправке сообщений может получится так, что сообщение с низким приоритетом никогда не будет отправлено, и так и будет валяться в своём ящике всё время.


Схема почтовых ящиков…


Сверху мы видим три почтовых ящика для отправки, которые управляются блоком Transmission Scheduler, и шесть почтовых ящиков для приёма. Active Core это грубо говоря сам интерфейс, Memory Access Controller разруливает трафик, а Acceptance Filters это аппаратные фильтры, которые пропускают или отбрасывают сообщения в зависимости от настроек этих самых фильтров, давая или не давая им попасть во входящие почтовые ящики.

Вся работа с почтовыми ящиками (кроме вычитывания входящих сообщений и отправки) происходит на аппаратном уровне.



И на конец последний пункт настройки — Operating Mode — режим работы или способ подключения.


Normal


К плате подключён трансивер и она работает к полноценный узел в сети из двух и более устройств. То есть обычная работа.


Loopback


Плата с трансивером будет передавать данные в шину и слушать себя же одновременно. Данные из шины получать не будет.


Silent


Плата с трансивером получает данные из шины, но сама ничего не передаёт. Этот вариант подойдёт когда вам нужно почитать CAN-шину в автомобиле, и при этом ничего туда случайно не отправить.


Loopback combined with Silent


Плата без трансивера. Все данные крутятся внутри МК. Этот режим подойдёт для тестирования программы при отсутствие трансивера и другого узла.


Теперь пришло время попрограммировать.




Поскольку наверняка не у всех читателей есть трансиверы под рукой, а попробовать хочется, мы вначале напишем программу для режима Loopback combined with Silent. Укажите его в настройках, остальное сделайте как на картинках и включите прерывания…


Прерывания для буфера RX_FIFO_0 (CAN RX0) будет вызываться когда прилетит кадр и пройдя через фильтры будет помещён в один из трёх почтовых ящиков. В какой именно ящик для нас не имеет значения, важно сразу же его прочитать, прямо в прерывании. Буфером RX_FIFO_1 мы не будем пользоваться, хватит и одного, позже мы вернёмся к этому вопросу.

Обратите внимание что у F103 (и у некоторых других камней) это прерывание пересекается с прерыванием от USB. Если вы планируете пользоваться USB, тогда включите прерывание для буфера RX_FIFO_1, буфером RX_FIFO_0 вы не будете пользоваться. Работе USB он не помешает.


Прерывание CAN SCE вызывается при возникновении ошибок.

Прерывание CAN TX вызывается когда кадр отправлен — оно нам не сильно интересно поэтому не включаем.



Объявляем глобально две структуры, два массива и переменную…

/* USER CODE BEGIN PV */
CAN_TxHeaderTypeDef TxHeader;
CAN_RxHeaderTypeDef RxHeader;
uint8_t TxData[8] = {0,};
uint8_t RxData[8] = {0,};
uint32_t TxMailbox = 0;

Структура TxHeader отвечает за отправку кадров, ниже мы её заполним.
Структура RxHeader для принятого кадра, из неё мы будем читать при приёме.
В массив TxData мы будем заносить полезные данные которые хотим передать.
В массиве RxData будут лежать полезные данные из принятого кадра.
С переменной TxMailbox ничего делать не нужно.



Добавляем колбек для приёма данных (для буфера RX_FIFO_0)

/* USER CODE BEGIN 0 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);	 
    }
}

При приёме любого кадра мы тут же забираем его из почтового ящика с помощью функции HAL_CAN_GetRxMessage(...) и мигаем светиком.


Если у вас настроено прерывание для буфера RX_FIFO_1, тогда колбек такой…

void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO1, &RxHeader, RxData) == HAL_OK)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);	 
    }
}



И колбек для ошибок…

void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan)
{
    uint32_t er = HAL_CAN_GetError(hcan);
    sprintf(trans_str,"ER CAN %lu %08lX", er, er);
    HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 100);
}



Перед бесконечным циклом заполняем структуру отвечающую за отправку кадров…

/* USER CODE BEGIN 2 */
TxHeader.StdId = 0x0378;
TxHeader.ExtId = 0;
TxHeader.RTR = CAN_RTR_DATA; //CAN_RTR_REMOTE
TxHeader.IDE = CAN_ID_STD;   // CAN_ID_EXT
TxHeader.DLC = 8;
TxHeader.TransmitGlobalTime = 0;


StdId — это идентификатор стандартного кадра.

ExtId — это идентификатор расширенного кадра. Мы будем отправлять стандартный поэтому сюда пишем 0.

RTR = CAN_RTR_DATA — это говорит о том, что мы отправляем кадр с данными (Data Frame). Если указать CAN_RTR_REMOTE, тогда это будет Remote Frame.

IDE = CAN_ID_STD — это говорит о том, что мы отправляем стандартный кадр. Если указать CAN_ID_EXT, тогда это будет расширенный кадр. В StdId нужно будет указать 0, а в ExtId записать расширенный идентификатор.

DLC = 8 — количество полезных байт передаваемых в кадре (от 1 до 8).

TransmitGlobalTime — относится к Time Triggered Communication Mode, мы это не используем поэтому пишем 0.



Заполняем массив для отправки полезных данных каким-нибудь хламом…

for(uint8_t i = 0; i < 8; i++)
{
    TxData[i] = (i + 10);
}



Запускаем CAN…

HAL_CAN_Start(&hcan);


Если CANов несколько, тогда запускаем первый…

HAL_CAN_Start(&hcan1);



И активируем события которые будут вызывать прерывания…

HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_ERROR | CAN_IT_BUSOFF | CAN_IT_LAST_ERROR_CODE);

CAN_IT_RX_FIFO0_MSG_PENDING — вызовет прерывание при получении сообщения в буфер CAN_RX_FIFO0. Колбек для этого мы прописали. Если используется буфер CAN_RX_FIFO1, тогда макрос такой — CAN_IT_RX_FIFO1_MSG_PENDING.

Остальное это прерывания из-за различных ошибок. Колбек для них мы тоже прописали. По большому счёту ошибки будут происходить только если что-то не в порядке с линией. Можно добавить в колбек мигалку красным светиком.

Вот все возможные события, которые можно добавить в функцию…


stm32f1xx_hal_can.h

И различные колбеки…


stm32f1xx_hal_can.c

Думаю что тут всё понятно из названий. Да и опять же, можно спокойно обходится без этих прерываний.



Теперь добавим один фильтр, который будет пропускать все сообщения. Хотя бы один фильтр должен быть настроен иначе работать не будет. Спускаемся вниз и находим функцию static void MX_CAN_Init(void).

Добавляем в неё структуру для конфигурации фильтров…

/* USER CODE BEGIN CAN_Init 0 */
CAN_FilterTypeDef  sFilterConfig;
/* USER CODE END CAN_Init 0 */


И настройку фильтра…

/* USER CODE BEGIN CAN_Init 2 */
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; 
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
//sFilterConfig.SlaveStartFilterBank = 14;

if(HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE END CAN_Init 2 */

Подробно про фильтры будет рассказано в следующей части, а здесь только краткое пояснение.

У каждого модуля CAN есть 14 фильтров. У F103 один модуль CAN соответственно у него 14 фильтров с 0 по 13. Камни в которых есть два модуля CAN имеют в наличии 28 фильтров, с 0 по 13 для CAN_1, и с 14 по 28 для CAN_2. Каждый фильтр называется «банком» и имеет порядковый номер. В данном случае мы используем банк-фильтр номер 0 (первый элемент структуры).

Пропускаем несколько элементов (в них записываются идентификаторы кадров, которые нужно принять) и видим что в элемент FilterFIFOAssignment мы записали макрос CAN_RX_FIFO0. Это значит что фильтр будет работать с буфером RX_FIFO_0. Поскольку мы не настроили никаких фильтров для буфера RX_FIFO_1, он не будет принимать участия в работе, все сообщения будут приходить только в буфер RX_FIFO_0. Если вы настраивали прерывание для буфера RX_FIFO_1, тогда в FilterFIFOAssignment нужно записать CAN_RX_FIFO1.

Чтобы задействовать оба буфера нужно настроить хотя бы два фильтра для разных идентификаторов и тогда можно будет в один буфер пропускать одни сообщения, а в другой другие. Тогда будет смысл в использовании обоих буферов.

В конце вызывается функция конфигурации фильтра.



В результате функция инициализации CAN будет выглядеть так…

static void MX_CAN_Init(void)
{
  /* USER CODE BEGIN CAN_Init 0 */
  CAN_FilterTypeDef  sFilterConfig;
  /* USER CODE END CAN_Init 0 */

  /* USER CODE BEGIN CAN_Init 1 */

  /* USER CODE END CAN_Init 1 */
  hcan.Instance = CAN1;
  hcan.Init.Prescaler = 4;
  hcan.Init.Mode = CAN_MODE_NORMAL;
  hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
  hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
  hcan.Init.TimeSeg2 = CAN_BS2_2TQ;
  hcan.Init.TimeTriggeredMode = DISABLE;
  hcan.Init.AutoBusOff = ENABLE;
  hcan.Init.AutoWakeUp = DISABLE;
  hcan.Init.AutoRetransmission = ENABLE;
  hcan.Init.ReceiveFifoLocked = DISABLE;
  hcan.Init.TransmitFifoPriority = ENABLE;
  if (HAL_CAN_Init(&hcan) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN CAN_Init 2 */
  sFilterConfig.FilterBank = 0;
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; 
  sFilterConfig.FilterIdHigh = 0x0000;
  sFilterConfig.FilterIdLow = 0x0000;
  sFilterConfig.FilterMaskIdHigh = 0x0000;
  sFilterConfig.FilterMaskIdLow = 0x0000;
  sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
  sFilterConfig.FilterActivation = ENABLE;
  //sFilterConfig.SlaveStartFilterBank = 14;

  if(HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE END CAN_Init 2 */

}




И на конец последнее что нужно сделать, это добавить в бесконечный цикл отправку сообщений…

/* USER CODE BEGIN WHILE */
while (1)
{
	while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0);

	if(HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK)
	{
		HAL_UART_Transmit(&huart1, (uint8_t*)"ER SEND\n", 8, 100);
	}
	
	HAL_Delay(500);
}


Функция HAL_CAN_GetTxMailboxesFreeLevel(...) возвращает количество свободных ящиков для отправки (как вы помните у нас их три), то есть она должна вернуть 1, 2 или 3. Если все ящики заняты (сообщения не успели улететь) тогда будет возвращаться 0. Соответственно мы тормозим программу пока не освободится хотя бы один ящик.

Функция HAL_CAN_AddTxMessage(...) отправляет сообщение в почтовый ящик (оттуда оно улетает автоматически). Аргументами мы передаём структуру в которой у нас записаны настройки сообщения (идентификатор и т.д.), и массив с полезными данными. Последний аргумент нам не интересен.

Собственно это всё, можно прошивать. Плата будет сама себе отправлять сообщения, а светик будет мигать в кобеке приёма.



Чтобы было немного интереснее, будем передавать сообщения с разными идентификаторами и менять нулевой элемент в массиве с полезными данными…

/* USER CODE BEGIN WHILE */
while (1)
{
	TxHeader.StdId = 0x0378;
	TxData[0] = 90;

	while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0);

	if(HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK)
	{
		HAL_UART_Transmit(&huart1, (uint8_t*)"ER SEND\n", 8, 100);
	}

	HAL_Delay(500);


	TxHeader.StdId = 0x0126;
	TxData[0] = 100;

	while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0);

	if(HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK)
	{
		HAL_UART_Transmit(&huart1, (uint8_t*)"ER SEND\n", 8, 100);
	}

	HAL_Delay(500);

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}



В колбеке будем проверять идентификаторы и выводить инфу в USART…

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);

        if(RxHeader.StdId == 0x0378)
        {
        	snprintf(trans_str, 128, "ID %04lX %d\n", RxHeader.StdId, RxData[0]);
        	HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 100);
        }
        else if(RxHeader.StdId == 0x0126)
        {
        	snprintf(trans_str, 128, "ID %04lX %d\n", RxHeader.StdId, RxData[0]);
        	HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 100);
        }
    }
}






Если у вас есть две платы с трансиверами, тогда можете настроить режим Normal…


И залить этот проект в обе платы — они начнут обмениваться данными между собой. Только пропишите разные идентификаторы для отправки для разных плат в бесконечном цикле.




Статья получилась большая поэтому описание настройки фильтров и использование двух CAN модулей на одном камне будет в следующей части.


Проект на Github.


Это всё, всем спасибо


Телеграм-чат istarik

Телеграм-чат STM32


  • 1725
Поддержать автора




Telegram-чат istarik

Задать вопрос по статье
Telegram-канал istarik

Известит Вас о новых публикациях






Комментарии (0)

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.