diff --git a/README.md b/README.md index 5b4e26a..99634e4 100644 --- a/README.md +++ b/README.md @@ -8,33 +8,33 @@ ## Три firmware-проекта -| Проект | Путь | Описание | -|--------|------|----------| -| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | -| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Сам обновляется только через USB ROM + blhost | -| Боевая прошивка | `firmware/tft_app/` | FreeRTOS + FatFS + бизнес-логика. Обновляется загрузчиком | +| Проект | Путь | Описание | +| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- | +| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | +| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Сам обновляется только через USB ROM + blhost / SWD | +| Production прошивка | `firmware/tft_app/` | FreeRTOS + FatFS + бизнес-логика. Обновляется загрузчиком | --- ## BSP -| Модуль | Путь | Описание | -|--------|------|----------| -| `bsp_led` | `bsp/led/` | Два UserLed (GPIO3[3], GPIO3[4]) | -| `bsp_tick` | `bsp/tick/` | SysTick / FreeRTOS-совместимый таймер | -| `bsp_uart_host` | `bsp/uart_host/` | LPUART1 — MCU-Link VCOM (J2) | -| `bsp_opto` | `bsp/opto/` | Оптоизолированные входы PS2801-4: EXT_IN1, EXT_IN2, RS_RX | -| `bsp_usb_cdc` | `bsp/usb_cdc/` | USB CDC ACM | -| generated | `bsp/generated/` | NXP Config Tools: pin_mux, clock_config, board, startup | +| Модуль | Путь | Описание | +| --------------- | ---------------- | --------------------------------------------------------- | +| `bsp_led` | `bsp/led/` | Два UserLed (GPIO3[3], GPIO3[4]) | +| `bsp_tick` | `bsp/tick/` | SysTick / FreeRTOS-совместимый таймер | +| `bsp_uart_host` | `bsp/uart_host/` | LPUART1 — MCU-Link VCOM (J2) | +| `bsp_opto` | `bsp/opto/` | Оптоизолированные входы PS2801-4: EXT_IN1, EXT_IN2, RS_RX | +| `bsp_usb_cdc` | `bsp/usb_cdc/` | USB CDC ACM | +| generated | `bsp/generated/` | NXP Config Tools: pin_mux, clock_config, board, startup | --- ## Тестирование -| Уровень | Где | Инструменты | Запуск | -|---------|-----|-------------|--------| -| Host unit-тесты | `tests/host/` | Unity + fff, clang | `just build::test-host` (devcontainer) | -| HIL target-тесты | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest + M5StampPLC | `just host::hil-run` (хост) | +| Уровень | Где | Инструменты | Запуск | +| ---------------- | ------------------------------ | -------------------------------------- | -------------------------------------- | +| Host unit-тесты | `tests/host/` | Unity + fff, clang | `just build::test-host` (devcontainer) | +| HIL target-тесты | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest + M5StampPLC | `just host::hil-run` (хост) | **Host-тесты** запускаются в devcontainer без железа. BSP-модули тестируются через fff-фейки и stub-хедеры. @@ -75,12 +75,12 @@ just host::debug-server # GDB-сервер для отладки ## Зависимости -| | Подход | -|--|--------| -| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored | -| Unity, fff, SEGGER RTT | vendored | -| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` | -| spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` | +| | Подход | +| --------------------------------------------- | -------------------- | +| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored | +| Unity, fff, SEGGER RTT | vendored | +| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` | +| spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` | Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей). diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 38b1aaf..c6b1fb0 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -15,6 +15,7 @@ add_subdirectory(uart_host) add_subdirectory(opto) add_subdirectory(can) add_subdirectory(button) +add_subdirectory(display) add_subdirectory(usb_cdc) add_subdirectory(sdram) add_subdirectory(qspi_flash) diff --git a/bsp/button/README.md b/bsp/button/README.md index d667710..3c21e9c 100644 --- a/bsp/button/README.md +++ b/bsp/button/README.md @@ -8,10 +8,10 @@ ## Аппаратура -| Кнопка | Пин MCU | GPIO | Схема | Нажатие | -|----------------|--------------|-----------|--------------------------------|---------| -| `BSP_BUTTON_1` | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW | -| `BSP_BUTTON_2` | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW | +| Кнопка | Пин MCU | GPIO | Схема | Нажатие | +| -------------- | ---------- | --------- | ----------------------------- | ------- | +| `BSP_BUTTON_1` | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW | +| `BSP_BUTTON_2` | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW | Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`) как INPUT с включённым гистерезисом, без внутренней подтяжки (`0x0100B0`). `bsp_button_init()` не трогает @@ -189,20 +189,20 @@ pytest: `tools/hil/04_test_button.py` — помечен `@pytest.mark.interacti Команды CLI прошивки: -| Команда | Ответ | Описание | -|--------------|-------------|--------------------------------------------| -| `PING` | `PONG` | Проверка канала | -| `READ ` | `1` / `0` | Сырое состояние (без debounce) | -| `STATE `| `1` / `0` | Стабильное состояние после debounce | -| `EVENT_P `| `1` / `0` | `get_event_pressed`, сбрасывает флаг | -| `EVENT_R `| `1` / `0` | `get_event_released`, сбрасывает флаг | +| Команда | Ответ | Описание | +| --------------- | --------- | ------------------------------------- | +| `PING` | `PONG` | Проверка канала | +| `READ ` | `1` / `0` | Сырое состояние (без debounce) | +| `STATE ` | `1` / `0` | Стабильное состояние после debounce | +| `EVENT_P ` | `1` / `0` | `get_event_pressed`, сбрасывает флаг | +| `EVENT_R ` | `1` / `0` | `get_event_released`, сбрасывает флаг | --- ## Зависимости | Зависимость | Тип | Описание | -|--------------|---------|----------------------------------------------| +| ------------ | ------- | -------------------------------------------- | | `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | | `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers | | `sdk_gpio` | PRIVATE | `fsl_gpio.h` — `GPIO_ReadPinInput()` | diff --git a/bsp/button/include/bsp/button.h b/bsp/button/include/bsp/button.h index 9412761..441f10a 100644 --- a/bsp/button/include/bsp/button.h +++ b/bsp/button/include/bsp/button.h @@ -27,7 +27,6 @@ #include "bsp/status.h" #include -#include /* ------------------------------------------------------------------------- * Типы diff --git a/bsp/can/README.md b/bsp/can/README.md index f482260..dc98ef5 100644 --- a/bsp/can/README.md +++ b/bsp/can/README.md @@ -34,11 +34,11 @@ ## Распределение Message Buffers -| MB | Назначение | -|-------|-----------------------------------------------------| -| 0 | Зарезервирован (ERR005829 workaround: inactive TX) | -| 1 | TX — отправка фреймов | -| 2..17 | RX — до 16 индивидуальных фильтров | +| MB | Назначение | +| ----- | -------------------------------------------------- | +| 0 | Зарезервирован (ERR005829 workaround: inactive TX) | +| 1 | TX — отправка фреймов | +| 2..17 | RX — до 16 индивидуальных фильтров | ERR005829 — errata FlexCAN на i.MX RT1050/1052: при гонке TX/RX арбитража MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMbInactive`. @@ -104,12 +104,12 @@ bsp_can_accept_all(); затем ждёт флага завершения с polling `FLEXCAN_GetMbStatusFlags()`. Возвращается по одному из условий: -| Условие | Возврат | -|---------|---------| -| Фрейм успешно отправлен | `BSP_OK` | -| TX MB занят предыдущей передачей | `BSP_ERR_BUSY` | -| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | -| Невалидные параметры | `BSP_ERR_PARAM` | +| Условие | Возврат | +| -------------------------------- | ----------------- | +| Фрейм успешно отправлен | `BSP_OK` | +| TX MB занят предыдущей передачей | `BSP_ERR_BUSY` | +| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | +| Невалидные параметры | `BSP_ERR_PARAM` | При 500 kbit/s максимальное время отправки одного фрейма — ~260 мкс. Для bare-metal и FreeRTOS-задачи это приемлемо. @@ -119,11 +119,11 @@ bsp_can_accept_all(); Polling с таймаутом. Обходит все активные RX MB, читает готовые фреймы во внутренний ring buffer, пытается извлечь один фрейм: -| Условие | Возврат | -|---------|---------| -| Фрейм найден (из буфера или MB) | `BSP_OK` | -| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | -| Невалидные параметры | `BSP_ERR_PARAM` | +| Условие | Возврат | +| ------------------------------- | ----------------- | +| Фрейм найден (из буфера или MB) | `BSP_OK` | +| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | +| Невалидные параметры | `BSP_ERR_PARAM` | ```c /* Неблокирующий опрос — timeout_ms = 0 */ @@ -161,10 +161,10 @@ bsp_can_register_rx_callback(can_isr_to_queue, NULL); Модуль не зависит от FreeRTOS и работает в обоих контекстах: -| Контекст | TX | RX | -|----------|----|----| -| bare-metal (`firmware/test`, HIL) | `bsp_can_send()` — blocking polling | `bsp_can_receive()` — polling | -| FreeRTOS (`firmware/tft_app`) | `bsp_can_send()` — из задачи | `bsp_can_receive()` — из задачи с `timeout_ms` | +| Контекст | TX | RX | +| --------------------------------- | ----------------------------------- | ---------------------------------------------- | +| bare-metal (`firmware/test`, HIL) | `bsp_can_send()` — blocking polling | `bsp_can_receive()` — polling | +| FreeRTOS (`firmware/tft_app`) | `bsp_can_send()` — из задачи | `bsp_can_receive()` — из задачи с `timeout_ms` | Для FreeRTOS с минимальной латентностью — будущий callback + `xQueueSendFromISR()`. Polling с `timeout_ms = 10` из задачи подходит для протоколов с интервалом > 10 мс. @@ -239,10 +239,10 @@ CAN-адаптер на стороне хоста — M5Stack с CAN-модул ## Зависимости -| Зависимость | Тип | Описание | -|-------------|-----|----------| -| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | -| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | -| `ring_buffer` | PRIVATE | Внутренний RX FIFO | -| `sdk_flexcan` | PRIVATE | `fsl_flexcan.h` — FlexCAN2 SDK драйвер | -| `clock_config` | PRIVATE | `BOARD_BOOTCLOCKRUN_CAN_CLK_ROOT` | +| Зависимость | Тип | Описание | +| -------------- | ------- | -------------------------------------- | +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | +| `ring_buffer` | PRIVATE | Внутренний RX FIFO | +| `sdk_flexcan` | PRIVATE | `fsl_flexcan.h` — FlexCAN2 SDK драйвер | +| `clock_config` | PRIVATE | `BOARD_BOOTCLOCKRUN_CAN_CLK_ROOT` | diff --git a/bsp/display/CMakeLists.txt b/bsp/display/CMakeLists.txt index e69de29..b5bc476 100644 --- a/bsp/display/CMakeLists.txt +++ b/bsp/display/CMakeLists.txt @@ -0,0 +1,17 @@ +# bsp/display/CMakeLists.txt BSP ELCDIF RGB Display + +if(BUILD_TESTS_HOST) + return() +endif() + +add_library(bsp_display STATIC src/display.c) + +target_include_directories( + bsp_display + PUBLIC include/ + PRIVATE src/) + +target_link_libraries( + bsp_display + PUBLIC bsp_status + PRIVATE bsp_board sdk_elcdif) diff --git a/bsp/display/include/bsp/display.h b/bsp/display/include/bsp/display.h new file mode 100644 index 0000000..6058930 --- /dev/null +++ b/bsp/display/include/bsp/display.h @@ -0,0 +1,127 @@ +/** + * @file display.h + * @brief BSP: ELCDIF display driver — TFT4 / TFT7 / TFT8 / TFT10. + * + * Bare-metal compatible: callback-based FRAME_DONE notification. + * No FreeRTOS dependency in this layer. + */ + +#ifndef BSP_DISPLAY_DISPLAY_H_ +#define BSP_DISPLAY_DISPLAY_H_ + +#include "bsp/status.h" + +#include +#include + +/* ── Максимальные размеры (для статического выделения буферов) ───────── */ + +/** @brief Максимальная ширина среди поддерживаемых дисплеев (TFT7: 1024). */ +#define BSP_DISPLAY_MAX_WIDTH 1024U + +/** @brief Максимальная высота среди поддерживаемых дисплеев (TFT7/TFT8: 600). */ +#define BSP_DISPLAY_MAX_HEIGHT 600U + +/* ── Типы дисплеев ───────────────────────────────────────────────────── */ + +typedef enum bsp_display_type_e +{ + BSP_DISPLAY_TFT4 = 0U, /**< 480 × 272. Нет ножек ориентации/MODE/DITHB. */ + BSP_DISPLAY_TFT7, /**< 1024 × 600. LR + UD + MODE + DITHB. */ + BSP_DISPLAY_TFT8, /**< 800 × 600. LR + UD + MODE + DITHB. */ + BSP_DISPLAY_TFT10, /**< Зарезервировано — спецификации уточняются. */ + BSP_DISPLAY_COUNT, +} bsp_display_type_t; + +/* ── Ориентация ──────────────────────────────────────────────────────── */ + +typedef enum bsp_display_rotation_e +{ + BSP_DISPLAY_ROTATE_0 = 0U, /**< Горизонтальная (нормальная). LR=1 UD=0. */ + BSP_DISPLAY_ROTATE_90, /**< Вертикальная CW 90°. LR=1 UD=1. */ + BSP_DISPLAY_ROTATE_180, /**< Горизонтальная перевёрнутая. LR=0 UD=1. */ + BSP_DISPLAY_ROTATE_270, /**< Вертикальная CCW 90°. LR=0 UD=0. */ +} bsp_display_rotation_t; + +/* ── Размер дисплея ──────────────────────────────────────────────────── */ + +typedef struct bsp_display_size_s +{ + uint16_t width; + uint16_t height; +} bsp_display_size_t; + +/* ── Callback FRAME_DONE ─────────────────────────────────────────────── */ + +/** + * Вызывается из LCDIF ISR при завершении передачи кадра. + * + * Реализация ДОЛЖНА быть ISR-safe: + * — допустимо: запись в volatile bool, xSemaphoreGiveFromISR() и т.п. + * — запрещено: любые блокирующие вызовы. + */ +typedef void (*bsp_display_frame_cb_t)(void); + +/* ── API ─────────────────────────────────────────────────────────────── */ + +/** + * @brief Инициализировать ELCDIF для выбранного типа дисплея. + * + * Настраивает пиксельный клок, GPIO подсветки и ножки ориентации + * (TFT7/TFT8/TFT10), запускает ELCDIF в RGB-режиме, включает IRQ FRAME_DONE. + * + * @note TFT4 требует инициализации Video PLL — возвращает BSP_ERR_NOT_SUPPORTED + * до реализации (TODO). + * @note Вызывать однократно. Повторный вызов без deinit — no-op, не ошибка. + * + * @param type Тип дисплея. + * @param framebuffer_addr Физический адрес первого фреймбуфера. + * Должен быть выровнен по 64 байт, в NonCacheable SDRAM. + * @param on_frame_done ISR-safe callback по завершении кадра; NULL — без callback. + * @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED + */ +bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr, + bsp_display_frame_cb_t p_on_frame_done); + +/** + * @brief Остановить ELCDIF, выключить подсветку, деинициализировать. + * + * Безопасен при вызове до bsp_display_init() или повторно. + * Всегда возвращает BSP_OK. + */ +bsp_status_t bsp_display_deinit(void); + +/** + * @brief Переключить ориентацию через ножки LR/UD (TFT7/TFT8/TFT10). + * + * TFT4 поддерживает только ROTATE_0; остальные варианты → BSP_ERR_NOT_SUPPORTED. + * + * @return BSP_OK | BSP_ERR_NOT_SUPPORTED | BSP_ERR_INIT + */ +bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation); + +/** + * @brief Указать ELCDIF следующий фреймбуфер. + * + * Безопасен из ISR и из задачи. Переключение произойдёт аппаратно + * по окончании текущего кадра. + * + * @param framebuffer_addr Физический адрес следующего буфера. + */ +void bsp_display_set_next_buffer(uint32_t framebuffer_addr); + +/** + * @brief Вернуть реальные размеры активного дисплея. + * + * @return Указатель на bsp_display_size_t; NULL до вызова bsp_display_init(). + */ +const bsp_display_size_t *bsp_display_get_size(void); + +/** + * @brief Вернуть тип активного дисплея. + * + * @return BSP_DISPLAY_COUNT если не инициализирован. + */ +bsp_display_type_t bsp_display_get_type(void); + +#endif /* BSP_DISPLAY_DISPLAY_H_ */ \ No newline at end of file diff --git a/bsp/display/src/display.c b/bsp/display/src/display.c new file mode 100644 index 0000000..513945e --- /dev/null +++ b/bsp/display/src/display.c @@ -0,0 +1,414 @@ +/** + * @file display.c + * @brief BSP: ELCDIF display driver — TFT4 / TFT7 / TFT8 / TFT10. + * + * Пиксельный клок: + * TFT7 — PLL2 (mux=0), pre_div=2, div=4 → 528/3/5 = 35.2 МГц + * TFT8 — PLL2 (mux=0), pre_div=2, div=3 → 528/3/4 = 44.0 МГц + * TFT4 — Video PLL (mux=2) — TODO: требует CLOCK_InitVideoPll. + * Возвращает BSP_ERR_NOT_SUPPORTED до реализации. + * + * Управляющие GPIO (TFT7/TFT8/TFT10): + * LcdLed GPIO1[20] — подсветка, active-high + * Lcdlr_value GPIO1[28] — горизонтальный скан SHlr_value + * LcdMode GPIO1[29] — DE/SYNC mode: HIGH=DE mode (обязательно для ELCDIF) + * LcdUd GPIO1[30] — вертикальный скан UPDN + * LcdDithb GPIO1[31] — dithering bypass: HIGH=disable (IC default) + * + * IOMUXC конфигурируется в BOARD_InitPins() — здесь только GPIO_PinWrite. + * + * ISR LCDIF_IRQHandler размещён в ITCM (AT_QUICKACCESS_SECTION_CODE). + */ + +#include "bsp/display.h" + +#include "fsl_clock.h" +#include "fsl_elcdif.h" +#include "fsl_gpio.h" + +/* ── GPIO пины ───────────────────────────────────────────────────────── */ + +#define DISPLAY_BACKLIGHT_GPIO GPIO1 +#define DISPLAY_BACKLIGHT_PIN 20U + +#define DISPLAY_lr_value_GPIO GPIO1 +#define DISPLAY_lr_value_PIN 28U +#define DISPLAY_MODE_GPIO GPIO1 +#define DISPLAY_MODE_PIN 29U +#define DISPLAY_UD_GPIO GPIO1 +#define DISPLAY_UD_PIN 30U +#define DISPLAY_DITHB_GPIO GPIO1 +#define DISPLAY_DITHB_PIN 31U + +/* ── Делители пиксельного клока (PLL2 = 528 МГц) ───────────────────── */ + +#define DISPLAY_CLK_MUX_PLL2 0U /**< kCLOCK_LcdifPreMux = PLL2. */ +#define DISPLAY_CLK_MUX_VIDEO_PLL 2U /**< kCLOCK_LcdifPreMux = Video PLL (TFT4). */ + +#define DISPLAY_TFT7_CLK_PREDIV 2U /**< ÷3 → 528/3 = 176 МГц. */ +#define DISPLAY_TFT7_CLK_DIV 4U /**< ÷5 → 176/5 = 35.2 МГц. */ +#define DISPLAY_TFT8_CLK_PREDIV 2U /**< ÷3 → 528/3 = 176 МГц. */ +#define DISPLAY_TFT8_CLK_DIV 3U /**< ÷4 → 176/4 = 44.0 МГц. */ + +/* ── Тайминги TFT4 (480 × 272) ───────────────────────────────────────── */ + +#define DISPLAY_TFT4_W 480U +#define DISPLAY_TFT4_H 272U +#define DISPLAY_TFT4_HSW 41U +#define DISPLAY_TFT4_HFP 2U +#define DISPLAY_TFT4_HBP 2U +#define DISPLAY_TFT4_VSW 10U +#define DISPLAY_TFT4_VFP 2U +#define DISPLAY_TFT4_VBP 2U + +/* ── Тайминги TFT7 (1024 × 600) ──────────────────────────────────────── */ + +#define DISPLAY_TFT7_W 1024U +#define DISPLAY_TFT7_H 600U +#define DISPLAY_TFT7_HSW 1U +#define DISPLAY_TFT7_HFP 210U +#define DISPLAY_TFT7_HBP 46U +#define DISPLAY_TFT7_VSW 1U +#define DISPLAY_TFT7_VFP 12U +#define DISPLAY_TFT7_VBP 23U + +/* ── Тайминги TFT8 (800 × 600) ───────────────────────────────────────── */ + +#define DISPLAY_TFT8_W 800U +#define DISPLAY_TFT8_H 600U +#define DISPLAY_TFT8_HSW 1U +#define DISPLAY_TFT8_HFP 210U +#define DISPLAY_TFT8_HBP 46U +#define DISPLAY_TFT8_VSW 1U +#define DISPLAY_TFT8_VFP 12U +#define DISPLAY_TFT8_VBP 23U + +/* ── Полярность (одинакова для всех трёх дисплеев) ───────────────────── */ + +#define DISPLAY_POL_FLAGS \ + (kELCDIF_DataEnableActiveHigh | kELCDIF_VsyncActiveLow | kELCDIF_HsyncActiveLow | \ + kELCDIF_DriveDataOnRisingClkEdge) + +/* ── IRQ приоритет ───────────────────────────────────────────────────── */ + +#define DISPLAY_IRQ_PRIORITY 2U + +/* ── Внутренние типы ─────────────────────────────────────────────────── */ + +typedef struct display_hw_cfg_s +{ + uint16_t width; + uint16_t height; + uint8_t hsw; + uint8_t hfp; + uint8_t hbp; + uint8_t vsw; + uint8_t vfp; + uint8_t vbp; + uint32_t pol_flags; + uint8_t clk_mux; + uint8_t clk_pre_div; + uint8_t clk_div; + bool has_orientation_pins; /**< false = TFT4. */ +} display_hw_cfg_t; + +typedef struct display_state_s +{ + bsp_display_type_t type; + bsp_display_size_t size; + bsp_display_frame_cb_t frame_cb; + bool initialised; +} display_state_t; + +/* ── Таблица конфигураций ─────────────────────────────────────────────── */ + +static const display_hw_cfg_t K_HW_CFG[BSP_DISPLAY_COUNT] = { + + [BSP_DISPLAY_TFT4] = { + .width = DISPLAY_TFT4_W, + .height = DISPLAY_TFT4_H, + .hsw = DISPLAY_TFT4_HSW, + .hfp = DISPLAY_TFT4_HFP, + .hbp = DISPLAY_TFT4_HBP, + .vsw = DISPLAY_TFT4_VSW, + .vfp = DISPLAY_TFT4_VFP, + .vbp = DISPLAY_TFT4_VBP, + .pol_flags = DISPLAY_POL_FLAGS, + .clk_mux = DISPLAY_CLK_MUX_VIDEO_PLL, + .clk_pre_div = 0U, /* placeholder — Video PLL TODO */ + .clk_div = 0U, + .has_orientation_pins = false, + }, + + [BSP_DISPLAY_TFT7] = { + .width = DISPLAY_TFT7_W, + .height = DISPLAY_TFT7_H, + .hsw = DISPLAY_TFT7_HSW, + .hfp = DISPLAY_TFT7_HFP, + .hbp = DISPLAY_TFT7_HBP, + .vsw = DISPLAY_TFT7_VSW, + .vfp = DISPLAY_TFT7_VFP, + .vbp = DISPLAY_TFT7_VBP, + .pol_flags = DISPLAY_POL_FLAGS, + .clk_mux = DISPLAY_CLK_MUX_PLL2, + .clk_pre_div = DISPLAY_TFT7_CLK_PREDIV, + .clk_div = DISPLAY_TFT7_CLK_DIV, + .has_orientation_pins = true, + }, + + [BSP_DISPLAY_TFT8] = { + .width = DISPLAY_TFT8_W, + .height = DISPLAY_TFT8_H, + .hsw = DISPLAY_TFT8_HSW, + .hfp = DISPLAY_TFT8_HFP, + .hbp = DISPLAY_TFT8_HBP, + .vsw = DISPLAY_TFT8_VSW, + .vfp = DISPLAY_TFT8_VFP, + .vbp = DISPLAY_TFT8_VBP, + .pol_flags = DISPLAY_POL_FLAGS, + .clk_mux = DISPLAY_CLK_MUX_PLL2, + .clk_pre_div = DISPLAY_TFT8_CLK_PREDIV, + .clk_div = DISPLAY_TFT8_CLK_DIV, + .has_orientation_pins = true, + }, + + /* TFT10: спецификации не определены — TODO */ + [BSP_DISPLAY_TFT10] = { 0 }, +}; + +/* ── Состояние модуля ─────────────────────────────────────────────────── */ + +static display_state_t g_s_display = { + .type = BSP_DISPLAY_COUNT, + .frame_cb = NULL, + .initialised = false, +}; + +/* ── ISR ─────────────────────────────────────────────────────────────── */ + +/* + * ISR размещён в ITCM для минимальной задержки и исключения кэш-промахов. + * Только вызывает зарегистрированный callback — не трогает состояние. + */ +// NOLINTNEXTLINE(readability-identifier-naming) — SDK-mandated ISR symbol +AT_QUICKACCESS_SECTION_CODE(void LCDIF_IRQHandler(void)); +void LCDIF_IRQHandler(void) // NOLINT(readability-identifier-naming) +{ + uint32_t flags = ELCDIF_GetInterruptStatus(LCDIF); + ELCDIF_ClearInterruptStatus(LCDIF, flags); + + if (((flags & (uint32_t) kELCDIF_CurFrameDone) != 0U) && (g_s_display.frame_cb != NULL)) + { + g_s_display.frame_cb(); + } + + __DSB(); +} + +/* ── Вспомогательные функции ─────────────────────────────────────────── */ + +/** + * @brief Настроить делители пиксельного клока. + * + * TFT4 требует инициализации Video PLL (TODO) — возвращает ошибку. + * TFT7/TFT8 используют PLL2 (528 МГц), который уже инициализирован + * в BOARD_BootClockRUN(). + */ +static bsp_status_t init_pixelclock(const display_hw_cfg_t *p_cfg) +{ + if (p_cfg->clk_mux == DISPLAY_CLK_MUX_VIDEO_PLL) + { + /* TODO: CLOCK_InitVideoPll() для TFT4. + * Video PLL деинициализирован в BOARD_BootClockRUN() — нужна + * дополнительная инициализация перед использованием. */ + return BSP_ERR_NOT_SUPPORTED; + } + + CLOCK_SetMux(kCLOCK_LcdifPreMux, p_cfg->clk_mux); + CLOCK_SetDiv(kCLOCK_LcdifPreDiv, p_cfg->clk_pre_div); + CLOCK_SetDiv(kCLOCK_LcdifDiv, p_cfg->clk_div); + return BSP_OK; +} + +/** @brief Включить подсветку (GPIO1[20] = HIGH). */ +static void init_backlight(void) +{ + GPIO_PinWrite(DISPLAY_BACKLIGHT_GPIO, DISPLAY_BACKLIGHT_PIN, 1U); +} + +/** + * @brief Установить ножки lr_value/UD в состояние ROTATE_0 по умолчанию. + * + * Итоговая ориентация задаётся вызовом bsp_display_set_rotation() + * после bsp_display_init(). + */ +static void init_orientation_pins(void) +{ + GPIO_PinWrite(DISPLAY_lr_value_GPIO, DISPLAY_lr_value_PIN, 1U); /* ROTATE_0: lr_value=1 */ + GPIO_PinWrite(DISPLAY_UD_GPIO, DISPLAY_UD_PIN, 0U); /* ROTATE_0: UD=0 */ +} + +/** + * @brief Инициализировать MODE и DITHB пины контроллера HX8264-D02. + * + * MODE=1: DE mode (обязательно для ELCDIF, использующего DE/enable сигнал). + * DITHB=1: отключить дизеринг (IC default согласно даташиту, "normally pull high"). + * + * Вызывается только для дисплеев с has_orientation_pins = true. + */ +static void init_mode_dither_pins(void) +{ + GPIO_PinWrite(DISPLAY_MODE_GPIO, DISPLAY_MODE_PIN, 1U); /* MODE=1: DE mode */ + GPIO_PinWrite(DISPLAY_DITHB_GPIO, DISPLAY_DITHB_PIN, 1U); /* DITHB=1: disable */ +} + +/** @brief Включить IRQ FRAME_DONE с фиксированным приоритетом. */ +static void enable_lcd_interrupt(void) +{ + NVIC_SetPriority(LCDIF_IRQn, DISPLAY_IRQ_PRIORITY); + EnableIRQ(LCDIF_IRQn); + ELCDIF_EnableInterrupts(LCDIF, kELCDIF_CurFrameDoneInterruptEnable); +} + +/** @brief Заполнить конфигурацию ELCDIF из таблицы + адрес буфера. */ +static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer_addr, + elcdif_rgb_mode_config_t *p_out) +{ + p_out->panelWidth = p_cfg->width; + p_out->panelHeight = p_cfg->height; + p_out->hsw = p_cfg->hsw; + p_out->hfp = p_cfg->hfp; + p_out->hbp = p_cfg->hbp; + p_out->vsw = p_cfg->vsw; + p_out->vfp = p_cfg->vfp; + p_out->vbp = p_cfg->vbp; + p_out->polarityFlags = p_cfg->pol_flags; + p_out->bufferAddr = framebuffer_addr; + p_out->pixelFormat = kELCDIF_PixelFormatXRGB8888; + p_out->dataBus = kELCDIF_DataBus24Bit; +} + +/* ── Реализация API ───────────────────────────────────────────────────── */ + +bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr, + bsp_display_frame_cb_t p_on_frame_done) +{ + if ((uint32_t) type >= (uint32_t) BSP_DISPLAY_COUNT) + { + return BSP_ERR_PARAM; + } + + if (g_s_display.initialised) + { + return BSP_OK; /* идемпотентен */ + } + + const display_hw_cfg_t *p_cfg = &K_HW_CFG[type]; + + bsp_status_t status = init_pixelclock(p_cfg); + if (status != BSP_OK) + { + return status; + } + + CLOCK_EnableClock(kCLOCK_LcdPixel); + init_backlight(); + + if (p_cfg->has_orientation_pins) + { + init_orientation_pins(); + init_mode_dither_pins(); + } + + /* Сохранить callback ДО включения IRQ — исключить гонку */ + g_s_display.frame_cb = p_on_frame_done; + + elcdif_rgb_mode_config_t elcdif_cfg; + build_elcdif_cfg(p_cfg, framebuffer_addr, &elcdif_cfg); + ELCDIF_RgbModeInit(LCDIF, &elcdif_cfg); + enable_lcd_interrupt(); + ELCDIF_RgbModeStart(LCDIF); + + g_s_display.type = type; + g_s_display.size.width = p_cfg->width; + g_s_display.size.height = p_cfg->height; + g_s_display.initialised = true; + + return BSP_OK; +} + +bsp_status_t bsp_display_deinit(void) +{ + if (g_s_display.initialised) + { + ELCDIF_RgbModeStop(LCDIF); + DisableIRQ(LCDIF_IRQn); + ELCDIF_Deinit(LCDIF); + CLOCK_DisableClock(kCLOCK_LcdPixel); + GPIO_PinWrite(DISPLAY_BACKLIGHT_GPIO, DISPLAY_BACKLIGHT_PIN, 0U); + + g_s_display.frame_cb = NULL; + g_s_display.type = BSP_DISPLAY_COUNT; + g_s_display.initialised = false; + } + return BSP_OK; +} + +bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation) +{ + if (!g_s_display.initialised) + { + return BSP_ERR_INIT; + } + + const display_hw_cfg_t *p_cfg = &K_HW_CFG[g_s_display.type]; + + if (!p_cfg->has_orientation_pins) + { + return (rotation == BSP_DISPLAY_ROTATE_0) ? BSP_OK : BSP_ERR_NOT_SUPPORTED; + } + + uint8_t lr_value; + uint8_t ud_value; + + switch (rotation) + { + case BSP_DISPLAY_ROTATE_0: + lr_value = 1U; + ud_value = 0U; + break; + case BSP_DISPLAY_ROTATE_90: + lr_value = 1U; + ud_value = 1U; + break; + case BSP_DISPLAY_ROTATE_180: + lr_value = 0U; + ud_value = 1U; + break; + case BSP_DISPLAY_ROTATE_270: + lr_value = 0U; + ud_value = 0U; + break; + default: + return BSP_ERR_PARAM; + } + + GPIO_PinWrite(DISPLAY_lr_value_GPIO, DISPLAY_lr_value_PIN, lr_value); + GPIO_PinWrite(DISPLAY_UD_GPIO, DISPLAY_UD_PIN, ud_value); + return BSP_OK; +} + +void bsp_display_set_next_buffer(uint32_t framebuffer_addr) +{ + ELCDIF_SetNextBufferAddr(LCDIF, framebuffer_addr); +} + +const bsp_display_size_t *bsp_display_get_size(void) +{ + return g_s_display.initialised ? &g_s_display.size : NULL; +} + +bsp_display_type_t bsp_display_get_type(void) +{ + return g_s_display.type; +} \ No newline at end of file diff --git a/bsp/generated/README.md b/bsp/generated/README.md index d1664f9..bfa2c3d 100644 --- a/bsp/generated/README.md +++ b/bsp/generated/README.md @@ -43,12 +43,12 @@ bsp/ Внешний кварц **24 МГц**. Режим максимальной производительности (внешнее питание, энергопотребление не критично). -| Параметр | Значение | -|---|---| -| ARM PLL (PLL1) | 1200 МГц (VDIV = 50) | -| CPU / AHB | 600 МГц (ARM_PODF /2, AHB_PODF /1) | -| IPG | 150 МГц (IPG_PODF /4) | -| PLL1 bypass clock | REF_CLK_24M (внешний кварц) | +| Параметр | Значение | +| ------------------------ | ----------------------------------------- | +| ARM PLL (PLL1) | 1200 МГц (VDIV = 50) | +| CPU / AHB | 600 МГц (ARM_PODF /2, AHB_PODF /1) | +| IPG | 150 МГц (IPG_PODF /4) | +| PLL1 bypass clock | REF_CLK_24M (внешний кварц) | | Внутренний RC осциллятор | включён (используется BootROM при старте) | **SEMC и FlexSPI** в Clocks Tool настроены формально — реальная инициализация выполняется через **DCD** (SDRAM) и **FDCB** (QSPI Flash) до передачи управления прикладному коду. Для корректной сборки необходимы следующие дефайны: diff --git a/bsp/led/README.md b/bsp/led/README.md index b2fc7db..6bce6e4 100644 --- a/bsp/led/README.md +++ b/bsp/led/README.md @@ -7,9 +7,9 @@ ## Аппаратная часть | `led_id_t` | Сигнал | GPIO | Pin | Координата | Активный уровень | -|-----------------|------------|-------|-----|------------|-----------------| -| `LED_HEARTBEAT` | `UserLed1` | GPIO3 | 3 | M4 | LOW (0 = горит) | -| `LED_APP` | `UserLed2` | GPIO3 | 4 | P2 | LOW (0 = горит) | +| --------------- | ---------- | ----- | --- | ---------- | ---------------- | +| `LED_HEARTBEAT` | `UserLed1` | GPIO3 | 3 | M4 | LOW (0 = горит) | +| `LED_APP` | `UserLed2` | GPIO3 | 4 | P2 | LOW (0 = горит) | Пины сконфигурированы в `generated/pin_mux.h` (MCUXpresso Config Tools). `INIT_GPIO_VALUE = 1U` — оба LED выключены сразу после `led_init()`. diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h index e0b98a8..dedcff8 100644 --- a/bsp/opto/include/bsp/opto.h +++ b/bsp/opto/include/bsp/opto.h @@ -198,7 +198,7 @@ extern "C" * Должна вызываться из main loop на каждой итерации. НЕ вызывать из ISR. * Каналы MODE_PROTO пропускаются — для них используется коллбэк из ISR. */ - //FIXME: poll + //FIXME: poll void bsp_opto_process(void); /** diff --git a/bsp/qspi_flash/REFACTORING.md b/bsp/qspi_flash/REFACTORING.md deleted file mode 100644 index c622b83..0000000 --- a/bsp/qspi_flash/REFACTORING.md +++ /dev/null @@ -1,26 +0,0 @@ -Нашёл и закрыл основную причину HardFault. - -Что было не так - -Ваша CodeQuickAccess/.ram_function секция не копировалась в ITCM при старте, потому что для startup_MIMXRT1052.S не были заданы макросы __STARTUP_INITIALIZE_RAMFUNCTION (и __STARTUP_CLEAR_BSS). -SysTick включался до bsp_qspi_init(), а в драйвере не было маскирования IRQ на время FlexSPI IP-операций, что опасно при XIP с той же флешки. -Что я изменил - -Добавил startup-дефайны в firmware/test/CMakeLists.txt (line 20): - -- `__STARTUP_INITIALIZE_RAMFUNCTION` -- `__STARTUP_CLEAR_BSS` -- -Переставил порядок инициализации в firmware/test/src/main.c (line 38): -bsp_qspi_init() теперь до bsp_tick_init(). - -Усилил XIP-safe поведение в bsp/qspi_flash/src/qspi_flash.c (line 205): -добавил qspi_irq_lock()/qspi_irq_unlock(); - -обернул критичные операции (init/read/write/erase) под IRQ lock; -публичные QSPI API размещены в CodeQuickAccess (ITCM). - - -Нашёл вероятный корень: в драйвере жёстко зашит watermark = 8 bytes, а в железе он может быть другим, из-за чего TX/RX FIFO-петли начинают висеть. Сейчас внесу фикс: динамический watermark из регистров + ожидание IPCMDDONE как в fsl_flexspi. - -Да, это мой хвост после рефактора watermark-констант. Сейчас поправлю qspi_write_fifo на динамический watermark и сразу проверю сборку, чтобы не оставить таких разрывов. \ No newline at end of file diff --git a/bsp/sd/README.md b/bsp/sd/README.md index c275601..6cb27b4 100644 --- a/bsp/sd/README.md +++ b/bsp/sd/README.md @@ -38,13 +38,13 @@ sdk_usdhc ← NXP HAL: fsl_usdhc ## Аппаратный контекст -| Сигнал | Пин MCU | Конфигурация | -|----------|-----------------|---------------------------------------------------| -| CLK | GPIO_SD_B0_01 | USDHC1_CLK, периферийный режим | -| CMD | GPIO_SD_B0_00 | USDHC1_CMD, периферийный режим | -| D0–D3 | GPIO_SD_B0_02–05| USDHC1_DATA0–3, периферийный режим | -| CD_B | GPIO_B1_12 | USDHC1_CD_B — детект через GPIO2[28] | -| SdPwr | GPIO_AD_B1_03 | GPIO1[19], active-low, управляется SDK через BSP | +| Сигнал | Пин MCU | Конфигурация | +| ------ | ---------------- | ------------------------------------------------ | +| CLK | GPIO_SD_B0_01 | USDHC1_CLK, периферийный режим | +| CMD | GPIO_SD_B0_00 | USDHC1_CMD, периферийный режим | +| D0–D3 | GPIO_SD_B0_02–05 | USDHC1_DATA0–3, периферийный режим | +| CD_B | GPIO_B1_12 | USDHC1_CD_B — детект через GPIO2[28] | +| SdPwr | GPIO_AD_B1_03 | GPIO1[19], active-low, управляется SDK через BSP | **CD_B** подключён как периферийный сигнал USDHC1, а не как GPIO. Детект карты читается через `USDHC_GetPresentStatusFlags` → `kUSDHC_CardInsertedFlag`. @@ -92,13 +92,13 @@ host-контроллер (`SD_HostInit`). ## Разделение ответственности: bsp_sd vs sdmmc_config vs port_fatfs_sd -| Слой | Что делает | Где живёт | -|-----------------------|---------------------------------------------------------|------------------------------------| -| `sdmmc_config` | Константы платы, `BOARD_SD_Config`, GPIO питания, pads | `bsp/generated/` | -| `bsp_sd` | `SD_HostInit/Deinit`, идемпотентность, card detect | `bsp/sd/` | -| `port_fatfs_sd` | `microsd_disk_*` → `fsl_sd_disk` (FatFS diskio glue) | `port/fatfs/sd/` | -| `firmware_test_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (bare-metal ffconf) | `firmware/test/fatfs/` | -| `tft_app_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (FreeRTOS ffconf) | `firmware/tft_app/fatfs/` (будущее)| +| Слой | Что делает | Где живёт | +| --------------------- | ------------------------------------------------------- | ----------------------------------- | +| `sdmmc_config` | Константы платы, `BOARD_SD_Config`, GPIO питания, pads | `bsp/generated/` | +| `bsp_sd` | `SD_HostInit/Deinit`, идемпотентность, card detect | `bsp/sd/` | +| `port_fatfs_sd` | `microsd_disk_*` → `fsl_sd_disk` (FatFS diskio glue) | `port/fatfs/sd/` | +| `firmware_test_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (bare-metal ffconf) | `firmware/test/fatfs/` | +| `tft_app_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (FreeRTOS ffconf) | `firmware/tft_app/fatfs/` (будущее) | **Почему `ff.c` и `fsl_sd_disk.c` не компилируются один раз как общая библиотека:** оба включают `ff.h` → `ffconf.h`, который разный для `firmware_test` (bare-metal, diff --git a/bsp/sdram/README.md b/bsp/sdram/README.md index 23dd743..e69de29 100644 --- a/bsp/sdram/README.md +++ b/bsp/sdram/README.md @@ -1,3 +0,0 @@ -# Вопросы - -> Да, про HIL в SDRAM точно подмечено: мы грузим в RAM elf, а не подготовленные с помощью nxpimage hab образы. То есть, dcd у нас не вшит и BootROM его не прочитает, тем более, что при загрузке в RAM теста, насколько я понимаю мы вообще игнорируем стадию bootROM и заставляем контроллер прыгнуть на предопределенный нами ProgrammCounter. Поправь меня если я ошибаюсь в своих выводах. Мне видится вариант 1 самым логичным и корректным. Главное понимать, что частый прогон HIL теста SDRAM будет изнашивать Flash -память - то есть этот тест - не совсем частая история. Тут же возникает другой вопрос - firmware_test должен быть написанным полностью? Понятно, что можно реализовать только ту его часть, что отвечает за SDRAM тесты. Но тут ведь тоже есть нюанс - мы установили, что firmware_test связывается с хостом по USB-CDC, этот момент тоже надо учитывать, и похоже он ломает всю парадигму наших HIL тестов. Отсюда я заключаю: возможно не стоит делать HIL тест для SDRAM, а нужно просто заложить возможность проведения длительных тестов на производстве при загрузке firmware_test на таргет. По умолчанию будут прогоняться быстрые тесты, но по желанию оператора, либо раз в десять устройств будет выполняться долгий тест (что думаешь на этот счет?) diff --git a/bsp/sdram/include/bsp/sdram.h b/bsp/sdram/include/bsp/sdram.h index bb5a7c7..6dfd311 100644 --- a/bsp/sdram/include/bsp/sdram.h +++ b/bsp/sdram/include/bsp/sdram.h @@ -32,8 +32,6 @@ #include "bsp/status.h" -#include - /** * @brief Размер расширенного тестового региона, байт (27 MB). * diff --git a/bsp/uart_host/README.md b/bsp/uart_host/README.md index 79e0811..c9b1afd 100644 --- a/bsp/uart_host/README.md +++ b/bsp/uart_host/README.md @@ -59,11 +59,11 @@ target_compile_definitions(firmware_test PRIVATE ) ``` -| Define | Дефолт | Описание | -|--------|--------|----------| -| `BSP_UART_HOST_RX_BUFFER_SIZE` | `256` | Размер RX ring buffer. **Должен быть степенью двойки.** | -| `BSP_UART_HOST_SRC_CLOCK_HZ` | `24000000` | Частота источника тактирования LPUART1. | -| `BSP_UART_HOST_IRQ_PRIORITY` | `5` | Приоритет `LPUART1_IRQn`. Должен быть ≥ `configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS. | +| Define | Дефолт | Описание | +| ------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------- | +| `BSP_UART_HOST_RX_BUFFER_SIZE` | `256` | Размер RX ring buffer. **Должен быть степенью двойки.** | +| `BSP_UART_HOST_SRC_CLOCK_HZ` | `24000000` | Частота источника тактирования LPUART1. | +| `BSP_UART_HOST_IRQ_PRIORITY` | `5` | Приоритет `LPUART1_IRQn`. Должен быть ≥ `configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS. | --- @@ -156,9 +156,9 @@ void test_something(void) { ## Зависимости -| Зависимость | Тип | Описание | -|-------------|-----|----------| -| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | -| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | -| `utils` (ring_buffer) | PRIVATE | RX ring buffer | -| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` | \ No newline at end of file +| Зависимость | Тип | Описание | +| --------------------- | ------- | --------------------------------- | +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | +| `utils` (ring_buffer) | PRIVATE | RX ring buffer | +| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` | \ No newline at end of file diff --git a/bsp/usb_cdc/README.md b/bsp/usb_cdc/README.md index 477555f..3765c94 100644 --- a/bsp/usb_cdc/README.md +++ b/bsp/usb_cdc/README.md @@ -11,11 +11,11 @@ CLI команды, обновление конфигурации. Работа ## Аппаратура -| Сигнал | Пин MCU | Назначение | -|--------------|----------------|----------------------------------| -| USB_OTG1_DN | USB_OTG1_DN | USB1 Data− | -| USB_OTG1_DP | USB_OTG1_DP | USB1 Data+ | -| USB_OTG1_VBUS| USB_OTG1_VBUS | VBUS detect (self-powered) | +| Сигнал | Пин MCU | Назначение | +| ------------- | ------------- | -------------------------- | +| USB_OTG1_DN | USB_OTG1_DN | USB1 Data− | +| USB_OTG1_DP | USB_OTG1_DP | USB1 Data+ | +| USB_OTG1_VBUS | USB_OTG1_VBUS | VBUS detect (self-powered) | Встроенный HS PHY (480 MHz PLL). Контроллер: EHCI0 (`kUSB_ControllerEhci0`). Скорость: High-Speed (480 Mbit/s) при поддержке хоста, fallback Full-Speed (12 Mbit/s). @@ -60,14 +60,14 @@ Host → USB1_DP/DN → [EHCI0 DMA] Модуль использует **lite** вариант NXP USB стека (не full class framework). Это сознательное решение: -| Аспект | Full stack | Lite stack (наш выбор) | -|--------|-----------|----------------------| -| Class framework | `usb_device_class.h`, `class_handle_t` | Отсутствует | -| `usb_device_ch9.c` | SDK middleware, тянет class driver | Приватная копия в `src/` | -| CDC ACM хедер | Полный: struct + API функции | Только define-ы request codes | -| Callbacks | Через class driver dispatch | Напрямую в `usb_cdc.c` | -| Размер кода | ~12 KB | ~6 KB | -| Гибкость | Multi-class composite | Один CDC ACM | +| Аспект | Full stack | Lite stack (наш выбор) | +| ------------------ | -------------------------------------- | ----------------------------- | +| Class framework | `usb_device_class.h`, `class_handle_t` | Отсутствует | +| `usb_device_ch9.c` | SDK middleware, тянет class driver | Приватная копия в `src/` | +| CDC ACM хедер | Полный: struct + API функции | Только define-ы request codes | +| Callbacks | Через class driver dispatch | Напрямую в `usb_cdc.c` | +| Размер кода | ~12 KB | ~6 KB | +| Гибкость | Multi-class composite | Один CDC ACM | Lite stack достаточен для одного CDC ACM интерфейса. Переход на full stack понадобится только при добавлении composite device (CDC + MSC). @@ -169,12 +169,12 @@ NVIC enable → USB_DeviceRun. Включает задержку 5 мс для Неблокирующая отправка. Копирует данные в NonCacheable TX буфер и ставит в очередь USB IN transfer. Максимум `BSP_USB_CDC_MAX_PACKET_SIZE` (512) байт за вызов. -| Возврат | Условие | -|---------|---------| -| `BSP_OK` | Transfer поставлен в очередь | -| `BSP_ERR_BUSY` | Предыдущий transfer не завершён | -| `BSP_ERR_NOT_READY` | Хост не подключён | -| `BSP_ERR_INVALID` | `data == NULL`, `len == 0` или `len > 512` | +| Возврат | Условие | +| ------------------- | ------------------------------------------ | +| `BSP_OK` | Transfer поставлен в очередь | +| `BSP_ERR_BUSY` | Предыдущий transfer не завершён | +| `BSP_ERR_NOT_READY` | Хост не подключён | +| `BSP_ERR_INVALID` | `data == NULL`, `len == 0` или `len > 512` | Проверить готовность TX канала перед отправкой: `bsp_usb_cdc_write_ready()`. @@ -250,10 +250,10 @@ USB_OTG1_IRQHandler (usb_cdc_hw.c) Модуль работает без изменений в контексте FreeRTOS-задачи: -| Контекст | TX | RX | -|----------|----|----| -| bare-metal | `bsp_usb_cdc_write()` — non-blocking | `bsp_usb_cdc_read()` — polling | -| FreeRTOS | Из задачи, `write_ready()` + `vTaskDelay()` | Из задачи с yield | +| Контекст | TX | RX | +| ---------- | ------------------------------------------- | ------------------------------ | +| bare-metal | `bsp_usb_cdc_write()` — non-blocking | `bsp_usb_cdc_read()` — polling | +| FreeRTOS | Из задачи, `write_ready()` + `vTaskDelay()` | Из задачи с yield | Для минимальной латентности в FreeRTOS — будущий `USB_DEVICE_CONFIG_USE_TASK=1` с `bsp_usb_cdc_poll()` из выделенной задачи. @@ -295,9 +295,9 @@ just host::hil-usb-cdc Команды CLI прошивки: -| Команда | Ответ | Описание | -|---------|-------|----------| -| `PING` | `PONG` | Проверка канала | +| Команда | Ответ | Описание | +| ------------- | -------- | ---------------- | +| `PING` | `PONG` | Проверка канала | | `ECHO ` | `` | Echo-back данных | ### Host unit-тесты @@ -311,15 +311,15 @@ just host::hil-usb-cdc Все настройки находятся в приватных хедерах `src/`: -| Файл | Настройка | Значение | Описание | -|------|-----------|----------|----------| -| `usb_device_config.h` | `USB_DEVICE_CONFIG_EHCI` | `1` | Контроллер EHCI0 | -| `usb_device_config.h` | `USB_DEVICE_CONFIG_ENDPOINTS` | `4` | EP0 + interrupt IN + bulk IN/OUT | -| `usb_device_config.h` | `USB_DEVICE_CONFIG_SELF_POWER` | `1` | Self-powered device | -| `usb_device_descriptor.h` | `USB_DEVICE_VID` | `0x1234` | Vendor ID (placeholder) | -| `usb_device_descriptor.h` | `USB_DEVICE_PID` | `0x0001` | Product ID (placeholder) | -| `usb_cdc_hw.c` | `USB_DEVICE_INTERRUPT_PRIORITY` | `3` | NVIC приоритет | -| `usb_cdc_hw.c` | `BOARD_USB_PHY_D_CAL` | `0x0C` | PHY калибровка | +| Файл | Настройка | Значение | Описание | +| ------------------------- | ------------------------------- | -------- | -------------------------------- | +| `usb_device_config.h` | `USB_DEVICE_CONFIG_EHCI` | `1` | Контроллер EHCI0 | +| `usb_device_config.h` | `USB_DEVICE_CONFIG_ENDPOINTS` | `4` | EP0 + interrupt IN + bulk IN/OUT | +| `usb_device_config.h` | `USB_DEVICE_CONFIG_SELF_POWER` | `1` | Self-powered device | +| `usb_device_descriptor.h` | `USB_DEVICE_VID` | `0x1234` | Vendor ID (placeholder) | +| `usb_device_descriptor.h` | `USB_DEVICE_PID` | `0x0001` | Product ID (placeholder) | +| `usb_cdc_hw.c` | `USB_DEVICE_INTERRUPT_PRIORITY` | `3` | NVIC приоритет | +| `usb_cdc_hw.c` | `BOARD_USB_PHY_D_CAL` | `0x0C` | PHY калибровка | --- @@ -348,15 +348,15 @@ bsp/usb_cdc/ ## Зависимости -| Зависимость | Тип | Описание | -|-------------|-----|----------| -| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | -| `bsp_board` | PRIVATE | Транзитивно: `clock_config.h`, `pin_mux.h`, SDK headers | -| `sdk_usb_device_ehci` | PRIVATE | EHCI контроллер + DCI абстракция | -| `sdk_usb_phy` | PRIVATE | USB PHY инициализация (480 MHz PLL) | -| `sdk_osa_bm` | PRIVATE | OS Abstraction Layer (bare-metal, generic list) | -| `sdk_usb_common` | PRIVATE (транзитивно) | USB common headers (`usb.h`, `usb_misc.h`) | -| `sdk_usb_config` | PRIVATE (транзитивно) | INTERFACE: проброс конфиг-хедеров в SDK | +| Зависимость | Тип | Описание | +| --------------------- | --------------------- | ------------------------------------------------------- | +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_board` | PRIVATE | Транзитивно: `clock_config.h`, `pin_mux.h`, SDK headers | +| `sdk_usb_device_ehci` | PRIVATE | EHCI контроллер + DCI абстракция | +| `sdk_usb_phy` | PRIVATE | USB PHY инициализация (480 MHz PLL) | +| `sdk_osa_bm` | PRIVATE | OS Abstraction Layer (bare-metal, generic list) | +| `sdk_usb_common` | PRIVATE (транзитивно) | USB common headers (`usb.h`, `usb_misc.h`) | +| `sdk_usb_config` | PRIVATE (транзитивно) | INTERFACE: проброс конфиг-хедеров в SDK | ### Зависимости на уровне SDK CMake diff --git a/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld b/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld index e798502..2b2d68e 100644 --- a/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld +++ b/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld @@ -29,7 +29,7 @@ ENTRY(Reset_Handler) HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x0400; STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x0400; VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0; -NCACHE_HEAP_START = DEFINED(__heap_noncacheable__) ? 0x82000000 - HEAP_SIZE : 0x81E00000 - HEAP_SIZE; +NCACHE_HEAP_START = DEFINED(__heap_noncacheable__) ? 0x82000000 - HEAP_SIZE : 0x82000000 - HEAP_SIZE; NCACHE_HEAP_SIZE = DEFINED(__heap_noncacheable__) ? HEAP_SIZE : 0x0000; /* Specify the memory areas */ @@ -40,8 +40,8 @@ MEMORY m_interrupts (RX) : ORIGIN = 0x60002000, LENGTH = 0x00000400 m_text (RX) : ORIGIN = 0x60002400, LENGTH = 0x03FFDC00 m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 - m_data (RW) : ORIGIN = 0x80000000, LENGTH = DEFINED(__heap_noncacheable__) ? 0x01E00000 : 0x01E00000 - HEAP_SIZE - m_ncache (RW) : ORIGIN = 0x81E00000, LENGTH = DEFINED(__heap_noncacheable__) ? 0x00200000 - HEAP_SIZE : 0x00200000 + m_data (RW) : ORIGIN = 0x80000000, LENGTH = 0x01600000 /* 22 MB — хватает для всего */ + m_ncache (RW) : ORIGIN = 0x81600000, LENGTH = DEFINED(__heap_noncacheable__) ? 0x00A00000 - HEAP_SIZE : 0x00A00000 /* 10 MB — хватает на tft_app */ m_data2 (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 m_data3 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 m_heap (RW) : ORIGIN = NCACHE_HEAP_START, LENGTH = HEAP_SIZE diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index 6b7ed7b..6740193 100644 --- a/docs/DEV_ARCH.md +++ b/docs/DEV_ARCH.md @@ -65,18 +65,18 @@ HIL-тесты через pyOCD + pytest, GDB-сервер для отладки ## 3. Что устанавливается и где -| Инструмент | Хост | Devcontainer | -|------------|------|--------------| -| `just` | ✅ | ✅ Dockerfile | -| `docker` | ✅ | — | -| `uv` | ✅ | ✅ Dockerfile | +| Инструмент | Хост | Devcontainer | +| ----------------------------------- | --------------- | --------------- | +| `just` | ✅ | ✅ Dockerfile | +| `docker` | ✅ | — | +| `uv` | ✅ | ✅ Dockerfile | | `spsdk` (sdphost, blhost, nxpimage) | ✅ `tools/host/` | ✅ `tools/host/` | -| `pyocd` + `pyserial` + `pytest` | ✅ `tools/hil/` | — | -| `mpremote` | ✅ `tools/hil/` | — | -| ARM GCC toolchain | — | ✅ | -| `cmake` / `ninja` | — | ✅ | -| `clang` / `clangd` / `clang-tidy` | — | ✅ | -| Unity / fff | — | ✅ vendored | +| `pyocd` + `pyserial` + `pytest` | ✅ `tools/hil/` | — | +| `mpremote` | ✅ `tools/hil/` | — | +| ARM GCC toolchain | — | ✅ | +| `cmake` / `ninja` | — | ✅ | +| `clang` / `clangd` / `clang-tidy` | — | ✅ | +| Unity / fff | — | ✅ vendored | `spsdk` и `pyocd` — отдельные uv-проекты с разными ролями: @@ -226,11 +226,11 @@ HIL_USB_CDC_TIMEOUT=5.0 ### 5.1 Предварительные требования -| Платформа | Что нужно до bootstrap | -|-----------|------------------------| -| Linux | `docker`, `git`, `curl` | -| macOS | Docker Desktop, `git` (Xcode CLT) | -| Windows | Docker Desktop, Git for Windows → **Git Bash** | +| Платформа | Что нужно до bootstrap | +| --------- | ---------------------------------------------- | +| Linux | `docker`, `git`, `curl` | +| macOS | Docker Desktop, `git` (Xcode CLT) | +| Windows | Docker Desktop, Git for Windows → **Git Bash** | ### 5.2 Единственная команда @@ -279,11 +279,11 @@ just host::m5-deploy ### 6.1 Четыре типа сборки -| Пресет | Toolchain | Назначение | Линкер-скрипт | -|--------|-----------|------------|---------------| -| `Debug` / `Release` | ARM GCC | firmware_test, bootloader, tft_app | `flexspi_nor.ld` | -| `host-debug` / `host-release` | clang (хост) | Unity + fff тесты | — | -| `target-debug` | ARM GCC | HIL target-прошивки | `ram.ld` | +| Пресет | Toolchain | Назначение | Линкер-скрипт | +| ----------------------------- | ------------ | ---------------------------------- | ---------------- | +| `Debug` / `Release` | ARM GCC | firmware_test, bootloader, tft_app | `flexspi_nor.ld` | +| `host-debug` / `host-release` | clang (хост) | Unity + fff тесты | — | +| `target-debug` | ARM GCC | HIL target-прошивки | `ram.ld` | ### 6.2 CMake пресеты @@ -310,12 +310,12 @@ buildPresets (HIL): ### 6.3 Boot-стратегии -| Прошивка | Стратегия | Инструмент загрузки | -|----------|-----------|---------------------| -| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash | -| `bootloader` | Копирование в ITCM | SPSDK → Flash | -| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash | -| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM | +| Прошивка | Стратегия | Инструмент загрузки | +| ---------------------------- | ---------------------------------- | ------------------- | +| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash | +| `bootloader` | Копирование в ITCM | SPSDK → Flash | +| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash | +| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM | **HIL boot-стратегия:** pyOCD настраивает FLEXRAM (128 KB ITCM + 128 KB DTCM + 256 KB OCRAM), записывает PT_LOAD сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется — прошивка исчезает при отключении питания. @@ -325,19 +325,19 @@ buildPresets (HIL): ### 7.1 Карта задач по контекстам -| Задача | Где | -|--------|-----| -| Написание кода, clangd, форматирование | devcontainer | -| Статический анализ (clang-tidy) | devcontainer | -| Host unit-тесты (Unity + fff) | devcontainer | -| Сборка ARM firmware (ELF) | devcontainer | -| Сборка HIL target-прошивок | devcontainer | -| Подготовка HAB-образов (nxpimage) | devcontainer | -| Прошивка платы через USB ROM | хост | -| Прошивка платы через SWD | хост | -| HIL-тесты (pyOCD + pytest + M5) | хост | -| Отладка — GDB-сервер (pyOCD) | хост | -| Отладка — GDB-клиент (cortex-debug) | devcontainer → хост по TCP | +| Задача | Где | +| -------------------------------------- | -------------------------- | +| Написание кода, clangd, форматирование | devcontainer | +| Статический анализ (clang-tidy) | devcontainer | +| Host unit-тесты (Unity + fff) | devcontainer | +| Сборка ARM firmware (ELF) | devcontainer | +| Сборка HIL target-прошивок | devcontainer | +| Подготовка HAB-образов (nxpimage) | devcontainer | +| Прошивка платы через USB ROM | хост | +| Прошивка платы через SWD | хост | +| HIL-тесты (pyOCD + pytest + M5) | хост | +| Отладка — GDB-сервер (pyOCD) | хост | +| Отладка — GDB-клиент (cortex-debug) | devcontainer → хост по TCP | ### 7.2 Типичная сессия разработки @@ -369,15 +369,15 @@ just host::debug-server # запустить pyOCD GDB-серв ### 7.3 VSCode Tasks (внутри devcontainer) -| Таск | Команда | -|------|---------| -| 🔨 Build | `just build::build--` | -| 🧪 Host Tests (Debug) | `just build::test-host` | -| 🧪 Host Tests (Release) | `just build::test-host-release` | -| 🎯 Build HIL Target Tests | `just build::build-hil` | -| 📦 HAB Image | `just build::hab--` | -| 📦 HAB All (Debug/Release) | `just build::hab-all-debug/release` | -| 🗑️ Clean | `just build::clean` | +| Таск | Команда | +| ------------------------- | ------------------------------------ | +| 🔨 Build | `just build::build--` | +| 🧪 Host Tests (Debug) | `just build::test-host` | +| 🧪 Host Tests (Release) | `just build::test-host-release` | +| 🎯 Build HIL Target Tests | `just build::build-hil` | +| 📦 HAB Image | `just build::hab--` | +| 📦 HAB All (Debug/Release) | `just build::hab-all-debug/release` | +| 🗑️ Clean | `just build::clean` | --- diff --git a/docs/HOW_TO_DEBUG.md b/docs/HOW_TO_DEBUG.md index dc96e56..962960f 100644 --- a/docs/HOW_TO_DEBUG.md +++ b/docs/HOW_TO_DEBUG.md @@ -4,7 +4,7 @@ Отладка построена на проброске GDB-сервера с хоста в devcontainer по TCP. Это позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, не проводя USB-пробник внутрь Docker. -``` +```bash ┌─────────────────────────────────────┐ ┌──────────────────────────────────┐ │ Хост (macOS/Linux) │ │ DevContainer │ │ │ │ │ @@ -26,25 +26,25 @@ ### На хосте -| Компонент | Роль | Источник | -|---|---|---| -| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` | -| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате | -| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` | -| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` | -| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` | -| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool | +| Компонент | Роль | Источник | +| --------------------------------- | ------------------------------- | -------------------------- | +| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` | +| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате | +| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` | +| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` | +| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` | +| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool | ### В devcontainer -| Компонент | Роль | -|---|---| -| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте | -| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры | -| `.vscode/launch.json` | Конфигурации запуска отладки | -| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом | -| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) | -| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии | +| Компонент | Роль | +| -------------------------------------- | ---------------------------------------------- | +| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте | +| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры | +| `.vscode/launch.json` | Конфигурации запуска отладки | +| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом | +| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) | +| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии | ### Конфигурация @@ -62,11 +62,11 @@ FCB_PATH=tools/host/dcd/w25q128_fdcb.bin ## Прошивки, поддерживаемые отладкой -| Конфигурация VSCode | ELF | Особенности | -|---|---|---| -| `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль | -| `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление | -| `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view | +| Конфигурация VSCode | ELF | Особенности | +| ----------------------------- | ------------------------------- | ---------------------------- | +| `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль | +| `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление | +| `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view | Все три — XIP-прошивки, исполняются напрямую из QuadSPI NOR Flash (`0x60000000`). @@ -225,4 +225,4 @@ just host::debug-server ├── flash_swd.py # FCB + HAB → Flash через pyOCD └── dcd/ └── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI -``` \ No newline at end of file +``` diff --git a/docs/HOW_TO_FLASH.md b/docs/HOW_TO_FLASH.md index cda670d..3d80ef1 100644 --- a/docs/HOW_TO_FLASH.md +++ b/docs/HOW_TO_FLASH.md @@ -2,10 +2,10 @@ Поддерживаются два независимых способа прошивки: -| Способ | Интерфейс | Требование | Инструмент | -|---|---|---|---| -| **USB SDP** | USB ↔ ROM-загрузчик | BOOT_MODE = 01 (Serial Downloader) | `spsdk` (sdphost + blhost) | -| **SWD** | MCU-Link ↔ CMSIS-DAP | Плата в любом режиме загрузки | `pyocd` | +| Способ | Интерфейс | Требование | Инструмент | +| ----------- | -------------------- | ---------------------------------- | -------------------------- | +| **USB SDP** | USB ↔ ROM-загрузчик | BOOT_MODE = 01 (Serial Downloader) | `spsdk` (sdphost + blhost) | +| **SWD** | MCU-Link ↔ CMSIS-DAP | Плата в любом режиме загрузки | `pyocd` | --- @@ -127,11 +127,11 @@ just host::flash-swd-app-release ### 2.4 Зависимости -| Файл | Назначение | -|---|---| -| `tools/host/flash_swd.py` | Скрипт сборки образа и вызова pyOCD | -| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 в режиме Quad SPI | -| `tools/hil/` (uv-проект) | pyocd, вызывается через `uv run` | +| Файл | Назначение | +| --------------------------------- | ----------------------------------- | +| `tools/host/flash_swd.py` | Скрипт сборки образа и вызова pyOCD | +| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 в режиме Quad SPI | +| `tools/hil/` (uv-проект) | pyocd, вызывается через `uv run` | FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP SecureProvisioningTool для W25Q128 в режиме Quad SPI и хранится в репозитории — пересоздавать не нужно. @@ -140,15 +140,15 @@ FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP Secure ## Сравнение способов -| | USB SDP | SWD | -|---|---|---| -| Переключение BOOT_MODE | Нужно | Не нужно | -| Power cycle после записи | Не нужен | **Обязателен** | -| FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** | -| Скорость записи | ~50–100 kB/s | ~8–10 kB/s | -| Совместимость с отладкой | Раздельно | MCU-Link монопольный | -| Производственный сценарий | ✓ | — | -| Итеративная разработка | Неудобно (смена режима) | ✓ | +| | USB SDP | SWD | +| ------------------------- | -------------------------------- | -------------------- | +| Переключение BOOT_MODE | Нужно | Не нужно | +| Power cycle после записи | Не нужен | **Обязателен** | +| FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** | +| Скорость записи | ~50–100 kB/s | ~8–10 kB/s | +| Совместимость с отладкой | Раздельно | MCU-Link монопольный | +| Производственный сценарий | ✓ | — | +| Итеративная разработка | Неудобно (смена режима) | ✓ | --- diff --git a/docs/testing/hil/HIL_BENCH.md b/docs/testing/hil/HIL_BENCH.md index aec37dd..92c4614 100644 --- a/docs/testing/hil/HIL_BENCH.md +++ b/docs/testing/hil/HIL_BENCH.md @@ -6,10 +6,10 @@ ## Оборудование -| Устройство | Роль | -|-------------------|-------------------------------------------------------------| -| MIMXRT1052CVJ5B | Таргет — плата под тестом | -| M5Stack StamPLC | Промежуточная платформа стенда: питание таргета + сигналы | +| Устройство | Роль | +| -------------------- | ---------------------------------------------------------- | +| MIMXRT1052CVJ5B | Таргет — плата под тестом | +| M5Stack StamPLC | Промежуточная платформа стенда: питание таргета + сигналы | | MCU-Link (CMSIS-DAP) | SWD-probe + VCOM (pyOCD загружает ELF, pytest читает UART) | --- @@ -18,9 +18,9 @@ ### Питание таргета -| M5 реле | Куда | Назначение | -|---------|------------------|---------------------| -| RLY1 | VIN таргета | Управление питанием | +| M5 реле | Куда | Назначение | +| ------- | ----------- | ------------------- | +| RLY1 | VIN таргета | Управление питанием | Питание включается и выключается автоматически фикстурой `m5` в `conftest.py`: @@ -32,11 +32,11 @@ Таргет: оптопары PS2801-4, **неинвертирующие** (active-HIGH). M5: реле AW9523B через ULN2003A, нормально разомкнутые (NO). -| M5 реле | Сигнал таргета | BSP канал | MCU пин | GPIO | -|---------|----------------|--------------------|---------------|-----------| -| RLY2 | RS_RX | `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] | -| RLY3 | EXT_IN1 | `BSP_OPTO_CH_IN1` | GPIO_AD_B1_06 | GPIO1[22] | -| RLY4 | EXT_IN2 | `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | +| M5 реле | Сигнал таргета | BSP канал | MCU пин | GPIO | +| ------- | -------------- | ----------------- | ------------- | --------- | +| RLY2 | RS_RX | `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] | +| RLY3 | EXT_IN1 | `BSP_OPTO_CH_IN1` | GPIO_AD_B1_06 | GPIO1[22] | +| RLY4 | EXT_IN2 | `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | **Логика сигнала:** @@ -57,13 +57,13 @@ _OPTO_TO_RELAY = {1: 3, 2: 4, 3: 2} ### Порты и переменные окружения -| Переменная | Значение по умолчанию | Назначение | -|----------------------|-----------------------|---------------------------| -| `HIL_VCOM_PORT` | `/dev/ttyACM0` | MCU-Link VCOM (UART CLI) | -| `HIL_VCOM_BAUD` | `115200` | Скорость UART CLI | -| `HIL_M5_PORT` | `/dev/ttyACM1` | M5StampPLC USB CDC | -| `HIL_M5_BAUD` | `115200` | Скорость M5 агента | -| `HIL_BUILD_DIR` | `build/target-debug` | Путь к собранным ELF | +| Переменная | Значение по умолчанию | Назначение | +| --------------- | --------------------- | ------------------------ | +| `HIL_VCOM_PORT` | `/dev/ttyACM0` | MCU-Link VCOM (UART CLI) | +| `HIL_VCOM_BAUD` | `115200` | Скорость UART CLI | +| `HIL_M5_PORT` | `/dev/ttyACM1` | M5StampPLC USB CDC | +| `HIL_M5_BAUD` | `115200` | Скорость M5 агента | +| `HIL_BUILD_DIR` | `build/target-debug` | Путь к собранным ELF | На macOS порты выглядят как `/dev/cu.usbmodem*`. Задаются в `.env` в корне репозитория. @@ -73,11 +73,11 @@ _OPTO_TO_RELAY = {1: 3, 2: 4, 3: 2} ### M5Stack StamPLC — `tools/hil/m5/` -| Файл | Назначение | -|--------------|----------------------------------------------------------------| -| `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, подает сигналы на таргет | -| `cli.py` | Интерактивный CLI для ручного тестирования агента | -| `power.py` | Скрипт управления питанием таргета (RLY1) из командной строки | +| Файл | Назначение | +| ---------- | --------------------------------------------------------------------------------------- | +| `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, подает сигналы на таргет | +| `cli.py` | Интерактивный CLI для ручного тестирования агента | +| `power.py` | Скрипт управления питанием таргета (RLY1) из командной строки | **Протокол агента:** JSON-lines через USB CDC (115200 бод). @@ -88,19 +88,19 @@ M5 → хост: {"ok": true, "opto_ch": 1, "relay": 3, "state": true}\r\n **Доступные команды агента:** -| Команда | Параметры | Действие | -|----------------|------------------------------------|---------------------------------------------------| -| `ping` | — | Проверка связи | -| `info` | — | Версия, состояние CAN и AW9523, статус реле | -| `power` | `state: bool` | RLY1 — питание таргета | -| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле | -| `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле | -| `relay_all_off`| — | Выключить все реле | -| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) | -| `opto_all_off` | — | Выключить все оптоканалы | -| `input_read` | `ch: 1-8` | Прочитать вход стенда SYS_IN (оптопара на M5) | -| `can_send` | `id: int, data: list[int], ext: bool=false` | Отправить CAN-фрейм с шины M5 | -| `can_recv` | `timeout_ms: int=500` | Принять CAN-фрейм на M5 (ошибка при таймауте) | +| Команда | Параметры | Действие | +| --------------- | ------------------------------------------- | ---------------------------------------------- | +| `ping` | — | Проверка связи | +| `info` | — | Версия, состояние CAN и AW9523, статус реле | +| `power` | `state: bool` | RLY1 — питание таргета | +| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле | +| `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле | +| `relay_all_off` | — | Выключить все реле | +| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) | +| `opto_all_off` | — | Выключить все оптоканалы | +| `input_read` | `ch: 1-8` | Прочитать вход стенда SYS_IN (оптопара на M5) | +| `can_send` | `id: int, data: list[int], ext: bool=false` | Отправить CAN-фрейм с шины M5 | +| `can_recv` | `timeout_ms: int=500` | Принять CAN-фрейм на M5 (ошибка при таймауте) | **Деплой агента на M5:** diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md index 73955fc..dfed53c 100644 --- a/docs/testing/hil/HIL_CREATE_TEST.md +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -4,7 +4,7 @@ ```bash devcontainer хост -───────────────────────────────── ──────────────────────────────────── +───────────────────────────────── ──────────────────────────────────── tests/target// tools/hil/ main.c ← C-прошивка с CLI test_.py ← pytest-тесты CMakeLists.txt conftest.py ← фикстуры (общие) @@ -19,11 +19,11 @@ just/build.just Три типа тестов: -| Тип | Использует M5 | Запуск | Когда применять | -|-----|--------------|--------|-----------------| -| **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов | -| **С M5** | Да | `hil-run` | Тестирование GPIO, оптовходов, реле, питания | -| **Интерактивный** | Нет / Да | `hil-run-interactive` | Периферия требует действий оператора: кнопки, дисплей | +| Тип | Использует M5 | Запуск | Когда применять | +| ----------------- | ------------- | --------------------- | ----------------------------------------------------- | +| **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов | +| **С M5** | Да | `hil-run` | Тестирование GPIO, оптовходов, реле, питания | +| **Интерактивный** | Нет / Да | `hil-run-interactive` | Периферия требует действий оператора: кнопки, дисплей | Интерактивные тесты помечаются `@pytest.mark.interactive` и **никогда не входят в `hil-run`** — они требуют живого оператора и не пригодны для CI. diff --git a/docs/testing/hil/HIL_FIXTURES.md b/docs/testing/hil/HIL_FIXTURES.md index 054da53..69cb60a 100644 --- a/docs/testing/hil/HIL_FIXTURES.md +++ b/docs/testing/hil/HIL_FIXTURES.md @@ -23,10 +23,10 @@ def uart(request): ## 2. `yield` vs `return` -| Конструкция | Setup | Teardown | Когда использовать | -|-------------|-------|----------|--------------------| -| `return` | ✅ | ❌ | Ресурс не требует очистки (загрузка ELF) | -| `yield` | ✅ | ✅ | Ресурс нужно освободить (порт, реле, питание) | +| Конструкция | Setup | Teardown | Когда использовать | +| ----------- | ----- | -------- | --------------------------------------------- | +| `return` | ✅ | ❌ | Ресурс не требует очистки (загрузка ELF) | +| `yield` | ✅ | ✅ | Ресурс нужно освободить (порт, реле, питание) | ```python # return — teardown не нужен @@ -56,12 +56,12 @@ def m5(request): Scope определяет **как долго живёт** экземпляр фикстуры. -| Scope | Создаётся | Уничтожается | Типичное применение в HIL | -|-------|-----------|--------------|---------------------------| -| `function` | Перед каждым `test_*()` | После каждого `test_*()` | Сброс состояния стенда | -| `class` | Перед первым тестом класса | После последнего теста класса | Группа связанных тестов | -| `module` | Перед первым тестом файла | После последнего теста файла | **Загрузка ELF, открытие UART** | -| `session` | Один раз на весь pytest-запуск | В самом конце | Подключение к M5, глобальный setup | +| Scope | Создаётся | Уничтожается | Типичное применение в HIL | +| ---------- | ------------------------------ | ----------------------------- | ---------------------------------- | +| `function` | Перед каждым `test_*()` | После каждого `test_*()` | Сброс состояния стенда | +| `class` | Перед первым тестом класса | После последнего теста класса | Группа связанных тестов | +| `module` | Перед первым тестом файла | После последнего теста файла | **Загрузка ELF, открытие UART** | +| `session` | Один раз на весь pytest-запуск | В самом конце | Подключение к M5, глобальный setup | ### Почему для HIL основной scope — `module` @@ -202,18 +202,18 @@ tools/hil/ ### Что живёт в `conftest.py` проекта -| Фикстура / функция | Scope | Назначение | -|---------------------|-------|------------| -| `_load_elf()` | вспомогательная | pyOCD: halt → FLEXRAM → load ELF → run | -| `_uart_context()` | контекстный менеджер | Открыть VCOM, дождаться `READY\r\n`, гарантировать `close()` | -| `_make_uart_fixture()` | фабрика | Генерирует `uart_*` фикстуры из `_UART_FIXTURE_MAP` | -| `_UART_FIXTURE_MAP` | словарь | Связь `uart_` → `loaded_` для всех тестов | -| `loaded_host_uart` | module | Загрузить `test_host_uart.elf` | -| `loaded_hil_opto` | module | Загрузить `test_hil_opto.elf`, зависит от `m5` | -| `uart` / `uart_opto` / ... | module | Создаются автоматически через `_UART_FIXTURE_MAP` | -| `usb_cdc_port` | module | USB CDC порт таргета, использует `cfg.TARGET_VCOM_*` | -| `m5` | module | Подключиться к M5, включить питание | -| `uart_cmd()` | обычная функция | Отправить команду, прочитать ответ | +| Фикстура / функция | Scope | Назначение | +| -------------------------- | -------------------- | ------------------------------------------------------------ | +| `_load_elf()` | вспомогательная | pyOCD: halt → FLEXRAM → load ELF → run | +| `_uart_context()` | контекстный менеджер | Открыть VCOM, дождаться `READY\r\n`, гарантировать `close()` | +| `_make_uart_fixture()` | фабрика | Генерирует `uart_*` фикстуры из `_UART_FIXTURE_MAP` | +| `_UART_FIXTURE_MAP` | словарь | Связь `uart_` → `loaded_` для всех тестов | +| `loaded_host_uart` | module | Загрузить `test_host_uart.elf` | +| `loaded_hil_opto` | module | Загрузить `test_hil_opto.elf`, зависит от `m5` | +| `uart` / `uart_opto` / ... | module | Создаются автоматически через `_UART_FIXTURE_MAP` | +| `usb_cdc_port` | module | USB CDC порт таргета, использует `cfg.TARGET_VCOM_*` | +| `m5` | module | Подключиться к M5, включить питание | +| `uart_cmd()` | обычная функция | Отправить команду, прочитать ответ | --- diff --git a/docs/testing/host/HOST_CREATE_TEST.md b/docs/testing/host/HOST_CREATE_TEST.md index 0c78580..74cf02b 100644 --- a/docs/testing/host/HOST_CREATE_TEST.md +++ b/docs/testing/host/HOST_CREATE_TEST.md @@ -3,7 +3,7 @@ ## Обзор стека ```bash -devcontainer +devcontainer ───────────────────────────────────────────────────────────────── tests/host//test_.c ← тест (Unity + опционально fff) tests/host/CMakeLists.txt ← регистрация через add_host_test() @@ -20,10 +20,10 @@ CMakePresets.json just/build.just Перед написанием кода определи к какой категории относится модуль: -| Категория | Описание | Инструментарий | -|-----------|----------|----------------| -| **A** | Нет вызовов NXP SDK: алгоритмы, парсеры, FSM, структуры данных | Unity | -| **B** | BSP-модуль вызывает `fsl_*.h`, USB-стек и т.д. | Unity + fff + stub-хедеры | +| Категория | Описание | Инструментарий | +| --------- | -------------------------------------------------------------- | ------------------------- | +| **A** | Нет вызовов NXP SDK: алгоритмы, парсеры, FSM, структуры данных | Unity | +| **B** | BSP-модуль вызывает `fsl_*.h`, USB-стек и т.д. | Unity + fff + stub-хедеры | **Признак категории A:** в `.c` файле модуля нет ни одного `#include "fsl_*.h"`. **Признак категории B:** есть хотя бы один такой include. @@ -146,11 +146,11 @@ status_t SDK_Function_B(DRIVER_Type *base, const uint8_t *data, size_t len); ### Уже существующие stubs в `tests/host/mocks/` -| Файл | Что заменяет | Используется в | -|------|-------------|----------------| -| `fsl_gpio.h` | GPIO драйвер | `test_bsp_led` | -| `pin_mux.h` | Макросы пинов из `generated/` | `test_bsp_led` | -| `board.h` | `board_hw_init()` | `test_bsp_led` | +| Файл | Что заменяет | Используется в | +| ------------ | ----------------------------- | -------------- | +| `fsl_gpio.h` | GPIO драйвер | `test_bsp_led` | +| `pin_mux.h` | Макросы пинов из `generated/` | `test_bsp_led` | +| `board.h` | `board_hw_init()` | `test_bsp_led` | Если нужный stub уже есть — ничего создавать не нужно, просто укажи `mocks/` в `MOCKS` аргументе `add_host_test()`. @@ -184,12 +184,12 @@ add_host_test( ### Аргументы `add_host_test()` -| Аргумент | Обязателен | Описание | -|----------|-----------|----------| -| `NAME` | ✓ | Имя исполняемого файла и теста в CTest | -| `SOURCES` | ✓ | Тестовый `.c` + исходники тестируемых модулей | -| `INCLUDES` | — | Дополнительные include-пути (для `#include "bsp/led.h"` и т.д.) | -| `MOCKS` | — | Директории со stub-хедерами (подключаются с высшим приоритетом) | +| Аргумент | Обязателен | Описание | +| ---------- | ---------- | --------------------------------------------------------------- | +| `NAME` | ✓ | Имя исполняемого файла и теста в CTest | +| `SOURCES` | ✓ | Тестовый `.c` + исходники тестируемых модулей | +| `INCLUDES` | — | Дополнительные include-пути (для `#include "bsp/led.h"` и т.д.) | +| `MOCKS` | — | Директории со stub-хедерами (подключаются с высшим приоритетом) | `lib_external` (Unity + fff) подключается автоматически — добавлять не нужно. diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 0c11de0..9c666f0 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable( src/tests/test_sdram.c src/tests/test_qspi.c src/tests/test_usd.c + src/tests/test_display.c ${BSP_GENERATED}/clock_config.c ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE}) @@ -26,8 +27,8 @@ target_include_directories(firmware_test PRIVATE src/) target_compile_definitions( ${TARGET_NAME} PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1 - __STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS - __STARTUP_INITIALIZE_NONCACHEDATA) + DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8 __STARTUP_INITIALIZE_RAMFUNCTION + __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_NONCACHEDATA) # # ----------------------------------------------------------------------------- # Зависимости — только то что нужно для входного контроля bsp_board транзитивно @@ -37,6 +38,7 @@ target_link_libraries( ${TARGET_NAME} PRIVATE bsp_board bsp_led + bsp_display bsp_tick bsp_boot_xip bsp_usb_cdc diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md index b367133..050d144 100644 --- a/firmware/test/PLAN.md +++ b/firmware/test/PLAN.md @@ -248,7 +248,7 @@ bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из --- -## Матрица тестов — итоговая +## Матрица тестов — итоговая | ID | Название | Тип | Critical | HIL (M5) | BSP | Статус | | ---------- | -------------- | ----------- | -------- | -------- | ----------------- | ------ | @@ -265,7 +265,7 @@ bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из --- -## Зависимости между этапами +## Зависимости между этапами ``` ✅ Этап 1 (протокол v2 + runner) diff --git a/firmware/test/README.md b/firmware/test/README.md index 3180f55..8d3fa1e 100644 --- a/firmware/test/README.md +++ b/firmware/test/README.md @@ -190,17 +190,17 @@ main.c **BSP-зависимости тест-модулей:** -| Тест | BSP модуль | -|---|---| -| `test_sdram` | `bsp_sdram` | -| `test_qspi` | `bsp_qspi` | -| `test_usd` | `bsp_usd` | -| `test_display` | существующий display BSP | -| `test_buttons` | `bsp_button` ✅ | -| `test_can` | `bsp_can` ✅ | -| `test_uart_ttl` | `bsp_uart_host` ✅ | +| Тест | BSP модуль | +| --------------- | ------------------------------ | +| `test_sdram` | `bsp_sdram` | +| `test_qspi` | `bsp_qspi` | +| `test_usd` | `bsp_usd` | +| `test_display` | существующий display BSP | +| `test_buttons` | `bsp_button` ✅ | +| `test_can` | `bsp_can` ✅ | +| `test_uart_ttl` | `bsp_uart_host` ✅ | | `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ | -| `test_opto` | `bsp_opto` ✅ | +| `test_opto` | `bsp_opto` ✅ | --- @@ -253,13 +253,13 @@ main.c ### Транспорт -| Параметр | Значение | -|---|---| -| Интерфейс | USB CDC ACM, разъём J2 | -| Кодировка | UTF-8 | -| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` | -| Максимальная длина строки | 128 байт включая `\n` | -| CR+LF | Принимается (таргет отбрасывает `\r`) | +| Параметр | Значение | +| ------------------------- | ---------------------------------------------------------- | +| Интерфейс | USB CDC ACM, разъём J2 | +| Кодировка | UTF-8 | +| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` | +| Максимальная длина строки | 128 байт включая `\n` | +| CR+LF | Принимается (таргет отбрасывает `\r`) | Нет хэндшейка, нет sequence number, нет подтверждений доставки. Таргет идемпотентен для `ping` и `run` — при потере строки хост повторяет. @@ -378,10 +378,10 @@ main.c } ``` -| `status` | Смысл | -|---|---| -| `"pass"` | Тест пройден | -| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) | +| `status` | Смысл | +| -------- | ------------------------------------------------------------------------ | +| `"pass"` | Тест пройден | +| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) | | `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше | Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`. @@ -482,17 +482,17 @@ main.c ## Матрица тестов -| ID | Название | Тип | Critical | M5 HIL | Confirm | -|---|---|---|---|---|---| -| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | -| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | -| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm | -| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() | -| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only | -| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ | -| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | -| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ | -| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | +| ID | Название | Тип | Critical | M5 HIL | Confirm | +| ---------- | ---------------- | ---------------- | -------- | ------ | ------------- | +| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | +| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | +| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm | +| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() | +| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only | +| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ | +| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | +| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ | +| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | **Типы confirm:** @@ -668,10 +668,10 @@ const test_module_t k_test_foo = { ### Что покрыто -| Таргет | Что тестирует | Тест-файл | -|---|---|---| -| `test_protocol` | сериализация JSON (все event types) | `tests/host/protocol/test_protocol.c` | -| `test_cli` | парсинг входящих строк, диспатч по type | `tests/host/cli/test_cli.c` | +| Таргет | Что тестирует | Тест-файл | +| ---------------------- | ------------------------------------------------ | ------------------------------------------ | +| `test_protocol` | сериализация JSON (все event types) | `tests/host/protocol/test_protocol.c` | +| `test_cli` | парсинг входящих строк, диспатч по type | `tests/host/cli/test_cli.c` | | `test_firmware_runner` | state machine (IDLE/PRE_CONFIRM/RUNNING), реестр | `tests/host/runner/test_firmware_runner.c` | ### Запуск @@ -719,13 +719,13 @@ void test_run_all_critical_fail_skips_remaining(void) Стандартный список моков для каждого теста: -| Зависимость | fff fake | -|---|---| -| `cli_send()` | `FAKE_VOID_FUNC(cli_send, const char *)` + custom_fake с копией | -| `bsp_tick_get_ms()` | `FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms)` | -| `bsp_usb_cdc_poll()` | `FAKE_VOID_FUNC(bsp_usb_cdc_poll)` | -| `cli_process()` | `FAKE_VOID_FUNC(cli_process)` | -| `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению | +| Зависимость | fff fake | +| ----------------------------- | --------------------------------------------------------------- | +| `cli_send()` | `FAKE_VOID_FUNC(cli_send, const char *)` + custom_fake с копией | +| `bsp_tick_get_ms()` | `FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms)` | +| `bsp_usb_cdc_poll()` | `FAKE_VOID_FUNC(bsp_usb_cdc_poll)` | +| `cli_process()` | `FAKE_VOID_FUNC(cli_process)` | +| `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению | > **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель > на стековую переменную внутри `execute_test()`. После возврата указатель @@ -750,11 +750,11 @@ void test_run_all_critical_fail_skips_remaining(void) > Не пересматривать без явного запроса. -| Решение | Обоснование | -|---|---| -| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) | -| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен | -| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC | -| IR и RTC — не реализуются | Вне scope рекламационной диагностики | -| Тесты атомарны | Инженер сам решает что проверять | -| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики | +| Решение | Обоснование | +| -------------------------------------------- | ------------------------------------------------------------------ | +| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) | +| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен | +| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC | +| IR и RTC — не реализуются | Вне scope рекламационной диагностики | +| Тесты атомарны | Инженер сам решает что проверять | +| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики | diff --git a/firmware/test/src/main.c b/firmware/test/src/main.c index 52503fb..e7c981d 100644 --- a/firmware/test/src/main.c +++ b/firmware/test/src/main.c @@ -25,7 +25,6 @@ #include "protocol.h" #include "test_runner.h" -#include #include int main(void) diff --git a/firmware/test/src/protocol.h b/firmware/test/src/protocol.h index bb409b7..dc3153d 100644 --- a/firmware/test/src/protocol.h +++ b/firmware/test/src/protocol.h @@ -24,7 +24,7 @@ #include /** @brief Строка версии прошивки, вставляемая в session_start. */ -#define FIRMWARE_TEST_VERSION "0.1.0" +#define FIRMWARE_TEST_VERSION "0.1.4" /** @brief Таймаут подтверждения по умолчанию, мс. */ #define PROTOCOL_CONFIRM_TIMEOUT_MS 30000U diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index 39619dd..f352d82 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -23,33 +23,19 @@ #include #include -/* ── Реестр тестов ───────────────────────────────────────────────────────── - * - * Добавление нового теста (Этап 2+): - * 1. Раскомментировать extern-объявление нужного модуля. - * 2. Добавить &k_test_ в k_registry[]. - * - * extern const test_module_t k_test_sdram; -*extern const test_module_t k_test_qspi; -*extern const test_module_t k_test_usd; -*extern const test_module_t k_test_display; -*extern const test_module_t k_test_buttons; -*extern const test_module_t k_test_can; -*extern const test_module_t k_test_uart_ttl; -*extern const test_module_t k_test_uart_iso; -*extern const test_module_t k_test_opto; -* ─────────────────────────────────────────────────────────────────────────*/ - +/* ── Реестр тестов ─────────────────────────────────────────────────────────*/ #ifndef UNIT_TEST extern const test_module_t K_TEST_SDRAM; extern const test_module_t K_TEST_QSPI; extern const test_module_t K_TEST_USD; +extern const test_module_t K_TEST_DISPLAY; static const test_module_t *const k_registry[] = { /* populated starting from Этап 2 */ &K_TEST_SDRAM, &K_TEST_QSPI, &K_TEST_USD, + &K_TEST_DISPLAY, }; #define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0])) diff --git a/firmware/test/src/tests/test_display.c b/firmware/test/src/tests/test_display.c new file mode 100644 index 0000000..3768f80 --- /dev/null +++ b/firmware/test/src/tests/test_display.c @@ -0,0 +1,311 @@ +/** + * @file test_display.c + * @brief Тест-модуль firmware_test: TFT-дисплей (ELCDIF RGB888). + * + * Два этапа: + * + * Этап 1 — Цвет (все дисплеи, ~1 мин) + * 4 шага: Red → Green → Blue → White. + * Каждый шаг: заливка сплошным цветом → ожидание FRAME_DONE + * → confirm оператора (таймаут 15 с). + * + * Этап 2 — Ротация (только TFT7/TFT8/TFT10, ~30 с) + * Диагностирует непропаянные LR/UD пины. + * Паттерн: левая половина RED, правая BLUE. + * ROTATE_0 confirm → ROTATE_90 confirm → восстановить ROTATE_0. + * + * Тип дисплея определяется через DISPLAY_TEST_TYPE: + * — сейчас: жёсткий define (BSP_DISPLAY_TFT8) через CMake. + * — TODO: читать из Flash-конфига (Вариант C, см. PLAN.md Этап 5). + * + * Фреймбуфер — статический, в NonCacheable SDRAM (секция NonCacheable). + * Размер по максимальному дисплею: BSP_DISPLAY_MAX_HEIGHT × BSP_DISPLAY_MAX_WIDTH. + * + * Frame sync: volatile bool g_s_frame_done — устанавливается в ISR callback, + * сбрасывается перед каждым bsp_display_set_next_buffer(). + * USB CDC поллится в цикле ожидания — стек остаётся живым. + */ + +#include "bsp/display.h" +#include "bsp/usb_cdc.h" +#include "fsl_common.h" +#include "test_module.h" +#include "test_runner.h" + +#include +#include +#include +#include +/* ── Тип дисплея ─────────────────────────────────────────────────────── */ + +/* + * TODO (Вариант C): заменить на чтение из Flash-конфига платы. + * До реализации конфига тип задаётся через CMake: + * target_compile_definitions(firmware_test PRIVATE + * DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8) + */ +#ifndef DISPLAY_TEST_TYPE +#define DISPLAY_TEST_TYPE BSP_DISPLAY_TFT8 +#endif + +/* ── Константы ───────────────────────────────────────────────────────── */ + +/** @brief XRGB8888 цвета для тестовых заливок. */ +#define DISPLAY_COLOR_RED 0x00FF0000UL +#define DISPLAY_COLOR_GREEN 0x0000FF00UL +#define DISPLAY_COLOR_BLUE 0x000000FFUL +#define DISPLAY_COLOR_WHITE 0x00FFFFFFUL + +/** @brief Таймаут confirm для каждого шага, мс. */ +#define DISPLAY_CONFIRM_TIMEOUT_MS 15000U + +/* ── Фреймбуфер в NonCacheable SDRAM ────────────────────────────────── */ + +/** @brief Один кадр максимального разрешения, XRGB8888. */ +static AT_NONCACHEABLE_SECTION_ALIGN( + uint32_t g_s_framebuf[BSP_DISPLAY_MAX_HEIGHT * BSP_DISPLAY_MAX_WIDTH], 64U); + +/* ── Состояние модуля ────────────────────────────────────────────────── */ + +static bool g_s_ready; +static volatile bool g_s_frame_done; + +/* ── Frame-done callback ─────────────────────────────────────────────── */ + +/** @brief Вызывается из LCDIF ISR. ISR-safe: только запись в volatile. */ +static void display_frame_cb(void) +{ + g_s_frame_done = true; +} + +/* ── Вспомогательные функции ─────────────────────────────────────────── */ + +/** + * @brief Залить весь буфер одним цветом XRGB8888. + * + * Поллит USB CDC каждые 16 KB для поддержания USB-стека. + */ +static void fill_solid(uint32_t color) +{ + const bsp_display_size_t *p_sz = bsp_display_get_size(); + const uint32_t total = (uint32_t) p_sz->width * (uint32_t) p_sz->height; + + for (uint32_t i = 0U; i < total; i++) + { + if ((i & 0x3FFFU) == 0U) + { + bsp_usb_cdc_poll(); + } + g_s_framebuf[i] = color; + } +} + +/** + * @brief Залить левую половину color_l, правую — color_r. + * + * Используется в тесте ротации для детектирования непропаянных LR/UD пинов. + */ +static void fill_half(uint32_t color_l, uint32_t color_r) +{ + const bsp_display_size_t *p_sz = bsp_display_get_size(); + const uint16_t HALF = p_sz->width / 2U; + + for (uint16_t row = 0U; row < p_sz->height; row++) + { + const uint32_t BASE = (uint32_t) row * (uint32_t) p_sz->width; + for (uint16_t col = 0U; col < p_sz->width; col++) + { + g_s_framebuf[BASE + col] = (col < HALF) ? color_l : color_r; + } + if ((row & 0x1FU) == 0U) + { + bsp_usb_cdc_poll(); + } + } +} + +/** + * @brief Отправить буфер в ELCDIF и заблокироваться до завершения кадра. + * + * Поллит USB CDC в цикле ожидания. + */ +static void wait_frame(void) +{ + g_s_frame_done = false; + bsp_display_set_next_buffer((uint32_t) g_s_framebuf); + while (!g_s_frame_done) + { + bsp_usb_cdc_poll(); + } +} + +/* ── Шаги теста ──────────────────────────────────────────────────────── */ + +/** + * @brief Залить цвет, отобразить, запросить подтверждение оператора. + * + * @param color XRGB8888 цвет заливки. + * @param p_id ID confirm_request. + * @param p_prompt Инструкция оператору. + * @param p_out Заполняется при FAIL. + * @return true при подтверждении оператором. + */ +static bool step_color(uint32_t color, const char *p_id, const char *p_prompt, test_result_t *p_out) +{ + fill_solid(color); + wait_frame(); + + const confirm_params_t params = { + .id = p_id, + .prompt = p_prompt, + .timeout_ms = DISPLAY_CONFIRM_TIMEOUT_MS, + }; + + if (!test_runner_wait_confirm(¶ms)) + { + (void) snprintf(p_out->detail, TEST_DETAIL_SIZE, "%s not confirmed", p_id); + p_out->status = TEST_STATUS_FAIL; + return false; + } + return true; +} + +/** + * @brief Применить ротацию, залить паттерн, запросить confirm. + * + * Вспомогательная функция для step_rotation() — держит её в лимите. + * + * @param rotation Ориентация для установки. + * @param p_id ID confirm_request. + * @param p_prompt Инструкция оператору. + * @param p_out Заполняется при FAIL. + * @return true при подтверждении. + */ +static bool step_rot_apply_and_confirm(bsp_display_rotation_t rotation, const char *p_id, + const char *p_prompt, test_result_t *p_out) +{ + (void) bsp_display_set_rotation(rotation); + fill_half(DISPLAY_COLOR_RED, DISPLAY_COLOR_BLUE); + wait_frame(); + + const confirm_params_t params = { + .id = p_id, + .prompt = p_prompt, + .timeout_ms = DISPLAY_CONFIRM_TIMEOUT_MS, + }; + + if (!test_runner_wait_confirm(¶ms)) + { + (void) snprintf(p_out->detail, TEST_DETAIL_SIZE, "%s not confirmed", p_id); + p_out->status = TEST_STATUS_FAIL; + return false; + } + return true; +} + +/** + * @brief Этап 2: тест ротации — диагностика непропаянных LR/UD пинов. + * + * ROTATE_0: левая зона RED, правая BLUE → confirm. + * ROTATE_90: те же данные в буфере, аппаратный флип → confirm. + * Восстанавливает ROTATE_0 независимо от результата. + */ +static bool step_rotation(test_result_t *p_out) +{ + bool ok = step_rot_apply_and_confirm(BSP_DISPLAY_ROTATE_0, "display_rot0", + "Screen: left RED, right BLUE?", p_out); + if (ok) + { + ok = step_rot_apply_and_confirm(BSP_DISPLAY_ROTATE_90, "display_rot90", + "Color zones changed orientation?", p_out); + } + + /* Восстановить ROTATE_0 в любом исходе */ + (void) bsp_display_set_rotation(BSP_DISPLAY_ROTATE_0); + return ok; +} + +/* ── Реализация тест-модуля ───────────────────────────────────────────── */ + +static void display_test_init(void) +{ + g_s_ready = false; + g_s_frame_done = false; + + bsp_status_t status = bsp_display_init((bsp_display_type_t) DISPLAY_TEST_TYPE, + (uint32_t) g_s_framebuf, display_frame_cb); + if (status != BSP_OK) + { + return; + } + + /* Явная установка ROTATE_0 — детерминированное начальное состояние */ + (void) bsp_display_set_rotation(BSP_DISPLAY_ROTATE_0); + g_s_ready = true; +} + +static test_result_t display_test_run(void) +{ + if (!g_s_ready) + { + test_result_t result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U }; + (void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", + "display init failed — check DISPLAY_TEST_TYPE"); + return result; + } + + test_result_t fail_result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U, .detail = { 0 } }; + + /* Этап 1: цвет */ + if (!step_color(DISPLAY_COLOR_RED, "display_red", "Screen is solid red?", &fail_result)) + { + return fail_result; + } + if (!step_color(DISPLAY_COLOR_GREEN, "display_green", "Screen is solid green?", &fail_result)) + { + return fail_result; + } + if (!step_color(DISPLAY_COLOR_BLUE, "display_blue", "Screen is solid blue?", &fail_result)) + { + return fail_result; + } + if (!step_color(DISPLAY_COLOR_WHITE, "display_white", "Screen is solid white?", &fail_result)) + { + return fail_result; + } + + /* Этап 2: ротация — только для дисплеев с ножками LR/UD */ + if (bsp_display_get_type() != BSP_DISPLAY_TFT4) + { + if (!step_rotation(&fail_result)) + { + return fail_result; + } + } + + return (test_result_t){ + .status = TEST_STATUS_PASS, + .duration_ms = 0U, + .detail = { 0 }, + }; +} + +static void display_test_deinit(void) +{ + (void) bsp_display_deinit(); + g_s_ready = false; + g_s_frame_done = false; +} + +/* ── Дескриптор модуля ────────────────────────────────────────────────── */ + +const test_module_t K_TEST_DISPLAY = { + .id = "display", + .name = "TFT Display RGB888", + .critical = false, + .requires_hil = false, + .pre_confirm_prompt = NULL, + .init = display_test_init, + .run = display_test_run, + .deinit = display_test_deinit, +}; \ No newline at end of file diff --git a/just/ci_workflow.md b/just/ci_workflow.md index 23cee4d..47bc228 100644 --- a/just/ci_workflow.md +++ b/just/ci_workflow.md @@ -2,84 +2,83 @@ ## Обзор -В репозитории настроен рабочий GitHub Actions pipeline, который успешно запускается на событиях `push`, `pull_request` и при ручном запуске через `workflow_dispatch`.[cite:23][cite:99] Пайплайн специально привязан к реальному окружению разработки проекта: сборка и тесты выполняются внутри того же devcontainer-образа, который описан в `.devcontainer/Dockerfile`, а не в вручную собранной среде на `ubuntu-latest`.[cite:125] +В репозитории настроен рабочий GitHub Actions pipeline, который успешно запускается на событиях `push`, `pull_request` и при ручном запуске через `workflow_dispatch`. Пайплайн специально привязан к реальному окружению разработки проекта: сборка и тесты выполняются внутри того же devcontainer-образа, который описан в `.devcontainer/Dockerfile`, а не в вручную собранной среде на `ubuntu-latest`. -Такой подход уже устранил основные проблемы, которые проявились при первичном поднятии CI: отсутствие ARM toolchain, отсутствие `ninja`, установка неправильного `just` и несовместимость прав доступа при работе с bind-mounted workspace внутри Docker.[cite:125][cite:119][cite:127] +Такой подход уже устранил основные проблемы, которые проявились при первичном поднятии CI: отсутствие ARM toolchain, отсутствие `ninja`, установка неправильного `just` и несовместимость прав доступа при работе с bind-mounted workspace внутри Docker. ## Текущая архитектура -Workflow разделён на две job’ы: `build` и `test`.[cite:156] Такое разделение делает пайплайн проще для сопровождения, позволяет отдельно анализировать результаты стадии сборки и создаёт хороший фундамент для следующих этапов — `lint`, coverage, release packaging и аппаратных проверок.[cite:156][cite:147] +Workflow разделён на две job’ы: `build` и `test`.[cite:156] Такое разделение делает пайплайн проще для сопровождения, позволяет отдельно анализировать результаты стадии сборки и создаёт хороший фундамент для следующих этапов — `lint`, coverage, release packaging и аппаратных проверок. Среда выполнения строится из Dockerfile devcontainer-а проекта, в котором уже определены ARM GCC toolchain в `/opt/arm-toolchain`, обновлённый `PATH`, а также установлены `cmake`, `ninja-build`, `clang-17`, `uv` и `just`.[cite:125] Поскольку все ключевые зависимости уже зафиксированы именно там, использование этого же образа в CI делает поведение раннера максимально близким к локальной разработке.[cite:125] ## Как работает workflow -Workflow реагирует на три типа событий: `push`, `pull_request` и `workflow_dispatch`.[cite:23][cite:99] Это даёт удобный баланс между автоматической проверкой обычных коммитов и возможностью вручную перезапускать pipeline для отладки инфраструктурных или нестабильных падений без обязательного нового изменения в коде.[cite:99][cite:106] - -Внутри job используется Docker Buildx и `docker/build-push-action`, а кэширование слоёв контейнера подключено через backend GitHub Actions cache с помощью `cache-from: type=gha` и `cache-to: type=gha`.[cite:142][cite:143][cite:146] За счёт этого повторные прогоны не пересобирают devcontainer с нуля, а переиспользуют уже собранные Docker-слои, что заметно ускоряет пайплайн после первого успешного заполнения кэша.[cite:142][cite:143] +Workflow реагирует на три типа событий: `push`, `pull_request` и `workflow_dispatch`. Это даёт удобный баланс между автоматической проверкой обычных коммитов и возможностью вручную перезапускать pipeline для отладки инфраструктурных или нестабильных падений без обязательного нового изменения в коде. +Внутри job используется Docker Buildx и `docker/build-push-action`, а кэширование слоёв контейнера подключено через backend GitHub Actions cache с помощью `cache-from: type=gha` и `cache-to: type=gha`. За счёт этого повторные прогоны не пересобирают devcontainer с нуля, а переиспользуют уже собранные Docker-слои, что заметно ускоряет пайплайн после первого успешного заполнения кэша. ## Что делает job `build` -Job `build` выполняет checkout репозитория, инициализирует Buildx, собирает devcontainer image с поддержкой кэша, проверяет версии инструментов внутри контейнера, синхронизирует Python tooling в `tools/host` через `uv sync`, а затем запускает `just ci::build` внутри контейнера.[cite:142][cite:143][cite:125] После успешной сборки workflow выгружает директорию `build/` как GitHub artifact, чтобы результаты можно было сохранить и использовать на следующих стадиях.[cite:156][cite:153] +Job `build` выполняет checkout репозитория, инициализирует Buildx, собирает devcontainer image с поддержкой кэша, проверяет версии инструментов внутри контейнера, синхронизирует Python tooling в `tools/host` через `uv sync`, а затем запускает `just ci::build` внутри контейнера. После успешной сборки workflow выгружает директорию `build/` как GitHub artifact, чтобы результаты можно было сохранить и использовать на следующих стадиях. -Важная техническая деталь — команды внутри контейнера запускаются с `--user root`.[cite:127][cite:135] Это требуется из-за того, что `GITHUB_WORKSPACE` подключается в контейнер как bind mount, а в GitHub Actions non-root пользователь внутри Docker часто не получает права на запись в такую директорию; ранее это как раз ломало создание `.venv` во время `uv sync`.[cite:127][cite:129][cite:135] +Важная техническая деталь — команды внутри контейнера запускаются с `--user root`.Это требуется из-за того, что `GITHUB_WORKSPACE` подключается в контейнер как bind mount, а в GitHub Actions non-root пользователь внутри Docker часто не получает права на запись в такую директорию; ранее это как раз ломало создание `.venv` во время `uv sync`. ## Что делает job `test` -Job `test` зависит от `build`, скачивает artifact с директорией `build/`, заново поднимает тот же devcontainer image с использованием cached layers, синхронизирует `tools/host` и запускает `just ci::test` внутри контейнера.[cite:156][cite:142][cite:143] На практике это означает, что host unit-тесты работают в той же программной среде, что и стадия сборки, но при этом выделены в отдельный CI-этап.[cite:125][cite:156] +Job `test` зависит от `build`, скачивает artifact с директорией `build/`, заново поднимает тот же devcontainer image с использованием cached layers, синхронизирует `tools/host` и запускает `just ci::test` внутри контейнера. На практике это означает, что host unit-тесты работают в той же программной среде, что и стадия сборки, но при этом выделены в отдельный CI-этап. -Так как GitHub-hosted runner’ы эфемерны, сам Docker image не передаётся напрямую между job’ами.[cite:143] Поэтому обмен между `build` и `test` организован двумя способами: ускорение повторной сборки образа идёт через Docker layer cache, а результаты проекта передаются через GitHub artifacts.[cite:143][cite:153][cite:156] +Так как GitHub-hosted runner’ы эфемерны, сам Docker image не передаётся напрямую между job’ами.[cite:143] Поэтому обмен между `build` и `test` организован двумя способами: ускорение повторной сборки образа идёт через Docker layer cache, а результаты проекта передаются через GitHub artifacts. ## Почему эта схема хорошо подходит проекту -Этот репозиторий нельзя считать обычным desktop C-проектом: он завязан на фиксированное расположение embedded toolchain и на специально подготовленный devcontainer.[cite:125] Ранние попытки выполнять pipeline прямо на runner’е падали, потому что проект ожидал наличие `/opt/arm-toolchain`, установленный `Ninja` и современный бинарник `just`, который понимает атрибуты вроде `[doc(...)]`.[cite:125][cite:97][cite:119] +Этот репозиторий нельзя считать обычным desktop C-проектом: он завязан на фиксированное расположение embedded toolchain и на специально подготовленный devcontainer.[cite:125] Ранние попытки выполнять pipeline прямо на runner’е падали, потому что проект ожидал наличие `/opt/arm-toolchain`, установленный `Ninja` и современный бинарник `just`, который понимает атрибуты вроде `[doc(...)]`. Перенос CI внутрь devcontainer image устраняет этот класс расхождений и делает Dockerfile единым источником истины для окружения, версий и путей.[cite:125] Это упрощает дальнейшее сопровождение: при изменении инструментария достаточно обновить Dockerfile, и эти же изменения автоматически начнут действовать как локально, так и в CI.[cite:125] ## Чего workflow пока не делает -Текущий pipeline пока не включает обязательную стадию `lint` и статический анализ, потому что в `just/ci.just` для `lint` пока ещё оставлена заглушка, а не полноценный вызов `clang-format` и `clang-tidy`.[cite:1] Он также пока не формирует release/HAB artifacts в CI, хотя в репозитории уже есть соответствующие рецепты `just ci::release` и связанные сборочные шаги.[cite:1] +Текущий pipeline пока не включает обязательную стадию `lint` и статический анализ, потому что в `just/ci.just` для `lint` пока ещё оставлена заглушка, а не полноценный вызов `clang-format` и `clang-tidy`. Он также пока не формирует release/HAB artifacts в CI, хотя в репозитории уже есть соответствующие рецепты `just ci::release` и связанные сборочные шаги. -Также pipeline пока не запускает HIL-сценарии.[cite:1] Это ожидаемо и правильно для текущего этапа: hardware-in-the-loop проверки требуют физического оборудования и в дальнейшем должны выполняться отдельно на self-hosted runner рядом с bench-стендом, а не на GitHub-hosted машинах.[cite:1] +Также pipeline пока не запускает HIL-сценарии. Это ожидаемо и правильно для текущего этапа: hardware-in-the-loop проверки требуют физического оборудования и в дальнейшем должны выполняться отдельно на self-hosted runner рядом с bench-стендом, а не на GitHub-hosted машинах. ## Сильные стороны текущего решения У текущей реализации уже есть несколько сильных сторон: -- Она воспроизводима, потому что сборка и тесты выполняются в том же образе, что и локальная разработка.[cite:125] -- Она ускоряется на повторных прогонах за счёт Docker layer caching через GitHub Actions cache backend.[cite:142][cite:143] -- Она модульна, потому что `build` и `test` вынесены в отдельные job’ы, связанные артефактами.[cite:156][cite:153] -- Она удобна для отладки, потому что build outputs сохраняются как artifacts, а workflow можно запускать вручную через `workflow_dispatch`.[cite:99][cite:156] -- Она хорошо вписана в структуру проекта, потому что использует уже существующие `just`-точки входа, а не дублирует build-логику в YAML.[cite:1] +- Она воспроизводима, потому что сборка и тесты выполняются в том же образе, что и локальная разработка. +- Она ускоряется на повторных прогонах за счёт Docker layer caching через GitHub Actions cache backend. +- Она модульна, потому что `build` и `test` вынесены в отдельные job’ы, связанные артефактами. +- Она удобна для отладки, потому что build outputs сохраняются как artifacts, а workflow можно запускать вручную через `workflow_dispatch`. +- Она хорошо вписана в структуру проекта, потому что использует уже существующие `just`-точки входа, а не дублирует build-логику в YAML. ## Текущие ограничения -Главное ограничение сейчас состоит в том, что pipeline проверяет собираемость и host unit-тесты, но ещё не закрывает style gate, static analysis, coverage и release packaging.[cite:1] Второе ограничение — Docker image пересобирается в каждой job, поэтому даже при наличии кэша остаётся неизбежный накладной расход по времени по сравнению с вариантом, где используется заранее опубликованный образ из registry.[cite:143] +Главное ограничение сейчас состоит в том, что pipeline проверяет собираемость и host unit-тесты, но ещё не закрывает style gate, static analysis, coverage и release packaging. Второе ограничение — Docker image пересобирается в каждой job, поэтому даже при наличии кэша остаётся неизбежный накладной расход по времени по сравнению с вариантом, где используется заранее опубликованный образ из registry.[cite:143] -Есть и архитектурное ограничение GitHub-hosted runner’ов для аппаратной части.[cite:127][cite:1] Прошивка через USB, pyOCD-сценарии и управление стендом должны в будущем быть вынесены в отдельную hardware lane на self-hosted runner.[cite:1] +Есть и архитектурное ограничение GitHub-hosted runner’ов для аппаратной части.[cite:127] Прошивка через USB, pyOCD-сценарии и управление стендом должны в будущем быть вынесены в отдельную hardware lane на self-hosted runner. ## Рекомендуемые следующие шаги ### Шаг 1 — добавить `lint` job -Самое логичное следующее улучшение — реализовать полноценную стадию `lint` в `just/ci.just` и подключить отдельную job в workflow.[cite:1] В эту стадию стоит включить `clang-format --dry-run --Werror`, `clang-tidy` и необходимые исключения для generated-кода или vendor-зависимостей, чтобы избежать лишнего шума в CI.[cite:1] +Самое логичное следующее улучшение — реализовать полноценную стадию `lint` в `just/ci.just` и подключить отдельную job в workflow. В эту стадию стоит включить `clang-format --dry-run --Werror`, `clang-tidy` и необходимые исключения для generated-кода или vendor-зависимостей, чтобы избежать лишнего шума в CI. ### Шаг 2 — добавить coverage -После стабилизации `lint` полезно подключить экспорт coverage для host-тестов.[cite:1] В `ci.just` уже существует закрытый рецепт `_coverage`, и его можно развить до генерации XML-отчёта, выгрузки артефактов и последующей интеграции с внешним coverage-сервисом, если это будет нужно.[cite:1] +После стабилизации `lint` полезно подключить экспорт coverage для host-тестов. В `ci.just` уже существует закрытый рецепт `_coverage`, и его можно развить до генерации XML-отчёта, выгрузки артефактов и последующей интеграции с внешним coverage-сервисом, если это будет нужно. ### Шаг 3 — выделить release workflow -Release packaging лучше оформлять отдельным workflow или отдельной gated job, запускаемой только по тегам, на `main` или вручную через `workflow_dispatch`.[cite:1] Это позволит не замедлять обычный PR-цикл, но при этом использовать `just ci::release` и публикацию HAB-артефактов тогда, когда это действительно нужно.[cite:1] +Release packaging лучше оформлять отдельным workflow или отдельной gated job, запускаемой только по тегам, на `main` или вручную через `workflow_dispatch`. Это позволит не замедлять обычный PR-цикл, но при этом использовать `just ci::release` и публикацию HAB-артефактов тогда, когда это действительно нужно. ### Шаг 4 — публиковать devcontainer image в GHCR -Следующий сильный шаг по оптимизации — публиковать devcontainer image в GHCR и затем запускать CI уже на базе заранее собранного образа, а не пересобирать его в каждой job.[cite:142][cite:146] Это ещё сильнее сократит время старта pipeline и сделает масштабирование на `lint`, `coverage` и `release` заметно проще.[cite:142][cite:143] +Следующий сильный шаг по оптимизации — публиковать devcontainer image в GHCR и затем запускать CI уже на базе заранее собранного образа, а не пересобирать его в каждой job.Это ещё сильнее сократит время старта pipeline и сделает масштабирование на `lint`, `coverage` и `release` заметно проще. ### Шаг 5 — добавить self-hosted HIL lane -Финальное крупное направление развития — выделенный аппаратный workflow на self-hosted runner с доступом к MCU-Link, target board и M5StampPLC.[cite:1] Такую lane лучше запускать вручную, по расписанию или по label-триггеру, а не делать обязательной для каждого PR, поскольку аппаратные проверки медленнее, менее стабильны и по природе отличаются от быстрых software regression checks.[cite:1] +Финальное крупное направление развития — выделенный аппаратный workflow на self-hosted runner с доступом к MCU-Link, target board и M5StampPLC. Такую lane лучше запускать вручную, по расписанию или по label-триггеру, а не делать обязательной для каждого PR, поскольку аппаратные проверки медленнее, менее стабильны и по природе отличаются от быстрых software regression checks. ## Целевое состояние -Зрелая версия этого CI/CD контура, вероятно, будет состоять из четырёх независимых линий: быстрый PR-pipeline (`build`, `test`, `lint`), optional coverage reporting, отдельный release workflow и отдельный self-hosted HIL pipeline.[cite:1][cite:156] Такая структура сохранит короткий feedback loop для обычной разработки и одновременно покроет полный жизненный цикл embedded-проекта: от изменений в исходниках до production artifacts и аппаратной валидации на стенде.[cite:1] +Зрелая версия этого CI/CD контура, вероятно, будет состоять из четырёх независимых линий: быстрый PR-pipeline (`build`, `test`, `lint`), optional coverage reporting, отдельный release workflow и отдельный self-hosted HIL pipeline.[cite:156] Такая структура сохранит короткий feedback loop для обычной разработки и одновременно покроет полный жизненный цикл embedded-проекта: от изменений в исходниках до production artifacts и аппаратной валидации на стенде. diff --git a/tests/host/protocol/test_protocol.c b/tests/host/protocol/test_protocol.c index f2a8066..b5c22bf 100644 --- a/tests/host/protocol/test_protocol.c +++ b/tests/host/protocol/test_protocol.c @@ -220,7 +220,7 @@ void test_confirm_request_default_timeout_on_zero(void) protocol_send_confirm_request(¶ms); /* timeout_ms == 0 → подставляется PROTOCOL_CONFIRM_TIMEOUT_MS */ - TEST_ASSERT_NOT_NULL(strstr(s_captured, "\"timeout_ms\":15000")); + TEST_ASSERT_NOT_NULL(strstr(s_captured, "\"timeout_ms\":30000")); } /* ── Тесты: pong ───────────────────────────────────────────────────────── */ diff --git a/tools/hil/README.md b/tools/hil/README.md index c36335a..36b358d 100644 --- a/tools/hil/README.md +++ b/tools/hil/README.md @@ -38,11 +38,11 @@ tools/hil/ Полное описание стека HIL-тестирования — в `docs/testing/hil/`: -| Документ | Содержимое | -|----------|-----------| -| [HIL_HOWTO.md](../../docs/testing/hil/HIL_HOWTO.md) | Как запускать HIL-тесты (пошагово) | -| [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле | -| [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест | +| Документ | Содержимое | +| --------------------------------------------------------------- | ---------------------------------------------- | +| [HIL_HOWTO.md](../../docs/testing/hil/HIL_HOWTO.md) | Как запускать HIL-тесты (пошагово) | +| [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле | +| [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест | --- diff --git a/tools/host/README.md b/tools/host/README.md index ffdc5dc..f73cde0 100644 --- a/tools/host/README.md +++ b/tools/host/README.md @@ -9,7 +9,7 @@ Python-окружение на базе [uv](https://docs.astral.sh/uv/) для ## Структура -```иbash +```bash tools/host/ ├── flash_usb.py — прошивка через USB ROM: sdphost → Flashloader → Flash ├── flash_swd.py — прошивка через SWD: FCB + HAB → pyOCD → Flash diff --git a/utils/README.md b/utils/README.md index 1b575f8..d891f07 100644 --- a/utils/README.md +++ b/utils/README.md @@ -13,7 +13,7 @@ ## Модули -| Модуль | Путь | Описание | -|--------|------|----------| -| `ring_buffer` | [ring_buffer/](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free | -| `log` | [log/](log/README.md) | Платформонезависимый логгер с callback-транспортом | +| Модуль | Путь | Описание | +| ------------- | ------------------------------------- | -------------------------------------------------- | +| `ring_buffer` | [ring_buffer/](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free | +| `log` | [log/](log/README.md) | Платформонезависимый логгер с callback-транспортом |