# bsp_qspi_flash: in progress

This commit is contained in:
Dmitry Akimov 2026-04-22 11:19:52 +03:00
parent 0a45805350
commit e4571c2d3b
11 changed files with 1772 additions and 19 deletions

View file

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

View file

@ -99,6 +99,7 @@ static void board_mpu_init(void)
void board_hw_init(void)
{
BOARD_InitPins();
BOARD_BootClockRUN();
board_mpu_init();

View file

@ -0,0 +1,17 @@
# bsp/qspi_flash/CMakeLists.txt BSP QSPI — W25Q64/128/256/512 через FlexSPI1.
if(BUILD_TESTS_HOST)
return()
endif()
add_library(bsp_qspi_flash STATIC src/qspi_flash.c)
target_include_directories(
bsp_qspi_flash
PUBLIC include/
PRIVATE src/)
target_link_libraries(
bsp_qspi_flash
PUBLIC bsp_status
PRIVATE bsp_board sdk_flexspi)

174
bsp/qspi_flash/README.md Normal file
View file

@ -0,0 +1,174 @@
# bsp_qspi_flash — QSPI Flash W25Q64/128/256/512
> Расположение: `bsp/qspi_flash/`
> Публичный заголовок: `bsp/qspi_flash/include/bsp/qspi_flash.h`
> Реализация: `bsp/qspi_flash/src/qspi_flash.c`
---
## Поддерживаемое железо
| Чип | JEDEC mfr | JEDEC cap | Размер | LUT-таблица |
|---------|-----------|-----------|--------|-------------|
| W25Q64 | 0xEF | 0x17 | 8 MB | `K_LUT_3B` |
| W25Q128 | 0xEF | 0x18 | 16 MB | `K_LUT_3B` |
| W25Q256 | 0xEF | 0x19 | 32 MB | `K_LUT_4B` |
| W25Q512 | 0xEF | 0x20 | 64 MB | `K_LUT_4B` |
Интерфейс MCU: **FlexSPI1, порт A1** (`FLEXSPI_PortA1`).
---
## XIP-безопасность
### Проблема
Прошивка исполняется XIP из того же Flash через FlexSPI AHB-интерфейс.
CPU непрерывно читает инструкции через AHB, который маршрутизируется по
LUT-слоту 0. Любая IP-команда FlexSPI прерывает AHB-путь на время
выполнения команды. Если в момент IP-команды CPU попытается фетчить
инструкцию из Flash — HardFault.
### Решение
Все функции, обращающиеся к регистрам FlexSPI (IPCMD, IPCR0/1, RFDR, TFDR,
LUT, INTR, …), размещены в **ITCM** через `AT_QUICKACCESS_SECTION_CODE`.
Инструкции этих функций загружаются в ITCM при старте и CPU не обращается
к Flash во время их выполнения.
AHB prefetch отключается (`AHBCR.PREFETCHEN = 0`) перед каждой серией
IP-транзакций и восстанавливается после (`qspi_ahb_disable` / `qspi_ahb_enable`).
Это дополнительно предотвращает спекулятивные AHB-чтения Flash во время
IP-команд.
---
## Стратегия адресации W25Q256/512
### Почему не Enter 4-Byte Mode (0xB7)
FDCB задаёт XIP слот 0 с 24-bit адресацией для **всех** чипов. Если
переключить чип в 4-byte mode командой 0xB7, следующий AHB-фетч
инструкции из Flash пойдёт через слот 0 с 24-bit адресом, а чип теперь
ждёт 32-bit — **HardFault**.
### Dedicated 4-byte address opcodes
W25Q256/512 имеют отдельный набор opcodes, принимающих 32-bit адрес
**независимо от текущего режима адресации чипа**:
| Операция | 3-byte opcode (W25Q64/128) | 4-byte opcode (W25Q256/512) |
|-------------------|----------------------------|-----------------------------|
| Sector Erase 4KB | `0x20` | `0x21` |
| Block Erase 32KB | `0x52` | `0x5C` |
| Block Erase 64KB | `0xD8` | `0xDC` |
| Quad Page Program | `0x32` | `0x34` |
| Quad Output Read | `0x6B` | `0x6C` |
Слот 0 (XIP) **не изменяется**. Чип остаётся в 24-bit режиме. XIP работает
непрерывно на всех чипах.
---
## LUT-слоты
| Слот | Константа | Команда | Зависит от чипа |
|------|----------------|----------------------------|-----------------|
| 0 | (XIP, FDCB) | Quad Read | Нет (не трогаем)|
| 1 | LSEQ_READ_SR1 | Read SR1 (0x05) | Нет |
| 2 | LSEQ_WR_EN | Write Enable (0x06) | Нет |
| 3 | LSEQ_ERASE_4K | Sector Erase (0x20/0x21) | Да |
| 4 | LSEQ_PP_QUAD | Quad Page Prog (0x32/0x34) | Да |
| 5 | LSEQ_JEDEC | Read JEDEC (0x9F) | Нет |
| 6 | LSEQ_READ_SR2 | Read SR2 (0x35) | Нет |
| 7 | LSEQ_WR_SR2 | Write SR2 (0x31) | Нет |
| 8 | LSEQ_READ_SR3 | Read SR3 (0x15) | Нет |
| 9 | LSEQ_ERASE_32K | Block Erase 32KB (0x52/0x5C)| Да |
| 10 | LSEQ_ERASE_64K | Block Erase 64KB (0xD8/0xDC)| Да |
| 11 | LSEQ_IP_READ | Quad Out Read (0x6B/0x6C) | Да |
---
## Публичный API
```c
/* Инициализация — первый вызов */
bsp_status_t bsp_qspi_init(void);
/* Чтение JEDEC ID */
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
/* Стирание */
bsp_status_t bsp_qspi_erase_sector(uint32_t addr); /* 4KB, ~45 мс */
bsp_status_t bsp_qspi_erase_block_32k(uint32_t addr); /* 32KB, ~120 мс */
bsp_status_t bsp_qspi_erase_block_64k(uint32_t addr); /* 64KB, ~150 мс */
/* Запись одной страницы */
bsp_status_t bsp_qspi_write_page(uint32_t addr, const uint8_t *p_data); /* ~3 мс */
/* Чтение произвольного числа байт (через IP-команду, не AHB) */
bsp_status_t bsp_qspi_read(uint32_t addr, uint8_t *p_buf, size_t size);
/* Размер Flash — доступен после init */
uint32_t bsp_qspi_flash_size(void);
```
---
## Выбор операции стирания для массовой очистки
| Объём очистки | Рекомендуемая операция | Примерное время |
|------------------|------------------------|-----------------|
| < 32 KB | `erase_sector` (4KB) | пропорционально |
| 32 KB — 1 MB | `erase_block_32k` | 120 мс / 32KB |
| > 1 MB | `erase_block_64k` | 150 мс / 64KB |
Пример: очистка 4 MB через 64KB блоки = 64 операции × 150 мс ≈ **9.6 с**
против 1024 × 45 мс ≈ **46 с** через 4KB секторы.
---
## Порядок вызова
```c
/* 1. Инициализация (один раз) */
if (bsp_qspi_init() != BSP_OK) { /* обработка ошибки */ }
/* 2. Операции */
bsp_qspi_erase_sector(addr); /* блокирует ~45 мс */
bsp_qspi_write_page(addr, buf); /* блокирует ~3 мс */
bsp_qspi_read(addr, buf, 256); /* быстро */
```
---
## CMake
```cmake
# bsp/qspi/CMakeLists.txt
target_link_libraries(bsp_qspi
PUBLIC bsp_status
PRIVATE bsp_board sdk_flexspi
)
```
Потребители (например, `firmware_test`) линкуют `bsp_qspi`:
```cmake
target_link_libraries(firmware_test PRIVATE ... bsp_qspi)
```
---
## Известные ограничения
- `bsp_qspi_write_page()` записывает строго одну страницу (256 байт).
Запись через границу страницы не поддерживается — вызывающий код
обязан выровнять адрес.
- Нет timeout в `qspi_wait_not_busy()`. Зависание из-за дефектного чипа
потребует watchdog reset. Для диагностической прошивки это приемлемо.
- Chip Erase (0xC7) не реализован — слишком деструктивно для XIP-прошивки.
Используй `erase_block_64k` в цикле по всему адресному пространству.

View file

@ -0,0 +1,169 @@
/**
* @file qspi_flash.h
* @brief Публичный API BSP QSPI W25Q64/128/256/512 через FlexSPI1.
*
* Поддерживаемые чипы Winbond:
*
* | Чип | JEDEC cap | Размер | Адресация в IP-командах |
* |---------|-----------|--------|-------------------------|
* | W25Q64 | 0x17 | 8 MB | 3-byte opcodes |
* | W25Q128 | 0x18 | 16 MB | 3-byte opcodes |
* | W25Q256 | 0x19 | 32 MB | Dedicated 4-byte opcodes |
* | W25Q512 | 0x20 | 64 MB | Dedicated 4-byte opcodes |
*
* Порядок вызова:
* 1. bsp_qspi_init() детекция чипа, загрузка LUT.
* 2. bsp_qspi_*() операции с Flash.
*
* XIP-безопасность:
* Слот 0 FlexSPI LUT (XIP read, задаётся FDCB) никогда не изменяется.
* Для W25Q256/512 используются dedicated 4-byte address opcodes
* без переключения режима адресации чипа (0xB7), что гарантирует
* непрерывность XIP на всех поддерживаемых чипах.
*
* @note Этот заголовок не включает NXP SDK headers.
*/
#ifndef BSP_QSPI_H_
#define BSP_QSPI_H_
#include "bsp/status.h"
#include <stddef.h>
#include <stdint.h>
/* ── Производитель ─────────────────────────────────────────────────────── */
/** @brief JEDEC Manufacturer ID: Winbond Electronics. */
#define BSP_QSPI_MFR_WINBOND 0xEFU
/* ── Коды ёмкости (JEDEC device_id, младший байт) ─────────────────────── */
#define BSP_QSPI_CAP_64MBIT 0x17U /**< W25Q64, 8 MB */
#define BSP_QSPI_CAP_128MBIT 0x18U /**< W25Q128, 16 MB */
#define BSP_QSPI_CAP_256MBIT 0x19U /**< W25Q256, 32 MB */
#define BSP_QSPI_CAP_512MBIT 0x20U /**< W25Q512, 64 MB */
/* ── Геометрия Flash ───────────────────────────────────────────────────── */
/** @brief Размер страницы, байт. */
#define BSP_QSPI_PAGE_SIZE 256U
/** @brief Размер сектора 4 KB, байт. */
#define BSP_QSPI_SECTOR_SIZE 0x1000U
/** @brief Размер блока 32 KB, байт. */
#define BSP_QSPI_BLOCK_32K_SIZE 0x8000U
/** @brief Размер блока 64 KB, байт. */
#define BSP_QSPI_BLOCK_64K_SIZE 0x10000U
/* ── Типы ──────────────────────────────────────────────────────────────── */
/**
* @brief JEDEC ID, прочитанный командой 0x9F.
*/
typedef struct
{
uint8_t manufacturer_id; /**< Производитель (0xEF = Winbond). */
uint16_t device_id; /**< [15:8]=тип памяти, [7:0]=код ёмкости. */
} bsp_qspi_jedec_t;
/* ── Публичный API ─────────────────────────────────────────────────────── */
/**
* @brief Инициализация QSPI-подсистемы.
*
* Читает JEDEC ID (0x9F), определяет чип Winbond, загружает LUT-слоты 1-11
* с opcode'ами, соответствующими режиму адресации чипа. Активирует Quad-
* режим (SR2.QE). Слот 0 (XIP, задан FDCB) не изменяется.
*
* Должна быть вызвана до любой другой bsp_qspi_* функции.
*
* @retval BSP_OK Чип опознан, LUT загружен, Quad включён.
* @retval BSP_ERR Ошибка FlexSPI или неизвестный/неподдерживаемый чип.
*/
bsp_status_t bsp_qspi_init(void);
/**
* @brief Чтение JEDEC ID (команда 0x9F).
*
* @param[out] p_jedec Результат. Не NULL.
* @retval BSP_OK / BSP_ERR.
*/
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
/**
* @brief Стирание сектора 4 KB.
*
* Блокирующая операция (~45 мс типичное). Во время стирания Flash BUSY
* IP-команды записи/стирания недоступны, XIP-чтения работают.
* Вызывающий код должен вызвать bsp_usb_cdc_poll() до и после.
*
* @param[in] addr Байтовый flash-адрес. Должен быть выровнен на
* BSP_QSPI_SECTOR_SIZE.
* @retval BSP_OK / BSP_ERR.
*/
bsp_status_t bsp_qspi_erase_sector(uint32_t addr);
/**
* @brief Стирание блока 32 KB.
*
* Блокирующая операция (~120 мс типичное, 1600 мс макс).
* Оптимален для очистки областей кратных 32 KB.
* Вызывающий код должен вызвать bsp_usb_cdc_poll() до и после.
*
* @param[in] addr Байтовый flash-адрес. Должен быть выровнен на
* BSP_QSPI_BLOCK_32K_SIZE.
* @retval BSP_OK / BSP_ERR.
*/
bsp_status_t bsp_qspi_erase_block_32k(uint32_t addr);
/**
* @brief Стирание блока 64 KB.
*
* Блокирующая операция (~150 мс типичное, 2000 мс макс).
* Оптимальный выбор для очистки многомегабайтных областей:
* 1 MB = 16 блоков × 150 мс 2.4 с (vs ~11.5 с через 4 KB секторы).
* Вызывающий код должен вызвать bsp_usb_cdc_poll() до и после.
*
* @param[in] addr Байтовый flash-адрес. Должен быть выровнен на
* BSP_QSPI_BLOCK_64K_SIZE.
* @retval BSP_OK / BSP_ERR.
*/
bsp_status_t bsp_qspi_erase_block_64k(uint32_t addr);
/**
* @brief Запись одной страницы (256 байт).
*
* Блокирующая операция (~3 мс типичное).
* Сектор должен быть предварительно стёрт.
*
* @param[in] addr Байтовый flash-адрес. Должен быть выровнен на
* BSP_QSPI_PAGE_SIZE.
* @param[in] p_data Указатель на BSP_QSPI_PAGE_SIZE байт. Не NULL.
* @retval BSP_OK / BSP_ERR.
*/
bsp_status_t bsp_qspi_write_page(uint32_t addr, const uint8_t *p_data);
/**
* @brief Чтение произвольного числа байт через IP-команду.
*
* Не использует AHB/XIP-путь. Корректно обрабатывает адреса выше 16 MB
* для W25Q256/512 через dedicated 4-byte read opcode (0x6C).
*
* @param[in] addr Байтовый flash-адрес.
* @param[out] p_buf Буфер приёма. Не NULL.
* @param[in] size Количество байт. > 0.
* @retval BSP_OK / BSP_ERR.
*/
bsp_status_t bsp_qspi_read(uint32_t addr, uint8_t *p_buf, size_t size);
/**
* @brief Возвращает размер Flash-чипа, определённый в bsp_qspi_init().
*
* @return Размер Flash в байтах, или 0 если init не была вызвана.
*/
uint32_t bsp_qspi_flash_size(void);
#endif /* BSP_QSPI_H_ */

View file

@ -0,0 +1,822 @@
/**
* @file qspi.c
* @brief BSP QSPI W25Q64/128/256/512 через FlexSPI1.
*
* ## XIP-безопасность
*
* Прошивка исполняется XIP из Flash через FlexSPI AHB-интерфейс (слот 0).
* Любая IP-команда FlexSPI прерывает AHB-путь на время выполнения.
* Чтобы CPU не пытался фетчить инструкции из Flash во время IP-команды,
* все функции, обращающиеся к регистрам FlexSPI, размещаются в ITCM через
* QSPI_RAMFUNC. AHB prefetch отключается перед каждой
* серией IP-транзакций и восстанавливается после.
*
* ## Стратегия адресации W25Q256/512
*
* Слот 0 FDCB использует 24-bit адресацию на всех чипах. Чтобы не сломать
* XIP, режим адресации чипа (команда 0xB7) не переключается. Вместо этого
* для W25Q256/512 загружаются dedicated 4-byte address opcodes:
* - Sector Erase: 0x20 0x21
* - Block Erase 32KB: 0x52 0x5C
* - Block Erase 64KB: 0xD8 0xDC
* - Quad Page Program: 0x32 0x34
* - IP Quad Output Read: 0x6B 0x6C
* Эти opcodes принимают 32-bit адрес независимо от текущего режима чипа.
*
* ## LUT-слоты
*
* Slot 0 XIP quad read (FDCB, НЕ трогается)
* Slot 1 Read Status Reg-1 (0x05)
* Slot 2 Write Enable (0x06)
* Slot 3 Sector Erase 4KB (0x20 / 0x21)
* Slot 4 Quad Page Program (0x32 / 0x34)
* Slot 5 Read JEDEC ID (0x9F)
* Slot 6 Read Status Reg-2 (0x35)
* Slot 7 Write Status Reg-2 (0x31)
* Slot 8 Read Status Reg-3 (0x15)
* Slot 9 Block Erase 32KB (0x52 / 0x5C)
* Slot 10 Block Erase 64KB (0xD8 / 0xDC)
* Slot 11 IP Quad Output Read (0x6B / 0x6C)
*/
#include "bsp/qspi_flash.h"
#include "fsl_common.h"
#include "fsl_flexspi.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/* ── Привязка к железу ──────────────────────────────────────────────────── */
/** @brief FlexSPI instance для W25Qxx. */
#define QSPI_BASE FLEXSPI
/** @brief FlexSPI порт, к которому подключён W25Qxx. */
#define QSPI_PORT kFLEXSPI_PortA1
/* ── Индексы LUT-слотов ─────────────────────────────────────────────────── */
#define LSEQ_READ_SR1 1U /**< Read Status Register 1 (0x05) */
#define LSEQ_WR_EN 2U /**< Write Enable (0x06) */
#define LSEQ_ERASE_4K 3U /**< Sector Erase 4KB */
#define LSEQ_PP_QUAD 4U /**< Quad Page Program */
#define LSEQ_JEDEC 5U /**< Read JEDEC ID (0x9F) */
#define LSEQ_READ_SR2 6U /**< Read Status Register 2 (0x35) */
#define LSEQ_WR_SR2 7U /**< Write Status Register 2(0x31) */
#define LSEQ_READ_SR3 8U /**< Read Status Register 3 (0x15) */
#define LSEQ_ERASE_32K 9U /**< Block Erase 32KB */
#define LSEQ_ERASE_64K 10U /**< Block Erase 64KB */
#define LSEQ_IP_READ 11U /**< IP Quad Output Read */
/** @brief Первый слот BSP (слот 0 = XIP FDCB). */
#define LUT_BSP_FIRST_SLOT 1U
/** @brief Количество слотов BSP (1..11). */
#define LUT_BSP_SLOT_COUNT 11U
/** @brief Слов на одну LUT-последовательность. */
#define LUT_WORDS_PER_SEQ 4U
/** @brief Всего слов в таблице (слоты 0-11). */
#define LUT_TOTAL_WORDS ((LSEQ_IP_READ + 1U) * LUT_WORDS_PER_SEQ)
/* ── Адресация ──────────────────────────────────────────────────────────── */
/** @brief 3-byte адресация: 24-bit (W25Q64/128, стандартные opcodes). */
#define ADDR_BITS_24 0x18U
/**
* @brief 4-byte адресация: 32-bit (W25Q256/512, dedicated opcodes).
*
* Используется с opcode-вариантами, принимающими 32-bit адрес без
* переключения режима адресации чипа командой 0xB7.
*/
#define ADDR_BITS_32 0x20U
/* ── Регистровые константы ──────────────────────────────────────────────── */
#define QSPI_LUT_KEY 0x5AF05AF0UL /**< Ключ для разблокировки LUT. */
#define QSPI_LUTCR_UNLOCK 0x02U /**< LUTCR: unlock. */
#define QSPI_LUTCR_LOCK 0x01U /**< LUTCR: lock. */
/* ── Биты статусных регистров ───────────────────────────────────────────── */
#define SR1_BUSY_BIT 0x01U /**< SR1.WIP — Write In Progress. */
#define SR2_QE_BIT 0x02U /**< SR2.QE — Quad Enable. */
/* ── Параметры FIFO ─────────────────────────────────────────────────────── */
/** @brief Байт на watermark-unit (TXWMRK=0 / RXWMRK=0 → 1 unit = 8 bytes). */
#define FIFO_WM_BYTES 8U
/** @brief 32-bit слов в одном watermark-unit. */
#define FIFO_WM_WORDS (FIFO_WM_BYTES / QSPI_RFDR_WORD_BYTES)
/** @brief Байт в одном слове RFDR/TFDR. */
#define QSPI_RFDR_WORD_BYTES 4U
/* ── Параметры SR-чтения ────────────────────────────────────────────────── */
/**
* @brief Размер читаемого буфера для однобайтных SR-команд.
*
* FlexSPI FIFO работает минимальными единицами в 4 байта (RXWMRK=0, 1 FILL
* unit = 4 bytes). Читаем 4 байта, используем только byte[0].
*/
#define SR_READ_LEN 4U
/* ── Размеры Flash по capacity byte ─────────────────────────────────────── */
#define QSPI_SIZE_8MB 0x00800000UL /**< W25Q64: 8 MB */
#define QSPI_SIZE_16MB 0x01000000UL /**< W25Q128: 16 MB */
#define QSPI_SIZE_32MB 0x02000000UL /**< W25Q256: 32 MB */
#define QSPI_SIZE_64MB 0x04000000UL /**< W25Q512: 64 MB */
/* ── Macro: построение LUT-таблицы ─────────────────────────────────────── */
/*
* Параметры:
* E4K opcode Sector Erase 4KB (0x20 | 0x21)
* E32K opcode Block Erase 32KB (0x52 | 0x5C)
* E64K opcode Block Erase 64KB (0xD8 | 0xDC)
* PP opcode Quad Page Program (0x32 | 0x34)
* RD opcode Quad Output Read (0x6B | 0x6C)
* ABITS биты адреса (ADDR_BITS_24 | ADDR_BITS_32)
*
* Слот 0 = нули (XIP FDCB, не записывается в HW).
* Слоты без data-фазы (Erase, WE) используют 1 word; PP и IP_READ 2 words;
* остальные words в слоте остаются нулями (STOP).
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
#define BUILD_LUT(E4K, E32K, E64K, PP, RD, ABITS) \
{ \
[LUT_WORDS_PER_SEQ * \
LSEQ_READ_SR1] = FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, 0x05U, \
kFLEXSPI_Command_READ_SDR, kFLEXSPI_1PAD, 0x04U), \
[LUT_WORDS_PER_SEQ * LSEQ_WR_EN] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, 0x06U, kFLEXSPI_Command_STOP, \
kFLEXSPI_1PAD, 0x00U), \
[LUT_WORDS_PER_SEQ * LSEQ_ERASE_4K] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, (E4K), \
kFLEXSPI_Command_RADDR_SDR, kFLEXSPI_1PAD, (ABITS)), \
[LUT_WORDS_PER_SEQ * LSEQ_PP_QUAD] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, (PP), \
kFLEXSPI_Command_RADDR_SDR, kFLEXSPI_1PAD, (ABITS)), \
[LUT_WORDS_PER_SEQ * LSEQ_PP_QUAD + 1U] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_WRITE_SDR, kFLEXSPI_4PAD, 0x04U, \
kFLEXSPI_Command_STOP, kFLEXSPI_1PAD, 0x00U), \
[LUT_WORDS_PER_SEQ * LSEQ_JEDEC] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, 0x9FU, \
kFLEXSPI_Command_READ_SDR, kFLEXSPI_1PAD, 0x04U), \
[LUT_WORDS_PER_SEQ * LSEQ_READ_SR2] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, 0x35U, \
kFLEXSPI_Command_READ_SDR, kFLEXSPI_1PAD, 0x04U), \
[LUT_WORDS_PER_SEQ * LSEQ_WR_SR2] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, 0x31U, \
kFLEXSPI_Command_WRITE_SDR, kFLEXSPI_1PAD, 0x04U), \
[LUT_WORDS_PER_SEQ * LSEQ_READ_SR3] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, 0x15U, \
kFLEXSPI_Command_READ_SDR, kFLEXSPI_1PAD, 0x04U), \
[LUT_WORDS_PER_SEQ * LSEQ_ERASE_32K] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, (E32K), \
kFLEXSPI_Command_RADDR_SDR, kFLEXSPI_1PAD, (ABITS)), \
[LUT_WORDS_PER_SEQ * LSEQ_ERASE_64K] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, (E64K), \
kFLEXSPI_Command_RADDR_SDR, kFLEXSPI_1PAD, (ABITS)), \
[LUT_WORDS_PER_SEQ * LSEQ_IP_READ] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_SDR, kFLEXSPI_1PAD, (RD), \
kFLEXSPI_Command_RADDR_SDR, kFLEXSPI_1PAD, (ABITS)), \
[LUT_WORDS_PER_SEQ * LSEQ_IP_READ + 1U] = \
FLEXSPI_LUT_SEQ(kFLEXSPI_Command_DUMMY_SDR, kFLEXSPI_4PAD, 0x08U, \
kFLEXSPI_Command_READ_SDR, kFLEXSPI_4PAD, 0x04U), \
}
/** @brief LUT для W25Q64/128 — стандартные 3-byte opcodes. */
static const uint32_t K_LUT_3B[LUT_TOTAL_WORDS] =
BUILD_LUT(0x20U, 0x52U, 0xD8U, 0x32U, 0x6BU, ADDR_BITS_24);
/** @brief LUT для W25Q256/512 — dedicated 4-byte opcodes. */
static const uint32_t K_LUT_4B[LUT_TOTAL_WORDS] =
BUILD_LUT(0x21U, 0x5CU, 0xDCU, 0x34U, 0x6CU, ADDR_BITS_32);
/* ── Состояние модуля ───────────────────────────────────────────────────── */
static bool g_s_init = false; /**< true после успешного init(). */
static uint32_t g_s_flash_size = 0U; /**< Размер Flash в байтах. */
/* ── ITCM: базовые примитивы FlexSPI ───────────────────────────────────── */
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static uint32_t qspi_irq_lock(void))
{
const uint32_t primask = __get_PRIMASK();
__disable_irq();
__DSB();
__ISB();
return primask;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_irq_unlock(uint32_t primask))
{
__DSB();
__ISB();
if ((primask & 1U) == 0U)
{
__enable_irq();
}
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_ahb_disable(void))
{
QSPI_BASE->AHBCR &= ~FLEXSPI_AHBCR_PREFETCHEN_MASK;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_ahb_enable(void))
{
QSPI_BASE->AHBCR |= FLEXSPI_AHBCR_PREFETCHEN_MASK;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_sw_reset(void))
{
QSPI_BASE->MCR0 |= FLEXSPI_MCR0_SWRESET_MASK;
while ((QSPI_BASE->MCR0 & FLEXSPI_MCR0_SWRESET_MASK) != 0U)
{
}
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_wait_idle(void))
{
while (((QSPI_BASE->STS0 & FLEXSPI_STS0_ARBIDLE_MASK) == 0U) ||
((QSPI_BASE->STS0 & FLEXSPI_STS0_SEQIDLE_MASK) == 0U))
{
}
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_check_error(uint32_t intr))
{
const uint32_t ERR_FLAGS = (uint32_t) kFLEXSPI_SequenceExecutionTimeoutFlag |
(uint32_t) kFLEXSPI_IpCommandSequenceErrorFlag |
(uint32_t) kFLEXSPI_IpCommandGrantTimeoutFlag;
const uint32_t ERR = intr & ERR_FLAGS;
status_t result = kStatus_Success;
if (ERR != 0U)
{
if ((ERR & (uint32_t) kFLEXSPI_SequenceExecutionTimeoutFlag) != 0U)
{
result = kStatus_FLEXSPI_SequenceExecutionTimeout;
}
else if ((ERR & (uint32_t) kFLEXSPI_IpCommandSequenceErrorFlag) != 0U)
{
result = kStatus_FLEXSPI_IpCommandSequenceError;
}
else
{
result = kStatus_FLEXSPI_IpCommandGrantTimeout;
}
QSPI_BASE->INTR = ERR;
QSPI_BASE->IPTXFCR |= FLEXSPI_IPTXFCR_CLRIPTXF_MASK;
QSPI_BASE->IPRXFCR |= FLEXSPI_IPRXFCR_CLRIPRXF_MASK;
}
return result;
}
/* ── ITCM: RX FIFO ──────────────────────────────────────────────────────── */
/**
* @brief Читает FIFO_WM_BYTES (8 байт) из RX FIFO в p_dst.
*
* Ожидает флага watermark-available, читает 2 слова, делает pop.
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_chunk(uint8_t *p_dst))
{
uint32_t intr;
status_t err;
do
{
intr = QSPI_BASE->INTR;
err = qspi_check_error(intr);
if (err != kStatus_Success)
{
return err;
}
} while ((intr & (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag) == 0U);
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
for (uint32_t w = 0U; w < FIFO_WM_WORDS; w++)
{
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
const uint32_t WORD = QSPI_BASE->RFDR[w];
const uint32_t BASE = w * QSPI_RFDR_WORD_BYTES;
p_dst[BASE + 0U] = (uint8_t) (WORD & 0xFFU);
p_dst[BASE + 1U] = (uint8_t) ((WORD >> 8U) & 0xFFU);
p_dst[BASE + 2U] = (uint8_t) ((WORD >> 16U) & 0xFFU);
p_dst[BASE + 3U] = (uint8_t) ((WORD >> 24U) & 0xFFU);
}
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag;
return kStatus_Success;
}
/**
* @brief Читает tail-байты (< FIFO_WM_BYTES) из RX FIFO.
*
* Ожидает через IPRXFSTS.FILL. Вызывать когда remain < FIFO_WM_BYTES.
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint32_t remain))
{
const uint32_t WORDS_NEEDED = (remain + QSPI_RFDR_WORD_BYTES - 1U) / QSPI_RFDR_WORD_BYTES;
bool done = false;
while (!done)
{
const uint32_t FILL =
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
if (FILL >= WORDS_NEEDED)
{
done = true;
}
else
{
const status_t ERR = qspi_check_error(QSPI_BASE->INTR);
if (ERR != kStatus_Success)
{
return ERR;
}
}
}
uint32_t bytes_left = remain;
for (uint32_t w = 0U; w < WORDS_NEEDED; w++)
{
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
const uint32_t WORD = QSPI_BASE->RFDR[w];
const uint32_t BYTES =
(bytes_left < QSPI_RFDR_WORD_BYTES) ? bytes_left : QSPI_RFDR_WORD_BYTES;
const uint32_t BASE = w * QSPI_RFDR_WORD_BYTES;
for (uint32_t b = 0U; b < BYTES; b++)
{
p_dst[BASE + b] = (uint8_t) ((WORD >> (8U * b)) & 0xFFU);
}
bytes_left -= BYTES;
}
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag;
return kStatus_Success;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_fifo(uint8_t *p_buf, uint32_t len))
{
uint32_t offset = 0U;
while ((len - offset) >= FIFO_WM_BYTES)
{
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) */
const status_t ERR = qspi_read_chunk(p_buf + offset);
if (ERR != kStatus_Success)
{
return ERR;
}
offset += FIFO_WM_BYTES;
}
if (offset < len)
{
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) */
return qspi_read_tail(p_buf + offset, len - offset);
}
return kStatus_Success;
}
/* ── ITCM: TX FIFO ──────────────────────────────────────────────────────── */
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_write_fifo(const uint8_t *p_buf, uint32_t len))
{
const uint8_t *p = p_buf;
uint32_t remain = len;
while (remain > 0U)
{
uint32_t intr;
status_t err;
do
{
intr = QSPI_BASE->INTR;
err = qspi_check_error(intr);
if (err != kStatus_Success)
{
return err;
}
} while ((intr & (uint32_t) kFLEXSPI_IpTxFifoWatermarkEmptyFlag) == 0U);
const uint32_t CHUNK = (remain > FIFO_WM_BYTES) ? FIFO_WM_BYTES : remain;
const uint32_t WR_WORDS = (CHUNK + QSPI_RFDR_WORD_BYTES - 1U) / QSPI_RFDR_WORD_BYTES;
for (uint32_t w = 0U; w < WR_WORDS; w++)
{
const uint32_t OFF = w * QSPI_RFDR_WORD_BYTES;
const uint32_t BYTES =
((CHUNK - OFF) < QSPI_RFDR_WORD_BYTES) ? (CHUNK - OFF) : QSPI_RFDR_WORD_BYTES;
uint32_t word = 0U;
for (uint32_t b = 0U; b < BYTES; b++)
{
word |= ((uint32_t) *p) << (8U * b);
p++;
}
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
QSPI_BASE->TFDR[w] = word;
}
remain -= CHUNK;
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpTxFifoWatermarkEmptyFlag;
}
return kStatus_Success;
}
/* ── ITCM: LUT update ───────────────────────────────────────────────────── */
/**
* @brief Записывает num_slots LUT-слотов начиная с first_slot.
*
* p_lut индексируется от слота 0 (слот 0 не записывается).
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_lut_update(uint32_t first_slot, const uint32_t *p_lut,
uint32_t num_slots))
{
qspi_wait_idle();
QSPI_BASE->LUTKEY = QSPI_LUT_KEY;
QSPI_BASE->LUTCR = QSPI_LUTCR_UNLOCK;
const uint32_t FIRST_WORD = first_slot * LUT_WORDS_PER_SEQ;
const uint32_t WORD_COUNT = num_slots * LUT_WORDS_PER_SEQ;
for (uint32_t i = 0U; i < WORD_COUNT; i++)
{
QSPI_BASE->LUT[FIRST_WORD + i] = p_lut[FIRST_WORD + i];
}
QSPI_BASE->LUTKEY = QSPI_LUT_KEY;
QSPI_BASE->LUTCR = QSPI_LUTCR_LOCK;
}
/* ── ITCM: IP-команды ───────────────────────────────────────────────────── */
/**
* @brief Подготавливает IP-транзакцию (общая часть для read/write/cmd).
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static void qspi_ip_setup(uint32_t seq_idx, uint32_t addr,
uint32_t data_len))
{
QSPI_BASE->FLSHCR2[QSPI_PORT] |= FLEXSPI_FLSHCR2_CLRINSTRPTR_MASK;
QSPI_BASE->INTR = FLEXSPI_INTR_AHBCMDERR_MASK | FLEXSPI_INTR_IPCMDERR_MASK |
FLEXSPI_INTR_AHBCMDGE_MASK | FLEXSPI_INTR_IPCMDGE_MASK;
QSPI_BASE->IPCR0 = addr;
QSPI_BASE->IPTXFCR |= FLEXSPI_IPTXFCR_CLRIPTXF_MASK;
QSPI_BASE->IPRXFCR |= FLEXSPI_IPRXFCR_CLRIPRXF_MASK;
QSPI_BASE->IPCR1 =
FLEXSPI_IPCR1_IDATSZ(data_len) | FLEXSPI_IPCR1_ISEQID(seq_idx) | FLEXSPI_IPCR1_ISEQNUM(0U);
QSPI_BASE->IPCMD |= FLEXSPI_IPCMD_TRG_MASK;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_read(uint32_t seq_idx, uint32_t addr,
uint8_t *p_rx, uint32_t data_len))
{
qspi_ip_setup(seq_idx, addr, data_len);
const status_t RESULT = qspi_read_fifo(p_rx, data_len);
qspi_wait_idle();
return RESULT;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_write(uint32_t seq_idx, uint32_t addr,
const uint8_t *p_tx, uint32_t data_len))
{
qspi_ip_setup(seq_idx, addr, data_len);
const status_t RESULT = qspi_write_fifo(p_tx, data_len);
qspi_wait_idle();
return RESULT;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_cmd(uint32_t seq_idx, uint32_t addr))
{
qspi_ip_setup(seq_idx, addr, 0U);
qspi_wait_idle();
return qspi_check_error(QSPI_BASE->INTR);
}
/* ── ITCM: Flash management ─────────────────────────────────────────────── */
/**
* @brief Ожидает снятия SR1.BUSY. Выполняется из ITCM на время erase/program.
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_wait_not_busy(void))
{
uint8_t sr1_buf[SR_READ_LEN];
status_t result;
do
{
result = qspi_ip_read(LSEQ_READ_SR1, 0U, sr1_buf, SR_READ_LEN);
if (result != kStatus_Success)
{
return result;
}
} while ((sr1_buf[0U] & SR1_BUSY_BIT) != 0U);
return kStatus_Success;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_write_enable(uint32_t addr))
{
return qspi_ip_cmd(LSEQ_WR_EN, addr);
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_sr(uint32_t seq_idx, uint8_t *p_val))
{
uint8_t buf[SR_READ_LEN];
const status_t RESULT = qspi_ip_read(seq_idx, 0U, buf, SR_READ_LEN);
if (RESULT == kStatus_Success)
{
*p_val = buf[0U];
}
return RESULT;
}
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_write_sr2(uint8_t val))
{
status_t result = qspi_write_enable(0U);
if (result != kStatus_Success)
{
return result;
}
const uint8_t BUF[1U] = { val };
result = qspi_ip_write(LSEQ_WR_SR2, 0U, BUF, 1U);
if (result != kStatus_Success)
{
return result;
}
return qspi_wait_not_busy();
}
/**
* @brief Гарантирует установку SR2.QE (Quad Enable).
*
* Если бит не установлен записывает SR2 через команду 0x31.
*/
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ensure_quad_enabled(void))
{
uint8_t sr2 = 0U;
const status_t RES = qspi_read_sr(LSEQ_READ_SR2, &sr2);
if (RES != kStatus_Success)
{
return RES;
}
if ((sr2 & SR2_QE_BIT) != 0U)
{
return kStatus_Success; /* Уже установлен */
}
return qspi_write_sr2((uint8_t) (sr2 | SR2_QE_BIT));
}
/* ── Вспомогательные функции (не ITCM) ─────────────────────────────────── */
AT_QUICKACCESS_SECTION_CODE(static bsp_status_t to_bsp(status_t s))
{
return (s == (status_t) kStatus_Success) ? BSP_OK : BSP_ERR_HW;
}
/**
* @brief Определяет параметры чипа по capacity byte JEDEC ID.
*
* @param[in] cap_byte Байт ёмкости (jedec_buf[2]).
* @param[out] p_flash_size Размер Flash в байтах.
* @param[out] p_lut Указатель на подходящую LUT-таблицу.
*/
AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_detect_chip(uint8_t cap_byte,
uint32_t *p_flash_size,
const uint32_t **p_lut))
{
switch (cap_byte)
{
case BSP_QSPI_CAP_64MBIT:
*p_flash_size = QSPI_SIZE_8MB;
*p_lut = K_LUT_3B;
break;
case BSP_QSPI_CAP_128MBIT:
*p_flash_size = QSPI_SIZE_16MB;
*p_lut = K_LUT_3B;
break;
case BSP_QSPI_CAP_256MBIT:
*p_flash_size = QSPI_SIZE_32MB;
*p_lut = K_LUT_4B;
break;
case BSP_QSPI_CAP_512MBIT:
*p_flash_size = QSPI_SIZE_64MB;
*p_lut = K_LUT_4B;
break;
default:
return BSP_ERR_HW;
}
return BSP_OK;
}
/**
* @brief Общая реализация erase-операций.
*
* Паттерн: AHBDisable WriteEnable IP cmd WaitBusy Reset AHBEnable.
*/
AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_do_erase(uint32_t seq_idx, uint32_t addr))
{
if (!g_s_init)
{
return BSP_ERR_HW;
}
const uint32_t primask = qspi_irq_lock();
qspi_ahb_disable();
status_t result = qspi_write_enable(addr);
if (result == kStatus_Success)
{
result = qspi_ip_cmd(seq_idx, addr);
}
if (result == kStatus_Success)
{
result = qspi_wait_not_busy();
}
qspi_sw_reset();
qspi_ahb_enable();
qspi_irq_unlock(primask);
return to_bsp(result);
}
/* ── Публичный API ──────────────────────────────────────────────────────── */
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_init(void))
{
const uint32_t PRIMASK = qspi_irq_lock();
bsp_status_t bsp_result;
qspi_ahb_disable();
/* Шаг 1: записываем JEDEC-слот (одинаков для всех чипов) */
qspi_lut_update(LSEQ_JEDEC, K_LUT_3B, 1U);
/* Шаг 2: читаем JEDEC ID */
uint8_t jedec_buf[SR_READ_LEN] = { 0U };
status_t result = qspi_ip_read(LSEQ_JEDEC, 0U, jedec_buf, SR_READ_LEN);
if (result != kStatus_Success)
{
bsp_result = BSP_ERR_HW;
goto DONE;
}
/* Шаг 3: проверяем производителя */
if (jedec_buf[0U] != BSP_QSPI_MFR_WINBOND)
{
bsp_result = BSP_ERR_HW;
goto DONE;
}
/* Шаг 4: определяем чип и выбираем LUT */
uint32_t flash_size = 0U;
const uint32_t *p_lut = K_LUT_3B;
const bsp_status_t DETECT = qspi_detect_chip(jedec_buf[2U], &flash_size, &p_lut);
if (DETECT != BSP_OK)
{
bsp_result = BSP_ERR_HW;
goto DONE;
}
/* Шаг 5: загружаем полный LUT (слоты 1-11) */
qspi_lut_update(LUT_BSP_FIRST_SLOT, p_lut, LUT_BSP_SLOT_COUNT);
/* Шаг 6: активируем Quad-режим */
result = qspi_ensure_quad_enabled();
if (result != kStatus_Success)
{
bsp_result = BSP_ERR_HW;
goto DONE;
}
g_s_flash_size = flash_size;
g_s_init = true;
bsp_result = BSP_OK;
DONE:
qspi_sw_reset();
qspi_ahb_enable();
qspi_irq_unlock(PRIMASK);
return bsp_result;
}
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec))
{
if (p_jedec == NULL)
{
return BSP_ERR_HW;
}
const uint32_t primask = qspi_irq_lock();
qspi_ahb_disable();
uint8_t buf[SR_READ_LEN] = { 0U };
const status_t RESULT = qspi_ip_read(LSEQ_JEDEC, 0U, buf, SR_READ_LEN);
qspi_sw_reset();
qspi_ahb_enable();
qspi_irq_unlock(primask);
if (RESULT != kStatus_Success)
{
return BSP_ERR_HW;
}
p_jedec->manufacturer_id = buf[0U];
p_jedec->device_id = ((uint16_t) buf[1U] << 8U) | (uint16_t) buf[2U];
return BSP_OK;
}
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_erase_sector(uint32_t addr))
{
return qspi_do_erase(LSEQ_ERASE_4K, addr);
}
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_erase_block_32k(uint32_t addr))
{
return qspi_do_erase(LSEQ_ERASE_32K, addr);
}
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_erase_block_64k(uint32_t addr))
{
return qspi_do_erase(LSEQ_ERASE_64K, addr);
}
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_write_page(uint32_t addr, const uint8_t *p_data))
{
if (!g_s_init || (p_data == NULL))
{
return BSP_ERR_HW;
}
const uint32_t PRIMASK = qspi_irq_lock();
qspi_ahb_disable();
status_t result = qspi_write_enable(addr);
if (result == kStatus_Success)
{
result = qspi_ip_write(LSEQ_PP_QUAD, addr, p_data, BSP_QSPI_PAGE_SIZE);
}
if (result == kStatus_Success)
{
result = qspi_wait_not_busy();
}
qspi_sw_reset();
qspi_ahb_enable();
qspi_irq_unlock(PRIMASK);
return to_bsp(result);
}
AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_read(uint32_t addr, uint8_t *p_buf, size_t size))
{
if (!g_s_init || (p_buf == NULL) || (size == 0U))
{
return BSP_ERR_HW;
}
const uint32_t PRIMASK = qspi_irq_lock();
qspi_ahb_disable();
/*
* Используем слот LSEQ_IP_READ (11), настроенный с корректным opcode и
* ABITS для данного чипа. Слот 0 (XIP FDCB) не используется его
* opcode и режим адресации зафиксированы FDCB и могут не совпадать
* с тем, что нужно для IP-чтений (особенно для W25Q256/512).
*/
const status_t RESULT = qspi_ip_read(LSEQ_IP_READ, addr, p_buf, (uint32_t) size);
qspi_sw_reset();
qspi_ahb_enable();
qspi_irq_unlock(PRIMASK);
return to_bsp(RESULT);
}
uint32_t bsp_qspi_flash_size(void)
{
return g_s_flash_size;
}

