# test_display: bsp_display + test_template

This commit is contained in:
Dmitry Akimov 2026-05-08 10:50:58 +03:00
parent da48fbf75d
commit 22f98c311a
37 changed files with 1282 additions and 458 deletions

View file

@ -8,33 +8,33 @@
## Три firmware-проекта ## Три firmware-проекта
| Проект | Путь | Описание | | Проект | Путь | Описание |
|--------|------|----------| | ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | | Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Сам обновляется только через USB ROM + blhost | | Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Сам обновляется только через USB ROM + blhost / SWD |
| Боевая прошивка | `firmware/tft_app/` | FreeRTOS + FatFS + бизнес-логика. Обновляется загрузчиком | | Production прошивка | `firmware/tft_app/` | FreeRTOS + FatFS + бизнес-логика. Обновляется загрузчиком |
--- ---
## BSP ## BSP
| Модуль | Путь | Описание | | Модуль | Путь | Описание |
|--------|------|----------| | --------------- | ---------------- | --------------------------------------------------------- |
| `bsp_led` | `bsp/led/` | Два UserLed (GPIO3[3], GPIO3[4]) | | `bsp_led` | `bsp/led/` | Два UserLed (GPIO3[3], GPIO3[4]) |
| `bsp_tick` | `bsp/tick/` | SysTick / FreeRTOS-совместимый таймер | | `bsp_tick` | `bsp/tick/` | SysTick / FreeRTOS-совместимый таймер |
| `bsp_uart_host` | `bsp/uart_host/` | LPUART1 — MCU-Link VCOM (J2) | | `bsp_uart_host` | `bsp/uart_host/` | LPUART1 — MCU-Link VCOM (J2) |
| `bsp_opto` | `bsp/opto/` | Оптоизолированные входы PS2801-4: EXT_IN1, EXT_IN2, RS_RX | | `bsp_opto` | `bsp/opto/` | Оптоизолированные входы PS2801-4: EXT_IN1, EXT_IN2, RS_RX |
| `bsp_usb_cdc` | `bsp/usb_cdc/` | USB CDC ACM | | `bsp_usb_cdc` | `bsp/usb_cdc/` | USB CDC ACM |
| generated | `bsp/generated/` | NXP Config Tools: pin_mux, clock_config, board, startup | | generated | `bsp/generated/` | NXP Config Tools: pin_mux, clock_config, board, startup |
--- ---
## Тестирование ## Тестирование
| Уровень | Где | Инструменты | Запуск | | Уровень | Где | Инструменты | Запуск |
|---------|-----|-------------|--------| | ---------------- | ------------------------------ | -------------------------------------- | -------------------------------------- |
| Host unit-тесты | `tests/host/` | Unity + fff, clang | `just build::test-host` (devcontainer) | | 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` (хост) | | HIL target-тесты | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest + M5StampPLC | `just host::hil-run` (хост) |
**Host-тесты** запускаются в devcontainer без железа. BSP-модули тестируются через fff-фейки и stub-хедеры. **Host-тесты** запускаются в devcontainer без железа. BSP-модули тестируются через fff-фейки и stub-хедеры.
@ -75,12 +75,12 @@ just host::debug-server # GDB-сервер для отладки
## Зависимости ## Зависимости
| | Подход | | | Подход |
|--|--------| | --------------------------------------------- | -------------------- |
| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored | | NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored |
| Unity, fff, SEGGER RTT | vendored | | Unity, fff, SEGGER RTT | vendored |
| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` | | pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` |
| spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` | | spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` |
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей). Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей).

View file

@ -15,6 +15,7 @@ add_subdirectory(uart_host)
add_subdirectory(opto) add_subdirectory(opto)
add_subdirectory(can) add_subdirectory(can)
add_subdirectory(button) add_subdirectory(button)
add_subdirectory(display)
add_subdirectory(usb_cdc) add_subdirectory(usb_cdc)
add_subdirectory(sdram) add_subdirectory(sdram)
add_subdirectory(qspi_flash) add_subdirectory(qspi_flash)

View file

@ -8,10 +8,10 @@
## Аппаратура ## Аппаратура
| Кнопка | Пин MCU | GPIO | Схема | Нажатие | | Кнопка | Пин MCU | GPIO | Схема | Нажатие |
|----------------|--------------|-----------|--------------------------------|---------| | -------------- | ---------- | --------- | ----------------------------- | ------- |
| `BSP_BUTTON_1` | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW | | `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 | | `BSP_BUTTON_2` | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW |
Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`) как INPUT с включённым Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`) как INPUT с включённым
гистерезисом, без внутренней подтяжки (`0x0100B0`). `bsp_button_init()` не трогает гистерезисом, без внутренней подтяжки (`0x0100B0`). `bsp_button_init()` не трогает
@ -189,20 +189,20 @@ pytest: `tools/hil/04_test_button.py` — помечен `@pytest.mark.interacti
Команды CLI прошивки: Команды CLI прошивки:
| Команда | Ответ | Описание | | Команда | Ответ | Описание |
|--------------|-------------|--------------------------------------------| | --------------- | --------- | ------------------------------------- |
| `PING` | `PONG` | Проверка канала | | `PING` | `PONG` | Проверка канала |
| `READ <idx>` | `1` / `0` | Сырое состояние (без debounce) | | `READ <idx>` | `1` / `0` | Сырое состояние (без debounce) |
| `STATE <idx>`| `1` / `0` | Стабильное состояние после debounce | | `STATE <idx>` | `1` / `0` | Стабильное состояние после debounce |
| `EVENT_P <idx>`| `1` / `0` | `get_event_pressed`, сбрасывает флаг | | `EVENT_P <idx>` | `1` / `0` | `get_event_pressed`, сбрасывает флаг |
| `EVENT_R <idx>`| `1` / `0` | `get_event_released`, сбрасывает флаг | | `EVENT_R <idx>` | `1` / `0` | `get_event_released`, сбрасывает флаг |
--- ---
## Зависимости ## Зависимости
| Зависимость | Тип | Описание | | Зависимость | Тип | Описание |
|--------------|---------|----------------------------------------------| | ------------ | ------- | -------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | | `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers | | `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers |
| `sdk_gpio` | PRIVATE | `fsl_gpio.h``GPIO_ReadPinInput()` | | `sdk_gpio` | PRIVATE | `fsl_gpio.h``GPIO_ReadPinInput()` |

View file

@ -27,7 +27,6 @@
#include "bsp/status.h" #include "bsp/status.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
/* ------------------------------------------------------------------------- /* -------------------------------------------------------------------------
* Типы * Типы

View file

@ -34,11 +34,11 @@
## Распределение Message Buffers ## Распределение Message Buffers
| MB | Назначение | | MB | Назначение |
|-------|-----------------------------------------------------| | ----- | -------------------------------------------------- |
| 0 | Зарезервирован (ERR005829 workaround: inactive TX) | | 0 | Зарезервирован (ERR005829 workaround: inactive TX) |
| 1 | TX — отправка фреймов | | 1 | TX — отправка фреймов |
| 2..17 | RX — до 16 индивидуальных фильтров | | 2..17 | RX — до 16 индивидуальных фильтров |
ERR005829 — errata FlexCAN на i.MX RT1050/1052: при гонке TX/RX арбитража ERR005829 — errata FlexCAN на i.MX RT1050/1052: при гонке TX/RX арбитража
MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMbInactive`. MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMbInactive`.
@ -104,12 +104,12 @@ bsp_can_accept_all();
затем ждёт флага завершения с polling `FLEXCAN_GetMbStatusFlags()`. затем ждёт флага завершения с polling `FLEXCAN_GetMbStatusFlags()`.
Возвращается по одному из условий: Возвращается по одному из условий:
| Условие | Возврат | | Условие | Возврат |
|---------|---------| | -------------------------------- | ----------------- |
| Фрейм успешно отправлен | `BSP_OK` | | Фрейм успешно отправлен | `BSP_OK` |
| TX MB занят предыдущей передачей | `BSP_ERR_BUSY` | | TX MB занят предыдущей передачей | `BSP_ERR_BUSY` |
| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | | Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` |
| Невалидные параметры | `BSP_ERR_PARAM` | | Невалидные параметры | `BSP_ERR_PARAM` |
При 500 kbit/s максимальное время отправки одного фрейма — ~260 мкс. При 500 kbit/s максимальное время отправки одного фрейма — ~260 мкс.
Для bare-metal и FreeRTOS-задачи это приемлемо. Для bare-metal и FreeRTOS-задачи это приемлемо.
@ -119,11 +119,11 @@ bsp_can_accept_all();
Polling с таймаутом. Обходит все активные RX MB, читает готовые фреймы Polling с таймаутом. Обходит все активные RX MB, читает готовые фреймы
во внутренний ring buffer, пытается извлечь один фрейм: во внутренний ring buffer, пытается извлечь один фрейм:
| Условие | Возврат | | Условие | Возврат |
|---------|---------| | ------------------------------- | ----------------- |
| Фрейм найден (из буфера или MB) | `BSP_OK` | | Фрейм найден (из буфера или MB) | `BSP_OK` |
| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | | Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` |
| Невалидные параметры | `BSP_ERR_PARAM` | | Невалидные параметры | `BSP_ERR_PARAM` |
```c ```c
/* Неблокирующий опрос — timeout_ms = 0 */ /* Неблокирующий опрос — timeout_ms = 0 */
@ -161,10 +161,10 @@ bsp_can_register_rx_callback(can_isr_to_queue, NULL);
Модуль не зависит от FreeRTOS и работает в обоих контекстах: Модуль не зависит от FreeRTOS и работает в обоих контекстах:
| Контекст | TX | RX | | Контекст | TX | RX |
|----------|----|----| | --------------------------------- | ----------------------------------- | ---------------------------------------------- |
| bare-metal (`firmware/test`, HIL) | `bsp_can_send()` — blocking polling | `bsp_can_receive()` — polling | | 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 (`firmware/tft_app`) | `bsp_can_send()` — из задачи | `bsp_can_receive()` — из задачи с `timeout_ms` |
Для FreeRTOS с минимальной латентностью — будущий callback + `xQueueSendFromISR()`. Для FreeRTOS с минимальной латентностью — будущий callback + `xQueueSendFromISR()`.
Polling с `timeout_ms = 10` из задачи подходит для протоколов с интервалом > 10 мс. Polling с `timeout_ms = 10` из задачи подходит для протоколов с интервалом > 10 мс.
@ -239,10 +239,10 @@ CAN-адаптер на стороне хоста — M5Stack с CAN-модул
## Зависимости ## Зависимости
| Зависимость | Тип | Описание | | Зависимость | Тип | Описание |
|-------------|-----|----------| | -------------- | ------- | -------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | | `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | | `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов |
| `ring_buffer` | PRIVATE | Внутренний RX FIFO | | `ring_buffer` | PRIVATE | Внутренний RX FIFO |
| `sdk_flexcan` | PRIVATE | `fsl_flexcan.h` — FlexCAN2 SDK драйвер | | `sdk_flexcan` | PRIVATE | `fsl_flexcan.h` — FlexCAN2 SDK драйвер |
| `clock_config` | PRIVATE | `BOARD_BOOTCLOCKRUN_CAN_CLK_ROOT` | | `clock_config` | PRIVATE | `BOARD_BOOTCLOCKRUN_CAN_CLK_ROOT` |

View file

@ -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)

View file

@ -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 <stdbool.h>
#include <stdint.h>
/* ── Максимальные размеры (для статического выделения буферов) ───────── */
/** @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_ */

414
bsp/display/src/display.c Normal file
View file

@ -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;
}

View file

@ -43,12 +43,12 @@ bsp/
Внешний кварц **24 МГц**. Режим максимальной производительности (внешнее питание, энергопотребление не критично). Внешний кварц **24 МГц**. Режим максимальной производительности (внешнее питание, энергопотребление не критично).
| Параметр | Значение | | Параметр | Значение |
|---|---| | ------------------------ | ----------------------------------------- |
| ARM PLL (PLL1) | 1200 МГц (VDIV = 50) | | ARM PLL (PLL1) | 1200 МГц (VDIV = 50) |
| CPU / AHB | 600 МГц (ARM_PODF /2, AHB_PODF /1) | | CPU / AHB | 600 МГц (ARM_PODF /2, AHB_PODF /1) |
| IPG | 150 МГц (IPG_PODF /4) | | IPG | 150 МГц (IPG_PODF /4) |
| PLL1 bypass clock | REF_CLK_24M (внешний кварц) | | PLL1 bypass clock | REF_CLK_24M (внешний кварц) |
| Внутренний RC осциллятор | включён (используется BootROM при старте) | | Внутренний RC осциллятор | включён (используется BootROM при старте) |
**SEMC и FlexSPI** в Clocks Tool настроены формально — реальная инициализация выполняется через **DCD** (SDRAM) и **FDCB** (QSPI Flash) до передачи управления прикладному коду. Для корректной сборки необходимы следующие дефайны: **SEMC и FlexSPI** в Clocks Tool настроены формально — реальная инициализация выполняется через **DCD** (SDRAM) и **FDCB** (QSPI Flash) до передачи управления прикладному коду. Для корректной сборки необходимы следующие дефайны:

View file

@ -7,9 +7,9 @@
## Аппаратная часть ## Аппаратная часть
| `led_id_t` | Сигнал | GPIO | Pin | Координата | Активный уровень | | `led_id_t` | Сигнал | GPIO | Pin | Координата | Активный уровень |
|-----------------|------------|-------|-----|------------|-----------------| | --------------- | ---------- | ----- | --- | ---------- | ---------------- |
| `LED_HEARTBEAT` | `UserLed1` | GPIO3 | 3 | M4 | LOW (0 = горит) | | `LED_HEARTBEAT` | `UserLed1` | GPIO3 | 3 | M4 | LOW (0 = горит) |
| `LED_APP` | `UserLed2` | GPIO3 | 4 | P2 | LOW (0 = горит) | | `LED_APP` | `UserLed2` | GPIO3 | 4 | P2 | LOW (0 = горит) |
Пины сконфигурированы в `generated/pin_mux.h` (MCUXpresso Config Tools). Пины сконфигурированы в `generated/pin_mux.h` (MCUXpresso Config Tools).
`INIT_GPIO_VALUE = 1U`оба LED выключены сразу после `led_init()`. `INIT_GPIO_VALUE = 1U`оба LED выключены сразу после `led_init()`.

View file

@ -198,7 +198,7 @@ extern "C"
* Должна вызываться из main loop на каждой итерации. НЕ вызывать из ISR. * Должна вызываться из main loop на каждой итерации. НЕ вызывать из ISR.
* Каналы MODE_PROTO пропускаются для них используется коллбэк из ISR. * Каналы MODE_PROTO пропускаются для них используется коллбэк из ISR.
*/ */
//FIXME: poll //FIXME: poll
void bsp_opto_process(void); void bsp_opto_process(void);
/** /**

View file

@ -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 и сразу проверю сборку, чтобы не оставить таких разрывов.

View file

@ -38,13 +38,13 @@ sdk_usdhc ← NXP HAL: fsl_usdhc
## Аппаратный контекст ## Аппаратный контекст
| Сигнал | Пин MCU | Конфигурация | | Сигнал | Пин MCU | Конфигурация |
|----------|-----------------|---------------------------------------------------| | ------ | ---------------- | ------------------------------------------------ |
| CLK | GPIO_SD_B0_01 | USDHC1_CLK, периферийный режим | | CLK | GPIO_SD_B0_01 | USDHC1_CLK, периферийный режим |
| CMD | GPIO_SD_B0_00 | USDHC1_CMD, периферийный режим | | CMD | GPIO_SD_B0_00 | USDHC1_CMD, периферийный режим |
| D0D3 | GPIO_SD_B0_0205| USDHC1_DATA03, периферийный режим | | D0D3 | GPIO_SD_B0_0205 | USDHC1_DATA03, периферийный режим |
| CD_B | GPIO_B1_12 | USDHC1_CD_B — детект через GPIO2[28] | | CD_B | GPIO_B1_12 | USDHC1_CD_B — детект через GPIO2[28] |
| SdPwr | GPIO_AD_B1_03 | GPIO1[19], active-low, управляется SDK через BSP | | SdPwr | GPIO_AD_B1_03 | GPIO1[19], active-low, управляется SDK через BSP |
**CD_B** подключён как периферийный сигнал USDHC1, а не как GPIO. Детект карты **CD_B** подключён как периферийный сигнал USDHC1, а не как GPIO. Детект карты
читается через `USDHC_GetPresentStatusFlags``kUSDHC_CardInsertedFlag`. читается через `USDHC_GetPresentStatusFlags``kUSDHC_CardInsertedFlag`.
@ -92,13 +92,13 @@ host-контроллер (`SD_HostInit`).
## Разделение ответственности: bsp_sd vs sdmmc_config vs port_fatfs_sd ## Разделение ответственности: bsp_sd vs sdmmc_config vs port_fatfs_sd
| Слой | Что делает | Где живёт | | Слой | Что делает | Где живёт |
|-----------------------|---------------------------------------------------------|------------------------------------| | --------------------- | ------------------------------------------------------- | ----------------------------------- |
| `sdmmc_config` | Константы платы, `BOARD_SD_Config`, GPIO питания, pads | `bsp/generated/` | | `sdmmc_config` | Константы платы, `BOARD_SD_Config`, GPIO питания, pads | `bsp/generated/` |
| `bsp_sd` | `SD_HostInit/Deinit`, идемпотентность, card detect | `bsp/sd/` | | `bsp_sd` | `SD_HostInit/Deinit`, идемпотентность, card detect | `bsp/sd/` |
| `port_fatfs_sd` | `microsd_disk_*``fsl_sd_disk` (FatFS diskio glue) | `port/fatfs/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/` | | `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/` (будущее)| | `tft_app_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (FreeRTOS ffconf) | `firmware/tft_app/fatfs/` (будущее) |
**Почему `ff.c` и `fsl_sd_disk.c` не компилируются один раз как общая библиотека:** **Почему `ff.c` и `fsl_sd_disk.c` не компилируются один раз как общая библиотека:**
оба включают `ff.h``ffconf.h`, который разный для `firmware_test` (bare-metal, оба включают `ff.h``ffconf.h`, который разный для `firmware_test` (bare-metal,

View file

@ -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 на таргет. По умолчанию будут прогоняться быстрые тесты, но по желанию оператора, либо раз в десять устройств будет выполняться долгий тест (что думаешь на этот счет?)

View file

@ -32,8 +32,6 @@
#include "bsp/status.h" #include "bsp/status.h"
#include <stdint.h>
/** /**
* @brief Размер расширенного тестового региона, байт (27 MB). * @brief Размер расширенного тестового региона, байт (27 MB).
* *

View file

@ -59,11 +59,11 @@ target_compile_definitions(firmware_test PRIVATE
) )
``` ```
| Define | Дефолт | Описание | | Define | Дефолт | Описание |
|--------|--------|----------| | ------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------- |
| `BSP_UART_HOST_RX_BUFFER_SIZE` | `256` | Размер RX ring buffer. **Должен быть степенью двойки.** | | `BSP_UART_HOST_RX_BUFFER_SIZE` | `256` | Размер RX ring buffer. **Должен быть степенью двойки.** |
| `BSP_UART_HOST_SRC_CLOCK_HZ` | `24000000` | Частота источника тактирования LPUART1. | | `BSP_UART_HOST_SRC_CLOCK_HZ` | `24000000` | Частота источника тактирования LPUART1. |
| `BSP_UART_HOST_IRQ_PRIORITY` | `5` | Приоритет `LPUART1_IRQn`. Должен быть ≥ `configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS. | | `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_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | | `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов |
| `utils` (ring_buffer) | PRIVATE | RX ring buffer | | `utils` (ring_buffer) | PRIVATE | RX ring buffer |
| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` | | `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` |

View file

@ -11,11 +11,11 @@ CLI команды, обновление конфигурации. Работа
## Аппаратура ## Аппаратура
| Сигнал | Пин MCU | Назначение | | Сигнал | Пин MCU | Назначение |
|--------------|----------------|----------------------------------| | ------------- | ------------- | -------------------------- |
| USB_OTG1_DN | USB_OTG1_DN | USB1 Data | | USB_OTG1_DN | USB_OTG1_DN | USB1 Data |
| USB_OTG1_DP | USB_OTG1_DP | USB1 Data+ | | USB_OTG1_DP | USB_OTG1_DP | USB1 Data+ |
| USB_OTG1_VBUS| USB_OTG1_VBUS | VBUS detect (self-powered) | | USB_OTG1_VBUS | USB_OTG1_VBUS | VBUS detect (self-powered) |
Встроенный HS PHY (480 MHz PLL). Контроллер: EHCI0 (`kUSB_ControllerEhci0`). Встроенный HS PHY (480 MHz PLL). Контроллер: EHCI0 (`kUSB_ControllerEhci0`).
Скорость: High-Speed (480 Mbit/s) при поддержке хоста, fallback Full-Speed (12 Mbit/s). Скорость: 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). Модуль использует **lite** вариант NXP USB стека (не full class framework).
Это сознательное решение: Это сознательное решение:
| Аспект | Full stack | Lite stack (наш выбор) | | Аспект | Full stack | Lite stack (наш выбор) |
|--------|-----------|----------------------| | ------------------ | -------------------------------------- | ----------------------------- |
| Class framework | `usb_device_class.h`, `class_handle_t` | Отсутствует | | Class framework | `usb_device_class.h`, `class_handle_t` | Отсутствует |
| `usb_device_ch9.c` | SDK middleware, тянет class driver | Приватная копия в `src/` | | `usb_device_ch9.c` | SDK middleware, тянет class driver | Приватная копия в `src/` |
| CDC ACM хедер | Полный: struct + API функции | Только define-ы request codes | | CDC ACM хедер | Полный: struct + API функции | Только define-ы request codes |
| Callbacks | Через class driver dispatch | Напрямую в `usb_cdc.c` | | Callbacks | Через class driver dispatch | Напрямую в `usb_cdc.c` |
| Размер кода | ~12 KB | ~6 KB | | Размер кода | ~12 KB | ~6 KB |
| Гибкость | Multi-class composite | Один CDC ACM | | Гибкость | Multi-class composite | Один CDC ACM |
Lite stack достаточен для одного CDC ACM интерфейса. Переход на full stack Lite stack достаточен для одного CDC ACM интерфейса. Переход на full stack
понадобится только при добавлении composite device (CDC + MSC). понадобится только при добавлении composite device (CDC + MSC).
@ -169,12 +169,12 @@ NVIC enable → USB_DeviceRun. Включает задержку 5 мс для
Неблокирующая отправка. Копирует данные в NonCacheable TX буфер и ставит в очередь Неблокирующая отправка. Копирует данные в NonCacheable TX буфер и ставит в очередь
USB IN transfer. Максимум `BSP_USB_CDC_MAX_PACKET_SIZE` (512) байт за вызов. USB IN transfer. Максимум `BSP_USB_CDC_MAX_PACKET_SIZE` (512) байт за вызов.
| Возврат | Условие | | Возврат | Условие |
|---------|---------| | ------------------- | ------------------------------------------ |
| `BSP_OK` | Transfer поставлен в очередь | | `BSP_OK` | Transfer поставлен в очередь |
| `BSP_ERR_BUSY` | Предыдущий transfer не завершён | | `BSP_ERR_BUSY` | Предыдущий transfer не завершён |
| `BSP_ERR_NOT_READY` | Хост не подключён | | `BSP_ERR_NOT_READY` | Хост не подключён |
| `BSP_ERR_INVALID` | `data == NULL`, `len == 0` или `len > 512` | | `BSP_ERR_INVALID` | `data == NULL`, `len == 0` или `len > 512` |
Проверить готовность TX канала перед отправкой: `bsp_usb_cdc_write_ready()`. Проверить готовность TX канала перед отправкой: `bsp_usb_cdc_write_ready()`.
@ -250,10 +250,10 @@ USB_OTG1_IRQHandler (usb_cdc_hw.c)
Модуль работает без изменений в контексте FreeRTOS-задачи: Модуль работает без изменений в контексте FreeRTOS-задачи:
| Контекст | TX | RX | | Контекст | TX | RX |
|----------|----|----| | ---------- | ------------------------------------------- | ------------------------------ |
| bare-metal | `bsp_usb_cdc_write()` — non-blocking | `bsp_usb_cdc_read()` — polling | | bare-metal | `bsp_usb_cdc_write()` — non-blocking | `bsp_usb_cdc_read()` — polling |
| FreeRTOS | Из задачи, `write_ready()` + `vTaskDelay()` | Из задачи с yield | | FreeRTOS | Из задачи, `write_ready()` + `vTaskDelay()` | Из задачи с yield |
Для минимальной латентности в FreeRTOS — будущий `USB_DEVICE_CONFIG_USE_TASK=1` Для минимальной латентности в FreeRTOS — будущий `USB_DEVICE_CONFIG_USE_TASK=1`
с `bsp_usb_cdc_poll()` из выделенной задачи. с `bsp_usb_cdc_poll()` из выделенной задачи.
@ -295,9 +295,9 @@ just host::hil-usb-cdc
Команды CLI прошивки: Команды CLI прошивки:
| Команда | Ответ | Описание | | Команда | Ответ | Описание |
|---------|-------|----------| | ------------- | -------- | ---------------- |
| `PING` | `PONG` | Проверка канала | | `PING` | `PONG` | Проверка канала |
| `ECHO <data>` | `<data>` | Echo-back данных | | `ECHO <data>` | `<data>` | Echo-back данных |
### Host unit-тесты ### Host unit-тесты
@ -311,15 +311,15 @@ just host::hil-usb-cdc
Все настройки находятся в приватных хедерах `src/`: Все настройки находятся в приватных хедерах `src/`:
| Файл | Настройка | Значение | Описание | | Файл | Настройка | Значение | Описание |
|------|-----------|----------|----------| | ------------------------- | ------------------------------- | -------- | -------------------------------- |
| `usb_device_config.h` | `USB_DEVICE_CONFIG_EHCI` | `1` | Контроллер EHCI0 | | `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_ENDPOINTS` | `4` | EP0 + interrupt IN + bulk IN/OUT |
| `usb_device_config.h` | `USB_DEVICE_CONFIG_SELF_POWER` | `1` | Self-powered device | | `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_VID` | `0x1234` | Vendor ID (placeholder) |
| `usb_device_descriptor.h` | `USB_DEVICE_PID` | `0x0001` | Product 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` | `USB_DEVICE_INTERRUPT_PRIORITY` | `3` | NVIC приоритет |
| `usb_cdc_hw.c` | `BOARD_USB_PHY_D_CAL` | `0x0C` | PHY калибровка | | `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_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: `clock_config.h`, `pin_mux.h`, SDK headers | | `bsp_board` | PRIVATE | Транзитивно: `clock_config.h`, `pin_mux.h`, SDK headers |
| `sdk_usb_device_ehci` | PRIVATE | EHCI контроллер + DCI абстракция | | `sdk_usb_device_ehci` | PRIVATE | EHCI контроллер + DCI абстракция |
| `sdk_usb_phy` | PRIVATE | USB PHY инициализация (480 MHz PLL) | | `sdk_usb_phy` | PRIVATE | USB PHY инициализация (480 MHz PLL) |
| `sdk_osa_bm` | PRIVATE | OS Abstraction Layer (bare-metal, generic list) | | `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_common` | PRIVATE (транзитивно) | USB common headers (`usb.h`, `usb_misc.h`) |
| `sdk_usb_config` | PRIVATE (транзитивно) | INTERFACE: проброс конфиг-хедеров в SDK | | `sdk_usb_config` | PRIVATE (транзитивно) | INTERFACE: проброс конфиг-хедеров в SDK |
### Зависимости на уровне SDK CMake ### Зависимости на уровне SDK CMake

View file

@ -29,7 +29,7 @@ ENTRY(Reset_Handler)
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x0400; HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x0400;
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x0400; STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x0400;
VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0; 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; NCACHE_HEAP_SIZE = DEFINED(__heap_noncacheable__) ? HEAP_SIZE : 0x0000;
/* Specify the memory areas */ /* Specify the memory areas */
@ -40,8 +40,8 @@ MEMORY
m_interrupts (RX) : ORIGIN = 0x60002000, LENGTH = 0x00000400 m_interrupts (RX) : ORIGIN = 0x60002000, LENGTH = 0x00000400
m_text (RX) : ORIGIN = 0x60002400, LENGTH = 0x03FFDC00 m_text (RX) : ORIGIN = 0x60002400, LENGTH = 0x03FFDC00
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000
m_data (RW) : ORIGIN = 0x80000000, LENGTH = DEFINED(__heap_noncacheable__) ? 0x01E00000 : 0x01E00000 - HEAP_SIZE m_data (RW) : ORIGIN = 0x80000000, LENGTH = 0x01600000 /* 22 MB — хватает для всего */
m_ncache (RW) : ORIGIN = 0x81E00000, LENGTH = DEFINED(__heap_noncacheable__) ? 0x00200000 - HEAP_SIZE : 0x00200000 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_data2 (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000
m_data3 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 m_data3 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000
m_heap (RW) : ORIGIN = NCACHE_HEAP_START, LENGTH = HEAP_SIZE m_heap (RW) : ORIGIN = NCACHE_HEAP_START, LENGTH = HEAP_SIZE

View file

@ -65,18 +65,18 @@ HIL-тесты через pyOCD + pytest, GDB-сервер для отладки
## 3. Что устанавливается и где ## 3. Что устанавливается и где
| Инструмент | Хост | Devcontainer | | Инструмент | Хост | Devcontainer |
|------------|------|--------------| | ----------------------------------- | --------------- | --------------- |
| `just` | ✅ | ✅ Dockerfile | | `just` | ✅ | ✅ Dockerfile |
| `docker` | ✅ | — | | `docker` | ✅ | — |
| `uv` | ✅ | ✅ Dockerfile | | `uv` | ✅ | ✅ Dockerfile |
| `spsdk` (sdphost, blhost, nxpimage) | ✅ `tools/host/` | ✅ `tools/host/` | | `spsdk` (sdphost, blhost, nxpimage) | ✅ `tools/host/` | ✅ `tools/host/` |
| `pyocd` + `pyserial` + `pytest` | ✅ `tools/hil/` | — | | `pyocd` + `pyserial` + `pytest` | ✅ `tools/hil/` | — |
| `mpremote` | ✅ `tools/hil/` | — | | `mpremote` | ✅ `tools/hil/` | — |
| ARM GCC toolchain | — | ✅ | | ARM GCC toolchain | — | ✅ |
| `cmake` / `ninja` | — | ✅ | | `cmake` / `ninja` | — | ✅ |
| `clang` / `clangd` / `clang-tidy` | — | ✅ | | `clang` / `clangd` / `clang-tidy` | — | ✅ |
| Unity / fff | — | ✅ vendored | | Unity / fff | — | ✅ vendored |
`spsdk` и `pyocd` — отдельные uv-проекты с разными ролями: `spsdk` и `pyocd` — отдельные uv-проекты с разными ролями:
@ -226,11 +226,11 @@ HIL_USB_CDC_TIMEOUT=5.0
### 5.1 Предварительные требования ### 5.1 Предварительные требования
| Платформа | Что нужно до bootstrap | | Платформа | Что нужно до bootstrap |
|-----------|------------------------| | --------- | ---------------------------------------------- |
| Linux | `docker`, `git`, `curl` | | Linux | `docker`, `git`, `curl` |
| macOS | Docker Desktop, `git` (Xcode CLT) | | macOS | Docker Desktop, `git` (Xcode CLT) |
| Windows | Docker Desktop, Git for Windows → **Git Bash** | | Windows | Docker Desktop, Git for Windows → **Git Bash** |
### 5.2 Единственная команда ### 5.2 Единственная команда
@ -279,11 +279,11 @@ just host::m5-deploy
### 6.1 Четыре типа сборки ### 6.1 Четыре типа сборки
| Пресет | Toolchain | Назначение | Линкер-скрипт | | Пресет | Toolchain | Назначение | Линкер-скрипт |
|--------|-----------|------------|---------------| | ----------------------------- | ------------ | ---------------------------------- | ---------------- |
| `Debug` / `Release` | ARM GCC | firmware_test, bootloader, tft_app | `flexspi_nor.ld` | | `Debug` / `Release` | ARM GCC | firmware_test, bootloader, tft_app | `flexspi_nor.ld` |
| `host-debug` / `host-release` | clang (хост) | Unity + fff тесты | — | | `host-debug` / `host-release` | clang (хост) | Unity + fff тесты | — |
| `target-debug` | ARM GCC | HIL target-прошивки | `ram.ld` | | `target-debug` | ARM GCC | HIL target-прошивки | `ram.ld` |
### 6.2 CMake пресеты ### 6.2 CMake пресеты
@ -310,12 +310,12 @@ buildPresets (HIL):
### 6.3 Boot-стратегии ### 6.3 Boot-стратегии
| Прошивка | Стратегия | Инструмент загрузки | | Прошивка | Стратегия | Инструмент загрузки |
|----------|-----------|---------------------| | ---------------------------- | ---------------------------------- | ------------------- |
| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash | | `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash |
| `bootloader` | Копирование в ITCM | SPSDK → Flash | | `bootloader` | Копирование в ITCM | SPSDK → Flash |
| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash | | `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash |
| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM | | 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 не используется — прошивка исчезает при отключении питания. **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 Карта задач по контекстам ### 7.1 Карта задач по контекстам
| Задача | Где | | Задача | Где |
|--------|-----| | -------------------------------------- | -------------------------- |
| Написание кода, clangd, форматирование | devcontainer | | Написание кода, clangd, форматирование | devcontainer |
| Статический анализ (clang-tidy) | devcontainer | | Статический анализ (clang-tidy) | devcontainer |
| Host unit-тесты (Unity + fff) | devcontainer | | Host unit-тесты (Unity + fff) | devcontainer |
| Сборка ARM firmware (ELF) | devcontainer | | Сборка ARM firmware (ELF) | devcontainer |
| Сборка HIL target-прошивок | devcontainer | | Сборка HIL target-прошивок | devcontainer |
| Подготовка HAB-образов (nxpimage) | devcontainer | | Подготовка HAB-образов (nxpimage) | devcontainer |
| Прошивка платы через USB ROM | хост | | Прошивка платы через USB ROM | хост |
| Прошивка платы через SWD | хост | | Прошивка платы через SWD | хост |
| HIL-тесты (pyOCD + pytest + M5) | хост | | HIL-тесты (pyOCD + pytest + M5) | хост |
| Отладка — GDB-сервер (pyOCD) | хост | | Отладка — GDB-сервер (pyOCD) | хост |
| Отладка — GDB-клиент (cortex-debug) | devcontainer → хост по TCP | | Отладка — GDB-клиент (cortex-debug) | devcontainer → хост по TCP |
### 7.2 Типичная сессия разработки ### 7.2 Типичная сессия разработки
@ -369,15 +369,15 @@ just host::debug-server # запустить pyOCD GDB-серв
### 7.3 VSCode Tasks (внутри devcontainer) ### 7.3 VSCode Tasks (внутри devcontainer)
| Таск | Команда | | Таск | Команда |
|------|---------| | ------------------------- | ------------------------------------ |
| 🔨 Build | `just build::build-<project>-<type>` | | 🔨 Build | `just build::build-<project>-<type>` |
| 🧪 Host Tests (Debug) | `just build::test-host` | | 🧪 Host Tests (Debug) | `just build::test-host` |
| 🧪 Host Tests (Release) | `just build::test-host-release` | | 🧪 Host Tests (Release) | `just build::test-host-release` |
| 🎯 Build HIL Target Tests | `just build::build-hil` | | 🎯 Build HIL Target Tests | `just build::build-hil` |
| 📦 HAB Image | `just build::hab-<project>-<type>` | | 📦 HAB Image | `just build::hab-<project>-<type>` |
| 📦 HAB All (Debug/Release) | `just build::hab-all-debug/release` | | 📦 HAB All (Debug/Release) | `just build::hab-all-debug/release` |
| 🗑️ Clean | `just build::clean` | | 🗑️ Clean | `just build::clean` |
--- ---

View file

@ -4,7 +4,7 @@
Отладка построена на проброске GDB-сервера с хоста в devcontainer по TCP. Это позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, не проводя USB-пробник внутрь Docker. Отладка построена на проброске GDB-сервера с хоста в devcontainer по TCP. Это позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, не проводя USB-пробник внутрь Docker.
``` ```bash
┌─────────────────────────────────────┐ ┌──────────────────────────────────┐ ┌─────────────────────────────────────┐ ┌──────────────────────────────────┐
│ Хост (macOS/Linux) │ │ DevContainer │ │ Хост (macOS/Linux) │ │ DevContainer │
│ │ │ │ │ │ │ │
@ -26,25 +26,25 @@
### На хосте ### На хосте
| Компонент | Роль | Источник | | Компонент | Роль | Источник |
|---|---|---| | --------------------------------- | ------------------------------- | -------------------------- |
| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` | | `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` |
| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате | | `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате |
| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` | | `just host::debug-server` | Запуск GDB-сервера | `just/host.just` |
| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` | | `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` |
| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` | | `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` |
| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool | | `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool |
### В devcontainer ### В devcontainer
| Компонент | Роль | | Компонент | Роль |
|---|---| | -------------------------------------- | ---------------------------------------------- |
| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте | | `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте |
| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры | | `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры |
| `.vscode/launch.json` | Конфигурации запуска отладки | | `.vscode/launch.json` | Конфигурации запуска отладки |
| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом | | `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом |
| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) | | `build/Debug/*.elf` | Символы для GDB (DWARF debug info) |
| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии | | `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии |
### Конфигурация ### Конфигурация
@ -62,11 +62,11 @@ FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
## Прошивки, поддерживаемые отладкой ## Прошивки, поддерживаемые отладкой
| Конфигурация VSCode | ELF | Особенности | | Конфигурация VSCode | ELF | Особенности |
|---|---|---| | ----------------------------- | ------------------------------- | ---------------------------- |
| `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль | | `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль |
| `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление | | `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление |
| `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view | | `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view |
Все три — XIP-прошивки, исполняются напрямую из QuadSPI NOR Flash (`0x60000000`). Все три — XIP-прошивки, исполняются напрямую из QuadSPI NOR Flash (`0x60000000`).
@ -225,4 +225,4 @@ just host::debug-server
├── flash_swd.py # FCB + HAB → Flash через pyOCD ├── flash_swd.py # FCB + HAB → Flash через pyOCD
└── dcd/ └── dcd/
└── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI └── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI
``` ```

View file

@ -2,10 +2,10 @@
Поддерживаются два независимых способа прошивки: Поддерживаются два независимых способа прошивки:
| Способ | Интерфейс | Требование | Инструмент | | Способ | Интерфейс | Требование | Инструмент |
|---|---|---|---| | ----------- | -------------------- | ---------------------------------- | -------------------------- |
| **USB SDP** | USB ↔ ROM-загрузчик | BOOT_MODE = 01 (Serial Downloader) | `spsdk` (sdphost + blhost) | | **USB SDP** | USB ↔ ROM-загрузчик | BOOT_MODE = 01 (Serial Downloader) | `spsdk` (sdphost + blhost) |
| **SWD** | MCU-Link ↔ CMSIS-DAP | Плата в любом режиме загрузки | `pyocd` | | **SWD** | MCU-Link ↔ CMSIS-DAP | Плата в любом режиме загрузки | `pyocd` |
--- ---
@ -127,11 +127,11 @@ just host::flash-swd-app-release
### 2.4 Зависимости ### 2.4 Зависимости
| Файл | Назначение | | Файл | Назначение |
|---|---| | --------------------------------- | ----------------------------------- |
| `tools/host/flash_swd.py` | Скрипт сборки образа и вызова pyOCD | | `tools/host/flash_swd.py` | Скрипт сборки образа и вызова pyOCD |
| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 в режиме Quad SPI | | `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 в режиме Quad SPI |
| `tools/hil/` (uv-проект) | pyocd, вызывается через `uv run` | | `tools/hil/` (uv-проект) | pyocd, вызывается через `uv run` |
FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP SecureProvisioningTool FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP SecureProvisioningTool
для W25Q128 в режиме Quad SPI и хранится в репозитории — пересоздавать не нужно. для W25Q128 в режиме Quad SPI и хранится в репозитории — пересоздавать не нужно.
@ -140,15 +140,15 @@ FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP Secure
## Сравнение способов ## Сравнение способов
| | USB SDP | SWD | | | USB SDP | SWD |
|---|---|---| | ------------------------- | -------------------------------- | -------------------- |
| Переключение BOOT_MODE | Нужно | Не нужно | | Переключение BOOT_MODE | Нужно | Не нужно |
| Power cycle после записи | Не нужен | **Обязателен** | | Power cycle после записи | Не нужен | **Обязателен** |
| FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** | | FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** |
| Скорость записи | ~50100 kB/s | ~810 kB/s | | Скорость записи | ~50100 kB/s | ~810 kB/s |
| Совместимость с отладкой | Раздельно | MCU-Link монопольный | | Совместимость с отладкой | Раздельно | MCU-Link монопольный |
| Производственный сценарий | ✓ | — | | Производственный сценарий | ✓ | — |
| Итеративная разработка | Неудобно (смена режима) | ✓ | | Итеративная разработка | Неудобно (смена режима) | ✓ |
--- ---

View file

@ -6,10 +6,10 @@
## Оборудование ## Оборудование
| Устройство | Роль | | Устройство | Роль |
|-------------------|-------------------------------------------------------------| | -------------------- | ---------------------------------------------------------- |
| MIMXRT1052CVJ5B | Таргет — плата под тестом | | MIMXRT1052CVJ5B | Таргет — плата под тестом |
| M5Stack StamPLC | Промежуточная платформа стенда: питание таргета + сигналы | | M5Stack StamPLC | Промежуточная платформа стенда: питание таргета + сигналы |
| MCU-Link (CMSIS-DAP) | SWD-probe + VCOM (pyOCD загружает ELF, pytest читает UART) | | MCU-Link (CMSIS-DAP) | SWD-probe + VCOM (pyOCD загружает ELF, pytest читает UART) |
--- ---
@ -18,9 +18,9 @@
### Питание таргета ### Питание таргета
| M5 реле | Куда | Назначение | | M5 реле | Куда | Назначение |
|---------|------------------|---------------------| | ------- | ----------- | ------------------- |
| RLY1 | VIN таргета | Управление питанием | | RLY1 | VIN таргета | Управление питанием |
Питание включается и выключается автоматически фикстурой `m5` в `conftest.py`: Питание включается и выключается автоматически фикстурой `m5` в `conftest.py`:
@ -32,11 +32,11 @@
Таргет: оптопары PS2801-4, **неинвертирующие** (active-HIGH). Таргет: оптопары PS2801-4, **неинвертирующие** (active-HIGH).
M5: реле AW9523B через ULN2003A, нормально разомкнутые (NO). M5: реле AW9523B через ULN2003A, нормально разомкнутые (NO).
| M5 реле | Сигнал таргета | BSP канал | MCU пин | GPIO | | M5 реле | Сигнал таргета | BSP канал | MCU пин | GPIO |
|---------|----------------|--------------------|---------------|-----------| | ------- | -------------- | ----------------- | ------------- | --------- |
| RLY2 | RS_RX | `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] | | 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] | | 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] | | 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_PORT` | `/dev/ttyACM0` | MCU-Link VCOM (UART CLI) |
| `HIL_VCOM_BAUD` | `115200` | Скорость UART CLI | | `HIL_VCOM_BAUD` | `115200` | Скорость UART CLI |
| `HIL_M5_PORT` | `/dev/ttyACM1` | M5StampPLC USB CDC | | `HIL_M5_PORT` | `/dev/ttyACM1` | M5StampPLC USB CDC |
| `HIL_M5_BAUD` | `115200` | Скорость M5 агента | | `HIL_M5_BAUD` | `115200` | Скорость M5 агента |
| `HIL_BUILD_DIR` | `build/target-debug` | Путь к собранным ELF | | `HIL_BUILD_DIR` | `build/target-debug` | Путь к собранным ELF |
На macOS порты выглядят как `/dev/cu.usbmodem*`. Задаются в `.env` в корне репозитория. На macOS порты выглядят как `/dev/cu.usbmodem*`. Задаются в `.env` в корне репозитория.
@ -73,11 +73,11 @@ _OPTO_TO_RELAY = {1: 3, 2: 4, 3: 2}
### M5Stack StamPLC — `tools/hil/m5/` ### M5Stack StamPLC — `tools/hil/m5/`
| Файл | Назначение | | Файл | Назначение |
|--------------|----------------------------------------------------------------| | ---------- | --------------------------------------------------------------------------------------- |
| `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, подает сигналы на таргет | | `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, подает сигналы на таргет |
| `cli.py` | Интерактивный CLI для ручного тестирования агента | | `cli.py` | Интерактивный CLI для ручного тестирования агента |
| `power.py` | Скрипт управления питанием таргета (RLY1) из командной строки | | `power.py` | Скрипт управления питанием таргета (RLY1) из командной строки |
**Протокол агента:** JSON-lines через USB CDC (115200 бод). **Протокол агента:** JSON-lines через USB CDC (115200 бод).
@ -88,19 +88,19 @@ M5 → хост: {"ok": true, "opto_ch": 1, "relay": 3, "state": true}\r\n
**Доступные команды агента:** **Доступные команды агента:**
| Команда | Параметры | Действие | | Команда | Параметры | Действие |
|----------------|------------------------------------|---------------------------------------------------| | --------------- | ------------------------------------------- | ---------------------------------------------- |
| `ping` | — | Проверка связи | | `ping` | — | Проверка связи |
| `info` | — | Версия, состояние CAN и AW9523, статус реле | | `info` | — | Версия, состояние CAN и AW9523, статус реле |
| `power` | `state: bool` | RLY1 — питание таргета | | `power` | `state: bool` | RLY1 — питание таргета |
| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле | | `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле |
| `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле | | `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле |
| `relay_all_off`| — | Выключить все реле | | `relay_all_off` | — | Выключить все реле |
| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) | | `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) |
| `opto_all_off` | — | Выключить все оптоканалы | | `opto_all_off` | — | Выключить все оптоканалы |
| `input_read` | `ch: 1-8` | Прочитать вход стенда SYS_IN (оптопара на M5) | | `input_read` | `ch: 1-8` | Прочитать вход стенда SYS_IN (оптопара на M5) |
| `can_send` | `id: int, data: list[int], ext: bool=false` | Отправить CAN-фрейм с шины M5 | | `can_send` | `id: int, data: list[int], ext: bool=false` | Отправить CAN-фрейм с шины M5 |
| `can_recv` | `timeout_ms: int=500` | Принять CAN-фрейм на M5 (ошибка при таймауте) | | `can_recv` | `timeout_ms: int=500` | Принять CAN-фрейм на M5 (ошибка при таймауте) |
**Деплой агента на M5:** **Деплой агента на M5:**

View file

@ -4,7 +4,7 @@
```bash ```bash
devcontainer хост devcontainer хост
───────────────────────────────── ──────────────────────────────────── ───────────────────────────────── ────────────────────────────────────
tests/target/<name>/ tools/hil/ tests/target/<name>/ tools/hil/
main.c ← C-прошивка с CLI test_<name>.py ← pytest-тесты main.c ← C-прошивка с CLI test_<name>.py ← pytest-тесты
CMakeLists.txt conftest.py ← фикстуры (общие) CMakeLists.txt conftest.py ← фикстуры (общие)
@ -19,11 +19,11 @@ just/build.just
Три типа тестов: Три типа тестов:
| Тип | Использует M5 | Запуск | Когда применять | | Тип | Использует M5 | Запуск | Когда применять |
|-----|--------------|--------|-----------------| | ----------------- | ------------- | --------------------- | ----------------------------------------------------- |
| **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов | | **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов |
| **С M5** | Да | `hil-run` | Тестирование GPIO, оптовходов, реле, питания | | **С M5** | Да | `hil-run` | Тестирование GPIO, оптовходов, реле, питания |
| **Интерактивный** | Нет / Да | `hil-run-interactive` | Периферия требует действий оператора: кнопки, дисплей | | **Интерактивный** | Нет / Да | `hil-run-interactive` | Периферия требует действий оператора: кнопки, дисплей |
Интерактивные тесты помечаются `@pytest.mark.interactive` и **никогда не входят в `hil-run`** — они требуют живого оператора и не пригодны для CI. Интерактивные тесты помечаются `@pytest.mark.interactive` и **никогда не входят в `hil-run`** — они требуют живого оператора и не пригодны для CI.

View file

@ -23,10 +23,10 @@ def uart(request):
## 2. `yield` vs `return` ## 2. `yield` vs `return`
| Конструкция | Setup | Teardown | Когда использовать | | Конструкция | Setup | Teardown | Когда использовать |
|-------------|-------|----------|--------------------| | ----------- | ----- | -------- | --------------------------------------------- |
| `return` | ✅ | ❌ | Ресурс не требует очистки (загрузка ELF) | | `return` | ✅ | ❌ | Ресурс не требует очистки (загрузка ELF) |
| `yield` | ✅ | ✅ | Ресурс нужно освободить (порт, реле, питание) | | `yield` | ✅ | ✅ | Ресурс нужно освободить (порт, реле, питание) |
```python ```python
# return — teardown не нужен # return — teardown не нужен
@ -56,12 +56,12 @@ def m5(request):
Scope определяет **как долго живёт** экземпляр фикстуры. Scope определяет **как долго живёт** экземпляр фикстуры.
| Scope | Создаётся | Уничтожается | Типичное применение в HIL | | Scope | Создаётся | Уничтожается | Типичное применение в HIL |
|-------|-----------|--------------|---------------------------| | ---------- | ------------------------------ | ----------------------------- | ---------------------------------- |
| `function` | Перед каждым `test_*()` | После каждого `test_*()` | Сброс состояния стенда | | `function` | Перед каждым `test_*()` | После каждого `test_*()` | Сброс состояния стенда |
| `class` | Перед первым тестом класса | После последнего теста класса | Группа связанных тестов | | `class` | Перед первым тестом класса | После последнего теста класса | Группа связанных тестов |
| `module` | Перед первым тестом файла | После последнего теста файла | **Загрузка ELF, открытие UART** | | `module` | Перед первым тестом файла | После последнего теста файла | **Загрузка ELF, открытие UART** |
| `session` | Один раз на весь pytest-запуск | В самом конце | Подключение к M5, глобальный setup | | `session` | Один раз на весь pytest-запуск | В самом конце | Подключение к M5, глобальный setup |
### Почему для HIL основной scope — `module` ### Почему для HIL основной scope — `module`
@ -202,18 +202,18 @@ tools/hil/
### Что живёт в `conftest.py` проекта ### Что живёт в `conftest.py` проекта
| Фикстура / функция | Scope | Назначение | | Фикстура / функция | Scope | Назначение |
|---------------------|-------|------------| | -------------------------- | -------------------- | ------------------------------------------------------------ |
| `_load_elf()` | вспомогательная | pyOCD: halt → FLEXRAM → load ELF → run | | `_load_elf()` | вспомогательная | pyOCD: halt → FLEXRAM → load ELF → run |
| `_uart_context()` | контекстный менеджер | Открыть VCOM, дождаться `READY\r\n`, гарантировать `close()` | | `_uart_context()` | контекстный менеджер | Открыть VCOM, дождаться `READY\r\n`, гарантировать `close()` |
| `_make_uart_fixture()` | фабрика | Генерирует `uart_*` фикстуры из `_UART_FIXTURE_MAP` | | `_make_uart_fixture()` | фабрика | Генерирует `uart_*` фикстуры из `_UART_FIXTURE_MAP` |
| `_UART_FIXTURE_MAP` | словарь | Связь `uart_<n>``loaded_<n>` для всех тестов | | `_UART_FIXTURE_MAP` | словарь | Связь `uart_<n>``loaded_<n>` для всех тестов |
| `loaded_host_uart` | module | Загрузить `test_host_uart.elf` | | `loaded_host_uart` | module | Загрузить `test_host_uart.elf` |
| `loaded_hil_opto` | module | Загрузить `test_hil_opto.elf`, зависит от `m5` | | `loaded_hil_opto` | module | Загрузить `test_hil_opto.elf`, зависит от `m5` |
| `uart` / `uart_opto` / ... | module | Создаются автоматически через `_UART_FIXTURE_MAP` | | `uart` / `uart_opto` / ... | module | Создаются автоматически через `_UART_FIXTURE_MAP` |
| `usb_cdc_port` | module | USB CDC порт таргета, использует `cfg.TARGET_VCOM_*` | | `usb_cdc_port` | module | USB CDC порт таргета, использует `cfg.TARGET_VCOM_*` |
| `m5` | module | Подключиться к M5, включить питание | | `m5` | module | Подключиться к M5, включить питание |
| `uart_cmd()` | обычная функция | Отправить команду, прочитать ответ | | `uart_cmd()` | обычная функция | Отправить команду, прочитать ответ |
--- ---

View file

@ -3,7 +3,7 @@
## Обзор стека ## Обзор стека
```bash ```bash
devcontainer devcontainer
───────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────
tests/host/<n>/test_<n>.c ← тест (Unity + опционально fff) tests/host/<n>/test_<n>.c ← тест (Unity + опционально fff)
tests/host/CMakeLists.txt ← регистрация через add_host_test() tests/host/CMakeLists.txt ← регистрация через add_host_test()
@ -20,10 +20,10 @@ CMakePresets.json just/build.just
Перед написанием кода определи к какой категории относится модуль: Перед написанием кода определи к какой категории относится модуль:
| Категория | Описание | Инструментарий | | Категория | Описание | Инструментарий |
|-----------|----------|----------------| | --------- | -------------------------------------------------------------- | ------------------------- |
| **A** | Нет вызовов NXP SDK: алгоритмы, парсеры, FSM, структуры данных | Unity | | **A** | Нет вызовов NXP SDK: алгоритмы, парсеры, FSM, структуры данных | Unity |
| **B** | BSP-модуль вызывает `fsl_*.h`, USB-стек и т.д. | Unity + fff + stub-хедеры | | **B** | BSP-модуль вызывает `fsl_*.h`, USB-стек и т.д. | Unity + fff + stub-хедеры |
**Признак категории A:** в `.c` файле модуля нет ни одного `#include "fsl_*.h"`. **Признак категории A:** в `.c` файле модуля нет ни одного `#include "fsl_*.h"`.
**Признак категории B:** есть хотя бы один такой include. **Признак категории 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/` ### Уже существующие stubs в `tests/host/mocks/`
| Файл | Что заменяет | Используется в | | Файл | Что заменяет | Используется в |
|------|-------------|----------------| | ------------ | ----------------------------- | -------------- |
| `fsl_gpio.h` | GPIO драйвер | `test_bsp_led` | | `fsl_gpio.h` | GPIO драйвер | `test_bsp_led` |
| `pin_mux.h` | Макросы пинов из `generated/` | `test_bsp_led` | | `pin_mux.h` | Макросы пинов из `generated/` | `test_bsp_led` |
| `board.h` | `board_hw_init()` | `test_bsp_led` | | `board.h` | `board_hw_init()` | `test_bsp_led` |
Если нужный stub уже есть — ничего создавать не нужно, просто укажи `mocks/` в `MOCKS` аргументе `add_host_test()`. Если нужный stub уже есть — ничего создавать не нужно, просто укажи `mocks/` в `MOCKS` аргументе `add_host_test()`.
@ -184,12 +184,12 @@ add_host_test(
### Аргументы `add_host_test()` ### Аргументы `add_host_test()`
| Аргумент | Обязателен | Описание | | Аргумент | Обязателен | Описание |
|----------|-----------|----------| | ---------- | ---------- | --------------------------------------------------------------- |
| `NAME` | ✓ | Имя исполняемого файла и теста в CTest | | `NAME` | ✓ | Имя исполняемого файла и теста в CTest |
| `SOURCES` | ✓ | Тестовый `.c` + исходники тестируемых модулей | | `SOURCES` | ✓ | Тестовый `.c` + исходники тестируемых модулей |
| `INCLUDES` | — | Дополнительные include-пути (для `#include "bsp/led.h"` и т.д.) | | `INCLUDES` | — | Дополнительные include-пути (для `#include "bsp/led.h"` и т.д.) |
| `MOCKS` | — | Директории со stub-хедерами (подключаются с высшим приоритетом) | | `MOCKS` | — | Директории со stub-хедерами (подключаются с высшим приоритетом) |
`lib_external` (Unity + fff) подключается автоматически — добавлять не нужно. `lib_external` (Unity + fff) подключается автоматически — добавлять не нужно.

View file

@ -14,6 +14,7 @@ add_executable(
src/tests/test_sdram.c src/tests/test_sdram.c
src/tests/test_qspi.c src/tests/test_qspi.c
src/tests/test_usd.c src/tests/test_usd.c
src/tests/test_display.c
${BSP_GENERATED}/clock_config.c ${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE} ${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE}) ${BSP_SYSCALLS_FILE})
@ -26,8 +27,8 @@ target_include_directories(firmware_test PRIVATE src/)
target_compile_definitions( target_compile_definitions(
${TARGET_NAME} ${TARGET_NAME}
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1 PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1
__STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8 __STARTUP_INITIALIZE_RAMFUNCTION
__STARTUP_INITIALIZE_NONCACHEDATA) __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_NONCACHEDATA)
# #
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно # Зависимости — только то что нужно для входного контроля bsp_board транзитивно
@ -37,6 +38,7 @@ target_link_libraries(
${TARGET_NAME} ${TARGET_NAME}
PRIVATE bsp_board PRIVATE bsp_board
bsp_led bsp_led
bsp_display
bsp_tick bsp_tick
bsp_boot_xip bsp_boot_xip
bsp_usb_cdc bsp_usb_cdc

View file

@ -248,7 +248,7 @@ bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из
--- ---
## Матрица тестов — итоговая ## Матрица тестов — итоговая
| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус | | 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) ✅ Этап 1 (протокол v2 + runner)

View file

@ -190,17 +190,17 @@ main.c
**BSP-зависимости тест-модулей:** **BSP-зависимости тест-модулей:**
| Тест | BSP модуль | | Тест | BSP модуль |
|---|---| | --------------- | ------------------------------ |
| `test_sdram` | `bsp_sdram` | | `test_sdram` | `bsp_sdram` |
| `test_qspi` | `bsp_qspi` | | `test_qspi` | `bsp_qspi` |
| `test_usd` | `bsp_usd` | | `test_usd` | `bsp_usd` |
| `test_display` | существующий display BSP | | `test_display` | существующий display BSP |
| `test_buttons` | `bsp_button` ✅ | | `test_buttons` | `bsp_button` |
| `test_can` | `bsp_can` ✅ | | `test_can` | `bsp_can` |
| `test_uart_ttl` | `bsp_uart_host` ✅ | | `test_uart_ttl` | `bsp_uart_host` |
| `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ | | `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 | | Интерфейс | USB CDC ACM, разъём J2 |
| Кодировка | UTF-8 | | Кодировка | UTF-8 |
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` | | Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
| Максимальная длина строки | 128 байт включая `\n` | | Максимальная длина строки | 128 байт включая `\n` |
| CR+LF | Принимается (таргет отбрасывает `\r`) | | CR+LF | Принимается (таргет отбрасывает `\r`) |
Нет хэндшейка, нет sequence number, нет подтверждений доставки. Нет хэндшейка, нет sequence number, нет подтверждений доставки.
Таргет идемпотентен для `ping` и `run` — при потере строки хост повторяет. Таргет идемпотентен для `ping` и `run` — при потере строки хост повторяет.
@ -378,10 +378,10 @@ main.c
} }
``` ```
| `status` | Смысл | | `status` | Смысл |
|---|---| | -------- | ------------------------------------------------------------------------ |
| `"pass"` | Тест пройден | | `"pass"` | Тест пройден |
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) | | `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
| `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше | | `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше |
Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`. Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`.
@ -482,17 +482,17 @@ main.c
## Матрица тестов ## Матрица тестов
| ID | Название | Тип | Critical | M5 HIL | Confirm | | ID | Название | Тип | Critical | M5 HIL | Confirm |
|---|---|---|---|---|---| | ---------- | ---------------- | ---------------- | -------- | ------ | ------------- |
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | | `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm | | `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() | | `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() |
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only | | `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only |
| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ | | `can` | CAN loopback | HIL | ❌ | ✅ | ❌ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | | `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ | | `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ |
| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | | `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
**Типы confirm:** **Типы confirm:**
@ -668,10 +668,10 @@ const test_module_t k_test_foo = {
### Что покрыто ### Что покрыто
| Таргет | Что тестирует | Тест-файл | | Таргет | Что тестирует | Тест-файл |
|---|---|---| | ---------------------- | ------------------------------------------------ | ------------------------------------------ |
| `test_protocol` | сериализация JSON (все event types) | `tests/host/protocol/test_protocol.c` | | `test_protocol` | сериализация JSON (все event types) | `tests/host/protocol/test_protocol.c` |
| `test_cli` | парсинг входящих строк, диспатч по type | `tests/host/cli/test_cli.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` | | `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 | | Зависимость | fff fake |
|---|---| | ----------------------------- | --------------------------------------------------------------- |
| `cli_send()` | `FAKE_VOID_FUNC(cli_send, const char *)` + custom_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_tick_get_ms()` | `FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms)` |
| `bsp_usb_cdc_poll()` | `FAKE_VOID_FUNC(bsp_usb_cdc_poll)` | | `bsp_usb_cdc_poll()` | `FAKE_VOID_FUNC(bsp_usb_cdc_poll)` |
| `cli_process()` | `FAKE_VOID_FUNC(cli_process)` | | `cli_process()` | `FAKE_VOID_FUNC(cli_process)` |
| `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению | | `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению |
> **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель > **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель
> на стековую переменную внутри `execute_test()`. После возврата указатель > на стековую переменную внутри `execute_test()`. После возврата указатель
@ -750,11 +750,11 @@ void test_run_all_critical_fail_skips_remaining(void)
> Не пересматривать без явного запроса. > Не пересматривать без явного запроса.
| Решение | Обоснование | | Решение | Обоснование |
|---|---| | -------------------------------------------- | ------------------------------------------------------------------ |
| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) | | Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) |
| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен | | Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен |
| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC | | SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC |
| IR и RTC — не реализуются | Вне scope рекламационной диагностики | | IR и RTC — не реализуются | Вне scope рекламационной диагностики |
| Тесты атомарны | Инженер сам решает что проверять | | Тесты атомарны | Инженер сам решает что проверять |
| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики | | Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики |

View file

@ -25,7 +25,6 @@
#include "protocol.h" #include "protocol.h"
#include "test_runner.h" #include "test_runner.h"
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
int main(void) int main(void)

View file

@ -24,7 +24,7 @@
#include <stdint.h> #include <stdint.h>
/** @brief Строка версии прошивки, вставляемая в session_start. */ /** @brief Строка версии прошивки, вставляемая в session_start. */
#define FIRMWARE_TEST_VERSION "0.1.0" #define FIRMWARE_TEST_VERSION "0.1.4"
/** @brief Таймаут подтверждения по умолчанию, мс. */ /** @brief Таймаут подтверждения по умолчанию, мс. */
#define PROTOCOL_CONFIRM_TIMEOUT_MS 30000U #define PROTOCOL_CONFIRM_TIMEOUT_MS 30000U

View file

@ -23,33 +23,19 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
/* ── Реестр тестов ───────────────────────────────────────────────────────── /* ── Реестр тестов ─────────────────────────────────────────────────────────*/
*
* Добавление нового теста (Этап 2+):
* 1. Раскомментировать extern-объявление нужного модуля.
* 2. Добавить &k_test_<name> в 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 #ifndef UNIT_TEST
extern const test_module_t K_TEST_SDRAM; extern const test_module_t K_TEST_SDRAM;
extern const test_module_t K_TEST_QSPI; extern const test_module_t K_TEST_QSPI;
extern const test_module_t K_TEST_USD; extern const test_module_t K_TEST_USD;
extern const test_module_t K_TEST_DISPLAY;
static const test_module_t *const k_registry[] = { static const test_module_t *const k_registry[] = {
/* populated starting from Этап 2 */ /* populated starting from Этап 2 */
&K_TEST_SDRAM, &K_TEST_SDRAM,
&K_TEST_QSPI, &K_TEST_QSPI,
&K_TEST_USD, &K_TEST_USD,
&K_TEST_DISPLAY,
}; };
#define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0])) #define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0]))

View file

@ -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 <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ── Тип дисплея ─────────────────────────────────────────────────────── */
/*
* 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(&params))
{
(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(&params))
{
(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,
};

View file

@ -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] Среда выполнения строится из Dockerfile devcontainer-а проекта, в котором уже определены ARM GCC toolchain в `/opt/arm-toolchain`, обновлённый `PATH`, а также установлены `cmake`, `ninja-build`, `clang-17`, `uv` и `just`.[cite:125] Поскольку все ключевые зависимости уже зафиксированы именно там, использование этого же образа в CI делает поведение раннера максимально близким к локальной разработке.[cite:125]
## Как работает workflow ## Как работает workflow
Workflow реагирует на три типа событий: `push`, `pull_request` и `workflow_dispatch`.[cite:23][cite:99] Это даёт удобный баланс между автоматической проверкой обычных коммитов и возможностью вручную перезапускать pipeline для отладки инфраструктурных или нестабильных падений без обязательного нового изменения в коде.[cite:99][cite:106] 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 используется 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]
## Что делает job `build` ## Что делает 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`
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] Перенос CI внутрь devcontainer image устраняет этот класс расхождений и делает Dockerfile единым источником истины для окружения, версий и путей.[cite:125] Это упрощает дальнейшее сопровождение: при изменении инструментария достаточно обновить Dockerfile, и эти же изменения автоматически начнут действовать как локально, так и в CI.[cite:125]
## Чего workflow пока не делает ## Чего 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] - Она ускоряется на повторных прогонах за счёт Docker layer caching через GitHub Actions cache backend.
- Она модульна, потому что `build` и `test` вынесены в отдельные jobы, связанные артефактами.[cite:156][cite:153] - Она модульна, потому что `build` и `test` вынесены в отдельные jobы, связанные артефактами.
- Она удобна для отладки, потому что build outputs сохраняются как artifacts, а workflow можно запускать вручную через `workflow_dispatch`.[cite:99][cite:156] - Она удобна для отладки, потому что build outputs сохраняются как artifacts, а workflow можно запускать вручную через `workflow_dispatch`.
- Она хорошо вписана в структуру проекта, потому что использует уже существующие `just`-точки входа, а не дублирует build-логику в YAML.[cite:1] - Она хорошо вписана в структуру проекта, потому что использует уже существующие `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 ### Шаг 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 ### Шаг 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 ### Шаг 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 ### Шаг 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 ### Шаг 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 и аппаратной валидации на стенде.

View file

@ -220,7 +220,7 @@ void test_confirm_request_default_timeout_on_zero(void)
protocol_send_confirm_request(&params); protocol_send_confirm_request(&params);
/* timeout_ms == 0 → подставляется PROTOCOL_CONFIRM_TIMEOUT_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 ───────────────────────────────────────────────────────── */ /* ── Тесты: pong ───────────────────────────────────────────────────────── */

View file

@ -38,11 +38,11 @@ tools/hil/
Полное описание стека HIL-тестирования — в `docs/testing/hil/`: Полное описание стека HIL-тестирования — в `docs/testing/hil/`:
| Документ | Содержимое | | Документ | Содержимое |
|----------|-----------| | --------------------------------------------------------------- | ---------------------------------------------- |
| [HIL_HOWTO.md](../../docs/testing/hil/HIL_HOWTO.md) | Как запускать HIL-тесты (пошагово) | | [HIL_HOWTO.md](../../docs/testing/hil/HIL_HOWTO.md) | Как запускать HIL-тесты (пошагово) |
| [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле | | [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле |
| [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест | | [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест |
--- ---

View file

@ -9,7 +9,7 @@ Python-окружение на базе [uv](https://docs.astral.sh/uv/) для
## Структура ## Структура
```иbash ```bash
tools/host/ tools/host/
├── flash_usb.py — прошивка через USB ROM: sdphost → Flashloader → Flash ├── flash_usb.py — прошивка через USB ROM: sdphost → Flashloader → Flash
├── flash_swd.py — прошивка через SWD: FCB + HAB → pyOCD → Flash ├── flash_swd.py — прошивка через SWD: FCB + HAB → pyOCD → Flash

View file

@ -13,7 +13,7 @@
## Модули ## Модули
| Модуль | Путь | Описание | | Модуль | Путь | Описание |
|--------|------|----------| | ------------- | ------------------------------------- | -------------------------------------------------- |
| `ring_buffer` | [ring_buffer/](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free | | `ring_buffer` | [ring_buffer/](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free |
| `log` | [log/](log/README.md) | Платформонезависимый логгер с callback-транспортом | | `log` | [log/](log/README.md) | Платформонезависимый логгер с callback-транспортом |