41в. Quad-SPI: внешняя флеш как память и код из неё
Раздел 41 назвал главное свойство Quad-SPI: контроллер показывает внешнюю флеш как обычную область адресов, и из неё можно даже исполнять код. Здесь — как это устроено: из каких фаз состоит команда, почему чтение на четырёх линиях в четыре раза быстрее не во всём, как включить отображение в память по регистрам, как стирать и писать ту же флеш и как положить в неё код.
Опоры: справочное руководство RM0385 (F74x/F75x, глава QUADSPI), руководство по ядру Cortex-M7 (DUI 0646B), пакет поддержки платы STM32746G-Discovery и драйвер флеш N25Q128A из пака Keil, описание пака (алгоритмы программирования). Код собран armclang для F746; такты и скорости — скриптом на ПК. QEMU контроллера не моделирует.
Как это называется в документации
Оригинал Перевод QUADSPI контроллер флеш с одной, двумя или четырьмя линиями данных OCTOSPI его развитие на восемь линий у новых серий indirect mode косвенный режим: команды через регистры status polling mode автоматический опрос статуса флеш memory-mapped mode отображение в память: флеш видна как область адресов XIP (execute in place) исполнение кода прямо из внешней флеш dummy cycles пустые такты между адресом и данными FSIZE размер флеш: байт = 2^(FSIZE+1) nCS выбор микросхемы флеш
41в.1 Три режима и где они есть
QUADSPI у F7 работает в трёх режимах (RM0385, 14.1):
- косвенный — любая команда через регистры: чтение, запись, стирание;
- автоматический опрос — контроллер сам периодически читает регистр статуса флеш и сообщает о совпадении с маской;
- отображение в память — флеш видна с адреса 0x9000 0000 как внутренняя память, только на чтение.
Quad-SPI есть у F412, F446, F469 и старше (раздел 41), у F7 и H7. У новых кристаллов — STM32L4R9, STM32H7B3, STM32U585 — блок называется OCTOSPI: до восьми линий данных, а устроен так же. Здесь разобран QUADSPI F746.
41в.2 Команда: фазы и линии
Каждое обращение к флеш — команда из фаз (RM0385, 14.3.3): инструкция (8 бит), адрес (8–32 бита), дополнительные байты, пустые такты, данные. Для каждой фазы отдельно задаётся число линий: IMODE для инструкции, ADMODE для адреса, DMODE для данных — 00 фазы нет, 01 одна линия, 10 две, 11 четыре. Пустые такты — поле DCYC, 0–31. Всё это лежит в регистре QUADSPI_CCR вместе с кодом инструкции и режимом FMODE.
Запись вида «1-4-4» и значит: инструкция по одной линии, адрес и данные —
по четырём. Команда 0xEB флеш N25Q128A — именно такая, с 10 пустыми
тактами (драйвер BSP, N25Q128A_DUMMY_CYCLES_READ_QUAD). Тактов на
чтение, адрес 24 бита, QUADSPI на 108 МГц (216 МГц / 2, как в BSP):
| Команда | Линии | Пустых | 4 байта | 256 байт | Поток |
|---|---|---|---|---|---|
| 0x0B быстрое чтение | 1-1-1 | 8 | 72 такта, 666,7 нс | 2088 тактов, 19,33 мкс | 13,2 МБ/с |
| 0xEB | 1-4-4 | 10 | 32 такта, 296,3 нс | 536 тактов, 4,96 мкс | 51,6 МБ/с |
На длинном чтении четыре линии дают почти вчетверо больше (предел — 54 МБ/с). На одном слове выигрыш вдвое: инструкция по-прежнему идёт по одной линии 8 тактов, и пустые такты никуда не деваются. Поэтому код, прыгающий по флеш, выигрывает от Quad-SPI меньше, чем чтение подряд, — и поэтому важны кэш и упреждающее чтение (41в.4).
41в.3 Настройка по регистрам
У STM32746G-Discovery флеш N25Q128A на 16 МБ: nCS — PB6, CLK — PB2, IO0–IO3 — PD11, PD12, PE2, PD13 (AF9 и AF10). Числа BSP в регистрах:
- CR: PRESCALER 1 — 216 / (1 + 1) = 108 МГц; порог FIFO 4 байта; SSHIFT — выборка на полтакта позже (для задержек на плате).
- DCR: FSIZE 23 — байт 2^(23 + 1) = 16 МБ; CSHT 5 — nCS не меньше 6 тактов между командами (в BSP пометка: «не меньше 50 нс»).
- CCR для отображения: FMODE 11, 0xEB, 1-4-4, 24 бита, DCYC 10 — значение 0x0F28 2DEB.
void qspi_init(void) /* выводы PB2, PB6, PD11–13, PE2 — AF9/AF10, их настраивает CubeMX */
{
RCC->AHB3ENR |= RCC_AHB3ENR_QSPIEN;
QUADSPI->CR = (1u << QUADSPI_CR_PRESCALER_Pos) /* 216 / (1 + 1) = 108 МГц */
| (3u << QUADSPI_CR_FTHRES_Pos) /* порог FIFO — 4 байта */
| QUADSPI_CR_SSHIFT; /* выборка на полтакта позже */
QUADSPI->DCR = (23u << QUADSPI_DCR_FSIZE_Pos) /* 2^(23 + 1) = 16 МБ */
| (5u << QUADSPI_DCR_CSHT_Pos); /* nCS между командами — 6 тактов */
QUADSPI->CR |= QUADSPI_CR_EN;
}
После записи CCR с FMODE = 11 флеш — это массив с 0x9000 0000. Функция
qspi_read_word(offset) из файла примера у armclang — одна команда
загрузки:
qspi_read_word: MOV r1,#0x90000000
LDR r0,[r0,r1]
Никакого кода обмена: команду 0xEB контроллер шлёт сам, как только процессор или DMA читают окно.
41в.4 Отображение в память: что можно и чего нельзя
RM0385 (14.3.7 и 14.3.11) ставит правила:
- в этом режиме — только чтение; стирать и писать — косвенным режимом;
- обращаться к окну до настройки и включения контроллера запрещено;
- адрес за пределами FSIZE, но внутри 256 МБ окна — ошибка шины: у ядра BusFault (или HardFault, если BusFault не включён), у DMA — ошибка передачи;
- окно не больше 256 МБ, даже если флеш больше;
- контроллер читает вперёд: FIFO служит буфером упреждения, и при чтении подряд следующий байт уже на месте — это и есть поддержка XIP.
Цена упреждения: контроллер по умолчанию не останавливает его и держит
nCS внизу, даже если обращений давно нет, а флеш с опущенным nCS
потребляет больше. Бит TCEN в CR и таймаут в LPTR отпускают nCS после
паузы. И пока идёт упреждение, флаг BUSY не падает — а поля CCR можно
менять только при BUSY = 0. Выйти из отображения можно битом ABORT: он
сбрасывается сам, когда отмена закончена (функция qspi_leave_mapped).
41в.5 Стирание и запись
Флеш N25Q128A: страница 256 байт, подсектор 4 КБ, сектор 64 КБ — 65 536 страниц, 4096 подсекторов, 256 секторов. Образ прошивки на 1 МБ — это 4096 страниц и 256 подсекторов (или 16 секторов). Порядок для каждого куска:
- Выйти из отображения (ABORT).
- Команда 0x06 — разрешить запись (
qspi_write_enable: только инструкция, без адреса и данных). - Стирание подсектора 0x20 с адресом.
- Ждать, пока флеш закончит: автоматический опрос статуса 0x05 с маской
на бит WIP (
qspi_wait_ready). Процессор не читает статус в цикле — контроллер сам шлёт 0x05 и поднимет флаг SMF при WIP = 0. - Снова 0x06 и запись страницы (256 байт за команду).
- Снова ждать WIP.
- Вернуть отображение.
Сколько ждать: драйвер BSP даёт на стирание подсектора до 800 мс, сектора — до 3 с, всей микросхемы — до 250 с.
Функции, которые это делают, не могут лежать в той же флеш: пока идёт
косвенная команда, окна 0x9000 0000 нет. Их держат во встроенной Flash или
в ОЗУ. И на F7 после записи надо сбросить D-кэш на изменённый участок
(SCB_InvalidateDCache_by_Addr, раздел 11б): иначе ядро прочитает
старые данные из кэша.
41в.6 Код во внешней флеш
По карте памяти Cortex-M7 (DUI 0646B, таблицы 2-11 и 2-12) область 0x8000 0000–0x9FFF FFFF — External RAM: тип Normal, исполнять можно, кэш — сквозная запись. Значит, окно QUADSPI с 0x9000 0000 годится для кода без настройки MPU, и кэш команд прячет задержку первого обращения.
Что нужно сделать:
- в скрипте компоновщика (раздел 11) — область с 0x9000 0000 размером 16 МБ и секция для кода или констант (картинки, шрифты, таблицы), а функции помечают атрибутом этой секции;
- программатору — алгоритм записи внешней флеш: у Keil в паке F7 для
этой платы есть
STM32F7xx_QSPI_Disco.FLMс началом 0x9000 0000 и размером 16 МБ; у STM32CubeProgrammer — свой внешний загрузчик; - при старте —
qspi_initи отображение до первого вызова кода из флеш: сама настройка должна лежать во встроенной Flash.
Удобное разделение: код, от которого зависит запуск (стартовый код, настройка тактирования и QUADSPI, обновление прошивки), — во встроенной Flash; большие данные и редко вызываемый код — снаружи.
41в.7 Грабли
- Обращение к окну до настройки. Отладчик, открывший окно памяти на
0x9000 0000, или конструктор C++, прочитавший константу из внешней
флеш до
qspi_init, — и BusFault или HardFault. - Размер FSIZE меньше флеш. Чтение за пределы FSIZE — ошибка шины, а не ноль; ставьте FSIZE по размеру флеш: 16 МБ — 23.
- Пустые такты не совпадают с флеш. DCYC должен совпадать с числом пустых тактов, на которое настроена флеш (у N25Q128A — поле в её регистре конфигурации). Иначе данные сдвинуты на полбайта или байт: «всё читается, но не то».
- Стирание из кода во внешней флеш. Пока идёт косвенная команда, окна нет, и процессор не может выбрать следующую команду. Код записи — во встроенной Flash или ОЗУ.
- Старые данные после записи. Кэш F7 держит строки внешней флеш; после стирания и записи — сброс D-кэша на изменённый участок.
- SSHIFT в DDR. В режиме двойной скорости (DDRM = 1) сдвиг выборки не поддерживается: бит SSHIFT надо снять (RM0385, 14.3.9).
- nCS внизу навсегда. Упреждающее чтение держит флеш выбранной; там, где важен ток, — TCEN и таймаут.
41в.8 Итоги
- Три режима: косвенный, автоматический опрос статуса, отображение с 0x9000 0000 (только чтение, окно до 256 МБ).
- Команда — фазы инструкции, адреса, пустых тактов и данных; линии для каждой задаются отдельно. 0xEB «1-4-4» с 10 пустыми: 32 такта на слово, 296 нс при 108 МГц; поток до 51,6 МБ/с на 256 байтах.
- N25Q128A у 746G-Discovery: FSIZE 23, PRESCALER 1, CSHT 5, CCR 0x0F28 2DEB.
- Стирание и запись — косвенным режимом, ожидание — автоматическим опросом WIP; код записи — не во внешней флеш; после записи — сброс D-кэша.
- На Cortex-M7 окно 0x9000 0000 — Normal и исполняемое: код можно держать снаружи без MPU, программатору нужен алгоритм внешней флеш.
Полные исходники
qspi41c.c — Quad-SPI на STM32F746 по регистрам с флеш N25Q128A.
/* 41в: Quad-SPI по регистрам на STM32F746 — флеш N25Q128A (16 МБ), как у STM32746G-Discovery.
Собирается armclang: -DSTM32F746xx. */
#include <stdint.h>
/* поля QUADSPI_CCR — RM0385, 14.5.6 */
#define CCR(fmode, dmode, dcyc, adsize, admode, imode, inst) \
(((uint32_t)(fmode) << QUADSPI_CCR_FMODE_Pos) | ((uint32_t)(dmode) << QUADSPI_CCR_DMODE_Pos) | \
((uint32_t)(dcyc) << QUADSPI_CCR_DCYC_Pos) | ((uint32_t)(adsize) << QUADSPI_CCR_ADSIZE_Pos) | \
((uint32_t)(admode) << QUADSPI_CCR_ADMODE_Pos) | ((uint32_t)(imode) << QUADSPI_CCR_IMODE_Pos) | (inst))
enum { F_WRITE = 0, F_READ = 1, F_POLL = 2, F_MAPPED = 3 }; /* FMODE */
enum { L_NONE = 0, L_1 = 1, L_2 = 2, L_4 = 3 }; /* IMODE, ADMODE, DMODE */
enum { A_24 = 2 }; /* ADSIZE */
#define CCR_MAPPED CCR(F_MAPPED, L_4, 10, A_24, L_4, L_1, 0xEB) /* 0xEB: 1-4-4, 10 пустых */
_Static_assert(CCR_MAPPED == 0x0F282DEBu, "CCR режима отображения");
_Static_assert(QSPI_BASE == 0x90000000u && QSPI_R_BASE == 0xA0001000u, "окно флеш и регистры");
static void qspi_wait_idle(void)
{
while (QUADSPI->SR & QUADSPI_SR_BUSY) {
}
}
void qspi_init(void) /* выводы PB2, PB6, PD11–13, PE2 — AF9/AF10, их настраивает CubeMX */
{
RCC->AHB3ENR |= RCC_AHB3ENR_QSPIEN;
QUADSPI->CR = (1u << QUADSPI_CR_PRESCALER_Pos) /* 216 / (1 + 1) = 108 МГц */
| (3u << QUADSPI_CR_FTHRES_Pos) /* порог FIFO — 4 байта */
| QUADSPI_CR_SSHIFT; /* выборка на полтакта позже */
QUADSPI->DCR = (23u << QUADSPI_DCR_FSIZE_Pos) /* 2^(23 + 1) = 16 МБ */
| (5u << QUADSPI_DCR_CSHT_Pos); /* nCS между командами — 6 тактов */
QUADSPI->CR |= QUADSPI_CR_EN;
}
/* команда без адреса и данных: 0x06 — разрешить запись */
void qspi_write_enable(void)
{
qspi_wait_idle();
QUADSPI->CCR = CCR(F_WRITE, L_NONE, 0, 0, L_NONE, L_1, 0x06);
qspi_wait_idle();
}
/* ждать конца стирания или записи: контроллер сам читает статус 0x05, пока бит WIP не станет 0 */
void qspi_wait_ready(void)
{
qspi_wait_idle();
QUADSPI->DLR = 0; /* один байт статуса */
QUADSPI->PSMKR = 0x01; /* маска: WIP */
QUADSPI->PSMAR = 0x00; /* ждём WIP = 0 */
QUADSPI->PIR = 16; /* пауза между опросами, тактов */
QUADSPI->CR |= QUADSPI_CR_APMS; /* остановиться при совпадении */
QUADSPI->CCR = CCR(F_POLL, L_1, 0, 0, L_NONE, L_1, 0x05);
while (!(QUADSPI->SR & QUADSPI_SR_SMF)) {
}
QUADSPI->FCR = QUADSPI_FCR_CSMF;
}
/* включить отображение: флеш видна с 0x90000000, только чтение */
void qspi_memory_mapped(void)
{
qspi_wait_idle();
QUADSPI->CCR = CCR_MAPPED;
}
/* выйти из отображения перед стиранием и записью: BUSY держится из-за упреждающего чтения */
void qspi_leave_mapped(void)
{
QUADSPI->CR |= QUADSPI_CR_ABORT;
while (QUADSPI->CR & QUADSPI_CR_ABORT) {
}
}
uint32_t qspi_read_word(uint32_t offset)
{
return *(volatile const uint32_t *)(QSPI_BASE + offset);
}
Дальше: раздел 42 «Файловая система FatFS» — как поверх внешней памяти хранить файлы.