Files
templates/c/set-protocol/ports/stm32-bxcan/README.md

32 KiB
Raw Blame History

ProtoCAN: порт STM32 bxCAN (STM32 HAL)

stm32-bxcan — порт прикладного ProtoCAN для STM32 HAL, перенесённый из SETCAN. Модуль принимает кадры, управляет HAL CAN/RTC/TIM и вызывает обработчики приложения. Формат 29-битного идентификатора и его переносимая реализация принадлежат общему ядру c/set-protocol в этом же репозитории templates.

Порт использует classic bxCAN (HAL_CAN_*), RTC и TIM. Для FDCAN на STM32G4/H7 требуется другой аппаратный порт. Здесь нет .ioc, startup-файлов, linker script или настроек тактирования: их предоставляет проект STM32CubeIDE.

Схема подключения: приложение → stm32-bxcan → STM32 HAL → bxCAN/RTC/TIM. Для упаковки и разбора CAN ID порт вызывает общее ядро pcan_id. Старый API PROTOCAN_* и имена protocan.c/.h сохранены для существующих прошивок. Отдельный репозиторий и вложенный сабмодуль SETCAN больше не нужны.

Исходная версия: SETCAN c8eec559785ab809469483dc58d3976ec9db340a. История SETCAN включена в историю templates отдельным родителем коммита переноса; прежние пути можно посмотреть через git show c8eec55:Inc/protocan.h и git log c8eec55.

Состав

c/set-protocol/
├── include/pcan_id.h
├── src/pcan_id.c
└── ports/stm32-bxcan/
    ├── protocan.h   # типы протокола, настройки устройства и публичный API
    ├── protocan.c   # приём, буферизация, разбор и отправка CAN-кадров
    └── README.md

Модуль зависит от файлов main.h и can.h, сгенерированных STM32CubeMX, а также от HAL-драйверов CAN, RTC и TIM. В сборку добавляются c/set-protocol/ports/stm32-bxcan/protocan.c и c/set-protocol/src/pcan_id.c. В include paths добавляются c/set-protocol/ports/stm32-bxcan, c/set-protocol/include и каталог CubeMX с main.h/can.h. Если прошивка уже собирает общее ядро SETProtocol, второй раз добавлять pcan_id.c не нужно. HAL-порт не входит в host-сборку CMake общего ядра.

Файл Зависимости
protocan.h main.h, can.h, общий pcan_id.h
protocan.c protocan.h, STM32 HAL CAN/RTC/TIM, CMSIS
../../src/pcan_id.c переносимое ядро C99, ../../include

Документация протокола

Каноническое описание теперь находится рядом с переносимой реализацией:

Файлы в doc/setcan/protocan сохранены как исторический снимок SETCAN и больше не являются источником истины.

Формат расширенного CAN ID

Протокол размещает служебные поля в 29-битном Extended ID:

Биты Поле Размер Назначение
28 Priority 1 0 — критический, 1 — стандартный приоритет
27 Route 1 0 — от управляющего модуля, 1 — от устройства
26..24 DeviceType 3 тип устройства, 0..7
23..20 DeviceID 4 номер устройства, 0..15
19..16 MsgType 4 тип сообщения
15..0 MsgBody 16 тип команды, адрес, ID датчика или код ошибки

Типы сообщений:

Значение Тип
0x0 широковещательное (BROADCAST)
0x1 дискретное (DISCRETE)
0x2 аналоговое (ANALOG)
0x3 общее адресное пространство
0x4 Modbus Coil
0x5 Modbus Discrete
0x6 Modbus Holding
0x7 Modbus Input
0x8 ошибка
0x9 управление загрузчиком (BOOT_CONTROL, зарезервировано)
0xA данные прошивки, слот A (BOOT_DATA_A, зарезервировано)
0xB данные прошивки, слот B (BOOT_DATA_B, зарезервировано)
0xC состояние загрузчика (BOOT_STATUS, зарезервировано)
0xD обнаружение загрузчиков (BOOT_DISCOVERY, зарезервировано)
0xE настройка привязки датчика (SETTINGS)
0xF пульс присутствия устройства

