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

603 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()` в основном цикле.