# bsp_sd: on process

This commit is contained in:
Dmitry Akimov 2026-05-06 11:13:46 +03:00
parent a1c6b1a30f
commit 3ec4aad771
24 changed files with 3327 additions and 633 deletions

View file

@ -18,6 +18,7 @@ add_subdirectory(button)
add_subdirectory(usb_cdc)
add_subdirectory(sdram)
add_subdirectory(qspi_flash)
add_subdirectory(sd)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w)
@ -31,6 +32,27 @@ target_compile_definitions(bsp_board PUBLIC CPU_MIMXRT1052CVJ5B)
# sdk_device PUBLIC — транзитивно подтянется во все firmware таргеты
target_link_libraries(bsp_board PUBLIC sdk_device sdk_clock sdk_common
sdk_lpuart)
# -----------------------------------------------------------------------------
# bsp_sdmmc_config — board-level конфигурация SDMMC (BOARD_SD_Config и пр.)
# Отдельный таргет, чтобы sdk_sdmmc_sd не тянулся во все бинарники через
# bsp_board. Линкуется только теми, кто реально работает с SD: bsp_usd →
# bsp_sdmmc_config
# -----------------------------------------------------------------------------
add_library(bsp_sdmmc_config STATIC generated/sdmmc_config.c)
target_include_directories(bsp_sdmmc_config PUBLIC generated)
target_compile_definitions(bsp_sdmmc_config PUBLIC SD_ENABLED)
target_link_libraries(
bsp_sdmmc_config
PUBLIC bsp_board # board.h, pin_mux.h, clock_config.h, fsl_gpio.h
PUBLIC sdk_sdmmc_sd # fsl_sd.h, fsl_sdmmc_host.h, sdmmchost_t
)
target_compile_options(bsp_sdmmc_config PRIVATE -w)
# ------------------------------------------------------------------
# Boot-стратегии — INTERFACE
# ------------------------------------------------------------------

View file

@ -0,0 +1,150 @@
/*
* sdmmc_config.c board-level реализация SDMMC для MIMXRT1052CVJ5B.
*/
#define SD_ENABLED /* нужен для fsl_sd.h, до включений */
#include "sdmmc_config.h"
#include "fsl_iomuxc.h"
/* ---------------------------------------------------------------------------
* Статические объекты хоста (не экспортируются)
* ------------------------------------------------------------------------- */
/* DMA-дескрипторы: некэшируемая секция, выравнивание по требованию USDHC. */
AT_NONCACHEABLE_SECTION_ALIGN(
static uint32_t s_dma_buf[BOARD_SDMMC_HOST_DMA_DESCRIPTOR_BUFFER_SIZE],
SDMMCHOST_DMA_DESCRIPTOR_BUFFER_ALIGN_SIZE);
static sdmmchost_t s_host;
static sd_detect_card_t s_cd;
static sd_io_voltage_t s_io_voltage = {
.type = BOARD_SDMMC_SD_IO_VOLTAGE_TYPE,
.func = NULL, /* управление через регистры USDHC, не через внешний LDO */
};
/* ---------------------------------------------------------------------------
* Внутренние функции
* ------------------------------------------------------------------------- */
/*
* Возвращает частоту источника USDHC1.
*
* BOARD_BootClockRUN() уже настроил SysPll и PFD0 не трогаем PLL.
* Просто возвращаем известное значение из clock_config.h.
*/
static uint32_t get_usdhc1_src_clock_hz(void)
{
return BOARD_BOOTCLOCKRUN_USDHC1_CLK_ROOT; /* 198 000 000 Hz */
}
/*
* Управление питанием карты: GPIO1[19] (SdPwr), active-high.
*/
static void sd_power_control(bool enable)
{
GPIO_PinWrite(BOARD_SDMMC_SD_PWR_GPIO_BASE, BOARD_SDMMC_SD_PWR_GPIO_PIN, enable ? 1U : 0U);
}
/*
* Инициализация GPIO питания карты как выход, начальное состояние выкл.
* Вызывается однократно из BOARD_SD_Config().
*/
static void sd_power_init(void)
{
const gpio_pin_config_t cfg = {
.direction = kGPIO_DigitalOutput,
.outputLogic = 0U, /* питание выключено при старте */
.interruptMode = kGPIO_NoIntmode,
};
GPIO_PinInit(BOARD_SDMMC_SD_PWR_GPIO_BASE, BOARD_SDMMC_SD_PWR_GPIO_PIN, &cfg);
}
/*
* Динамическая настройка пад-конфигурации линий SD в зависимости от частоты.
* Вызывается SDMMC стеком при смене скорости (HS, SDR50, SDR104).
*
* speed/strength подобраны по таблице EVK для данной платы проверить
* при трассировке > 50 MHz.
*/
static void sd_pin_config(uint32_t freq)
{
uint32_t speed;
uint32_t strength;
if (freq <= 50000000U)
{
speed = 0U;
strength = 7U;
}
else if (freq <= 100000000U)
{
speed = 2U;
strength = 7U;
}
else
{
speed = 3U;
strength = 7U;
}
const uint32_t pad = IOMUXC_SW_PAD_CTL_PAD_SPEED(speed) | IOMUXC_SW_PAD_CTL_PAD_SRE_MASK |
IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK |
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK |
IOMUXC_SW_PAD_CTL_PAD_PUS(1) /* 47k pull-up */
| IOMUXC_SW_PAD_CTL_PAD_DSE(strength);
const uint32_t clk_pad = IOMUXC_SW_PAD_CTL_PAD_SPEED(speed) | IOMUXC_SW_PAD_CTL_PAD_SRE_MASK |
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK |
IOMUXC_SW_PAD_CTL_PAD_PUS(0) /* no pull on CLK */
| IOMUXC_SW_PAD_CTL_PAD_DSE(strength);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_00_USDHC1_CMD, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_01_USDHC1_CLK, clk_pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_02_USDHC1_DATA0, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_03_USDHC1_DATA1, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
}
/* ---------------------------------------------------------------------------
* Публичный API
* ------------------------------------------------------------------------- */
#ifdef SD_ENABLED
void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *user_data)
{
assert(card != NULL);
/* --- host --- */
s_host.dmaDesBuffer = s_dma_buf;
s_host.dmaDesBufferWordsNum = BOARD_SDMMC_HOST_DMA_DESCRIPTOR_BUFFER_SIZE;
s_host.enableCacheControl = BOARD_SDMMC_HOST_CACHE_CONTROL;
sd_card_t *sd = (sd_card_t *) card;
sd->host = &s_host;
sd->host->hostController.base = BOARD_SDMMC_SD_HOST_BASEADDR;
sd->host->hostController.sourceClock_Hz = get_usdhc1_src_clock_hz();
/* --- card detect: HostCD, GPIO-прерывание не нужно --- */
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByHostCD */
s_cd.cardDetected = NULL; /* SDK читает PRSSTAT самостоятельно */
s_cd.callback = cd; /* обычно NULL из bsp_sd */
s_cd.userData = user_data;
sd->usrParam.cd = &s_cd;
sd->usrParam.pwr = sd_power_control;
sd->usrParam.ioStrength = sd_pin_config;
sd->usrParam.ioVoltage = &s_io_voltage;
sd->usrParam.maxFreq = BOARD_SDMMC_SD_HOST_SUPPORT_SDR104_FREQ;
/* --- GPIO питания --- */
sd_power_init();
/* --- приоритет прерывания хоста --- */
NVIC_SetPriority(BOARD_SDMMC_SD_HOST_IRQ, host_irq_priority);
}
#endif /* SD_ENABLED */

View file

@ -0,0 +1,80 @@
/*
* sdmmc_config.h board-level конфигурация SDMMC для MIMXRT1052CVJ5B.
*
* SD_CD : GPIO_B1_12 USDHC1.usdhc_cd_b (периферийный сигнал, не GPIO).
* Детект через USDHC_GetPresentStatusFlags kSD_DetectCardByHostCD.
* GPIO-прерывание на CD не нужно.
*
* SD_PWR: GPIO_AD_B1_03 GPIO1[19] (SdPwr).
* Active-high: 1 = питание включено.
*/
#ifndef BSP_SDMMC_CONFIG_H
#define BSP_SDMMC_CONFIG_H
#include "clock_config.h"
#include "fsl_common.h"
#include "fsl_gpio.h"
#include "fsl_sdmmc_common.h"
#include "fsl_sdmmc_host.h"
#ifdef SD_ENABLED
#include "fsl_sd.h"
#endif
/* ---------------------------------------------------------------------------
* Host controller
* ------------------------------------------------------------------------- */
#define BOARD_SDMMC_SD_HOST_BASEADDR USDHC1
#define BOARD_SDMMC_SD_HOST_IRQ USDHC1_IRQn
#define BOARD_SDMMC_SD_HOST_IRQ_PRIORITY 5U
/* ---------------------------------------------------------------------------
* Card detect
* CD_B подключён как периферийный сигнал USDHC1 детект через регистр
* PRSSTAT, а не через GPIO. Прерывание и callback на CD не используются.
* ------------------------------------------------------------------------- */
#define BOARD_SDMMC_SD_CD_TYPE kSD_DetectCardByHostCD
#define BOARD_SDMMC_SD_CD_DEBOUNCE_MS 100U
/* ---------------------------------------------------------------------------
* Card power GPIO1[19] (GPIO_AD_B1_03, SdPwr), active-high
* ------------------------------------------------------------------------- */
#define BOARD_SDMMC_SD_PWR_GPIO_BASE GPIO1
#define BOARD_SDMMC_SD_PWR_GPIO_PIN 19U
/* ---------------------------------------------------------------------------
* IO voltage управляется хостом (LDO внутри USDHC)
* ------------------------------------------------------------------------- */
#define BOARD_SDMMC_SD_IO_VOLTAGE_TYPE kSD_IOVoltageCtrlByHost
/* ---------------------------------------------------------------------------
* DMA и кэш
* ------------------------------------------------------------------------- */
#define BOARD_SDMMC_HOST_DMA_DESCRIPTOR_BUFFER_SIZE 32U
#define BOARD_SDMMC_DATA_BUFFER_ALIGN_SIZE 32U
#define BOARD_SDMMC_HOST_CACHE_CONTROL kSDMMCHOST_CacheControlRWBuffer
/* ---------------------------------------------------------------------------
* Максимальная частота (SDR104)
* ------------------------------------------------------------------------- */
#define BOARD_SDMMC_SD_HOST_SUPPORT_SDR104_FREQ 200000000U
#if defined(__cplusplus)
extern "C"
{
#endif
#ifdef SD_ENABLED
/*
* Конфигурирует sd_card_t: host, CD, power, IO voltage, частота.
* cd и userData передаются NULL из bsp_sd CD управляется хостом.
*/
void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *user_data);
#endif
#if defined(__cplusplus)
}
#endif
#endif /* BSP_SDMMC_CONFIG_H */

View file

@ -23,23 +23,38 @@
### Проблема
Прошивка исполняется XIP из того же Flash через FlexSPI AHB-интерфейс.
CPU непрерывно читает инструкции через AHB, который маршрутизируется по
LUT-слоту 0. Любая IP-команда FlexSPI прерывает AHB-путь на время
выполнения команды. Если в момент IP-команды CPU попытается фетчить
инструкцию из Flash — HardFault.
Прошивка исполняется XIP из Flash через FlexSPI AHB-интерфейс.
CPU непрерывно фетчит инструкции из Flash по AHB — через LUT-слот 0.
Любая IP-команда FlexSPI блокирует AHB-путь на время выполнения.
Если в этот момент CPU попытается фетчить инструкцию из Flash — **HardFault**.
### Решение
### Решение: ITCM + IRQ lock
Все функции, обращающиеся к регистрам FlexSPI (IPCMD, IPCR0/1, RFDR, TFDR,
LUT, INTR, …), размещены в **ITCM** через `AT_QUICKACCESS_SECTION_CODE`.
Инструкции этих функций загружаются в ITCM при старте и CPU не обращается
к Flash во время их выполнения.
Все функции, обращающиеся к регистрам FlexSPI, размещены в **ITCM**
(`0x00000000`) через `AT_QUICKACCESS_SECTION_CODE`. ITCM подключён к CPU
по выделенной шине (не AHB), поэтому фетч инструкций из ITCM не конкурирует
с IP-командами FlexSPI.
AHB prefetch отключается (`AHBCR.PREFETCHEN = 0`) перед каждой серией
IP-транзакций и восстанавливается после (`qspi_ahb_disable` / `qspi_ahb_enable`).
Это дополнительно предотвращает спекулятивные AHB-чтения Flash во время
IP-команд.
Дополнительно, каждая публичная операция выполняется под **IRQ lock**
(`qspi_irq_lock` / `qspi_irq_unlock` с `__get_PRIMASK()` + `DSB` + `ISB`):
это гарантирует, что прерывание не застанет FlexSPI в середине IP-транзакции.
IRQ unlock восстанавливает предыдущий `PRIMASK`, не включает IRQ безусловно —
вызов из уже заблокированного контекста корректен.
AHB prefetch отключается (`AHBCR.PREFETCHEN = 0`) перед серией IP-транзакций
и восстанавливается после.
### Обязательный дефайн в CMakeLists потребителя
```cmake
target_compile_definitions(firmware_test PRIVATE
__STARTUP_INITIALIZE_RAMFUNCTION # ← обязательно
__STARTUP_CLEAR_BSS)
```
Без `__STARTUP_INITIALIZE_RAMFUNCTION` startup-файл NXP SDK не копирует
`CodeQuickAccess` секцию из Flash в ITCM. В ITCM остаются нули. Первый же
вызов любой ITCM-функции вызывает **HardFault**.
---
@ -47,33 +62,31 @@ IP-команд.
### Почему не Enter 4-Byte Mode (0xB7)
FDCB задаёт XIP слот 0 с 24-bit адресацией для **всех** чипов. Если
переключить чип в 4-byte mode командой 0xB7, следующий AHB-фетч
инструкции из Flash пойдёт через слот 0 с 24-bit адресом, а чип теперь
ждёт 32-bit — **HardFault**.
FDCB фиксирует XIP-слот 0 в режиме 24-bit адресации на всех чипах.
Переключение чипа командой 0xB7 сломало бы XIP — AHB продолжал бы
посылать 24-bit адреса, чип ждал бы 32-bit → **HardFault**.
### Dedicated 4-byte address opcodes
W25Q256/512 имеют отдельный набор opcodes, принимающих 32-bit адрес
**независимо от текущего режима адресации чипа**:
W25Q256/512 принимают 32-bit адрес через отдельный набор opcodes — без
изменения режима адресации чипа:
| Операция | 3-byte opcode (W25Q64/128) | 4-byte opcode (W25Q256/512) |
|-------------------|----------------------------|-----------------------------|
| Операция | W25Q64/128 (3-byte) | W25Q256/512 (4-byte) |
|-------------------|---------------------|----------------------|
| Sector Erase 4KB | `0x20` | `0x21` |
| Block Erase 32KB | `0x52` | `0x5C` |
| Block Erase 64KB | `0xD8` | `0xDC` |
| Quad Page Program | `0x32` | `0x34` |
| Quad Output Read | `0x6B` | `0x6C` |
| IP Quad Out Read | `0x6B` | `0x6C` |
Слот 0 (XIP) **не изменяется**. Чип остаётся в 24-bit режиме. XIP работает
непрерывно на всех чипах.
Слот 0 (XIP) **не изменяется**. XIP работает непрерывно на всех чипах.
---
## LUT-слоты
| Слот | Константа | Команда | Зависит от чипа |
|------|----------------|----------------------------|-----------------|
|------|----------------|-----------------------------|-----------------|
| 0 | (XIP, FDCB) | Quad Read | Нет (не трогаем)|
| 1 | LSEQ_READ_SR1 | Read SR1 (0x05) | Нет |
| 2 | LSEQ_WR_EN | Write Enable (0x06) | Нет |
@ -87,15 +100,30 @@ W25Q256/512 имеют отдельный набор opcodes, принимающ
| 10 | LSEQ_ERASE_64K | Block Erase 64KB (0xD8/0xDC)| Да |
| 11 | LSEQ_IP_READ | Quad Out Read (0x6B/0x6C) | Да |
Слоты 111 обновляются в `bsp_qspi_init()` под конкретный чип.
Слот 0 никогда не изменяется BSP-кодом.
---
## Watermark FIFO
Размер watermark-юнита читается из регистров `IPRXFCR.RXWMRK` и
`IPTXFCR.TXWMRK` в рантайме — не зашит константой. Это гарантирует
корректную работу если FDCB или SDK изменили настройки watermark по
умолчанию.
---
## Публичный API
Все публичные функции размещены в ITCM (`AT_QUICKACCESS_SECTION_CODE`) и
выполняются под IRQ lock.
```c
/* Инициализация — первый вызов */
/* Инициализация — вызвать до bsp_tick_init() и любой другой функции модуля */
bsp_status_t bsp_qspi_init(void);
/* Чтение JEDEC ID */
/* Чтение JEDEC ID (0x9F) */
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
/* Стирание */
@ -103,72 +131,85 @@ bsp_status_t bsp_qspi_erase_sector(uint32_t addr); /* 4KB, ~45 мс */
bsp_status_t bsp_qspi_erase_block_32k(uint32_t addr); /* 32KB, ~120 мс */
bsp_status_t bsp_qspi_erase_block_64k(uint32_t addr); /* 64KB, ~150 мс */
/* Запись одной страницы */
/* Запись одной страницы (256 байт) */
bsp_status_t bsp_qspi_write_page(uint32_t addr, const uint8_t *p_data); /* ~3 мс */
/* Чтение произвольного числа байт (через IP-команду, не AHB) */
/* Чтение через IP-команду (не AHB/XIP) */
bsp_status_t bsp_qspi_read(uint32_t addr, uint8_t *p_buf, size_t size);
/* Размер Flash — доступен после init */
/* Размер Flash — доступен после bsp_qspi_init() */
uint32_t bsp_qspi_flash_size(void);
```
---
## Выбор операции стирания для массовой очистки
| Объём очистки | Рекомендуемая операция | Примерное время |
|------------------|------------------------|-----------------|
| < 32 KB | `erase_sector` (4KB) | пропорционально |
| 32 KB — 1 MB | `erase_block_32k` | 120 мс / 32KB |
| > 1 MB | `erase_block_64k` | 150 мс / 64KB |
Пример: очистка 4 MB через 64KB блоки = 64 операции × 150 мс ≈ **9.6 с**
против 1024 × 45 мс ≈ **46 с** через 4KB секторы.
Все функции возвращают `BSP_OK` при успехе или `BSP_ERR_HW` при ошибке
FlexSPI / неверном аргументе. `BSP_ERR_HW` должен присутствовать в
`bsp/common/include/bsp/status.h`.
---
## Порядок вызова
## Порядок инициализации
`bsp_qspi_init()` должна вызываться **до** `bsp_tick_init()`:
```c
/* 1. Инициализация (один раз) */
if (bsp_qspi_init() != BSP_OK) { /* обработка ошибки */ }
/* 2. Операции */
bsp_qspi_erase_sector(addr); /* блокирует ~45 мс */
bsp_qspi_write_page(addr, buf); /* блокирует ~3 мс */
bsp_qspi_read(addr, buf, 256); /* быстро */
/* main.c */
board_hw_init();
bsp_qspi_init(); /* ← сначала QSPI, до SysTick */
bsp_tick_init(); /* ← потом SysTick */
bsp_usb_cdc_init();
```
Причина: `bsp_qspi_init()` и все ITCM-функции выполняются под полным
IRQ lock. Если SysTick уже запущен и прерывание сработает в момент
IP-команды — возможен AHB stall. Порядок инициализации устраняет эту
гонку при первом запуске.
---
## Выбор операции стирания
| Объём очистки | Рекомендация | Время |
|---------------|------------------------|-----------------|
| < 32 KB | `erase_sector` (4KB) | пропорционально |
| 32 KB — 1 MB | `erase_block_32k` | ~120 мс / 32KB |
| > 1 MB | `erase_block_64k` | ~150 мс / 64KB |
Пример: 4 MB через 64KB = 64 × 150 мс ≈ **9.6 с**
против 1024 × 45 мс ≈ **46 с** через 4KB.
---
## CMake
```cmake
# bsp/qspi/CMakeLists.txt
target_link_libraries(bsp_qspi
# bsp/qspi_flash/CMakeLists.txt
target_link_libraries(bsp_qspi_flash
PUBLIC bsp_status
PRIVATE bsp_board sdk_flexspi
)
```
Потребители (например, `firmware_test`) линкуют `bsp_qspi`:
Потребитель (`firmware_test`):
```cmake
target_link_libraries(firmware_test PRIVATE ... bsp_qspi)
target_link_libraries(firmware_test PRIVATE
bsp_qspi_flash
...
)
target_compile_definitions(firmware_test PRIVATE
__STARTUP_INITIALIZE_RAMFUNCTION # обязательно для ITCM-функций
__STARTUP_CLEAR_BSS
)
```
---
## Известные ограничения
- `bsp_qspi_write_page()` записывает строго одну страницу (256 байт).
Запись через границу страницы не поддерживается — вызывающий код
обязан выровнять адрес.
- `bsp_qspi_write_page()` — строго одна страница (256 байт). Адрес обязан
быть выровнен на `BSP_QSPI_PAGE_SIZE`. Запись через границу страницы не
поддерживается.
- Нет timeout в `qspi_wait_not_busy()`. Зависание из-за дефектного чипа
потребует watchdog reset. Для диагностической прошивки это приемлемо.
- Chip Erase (0xC7) не реализован — слишком деструктивно для XIP-прошивки.
Используй `erase_block_64k` в цикле по всему адресному пространству.
- Chip Erase (0xC7) не реализован — слишком деструктивно при XIP-исполнении.

10
bsp/sd/CMakeLists.txt Normal file
View file

@ -0,0 +1,10 @@
add_library(bsp_sd STATIC src/sd.c)
target_include_directories(bsp_sd PUBLIC include)
target_link_libraries(
bsp_sd
PUBLIC bsp_status # bsp_status_t
PRIVATE bsp_sdmmc_config # BOARD_SD_Config, sdmmc_config.h тащит sdk_sdmmc_sd
# транзитивно
)

130
bsp/sd/README.md Normal file
View file

@ -0,0 +1,130 @@
# bsp_sd — SD host-контроллер (USDHC1)
Модуль инициализирует SD host-контроллер и проверяет наличие карты.
Файловой системой не занимается — это ответственность `bsp_usd` (поверх) или
приложения напрямую.
## Место в архитектуре
Каждый слой знает только о слое ниже — зависимости не пересекают границы.
```bash
firmware_test / tft_app
bsp_usd ← монтирование FatFS, тест R/W
├──► firmware_test_fatfs ← ff.c + fsl_sd_disk + diskio (bare-metal ffconf)
│ tft_app_fatfs ← ff.c + fsl_sd_disk + diskio (FreeRTOS ffconf)
│ │
│ ▼
│ port/fatfs/sd ← diskio_sd.c: microsd_disk_* → fsl_sd_disk
│ │
▼ ▼
bsp_sd ← этот модуль: host init / deinit / card detect
bsp/generated/sdmmc_config ← board-level: BOARD_SD_Config, GPIO питания, pad config
sdk_sdmmc_sd ← NXP: fsl_sd, fsl_sdmmc_common, fsl_sdmmc_host (non-blocking)
sdk_usdhc ← NXP HAL: fsl_usdhc
```
---
## Аппаратный контекст
| Сигнал | Пин MCU | Конфигурация |
|----------|-----------------|---------------------------------------------------|
| CLK | GPIO_SD_B0_01 | USDHC1_CLK, периферийный режим |
| CMD | GPIO_SD_B0_00 | USDHC1_CMD, периферийный режим |
| D0D3 | GPIO_SD_B0_0205| USDHC1_DATA03, периферийный режим |
| CD_B | GPIO_B1_12 | USDHC1_CD_B — детект через USDHC PRSSTAT |
| SdPwr | GPIO_AD_B1_03 | GPIO1[19], active-high, управляется SDK через BSP |
**CD_B** подключён как периферийный сигнал USDHC1, а не как GPIO. Детект карты
читается через `USDHC_GetPresentStatusFlags``kUSDHC_CardInsertedFlag`.
GPIO-прерывание на CD не используется (`kSD_DetectCardByHostCD`).
**SdPwr** инициализируется в `BOARD_SD_Config()` как GPIO-выход, выключен при старте.
SDK включает питание автоматически в процессе `SD_HostInit()` через callback.
---
## API
### `bsp_sd_init(void)`
Конфигурирует SDMMC host однократно (`BOARD_SD_Config`) и запускает
host-контроллер (`SD_HostInit`).
Повторный вызов без `bsp_sd_deinit` — no-op, возвращает `BSP_OK`.
Возвращает:
- `BSP_OK` — host готов к работе;
- `BSP_ERR_INIT``SD_HostInit` вернул ошибку.
### `bsp_sd_deinit(void)`
Останавливает host-контроллер и отключает питание карты.
Безопасен при вызове до `init` или повторно после `deinit`.
Возвращает:
- `BSP_OK` — всегда.
### `bsp_sd_is_inserted(void)`
Читает регистр `USDHC1 PRSSTAT`. Не требует предварительного `bsp_sd_init()`
включает тактирование USDHC1 самостоятельно через `CLOCK_EnableClock`.
Возвращает:
- `true` — карта вставлена;
- `false` — карта отсутствует.
---
## Разделение ответственности: bsp_sd vs sdmmc_config vs port_fatfs_sd
| Слой | Что делает | Где живёт |
|-----------------------|---------------------------------------------------------|------------------------|
| `sdmmc_config` | Константы платы, `BOARD_SD_Config`, GPIO питания, pads | `bsp/generated/` |
| `bsp_sd` | `SD_HostInit/Deinit`, идемпотентность, card detect | `bsp/sd/` |
| `port_fatfs_sd` | `microsd_disk_*``fsl_sd_disk` (FatFS diskio glue) | `port/fatfs/sd/` |
| `firmware_test_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (bare-metal ffconf)| `firmware/test/fatfs/` |
| `tft_app_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (FreeRTOS ffconf) | `firmware/tft_app/fatfs/` (будущее) |
**Почему `ff.c` и `fsl_sd_disk.c` не компилируются один раз как общая библиотека:**
оба включают `ff.h``ffconf.h`, который разный для `firmware_test` (bare-metal,
`FF_FS_REENTRANT=0`) и `tft_app` (FreeRTOS, `FF_FS_REENTRANT=1`, `FF_VOLUMES=3`).
Общий только `port_fatfs_sd` — он не включает `ff.h` напрямую.
---
## Зависимости
```cmake
target_link_libraries(bsp_sd
PUBLIC bsp_status # bsp_status_t
PRIVATE bsp_sdmmc_config # BOARD_SD_Config, sdmmc_config.h, sdk_sdmmc_sd
)
```
`sdk_sdmmc_sd` — транзитивно через `bsp_sdmmc_config`.
`sdk_usdhc` — транзитивно через `sdk_sdmmc_sd`.
---
## Ограничения
- Модуль рассчитан на одну карту (USDHC1, `g_sd` — единственный дескриптор).
- `bsp_sd_is_inserted()` читает аппаратный регистр без дебаунса. При
механическом детекте возможны ложные срабатывания в момент вставки/извлечения —
добавляй дебаунс в вызывающем коде если нужно.
- Hot-swap не поддерживается: `bsp_sd_deinit()` + `bsp_sd_init()` между сессиями.

35
bsp/sd/include/bsp/sd.h Normal file
View file

@ -0,0 +1,35 @@
/*
* bsp_sd инициализация SD host-контроллера и детект карты.
*
* Модуль управляет только железом (USDHC host, питание карты).
* Монтирование файловой системы в bsp_usd поверх этого модуля.
*/
#ifndef BSP_SD_H
#define BSP_SD_H
#include "bsp/status.h"
#include <stdbool.h>
/*
* Инициализировать SD host-контроллер.
* Вызывать до любых операций с картой.
* Повторный вызов без deinit возвращает BSP_OK (идемпотентен).
*/
bsp_status_t bsp_sd_init(void);
/*
* Деинициализировать SD host-контроллер и отключить питание карты.
* Безопасен при вызове до init или после deinit.
*/
bsp_status_t bsp_sd_deinit(void);
/*
* Проверить физическое наличие карты через регистр USDHC PRSSTAT.
* Не требует предварительного вызова bsp_sd_init().
* Включает тактирование USDHC1 на время чтения регистра.
*/
bool bsp_sd_is_inserted(void);
#endif /* BSP_SD_H */

93
bsp/sd/src/sd.c Normal file
View file

@ -0,0 +1,93 @@
/*
* bsp_sd реализация.
*/
#include "bsp/sd.h"
#include "fsl_clock.h"
#include "fsl_sd.h"
#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */
/* ---------------------------------------------------------------------------
* Глобальный дескриптор карты нужен SDK-стеку (передаётся по указателю
* в BOARD_SD_Config и sd_disk_initialize через g_sd).
* Объявлен без static fsl_sd_disk.c ссылается на него как extern sd_card_t g_sd.
* ------------------------------------------------------------------------- */
extern sd_card_t g_sd;
/* ---------------------------------------------------------------------------
* Состояние модуля
* ------------------------------------------------------------------------- */
static bool g_s_host_configured = false;
static bool g_s_initialized = false;
/* ---------------------------------------------------------------------------
* Внутренние функции
* ------------------------------------------------------------------------- */
/*
* Конфигурирует SDMMC host однократно.
* Повторный вызов no-op: BOARD_SD_Config не идемпотентен сам по себе
* (переинициализирует GPIO питания), поэтому защищаем флагом.
*/
static void ensure_host_configured(void)
{
if (g_s_host_configured)
{
return;
}
/* cd=NULL, userData=NULL: CD управляется хостом через PRSSTAT */
BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
g_s_host_configured = true;
}
/* ---------------------------------------------------------------------------
* Публичный API
* ------------------------------------------------------------------------- */
bsp_status_t bsp_sd_init(void)
{
if (g_s_initialized)
{
return BSP_OK;
}
ensure_host_configured(); /* только BOARD_SD_Config — заполняет g_sd */
/*
* Полный init (host + card) происходит в sd_disk_initialize SD_Init,
* который вызывается из f_mount disk_initialize.
*/
g_s_initialized = true;
return BSP_OK;
}
bsp_status_t bsp_sd_deinit(void)
{
if (!g_s_initialized)
{
return BSP_OK;
}
SD_HostDeinit(&g_sd);
SD_SetCardPower(&g_sd, false);
g_s_initialized = false;
return BSP_OK;
}
bool bsp_sd_is_inserted(void)
{
/*
* CD_B подключён как периферийный сигнал USDHC1, поэтому детект
* через регистр PRSSTAT, а не GPIO.
*
* USDHC1 может быть ещё не тактирован в момент вызова (до init),
* поэтому явно включаем clock. После init clock уже включён SDK-стеком,
* повторный вызов CLOCK_EnableClock безвреден.
*/
CLOCK_EnableClock(kCLOCK_Usdhc1);
uint32_t prsstat = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR);
return (prsstat & (uint32_t) kUSDHC_CardInsertedFlag) != 0U;
}

View file

@ -3,6 +3,8 @@
set(TARGET_NAME firmware_test)
add_subdirectory(fatfs)
add_executable(
${TARGET_NAME}
src/main.c
@ -38,7 +40,9 @@ target_link_libraries(
bsp_boot_xip
bsp_usb_cdc
bsp_sdram
bsp_qspi_flash)
bsp_qspi_flash
bsp_sd
firmware_test_fatfs)
# -----------------------------------------------------------------------------
# Linker script

View file

@ -1,8 +1,6 @@
# firmware_test — Plan of Development
> Документ для нового треда. Содержит все принятые решения, текущий статус и
> пошаговый план дальнейшей разработки.
> Версия: 0.2 | Обновлён после реализации скелета + bsp_usb_cdc.
> Версия: 0.4 | Обновлён после завершения Этапов 13 (bsp_sdram + bsp_qspi_flash).
---
@ -12,548 +10,282 @@
Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика.
**Стенд:**
- Хост подключается через USB CDC ACM (J2) — единственный канал
- Тесты с внешними сигналами управляются через M5StampPLC (реле, оптовходы)
- Отдельный компьютер-сервер запускает pytest
- Хост подключается через USB CDC ACM — единственный канал firmware_test
- Тесты с внешними сигналами управляются через M5StampPLC
- Сервер запускает `tools/hil/` (разработка) или `tools/production/` (производство)
---
## Статус на момент создания документа
## Текущий статус
| Компонент | Статус | Примечание |
|---|---|---|
| `bsp_usb_cdc` | ✅ Готов | HIL тест пройден (`05_test_usb_cdc.py`) |
| `firmware_test` скелет | ✅ Готов | `main.c` + `cli.c` + PING работает |
| Host-тест CLI | ✅ Готов | `test_cli.c`, 8 тестов, зелёные |
| `bsp_sdram` | ⬜ Не начат | |
| `bsp_qspi` | ⬜ Не начат | |
| `bsp_usd` | ⬜ Не начат | |
| Протокол v2 | ⬜ Не начат | Эволюция от cmd/ok к event-driven |
| Test runner | ⬜ Не начат | |
| Provisioning | ⬜ Не начат | |
|----------------------------|--------|------------|
| `bsp_usb_cdc` | ✅ | HIL тест пройден |
| firmware_test скелет | ✅ | `main.c` + `cli.c` |
| Протокол v2 + test_runner | ✅ | JSON-lines event-driven |
| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention |
| `bsp_qspi_flash` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock |
| `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
| `bsp_usd` | ⬜ | Этап 4 |
| `test_usd` | ⬜ | Этап 4 |
| Display test | ⬜ | Этап 5 |
| Button test | ⬜ | Этап 5 |
| CAN test | ⬜ | Этап 6 (bsp_can ✅) |
| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) |
| UART ISO test | ⬜ | Этап 6 |
| Opto test | ⬜ | Этап 6 (bsp_opto ✅) |
| Provisioning | ⬜ | Этап 7 |
### Закрытые архитектурные решения
---
## Закрытые архитектурные решения
> Не пересматривать без явного запроса.
- **[DECISION] Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
- **[DECISION] Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"cmd"` / `"type"`.
- **[DECISION] SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
- **[DECISION] SDRAM тест — не HIL ELF:** тест прогоняется командами через firmware_test, не отдельным ELF.
- **[DECISION] IR и RTC:** не реализуются.
- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`.
- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
- **SDRAM тест:** прогоняется командами через firmware_test, не отдельным HIL ELF.
- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION` в CMakeLists.
- **QSPI IRQ lock:** `__get_PRIMASK()` + DSB/ISB. Публичные API под полным lock.
- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7).
- **Порядок init в main.c:** `bsp_qspi_init()` до `bsp_tick_init()`.
- **IR и RTC:** не реализуются.
- **Производственный runner:** Вариант D — отдельный `tools/production/` без pytest.
---
## Протокол v2 — решение, требующее принятия в новом треде
## Этап 4 — `bsp_usd` + `test_usd`
Текущий скелет использует упрощённый протокол:
```json
{"cmd":"PING"} → {"ok":true,"result":"PONG"}
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| Интерфейс | USDHC (SDIO) |
| Карта | microSD, вставляется оператором перед тестом |
| Файловая система | FatFS (SDK middleware) |
| Детект карты | GPIO (CD pin) или опрос через USDHC status |
### BSP API (предварительно)
```c
/* bsp/usd/include/bsp/usd.h */
typedef enum {
BSP_USD_OK = 0,
BSP_USD_ERR_NO_CARD, /* карта не вставлена */
BSP_USD_ERR_INIT, /* USDHC или FatFS init failed */
BSP_USD_ERR_MOUNT, /* f_mount() failed */
BSP_USD_ERR_RW, /* read/write/compare failed */
} bsp_usd_status_t;
bsp_usd_status_t bsp_usd_init(void);
bsp_usd_status_t bsp_usd_is_card_present(void);
bsp_usd_status_t bsp_usd_test_rw(void); /* write + read + compare тестового файла */
void bsp_usd_deinit(void);
```
TODO.md описывает расширенный протокол с типами событий:
```json
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
{"type":"test_begin","id":"sdram","name":"SDRAM 32MB","critical":true}
{"type":"test_result","id":"sdram","status":"pass","ms":312}
{"type":"summary","passed":7,"failed":0,"overall":"pass"}
### test_usd — шаги
| Шаг | Действие | Время |
|---|---|---|
| 1: Card detect | `bsp_usd_is_card_present()` | < 1 мс |
| 2: Mount | `f_mount()` — FAT/exFAT | < 200 мс |
| 3: Write | Записать 4 KB тестовый файл | < 500 мс |
| 4: Read + Compare | Прочитать и сравнить побайтово | < 200 мс |
| 5: Unmount | `f_unmount()` | < 50 мс |
### Интерактивность
Тест помечен `requires_hil = false`, `pre_confirm_prompt = "Вставьте microSD и нажмите OK"`.
test_runner ждёт `{"type":"confirm","id":"usd_insert","confirmed":true}` до вызова `run()`.
Отказ или таймаут 30 с`TEST_STATUS_SKIP`.
### Файлы
```
**Вопросы для обсуждения в треде:**
1. Переходить ли на v2 сразу или итерационно (сначала SDRAM с cmd/ok, потом рефакторинг)?
2. Как обрабатывать `confirm_request` (display test) в cli.c — отдельный тип входящего сообщения?
3. `session_start` — посылать ли при каждом `READY` или только по команде `START_SESSION`?
**Рекомендация:** реализовать v2 до добавления первого теста, иначе рефакторинг CLI затронет уже написанные тест-модули.
---
## Структура файлов — целевое состояние
```
firmware/test/
bsp/usd/
├── CMakeLists.txt
├── README.md
└── src/
├── main.c # ✅ готов
├── cli.h / cli.c # ✅ готов (v1), требует эволюции до v2
├── protocol.h / protocol.c # ⬜ новый: сериализация ответов
├── test_runner.h / .c # ⬜ новый: реестр + sequencer
├── provisioning.h / .c # ⬜ новый: chip UID + provision_ack
└── tests/
├── test_sdram.h / .c # ⬜
├── test_qspi.h / .c # ⬜
├── test_usd.h / .c # ⬜
├── test_display.h / .c # ⬜ интерактивный (кнопки)
├── test_can.h / .c # ⬜ HIL (M5StampPLC)
├── test_uart.h / .c # ⬜ HIL TTL + ISO
└── test_opto.h / .c # ⬜ HIL (M5StampPLC)
├── include/bsp/usd.h
└── src/usd.c
bsp/
├── sdram/ # ⬜ CMakeLists.txt + sdram.c (структура есть)
├── qspi/ # ⬜ новый модуль
└── usd/ # ⬜ новый модуль
firmware/test/src/tests/test_usd.c
```
### CMake
```cmake
# bsp/usd/CMakeLists.txt
target_link_libraries(bsp_usd
PUBLIC bsp_status
PRIVATE bsp_board sdk_usdhc middleware_fatfs
)
```
### Открытые вопросы перед реализацией
- [ ] Есть ли `sdk_usdhc` таргет в `sdk/CMakeLists.txt`?
- [ ] Карта вставлена постоянно или оператор вставляет каждый раз?
- [ ] Нужен ли Card Detect GPIO или только USDHC status?
- [ ] Файловая система: FAT32 или exFAT (размер карт)?
---
## Матрица тестов
## Этап 5 — Display + Button (интерактивные)
| ID | Название | Тип | Critical | HIL (M5) | HIL ELF | BSP | Статус |
|---|---|---|---|---|---|---|---|
| — | PING | cmd | — | — | — | — | ✅ |
| `sdram` | SDRAM 32MB | self | ✅ | ❌ | ❌ | `bsp_sdram` | ⬜ |
| `qspi` | QSPI Flash | self | ✅ | ❌ | ❌ | `bsp_qspi` | ⬜ |
| `usd` | uSD (SDIO) | self | ✅ | ❌ | ❌ | `bsp_usd` | ⬜ |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ❌ | существующий BSP | ⬜ |
| `can` | CAN | HIL | ❌ | ✅ | ❌ | `bsp_can` ✅ | ⬜ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | `bsp_uart_host` ✅ | ⬜ |
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | ❌ | уточнить | ⬜ |
| `opto` | Opto-in +24V | HIL | ❌ | ✅ | ❌ | `bsp_opto` ✅ | ⬜ |
### test_display
**Колонка "HIL ELF":** отдельная RAM-прошивка через pyOCD. Для firmware_test тестов — не нужна, тесты идут через USB CDC.
| Параметр | Значение |
|---|---|
| Critical | ❌ |
| HIL | ❌ |
| Confirm | Внутри `run()` — 4 отдельных confirm |
**Колонка "HIL (M5)":** нужна ли M5StampPLC для управления внешними сигналами.
Шаги: заливка Red → confirm → Green → confirm → Blue → confirm → White → confirm.
Каждый шаг посылает `confirm_request`, ждёт `confirm` с таймаутом 15 с.
Итог = AND всех четырёх подтверждений.
---
`detail` при FAIL содержит ID первого непрошедшего шага: `"display_blue not confirmed"`.
## Пошаговый план
### test_buttons
### Этап 0 — Вопросы, требующие ответа до кода
| Параметр | Значение |
|---|---|
| Critical | ❌ |
| HIL | ❌ |
| Confirm | prompt only (детект через bsp_button) |
Обсудить в начале треда:
Шаги: Test_But_1 → Test_But_2. Таргет посылает `confirm_request` как инструкцию
оператору, детектирует нажатие через `bsp_button` — JSON confirm не нужен.
Таймаут 10 с на каждую кнопку.
- Протокол v2: переходить сейчас или после SDRAM?
- `uart_iso`: есть отдельный BSP модуль или это тот же `bsp_uart_host` с другими параметрами?
- `usd`: карта вставлена постоянно на плате или оператор вставляет перед тестом?
- QSPI: какой конкретно чип (W25Q128?), есть ли уже sdk_flexspi таргет в CMake?
### Аппаратный контекст кнопок
---
### Этап 1 — Протокол v2 + Test runner скелет
**Цель:** эволюция cli.c → protocol.c + test_runner.c. После этого этапа добавление каждого теста — одна строка в реестре.
**Файлы:**
```
firmware/test/src/
├── cli.c ← упрощается: только IO (read/write/buffer)
├── protocol.h/.c ← новый: сериализация всех типов сообщений
├── test_module.h ← новый: интерфейс тест-модуля
└── test_runner.h/.c ← новый: реестр + sequencer
```
**Интерфейс тест-модуля (`test_module.h`):**
```c
typedef enum { TEST_STATUS_PASS = 0, TEST_STATUS_FAIL, TEST_STATUS_SKIP } test_status_t;
typedef struct {
test_status_t status;
uint32_t duration_ms;
char detail[96];
} test_result_t;
typedef struct {
const char *id;
const char *name;
bool critical;
bool requires_hil;
void (*init)(void);
test_result_t (*run)(void);
void (*deinit)(void);
} test_module_t;
```
**Сериализация (protocol.h):** функции `protocol_send_session_start()`,
`protocol_send_test_begin()`, `protocol_send_test_result()`,
`protocol_send_summary()`, `protocol_send_confirm_request()` — все через `cli_send()`.
**Команды хоста v2:**
| Входящий тип | Поле | Действие |
| Кнопка | Пин MCU | GPIO |
|---|---|---|
| `cmd` | `"run_all"` | Запустить все тесты по реестру |
| `cmd` | `"run"` + `"id"` | Запустить один тест |
| `cmd` | `"ping"` | `{"type":"pong"}` |
| `confirm` | `"id"` + `"confirmed"` | Ответ оператора на display тест |
**Host-тесты:** расширить `test_cli.c` + добавить `test_protocol.c` (Категория A).
| Test_But_1 | GPIO_B1_14 | GPIO2[30] |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] |
---
### Этап 2 — `bsp_sdram`
## Этап 6 — CAN + UART + Opto (HIL, M5StampPLC)
Все три теста `requires_hil = true`. Запускаются только при наличии стенда.
BSP для всех трёх уже готов.
### test_can
M5StampPLC отправляет CAN-фрейм → плата принимает → сравниваем ID и payload.
**Шаги:**
1. M5 → `{"cmd":"can_send","id":0x100,"data":[0xDE,0xAD,0xBE,0xEF]}` (через `confirm_request`)
2. Таргет: `uart_cmd("CAN_RECV 500")``"100 DEADBEEF"` или `"TIMEOUT"`
3. Ответный: таргет посылает → M5 `can_recv` → верификация
### test_uart_ttl
M5 loopback через UART TTL → echo-верификация.
### test_uart_iso
M5 RLY2 → RS_RX оптовход (BSP_OPTO_CH_RS) → детект ACTIVE/INACTIVE.
Использует `bsp_opto` с `rs_as_gpio=true`.
### test_opto
M5 RLY3/RLY4 → EXT_IN1/IN2 → детект ACTIVE/INACTIVE через `bsp_opto`.
**Параметры стенда (из HIL_BENCH.md):**
```
RLY2 → RS_RX (BSP_OPTO_CH_RS) GPIO1[23]
RLY3 → EXT_IN1 (BSP_OPTO_CH_IN1) GPIO1[22]
RLY4 → EXT_IN2 (BSP_OPTO_CH_IN2) GPIO1[21]
```
### HIL pytest для Этапа 6
Тесты firmware_test через USB CDC — отдельные от существующих HIL ELF тестов:
```
tools/hil/
├── conftest.py ← добавить фикстуру firmware_cdc (USB CDC клиент)
├── 06_test_firmware_can.py ← M5 + USB CDC
├── 06_test_firmware_uart.py
└── 06_test_firmware_opto.py
```
Фикстура `firmware_cdc` открывает CDC порт firmware_test (прошит в Flash),
посылает JSON команды, читает события. Аналог `uart_cmd` для USB CDC.
---
## Этап 7 — Provisioning
### Что нужно
1. Читать `OCOTP_UNIQUE_ID` (или `OCOTP_MAC0/1`) через SDK fsl_ocotp.
2. Посылать `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary`.
3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста.
4. Записывать статус в Flash (первый сектор после прошивки, вне XIP).
### BSP (предварительно)
API уже описан в `firmware_test_plan.md` (закрытое решение):
```c
bsp_sdram_status_t bsp_sdram_init(void);
bsp_sdram_status_t bsp_sdram_test_fast(bsp_sdram_result_t *result);
bsp_sdram_status_t bsp_sdram_test_full(bsp_sdram_result_t *result);
/* bsp/provisioning/include/bsp/provisioning.h */
bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */
```
**Файлы:**
```
bsp/sdram/
├── CMakeLists.txt ← создать
├── include/bsp/sdram.h ← по spec из firmware_test_plan.md
└── src/sdram.c
```
### Открытые вопросы
**Тест модуль firmware_test:**
```
firmware/test/src/tests/test_sdram.c
```
Команды через реестр: `run` + `id: "sdram"`. Отдельной HIL ELF нет.
**HIL pytest (`tools/hil/test_sdram.py`):** подключается к firmware_test через USB CDC.
Фикстура `cdc_firmware_test` — firmware_test прошит в Flash, pytest открывает CDC порт.
**Just рецепты:** `hil-sdram` (без `--slow`), `hil-sdram-full` (с `@pytest.mark.slow`).
- [ ] Что именно записывать как "пройдено": флаг в Flash или только отправить UID?
- [ ] Нужна ли защита от повторного provisioning (write-once)?
---
### Этап 3 — `bsp_qspi` + QSPI тест
## Матрица тестов — итоговая
**Аппаратный контекст:** W25Q64FVSSIG (из схемы, 8MB SPI NOR Flash), интерфейс FlexSPI.
**API:**
```c
bsp_qspi_status_t bsp_qspi_init(void);
bsp_qspi_status_t bsp_qspi_read_jedec_id(uint8_t *manufacturer, uint16_t *device_id);
bsp_qspi_status_t bsp_qspi_test(bsp_qspi_result_t *result); /* erase sector + write + verify */
```
**Что проверяет тест:**
1. JEDEC ID совпадает с ожидаемым для W25Q64 (`0xEF`, `0x4017`)
2. Erase тестового сектора (последний сектор, чтобы не трогать прошивку)
3. Write + Read + Compare 256 байт
**Нет HIL ELF, нет M5.** Тест полностью самостоятельный.
**Вопрос перед началом:** проверить есть ли `sdk_flexspi` таргет в `sdk/CMakeLists.txt`.
---
### Этап 4 — `bsp_usd` + uSD тест
**Аппаратный контекст:** SDMMC (uSD слот), интерфейс USDHC. SDK таргет `sdk_usdhc` есть в матрице.
**Стратегия:** использовать FatFS из SDK middleware (уже vendored в `middleware/fatfs/`).
**API:**
```c
bsp_usd_status_t bsp_usd_init(void); /* USDHC init + mount FAT */
bsp_usd_status_t bsp_usd_test(bsp_usd_result_t *result); /* write + read + verify */
bsp_usd_status_t bsp_usd_deinit(void); /* unmount */
```
**Поведение при отсутствии карты:** `TEST_STATUS_SKIP` (критически важно для
производственного прогона — карта может быть не вставлена).
**Вопрос:** карта вставлена постоянно или оператор вставляет? Если оператор — нужен `confirm_request` перед тестом.
---
### Этап 5 — Display тест (интерактивный)
**Что проверяем:** RGB888 интерфейс + подсветка + реакция оператора через кнопки.
**Последовательность:**
```
firmware → LCD: залить R (красный)
firmware → хост: {"type":"confirm_request","id":"display_red","timeout_ms":15000}
оператор: нажать кнопку PASS (Test_But_1) или FAIL (Test_But_2)
firmware → хост: {"type":"confirm_ack","id":"display_red","confirmed":true/false}
повторить для G, B, W
итоговый результат = AND всех подтверждений
```
**Особенности:**
- Одновременно тестируются кнопки (`bsp_button` уже есть с HIL тестами)
- Таймаут 15 с`TEST_STATUS_SKIP`
- Маркер `@pytest.mark.interactive` в pytest — не входит в `hil-run`
**HIL pytest:** `tools/hil/test_display.py` с `_operator_prompt()` через `/dev/tty`.
---
### Этап 6 — HIL тесты: CAN, UART, Opto в firmware_test
Эти BSP модули уже реализованы и имеют отдельные HIL ELF тесты.
Задача этапа — **интегрировать их в firmware_test** как тест-модули,
запускаемые через протокол v2.
#### 6.1 CAN (`bsp_can` ✅)
- Стенд: M5StampPLC подключён к CAN шине платы через интерфейсную плату
- Тест: M5 посылает CAN фрейм → плата принимает → сравниваем
#### 6.2 UART TTL (`bsp_uart_host` ✅)
- Стенд: M5 UART ↔ UART TTL платы (loopback или echo)
- Уточнить: какой UART порт на плате (LPUART1 занят MCU-Link, какой свободен?)
#### 6.3 UART ISO
- Уточнить наличие отдельного BSP модуля или это конфигурация `bsp_uart_host`
- +24V уровни через интерфейсную плату
#### 6.4 Opto (`bsp_opto` ✅)
- Стенд: M5 реле → оптовходы EXT_IN1, EXT_IN2, RS_RX
- Логика уже отработана в `02_test_opto.py` (HIL ELF)
- Переиспользовать: тот же M5 агент, другой транспорт (USB CDC вместо UART)
**Важно для всех HIL тестов этапа 6:** pytest для firmware_test использует
`cdc_firmware_test` фикстуру (USB CDC), а не `uart_<n>` (UART + pyOCD).
M5StampPLC управляет сигналами так же, как в существующих HIL ELF тестах.
---
### Этап 7 — Provisioning
**Источник UID:** OCOTP регистры через `OCOTP_GetFuseData()` (NXP HAL).
```c
void provisioning_run(provision_info_t *out);
```
**Поток:**
```
firmware → хост: {"type":"provision_ready","chip_uid":"A3F2...","fw":"0.1.0"}
хост → БД: uid ↔ fw_version (логика на хосте)
хост → firmware: {"type":"cmd","cmd":"provision_ack","fw":"1.0.0","bootloader":"1.0.0"}
firmware → хост: {"type":"provision_done","recorded":true}
```
**Запускается только при `overall == pass`.** В pytest отдельная фикстура
`provision_firmware_test`.
| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус |
|-------------|----------------|-------------|----------|----------|------------------|--------|
| — | PING | cmd | — | ❌ | — | ✅ |
| `sdram` | SDRAM 32MB | self | ✅ | ❌ | `bsp_sdram` ✅ | ✅ |
| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash` ✅ | ✅ |
| `usd` | uSD (SDIO) | interactive | ✅ | ❌ | `bsp_usd` ⬜ | ⬜ |
| `display` | Display RGB888 | interactive | ❌ | ❌ | существующий BSP | ⬜ |
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` ✅ | ⬜ |
| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` ✅ | ⬜ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host`✅| ⬜ |
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ |
| `opto` | Opto-in EXT | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ |
---
## Зависимости между этапами
```
Этап 1 (протокол v2 + runner)
├── Этап 2 (bsp_sdram)
│ └── HIL: test_sdram.py
├── Этап 3 (bsp_qspi)
│ └── HIL: test_qspi.py
├── Этап 4 (bsp_usd)
│ └── HIL: test_usd.py
├── Этап 5 (display, interactive)
│ └── HIL: test_display.py (@pytest.mark.interactive)
└── Этап 6 (CAN + UART + Opto)
└── HIL: test_can.py, test_uart.py, test_opto.py
Этап 7 (provisioning) → зависит от всех предыдущих
```
✅ Этап 1 (протокол v2 + runner)
✅ Этап 2 (bsp_sdram + test_sdram)
✅ Этап 3 (bsp_qspi_flash + test_qspi)
⬜ Этап 4 (bsp_usd + test_usd) ← ТЕКУЩИЙ
⬜ Этап 5 (display + buttons)
⬜ Этап 6 (CAN + UART + Opto, HIL)
⬜ Этап 7 (provisioning)
⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7
```
---
## BSP модули — итог
## Хостовое ПО производственного прогона (Этап 8)
| BSP | Статус | Нужен HIL ELF | Нужен в firmware_test |
|---|---|---|---|
| `bsp_usb_cdc` | ✅ | ✅ (есть) | ✅ (есть) |
| `bsp_led` | ✅ | ❌ | ✅ |
| `bsp_tick` | ✅ | ❌ | ✅ |
| `bsp_button` | ✅ | ✅ (есть) | ✅ (display тест) |
| `bsp_opto` | ✅ | ✅ (есть) | ✅ этап 6 |
| `bsp_can` | ✅ | ✅ (есть) | ✅ этап 6 |
| `bsp_uart_host` | ✅ | ✅ (есть) | ✅ этап 6 |
| `bsp_sdram` | ⬜ | ❌ | ✅ этап 2 |
| `bsp_qspi` | ⬜ | ❌ | ✅ этап 3 |
| `bsp_usd` | ⬜ | ❌ | ✅ этап 4 |
**Решение принято (Вариант D):** отдельное приложение `tools/production/`,
без pytest, с TUI (Textual).
---
Подробная архитектура описана в предыдущей версии плана (v0.2, раздел
"Открытый вопрос: ПО на стороне хоста").
## Контекст для нового треда — что передать
```
Системный промпт: тот же (роль + правила).
### Открытые вопросы (перед Этапом 8)
Приложить файлы:
- firmware_test_plan.md (обновлённый)
- usb_cdc.md
- firmware/test/src/main.c (текущий)
- firmware/test/src/cli.h/.c (текущий)
- firmware/test/CMakeLists.txt (текущий)
- tests/host/CMakeLists.txt
- tests/host/cli/test_cli.c
- этот план (PLAN.md)
Первый вопрос нового треда:
"Начинаем Этап 1 — протокол v2 и test_runner.
Ответы на открытые вопросы: [...]"
```
---
## Открытый вопрос: ПО на стороне хоста для производственного прогона
### Контекст и разделение ответственности
Принципиально важно разделить два окружения:
| Окружение | Кто запускает | Инструмент | Что тестирует |
|---|---|---|---|
| **Разработка** | Разработчик, локальный ПК | pytest (`just host::hil-*`) | Отдельные BSP модули через HIL ELF + UART |
| **Производство / Сервис** | Оператор, сервер | ??? | Плата целиком через firmware_test + USB CDC |
Это два **принципиально разных** use case с разными требованиями к UX,
надёжности и изоляции. Смешивать их в одном pytest-прогоне нельзя.
---
### Почему "просто pytest" недостаточно для производства
Производственный прогон отличается от HIL тестов разработчика по нескольким осям:
**Оператор — не разработчик.** Он не читает pytest output в терминале.
Ему нужно видеть: какой тест сейчас идёт, прошёл или нет, что делать дальше
(вставить карту, посмотреть на дисплей, нажать кнопку).
**Последовательность фиксирована.** Прогон всегда идёт по реестру:
flash → READY → run_all → summary → provisioning. Никакого выбора тестов.
**Результат — не лог, а запись в БД.** `chip_uid` + результат + версия прошивки
должны сохраняться. pytest ничего не знает о БД.
**Интерактивные тесты** (display) требуют управляемого диалога с оператором,
а не `input()` в терминале с флагом `-s`.
---
### Варианты хостового ПО — анализ
#### Вариант A: pytest + плагины + HTML отчёт
Обернуть весь прогон в pytest, добавить `pytest-html` для отчётов,
интерактивные тесты через `@pytest.mark.interactive` с `-s`.
**Плюсы:** минимум нового кода, знакомый инструмент.
**Минусы:** оператор смотрит в терминал; интерактивность через `/dev/tty` хрупкая;
provisioning (запись в БД) — костыль в фикстуре; нет живого статуса
"тест N из M идёт X секунд". Для производства неприемлемо.
#### Вариант B: TUI — Python + Textual / Rich
Самостоятельное Python-приложение с текстовым интерфейсом.
Управляет всем: flash через spsdk, M5StampPLC, USB CDC, provisioning, БД.
pytest не используется как runner — только как библиотека для assert-логики
(или вообще не используется).
**Плюсы:** полный контроль над UX; живой прогресс; чёткие диалоги оператора;
нативная запись в БД; изолировано от HIL тестов разработчика полностью.
**Минусы:** значительный объём разработки хостового ПО;
нужен отдельный репозиторий или директория `tools/production/`.
#### Вариант C: pytest как backend, TUI/GUI как frontend
pytest запускается программно через `pytest.main()` или subprocess,
результаты передаются через JSON reporter (`pytest-json-report`) в
отдельное GUI/TUI приложение которое их отображает.
**Плюсы:** переиспользуем pytest инфраструктуру (фикстуры, параллелизм).
**Минусы:** архитектурно сложно; интерактивные тесты всё равно требуют
кастомного канала; два процесса вместо одного.
#### Вариант D: pytest только для разработки, отдельный runner для производства
**Разработка:** pytest (`just host::hil-*`) — тесты отдельных BSP модулей
через HIL ELF. Остаётся как есть.
**Производство:** отдельное Python-приложение (`tools/production/run.py`)
без pytest. Использует те же низкоуровневые библиотеки
(`pyserial`, `spsdk`, M5 агент) но со своим runner-ом и TUI.
Тест-логика на стороне прошивки (в `test_runner.c`) — единственная
точка истины. Хостовое ПО только отправляет команды и интерпретирует события.
**Это рекомендуемый вариант** — наиболее чистое разделение.
---
### Рекомендуемая архитектура (Вариант D, детально)
```
tools/
├── hil/ # СУЩЕСТВУЮЩИЙ — разработчик, локальный ПК
│ ├── conftest.py # pytest фикстуры
│ ├── 01_test_uart.py # HIL тесты отдельных BSP модулей
│ ├── 02_test_opto.py
│ └── ...
└── production/ # НОВЫЙ — производство / сервис
├── pyproject.toml # отдельное окружение uv
├── run.py # точка входа: python run.py
├── runner/
│ ├── flasher.py # spsdk: flash firmware_test.elf
│ ├── cdc_client.py # USB CDC: send cmd, recv events
│ ├── m5_client.py # переиспользовать tools/hil/m5/
│ ├── test_sequence.py # порядок: flash→READY→run_all→provision
│ └── db.py # запись chip_uid + результатов
└── ui/
├── tui.py # Textual TUI: живой прогресс + диалоги
└── report.py # HTML / JSON отчёт после прогона
```
**Поток производственного прогона:**
```
Оператор запускает: python tools/production/run.py
├── flasher.py: spsdk → flash firmware_test.elf → Reset
├── cdc_client: ждёт {"type":"session_start",...}
├── TUI: показывает "Подключение... ОК"
├── cdc_client: → {"type":"cmd","cmd":"run_all"}
├── [цикл событий]
│ ├── {"type":"test_begin","id":"sdram"} → TUI: "SDRAM... ⏳"
│ ├── {"type":"test_result","id":"sdram","status":"pass"} → TUI: "SDRAM ✅ 47ms"
│ ├── {"type":"confirm_request","id":"display_red"} →
│ │ TUI: диалог оператора "Экран красный? [PASS/FAIL]"
│ │ cdc_client: → {"type":"confirm","id":"display_red","confirmed":true}
│ └── ...
├── {"type":"summary","overall":"pass"} → TUI: финальный результат
├── {"type":"provision_ready","chip_uid":"..."} →
│ db.py: записать в БД
│ cdc_client: → {"type":"cmd","cmd":"provision_ack",...}
└── TUI: "Плата принята ✅ | UID: A3F2..." | печать этикетки?
```
---
### Изоляция от HIL тестов разработчика
| Аспект | HIL тесты (разработка) | Production runner |
|---|---|---|
| Инструмент | pytest | Самостоятельный Python runner |
| Директория | `tools/hil/` | `tools/production/` |
| Окружение uv | `tools/hil/pyproject.toml` | `tools/production/pyproject.toml` |
| Just рецепты | `just host::hil-*` | `just host::production-run` |
| Прошивка | HIL ELF в RAM (pyOCD) | firmware_test в Flash (spsdk) |
| Транспорт | UART CLI (pyserial) | USB CDC JSON-lines |
| UX | Терминал / pytest output | TUI (Textual) |
| БД | Нет | Да |
| M5StampPLC | Да (реле для сигналов) | Да (те же реле) |
| CI | Да (автоматический) | Нет |
Общий код (M5 агент, низкоуровневый spsdk wrapper) можно вынести в
`tools/shared/` и подключать как локальный пакет в обоих `pyproject.toml`.
---
### Открытые вопросы для обсуждения
- [ ] **TUI библиотека:** Textual (современный, богатый) или Rich (проще, достаточно)?
Или вообще минималистичный вывод без TUI фреймворка на первой итерации?
- [ ] **БД:** SQLite локально на сервере или REST API на внешний сервис?
- [ ] **Этикетка:** нужна ли автоматическая печать после provisioning?
- [ ] **Несколько стендов:** один сервер управляет несколькими платами параллельно
или всегда одна плата?
- [ ] **Первая итерация:** допустимо ли начать с простого `run.py` без TUI
(plain print + input) и добавить TUI позже?
- [ ] TUI: Textual или Rich или plain print на первой итерации?
- [ ] БД: SQLite локально или REST API?
- [ ] Несколько стендов параллельно или всегда один?
- [ ] Этикетка: нужна ли автоматическая печать после provisioning?

View file

@ -0,0 +1,29 @@
# firmware_test_fatfs — FatFS, скомпилированный с bare-metal ffconf.h.
#
# ff.c, fsl_sd_disk.c, diskio_sd.c включают ff.h → ffconf.h. Все три
# компилируются здесь, чтобы видели один и тот же ffconf.h из
# ${CMAKE_CURRENT_SOURCE_DIR}/include.
#
# tft_app заведёт свой аналогичный таргет с FreeRTOS ffconf.h — эти два таргета
# никак не пересекаются.
add_library(
firmware_test_fatfs STATIC
${SDK_FATFS_FF_SRC} # sdk/middleware/fatfs/source/ff.c
${SDK_FATFS_SD_DISK_SRC} # sdk/middleware/fatfs/source/fsl_sd_disk/fsl_sd_disk.c
${PORT_FATFS_SD_SRC} # port/fatfs/sd/src/diskio_sd.c
src/diskio.c)
# include/ первым — ffconf.h отсюда должен перекрыть любой шаблонный
target_include_directories(
firmware_test_fatfs
PUBLIC include # ffconf.h, виден потребителям (bsp_usd, test_usd)
PRIVATE src)
target_link_libraries(
firmware_test_fatfs
PUBLIC port_fatfs_sd # diskio_sd.h, ff.h, fsl_sd_disk.h, bsp_sd
PRIVATE bsp_sdmmc_config) # SD_ENABLED транзитивно из bsp_sd, но явно для
# ясности
target_compile_options(firmware_test_fatfs PRIVATE -w)

