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
через 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, это значит:
- запись поверх одного сектора — 100 тыс. сохранений;
- LittleFS с
block_cycles−1 — 6,2 млн; - с 500 — 211 млн, с 100 — 330 млн.
Меньше 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 Итоги
- LittleFS не пишет поверх: метаданные — журнал коммитов с CRC в паре блоков, данные — в свободные блоки, файл переключается последним коммитом.
- 1000 обрывов на каждый из трёх размеров файла: мусора 0 у LittleFS, 998–1000 у записи поверх со стиранием.
- Файл до 256 байт встроен в метаданные и не занимает блоков; 257 байт — уже блок 4 КБ.
- Маленький файл стоит одно стирание на 31 сохранение. С
block_cycles100 два миллиона сохранений разошлись по всем 128 блокам, самый стёртый — 606 раз; с −1 пара 0–1 получила по 32 259. - Выравнивание — только по свободным блокам: при 14 свободных из 128 пара суперблока не переезжает и набирает 43 479 стираний.
- Драйвер — четыре функции;
progобязан резать по страницам 256 байт: 96 775 из 200 013 вызовов пересекали границу. - Код 15 830 байт (
-Oz, Cortex-M4); буферы и структуры в ОЗУ — 996 байт.
Полные исходники
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 «Регулирование: обратная связь, П- и ПИД-регулятор».