diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index caef660..9b8aca7 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(sdram) add_subdirectory(qspi_flash) add_subdirectory(sd) add_subdirectory(mqs) +add_subdirectory(provisioning) # Подавляем предупреждения при компиляции собственных .c файлов библиотеки target_compile_options(bsp_board PRIVATE -w) diff --git a/bsp/README.md b/bsp/README.md index 7594132..444b006 100644 --- a/bsp/README.md +++ b/bsp/README.md @@ -45,12 +45,15 @@ bsp/ │ └── mocks/ # fff-заглушки для host-тестов ├── opto/ # bsp_opto — оптоизолированные входы PS2801-4 ├── can/ # bsp_can — FlexCAN2 (трансивер SN65HVD230D) +│ └── mocks/ # fff-заглушки для host-тестов ├── button/ # bsp_button — тактовые кнопки SWT6x6 с debounce ├── display/ # bsp_display — TFT-дисплей ├── usb_cdc/ # bsp_usb_cdc — USB CDC ACM ├── sdram/ # bsp_sdram — внешний SDRAM через SEMC ├── qspi_flash/ # bsp_qspi_flash — QSPI Flash W25Q64/128/256/512 -└── sd/ # bsp_sd — SD host-контроллер (USDHC1) +├── sd/ # bsp_sd — SD host-контроллер (USDHC1) +├── mqs/ # bsp_mqs — MQS аудио-выход (SAI3, MQS, eDMA + управление громкостью) +└── provisioning/ # bsp_provisioning — OCOTP UID (уникальный идентификатор чипа) ``` --- @@ -84,19 +87,21 @@ target_link_libraries(test_hil_opto PRIVATE bsp_board bsp_boot_ram ...) ### Компоненты периферии -| Библиотека | Модуль | README | -| ---------------- | ------------- | -------------------------------------------- | -| `bsp_led` | `led/` | [led/README.md](led/README.md) | -| `bsp_tick` | `tick/` | [tick/README.md](tick/README.md) | -| `bsp_uart_host` | `uart_host/` | [uart_host/README.md](uart_host/README.md) | -| `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) | -| `bsp_can` | `can/` | [can/README.md](can/README.md) | -| `bsp_button` | `button/` | [button/README.md](button/README.md) | -| `bsp_display` | `display/` | [display/README.md](display/README.md) | -| `bsp_usb_cdc` | `usb_cdc/` | [usb_cdc/README.md](usb_cdc/README.md) | -| `bsp_sdram` | `sdram/` | [sdram/README.md](sdram/README.md) | -| `bsp_qspi_flash` | `qspi_flash/` | [qspi_flash/README.md](qspi_flash/README.md) | -| `bsp_sd` | `sd/` | [sd/README.md](sd/README.md) | +| Библиотека | Модуль | README | +| ------------------ | --------------- | ------------------------------------------------ | +| `bsp_led` | `led/` | [led/README.md](led/README.md) | +| `bsp_tick` | `tick/` | [tick/README.md](tick/README.md) | +| `bsp_uart_host` | `uart_host/` | [uart_host/README.md](uart_host/README.md) | +| `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) | +| `bsp_can` | `can/` | [can/README.md](can/README.md) | +| `bsp_button` | `button/` | [button/README.md](button/README.md) | +| `bsp_display` | `display/` | [display/README.md](display/README.md) | +| `bsp_usb_cdc` | `usb_cdc/` | [usb_cdc/README.md](usb_cdc/README.md) | +| `bsp_sdram` | `sdram/` | [sdram/README.md](sdram/README.md) | +| `bsp_qspi_flash` | `qspi_flash/` | [qspi_flash/README.md](qspi_flash/README.md) | +| `bsp_sd` | `sd/` | [sd/README.md](sd/README.md) | +| `bsp_mqs` | `mqs/` | [mqs/README.md](mqs/README.md) | +| `bsp_provisioning` | `provisioning/` | [provisioning/README.md](provisioning/README.md) | --- diff --git a/bsp/mqs/README.md b/bsp/mqs/README.md index e69de29..a0074a6 100644 --- a/bsp/mqs/README.md +++ b/bsp/mqs/README.md @@ -0,0 +1,263 @@ +# bsp_mqs — MQS аудио-выход (SAI3 + eDMA + LM4875M) + +Воспроизведение PCM16 аудио через MQS-периферию MIMXRT1052CVJ5B. +Физический выход — один канал (`MQS_RIGHT`, `GPIO_AD_B0_04`); SAI3 требует +стерео-буфер — оба канала буфера всегда идентичны. +Управление коэффициентом усиления — FlexPWM4 SM0 PWM_A (`GPIO_AD_B1_08`) +через RC-фильтр и буферный ОУ LM358 на вход `DC_VOL` усилителя LM4875M. + +--- + +## Аппаратура + +### MQS / SAI3 + +| Сигнал | Пин MCU | Корпус | Интерфейс | Примечание | +| --------- | ------------- | ------ | --------- | ----------------------------------------------------- | +| MQS_RIGHT | GPIO_AD_B0_04 | F11 | MQS RIGHT | Выход SAI3 TX, мультиплексирован в `BOARD_InitPins()` | + +### Усилитель LM4875M + +| Сигнал | Пин MCU | Корпус | Интерфейс | Примечание | +| ------------- | ------------- | ------ | ----------- | ------------------------------------------ | +| VolumeControl | GPIO_AD_B1_08 | H13 | PWM4 A, SM0 | Управление DC_VOL усилителя через ОУ LM358 | + +**Сигнальный тракт:** + +```bash +MQS_RIGHT → R53+C97 → R54+C98 → R55+C99 → SOUND_OUT → C100 → LM4875M -Vin +GPIO_AD_B1_08 (PWM4_A) → R56 (1K) → LM358 +IN1 → R57 (1K) → LM4875M DC_VOL +``` + +RC-фильтр на сигнальном пути: три каскада R=2.4 кОм, C=0.1 мкФ (τ = 240 мкс каждый). +RC-фильтр на DC_VOL: R56 (1 кОм) + C101 (1 мкФ), τ = 1 мс. +`HP_SEN` LM4875M подтянут к GND — усилитель включается автоматически при +наличии сигнала на входе. + +--- + +## Тактирование + +### SAI3 / MQS + +```bash +Audio PLL = 24 MHz × (30 + 66/625) = 722.534 MHz +SAI3_CLK_ROOT = Audio PLL / 8 / 8 = 11 289 600 Hz + (kCLOCK_Sai3Mux=2, Sai3PreDiv=7, Sai3Div=7) + +BCLK = 44100 × 16 бит × 2 канала = 1 411 200 Hz +Делитель MCLK = 11 289 600 / 1 411 200 = 8 (точно) + +MQS_MCLK = SAI3_CLK_ROOT / 64 / (3+1) = 11 289 600 / 256 = 44 100 Hz + (Oversample=64, CLK_DIV=3, выставляется в bsp_mqs_init()) +``` + +### FlexPWM4 (усилитель) + +```bash +IPG clock = AHB / 4 = 600 / 4 = 150 MHz +PWM clock = IPG / 16 = 9.375 MHz (Prescale_Divide_16) +Fpwm = 9 375 000 / 586 / 2 = 12 000 Hz (SignedCenterAligned) +``` + +Тактирование настраивается в `BOARD_BootClockRUN()` (`generated/clock_config.c`). +`bsp_mqs_init()` и `bsp_mqs_amp_init()` только открывают clock gates — +PLL и делители не трогают. + +--- + +## Архитектура + +```mermaid +flowchart TD + subgraph HOST + A["bsp_mqs_play() / bsp_mqs_play_blocking()"] + end + + subgraph SAI3_DMA + A --> B["SAI_TransferSendEDMA()\nDMA0 канал 0"] + B --> C["SAI3 TX FIFO\nwatermark = FIFO/2 = 16"] + C --> D["MQS_RIGHT\nGPIO_AD_B0_04"] + end + + subgraph AMP + E["bsp_mqs_amp_init()"] --> F["PWM4 SM0 PWM_A\n12 kHz, duty 50%"] + F --> G["RC-фильтр\nR56+C101"] + G --> H["LM358 буфер"] + H --> I["DC_VOL LM4875M"] + end + + subgraph CALLBACK + B --> J["mqs_edma_callback() ISR"] + J --> K["g_s_dma_done = true\nuser_cb()"] + end + + D --> L["RC-фильтр 3×\nR53-55 + C97-99"] + L --> M["LM4875M\nSOUND_OUT → P8"] +``` + +- **TX** — `SAI_TransferSendEDMA()`. DMA0 канал 0 зарезервирован за `bsp_mqs`. +- **Blocking** — polling флага `g_s_dma_done` (взводится из ISR). +- **Async** — колбэк `bsp_mqs_done_cb_t` из ISR-контекста. +- **Singleton** — один экземпляр, один DMA-канал. + +--- + +## API + +```c +/* SAI3 + eDMA + MQS */ +bsp_status_t bsp_mqs_init(void); +void bsp_mqs_deinit(void); + +bsp_status_t bsp_mqs_play(const int16_t *p_buf, size_t n_frames, + bsp_mqs_done_cb_t cb, void *p_user); +bsp_status_t bsp_mqs_play_blocking(const int16_t *p_buf, size_t n_frames); + +void bsp_mqs_stop(void); +bool bsp_mqs_is_busy(void); + +/* Усилитель LM4875M (PWM4) */ +bsp_status_t bsp_mqs_amp_init(void); +void bsp_mqs_amp_set_volume(uint8_t percent); +void bsp_mqs_amp_deinit(void); +``` + +**Коды возврата `bsp_mqs_play()` / `bsp_mqs_play_blocking()`:** + +| Код | Условие | +| ----------------- | -------------------------------------- | +| `BSP_OK` | Передача запущена / завершена | +| `BSP_ERR_BUSY` | Предыдущая передача ещё не завершена | +| `BSP_ERR_INVALID` | `p_buf == NULL` или `n_frames == 0` | +| `BSP_ERR_HW` | `SAI_TransferSendEDMA()` вернул ошибку | + +**Коды возврата `bsp_mqs_amp_init()`:** + +| Код | Условие | +| ------------ | ----------------------------------------------- | +| `BSP_OK` | PWM4 настроен и запущен | +| `BSP_ERR_HW` | `PWM_Init()` или `PWM_SetupPwm()` вернул ошибку | + +--- + +## Параметры аудио-потока + +| Параметр | Значение | +| --------------------- | ---------------------- | +| Частота дискретизации | 44 100 Гц | +| Разрядность | 16 бит (signed PCM) | +| Каналы в буфере | 2 (stereo interleaved) | +| Байт на фрейм | 4 (L + R, по 2 байта) | +| DMA канал | DMA0 канал 0 | +| DMAMUX source | `kDmaRequestMuxSai3Tx` | +| FIFO watermark | 16 слов (FIFO/2) | + +--- + +## Быстрый старт + +```c +#include "bsp/mqs.h" + +/* Буфер — в некэшируемой памяти, выровнен по 4 байтам */ +#define N_FRAMES (44100U) /* 1 секунда стерео */ +static AT_NONCACHEABLE_SECTION_ALIGN(int16_t buf[N_FRAMES * BSP_MQS_CHANNELS], 4U); + +/* После board_hw_init() + bsp_tick_init(): */ + +/* 1. Усилитель — первым, до MQS */ +bsp_mqs_amp_init(); +bsp_delay(300); /* дать C103/C105 LM4875M зарядиться */ + +/* 2. SAI3 + DMA + MQS */ +bsp_mqs_init(); + +/* 3. Заполнить буфер */ +for (size_t i = 0; i < N_FRAMES; i++) { + int16_t s = /* ваш сэмпл */; + buf[i * 2 + 0] = s; /* L */ + buf[i * 2 + 1] = s; /* R */ +} + +/* 4a. Синхронное воспроизведение (firmware_test) */ +bsp_mqs_play_blocking(buf, N_FRAMES); + +/* 4b. Асинхронное воспроизведение (tft_app) */ +static volatile bool done; +static void on_done(void *p) { done = true; } +done = false; +bsp_mqs_play(buf, N_FRAMES, on_done, NULL); +while (!done) { /* keepalive */ } + +/* Регулировка громкости: 0–100% */ +bsp_mqs_amp_set_volume(75U); +``` + +**Формат буфера:** + +```bash +buf[0] = L0, buf[1] = R0, /* фрейм 0 */ +buf[2] = L1, buf[3] = R1, /* фрейм 1 */ +... +``` + +Физически выводится только `MQS_RIGHT` — левый и правый каналы должны +быть идентичны. + +--- + +## Ограничения и требования + +- Буфер обязан находиться в **некэшируемой памяти** (`AT_NONCACHEABLE_SECTION_ALIGN`) + или кэш должен быть явно вычищен перед вызовом `bsp_mqs_play()`. +- Одновременно может воспроизводиться **только один буфер**. Повторный вызов + до завершения вернёт `BSP_ERR_BUSY`. +- `bsp_mqs_amp_init()` вызывать **до** `bsp_mqs_init()` и до первого `bsp_mqs_play()`. +- Поле `pwmchannelenable = true` в `pwm_signal_param_t` **обязательно** для + новых версий NXP SDK — без него `PWM_SetupPwm()` не устанавливает `OUTEN` + и ШИМ не выходит на пин. +- `bsp_mqs_play_blocking()` крутит polling-цикл. Не вызывать из ISR. +- В длинных тест-функциях вызывать `bsp_usb_cdc_poll()` каждые ~4 KB + для поддержания USB живым. +- DMA0 канал 0 зарезервирован за `bsp_mqs`. Остальные модули — каналы 1+. + +--- + +## Известные особенности SDK + +**`pwmchannelenable` (критично):** в NXP SDK начиная с версии 2.13+ структура +`pwm_signal_param_t` содержит поле `pwmchannelenable`. При инициализации +через designated initializers без явного указания этого поля оно равно `false`, +и `PWM_SetupPwm()` не устанавливает бит `OUTEN` → ШИМ не выходит на пин. +После отладочной сессии ошибка маскируется: отладчик оставляет `OUTEN` +установленным от предыдущего сеанса, и при software reset PWM работает. +Воспроизводится только при cold reset (power cycle). + +**Fault disable mapping:** без явного вызова `PWM_SetupFaultDisableMap()` +регистр `DISA` после cold reset равен `0x0000`, и PWM может быть заблокирован +неинициализированными Fault1/2/3 входами. `bsp_mqs_amp_init()` явно вызывает +`PWM_SetupFaultDisableMap()` для всех четырёх fault-входов. + +--- + +## CMake + +```cmake +# firmware/test/CMakeLists.txt +target_link_libraries(firmware_test PRIVATE bsp_mqs) +``` + +**Зависимости модуля:** + +| Зависимость | Тип | Описание | +| -------------- | ------- | ------------------------------------------- | +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_board` | PUBLIC | Транзитивно: `BOARD_*`, IOMUXC, clock gates | +| `sdk_sai_edma` | PRIVATE | `fsl_sai_edma.h` — SAI TX через eDMA | +| `sdk_edma` | PRIVATE | `fsl_edma.h` — DMA0 handle | +| `sdk_dmamux` | PRIVATE | `fsl_dmamux.h` — маршрутизация DMA-запросов | +| `sdk_clock` | PRIVATE | `fsl_clock.h` — clock gates | +| `sdk_pwm` | PRIVATE | `fsl_pwm.h` — FlexPWM4 SM0 (усилитель) | +| `sdk_xbara` | PRIVATE | `fsl_xbara.h` — XBARA | + diff --git a/bsp/provisioning/CMakeLists.txt b/bsp/provisioning/CMakeLists.txt new file mode 100644 index 0000000..1131f18 --- /dev/null +++ b/bsp/provisioning/CMakeLists.txt @@ -0,0 +1,17 @@ +# bsp/provisioning/CMakeLists.txt + +if(BUILD_TESTS_HOST) + return() +endif() + +add_library(bsp_provisioning STATIC src/provisioning.c) + +target_include_directories( + bsp_provisioning + PUBLIC include/ + PRIVATE src/) + +target_link_libraries( + bsp_provisioning + PUBLIC bsp_status + PRIVATE bsp_board) diff --git a/bsp/provisioning/README.md b/bsp/provisioning/README.md new file mode 100644 index 0000000..0ee86c0 --- /dev/null +++ b/bsp/provisioning/README.md @@ -0,0 +1,87 @@ +# bsp_provisioning — Provisioning / OCOTP UID + +Компонент читает уникальный 64-битный идентификатор чипа из OCOTP shadow registers. +Используется в `firmware_test` для команды `get_uid` протокола v2. + +--- + +## Аппаратура + +| Периферия | Назначение | +| --------- | ---------------------- | +| OCOTP | On-Chip OTP Controller | + +Shadow registers загружаются из eFuse-массива при каждом сбросе (MIMXRT1052RM §46.3.1). +Чтение немедленное через memory-mapped регистры `OCOTP->CFG0` / `OCOTP->CFG1` — +инициализация контроллера, fuse programming и дополнительные задержки не нужны. + +**UID-ячейки (MIMXRT1052RM Table 46-2):** + +| Регистр | Содержимое | +| ---------- | ------------ | +| OCOTP_CFG0 | UID\[31:0\] | +| OCOTP_CFG1 | UID\[63:32\] | + +--- + +## API + +```c +#include "bsp/provisioning.h" + +#define BSP_PROV_UID_LEN 8U /* байт */ + +bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); +``` + +`bsp_prov_read_uid()` — единственная публичная функция. + +- Читает `OCOTP->CFG0` и `OCOTP->CFG1` напрямую через memory-mapped регистры + (паттерн идентичен `fsl_silicon_id_soc.c` из NXP SDK). +- Заполняет `p_uid[0..7]` в little-endian порядке: `p_uid[0..3]` = CFG0, `p_uid[4..7]` = CFG1. + Порядок байт совпадает с NXP-инструментами (MCUXpresso, blhost). +- Возвращает `BSP_ERR` если `p_uid == NULL` или `len < 8`. + +--- + +## Использование + +```c +#include "bsp/provisioning.h" +#include + +uint8_t uid[BSP_PROV_UID_LEN]; +if (bsp_prov_read_uid(uid, sizeof(uid)) == BSP_OK) +{ + /* hex: "AABBCCDDEEFF0011" */ + for (size_t i = 0; i < BSP_PROV_UID_LEN; i++) + { + printf("%02X", uid[i]); + } +} +``` + +--- + +## CMake + +```cmake +target_link_libraries(firmware_test PRIVATE bsp_provisioning) +``` + +**Зависимости:** + +| Зависимость | Тип | Описание | +| ------------ | ------- | ------------------------------- | +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers | + + +--- + +## Особенности + +- Прямое чтение регистров не требует `OCOTP_Init()` — инициализация контроллера + нужна только при записи fuse. Попытка вызвать `OCOTP_Init()` перед чтением + приводит к зависанию (контроллер занят после USB CDC init). +- Компонент не имеет host-stub: защищён `if(BUILD_TESTS_HOST) return()` в CMakeLists. diff --git a/bsp/provisioning/include/bsp/provisioning.h b/bsp/provisioning/include/bsp/provisioning.h new file mode 100644 index 0000000..4607b9a --- /dev/null +++ b/bsp/provisioning/include/bsp/provisioning.h @@ -0,0 +1,41 @@ +/** + * @file bsp/provisioning/include/bsp/provisioning.h + * @brief BSP Provisioning — чтение уникального идентификатора чипа (OCOTP UID). + * + * Предоставляет единственный примитив: чтение 8-байтового UID из OCOTP_CFG0/CFG1. + * Не выполняет записи в OTP-ячейки и не управляет Flash. + * + * Аппаратура: MIMXRT1052RM §46 "On-Chip OTP Controller (OCOTP_CTRL)". + * Shadow registers загружаются из eFuse-массива при сбросе — чтение немедленное, + * fuse-programming не требуется. + */ + +#ifndef BSP_PROVISIONING_H_ +#define BSP_PROVISIONING_H_ + +#include "bsp/status.h" + +#include +#include + +/** @brief Длина UID в байтах (OCOTP_CFG0 + OCOTP_CFG1). */ +#define BSP_PROV_UID_LEN 8U + +/** + * @brief Прочитать уникальный идентификатор чипа из OCOTP. + * + * Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]). + * Результат записывается в big-endian порядке: p_uid[0] — старший байт CFG1, + * p_uid[7] — младший байт CFG0. Hex-строка совпадает с визуальным порядком слов + * в Reference Manual (MIMXRT1052RM Table 46-2). + * + * Функция выполняет OCOTP_Init() и включает clock gate перед чтением. + * Clock gate остаётся открытым после вызова (паттерн проекта). + * + * @param[out] p_uid Буфер для UID. Должен быть не менее BSP_PROV_UID_LEN байт. + * @param[in] len Размер буфера в байтах. Должен быть >= BSP_PROV_UID_LEN. + * @return BSP_OK при успехе, BSP_ERR если p_uid == NULL или len < BSP_PROV_UID_LEN. + */ +bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); + +#endif /* BSP_PROVISIONING_H_ */ \ No newline at end of file diff --git a/bsp/provisioning/src/provisioning.c b/bsp/provisioning/src/provisioning.c new file mode 100644 index 0000000..d0e0861 --- /dev/null +++ b/bsp/provisioning/src/provisioning.c @@ -0,0 +1,54 @@ +/** + * @file bsp/provisioning/src/bsp_provisioning.c + * @brief Реализация bsp_provisioning: чтение UID из OCOTP_CFG0/CFG1. + * + * На i.MX RT1052 FSL_FEATURE_OCOTP_HAS_TIMING_CTRL == 0, поэтому + * OCOTP_Init(OCOTP, 0U) вызывается без частоты (частота не используется драйвером). + * + * Shadow registers загружаются из eFuse при сбросе (MIMXRT1052RM §46.3.1). + * OCOTP_ReadFuseShadowRegister() читает уже готовые значения без fuse programming. + * + * Индексы shadow registers (MIMXRT1052RM Table 46-2): + * Bank 0, Word 1 (index 1) → OCOTP_CFG0 → UID[31:0] + * Bank 0, Word 2 (index 2) → OCOTP_CFG1 → UID[63:32] + * index = bank * 4 + word + */ + +#include "bsp/provisioning.h" + +#include "fsl_clock.h" +#include "fsl_common.h" +/* -------------------------------------------------------------------------- + * Константы + * ----------------------------------------------------------------------- */ + +/** @brief Shadow register index для OCOTP_CFG0 (Bank 0, Word 1 → UID[31:0]). */ +#define PROV_OCOTP_IDX_CFG0 1U + +/** @brief Shadow register index для OCOTP_CFG1 (Bank 0, Word 2 → UID[63:32]). */ +#define PROV_OCOTP_IDX_CFG1 2U + +/* -------------------------------------------------------------------------- + * Public API + * ----------------------------------------------------------------------- */ + +bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len) +{ + if ((p_uid == NULL) || (len < BSP_PROV_UID_LEN)) + { + return BSP_ERR_PARAM; + } + + /* + * Shadow registers загружаются из eFuse при сбросе и доступны напрямую + * через memory-mapped адреса без инициализации контроллера. + * Паттерн идентичен fsl_silicon_id_soc.c из NXP SDK. + * CFG0 = UID[31:0], CFG1 = UID[63:32]. + */ + /* NOLINTNEXTLINE(bugprone-casting-through-void) */ + *((uint32_t *) (uintptr_t) &p_uid[0U]) = OCOTP->CFG0; + /* NOLINTNEXTLINE(bugprone-casting-through-void) */ + *((uint32_t *) (uintptr_t) &p_uid[4U]) = OCOTP->CFG1; + + return BSP_OK; +} \ No newline at end of file diff --git a/docs/testing/PROTOCOL.md b/docs/testing/PROTOCOL.md index 761c0f4..579c2b5 100644 --- a/docs/testing/PROTOCOL.md +++ b/docs/testing/PROTOCOL.md @@ -152,6 +152,23 @@ sequenceDiagram ]} ``` +### `get_uid` — чтение уникального идентификатора чипа + +Запрос UID из OCOTP. Может быть отправлен в любой момент когда runner не BUSY. + +```json +→ {"type":"cmd","cmd":"get_uid"} +← {"type":"uid_response","uid":"AABBCCDDEEFF0011"} +``` + +`uid` — 16 hex-символов (8 байт big-endian): CFG1[63:32] + CFG0[31:0]. + +При ошибке чтения OCOTP: + +```json +← {"ok":false,"error":"UID_READ_ERR"} +``` + ### `run_selected` — запуск подмножества тестов Запускает тесты по списку ID. Порядок выполнения — по реестру таргета, diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 15b8125..798068e 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -48,6 +48,7 @@ target_link_libraries( bsp_tick bsp_boot_xip bsp_usb_cdc + bsp_provisioning bsp_sdram bsp_qspi_flash bsp_uart_host diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md index 8608440..7fa9d46 100644 --- a/firmware/test/PLAN.md +++ b/firmware/test/PLAN.md @@ -1,6 +1,6 @@ # firmware_test — План разработки -> Версия: 0.7 | Обновлён после завершения Этапов 6а–6д, 6е, 6ж (протокол, test_opto, test_can, HIL pytest firmware_cdc). +> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified). --- @@ -36,7 +36,7 @@ | HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура | | HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | | HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` | -| `bsp_mqs` + `test_mqs` | ⬜ | Этап 6г | +| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified | | Provisioning | ⬜ | Этап 7 | | TUI сервисного инженера | ⬜ | Этап 8 | @@ -44,16 +44,16 @@ ## Матрица тестов — итоговая -| ID | Название | Critical | HIL | Тип | BSP | Статус | -| --------- | ------------------ | -------- | --- | ----------- | ------------------- | ------ | -| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ | -| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ | -| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ | -| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | -| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | -| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` (⬜ новый) | ⬜ | -| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | -| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | +| ID | Название | Critical | HIL | Тип | BSP | Статус | +| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ | +| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ | +| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ | +| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ | +| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | +| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | +| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ | +| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | +| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | **Убранные тесты (закрытые решения):** @@ -93,8 +93,19 @@ - **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`. - **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен один канал. Буфер всегда стерео (L+R идентичны). -- **MQS тест:** захардкоженная мелодия 3–5 с, `confirm_request("mqs_tone")`, - оператор слышит → OK/FAIL. `critical=false`, `requires_hil=false`. +- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая), + `confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL. + `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`. +- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`. + Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M. + Нарушение порядка приводит к щелчку при старте или отсутствию звука. +- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking), + параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с. +- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно + выставлять в `true`. При инициализации через designated initializers без явного + указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит + на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого + прогона). Воспроизводится только при cold reset. - **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` @@ -126,9 +137,9 @@ --- -## Этап 6 — test_can + test_opto + test_mqs + протокол ← ТЕКУЩИЙ +## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН -### 6а — Расширение протокола +### 6а — Расширение протокола ✅ **Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md` @@ -173,7 +184,7 @@ - Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc - `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция -### 6б — test_opto.c +### 6б — test_opto.c ✅ **Файл:** `firmware/test/src/tests/test_opto.c` @@ -193,7 +204,7 @@ - FAIL при несоответствии: `detail = " state mismatch: expected ACTIVE got INACTIVE"` - Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг -### 6в — test_can.c +### 6в — test_can.c ✅ **Файл:** `firmware/test/src/tests/test_can.c` @@ -223,28 +234,32 @@ confirm_request("can_tx_verify") - `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует - Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()` -### 6г — bsp_mqs + test_mqs.c +### 6г — bsp_mqs + test_mqs.c ✅ -**Файлы:** `bsp/mqs/` (новый BSP-модуль) + `firmware/test/src/tests/test_mqs.c` +**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c` **bsp_mqs:** -- MQS требует стерео PCM (L+R), на плате один физический канал -- Буфер: всегда стерео (L == R, оба канала идентичны) -- API минимальный: `bsp_mqs_init()`, `bsp_mqs_play(buf, len)`, `bsp_mqs_stop()`, `bsp_mqs_deinit()` -- Реализация — на основе наработок (предоставит разработчик) +- SAI3 + eDMA (DMA0 канал 0) + MQS периферия +- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT` +- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358 +- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`, + `bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume` **test_mqs:** -- Захардкоженная мелодия, 3–5 секунд -- `confirm_request("mqs_tone")` → оператор слышит → OK/FAIL -- Таймаут: 15 с (аналогично display) + +- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида +- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле +- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL +- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг) - `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL` -### 6д — HIL pytest для firmware_test +### 6д — HIL pytest для firmware_test ✅ **Файлы:** + ``` -tools/hil/conftest.py ← новая фикстура firmware_cdc +tools/hil/conftest.py ← фикстура firmware_cdc tools/hil/06_test_firmware_opto.py tools/hil/06_test_firmware_can.py ``` @@ -257,7 +272,7 @@ def firmware_cdc(m5): """ Открывает USB CDC порт firmware_test. firmware_test уже прошит в Flash (не загружается pyOCD). - Ждёт session_start, возвращает FirmwareCdcClient. + Проверяет живость через ping → pong. """ ``` @@ -268,19 +283,6 @@ def firmware_cdc(m5): - `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}` - `run_test(id)` — запустить тест, вернуть test_result dict -**Сценарий `06_test_firmware_can.py`:** - -```python -def test_can_rx(firmware_cdc, m5): - # Запустить тест can через firmware_test - # При confirm_request("can_rx_ready") — M5 шлёт фрейм, затем confirm - ... - -def test_can_tx(firmware_cdc, m5): - # При confirm_request("can_tx_verify") — M5 принимает фрейм, верифицирует - ... -``` - **Justfile:** ```bash @@ -348,7 +350,7 @@ tools/shared/ **Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP) -```bash +``` ┌─ Прошивка платы ─────────────────────────────────┐ │ Обнаружен BootROM (SDP режим) │ │ │ @@ -366,7 +368,7 @@ tools/shared/ **Режим B — Диагностика** (триггер: session_start получен по CDC) -```bash +``` ┌─ Диагностика платы fw:0.1.0 ─────────────────────┐ │ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │ ├────────────────────────────────────────────────────┤ @@ -377,8 +379,8 @@ tools/shared/ │ ☑ TFT Display │ mount failed: 5 │ │ ☑ Кнопки │ display ✓ PASS │ │ ☑ MQS Audio │ buttons ✓ PASS │ -│ ☑ CAN loopback [HIL] │ ... │ -│ ☑ Оптовходы [HIL] │ │ +│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │ +│ ☑ Оптовходы [HIL] │ ... │ ├────────────────────────────────────────────────────┤ │ [ Запустить выбранные ] [ Все тесты ] │ │ ████████████████░░░░ 80% Тест: display │ @@ -393,6 +395,7 @@ tools/shared/ | Тип теста | Источник confirm | Действие TUI | | -------------------- | ------------------ | --------------------------------------------- | | standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown | +| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown | | standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result | | HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) | @@ -432,7 +435,8 @@ just host::service-flash # прошить без TUI (для автом ``` **Перепрошивка (нужна новая версия firmware_test или production):** -``` + +```bash 1. Перемычка BOOT_MOD_1 → 3V3 2. Reset, подключить USB 3. TUI обнаружил 1FC9:0130 → Режим A @@ -456,10 +460,9 @@ just host::service-flash # прошить без TUI (для автом ✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) ✅ Этап 6е HIL pytest: 06_test_firmware_opto.py ✅ Этап 6ж HIL pytest: 06_test_firmware_can.py +✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация -⬜ Этап 6г bsp_mqs + test_mqs.c ← СЛЕДУЮЩИЙ ШАГ - -⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) +⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ ⬜ Этап 8а tools/production/ скелет + models + clients ⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты) @@ -474,12 +477,12 @@ just host::service-flash # прошить без TUI (для автом ## Зависимости между этапами -```bash -✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ⬜ 6г (mqs) +``` +✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs) ↓ ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) ↓ ⬜ 7 (provisioning) ↓ ⬜ 8 (TUI) -``` +``` \ No newline at end of file diff --git a/firmware/test/src/cli.c b/firmware/test/src/cli.c index a8eefcd..738d58f 100644 --- a/firmware/test/src/cli.c +++ b/firmware/test/src/cli.c @@ -22,6 +22,7 @@ #include "cli.h" +#include "bsp/provisioning.h" #include "bsp/usb_cdc.h" #include "protocol.h" #include "test_runner.h" @@ -316,6 +317,20 @@ static void handle_cmd(const char *p_line) return; } + if (strcmp(cmd_name, "get_uid") == 0) + { + uint8_t uid[BSP_PROV_UID_LEN]; + if (bsp_prov_read_uid(uid, sizeof(uid)) == BSP_OK) + { + protocol_send_uid_response(uid); + } + else + { + protocol_send_error("UID_READ_ERR"); + } + return; + } + if (strcmp(cmd_name, "list_tests") == 0) { test_runner_send_list(); diff --git a/firmware/test/src/main.c b/firmware/test/src/main.c index a9cbedd..9923176 100644 --- a/firmware/test/src/main.c +++ b/firmware/test/src/main.c @@ -1,206 +1,4 @@ -/** - * @file main_mqs_test.c - * @brief Изолированный тест bsp_mqs + bsp_mqs_amp. - * - * Подменяет firmware/test/src/main.c на время отладки BSP. - * Вывод диагностики — UART (MCU-Link VCOM, 115200 бод). - * - * Два режима — переключаются комментарием в main(): - * TEST_MODE_POLLING — SAI_WriteBlocking напрямую, без DMA. - * Если звук есть → SAI/MQS OK, проблема в DMA. - * Если нет → проблема в SAI/MQS/усилителе. - * TEST_MODE_DMA — bsp_mqs_play_blocking через eDMA. - */ -#include "board.h" -#include "bsp/led.h" -#include "bsp/mqs.h" -#include "bsp/tick.h" -#include "bsp/uart_host.h" -#include "fsl_sai.h" - -#include -#include - -/* -------------------------------------------------------------------------- - * Выбор режима: раскомментируй один - * ----------------------------------------------------------------------- */ -//#define TEST_MODE_POLLING -#define TEST_MODE_DMA - -/* -------------------------------------------------------------------------- - * Буфер мелодии в некэшируемой секции - * ----------------------------------------------------------------------- */ - -#define MQS_NOTE_SAMPLES (44100U * 2U) /* 2 с на ноту */ -#define MQS_TOTAL_FRAMES (MQS_NOTE_SAMPLES * 2U) /* 2 ноты */ -#define MQS_BUF_ELEMENTS (MQS_TOTAL_FRAMES * BSP_MQS_CHANNELS) - -static AT_NONCACHEABLE_SECTION_ALIGN(int16_t s_melody_buf[MQS_BUF_ELEMENTS], 4U); - -/* -------------------------------------------------------------------------- - * Синусоида (целочисленная, LUT по четвертям) - * ----------------------------------------------------------------------- */ - -static int16_t sine_sample(uint32_t phase, uint32_t period) -{ - static const int16_t k_lut[25] = { - 0, 2998, 5944, 8788, 11479, 13970, 16212, 18162, 19784, 21043, 21910, 22369, 22404, - 22005, 21170, 19898, 18199, 16084, 13567, 10670, 7416, 3831, 0, -3831, -7416, - }; - uint32_t idx = (phase * 96U) / period % 96U; - uint32_t quarter = idx / 24U; - uint32_t pos = idx % 24U; - int16_t val; - switch (quarter) - { - case 0U: - val = k_lut[pos]; - break; - case 1U: - val = k_lut[24U - pos]; - break; - case 2U: - val = -k_lut[pos]; - break; - default: - val = -k_lut[24U - pos]; - break; - } - return val; -} - -static void build_melody(void) -{ - const uint32_t k_p_a4 = 100U; /* A4 ≈ 440 Гц */ - const uint32_t k_p_e5 = 67U; /* E5 ≈ 659 Гц */ - uint32_t out = 0U; - - for (uint32_t i = 0U; i < MQS_NOTE_SAMPLES; i++) - { - int16_t s = sine_sample(i % k_p_a4, k_p_a4); - s_melody_buf[out++] = s; /* L */ - s_melody_buf[out++] = s; /* R */ - } - for (uint32_t i = 0U; i < MQS_NOTE_SAMPLES; i++) - { - int16_t s = sine_sample(i % k_p_e5, k_p_e5); - s_melody_buf[out++] = s; /* L */ - s_melody_buf[out++] = s; /* R */ - } -} - -/* -------------------------------------------------------------------------- - * Режим A: SAI polling напрямую, без DMA - * - * SAI_WriteBlocking — блокирует до записи всех байт через FIFO. - * Не требует DMA вообще. Если звук есть — SAI и MQS живые. - * ----------------------------------------------------------------------- */ - -#ifdef TEST_MODE_POLLING -static void run_test(void) -{ - bsp_uart_host_write_str("[5A] Bare-metal SAI polling...\r\n"); - - /* 1. Глушим передатчик и отключаем запросы DMA, чтобы не мешали поллингу */ - SAI3->TCSR &= ~(I2S_TCSR_TE_MASK | I2S_TCSR_BCE_MASK | I2S_TCSR_FRDE_MASK); - - /* 2. Сбрасываем ошибку Underrun (FEF) и чистим FIFO (FR) */ - SAI3->TCSR |= I2S_TCSR_FEF_MASK | I2S_TCSR_FR_MASK; - - /* 3. Предзагружаем FIFO (закинем 16 сэмплов ДО включения) */ - /* Это спасет аппаратуру от мгновенного Underrun'а при старте */ - for (uint32_t i = 0; i < 16; i++) - { - SAI3->TDR[0] = s_melody_buf[i]; - } - - /* 4. Включаем передатчик и генерацию Bit Clock */ - SAI3->TCSR |= (I2S_TCSR_TE_MASK | I2S_TCSR_BCE_MASK); - - /* 5. Качаем остальной буфер вручную */ - for (uint32_t i = 16; i < MQS_BUF_ELEMENTS; i++) - { - /* Ждем, пока в FIFO появится свободное место (FWF == 1) */ - while ((SAI3->TCSR & I2S_TCSR_FWF_MASK) == 0) - { - /* Если всё-таки поймали опустошение - перезапускаем TX на лету */ - if (SAI3->TCSR & I2S_TCSR_FEF_MASK) - { - SAI3->TCSR |= I2S_TCSR_FEF_MASK; /* Сброс ошибки */ - SAI3->TCSR |= I2S_TCSR_TE_MASK | I2S_TCSR_BCE_MASK; /* Рестарт TX */ - } - } - /* Пишем следующий сэмпл */ - SAI3->TDR[0] = s_melody_buf[i]; - } - - /* 6. Дожидаемся, пока уйдут остатки из FIFO, и выключаем */ - while (SAI3->TCSR & I2S_TCSR_FWF_MASK) - { - } - SAI3->TCSR &= ~(I2S_TCSR_TE_MASK | I2S_TCSR_BCE_MASK); - - bsp_uart_host_write_str("[5A] Done.\r\n"); -} -#endif - -/* -------------------------------------------------------------------------- - * Режим B: DMA через bsp_mqs_play_blocking - * ----------------------------------------------------------------------- */ - -#ifdef TEST_MODE_DMA -static void run_test(void) -{ - bsp_uart_host_write_str("[5B] DMA play, ~4s...\r\n"); - - bsp_status_t st = bsp_mqs_play_blocking(s_melody_buf, MQS_TOTAL_FRAMES); - - bsp_uart_host_write_str(st == BSP_OK ? "[5B] OK\r\n" : "[5B] FAIL\r\n"); -} -#endif - -/* -------------------------------------------------------------------------- - * main - * ----------------------------------------------------------------------- */ - -int main(void) -{ - board_hw_init(); - bsp_tick_init(); - bsp_led_init(); - (void) bsp_uart_host_init(115200U); - - bsp_led_on(LED_HEARTBEAT); - bsp_uart_host_write_str("\r\n=== MQS BSP TEST ===\r\n"); - - /* Шаг 1 — усилитель */ - bsp_uart_host_write_str("[1] amp_init...\r\n"); - bsp_status_t st = bsp_mqs_amp_init(); - bsp_uart_host_write_str(st == BSP_OK ? "[1] OK\r\n" : "[1] FAIL\r\n"); - - /* Шаг 2 — SAI + DMA + MQS */ - bsp_uart_host_write_str("[2] mqs_init...\r\n"); - st = bsp_mqs_init(); - bsp_uart_host_write_str(st == BSP_OK ? "[2] OK\r\n" : "[2] FAIL\r\n"); - - /* Шаг 3 — генерация буфера */ - bsp_uart_host_write_str("[3] build_melody...\r\n"); - build_melody(); - bsp_uart_host_write_str("[3] done.\r\n"); - - /* Основной цикл */ - for (;;) - { - bsp_led_toggle(LED_HEARTBEAT); - run_test(); - bsp_uart_host_write_str("[pause] 3s\r\n"); - bsp_delay(3000U); - } - - return 0; -} -#if 0 /** * @file main.c * @brief firmware_test — точка входа. @@ -220,7 +18,6 @@ int main(void) * 8. Главный цикл — poll + cli_process */ #include "board.h" -#include "bsp/can.h" #include "bsp/led.h" #include "bsp/tick.h" #include "bsp/usb_cdc.h" @@ -232,9 +29,8 @@ int main(void) int main(void) { - const uint32_t CONNECT_BLINK_MS = 200U; - const uint32_t ERROR_BLINK_MS = 250; - static const bsp_can_config_t K_CAN_CFG = { .bitrate = 125000U }; + const uint32_t CONNECT_BLINK_MS = 200U; + const uint32_t ERROR_BLINK_MS = 250; board_hw_init(); @@ -267,4 +63,3 @@ int main(void) test_runner_process(); } } -#endif \ No newline at end of file diff --git a/firmware/test/src/protocol.c b/firmware/test/src/protocol.c index 9342f05..a8db0c9 100644 --- a/firmware/test/src/protocol.c +++ b/firmware/test/src/protocol.c @@ -145,4 +145,16 @@ void protocol_send_error(const char *p_code) char buf[PROTO_BUF_SIZE]; (void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code); cli_send(buf); +} + +void protocol_send_uid_response(const uint8_t *p_uid) +{ + char buf[PROTO_BUF_SIZE]; + (void) snprintf(buf, sizeof(buf), + "{\"type\":\"uid_response\"," + "\"uid\":\"%02X%02X%02X%02X%02X%02X%02X%02X\"}\n", + (unsigned int) p_uid[0U], (unsigned int) p_uid[1U], (unsigned int) p_uid[2U], + (unsigned int) p_uid[3U], (unsigned int) p_uid[4U], (unsigned int) p_uid[5U], + (unsigned int) p_uid[6U], (unsigned int) p_uid[7U]); + cli_send(buf); } \ No newline at end of file diff --git a/firmware/test/src/protocol.h b/firmware/test/src/protocol.h index 9f59d24..0bbff09 100644 --- a/firmware/test/src/protocol.h +++ b/firmware/test/src/protocol.h @@ -91,4 +91,13 @@ void protocol_send_test_list(const test_module_t *const *p_pp_registry, size_t c */ void protocol_send_error(const char *p_code); +/** + * @brief Отправить ответ на команду get_uid. + * + * Формат: {"type":"uid_response","uid":"AABBCCDDEEFF0011"} + * uid — 16 hex-символов (8 байт big-endian, без разделителей).+ * + * @param[in] p_uid Буфер с UID длиной BSP_PROV_UID_LEN байт. + */ +void protocol_send_uid_response(const uint8_t *p_uid); + #endif /* PROTOCOL_H_ */ \ No newline at end of file diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README.md index b87c127..6738adf 100644 --- a/firmware/test/src/tests/README.md +++ b/firmware/test/src/tests/README.md @@ -215,7 +215,7 @@ sequenceDiagram **SKIP — оператор нажал Cancel:** -``` +```bash → {"type":"confirm","id":"usd","confirmed":false} ← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator declined"} ``` @@ -369,6 +369,59 @@ sequenceDiagram --- +## MQS аудио-выход — слуховая проверка + +| Параметр | Значение | +| ---------------- | ----------------------------------------- | +| ID | `mqs` | +| Критичный | ❌ Нет | +| HIL | ❌ Нет | +| Тип | Interactive (in-run confirm) | +| Время выполнения | ~4 с воспроизведение + до 15 с на confirm | + +Тест воспроизводит мелодию (~4 с: нота A4 затем E5) через MQS-выход +(`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M. +Оператор подтверждает слышимость тона. + +```mermaid +sequenceDiagram + participant H as HOST + participant T as TARGET + H->>T: run("mqs") + T->>H: test_begin + Note over T: ~4 с воспроизведение A4 + E5 + T->>H: confirm_request("mqs_tone", timeout=15s) + Note over H: оператор слышит тон + H->>T: confirm("mqs_tone", true) + T->>H: test_result: pass/fail +``` + +**PASS:** + +```bash +→ {"type":"cmd","cmd":"run","id":"mqs"} +← {"type":"test_begin","id":"mqs","name":"MQS Audio Out","critical":false} +← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} +→ {"type":"confirm","id":"mqs_tone","confirmed":true} +← {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} +``` + +**FAIL — оператор не слышит или истёк таймаут:** + +```bash +← {"type":"test_result","id":"mqs","status":"fail","ms":15001, + "detail":"operator: no sound"} +``` + +**FAIL — ошибка воспроизведения (DMA/SAI):** + +```bash +← {"type":"test_result","id":"mqs","status":"fail","ms":1, + "detail":"mqs play error"} +``` + +--- + ## Запуск всего набора (run_all) Тесты запускаются строго в порядке реестра. При провале критичного теста @@ -392,7 +445,17 @@ sequenceDiagram ← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} ... ← {"type":"test_result","id":"buttons","status":"pass","ms":6200,"detail":""} -← {"type":"summary","passed":5,"failed":0,"skipped":0,"overall":"pass"} +← {"type":"test_begin","id":"mqs","name":"MQS Audio Out","critical":false} +← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} + ... оператор слышит и подтверждает ... +← {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} + ...confirm цикл 6 HIL шагов... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +← {"type":"test_begin","id":"can","name":"CAN loopback","critical":false} + ...confirm цикл 2 HIL шагов... +← {"type":"test_result","id":"can","status":"pass","ms":1240,"detail":""} +← {"type":"summary","passed":8,"failed":0,"skipped":0,"overall":"pass"} ``` **SKIP-каскад при critical fail:** @@ -404,7 +467,7 @@ sequenceDiagram ← {"type":"test_begin","id":"usd",...} ← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} ... -← {"type":"summary","passed":0,"failed":1,"skipped":4,"overall":"fail"} +← {"type":"summary","passed":0,"failed":1,"skipped":7,"overall":"fail"} ``` --- @@ -418,8 +481,9 @@ sequenceDiagram | 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) | | 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | | 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | -| 6 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | -| 7 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | +| 6 | `mqs` | MQS Audio Out | ❌ | ❌ | Interactive (in-run confirm) | +| 7 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | +| 8 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | --- @@ -442,6 +506,8 @@ sequenceDiagram | `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | | `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | | `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | +| `mqs` | `mqs play error` | SAI3/DMA не запустился | +| `mqs` | `operator: no sound` | Нет звука / усилитель не работает | | `opto` | ` mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | | `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | | `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | @@ -464,6 +530,7 @@ sequenceDiagram {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, + {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false}, {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}, {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true} ]} diff --git a/firmware/test/src/tests/test_mqs.c b/firmware/test/src/tests/test_mqs.c index 28fa5d7..8a41979 100644 --- a/firmware/test/src/tests/test_mqs.c +++ b/firmware/test/src/tests/test_mqs.c @@ -1,19 +1,20 @@ /** * @file test_mqs.c - * @brief Тест MQS: воспроизведение тестового тона через SAI1 + eDMA + MQS. + * @brief Тест MQS: воспроизведение тестового тона через SAI3 + eDMA + MQS. * * Алгоритм: - * 1. init() — bsp_mqs_init(), генерация мелодии в s_melody_buf. + * 1. init() — bsp_mqs_amp_init(), задержка 300 мс, bsp_mqs_init(), + * генерация мелодии в s_melody_buf. * 2. run() — воспроизвести мелодию (~4 с) с USB keepalive в цикле, * затем confirm_request("mqs_tone") — оператор подтверждает. - * 3. deinit() — bsp_mqs_stop(), bsp_mqs_deinit(). + * 3. deinit() — bsp_mqs_stop(), bsp_mqs_amp_deinit(), bsp_mqs_deinit(). * * Мелодия: стерео PCM16 44100 Гц, две ноты (A4/E5) по 2 с каждая. * Буфер — статический глобальный в некэшируемой секции (OCRAM NonCacheable). * L == R (монофонический выход на плате). * * Протокол: - * ← confirm_request("mqs_tone", "Слышен звуковой сигнал?", 15000) + * ← confirm_request("mqs_tone", "Do you hear a tone?", 15000) * → confirm("mqs_tone", true/false) * ← test_result pass/fail/skip */ @@ -159,9 +160,17 @@ static bsp_status_t mqs_play_with_poll(void) static void test_mqs_init(void) { - mqs_build_melody(); - (void) bsp_mqs_init(); + /* + * Порядок инициализации критичен (см. bsp/mqs/README.md): + * 1. amp_init — запускает PWM4, начинает заряд C103/C105 LM4875M. + * 2. bsp_delay(300) — ждём заряда фильтрующих конденсаторов усилителя. + * 3. mqs_init — запускает SAI3 + DMA. + * 4. mqs_build_melody — генерация буфера (однократно, флаг g_s_melody_ready). + */ (void) bsp_mqs_amp_init(); + bsp_delay(300U); + (void) bsp_mqs_init(); + mqs_build_melody(); } static test_result_t test_mqs_run(void) @@ -175,22 +184,20 @@ static test_result_t test_mqs_run(void) }; } - /* Запрашиваем подтверждение оператора. - * API: bool test_runner_wait_confirm(const confirm_params_t *p_params) - * Возвращает true если оператор подтвердил, false при отказе или таймауте. - * Таймаут детектируется отдельно через флаг в confirm_params_t. */ - const confirm_params_t PARAMS = { + /* + * Запрашиваем подтверждение оператора. + * Таймаут 15 с — аналогично display-тесту (закрытое решение, Этап 5). + */ + const confirm_params_t K_PARAMS = { .id = "mqs_tone", - .prompt = "Have you heard a tone?", + .prompt = "Do you hear a tone?", .timeout_ms = 15000U, }; - bool confirmed = test_runner_wait_confirm(&PARAMS); + bool confirmed = test_runner_wait_confirm(&K_PARAMS); if (!confirmed) { - /* Различаем таймаут и явный отказ через поле last_confirm_timed_out - * если оно есть в API, иначе унифицируем как fail. */ return (test_result_t){ .status = TEST_STATUS_FAIL, .detail = "operator: no sound", diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index 46e156c..0f453e3 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -62,7 +62,6 @@ add_sdk_driver(sai_edma fsl_sai_edma.c) add_sdk_driver(edma fsl_edma.c) add_sdk_driver(dmamux fsl_dmamux.c) add_sdk_driver(xbara fsl_xbara.c) -add_sdk_driver(xbarb fsl_xbarb.c) # Драйверы которые зависят от clock target_link_libraries(sdk_lpuart PUBLIC sdk_clock)