# firmware_test: provisionig - done

This commit is contained in:
Dmitry Akimov 2026-06-29 13:38:32 +03:00
parent 2d6aaaecd2
commit 3aa8974def
17 changed files with 689 additions and 296 deletions

View file

@ -21,6 +21,7 @@ add_subdirectory(sdram)
add_subdirectory(qspi_flash) add_subdirectory(qspi_flash)
add_subdirectory(sd) add_subdirectory(sd)
add_subdirectory(mqs) add_subdirectory(mqs)
add_subdirectory(provisioning)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки # Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w) target_compile_options(bsp_board PRIVATE -w)

View file

@ -45,12 +45,15 @@ bsp/
│ └── mocks/ # fff-заглушки для host-тестов │ └── mocks/ # fff-заглушки для host-тестов
├── opto/ # bsp_opto — оптоизолированные входы PS2801-4 ├── opto/ # bsp_opto — оптоизолированные входы PS2801-4
├── can/ # bsp_can — FlexCAN2 (трансивер SN65HVD230D) ├── can/ # bsp_can — FlexCAN2 (трансивер SN65HVD230D)
│ └── mocks/ # fff-заглушки для host-тестов
├── button/ # bsp_button — тактовые кнопки SWT6x6 с debounce ├── button/ # bsp_button — тактовые кнопки SWT6x6 с debounce
├── display/ # bsp_display — TFT-дисплей ├── display/ # bsp_display — TFT-дисплей
├── usb_cdc/ # bsp_usb_cdc — USB CDC ACM ├── usb_cdc/ # bsp_usb_cdc — USB CDC ACM
├── sdram/ # bsp_sdram — внешний SDRAM через SEMC ├── sdram/ # bsp_sdram — внешний SDRAM через SEMC
├── qspi_flash/ # bsp_qspi_flash — QSPI Flash W25Q64/128/256/512 ├── 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 | | Библиотека | Модуль | README |
| ---------------- | ------------- | -------------------------------------------- | | ------------------ | --------------- | ------------------------------------------------ |
| `bsp_led` | `led/` | [led/README.md](led/README.md) | | `bsp_led` | `led/` | [led/README.md](led/README.md) |
| `bsp_tick` | `tick/` | [tick/README.md](tick/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_uart_host` | `uart_host/` | [uart_host/README.md](uart_host/README.md) |
| `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) | | `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) |
| `bsp_can` | `can/` | [can/README.md](can/README.md) | | `bsp_can` | `can/` | [can/README.md](can/README.md) |
| `bsp_button` | `button/` | [button/README.md](button/README.md) | | `bsp_button` | `button/` | [button/README.md](button/README.md) |
| `bsp_display` | `display/` | [display/README.md](display/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_usb_cdc` | `usb_cdc/` | [usb_cdc/README.md](usb_cdc/README.md) |
| `bsp_sdram` | `sdram/` | [sdram/README.md](sdram/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_qspi_flash` | `qspi_flash/` | [qspi_flash/README.md](qspi_flash/README.md) |
| `bsp_sd` | `sd/` | [sd/README.md](sd/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) |
--- ---

View file

@ -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 */ }
/* Регулировка громкости: 0100% */
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 |

View file

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

View file

@ -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 <stdio.h>
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.

View file

@ -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 <stddef.h>
#include <stdint.h>
/** @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_ */

View file

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

View file

@ -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` — запуск подмножества тестов ### `run_selected` — запуск подмножества тестов
Запускает тесты по списку ID. Порядок выполнения — по реестру таргета, Запускает тесты по списку ID. Порядок выполнения — по реестру таргета,

View file

@ -48,6 +48,7 @@ target_link_libraries(
bsp_tick bsp_tick
bsp_boot_xip bsp_boot_xip
bsp_usb_cdc bsp_usb_cdc
bsp_provisioning
bsp_sdram bsp_sdram
bsp_qspi_flash bsp_qspi_flash
bsp_uart_host bsp_uart_host

View file

@ -1,6 +1,6 @@
# firmware_test — План разработки # 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_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура |
| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | | HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` |
| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.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 | | Provisioning | ⬜ | Этап 7 |
| TUI сервисного инженера | ⬜ | Этап 8 | | TUI сервисного инженера | ⬜ | Этап 8 |
@ -44,16 +44,16 @@
## Матрица тестов — итоговая ## Матрица тестов — итоговая
| ID | Название | Critical | HIL | Тип | BSP | Статус | | ID | Название | Critical | HIL | Тип | BSP | Статус |
| --------- | ------------------ | -------- | --- | ----------- | ------------------- | ------ | | --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ |
| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` | ✅ | | `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ |
| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` | ✅ | | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ |
| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` | ✅ | | `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ |
| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` | ✅ | | `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ |
| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` | ✅ | | `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ |
| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` (⬜ новый) | ⬜ | | `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ |
| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` | ✅ | | `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ |
| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` | ✅ | | `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ |
**Убранные тесты (закрытые решения):** **Убранные тесты (закрытые решения):**
@ -93,8 +93,19 @@
- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`. - **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`.
- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен - **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен
один канал. Буфер всегда стерео (L+R идентичны). один канал. Буфер всегда стерео (L+R идентичны).
- **MQS тест:** захардкоженная мелодия 35 с, `confirm_request("mqs_tone")`, - **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая),
оператор слышит → OK/FAIL. `critical=false`, `requires_hil=false`. `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 на - **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на
`CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate
может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` может быть закрыт → 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` **Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md`
@ -173,7 +184,7 @@
- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc - Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc
- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция - `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` **Файл:** `firmware/test/src/tests/test_opto.c`
@ -193,7 +204,7 @@
- FAIL при несоответствии: `detail = "<id> state mismatch: expected ACTIVE got INACTIVE"` - FAIL при несоответствии: `detail = "<id> state mismatch: expected ACTIVE got INACTIVE"`
- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг - Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг
### 6в — test_can.c ### 6в — test_can.c
**Файл:** `firmware/test/src/tests/test_can.c` **Файл:** `firmware/test/src/tests/test_can.c`
@ -223,28 +234,32 @@ confirm_request("can_tx_verify")
- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует - `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует
- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()` - 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:** **bsp_mqs:**
- MQS требует стерео PCM (L+R), на плате один физический канал - SAI3 + eDMA (DMA0 канал 0) + MQS периферия
- Буфер: всегда стерео (L == R, оба канала идентичны) - Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT`
- API минимальный: `bsp_mqs_init()`, `bsp_mqs_play(buf, len)`, `bsp_mqs_stop()`, `bsp_mqs_deinit()` - Усилитель 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:** **test_mqs:**
- Захардкоженная мелодия, 35 секунд
- `confirm_request("mqs_tone")` → оператор слышит → OK/FAIL - Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида
- Таймаут: 15 с (аналогично display) - Воспроизведение через `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` - `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_opto.py
tools/hil/06_test_firmware_can.py tools/hil/06_test_firmware_can.py
``` ```
@ -257,7 +272,7 @@ def firmware_cdc(m5):
""" """
Открывает USB CDC порт firmware_test. Открывает USB CDC порт firmware_test.
firmware_test уже прошит в Flash (не загружается pyOCD). 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}` - `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}`
- `run_test(id)` — запустить тест, вернуть test_result dict - `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:** **Justfile:**
```bash ```bash
@ -348,7 +350,7 @@ tools/shared/
**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP) **Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP)
```bash ```
┌─ Прошивка платы ─────────────────────────────────┐ ┌─ Прошивка платы ─────────────────────────────────┐
│ Обнаружен BootROM (SDP режим) │ │ Обнаружен BootROM (SDP режим) │
│ │ │ │
@ -366,7 +368,7 @@ tools/shared/
**Режим B — Диагностика** (триггер: session_start получен по CDC) **Режим B — Диагностика** (триггер: session_start получен по CDC)
```bash ```
┌─ Диагностика платы fw:0.1.0 ─────────────────────┐ ┌─ Диагностика платы fw:0.1.0 ─────────────────────┐
│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │ │ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │
├────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────┤
@ -377,8 +379,8 @@ tools/shared/
│ ☑ TFT Display │ mount failed: 5 │ │ ☑ TFT Display │ mount failed: 5 │
│ ☑ Кнопки │ display ✓ PASS │ │ ☑ Кнопки │ display ✓ PASS │
│ ☑ MQS Audio │ buttons ✓ PASS │ │ ☑ MQS Audio │ buttons ✓ PASS │
│ ☑ CAN loopback [HIL] │ ... │ ☑ CAN loopback [HIL] │ mqs ✓ PASS
│ ☑ Оптовходы [HIL] │ │ ☑ Оптовходы [HIL] │ ...
├────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────┤
│ [ Запустить выбранные ] [ Все тесты ] │ │ [ Запустить выбранные ] [ Все тесты ] │
│ ████████████████░░░░ 80% Тест: display │ │ ████████████████░░░░ 80% Тест: display │
@ -393,6 +395,7 @@ tools/shared/
| Тип теста | Источник confirm | Действие TUI | | Тип теста | Источник confirm | Действие TUI |
| -------------------- | ------------------ | --------------------------------------------- | | -------------------- | ------------------ | --------------------------------------------- |
| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown | | standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown |
| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown |
| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result | | standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result |
| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) | | HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) |
@ -432,7 +435,8 @@ just host::service-flash <bin> # прошить без TUI (для автом
``` ```
**Перепрошивка (нужна новая версия firmware_test или production):** **Перепрошивка (нужна новая версия firmware_test или production):**
```
```bash
1. Перемычка BOOT_MOD_1 → 3V3 1. Перемычка BOOT_MOD_1 → 3V3
2. Reset, подключить USB 2. Reset, подключить USB
3. TUI обнаружил 1FC9:0130 → Режим A 3. TUI обнаружил 1FC9:0130 → Режим A
@ -456,10 +460,9 @@ just host::service-flash <bin> # прошить без TUI (для автом
✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) ✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc)
✅ Этап 6е HIL pytest: 06_test_firmware_opto.py ✅ Этап 6е HIL pytest: 06_test_firmware_opto.py
✅ Этап 6ж HIL pytest: 06_test_firmware_can.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а tools/production/ скелет + models + clients
⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты) ⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты)
@ -474,8 +477,8 @@ just host::service-flash <bin> # прошить без TUI (для автом
## Зависимости между этапами ## Зависимости между этапами
```bash ```
✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → 6г (mqs) ✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → 6г (mqs)
✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest)

View file

@ -22,6 +22,7 @@
#include "cli.h" #include "cli.h"
#include "bsp/provisioning.h"
#include "bsp/usb_cdc.h" #include "bsp/usb_cdc.h"
#include "protocol.h" #include "protocol.h"
#include "test_runner.h" #include "test_runner.h"
@ -316,6 +317,20 @@ static void handle_cmd(const char *p_line)
return; 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) if (strcmp(cmd_name, "list_tests") == 0)
{ {
test_runner_send_list(); test_runner_send_list();

View file

@ -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 <stdint.h>
#include <string.h>
/* --------------------------------------------------------------------------
* Выбор режима: раскомментируй один
* ----------------------------------------------------------------------- */
//#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 * @file main.c
* @brief firmware_test точка входа. * @brief firmware_test точка входа.
@ -220,7 +18,6 @@ int main(void)
* 8. Главный цикл poll + cli_process * 8. Главный цикл poll + cli_process
*/ */
#include "board.h" #include "board.h"
#include "bsp/can.h"
#include "bsp/led.h" #include "bsp/led.h"
#include "bsp/tick.h" #include "bsp/tick.h"
#include "bsp/usb_cdc.h" #include "bsp/usb_cdc.h"
@ -232,9 +29,8 @@ int main(void)
int main(void) int main(void)
{ {
const uint32_t CONNECT_BLINK_MS = 200U; const uint32_t CONNECT_BLINK_MS = 200U;
const uint32_t ERROR_BLINK_MS = 250; const uint32_t ERROR_BLINK_MS = 250;
static const bsp_can_config_t K_CAN_CFG = { .bitrate = 125000U };
board_hw_init(); board_hw_init();
@ -267,4 +63,3 @@ int main(void)
test_runner_process(); test_runner_process();
} }
} }
#endif

View file

@ -146,3 +146,15 @@ void protocol_send_error(const char *p_code)
(void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code); (void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code);
cli_send(buf); 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);
}

View file

@ -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); 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_ */ #endif /* PROTOCOL_H_ */

View file

@ -215,7 +215,7 @@ sequenceDiagram
**SKIP — оператор нажал Cancel:** **SKIP — оператор нажал Cancel:**
``` ```bash
→ {"type":"confirm","id":"usd","confirmed":false} → {"type":"confirm","id":"usd","confirmed":false}
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator declined"} ← {"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) ## Запуск всего набора (run_all)
Тесты запускаются строго в порядке реестра. При провале критичного теста Тесты запускаются строго в порядке реестра. При провале критичного теста
@ -392,7 +445,17 @@ sequenceDiagram
← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} ← {"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":"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:** **SKIP-каскад при critical fail:**
@ -404,7 +467,7 @@ sequenceDiagram
← {"type":"test_begin","id":"usd",...} ← {"type":"test_begin","id":"usd",...}
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} ← {"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) | | 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) |
| 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | | 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) |
| 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | | 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) |
| 6 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | | 6 | `mqs` | MQS Audio Out | ❌ | ❌ | Interactive (in-run confirm) |
| 7 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | | 7 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) |
| 8 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) |
--- ---
@ -442,6 +506,8 @@ sequenceDiagram
| `display` | `<id> not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | | `display` | `<id> not confirmed` | Оператор не подтвердил / истёк таймаут 15 с |
| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | | `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с |
| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | | `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с |
| `mqs` | `mqs play error` | SAI3/DMA не запустился |
| `mqs` | `operator: no sound` | Нет звука / усилитель не работает |
| `opto` | `<id> mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | | `opto` | `<id> mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход |
| `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | | `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён |
| `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | | `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":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false},
{"id":"display","name":"TFT Display RGB888","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":"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":"opto","name":"Opto Inputs","critical":false,"requires_hil":true},
{"id":"can","name":"CAN loopback","critical":false,"requires_hil":true} {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}
]} ]}