View file

@ -10,6 +10,7 @@ add_executable(
src/protocol.c
src/test_runner.c
src/tests/test_sdram.c
src/tests/test_qspi.c
${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE})
@ -17,14 +18,25 @@ add_executable(
target_include_directories(firmware_test PRIVATE src/)
target_compile_definitions(
${TARGET_NAME} PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1)
${TARGET_NAME}
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512
BOARD_MPU_SDRAM=1
__STARTUP_INITIALIZE_RAMFUNCTION
__STARTUP_CLEAR_BSS)
#
# -----------------------------------------------------------------------------
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно
# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны
# -----------------------------------------------------------------------------
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
bsp_boot_xip bsp_usb_cdc bsp_sdram)
target_link_libraries(
${TARGET_NAME}
PRIVATE bsp_board
bsp_led
bsp_tick
bsp_boot_xip
bsp_usb_cdc
bsp_sdram
bsp_qspi_flash)
# -----------------------------------------------------------------------------
# Linker script

View file

@ -19,6 +19,7 @@
*/
#include "board.h"
#include "bsp/led.h"
#include "bsp/qspi_flash.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "cli.h"
@ -31,12 +32,19 @@
int main(void)
{
const uint32_t CONNECT_BLINK_MS = 200U;
const uint32_t ERROR_BLINK_MS = 50;
const uint32_t ERROR_BLINK_MS = 250;
board_hw_init();
bsp_led_init();
bsp_status_t status = bsp_qspi_init();
bsp_tick_init();
while (1)
{
bsp_led_toggle(LED_HEARTBEAT);
bsp_delay(ERROR_BLINK_MS);
}
#if 0
if (bsp_usb_cdc_init() != BSP_OK)
{
bsp_led_toggle(LED_HEARTBEAT);
@ -62,4 +70,5 @@ int main(void)
cli_process();
test_runner_process();
}
#endif
}

