From 6a643ae79016de673bd7045ba0fee02aaf9b6fcb Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Thu, 9 Jul 2026 09:57:54 +0300 Subject: [PATCH] # bootloader: phase 2 + host test docs --- CMakePresets.json | 6 +- firmware/bootloader/PLAN.md | 76 ++++++- .../mcuboot_port/bootutil_sources.cmake | 67 +++++++ firmware/bootloader/mcuboot_port/flash_map.h | 66 ++++++ .../flash_map_backend/flash_map_backend.h | 41 ++++ firmware/bootloader/mcuboot_port/keys.c | 27 +++ .../keys/bootloader_test_ecdsa_pub.c | 25 +++ .../mcuboot_config/mcuboot_config.h | 61 ++++++ .../mcuboot_config/mcuboot_logging.h | 21 ++ .../mcuboot_port/sysflash/sysflash.h | 26 +++ tests/host/CMakeLists.txt | 26 +++ tests/host/button/README.md | 56 ++++++ tests/host/can/README.md | 66 ++++++ tests/host/cli/README.md | 55 +++++ tests/host/led/README.md | 42 ++++ tests/host/log/README.md | 58 ++++++ tests/host/mcuboot_port/README.md | 60 ++++++ .../mcuboot_port/fake_flash_map_backend.c | 189 ++++++++++++++++++ .../mcuboot_port/fake_flash_map_backend.h | 34 ++++ .../host/mcuboot_port/fixtures/corrupt_v1.bin | Bin 0 -> 32768 bytes tests/host/mcuboot_port/fixtures/valid_v1.bin | Bin 0 -> 32768 bytes tests/host/mcuboot_port/fixtures/valid_v2.bin | Bin 0 -> 32768 bytes .../fixtures/valid_v2_unconfirmed.bin | Bin 0 -> 32768 bytes tests/host/mcuboot_port/host_link_shims.c | 47 +++++ tests/host/mcuboot_port/test_boot_select.c | 149 ++++++++++++++ tests/host/opto/README.md | 68 +++++++ tests/host/prio_queue/README.md | 56 ++++++ tests/host/protocol/README.md | 43 ++++ tests/host/ring_buffer/README.md | 51 +++++ tests/host/runner/README.md | 61 ++++++ tests/host/timeout/README.md | 43 ++++ tests/host/uart_host/README.md | 52 +++++ utils/prio_queue/README.md | 2 +- 33 files changed, 1569 insertions(+), 5 deletions(-) create mode 100644 firmware/bootloader/mcuboot_port/bootutil_sources.cmake create mode 100644 firmware/bootloader/mcuboot_port/flash_map.h create mode 100644 firmware/bootloader/mcuboot_port/flash_map_backend/flash_map_backend.h create mode 100644 firmware/bootloader/mcuboot_port/keys.c create mode 100644 firmware/bootloader/mcuboot_port/keys/bootloader_test_ecdsa_pub.c create mode 100644 firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_config.h create mode 100644 firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_logging.h create mode 100644 firmware/bootloader/mcuboot_port/sysflash/sysflash.h create mode 100644 tests/host/button/README.md create mode 100644 tests/host/can/README.md create mode 100644 tests/host/cli/README.md create mode 100644 tests/host/led/README.md create mode 100644 tests/host/mcuboot_port/README.md create mode 100644 tests/host/mcuboot_port/fake_flash_map_backend.c create mode 100644 tests/host/mcuboot_port/fake_flash_map_backend.h create mode 100644 tests/host/mcuboot_port/fixtures/corrupt_v1.bin create mode 100644 tests/host/mcuboot_port/fixtures/valid_v1.bin create mode 100644 tests/host/mcuboot_port/fixtures/valid_v2.bin create mode 100644 tests/host/mcuboot_port/fixtures/valid_v2_unconfirmed.bin create mode 100644 tests/host/mcuboot_port/host_link_shims.c create mode 100644 tests/host/mcuboot_port/test_boot_select.c create mode 100644 tests/host/opto/README.md create mode 100644 tests/host/prio_queue/README.md create mode 100644 tests/host/protocol/README.md create mode 100644 tests/host/ring_buffer/README.md create mode 100644 tests/host/runner/README.md create mode 100644 tests/host/timeout/README.md create mode 100644 tests/host/uart_host/README.md diff --git a/CMakePresets.json b/CMakePresets.json index b70805f..51d9582 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -150,7 +150,8 @@ "test_prio_queue", "uart_host_mock_example", "test_ring_buffer", - "test_timeout_pattern" + "test_timeout_pattern", + "test_mcuboot_boot_select" ] }, { @@ -169,7 +170,8 @@ "test_prio_queue", "uart_host_mock_example", "test_ring_buffer", - "test_timeout_pattern" + "test_timeout_pattern", + "test_mcuboot_boot_select" ] }, { diff --git a/firmware/bootloader/PLAN.md b/firmware/bootloader/PLAN.md index 0035c62..721e27c 100644 --- a/firmware/bootloader/PLAN.md +++ b/firmware/bootloader/PLAN.md @@ -6,7 +6,7 @@ |---|---|---| | 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) | | 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже | -| 2 — bootutil (Direct-XIP) | не начата | | +| 2 — bootutil (Direct-XIP) | 🟡 host-тесты завершены | реальный bootutil+TinyCrypt+imgtool-фикстуры, 5/5 тестов зелёные; ARM-сторона (flash_map_backend над bsp_qspi_flash, boot_select.c/jump, сборка/железо) — впереди | | 3 — SD-путь установки | не начата | | | 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | | | 5 — HAB Release + service-tui | не начата | | @@ -150,7 +150,79 @@ - `firmware/bootloader/src/boot_select.c` — вызов `boot_go` (Direct-XIP путь), получение адреса entry point выбранного слота. -**Верификация — до всякого железа**: +**Решения, принятые в обсуждении Фазы 2 (не пересматриваются):** +- Крипто-бэкенд — **TinyCrypt**, не mbedTLS: для ECDSA-P256+SHA-256 нужно 6 файлов (~2142 строк), + всё уже вендорено (`sdk/middleware/mcuboot_opensource/ext/tinycrypt` + минимальный ASN.1-парсер из + `ext/mbedtls-asn1`). Полноценный mbedTLS не вендорен вообще — потребовал бы ~8000+ новых строк. +- FIH-профиль — **MCUBOOT_FIH_PROFILE_LOW** (double-read защита + CFI-счётчики), без RNG-задержки + (та требует профиль HIGH и реальную mbedTLS-энтропию — не наш случай). +- **MCUBOOT_DIRECT_XIP_REVERT — включён.** Если новый образ ни разу не подтверждён (`boot_set_confirmed()`, + вызов — будущая ответственность tft_app), следующая загрузка стирает его и откатывается. Проверено + host-тестом (`test_boot_go_reverts_unconfirmed_image`). +- Heap **не нужен** — `malloc`/`free` в `loader.c` компилируются только под `!MCUBOOT_DIRECT_XIP`. + Линкер-скрипт бюджет 256 КБ (Фаза 1) не трогаем. +- Тестовый ключ — уже вендоренный публичный sample-ключ MCUboot + (`sdk/middleware/mcuboot_opensource/root-ec-p256.pem`), публичная часть встроена как C-массив через + `imgtool.py getpub --lang c` (`mcuboot_port/keys/bootloader_test_ecdsa_pub.c`). Не production-секрет — + для серийного производства нужен отдельный ключ вне репозитория (аналог HAB SRK-церемонии). + +**Итог host-части (выполнено):** +- `firmware/bootloader/mcuboot_port/` — `sysflash/sysflash.h`, `mcuboot_config/{mcuboot_config.h, + mcuboot_logging.h}`, `flash_map_backend/flash_map_backend.h`, `flash_map.h`, `keys.c` + + `keys/bootloader_test_ecdsa_pub.c`, `bootutil_sources.cmake` (общий список исходников bootutil + + TinyCrypt + ASN.1, `include()`-ится и ARM-таргетом, и host-тестами — не дублируется). +- Финальный набор файлов bootutil для нашего режима (Direct-XIP + Revert, ECDSA-only, без + measured-boot/encryption): `loader.c, bootutil_misc.c, bootutil_public.c, tlv.c, image_validate.c, + image_ecdsa.c, fault_injection_hardening.c, swap_scratch.c`. Важная находка: + **`swap_scratch.c` нужен несмотря на название** — `boot_read_image_header()` внутри него обёрнут в + `#if !defined(MCUBOOT_SWAP_USING_MOVE)`, то есть это и есть дефолтная (не swap-move) реализация чтения + заголовка слота, которую `loader.c` вызывает безусловно для любого режима, включая Direct-XIP. + `swap_move.c` (с альтернативной версией той же функции под `MCUBOOT_SWAP_USING_MOVE`) не нужен. +- `tests/host/mcuboot_port/` — `fake_flash_map_backend.c/.h` (in-memory реализация контракта + `flash_map.h` вместо `bsp_qspi_flash`), `host_link_shims.c` (см. ниже), `test_boot_select.c` + (5 тестов), `fixtures/` (imgtool-подписанные `valid_v1.bin`/`valid_v2.bin`/`valid_v2_unconfirmed.bin`/ + `corrupt_v1.bin`, 32 КБ каждый, slot-size уменьшен относительно реальных 2 МБ — тестируем логику + выбора слота, не абсолютные размеры). +- **Обход двух host-специфичных проблем линковки** (не существуют на реальном ARM-таргете, + `arm-none-eabi-gcc` не использует leading-underscore mangling и `--gc-sections` вырезает мёртвый код): + - `fih_panic_loop()` в `fault_injection_hardening.c` — ARM inline-asm self-reference по имени без + подчёркивания. Зависит от ABI хоста, не просто от "это host-тест": + **Mach-O (macOS)** — C-функция манглится в `_fih_panic_loop`, inline asm ищет + `fih_panic_loop` без подчёркивания и не находит → нужен отдельный символ через + `asm("fih_panic_loop")`-label. + **ELF (Linux, напр. clang-17 в devcontainer)** — C-символы НЕ манглятся, `fih_panic_loop` + резолвится сам на себя нативно, как на реальном ARM — наш шим здесь не нужен и ломает сборку + (`multiple definition of 'fih_panic_loop'`, поймано на Release/Debug пресетах в devcontainer). + Шим в `host_link_shims.c` обёрнут в `#if defined(__APPLE__)` — активен только там, где реально нужен. + - `mbedtls_mpi_read_binary` — недостижимый RSA-путь ASN.1 (`mbedtls_asn1_get_mpi`), попадающий в + объектный файл `asn1parse.c` целиком; на host (оба ABI) без `--gc-sections` требует явной + (недостижимой по рантайму) заглушки — платформенно-независимая часть `host_link_shims.c`. + - Проверено на обеих платформах: macOS (Homebrew clang, локально) и Linux/devcontainer (clang-17, + Debug + Release пресеты) — `just build::test-host` и `test-host-release` зелёные 13/13 на обеих. +- **Обход бага clang 22.1.8 (Homebrew)**: `-fsanitize=address,undefined` ломает генерацию CFI-директив + на больших функциях `loader.c` ("invalid CFI advance_loc expression" на этапе ассемблирования; без + санитайзеров те же файлы собираются чисто). Отключены санитайзеры точечно для вендоренных файлов + bootutil/TinyCrypt/ASN.1 через `set_source_files_properties` в `bootutil_sources.cmake` — для + собственного кода (`fake_flash_map_backend.c`, тест) ASan/UBSan остаются включены. +- `just build::test-host` — **13/13 тестов зелёные**, включая 5/5 новых + (`test_boot_go_slot_a_only_valid`, `test_boot_go_picks_higher_version`, + `test_boot_go_ignores_corrupted_slot`, `test_boot_go_no_valid_image`, + `test_boot_go_reverts_unconfirmed_image`). + +**Осталось для Фазы 2 (ARM-сторона, ещё не сделано):** +- `firmware/bootloader/mcuboot_port/flash_map_backend.c` — реальный шим над `bsp_qspi_flash` + (`bsp_qspi_read`/`bsp_qspi_write_page`/`bsp_qspi_erase_sector`), по образцу + `sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/flashapi/flash_api.c`. +- `firmware/bootloader/src/boot_select.c` — `boot_go()` + прыжок в выбранный образ. Референс — + `sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot()`: `flash_device_base()` → + `vt = flash_base + rsp->br_image_off + rsp->br_hdr->ih_hdr_size` → `__set_MSP(vt->msp)` → + `((void(*)(void))vt->reset)()`. CMSIS-интринсики, ассемблер не нужен. +- Подключить `bsp_qspi_flash` в `firmware/bootloader/CMakeLists.txt`, собрать под ARM (проверить что + `m_text` укладывается в 247 КБ бюджет Фазы 1). +- Аппаратная проверка: образ-заглушка (просто зажигает LED), подписанный тем же тестовым ключом, залит + вручную через SWD в Slot A (`0x60040000`) — подтвердить что bootloader реально в него прыгает. + +**Верификация — до всякого железа** (план, для истории): - Host-юнит-тесты (`tests/host/`, Unity + fff, по образцу существующих `tests/host/protocol/`, `tests/host/cli/`) с фейковым flash-буфером в памяти вместо `bsp_qspi_flash`: валидный образ в Slot A только → выбран A; оба слота валидны, версия Б выше → выбран Б; повреждённый TLV/подпись в diff --git a/firmware/bootloader/mcuboot_port/bootutil_sources.cmake b/firmware/bootloader/mcuboot_port/bootutil_sources.cmake new file mode 100644 index 0000000..7ff1b67 --- /dev/null +++ b/firmware/bootloader/mcuboot_port/bootutil_sources.cmake @@ -0,0 +1,67 @@ +# mcuboot_port/bootutil_sources.cmake +# +# Общий список файлов bootutil (MCUboot) + TinyCrypt + ASN.1-парсер, +# используемых и реальным ARM-таргетом (firmware/bootloader), и host-тестами +# (tests/host/mcuboot_port/) — чтобы не дублировать ~13 путей в двух местах. +# +# Состав определён вручную (не через .cmake файлы NXP-порта — см. +# firmware/bootloader/PLAN.md, Фаза 2) под конкретный режим: +# MCUBOOT_DIRECT_XIP + MCUBOOT_DIRECT_XIP_REVERT, MCUBOOT_IMAGE_NUMBER=1, +# MCUBOOT_SIGN_EC256 + MCUBOOT_USE_TINYCRYPT, без encryption, без +# measured-boot/shared-data (поэтому НЕ включены: boot_record.c, caps.c, +# encrypted.c, image_rsa.c, image_ed25519.c, swap_move.c, swap_misc.c, +# fault_injection_hardening_delay_rng_mbedtls.c — последний нужен только +# профилю MCUBOOT_FIH_PROFILE_HIGH, мы используем LOW). +# +# swap_scratch.c — ВКЛЮЧЁН, несмотря на название: boot_read_image_header() +# внутри него обёрнут в `#if !defined(MCUBOOT_SWAP_USING_MOVE)` — то есть +# это и есть дефолтная (не swap-move) реализация чтения заголовка слота, +# которую loader.c вызывает безусловно для ЛЮБОГО режима, включая +# Direct-XIP. swap_move.c (со своей версией той же функции под +# MCUBOOT_SWAP_USING_MOVE) не нужен. Остальные функции swap_scratch.c +# (собственно swap-логика) не достижимы в Direct-XIP и вырезаются линкером +# ARM-таргета через --gc-sections; для host-тестов — мёртвый, но безобидный +# код. + +set(MCUBOOT_OPENSOURCE_DIR ${CMAKE_SOURCE_DIR}/sdk/middleware/mcuboot_opensource) +set(MCUBOOT_BOOTUTIL_DIR ${MCUBOOT_OPENSOURCE_DIR}/boot/bootutil) +set(MCUBOOT_EXT_DIR ${MCUBOOT_OPENSOURCE_DIR}/ext) + +set(MCUBOOT_BOOTUTIL_SOURCES + ${MCUBOOT_BOOTUTIL_DIR}/src/loader.c + ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_misc.c + ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c + ${MCUBOOT_BOOTUTIL_DIR}/src/tlv.c + ${MCUBOOT_BOOTUTIL_DIR}/src/image_validate.c + ${MCUBOOT_BOOTUTIL_DIR}/src/image_ecdsa.c + ${MCUBOOT_BOOTUTIL_DIR}/src/fault_injection_hardening.c + ${MCUBOOT_BOOTUTIL_DIR}/src/swap_scratch.c + ${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc.c + ${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc_dsa.c + ${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/sha256.c + ${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/utils.c + ${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/asn1parse.c + ${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/platform_util.c) + +set(MCUBOOT_BOOTUTIL_INCLUDES + ${MCUBOOT_BOOTUTIL_DIR}/include + ${MCUBOOT_BOOTUTIL_DIR}/src + ${MCUBOOT_EXT_DIR}/tinycrypt/lib/include + ${MCUBOOT_EXT_DIR}/mbedtls-asn1/include + ${CMAKE_CURRENT_LIST_DIR} # sysflash.h, mcuboot_config.h, flash_map.h, flash_map_backend.h +) + +# ------------------------------------------------------------------------ +# Обход бага clang 22.1.8 (Homebrew): -fsanitize=address,undefined ломает +# генерацию CFI-директив на некоторых больших функциях bootutil (напр. +# loader.c::context_boot_go) — "invalid CFI advance_loc expression" на +# этапе ассемблирования. Без санитайзеров те же файлы собираются чисто — +# похоже на баг конкретной версии тулчейна, а не проблему в bootutil или +# нашем коде. Отключаем санитайзеры только для вендоренных исходников +# bootutil/TinyCrypt/ASN.1 (не для нашего кода — там ASan/UBSan остаются). +# ------------------------------------------------------------------------ +if(CMAKE_C_COMPILER_ID MATCHES "Clang") + set_source_files_properties(${MCUBOOT_BOOTUTIL_SOURCES} + PROPERTIES COMPILE_OPTIONS + "-fno-sanitize=address,undefined") +endif() diff --git a/firmware/bootloader/mcuboot_port/flash_map.h b/firmware/bootloader/mcuboot_port/flash_map.h new file mode 100644 index 0000000..6297f2a --- /dev/null +++ b/firmware/bootloader/mcuboot_port/flash_map.h @@ -0,0 +1,66 @@ +/** + * @file flash_map.h + * @brief Контракт bootutil на "область флеша" — реализуется + * flash_map_backend.c (реальный, над bsp_qspi_flash) или + * fake_flash_map_backend.c (host-тесты, in-memory буфер). + * + * Урезано под наш случай относительно оригинального Apache-2.0 заголовка + * MCUboot (sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/include/flash_map.h): + * без sblconfig.h, без encrypted_xip externs, без boot_image_names[] — этого + * NXP-специфичного слоя у нас нет. + */ + +#ifndef FLASH_MAP_H_ +#define FLASH_MAP_H_ + +#include + +/** + * @brief Область на flash-устройстве. + * + * Несколько устройств в системе не предполагается (см. FLASH_DEVICE_ID в + * sysflash.h) — fa_device_id всегда равен FLASH_DEVICE_ID. + */ +struct flash_area +{ + uint8_t fa_id; /**< ID области, уникален в системе. */ + uint8_t fa_device_id; /**< ID flash-устройства. */ + uint16_t pad16; + uint32_t fa_off; /**< Смещение области от начала устройства. */ + uint32_t fa_size; /**< Размер области, байт. */ +}; + +/** @brief Сектор внутри области (смещение относительно начала области). */ +struct flash_sector +{ + uint32_t fs_off; + uint32_t fs_size; +}; + +/** @brief Базовый адрес flash-устройства в адресном пространстве MCU (XIP). */ +int flash_device_base(uint8_t fd_id, uintptr_t *ret); + +int flash_area_open(uint8_t id, const struct flash_area **area); +void flash_area_close(const struct flash_area *area); + +/* Read/write/erase — смещение относительно начала области. */ +int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len); +int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len); +int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len); + +/** @brief Минимальное выравнивание записи. */ +uint8_t flash_area_align(const struct flash_area *area); + +/** @brief Значение стёртого байта (0xFF для NOR). */ +uint8_t flash_area_erased_val(const struct flash_area *area); + +/** @brief Прочитать len байт с off и проверить что это стёртая область. */ +int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len); + +int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors); +int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector); + +int flash_area_id_from_image_slot(int slot); +int flash_area_id_to_multi_image_slot(int image_index, int area_id); + +#endif /* FLASH_MAP_H_ */ diff --git a/firmware/bootloader/mcuboot_port/flash_map_backend/flash_map_backend.h b/firmware/bootloader/mcuboot_port/flash_map_backend/flash_map_backend.h new file mode 100644 index 0000000..5536354 --- /dev/null +++ b/firmware/bootloader/mcuboot_port/flash_map_backend/flash_map_backend.h @@ -0,0 +1,41 @@ +/** + * @file flash_map_backend.h + * @brief Инлайн-аксессоры flash_area/flash_sector, требуемые bootutil. + */ + +#ifndef FLASH_MAP_BACKEND_H_ +#define FLASH_MAP_BACKEND_H_ + +#include "flash_map.h" + +static inline uint8_t flash_area_get_id(const struct flash_area *fa) +{ + return fa->fa_id; +} + +static inline uint8_t flash_area_get_device_id(const struct flash_area *fa) +{ + return fa->fa_device_id; +} + +static inline uint32_t flash_area_get_off(const struct flash_area *fa) +{ + return fa->fa_off; +} + +static inline uint32_t flash_area_get_size(const struct flash_area *fa) +{ + return fa->fa_size; +} + +static inline uint32_t flash_sector_get_off(const struct flash_sector *fs) +{ + return fs->fs_off; +} + +static inline uint32_t flash_sector_get_size(const struct flash_sector *fs) +{ + return fs->fs_size; +} + +#endif /* FLASH_MAP_BACKEND_H_ */ diff --git a/firmware/bootloader/mcuboot_port/keys.c b/firmware/bootloader/mcuboot_port/keys.c new file mode 100644 index 0000000..3e32cf1 --- /dev/null +++ b/firmware/bootloader/mcuboot_port/keys.c @@ -0,0 +1,27 @@ +/** + * @file keys.c + * @brief Таблица публичных ключей bootutil. + * + * MCUBOOT_HW_KEY / MCUBOOT_BUILTIN_KEY не определены (см. mcuboot_config.h) + * — bootutil использует стандартный путь: TLV образа несёт хэш ключа, + * bootutil ищет совпадение в bootutil_keys[] и проверяет подпись найденным + * ключом. По образцу sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/keys.c. + */ + +#include +#include + +#if defined(MCUBOOT_SIGN_EC256) +#include "keys/bootloader_test_ecdsa_pub.c" +#else +#error "No public key available for given signing algorithm." +#endif + +const struct bootutil_key bootutil_keys[] = { + { + .key = ecdsa_pub_key, + .len = &ecdsa_pub_key_len, + }, +}; + +const int bootutil_key_cnt = 1; diff --git a/firmware/bootloader/mcuboot_port/keys/bootloader_test_ecdsa_pub.c b/firmware/bootloader/mcuboot_port/keys/bootloader_test_ecdsa_pub.c new file mode 100644 index 0000000..76badff --- /dev/null +++ b/firmware/bootloader/mcuboot_port/keys/bootloader_test_ecdsa_pub.c @@ -0,0 +1,25 @@ +/** + * @file bootloader_test_ecdsa_pub.c + * @brief Публичный ключ ECDSA P-256 — сгенерирован из тестового + * sample-ключа MCUboot (sdk/middleware/mcuboot_opensource/root-ec-p256.pem) + * командой `imgtool.py getpub --lang c`. + * + * ВНИМАНИЕ: это публичный, широко известный sample-ключ проекта MCUboot, + * НЕ производственный секрет. Используется для Фазы 2 (host-тесты + первая + * проверка на железе). Перед серийным производством должен быть заменён + * на реальный production-ключ (приватная часть — вне репозитория, см. + * жизненный цикл ключей HAB в docs/mimxrt1052/HAB_GUIDE.md — аналогичная + * процедура нужна для ключа подписи tft_app-образов). + */ + +/* Autogenerated by imgtool.py, do not edit. */ +const unsigned char ecdsa_pub_key[] = { + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x2a, + 0xcb, 0x40, 0x3c, 0xe8, 0xfe, 0xed, 0x5b, 0xa4, 0x49, 0x95, 0xa1, 0xa9, 0x1d, 0xae, + 0xe8, 0xdb, 0xbe, 0x19, 0x37, 0xcd, 0x14, 0xfb, 0x2f, 0x24, 0x57, 0x37, 0xe5, 0x95, + 0x39, 0x88, 0xd9, 0x94, 0xb9, 0xd6, 0x5a, 0xeb, 0xd7, 0xcd, 0xd5, 0x30, 0x8a, 0xd6, + 0xfe, 0x48, 0xb2, 0x4a, 0x6a, 0x81, 0x0e, 0xe5, 0xf0, 0x7d, 0x8b, 0x68, 0x34, 0xcc, + 0x3a, 0x6a, 0xfc, 0x53, 0x8e, 0xfa, 0xc1, +}; +const unsigned int ecdsa_pub_key_len = 91; diff --git a/firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_config.h b/firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_config.h new file mode 100644 index 0000000..14e5cdf --- /dev/null +++ b/firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_config.h @@ -0,0 +1,61 @@ +/** + * @file mcuboot_config.h + * @brief Конфигурация bootutil (MCUboot) для загрузчика TFT. + * + * В отличие от шаблона NXP (nxp_mcux_sdk/include/mcuboot_config/mcuboot_config.h) + * задаёт финальные макросы напрямую, без Kconfig-подобной прослойки — + * решения зафиксированы в firmware/bootloader/PLAN.md, Фаза 2: + * + * - Direct-XIP с revert: два слота, оба могут содержать валидный образ, + * bootutil выбирает более новую валидную версию; если она ни разу не + * подтверждена (confirm) — следующая загрузка откатится на предыдущую + * (см. MCUBOOT_DIRECT_XIP_REVERT). + * - ECDSA P-256 + TinyCrypt — компактный, полностью вендорен в репозитории + * (sdk/middleware/mcuboot_opensource/ext/tinycrypt), в отличие от + * mbedTLS (не вендорен, потребовал бы ~8000+ новых строк). + * - FIH профиль LOW — часть защиты bootutil (double-read сравнений) без + * RNG-задержки (та требует mbedTLS-энтропию, доступно только в профиле + * HIGH — не наш случай). + * - Heap НЕ нужен: malloc/free в bootutil (loader.c) вызываются только в + * swap-режиме, недостижимы под MCUBOOT_DIRECT_XIP. + */ + +#ifndef MCUBOOT_CONFIG_H_ +#define MCUBOOT_CONFIG_H_ + +/* ── Схема подписи ─────────────────────────────────────────────────────── */ +#define MCUBOOT_SIGN_EC256 + +/* ── Крипто-бэкенд ─────────────────────────────────────────────────────── */ +#define MCUBOOT_USE_TINYCRYPT + +/* ── Режим обновления ─────────────────────────────────────────────────── */ +#define MCUBOOT_DIRECT_XIP +#define MCUBOOT_DIRECT_XIP_REVERT + +/* Проверять подпись активного слота при каждой загрузке, не только при + * установке нового образа. */ +#define MCUBOOT_VALIDATE_PRIMARY_SLOT + +/* ── Образы ────────────────────────────────────────────────────────────── */ +#define MCUBOOT_IMAGE_NUMBER 1 + +/* ── Flash-абстракция ─────────────────────────────────────────────────── */ +#define MCUBOOT_USE_FLASH_AREA_GET_SECTORS + +/* Slot A/Б = 2 МБ (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md), сектор + * W25Qxx = 4 КБ → 2 МБ / 4 КБ = 512 секторов на слот. */ +#define MCUBOOT_MAX_IMG_SECTORS 512 + +/* ── Fault injection hardening ────────────────────────────────────────── */ +#define MCUBOOT_FIH_PROFILE_LOW + +/* ── Логирование — отключено, BOOT_LOG_* становятся no-op (bootutil_log.h) */ + +/* ── Watchdog — не используется в bootloader ─────────────────────────── */ +#define MCUBOOT_WATCHDOG_FEED() \ + do \ + { \ + } while (0) + +#endif /* MCUBOOT_CONFIG_H_ */ diff --git a/firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_logging.h b/firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_logging.h new file mode 100644 index 0000000..ca08b3f --- /dev/null +++ b/firmware/bootloader/mcuboot_port/mcuboot_config/mcuboot_logging.h @@ -0,0 +1,21 @@ +/** + * @file mcuboot_logging.h + * @brief Логирование bootutil — не используется (MCUBOOT_HAVE_LOGGING не + * определён в mcuboot_config.h, BOOT_LOG_* становятся no-op через + * bootutil_log.h). Этот заголовок обязателен к существованию — + * часть заголовков bootutil (bootutil/crypto/sha.h) включает его + * безусловно, независимо от MCUBOOT_HAVE_LOGGING. + */ + +#ifndef MCUBOOT_LOGGING_H_ +#define MCUBOOT_LOGGING_H_ + +#define MCUBOOT_LOG_MODULE_DECLARE(domain) +#define MCUBOOT_LOG_MODULE_REGISTER(domain) + +#define MCUBOOT_LOG_ERR(...) +#define MCUBOOT_LOG_WRN(...) +#define MCUBOOT_LOG_INF(...) +#define MCUBOOT_LOG_DBG(...) + +#endif /* MCUBOOT_LOGGING_H_ */ diff --git a/firmware/bootloader/mcuboot_port/sysflash/sysflash.h b/firmware/bootloader/mcuboot_port/sysflash/sysflash.h new file mode 100644 index 0000000..741db78 --- /dev/null +++ b/firmware/bootloader/mcuboot_port/sysflash/sysflash.h @@ -0,0 +1,26 @@ +/** + * @file sysflash.h + * @brief Отображение логических слотов bootutil на flash-area ID. + * + * MCUBOOT_IMAGE_NUMBER=1 → два ID: Slot A (primary=0), Slot Б (secondary=1). + * Без scratch — Direct-XIP не использует область подкачки. Смещения/размеры + * самих областей заданы в flash_map_backend.c (см. + * docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). + */ + +#ifndef SYSFLASH_H_ +#define SYSFLASH_H_ + +#include "mcuboot_config/mcuboot_config.h" + +#define FLASH_AREA_IMAGE_PRIMARY(x) (((x) == 0) ? 0 : 255) +#define FLASH_AREA_IMAGE_SECONDARY(x) (((x) == 0) ? 1 : 255) + +#define MCUBOOT_IMAGE_SLOT_NUMBER (MCUBOOT_IMAGE_NUMBER * 2) + +/** @brief Единственное flash-устройство в системе — W25Qxx через FlexSPI. */ +#define FLASH_DEVICE_ID 1 + +int flash_area_id_from_multi_image_slot(int image_index, int slot); + +#endif /* SYSFLASH_H_ */ diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 0fd3bc2..34b624c 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -218,3 +218,29 @@ add_host_test( ${BSP_MOCKS_DIR}) target_compile_definitions(test_firmware_runner PRIVATE UNIT_TEST) + +# ----------------------------------------------------------------------------- +# bootutil (MCUboot Direct-XIP) — выбор слота. Реальный bootutil + TinyCrypt +# поверх fake_flash_map_backend.c (in-memory буфер вместо bsp_qspi_flash). +# См. firmware/bootloader/PLAN.md, Фаза 2. +# ----------------------------------------------------------------------------- +include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake) + +add_host_test( + NAME + test_mcuboot_boot_select + SOURCES + mcuboot_port/test_boot_select.c + mcuboot_port/fake_flash_map_backend.c + mcuboot_port/host_link_shims.c + ${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/keys.c + ${MCUBOOT_BOOTUTIL_SOURCES} + INCLUDES + ${MCUBOOT_BOOTUTIL_INCLUDES} + ${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port) + +target_compile_definitions( + test_mcuboot_boot_select + PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures") + +target_compile_options(test_mcuboot_boot_select PRIVATE -w) diff --git a/tests/host/button/README.md b/tests/host/button/README.md new file mode 100644 index 0000000..5f4cb6e --- /dev/null +++ b/tests/host/button/README.md @@ -0,0 +1,56 @@ +# test_bsp_button + +## Модуль под тестом + +`bsp/button/src/button.c` (`bsp/button/include/bsp/button.h`) + +## Категория + +B — BSP-модуль, зависит от `fsl_gpio.h`. Unity + fff. + +## Моки + +- `GPIO_ReadPinInput` — единственная SDK-функция, вызываемая `button.c`, + fff value-фейк. Управляется напрямую (`return_val`) для одной кнопки или + через `SET_RETURN_SEQ` для чередования значений нескольких кнопок в одном + `bsp_button_poll()`. +- `GPIO_PinInit` не мокируется — `bsp_button_init()` не трогает GPIO + напрямую. + +## Что проверяется + +- **Инициализация** — `bsp_button_init()` возвращает `BSP_OK`; повторный + вызов сбрасывает накопленное состояние (состояние нажатия и события). +- **Сырое чтение** (`bsp_button_read`) — LOW = нажата, HIGH = не нажата, + невалидный индекс безопасно возвращает `false`. +- **Debounce на нажатие** — событие не срабатывает до порога в 4 стабильных + сэмпла, срабатывает на 4-м, состояние остаётся стабильным при удержании, + повторных событий на удержании нет. +- **Потребление события** — флаг события сбрасывается после первого чтения + (`get_event_pressed`/`get_event_released`). +- **Debounce на отпускание** — аналогичная логика для перехода в + «отпущено». +- **Сброс счётчика при глитче** — единичный сэмпл противоположного уровня + посреди серии сбрасывает накопленный счётчик debounce. +- **Независимость кнопок** — `BSP_BUTTON_1` и `BSP_BUTTON_2` не влияют друг + на друга при чередующихся значениях GPIO. +- **Граничные индексы** — некорректный `bsp_button_t` не приводит к падению + ни в одной публичной функции. + +## Гарантии + +- Debounce требует ровно 4 подряд идущих одинаковых сэмпла для регистрации + перехода состояния. +- Любой единичный «дребезг» (глитч) сбрасывает накопленный счётчик — не + засчитывается частично. +- События нажатия/отпускания одноразовые: второй подряд вызов `get_event_*` + без нового перехода возвращает `false`. +- Кнопки полностью независимы друг от друга. +- Невалидный индекс кнопки не вызывает падения — все геттеры возвращают + безопасное значение по умолчанию. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_bsp_button -V +``` diff --git a/tests/host/can/README.md b/tests/host/can/README.md new file mode 100644 index 0000000..57dfd03 --- /dev/null +++ b/tests/host/can/README.md @@ -0,0 +1,66 @@ +# test_bsp_can + +## Модуль под тестом + +`bsp/can/src/can.c` + `utils/ring_buffer/ring_buffer.c` (реальный, не +мокается — используется как внутренняя зависимость `can.c`). + +## Категория + +B — BSP-модуль, зависит от FlexCAN SDK (`fsl_flexcan.h`, `fsl_clock.h`). +Unity + fff. + +## Моки + +Набор fff-фейков SDK FlexCAN: `FLEXCAN_GetDefaultConfig`, +`FLEXCAN_CalculateImprovedTimingValues`, `FLEXCAN_Init`, `FLEXCAN_Deinit`, +`FLEXCAN_SetTxMbConfig`, `FLEXCAN_SetRxMbConfig`, +`FLEXCAN_SetRxIndividualMask`, `FLEXCAN_WriteTxMb`, `FLEXCAN_ReadRxMb`, +`FLEXCAN_GetMbStatusFlags`, `FLEXCAN_ClearMbStatusFlags`, а также +`bsp_tick_get_ms` (управляемое время для тестов таймаутов) и +`CLOCK_EnableClock` (ERRATA 50235 workaround в `bsp_can_init()`). +Кастомные `custom_fake`: `read_rx_mb_inject` (подставляет заранее +подготовленный RX-фрейм), `get_mb_flags_once` (флаг готовности только на +первый вызов), `tick_advancing` (линейно растущее время для таймаутов), +`capture_tx_frame` (захват TX-фрейма для проверки конвертации ID/данных). + +## Что проверяется + +- **Init / Deinit** — успешная инициализация; отклонение `NULL`-конфига, + нулевого и слишком высокого bitrate, ошибки расчёта тайминга; повторная + инициализация вызывает `Deinit` перед новым `Init`; `deinit` без `init` + — no-op. +- **TX** (`bsp_can_send`) — успешная отправка, `NULL`-фрейм, `dlc > 8`, + отправка без инициализации, занятый MB (`BSP_ERR_BUSY`), таймаут + ожидания готовности (`BSP_ERR_TIMEOUT`). +- **Фильтрация** (`bsp_can_set_filter` / `bsp_can_accept_all`) — STD/EXT + ID, индекс фильтра вне диапазона, вызов без инициализации, + `accept_all` настраивает минимум 2 RX MB и деактивирует ранее + настроенные фильтры. +- **RX** (`bsp_can_receive`) — успешный приём STD/EXT фрейма, таймаут, + `NULL`-указатель, приём без инициализации, неблокирующий опрос при + `timeout_ms == 0`. +- **Callback-заглушка** — `bsp_can_register_rx_callback` возвращает + `BSP_ERR_NOT_SUPPORTED`. +- **Конвертация фреймов** — корректность кодирования STD/EXT ID + (`FLEXCAN_ID_STD`/`FLEXCAN_ID_EXT`) и порядка байт данных при переводе + между `bsp_can_frame_t` и SDK `flexcan_frame_t`. + +## Гарантии + +- Все публичные функции проверяют, что модуль инициализирован, и + возвращают `BSP_ERR_PARAM` в противном случае. +- Параметры валидируются перед обращением к SDK: `dlc <= 8`, bitrate в + допустимом диапазоне, индекс фильтра в пределах `BSP_CAN_FILTER_MAX`. +- TX и RX корректно завершаются по таймауту (`BSP_ERR_TIMEOUT`), если MB не + становится готовым. +- STD/EXT кодирование идентификатора и порядок байт данных не искажаются + при конвертации между форматами приложения и SDK. +- `bsp_can_accept_all()` деактивирует ранее настроенные пользовательские + фильтры перед установкой приёма «всех кадров». + +## Запуск + +```bash +ctest --preset host-debug-test -R test_bsp_can -V +``` diff --git a/tests/host/cli/README.md b/tests/host/cli/README.md new file mode 100644 index 0000000..7d44009 --- /dev/null +++ b/tests/host/cli/README.md @@ -0,0 +1,55 @@ +# test_cli + +## Модуль под тестом + +`firmware/test/src/cli.c` — построчный разбор JSON-протокола от USB CDC и +диспетчеризация команд. + +## Категория + +B — зависит от `bsp_usb_cdc`, `protocol_send_*`, `test_runner_*`, +`bsp_prov_read_uid`, подменяемых через fff. + +## Моки + +`bsp_usb_cdc_write`/`bsp_usb_cdc_read` (чтение эмулируется хелпером +`inject()`, который наполняет внутренний буфер «как будто USB» и вызывает +`cli_process()`), `protocol_send_pong`, `protocol_send_error`, +`protocol_send_uid_response`, `protocol_send_version_response`, +`test_runner_run_all`, `test_runner_run_single`, `test_runner_run_selected`, +`test_runner_on_confirm`, `test_runner_send_list`, `bsp_prov_read_uid`. +Кастомные `custom_fake` (`capture_run_single`, `capture_on_confirm`, +`capture_uid_response`, `capture_run_selected`) копируют строковые +аргументы по значению, т.к. `cli.c` передаёt указатели на собственные +локальные буферы, которые становятся dangling после возврата +`cli_process()`. + +## Что проверяется + +- **Команды** (`type: cmd`) — `ping`→pong, `run_all`→запуск всех, + `run`+`id`→запуск одного с передачей id, `get_uid` (успех и ошибка + чтения UID), `get_version`, `list_tests`, `run_selected` с массивом id. +- **Подтверждения** (`type: confirm`) — диспетчеризация `confirmed: + true/false` с корректным id. +- **Ошибки протокола** — отсутствует `type` или обязательное поле + (`id`/`confirmed`/`tests`) → `PARSE_ERR`; неизвестный `type`/`cmd` → + `UNKNOWN_CMD`; строка длиннее `CLI_LINE_BUF_SIZE` → `LINE_TOO_LONG`. +- **Построчный ввод** — пустая строка игнорируется, `\r\n` обрабатывается + как `\n`, две команды в одном чтении диспетчеризуются обе по отдельности. + +## Гарантии + +- Каждая валидная команда приводит ровно к одному вызову соответствующего + обработчика (`test_runner_*`/`protocol_send_*`). +- Любая ошибка парсинга или неизвестная команда отправляет ровно один + `protocol_send_error()` с точным, стабильным кодом ошибки. +- Построчный парсер корректно разделяет несколько команд в одном чтении и + не путает `\r\n` и `\n`. +- Переполнение буфера строки не приводит к падению — возвращается + контролируемая ошибка `LINE_TOO_LONG`. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_cli -V +``` diff --git a/tests/host/led/README.md b/tests/host/led/README.md new file mode 100644 index 0000000..a35c19d --- /dev/null +++ b/tests/host/led/README.md @@ -0,0 +1,42 @@ +# test_bsp_led + +## Модуль под тестом + +`bsp/led/src/led.c` (`bsp/led/include/bsp/led.h`) + +## Категория + +B — BSP-модуль, зависит от `fsl_gpio.h`. Unity + fff. + +## Моки + +- `GPIO_PinInit` — фейк, конфигурация захватывается через `custom_fake` + (`GPIO_PinInit_capture`) и копируется по значению, т.к. `gpio_pin_config_t` + живёт на стеке `bsp_led_init()` и становится dangling после возврата. +- `GPIO_PinWrite` — фейк, проверяется последний переданный уровень + (`arg2_val`). + +## Что проверяется + +- **Инициализация** — `GPIO_PinInit` вызывается для каждого LED, пины + настраиваются как output, начальный уровень HIGH (LED выключен), оба LED + выключены сразу после `bsp_led_init()`. +- **on / off** — `bsp_led_on()`/`bsp_led_off()` меняют логическое состояние и + пишут в GPIO инвертированный уровень. +- **toggle** — переключение из off→on, on→off, двойной toggle возвращает + исходное состояние. +- **set** — `bsp_led_set(led, bool)` включает/выключает по явному флагу. +- **Независимость** — состояние `LED_HEARTBEAT` и `LED_APP` не пересекается. + +## Гарантии + +- Полярность active-LOW: `on` → GPIO=0, `off` → GPIO=1. +- После инициализации оба LED гарантированно выключены. +- `toggle`, применённый чётное число раз, возвращает исходное состояние. +- LED-каналы независимы друг от друга. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_bsp_led -V +``` diff --git a/tests/host/log/README.md b/tests/host/log/README.md index e69de29..23f79f4 100644 --- a/tests/host/log/README.md +++ b/tests/host/log/README.md @@ -0,0 +1,58 @@ +# test_log + +## Модуль под тестом + +`utils/log/log.c` (`utils/log/log.h`) + +## Категория + +A с fff-хуками — `log.c` не зависит от `fsl_*.h`, но имеет weak-хуки +(mutex, timestamp), которые в тесте переопределяются строгими fff-фейками; +линковщик выбирает их поверх слабых реализаций из `log.c` автоматически. + +## Моки + +- `log_mutex_init` / `log_mutex_lock` / `log_mutex_unlock` — fff void-фейки, + переопределяющие weak-реализации. Для проверки порядка вызовов lock/unlock + используют `custom_fake`, инкрементирующий общий монотонный счётчик. +- `log_get_timestamp_ms` — fff value-фейк, управляет значением метки времени + в выводе. +- `capture_cb` — тестовый callback вместо реального UART-транспорта: + сохраняет содержимое и длину каждого вызова в `g_s_capture` для проверки + через `strstr()`. + +## Что проверяется + +- **Init** — отсутствие вывода до `log_init()` и при `NULL`-callback; + callback вызывается один раз на каждый `LOG_*`; повторный `log_init()` + заменяет предыдущий callback. +- **Output** — наличие символа уровня (`[E]`/`[W]`/`[I]`/`[D]`/`[V]`), тега, + текста сообщения (включая форматирование с аргументами) и `\r\n` в конце + строки. +- **Timestamp** — `log_get_timestamp_ms()` вызывается ровно один раз на + каждый `LOG_*`, возвращённое значение попадает в вывод. +- **Overflow** — строка длиннее `LOG_BUF_SIZE` (256 байт) обрезается, + callback не получает больше `LOG_BUF_SIZE` байт, `\r\n` сохраняется в + конце даже при обрезке. +- **Mutex** — порядок `lock → callback → unlock` проверяется через + монотонный счётчик последовательности; lock/unlock вызываются ровно один + раз на лог. +- **Context** — указатель контекста, переданный в `log_init()`, доходит до + callback без изменений (`NULL` и ненулевой указатель). + +## Гарантии + +- Без вызова `log_init()` (или с `NULL`-callback) логгер не производит + вывод. +- Вывод никогда не превышает `LOG_BUF_SIZE` байт и всегда заканчивается + `\r\n`, даже при переполнении входной строки. +- Каждый лог-вызов оборачивается ровно одной парой lock/unlock, callback + вызывается строго между ними. +- Контекст, переданный в `log_init()`, не искажается при передаче в + callback. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_log -V +``` diff --git a/tests/host/mcuboot_port/README.md b/tests/host/mcuboot_port/README.md new file mode 100644 index 0000000..9000f07 --- /dev/null +++ b/tests/host/mcuboot_port/README.md @@ -0,0 +1,60 @@ +# test_mcuboot_boot_select + +## Модуль под тестом + +Выбор загрузочного слота реальным `bootutil` (MCUboot Direct-XIP + Revert, +см. `firmware/bootloader/PLAN.md`, Фаза 2). В тесте линкуется настоящий +`bootutil` (`loader.c`, `image_validate.c`, `tlv.c`, ...) и TinyCrypt — +тестируется реальная проверка подписи/версии/TLV, а не заглушка +криптографии. + +## Категория + +B — интеграционный host-тест: реальная библиотека `bootutil` собирается +поверх фейкового flash-бэкенда вместо `bsp_qspi_flash`. + +## Моки + +- `fake_flash_map_backend.c/.h` — полноценная тестовая реализация контракта + `flash_map.h` поверх статического буфера в памяти хоста (два смежных + слота по `FAKE_FLASH_SLOT_SIZE` = 32 KB) вместо `bsp_qspi_flash`. Это + фейк, а не fff-мок: `fake_flash_reset()` стирает оба слота в `0xFF`, + `fake_flash_write_slot()` записывает подготовленный образ в нужный слот. +- `host_link_shims.c` — заглушки символов, недостающих только при + линковке `bootutil` на хосте (`fih_panic_loop` под Mach-O ABI на macOS, + недостижимый в ECDSA-only сборке `mbedtls_mpi_read_binary`); не участвуют + в тестируемой логике выбора слота. +- `fixtures/*.bin` — реальные, подписанные `imgtool` тестовым ключом + MCUboot образы (`valid_v1`, `valid_v2`, `valid_v2_unconfirmed`, + `corrupt_v1`), загружаются в фейковый flash перед вызовом `boot_go()`. + +## Что проверяется + +- Валиден только Slot A → выбирается Slot A. +- Валидны оба слота, версия образа в Slot Б выше → выбирается Slot Б. +- Slot Б повреждён (битый хэш/подпись) → игнорируется, выбирается + валидный Slot A. +- Оба слота пусты/невалидны → `boot_go()` возвращает ошибку. +- Direct-XIP Revert: образ выбран, но ни разу не подтверждён + (`boot_set_confirmed()` не вызывался) → при следующей загрузке `bootutil` + стирает слот, повторный `boot_go()` не находит образ. + +## Гарантии + +- `bootutil` выбирает валидный образ с максимальной версией среди слотов. +- Образы с некорректной подписью/хэшем не выбираются и не приводят к + падению — они просто игнорируются в пользу валидного слота. +- Отсутствие валидного образа в обоих слотах даёт явную ошибку `boot_go()`, + а не неопределённое поведение. +- Неподтверждённый (unconfirmed) Direct-XIP образ откатывается (слот + стирается) при следующей загрузке — anti-brick гарантия механизма + Revert. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_mcuboot_boot_select -V +``` + +Путь к фикстурам (`FIXTURES_DIR`) прокидывается автоматически через +`target_compile_definitions` в `tests/host/CMakeLists.txt`. diff --git a/tests/host/mcuboot_port/fake_flash_map_backend.c b/tests/host/mcuboot_port/fake_flash_map_backend.c new file mode 100644 index 0000000..b06875b --- /dev/null +++ b/tests/host/mcuboot_port/fake_flash_map_backend.c @@ -0,0 +1,189 @@ +/** + * @file fake_flash_map_backend.c + * @brief Реализация flash_map.h поверх статического буфера в памяти хоста. + * См. fake_flash_map_backend.h. + */ + +#include "fake_flash_map_backend.h" + +#include "flash_map.h" +#include "sysflash/sysflash.h" + +#include + +#define ERASED_VAL 0xFFU + +/** @brief Slot A на [0, FAKE_FLASH_SLOT_SIZE), Slot Б сразу за ним. */ +static uint8_t g_s_flash_buf[2U * FAKE_FLASH_SLOT_SIZE]; + +static const struct flash_area g_s_areas[2] = { + { .fa_id = 0U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U, + .fa_off = 0U, .fa_size = FAKE_FLASH_SLOT_SIZE }, + { .fa_id = 1U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U, + .fa_off = FAKE_FLASH_SLOT_SIZE, .fa_size = FAKE_FLASH_SLOT_SIZE }, +}; + +/* ── Test-only helpers ────────────────────────────────────────────────── */ + +void fake_flash_reset(void) +{ + memset(g_s_flash_buf, ERASED_VAL, sizeof(g_s_flash_buf)); +} + +void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len) +{ + uint8_t *p_dst = g_s_flash_buf + ((size_t) slot_idx * FAKE_FLASH_SLOT_SIZE); + memcpy(p_dst, p_data, len); +} + +/* ── flash_map.h contract ─────────────────────────────────────────────── */ + +int flash_device_base(uint8_t fd_id, uintptr_t *ret) +{ + if (fd_id != FLASH_DEVICE_ID) + { + return -1; + } + *ret = (uintptr_t) g_s_flash_buf; + return 0; +} + +int flash_area_open(uint8_t id, const struct flash_area **area) +{ + if (id >= 2U) + { + return -1; + } + *area = &g_s_areas[id]; + return 0; +} + +void flash_area_close(const struct flash_area *area) +{ + (void) area; +} + +int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len) +{ + if (off + len > area->fa_size) + { + return -1; + } + memcpy(dst, g_s_flash_buf + area->fa_off + off, len); + return 0; +} + +int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len) +{ + if (off + len > area->fa_size) + { + return -1; + } + memcpy(g_s_flash_buf + area->fa_off + off, src, len); + return 0; +} + +int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len) +{ + if (off + len > area->fa_size) + { + return -1; + } + memset(g_s_flash_buf + area->fa_off + off, ERASED_VAL, len); + return 0; +} + +uint8_t flash_area_align(const struct flash_area *area) +{ + (void) area; + return 1U; +} + +uint8_t flash_area_erased_val(const struct flash_area *area) +{ + (void) area; + return ERASED_VAL; +} + +int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len) +{ + if (flash_area_read(area, off, dst, len) != 0) + { + return -1; + } + + const uint8_t *p_buf = (const uint8_t *) dst; + for (uint32_t i = 0U; i < len; i++) + { + if (p_buf[i] != ERASED_VAL) + { + return 0; + } + } + return 1; +} + +int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector) +{ + if (off >= fa->fa_size) + { + return -1; + } + sector->fs_off = (off / FAKE_FLASH_SECTOR_SIZE) * FAKE_FLASH_SECTOR_SIZE; + sector->fs_size = FAKE_FLASH_SECTOR_SIZE; + return 0; +} + +int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors) +{ + const struct flash_area *fa; + uint32_t max_cnt = *count; + + if (flash_area_open((uint8_t) fa_id, &fa) != 0) + { + return -1; + } + + uint32_t rem_len = fa->fa_size; + *count = 0U; + while ((rem_len > 0U) && (*count < max_cnt)) + { + sectors[*count].fs_off = FAKE_FLASH_SECTOR_SIZE * (*count); + sectors[*count].fs_size = FAKE_FLASH_SECTOR_SIZE; + (*count)++; + rem_len -= FAKE_FLASH_SECTOR_SIZE; + } + + return 0; +} + +int flash_area_id_from_multi_image_slot(int image_index, int slot) +{ + switch (slot) + { + case 0: + return FLASH_AREA_IMAGE_PRIMARY(image_index); + case 1: + return FLASH_AREA_IMAGE_SECONDARY(image_index); + default: + return -1; + } +} + +int flash_area_id_from_image_slot(int slot) +{ + return flash_area_id_from_multi_image_slot(0, slot); +} + +int flash_area_id_to_multi_image_slot(int image_index, int area_id) +{ + if (area_id == FLASH_AREA_IMAGE_PRIMARY(image_index)) + { + return 0; + } + if (area_id == FLASH_AREA_IMAGE_SECONDARY(image_index)) + { + return 1; + } + return -1; +} diff --git a/tests/host/mcuboot_port/fake_flash_map_backend.h b/tests/host/mcuboot_port/fake_flash_map_backend.h new file mode 100644 index 0000000..0158dad --- /dev/null +++ b/tests/host/mcuboot_port/fake_flash_map_backend.h @@ -0,0 +1,34 @@ +/** + * @file fake_flash_map_backend.h + * @brief Test-only реализация контракта flash_map.h поверх обычной памяти + * хоста — заменяет bsp_qspi_flash для host-тестов bootutil (Direct-XIP + * выбор слота, см. firmware/bootloader/PLAN.md, Фаза 2). + * + * Слот A (fa_id=0) и слот Б (fa_id=1) — смежные регионы одного статического + * буфера FAKE_SLOT_SIZE байт каждый, имитируют один flash-девайс с двумя + * областями (как и на реальном железе — см. BOOTLOADER_FLASH_MAP.md). + */ + +#ifndef FAKE_FLASH_MAP_BACKEND_H_ +#define FAKE_FLASH_MAP_BACKEND_H_ + +#include +#include + +#define FAKE_FLASH_SECTOR_SIZE 0x1000U /* 4 KB — как реальный W25Qxx */ +#define FAKE_FLASH_SLOT_SIZE 0x8000U /* 32 KB — уменьшенный тестовый слот */ + +/** @brief Заполнить всю fake-флеш 0xFF (оба слота — "стёрты"). */ +void fake_flash_reset(void); + +/** + * @brief Записать содержимое (например, imgtool-подписанный фикстур-образ) + * в начало указанного слота. + * + * @param[in] slot_idx 0 — Slot A, 1 — Slot Б. + * @param[in] p_data Данные образа. + * @param[in] len Длина данных, <= FAKE_FLASH_SLOT_SIZE. + */ +void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len); + +#endif /* FAKE_FLASH_MAP_BACKEND_H_ */ diff --git a/tests/host/mcuboot_port/fixtures/corrupt_v1.bin b/tests/host/mcuboot_port/fixtures/corrupt_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..6251b0eb90da055fe9021d5cdda13db22f9ed84d GIT binary patch literal 32768 zcmeIwzf06%90&049UVd^nvjG!_+wIZkU~9#BM^^6NjS7tQIMiVatcH6ChwNqP8VHt zkjh8|<<^2|X(*GNB!5A81tLOAiJIhpU`eA;+A z_^c!PJyE%NssEhE5gDdzZZD& THh0*+xwliC{!c+(P#%n@243x^s`P`U(h|Dq`Sqap zY(9})R+SYkI@$Yi@Vxb9bJs}naykEZta>&!R}mzF+IYBp@Y%#<=KOe|>1oHx=tkk^ zgU#gD^oK~}#godVg-rcX_)+lg@%Ntv?Jq8qJ@NhRH#vo=#LUCtwe8nE?^~mX@zR}p z{pT!4fB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{t_4DW2N+sRpLVSuzpd|R K&#jpo9=Qfqk+pOH literal 0 HcmV?d00001 diff --git a/tests/host/mcuboot_port/fixtures/valid_v2.bin b/tests/host/mcuboot_port/fixtures/valid_v2.bin new file mode 100644 index 0000000000000000000000000000000000000000..4eaa7be9b899e022d3bb8c647bec6c321b4a2d6c GIT binary patch literal 32768 zcmeIw&nv@m9LMp`_iKC$g%ah!cO0Z?aZpRKgRztRI68d$9wf@eF6Ojdv=VZet(~W2 za*_F_{D`!a9DK`Zru;~vq)C3{$Bh3$&GS{CdL7<}&->B4%}CFS@5q@aWm~rXG$B4o zP75sk^C%-BQ7TmNXfHh7tgEj77#MAS+19%@c2sM*l6M{Q7_CQ9^G(XkTv&#l40mx& zgHlxaaBkjoyELcn54}`HU%$(dmJ2)IMTYFk8Io&g}m{9D=0-JZmV^LqP!tFVIb1a7d0tg_000Iag zfB*srAb<1Px`mTd9qmog za)Ndk(cz$*K}TpqizXrw8p=V$`TkGH_4&dB-{D!F4^K;W?{zpy^mrnt?X;e?5DulJ z1>XJVi9Jn+st^zRw=e(rbo1J+gGYmne?O<%7jp4Lb?m}cdN_M``Ng(wDi7hC- z-1phl*1>PTr`spiM!I{V@!GP=djl2uWan6a)rXnPrRROcj>jtzy z97liv0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK;XOxj4r;~OMTs$Z0dSg*)Tso Fbp&Z+xRC$= literal 0 HcmV?d00001 diff --git a/tests/host/mcuboot_port/host_link_shims.c b/tests/host/mcuboot_port/host_link_shims.c new file mode 100644 index 0000000..c9ff34c --- /dev/null +++ b/tests/host/mcuboot_port/host_link_shims.c @@ -0,0 +1,47 @@ +/** + * @file host_link_shims.c + * @brief Заглушки символов, недостающих только при линковке bootutil на + * хосте, но не на реальном ARM-таргете. Не часть mcuboot_port/ + * (портируемого слоя) — специфично для host-тестов. + * + * 1. fih_panic_loop() — тело в bootutil/src/fault_injection_hardening.c + * написано как ARM inline asm, self-reference по имени без подчёркивания + * ("b fih_panic_loop"). На arm-none-eabi-gcc (реальный таргет) это + * резолвится нативно — там C-символы не манглятся подчёркиванием. + * + * На хосте картина зависит от ABI, а не просто от "это host-тест": + * - Mach-O (macOS): C-функция "fih_panic_loop" компилируется в символ + * "_fih_panic_loop" — inline asm ищет ровно "fih_panic_loop" без + * подчёркивания и не находит. Нужен явный символ через GNU asm-label + * (см. ниже, только под __APPLE__). + * - ELF (Linux, напр. devcontainer clang-17): C-символы НЕ манглятся + * подчёркиванием — "fih_panic_loop" резолвится сам на себя нативно, + * как и на ARM. Наш шим здесь не нужен и создаёт konфликт + * ("multiple definition") с уже существующим определением в + * fault_injection_hardening.c — поэтому строго под __APPLE__. + * + * 2. mbedtls_mpi_read_binary() — используется только mbedtls_asn1_get_mpi() + * (RSA-путь ASN.1, ext/mbedtls-asn1/src/asn1parse.c), недостижимо в нашей + * ECDSA-only конфигурации. На ARM-таргете --gc-sections вырезает мёртвый + * вызов до того, как он потребует символ; host-линковка (Mach-O и ELF + * одинаково) без --gc-sections требует явного разрешения — недостижимая + * по рантайму заглушка, платформенно-независима. + */ + +#if defined(__APPLE__) +void fih_panic_loop_impl(void) asm("fih_panic_loop"); +void fih_panic_loop_impl(void) +{ + for (;;) + { + } +} +#endif /* __APPLE__ */ + +int mbedtls_mpi_read_binary(void *X, const unsigned char *buf, unsigned long len) +{ + (void) X; + (void) buf; + (void) len; + return -1; /* недостижимо — ECDSA-only, RSA-путь ASN.1 не вызывается */ +} diff --git a/tests/host/mcuboot_port/test_boot_select.c b/tests/host/mcuboot_port/test_boot_select.c new file mode 100644 index 0000000..9d14031 --- /dev/null +++ b/tests/host/mcuboot_port/test_boot_select.c @@ -0,0 +1,149 @@ +/** + * @file test_boot_select.c + * @brief Host-тесты выбора слота bootutil (MCUboot Direct-XIP + Revert). + * + * Реальный bootutil (loader.c, image_validate.c, tlv.c, ...) + TinyCrypt + * поверх fake_flash_map_backend.c (in-memory буфер вместо bsp_qspi_flash). + * Фикстуры в fixtures/ подписаны настоящим imgtool тестовым ключом + * MCUboot (root-ec-p256.pem, см. mcuboot_port/keys/bootloader_test_ecdsa_pub.c) + * — тестируется реальная проверка подписи/версии/TLV, не мок. + * + * Сценарии — firmware/bootloader/PLAN.md, Фаза 2: + * 1. Валиден только Slot A → выбран A. + * 2. Оба валидны, версия Б выше → выбран Б. + * 3. Slot Б повреждён (битый хэш/подпись) → игнорируется, выбран A. + * 4. Оба слота пусты/невалидны → boot_go() возвращает ошибку (триггер + * top-level состояния "нет образа" из Фазы 3). + * 5. Direct-XIP Revert: образ выбран, но ни разу не confirmed → при + * следующей загрузке bootutil стирает слот и boot_go() проваливается. + */ + +#include "unity.h" + +#include "fake_flash_map_backend.h" + +#include "bootutil/bootutil.h" +#include "bootutil/fault_injection_hardening.h" + +#include +#include + +#ifndef FIXTURES_DIR +#error "FIXTURES_DIR must be defined by CMake (see tests/host/CMakeLists.txt)" +#endif + +static uint8_t g_s_fixture_buf[FAKE_FLASH_SLOT_SIZE]; + +/** + * @brief Загрузить фикстур-файл в g_s_fixture_buf. + * @return Число прочитанных байт. + */ +static size_t load_fixture(const char *p_name) +{ + char path[256]; + (void) snprintf(path, sizeof(path), "%s/%s", FIXTURES_DIR, p_name); + + FILE *p_file = fopen(path, "rb"); + TEST_ASSERT_NOT_NULL_MESSAGE(p_file, path); + + size_t n = fread(g_s_fixture_buf, 1U, sizeof(g_s_fixture_buf), p_file); + (void) fclose(p_file); + + TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, n); + return n; +} + +void setUp(void) +{ + fake_flash_reset(); +} + +void tearDown(void) +{ +} + +/* ── Сценарий 1 — валиден только Slot A ──────────────────────────────── */ + +void test_boot_go_slot_a_only_valid(void) +{ + fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin")); + + struct boot_rsp rsp; + fih_ret fih_rc = boot_go(&rsp); + + TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS)); + TEST_ASSERT_EQUAL_UINT32(0U, rsp.br_image_off); +} + +/* ── Сценарий 2 — оба валидны, побеждает более новая версия ──────────── */ + +void test_boot_go_picks_higher_version(void) +{ + fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin")); + fake_flash_write_slot(1, g_s_fixture_buf, load_fixture("valid_v2.bin")); + + struct boot_rsp rsp; + fih_ret fih_rc = boot_go(&rsp); + + TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS)); + TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, rsp.br_image_off); +} + +/* ── Сценарий 3 — повреждённый Slot Б игнорируется ───────────────────── */ + +void test_boot_go_ignores_corrupted_slot(void) +{ + fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin")); + fake_flash_write_slot(1, g_s_fixture_buf, load_fixture("corrupt_v1.bin")); + + struct boot_rsp rsp; + fih_ret fih_rc = boot_go(&rsp); + + TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS)); + TEST_ASSERT_EQUAL_UINT32(0U, rsp.br_image_off); +} + +/* ── Сценарий 4 — оба слота пусты → нет загружаемого образа ──────────── */ + +void test_boot_go_no_valid_image(void) +{ + /* fake_flash_reset() в setUp уже оставил оба слота стёртыми (0xFF) */ + struct boot_rsp rsp; + fih_ret fih_rc = boot_go(&rsp); + + TEST_ASSERT_FALSE(FIH_EQ(fih_rc, FIH_SUCCESS)); +} + +/* ── Сценарий 5 — Direct-XIP Revert: неподтверждённый образ стирается ── */ + +void test_boot_go_reverts_unconfirmed_image(void) +{ + fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v2_unconfirmed.bin")); + + struct boot_rsp rsp; + + /* Первая загрузка: образ валиден, выбран, но boot_set_confirmed() никто + * не вызвал (симулируем что tft_app не подтвердила себя). */ + fih_ret fih_rc = boot_go(&rsp); + TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS)); + + /* Вторая загрузка "после перезагрузки" — bootutil видит copy_done=SET, + * image_ok не SET → стирает слот и не находит образ. */ + fih_rc = boot_go(&rsp); + TEST_ASSERT_FALSE(FIH_EQ(fih_rc, FIH_SUCCESS)); +} + +/* ── Точка входа ───────────────────────────────────────────────────────── */ + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_boot_go_slot_a_only_valid); + RUN_TEST(test_boot_go_picks_higher_version); + RUN_TEST(test_boot_go_ignores_corrupted_slot); + RUN_TEST(test_boot_go_no_valid_image); + RUN_TEST(test_boot_go_reverts_unconfirmed_image); + + return UNITY_END(); +} diff --git a/tests/host/opto/README.md b/tests/host/opto/README.md new file mode 100644 index 0000000..6d847d0 --- /dev/null +++ b/tests/host/opto/README.md @@ -0,0 +1,68 @@ +# test_bsp_opto + +## Модуль под тестом + +`bsp/opto/src/opto.c` (`bsp/opto/include/bsp/opto.h`) + +## Категория + +B — BSP-модуль, зависит от `fsl_gpio.h` и `bsp/tick.h`. Unity + fff. + +## Моки + +SDK GPIO/NVIC: `GPIO_PinInit`, `GPIO_PinWrite`, `GPIO_PinRead`, +`GPIO_SetPinInterruptConfig`, `GPIO_EnableInterrupts`, +`GPIO_DisableInterrupts`, `GPIO_GetPinsInterruptFlags`, +`GPIO_ClearPinsInterruptFlags`, `EnableIRQ`. Плюс BSP-зависимости: +`BOARD_InitRS_GPIO` (`board.h`-стаб), `bsp_tick_get_ms` (`bsp/tick.h`-стаб). +Кастомные `custom_fake`: захват конфигурации `GPIO_PinInit` и +`GPIO_SetPinInterruptConfig` по значению (пин + структура конфигурации). +ISR-обработчик `GPIO1_Combined_16_31_IRQHandler()` вызывается напрямую из +теста (`simulate_isr`) с заранее выставленными +`GPIO_GetPinsInterruptFlags`/`GPIO_PinRead` — реальное прерывание не +эмулируется, дёргается тот же код, что вызвало бы железо. + +## Что проверяется + +- **Инициализация** — число и параметры настраиваемых каналов в + зависимости от `rs_as_gpio` (2 канала IN1/IN2, либо 3 с RS), направление + всегда input, корректные номера пинов, IRQ включается на каждый канал и + глобально, `BOARD_InitRS_GPIO()` вызывается только при `rs_as_gpio == + true`. +- **Начальный фронт для MODE_LEVEL** — если пин LOW (INACTIVE) при init, + выбирается RISING; если HIGH (ACTIVE) — FALLING. +- **Переключение фронта в ISR** (MODE_LEVEL) — после RISING фронта ISR + переключает ожидание на FALLING и обратно. +- **bsp_opto_read** — начальное состояние по уровню пина, отключённый + RS-канал и некорректный номер канала всегда дают INACTIVE. +- **bsp_opto_process — дебаунс (MODE_LEVEL)** — коллбэк не срабатывает до + истечения `debounce_ms`, срабатывает после с правильными + каналом/состоянием, не срабатывает повторно если состояние не изменилось + или уже обработано предыдущим `process()`. +- **Независимость каналов** — срабатывание одного канала не влияет на + состояние и коллбэки другого. +- **MODE_PROTO** — коллбэк вызывается синхронно прямо в ISR (не в + `process()`), после срабатывания IRQ канала отключается, + `bsp_opto_process()` не генерирует для него коллбэков, + `bsp_opto_proto_arm()` перевзводит прерывание (для PROTO) и является + no-op для LEVEL-канала, `bsp_opto_read()` для PROTO-канала всегда + INACTIVE. + +## Гарантии + +- Полярность active-HIGH: `raw=1` → `ACTIVE`, `raw=0` → `INACTIVE`. +- MODE_LEVEL: переходы состояния дебаунсятся (`debounce_ms`) и + подтверждаются только в `bsp_opto_process()`; ISR лишь фиксирует + «pending» и переключает направление ожидаемого фронта. +- MODE_PROTO: обрабатывается целиком в ISR, коллбэк синхронный, + `bsp_opto_process()` его не трогает. +- Каналы полностью независимы — событие на одном не искажает состояние + другого. +- Некорректный номер канала не приводит к падению, все геттеры + возвращают безопасное значение по умолчанию (`INACTIVE`). + +## Запуск + +```bash +ctest --preset host-debug-test -R test_bsp_opto -V +``` diff --git a/tests/host/prio_queue/README.md b/tests/host/prio_queue/README.md new file mode 100644 index 0000000..1e30490 --- /dev/null +++ b/tests/host/prio_queue/README.md @@ -0,0 +1,56 @@ +# test_prio_queue + +## Модуль под тестом + +`utils/prio_queue/prio_queue.c` (`utils/prio_queue/prio_queue.h`) — очередь +с приоритетами на отсортированном массиве фиксированной ёмкости, с +вытеснением наименее приоритетного элемента при переполнении. + +## Категория + +A — платформонезависимый модуль. Только Unity. + +## Моки + +Нет. + +## Что проверяется + +- **init** — размер 0, `peek`/`at` на пустой очереди возвращают `NULL`. +- **insert_order** — сортировка по приоритету не зависит от порядка + вставки (по возрастанию, по убыванию, вперемешку). +- **fifo** — при равных приоритетах порядок вставки сохраняется + (стабильная сортировка), включая перемешанные группы разных приоритетов. +- **full_eviction** — вставка в заполненную очередь: более приоритетный + элемент вытесняет худший (`PQ_EVICTED`), менее или равно приоритетный + отклоняется (`PQ_FULL`); размер очереди не меняется ни в одном из + случаев. +- **evict_correct** — вытесняется именно наименее приоритетный элемент, + верхушка (`peek`) остаётся корректной после вытеснения. +- **peek** — не удаляет элемент при повторных вызовах, указывает на + элемент во внутреннем хранилище очереди. +- **remove_at** — удаление первого/среднего/последнего элемента со сдвигом + остальных, no-op при индексе вне диапазона или на пустой очереди. +- **at** — доступ по индексу, `NULL` для индекса вне диапазона. +- **size** — инкремент при вставке, декремент при удалении, не меняется + при `PQ_FULL`. +- **stress** — полный цикл вставки всех элементов и последовательного + извлечения с проверкой сохранения порядка; чередование вставок с + одинаковыми и разными приоритетами. + +## Гарантии + +- `peek()` / `at(0)` всегда возвращает элемент с наивысшим приоритетом + (наименьшим значением согласно переданной `cmp`-функции). +- При равных приоритетах порядок извлечения строго FIFO. +- При переполнении: более приоритетная вставка вытесняет наименее + приоритетный существующий элемент (`PQ_EVICTED`), иначе отклоняется + (`PQ_FULL`) без изменения состояния очереди. +- `remove_at()` на некорректном индексе или пустой очереди — безопасный + no-op, не приводит к падению или порче данных. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_prio_queue -V +``` diff --git a/tests/host/protocol/README.md b/tests/host/protocol/README.md new file mode 100644 index 0000000..b943ad5 --- /dev/null +++ b/tests/host/protocol/README.md @@ -0,0 +1,43 @@ +# test_protocol + +## Модуль под тестом + +`firmware/test/src/protocol.c` — сериализация JSON-событий протокола +(`session_start`, `test_begin`, `test_result`, `summary`, `confirm_request`, +`pong`, `error`). + +## Категория + +B — зависит от `cli_send()` и `bsp_tick_get_ms()`, подменяемых через fff. + +## Моки + +- `cli_send` — fff void-фейк; итоговая строка захватывается через + `custom_fake` (`capture_cli_send`) в статический буфер, т.к. аргумент — + указатель на стековый буфер `protocol_send_*()`, живой только до возврата. +- `bsp_tick_get_ms` — fff value-фейк, управляет значением `uptime_ms` в + `session_start`. +- `bsp_delay` — fff void-фейк-заглушка (не используется в проверках). + +## Что проверяется + +Точный JSON-текст, передаваемый в `cli_send()`, для каждого типа сообщения: +`session_start` (нулевой и ненулевой uptime, версия прошивки), `test_begin` +(`critical: true/false`), `test_result` (`pass`, `fail` с деталями, `skip`), +`summary` (общий результат pass/fail), `confirm_request` (кастомный таймаут +и подстановка таймаута по умолчанию при `timeout_ms == 0`), `pong`, `error` +(разные коды ошибок). + +## Гарантии + +- Формат JSON точно соответствует протоколу — сравнивается побайтово с + эталонной строкой, а не только наличием отдельных полей. +- Каждый `protocol_send_*()` вызывает `cli_send()` ровно один раз. +- `confirm_request` с `timeout_ms == 0` всегда получает + `PROTOCOL_CONFIRM_TIMEOUT_MS` вместо нуля. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_protocol -V +``` diff --git a/tests/host/ring_buffer/README.md b/tests/host/ring_buffer/README.md new file mode 100644 index 0000000..1fffe6e --- /dev/null +++ b/tests/host/ring_buffer/README.md @@ -0,0 +1,51 @@ +# test_ring_buffer + +## Модуль под тестом + +`utils/ring_buffer/ring_buffer.c` (`utils/ring_buffer/ring_buffer.h`) + +## Категория + +A — платформонезависимый модуль, только стандартная библиотека C. Только Unity. + +## Моки + +Нет — модуль не имеет внешних зависимостей, тестируется напрямую. + +## Что проверяется + +- **Init** — корректная инициализация с валидными параметрами; отклонение + `NULL`-указателей, нулевого размера и размера, не являющегося степенью + двойки; приём всех степеней двойки от 1 до 256. +- **Put / Get** — запись/чтение одного байта, `get` из пустого буфера, `put` в + полный буфер, порядок FIFO. +- **Write / Read** — блочная запись/чтение, частичная запись при нехватке + места, частичное чтение при нехватке данных, чтение из пустого буфера. +- **Full / Empty** — граничные состояния и счётчики (`count`, `free`) до и + после заполнения/опустошения. +- **Wraparound** — несколько циклов заполнения/опустошения, переход индексов + через границу массива, сохранение целостности данных при частичном + перекрытии индексов после сдвига к краю буфера. +- **SPSC simulation** — 200 итераций чередующихся `put`/`get` (имитация + ISR-producer / task-consumer в один поток) с проверкой сохранения порядка + данных. +- **Reset** — сброс состояния и корректное повторное использование буфера + без остаточных артефактов. + +## Гарантии + +- `ring_buffer_init()` отклоняет `NULL`-указатели, нулевой и не + степень-двойки размер. +- Данные извлекаются в порядке FIFO для любых сочетаний put/get и write/read. +- Индексация корректно оборачивается через границу массива (маска `&`) на + любом протестированном размере — данные не повреждаются при wraparound. +- `put`/`write` в полный буфер и `get`/`read` из пустого не повреждают + состояние и не пишут за границы переданных буферов. +- `ring_buffer_reset()` приводит буфер в состояние, идентичное состоянию + сразу после `init`. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_ring_buffer -V +``` diff --git a/tests/host/runner/README.md b/tests/host/runner/README.md new file mode 100644 index 0000000..f5d0839 --- /dev/null +++ b/tests/host/runner/README.md @@ -0,0 +1,61 @@ +# test_firmware_runner + +## Модуль под тестом + +`firmware/test/src/test_runner.c` — state machine выполнения реестра +тестовых модулей. + +## Категория + +B — зависит от `bsp_tick_get_ms`, `bsp_delay`, `bsp_usb_cdc_poll`, +`cli_process`, `protocol_send_*`, подменяемых через fff. + +## Моки + +`bsp_tick_get_ms`, `bsp_delay`, `bsp_usb_cdc_poll`, `cli_process`, +`protocol_send_test_begin`, `protocol_send_test_result`, +`protocol_send_summary`, `protocol_send_confirm_request`, +`protocol_send_error`, `protocol_send_pong`, `protocol_send_session_start`, +`protocol_send_test_list`. UNIT_TEST seam: реестр тестовых модулей +(`g_unit_test_registry` / `g_unit_test_registry_size`) подставляется этим +тестовым файлом вместо реального реестра прошивки — `set_registry()` +меняет состав между тестами (сборка выполняется с `-DUNIT_TEST`). Кастомный +`custom_fake` `capture_test_result` копирует `test_result_t` по значению, +т.к. `protocol_send_test_result` получает указатель на стековую переменную +`execute_test()`. + +## Что проверяется + +- **Базовое состояние** — runner не занят изначально; `run_all` на пустом + реестре сразу шлёт summary с нулевыми счётчиками; `run_single` с + неизвестным id шлёт `UNKNOWN_TEST`. +- **Одиночный запуск** — успешный тест шлёт `test_begin` + `test_result` и + освобождает runner; проваленный тест шлёт результат со статусом FAIL. +- **run_all** — несколько успешных тестов дают верный summary; + критический (`critical=true`) провал прерывает выполнение — оставшиеся + тесты получают статус SKIP, а `summary` отражает pass/fail/skip и + итоговый `overall`. +- **pre_confirm state machine** — модуль с `pre_confirm_prompt` переводит + runner в состояние ожидания (busy) и шлёт `confirm_request`, не запуская + тест; повторный запуск во время ожидания получает ошибку `BUSY`; + `on_confirm(id, true)` выполняет тест; `on_confirm(id, false)` помечает + тест как SKIP; истечение `PROTOCOL_CONFIRM_TIMEOUT_MS` без ответа тоже + даёт SKIP; подтверждение с чужим id игнорируется как устаревшее (stale). + +## Гарантии + +- Критический провал теста в `run_all` прерывает выполнение остальных — + они получают статус SKIP, а не PASS/FAIL. +- Runner не принимает новый запуск, пока занят (`BUSY`), в частности во + время ожидания pre-confirm. +- Тест с pre-confirm не выполняется без явного положительного + подтверждения: отказ, таймаут ожидания или подтверждение с чужим id не + приводят к выполнению теста. +- `summary` всегда отражает фактические счётчики passed/failed/skipped и + корректный итоговый `overall`. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_firmware_runner -V +``` diff --git a/tests/host/timeout/README.md b/tests/host/timeout/README.md new file mode 100644 index 0000000..b81c2e3 --- /dev/null +++ b/tests/host/timeout/README.md @@ -0,0 +1,43 @@ +# test_timeout_pattern + +## Модуль под тестом + +Не отдельный файл, а паттерн таймаута +`(uint32_t)(now - start) >= (uint32_t)timeout`, используемый в +`bsp_uart_read()`, `bsp_delay()` и других BSP-модулях (счётчик — +`bsp_tick_get_ms()`). Тестируется сама математика, изолированно от железа. + +## Категория + +A — чистая математика, без внешних зависимостей. Только Unity. + +## Моки + +Нет. + +## Что проверяется + +- **Нормальные случаи** — истёкший/неистёкший таймаут при `elapsed < + timeout`, `elapsed == timeout`, `elapsed > timeout`; нулевой `elapsed`; + `timeout == 0` (истекает немедленно). +- **Wraparound** — счётчик `now`/`start` переполняется через `UINT32_MAX`; + беззнаковая арифметика корректно вычисляет `elapsed` в этих случаях + (проверено на нескольких комбинациях старта у границы `UINT32_MAX`). +- **Антипаттерн** — отдельный тест демонстрирует, что альтернативная формула + `now >= start + timeout` ломается при переполнении `start + timeout`, в + отличие от используемого паттерна. + +## Гарантии + +- Формула `(uint32_t)(now - start) >= timeout` корректна при любом взаимном + положении `now` и `start`, включая переполнение 32-битного счётчика. +- `elapsed == timeout` считается истёкшим таймаутом (граница включительно). +- `timeout == 0` всегда трактуется как немедленно истёкший. +- Задокументировано и подтверждено тестом, почему альтернативная формула + `now >= start + timeout` небезопасна и не должна использоваться. + +## Запуск + +```bash +ctest --preset host-debug-test -R test_timeout_pattern -V +``` diff --git a/tests/host/uart_host/README.md b/tests/host/uart_host/README.md new file mode 100644 index 0000000..29727ed --- /dev/null +++ b/tests/host/uart_host/README.md @@ -0,0 +1,52 @@ +# uart_host_mock_example + +## Модуль под тестом + +Не сам `bsp/uart_host/src/uart_host.c` (он верифицируется отдельными +HIL-тестами через pyserial на реальном железе), а корректность вызова его +публичного API (`bsp/uart_host/include/bsp/uart_host.h`) кодом верхнего +уровня — через готовый мок `bsp/uart_host/mocks/uart_host_mock.c`. + +## Категория + +B — зависит от API `bsp_uart_host`, подменяемого через fff. + +## Моки + +Полный fff-мок API `bsp_uart_host`, поставляемый модулем +`bsp/uart_host/mocks/uart_host_mock.{c,h}`: +`bsp_uart_host_init`, `bsp_uart_host_deinit`, `bsp_uart_host_write`, +`bsp_uart_host_write_str`, `bsp_uart_host_read`, `bsp_uart_host_read_byte`, +`bsp_uart_host_rx_available`, `bsp_uart_host_rx_flush`. Сброс всех фейков — +макросом `UART_HOST_MOCK_RESET_ALL()` в `setUp()`. + +## Что проверяется + +- **init** — baud rate передаётся без искажений, ошибка инициализации + (`BSP_ERR_INIT`) пробрасывается наверх. +- **write / write_str** — корректные буфер/длина или строка передаются в + API, ошибка при неинициализированном UART пробрасывается. +- **read_byte** — успешное значение, таймаут (`-1`), конвертация + `BSP_UART_HOST_WAIT_FOREVER` → `UINT32_MAX`. +- **read** — корректные буфер/размер/таймаут передаются, частичное чтение + (меньше запрошенного) не трактуется как ошибка. +- **rx helpers** — `rx_available` возвращает счётчик, `rx_flush` + вызывается. +- **Сброс мока** — `UART_HOST_MOCK_RESET_ALL()` обнуляет счётчики вызовов и + `return_val` между тестами. + +## Гарантии + +- Код верхнего уровня передаёт в `bsp_uart_host_*` ровно те аргументы, + которые получил сам (baud rate, буфер, длина, таймаут). +- Коды ошибок и специальные значения (таймаут `-1`, + `BSP_UART_HOST_WAIT_FOREVER`) не теряются и не искажаются на пути через + API. +- Частичное чтение — штатный случай, а не ошибка. +- Между тестами мок гарантированно возвращается в чистое состояние. + +## Запуск + +```bash +ctest --preset host-debug-test -R uart_host_mock_example -V +``` diff --git a/utils/prio_queue/README.md b/utils/prio_queue/README.md index e4d5096..c1ca91d 100644 --- a/utils/prio_queue/README.md +++ b/utils/prio_queue/README.md @@ -71,7 +71,7 @@ prio_queue_remove_at(&q, 0); /* удалить верхний после об **Политика вытеснения при полном буфере:** -``` +```bash Очередь полна [A(1) B(2) C(3)], вставляем D(2): → D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED