diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index f1aff58..5943fbe 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -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) diff --git a/bsp/generated/board.c b/bsp/generated/board.c index 419022e..73b70c8 100644 --- a/bsp/generated/board.c +++ b/bsp/generated/board.c @@ -99,6 +99,7 @@ static void board_mpu_init(void) void board_hw_init(void) { + BOARD_InitPins(); BOARD_BootClockRUN(); board_mpu_init(); diff --git a/bsp/qspi_flash/CMakeLists.txt b/bsp/qspi_flash/CMakeLists.txt new file mode 100644 index 0000000..e11ba5b --- /dev/null +++ b/bsp/qspi_flash/CMakeLists.txt @@ -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) diff --git a/bsp/qspi_flash/README.md b/bsp/qspi_flash/README.md new file mode 100644 index 0000000..95882f1 --- /dev/null +++ b/bsp/qspi_flash/README.md @@ -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` в цикле по всему адресному пространству. \ No newline at end of file diff --git a/bsp/qspi_flash/include/bsp/qspi_flash.h b/bsp/qspi_flash/include/bsp/qspi_flash.h new file mode 100644 index 0000000..20ee9d7 --- /dev/null +++ b/bsp/qspi_flash/include/bsp/qspi_flash.h @@ -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 +#include + +/* ── Производитель ─────────────────────────────────────────────────────── */ + +/** @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_ */ \ No newline at end of file diff --git a/bsp/qspi_flash/src/qspi_flash.c b/bsp/qspi_flash/src/qspi_flash.c new file mode 100644 index 0000000..03171cb --- /dev/null +++ b/bsp/qspi_flash/src/qspi_flash.c @@ -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 +#include +#include + +/* ── Привязка к железу ──────────────────────────────────────────────────── */ + +/** @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; +} diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index e0d9754..d87d125 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -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 diff --git a/firmware/test/src/main.c b/firmware/test/src/main.c index f312566..c2e9695 100644 --- a/firmware/test/src/main.c +++ b/firmware/test/src/main.c @@ -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 } diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index d3f3e8e..59e8600 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -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])) diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README.md index 3f34c42..4e2bbdf 100644 --- a/firmware/test/src/tests/README.md +++ b/firmware/test/src/tests/README.md @@ -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ирания/записи (~45–150 мс) 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 не проверяет адреса в диапазоне 16–32 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 мс** | --- @@ -415,4 +570,4 @@ static const test_module_t *const k_registry[] = { ``` Critical тесты идут первыми — при их провале HIL и интерактивные тесты -пропускаются автоматически, экономя время диагностики. +пропускаются автоматически, экономя время диагностики. \ No newline at end of file diff --git a/firmware/test/src/tests/test_qspi.c b/firmware/test/src/tests/test_qspi.c new file mode 100644 index 0000000..64c104c --- /dev/null +++ b/firmware/test/src/tests/test_qspi.c @@ -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 +#include +#include +#include +#include +#include + +/* ── Константы ─────────────────────────────────────────────────────────── */ + +/** @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, +}; \ No newline at end of file