Разметка MsgBody зависит от типа сообщения:

  • broadcast: младшие 4 бита — дополнительное поле, старшие 12 бит — команда;
  • discrete: младшие 12 бит — данные/адрес, старшие 4 бита — подтип;
  • analog: младшие 12 бит — ID датчика, старшие 4 бита — тип величины;
  • Modbus: младшие 4 бита — количество регистров, старшие 12 бит — начальный адрес;
  • SETTINGS: старший байт — номер сборки Z, младший байт — позиция Y;
  • error: младший байт — код ошибки, старший байт — дополнительная информация.

Для регистров uint16_t в CAN payload используется порядок байтов little-endian: сначала младший байт, затем старший.

Пара DeviceType/DeviceID образует уникальный адрес прибора. Три бита DeviceType задают 8 типов, четыре бита DeviceID — 16 экземпляров каждого типа; всего на одной шине можно адресовать до 128 приборов. Загрузочные кадры так же адресуются этой парой и не требуют изменения 29-битного CAN ID.

Разметка ID реализована C-битовыми полями. Она соответствует используемому STM32 GCC ABI, но не является переносимой между произвольными компиляторами без проверки фактического расположения битов.

Подключение к STM32-проекту

1. Добавить файлы

Скопировать или подключить к сборке:

  • c/set-protocol/ports/stm32-bxcan/protocan.h;
  • c/set-protocol/ports/stm32-bxcan/protocan.c;
  • c/set-protocol/src/pcan_id.c (если ещё не собирается в составе ядра).

Каталоги c/set-protocol/ports/stm32-bxcan и c/set-protocol/include добавить в include paths компилятора.

2. Настроить устройство

В protocan.h для прибора привязки DS18B20 заданы выбранные значения:

#define CURRENT_TYPE_DEVICE 0b111  /* DeviceType = 0x7 */
#define CURRENT_ID_DEVICE   0b1111 /* DeviceID   = 0xF */

При необходимости изменить размер программного RX-буфера:

#define PROTOCAN_RX_BUFFER_SIZE 128

Полезная ёмкость кольцевого буфера на один элемент меньше заданного размера, то есть при значении 128 в нём помещается 127 ожидающих кадров.

3. Настроить CubeMX

В проекте должны быть инициализированы:

  • CAN с поддержкой Extended ID;
  • RTC;
  • базовый таймер, задающий период отправки пульса.

Модуль использует CAN FIFO0 и фильтры с номерами 0, 1, 2. Адресованный фильтр принимает Route=0 (запрос от ПМ) для текущих DeviceType/DeviceID; отдельные фильтры принимают broadcast и pulse. В PROTOCAN_CONFIG_FILTER() также жёстко задано SlaveStartFilterBank = 14. Если приложение уже использует фильтры, номера и границу банков нужно согласовать.

4. Инициализировать и запустить периферию

Пример для main.c:

#include "protocan.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_CAN_Init();
    MX_RTC_Init();
    MX_TIM2_Init();

    PROTOCAN_INIT_StatusTypeDef init_status =
        PROTOCAN_INIT(&hcan, &hrtc, &htim2);

    if (init_status != PROTOCAN_INIT_OK) {
        Error_Handler();
    }

    /* PROTOCAN_INIT() этого не делает. */
    if (HAL_CAN_Start(&hcan) != HAL_OK) {
        Error_Handler();
    }
    if (HAL_CAN_ActivateNotification(
            &hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) {
        Error_Handler();
    }
    if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK) {
        Error_Handler();
    }

    while (1) {
        (void)PROTOCAN_ProcessAllRxMsgs();
    }
}

PROTOCAN_INIT() требует ненулевые указатели на CAN, RTC и TIM, настраивает CAN-фильтры и включает отправку пульса. Функцию следует вызвать после MX_CAN_Init(), MX_RTC_Init() и MX_TIMx_Init(), но до запуска CAN.

5. Подключить callbackи HAL

Если в HAL включены USE_HAL_CAN_REGISTER_CALLBACKS == 1 и USE_HAL_TIM_REGISTER_CALLBACKS == 1, PROTOCAN_INIT() зарегистрирует callbackи автоматически.

Если регистрация callbackов отключена, добавить перенаправление из стандартных HAL-callbackов:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan_ptr)
{
    if (hcan_ptr == &hcan) {
        ProtoCanRxFifo0MsgPendingCallback(hcan_ptr);
    }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr)
{
    if (htim_ptr == &htim2) {
        ProtoCanPulseCallback(htim_ptr);
    }
}

Не следует одновременно регистрировать callback через HAL и вручную вызывать его из стандартного callback — иначе один кадр или событие таймера может быть обработано дважды.

Приём сообщений

В прерывании ProtoCanRxFifo0MsgPendingCallback() модуль вычитывает все кадры из FIFO0:

  • стандартные CAN ID игнорируются;
  • PULSE не занимает место в RX-буфере, а сразу обновляет внутреннюю таблицу присутствующих устройств;
  • остальные Extended-кадры помещаются в кольцевой буфер;
  • при переполнении новые кадры молча отбрасываются.

Обработку прикладной логики следует выполнять вне прерывания одним из трёх способов:

PROTOCAN_ProcessSingleRxMsg(); /* обработать не более одного кадра */
PROTOCAN_ProcessAllRxMsgs();   /* обработать все накопленные кадры */
PROTOCAN_LoopProcessRxMsgs();  /* бесконечный блокирующий цикл */

Для обычного while (1) удобнее PROTOCAN_ProcessAllRxMsgs(). Она возвращает PROTOCAN_TIMEOUT, если очередь была пуста, PROTOCAN_OK после успешной обработки или код первой ошибки.

Свои обработчики входящих команд

Большинство обработчиков объявлены как __weak. Приложение может определить функцию с тем же именем и заменить демонстрационную реализацию своей логикой.

Пример обработки запроса температуры:

PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg msg)
{
    msgBodyAnalogType body = {0};
    body.Body = msg.eID.Fields.MsgBody;

    uint16_t sensor_id = body.Fields.SensorID;
    /* Прочитать датчик sensor_id и сформировать нужный ответ. */

    return PROTOCAN_OK;
}

Доступные точки переопределения находятся в protocan.h:

  • ProtoCanMsgToBroadcast...() — статус, включение/выключение, restart, RTC;
  • ProtoCanMsgToDiscrete...() — аварии, предупреждения, флаги и команды;
  • ProtoCanMsgToAnalog...() — универсальные данные, настройки, U/I/T;
  • ProtoCanMsgToSettings() — GET/WRITE/CLEAR привязки ROM к локации;
  • ProtoCanMsgToGeneralAddressSpace();
  • ProtoCanMsgToModbus...();
  • PROTOCAN_RequestError().

Стандартные слабые обработчики в основном являются демонстрационными: часть возвращает PROTOCAN_OK без действий, часть отправляет текстовые ответы вроде TS0001 или GAS-0001. Для рабочего изделия их обычно нужно переопределить.

SETTINGS: привязка DS18B20 к локации

Для прибора DeviceType=0x7, DeviceID=0xF используется MsgType=0xE:

Body = (Z << 8) | Y

запрос от ПМ = 0x17FEZZYY
ответ прибора = 0x1FFEZZYY
Запрос Операция
DLC=0 GET: прочитать ROM локации
DLC=8, ROM не нулевой WRITE/REPLACE: записать или заменить датчик
DLC=8, восемь нулей CLEAR: очистить локацию
другой DLC или RTR автоматический ответ INVALID_DLC

Успешный ответ имеет DLC=8 и содержит текущее состояние локации: ROM после GET/WRITE либо восемь нулей после CLEAR. Ошибка сохраняет тот же Body, имеет DLC=1 и передаёт ProtoCanSettingsResultType в Data[0].

PROTOCAN_SettingsProcessing() выполняет транспортный разбор и вызывает слабую функцию ProtoCanMsgToSettings(). Приложение переопределяет её и выполняет проверку family code/CRC8, поиск ROM на 1-Wire и запись EEPROM:

PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(
    const ProtoCanSettingsMsg_t *message)
{
    uint8_t current_rom[PROTOCAN_SETTINGS_ROM_SIZE] = {0};

    switch (message->Operation) {
    case PROTOCAN_SETTINGS_GET:
        /* Загрузить ROM локации в current_rom; свободная локация = нули. */
        return PROTOCAN_SEND_SETTINGS_RESPONSE(
            message->AssemblySerial, message->Position, current_rom);

    case PROTOCAN_SETTINGS_WRITE:
        /* Проверить ROM и атомарно записать его в каталог/EEPROM. */
        return PROTOCAN_SEND_SETTINGS_RESPONSE(
            message->AssemblySerial, message->Position, message->Rom);

    case PROTOCAN_SETTINGS_CLEAR:
        /* Удалить локацию из каталога/EEPROM. */
        return PROTOCAN_SEND_SETTINGS_RESPONSE(
            message->AssemblySerial, message->Position, current_rom);

    default:
        return PROTOCAN_SEND_SETTINGS_ERROR(
            message->AssemblySerial, message->Position,
            PROTOCAN_SETTINGS_RESULT_INVALID_DLC);
    }
}

Слабая реализация специально не подтверждает операцию: без прикладного каталога библиотека не должна сообщать, что ROM был сохранён.

Отправка

Публичная точка отправки — PROTOCAN_SEND(id, data). Сначала заполняется общий Extended ID, затем соответствующая часть ProtoCanData_t.

Пример отправки диапазона 16-битных регистров общего адресного пространства:

uint16_t registers[] = { 0x1234, 0x5678, 0x9ABC };

ProtoCanId_t id = {0};
id.Fields.Priority   = PROTOCAN_PRIORITY_STANDARD;
id.Fields.Route      = PROTOCAN_ROUTE_FROM_DEVICE;
id.Fields.DeviceType = CURRENT_TYPE_DEVICE;
id.Fields.DeviceID   = CURRENT_ID_DEVICE;
id.Fields.MsgType    = PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE;

ProtoCanData_t tx = {0};
tx.GeneralAddressSpaceData.RegStartAdr = 100;
tx.GeneralAddressSpaceData.Data        = registers;
tx.GeneralAddressSpaceData.RegCount    = 3;

if (PROTOCAN_SEND(id, tx) != PROTOCAN_OK) {
    /* Очередь CAN занята или произошла ошибка HAL. */
}

Массив автоматически разбивается на кадры максимум по четыре регистра (8 байт) с увеличением адреса в MsgBody.

Пример сообщения об ошибке без payload:

ProtoCanId_t id = {0};
id.Fields.Priority   = PROTOCAN_PRIORITY_CRITICAL;
id.Fields.Route      = PROTOCAN_ROUTE_FROM_DEVICE;
id.Fields.DeviceType = CURRENT_TYPE_DEVICE;
id.Fields.DeviceID   = CURRENT_ID_DEVICE;
id.Fields.MsgType    = PROTOCAN_MSGTYPE_ERROR;

ProtoCanData_t tx = {0};
tx.ErrorData.Code = 0x12;
tx.ErrorData.Info = 0x34;

(void)PROTOCAN_SEND(id, tx);

Для BROADCAST, DISCRETE и ANALOG используется tx.CoreData; для Modbus-типов — tx.ModbusData, для SETTINGStx.SettingsData. Для успешных ответов и ошибок SETTINGS удобнее использовать готовые функции PROTOCAN_SEND_SETTINGS_RESPONSE() и PROTOCAN_SEND_SETTINGS_ERROR(). Если количество передаваемых элементов больше нуля, соответствующий указатель Data должен быть валиден до завершения вызова.

Пульс устройства

Каждое прерывание переданного в PROTOCAN_INIT() таймера вызывает ProtoCanPulseCallback() и отправляет кадр:

  • MsgType = PROTOCAN_MSGTYPE_PULSE;
  • DLC = 1;
  • Data[0] — циклический счётчик 0..255.

Период пульса полностью определяется настройками таймера. Broadcast-команда PROTOCAN_BROADCAST_ONOFF в стандартной реализации переключает его отправку.

Полученный пульс помечает удалённое устройство как активное и обнуляет TimeFromLastPulse. Увеличение этого времени и перевод устройства в offline в текущем модуле не реализованы — если это требуется, контроль таймаута нужно добавить в приложение.

Синхронизация RTC

Broadcast-команда PROTOCAN_BROADCAST_RTCSETUP ожидает ровно 7 байт:

Индекс Значение
0 часы, 0..23
1 минуты, 0..59
2 секунды, 0..59
3 год как смещение от 2000, 0..99
4 месяц, 1..12
5 число месяца
6 день недели в формате, ожидаемом данной прошивкой

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

Прошивка приборов по CAN

Ниже зафиксирован формат планируемого загрузочного сервиса. Значения MsgType=0x9..0xD зарезервированы в протоколе, но обработчики загрузчика в текущих protocan.c/.h ещё не реализованы.

Загрузчик работает поверх classic CAN 2.0B с Extended ID. Управляющий модуль использует Route=0, прибор отвечает с Route=1. Команды стирания и записи всегда должны быть адресованы конкретной паре DeviceType/DeviceID; широковещательный режим допустим только для обнаружения.

Карта загрузочных сообщений

MsgType Имя Назначение MsgBody CAN payload
0x9 BOOT_CONTROL SessionID:8 | Command:8 параметры команды
0xA BOOT_DATA_A номер 8-байтового блока слота A 8 байт образа
0xB BOOT_DATA_B номер 8-байтового блока слота B 8 байт образа
0xC BOOT_STATUS SessionID:8 | Command:8 статус и прогресс
0xD BOOT_DISCOVERY подтип запроса/ответа идентификация прибора

MsgBody в кадрах данных является не байтовым адресом, а номером блока:

block_offset = (uint32_t)MsgBody * 8U;

if (MsgType == PROTOCAN_MSGTYPE_BOOT_DATA_A) {
    address = SLOT_A_BASE + block_offset;
} else if (MsgType == PROTOCAN_MSGTYPE_BOOT_DATA_B) {
    address = SLOT_B_BASE + block_offset;
}

Диапазон MsgBody=0x0000..0xFFFF адресует 65536 блоков:

65536 блоков * 8 байт = 524288 байт = 512 КиБ на слот

Таким образом, BOOT_DATA_A и BOOT_DATA_B адресуют два логических слота по 512 КиБ, всего 1 МиБ пространства образов. Физические SLOT_A_BASE и SLOT_B_BASE задаёт конкретный загрузчик. Если внутренняя Flash имеет ровно 1 МиБ, два полных слота в ней не поместятся вместе с загрузчиком и метаданными: нужно уменьшить слоты, выбрать MCU с большей Flash или хранить staging-образ во внешней памяти.

Кадр данных

Extended CAN ID
  Priority   = STANDARD
  Route      = FROM_PM
  DeviceType = тип целевого прибора
  DeviceID   = экземпляр целевого прибора
  MsgType    = 0xA (слот A) или 0xB (слот B)
  MsgBody    = BlockIndex, 0x0000..0xFFFF

DATA[0..7]   = очередные 8 байт образа

Например, MsgBody=0x0123 задаёт смещение 0x0123 * 8 = 0x0918 от начала выбранного слота. Последний неполный блок дополняется значениями 0xFF; фактический размер передаётся командой BEGIN_UPDATE, поэтому CRC32 считается только по байтам образа.

Управляющие команды

В BOOT_CONTROL поле MsgBody имеет формат:

15........8 7.........0
 SessionID     Command

Рекомендуемые команды:

Код Команда Назначение
0x01 IDENTIFY прочитать тип, аппаратную и программную версии
0x02 ENTER_BOOT перейти из приложения в загрузчик
0x03 BEGIN_IMAGE передать размер и CRC32 образа
0x04 BEGIN_COMPAT передать тип, аппаратную и программную версии
0x05 ERASE подготовить неактивный слот
0x06 VERIFY проверить размер, CRC32 и подпись
0x07 COMMIT назначить проверенный слот кандидатом на запуск
0x08 CONFIRM подтвердить успешный запуск новой программы
0x09 REBOOT перезагрузить прибор
0x0A ABORT отменить текущую сессию
0x0B QUERY_PROGRESS запросить слот и следующий ожидаемый блок

BEGIN_IMAGE содержит размер и CRC образа:

DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian

BEGIN_COMPAT содержит совместимость и версию:

DATA[0..1] ProductType, uint16 little-endian
DATA[2]    минимальная HardwareRevision
DATA[3]    максимальная HardwareRevision
DATA[4..7] FirmwareVersion, uint32 little-endian

Обе команды передаются с одним SessionID. До стирания Flash загрузчик обязан получить обе части метаданных и проверить ProductType, аппаратную ревизию, размер образа, границы выбранного слота и допустимость версии.

Ответ состояния

BOOT_STATUS возвращает результат команды и точку продолжения:

MsgBody[15..8] = SessionID
MsgBody[7..0]  = команда, на которую дан ответ

