Embedded HandbookПриложения
Все разделы › VI. Практикум
STM32F407, STM32F103 · уровень средний · 19 мин чтения
Открыть в интерактивном справочнике

42а. LittleFS на SPI NOR: файловая система, которая переживает отключение питания

В разделе 42 FatFS легла на W25Q128 и показала две слабости. Питание, пропавшее посреди записи, оставляет файл нулевой длины или с мусором. Таблица размещения всё время пишется в одни и те же секторы и изнашивает их первыми. Для одного значения в 173а хватило своего журнала. Когда нужны именно файлы — настройки, калибровки, журнал событий, — на голую SPI NOR ставят LittleFS: она не пишет поверх старого и сама разносит стирания по микросхеме.

Здесь — как LittleFS добивается целостности при обрыве, что значит каждая настройка lfs_config, драйвер из четырёх функций поверх SPI и цифры опыта: тысяча обрывов на каждый размер файла, износ блоков за два миллиона сохранений, место на диске и в памяти.

Опоры: исходники LittleFS 2.11 (формат диска 2.1) из дерева Zephyr 4.4.2, каталог modules/fs/littlefs: lfs.h, lfs.c, описание устройства DESIGN.md. Опыт — LittleFS, собранная gcc на ПК поверх модели NOR-флеш с выдёргиванием питания. Драйвер для F407 собран armclang, команды W25Q128 — те же, что в 41.5.

Как это называется в документации

Оригинал Перевод
block device блочное устройство: четыре функции доступа к флеш
block, erase block блок: единица стирания, у W25Q — сектор 4 КБ
metadata pair пара блоков метаданных
commit коммит: порция записей в журнал метаданных с одной контрольной суммой
compaction уплотнение: перенос живых записей во второй блок пары
revision count номер ревизии блока
copy-on-write (COW) запись в новое место вместо перезаписи
inline file встроенный файл: лежит прямо в метаданных
CTZ skip-list список блоков файла с указателями через ступени
lookahead buffer окно поиска свободных блоков, бит на блок
dynamic wear leveling динамическое выравнивание износа: только по свободным блокам
superblock суперблок: описание тома в блоках 0 и 1

42а.1 Как LittleFS не портит данные при обрыве

FAT меняет данные на месте: кластеры, таблицу и каталог — три записи, между которыми том несогласован. LittleFS на месте не меняет ничего.

Метаданные — имена, размеры, где лежат файлы — хранятся в паре блоков. Пара — журнал: изменения дописываются в конец блока коммитами, у каждого коммита своя 32-битная CRC. Старые записи при этом не трогаются — перезапись флеш потребовала бы стирания. Когда блок заполнен, живые записи переносятся во второй блок пары: он стирается, получает ревизию на единицу больше и заполняется целиком. Пока новая копия не дописана до CRC, старая цела в первом блоке.

Данные файла больше 256 байт лежат в отдельных блоках, и новые данные всегда пишутся в свободные блоки. Старые блоки остаются как есть. Последний шаг сохранения — коммит в метаданные, который переключает файл на новые блоки. Файл до 256 байт (inline_max нашей настройки) своих блоков не имеет вовсе: он лежит прямо в коммите.

При монтировании LittleFS берёт блок пары с более свежей ревизией и в нём — последний коммит с верной CRC. Недописанный коммит CRC не проходит и отбрасывается: файл остаётся в прежней версии.

Как LittleFS сохраняет файл и итог тысячи обрывов: пара блоков метаданных, в блоке с большей ревизией коммиты с CRC, оборванный коммит с несошедшейся CRC отбрасывается, прошлое уплотнение лежит во втором блоке; данные пишутся в свободные блоки, старые не стираются сразу; справа — сколько раз из 1000 обрывов файл оказался мусором: у LittleFS 0 для файлов 100, 1000 и 10 000 байт, у записи поверх со стиранием 998, 1000 и 1000

Отсюда и выравнивание износа. Данные уходят в свободные блоки, и распределитель проходит по тому по кругу, после каждого монтирования — со случайного места. Пару метаданных LittleFS через block_cycles стираний переносит в другие блоки (42а.5).


42а.2 Настройка: lfs_config

LittleFS ничего не знает о микросхеме. Ей дают структуру lfs_config: четыре функции доступа и размеры. Значения ниже — для первых 512 КБ W25Q128; смысл — по комментариям lfs.h.

Поле У нас Что значит
read_size 16 наименьшее чтение; все чтения кратны ему
prog_size 16 наименьшая запись; все записи кратны ему
block_size 4096 блок стирания; файл больше встроенного занимает минимум один блок
block_count 128 сколько блоков в томе: 128 × 4 КБ = 512 КБ
cache_size 256 размер кэша; нужны кэш чтения, кэш записи и по одному на открытый файл
lookahead_size 16 окно поиска свободных блоков, бит на блок: 16 байт — 128 блоков
block_cycles 500 через сколько стираний пара метаданных переезжает; советуют 100–1000, −1 — не переезжать

Жёсткие условия проверяет lfs_init: cache_size кратен read_size и prog_size, block_size кратен cache_size, блок не меньше 128 байт, block_cycles не равен нулю. Ноль в 2.11 больше не поддерживается — так сказано прямо в lfs.c: либо −1, либо число.

W25Q умеет записать и один байт, так что prog_size мог быть и 1. Мы берём 16: так же кратно странице 256, и cache_size 256 кратен обоим. inline_max оставлен нулём, то есть наибольшим возможным: не больше cache_size и восьмой части блока. В прогоне это 256 байт.

Буферы отданы статическими (read_buffer, prog_buffer, lookahead_buffer, буфер файла в lfs_file_config). Сборка с LFS_NO_MALLOC убирает lfs_file_open, которой нужна куча; остаётся lfs_file_opencfg со своим буфером.


42а.3 🔧 Драйвер: четыре функции поверх SPI

W25Q128 висит на SPI1 F407 (PA5 — SCK, PA6 — MISO, PA7 — MOSI, PA4 — CS программно), команды — из 41.5: 0x03 чтение, 0x02 запись страницы, 0x20 стирание сектора 4 КБ, 0x06 разрешение записи, 0x05 статус. Блок LittleFS равен сектору, так что erase — одна команда 0x20, а read — одна 0x03 на любой длины кусок.

Внимания требует только запись. LittleFS отдаёт в prog кусок до cache_size байт с любого смещения, кратного 16, — он может пересечь границу страницы 256 байт. А Page Program за границу не переходит: лишнее заворачивается в начало той же страницы (41.5). В опыте из 200 013 вызовов записи 96 775 пересекали границу страницы — почти каждый второй. Поэтому драйвер режет кусок по страницам:

static int bd_prog(const struct lfs_config *c, lfs_block_t block,
                   lfs_off_t off, const void *buffer, lfs_size_t size)
{
    const uint8_t *p = buffer;
    uint32_t addr = (LFS_FIRST_SECTOR + block) * W25_SECTOR + off;
    (void)c;
    while (size > 0u) {                        /* кусками до границы страницы */
        uint32_t n = W25_PAGE - addr % W25_PAGE;
        if (n > size) {
            n = size;
        }
        w25_write_enable();
        CS_LOW();
        w25_cmd_addr(W25_CMD_PAGE_PROGRAM, addr);
        for (uint32_t i = 0; i < n; i++) {
            spi_xfer(p[i]);
        }
        CS_HIGH();
        w25_wait_ready();
        addr += n;
        p += n;
        size -= n;
    }
    return LFS_ERR_OK;
}

Настройка и проверки размеров при сборке — те же условия, что lfs_init проверит при запуске, плюс свои:

static const struct lfs_config cfg = {
    .read  = bd_read,  .prog = bd_prog,  .erase = bd_erase,  .sync = bd_sync,
    .read_size      = 16,
    .prog_size      = 16,
    .block_size     = W25_SECTOR,
    .block_count    = LFS_BLOCKS,
    .block_cycles   = LFS_CYCLES,
    .cache_size     = LFS_CACHE,
    .lookahead_size = sizeof lookahead_buf,
    .read_buffer = read_buf,  .prog_buffer = prog_buf,  .lookahead_buffer = lookahead_buf,
};

/* Первые четыре — условия, которые lfs_init проверяет при запуске; здесь — при сборке. */
_Static_assert(LFS_CACHE % 16u == 0u, "cache_size кратен read_size и prog_size");
_Static_assert(W25_SECTOR % LFS_CACHE == 0u, "block_size кратен cache_size");
_Static_assert(W25_SECTOR >= 128u, "блок вмещает указатели CTZ");
_Static_assert(LFS_CYCLES != 0, "block_cycles = 0 не поддерживается: -1 или 100…1000");

Сохранение настроек — открыть с обрезкой, записать, закрыть. Новая версия становится видна в lfs_file_close (или lfs_file_sync): до этого момента обрыв оставит старую.

int settings_save(const struct settings *s)
{
    int err = lfs_file_opencfg(&lfs, &file, "settings",
                               LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC, &file_cfg);
    if (err) {
        return err;
    }
    lfs_ssize_t n = lfs_file_write(&lfs, &file, s, sizeof *s);
    err = lfs_file_close(&lfs, &file);         /* здесь новая версия становится видна */
    return (n != (lfs_ssize_t)sizeof *s) ? LFS_ERR_IO : err;
}

Весь файл — lfs42b.c: настройка SPI1, bd_read, bd_erase, монтирование с форматированием при первом запуске, чтение настроек. Ожидание готовности здесь без тайм-аута; в изделии его ограничивают, как сказано в 41.5. Для F103 меняется только spi1_init: выводы у F1 настраиваются через CRL, а не MODER и AFR (раздел 8), остальное то же.


42а.4 Опыт: тысяча обрывов питания

LittleFS собрана на ПК вместе с моделью NOR-флеш: 128 блоков по 4096 байт, стирание ставит 0xFF, запись только сбрасывает биты. Каждое стирание и каждый вызов записи — шаг. Обрыв выбирается на случайном шаге одного сохранения: оборванная запись оставляет часть байтов записанными и один — наполовину, оборванное стирание — блок со случайной смесью старого и 0xFF. Затем том монтируется заново, и файл сверяется с обеими версиями.

Для сравнения — наивный способ: файл лежит по постоянному адресу, его секторы стираются и записываются заново постранично.

Файл Шагов на сохранение LittleFS: старый / новый / мусор Поверх: шагов Поверх: старый / новый / мусор
100 байт 1–2 1000 / 0 / 0 2 0 / 2 / 998
1000 байт 6–7 988 / 12 / 0 5 0 / 0 / 1000
10 000 байт 44–45 999 / 1 / 0 43 0 / 0 / 1000

По 1000 обрывов на строку. LittleFS ни разу не отдала мусор и ни разу не отказалась монтироваться. Новая версия появляется только при обрыве уже после того, как легла CRC последнего коммита, — отсюда 12 и 1 «новых». Обрывы на стирании LittleFS тоже пережила: у файла в 100 байт на стирание пришлись 521 обрыв из 1000, у 1000 байт — 269, у 10 000 — 89.

Запись поверх портит файл почти всегда: старая версия стёрта на первом же шаге, новая ещё не дописана. Два «новых» у файла в 100 байт — обрыв на последнем байте, когда уцелевшие биты случайно совпали с нужными.

Ещё одна проверка модели: LittleFS ни разу не попыталась поднять бит записью — во всех прогонах она писала только в стёртое.


42а.5 Износ: куда уходят стирания

Тот же файл в 100 байт сохранён два миллиона раз на пустом томе, при разных block_cycles:

block_cycles Задето блоков Стираний всего Блоки 0–1 Остальные блоки
−1 2 64 516 32 259 0
500 128 64 518 251 501–947
100 128 64 522 53 303–606

Стираний почти поровну при любой настройке: одно на 31 сохранение. Файл встроенный, коммиты дописываются в блок, и стирание нужно только при уплотнении. Разница — где они ложатся. С −1 все стирания достаются паре суперблока 0–1, по 32 259 на блок. С числом пара переезжает, и стирания расходятся по всем 128 блокам.

При ресурсе 100 000 циклов, как в 42.7, это значит:

Меньше block_cycles — ровнее износ, но чаще переезды. Файл в 10 000 байт показывает, что с данными выравнивание работает само: за 2000 сохранений 6015 стираний, по три на сохранение, задеты все 128 блоков, блоки 2–127 — от 24 до 49 стираний и при 500, и при −1. Блоки данных и так берутся по кругу; block_cycles касается только метаданных.

Полный том. LittleFS выравнивает износ только по свободным блокам. Статичные файлы занимают блоки, которые не стираются, и все стирания сходятся на остаток. Том заполнен файлами по 4000 байт, затем те же два миллиона сохранений, block_cycles 100:

Занято Свободно Задето блоков Больше всего стираний у блока
100 файлов в корне 26 27 3838
112 файлов в корне 14 3 43 479
112 файлов, настройки в каталоге d/ 12 16 6262

