SETCAN
SETCAN — небольшой модуль прикладного протокола поверх классического CAN для микроконтроллеров STM32 и библиотеки STM32 HAL. Модуль принимает только кадры с расширенным 29-битным идентификатором, складывает их в кольцевой буфер и передаёт обработчикам по типу сообщения. Также он умеет отправлять широковещательные, дискретные, аналоговые, адресные, Modbus-подобные сообщения, команды привязки датчиков SETTINGS, ошибки и периодический «пульс» устройства.
Это не самостоятельный проект STM32CubeIDE: здесь нет .ioc, startup-файлов, linker script или настроек тактирования. Каталоги Inc и Src нужно добавить в существующую прошивку STM32.
Состав
SETCAN/
├── Inc/
│ └── protocan.h # типы протокола, настройки устройства и публичный API
└── Src/
└── protocan.c # приём, буферизация, разбор и отправка CAN-кадров
Модуль зависит от файлов main.h и can.h, сгенерированных STM32CubeMX, а также от HAL-драйверов CAN, RTC и TIM.
Формат расширенного 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 |
ошибка |
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: сначала младший байт, затем старший.
Разметка ID реализована C-битовыми полями. Она соответствует используемому STM32 GCC ABI, но не является переносимой между произвольными компиляторами без проверки фактического расположения битов.
Подключение к STM32-проекту
1. Добавить файлы
Скопировать или подключить к сборке:
Inc/protocan.h;Src/protocan.c.
Каталог Inc добавить в include paths компилятора.
2. Настроить устройство
В Inc/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, для SETTINGS — tx.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 | день недели в формате, ожидаемом данной прошивкой |
Перед записью проверяются диапазоны и количество дней с учётом високосного года.
Важные ограничения текущей реализации
- Модуль рассчитан на classic CAN с payload до 8 байт, не на CAN FD.
PROTOCAN_INIT()не запускает CAN/таймер и не включает CAN notification.- Все три аппаратных фильтра и номера банков заданы внутри библиотеки.
- Фильтр адресованных сообщений рассчитан на устройство: он принимает Route=0 от ПМ. Для использования библиотеки на стороне ПМ потребуется отдельная конфигурация фильтров Route=1.
- Проверку ROM, физического датчика и EEPROM выполняет приложение в переопределённом
ProtoCanMsgToSettings(). - Таблица присутствующих устройств обновляется при пульсе, но автоматического offline-таймаута нет.
- Буфер переполнения не ведёт счётчик потерь и не сообщает ошибку приложению.
- В репозитории нет тестов, примера полного STM32-проекта и системы сборки.
- Несколько внутренних функций отправки реализованы только в
protocan.cи не входят в публичный заголовок; для прикладного кода следует использоватьPROTOCAN_SEND().
Краткий порядок запуска
- Добавить
protocan.c/.hв STM32-проект. - Задать
CURRENT_TYPE_DEVICEиCURRENT_ID_DEVICE. - Настроить CAN, RTC и периодический TIM в CubeMX.
- Вызвать
PROTOCAN_INIT()послеMX_..._Init(). - Запустить CAN, активировать
CAN_IT_RX_FIFO0_MSG_PENDINGи запустить таймер с прерыванием. - При необходимости подключить HAL-callback’и вручную.
- Переопределить нужные
__weak-обработчики. - Вызывать
PROTOCAN_ProcessAllRxMsgs()в основном цикле.