View file

@ -0,0 +1,78 @@
/*
* ffconf.h конфигурация FatFS для firmware_test (bare-metal, только SD).
*
* Ключевые отличия от tft_app:
* FF_FS_REENTRANT = 0 нет RTOS, нет мьютексов
* FF_VOLUMES = 3 0: зарезервирован, 1: зарезервирован, 2: SD
* FF_MAX_SS = 512 SD всегда 512 байт/сектор, ioctl не нужен
*/
#ifndef _FFCONF_H_
#define _FFCONF_H_
#define FFCONF_DEF 80286
/*---------------------------------------------------------------------------/
/ MSDK adaptation
/---------------------------------------------------------------------------*/
#define SD_DISK_ENABLE 1
/*---------------------------------------------------------------------------/
/ Function Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_READONLY 0
#define FF_FS_MINIMIZE 0
#define FF_USE_FIND 0
#define FF_USE_MKFS 0 /* f_mkfs не нужна — карта уже отформатирована */
#define FF_USE_FASTSEEK 0
#define FF_USE_EXPAND 0
#define FF_USE_CHMOD 0
#define FF_USE_LABEL 0
#define FF_USE_FORWARD 0
#define FF_USE_STRFUNC 0
#define FF_PRINT_LLI 0
#define FF_PRINT_FLOAT 0
#define FF_STRF_ENCODE 3
/*---------------------------------------------------------------------------/
/ Locale
/---------------------------------------------------------------------------*/
#define FF_CODE_PAGE 437 /* U.S. — минимальный, имена файлов ASCII */
#define FF_USE_LFN 0 /* только 8.3 — достаточно для FWTEST.TMP */
#define FF_MAX_LFN 255
#define FF_LFN_UNICODE 0
#define FF_LFN_BUF 255
#define FF_SFN_BUF 12
#define FF_FS_RPATH 0 /* относительные пути не нужны */
/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/---------------------------------------------------------------------------*/
#define FF_VOLUMES 3 /* 0: зарезервирован, 1: зарезервирован, 2: SD */
#define FF_STR_VOLUME_ID 0
#define FF_MULTI_PARTITION 0
#define FF_MIN_SS 512
#define FF_MAX_SS 512 /* SD: всегда 512, GET_SECTOR_SIZE не нужен */
#define FF_LBA64 0
#define FF_MIN_GPT 0x10000000
#define FF_USE_TRIM 0
/*---------------------------------------------------------------------------/
/ System Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_TINY 0
#define FF_FS_EXFAT 0 /* exFAT требует LFN — оба отключены */
#define FF_FS_NORTC 1
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY 1
#define FF_NORTC_YEAR 2024
#define FF_FS_NOFSINFO 0
#define FF_FS_LOCK 0
#define FF_FS_REENTRANT 0 /* bare-metal: нет RTOS, нет мьютексов */
/* FF_FS_TIMEOUT и FF_SYNC_t не нужны */
#endif /* _FFCONF_H_ */