Вторая строка хуже даже, чем без выравнивания на пустом томе. Корневой каталог живёт в паре суперблока, а суперблок переезжать умеет, только расширяясь, — и lfs.c не расширяет его, если свободно не больше восьмой части тома: 16 блоков из 128. При 14 свободных пара 0–1 стирается 43 479 раз, и block_cycles 500 даёт то же число. Часто переписываемый файл в своём каталоге получает свою пару, а она переезжает как обычно.


42а.6 Сколько места и памяти

Сколько блоков по 4 КБ добавляет файл на пустом томе (сам том пустым занимает 2 блока — пару суперблока):

Файл Блоков
1, 100 или 256 байт 0 — встроен в метаданные
257, 1000, 4000 или 4096 байт 1
8000 байт 2
16 000 байт 4
10 файлов по 100 байт 0
10 файлов по 1000 байт 10, то есть 40 КБ

Граница резкая: 256 байт — ноль блоков, 257 — целый блок 4 КБ. Файл в 1000 байт у SPIFFS в 155.3 занимал 1255 байт, у LittleFS — 4096. Мелкие журналы поэтому не дробят на сотни файлов, а держат в одном, дописывая.

Код LittleFS 2.11, armclang под Cortex-M4, без отладочной печати, assert и кучи:

Код Константы
-Oz 15 830 байт 950 байт
-O2 21 794 байт 606 байт

Под Cortex-M3 (F103) с -Oz — 15 822 байта кода. Против FatFS с записью (~10 КБ, 42.8) это больше. Зато ОЗУ драйвера из 42а.3 — 996 байт на всё: три буфера по 256, 16 байт окна поиска, lfs_t 128 байт и lfs_file_t 84 байта. Стек в это число не входит. У FatFS при секторе 4 КБ только структура тома — 4 КБ с лишним.


42а.7 Грабли

Запись заворачивается внутри страницы. Драйвер передал кусок из prog одной командой 0x02. LittleFS пишет до 256 байт с любого смещения, кратного 16, и почти половина вызовов пересекает границу страницы. Хвост ложится на начало той же страницы. Резать по страницам — в драйвере.

block_cycles = 0. Кажется безобидным «по умолчанию», но в 2.11 ноль больше не поддерживается, и lfs_init его не пропускает. Ставят −1 или 100–1000.

Том забит мелкими файлами. Каждый файл больше 256 байт — минимум один блок 4 КБ. Сотня файлов по 300 байт заняла в опыте 100 блоков — 400 КБ из 512.

Износ на почти полном томе. Выравнивание только по свободным блокам. Меньше восьмой части тома свободно — пара суперблока перестаёт переезжать, и часто переписываемый файл в корне стирает её в десятки раз чаще остальных. Держите запас места и такие файлы — в своём каталоге.

Сохранение то быстрое, то долгое. Обычно — один вызов записи. Раз в 31 сохранение маленького файла приходится уплотнение со стиранием сектора — до 400 мс у W25Q128 (41.5). Не сохраняйте из прерывания и из быстрого цикла управления.

Обрыв до lfs_file_close. Всё, что записано lfs_file_write, но не закрыто и не синхронизировано, при обрыве пропадает — остаётся старая версия. Это не ошибка, а смысл: файл меняется целиком или никак.

Форматирование по любой ошибке монтирования. lfs_mount отвечает ошибкой и тогда, когда драйвер читает не тот адрес. Форматирование в ответ сотрёт настоящий том. На своей микросхеме это допустимо при первом запуске, но ошибку драйвера лучше отличить: прочитать JEDEC ID (41.5) до монтирования.


42а.8 Итоги

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

lfs42b.c — LittleFS на W25Q128 по SPI1 у STM32F407.

/* Раздел 42а — LittleFS на W25Q128 по SPI1 у STM32F407.

   Флеш подключена к SPI1: PA5 — SCK, PA6 — MISO, PA7 — MOSI (AF5),
   PA4 — CS программно. Под файловую систему — первые 128 секторов
   по 4 КБ (512 КБ); остальное место микросхемы свободно для других нужд.
   Проверяется сборкой armclang без предупреждений; часть статических
   утверждений повторяет проверки lfs_init из lfs.c. */
#include <stdint.h>
#include "stm32f4xx.h"
#include "lfs.h"

