Как отправлять несколько CAN-сообщений с разной периодичностью в Ардуино? Разбираем правильный подход

Часто приходят интересные вопросы от разработчиков, которые создают свои проекты на базе нашего CAN-шилда. Вот один из недавних:

«Есть необходимость запускать три разных CAN-сообщения с разной периодичностью: первое — 0,1 с, второе — 0,2 с, третье — 0,5 с. Подскажите, лучше каждому поставить свой генератор ШИМ с заданными параметрами, или поставить один на 0,1 с и каждому сообщению организовать счётчик, или есть решение получше?»

Вопрос отличный! Давайте разберем, почему аппаратные ШИМ-генераторы (PWM) здесь вообще не применимы, как делать не нужно, и как реализовать эту задачу красиво, профессионально и без зависания микроконтроллера.


Почему ШИМ и обычные счетчики — это плохой путь?

  1. ШИМ тут не поможет. ШИМ-генераторы в микроконтроллерах (STM32 или Arduino) управляют физическими пинами платы на высокой частоте (выдают аналоговый сигнал за счет скважности). Отправить цифровой пакет данных в CAN-контроллер с помощью аппаратного ШИМ невозможно.
  2. Один таймер и счетчики — рабочая, но «костыльная» схема. Если сделать один таймер на 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 аналоговых входов шилда!
}

В чем главные плюсы такого кода?

  1. Абсолютная гибкость: Вы можете изменить PERIOD_MSG_2 с 200 мс на 145 мс одной строчкой в начале скетча. Никакая другая логика программы не сломается.
  2. Многозадачность: Микроконтроллер выполняет этот код мгновенно. Свободное время процессора можно пустить на опрос физических кнопок, датчиков или обработку входящих CAN-сообщений.
  3. Безопасность совпадений: Иногда (например, на первой секунде работы) таймеры сработают одновременно. Метод Can.write() отправляет данные в аппаратные почтовые ящики (Mailboxes) процессора STM32. Чип сам выстроит их в очередь и отправит в сеть последовательно с разницей в микросекунды. Ни один пакет не потеряется.

Где взять надежное железо для CAN-проектов?
Если вы хотите собирать устройства, которые работают без сбоев годами — забудьте про «бутерброды» из Arduino Nano и китайских плат MCP2515. Используйте специализированное железо: