# SETCAN `SETCAN` — небольшой модуль прикладного протокола поверх классического CAN для микроконтроллеров STM32 и библиотеки STM32 HAL. Модуль принимает только кадры с расширенным 29-битным идентификатором, складывает их в кольцевой буфер и передаёт обработчикам по типу сообщения. Также он умеет отправлять широковещательные, дискретные, аналоговые, адресные, Modbus-подобные сообщения, ошибки и периодический «пульс» устройства. Это не самостоятельный проект STM32CubeIDE: здесь нет `.ioc`, startup-файлов, linker script или настроек тактирования. Каталоги `Inc` и `Src` нужно добавить в существующую прошивку STM32. ## Состав ```text 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` | ошибка | | `0xF` | пульс присутствия устройства | Разметка `MsgBody` зависит от типа сообщения: - broadcast: младшие 4 бита — дополнительное поле, старшие 12 бит — команда; - discrete: младшие 12 бит — данные/адрес, старшие 4 бита — подтип; - analog: младшие 12 бит — ID датчика, старшие 4 бита — тип величины; - Modbus: младшие 4 бита — количество регистров, старшие 12 бит — начальный адрес; - 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` задать значения конкретного узла: ```c #define CURRENT_TYPE_DEVICE 0b001 /* 3 бита: 0..7 */ #define CURRENT_ID_DEVICE 0b0010 /* 4 бита: 0..15 */ ``` При необходимости изменить размер программного RX-буфера: ```c #define PROTOCAN_RX_BUFFER_SIZE 128 ``` Полезная ёмкость кольцевого буфера на один элемент меньше заданного размера, то есть при значении `128` в нём помещается 127 ожидающих кадров. ### 3. Настроить CubeMX В проекте должны быть инициализированы: - CAN с поддержкой Extended ID; - RTC; - базовый таймер, задающий период отправки пульса. Модуль использует CAN FIFO0 и фильтры с номерами `0`, `1`, `2`. В `PROTOCAN_CONFIG_FILTER()` также жёстко задано `SlaveStartFilterBank = 14`. Если приложение уже использует фильтры, номера и границу банков нужно согласовать. ### 4. Инициализировать и запустить периферию Пример для `main.c`: ```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’ов: ```c 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-кадры помещаются в кольцевой буфер; - при переполнении новые кадры молча отбрасываются. Обработку прикладной логики следует выполнять вне прерывания одним из трёх способов: ```c PROTOCAN_ProcessSingleRxMsg(); /* обработать не более одного кадра */ PROTOCAN_ProcessAllRxMsgs(); /* обработать все накопленные кадры */ PROTOCAN_LoopProcessRxMsgs(); /* бесконечный блокирующий цикл */ ``` Для обычного `while (1)` удобнее `PROTOCAN_ProcessAllRxMsgs()`. Она возвращает `PROTOCAN_TIMEOUT`, если очередь была пуста, `PROTOCAN_OK` после успешной обработки или код первой ошибки. ## Свои обработчики входящих команд Большинство обработчиков объявлены как `__weak`. Приложение может определить функцию с тем же именем и заменить демонстрационную реализацию своей логикой. Пример обработки запроса температуры: ```c 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; - `ProtoCanMsgToGeneralAddressSpace()`; - `ProtoCanMsgToModbus...()`; - `PROTOCAN_RequestError()`. Стандартные слабые обработчики в основном являются демонстрационными: часть возвращает `PROTOCAN_OK` без действий, часть отправляет текстовые ответы вроде `TS0001` или `GAS-0001`. Для рабочего изделия их обычно нужно переопределить. ## Отправка Публичная точка отправки — `PROTOCAN_SEND(id, data)`. Сначала заполняется общий Extended ID, затем соответствующая часть `ProtoCanData_t`. Пример отправки диапазона 16-битных регистров общего адресного пространства: ```c 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: ```c 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`. Если количество передаваемых элементов больше нуля, соответствующий указатель `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` (бит 27). Это стоит сверить с принятой в системе трактовкой `PROTOCAN_ROUTE_FROM_PM`/`PROTOCAN_ROUTE_FROM_DEVICE`. - Таблица присутствующих устройств обновляется при пульсе, но автоматического offline-таймаута нет. - Буфер переполнения не ведёт счётчик потерь и не сообщает ошибку приложению. - В репозитории нет тестов, примера полного STM32-проекта и системы сборки. - Несколько внутренних функций отправки реализованы только в `protocan.c` и не входят в публичный заголовок; для прикладного кода следует использовать `PROTOCAN_SEND()`. ## Краткий порядок запуска 1. Добавить `protocan.c/.h` в STM32-проект. 2. Задать `CURRENT_TYPE_DEVICE` и `CURRENT_ID_DEVICE`. 3. Настроить CAN, RTC и периодический TIM в CubeMX. 4. Вызвать `PROTOCAN_INIT()` после `MX_..._Init()`. 5. Запустить CAN, активировать `CAN_IT_RX_FIFO0_MSG_PENDING` и запустить таймер с прерыванием. 6. При необходимости подключить HAL-callback’и вручную. 7. Переопределить нужные `__weak`-обработчики. 8. Вызывать `PROTOCAN_ProcessAllRxMsgs()` в основном цикле.