feat(protocan-boot): Добавь порты MCU и сервисы загрузки

Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент.

Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
This commit is contained in:
2026-10-23 14:16:19 +03:00
parent 876b58517c
commit c6390ae1c9
147 changed files with 42760 additions and 104 deletions
@@ -0,0 +1,109 @@
# STM32: общий слой ProtoCAN Boot v1
Порты [F103C8](../stm32f103/README.md), [F407VE](../stm32f407/README.md) и
[G474VE](../stm32g474/README.md) подключают bxCAN/FDCAN и Flash к текущему `pcan_boot`.
F103/F407 предоставляют как лёгкий CMSIS boot main для существующих проектов,
так и независимый HAL-порт. G474 использует HAL FDCAN/Flash.
`pcan_boot_stm32.inc` включается семейным `.c`; отдельно его компилировать не надо.
Сохраните рядом каталоги `stm32-common` и нужного семейства.
Reusable `pcan_boot_stm32*.c` не содержит `main` или startup. Готовые `boot_main.c`
и вход из приложения находятся в семейных каталогах. Для F103/F407 общая
CMSIS-реализация транспорта, callbacks и безопасного handoff — `boot_native.inc`;
G474 также содержит `boot_startup.c` для исходных Keil/GCC-сборок.
Тактирование платы, linker и привязка USB/CAN-приложения остаются в проектах.
KONOR, climate и CANdle G474 уже подключены через свои submodule: локальные
boot main/control заменены include-обёртками, состав Keil/CMake обновлён.
UART использует общий `rs485-boot` с сохранением SETGUI v1 wire-протокола.
[Схема подключённых проектов и проверки сборок](../../projects/STM32.md).
## Подключение reusable HAL-портов F103/F407
1. Добавьте в сборку `src/pcan_boot.c` и семейный `pcan_boot_stm32f103.c`
либо `pcan_boot_stm32f407.c`; в include paths — `include`, каталог порта,
CMSIS, HAL и каталог вашего `stm32f1xx_hal_conf.h` / `stm32f4xx_hal_conf.h`.
2. В HAL включите CAN, FLASH, RCC, GPIO и CORTEX. Выполните `HAL_Init`, настройте
тактирование, SysTick и GPIO, вызовите `HAL_CAN_Init` для CAN1. Настройка скорости
шины принадлежит проекту; исходные устройства используют 500 кбит/с.
3. Выделите CAN1, FIFO0 и банки фильтров 0/1 загрузчику. Перед вызовом init CAN
должен находиться в состоянии READY, без чужих активных фильтров. Init настраивает
адресный фильтр и broadcast discovery, затем вызывает `HAL_CAN_Start`.
Оба приоритета ProtoCAN разрешены, standard и remote кадры отвергаются.
4. Не включайте CAN TX/RX IRQ для этого handle и не отправляйте через него кадры
из других контекстов: порт использует опрос и сам читает TXOK до reset.
5. Вызывайте семейный `poll` из основного цикла только после успешного init.
Стирание Flash выполняется синхронно; вызов из IRQ не допускается.
6. Linker приложения, VTOR и переход после reset должны соответствовать карте
выбранного профиля. Изменение адресов требует отдельного согласования
геометрии стирания, linker и host-клиента.
Пример для F103; `hcan` уже инициализирован проектом:
```c
#include "pcan_boot_stm32f103.h" /* Подключает HAL-порт и текущий API общего автомата. */
static pcan_boot_stm32_t updater; /* Сохраняет состояние между итерациями главного цикла. */
static bool start_updater(CAN_HandleTypeDef *hcan) /* Вызывается после настройки CAN1, clock tree, GPIO и SysTick. */
{ /* Настройка идентичности прибора не зависит от аппаратной геометрии Flash. */
pcan_boot_config_t config = {0}; /* Не зависит от порядка полей расширяемого API. */
config.device_type = 7U; /* Пример: тип 7 должен совпадать с выбранным адресом в клиенте прошивки. */
config.device_id = 13U; /* Пример: номер 13 повторяет исходный адрес загрузчика KONOR. */
config.ack_window = 16U; /* Подтверждает каждые 16 CAN-блоков по восемь байт. */
config.session_timeout_ms = 30000U; /* Пример: 30 секунд без активности прерывают RAM-сессию. */
return pcan_boot_stm32f103_init(&updater, hcan, &config); /* Настраивает single-slot геометрию, фильтры 0/1 и запускает CAN. */
} /* После успеха основной цикл регулярно вызывает pcan_boot_stm32f103_poll(&updater). */
```
Product/HW/FW в примере равны нулю; для изделия задайте их явно и согласуйте
с manifest клиента. Порт сохраняет `ack_window` и `session_timeout_ms`, но сам
задаёт `active_slot=0`, `slot_count=1`, `max_image_size` по физическому профилю.
Конфигурацию `config` можно создавать на стеке; состояние `updater` и `hcan`
должно существовать всё время работы загрузчика.
## Flash, подтверждения и reset
- Запись последовательная, блоки по восемь байт; неполная аппаратная единица
допустима только в конце образа и дополняется `FF`. CRC учитывает полезную длину.
- HAL-ошибки и readback каждого слова проверяются; после erase/program Flash
снова блокируется, включая обработанные отказы. На F407 перед readback
вызывается `FLASH_FlushCaches`, чтобы соседние записанные слова не читались
из устаревшей строки Flash-кэша; состояние включения кэшей сохраняется HAL.
- VERIFY перечитывает Flash, сравнивает её CRC и проверяет MSP/Thumb reset vector.
Точка входа должна принадлежать фактическому образу, MSP — основной SRAM,
с выравниванием восемь байт. CCM F407 в данном профиле для стека не используется.
- COMMIT повторно проверяет образ и устанавливает признак в RAM.
REBOOT разрешён только в состоянии COMMITTED. Ответ перед reset должен
завершиться с TXOK; отсутствие mailbox, ошибка шины или тайм-аут 100 мс
не вызывают reset. Для этого HAL tick должен работать при ожидании TX.
- BUSY перед стиранием, между секторами F407 и после erase сохраняет активность
сессии. На однобанковой Flash код не исполняется во время аппаратного стирания;
непрерывные ответы в этот интервал не гарантируются. Настройте тайм-аут клиента
больше максимального времени стирания одной аппаратной операции.
## Запуск приложения и потеря питания
Порт не меняет Option Bytes, защиту секторов и VDD; F407 требует 2,7..3,6 В
для программирования x32. Переход из работающего приложения в загрузчик,
backup/SRAM-флаг запроса, отключение периферии и безопасная замена MSP
остаются в `main` и startup устройства.
`*_vectors_valid()` проверяет только векторы и диапазоны. После внешнего reset
этого недостаточно для доказательства целостности образа: RAM-признак COMMIT
потерян, а частично записанное приложение уже может иметь корректные векторы.
Эти профили сохраняют исходную single-slot модель без постоянных метаданных,
rollback, resume после питания и аутентификации. Для защищённого запуска
проекту нужен отдельный постоянный manifest с полной проверкой образа.
## Проверка
`python tests/run_tests.py` из корня `protocan-boot` запускает три production-порта
с моделью HAL и Flash, наряду с существующими проверками ядра и TI-портов.
Можно также использовать обычные CMake/CTest: цели `test_stm32f103`,
`test_stm32f407`, `test_stm32g474` работают на Windows и POSIX.
Проверяются все хвосты CAN-блока, полный ERASE/DATA/VERIFY/COMMIT/REBOOT,
повреждение readback, отказы HAL/TX, фильтрация RX и сохранность памяти
перед приложением и за верхней границей; для G474 проверяются оба DBANK-режима,
переход между банками, запрет swap, ёмкость Flash и старое shifted-DLC HAL.
Эти тесты не заменяют проверку
на реальной плате с её linker, питанием, трансивером и SysTick.
@@ -0,0 +1,127 @@
static __attribute__((naked, noreturn)) void native_enter(uint32_t sp __attribute__((unused)), uint32_t pc __attribute__((unused))) /* Меняет стек без последующего обращения C-кода к старому кадру функции. */
{ /* AAPCS передаёт новый MSP в r0, Thumb entry в r1; последовательность совместима с GCC и Arm Compiler 6. */
__asm volatile("msr msp, r0\n" "movs r2, #0\n" "msr control, r2\n" "dsb\n" "isb\n" "cpsie i\n" "bx r1\n"); /* Возвращает привилегированный режим MSP, синхронизирует конвейер и передаёт управление Thumb-вектору. */
} /* Завершает переход без эпилога C после замены MSP. */
void SysTick_Handler(void) { ++ticks_ms; } /* Сохраняет миллисекундные часы только в самостоятельной сборке загрузчика. */
static bool native_vectors_valid(uint32_t size) /* Проверяет начальный стек и reset handler в пределах фактического образа. */
{ /* До чтения Flash отвергает некорректный размер. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)NATIVE_APP_BASE; /* Таблица векторов лежит в начале зарезервированной области приложения. */
uint32_t sp, pc; /* Хранит начальные значения Cortex-M без невыровненного доступа. */
if (size < 8U || size > NATIVE_APP_SIZE) return false; /* Для двух векторов нужно минимум восемь байт; верхняя граница профиля исключена. */
sp = vectors[0]; pc = vectors[1]; /* Читает MSP и Thumb-точку входа только после проверки геометрии. */
return sp > 0x20000000UL && sp <= NATIVE_RAM_END && (sp & 7U) == 0U && (pc & 1U) != 0U && (pc & ~1UL) >= NATIVE_APP_BASE && (pc & ~1UL) < NATIVE_APP_BASE + size; /* Требует восемь байт выравнивания стека и точку входа внутри выбранной длины приложения. */
} /* Структурная проверка не заменяет постоянный manifest после отключения питания. */
static void native_jump(uint32_t address) /* Отключает загрузочную периферию перед запуском приложения либо ROM. */
{ /* Вызывающий код предварительно проверяет векторы либо использует фиксированный заводской ROM. */
uint32_t index; /* Перебирает все банки NVIC Cortex-M3/M4 с запасом до восьми банков. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)address; /* Сохраняет адрес новой таблицы до отключения прерываний. */
uint32_t sp = vectors[0], pc = vectors[1]; /* Считывает аргументы перехода, пока C-стек ещё принадлежит загрузчику. */
__disable_irq(); SysTick->CTRL = 0U; /* Не позволяет SysTick загрузчика вызвать обработчик нового приложения в промежуточном состоянии. */
native_peripheral_stop(); /* Семейный порт отключает CAN и используемый UART, не трогая резервную память. */
for (index = 0U; index < 8U; ++index) { NVIC->ICER[index] = 0xFFFFFFFFUL; NVIC->ICPR[index] = 0xFFFFFFFFUL; } /* Сбрасывает enable/pending всех внешних IRQ записью W1C по CMSIS. */
SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk; /* Убирает отложенные SysTick и PendSV до смены VTOR. */
SCB->VTOR = address; /* Согласует векторизацию с linker нового образа. */
native_enter(sp, pc); /* Передаёт управление без возврата и без обращения к старому C-стеку. */
} /* Завершает общую процедуру handoff. */
static bool native_image_valid(uint32_t size, uint32_t crc) /* Проверяет реальную Flash независимо от CRC принятого CAN/UART-потока. */
{ /* Одно правило проверки используется обоими транспортами KONOR. */
return native_vectors_valid(size) && pcan_boot_crc32_update(0U, (const uint8_t *)(uintptr_t)NATIVE_APP_BASE, size) == crc; /* Проверяет точную полезную длину, исключая заполнение FF последней аппаратной единицы. */
} /* Завершает проверку векторов и readback CRC. */
static bool native_write_bytes(uint32_t image_size, uint32_t offset, const uint8_t *data, uint16_t length) /* Записывает CAN-блок или UART-часть с одной проверкой физической геометрии. */
{ /* Неполная аппаратная единица разрешена только в самом конце образа. */
uint32_t done = 0U; bool ok = true; /* Считает полезные байты и сохраняет результат до восстановления блокировки Flash. */
if (image_size < 8U || image_size > NATIVE_APP_SIZE || data == NULL || length == 0U || offset > image_size || length > image_size - offset || offset % NATIVE_PROGRAM_UNIT != 0U) return false; /* Проверяет диапазоны вычитанием, исключая переполнение и неправильное выравнивание. */
if (length % NATIVE_PROGRAM_UNIT != 0U && offset + length != image_size) return false; /* Не допускает повторного программирования уже дополненного слова в середине потока. */
if (!native_unlock()) return false; /* Не изменяет заблокированную или занятую Flash. */
while (done < length) { /* Делит полезные октеты на аппаратные полуслова F1 или слова F4. */
uint32_t word = 0xFFFFFFFFUL, count = length - done; /* По умолчанию хвост аппаратной записи остаётся стёртым FF. */
if (count > NATIVE_PROGRAM_UNIT) count = NATIVE_PROGRAM_UNIT; /* Не копирует больше одной аппаратной единицы за итерацию. */
(void)memcpy(&word, data + done, count); /* Не читает за фактическим хвостом последнего блока. */
if (!native_program(NATIVE_APP_BASE + offset + done, word)) { ok = false; break; } /* Проверяет аппаратные флаги и фактический readback каждого записанного слова. */
done += count; /* Продвигается только на число полезных байтов без дополнения. */
} /* Завершает программирование текущей части образа. */
native_lock(); /* Восстанавливает защиту Flash как после успеха, так и после обработанной ошибки. */
return ok; /* Передаёт отказ общему CAN либо UART-автомату. */
} /* Завершает общую безопасную запись Flash. */
static bool native_send(void *user, uint32_t id, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Передаёт служебный CAN-ответ с ожиданием реального TXOK до reset. */
{ /* Использует только mailbox 0 выделенного загрузочного CAN1. */
uint32_t start = ticks_ms, low = 0U, high = 0U; uint8_t index; bool sent; /* Сохраняет начало 100-мс тайм-аута и регистровые слова payload. */
(void)user; /* Самостоятельный загрузчик содержит один статический экземпляр CAN. */
if (length > 8U || id > 0x1FFFFFFFUL || (length != 0U && data == NULL)) return false; /* Не допускает чтение за DLC или передачу идентификатора шире 29 бит. */
while ((CAN1->TSR & CAN_TSR_TME0) == 0U) if ((uint32_t)(ticks_ms - start) >= 100U) return false; /* Ограничивает ожидание свободного mailbox 100 миллисекундами с учётом переполнения часов. */
for (index = 0U; index < length; ++index) { if (index < 4U) low |= (uint32_t)data[index] << (index * 8U); else high |= (uint32_t)data[index] << ((index - 4U) * 8U); } /* Читает только фактические октеты DLC, оставляя остальные позиции нулевыми. */
CAN1->TSR = CAN_TSR_RQCP0; /* Сбрасывает прежние TXOK/ошибки mailbox 0 записью W1C. */
CAN1->sTxMailBox[0].TDTR = length; CAN1->sTxMailBox[0].TDLR = low; CAN1->sTxMailBox[0].TDHR = high; /* Загружает длину и два безопасно сформированных слова данных. */
CAN1->sTxMailBox[0].TIR = (id << 3U) | CAN_TI0R_IDE | CAN_TI0R_TXRQ; /* Запускает расширенный data-кадр ProtoCAN без remote-флага. */
while ((CAN1->TSR & CAN_TSR_RQCP0) == 0U) if ((uint32_t)(ticks_ms - start) >= 100U) { CAN1->TSR = CAN_TSR_ABRQ0; return false; } /* Прерывает неподтверждённую передачу и запрещает reset после тайм-аута. */
sent = (CAN1->TSR & CAN_TSR_TXOK0) != 0U; CAN1->TSR = CAN_TSR_RQCP0; /* Проверяет ACK шины до очистки аппаратного результата. */
return sent; /* Ядро перезапустит MCU только после успешно отправленного ответа. */
} /* Завершает синхронную передачу ответа CAN. */
static bool native_erase_slot(void *user, pcan_boot_byte_t slot) /* Подключает проверенное стирание профиля к текущему ProtoCAN API. */
{ /* Аппаратная реализация семейства никогда не затрагивает загрузчик или EEPROM-кольцо. */
bool ok; (void)user; /* Хранит результат до финального обновления времени сессии. */
if (slot != 0U || boot.manifest.image_size < 8U || boot.manifest.image_size > NATIVE_APP_SIZE) return false; /* Разрешает только логический слот A и полный набор векторов. */
(void)pcan_boot_report_busy(&boot, PCAN_BOOT_CMD_ERASE, ticks_ms); /* Предупреждает клиента перед синхронным Flash stall. */
ok = native_erase(boot.manifest.image_size); /* Стирает только реально необходимые страницы или секторы. */
if (ok) (void)pcan_boot_report_busy(&boot, PCAN_BOOT_CMD_ERASE, ticks_ms); /* Продлевает сессию после завершения длительной аппаратной операции. */
return ok; /* Сообщает результат стирания общему автомату. */
} /* Завершает callback стирания CAN. */
static bool native_write_slot(void *user, pcan_boot_byte_t slot, uint32_t offset, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Подключает единый Flash writer к последовательным блокам CAN. */
{ /* Последовательность номеров блоков и максимум DLC уже проверены общим автоматом. */
(void)user; /* Единственный образ управляется статическим экземпляром загрузчика. */
return slot == 0U && offset == boot.bytes_received && native_write_bytes(boot.manifest.image_size, offset, data, length); /* Не допускает произвольное смещение даже при прямом вызове callback. */
} /* Завершает callback записи CAN. */
static pcan_boot_status_t native_verify(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Различает повреждение реальной Flash и неверную таблицу векторов. */
{ /* Операция не доверяет одному CRC входящего потока. */
(void)user; /* Использует фиксированную область выбранного семейного профиля. */
if (slot != 0U || manifest == NULL || !native_vectors_valid(manifest->image_size)) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Отвергает неверный слот, геометрию и точку входа до чтения всего образа. */
return native_image_valid(manifest->image_size, manifest->image_crc32) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_CRC_ERROR; /* Перечитывает Flash общим CRC32 ProtoCAN. */
} /* Завершает независимую проверку принятого образа. */
static bool native_commit(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) { return native_verify(user, slot, manifest) == PCAN_BOOT_STATUS_OK; } /* COMMIT повторно проверяет single-slot образ без имитации постоянных метаданных. */
static bool native_can_reboot(void *user) { (void)user; return boot.state == PCAN_BOOT_STATE_COMMITTED; } /* Протокольный reset разрешён только после VERIFY и COMMIT. */
static void native_reboot(void *user) { (void)user; NVIC_SystemReset(); } /* Вызывается ядром только после успешного TXOK ответа REBOOT. */
static bool native_init(void) /* Инициализирует текущий API ядра без позиционных структур старой версии. */
{ /* Идентичность устройства задаёт профиль либо конфигурация проекта. */
pcan_boot_config_t config = {0}; pcan_boot_port_t callbacks = {0}; /* Обнуляет необязательные поля и callbacks расширяемого интерфейса. */
config.device_type = PCAN_BOOT_DEVICE_TYPE; config.device_id = PCAN_BOOT_DEVICE_ID; config.product_type = PCAN_BOOT_PRODUCT_TYPE; /* Сохраняет адрес и Product исходного проекта. */
config.hardware_revision = PCAN_BOOT_HW_REVISION; config.firmware_version = PCAN_BOOT_FW_VERSION; /* Сохраняет совместимость manifest по HW/FW. */
config.active_slot = 0U; config.slot_count = 1U; config.max_image_size = NATIVE_APP_SIZE; /* Явно использует single-slot A вместо прежней фиктивной A/B-конфигурации. */
config.ack_window = PCAN_BOOT_ACK_WINDOW; config.session_timeout_ms = 30000UL; /* Сохраняет исходное окно ACK и прерывает неактивную RAM-сессию через 30 секунд. */
callbacks.send = native_send; callbacks.erase_slot = native_erase_slot; callbacks.write_slot = native_write_slot; /* Подключает аппаратный транспорт и защищённую область Flash. */
callbacks.verify_status = native_verify; callbacks.set_pending_slot = native_commit; callbacks.can_reboot = native_can_reboot; callbacks.reboot = native_reboot; /* Подключает readback, COMMIT и согласованную политику reset. */
return pcan_boot_init(&boot, &config, &callbacks, NULL); /* Проверяет конфигурацию и создаёт единственный экземпляр общего автомата. */
} /* Завершает настройку ProtoCAN независимо от включения аппаратной шины. */
static bool native_can_init(void) /* Настраивает выделенный CAN1 и два фильтра для адресных запросов и discovery. */
{ /* Тактирование GPIO и CAN включает семейная функция; UART во время Flash/CAN init не изменяется. */
static const uint8_t shifts[8] = {0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U}; /* RCC кодирует APB1: значения 0..3 без деления, 4..7 дают делители 2,4,8,16. */
uint32_t clock = SystemCoreClock >> shifts[(RCC->CFGR >> NATIVE_PPRE1_SHIFT) & 7U]; /* Получает действительную частоту APB1 в Гц, без удвоения timer clocks. */
uint32_t tq, btr = 0U, started, index; bool found = false; /* Сохраняет bit timing, тайм-аут init и индекс фильтра. */
if (clock % PCAN_BOOT_CAN_BITRATE != 0U) return false; /* Не округляет скорость шины при неподходящем clock tree. */
for (tq = 18U; tq >= 8U; --tq) { /* Сохраняет диапазон 8..18 time quanta из исходного KONOR. */
uint32_t divisor = clock / PCAN_BOOT_CAN_BITRATE, prescaler = divisor / tq, ts2 = tq / 5U; /* Выбирает sample point около 80% при допустимом целочисленном делителе. */
if (divisor % tq == 0U && prescaler >= 1U && prescaler <= 1024U) { btr = ((ts2 - 1U) << 20U) | ((tq - 2U - ts2) << 16U) | (prescaler - 1U); found = true; break; } /* Кодирует BRP/TS1/TS2 с аппаратным смещением минус один и SJW=1 tq. */
} /* Завершает поиск точной скорости для штатного тактирования F103/F407. */
if (!found) return false; /* Не включает шину с неизвестными битовыми интервалами. */
native_gpio_init(); CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM; started = ticks_ms; /* Включает семейные GPIO и запрашивает init с автоматическим восстановлением bus-off. */
while ((CAN1->MSR & CAN_MSR_INAK) == 0U) if ((uint32_t)(ticks_ms - started) >= 100U) return false; /* Ограничивает вход в CAN init ста миллисекундами. */
CAN1->BTR = btr; CAN1->FMR |= CAN_FMR_FINIT; CAN1->FA1R = 0U; CAN1->FS1R |= 3U; CAN1->FM1R &= ~3UL; CAN1->FFA1R &= ~3UL; /* Настраивает только два 32-битных mask-фильтра в FIFO0 и отключает чужие банки загрузчика. */
for (index = 0U; index < 2U; ++index) { /* Банк 0 принимает собственный адрес, банк 1 — только broadcast discovery. */
uint32_t id = index == 0U ? pcan_boot_make_id(0U, 0U, PCAN_BOOT_DEVICE_TYPE, PCAN_BOOT_DEVICE_ID, 0U, 0U) : pcan_boot_make_id(0U, 0U, 7U, 15U, PCAN_BOOT_MSG_DISCOVERY, 0U); /* Не маскирует приоритет, сохраняя оба штатных уровня ProtoCAN. */
uint32_t mask = index == 0U ? 0x0FF00000UL : 0x0FFFFFFFUL; /* Адресный фильтр оставляет MsgType/Body открытыми; discovery сравнивает их точно. */
CAN1->sFilterRegister[index].FR1 = (id << 3U) | CAN_RI0R_IDE; CAN1->sFilterRegister[index].FR2 = (mask << 3U) | CAN_RI0R_IDE | CAN_RI0R_RTR; /* Исключает standard и remote кадры на аппаратном уровне. */
} /* Завершает настройку обоих фильтров. */
CAN1->FA1R |= 3U; CAN1->FMR &= ~CAN_FMR_FINIT; CAN1->MCR &= ~CAN_MCR_INRQ; started = ticks_ms; /* Активирует фильтры и переводит CAN в рабочий режим. */
while ((CAN1->MSR & CAN_MSR_INAK) != 0U) if ((uint32_t)(ticks_ms - started) >= 100U) return false; /* Отсутствие доступной шины не должно навсегда блокировать обычный запуск приложения. */
return true; /* Рабочий CAN готов принимать адресные команды и discovery. */
} /* Завершает общую аппаратную настройку bxCAN. */
static void native_can_poll(void) /* Обрабатывает не больше трёх кадров — аппаратной глубины FIFO bxCAN. */
{ /* Длительные операции Flash выполняются только из основного цикла. */
uint32_t count; pcan_boot_poll(&boot, ticks_ms); /* Обновляет тайм-аут сессии и отложенное discovery независимо от входящих команд. */
for (count = 0U; count < 3U && (CAN1->RF0R & CAN_RF0R_FMP0) != 0U; ++count) { /* Ограничивает время одного вызова на нагруженной шине. */
uint32_t rir = CAN1->sFIFOMailBox[0].RIR, low = CAN1->sFIFOMailBox[0].RDLR, high = CAN1->sFIFOMailBox[0].RDHR; /* Сохраняет поля до освобождения аппаратного FIFO. */
uint8_t dlc = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 15U), data[8], index; /* Читает входящий DLC отдельно от фиксированного буфера classic CAN. */
for (index = 0U; index < 8U; ++index) data[index] = (uint8_t)(index < 4U ? low >> (index * 8U) : high >> ((index - 4U) * 8U)); /* Извлекает октеты без невыровненного чтения аппаратных регистров. */
CAN1->RF0R |= CAN_RF0R_RFOM0; /* Освобождает обработанный элемент FIFO0 до потенциально долгого erase. */
if ((rir & CAN_RI0R_IDE) != 0U && (rir & CAN_RI0R_RTR) == 0U && dlc <= 8U) { pcan_boot_poll(&boot, ticks_ms); (void)pcan_boot_process(&boot, (rir >> 3U) & 0x1FFFFFFFUL, data, dlc); } /* Повторно проверяет формат внешнего кадра и вызывает общий автомат. */
} /* Завершает ограниченный пакет входящих кадров. */
} /* Завершает обслуживание транспорта и часов ядра. */
@@ -0,0 +1,10 @@
#ifndef PCAN_BOOT_STM32_H /* Защищает общие объявления двух STM32-портов от повторного включения. */
#define PCAN_BOOT_STM32_H /* Фиксирует включение интерфейса single-slot загрузчика. */
#include "../../include/pcan_boot.h" /* Использует актуальные именованные поля конфигурации и callbacks ядра. */
typedef struct { /* Состояние принадлежит одному загрузчику и одному выделенному bxCAN. */
pcan_boot_t boot; /* Хранит сессию, manifest, CRC потока и состояние общего автомата. */
void *can; /* Ссылается на CAN_HandleTypeDef bxCAN либо FDCAN_HandleTypeDef G474; время жизни задаёт проект. */
uint32_t written; /* Следующее допустимое смещение записи, в байтах от начала приложения. */
bool ready; /* Разрешает reset только после проверки Flash и успешного COMMIT текущего образа. */
} pcan_boot_stm32_t; /* Не содержит startup, main или обработчиков прерываний проекта. */
#endif /* Завершает общий интерфейс STM32F103/F407. */
@@ -0,0 +1,167 @@
static bool send_frame(void *user, uint32_t id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) /* Передаёт только объявленные октеты и дожидается физического завершения ACK. */
{ /* Порт владеет CAN1 без TX IRQ, поэтому флаги завершения не очищаются чужим обработчиком. */
pcan_boot_stm32_t *port = user; /* Получает состояние выделенного аппаратного порта. */
CAN_HandleTypeDef *can = port->can; /* Восстанавливает тип handle семейства HAL текущей единицы трансляции. */
CAN_TxHeaderTypeDef header = {0}; /* Не оставляет мусора в стандартном ID и timestamp-флагах. */
uint8_t bytes[8] = {0}; /* CAN 2.0 передаёт не больше восьми октетов; короткий payload дополняется безопасными нулями. */
uint32_t mailbox, mask, started = HAL_GetTick(); /* Запоминает mailbox, его TXOK-флаг и начало тайм-аута в мс. */
if (dlc > 8U || id > 0x1FFFFFFFUL || (dlc != 0U && data == NULL)) return false; /* Запрещает чтение за payload и идентификатор шире 29 бит. */
if (dlc != 0U) (void)memcpy(bytes, data, dlc); /* Не читает отсутствующие байты коротких служебных сообщений. */
while (HAL_CAN_GetTxMailboxesFreeLevel(can) == 0U) { /* Ожидает освобождения аппаратного передатчика. */
if ((uint32_t)(HAL_GetTick() - started) >= 100U) return false; /* Ограничивает ожидание 100 мс, включая переполнение счётчика времени. */
} /* Завершает ожидание свободного mailbox. */
header.ExtId = id; /* Передаёт адресный ProtoCAN ID без преобразования полей. */
header.IDE = CAN_ID_EXT; /* ProtoCAN использует только расширенные 29-битные идентификаторы. */
header.RTR = CAN_RTR_DATA; /* Все ответы загрузчика содержат обычные данные, а не remote-запрос. */
header.DLC = dlc; /* Фиксирует фактическое число объявленных октетов кадра. */
can->Instance->TSR = CAN_TSR_RQCP0 | CAN_TSR_RQCP1 | CAN_TSR_RQCP2; /* Очищает прежние TXOK/ошибки записью W1C перед новой отправкой; CAN принадлежит только этому порту. */
if (HAL_CAN_AddTxMessage(can, &header, bytes, &mailbox) != HAL_OK) return false; /* Не считает не поставленный в очередь ответ отправленным. */
while (HAL_CAN_IsTxMessagePending(can, mailbox) != 0U) { /* Сохраняет CAN до завершения ответа, включая команду REBOOT. */
if ((uint32_t)(HAL_GetTick() - started) >= 100U) { /* Не зависает навсегда при отсутствии ACK от шины. */
(void)HAL_CAN_AbortTxRequest(can, mailbox); /* Снимает просроченный запрос передачи перед возвратом ошибки. */
return false; /* Ядро не выполнит reset после неподтверждённого ответа. */
} /* Завершает обработку тайм-аута TX. */
} /* Аппаратная передача завершилась либо была отвергнута CAN. */
mask = mailbox == CAN_TX_MAILBOX0 ? CAN_TSR_TXOK0 : mailbox == CAN_TX_MAILBOX1 ? CAN_TSR_TXOK1 : CAN_TSR_TXOK2; /* Сопоставляет bitmask mailbox HAL с флагом успешной передачи bxCAN. */
return (can->Instance->TSR & mask) != 0U; /* Отличает подтверждённый ACK от снятия запроса вследствие ошибки. */
} /* Завершает синхронный транспорт служебных ответов. */
bool STM32_BOOT_VECTORS(void) /* Проверяет только структурную корректность таблицы векторов установленного приложения. */
{ /* Проверка CRC образа выполняется отдельно при VERIFY. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)STM32_BOOT_BASE; /* Читает первые два слова из фиксированной области приложения. */
uint32_t sp = vectors[0], pc = vectors[1]; /* Извлекает начальный стек и Thumb-адрес reset handler. */
return sp > 0x20000000UL && sp <= STM32_BOOT_RAM_END && (sp & 7U) == 0U && (pc & 1U) != 0U && (pc & ~1UL) >= STM32_BOOT_BASE && (pc & ~1UL) < STM32_BOOT_END; /* Требует доступный стек с выравниванием 8 байт и точку входа внутри приложения. */
} /* Завершает проверку векторов без изменения MCU. */
static bool erase_slot(void *user, pcan_boot_byte_t slot) /* Стирает единственную область после проверки manifest общим ядром. */
{ /* Сохранение старого приложения и rollback в этом профиле отсутствуют. */
pcan_boot_stm32_t *port = user; /* Получает manifest и состояние активации текущего загрузчика. */
uint32_t size = port->boot.manifest.image_size; /* Ограничивает стирание фактической длиной образа в байтах. */
bool ok, locked; /* Сохраняет результаты операции и последующего восстановления защиты Flash. */
if (slot != 0U || size < 8U || size > STM32_BOOT_END - STM32_BOOT_BASE) return false; /* Не допускает чужой слот, неполные векторы и выход за область приложения. */
port->ready = false; /* Инвалидирует разрешение reset до первого изменения Flash. */
port->written = 0U; /* Начинает последовательную запись заново после стирания. */
(void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Предупреждает клиента перед синхронным блокированием Flash. */
if (HAL_FLASH_Unlock() != HAL_OK) return false; /* Не продолжает работу при отказе разблокировки Flash. */
ok = erase_region(port, size); /* Выполняет семейную операцию по страницам F1 либо секторам F4. */
locked = HAL_FLASH_Lock() == HAL_OK; /* Восстанавливает защиту Flash и при успехе, и при отказе стирания. */
if (ok && locked) (void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Обновляет время после блокировки Flash, чтобы длительное стирание не погасило сессию. */
return ok && locked; /* Не подтверждает ERASE при любом аппаратном отказе. */
} /* Завершает callback стирания. */
static bool write_slot(void *user, pcan_boot_byte_t slot, uint32_t offset, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Программирует последовательные блоки без пересечения защищённых областей. */
{ /* Крайний неполный блок дополняется единицами до аппаратной единицы программирования. */
pcan_boot_stm32_t *port = user; /* Получает длину manifest и ожидаемое следующее смещение. */
uint32_t done = 0U, size = port->boot.manifest.image_size; /* Считает только полезные байты без дополнения FF. */
bool ok = true, locked; /* Позволяет гарантированно закрыть Flash после любой ошибки. */
if (slot != 0U || data == NULL || length == 0U || length > 8U || size < 8U || size > STM32_BOOT_END - STM32_BOOT_BASE || offset != port->written || offset > size || length > size - offset || (offset % STM32_BOOT_UNIT) != 0U) return false; /* Проверяет адреса до сложения, выравнивание и принадлежность текущему образу. */
if ((length % STM32_BOOT_UNIT) != 0U && offset + length != size) return false; /* Дополнение FF допустимо только у последнего блока; повторная запись слова запрещена. */
if (HAL_FLASH_Unlock() != HAL_OK) return false; /* Не выполняет запись при заблокированной Flash. */
while (done < length) { /* Разбивает CAN-блок на аппаратные полуслова F1 либо слова F4. */
uint32_t value = 0xFFFFFFFFUL, portion = length - done; /* Стертое значение FF заполняет только хвост последней аппаратной записи. */
uint32_t address = STM32_BOOT_BASE + offset + done; /* Адрес остаётся в заранее проверенной области приложения. */
HAL_StatusTypeDef status; /* Отделяет аппаратное завершение записи от последующей синхронизации Flash-кэша. */
if (portion > STM32_BOOT_UNIT) portion = STM32_BOOT_UNIT; /* Ограничивает копирование размером аппаратной записи. */
(void)memcpy(&value, data + done, portion); /* Не читает за фактическим payload последнего блока. */
status = HAL_FLASH_Program(STM32_BOOT_PROGRAM, address, value); /* Дожидается завершения программирования выбранной аппаратной единицы. */
STM32_BOOT_SYNC(); /* На F407 сбрасывает строки Flash-кэша после записи, чтобы readback соседнего слова не получил старое FF. */
if (status != HAL_OK || memcmp((const void *)(uintptr_t)address, &value, STM32_BOOT_UNIT) != 0) { /* Проверяет результат HAL и чтение записанного значения, включая хвост FF. */
ok = false; /* Делает частично записанный образ непригодным для VERIFY/COMMIT. */
break; /* Не продолжает программирование после первого отказа. */
} /* Завершает проверку результата программирования. */
done += portion; /* Продвигает полезное смещение без включения дополнения FF в CRC. */
} /* Завершает аппаратные записи текущего CAN-блока. */
locked = HAL_FLASH_Lock() == HAL_OK; /* Обязательно восстанавливает аппаратную блокировку Flash. */
if (ok && locked) port->written += length; /* Подтверждает смещение только после полного успеха и блокировки. */
return ok && locked; /* Ошибка передаётся общему автомату как FLASH_ERROR. */
} /* Завершает callback последовательной записи. */
static pcan_boot_status_t verify_image(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Перечитывает реальную Flash вместо доверия одному CRC входящего потока. */
{ /* Проверяет геометрию до доступа к памяти и точный размер переданных данных. */
pcan_boot_stm32_t *port = user; /* Получает фактический счётчик успешно записанных байтов. */
const uint8_t *image = (const uint8_t *)(uintptr_t)STM32_BOOT_BASE; /* Отображает фиксированную область приложения в адресное пространство MCU. */
uint32_t offset, crc = 0U; /* Начинает общий потоковый CRC32 с нуля. */
if (slot != 0U || manifest == NULL || manifest->image_size < 8U || manifest->image_size > STM32_BOOT_END - STM32_BOOT_BASE || port->written != manifest->image_size) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Не читает произвольную память по недостоверному manifest. */
for (offset = 0U; offset < manifest->image_size;) { /* Проверяет весь полезный образ ограниченными порциями. */
uint32_t count = manifest->image_size - offset; /* Вычисляет остаток в байтах без выхода за границу образа. */
if (count > 1024U) count = 1024U; /* Порция 1 КиБ ограничивает объём одного вызова потокового CRC. */
crc = pcan_boot_crc32_update(crc, image + offset, count); /* Использует общий полином ProtoCAN и не включает дополнение FF. */
offset += count; /* Продвигает позицию чтения проверенного образа. */
} /* Завершает чтение Flash для независимой проверки CRC. */
if (crc != manifest->image_crc32) return PCAN_BOOT_STATUS_CRC_ERROR; /* Обнаруживает повреждение памяти после успешного приёма CAN. */
if (!STM32_BOOT_VECTORS() || ((*(const uint32_t *)(const void *)(image + 4U)) & ~1UL) >= STM32_BOOT_BASE + manifest->image_size) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Не принимает reset handler вне фактически загруженного образа. */
return PCAN_BOOT_STATUS_OK; /* Реальная Flash и векторы соответствуют полученному manifest. */
} /* Завершает проверку записанного образа. */
static bool commit_image(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Активирует только повторно проверенный single-slot образ. */
{ /* Не имитирует постоянные A/B-метаданные или защиту от потери питания. */
pcan_boot_stm32_t *port = user; /* Получает RAM-признак успешного COMMIT текущей сессии. */
port->ready = verify_image(user, slot, manifest) == PCAN_BOOT_STATUS_OK; /* Перечитывает Flash перед разрешением reset. */
return port->ready; /* Ядро перейдёт в COMMITTED только при успешной проверке. */
} /* Завершает активацию без записи служебных страниц. */
static bool can_reboot(void *user) /* Запрещает программный запуск недописанного приложения по команде протокола. */
{ /* Политика запуска после внешнего reset остаётся обязанностью проекта. */
pcan_boot_stm32_t *port = user; /* Получает состояние текущей сессии и подтверждение COMMIT. */
return port->ready && port->boot.state == PCAN_BOOT_STATE_COMMITTED; /* ABORT, новая сессия и ошибки сами по себе не дают права reset. */
} /* Завершает проверку допустимости протокольного reset. */
static void reboot(void *user) /* Выполняет reset только после синхронно отправленного ответа ядра. */
{ /* Повторно проверяет RAM-разрешение непосредственно перед сбросом. */
if (can_reboot(user)) NVIC_SystemReset(); /* Использует штатный CMSIS reset, оставляя startup и выбор приложения проекту. */
} /* Завершает callback перезапуска. */
bool STM32_BOOT_INIT(pcan_boot_stm32_t *port, CAN_HandleTypeDef *can, const pcan_boot_config_t *config) /* Подключает текущий API ядра к выделенным CAN1 и single-slot Flash. */
{ /* Тактирование, GPIO, bitrate, HAL_Init и SysTick должны быть настроены проектом заранее. */
pcan_boot_port_t callbacks = {0}; /* Новые необязательные callbacks ядра всегда остаются явно нулевыми. */
pcan_boot_config_t single; /* Копирует идентичность прибора, переопределяя геометрию профиля. */
CAN_FilterTypeDef filter = {0}; /* Подготавливает адресный фильтр 0 и discovery-фильтр 1 в FIFO0. */
uint32_t id, mask; /* Содержит идентификатор и аппаратную маску IDE/RTR сдвига bxCAN. */
if (port == NULL || can == NULL || config == NULL || can->Instance != CAN1) return false; /* Отклоняет неверные указатели и неподдерживаемый CAN2. */
single = *config; /* Сохраняет DeviceType/ID, Product, HW, FW, ack_window и тайм-аут сессии проекта. */
single.active_slot = 0U; /* Single-slot обновление всегда адресует логический слот A. */
single.slot_count = 1U; /* Исключает старую имитацию single-slot через фиктивный active_slot=1. */
single.max_image_size = STM32_BOOT_END - STM32_BOOT_BASE; /* Ядро отвергает превышение физической области ещё до erase. */
callbacks.send = send_frame; /* Подключает синхронную отправку подтверждённых CAN-ответов. */
callbacks.erase_slot = erase_slot; /* Подключает стирание только области приложения. */
callbacks.write_slot = write_slot; /* Подключает последовательное программирование с readback каждого слова. */
callbacks.verify_status = verify_image; /* Возвращает отдельные статусы CRC_ERROR и INVALID_SIZE для Flash/vectors. */
callbacks.set_pending_slot = commit_image; /* Согласует COMMIT с проверкой реального образа и RAM-признаком активации. */
callbacks.can_reboot = can_reboot; /* Не позволяет REBOOT обойти проверку завершённого образа. */
callbacks.reboot = reboot; /* Выполняет CMSIS reset после подтверждения ответа на шине. */
(void)memset(port, 0, sizeof(*port)); /* Очищает старые счётчики и разрешение активации до настройки сессии. */
port->can = can; /* Сохраняет заранее инициализированный handle HAL. */
if (!pcan_boot_init(&port->boot, &single, &callbacks, port)) { port->can = NULL; return false; } /* При ошибке адреса оставляет последующий poll безопасным. */
id = pcan_boot_make_id(0U, PCAN_BOOT_ROUTE_FROM_PM, single.device_type, single.device_id, 0U, 0U) << 3U; /* Переводит адрес прибора в регистровый формат bxCAN. */
mask = (0x0FF00000UL << 3U) | 6U; /* Проверяет Route/Type/ID, IDE=1 и RTR=0; приоритет, MsgType и Body остаются открытыми. */
id |= 4U; /* Устанавливает IDE в бите 2 аппаратного фильтра bxCAN. */
filter.FilterBank = 0U; /* Выделенный загрузчик использует первый банк фильтров. */
filter.FilterMode = CAN_FILTERMODE_IDMASK; /* Сравнивает адрес по маске вместо списка отдельных команд. */
filter.FilterScale = CAN_FILTERSCALE_32BIT; /* Сохраняет все 29 бит расширенного идентификатора. */
filter.FilterIdHigh = id >> 16U; /* Передаёт HAL старшую половину регистрового идентификатора. */
filter.FilterIdLow = id & 0xFFFFU; /* Передаёт HAL младшую половину, включая IDE/RTR. */
filter.FilterMaskIdHigh = mask >> 16U; /* Маскирует адрес прибора и направление обмена. */
filter.FilterMaskIdLow = mask & 0xFFFFU; /* Отвергает стандартные и remote-кадры до программного разбора. */
filter.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* Приём выполняется опросом FIFO0 без IRQ. */
filter.FilterActivation = ENABLE; /* Включает настроенный адресный фильтр. */
filter.SlaveStartFilterBank = 14U; /* Стандартная граница банков CAN1/CAN2 в HAL; порт не использует CAN2. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK) { port->can = NULL; return false; } /* Не запускает загрузчик с неподтверждённой настройкой адресного фильтра. */
id = (pcan_boot_make_id(0U, PCAN_BOOT_ROUTE_FROM_PM, 7U, 15U, PCAN_BOOT_MSG_DISCOVERY, 0U) << 3U) | 4U; /* Отдельно разрешает штатный broadcast discovery, сохраняя запрет broadcast-записи. */
mask = (0x0FFFFFFFUL << 3U) | 6U; /* Проверяет всё кроме приоритета, включая MsgType=DISCOVERY и пустой Body. */
filter.FilterBank = 1U; /* Второй выделенный банк обслуживает только обнаружение устройств. */
filter.FilterIdHigh = id >> 16U; /* Записывает старшую половину broadcast discovery ID. */
filter.FilterIdLow = id & 0xFFFFU; /* Записывает младшую половину с IDE=1 и RTR=0. */
filter.FilterMaskIdHigh = mask >> 16U; /* Ограничивает тип и адрес broadcast запроса. */
filter.FilterMaskIdLow = mask & 0xFFFFU; /* Ограничивает Body и формат кадра discovery. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK || HAL_CAN_Start(can) != HAL_OK) { port->can = NULL; return false; } /* Обезвреживает poll при отказе второго фильтра либо запуска CAN. */
return true; /* Оба фильтра настроены, CAN успешно вышел из режима init. */
} /* Завершает настройку одного аппаратного порта. */
void STM32_BOOT_POLL(pcan_boot_stm32_t *port) /* Обслуживает загрузчик только из основного цикла, где допустимо синхронное стирание. */
{ /* Вызов из CAN IRQ не допускается из-за длительных операций Flash. */
CAN_HandleTypeDef *can; /* Получает аппаратный handle после проверки состояния порта. */
uint32_t count; /* Ограничивает обработку глубиной аппаратного FIFO bxCAN. */
if (port == NULL || port->can == NULL) return; /* Позволяет безопасно пропустить ещё не настроенный порт. */
can = port->can; /* Восстанавливает семейный HAL-тип handle. */
pcan_boot_poll(&port->boot, HAL_GetTick()); /* Обновляет монотонное время и тайм-аут сессии до приёма команд. */
for (count = 0U; count < 3U && HAL_CAN_GetRxFifoFillLevel(can, CAN_RX_FIFO0) != 0U; ++count) { /* Не позволяет непрерывной нагрузке CAN захватить весь главный цикл. */
CAN_RxHeaderTypeDef header = {0}; /* Принимает IDE/RTR/DLC независимо от аппаратной настройки фильтра. */
uint8_t data[8]; /* Выделяет максимальный payload одного classic CAN-кадра. */
if (HAL_CAN_GetRxMessage(can, CAN_RX_FIFO0, &header, data) != HAL_OK) break; /* Не разбирает неполученный кадр при аппаратной ошибке. */
if (header.IDE == CAN_ID_EXT && header.RTR == CAN_RTR_DATA && header.DLC <= 8U && header.ExtId <= 0x1FFFFFFFUL) { /* Повторно проверяет внешний ввод до вызова общего decoder. */
pcan_boot_poll(&port->boot, HAL_GetTick()); /* Учитывает время предыдущего синхронного erase перед следующей командой. */
(void)pcan_boot_process(&port->boot, header.ExtId, data, (uint8_t)header.DLC); /* Передаёт адресные команды и восьмибайтовые блоки общему автомату. */
} /* Завершает обработку допустимого формата кадра. */
} /* Возвращает управление приложению после ограниченного пакета входящих кадров. */
} /* Завершает обслуживание транспорта и таймеров. */