Часто приходят интересные вопросы от разработчиков, которые создают свои проекты на базе нашего CAN-шилда. Вот один из недавних:
«Есть необходимость запускать три разных CAN-сообщения с разной периодичностью: первое — 0,1 с, второе — 0,2 с, третье — 0,5 с. Подскажите, лучше каждому поставить свой генератор ШИМ с заданными параметрами, или поставить один на 0,1 с и каждому сообщению организовать счётчик, или есть решение получше?»
Вопрос отличный! Давайте разберем, почему аппаратные ШИМ-генераторы (PWM) здесь вообще не применимы, как делать не нужно, и как реализовать эту задачу красиво, профессионально и без зависания микроконтроллера.
Почему ШИМ и обычные счетчики — это плохой путь?
- ШИМ тут не поможет. ШИМ-генераторы в микроконтроллерах (STM32 или Arduino) управляют физическими пинами платы на высокой частоте (выдают аналоговый сигнал за счет скважности). Отправить цифровой пакет данных в CAN-контроллер с помощью аппаратного ШИМ невозможно.
- Один таймер и счетчики — рабочая, но «костыльная» схема. Если сделать один таймер на 100 мс (0.1 с) и инкрементировать счетчик, то первое сообщение мы будем слать каждый такт, второе — каждый 2-й такт, а третье — каждый 5-й. Но что делать, если завтра вам понадобится отправить четвертое сообщение с периодом 75 мс или 1000 мс? Придется полностью пересчитывать всю логику, искать наименьший общий делитель частот и переписывать код.
Как делать правильно? Метод независимых программных таймеров
В автомобильных ЭБУ и серьезной промышленной автоматике используется подход независимых неблокирующих таймеров на базе функции millis().
Для каждого CAN-сообщения заводится своя собственная переменная времени. Микроконтроллер STM32 на огромной скорости крутит бесконечный цикл loop(). Как только время «натикало» для конкретного сообщения — оно отправляется в шину. Никаких взаимных зависимостей, жестких счетчиков и деления частот. Вы можете добавлять десятки сообщений с абсолютно любыми интервалами!

Реализация на нашем CAN-шилде (STM32)
Для отправки сообщений будем использовать наш Кан Шилд с открытым исходным кодом. Шаблон кода для эмулятора Кан шины вы найдёте в репозиториях на Гитхабе и GitVerse. Сама же плата есть на озоне, как голая плата, так и полностью собранная и в корпусе, вам останется только припаять провода, написать код (Ардуино или HAL, или даже на регистрах если хотите) и загрузить код в плату.
Благодаря встроенному аппаратному CAN-контроллеру в чипе STM32F103, плата мгновенно отправляет пакеты, даже если моменты отправки разных сообщений совпадут в одну миллисекунду. Очередь отправки отработает на уровне «железа».
Ниже — готовый рабочий скетч (Arduino IDE + библиотека STM32_CAN), решающий задачу автора вопроса:
#include <Arduino.h>
#include <STM32_CAN.h>
// Скорость шины данных (например, стандартная автомобильная 500 кбит/с)
const uint32_t CAN_SPEED = 500000;
// =========================================================================
// ПЕРИОДЫ ОТПРАВКИ ДЛЯ КАЖДОГО СООБЩЕНИЯ (в миллисекундах)
// =========================================================================
const unsigned long PERIOD_MSG_1 = 100; // 0.1 секунды
const unsigned long PERIOD_MSG_2 = 200; // 0.2 секунды
const unsigned long PERIOD_MSG_3 = 500; // 0.5 секунды
// Переменные для хранения времени последней отправки каждого пакета
unsigned long last_time_msg1 = 0;
unsigned long last_time_msg2 = 0;
unsigned long last_time_msg3 = 0;
// Инициализируем аппаратный CAN STM32
STM32_CAN Can(CAN1, DEF);
void setup() {
Can.begin();
Can.setBaudRate(CAN_SPEED);
}
void loop() {
// Получаем текущее время работы микроконтроллера
unsigned long current_time = millis();
// ---------------------------------------------------------------------
// ТАЙМЕР ДЛЯ СООБЩЕНИЯ №1 (Период: 100 мс / ID: 0x101)
// ---------------------------------------------------------------------
if (current_time - last_time_msg1 >= PERIOD_MSG_1) {
last_time_msg1 = current_time;
CAN_message_t msg1;
msg1.id = 0x101;
msg1.len = 8;
for(int i=0; i<8; i++) msg1.buf[i] = 0x11; // Тестовые данные
Can.write(msg1);
}
// ---------------------------------------------------------------------
// ТАЙМЕР ДЛЯ СООБЩЕНИЯ №2 (Период: 200 мс / ID: 0x202)
// ---------------------------------------------------------------------
if (current_time - last_time_msg2 >= PERIOD_MSG_2) {
last_time_msg2 = current_time;
CAN_message_t msg2;
msg2.id = 0x202;
msg2.len = 8;
for(int i=0; i<8; i++) msg2.buf[i] = 0x22; // Тестовые данные
Can.write(msg2);
}
// ---------------------------------------------------------------------
// ТАЙМЕР ДЛЯ СООБЩЕНИЯ №3 (Период: 500 мс / ID: 0x303)
// --------------------------------=====================================
if (current_time - last_time_msg3 >= PERIOD_MSG_3) {
last_time_msg3 = current_time;
CAN_message_t msg3;
msg3.id = 0x303;
msg3.len = 8;
for(int i=0; i<8; i++) msg3.buf[i] = 0x33; // Тестовые данные
Can.write(msg3);
}
// Плата не заблокирована функцией delay(), цикл loop() крутится свободно.
// Здесь вы можете параллельно опрашивать 6 аналоговых входов шилда!
}
В чем главные плюсы такого кода?
- Абсолютная гибкость: Вы можете изменить
PERIOD_MSG_2с 200 мс на 145 мс одной строчкой в начале скетча. Никакая другая логика программы не сломается. - Многозадачность: Микроконтроллер выполняет этот код мгновенно. Свободное время процессора можно пустить на опрос физических кнопок, датчиков или обработку входящих CAN-сообщений.
- Безопасность совпадений: Иногда (например, на первой секунде работы) таймеры сработают одновременно. Метод
Can.write()отправляет данные в аппаратные почтовые ящики (Mailboxes) процессора STM32. Чип сам выстроит их в очередь и отправит в сеть последовательно с разницей в микросекунды. Ни один пакет не потеряется.
Где взять надежное железо для CAN-проектов?
Если вы хотите собирать устройства, которые работают без сбоев годами — забудьте про «бутерброды» из Arduino Nano и китайских плат MCP2515. Используйте специализированное железо:
- Голая плата на Ozon — для тех, кто любит паять сам.
- Собранный шилд в корпусе на Ozon — готовое фабричное устройство в защитном кожухе.
- Готовые архитектурные шаблоны и примеры: репозитории на GitHub и GitVerse.