View file

@ -0,0 +1,62 @@
/*
* diskio.c FatFS diskio диспетчер для firmware_test.
*
* FF_VOLUMES=1: единственный диск "0:/" = SDDISK.
* W25Q и RAM-диск отсутствуют нет зависимости на bsp_qspi_flash.
*/
#include "diskio.h"
#include "port/fatfs/diskio_sd.h"
#define SDDISK 2U
DSTATUS disk_initialize(BYTE pdrv)
{
if (pdrv == SDDISK)
{
return microsd_disk_initialize(pdrv);
}
return STA_NOINIT;
}
DSTATUS disk_status(BYTE pdrv)
{
if (pdrv == SDDISK)
{
return microsd_disk_status(pdrv);
}
return STA_NOINIT;
}
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
if (pdrv == SDDISK)
{
return microsd_disk_read(pdrv, buff, sector, count);
}
return RES_PARERR;
}
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
if (pdrv == SDDISK)
{
return microsd_disk_write(pdrv, buff, sector, count);
}
return RES_PARERR;
}
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
if (pdrv == SDDISK)
{
return microsd_disk_ioctl(pdrv, cmd, buff);
}
return RES_PARERR;
}

View file

@ -19,9 +19,11 @@
*/
#include "board.h"
#include "bsp/led.h"
#include "bsp/sd.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "cli.h"
#include "ff.h"
#include "protocol.h"
#include "test_runner.h"
@ -51,6 +53,7 @@ int main(void)
bsp_delay(CONNECT_BLINK_MS);
}
#if 0
bsp_led_on(LED_APP);
cli_init();
@ -62,4 +65,55 @@ int main(void)
cli_process();
test_runner_process();
}
#endif
static FATFS s_fs;
const char *step = "card_detect";
bool passed = false;
if (!bsp_sd_is_inserted())
{
/* нет карты — пропускаем, это не ошибка стека */
bsp_usb_cdc_write((const uint8_t *) "The card is not inserted.\r\n", 27);
goto sd_smoke_done;
}
step = "sd_init";
if (bsp_sd_init() != BSP_OK)
{
goto sd_smoke_fail;
}
step = "f_mount";
FRESULT res = f_mount(&s_fs, "2:/", 1);
if (res != FR_OK)
{
bsp_sd_deinit();
goto sd_smoke_fail;
}
/* всё ок */
res = f_unmount("2:/");
if (res != FR_OK)
{
bsp_sd_deinit();
goto sd_smoke_fail;
}
bsp_sd_deinit();
passed = true;
bsp_usb_cdc_write((const uint8_t *) "The test passed.\r\n", 18);
goto sd_smoke_done;
sd_smoke_fail:
{
/* маленький буфер: step не длиннее 16 символов */
bsp_usb_cdc_write((const uint8_t *) "The test failed.\r\n", 18);
}
(void) passed;
sd_smoke_done:
while (1)
{
bsp_usb_cdc_poll();
}
}

View file

@ -2,3 +2,4 @@ if(BUILD_TESTS_HOST)
return()
endif()
add_subdirectory(log)
add_subdirectory(fatfs)

View file

@ -39,8 +39,8 @@ port/
├── CMakeLists.txt
├── README.md ← этот файл
└── log/ ← UART-адаптер для utils/log
├── fatfs/ ← diskio поверх bsp_sd / bsp_qspi
# Планируется:
├── fatfs/ ← diskio поверх bsp_sdio / bsp_qspi
└── freertos/ ← heap_4.c, configASSERT, vApplicationHooks
```
@ -49,7 +49,7 @@ port/
## Соглашения
**Именование таргетов:** `port_<что>_<транспорт>` — например `port_log_uart`,
`port_fatfs_sdio`. Позволяет иметь несколько адаптеров для одной библиотеки.
`port_fatfs_sd`. Позволяет иметь несколько адаптеров для одной библиотеки.
**Include-путь:** `#include "port/<модуль>.h"` — публичные заголовки
всегда в `port/<модуль>/include/port/`.

View file

@ -0,0 +1 @@
add_subdirectory(sd)

View file

@ -0,0 +1,19 @@
# port_fatfs_sd — INTERFACE: не компилирует diskio_sd.c сам.
#
# Причина: diskio_sd.c включает diskio.h → ff.h → ffconf.h, который разный у
# firmware_test (bare-metal) и tft_app (FreeRTOS). Каждый бинарник компилирует
# diskio_sd.c в свой FatFS-таргет сам, используя PORT_FATFS_SD_SRC.
add_library(port_fatfs_sd INTERFACE)
target_include_directories(port_fatfs_sd INTERFACE include)
target_link_libraries(
port_fatfs_sd
INTERFACE sdk_fatfs_headers # ff.h, fsl_sd_disk.h + SDK_FATFS_*_SRC
bsp_sd) # g_sd, bsp_sd_is_inserted()
set(PORT_FATFS_SD_SRC
${CMAKE_CURRENT_SOURCE_DIR}/src/diskio_sd.c
CACHE INTERNAL
"port_fatfs_sd: путь к diskio_sd.c (компилировать per-binary)")

View file

@ -0,0 +1,20 @@
/*
* diskio_sd реализация FatFS diskio-интерфейса для microSD через fsl_sd_disk.
*
* Функции microsd_disk_* вызываются диспетчером diskio.c каждого бинарника.
* Никогда не вызывать напрямую из прикладного кода.
*/
#ifndef PORT_FATFS_DISKIO_SD_H
#define PORT_FATFS_DISKIO_SD_H
#include "diskio.h" /* DSTATUS, DRESULT, BYTE, LBA_t, UINT */
#include "ff.h"
DSTATUS microsd_disk_initialize(BYTE pdrv);
DSTATUS microsd_disk_status(BYTE pdrv);
DRESULT microsd_disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT microsd_disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count);
DRESULT microsd_disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);
#endif /* PORT_FATFS_DISKIO_SD_H */

View file

@ -0,0 +1,37 @@
/*
* diskio_sd тонкая обёртка fsl_sd_disk FatFS diskio для microSD.
*
* fsl_sd_disk.c из SDK уже реализует sd_disk_* поверх fsl_sd / USDHC.
* Наша задача только пробросить вызовы и изолировать номер диска:
* sd_disk_* всегда работают с диском 0 внутри себя, pdrv мы передаём
* для соблюдения сигнатуры, но fsl_sd_disk игнорирует его значение.
*/
#include "port/fatfs/diskio_sd.h"
#include "fsl_sd_disk.h"
DSTATUS microsd_disk_initialize(BYTE pdrv)
{
return sd_disk_initialize(pdrv);
}
DSTATUS microsd_disk_status(BYTE pdrv)
{
return sd_disk_status(pdrv);
}
DRESULT microsd_disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
return sd_disk_read(pdrv, buff, sector, count);
}
DRESULT microsd_disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
return sd_disk_write(pdrv, buff, sector, count);
}
DRESULT microsd_disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
return sd_disk_ioctl(pdrv, cmd, buff);
}

300
project_tree.txt Normal file
View file

@ -0,0 +1,300 @@
.
├── bootstrap.sh
├── bsp
│   ├── button
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── can
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── mocks
│   │   ├── README.md
│   │   └── src
│   ├── CMakeLists.txt
│   ├── common
│   │   ├── CMakeLists.txt
│   │   └── include
│   ├── display
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── generated
│   │   ├── board
│   │   ├── board.c
│   │   ├── board.h
│   │   ├── clock_config.c
│   │   ├── clock_config.h
│   │   ├── pin_mux.c
│   │   ├── pin_mux.h
│   │   ├── README.md
│   │   ├── startup
│   │   ├── syscalls.c
│   │   └── TFT_Board.mex
│   ├── led
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── opto
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── qspi_flash
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   ├── REFACTORING.md
│   │   └── src
│   ├── README.md
│   ├── sdram
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── tick
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── uart_host
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── mocks
│   │   ├── README.md
│   │   └── src
│   └── usb_cdc
│   ├── CMakeLists.txt
│   ├── include
│   ├── README.md
│   └── src
├── cmake
│   ├── linker
│   │   ├── MIMXRT1052xxxxx_flexspi_nor_sdram.ld
│   │   ├── MIMXRT1052xxxxx_flexspi_nor.ld
│   │   ├── MIMXRT1052xxxxx_ram.ld
│   │   ├── MIMXRT1052xxxxx_sdram_txt.ld
│   │   └── MIMXRT1052xxxxx_sdram.ld
│   ├── toolchain_arm.cmake
│   └── toolchain_host.cmake
├── CMakeLists.txt
├── CMakePresets.json
├── docs
│   ├── DEV_ARCH.md
│   ├── hardware
│   │   ├── board
│   │   ├── m5stamPLC
│   │   ├── MCULINKINSERT.pdf
│   │   └── tft_displays
│   ├── HOW_TO_DEBUG.md
│   ├── HOW_TO_FLASH.md
│   ├── mimxrt1052
│   │   ├── BOOT_FLAGS.md
│   │   ├── HAB_GUIDE.md
│   │   ├── manufacturing_user's_guide.pdf
│   │   └── mcu_rm.pdf
│   └── testing
│   ├── hil
│   ├── host
│   └── PROTOCOL.md
├── firmware
│   ├── bootloader
│   ├── test
│   │   ├── CMakeLists.txt
│   │   ├── PLAN.md
│   │   ├── README.md
│   │   └── src
│   └── tft_app
├── just
│   ├── build.just
│   ├── ci_workflow.md
│   ├── ci.just
│   └── host.just
├── Justfile
├── lib
│   ├── CMakeLists.txt
│   ├── fff
│   │   ├── buildandtest
│   │   ├── CMakeLists.txt
│   │   ├── examples
│   │   ├── fakegen.rb
│   │   ├── fff.h
│   │   ├── LICENSE
│   │   ├── README.md
│   │   └── test
│   ├── SEGGER
│   │   ├── RTT
│   │   └── wrapper
│   └── Unity
│   ├── auto
│   ├── CMakeLists.txt
│   ├── docs
│   ├── examples
│   ├── extras
│   ├── library.json
│   ├── LICENSE.txt
│   ├── meson_options.txt
│   ├── meson.build
│   ├── platformio-build.py
│   ├── README.md
│   ├── src
│   ├── test
│   └── unityConfig.cmake
├── port
│   ├── CMakeLists.txt
│   ├── log
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   └── README.md
├── project_tree.txt
├── pyocd_debug.yaml
├── pyocd.yaml
├── README.md
├── sdk
│   ├── boards
│   │   └── evkbimxrt1050
│   ├── CMakeLists.txt
│   ├── CMSIS
│   │   ├── Core
│   │   ├── Driver
│   │   ├── DSP
│   │   ├── LICENSE.txt
│   │   ├── NN
│   │   └── RTOS2
│   ├── components
│   │   ├── audio
│   │   ├── button
│   │   ├── codec
│   │   ├── common_task
│   │   ├── crc
│   │   ├── display
│   │   ├── exception_handling
│   │   ├── flash
│   │   ├── gpio
│   │   ├── i2c
│   │   ├── internal_flash
│   │   ├── led
│   │   ├── lists
│   │   ├── log
│   │   ├── mem_manager
│   │   ├── messaging
│   │   ├── mx25r_flash
│   │   ├── osa
│   │   ├── panic
│   │   ├── phy
│   │   ├── pmic
│   │   ├── reset
│   │   ├── rng
│   │   ├── rtt
│   │   ├── sensor
│   │   ├── serial_manager
│   │   ├── silicon_id
│   │   ├── spi
│   │   ├── timer
│   │   ├── timer_manager
│   │   ├── touch
│   │   ├── uart
│   │   ├── video
│   │   └── wifi_bt_module
│   ├── COPYING-BSD-3
│   ├── devices
│   │   └── MIMXRT1052
│   ├── docs
│   │   └── readme.md
│   ├── LA_OPT_NXP_Software_License.txt
│   ├── middleware
│   │   ├── audio_voice
│   │   ├── cjson
│   │   ├── dhara
│   │   ├── fatfs
│   │   ├── freemaster
│   │   ├── libjpeg
│   │   ├── littlefs
│   │   ├── mcuboot_opensource
│   │   ├── pkcs11
│   │   ├── pngdec
│   │   ├── sdmmc
│   │   ├── tfm
│   │   ├── tinycbor
│   │   └── usb
│   ├── MIMXRT1052xxxxB_manifest_v3_15.xml
│   ├── README.md
│   ├── rtos
│   │   └── freertos
│   ├── sdk_tree.txt
│   ├── SW-Content-Register.txt
│   └── tools
│   └── cmake_toolchain_files
├── tests
│   ├── CMakeLists.txt
│   ├── host
│   │   ├── button
│   │   ├── can
│   │   ├── cli
│   │   ├── CMakeLists.txt
│   │   ├── led
│   │   ├── log
│   │   ├── mocks
│   │   ├── opto
│   │   ├── prio_queue
│   │   ├── protocol
│   │   ├── README.md
│   │   ├── ring_buffer
│   │   ├── runner
│   │   ├── timeout
│   │   └── uart_host
│   └── target
│   ├── CMakeLists.txt
│   ├── hil_button
│   ├── hil_can
│   ├── hil_opto
│   ├── hil_usb_cdc
│   └── host_uart
├── tools
│   ├── hil
│   │   ├── __pycache__
│   │   ├── 01_test_uart.py
│   │   ├── 02_test_opto.py
│   │   ├── 03_test_can.py
│   │   ├── 04_test_button.py
│   │   ├── 05_test_usb_cdc.py
│   │   ├── conftest.py
│   │   ├── env_config.py
│   │   ├── load_and_run.py
│   │   ├── m5
│   │   ├── pyocd_utils.py
│   │   ├── pyproject.toml
│   │   ├── README.md
│   │   └── uv.lock
│   ├── host
│   │   ├── dcd
│   │   ├── flash_swd.py
│   │   ├── flash_usb.py
│   │   ├── hab
│   │   ├── pyproject.toml
│   │   ├── README.md
│   │   └── uv.lock
│   ├── production
│   └── shared
└── utils
├── CMakeLists.txt
├── log
│   ├── log.c
│   ├── log.h
│   └── README.md
├── prio_queue
│   ├── prio_queue.c
│   ├── prio_queue.h
│   └── README.md
├── README.md
└── ring_buffer
├── README.md
├── ring_buffer.c
└── ring_buffer.h
174 directories, 124 files