/* ── команды W25Q128 (те же, что в разделе 41.5) ─────────────────────── */
#define W25_CMD_WRITE_ENABLE   0x06u
#define W25_CMD_READ_STATUS1   0x05u
#define W25_CMD_READ_DATA      0x03u
#define W25_CMD_PAGE_PROGRAM   0x02u
#define W25_CMD_SECTOR_ERASE   0x20u   /* 4 КБ */
#define W25_STATUS_BUSY        0x01u
#define W25_PAGE               256u
#define W25_SECTOR             4096u
#define W25_SECTORS            4096u   /* 16 МБ */

#define LFS_FIRST_SECTOR       0u
#define LFS_BLOCKS             128u    /* 512 КБ */
#define LFS_CACHE              256u
#define LFS_CYCLES             500     /* -1 — без переезда метаданных */

#define CS_LOW()   (GPIOA->BSRR = GPIO_BSRR_BR4)
#define CS_HIGH()  (GPIOA->BSRR = GPIO_BSRR_BS4)

void spi1_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    GPIOA->BSRR = GPIO_BSRR_BS4;                               /* CS вверху до настройки вывода */
    GPIOA->MODER = (GPIOA->MODER & ~0x0000FF00u)
                 | (1u << 8) | (2u << 10) | (2u << 12) | (2u << 14);  /* PA4 выход, PA5–7 — AF */
    GPIOA->OSPEEDR |= 0x0000FF00u;                             /* PA4–7: высокая скорость фронтов */
    GPIOA->AFR[0] = (GPIOA->AFR[0] & ~0xFFF00000u)
                  | (5u << 20) | (5u << 24) | (5u << 28);      /* AF5 — SPI1 */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI      /* ведущий, NSS программно */
              | SPI_CR1_BR_0                                   /* fPCLK / 4: 84 / 4 = 21 МГц */
              | SPI_CR1_SPE;                                   /* режим 0: CPOL = 0, CPHA = 0 */
}

static uint8_t spi_xfer(uint8_t data)
{
    while (!(SPI1->SR & SPI_SR_TXE)) { }
    *(volatile uint8_t *)&SPI1->DR = data;
    while (!(SPI1->SR & SPI_SR_RXNE)) { }
    return *(volatile uint8_t *)&SPI1->DR;
}

static void w25_cmd_addr(uint8_t cmd, uint32_t addr)
{
    spi_xfer(cmd);
    spi_xfer((uint8_t)(addr >> 16));
    spi_xfer((uint8_t)(addr >> 8));
    spi_xfer((uint8_t)addr);
}

static void w25_write_enable(void)
{
    CS_LOW();
    spi_xfer(W25_CMD_WRITE_ENABLE);
    CS_HIGH();
}

static void w25_wait_ready(void)
{
    CS_LOW();
    spi_xfer(W25_CMD_READ_STATUS1);
    while (spi_xfer(0xFF) & W25_STATUS_BUSY) { }
    CS_HIGH();
}

/* ── четыре функции блочного устройства для LittleFS ──────────────────── */
static int bd_read(const struct lfs_config *c, lfs_block_t block,
                   lfs_off_t off, void *buffer, lfs_size_t size)
{
    uint8_t *p = buffer;
    (void)c;
    CS_LOW();
    w25_cmd_addr(W25_CMD_READ_DATA, (LFS_FIRST_SECTOR + block) * W25_SECTOR + off);
    while (size--) {
        *p++ = spi_xfer(0xFF);
    }
    CS_HIGH();
    return LFS_ERR_OK;
}

static int bd_prog(const struct lfs_config *c, lfs_block_t block,
                   lfs_off_t off, const void *buffer, lfs_size_t size)
{
    const uint8_t *p = buffer;
    uint32_t addr = (LFS_FIRST_SECTOR + block) * W25_SECTOR + off;
    (void)c;
    while (size > 0u) {                        /* кусками до границы страницы */
        uint32_t n = W25_PAGE - addr % W25_PAGE;
        if (n > size) {
            n = size;
        }
        w25_write_enable();
        CS_LOW();
        w25_cmd_addr(W25_CMD_PAGE_PROGRAM, addr);
        for (uint32_t i = 0; i < n; i++) {
            spi_xfer(p[i]);
        }
        CS_HIGH();
        w25_wait_ready();
        addr += n;
        p += n;
        size -= n;
    }
    return LFS_ERR_OK;
}

static int bd_erase(const struct lfs_config *c, lfs_block_t block)
{
    (void)c;
    w25_write_enable();
    CS_LOW();
    w25_cmd_addr(W25_CMD_SECTOR_ERASE, (LFS_FIRST_SECTOR + block) * W25_SECTOR);
    CS_HIGH();
    w25_wait_ready();
    return LFS_ERR_OK;
}

static int bd_sync(const struct lfs_config *c)
{
    (void)c;
    return LFS_ERR_OK;                         /* запись синхронная, кэша нет */
}

static uint8_t read_buf[LFS_CACHE], prog_buf[LFS_CACHE], file_buf[LFS_CACHE];
static uint32_t lookahead_buf[4];              /* 16 байт = 128 бит, по биту на блок */

static const struct lfs_config cfg = {
    .read  = bd_read,  .prog = bd_prog,  .erase = bd_erase,  .sync = bd_sync,
    .read_size      = 16,
    .prog_size      = 16,
    .block_size     = W25_SECTOR,
    .block_count    = LFS_BLOCKS,
    .block_cycles   = LFS_CYCLES,
    .cache_size     = LFS_CACHE,
    .lookahead_size = sizeof lookahead_buf,
    .read_buffer = read_buf,  .prog_buffer = prog_buf,  .lookahead_buffer = lookahead_buf,
};

/* Первые четыре — условия, которые lfs_init проверяет при запуске; здесь — при сборке. */
_Static_assert(LFS_CACHE % 16u == 0u, "cache_size кратен read_size и prog_size");
_Static_assert(W25_SECTOR % LFS_CACHE == 0u, "block_size кратен cache_size");
_Static_assert(W25_SECTOR >= 128u, "блок вмещает указатели CTZ");
_Static_assert(LFS_CYCLES != 0, "block_cycles = 0 не поддерживается: -1 или 100…1000");
/* Остальные — свои: геометрия W25Q128 и окно поиска свободных блоков. */
_Static_assert(sizeof lookahead_buf * 8u >= LFS_BLOCKS, "окно поиска свободных блоков покрывает весь том");
_Static_assert(LFS_FIRST_SECTOR + LFS_BLOCKS <= W25_SECTORS, "том помещается в 16 МБ");
_Static_assert(W25_PAGE % 16u == 0u, "страница кратна prog_size");

/* ── настройки прибора: один файл, сохраняется целиком ────────────────── */
struct settings {
    uint32_t magic;
    int32_t  offset_mv;
    uint32_t gain_q16;
    uint32_t boot_count;
};
_Static_assert(sizeof(struct settings) <= LFS_CACHE, "файл настроек помещается в метаданные (inline)");

static lfs_t lfs;
static lfs_file_t file;
static const struct lfs_file_config file_cfg = { .buffer = file_buf };

int storage_mount(void)
{
    int err = lfs_mount(&lfs, &cfg);
    if (err) {                                 /* первый запуск: тома нет */
        err = lfs_format(&lfs, &cfg);
        if (!err) {
            err = lfs_mount(&lfs, &cfg);
        }
    }
    return err;
}

int settings_save(const struct settings *s)
{
    int err = lfs_file_opencfg(&lfs, &file, "settings",
                               LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC, &file_cfg);
    if (err) {
        return err;
    }
    lfs_ssize_t n = lfs_file_write(&lfs, &file, s, sizeof *s);
    err = lfs_file_close(&lfs, &file);         /* здесь новая версия становится видна */
    return (n != (lfs_ssize_t)sizeof *s) ? LFS_ERR_IO : err;
}

int settings_load(struct settings *s)
{
    int err = lfs_file_opencfg(&lfs, &file, "settings", LFS_O_RDONLY, &file_cfg);
    if (err) {
        return err;                            /* LFS_ERR_NOENT — ещё не сохраняли */
    }
    lfs_ssize_t n = lfs_file_read(&lfs, &file, s, sizeof *s);
    lfs_file_close(&lfs, &file);
    return (n == (lfs_ssize_t)sizeof *s) ? LFS_ERR_OK : LFS_ERR_CORRUPT;
}

int main(void)
{
    struct settings s = { 0x5E77u, 0, 1u << 16, 0u };
    spi1_init();
    if (storage_mount() == LFS_ERR_OK) {
        (void)settings_load(&s);
        s.boot_count++;
        (void)settings_save(&s);
        lfs_unmount(&lfs);
    }
    for (;;) { }
}

Дальше: раздел 47 «Регулирование: обратная связь, П- и ПИД-регулятор».

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