# 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

@ -9,17 +9,17 @@
## Три 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) |
@ -32,7 +32,7 @@
## Тестирование ## Тестирование
| Уровень | Где | Инструменты | Запуск | | Уровень | Где | Инструменты | Запуск |
|---------|-----|-------------|--------| | ---------------- | ------------------------------ | -------------------------------------- | -------------------------------------- |
| 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` (хост) |
@ -76,7 +76,7 @@ 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` |

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

@ -9,7 +9,7 @@
## Аппаратура ## Аппаратура
| Кнопка | Пин 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 |
@ -190,19 +190,19 @@ 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

@ -35,7 +35,7 @@
## Распределение 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 индивидуальных фильтров |
@ -105,7 +105,7 @@ bsp_can_accept_all();
Возвращается по одному из условий: Возвращается по одному из условий:
| Условие | Возврат | | Условие | Возврат |
|---------|---------| | -------------------------------- | ----------------- |
| Фрейм успешно отправлен | `BSP_OK` | | Фрейм успешно отправлен | `BSP_OK` |
| TX MB занят предыдущей передачей | `BSP_ERR_BUSY` | | TX MB занят предыдущей передачей | `BSP_ERR_BUSY` |
| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | | Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` |
@ -120,7 +120,7 @@ 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` |
@ -162,7 +162,7 @@ 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` |
@ -240,7 +240,7 @@ 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 |

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

@ -44,7 +44,7 @@ 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) |

View file

@ -7,7 +7,7 @@
## Аппаратная часть ## Аппаратная часть
| `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 = горит) |

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

@ -39,10 +39,10 @@ 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 |
@ -93,12 +93,12 @@ 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

@ -60,7 +60,7 @@ 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. |
@ -157,7 +157,7 @@ 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 |

View file

@ -12,10 +12,10 @@ 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).
@ -61,7 +61,7 @@ Host → USB1_DP/DN → [EHCI0 DMA]
Это сознательное решение: Это сознательное решение:
| Аспект | 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 |
@ -170,7 +170,7 @@ NVIC enable → USB_DeviceRun. Включает задержку 5 мс для
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` | Хост не подключён |
@ -251,7 +251,7 @@ 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 |
@ -296,7 +296,7 @@ just host::hil-usb-cdc
Команды CLI прошивки: Команды CLI прошивки:
| Команда | Ответ | Описание | | Команда | Ответ | Описание |
|---------|-------|----------| | ------------- | -------- | ---------------- |
| `PING` | `PONG` | Проверка канала | | `PING` | `PONG` | Проверка канала |
| `ECHO <data>` | `<data>` | Echo-back данных | | `ECHO <data>` | `<data>` | Echo-back данных |
@ -312,7 +312,7 @@ 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 |
@ -349,7 +349,7 @@ 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 абстракция |

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

@ -66,7 +66,7 @@ HIL-тесты через pyOCD + pytest, GDB-сервер для отладки
## 3. Что устанавливается и где ## 3. Что устанавливается и где
| Инструмент | Хост | Devcontainer | | Инструмент | Хост | Devcontainer |
|------------|------|--------------| | ----------------------------------- | --------------- | --------------- |
| `just` | ✅ | ✅ Dockerfile | | `just` | ✅ | ✅ Dockerfile |
| `docker` | ✅ | — | | `docker` | ✅ | — |
| `uv` | ✅ | ✅ Dockerfile | | `uv` | ✅ | ✅ Dockerfile |
@ -227,7 +227,7 @@ 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** |
@ -280,7 +280,7 @@ 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` |
@ -311,7 +311,7 @@ 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 |
@ -326,7 +326,7 @@ 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 |
@ -370,7 +370,7 @@ 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` |

View file

@ -4,7 +4,7 @@
Отладка построена на проброске GDB-сервера с хоста в devcontainer по TCP. Это позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, не проводя USB-пробник внутрь Docker. Отладка построена на проброске GDB-сервера с хоста в devcontainer по TCP. Это позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, не проводя USB-пробник внутрь Docker.
``` ```bash
┌─────────────────────────────────────┐ ┌──────────────────────────────────┐ ┌─────────────────────────────────────┐ ┌──────────────────────────────────┐
│ Хост (macOS/Linux) │ │ DevContainer │ │ Хост (macOS/Linux) │ │ DevContainer │
│ │ │ │ │ │ │ │
@ -27,7 +27,7 @@
### На хосте ### На хосте
| Компонент | Роль | Источник | | Компонент | Роль | Источник |
|---|---|---| | --------------------------------- | ------------------------------- | -------------------------- |
| `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` |
@ -38,7 +38,7 @@
### В 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` | Конфигурации запуска отладки |
@ -63,7 +63,7 @@ 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 |

View file

