Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32U585 · уровень продвинутый · 41 мин чтения
Открыть в интерактивном справочнике

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 мс.

Журнал в MX25LM51245G: кольцо из 2048 секторов по 4 КБ с адреса 0x0380 0000; в каждом секторе слот 0 — заголовок с номером захода, слоты 1–127 — записи по 32 байта с CRC-32; оборванная запись остаётся в своём слоте, следующая ложится за ней; чтение идёт через окно 0x7000 0000 командой 0xEE11, запись — ABORT, WREN 0x06F9, 0x12ED и опрос WIP командой 0x05FA


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:

Сколько тактов занимает чтение. Инструкция 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 пакета платы:

  1. Контроллер: такт 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 байта.
  2. Блок задержки DLYB: подобрать длину линии на один такт (порядок — RM0456, 32.4.4) и взять фазу в четверть этой длины — у пакета это «emperic value», делитель 4.
  3. Сброс флеш командами 0x66/0x99 на всех трёх языках — SPI, OPI STR и OPI DTR — и пауза 100 мс (MX25LM51245G_RESET_MAX_TIME: «when SWreset during erase operation»).
  4. Дождаться WIP = 0, разрешить запись, CR2 0x300 ← 0x07 (6 пустых тактов), снова разрешить, CR2 0 ← 0x02 (DOPI), пауза 40 мс (MX25LM51245G_WRITE_REG_MAX_TIME).
  5. Контроллер — в режим 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 может только стирание целого сектора. Отсюда устройство журнала:

/* Раздел 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 Обрыв питания: подъём и проверка на модели

После сброса журнал ничего не помнит и восстанавливает всё по флеш:

  1. Прочитать заголовки всех секторов. Головной — с наибольшим номером захода среди целых (магия, CRC и номер, который сходится с местом).
  2. Целых заголовков нет — чистая флеш: стереть сектор 0 и начать с него.
  3. В головном секторе пройти все 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 Грабли


198ц.8 Итоги

Полные исходники

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: к ней подключаются клавиатура и флешка, на которую журнал можно переписать.

Открыть в интерактивном справочнике