# ProtoCAN: порт STM32 bxCAN (STM32 HAL) `stm32-bxcan` — порт прикладного ProtoCAN для STM32 HAL, перенесённый из SETCAN. Модуль принимает кадры, управляет HAL CAN/RTC/TIM и вызывает обработчики приложения. Формат 29-битного идентификатора и его переносимая реализация принадлежат общему ядру [`c/set-protocol`](../../README.md) в этом же репозитории `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`. ## Состав ```text 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` | ## Документация протокола Каноническое описание теперь находится рядом с переносимой реализацией: - [Структура ProtoCAN](../../docs/legacy/PROTOCOL.md); - [Прошивка по CAN](../../../protocan-boot/docs/BOOTLOADER.md); - [Правила ведения ОАП](../../docs/legacy/OAP.md); - [Машинные эталоны](../../tests/vectors/test-vectors.json); - [Историческая HTML-документация SETCAN](../../../../doc/setcan/index.html). Файлы в `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 заданы выбранные значения: ```c #define CURRENT_TYPE_DEVICE 0b111 /* DeviceType = 0x7 */ #define CURRENT_ID_DEVICE 0b1111 /* DeviceID = 0xF */ ``` При необходимости изменить размер программного RX-буфера: ```c #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`: ```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; - `ProtoCanMsgToSettings()` — GET/WRITE/CLEAR привязки ROM к локации; - `ProtoCanMsgToGeneralAddressSpace()`; - `ProtoCanMsgToModbus...()`; - `PROTOCAN_RequestError()`. Стандартные слабые обработчики в основном являются демонстрационными: часть возвращает `PROTOCAN_OK` без действий, часть отправляет текстовые ответы вроде `TS0001` или `GAS-0001`. Для рабочего изделия их обычно нужно переопределить. ## SETTINGS: привязка DS18B20 к локации Для прибора `DeviceType=0x7`, `DeviceID=0xF` используется `MsgType=0xE`: ```text 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: ```c 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-битных регистров общего адресного пространства: ```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`, для `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 | день недели в формате, ожидаемом данной прошивкой | Перед записью проверяются диапазоны и количество дней с учётом високосного года. ## Прошивка приборов по 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` в кадрах данных является не байтовым адресом, а номером блока: ```c 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 блоков: ```text 65536 блоков * 8 байт = 524288 байт = 512 КиБ на слот ``` Таким образом, `BOOT_DATA_A` и `BOOT_DATA_B` адресуют два логических слота по 512 КиБ, всего 1 МиБ пространства образов. Физические `SLOT_A_BASE` и `SLOT_B_BASE` задаёт конкретный загрузчик. Если внутренняя Flash имеет ровно 1 МиБ, два полных слота в ней не поместятся вместе с загрузчиком и метаданными: нужно уменьшить слоты, выбрать MCU с большей Flash или хранить staging-образ во внешней памяти. ### Кадр данных ```text 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` имеет формат: ```text 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 образа: ```text DATA[0..3] ImageSize, uint32 little-endian DATA[4..7] ImageCRC32, uint32 little-endian ``` `BEGIN_COMPAT` содержит совместимость и версию: ```text 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` возвращает результат команды и точку продолжения: ```text 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`: ```text активен 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()` в основном цикле.