View file

@ -1,19 +1,20 @@
/** /**
* @file test_mqs.c * @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 в цикле, * 2. run() воспроизвести мелодию (~4 с) с USB keepalive в цикле,
* затем confirm_request("mqs_tone") оператор подтверждает. * затем 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 с каждая. * Мелодия: стерео PCM16 44100 Гц, две ноты (A4/E5) по 2 с каждая.
* Буфер статический глобальный в некэшируемой секции (OCRAM NonCacheable). * Буфер статический глобальный в некэшируемой секции (OCRAM NonCacheable).
* L == R (монофонический выход на плате). * L == R (монофонический выход на плате).
* *
* Протокол: * Протокол:
* confirm_request("mqs_tone", "Слышен звуковой сигнал?", 15000) * confirm_request("mqs_tone", "Do you hear a tone?", 15000)
* confirm("mqs_tone", true/false) * confirm("mqs_tone", true/false)
* test_result pass/fail/skip * test_result pass/fail/skip
*/ */
@ -159,9 +160,17 @@ static bsp_status_t mqs_play_with_poll(void)
static void test_mqs_init(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(); (void) bsp_mqs_amp_init();
bsp_delay(300U);
(void) bsp_mqs_init();
mqs_build_melody();
} }
static test_result_t test_mqs_run(void) 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 при отказе или таймауте. * Таймаут 15 с аналогично display-тесту (закрытое решение, Этап 5).
* Таймаут детектируется отдельно через флаг в confirm_params_t. */ */
const confirm_params_t PARAMS = { const confirm_params_t K_PARAMS = {
.id = "mqs_tone", .id = "mqs_tone",
.prompt = "Have you heard a tone?", .prompt = "Do you hear a tone?",
.timeout_ms = 15000U, .timeout_ms = 15000U,
}; };
bool confirmed = test_runner_wait_confirm(&PARAMS); bool confirmed = test_runner_wait_confirm(&K_PARAMS);
if (!confirmed) if (!confirmed)
{ {
/* Различаем таймаут и явный отказ через поле last_confirm_timed_out
* если оно есть в API, иначе унифицируем как fail. */
return (test_result_t){ return (test_result_t){
.status = TEST_STATUS_FAIL, .status = TEST_STATUS_FAIL,
.detail = "operator: no sound", .detail = "operator: no sound",

View file

@ -62,7 +62,6 @@ add_sdk_driver(sai_edma fsl_sai_edma.c)
add_sdk_driver(edma fsl_edma.c) add_sdk_driver(edma fsl_edma.c)
add_sdk_driver(dmamux fsl_dmamux.c) add_sdk_driver(dmamux fsl_dmamux.c)
add_sdk_driver(xbara fsl_xbara.c) add_sdk_driver(xbara fsl_xbara.c)
add_sdk_driver(xbarb fsl_xbarb.c)
# Драйверы которые зависят от clock # Драйверы которые зависят от clock
target_link_libraries(sdk_lpuart PUBLIC sdk_clock) target_link_libraries(sdk_lpuart PUBLIC sdk_clock)