@ -3,7 +3,7 @@
Поддерживаются два независимых способа прошивки: Поддерживаются два независимых способа прошивки:
| Способ | Интерфейс | Требование | Инструмент | | Способ | Интерфейс | Требование | Инструмент |
|---|---|---|---| | ----------- | -------------------- | ---------------------------------- | -------------------------- |
| **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` |
@ -128,7 +128,7 @@ 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` |
@ -141,7 +141,7 @@ FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP Secure
## Сравнение способов ## Сравнение способов
| | USB SDP | SWD | | | USB SDP | SWD |
|---|---|---| | ------------------------- | -------------------------------- | -------------------- |
| Переключение BOOT_MODE | Нужно | Не нужно | | Переключение BOOT_MODE | Нужно | Не нужно |
| Power cycle после записи | Не нужен | **Обязателен** | | Power cycle после записи | Не нужен | **Обязателен** |
| FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** | | FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** |

View file

@ -7,7 +7,7 @@
## Оборудование ## Оборудование
| Устройство | Роль | | Устройство | Роль |
|-------------------|-------------------------------------------------------------| | -------------------- | ---------------------------------------------------------- |
| 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) |
@ -19,7 +19,7 @@
### Питание таргета ### Питание таргета
| M5 реле | Куда | Назначение | | M5 реле | Куда | Назначение |
|---------|------------------|---------------------| | ------- | ----------- | ------------------- |
| RLY1 | VIN таргета | Управление питанием | | RLY1 | VIN таргета | Управление питанием |
Питание включается и выключается автоматически фикстурой `m5` в `conftest.py`: Питание включается и выключается автоматически фикстурой `m5` в `conftest.py`:
@ -33,7 +33,7 @@
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] |
@ -58,7 +58,7 @@ _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 |
@ -74,7 +74,7 @@ _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) из командной строки |
@ -89,13 +89,13 @@ 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) |

View file

@ -20,7 +20,7 @@ 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` | Периферия требует действий оператора: кнопки, дисплей |

View file

@ -24,7 +24,7 @@ def uart(request):
## 2. `yield` vs `return` ## 2. `yield` vs `return`
| Конструкция | Setup | Teardown | Когда использовать | | Конструкция | Setup | Teardown | Когда использовать |
|-------------|-------|----------|--------------------| | ----------- | ----- | -------- | --------------------------------------------- |
| `return` | ✅ | ❌ | Ресурс не требует очистки (загрузка ELF) | | `return` | ✅ | ❌ | Ресурс не требует очистки (загрузка ELF) |
| `yield` | ✅ | ✅ | Ресурс нужно освободить (порт, реле, питание) | | `yield` | ✅ | ✅ | Ресурс нужно освободить (порт, реле, питание) |
@ -57,7 +57,7 @@ def m5(request):
Scope определяет **как долго живёт** экземпляр фикстуры. Scope определяет **как долго живёт** экземпляр фикстуры.
| Scope | Создаётся | Уничтожается | Типичное применение в HIL | | Scope | Создаётся | Уничтожается | Типичное применение в HIL |
|-------|-----------|--------------|---------------------------| | ---------- | ------------------------------ | ----------------------------- | ---------------------------------- |
| `function` | Перед каждым `test_*()` | После каждого `test_*()` | Сброс состояния стенда | | `function` | Перед каждым `test_*()` | После каждого `test_*()` | Сброс состояния стенда |
| `class` | Перед первым тестом класса | После последнего теста класса | Группа связанных тестов | | `class` | Перед первым тестом класса | После последнего теста класса | Группа связанных тестов |
| `module` | Перед первым тестом файла | После последнего теста файла | **Загрузка ELF, открытие UART** | | `module` | Перед первым тестом файла | После последнего теста файла | **Загрузка ELF, открытие UART** |
@ -203,7 +203,7 @@ 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` |

View file

@ -21,7 +21,7 @@ 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-хедеры |
@ -147,7 +147,7 @@ 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` |
@ -185,7 +185,7 @@ 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"` и т.д.) |

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

@ -191,7 +191,7 @@ 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` |
@ -254,7 +254,7 @@ main.c
### Транспорт ### Транспорт
| Параметр | Значение | | Параметр | Значение |
|---|---| | ------------------------- | ---------------------------------------------------------- |
| Интерфейс | USB CDC ACM, разъём J2 | | Интерфейс | USB CDC ACM, разъём J2 |
| Кодировка | UTF-8 | | Кодировка | UTF-8 |
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` | | Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
@ -379,7 +379,7 @@ main.c
``` ```
| `status` | Смысл | | `status` | Смысл |
|---|---| | -------- | ------------------------------------------------------------------------ |
| `"pass"` | Тест пройден | | `"pass"` | Тест пройден |
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) | | `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
| `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше | | `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше |
@ -483,7 +483,7 @@ 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 |
@ -669,7 +669,7 @@ 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` |
@ -720,7 +720,7 @@ 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)` |
@ -751,7 +751,7 @@ 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 |

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

@ -39,7 +39,7 @@ 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

@ -14,6 +14,6 @@
## Модули ## Модули
| Модуль | Путь | Описание | | Модуль | Путь | Описание |
|--------|------|----------| | ------------- | ------------------------------------- | -------------------------------------------------- |
| `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-транспортом |