diff --git a/c/protocan-boot/CMakeLists.txt b/c/protocan-boot/CMakeLists.txt index 47d6c8e..2548300 100644 --- a/c/protocan-boot/CMakeLists.txt +++ b/c/protocan-boot/CMakeLists.txt @@ -1,6 +1,6 @@ -# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают -# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки; -# запускать их следует через CTest из каталога сборки. +# РЎР±РѕСЂРєР° библиотеки protocan_boot. Состав РёСЃС…РѕРґРЅРёРєРѕРІ Рё публичные include-пути задают +# подключение Рє проекту потребителя. Файл также собирает Рё регистрирует хостовые проверки; +# запускать РёС… следует через CTest РёР· каталога СЃР±РѕСЂРєРё. cmake_minimum_required(VERSION 3.13) project(protocan_boot C) @@ -17,10 +17,31 @@ else() target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic) endif() -option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON) +option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON) if(PCAN_BOOT_BUILD_TESTS) enable_testing() add_executable(test_pcan_boot tests/test_pcan_boot.c) target_link_libraries(test_pcan_boot PRIVATE protocan_boot) add_test(NAME pcan_boot COMMAND test_pcan_boot) endif() + +if(PCAN_BOOT_BUILD_TESTS) + add_executable(test_f28335_can_boot tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c) + target_compile_definitions(test_f28335_can_boot PRIVATE PCAN_BOOT_WIDE_BYTES) + target_include_directories(test_f28335_can_boot PRIVATE tests/f28335_stubs) + add_test(NAME f28335_can_boot COMMAND test_f28335_can_boot) +endif() + +if(PCAN_BOOT_BUILD_TESTS) + add_executable(test_f28335_application tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c) + target_compile_definitions(test_f28335_application PRIVATE PCAN_BOOT_WIDE_BYTES F28335_BOOT_LOADER=0) + target_include_directories(test_f28335_application PRIVATE tests/f28335_stubs) + add_test(NAME f28335_application COMMAND test_f28335_application) + foreach(width native wide) + add_executable(test_f28335_store_${width} tests/test_f28335_boot_store.c ports/tms320f28335/f28335_boot_store.c) + if(width STREQUAL "wide") + target_compile_definitions(test_f28335_store_${width} PRIVATE PCAN_BOOT_WIDE_BYTES) + endif() + add_test(NAME f28335_store_${width} COMMAND test_f28335_store_${width}) + endforeach() +endif() diff --git a/c/protocan-boot/README.md b/c/protocan-boot/README.md index 174756f..b1bb5c7 100644 --- a/c/protocan-boot/README.md +++ b/c/protocan-boot/README.md @@ -97,7 +97,8 @@ typedef struct { } pcan_boot_port_t; ``` -Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`. +Обязательны `send`, `erase_slot`, `write_slot`; в A/B также `set_pending_slot`. +В single-slot этот callback необязателен и может завершать активацию образа. `authorize` проверяет политику до стирания (тип, версия, anti-rollback). `verify_image` выполняет платформенную проверку подписанного контейнера после CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки @@ -175,5 +176,12 @@ ctest --test-dir build --output-on-failure - включить watchdog и rollback при неподтверждённом запуске; - согласовать фильтры CAN для `MsgType=0x9..0xD`. -Проект пока не подключён ни к одной конкретной плате; это самостоятельный -шаблон для интеграции в будущие загрузчики приборов SET. +Ядро поддерживает платформенные порты; особенности интеграции перечислены ниже. + +## F28335 / большая EEPROM + +[Порт M95M02](ports/tms320f28335/README.md) подключён в `tms_periph/periph_167`. +Использует отдельный Flash-загрузчик и два слота EEPROM по 130560 байт. +Реализованы readback/CRC, проверка SCI8, журнал метаданных, пробный запуск, +CONFIRM/откат, PING/discovery и сервисный паспорт. Первичная установка Flash- +загрузчика и выбор Flash boot описаны в README порта. diff --git a/c/protocan-boot/docs/BOOTLOADER.md b/c/protocan-boot/docs/BOOTLOADER.md index f4ccf78..b180cdb 100644 --- a/c/protocan-boot/docs/BOOTLOADER.md +++ b/c/protocan-boot/docs/BOOTLOADER.md @@ -63,6 +63,10 @@ BlockIndex = 0x0000…0xFFFF | `0x09` | `REBOOT` | 0 | отсутствует | активная сессия | | `0x0A` | `ABORT` | 0 | отсутствует | активная сессия | | `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия | +| `0x0C` | `PING` | 0 | отсутствует | любое | +| `0x0D` | `CAPABILITIES` | 0 | отсутствует | любое, при поддержке порта | +| `0x0E` | `DEVICE_NAME` | 1 | номер страницы имени | любое, при поддержке порта | +| `0x0F` | `MEMORY_INFO` | 1 | номер страницы памяти | любое, при поддержке порта | ### `BEGIN_IMAGE` @@ -81,7 +85,8 @@ DATA[4..7] FirmwareVersion, uint32 little-endian ``` До `ERASE` прибор обязан получить обе части метаданных и проверить размер, -тип изделия, аппаратную ревизию, версию и политику anti-rollback. +тип изделия и аппаратную ревизию. Проверка версии/anti-rollback выполняется +только при наличии соответствующей политики платформы (callback authorize). ## `BOOT_STATUS` @@ -181,3 +186,16 @@ Boot metadata должна атомарно хранить: 5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`. 6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`. 7. Новое приложение после самопроверки выполняет `CONFIRM`. + +## F28335 M95M02 + +Реализованы отдельный Flash-загрузчик, A/B по 130560 байт, журнал, пробный запуск +и откат. Подробная раскладка памяти, сервисных ответов и первичная установка: +[README порта](../ports/tms320f28335/README.md). + +Общий discovery: Route=0, DeviceType=7, DeviceID=15, MsgType=D, Body=0, DLC=0. +Он только запрашивает IDENTIFY; операции изменения всегда адресные. +ENTER_BOOT может отложить ACK до готовности отдельного загрузчика после reset. +CONFIRM с ненулевой session может обрабатываться приложением без ENTER_BOOT. +`pcan_boot_poll` обслуживает задержку discovery и тайм-аут сессии, если он задан +портом. Старым портам с timeout=0 периодический вызов нужен только для discovery. diff --git a/c/protocan-boot/include/pcan_boot.h b/c/protocan-boot/include/pcan_boot.h index 7118b51..cf9f7a9 100644 --- a/c/protocan-boot/include/pcan_boot.h +++ b/c/protocan-boot/include/pcan_boot.h @@ -11,6 +11,13 @@ #include #include +/* C28x has 16-bit addressable chars; wire octets use the low eight bits. */ +#if defined(__TMS320C28XX__) || defined(PCAN_BOOT_WIDE_BYTES) +typedef unsigned int pcan_boot_byte_t; +#else +typedef uint8_t pcan_boot_byte_t; +#endif + #ifdef __cplusplus extern "C" { #endif @@ -41,7 +48,11 @@ typedef enum { PCAN_BOOT_CMD_CONFIRM = 0x08U, PCAN_BOOT_CMD_REBOOT = 0x09U, PCAN_BOOT_CMD_ABORT = 0x0AU, - PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU + PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU, + PCAN_BOOT_CMD_PING = 0x0CU, + PCAN_BOOT_CMD_CAPABILITIES = 0x0DU, + PCAN_BOOT_CMD_DEVICE_NAME = 0x0EU, + PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU } pcan_boot_command_t; typedef enum { @@ -66,7 +77,8 @@ typedef enum { PCAN_BOOT_STATE_READY_TO_ERASE, PCAN_BOOT_STATE_RECEIVING, PCAN_BOOT_STATE_VERIFIED, - PCAN_BOOT_STATE_FAILED + PCAN_BOOT_STATE_FAILED, + PCAN_BOOT_STATE_COMMITTED } pcan_boot_state_t; typedef struct { @@ -74,36 +86,47 @@ typedef struct { uint32_t image_crc32; uint32_t firmware_version; uint16_t product_type; - uint8_t hardware_revision_min; - uint8_t hardware_revision_max; + pcan_boot_byte_t hardware_revision_min; + pcan_boot_byte_t hardware_revision_max; } pcan_boot_manifest_t; typedef struct { - uint8_t device_type; - uint8_t device_id; + pcan_boot_byte_t device_type; + pcan_boot_byte_t device_id; uint16_t product_type; - uint8_t hardware_revision; + pcan_boot_byte_t hardware_revision; uint32_t firmware_version; - uint8_t active_slot; + pcan_boot_byte_t active_slot; /** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */ - uint8_t slot_count; + pcan_boot_byte_t slot_count; /** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */ uint32_t max_image_size; - uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */ + pcan_boot_byte_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */ + uint32_t session_timeout_ms; /**< 0 disables timeout for legacy ports. */ } pcan_boot_config_t; typedef struct { - bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc); - bool (*erase_slot)(void *user, uint8_t slot); - bool (*write_slot)(void *user, uint8_t slot, uint32_t offset, - const uint8_t *data, uint8_t length); + bool (*send)(void *user, uint32_t can_id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc); + bool (*erase_slot)(void *user, pcan_boot_byte_t slot); + bool (*write_slot)(void *user, pcan_boot_byte_t slot, uint32_t offset, + const pcan_boot_byte_t *data, pcan_boot_byte_t length); bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest); - bool (*verify_image)(void *user, uint8_t slot, + bool (*verify_image)(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest); - bool (*set_pending_slot)(void *user, uint8_t slot, + /** A/B metadata, or optional single-slot activation after verification. */ + bool (*set_pending_slot)(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest); bool (*confirm_running_slot)(void *user); void (*reboot)(void *user); + /** ENTER: 0 ready, 1 deferred ACK after reset, 2 denied. NULL = legacy ready. */ + unsigned int (*enter_boot)(void *user, pcan_boot_byte_t session); + bool (*can_reboot)(void *user); + /** Eight-octet service response, addressed by command + page. */ + bool (*service_info)(void *user, pcan_boot_byte_t command, + pcan_boot_byte_t page, pcan_boot_byte_t *out); + /** Optional precise result, replacing legacy boolean verify_image. */ + pcan_boot_status_t (*verify_status)(void *user, pcan_boot_byte_t slot, + const pcan_boot_manifest_t *manifest); } pcan_boot_port_t; typedef struct { @@ -115,11 +138,13 @@ typedef struct { uint32_t running_crc32; uint32_t bytes_received; uint16_t next_block; - uint8_t session_id; - uint8_t target_slot; - uint8_t last_status; + pcan_boot_byte_t session_id; + pcan_boot_byte_t target_slot; + pcan_boot_byte_t last_status; bool have_image; bool have_compat; + uint32_t now_ms, last_activity_ms, discovery_at; + bool discovery_pending; } pcan_boot_t; /** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */ @@ -134,16 +159,19 @@ void pcan_boot_abort(pcan_boot_t *boot); * Возвращает true, если кадр принадлежал загрузочному сервису этого прибора. */ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, - const uint8_t *data, uint8_t dlc); + const pcan_boot_byte_t *data, pcan_boot_byte_t dlc); + +/** Call regularly with a wrapping monotonic millisecond clock. */ +void pcan_boot_poll(pcan_boot_t *boot, uint32_t now_ms); /** Переносимые операции с 29-битным ProtoCAN ID. */ -uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, - uint8_t device_type, uint8_t device_id, - uint8_t msg_type, uint16_t msg_body); -uint8_t pcan_boot_id_route(uint32_t can_id); -uint8_t pcan_boot_id_device_type(uint32_t can_id); -uint8_t pcan_boot_id_device_id(uint32_t can_id); -uint8_t pcan_boot_id_msg_type(uint32_t can_id); +uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route, + pcan_boot_byte_t device_type, pcan_boot_byte_t device_id, + pcan_boot_byte_t msg_type, uint16_t msg_body); +pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id); +pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id); +pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id); +pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id); uint16_t pcan_boot_id_msg_body(uint32_t can_id); #ifdef __cplusplus diff --git a/c/protocan-boot/ports/tms320f28335/README.md b/c/protocan-boot/ports/tms320f28335/README.md new file mode 100644 index 0000000..3d45984 --- /dev/null +++ b/c/protocan-boot/ports/tms320f28335/README.md @@ -0,0 +1,156 @@ +# F28335 + M95M02: CAN, отдельный загрузчик и A/B + +Реализация состоит из Flash-загрузчика `periph_167/Bootloader`, переносимого +`f28335_boot_store.c`, аппаратного `f28335_boot_hw.c` и адаптера +`f28335_can_boot.c`. Приложение подключает их через `Source/Internal/can_boot.c`. +В сборке `EEPROM_M95M02=0` CAN Boot отключён. Для установленной M95M02 — значение 1. + +## Адрес и формат + +Device Type **3**, Device ID **7**, имя **F28335-M95M02**, Product **0x2833**, HW **1**. +В приложении FW берётся из `firmware_version.h`, формат `major<<24 | minor<<16 | patch`; +для версии 1.1.2 это `0x01010002`. Загрузчик сообщает версию подтверждённого образа +из журнала, при отсутствии такого образа — 0. Версия кандидата передаётся в BEGIN_COMPAT. +CAN **250000 бит/с**, Extended ID, окно **16 блоков по 8 октетов**. +ENTER_BOOT для session=1: `0x13790102`, STATUS: `0x1B7C0102`. + +Образ — SCI8 BIN (`AA 08`), чётная длина от 24 до **130560 байт** включительно. +Числа в протоколе little endian. Адреса внутри SCI8: сначала старшее 16-битное +слово, затем младшее; каждое слово little endian. В C28x `char` 16-битный, +поэтому `pcan_boot_byte_t` хранит октет в младших 8 битах. + +## Разметка EEPROM, байтовые адреса + +| Область | Начало | Конец включительно | Размер | +|---|---:|---:|---:| +| Журнал 0 | 0x00000 | 0x000FF | 256 | +| Журнал 1 | 0x00100 | 0x001FF | 256 | +| Slot A | 0x00200 | 0x1FFFF | 130560 | +| Slot B | 0x20000 | 0x3FDFF | 130560 | +| Резерв | 0x3FE00 | 0x3FEFB | 252 | +| Параметры | 0x3FEFC | 0x3FFFD | 258 | +| Последнее слово | 0x3FFFE | 0x3FFFF | 2 | + +В каждой странице журнала первые 64 октета содержат magic `FAB2`, версию 1, +sequence, active/pending/attempted, два manifest, CRC32 первых 56 октетов и +commit marker в октетах 60..63. Сначала обнуляется marker запасной страницы, +затем записывается тело с проверкой чтением, последним — marker. Предыдущая +страница остаётся действующей до полной публикации новой. Выбор sequence +учитывает переполнение u32. После ошибки публикации журнал перечитывается; +ошибка чтения блокирует последующие записи. + +## Сценарий обновления и отката + +1. Приложение принимает ENTER_BOOT, останавливает управление нагрузкой, выставляет 4 мА и выдерживает + секундный STOP, затем сохраняет session в NOINIT RAM 0x50..0x57. Watchdog выполняет reset. + ACK ENTER_BOOT отправляет уже Flash-загрузчик; приложение ничего не стирает. +2. BEGIN_IMAGE/BEGIN_COMPAT проверяют размер, Product, HW и сессию. Неактивный + слот выбирается автоматически; при первом образе это A. ERASE инвалидирует + только его manifest. Полного стирания EEPROM не требуется. +3. Последовательные DATA_A/B записываются с немедленным readback. VERIFY + проверяет потоковый CRC32, CRC32 EEPROM и полную таблицу SCI8. + Разрешена загрузка только в CPU word RAM `[0x8000,0xE000)`: максимум 64 секции, + без пересечений, усечений и хвостовых данных; entry должен попасть в секцию. + Запись в Flash, периферию, стек, RAM загрузчика и handoff запрещена. +4. COMMIT повторно проверяет образ и публикует pending в журнале. Подтверждённый + слот сохраняется. Повтор COMMIT безопасен. REBOOT выполняется после физического + CAN ACK ответа. Потеря ACK/тайм-аут передачи отменяет запрос reset. +5. Перед первым запуском кандидата загрузчик сохраняет attempted=1, затем + проверяет и копирует SCI8 в RAM. Следующий reset без CONFIRM сбрасывает pending + и запускает прежний active. Если прежнего образа нет — остаётся CAN recovery. +6. Новое приложение включает watchdog в начале запуска. До подтверждения + прерывания не кормят watchdog вместо основного цикла. После READY, 100 проходов + main и 5 секунд выполняется автоматический CONFIRM. Неудача сохранения + подтверждения через 10 секунд прекращает кормление watchdog, вызывая откат. + Адресная команда CONFIRM с ненулевой session также разрешена после READY. + CONFIRM в загрузчике и до первого запуска кандидата отклоняется. + +PING, IDENTIFY и паспорт доступны без ENTER_BOOT. QUERY_PROGRESS/ABORT привязаны +к сессии. Сессия в RAM истекает через 30 секунд без подходящих команд/данных. +После reset передачу начинают заново; параметры и подтверждённый образ остаются. +ABORT после COMMIT не отменяет опубликованный pending. Для его пробного запуска +можно открыть новую сессию и отправить REBOOT. Pending запрещено перезаписывать. +Повторы ENTER_BOOT, метаданных, ERASE до первого блока, VERIFY и COMMIT учитывают +потерю ответа. Broadcast никогда не разрешает запись. + +## Сервисные команды + +Запрос: MsgType=9, Body=`session<<8 | command`. IDENTIFY/PING/паспорт не требуют сессии. +Ответ паспорта: MsgType=D, Body=`page<<8 | command`, DLC=8. + +| Код | Имя | DLC запроса | Ответ | +|---:|---|---:|---| +| 01 | IDENTIFY | 0 | Product:u16, HW:u8, protocol:u8, FW:u32; Body=1 | +| 0C | PING | 0 | protocol, state, active, target, session, slot_count, last_status, 0 | +| 0D | CAPABILITIES | 0 | flags:u32, slot_capacity:u32 | +| 0E | DEVICE_NAME | 1 (page) | 8 ASCII-октетов имени; последний фрагмент с NUL | +| 0F | MEMORY_INFO | 1 (page) | Страницы ниже | + +MEMORY_INFO 0: EEPROM bytes:u32, page bytes:u16, mode:u8 (0 app/1 boot), running:u8. +Страница 1: адреса A:u32, B:u32. Страница 2: active, pending, attempted, storage_healthy, +session_timeout_ms:u32. Страница 3: parameters_offset:u32, parameters_bytes:u32. +Слот FF означает отсутствие образа/запущенного приложения. +CAPABILITIES bits 0..11: ping, discovery, identify, name, memory, A/B, confirm, +rollback, separate loader, session timeout, readback, SCI8 validation. +Приложение, запущенное старым SPI ROM без handoff загрузчика, сообщает только +базовые сервисы (0x1F) и отклоняет ENTER_BOOT. + +Обнаружение: MsgType=D, Body=0, DLC=0, Route=0. Адресный запрос или общий +DeviceType=7/DeviceID=15. Ответ IDENTIFY приходит с адреса прибора через +`2*(1+16*DeviceType+DeviceID)` мс. При нескольких одинаковых адресах конфликт +остаётся: каждому физическому устройству нужен уникальный адрес. +Python API: `protocan.can_boot.CanBootServices` — запросы и разбор паспорта; +`CanBootTransfer` — загрузка. Кнопка «Пинг» доступна в режиме ProtoCAN Boot +окна прошивки SETGUI. Для ENTER/VERIFY/COMMIT клиент ждёт до 15 секунд. + +## Аппаратная интеграция + +MBOX3 — адресные входящие Boot; MBOX4 — ответы; MBOX6 — точный broadcast discovery. +MBOX0..2 сохраняют legacy/UMP, MBOX5 остаётся heartbeat. ISR помещает сообщения +в очередь на 31 кадр. Чужие адреса, RTR и неподходящие типы отбрасываются до очереди. +При переполнении/потере mailbox сессия отменяется; повторная запись начинается заново. +SPI-драйвер загрузчика имеет ограничение ожидания байта и WIP, 24-битный адрес, +разбиение по страницам 256 байт и восстановление конфигурации SPI приложения. +Он работает только при остановленном Timer2. Обычный Seeprom_write приложения +ограничен областью параметров; старый RS EXTEND 0x44 запрещён для A/B. + +Загрузчик исполняется из внутренней Flash A 0x338000..0x33FF7F, entry 0x33FFF6. +Его RAM — 0xE000..0xFFFF; стек — 0x400..0x7FF. Приложение M95M02 использует +0x8000..0xD7FF для кода, 0xD800..0xDFFF для констант/инициализации, +0xE000..0xFFFF для .ebss после перехода в C runtime. Linker резервирует +NOINIT handoff в обоих проектах. CAN не содержит операций записи внутренней Flash. + +## Первичная установка + +1. Собрать приложение с EEPROM_M95M02=1 и обновлённым F28335.cmd; получить SCI8 BIN. + Старые BIN/OUT автоматически не заменены. Приложение должно содержать интеграцию + READY/CONFIRM, иначе на следующем reset оно останется неподтверждённым кандидатом. +2. Собрать отдельный загрузчик TI C2000 COFF compiler: + `Bootloader/build.ps1 -CompilerRoot <путь-к-ti-cgt-c2000>`. + Результат — `Bootloader/build/F28335_CAN_AB.out` и MAP. +3. Один раз установить этот OUT через JTAG и выбрать аппаратный Flash boot + согласно схеме платы. Не загружать поверх него старый application OUT с + секцией codestart во Flash. +4. После reset пустая EEPROM или старая разметка автоматически ведут в CAN recovery. + Передать новое приложение через ProtoCAN Boot, дождаться пробного запуска и + проверить MEMORY_INFO page 2: pending=FF, active=A. Затем проверить обновление в B. + +Старая SPI-ROM разметка с AA08 по адресу 0 несовместима с новым журналом. +Первое обновление заменяет её; параметры по прежнему адресу сохраняются. +Загрузчик не выполняет скрытую миграцию и не меняет boot pins программно. + +## Проверки и границы проверенного + +`python c/protocan-boot/tests/run_tests.py` (переменная CC — clang/gcc) проверяет +ядро, приложение/загрузчик, wide-octet режим, журнал, обрыв публикации на каждом +из 69 возможных положений, пробный запуск, CONFIRM, откат, SCI8 и границы памяти. +Python-тесты проверяют клиентские запросы, паспорт, фильтрацию ответов и лимит A/B. +Проверка синтаксиса использует настоящие DSP2833x headers. Сборка TI/CCS, +раскладка итогового MAP и физические CAN/SPI/reset на плате ещё не подтверждены. + +CRC32 защищает от повреждения, криптографической подписи и anti-rollback по +версии в этом порту нет. Удалённый CONFIRM подтверждает готовность приложения, +но не заменяет проверку датчиков/силовой части на стенде. + +Аппаратный формат SPI boot описан в [TI Boot ROM Reference Guide](https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/171/2742.Boot-ROM.pdf). +В A/B-схеме ROM выбирает Flash, а разбор EEPROM выполняет этот загрузчик. diff --git a/c/protocan-boot/ports/tms320f28335/f28335_boot_hw.c b/c/protocan-boot/ports/tms320f28335/f28335_boot_hw.c new file mode 100644 index 0000000..e3f7aa1 --- /dev/null +++ b/c/protocan-boot/ports/tms320f28335/f28335_boot_hw.c @@ -0,0 +1,181 @@ +/* Shared by the application and the separate Flash bootloader. */ +/* Аппаратный слой: общая RAM между reset, watchdog и SPI EEPROM. + * Один исходник компилируется и в приложение, и в отдельный Flash-загрузчик. + * В отличие от store.c этот файл требует настоящих регистров F28335. */ +#include "DSP2833x_Device.h" +#include "f28335_boot_platform.h" +#include "f28335_can_boot.h" + +/* Must be NOINIT in BOTH linkers, outside ROM scratch and application payload. */ +#pragma DATA_SECTION(boot_handoff,".boot_shared") +/* 8 слов NOINIT по адресу 0x50, одинаково зарезервированных обоими linker-файлами. + * [0..1] magic; [2] запрошенная session; [3] запущенный слот; [4..6] служебные + * поля; [7] XOR-контроль. Это кратковременная передача контекста через watchdog + * reset, а не замена энергонезависимого журнала EEPROM. */ +static volatile unsigned int boot_handoff[8]; +/* Проверяем magic и XOR прежде, чем трактовать RAM как запрос входа в boot. + * После включения питания содержимое NOINIT может быть произвольным. */ +static unsigned int handoff_valid(void) +{ + unsigned int i,sum=0; + for(i=0;i<8;++i) sum^=boot_handoff[i]; + return boot_handoff[0]==0xB283 && boot_handoff[1]==0x4D7C && sum==0xA95B; +} +/* Сначала снимаем признак действительности, затем заполняем поля и контроль. + * Первый magic записываем последним; volatile сохраняет обращения к общей RAM. + * request=0 означает обычный запуск, ненулевая session — явный ENTER_BOOT. */ +static void handoff_set(unsigned int request,unsigned int running) +{ + unsigned int i,sum=0; + boot_handoff[0]=0; boot_handoff[1]=0x4D7C; + boot_handoff[2]=request; boot_handoff[3]=running; + boot_handoff[4]=0xF335; boot_handoff[5]=0x0CCA; boot_handoff[6]=1; + sum=0xB283; + for(i=1;i<7;++i) sum^=boot_handoff[i]; + boot_handoff[7]=sum^0xA95B; + boot_handoff[0]=0xB283; /* publish last */ +} +/* Приложение узнаёт свой слот из handoff, а не предполагает, что оно active: + * во время пробного запуска выполняется pending, active ещё указывает на старый образ. */ +unsigned int F28335Boot_RunningSlot(void) +{ return handoff_valid()?boot_handoff[3]:F28335_AB_NONE; } +unsigned int F28335Boot_RequestedSession(void) +{ return handoff_valid() && boot_handoff[2]<256?boot_handoff[2]:0; } +/* Одновременно публикуем выбранный слот и потребляем запрос ENTER_BOOT: + * он не должен повторяться при следующем обычном reset. */ +void F28335Boot_SetRunning(unsigned int slot) { handoff_set(0,slot); } +/* Timer1 должен давать 1 мс на InterruptCount и в приложении, и в загрузчике. + * Тайм-ауты используют разность unsigned-счётчиков для переноса через wrap. */ +uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; } +/* Вызвать сразу после InitSysCtrl, который штатно отключает watchdog. + * Сбрасываем разрешение кормления из ISR до полной инициализации CAN-сервиса. + * Для приложения, запущенного нашим загрузчиком, включаем watchdog с делителем. */ +void F28335Boot_StartupWatchdog(void) +{ + F28335CanBoot_EarlyInit(); + if(F28335Boot_RunningSlot()<2) { + EALLOW; SysCtrlRegs.WDKEY=0x55; SysCtrlRegs.WDKEY=0xAA; + SysCtrlRegs.WDCR=0x002F; EDIS; + } +} +/* Платформенный Quiesce останавливает управление выходами перед reset. + * Session сохраняется до включения watchdog; ненулевую session загрузчик + * использует для отложенного ACK ENTER_BOOT. В бесконечном цикле watchdog + * намеренно не обслуживается — выход из функции должен произойти через reset. */ +void F28335Boot_Reset(unsigned int session) +{ + F28335Boot_Quiesce(); + DINT; + handoff_set(session,F28335_AB_NONE); + EALLOW; SysCtrlRegs.WDCR=0x0028; EDIS; + for(;;) { } +} + +/* Main context only. Legacy Timer2 EEPROM driver must be idle/stopped. Save + * its SPI configuration: it changes the character width during transactions. */ +typedef struct { unsigned int ccr,ctl,brr,tx,rx,ct,pri; } boot_spi_saved; +static void spi_cs(unsigned int high) +{ if(high) GpioDataRegs.GPASET.bit.GPIO19=1; else GpioDataRegs.GPACLEAR.bit.GPIO19=1; } +/* Доступ разрешён только из main при остановленном Timer2 старого SPI-драйвера. + * Это проверка занятости в однопоточном приложении, не универсальный mutex. + * Сохраняем режим SPI, поскольку штатный драйвер меняет разрядность передачи. + * CS сначала поднимаем, затем переключаем SPI на 8-битные транзакции. */ +static bool spi_take(boot_spi_saved *s) +{ + volatile unsigned int discard; + if(!CpuTimer2Regs.TCR.bit.TSS) return false; + s->ccr=SpiaRegs.SPICCR.all; s->ctl=SpiaRegs.SPICTL.all; s->brr=SpiaRegs.SPIBRR; + s->tx=SpiaRegs.SPIFFTX.all; s->rx=SpiaRegs.SPIFFRX.all; + s->ct=SpiaRegs.SPIFFCT.all; s->pri=SpiaRegs.SPIPRI.all; + EALLOW; + GpioCtrlRegs.GPAMUX2.bit.GPIO16=1; GpioCtrlRegs.GPAMUX2.bit.GPIO17=1; + GpioCtrlRegs.GPAMUX2.bit.GPIO18=1; GpioCtrlRegs.GPAMUX2.bit.GPIO19=0; + GpioDataRegs.GPASET.bit.GPIO19=1; GpioCtrlRegs.GPADIR.bit.GPIO19=1; + GpioCtrlRegs.GPAQSEL2.bit.GPIO17=3; + EDIS; + SpiaRegs.SPICCR.all=7; SpiaRegs.SPICTL.all=0x0E; /* master, phase=1, IRQ off */ + SpiaRegs.SPIBRR=CLKMULT*6+1; + SpiaRegs.SPIFFTX.all=0x8000; SpiaRegs.SPIFFRX.all=0; SpiaRegs.SPIFFCT.all=0; + SpiaRegs.SPIPRI.all=0x10; SpiaRegs.SPICCR.all=0x87; +/* Снимаем оставшийся RX/INT_FLAG от прежней операции, чтобы первый обмен + * не принял старое содержимое RXBUF за ответ EEPROM. */ + discard=SpiaRegs.SPIRXBUF; (void)discard; + return true; +} +/* Выход и при успехе, и при ошибке: CS не должен остаться активным. + * Настройки возвращаются при удержании SPI в reset; исходный SPISWRESET + * восстанавливается последним, после остальных регистров. */ +static void spi_release(const boot_spi_saved *s) +{ + volatile unsigned int discard; + spi_cs(1); discard=SpiaRegs.SPIRXBUF; (void)discard; + SpiaRegs.SPICCR.all=s->ccr&0x7FU; SpiaRegs.SPICTL.all=s->ctl; + SpiaRegs.SPIBRR=s->brr; SpiaRegs.SPIFFTX.all=s->tx; SpiaRegs.SPIFFRX.all=s->rx; + SpiaRegs.SPIFFCT.all=s->ct; SpiaRegs.SPIPRI.all=s->pri; SpiaRegs.SPICCR.all=s->ccr; +} +/* У F28335 8-битный символ для передачи выравнивается в старшие биты TXBUF. + * Полученный октет читается из младших битов RXBUF. Счётчик budget ограничивает + * ожидание даже при неработающем Timer1; это число итераций, а не миллисекунды. */ +static bool spi_octet(unsigned int tx,unsigned int *rx) +{ + uint32_t budget=100000UL; + SpiaRegs.SPITXBUF=(tx&255U)<<8; + while(!SpiaRegs.SPISTS.bit.INT_FLAG) if(--budget==0) return false; + *rx=SpiaRegs.SPIRXBUF&255U; return true; +} +/* Команда EEPROM и 24-битный байтовый адрес, старший октет адреса первым. + * CS удерживается вызывающей функцией на всю команду и полезные данные. */ +static bool spi_prefix(unsigned int command,uint32_t address) +{ + unsigned int discard; + return spi_octet(command,&discard) && spi_octet((unsigned int)(address>>16),&discard) && + spi_octet((unsigned int)(address>>8),&discard) && spi_octet((unsigned int)address,&discard); +} +/* RDSR (05h): WIP в бите 0 означает незавершённую внутреннюю запись EEPROM. + * Ожидание ограничено и 50 мс, и числом опросов на случай остановки tick. + * После каждого чтения статуса CS снимается, в том числе при ошибке SPI. */ +static bool spi_ready(void) +{ + unsigned int status,discard; uint32_t started=F28335Boot_Now(),budget=20000UL; + do { + spi_cs(0); + if(!spi_octet(5,&discard) || !spi_octet(255,&status)) { spi_cs(1); return false; } + spi_cs(1); + if(!(status&1U)) return true; + } while(--budget && (uint32_t)(F28335Boot_Now()-started)<50UL); + return false; +} +/* Читать разрешено всю физическую EEPROM, включая параметры. Проверка + * через вычитание защищает от переполнения адреса. spi_release обязателен + * после успешного захвата SPI независимо от результата последующего обмена. */ +bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n) +{ + boot_spi_saved saved; unsigned int i,value; bool ok; (void)u; + if(address>0x40000UL || n>0x40000UL-address || !spi_take(&saved)) return false; + ok=spi_ready(); + spi_cs(0); if(ok) ok=spi_prefix(3,address); + for(i=0;ok && i0x3FE00UL || n>0x3FE00UL-address || !spi_take(&saved)) return false; + ok=spi_ready(); + while(ok && n) { +/* Нельзя пересекать страницу одной командой WRITE: EEPROM может завернуть + * адрес внутри страницы. Для каждого фрагмента заново выдаём WREN (06h), + * WRITE (02h) и ждём снятия WIP. Сравнение readback выполняет верхний слой. */ + chunk=256U-(unsigned int)(address&255U); if(chunk>n) chunk=n; + spi_cs(0); ok=spi_octet(6,&discard); spi_cs(1); /* WREN */ + spi_cs(0); if(ok) ok=spi_prefix(2,address); + for(i=0;ok && i + +/* Формат журнала фиксирован: u32 little endian. Нельзя сохранять C-структуру + * через memcpy: выравнивание и sizeof на C28x отличаются от обычного ПК. */ +static uint32_t bs_get32(const pcan_boot_byte_t *p) +{ return (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24); } +static void bs_put32(pcan_boot_byte_t *p, uint32_t v) +{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(8*i))&255U); } +/* Потоковый CRC32: первый вызов получает crc=0, следующие — предыдущий результат. + * Внутренняя инверсия позволяет считать один CRC по нескольким порциям EEPROM. + * Маска 255 исключает старшие биты 16-битного представления октета на C28x. + * Это проверка повреждения данных, а не криптографическая подпись. */ +uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n) +{ unsigned int i,b; crc=~crc; for(i=0;i>1)^((crc&1U)?0xEDB88320UL:0); } return ~crc; } +static uint32_t bs_base(unsigned int slot) { return slot?F28335_AB_SLOT_B:F28335_AB_SLOT_A; } +/* Принимаем запись только при совпадении magic, версии, marker и CRC. + * Дополнительно проверяем смысл номеров слотов: pending не может быть active. + * Наличие правильного marker без правильного CRC не разрешает запуск. */ +static bool bs_record_valid(const pcan_boot_byte_t *p) +{ + return bs_get32(p)==0x32424146UL && bs_get32(p+4)==1 && + bs_get32(p+60)==0x51AFC03DUL && bs_get32(p+56)==f28335_boot_crc(0,p,56) && + (p[12]<2 || p[12]==F28335_AB_NONE) && + (p[13]<2 || p[13]==F28335_AB_NONE) && p[14]<=1 && + (p[13]==F28335_AB_NONE || p[13]!=p[12]); +} +/* Карта записи, смещения в октетах: + * 0..3 magic FAB2; 4..7 версия; 8..11 sequence; 12 active; 13 pending; + * 14 attempted; 15 резерв; 16..35 manifest A; 36..55 manifest B; + * 56..59 CRC первых 56 октетов; 60..63 marker завершённой публикации. + * Manifest: size, CRC, FW (по u32), Product (u16), HW min/max, 4 октета резерва. */ +static void bs_decode(f28335_boot_store_t *s, const pcan_boot_byte_t *p) +{ + unsigned int i; s->sequence=bs_get32(p+8); s->active=p[12]; + s->pending=p[13]; s->attempted=p[14]; + for(i=0;i<2;++i) { + const pcan_boot_byte_t *q=p+16+i*20; + s->image[i].image_size=bs_get32(q); s->image[i].image_crc32=bs_get32(q+4); + s->image[i].firmware_version=bs_get32(q+8); + s->image[i].product_type=(uint16_t)(q[12]|(q[13]<<8)); + s->image[i].hardware_revision_min=q[14]; s->image[i].hardware_revision_max=q[15]; + } +} +/* Читаем обе копии журнала. Нераспознанная разметка допускает CAN recovery + * с active=NONE; ошибка чтения запрещает запись, поскольку реальный active неизвестен. + * Эта функция не преобразует старый SCI8 по адресу 0 в новую A/B-разметку. */ +bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io) +{ + pcan_boot_byte_t a[64],b[64]; bool va,vb; + if(!s || !io || !io->read || !io->write) return false; + memset(s,0,sizeof(*s)); s->io=*io; s->active=s->pending=F28335_AB_NONE; + if(!io->read(io->user,0,a,64) || !io->read(io->user,256,b,64)) { s->io_fault=true; return false; } + va=bs_record_valid(a); vb=bs_record_valid(b); + if(!va && !vb) return true; +/* Сравнение sequence по модулю 2^32: разность менее половины диапазона + * считается более новой записью. При равенстве выбирается страница 1. + * Соседние публикации отличаются на единицу, в том числе после переполнения. */ + s->page=vb && (!va || (uint32_t)(bs_get32(b+8)-bs_get32(a+8))<0x80000000UL)?1:0; + bs_decode(s,s->page?b:a); s->valid=true; return true; +} +/* Публикация выполняется в другую страницу, текущую действующую не трогаем. + * Порядок принципиален: обнулить marker -> проверить -> записать тело -> + * проверить тело -> записать marker последним -> проверить всю запись. + * При обрыве питания загрузчик выбирает прежнюю либо полностью новую запись. + * Каждый memcmp учитывает sizeof элемента, поскольку элемент здесь — октет + * протокола, но на C28x занимает адресуемое 16-битное слово. */ +static bool bs_save(f28335_boot_store_t *s) +{ + pcan_boot_byte_t p[64],check[64],invalid[4]={0,0,0,0}; unsigned int i; + unsigned int page=s->valid?(s->page^1U):0; + uint32_t addr=(uint32_t)page*256; + memset(p,0,sizeof(p)); bs_put32(p,0x32424146UL); bs_put32(p+4,1); + bs_put32(p+8,s->sequence+1); p[12]=s->active; p[13]=s->pending; p[14]=s->attempted; + for(i=0;i<2;++i) { + pcan_boot_byte_t *q=p+16+i*20; + bs_put32(q,s->image[i].image_size); bs_put32(q+4,s->image[i].image_crc32); + bs_put32(q+8,s->image[i].firmware_version); + q[12]=s->image[i].product_type&255U; q[13]=(s->image[i].product_type>>8)&255U; + q[14]=s->image[i].hardware_revision_min; q[15]=s->image[i].hardware_revision_max; + } + bs_put32(p+56,f28335_boot_crc(0,p,56)); bs_put32(p+60,0x51AFC03DUL); + /* The old page is never erased. A torn write cannot publish a partial record. */ + if(!s->io.write(s->io.user,addr+60,invalid,4) || + !s->io.read(s->io.user,addr+60,check,4) || memcmp(check,invalid,sizeof(invalid)) || + !s->io.write(s->io.user,addr,p,60) || + !s->io.read(s->io.user,addr,check,60) || memcmp(p,check,60*sizeof(*p)) || + !s->io.write(s->io.user,addr+60,p+60,4) || + !s->io.read(s->io.user,addr,check,64) || memcmp(p,check,sizeof(p))) return false; + s->page=page; s->sequence++; s->valid=true; return true; +} +/* Изменения сначала готовятся в копии состояния next. Рабочее состояние + * заменяем только после публикации. Если запись/контрольное чтение сорвались, + * перечитываем журнал: marker мог успеть записаться до ошибки readback. + * После неудачи чтения io_fault блокирует дальнейшие изменения хранилища. */ +static bool bs_publish(f28335_boot_store_t *s, f28335_boot_store_t *next) +{ + if(bs_save(next)) { *s=*next; return true; } + /* A failed final read may still have committed: reload before any more writes. */ + { f28335_boot_io_t io=s->io; if(!f28335_boot_open(s,&io)) { + s->valid=false; s->io_fault=true; return false; } } + return false; +} +/* Логический ERASE: обнуляем manifest неактивного слота в журнале. + * Саму EEPROM стирать целиком не требуется — каждый байт будет перезаписан. + * Active защищён всегда; наличие pending запрещает начало новой записи, + * пока кандидат не подтверждён либо не снят при следующем выборе загрузки. */ +bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot) +{ + f28335_boot_store_t next=*s; + if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE) return false; + memset(&next.image[slot],0,sizeof(next.image[slot])); + return bs_publish(s,&next); +} +/* Записываем не более одного CAN-блока и немедленно сравниваем прочитанное. + * Проверка n <= capacity-off выполняется после off <= capacity: это исключает + * переполнение суммы off+n и выход из слота в соседний слот или параметры. */ +bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t off, + const pcan_boot_byte_t *p, unsigned int n) +{ + pcan_boot_byte_t check[8]; + if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE || !n || n>8 || + off>F28335_AB_SLOT_BYTES || n>F28335_AB_SLOT_BYTES-off) return false; + return s->io.write(s->io.user,bs_base(slot)+off,p,n) && + s->io.read(s->io.user,bs_base(slot)+off,check,n) && !memcmp(check,p,n*sizeof(*p)); +} +/* Слово SCI8 занимает два октета файла, младший октет идёт первым. + * Смещение увеличиваем только после успешного чтения; усечённый файл отклоняем. */ +static bool bs_u16(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint16_t *v) +{ + pcan_boot_byte_t p[2]; if(*off>size || size-*off<2 || + !s->io.read(s->io.user,base+*off,p,2)) return false; + *v=(uint16_t)(p[0]|(p[1]<<8)); *off+=2; return true; +} +/* Особенность таблицы TI: адрес u32 передаётся старшим словом вперёд, + * при этом внутри каждого слова остаётся little endian. Не заменять на get32. */ +static bool bs_addr(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint32_t *v) +{ + uint16_t hi,lo; if(!bs_u16(s,base,size,off,&hi) || !bs_u16(s,base,size,off,&lo)) return false; + *v=((uint32_t)hi<<16)|lo; return true; +} +/* The complete table is validated before any application RAM is written. + * Loader RAM is E000..FFFF and M0/M1. Flash, peripherals and XINTF are forbidden. */ +/* writer=NULL: только разбираем и проверяем таблицу, RAM приложения не меняем. + * writer задан: повторно проверяем таблицу и переносим слова в разрешённую RAM. + * Массивы starts/ends ограничены MAX_SECTIONS; они нужны для обнаружения + * пересекающихся секций, в том числе секций с разным порядком адресов. */ +static bool bs_table(f28335_boot_store_t *s,unsigned int slot,uint32_t size, + f28335_boot_ram_writer writer,void *user,uint32_t *entry) +{ + uint32_t starts[F28335_AB_MAX_SECTIONS],ends[F28335_AB_MAX_SECTIONS]; + uint32_t off=0,addr,end,base=bs_base(slot); unsigned int sections=0,i; + uint16_t count,word; bool entry_loaded=false; + if(!bs_u16(s,base,size,&off,&word) || word!=0x08AA) return false; +/* После ключа AA 08 пропускаем 16 резервных октетов; далее расположен entry. + * Нулевой count завершает таблицу строго на конце файла, без хвостовых данных. */ + off=18; if(!bs_addr(s,base,size,&off,entry)) return false; + for(;;) { + if(!bs_u16(s,base,size,&off,&count)) return false; + if(!count) return off==size && entry_loaded; + if(sections==F28335_AB_MAX_SECTIONS || !bs_addr(s,base,size,&off,&addr)) return false; + end=addr+count; +/* Разрешён только полуинтервал RAM [0x8000,0xE000) в словах CPU. + * Верхняя граница защищает RAM работающего загрузчика; нижняя — стек, + * handoff и регистры. Проверка размера payload использует уже октеты файла. */ + if(addr<0x8000UL || end>0xE000UL || end<=addr || + off>size || (uint32_t)count*2>size-off) return false; + for(i=0;istarts[i]) return false; + starts[sections]=addr; ends[sections++]=end; + if(*entry>=addr && *entry1 || m->image_size<24 || m->image_size>F28335_AB_SLOT_BYTES || + (m->image_size&1) || m->product_type!=0x2833 || + m->hardware_revision_min>1 || m->hardware_revision_max<1) return false; + for(off=0;offimage_size;off+=n) { + n=(m->image_size-off<32)?(unsigned int)(m->image_size-off):32; + if(!s->io.read(s->io.user,bs_base(slot)+off,p,n)) return false; + crc=f28335_boot_crc(crc,p,n); + } + if(crc!=m->image_crc32 || !bs_table(s,slot,m->image_size,0,0,&local_entry)) return false; + if(entry) *entry=local_entry; + return true; +} +/* COMMIT публикует проверенный образ как pending, не меняя active. + * Повторная проверка EEPROM непосредственно перед публикацией не позволяет + * назначить кандидатом образ, испорченный после предыдущего VERIFY. */ +bool f28335_boot_commit(f28335_boot_store_t *s,unsigned int slot,const pcan_boot_manifest_t *m) +{ + f28335_boot_store_t next=*s; + if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE || + !f28335_boot_verify(s,slot,m,0)) return false; + next.image[slot]=*m; next.pending=slot; next.attempted=0; + return bs_publish(s,&next); +} +/* Подтверждать можно только действительно выбранный для пробного запуска слот. + * Требование attempted исключает подтверждение ещё ни разу не запускавшегося + * кандидата. Повтор CONFIRM уже активного слота возвращает успех без записи. */ +bool f28335_boot_confirm(f28335_boot_store_t *s,unsigned int running_slot) +{ + f28335_boot_store_t next=*s; + if(s->io_fault || running_slot>1) return false; + if(s->pending==F28335_AB_NONE) return s->valid && s->active==running_slot; + if(s->pending!=running_slot || !s->attempted) return false; + next.active=running_slot; next.pending=F28335_AB_NONE; next.attempted=0; + return bs_publish(s,&next); +} +/* Выбор выполняется при старте загрузчика. attempted сохраняется ДО перехода + * в приложение: после зависания, watchdog reset или потери питания следующий + * старт уже знает о незавершённой пробе и возвращает прежний active. + * Успех функции с slot=NONE означает recovery, а не разрешение перехода в RAM. */ +bool f28335_boot_select(f28335_boot_store_t *s,unsigned int *slot) +{ + f28335_boot_store_t next=*s; *slot=F28335_AB_NONE; + if(s->io_fault) return false; + if(s->pending!=F28335_AB_NONE) { + if(!s->attempted && f28335_boot_verify(s,s->pending,&s->image[s->pending],0)) { + next.attempted=1; + if(!bs_publish(s,&next)) return false; + *slot=s->pending; return true; + } +/* Кандидат уже запускался без подтверждения либо не прошёл проверку. + * Снимаем pending отдельной публикацией; подтверждённый active остаётся прежним. */ + next.pending=F28335_AB_NONE; next.attempted=0; + if(!bs_publish(s,&next)) return false; + } + if(s->active<2 && f28335_boot_verify(s,s->active,&s->image[s->active],0)) *slot=s->active; + return true; +} +/* Сначала полная проверка CRC и всех адресов без записи, затем перенос слов. + * Платформа вызывает entry только при true; частичная загрузка при ошибке I/O + * не даёт разрешения на запуск приложения. */ +bool f28335_boot_load(f28335_boot_store_t *s,unsigned int slot, + f28335_boot_ram_writer writer,void *user,uint32_t *entry) +{ + return slot<2 && writer && entry && f28335_boot_verify(s,slot,&s->image[slot],entry) && + bs_table(s,slot,s->image[slot].image_size,writer,user,entry); +} diff --git a/c/protocan-boot/ports/tms320f28335/f28335_boot_store.h b/c/protocan-boot/ports/tms320f28335/f28335_boot_store.h new file mode 100644 index 0000000..f5065a9 --- /dev/null +++ b/c/protocan-boot/ports/tms320f28335/f28335_boot_store.h @@ -0,0 +1,55 @@ +#ifndef F28335_BOOT_STORE_H +#define F28335_BOOT_STORE_H +#include "../../include/pcan_boot.h" + +/* All EEPROM offsets/lengths below are OCTETS, CPU destinations are WORDS. */ +/* Физическая EEPROM 256 КиБ: две страницы журнала по 256 октетов, + * два слота по 130560 октетов. С 0x3FE00 начинается резерв, с 0x3FEFC — + * прежняя область параметров. Константы ниже не являются CPU word-адресами. */ +#define F28335_AB_SLOT_BYTES 0x1FE00UL +#define F28335_AB_SLOT_A 0x00200UL +#define F28335_AB_SLOT_B 0x20000UL +#define F28335_AB_PARAMS 0x3FEFCUL +#define F28335_AB_NONE 0xFFU +#define F28335_AB_RECORD 64U +#define F28335_AB_MAX_SECTIONS 64U + +/* Контракт I/O: address и n измеряются в октетах, независимо от sizeof(char). + * true означает, что выполнена вся операция; короткий обмен должен вернуть false. + * Callbacks вызываются синхронно из main, а user передаётся им без изменений. */ +typedef struct { + bool (*read)(void *, uint32_t, pcan_boot_byte_t *, unsigned int); + bool (*write)(void *, uint32_t, const pcan_boot_byte_t *, unsigned int); + void *user; +} f28335_boot_io_t; +typedef struct { + f28335_boot_io_t io; +/* sequence — номер последней публикации; page — выбранная копия журнала. + * active — подтверждённый слот; pending — кандидат; attempted — уже была проба. + * valid показывает наличие распознанной записи, io_fault запрещает изменения + * после ошибки чтения. Отсутствующий слот обозначается NONE, а не нулём. */ + uint32_t sequence; + unsigned int page, active, pending, attempted; + bool valid, io_fault; + pcan_boot_manifest_t image[2]; +} f28335_boot_store_t; + +uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n); +bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io); +/* Blank/unrecognised EEPROM opens successfully with active=NONE. I/O errors fail. */ +bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot); +bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t offset, + const pcan_boot_byte_t *data, unsigned int n); +bool f28335_boot_verify(f28335_boot_store_t *s, unsigned int slot, + const pcan_boot_manifest_t *m, uint32_t *entry); +bool f28335_boot_commit(f28335_boot_store_t *s, unsigned int slot, + const pcan_boot_manifest_t *m); +bool f28335_boot_confirm(f28335_boot_store_t *s, unsigned int running_slot); +/* Records a trial BEFORE returning it; a second unconfirmed boot rolls back. */ +bool f28335_boot_select(f28335_boot_store_t *s, unsigned int *slot); +/* Запись одного проверенного слова приложения по CPU word-адресу. + * Должна вернуть false при невозможности записи; после отказа запуск запрещён. */ +typedef bool (*f28335_boot_ram_writer)(void *, uint32_t, uint16_t); +bool f28335_boot_load(f28335_boot_store_t *s, unsigned int slot, + f28335_boot_ram_writer write_word, void *user, uint32_t *entry); +#endif diff --git a/c/protocan-boot/ports/tms320f28335/f28335_can_boot.c b/c/protocan-boot/ports/tms320f28335/f28335_can_boot.c new file mode 100644 index 0000000..5cbd7a4 --- /dev/null +++ b/c/protocan-boot/ports/tms320f28335/f28335_can_boot.c @@ -0,0 +1,276 @@ +/* Separate Flash recovery loader and application service adapter. */ +#include "DSP2833x_Device.h" +#include "f28335_can_boot.h" +#if EEPROM_M95M02 +#include "f28335_boot_platform.h" +#include +/* boot хранит текущую CAN-сессию в RAM, store — прочитанное состояние журнала. + * Сброс сессии не удаляет active/pending из EEPROM. */ +static pcan_boot_t boot; +static f28335_boot_store_t store; +/* Кольцевая очередь: ISR производит кадры, main потребляет. Из 32 элементов + * используется 31, чтобы head==tail однозначно означало пустую очередь. + * Долгие операции SPI запрещены внутри обработчика CAN. */ +static volatile unsigned int head,tail,overflow; +static struct { uint32_t id; unsigned int dlc; pcan_boot_byte_t data[8]; } queue[32]; +/* reset_requested только планирует reset; Service ждёт завершения TX. + * ready сообщает об окончании старта приложения, confirmed — о сохранённом + * подтверждении слота. healthy отражает результат открытия журнала при Init. */ +static unsigned int reset_requested,reset_session,ready,healthy,confirmed; +static uint32_t sent_at,ready_at; +static unsigned int main_passes; +static int tx_busy,tx_canceling; +/* Один исходящий mailbox исключает перезапись ещё передаваемого ответа. + * MDL содержит первые четыре октета, MDH — следующие четыре, младшим вперёд. + * Запрос CANTRS ещё не означает успешную доставку: CANTA проверяется в Service. + * Аппаратный CAN ACK не является отдельным прикладным подтверждением от ПК. */ +static bool transmit(void *u, uint32_t id, const pcan_boot_byte_t *d, pcan_boot_byte_t n) +{ + unsigned int i; unsigned long lo=0, hi=0; (void)u; + if (tx_busy) return false; + for(i=0;i1) return false; + if(!f28335_boot_confirm(&store,running)) return false; + boot.config.active_slot=running; confirmed=1; return true; +} +static bool can_reboot(void *u) { (void)u; return healthy; } +static void reboot(void *u) { (void)u; reset_session=0; reset_requested=1; } +/* Контракт enter_boot: 0 — загрузчик готов и ядро может отправить ACK; + * 1 — переход отложен, ACK придёт после reset; 2 — вход запрещён. + * Приложение требует handoff нашего загрузчика и подтверждённый текущий слот, + * чтобы не начать обновление из старого SPI-ROM старта или незавершённой пробы. */ +static unsigned int enter(void *u,pcan_boot_byte_t session) +{ + (void)u; + if(!healthy) return 2; + if(F28335_BOOT_LOADER) return 0; + /* No destructive fallback on boards still starting through the SPI ROM. */ + if(F28335Boot_RunningSlot()>1 || !confirmed) return 2; + reset_session=session; reset_requested=1; return 1; +} +static void info32(pcan_boot_byte_t *p,uint32_t v) +{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(i*8))&255U); } +/* Паспорт помещается в classic CAN по страницам из восьми октетов. + * NAME возвращает фрагменты строки с завершающим NUL; неизвестная страница + * отклоняется. MEMORY: 0 — ёмкость/страница/режим/running, 1 — базы A/B, + * 2 — active/pending/attempted/healthy/тайм-аут, 3 — область параметров. */ +static bool info(void *u,pcan_boot_byte_t command,pcan_boot_byte_t page,pcan_boot_byte_t *out) +{ + static const char name[]=F28335_CAN_BOOT_DEVICE_NAME; + unsigned int i,index; (void)u; + if(command==PCAN_BOOT_CMD_CAPABILITIES && page==0) { + /* bits: ping, discovery, identity, name, memory, A/B, confirm, rollback, + * separate loader, session timeout, readback, SCI8 validation. */ + info32(out,(F28335_BOOT_LOADER || F28335Boot_RunningSlot()<2)?0xFFFUL:0x1FUL); + info32(out+4,F28335_AB_SLOT_BYTES); return true; + } + if(command==PCAN_BOOT_CMD_DEVICE_NAME) { + if((unsigned int)page*8U>=sizeof(name)) return false; + for(i=0;i<8;++i) { index=(unsigned int)page*8U+i; + out[i]=indexMSGCTRL.bit.RTR) { ECanaRegs.CANRMP.all=mask; return; } + id=box->MSGID.all&0x1FFFFFFFUL; + type=pcan_boot_id_msg_type(id); + if(!(box->MSGID.all&0x80000000UL) || pcan_boot_id_route(id)!=0 || + (type!=9 && type!=10 && type!=11 && type!=13) || + !((pcan_boot_id_device_type(id)==F28335_CAN_BOOT_DEVICE_TYPE && + pcan_boot_id_device_id(id)==F28335_CAN_BOOT_DEVICE_ID) || + (type==13 && pcan_boot_id_device_type(id)==7 && pcan_boot_id_device_id(id)==15 && + pcan_boot_id_msg_body(id)==0))) { ECanaRegs.CANRMP.all=mask; return; } + if(next==tail) overflow=1; + else { + queue[head].id=box->MSGID.all&0x1FFFFFFFUL; + queue[head].dlc=box->MSGCTRL.bit.DLC; + lo=box->MDL.all; hi=box->MDH.all; + for(i=0;i<4;++i) { queue[head].data[i]=(lo>>(8U*i))&255U; + queue[head].data[i+4]=(hi>>(8U*i))&255U; } + head=next; + } + ECanaRegs.CANRMP.all=mask; +} + +void F28335CanBoot_RxISR(void) +{ + receive_box(&ECanaMboxes.MBOX3,8UL); + receive_box(&ECanaMboxes.MBOX6,64UL); +} +/* Вызывать после инициализации параметров, датчиков и прикладного режима. + * От этого момента отсчитывается интервал наблюдения перед авто-CONFIRM. */ +void F28335CanBoot_Ready(void) +{ ready=1; ready_at=F28335Boot_Now(); } +/* Используется таймерными ISR приложения. Во время пробы кормление watchdog + * должно доказывать выполнение main: работа одного ISR не доказывает готовность. + * Старый старт без handoff сохраняет прежнюю политику watchdog. */ +unsigned int F28335CanBoot_WatchdogAllowed(void) +{ return F28335Boot_RunningSlot()>1 || confirmed; } +/* Неблокирующий диспетчер относительно ожидания CAN: вызывать в каждом проходе main. + * Сами callbacks проверки/записи EEPROM синхронные. Порядок здесь важен: + * проба приложения -> завершение TX -> reset -> poll -> ошибки RX -> один кадр. */ +void F28335CanBoot_Service(void) +{ + uint32_t now=F28335Boot_Now(); + if(!F28335_BOOT_LOADER && ready && !confirmed && healthy && F28335Boot_RunningSlot()<2) { + /* A trial must keep executing main, not merely a timer ISR. */ +/* После 100 проходов main и 5 секунд пробуем сохранить CONFIRM. + * Если до 10 секунд сохранение не удалось, main перестаёт кормить watchdog, + * что позволяет следующему старту откатиться к подтверждённому образу. */ + if((uint32_t)(now-ready_at)<10000UL) ServiceDog(); + if(main_passes<100) ++main_passes; + if(main_passes==100 && (uint32_t)(now-ready_at)>=5000UL) (void)confirm(0); + } + if(tx_busy) { +/* После CANTRR ждём завершения отмены или запоздавшего ACK, прежде чем + * повторно использовать mailbox. reset_requested к этому моменту уже снят, + * поэтому позднее подтверждение не должно перезапустить устройство. */ + if(tx_canceling) { + if(!((ECanaRegs.CANAA.all|ECanaRegs.CANTA.all)&16UL)) return; + ECanaRegs.CANAA.all=16UL; ECanaRegs.CANTA.all=16UL; + tx_busy=tx_canceling=0; + } else if((uint32_t)(now-sent_at)>250UL) { + /* Cancel reset immediately, even if a late CAN ACK races CANTRR. */ + reset_requested=0; pcan_boot_abort(&boot); tail=head; + tx_canceling=1; ECanaRegs.CANTRR.all=16UL; return; + } else if(ECanaRegs.CANTA.all&16UL) { + ECanaRegs.CANTA.all=16UL; tx_busy=0; + } else return; + } + if(reset_requested) { F28335Boot_Reset(reset_session); return; } + pcan_boot_poll(&boot,now); + if(tx_busy) return; +/* После потери кадра нельзя молча продолжать запись: сбрасываем RAM-сессию + * и накопленную очередь. Подтверждённый образ и опубликованный pending сохраняются. */ + if(overflow || (ECanaRegs.CANRML.all&72UL)) { + overflow=0; ECanaRegs.CANRML.all=72UL; + pcan_boot_abort(&boot); tail=head; return; + } + if(tail!=head) { + pcan_boot_process(&boot,queue[tail].id,queue[tail].data,queue[tail].dlc); + tail=(tail+1U)&31U; + } +} +/* Используем session из NOINIT только для нового ENTER_BOOT после reset. + * Это отправка отложенного ACK готовности, а не восстановление принятых блоков + * или CRC незавершённой передачи: данные сессии через reset не сохраняются. */ +void F28335CanBoot_Resume(unsigned int session) +{ + if(F28335_BOOT_LOADER && session && session<256) + (void)pcan_boot_process(&boot,pcan_boot_make_id(1,0, + F28335_CAN_BOOT_DEVICE_TYPE,F28335_CAN_BOOT_DEVICE_ID,9,(session<<8)|2),0,0); +} +#else +void F28335CanBoot_EarlyInit(void) {} +void F28335CanBoot_Init(void) {} +void F28335CanBoot_RxISR(void) {} +void F28335CanBoot_Service(void) {} +void F28335CanBoot_Ready(void) {} +unsigned int F28335CanBoot_WatchdogAllowed(void) { return 1; } +void F28335CanBoot_Resume(unsigned int session) { (void)session; } +#endif diff --git a/c/protocan-boot/ports/tms320f28335/f28335_can_boot.h b/c/protocan-boot/ports/tms320f28335/f28335_can_boot.h new file mode 100644 index 0000000..17b44db --- /dev/null +++ b/c/protocan-boot/ports/tms320f28335/f28335_can_boot.h @@ -0,0 +1,28 @@ +#ifndef F28335_CAN_BOOT_H +#define F28335_CAN_BOOT_H +#include "../../include/pcan_boot.h" +/* Приложение задаёт версию из firmware_version.h до подключения этого заголовка. + * Формат поля FW: major[31:24], minor[23:16], patch[15:0]. Ноль — запасное + * значение при отсутствии версии сборки, а не прошитая здесь версия устройства. */ +#ifndef F28335_CAN_BOOT_FIRMWARE_VERSION +#define F28335_CAN_BOOT_FIRMWARE_VERSION 0UL /* Board supplies the build identity. */ +#endif +/* Адрес ProtoCAN состоит из Device Type (3 бита) и Device ID (4 бита). + * ProductType — отдельный код совместимости прошивки; это не CAN ID и не RS-адрес. */ +#define F28335_CAN_BOOT_DEVICE_TYPE 3U +#define F28335_CAN_BOOT_DEVICE_ID 7U +#define F28335_CAN_BOOT_PRODUCT 0x2833U +#define F28335_CAN_BOOT_DEVICE_NAME "F28335-M95M02" +/* Порядок интеграции приложения: + * StartupWatchdog/EarlyInit сразу после InitSysCtrl; Init после настройки + * таймера и EEPROM; Ready после старта прикладных подсистем; Service в main. + * RxISR вызывается из CAN ISR, WatchdogAllowed проверяется перед кормлением + * watchdog таймерным ISR. Resume применяется только отдельным загрузчиком. */ +void F28335CanBoot_Init(void); +void F28335CanBoot_EarlyInit(void); +void F28335CanBoot_RxISR(void); +void F28335CanBoot_Service(void); +void F28335CanBoot_Ready(void); +void F28335CanBoot_Resume(unsigned int session); +unsigned int F28335CanBoot_WatchdogAllowed(void); +#endif diff --git a/c/protocan-boot/src/pcan_boot.c b/c/protocan-boot/src/pcan_boot.c index 7b92239..2748d03 100644 --- a/c/protocan-boot/src/pcan_boot.c +++ b/c/protocan-boot/src/pcan_boot.c @@ -1,21 +1,16 @@ -/* - * Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами. - * Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные - * слотов предоставляет платформа. - */ - -#include "pcan_boot.h" +/* ProtoCAN update state machine; platform owns storage and reset. */ +#include "../include/pcan_boot.h" #include #define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL -static uint16_t get_u16(const uint8_t *p) +static uint16_t get_u16(const pcan_boot_byte_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } -static uint32_t get_u32(const uint8_t *p) +static uint32_t get_u32(const pcan_boot_byte_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) @@ -23,26 +18,26 @@ static uint32_t get_u32(const uint8_t *p) | ((uint32_t)p[3] << 24); } -static void put_u16(uint8_t *p, uint16_t value) +static void put_u16(pcan_boot_byte_t *p, uint16_t value) { - p[0] = (uint8_t)value; - p[1] = (uint8_t)(value >> 8); + p[0] = (pcan_boot_byte_t)(value & 0xFFU); + p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU); } -static void put_u32(uint8_t *p, uint32_t value) +static void put_u32(pcan_boot_byte_t *p, uint32_t value) { - p[0] = (uint8_t)value; - p[1] = (uint8_t)(value >> 8); - p[2] = (uint8_t)(value >> 16); - p[3] = (uint8_t)(value >> 24); + p[0] = (pcan_boot_byte_t)(value & 0xFFU); + p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU); + p[2] = (pcan_boot_byte_t)((value >> 16) & 0xFFU); + p[3] = (pcan_boot_byte_t)(value >> 24); } -static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length) +static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length) { size_t i; crc = ~crc; for (i = 0U; i < length; ++i) { - uint8_t bit; + pcan_boot_byte_t bit; crc ^= data[i]; for (bit = 0U; bit < 8U; ++bit) { crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U); @@ -51,9 +46,9 @@ static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length) return ~crc; } -uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, - uint8_t device_type, uint8_t device_id, - uint8_t msg_type, uint16_t msg_body) +uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route, + pcan_boot_byte_t device_type, pcan_boot_byte_t device_id, + pcan_boot_byte_t msg_type, uint16_t msg_body) { return ((((uint32_t)priority & 1U) << 28) | (((uint32_t)route & 1U) << 27) @@ -63,14 +58,14 @@ uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, | msg_body) & PCAN_BOOT_ID_MASK; } -uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); } -uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); } -uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); } -uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); } +pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 27) & 1U); } +pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 24) & 7U); } +pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 20) & 15U); } +pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 16) & 15U); } uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; } -static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body, - const uint8_t *data, uint8_t dlc) +static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t body, + const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) { uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE, boot->config.device_type, @@ -79,9 +74,12 @@ static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body, return boot->port.send(boot->port_user, id, data, dlc); } -static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status) +/* STATUS всегда содержит состояние передачи: status, target_slot, next_block, CRC32. + * CRC здесь относится к уже принятому потоку, а не заменяет проверку EEPROM. + * Номер следующего блока помогает клиенту продолжить обмен после потери ACK. */ +static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_byte_t status) { - uint8_t data[8]; + pcan_boot_byte_t data[8]; uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command); data[0] = status; data[1] = boot->target_slot; @@ -91,9 +89,11 @@ static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status) return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U); } +/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен + * без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */ static bool send_identity(pcan_boot_t *boot) { - uint8_t data[8]; + pcan_boot_byte_t data[8]; put_u16(&data[0], boot->config.product_type); data[2] = boot->config.hardware_revision; data[3] = PCAN_BOOT_VERSION; @@ -101,6 +101,9 @@ static bool send_identity(pcan_boot_t *boot) return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U); } +/* Отменяется только состояние протокола в RAM: manifest, счётчики и session. + * Функция не стирает EEPROM и не снимает ранее опубликованный pending. + * Ответственность за постоянное состояние слотов остаётся у платформы. */ void pcan_boot_abort(pcan_boot_t *boot) { if (boot == NULL) { @@ -118,19 +121,24 @@ void pcan_boot_abort(pcan_boot_t *boot) boot->have_compat = false; } +/* Конфигурация копируется в экземпляр; указатели callbacks остаются привязаны + * к port_user. Значения slot_count=0 и max_image_size=0 сохраняют исторические + * настройки по умолчанию. Для пустого A/B-хранилища допустим active_slot=NONE. */ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config, const pcan_boot_port_t *port, void *port_user) { - uint8_t slot_count; + pcan_boot_byte_t slot_count; if ((boot == NULL) || (config == NULL) || (port == NULL) || (config->device_type > 7U) || (config->device_id > 15U) - || (config->active_slot > 1U) + || ((config->active_slot > 1U) && (config->active_slot != PCAN_BOOT_SLOT_NONE)) || (port->send == NULL) || (port->erase_slot == NULL) || (port->write_slot == NULL)) { return false; } slot_count = config->slot_count == 0U ? 2U : config->slot_count; - if ((slot_count > 2U) || (config->active_slot >= slot_count) + if ((slot_count > 2U) || ((config->active_slot >= slot_count) && + (config->active_slot != PCAN_BOOT_SLOT_NONE || slot_count != 2U)) + || config->max_image_size > PCAN_BOOT_SLOT_SIZE || ((slot_count == 2U) && (port->set_pending_slot == NULL))) { return false; } @@ -149,7 +157,10 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config, return true; } -static bool metadata_complete(pcan_boot_t *boot, uint8_t command) +/* BEGIN_IMAGE и BEGIN_COMPAT приходят отдельными кадрами. До получения обоих + * не разрешаем ERASE. Проверяем размер, Product, HW и необязательную политику + * authorize. В A/B выбираем противоположный active слот; для первого образа — A. */ +static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command) { if (!boot->have_image || !boot->have_compat) { boot->state = PCAN_BOOT_STATE_METADATA; @@ -160,8 +171,11 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command) boot->state = PCAN_BOOT_STATE_FAILED; return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE); } - if ((boot->manifest.product_type != boot->config.product_type) - || (boot->config.hardware_revision < boot->manifest.hardware_revision_min) + if (boot->manifest.product_type != boot->config.product_type) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_DEVICE); + } + if ((boot->config.hardware_revision < boot->manifest.hardware_revision_min) || (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) { boot->state = PCAN_BOOT_STATE_FAILED; return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE); @@ -171,19 +185,51 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command) boot->state = PCAN_BOOT_STATE_FAILED; return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR); } - boot->target_slot = boot->config.slot_count == 1U + boot->target_slot = boot->config.active_slot == PCAN_BOOT_SLOT_NONE ? 0U : + boot->config.slot_count == 1U ? boot->config.active_slot - : (uint8_t)(boot->config.active_slot ^ 1U); + : (pcan_boot_byte_t)(boot->config.active_slot ^ 1U); boot->state = PCAN_BOOT_STATE_READY_TO_ERASE; return send_status(boot, command, PCAN_BOOT_STATUS_OK); } +/* Body состоит из session в старшем октете и command в младшем. + * Сначала проверяем DLC, чтобы обработчики не читали отсутствующие данные. + * Идентификация и паспорт не открывают сессию; CONFIRM обрабатывается отдельно, + * поскольку приложение не должно входить в загрузчик ради подтверждения себя. + * Остальные команды изменяют RAM-автомат только в допустимой сессии/состоянии. */ static bool process_control(pcan_boot_t *boot, uint16_t body, - const uint8_t *data, uint8_t dlc) + const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) { - uint8_t session = (uint8_t)(body >> 8); - uint8_t command = (uint8_t)body; + pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8); + pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU); + if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U : + (command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) { + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE); + } + if (command == PCAN_BOOT_CMD_PING) { + pcan_boot_byte_t reply[8] = {0}; + reply[0] = PCAN_BOOT_VERSION; reply[1] = boot->state; + reply[2] = boot->config.active_slot; reply[3] = boot->target_slot; + reply[4] = boot->session_id; reply[5] = boot->config.slot_count; + reply[6] = boot->last_status; + return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, command, reply, 8); + } + if (command >= PCAN_BOOT_CMD_CAPABILITIES && command <= PCAN_BOOT_CMD_MEMORY_INFO) { + pcan_boot_byte_t reply[8] = {0}; + pcan_boot_byte_t page = dlc ? data[0] : 0; + if (!boot->port.service_info || !boot->port.service_info(boot->port_user,command,page,reply)) + return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_COMMAND); + return send_frame(boot,PCAN_BOOT_MSG_DISCOVERY,(uint16_t)((page<<8)|command),reply,8); + } + if (command == PCAN_BOOT_CMD_CONFIRM) { + pcan_boot_byte_t saved=boot->session_id; bool sent; + boot->session_id=session; + sent=send_status(boot,command,session && boot->port.confirm_running_slot && + boot->port.confirm_running_slot(boot->port_user) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_INVALID_STATE); + boot->session_id=saved; return sent; + } if (command == PCAN_BOOT_CMD_IDENTIFY) { return send_identity(boot); } @@ -191,8 +237,18 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, if (session == 0U) { return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR); } + if (boot->state != PCAN_BOOT_STATE_IDLE && boot->session_id != session) + return send_status(boot,command,PCAN_BOOT_STATUS_BUSY); + if (boot->port.enter_boot) { + unsigned int result=boot->port.enter_boot(boot->port_user,session); + if(result==1) return true; + if(result!=0) return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE); + } + if (boot->session_id==session && boot->state!=PCAN_BOOT_STATE_IDLE) + return send_status(boot,command,PCAN_BOOT_STATUS_OK); pcan_boot_abort(boot); boot->session_id = session; + boot->last_activity_ms=boot->now_ms; boot->state = PCAN_BOOT_STATE_METADATA; return send_status(boot, command, PCAN_BOOT_STATUS_OK); } @@ -200,8 +256,11 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR); } + boot->last_activity_ms=boot->now_ms; switch (command) { case PCAN_BOOT_CMD_BEGIN_IMAGE: + if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.image_size==get_u32(data) && + boot->manifest.image_crc32==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK); if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) { return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); } @@ -211,6 +270,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, return metadata_complete(boot, command); case PCAN_BOOT_CMD_BEGIN_COMPAT: + if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.product_type==get_u16(data) && + boot->manifest.hardware_revision_min==data[2] && boot->manifest.hardware_revision_max==data[3] && + boot->manifest.firmware_version==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK); if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) { return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); } @@ -222,6 +284,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, return metadata_complete(boot, command); case PCAN_BOOT_CMD_ERASE: + if(boot->state==PCAN_BOOT_STATE_RECEIVING && boot->bytes_received==0) + return send_status(boot,command,PCAN_BOOT_STATUS_OK); if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) { return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); } @@ -236,6 +300,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, return send_status(boot, command, PCAN_BOOT_STATUS_OK); case PCAN_BOOT_CMD_VERIFY: + if(boot->state==PCAN_BOOT_STATE_VERIFIED || boot->state==PCAN_BOOT_STATE_COMMITTED) + return send_status(boot,command,PCAN_BOOT_STATUS_OK); if ((boot->state != PCAN_BOOT_STATE_RECEIVING) || (boot->bytes_received != boot->manifest.image_size)) { return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); @@ -244,7 +310,13 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, boot->state = PCAN_BOOT_STATE_FAILED; return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR); } - if ((boot->port.verify_image != NULL) + if (boot->port.verify_status) { + pcan_boot_status_t status=boot->port.verify_status(boot->port_user,boot->target_slot,&boot->manifest); + if(status!=PCAN_BOOT_STATUS_OK) { + boot->state=PCAN_BOOT_STATE_FAILED; return send_status(boot,command,status); + } + } + if (!boot->port.verify_status && (boot->port.verify_image != NULL) && !boot->port.verify_image(boot->port_user, boot->target_slot, &boot->manifest)) { boot->state = PCAN_BOOT_STATE_FAILED; @@ -254,21 +326,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, return send_status(boot, command, PCAN_BOOT_STATUS_OK); case PCAN_BOOT_CMD_COMMIT: + if(boot->state==PCAN_BOOT_STATE_COMMITTED) return send_status(boot,command,PCAN_BOOT_STATUS_OK); if (boot->state != PCAN_BOOT_STATE_VERIFIED) { return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); } - if ((boot->config.slot_count == 2U) + if ((boot->port.set_pending_slot != NULL) && !boot->port.set_pending_slot(boot->port_user, boot->target_slot, &boot->manifest)) { return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); } - return send_status(boot, command, PCAN_BOOT_STATUS_OK); - - case PCAN_BOOT_CMD_CONFIRM: - if ((boot->port.confirm_running_slot == NULL) - || !boot->port.confirm_running_slot(boot->port_user)) { - return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); - } + boot->state=PCAN_BOOT_STATE_COMMITTED; return send_status(boot, command, PCAN_BOOT_STATUS_OK); case PCAN_BOOT_CMD_QUERY_PROGRESS: @@ -280,7 +347,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, return send_status(boot, command, PCAN_BOOT_STATUS_OK); case PCAN_BOOT_CMD_REBOOT: - (void)send_status(boot, command, PCAN_BOOT_STATUS_OK); + if(!boot->port.reboot || (boot->port.can_reboot && !boot->port.can_reboot(boot->port_user))) + return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE); + if(!send_status(boot,command,PCAN_BOOT_STATUS_OK)) return false; if (boot->port.reboot != NULL) { boot->port.reboot(boot->port_user); } @@ -291,11 +360,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, } } -static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block, - const uint8_t *data, uint8_t dlc) +/* DATA не содержит session в Body: все 16 бит заняты номером блока. Поэтому + * кадры данных принимаются только в состоянии RECEIVING и для выбранного A/B. + * При неверном номере ничего не записываем и возвращаем ожидаемый next_block. + * Последний кадр дополняется клиентом до 8 октетов, но запись и CRC учитывают + * только оставшиеся октеты image_size. ACK выдаётся на границе окна или в конце. */ +static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t block, + const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) { - uint8_t expected_type; - uint8_t write_length; + pcan_boot_byte_t expected_type; + pcan_boot_byte_t write_length; uint32_t remaining; if (boot->state != PCAN_BOOT_STATE_RECEIVING) { @@ -309,8 +383,9 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block, if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) { return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR); } + boot->last_activity_ms=boot->now_ms; remaining = boot->manifest.image_size - boot->bytes_received; - write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining + write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (pcan_boot_byte_t)remaining : PCAN_BOOT_BLOCK_SIZE; if ((write_length == 0U) || !boot->port.write_slot(boot->port_user, boot->target_slot, @@ -329,10 +404,14 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block, return true; } +/* Общая точка входа для одного кадра после аппаратной фильтрации. + * Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила + * обновление чужого устройства. Broadcast допускается только для DISCOVERY + * с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, - const uint8_t *data, uint8_t dlc) + const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) { - uint8_t msg_type; + pcan_boot_byte_t msg_type; if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) { return false; } @@ -340,6 +419,17 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) { return false; } + if(msg_type==PCAN_BOOT_MSG_DISCOVERY && pcan_boot_id_route(can_id)==0 && + pcan_boot_id_msg_body(can_id)==0 && dlc==0 && + ((pcan_boot_id_device_type(can_id)==7 && pcan_boot_id_device_id(can_id)==15) || + (pcan_boot_id_device_type(can_id)==boot->config.device_type && + pcan_boot_id_device_id(can_id)==boot->config.device_id))) { + if(!boot->discovery_pending) { + boot->discovery_at=boot->now_ms+2U*(1U+16U*boot->config.device_type+boot->config.device_id); + boot->discovery_pending=true; + } + return true; + } if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM) || (pcan_boot_id_device_type(can_id) != boot->config.device_type) || (pcan_boot_id_device_id(can_id) != boot->config.device_id)) { @@ -353,3 +443,18 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, } return false; } + +/* Обслуживание времени не требует входящего кадра. Регулярно вызывать из main + * с монотонным счётчиком миллисекунд: иначе не истечёт сессия и не выйдет discovery. + * При занятом TX discovery остаётся pending до успешного send_identity. + * Вычитание unsigned учитывает переход счётчика через 0xFFFFFFFF. */ +void pcan_boot_poll(pcan_boot_t *boot,uint32_t now_ms) +{ + if(!boot) return; + boot->now_ms=now_ms; + if(boot->config.session_timeout_ms && boot->state!=PCAN_BOOT_STATE_IDLE && + (uint32_t)(now_ms-boot->last_activity_ms)>=boot->config.session_timeout_ms) + pcan_boot_abort(boot); + if(boot->discovery_pending && (uint32_t)(now_ms-boot->discovery_at)<0x80000000UL && send_identity(boot)) + boot->discovery_pending=false; +} diff --git a/c/protocan-boot/tests/f28335_stubs/DSP2833x_Device.h b/c/protocan-boot/tests/f28335_stubs/DSP2833x_Device.h new file mode 100644 index 0000000..00842d5 --- /dev/null +++ b/c/protocan-boot/tests/f28335_stubs/DSP2833x_Device.h @@ -0,0 +1,13 @@ +#ifndef TEST_DSP_H +#define TEST_DSP_H +#include +#define EALLOW +#define EDIS +struct test_reg { uint32_t all; struct { unsigned DLC, RTR; } bit; }; +struct MBOX { struct test_reg MSGID, MSGCTRL, MDL, MDH; }; +static struct { struct MBOX MBOX3, MBOX4, MBOX6; } ECanaMboxes; +static struct { struct test_reg CANME,CANMD,CANRMP,CANRML,CANMIM,CANTA,CANAA,CANTRS,CANTRR; } ECanaRegs; +static struct { struct test_reg LAM3,LAM6; } ECanaLAMRegs; +static struct { unsigned long InterruptCount; } CpuTimer1; +static struct { unsigned WDCR; } SysCtrlRegs; +#endif diff --git a/c/protocan-boot/tests/f28335_stubs/spise2p.h b/c/protocan-boot/tests/f28335_stubs/spise2p.h new file mode 100644 index 0000000..acb88b5 --- /dev/null +++ b/c/protocan-boot/tests/f28335_stubs/spise2p.h @@ -0,0 +1,7 @@ +#include +#ifndef EEPROM_M95M02 +#define EEPROM_M95M02 1 +#endif +#define BALSAM167_EEPROM_FIRMWARE_BYTES 261884UL +void Seeprom_write(unsigned long word, unsigned int *data, unsigned long bytes); +void Seeprom_read(unsigned long word, unsigned int *data, unsigned long bytes); diff --git a/c/protocan-boot/tests/run_tests.py b/c/protocan-boot/tests/run_tests.py new file mode 100644 index 0000000..5180dea --- /dev/null +++ b/c/protocan-boot/tests/run_tests.py @@ -0,0 +1,27 @@ +"""Host regression suite. CC may point to clang/gcc; this does not replace a TI build.""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[1] +cc = os.environ.get('CC') or shutil.which('clang') or shutil.which('gcc') +if not cc: + raise SystemExit('Set CC to a C99 host compiler') +store = 'ports/tms320f28335/f28335_boot_store.c' +cases = [ + ('core', [], ['src/pcan_boot.c', 'tests/test_pcan_boot.c']), + ('store_native', [], [store, 'tests/test_f28335_boot_store.c']), + ('store_wide', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'tests/test_f28335_boot_store.c']), + ('loader', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']), + ('application', ['-DPCAN_BOOT_WIDE_BYTES', '-DF28335_BOOT_LOADER=0'], + [store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']), +] +with tempfile.TemporaryDirectory(prefix='f28335-ab-') as tmp: + for name, defines, sources in cases: + exe = Path(tmp) / (name + '.exe') + subprocess.run([cc, '-std=c99', '-Wall', '-Wextra', '-Werror', '-Iinclude', + '-Itests/f28335_stubs', *defines, *sources, '-o', str(exe)], cwd=root, check=True) + sample = os.environ.get('F28335_SAMPLE_IMAGE') if name == 'store_wide' else None + subprocess.run([str(exe), *([sample] if sample else [])], check=True, timeout=30) diff --git a/c/protocan-boot/tests/test_f28335_boot_store.c b/c/protocan-boot/tests/test_f28335_boot_store.c new file mode 100644 index 0000000..7c0fe34 --- /dev/null +++ b/c/protocan-boot/tests/test_f28335_boot_store.c @@ -0,0 +1,118 @@ +#define _CRT_SECURE_NO_WARNINGS +#include "../ports/tms320f28335/f28335_boot_store.h" +#include +#include +#include +static unsigned char eeprom[0x40000],backup[0x40000]; +static long budget=-1; +static int read_fail; +static unsigned written_words; +static bool read_mem(void *u,uint32_t addr,pcan_boot_byte_t *p,unsigned int n) +{ unsigned i; (void)u; assert(addr+n<=sizeof(eeprom)); if(read_fail) return false; + for(i=0;i0) --budget; eeprom[addr+i]=(unsigned char)p[i]; } + return true; } +static f28335_boot_io_t io={read_mem,write_mem,0}; +static pcan_boot_byte_t image[40]; +static pcan_boot_manifest_t manifest; +static void make_image(void) +{ + memset(image,0,sizeof(image)); image[0]=0xAA; image[1]=8; + image[20]=0; image[21]=0x80; /* entry=8000, high-word first */ + image[22]=5; image[26]=0; image[27]=0x80; + image[28]=0x34; image[29]=0x12; + memset(&manifest,0,sizeof(manifest)); manifest.product_type=0x2833; + manifest.hardware_revision_min=manifest.hardware_revision_max=1; + manifest.image_size=40; manifest.image_crc32=f28335_boot_crc(0,image,40); + manifest.firmware_version=0x01010002; +} +static void image_to(f28335_boot_store_t *s,unsigned slot) +{ unsigned i; assert(f28335_boot_prepare(s,slot)); + for(i=0;i<40;i+=8) assert(f28335_boot_write(s,slot,i,image+i,8)); } +static bool ram(void *u,uint32_t address,uint16_t word) +{ (void)u; assert(address==0x8000+written_words); if(!written_words) assert(word==0x1234); + ++written_words; return true; } +static void reopen(f28335_boot_store_t *s) { assert(f28335_boot_open(s,&io)); } +int main(int argc,char **argv) +{ + f28335_boot_store_t s; unsigned slot; uint32_t entry; long cut; + memset(eeprom,0xA5,sizeof(eeprom)); make_image(); reopen(&s); + assert(s.active==255 && !s.valid); assert(f28335_boot_select(&s,&slot) && slot==255); + image_to(&s,0); assert(f28335_boot_verify(&s,0,&manifest,&entry) && entry==0x8000); + assert(f28335_boot_commit(&s,0,&manifest)); + assert(!f28335_boot_confirm(&s,0)); /* Cannot confirm an image not yet started. */ + reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.attempted); + assert(f28335_boot_load(&s,slot,ram,0,&entry) && written_words==5); + assert(!f28335_boot_confirm(&s,1)); assert(f28335_boot_confirm(&s,0)); + assert(f28335_boot_confirm(&s,0)); /* idempotent */ + assert(!f28335_boot_prepare(&s,0)); + assert(!f28335_boot_write(&s,0,0,image,8)); + assert(!f28335_boot_write(&s,1,F28335_AB_SLOT_BYTES,image,8)); + image_to(&s,1); memcpy(backup,eeprom,sizeof(eeprom)); + /* Every torn write position in journal publication keeps confirmed A. */ + for(cut=0;cut<=68;++cut) { + memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut; + (void)f28335_boot_commit(&s,1,&manifest); budget=-1; reopen(&s); + assert(s.active==0); assert(f28335_boot_verify(&s,0,&s.image[0],0)); + assert(s.pending==255 || s.pending==1); + } + memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_commit(&s,1,&manifest)); + memcpy(backup,eeprom,sizeof(eeprom)); + /* Torn trial bookkeeping must never select an unrecorded trial. */ + for(cut=0;cut<=68;++cut) { + memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut; + if(f28335_boot_select(&s,&slot)) { assert(slot==1); assert(s.attempted); } + budget=-1; reopen(&s); assert(s.active==0); + } + memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); + assert(f28335_boot_select(&s,&slot) && slot==1); + reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.pending==255); + image_to(&s,1); assert(f28335_boot_commit(&s,1,&manifest)); + assert(f28335_boot_select(&s,&slot) && slot==1); + memcpy(backup,eeprom,sizeof(eeprom)); + for(cut=0;cut<=68;++cut) { + memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut; + (void)f28335_boot_confirm(&s,1); budget=-1; reopen(&s); + assert(s.active==0 || s.active==1); + assert(f28335_boot_select(&s,&slot) && (slot==0 || slot==1)); + } + memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_confirm(&s,1)); + /* CRC correct but malicious SCI8 destination and entry are rejected. */ + image_to(&s,0); image[26]=0; image[27]=0xE0; + manifest.image_crc32=f28335_boot_crc(0,image,40); + image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); + image[27]=0x80; image[21]=0x90; manifest.image_crc32=f28335_boot_crc(0,image,40); + image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); + make_image(); image[22]=1; image[30]=1; image[35]=0x80; + manifest.image_crc32=f28335_boot_crc(0,image,40); + image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* overlapping sections */ + make_image(); image_to(&s,0); manifest.image_size=38; + manifest.image_crc32=f28335_boot_crc(0,image,38); + assert(!f28335_boot_verify(&s,0,&manifest,0)); /* missing terminator */ + make_image(); image[22]=4; manifest.image_crc32=f28335_boot_crc(0,image,40); + image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* trailing octets */ + make_image(); image_to(&s,0); eeprom[F28335_AB_SLOT_A+29]^=1; + assert(!f28335_boot_verify(&s,0,&manifest,0)); + /* Failed EEPROM reads fail closed, including subsequent writes. */ + read_fail=1; assert(!f28335_boot_open(&s,&io)); + assert(!f28335_boot_prepare(&s,0)); assert(!f28335_boot_write(&s,0,0,image,8)); read_fail=0; + for(entry=0x3FE00;entry<0x40000;++entry) assert(eeprom[entry]==0xA5); + if(argc>1) { + FILE *file=fopen(argv[1],"rb"); uint32_t offset,crc=0; + pcan_boot_byte_t bytes[32]; unsigned n; + assert(file); make_image(); + manifest.image_size=(uint32_t)fread(eeprom+F28335_AB_SLOT_A,1,F28335_AB_SLOT_BYTES,file); + assert(!ferror(file) && fgetc(file)==EOF); fclose(file); reopen(&s); + for(offset=0;offset +#include +#include +void ServiceDog(void); +#include "../ports/tms320f28335/f28335_can_boot.c" +static unsigned char memory[0x40000]; +static unsigned int running=255,resets,reset_arg,dog_feeds; +static int fail_write; +void ServiceDog(void) { ++dog_feeds; } +bool F28335Boot_Read(void *u,uint32_t a,pcan_boot_byte_t *p,unsigned int n) +{ unsigned i; (void)u; assert(a+n<=sizeof(memory)); for(i=0;i>(i*8))&255; } +int main(void) +{ + pcan_boot_byte_t image[40]={0xAA,8},meta[8],compat[8]={0x33,0x28,1,1,2,0,1,1}; + unsigned i,slot; pcan_boot_manifest_t m; + memset(memory,0xA5,sizeof(memory)); image[21]=0x80; image[22]=5; image[27]=0x80; + F28335CanBoot_Init(); assert(boot.config.max_image_size==130560 && boot.config.slot_count==2); + if(!F28335_BOOT_LOADER) { + /* SPI-ROM application without Flash-loader handoff cannot erase anything. */ + command(2,0,0); assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE && !reset_requested); + memset(&m,0,sizeof(m));m.image_size=40;m.image_crc32=f28335_boot_crc(0,image,40); + m.product_type=0x2833;m.hardware_revision_min=m.hardware_revision_max=1; + assert(f28335_boot_prepare(&store,0)); + for(i=0;i<40;i+=8)assert(f28335_boot_write(&store,0,i,image+i,8)); + assert(f28335_boot_commit(&store,0,&m)); assert(f28335_boot_select(&store,&slot)); + running=0; F28335CanBoot_Init(); + assert(!F28335CanBoot_WatchdogAllowed()); + command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE); + F28335CanBoot_Ready(); + for(i=0;i<100;++i) F28335CanBoot_Service(); + assert(!confirmed && store.pending==0); + CpuTimer1.InterruptCount=5000; F28335CanBoot_Service(); + assert(confirmed && store.active==0 && dog_feeds); + command(2,0,0); assert(reset_requested && reset_session==1 && boot.state==PCAN_BOOT_STATE_IDLE); + F28335CanBoot_Service();assert(resets==1 && reset_arg==1); + command(1,0,0);assert(ECanaMboxes.MBOX4.MDH.all==0x12345678UL); + } else { + command(2,0,0); put32(meta,40);put32(meta+4,f28335_boot_crc(0,image,40)); + command(3,meta,8);command(4,compat,8);assert(boot.target_slot==0); + command(5,0,0); + for(i=0;i<5;++i) { tx_busy=0; assert(pcan_boot_process(&boot, + pcan_boot_make_id(1,0,3,7,10,i),image+i*8,8)); } + command(6,0,0);assert(boot.state==PCAN_BOOT_STATE_VERIFIED); + command(7,0,0);assert(store.pending==0 && boot.state==PCAN_BOOT_STATE_COMMITTED); + command(7,0,0);assert(boot.last_status==0); + command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE); + command(9,0,0);assert(reset_requested);tx_busy=0; + F28335CanBoot_Service();assert(resets==1 && reset_arg==0); + command(10,0,0);command(2,0,0);command(3,meta,8);command(4,compat,8); + command(5,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_FLASH_ERROR); /* Pending is protected. */ + } + command(12,0,0); assert((ECanaMboxes.MBOX4.MSGID.all&65535)==12); + command(13,0,0); assert(ECanaMboxes.MBOX4.MDH.all==130560); + meta[0]=0;command(14,meta,1); assert(ECanaMboxes.MBOX4.MDL.all==0x33383246UL); /* F283 */ + meta[0]=3;command(15,meta,1);assert(ECanaMboxes.MBOX4.MDL.all==0x3FEFCUL); + assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,6,9,0x102),0,0)); + tx_busy=0; + assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),0,0)); + assert(boot.discovery_pending); pcan_boot_poll(&boot,boot.discovery_at); + assert(!boot.discovery_pending && (ECanaMboxes.MBOX4.MSGID.all&65535)==1); + assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x102),0,0)); + head=tail=0; ECanaRegs.CANRMP.all=8; + ECanaMboxes.MBOX3.MSGID.all=0x80000000UL|pcan_boot_make_id(1,0,3,7,9,12); + ECanaMboxes.MBOX3.MSGCTRL.bit.DLC=0; + F28335CanBoot_RxISR(); assert(head==1 && tail==0); + tx_busy=0; F28335CanBoot_Service(); assert(tail==1); + ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=1; F28335CanBoot_RxISR(); assert(head==1); + ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=0; + overflow=1;tx_busy=0;F28335CanBoot_Service();assert(boot.state==PCAN_BOOT_STATE_IDLE); + /* An ACK arriving after cancellation must never execute a queued reset. */ + tx_busy=1;tx_canceling=0;reset_requested=1;sent_at=0; + CpuTimer1.InterruptCount=300;ECanaRegs.CANTA.all=0;ECanaRegs.CANAA.all=0; + F28335CanBoot_Service();assert(!reset_requested && tx_canceling); + ECanaRegs.CANTA.all=16; F28335CanBoot_Service();assert(!tx_busy && resets==1); + for(i=0x3FE00;i<0x40000;++i)assert(memory[i]==0xA5); + puts(F28335_BOOT_LOADER?"F28335 loader CAN services: OK":"F28335 application handoff/confirmation: OK"); + return 0; +} diff --git a/c/protocan-boot/tests/test_pcan_boot.c b/c/protocan-boot/tests/test_pcan_boot.c index d871412..259dcb9 100644 --- a/c/protocan-boot/tests/test_pcan_boot.c +++ b/c/protocan-boot/tests/test_pcan_boot.c @@ -264,7 +264,7 @@ static void test_rejects_wrong_session_and_compatibility(void) data[3] = 3U; put_u32(&data[4], 1U); assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U)); - assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_HARDWARE); + assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_DEVICE); assert(boot.state == PCAN_BOOT_STATE_FAILED); } @@ -306,8 +306,41 @@ static void test_single_slot_update(void) assert(memcmp(fake.flash[0], image, sizeof(image)) == 0); } +static void test_services_timeout_and_retries(void) +{ + pcan_boot_t boot; fake_t fake; + uint8_t image[8]={0},compat[8]={0}; unsigned count; + setup(&boot,&fake); boot.config.session_timeout_ms=30000; + pcan_boot_poll(&boot,0xFFFFFF00UL); + assert(command(&boot,7,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0)); + assert(command(&boot,8,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0)); + assert(boot.session_id==7 && fake.last_data[0]==PCAN_BOOT_STATUS_BUSY); + assert(command(&boot,7,PCAN_BOOT_CMD_ERASE,image,1)); + assert(fake.last_data[0]==PCAN_BOOT_STATUS_INVALID_SIZE); + pcan_boot_poll(&boot,29743); assert(boot.state!=PCAN_BOOT_STATE_IDLE); + pcan_boot_poll(&boot,29744); assert(boot.state==PCAN_BOOT_STATE_IDLE); + assert(command(&boot,0,PCAN_BOOT_CMD_PING,NULL,0)); + assert(pcan_boot_id_msg_body(fake.last_id)==12); + count=fake.sends; + assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),NULL,0)); + assert(fake.sends==count); pcan_boot_poll(&boot,boot.discovery_at); + assert(fake.sends==count+1 && pcan_boot_id_msg_body(fake.last_id)==1); + assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x702),NULL,0)); + begin_update(&boot,image,8); + put_u16(compat,boot.manifest.product_type); + compat[2]=boot.manifest.hardware_revision_min; compat[3]=boot.manifest.hardware_revision_max; + put_u32(compat+4,boot.manifest.firmware_version); + assert(command(&boot,7,4,compat,8)); assert(fake.last_data[0]==0); + assert(command(&boot,7,5,NULL,0)); count=fake.erases; + assert(command(&boot,7,5,NULL,0)); assert(fake.erases==count && fake.last_data[0]==0); + assert(pcan_boot_process(&boot,request_id(PCAN_BOOT_MSG_DATA_B,0),image,8)); + assert(command(&boot,7,6,NULL,0)); assert(command(&boot,7,6,NULL,0)); assert(fake.last_data[0]==0); + assert(command(&boot,7,7,NULL,0)); assert(command(&boot,7,7,NULL,0)); assert(fake.last_data[0]==0); +} + int main(void) { + test_services_timeout_and_retries(); test_id_layout(); test_crc32_reference(); test_complete_update(); diff --git a/doc/setcan/DEVICE_REGISTRY.md b/doc/setcan/DEVICE_REGISTRY.md index f25a2cd..62df0ec 100644 --- a/doc/setcan/DEVICE_REGISTRY.md +++ b/doc/setcan/DEVICE_REGISTRY.md @@ -7,6 +7,7 @@ Device Name — отображаемое имя адреса, а не допол | DeviceType | DeviceID | Device Name | Назначение | |---|---|---|---| | `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы | +| `0x3` | `0x7` | **F28335-M95M02** | Загрузка прошивки по ProtoCAN в EEPROM M95M02 платы TMS320F28335; ProductType `0x2833` | | `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов | | `0x7` | `0xD` | configurator | Существующий конфигуратор | | `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков | diff --git a/doc/setcan/Протокол CAN и ОАП.md b/doc/setcan/Протокол CAN и ОАП.md index da42337..171ec3b 100644 --- a/doc/setcan/Протокол CAN и ОАП.md +++ b/doc/setcan/Протокол CAN и ОАП.md @@ -597,11 +597,11 @@ CRC32 обнаруживает случайное повреждение, но   64 битовых адресса - +     0x3 -0x0 - 0xF устройств +0x0 - 0xF устройств
0x7: F28335-M95M02
ProductType: 0x2833 0x3. Установка времени     diff --git a/doc/setcan/Протокол CAN и ОАП.xlsx b/doc/setcan/Протокол CAN и ОАП.xlsx index 7ddba5f..0ce7a42 100644 Binary files a/doc/setcan/Протокол CAN и ОАП.xlsx and b/doc/setcan/Протокол CAN и ОАП.xlsx differ diff --git a/python/protocan/can_boot.py b/python/protocan/can_boot.py index 8b80446..46e5a4b 100644 --- a/python/protocan/can_boot.py +++ b/python/protocan/can_boot.py @@ -37,6 +37,8 @@ class CanBootTransfer: def __init__(self, image, target: CanBootTarget): if not 0 < len(image.data) <= 512 * 1024: raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ") + if target.product_type == 0x2833 and len(image.data) > 0x1FE00: + raise ValueError("F28335 M95M02 A/B: образ не должен превышать 130560 байт") if not 0 <= image.version <= 0xFFFFFFFF: raise ValueError("Версия образа вне диапазона u32") self.image, self.target = image, target @@ -46,6 +48,10 @@ class CanBootTransfer: self.finished = False self._window_end = 0 + @property + def response_timeout_ms(self): + return 15000 if self.stage in ("enter", "verify", "commit") else 1500 + @property def percent(self): return min(100, self.next_block * 8 * 100 // len(self.image.data)) @@ -128,4 +134,93 @@ class CanBootTransfer: self.stage = "reboot" return self._control(9), "Перезапуск прибора" self.stage, self.finished = "finished", True + if self.target.product_type == 0x2833: + return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск" return [], "Прошивка ProtoCAN Boot завершена" + + +class CanBootServices: + """Read-only passport queries and explicit session service commands. + + Replies use message D, body=(page<<8)|command. IDENTIFY/discovery reply body=1. + All multibyte values are little endian; version is major:8/minor:8/patch:16. + """ + def __init__(self, target: CanBootTarget): + self.target = target + + def request(self, command: int, page: int = 0): + if command not in (1, 8, 11, 12, 13, 14, 15): + raise ValueError("Unsupported service command") + if not 0 <= page <= 255 or (page and command not in (14, 15)): + raise ValueError("Invalid service page") + t = self.target + payload = bytes([page]) if command in (14, 15) else b"" + return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, 9, + t.session_id << 8 | command), payload, to_can=True) + + @staticmethod + def discover(): + return build_frame(ProtoCanId.build(1, 0, 7, 15, 13, 0), b"", to_can=True) + + def decode(self, frame, *, discovery=False): + p = ProtoCanId.parse(frame.can_id) + if (frame.to_can or not frame.ide or frame.rtr or frame.is_error or p.pm != 1 + or p.msg_type not in (12, 13) or len(frame.data) != 8): + return None + if discovery: + if p.msg_type != 13 or p.body != 1: + return None + elif (p.device_type, p.device) != (self.target.device_type, self.target.device): + return None + if p.msg_type == 12: + if p.body >> 8 != self.target.session_id: + return None + status, slot, next_block, crc32 = struct.unpack("> 8 + d = frame.data + result = {"kind": "passport", "device_type": p.device_type, "device": p.device, "command": command, "page": page} + if command == 1 and page == 0: + product, hardware, protocol, version = struct.unpack("> 24, (version >> 16) & 255, version & 65535)) + elif command == 12 and page == 0: + result.update(protocol_version=d[0], state=d[1], active_slot=d[2], target_slot=d[3], + session_id=d[4], slot_count=d[5], last_status=d[6]) + elif command == 13 and page == 0: + flags, capacity = struct.unpack("