198ц. Мини-проект: журнал во внешней Octo-SPI флешке B-U585I-IOT02A
В 198х плата B-U585I-IOT02A научилась раз в секунду опрашивать свои датчики. Строки опроса нужно где-то хранить так, чтобы они пережили выключение. На плате для этого есть флеш MX25LM51245G на 64 МБ, подключённая к OCTOSPI2 восемью линиями данных. Здесь из неё сделан журнал по кругу: записи по 32 байта с CRC, кольцо из секторов по 4 КБ, подъём после обрыва питания в любой момент и чтение через окно отображения в память.
Раздел 41в разобрал Quad-SPI на F746: четыре линии, одна скорость. Здесь линий восемь, данные идут на обоих фронтах такта, а у флеш появляется второй «язык» команд. Включать его приходится самим: после подачи питания флеш понимает только однопроводный SPI.
Опоры: пакет платы B-U585I-IOT02A_BSP 1.1.0 (файлы
b_u585i_iot02a_ospi.c|h, компонент mx25lm51245g, лицензия
BSD-3-Clause), пакет STM32U5xx_DFP 2.2.1 (заголовок stm32u585xx.h,
драйвер HAL OSPI, алгоритм программатора .FLM), справочное руководство
RM0456 (главы 28 OCTOSPI, 29 OCTOSPIM и 32 DLYB). Из справочника:
41в — Quad-SPI и отображение в память, 42а — LittleFS на NOR-флеш,
11 — память и ресурс Flash, 204 — износ и надёжность, 198а — журнал во
встроенной Flash на батарейке, 2д — STM32U585 и такт 160 МГц. Даташита
самой MX25LM51245G в пакетах нет: всё о микросхеме здесь — из заголовков и
кода драйвера. U585 в QEMU нет, на плате код не запускался — проверка на
ПК, на моделях флеш и регистров.
Как это называется в документации
Оригинал Перевод STR (single transfer rate) одинарная скорость: данные на одном фронте такта DTR (double transfer rate) двойная скорость: данные на обоих фронтах OPI, SOPI, DOPI протокол на восьми линиях; SOPI — одинарная скорость, DOPI — двойная DQS (data strobe) строб данных: флеш сама отмечает, когда байт на линиях WIP (write in progress) бит статуса «идёт запись или стирание» configuration register 2 (CR2) регистр настройки 2: протокол и число пустых тактов subsector так пакет платы зовёт кусок стирания 4 КБ bulk erase стирание всей микросхемы delay block (DLYB) блок задержки: сдвигает момент выборки данных free-running clock (FRCK) такт на выводе CLK идёт всегда, а не только во время команды
198ц.1 Микросхема: сколько в ней и куда она подключена
Размеры — из заголовка mx25lm51245g.h пакета платы:
| Что | Размер | Сколько | Константа |
|---|---|---|---|
| вся флеш | 64 МБ (512 Мбит) | 1 | MX25LM51245G_FLASH_SIZE |
| блок стирания | 64 КБ | 1024 | MX25LM51245G_SECTOR_64K |
| наименьшее стирание | 4 КБ | 16 384 | MX25LM51245G_SUBSECTOR_4K |
| страница записи | 256 байт | 262 144 | MX25LM51245G_PAGE_SIZE |
С названиями путаница. Этот заголовок зовёт 64 КБ «сектором», а 4 КБ —
«подсектором». Перечисление в том же файле говорит обратное:
MX25LM51245G_ERASE_4K — «4K size Sector erase», MX25LM51245G_ERASE_64K —
«64K size Block erase». Так же, 4 КБ «сектор» и 64 КБ «блок», и в старой
версии этого драйвера из пакета 32L4R9IDISCOVERY_BSP 1.0.0. Дальше в
разделе сектор — 4 КБ, блок — 64 КБ.
Флеш сидит на OCTOSPI2: такт — PF4, DQS — PF12, выбор — PI5, линии IO0–IO3 —
PF0–PF3, IO4–IO7 — PH9–PH12, у всех альтернативная функция AF5 (заголовок
b_u585i_iot02a_ospi.h). Менеджер выводов OCTOSPIM пакет платы не трогает:
после сброса OCTOSPI2 и так выведен на свой порт 2 (RM0456, 29.4.3). Окно
отображения OCTOSPI2 начинается с 0x7000 0000 (OCTOSPI2_BASE).
OCTOSPI1 на этой плате занят другой памятью — PSRAM APS6408, у неё своё
окно с 0x9000 0000.
Программатору для этой флеш нужен свой алгоритм. В паке STM32U5xx_DFP 2.2.1
он есть — MX25LM51245G_STM32U585I_IOT02A.FLM. Его описание FlashDevice
(разбор ELF в проверке): начало 0x7000 0000, размер 0x0400 0000,
стирание блоками по 64 КБ, порция записи 4096 байт, после стирания 0xFF,
предельное ожидание записи — 10 000 мс, стирания — 6000 мс.
198ц.2 После сброса — SPI, в работе — восемь линий DTR
После подачи питания флеш принимает команды «1-1-1»: инструкция, адрес и
данные по одной линии (в заголовке: «Power on H/W default setting»).
Протокол меняют биты регистра CR2 по адресу 0: SOPI = 0x01 — восемь линий
одинарной скорости, DOPI = 0x02 — восемь линий двойной; оба нуля — снова
SPI. Команды во всех трёх режимах разные:
| Действие | SPI (1-1-1) | OPI DTR (8D-8D-8D) |
|---|---|---|
| разрешить запись | 0x06 |
0x06F9 |
| прочитать статус | 0x05 |
0x05FA: адрес 0, 5 пустых тактов, 2 байта, DQS |
| записать страницу | 0x12 (адрес 4 байта), 0x02 (3 байта) |
0x12ED |
| стереть 4 КБ | 0x21, 0x20 |
0x21DE |
| стереть 64 КБ | 0xDC, 0xD8 |
0xDC23 |
| стереть всё | 0x60 |
0x609F |
| прочитать | 0x0C, 8 пустых тактов (0x0B — 3 байта адреса) |
0xEE11, 6 пустых тактов, DQS |
| записать и прочитать CR2 | 0x72, 0x71 |
0x728D, 0x718E |
| сброс | 0x66, затем 0x99 |
0x6699, затем 0x9966 |
Что меняется в OPI:
- Код команды — два байта, и второй — всегда инверсия первого:
0x06превращается в0x06F9,0x05— в0x05FA. Так устроены все 42 команды раздела OPI заголовка (проверка сверяет каждую). - Адрес — всегда 4 байта. Драйвер пакета прямо отказывает, если в OPI попросить 3 байта. Даже чтение статуса в OPI идёт с адресом 0.
- Пустые такты — везде, где флеш отвечает: в DTR 5 тактов на регистрах
и 6 на чтении памяти (
mx25lm51245g_conf.h:DUMMY_CYCLES_REG_OCTAL_DTR= 5,DUMMY_CYCLES_READ_OCTAL_DTR= 6; для SPI-чтения — 8). Флеш должна ждать столько же — поэтому до включения DOPI в CR2 по адресу0x300пишут код0x07: «6 пустых тактов». - DQS: в DTR флеш ответ сопровождает стробом, и контроллер по нему ловит байты. Чтения идут с DQS, запись страницы — без него.
Сколько тактов занимает чтение. Инструкция 8 бит по одной линии — 8
тактов, 16 бит по восьми — 2 такта, в DTR — 1. Адрес 32 бита — 32, 4 и 2
такта. Данные: 8 тактов на байт, 1 такт и полтакта. Частота — 40 МГц, как
у пакета платы (calc198c.py):
| Чтение | Инструкция | Адрес | Пустые | 32 байта | 4 КБ | Поток |
|---|---|---|---|---|---|---|
SPI 0x0C |
8 | 32 | 8 | 304 такта, 7,6 мкс | 32 816 тактов, 820,4 мкс | 5 МБ/с |
OPI STR 0xEC13 |
2 | 4 | 6 | 44 такта, 1,1 мкс | 4108 тактов, 102,7 мкс | 40 МБ/с |
OPI DTR 0xEE11 |
1 | 2 | 6 | 25 тактов, 0,625 мкс | 2057 тактов, 51,43 мкс | 80 МБ/с |
Паузы выбора между командами здесь не считаны, а RM0456 (28.4.5) предупреждает, что после последнего байта контроллер может выдать до шести лишних тактов. На чтении 32 байт шесть таких тактов — почти четверть всей команды, на 4 КБ они не видны.
Порядок включения — тот же, что в BSP_OSPI_NOR_Init пакета платы:
- Контроллер: такт OCTOSPI2 от ядра 160 МГц —
OCTOSPISELпосле сброса 00, SYSCLK (RM0456, 11.8.47), делитель 4 — 40 МГц (комментарий пакета: «OctoSPI clock = 160MHz / ClockPrescaler = 40MHz»); тип памяти Micron, размерDEVSIZE= 25 — 2^26 байт = 64 МБ, между командами выбор поднят не меньше 2 тактов, порог FIFO — 4 байта. - Блок задержки DLYB: подобрать длину линии на один такт (порядок — RM0456, 32.4.4) и взять фазу в четверть этой длины — у пакета это «emperic value», делитель 4.
- Сброс флеш командами
0x66/0x99на всех трёх языках — SPI, OPI STR и OPI DTR — и пауза 100 мс (MX25LM51245G_RESET_MAX_TIME: «when SWreset during erase operation»). - Дождаться WIP = 0, разрешить запись, CR2
0x300←0x07(6 пустых тактов), снова разрешить, CR20←0x02(DOPI), пауза 40 мс (MX25LM51245G_WRITE_REG_MAX_TIME). - Контроллер — в режим Macronix (
MTYP= 001: в DTR байты идут в порядке D1/D0, RM0456, 28.7.2) и с задержкой выхода на четверть тактаDHQC. Проверить WIP уже в DOPI и прочитать CR2 обратно — там должно быть0x02.
Настройка контроллера и блок задержки:
static void ctrl_setup(int macronix)
{
OCTOSPI2->CR &= ~OCTOSPI_CR_EN;
OCTOSPI2->DCR1 = (macronix ? OCTOSPI_DCR1_MTYP_0 : 0u) /* 001 Macronix, 000 Micron */
| (25u << OCTOSPI_DCR1_DEVSIZE_Pos) /* 2^(25 + 1) = 64 МБ */
| (1u << OCTOSPI_DCR1_CSHT_Pos); /* NCS между командами — 2 такта */
OCTOSPI2->DCR2 = 3u << OCTOSPI_DCR2_PRESCALER_Pos; /* 160 МГц / 4 = 40 МГц */
OCTOSPI2->DCR3 = 0u;
OCTOSPI2->DCR4 = 0u;
OCTOSPI2->CR = (OCTOSPI2->CR & ~OCTOSPI_CR_FTHRES) | (3u << OCTOSPI_CR_FTHRES_Pos); /* порог 4 байта */
OCTOSPI2->TCR = macronix ? OCTOSPI_TCR_DHQC : 0u; /* выход сдвинут на четверть такта */
OCTOSPI2->CR |= OCTOSPI_CR_EN;
OCTOSPI2->DCR1 |= OCTOSPI_DCR1_FRCK; /* такт всегда идёт — после EN */
}
static int dlyb_tune(void)
{
uint32_t unit, lng = 0u, n;
DLYB_OCTOSPI2->CR = DLYB_CR_DEN | DLYB_CR_SEN;
for (unit = 0u; unit < 128u; unit++) {
DLYB_OCTOSPI2->CFGR = 12u | (unit << DLYB_CFGR_UNIT_Pos); /* все 12 ячеек */
uint32_t t0 = mx_now_ms();
while (!(DLYB_OCTOSPI2->CFGR & DLYB_CFGR_LNGF)) {
MX_POLL();
if (mx_now_ms() - t0 > 255u)
return -1;
}
lng = (DLYB_OCTOSPI2->CFGR & DLYB_CFGR_LNG_Msk) >> DLYB_CFGR_LNG_Pos;
if ((lng & 0x7FFu) != 0u && (lng & 0xC00u) != 0xC00u)
break; /* линия длиной в один такт */
}
if (unit == 128u)
return -1;
for (n = 10u; n > 0u && (lng >> n) == 0u; n--) {
}
if (n == 0u)
return -1;
DLYB_OCTOSPI2->CFGR = (n / 4u) | (unit << DLYB_CFGR_UNIT_Pos); /* SEL и UNIT пишутся при SEN = 1 */
DLYB_OCTOSPI2->CR = DLYB_CR_DEN; /* SEN = 0: выход на выбранной фазе */
OCTOSPI2->CR |= OCTOSPI_CR_ABORT;
while (OCTOSPI2->CR & OCTOSPI_CR_ABORT)
MX_POLL();
OCTOSPI2->DCR1 &= ~OCTOSPI_DCR1_FRCK;
return 0;
}
Всё включение целиком:
int mx_init(void)
{
uint8_t cr2[2];
uint8_t v;
pins();
RCC->AHB2ENR2 |= RCC_AHB2ENR2_OCTOSPI2EN;
RCC->AHB2RSTR2 |= RCC_AHB2RSTR2_OCTOSPI2RST;
RCC->AHB2RSTR2 &= ~RCC_AHB2RSTR2_OCTOSPI2RST;
dtr = 0;
ctrl_setup(0);
if (dlyb_tune() != 0)
return -1;
/* Флеш не сбрасывается вместе с МК: она может ждать команды в SPI, OPI STR или OPI DTR.
Сброс шлётся на всех трёх — поймёт тот, на котором она сейчас. */
if (cmd(FM_WRITE, I_1, MX_SPI_RESET_ENABLE, 0u, 0u, 0, 0, 0u) || cmd(FM_WRITE, I_1, MX_SPI_RESET_MEMORY, 0u, 0u, 0, 0, 0u)
|| cmd(FM_WRITE, I_8STR, MX_OPI_RESET_ENABLE, 0u, 0u, 0, 0, 0u) || cmd(FM_WRITE, I_8STR, MX_OPI_RESET_MEMORY, 0u, 0u, 0, 0, 0u)
|| cmd(FM_WRITE, I_8DTR, MX_OPI_RESET_ENABLE, 0u, 0u, 0, 0, 0u) || cmd(FM_WRITE, I_8DTR, MX_OPI_RESET_MEMORY, 0u, 0u, 0, 0, 0u))
return -1;
delay_ms(MX_T_RESET_MS); /* сброс мог прервать стирание */
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) != 0)
return -1;
v = MX_CR2_DC_6; /* CR2 0x300: 6 пустых тактов */
if (write_enable() || cmd(FM_WRITE, I_1 | A_1 | D_1, MX_SPI_WRITE_CFG2, 0u, MX_CR2_REG3, 0, &v, 1u))
return -1;
v = MX_CR2_DOPI; /* CR2 0x000: DOPI */
if (write_enable() || cmd(FM_WRITE, I_1 | A_1 | D_1, MX_SPI_WRITE_CFG2, 0u, MX_CR2_REG1, 0, &v, 1u))
return -1;
delay_ms(MX_T_WRITE_REG_MS);
ctrl_setup(1);
dtr = 1;
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) != 0
|| cmd(FM_READ, I_8DTR | A_8DTR | D_8DTR | DQS, MX_OPI_READ_CFG2, MX_DUMMY_REG_DTR, MX_CR2_REG1, cr2, 0, 2u))
return -1;
return cr2[0] == MX_CR2_DOPI ? 0 : -1;
}
FRCK стоит так же, как у пакета: HAL включает свободный такт при каждой
настройке, блок задержки его на время подбора выключает, а повторная
настройка под Macronix включает снова.
198ц.3 Команда регистрами: запись, стирание, ожидание
Любая команда косвенного режима — это FMODE в CR, длина в DLR, фазы
в CCR, пустые такты в TCR, код в IR и адрес в AR. Стартует она от
последней нужной записи (RM0456, 28.4.9): от записи AR, если адрес есть;
от записи IR, если адреса нет и данные не передаются; от первой записи
DR, если данные передаёт программа. Поэтому порядок в cmd именно такой:
static int cmd(uint32_t fmode, uint32_t ccr, uint32_t ir, uint32_t dcyc, uint32_t addr,
uint8_t *rx, const uint8_t *tx, uint32_t n)
{
if (wait_idle() != 0)
return -1;
OCTOSPI2->CR = (OCTOSPI2->CR & ~OCTOSPI_CR_FMODE) | fmode;
if (n)
OCTOSPI2->DLR = n - 1u;
OCTOSPI2->CCR = ccr;
OCTOSPI2->TCR = (OCTOSPI2->TCR & ~OCTOSPI_TCR_DCYC) | dcyc;
OCTOSPI2->IR = ir;
if (ccr & OCTOSPI_CCR_ADMODE)
OCTOSPI2->AR = addr;
for (uint32_t i = 0; tx && i < n; i++) {
if (wait_sr(OCTOSPI_SR_FTF, MX_T_POLL_MS) != 0)
return -1;
MX_DR8 = tx[i];
}
for (uint32_t i = 0; rx && i < n; i++) {
if (wait_sr(OCTOSPI_SR_FTF | OCTOSPI_SR_TCF, MX_T_POLL_MS) != 0)
return -1;
rx[i] = MX_DR8;
}
if (wait_sr(OCTOSPI_SR_TCF, MX_T_POLL_MS) != 0)
return -1;
OCTOSPI2->FCR = OCTOSPI_FCR_CTCF;
return 0;
}
Ждать WIP процессору не нужно: в режиме автоматического опроса контроллер
сам шлёт чтение статуса раз в PIR тактов и поднимает SMF, когда
(статус & маска) == образец; с APMS он после этого останавливается сам.
В DTR статус читается двумя байтами — так делает драйвер пакета:
static int poll(uint32_t mask, uint32_t match, uint32_t ms)
{
uint32_t ccr = dtr ? (I_8DTR | A_8DTR | D_8DTR | DQS) : (I_1 | D_1);
if (wait_idle() != 0)
return -1;
OCTOSPI2->PSMAR = match;
OCTOSPI2->PSMKR = mask;
OCTOSPI2->PIR = 0x10u; /* MX25LM51245G_AUTOPOLLING_INTERVAL_TIME */
OCTOSPI2->CR = (OCTOSPI2->CR & ~(OCTOSPI_CR_FMODE | OCTOSPI_CR_PMM)) | FM_POLL | OCTOSPI_CR_APMS;
OCTOSPI2->DLR = dtr ? 1u : 0u; /* в DTR статус читается двумя байтами */
OCTOSPI2->CCR = ccr;
OCTOSPI2->TCR = (OCTOSPI2->TCR & ~OCTOSPI_TCR_DCYC) | (dtr ? MX_DUMMY_REG_DTR : 0u);
OCTOSPI2->IR = dtr ? MX_OPI_READ_STATUS : MX_SPI_READ_STATUS;
if (dtr)
OCTOSPI2->AR = 0u;
if (wait_sr(OCTOSPI_SR_SMF, ms) != 0)
return -1;
OCTOSPI2->FCR = OCTOSPI_FCR_CSMF;
return 0;
}
static int write_enable(void)
{
if (dtr ? cmd(FM_WRITE, I_8DTR | NODATA_DTR, MX_OPI_WRITE_ENABLE, 0u, 0u, 0, 0, 0u)
: cmd(FM_WRITE, I_1, MX_SPI_WRITE_ENABLE, 0u, 0u, 0, 0, 0u))
return -1;
return poll(MX_SR_WEL, MX_SR_WEL, MX_T_POLL_MS); /* дождаться WEL = 1 */
}
Запись и стирание — по одной схеме: дождаться, что флеш свободна, разрешить запись и дождаться бита WEL, дать команду, снова ждать WIP. Пакет платы шлёт WREN перед каждой страницей и перед каждым стиранием — так же и здесь.
int mx_prog(uint32_t addr, const void *buf, uint32_t n)
{
if (n == 0u || n > 256u || (addr & 1u) || (n & 1u) || (addr & 255u) + n > 256u)
return -1;
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) || write_enable()
|| cmd(FM_WRITE, I_8DTR | A_8DTR | D_8DTR, MX_OPI_PAGE_PROG, 0u, addr, 0, buf, n))
return -1;
return poll(MX_SR_WIP, 0u, MX_T_POLL_MS);
}
int mx_erase4k(uint32_t addr)
{
if (addr & 4095u)
return -1;
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) || write_enable()
|| cmd(FM_WRITE, I_8DTR | A_8DTR | NODATA_DTR, MX_OPI_ERASE_4K, 0u, addr, 0, 0, 0u))
return -1;
return poll(MX_SR_WIP, 0u, MX_T_POLL_MS);
}
Проверка чётности в mx_prog — не придирка. RM0456 (28.4.9): в косвенном
режиме на восьми линиях DTR без DQS «the given starting address and the data
length must be even». Запись страницы как раз идёт без DQS.
Сколько ждать. Предельные времена в заголовке: стирание 4 КБ — 400 мс,
64 КБ — 1000 мс, всей флеш — 460 000 мс, то есть почти 8 минут; запись
регистра — 40 мс. В версии драйвера из пакета 32L4R9IDISCOVERY_BSP те же
цифры другие: 400 мс на 4 КБ, 2000 на 64 КБ и 300 000 на всю. Времени записи
страницы нет ни там, ни там; пакет платы ждёт WIP с тайм-аутом HAL — 5 с
(HAL_OSPI_TIMEOUT_DEFAULT_VALUE), так же ждёт и poll.
У NODATA_DTR своя история: HAL ставит бит DDTR и командам без данных,
если включён DHQC и инструкция идёт в DTR. Код повторяет это, чтобы
кадры совпадали с пакетом бит в бит.
198ц.4 Журнал по кругу: слот, заголовок, кольцо
NOR-флеш пишется только из 1 в 0: после стирания все байты 0xFF, запись
сбрасывает биты, вернуть 1 может только стирание целого сектора. Отсюда
устройство журнала:
- слот — 32 байта. В страницу 256 байт их входит ровно 8: запись никогда не пересекает край страницы. Длина и адрес чётные — правило DTR выполнено само;
- сектор 4 КБ — 128 слотов: слот 0 — заголовок, 1…127 — записи;
- заголовок хранит номер захода
sseq: сектор, в который пишут вsseq-й раз, — это(sseq − 1) % nsect. И номер первой записи сектора; - запись — номер, время, тип, до 16 байт данных и CRC-32 по первым 28
байтам. CRC — обычная с полиномом
0xEDB88320: у строки «123456789» она0xCBF43926, как и положено.
/* Раздел 198ц: журнал по кругу во внешней NOR-флеш.
Сектор 4 КБ = 128 слотов по 32 байта: слот 0 — заголовок сектора, 1…127 — записи.
Флеш видна через три функции: чтение, запись в пределах страницы и стирание сектора —
одинаково для MX25LM51245G на B-U585I-IOT02A и для модели флеш на ПК. */
#ifndef OSLOG_H
#define OSLOG_H
#include <stdint.h>
#define OSLOG_PAGE 256u /* страница записи MX25LM51245G */
#define OSLOG_SECTOR 4096u /* наименьшее стирание — 4 КБ */
#define OSLOG_SLOT 32u /* запись и заголовок — по 32 байта */
#define OSLOG_SLOTS (OSLOG_SECTOR / OSLOG_SLOT) /* 128 слотов в секторе */
#define OSLOG_DATA 16u /* полезных байт в записи */
#define OSLOG_MAGIC 0x314C534Fu /* "OSL1" */
struct oslog_rec { /* 32 байта: 8 записей на страницу, 127 на сектор */
uint32_t seq; /* номер записи: 1, 2, 3… без пропусков */
uint32_t time; /* время приложения, например секунды RTC */
uint16_t type; /* что за запись — решает приложение */
uint16_t len; /* сколько байт data занято */
uint8_t data[OSLOG_DATA];
uint32_t crc; /* CRC-32 первых 28 байт */
};
struct oslog_hdr { /* 32 байта в слоте 0 каждого сектора */
uint32_t magic;
uint32_t sseq; /* номер захода: 1, 2, 3…; сектор = (sseq - 1) % nsect */
uint32_t first; /* seq первой записи, которая ляжет в сектор */
uint8_t pad[16]; /* 0xFF */
uint32_t crc; /* CRC-32 первых 28 байт */
};
struct oslog_flash {
int (*read)(uint32_t addr, void *buf, uint32_t n);
int (*prog)(uint32_t addr, const void *buf, uint32_t n); /* не через край страницы */
int (*erase)(uint32_t addr); /* сектор 4 КБ */
};
struct oslog {
const struct oslog_flash *f;
uint32_t base; /* адрес области журнала во флеш, кратен 4 КБ */
uint32_t nsect; /* секторов в кольце, не меньше 2 */
uint32_t head; /* сектор, в который пишем */
uint32_t sseq; /* его номер захода */
uint32_t slot; /* следующий свободный слот, 128 — сектор полон */
uint32_t next; /* seq следующей записи */
uint32_t torn; /* испорченных слотов в текущем секторе при подъёме */
uint32_t skipped; /* слотов, пропущенных из-за ошибки проверки записи */
};
uint32_t oslog_crc32(const void *p, uint32_t n);
int oslog_mount(struct oslog *l);
int oslog_append(struct oslog *l, uint32_t time, uint16_t type, const void *data, uint16_t len);
uint32_t oslog_walk(const struct oslog *l, int (*cb)(const struct oslog_rec *r, void *ctx), void *ctx);
#endif
Слот пишется один раз и тут же читается обратно. Не совпало — слот
пропускается: он останется испорченным, а запись ляжет в следующий.
Переход в новый сектор — всегда через стирание, даже если сектор выглядит
чистым: стирание, оборванное на середине, может оставить сектор наполовину
в 0xFF, и по виду это не отличить.
static int rotate(struct oslog *l)
{
uint32_t n = (l->head + 1u) % l->nsect;
struct oslog_hdr h, chk;
memset(&h, 0xFF, sizeof h);
h.magic = OSLOG_MAGIC;
h.sseq = l->sseq + 1u;
h.first = l->next;
h.crc = oslog_crc32(&h, 28u);
if (l->f->erase(slot_addr(l, n, 0u)) != 0 || l->f->prog(slot_addr(l, n, 0u), &h, sizeof h) != 0
|| l->f->read(slot_addr(l, n, 0u), &chk, sizeof chk) != 0 || memcmp(&h, &chk, sizeof h) != 0)
return -1;
l->head = n;
l->sseq = h.sseq;
l->slot = 1u;
return 0;
}
int oslog_append(struct oslog *l, uint32_t time, uint16_t type, const void *data, uint16_t len)
{
struct oslog_rec r, chk;
if (len > OSLOG_DATA)
return -1;
memset(&r, 0, sizeof r);
r.seq = l->next;
r.time = time;
r.type = type;
r.len = len;
memcpy(r.data, data, len);
r.crc = oslog_crc32(&r, 28u);
for (int tries = 0; tries < 4; tries++) {
if (l->slot >= OSLOG_SLOTS && rotate(l) != 0)
return -1;
uint32_t a = slot_addr(l, l->head, l->slot++);
if (l->f->prog(a, &r, sizeof r) == 0 && l->f->read(a, &chk, sizeof chk) == 0
&& memcmp(&r, &chk, sizeof r) == 0) {
l->next++;
return 0;
}
l->skipped++;
}
return -1;
}
В прошивке кольцо — 2048 секторов, 8 МБ в конце флеш, с 0x0380 0000.
Это 260 096 записей: раз в минуту — полгода истории, 180,6 суток.
198ц.5 Обрыв питания: подъём и проверка на модели
После сброса журнал ничего не помнит и восстанавливает всё по флеш:
- Прочитать заголовки всех секторов. Головной — с наибольшим номером захода среди целых (магия, CRC и номер, который сходится с местом).
- Целых заголовков нет — чистая флеш: стереть сектор 0 и начать с него.
- В головном секторе пройти все 127 слотов. Последняя запись с верной CRC даёт номер следующей, свободное место — сразу за последним непустым слотом. Испорченные слоты не трогаются, они лишь считаются.
int oslog_mount(struct oslog *l)
{
struct oslog_hdr h;
struct oslog_rec r;
uint32_t best = 0u, first = 1u;
l->head = 0u;
for (uint32_t i = 0; i < l->nsect; i++) {
if (l->f->read(slot_addr(l, i, 0u), &h, sizeof h) != 0)
return -1;
if (hdr_ok(l, &h, i) && h.sseq > best) {
best = h.sseq;
first = h.first;
l->head = i;
}
}
l->torn = 0u;
l->skipped = 0u;
if (best == 0u) { /* чистая флеш: начать с сектора 0 */
l->head = l->nsect - 1u;
l->sseq = 0u;
l->next = 1u;
return rotate(l);
}
l->sseq = best;
l->next = first;
l->slot = 1u;
for (uint32_t s = 1u; s < OSLOG_SLOTS; s++) {
if (l->f->read(slot_addr(l, l->head, s), &r, sizeof r) != 0)
return -1;
if (blank(&r))
continue;
l->slot = s + 1u;
if (rec_ok(&r) && r.seq >= l->next)
l->next = r.seq + 1u;
else
l->torn++;
}
return 0;
}
Обход от новой записи к старой (oslog_walk) идёт по секторам назад, пока
номера захода идут подряд, и отдаёт только записи с верной CRC и строго
убывающими номерами. Так в выдачу не попадают ни оборванный слот, ни
записи наполовину стёртого старого сектора.
Модель флеш на ПК (sim.c, gcc 12.2.0 в образе stm-sys:1): 4 сектора
по 4 КБ; стирание — 0xFF; запись — только И с данными (1 → 0);
ошибкой программы считаются запись через край страницы, нечётные адрес
или длина и запись поверх уже записанного. Прогон пишет 1574 записи —
три с лишним круга — и перед каждой операцией делает копию флеш, на
копии обрывает операцию посередине, поднимает журнал, дописывает ещё 200
записей и поднимает снова. Основной прогон при этом идёт дальше без
обрыва. Как рвётся запись: первые k байт целы (k = 0, 1, 2, 15, 16, 27, 30,
31), в байте k не успел сброситься хотя бы один бит; ещё один случай —
запись легла целиком, а питание пропало до ответа флеш. Как рвётся
стирание: 0xFF уже в первых 0, 32, 2048, 4064 или 4095 байтах, либо в
каждой второй странице.
Что требует проверка после каждого обрыва: следующий номер — ровно за
последней записью, которую oslog_append успела подтвердить (или за
легшей целиком); обход без пропусков и с верными данными отдаёт не меньше
двух полных секторов; после 200 новых записей и второго подъёма — то же.
| Прогон | Итог |
|---|---|
| основной: 1574 записи, 4 сектора | 1587 команд записи, 13 стираний, в кольце 431 запись — с 1144-й по 1574-ю |
| обрывы | 14 274 (14 202 на записи, 72 на стирании), из них 1586 — «легла целиком», не прошло 0 |
| нарушения правил флеш | 0 |
| залипший бит в слоте | запись ушла в следующий слот, после подъёма испорченный слот 1, записей 432 |
Чтобы убедиться, что проверка ловит ошибки, run198c.sh собирает три
испорченные копии oslog.c:
| Порча | Что стало |
|---|---|
| запись считается целой без проверки CRC | не прошли 7840 обрывов из 14 274 |
| переход в сектор без стирания | журнал не начинается на флеш с мусором: mount_fresh rc=-1 |
| головной — первый целый заголовок, а не с наибольшим номером | не прошли все 14 274 |
198ц.6 Окно для чтения, прошивка и ресурс
Читать журнал удобнее всего через окно: после mx_map флеш с
0x7000 0000 — обычная память, memcpy из неё сам превращается в команды
0xEE11. Запись через окно RM0456 (28.4.11) не советует: «It is not
recommended to program the flash memory using the memory-mapped writes».
Поэтому WCCR = 0: запись в окно при DMODE = 000 — ошибка шины AHB
(RM0456, 28.4.17), и случайная запись не пройдёт молча.
int mx_map(void)
{
if (wait_idle() != 0)
return -1;
OCTOSPI2->CR &= ~OCTOSPI_CR_FMODE;
OCTOSPI2->CCR = I_8DTR | A_8DTR | D_8DTR | DQS;
OCTOSPI2->TCR = OCTOSPI_TCR_DHQC | MX_DUMMY_READ_DTR;
OCTOSPI2->IR = MX_OPI_READ_DTR;
OCTOSPI2->WCCR = 0u;
OCTOSPI2->CR |= FM_MAP;
return 0;
}
int mx_unmap(void) /* ABORT: упреждение остановлено, FIFO пуст */
{
OCTOSPI2->CR |= OCTOSPI_CR_ABORT;
uint32_t t0 = mx_now_ms();
while (OCTOSPI2->CR & OCTOSPI_CR_ABORT) {
MX_POLL();
if (mx_now_ms() - t0 > MX_T_POLL_MS)
return -1;
}
OCTOSPI2->CR &= ~OCTOSPI_CR_FMODE;
return wait_idle();
}
Пока в окне идёт упреждающее чтение, BUSY не падает и менять CCR
нельзя. Перед записью окно закрывают ABORT: он останавливает упреждение
и очищает FIFO, так что после записи из окна не прочитаются старые байты.
Обвязка журнала:
/* Раздел 198ц: чтение журнала — через окно отображения, запись и стирание — косвенным режимом. */
#include <stdint.h>
#include <string.h>
#include "mx_ospi.h"
#include "log_u585.h"
static int f_read(uint32_t a, void *buf, uint32_t n)
{
memcpy(buf, (const void *)(uintptr_t)(MX_MAP_BASE + a), n); /* обычное чтение памяти */
return 0;
}
static int f_prog(uint32_t a, const void *buf, uint32_t n)
{
int r = mx_unmap(); /* пока идёт запись, окна нет */
if (r == 0)
r = mx_prog(a, buf, n);
if (mx_map() != 0)
r = -1;
return r;
}
static int f_erase(uint32_t a)
{
int r = mx_unmap();
if (r == 0)
r = mx_erase4k(a);
if (mx_map() != 0)
r = -1;
return r;
}
static const struct oslog_flash ospi_flash = { f_read, f_prog, f_erase };
int log_open(struct oslog *l)
{
if (mx_init() != 0 || mx_map() != 0)
return -1;
l->f = &ospi_flash;
l->base = LOG_BASE;
l->nsect = LOG_NSECT;
return oslog_mount(l);
}
И сама прошивка — запись раз в минуту; такт 160 МГц — функция
clock_160mhz из раздела 2д, SysTick отсчитывает миллисекунды для
тайм-аутов драйвера:
int main(void)
{
static struct oslog l;
uint32_t next, minutes = 0u;
clock_160mhz();
SysTick->LOAD = 160000u - 1u; /* 160 МГц / 160 000 = 1 кГц */
SysTick->VAL = 0u;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
if (log_open(&l) != 0)
for (;;) {
}
next = ms;
for (;;) {
if ((int32_t)(ms - next) >= 0) {
next += 60000u;
minutes++;
(void)oslog_append(&l, ms / 1000u, 1u, &minutes, sizeof minutes);
}
__WFI();
}
}
Драйвер на модели регистров (mxmodel.c). mx_ospi.c и log_u585.c
собраны на ПК без правок, OCTOSPI2, DLYB_OCTOSPI2, RCC и порты —
структуры из stm32u585xx.h. В циклах ожидания драйвера (MX_POLL) шагает
модель: разбирает команду по CCR, TCR, IR, AR, DLR, сверяет кадр с
правилами пакета (линии, DTR, 16 бит кода, второй байт — инверсия первого,
пустые такты, DQS, тип памяти контроллера) и исполняет его на модели флеш:
WEL, WIP, CR2, переключение SPI → DOPI, запись 1 → 0, стирание. Команда на
«чужом» языке флеш не понимает — модель её пропускает.
| Опыт на модели | Итог |
|---|---|
| флеш застала сброс МК в SPI, OPI STR, OPI DTR с идущим стиранием | во всех трёх mx_init = 0, CR2 0x02 и 0x07, пропущено по 4 команды сброса на чужих языках, ошибок 0 |
| журнал поверх драйвера: 300 записей и подъём заново | 302 записи страниц (300 + 2 заголовка), 304 WREN, после подъёма следующий номер 301, в обходе 300 |
| порча: нет сброса на языке OPI DTR | из DTR флеш не выходит, mx_init = −1 |
| порча: запись и стирание без WREN | модель ловит «pp_without_wel» и «erase_without_wel» |
Сборка armclang 6.24, Cortex-M33 с FPU, -std=c11 -Os -Wall -Wextra
-pedantic — без предупреждений. Без компоновки: вектора и стартовый код
— из проекта приложения.
| Файл | Код, байт | Константы, байт |
|---|---|---|
oslog.c |
1210 | 56 |
mx_ospi.c |
1802 | 88 |
log_u585.c |
156 | 44 |
app_u585.c |
156 | 24 |
start2f.c (такт, 2д) |
140 | 8 |
| всего | 3464 | 220 |
Подъём по шине. 2048 заголовков и 127 слотов — 2175 чтений по 32 байта, каждое — отдельная команда в 25 тактов: 54 375 тактов, 1,36 мс на 40 МГц без пауз и задержек шины.
Ресурс. Одна запись в минуту, 127 записей в секторе — сектор заполняется за 127 минут. В кольце из 2048 секторов каждый стирается раз в 2048 × 127 = 260 096 минут — 2,02 раза в год. Если отдать журналу всю флеш, 16 384 сектора, — 0,25 раза в год и почти четыре года истории (1445 суток). Сколько циклов стирания выдерживает MX25LM51245G, в пакетах не написано — даташита микросхемы там нет, поэтому лет здесь не будет: возьмите число циклов из даташита и разделите на 2,02. Для сравнения, у встроенной Flash F103 в 198а ресурс 10 000 циклов — по нему журнал изнашивал её на полпроцента в год. Что ещё ограничивает срок — хранение данных и температура — разобрано в 204.
198ц.7 Грабли
- Флеш не сбрасывается вместе с МК. После перезапуска кнопкой или отладчиком она остаётся в DOPI, а то и посреди стирания, и SPI-команд не понимает. Поэтому сброс шлют на всех трёх языках и ждут 100 мс. На модели порча без сброса на языке DTR оставляет флеш в DTR навсегда.
- Нечётная длина в DTR. Без DQS адрес и длина косвенной команды должны быть чётными (RM0456, 28.4.9). Слот в 32 байта это правило выполняет сам; запись «сколько байт пришло» — нет.
- Запись через край страницы. Одна команда пишет от 1 до 256 байт в
пределах страницы (у драйвера: «Range 1 ~ MX25LM51245G_PAGE_SIZE»), и
пакет платы режет длинную запись по страницам сам. Что флеш сделает с
записью через край, в пакете не сказано —
mx_progеё не пускает. - Запись регистров при BUSY = 1 пропадает молча. RM0456 (28.4.15): «any
register write operation when BUSY = 1 has no effect and is not signaled
with an error response». Поэтому
cmd,pollиmx_mapсначала ждутBUSY= 0, а окно перед записью закрываютABORT. - Чтение окна не вовремя. При выключенном отображении любое чтение окна
— ошибка шины AHB, а за пределами
DEVSIZE, но внутри 256 МБ — у ядра HardFault (RM0456, 28.4.11 и 28.4.17). Отладчик с открытым окном памяти на0x7000 0000ловит это первым. - Порядок байт в DTR. Для Macronix нужен
MTYP= 001, иначе байты пары меняются местами: «читается, но не то». В SPI пакет держит режим Micron —MTYPменяют вместе с переходом в DOPI. - SSHIFT и DHQC. При
DDTR= 1 сдвиг выборкиSSHIFTдолжен быть снят;DHQCнельзя ставить при делителе 0 (RM0456, 28.4.5). - «Сектор» в одном файле — не «сектор» в другом. 4 КБ и 64 КБ разные версии драйвера зовут по-разному, и предельные времена у них разные: 64 КБ — 1000 или 2000 мс, вся флеш — 460 или 300 с.
- Недостёртый сектор выглядит чистым. Оборванное стирание может
оставить
0xFFпочти везде. Решать по виду «сектор пуст» нельзя — стирать перед каждым заходом. - Большое кольцо — долгий подъём. Заголовки читаются все: на 2048 секторах — 1,36 мс только по шине, на всю флеш, 16 384 сектора, — 10,32 мс.
198ц.8 Итоги
- MX25LM51245G: 64 МБ, блоки 64 КБ (1024), секторы 4 КБ (16 384), страница
256 байт (262 144); OCTOSPI2, окно с
0x7000 0000, выводы PF0–PF4, PF12, PH9–PH12, PI5 на AF5. - После сброса — SPI; CR2
0x300←0x07(6 пустых тактов), CR20←0x02(DOPI). В OPI код — два байта с инверсией, адрес — 4 байта, статус — с адресом 0 и 5 пустыми тактами, чтение0xEE11— 6 пустых и DQS. - Чтение 32 байт на 40 МГц: 304 такта в SPI, 44 в OPI STR, 25 в OPI DTR; поток до 5, 40 и 80 МБ/с.
- Запись: WREN и WEL,
0x12ED, опрос WIP контроллером; в DTR без DQS — чётные адрес и длина. Стирание 4 КБ — до 400 мс. - Журнал: слот 32 байта, 127 записей на сектор, заголовок с номером захода; подъём — наибольший заход и последняя верная CRC. 14 274 обрыва на модели — ни одной потерянной подтверждённой записи.
- Прошивка — 3464 байта кода; кольцо 2048 секторов — 260 096 записей, полгода поминутно, 2,02 стирания сектора в год.
Полные исходники
mx_ospi.c — MX25LM51245G на OCTOSPI2 регистрами.
/* Раздел 198ц: MX25LM51245G на OCTOSPI2 — регистрами. Порядок и значения повторяют
b_u585i_iot02a_ospi.c из пакета платы: такт 160 МГц / 4 = 40 МГц, CSHT 2 такта,
блок задержки DLYB, сброс флеш на трёх «языках», CR2: 6 пустых тактов, затем DOPI. */
#include "stm32u5xx.h"
#include "mx_ospi.h"
#ifndef MX_POLL
#define MX_POLL() /* на ПК здесь шагает модель */
#endif
#ifndef MX_DR8
#define MX_DR8 (*(volatile uint8_t *)&OCTOSPI2->DR) /* байтовый доступ к младшему байту DR */
#endif
/* Поля CCR: линии и скорость фаз инструкции (I), адреса (AD) и данных (D) */
#define I_1 OCTOSPI_CCR_IMODE_0 /* 1 линия, 8 бит */
#define I_8DTR (OCTOSPI_CCR_IMODE_2 | OCTOSPI_CCR_IDTR | OCTOSPI_CCR_ISIZE_0) /* 8 линий, 16 бит */
#define I_8STR (OCTOSPI_CCR_IMODE_2 | OCTOSPI_CCR_ISIZE_0)
#define A_1 (OCTOSPI_CCR_ADMODE_0 | OCTOSPI_CCR_ADSIZE) /* 1 линия, 32 бита */
#define A_8DTR (OCTOSPI_CCR_ADMODE_2 | OCTOSPI_CCR_ADDTR | OCTOSPI_CCR_ADSIZE)
#define D_1 OCTOSPI_CCR_DMODE_0
#define D_8DTR (OCTOSPI_CCR_DMODE_2 | OCTOSPI_CCR_DDTR)
#define DQS OCTOSPI_CCR_DQSE
#define NODATA_DTR OCTOSPI_CCR_DDTR /* HAL ставит DDTR и командам без данных, когда включён DHQC */
#define FM_WRITE 0u
#define FM_READ OCTOSPI_CR_FMODE_0
#define FM_POLL OCTOSPI_CR_FMODE_1
#define FM_MAP OCTOSPI_CR_FMODE
static int dtr; /* флеш уже в OPI DTR */
static void pins(void)
{
/* PF0–PF3 IO0–IO3, PF4 CLK, PF12 DQS, PH9–PH12 IO4–IO7, PI5 NCS — все AF5 */
static GPIO_TypeDef *const port[] = { GPIOF, GPIOF, GPIOF, GPIOF, GPIOF, GPIOF,
GPIOH, GPIOH, GPIOH, GPIOH, GPIOI };
static const uint8_t pin[] = { 0, 1, 2, 3, 4, 12, 9, 10, 11, 12, 5 };
static const uint8_t pull[] = { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; /* подтяжка вверх: DQS и NCS */
RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOFEN | RCC_AHB2ENR1_GPIOHEN | RCC_AHB2ENR1_GPIOIEN;
for (uint32_t i = 0; i < sizeof pin; i++) {
GPIO_TypeDef *g = port[i];
uint32_t p = pin[i];
g->OSPEEDR |= 3u << (2u * p); /* очень высокая скорость */
g->PUPDR = (g->PUPDR & ~(3u << (2u * p))) | ((uint32_t)pull[i] << (2u * p));
g->AFR[p >> 3] = (g->AFR[p >> 3] & ~(15u << (4u * (p & 7u)))) | (5u << (4u * (p & 7u)));
g->MODER = (g->MODER & ~(3u << (2u * p))) | (2u << (2u * p)); /* альтернативная функция */
}
}
static int wait_sr(uint32_t flag, uint32_t ms)
{
uint32_t t0 = mx_now_ms();
for (;;) {
MX_POLL();
if (OCTOSPI2->SR & flag)
return 0;
if (mx_now_ms() - t0 > ms)
return -1;
}
}
static int wait_idle(void) /* BUSY = 0 — можно менять CCR, TCR, IR */
{
uint32_t t0 = mx_now_ms();
for (;;) {
MX_POLL();
if (!(OCTOSPI2->SR & OCTOSPI_SR_BUSY))
return 0;
if (mx_now_ms() - t0 > MX_T_POLL_MS)
return -1;
}
}
/* Одна команда в косвенном режиме. Старт: запись AR, если есть адрес; иначе запись IR;
при передаче данных — первая запись в DR (RM0456, 28.4.9). */
static int cmd(uint32_t fmode, uint32_t ccr, uint32_t ir, uint32_t dcyc, uint32_t addr,
uint8_t *rx, const uint8_t *tx, uint32_t n)
{
if (wait_idle() != 0)
return -1;
OCTOSPI2->CR = (OCTOSPI2->CR & ~OCTOSPI_CR_FMODE) | fmode;
if (n)
OCTOSPI2->DLR = n - 1u;
OCTOSPI2->CCR = ccr;
OCTOSPI2->TCR = (OCTOSPI2->TCR & ~OCTOSPI_TCR_DCYC) | dcyc;
OCTOSPI2->IR = ir;
if (ccr & OCTOSPI_CCR_ADMODE)
OCTOSPI2->AR = addr;
for (uint32_t i = 0; tx && i < n; i++) {
if (wait_sr(OCTOSPI_SR_FTF, MX_T_POLL_MS) != 0)
return -1;
MX_DR8 = tx[i];
}
for (uint32_t i = 0; rx && i < n; i++) {
if (wait_sr(OCTOSPI_SR_FTF | OCTOSPI_SR_TCF, MX_T_POLL_MS) != 0)
return -1;
rx[i] = MX_DR8;
}
if (wait_sr(OCTOSPI_SR_TCF, MX_T_POLL_MS) != 0)
return -1;
OCTOSPI2->FCR = OCTOSPI_FCR_CTCF;
return 0;
}
/* Автоматический опрос статуса: контроллер сам читает регистр, пока (SR & mask) != match. */
static int poll(uint32_t mask, uint32_t match, uint32_t ms)
{
uint32_t ccr = dtr ? (I_8DTR | A_8DTR | D_8DTR | DQS) : (I_1 | D_1);
if (wait_idle() != 0)
return -1;
OCTOSPI2->PSMAR = match;
OCTOSPI2->PSMKR = mask;
OCTOSPI2->PIR = 0x10u; /* MX25LM51245G_AUTOPOLLING_INTERVAL_TIME */
OCTOSPI2->CR = (OCTOSPI2->CR & ~(OCTOSPI_CR_FMODE | OCTOSPI_CR_PMM)) | FM_POLL | OCTOSPI_CR_APMS;
OCTOSPI2->DLR = dtr ? 1u : 0u; /* в DTR статус читается двумя байтами */
OCTOSPI2->CCR = ccr;
OCTOSPI2->TCR = (OCTOSPI2->TCR & ~OCTOSPI_TCR_DCYC) | (dtr ? MX_DUMMY_REG_DTR : 0u);
OCTOSPI2->IR = dtr ? MX_OPI_READ_STATUS : MX_SPI_READ_STATUS;
if (dtr)
OCTOSPI2->AR = 0u;
if (wait_sr(OCTOSPI_SR_SMF, ms) != 0)
return -1;
OCTOSPI2->FCR = OCTOSPI_FCR_CSMF;
return 0;
}
static int write_enable(void)
{
if (dtr ? cmd(FM_WRITE, I_8DTR | NODATA_DTR, MX_OPI_WRITE_ENABLE, 0u, 0u, 0, 0, 0u)
: cmd(FM_WRITE, I_1, MX_SPI_WRITE_ENABLE, 0u, 0u, 0, 0, 0u))
return -1;
return poll(MX_SR_WEL, MX_SR_WEL, MX_T_POLL_MS); /* дождаться WEL = 1 */
}
static void delay_ms(uint32_t ms)
{
uint32_t t0 = mx_now_ms();
while (mx_now_ms() - t0 < ms)
MX_POLL();
}
/* Настройка контроллера: Micron для SPI, Macronix + DHQC для DTR (как MX_OSPI_NOR_Init). */
static void ctrl_setup(int macronix)
{
OCTOSPI2->CR &= ~OCTOSPI_CR_EN;
OCTOSPI2->DCR1 = (macronix ? OCTOSPI_DCR1_MTYP_0 : 0u) /* 001 Macronix, 000 Micron */
| (25u << OCTOSPI_DCR1_DEVSIZE_Pos) /* 2^(25 + 1) = 64 МБ */
| (1u << OCTOSPI_DCR1_CSHT_Pos); /* NCS между командами — 2 такта */
OCTOSPI2->DCR2 = 3u << OCTOSPI_DCR2_PRESCALER_Pos; /* 160 МГц / 4 = 40 МГц */
OCTOSPI2->DCR3 = 0u;
OCTOSPI2->DCR4 = 0u;
OCTOSPI2->CR = (OCTOSPI2->CR & ~OCTOSPI_CR_FTHRES) | (3u << OCTOSPI_CR_FTHRES_Pos); /* порог 4 байта */
OCTOSPI2->TCR = macronix ? OCTOSPI_TCR_DHQC : 0u; /* выход сдвинут на четверть такта */
OCTOSPI2->CR |= OCTOSPI_CR_EN;
OCTOSPI2->DCR1 |= OCTOSPI_DCR1_FRCK; /* такт всегда идёт — после EN */
}
/* Блок задержки: длина линии на один такт по RM0456 (32.4.4), фаза — четверть, как в пакете платы. */
static int dlyb_tune(void)
{
uint32_t unit, lng = 0u, n;
DLYB_OCTOSPI2->CR = DLYB_CR_DEN | DLYB_CR_SEN;
for (unit = 0u; unit < 128u; unit++) {
DLYB_OCTOSPI2->CFGR = 12u | (unit << DLYB_CFGR_UNIT_Pos); /* все 12 ячеек */
uint32_t t0 = mx_now_ms();
while (!(DLYB_OCTOSPI2->CFGR & DLYB_CFGR_LNGF)) {
MX_POLL();
if (mx_now_ms() - t0 > 255u)
return -1;
}
lng = (DLYB_OCTOSPI2->CFGR & DLYB_CFGR_LNG_Msk) >> DLYB_CFGR_LNG_Pos;
if ((lng & 0x7FFu) != 0u && (lng & 0xC00u) != 0xC00u)
break; /* линия длиной в один такт */
}
if (unit == 128u)
return -1;
for (n = 10u; n > 0u && (lng >> n) == 0u; n--) {
}
if (n == 0u)
return -1;
DLYB_OCTOSPI2->CFGR = (n / 4u) | (unit << DLYB_CFGR_UNIT_Pos); /* SEL и UNIT пишутся при SEN = 1 */
DLYB_OCTOSPI2->CR = DLYB_CR_DEN; /* SEN = 0: выход на выбранной фазе */
OCTOSPI2->CR |= OCTOSPI_CR_ABORT;
while (OCTOSPI2->CR & OCTOSPI_CR_ABORT)
MX_POLL();
OCTOSPI2->DCR1 &= ~OCTOSPI_DCR1_FRCK;
return 0;
}
int mx_init(void)
{
uint8_t cr2[2];
uint8_t v;
pins();
RCC->AHB2ENR2 |= RCC_AHB2ENR2_OCTOSPI2EN;
RCC->AHB2RSTR2 |= RCC_AHB2RSTR2_OCTOSPI2RST;
RCC->AHB2RSTR2 &= ~RCC_AHB2RSTR2_OCTOSPI2RST;
dtr = 0;
ctrl_setup(0);
if (dlyb_tune() != 0)
return -1;
/* Флеш не сбрасывается вместе с МК: она может ждать команды в SPI, OPI STR или OPI DTR.
Сброс шлётся на всех трёх — поймёт тот, на котором она сейчас. */
if (cmd(FM_WRITE, I_1, MX_SPI_RESET_ENABLE, 0u, 0u, 0, 0, 0u) || cmd(FM_WRITE, I_1, MX_SPI_RESET_MEMORY, 0u, 0u, 0, 0, 0u)
|| cmd(FM_WRITE, I_8STR, MX_OPI_RESET_ENABLE, 0u, 0u, 0, 0, 0u) || cmd(FM_WRITE, I_8STR, MX_OPI_RESET_MEMORY, 0u, 0u, 0, 0, 0u)
|| cmd(FM_WRITE, I_8DTR, MX_OPI_RESET_ENABLE, 0u, 0u, 0, 0, 0u) || cmd(FM_WRITE, I_8DTR, MX_OPI_RESET_MEMORY, 0u, 0u, 0, 0, 0u))
return -1;
delay_ms(MX_T_RESET_MS); /* сброс мог прервать стирание */
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) != 0)
return -1;
v = MX_CR2_DC_6; /* CR2 0x300: 6 пустых тактов */
if (write_enable() || cmd(FM_WRITE, I_1 | A_1 | D_1, MX_SPI_WRITE_CFG2, 0u, MX_CR2_REG3, 0, &v, 1u))
return -1;
v = MX_CR2_DOPI; /* CR2 0x000: DOPI */
if (write_enable() || cmd(FM_WRITE, I_1 | A_1 | D_1, MX_SPI_WRITE_CFG2, 0u, MX_CR2_REG1, 0, &v, 1u))
return -1;
delay_ms(MX_T_WRITE_REG_MS);
ctrl_setup(1);
dtr = 1;
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) != 0
|| cmd(FM_READ, I_8DTR | A_8DTR | D_8DTR | DQS, MX_OPI_READ_CFG2, MX_DUMMY_REG_DTR, MX_CR2_REG1, cr2, 0, 2u))
return -1;
return cr2[0] == MX_CR2_DOPI ? 0 : -1;
}
/* Запись страницы: WEL, команда 0x12ED, ожидание WIP = 0. В DTR без DQS адрес и длина чётные. */
int mx_prog(uint32_t addr, const void *buf, uint32_t n)
{
if (n == 0u || n > 256u || (addr & 1u) || (n & 1u) || (addr & 255u) + n > 256u)
return -1;
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) || write_enable()
|| cmd(FM_WRITE, I_8DTR | A_8DTR | D_8DTR, MX_OPI_PAGE_PROG, 0u, addr, 0, buf, n))
return -1;
return poll(MX_SR_WIP, 0u, MX_T_POLL_MS);
}
int mx_erase4k(uint32_t addr)
{
if (addr & 4095u)
return -1;
if (poll(MX_SR_WIP, 0u, MX_T_POLL_MS) || write_enable()
|| cmd(FM_WRITE, I_8DTR | A_8DTR | NODATA_DTR, MX_OPI_ERASE_4K, 0u, addr, 0, 0, 0u))
return -1;
return poll(MX_SR_WIP, 0u, MX_T_POLL_MS);
}
/* Отображение в память: чтение 0xEE11, 6 пустых тактов, DQS. Запись через окно не настроена:
WCCR = 0, и запись в окно даёт ошибку шины (RM0456, 28.4.17). */
int mx_map(void)
{
if (wait_idle() != 0)
return -1;
OCTOSPI2->CR &= ~OCTOSPI_CR_FMODE;
OCTOSPI2->CCR = I_8DTR | A_8DTR | D_8DTR | DQS;
OCTOSPI2->TCR = OCTOSPI_TCR_DHQC | MX_DUMMY_READ_DTR;
OCTOSPI2->IR = MX_OPI_READ_DTR;
OCTOSPI2->WCCR = 0u;
OCTOSPI2->CR |= FM_MAP;
return 0;
}
int mx_unmap(void) /* ABORT: упреждение остановлено, FIFO пуст */
{
OCTOSPI2->CR |= OCTOSPI_CR_ABORT;
uint32_t t0 = mx_now_ms();
while (OCTOSPI2->CR & OCTOSPI_CR_ABORT) {
MX_POLL();
if (mx_now_ms() - t0 > MX_T_POLL_MS)
return -1;
}
OCTOSPI2->CR &= ~OCTOSPI_CR_FMODE;
return wait_idle();
}
oslog.c — журнал по кругу во внешней NOR-флеш: подъём после сбоя, запись, обход.
/* Раздел 198ц: журнал по кругу во внешней NOR-флеш — подъём после сбоя, запись, обход. */
#include <string.h>
#include "oslog.h"
_Static_assert(sizeof(struct oslog_rec) == OSLOG_SLOT, "запись — ровно 32 байта");
_Static_assert(sizeof(struct oslog_hdr) == OSLOG_SLOT, "заголовок — ровно 32 байта");
_Static_assert(OSLOG_PAGE % OSLOG_SLOT == 0u, "слот не пересекает край страницы");
uint32_t oslog_crc32(const void *p, uint32_t n) /* CRC-32, полином 0xEDB88320 */
{
const uint8_t *b = p;
uint32_t c = 0xFFFFFFFFu;
while (n--) {
c ^= *b++;
for (int k = 0; k < 8; k++)
c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u)));
}
return ~c;
}
static uint32_t slot_addr(const struct oslog *l, uint32_t sect, uint32_t slot)
{
return l->base + sect * OSLOG_SECTOR + slot * OSLOG_SLOT;
}
static int blank(const void *p) /* все 32 байта — 0xFF */
{
const uint8_t *b = p;
for (uint32_t i = 0; i < OSLOG_SLOT; i++)
if (b[i] != 0xFFu)
return 0;
return 1;
}
static int hdr_ok(const struct oslog *l, const struct oslog_hdr *h, uint32_t sect)
{
return h->magic == OSLOG_MAGIC && h->sseq != 0u && h->sseq != 0xFFFFFFFFu
&& (h->sseq - 1u) % l->nsect == sect && h->crc == oslog_crc32(h, 28u);
}
static int rec_ok(const struct oslog_rec *r)
{
return r->seq != 0xFFFFFFFFu && r->len <= OSLOG_DATA && r->crc == oslog_crc32(r, 28u);
}
/* Переход в следующий сектор: стереть его всегда (даже если он выглядит чистым),
потом записать заголовок. Обрыв на любом шаге оставляет прежний сектор головным. */
static int rotate(struct oslog *l)
{
uint32_t n = (l->head + 1u) % l->nsect;
struct oslog_hdr h, chk;
memset(&h, 0xFF, sizeof h);
h.magic = OSLOG_MAGIC;
h.sseq = l->sseq + 1u;
h.first = l->next;
h.crc = oslog_crc32(&h, 28u);
if (l->f->erase(slot_addr(l, n, 0u)) != 0 || l->f->prog(slot_addr(l, n, 0u), &h, sizeof h) != 0
|| l->f->read(slot_addr(l, n, 0u), &chk, sizeof chk) != 0 || memcmp(&h, &chk, sizeof h) != 0)
return -1;
l->head = n;
l->sseq = h.sseq;
l->slot = 1u;
return 0;
}
/* Подъём: головной сектор — с наибольшим номером захода среди целых заголовков;
в нём последняя целая запись даёт следующий номер, а место — за последним непустым слотом. */
int oslog_mount(struct oslog *l)
{
struct oslog_hdr h;
struct oslog_rec r;
uint32_t best = 0u, first = 1u;
l->head = 0u;
for (uint32_t i = 0; i < l->nsect; i++) {
if (l->f->read(slot_addr(l, i, 0u), &h, sizeof h) != 0)
return -1;
if (hdr_ok(l, &h, i) && h.sseq > best) {
best = h.sseq;
first = h.first;
l->head = i;
}
}
l->torn = 0u;
l->skipped = 0u;
if (best == 0u) { /* чистая флеш: начать с сектора 0 */
l->head = l->nsect - 1u;
l->sseq = 0u;
l->next = 1u;
return rotate(l);
}
l->sseq = best;
l->next = first;
l->slot = 1u;
for (uint32_t s = 1u; s < OSLOG_SLOTS; s++) {
if (l->f->read(slot_addr(l, l->head, s), &r, sizeof r) != 0)
return -1;
if (blank(&r))
continue;
l->slot = s + 1u;
if (rec_ok(&r) && r.seq >= l->next)
l->next = r.seq + 1u;
else
l->torn++;
}
return 0;
}
/* Запись: слот пишется один раз и сразу читается обратно; не совпало — слот пропускается. */
int oslog_append(struct oslog *l, uint32_t time, uint16_t type, const void *data, uint16_t len)
{
struct oslog_rec r, chk;
if (len > OSLOG_DATA)
return -1;
memset(&r, 0, sizeof r);
r.seq = l->next;
r.time = time;
r.type = type;
r.len = len;
memcpy(r.data, data, len);
r.crc = oslog_crc32(&r, 28u);
for (int tries = 0; tries < 4; tries++) {
if (l->slot >= OSLOG_SLOTS && rotate(l) != 0)
return -1;
uint32_t a = slot_addr(l, l->head, l->slot++);
if (l->f->prog(a, &r, sizeof r) == 0 && l->f->read(a, &chk, sizeof chk) == 0
&& memcmp(&r, &chk, sizeof r) == 0) {
l->next++;
return 0;
}
l->skipped++;
}
return -1;
}
/* Обход от новой записи к старой: секторы назад от головного, пока номера захода идут подряд.
cb вернул не 0 — стоп. Результат — сколько записей отдано. */
uint32_t oslog_walk(const struct oslog *l, int (*cb)(const struct oslog_rec *r, void *ctx), void *ctx)
{
struct oslog_hdr h;
struct oslog_rec r;
uint32_t n = 0u, want = l->next;
for (uint32_t j = 0; j < l->nsect && j < l->sseq; j++) {
uint32_t sect = (l->head + l->nsect - j) % l->nsect;
if (l->f->read(slot_addr(l, sect, 0u), &h, sizeof h) != 0 || !hdr_ok(l, &h, sect) || h.sseq != l->sseq - j)
break;
for (uint32_t s = (j == 0u ? l->slot : OSLOG_SLOTS) - 1u; s >= 1u; s--) {
if (l->f->read(slot_addr(l, sect, s), &r, sizeof r) != 0)
return n;
if (!rec_ok(&r) || r.seq >= want || r.seq < h.first)
continue;
want = r.seq;
n++;
if (cb && cb(&r, ctx))
return n;
}
}
return n;
}
app_u585.c — прошивка B-U585I-IOT02A: запись в журнал раз в минуту.
/* Раздел 198ц: прошивка B-U585I-IOT02A — запись в журнал раз в минуту.
Такт 160 МГц — clock_160mhz() из раздела 2д; SysTick — миллисекунды. */
#include "stm32u5xx.h"
#include "log_u585.h"
void clock_160mhz(void);
static volatile uint32_t ms;
void SysTick_Handler(void)
{
ms++;
}
uint32_t mx_now_ms(void)
{
return ms;
}
int main(void)
{
static struct oslog l;
uint32_t next, minutes = 0u;
clock_160mhz();
SysTick->LOAD = 160000u - 1u; /* 160 МГц / 160 000 = 1 кГц */
SysTick->VAL = 0u;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
if (log_open(&l) != 0)
for (;;) {
}
next = ms;
for (;;) {
if ((int32_t)(ms - next) >= 0) {
next += 60000u;
minutes++;
(void)oslog_append(&l, ms / 1000u, 1u, &minutes, sizeof minutes);
}
__WFI();
}
}
Дальше: раздел 198ч «Мини-проект: USB-хост на B-U585I-IOT02A — клавиатура и флешка» — журнал лёг во внешнюю флеш, теперь та же плата становится хозяином шины USB: к ней подключаются клавиатура и флешка, на которую журнал можно переписать.