603 lines
32 KiB
Markdown
603 lines
32 KiB
Markdown
# 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()` в основном цикле.
|