DATA[0]        Status
DATA[1]        TargetSlot: 0 = A, 1 = B
DATA[2..3]     NextBlock, uint16 little-endian
DATA[4..7]     RunningCRC32, uint32 little-endian

Минимальный набор статусов:

Код Статус
0x00 OK
0x01 BUSY
0x02 INVALID_COMMAND
0x03 WRONG_DEVICE
0x04 WRONG_HARDWARE
0x05 INVALID_SIZE
0x06 CRC_ERROR
0x07 FLASH_ERROR
0x08 SEQUENCE_ERROR
0x09 SIGNATURE_ERROR
0x0A SESSION_ERROR
0x0B VOLTAGE_ERROR

NextBlock позволяет возобновить загрузку после разрыва связи. Для первой реализации допустимо подтверждать каждый блок. Для рабочей скорости лучше передавать окна по 16 кадров и подтверждать окно одним BOOT_STATUS; при необходимости протокол статуса можно расширить битовой картой потерянных блоков.

Выбор слота и безопасное обновление

GUI не должен самостоятельно перезаписывать активный слот. После получения BEGIN_IMAGE и BEGIN_COMPAT загрузчик выбирает неактивный слот и сообщает его в BOOT_STATUS:

активен A -> принимать BOOT_DATA_B
активен B -> принимать BOOT_DATA_A

Рекомендуемый цикл обновления:

  1. Обнаружить прибор и сверить DeviceType/DeviceID, ProductType, UID и версии.
  2. Выполнить адресную команду ENTER_BOOT и получить новый SessionID.
  3. Передать BEGIN_IMAGE и BEGIN_COMPAT; загрузчик выберет неактивный слот.
  4. Стереть выбранный слот и передать блоки BOOT_DATA_A или BOOT_DATA_B.
  5. Выполнить VERIFY: проверить размер, CRC32 и цифровую подпись образа.
  6. Выполнить COMMIT и перезагрузить устройство.
  7. Новое приложение вызывает CONFIRM после успешной самопроверки.
  8. При отсутствии подтверждения загрузчик возвращается к предыдущему слоту.

CRC32 обнаруживает случайное повреждение, но не защищает от подмены. Для серийных изделий образ следует подписывать, а открытый ключ проверки хранить в неизменяемой части загрузчика. Сам загрузчик не должен обновляться обычными командами BOOT_DATA_A/B.

Важные ограничения текущей реализации

  • Модуль рассчитан на classic CAN с payload до 8 байт, не на CAN FD.
  • PROTOCAN_INIT() не запускает CAN/таймер и не включает CAN notification.
  • Все три аппаратных фильтра и номера банков заданы внутри библиотеки.
  • Фильтр адресованных сообщений рассчитан на устройство: он принимает Route=0 от ПМ. Для использования библиотеки на стороне ПМ потребуется отдельная конфигурация фильтров Route=1.
  • Проверку ROM, физического датчика и EEPROM выполняет приложение в переопределённом ProtoCanMsgToSettings().
  • Таблица присутствующих устройств обновляется при пульсе, но автоматического offline-таймаута нет.
  • Буфер переполнения не ведёт счётчик потерь и не сообщает ошибку приложению.
  • У порта нет полного примера прошивки STM32; его сборку и проверку на плате выполняет проект устройства. Host-тесты общего ядра находятся в ../../tests.
  • Несколько внутренних функций отправки реализованы только в protocan.c и не входят в публичный заголовок; для прикладного кода следует использовать PROTOCAN_SEND().

Краткий порядок запуска

  1. Подключить templates к проекту устройства.
  2. Добавить protocan.c/.h и общий pcan_id.c в STM32-проект.
  3. Задать CURRENT_TYPE_DEVICE и CURRENT_ID_DEVICE.
  4. Настроить CAN, RTC и периодический TIM в CubeMX.
  5. Вызвать PROTOCAN_INIT() после MX_..._Init().
  6. Запустить CAN, активировать CAN_IT_RX_FIFO0_MSG_PENDING и запустить таймер с прерыванием.
  7. При необходимости подключить HAL-callbackи вручную.
  8. Переопределить нужные __weak-обработчики.
  9. Вызывать PROTOCAN_ProcessAllRxMsgs() в основном цикле.