View file

@ -42,9 +42,12 @@
#ifndef UNIT_TEST
extern const test_module_t K_TEST_SDRAM;
extern const test_module_t K_TEST_QSPI;
static const test_module_t *const k_registry[] = {
/* populated starting from Этап 2 */
&K_TEST_SDRAM,
&K_TEST_QSPI,
};
#define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0]))

View file

@ -13,7 +13,7 @@
- [Как читать эту таблицу](#как-читать-эту-таблицу)
- [test_sdram — SDRAM 32 MB](#test_sdram--sdram-32-mb)
- [test_qspi — QSPI Flash 8 MB](#test_qspi--qspi-flash-8-mb) *(запланирован)*
- [test_qspi — QSPI Flash W25Qxx](#test_qspi--qspi-flash-w25qxx)
- [test_usd — uSD SDIO](#test_usd--usd-sdio) *(запланирован)*
- [test_display — Display RGB888](#test_display--display-rgb888) *(запланирован)*
- [test_buttons — Test_But_1 / Test_But_2](#test_buttons--test_but_1--test_but_2) *(запланирован)*
@ -186,9 +186,9 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани
---
## test_qspi — QSPI Flash 8 MB
## test_qspi — QSPI Flash W25Qxx
**Файл:** `test_qspi.c` *(не реализован — Этап 3)*
**Файл:** `test_qspi.c`
**ID:** `qspi`
**Critical:** ✅ | **HIL:** ❌ | **Confirm:**
@ -196,22 +196,177 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани
| Параметр | Значение |
|---|---|
| Чип | W25Q64FVSSIG |
| Объём | 8 MB |
| Интерфейс | FlexSPI |
| Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 |
| Объём | 8 / 16 / 32 / 64 MB |
| Интерфейс MCU | FlexSPI1, порт A1 |
| BSP | `bsp_qspi` |
| Тестовый сектор | `flash_size - 4KB` (последний, динамически) |
### Что будет тестировать
### Инварианты выполнения
- JEDEC ID верификация: manufacturer=`0xEF`, device=`0x4017`.
- Erase последнего сектора (4 KB, не затрагивает прошивку).
- Write 256 байт → Read → Compare.
- `bsp_qspi_init()` вызывается в `init()` — если чип не опознан, `run()` возвращает FAIL.
- Слот 0 FlexSPI LUT (XIP read, задан FDCB) **никогда не изменяется** — прошивка
выполняется XIP на протяжении всего теста.
- Во время операций stирания/записи (~45150 мс) Flash находится в состоянии BUSY.
W25Q64/128/256/512 допускают READ-команды в состоянии BUSY — XIP-фетчи инструкций
продолжают работать.
- Тестовый сектор расположен в конце Flash и **не пересекается** с прошивкой
(прошивка занимает первые несколько MB).
- `bsp_usb_cdc_poll()` вызывается вокруг каждой блокирующей операции — USB CDC
остаётся отзывчивым во время теста.
### Гарантии при PASS
### Что тестирует — четыре шага
- FlexSPI контроллер инициализирован.
- Чип отвечает с ожидаемым JEDEC ID.
- Базовые операции чтения/записи/стирания работают.
#### Шаг 1: JEDEC ID (~1 мс)
Команда 0x9F. Читает manufacturer ID и device ID.
**Проверяет:**
- `manufacturer_id == 0xEF` (Winbond).
- `capacity_byte` принадлежит одному из: 0x17 (64Mbit), 0x18 (128Mbit),
0x19 (256Mbit), 0x20 (512Mbit).
По результату вычисляется адрес тестового сектора:
`test_addr = bsp_qspi_flash_size() - BSP_QSPI_SECTOR_SIZE`.
**Ловит:**
- FlexSPI контроллер не инициализирован или не отвечает.
- Неизвестный / неподдерживаемый чип.
- Обрыв или неправильное подключение QSPI-шины.
**Не ловит:**
- Деградацию конкретных ячеек памяти.
---
#### Шаг 2: Erase + Verify (~500 мс)
Стирает тестовый сектор (4KB). Читает первые 256 байт сектора.
Все байты должны быть `0xFF`.
**Покрытие:** способность чипа выполнить Sector Erase и корректно сообщить о завершении.
**Ловит:**
- Erase command не принимается чипом (неправильный opcode / режим адресации).
- SR1.WIP не снимается после стирания (erase не завершился).
- Данные после стирания не `0xFF` (ячейки застряли в `0x00`).
**Не ловит:**
- Проблемы с ячейками вне первой страницы тестового сектора.
---
#### Шаг 3: Write + Read + Compare (~200 мс)
Паттерн `byte[i] = i & 0xFF` (256 байт). Записывает первую страницу
тестового сектора командой Quad Page Program. Читает обратно командой
IP Quad Output Read (слот 11). Сравнивает побайтово.
**Покрытие:** полный цикл write → read на 256 байтах.
**Ловит:**
- Ошибки Page Program (неправильный opcode / адрес / данные).
- Ошибки IP Read (неправильный opcode / адрес).
- Stuck-at bits в ячейках тестовой страницы.
- Шина данных Flash (все 4 линии Quad-режима).
**Не ловит:**
- Деградацию ячеек вне первой страницы тестового сектора.
- Aliasing адресов (это проверяет Шаг 4).
---
#### Шаг 4: Address range — только W25Q256/512 (~100 мс)
Для чипов с `flash_size > 16 MB`.
Для W25Q64/128 шаг пропускается (24-bit покрывает весь чип).
**Проблема, которую ловит:**
При 24-bit адресации адрес обрезается до 24 бит. Для W25Q256/512
last-sector находится выше 16 MB:
```
W25Q256: last sector = 0x1FFF000, 0x1FFF000 & 0xFFFFFF = 0xFFF000
W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
```
Если dedicated 4-byte opcodes не работают (чип игнорирует старший байт
адреса), Шаг 3 **формально проходит** — запись и чтение ошибаются в одно и
то же место (`0xFFF000`) одинаково. Алиасирование остаётся незамеченным.
**Метод:**
После Шага 3 `test_addr` содержит паттерн `i & 0xFF`. Записываем `0xAA`
в anchor-сектор (`0x00FFF000`). Читаем `test_addr`:
- Получаем `i & 0xFF``test_addr` — это физически другой сектор → `PASS`.
- Получаем `0xAA``test_addr` физически совпадает с anchor → алиасирование
подтверждено → dedicated 4-byte opcodes не работают → `FAIL`.
**Ловит:**
- 24-bit алиасирование адресов для W25Q256/512.
- Несовместимость LUT dedicated 4-byte opcodes с конкретным чипом.
**Не ловит:**
- Алиасирование на W25Q64/128 (невозможно по архитектуре).
---
### Гарантии теста при PASS
- FlexSPI1 инициализирован и отвечает.
- Подключён поддерживаемый Winbond Flash с ожидаемым JEDEC ID.
- Команды Sector Erase, Quad Page Program, IP Read работают корректно.
- Для W25Q256/512: dedicated 4-byte address opcodes функционируют и
адресуют физически разные секторы выше и ниже 16 MB.
### Интерпретация FAIL
`detail` содержит текстовое описание шага и причины:
| Паттерн `detail` | Шаг | Вероятный диагноз |
|---|---|---|
| `JEDEC: read failed` | 1 | FlexSPI не инициализирован / нет связи |
| `JEDEC: mfr=0xXX exp=0xEF` | 1 | Неизвестный производитель чипа |
| `JEDEC: unknown cap=0xXX` | 1 | Незнакомая ёмкость, чип не в списке поддержки |
| `erase: sector erase failed` | 2 | FlexSPI ошибка при erase command |
| `erase verify failed at 0x…` | 2 | Сектор не стёрся, ячейки застряли |
| `rw: page write failed` | 3 | FlexSPI ошибка при Page Program |
| `rw mismatch at 0x… exp=… got=…` | 3 | Stuck-at bit или ошибка шины данных |
| `addr alias: 0x… mirrors 0x…` | 4 | Алиасирование — 4-byte opcodes не работают |
| `addr range mismatch at 0x…` | 4 | Неожиданные данные при чтении last sector |
**Анализ `rw mismatch`:** `exp XOR got` — биты, где `(exp ^ got) != 0`,
соответствуют сбойным линиям Quad-шины.
### Ограничения
- Тестирует только **1 страницу** (256 байт) — не покрывает весь объём чипа.
- Не проверяет endurance (многократные write/erase циклы).
- Не является заменой полного тестирования Flash (march-алгоритмы).
- Шаг 4 не проверяет адреса в диапазоне 1632 MB (только крайние точки).
### Типичное время выполнения
| Шаг | Чип | ~Время |
|---|---|---|
| 1: JEDEC ID | все | < 1 мс |
| 2: Erase + Verify | все | ~500 мс |
| 3: Write + Read | все | ~200 мс |
| 4: Address range | W25Q64/128 | 0 мс (skip) |
| 4: Address range | W25Q256/512 | ~100 мс |
| **Итого** | W25Q64/128 | **~700 мс** |
| **Итого** | W25Q256/512 | **~800 мс** |
---

View file

@ -0,0 +1,390 @@
/**
* @file test_qspi.c
* @brief Тест-модуль firmware_test: QSPI Flash W25Q64/128/256/512.
*
* Четыре шага:
*
* Шаг 1 JEDEC ID (~1 мс)
* Команда 0x9F. Проверяем manufacturer=0xEF (Winbond) и что capacity
* byte принадлежит известному чипу. По результату вычисляем адрес
* тестового сектора (последний 4KB в Flash) и размер чипа.
*
* Шаг 2 Erase + Verify (~500 мс)
* Стираем последний сектор. Читаем 256 байт, все должны быть 0xFF.
*
* Шаг 3 Write + Read + Compare (~200 мс)
* Паттерн: byte[i] = i & 0xFF, 256 байт. Пишем на первую страницу
* тестового сектора. Читаем и сравниваем побайтово.
*
* Шаг 4 Address range (только W25Q256/512, ~100 мс)
* Детектирует алиасирование адресов при неработающих dedicated 4-byte
* opcodes. Пишем 0xAA в anchor сектор (0x00FFF000), читаем test_addr.
* Если читаем 0xAA test_addr физически алиасирует на anchor_addr
* IP-команды работают в 3-byte режиме вместо 4-byte FAIL.
*
* Тестовый сектор = bsp_qspi_flash_size() - BSP_QSPI_SECTOR_SIZE.
* Адрес вычисляется после Шага 1 динамически поддерживает все чипы.
*/
#include "bsp/qspi_flash.h"
#include "bsp/usb_cdc.h"
#include "test_module.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/** @brief Число байт верификации после erase (первая страница сектора). */
#define QSPI_ERASE_VERIFY_LEN BSP_QSPI_PAGE_SIZE
/** @brief Ожидаемый байт в стёртом секторе. */
#define QSPI_ERASED_BYTE 0xFFU
/**
* @brief Граница 16 MB максимум 24-bit адресного пространства.
*
* Для чипов с flash_size > 16 MB (W25Q256/512) проверяется алиасирование.
*/
#define QSPI_ADDR_16MB 0x01000000UL
/**
* @brief Адрес anchor-сектора для проверки алиасирования.
*
* При 24-bit алиасировании:
* W25Q256: 0x1FFF000 & 0xFFFFFF = 0xFFF000 этот адрес
* W25Q512: 0x3FFF000 & 0xFFFFFF = 0xFFF000 тот же адрес
*
* Если dedicated 4-byte opcodes не работают, запись/чтение last-sector
* фактически попадает на этот адрес.
*/
#define QSPI_ALIAS_ANCHOR_ADDR 0x00FFF000UL
/** @brief Паттерн anchor-страницы. Отличается от основного (i & 0xFF). */
#define QSPI_ANCHOR_PATTERN 0xAAU
/* ── Буферы ─────────────────────────────────────────────────────────────── */
static uint8_t g_s_write_buf[BSP_QSPI_PAGE_SIZE]; /**< Данные для записи. */
static uint8_t g_s_read_buf[BSP_QSPI_PAGE_SIZE]; /**< Данные после чтения. */
static uint8_t g_s_anchor_buf[BSP_QSPI_PAGE_SIZE]; /**< Anchor для Шага 4. */
/* ── Состояние модуля ───────────────────────────────────────────────────── */
static bool g_s_ready = false;
/* ── Вспомогательные функции ────────────────────────────────────────────── */
static test_result_t make_fail(const char *p_detail)
{
test_result_t result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U };
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", p_detail);
return result;
}
/* ── Шаг 1: JEDEC ID ────────────────────────────────────────────────────── */
/**
* @brief Читает JEDEC ID, верифицирует производителя и chip capacity.
*
* @param[out] p_test_addr Адрес тестового сектора (последний 4KB).
* @param[out] p_result FAIL-результат при ошибке.
* @return true при успехе.
*/
static bool step_jedec(uint32_t *p_test_addr, test_result_t *p_result)
{
bsp_qspi_jedec_t jedec = { .manufacturer_id = 0U, .device_id = 0U };
bsp_usb_cdc_poll();
if (bsp_qspi_read_jedec_id(&jedec) != BSP_OK)
{
*p_result = make_fail("JEDEC: read failed");
return false;
}
if (jedec.manufacturer_id != BSP_QSPI_MFR_WINBOND)
{
char detail[TEST_DETAIL_SIZE];
(void) snprintf(detail, sizeof(detail), "JEDEC: mfr=0x%02X exp=0x%02X",
(unsigned int) jedec.manufacturer_id, (unsigned int) BSP_QSPI_MFR_WINBOND);
*p_result = make_fail(detail);
return false;
}
const uint8_t CAP = (uint8_t) (jedec.device_id & 0xFFU);
switch (CAP)
{
case BSP_QSPI_CAP_64MBIT:
case BSP_QSPI_CAP_128MBIT:
case BSP_QSPI_CAP_256MBIT:
case BSP_QSPI_CAP_512MBIT:
break;
default:
{
char detail[TEST_DETAIL_SIZE];
(void) snprintf(detail, sizeof(detail), "JEDEC: unknown cap=0x%02X dev=0x%04X",
(unsigned int) CAP, (unsigned int) jedec.device_id);
*p_result = make_fail(detail);
return false;
}
}
*p_test_addr = bsp_qspi_flash_size() - BSP_QSPI_SECTOR_SIZE;
return true;
}
/* ── Шаг 2: Erase + Verify ──────────────────────────────────────────────── */
/**
* @brief Стирает тестовый сектор и верифицирует (все байты = 0xFF).
*
* @param[in] test_addr Адрес тестового сектора.
* @param[out] p_result FAIL-результат при ошибке.
* @return true при успехе.
*/
static bool step_erase(uint32_t test_addr, test_result_t *p_result)
{
bsp_usb_cdc_poll();
if (bsp_qspi_erase_sector(test_addr) != BSP_OK)
{
*p_result = make_fail("erase: sector erase failed");
return false;
}
bsp_usb_cdc_poll();
(void) memset(g_s_read_buf, 0x00, sizeof(g_s_read_buf));
if (bsp_qspi_read(test_addr, g_s_read_buf, QSPI_ERASE_VERIFY_LEN) != BSP_OK)
{
*p_result = make_fail("erase verify: read failed");
return false;
}
bsp_usb_cdc_poll();
for (uint32_t i = 0U; i < QSPI_ERASE_VERIFY_LEN; i++)
{
if (g_s_read_buf[i] != QSPI_ERASED_BYTE)
{
char detail[TEST_DETAIL_SIZE];
(void) snprintf(detail, sizeof(detail),
"erase verify failed at 0x%08" PRIX32 " got=0x%02X", test_addr + i,
(unsigned int) g_s_read_buf[i]);
*p_result = make_fail(detail);
return false;
}
}
return true;
}
/* ── Шаг 3: Write + Read + Compare ─────────────────────────────────────── */
/**
* @brief Пишет паттерн (i & 0xFF) и верифицирует побайтово.
*
* @param[in] test_addr Адрес тестового сектора.
* @param[out] p_result FAIL-результат при ошибке.
* @return true при успехе.
*/
static bool step_write_read(uint32_t test_addr, test_result_t *p_result)
{
for (uint32_t i = 0U; i < BSP_QSPI_PAGE_SIZE; i++)
{
g_s_write_buf[i] = (uint8_t) (i & 0xFFU);
}
bsp_usb_cdc_poll();
if (bsp_qspi_write_page(test_addr, g_s_write_buf) != BSP_OK)
{
*p_result = make_fail("rw: page write failed");
return false;
}
bsp_usb_cdc_poll();
(void) memset(g_s_read_buf, 0x00, sizeof(g_s_read_buf));
if (bsp_qspi_read(test_addr, g_s_read_buf, BSP_QSPI_PAGE_SIZE) != BSP_OK)
{
*p_result = make_fail("rw: page read failed");
return false;
}
bsp_usb_cdc_poll();
for (uint32_t i = 0U; i < BSP_QSPI_PAGE_SIZE; i++)
{
if (g_s_read_buf[i] != g_s_write_buf[i])
{
char detail[TEST_DETAIL_SIZE];
(void) snprintf(detail, sizeof(detail),
"rw mismatch at 0x%08" PRIX32 " exp=0x%02X got=0x%02X", test_addr + i,
(unsigned int) g_s_write_buf[i], (unsigned int) g_s_read_buf[i]);
*p_result = make_fail(detail);
return false;
}
}
return true;
}
/* ── Шаг 4: Address range ───────────────────────────────────────────────── */
/**
* @brief Верифицирует отсутствие адресного алиасирования (W25Q256/512).
*
* После step_write_read: test_addr содержит паттерн i & 0xFF.
* Записываем 0xAA в anchor_addr (0x00FFF000). Если test_addr физически
* совпадает с anchor_addr (24-bit wrap), чтение test_addr вернёт 0xAA.
*
* Для W25Q64/128 (flash_size 16 MB): шаг пропускается.
*
* @param[in] test_addr Адрес тестового сектора.
* @param[in] flash_size Полный размер Flash.
* @param[out] p_result FAIL-результат при ошибке.
* @return true если адресация верна или тест неприменим.
*/
static bool step_address_range(uint32_t test_addr, uint32_t flash_size, test_result_t *p_result)
{
if (flash_size <= QSPI_ADDR_16MB)
{
return true; /* W25Q64/128: 24-bit покрывает весь чип */
}
/* Шаг A: стереть anchor сектор */
bsp_usb_cdc_poll();
if (bsp_qspi_erase_sector(QSPI_ALIAS_ANCHOR_ADDR) != BSP_OK)
{
*p_result = make_fail("addr range: anchor erase failed");
return false;
}
bsp_usb_cdc_poll();
/* Шаг B: записать 0xAA на anchor_addr */
(void) memset(g_s_anchor_buf, QSPI_ANCHOR_PATTERN, sizeof(g_s_anchor_buf));
if (bsp_qspi_write_page(QSPI_ALIAS_ANCHOR_ADDR, g_s_anchor_buf) != BSP_OK)
{
*p_result = make_fail("addr range: anchor write failed");
(void) bsp_qspi_erase_sector(QSPI_ALIAS_ANCHOR_ADDR);
return false;
}
bsp_usb_cdc_poll();
/* Шаг C: прочитать test_addr — должен содержать паттерн i & 0xFF */
(void) memset(g_s_read_buf, 0x00, sizeof(g_s_read_buf));
if (bsp_qspi_read(test_addr, g_s_read_buf, BSP_QSPI_PAGE_SIZE) != BSP_OK)
{
(void) bsp_qspi_erase_sector(QSPI_ALIAS_ANCHOR_ADDR);
*p_result = make_fail("addr range: high addr read failed");
return false;
}
bsp_usb_cdc_poll();
/* Шаг D: верификация */
bool ok = true;
char detail[TEST_DETAIL_SIZE];
detail[0U] = '\0';
for (uint32_t i = 0U; i < BSP_QSPI_PAGE_SIZE; i++)
{
const uint8_t EXPECTED = (uint8_t) (i & 0xFFU);
const uint8_t GOT = g_s_read_buf[i];
if (GOT != EXPECTED)
{
if (GOT == QSPI_ANCHOR_PATTERN)
{
(void) snprintf(detail, sizeof(detail),
"addr alias: 0x%08" PRIX32 " mirrors 0x%08" PRIX32 " (3-byte wrap)",
test_addr, QSPI_ALIAS_ANCHOR_ADDR);
}
else
{
(void) snprintf(detail, sizeof(detail),
"addr range mismatch at 0x%08" PRIX32 " exp=0x%02X got=0x%02X",
test_addr + i, (unsigned int) EXPECTED, (unsigned int) GOT);
}
ok = false;
break;
}
}
/* Шаг E: cleanup anchor */
bsp_usb_cdc_poll();
(void) bsp_qspi_erase_sector(QSPI_ALIAS_ANCHOR_ADDR);
bsp_usb_cdc_poll();
if (!ok)
{
*p_result = make_fail(detail);
return false;
}
return true;
}
/* ── Реализация тест-модуля ─────────────────────────────────────────────── */
static void qspi_test_init(void)
{
g_s_ready = (bsp_qspi_init() == BSP_OK);
}
static test_result_t qspi_test_run(void)
{
if (!g_s_ready)
{
return make_fail("init failed — FlexSPI or unknown chip");
}
test_result_t fail_result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U };
uint32_t test_addr = 0U;
if (!step_jedec(&test_addr, &fail_result))
{
return fail_result;
}
if (!step_erase(test_addr, &fail_result))
{
return fail_result;
}
if (!step_write_read(test_addr, &fail_result))
{
return fail_result;
}
if (!step_address_range(test_addr, bsp_qspi_flash_size(), &fail_result))
{
return fail_result;
}
return (test_result_t){
.status = TEST_STATUS_PASS,
.duration_ms = 0U,
.detail = { 0 },
};
}
static void qspi_test_deinit(void)
{
g_s_ready = false;
}
/* ── Дескриптор модуля ──────────────────────────────────────────────────── */
const test_module_t K_TEST_QSPI = {
.id = "qspi",
.name = "QSPI Flash W25Qxx",
.critical = true,
.requires_hil = false,
.pre_confirm_prompt = NULL,
.init = qspi_test_init,
.run = qspi_test_run,
.deinit = qspi_test_deinit,
};