View file

@ -33,7 +33,12 @@ target_compile_definitions(sdk_device PUBLIC CPU_MIMXRT1052CVJ5B)
# -----------------------------------------------------------------------------
# Утилиты — базовые вещи, нужны большинству драйверов
# -----------------------------------------------------------------------------
add_sdk_driver(common fsl_common.c)
add_library(sdk_common STATIC devices/MIMXRT1052/drivers/fsl_common.c
devices/MIMXRT1052/drivers/fsl_common_arm.c)
target_include_directories(sdk_common PUBLIC devices/MIMXRT1052/drivers)
target_compile_definitions(
sdk_common PUBLIC SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY=600000000U)
target_link_libraries(sdk_common PUBLIC sdk_device)
add_sdk_driver(clock fsl_clock.c)
# clock зависит от common
@ -50,6 +55,7 @@ add_sdk_driver(lpi2c fsl_lpi2c.c)
add_sdk_driver(pwm fsl_pwm.c)
add_sdk_driver(adc fsl_adc.c)
add_sdk_driver(elcdif fsl_elcdif.c)
add_sdk_driver(cache fsl_cache.c)
# Драйверы которые зависят от clock
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
@ -101,15 +107,67 @@ target_link_libraries(
sdk_usb_config sdk_osa_bm)
target_compile_options(sdk_usb_device_ehci PRIVATE -w)
# --- sdk_usb_device_ch9: Chapter 9 standard requests ---
# add_library(sdk_usb_device_ch9 STATIC middleware/usb/device/usb_device_ch9.c)
# target_include_directories( sdk_usb_device_ch9 PUBLIC middleware/usb/device
# middleware/usb/device/class) target_link_libraries(sdk_usb_device_ch9 PUBLIC
# sdk_usb_device_ehci) target_compile_options(sdk_usb_device_ch9 PRIVATE -w)
# --- sdk_usb_phy: USB PHY ---
add_library(sdk_usb_phy STATIC middleware/usb/phy/usb_phy.c)
target_include_directories(sdk_usb_phy PUBLIC middleware/usb/phy)
target_link_libraries(sdk_usb_phy PUBLIC sdk_device sdk_common sdk_usb_common
sdk_usb_config sdk_osa_bm)
target_compile_options(sdk_usb_phy PRIVATE -w)
# =============================================================================
# SDMMC middleware — SD-карта через USDHC, non-blocking (interrupt-driven).
#
# НЕ зависит от ffconf.h → безопасно компилировать один раз как общую
# статическую библиотеку. Оба бинарника (firmware_test, tft_app) линкуются к
# одному и тому же sdk_sdmmc_sd.
#
# =============================================================================
add_library(
sdk_sdmmc_sd STATIC
middleware/sdmmc/sd/fsl_sd.c middleware/sdmmc/common/fsl_sdmmc_common.c
middleware/sdmmc/osa/fsl_sdmmc_osa.c
middleware/sdmmc/host/usdhc/non_blocking/fsl_sdmmc_host.c)
target_include_directories(
sdk_sdmmc_sd
PUBLIC middleware/sdmmc/sd middleware/sdmmc/common middleware/sdmmc/osa
middleware/sdmmc/host/usdhc/non_blocking middleware/sdmmc/host/usdhc)
target_link_libraries(sdk_sdmmc_sd PUBLIC sdk_usdhc sdk_osa_bm sdk_cache
sdk_common)
# Подавляем предупреждения SDK — они нам не принадлежат.
target_compile_options(sdk_sdmmc_sd PRIVATE -w)
# =============================================================================
# FatFS — только заголовки и переменные с путями к исходникам.
#
# ff.c и fsl_sd_disk.c НЕ компилируются здесь, потому что оба включают ff.h →
# ffconf.h, который разный у firmware_test (bare-metal) и tft_app (FreeRTOS).
# Каждый бинарник сам добавляет их в свои sources.
#
# Использование в CMakeLists.txt бинарника:
# target_link_libraries(my_fatfs_target PRIVATE sdk_fatfs_headers)
# target_sources(my_fatfs_target PRIVATE ${SDK_FATFS_FF_SRC}
# ${SDK_FATFS_SD_DISK_SRC})
# =============================================================================
add_library(sdk_fatfs_headers INTERFACE)
target_include_directories(
sdk_fatfs_headers INTERFACE middleware/fatfs/source
middleware/fatfs/source/fsl_sd_disk)
# Транзитивно тащим SDMMC: fsl_sd_disk.c вызывает SD_*() из sdk_sdmmc_sd.
target_link_libraries(sdk_fatfs_headers INTERFACE sdk_sdmmc_sd)
# Пути к исходникам, которые должны компилироваться per-binary. CACHE INTERNAL —
# видны во всём дереве, не переопределяются.
set(SDK_FATFS_FF_SRC
${CMAKE_CURRENT_SOURCE_DIR}/middleware/fatfs/source/ff.c
CACHE INTERNAL
"FatFS: путь к ff.c (компилировать per-binary с нужным ffconf.h)")
set(SDK_FATFS_SD_DISK_SRC
${CMAKE_CURRENT_SOURCE_DIR}/middleware/fatfs/source/fsl_sd_disk/fsl_sd_disk.c
CACHE
INTERNAL
"FatFS: путь к fsl_sd_disk.c (компилировать per-binary с нужным ffconf.h)"
)

View file

@ -5,8 +5,10 @@
#ifndef _DISKIO_DEFINED
#define _DISKIO_DEFINED
#include "ff.h"
#ifdef __cplusplus
extern "C" {
extern "C"
{
#endif
/* Definitions of physical drive number for each drive */
@ -17,37 +19,34 @@ extern "C" {
#define SDSPIDISK 4 /* sdspi disk to physical drive 4 */
#define NANDDISK 5 /* nand disk to physical drive 5 */
/* Status of Disk Functions */
typedef BYTE DSTATUS;
/* Status of Disk Functions */
typedef BYTE DSTATUS;
/* Results of Disk Functions */
typedef enum {
/* Results of Disk Functions */
typedef enum
{
RES_OK = 0, /* 0: Successful */
RES_ERROR, /* 1: R/W Error */
RES_WRPRT, /* 2: Write Protected */
RES_NOTRDY, /* 3: Not Ready */
RES_PARERR /* 4: Invalid Parameter */
} DRESULT;
} DRESULT;
/*---------------------------------------*/
/* Prototypes for disk control functions */
/*---------------------------------------*/
/* Prototypes for disk control functions */
DSTATUS disk_initialize(BYTE pdrv);
DSTATUS disk_status(BYTE pdrv);
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
/* Disk Status Bits (DSTATUS) */
/* Disk Status Bits (DSTATUS) */
#define STA_NOINIT 0x01 /* Drive not initialized */
#define STA_NODISK 0x02 /* No medium in the drive */
#define STA_PROTECT 0x04 /* Write protected */
/* Command code for disk_ioctrl fucntion */
/* Generic command (Used by FatFs) */
@ -55,7 +54,8 @@ DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
#define GET_SECTOR_COUNT 1 /* Get media size (needed at FF_USE_MKFS == 1) */
#define GET_SECTOR_SIZE 2 /* Get sector size (needed at FF_MAX_SS != FF_MIN_SS) */
#define GET_BLOCK_SIZE 3 /* Get erase block size (needed at FF_USE_MKFS == 1) */
#define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at FF_USE_TRIM == 1) */
#define CTRL_TRIM \
4 /* Inform device that the data on the block of sectors is no longer used (needed at FF_USE_TRIM == 1) */
/* Generic command (Not used by FatFs) */
#define CTRL_POWER 5 /* Get/Set power status */

1738
sdk/sdk_tree.txt Normal file

File diff suppressed because it is too large Load diff