diff --git a/README.md b/README.md new file mode 100644 index 0000000..d17c1d0 --- /dev/null +++ b/README.md @@ -0,0 +1,310 @@ +# 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()` в основном цикле. +