把文件系统搬到外部 SPI Flash 实测:挂载从 2.4ms 变 18.2ms,第一次 compaction 就把看门狗喂崩了——W25Q64 的三本账得拆开算

2026年10月27日 0 By admin

上一篇《Flash 上跑 littlefs 落地实测》文末我留了个尾巴:把文件系统搬到外部 SPI Flash 上,同一套账重算一遍。当时我给的判断很轻率——”W25Q64 是 4KB 扇区、45ms 擦除、10 万次擦写,看着都比内部 Flash 好”。

真做了才发现,”看着都好”这三个字里藏了三处反向。写确实快,擦的典型值打平、最坏值差 5 倍,读慢了 12 倍——而 littlefs 的每一次元数据提交都要读一遍块、改、再写回去,读慢会沿着这条链路放大成挂载慢和 compaction 慢。

结论先摆出来,五条:

  • 寿命这一栏基本退出讨论了。 96 条/天的常态负载,1MB 外部分区算下来是两万三千年量级。131 篇和 135 篇围着寿命转,搬完家之后真正咬人的换成了擦除时间和总线带宽。
  • 擦除的典型值不能用来做设计。 45ms 是典型,400ms 是上限,差 8.9 倍;3 次擦除叠加的 compaction 最坏 1.205s,内部 Flash 同样路径只有 124ms。这个 10 倍只出现在手册上限里,实验室一辈子复现不了。
  • prog_size 填页大小是个坑。 littlefs 把元数据提交对齐到 prog_size,28B 的提交在 256 档占满 256B,一个 4KB 块只装 15 次——比内部 2KB 块的 63 次还少。参数的擦除次数因此是内部的 1.71 倍。改成 64,回到 63 次,是内部的 0.66 倍。
  • 挂载的瓶颈从 CPU 换成了总线。 内部 2.4ms 是 tag 解析(58%)拖的;外部 18.2ms 是 66% 花在等字节搬进来,tag 只剩 23%。同一个结论不能跨介质复用。
  • 外部 NOR 比 NAND 好带的地方在掉电语义。 编程只能 1→0,半编程的页必然被 CRC 抓到,不会产生静默错误;代价是擦除中掉电一次丢 63 条(整块),内部只丢 31 条。

工具照旧全离线:drafts/_tools/spi_flash_fs_calc.py,15 个模式、43 项自检断言,文中每个数都能复算。


一、先把三本账分开:写、擦、读

内部 Flash 和外部 NOR 最容易被混为一谈的地方,是把它们当成”同一件事的两种容量”。它们其实是两种不同的物理:

维度 内部 Flash(F103) 外部 NOR(W25Q64JV) 差
读 总线直连,单周期取指,24.1MB/s SPI 单线 18MHz,2.05MB/s 慢 11 倍
写 64B 半字串行 1.68ms 一次 64B 编程 325us 快 5.2 倍
擦单元 2KB 页 4KB 扇区 大 2 倍
擦典型 20ms(9.77us/B) 45ms(10.99us/B) 每字节贵 12%
擦最坏 40ms(19.53us/B) 400ms(97.66us/B) 每字节贵 5 倍
擦写寿命 10k 次/页 100k 次/扇区 10 倍

这张表读下来只有一句结论:写变快,擦变慢,读变得很慢。 而 littlefs 的写路径是”读元数据块 → 在 RAM 里改 → 写回去”,三本账全在里面,不能只看”擦写次数”一项。

131 篇我写过”选型先算擦写账”,那句话对环形页成立,对文件系统只对了一半。文件系统的写路径里,读和写各占一半,擦在最坏路径上占九成。账要按路径拆,不能按操作平均。


二、几何:4KB 扇区改的不只是容量

先把器件参数钉死(W25Q64JV 手册口径):

项 值
容量 8MB = 2048 个 4KB 扇区 = 128 个 64KB 块
页 256B,编程粒度 1..256B
扇区擦除 45ms 典型 / 400ms 最大
64KB 块擦除 150ms 典型 / 2s 最大
整片擦除 20s 典型 / 100s 最大
页编程 0.7ms 典型 / 3ms 最大(256B)
擦写寿命 100k 次/扇区
数据保持 20 年 @85C
Erase Suspend 20us 典型 / 50us 最大

有一条规则手册写得很清楚、但很多人是靠调试学到的:页编程不能跨页边界。如果你从地址 0x...FF 开始连发 4 个字节,第 1 个字节落在本页末,剩下 3 个不会落到下一页,而是绕回本页开头,把前 3 个字节覆盖掉。不是报错,不是回 0xFF,是静默覆盖。

所以驱动的编程函数必须自己拆页,且拆完要等 BUSY:

static int w25q_prog(const void *src, uint32_t addr, uint32_t size)
{
    const uint8_t *p = src;
    while (size) {
        uint32_t room = W25Q_PAGE - (addr & (W25Q_PAGE - 1));
        uint32_t n = size < room ? size : room;

        if (wren_and_wait() != 0) return LFS_ERR_IO;      /* 06h,且必须等 WIP=0 */
        cs_low();
        spi_xfer(0x02); spi_xfer(addr >> 16); spi_xfer(addr >> 8); spi_xfer(addr);
        spi_write(p, n);
        spi_wait_bsy_clear();                              /* 先等 BSY,再抬 CS */
        cs_high();
        if (w25q_wait_ready(5) != 0) return LFS_ERR_IO;    /* 3ms 上限,给 5ms */

        addr += n; p += n; size -= n;
    }
    return 0;
}

两个细节都是踩过的:

  • spi_wait_bsy_clear() 必须在抬 CS 之前。DMA 发完最后一字节时,SPI 移位寄存器里还有一个字节没出去,这时候抬 CS 会把那个字节截断——表现是”偶尔写坏一个字节”,概率跟时钟相位有关,换块板子就复现不了。
  • WREN 必须在 WIP=0 之后发。擦除刚结束时立刻发 06h,器件正忙,会把 WREN 忽略掉,于是紧接着的 02h 也被忽略,函数一路返回成功,读回来是 0xFF。这个坑不报错,只在下次挂载时以 CRC 失败的形式出现。

顺便说一件搬家的好处:外部 Flash 是独立器件,不用从 App 槽里抢空间。135 篇上 littlefs 的第一笔成本是两个 App 槽各让 42KB(130 篇那套 512KB 布局里,参数区原本只有 14KB),这次一分不用让,代价换成一颗料、四根线和 18ms 挂载。

littlefs 两侧的块尺寸也定了:数据块 4096B 扣掉 16B 的 CTZ 头记 4080B,装 63 条 64B 记录,尾部剩 48B;内部 2032B 装 31 条,尾部也剩 48B。块内记录数正好翻倍,这是外部器件唯一”自然变好”的一项。


三、prog_size 是三档选择题,而且是本篇最值钱的一节

移植的时候我照抄了一份常见的 HAL 例程,prog_size 填 256(页大小),cache_size 也填 256,跑起来一切正常。

第三周才开始怀疑:参数区的擦除次数涨了。查下去发现根因在 littlefs 的一个内部约定——元数据提交按 prog_size 对齐(lfs_dir_commit 里那句 align = lfs_min(prog_size, block_size))。28B 的提交在 prog_size = 256 下要占满一整页 256B,于是:

prog_size 提交长度 提交/4KB 块 擦除次数/条记录 相对内部 2KB 页
16 32B 127 0.02375 0.49 倍
64 64B 63 0.03175 0.66 倍
256 256B 15 0.08254 1.71 倍

三档之间差 3.5 倍,全都来自那 12 个字节的对齐余量。填 256 的时候,参数区的擦除次数比留在内部 Flash 上还多 71%。 这就是”搬了个家反而更费”的真实版本,而且它不报错、不告警,只在你三年后算寿命时出现。

三档怎么选,判据不是”越大越快”:

  • 16:元数据最密(127 次/块),但 64B 的记录从偏移 240 开始写就会跨 256B 页边界,驱动必须实现拆页。拆页逻辑我上面已经写了,多 15 行。
  • 64:64 整除 256,任何 64B 对齐的写都不会跨页,拆页代码可以永远不进这条分支;元数据密度 63 次/块,和内部 Flash 持平。这是本篇选的档。
  • 256:只有”写整页、不写小记录”的场景(比如固件槽、图片资源)才合适。日志/参数这种小记录高频追加的负载,选它等于自罚三倍擦除。

配置长这样:

static struct lfs_config cfg = {
    .read  = w25q_read,   .prog  = w25q_prog,
    .erase = w25q_erase,  .sync  = w25q_sync,
    .read_size      = 16,
    .prog_size      = 64,      /* 不是 256!64 整除页大小,永不跨页 */
    .block_size     = 4096,    /* 必须等于扇区,填小了会一块多擦 */
    .block_count    = 256,     /* 1MB / 4KB */
    .cache_size     = 256,
    .lookahead_size = 32,      /* 256 块 / 8,正好 32B */
    .block_cycles   = 500,
};

block_size 填小于 4096 是最危险的一种错:littlefs 会把一个扇区当两块用,erase 回调对同一个扇区被调两次,第二次擦掉的是刚写完的数据。现象是”写进去读不出来”,而且只在块边界上偶发。

cache_size 小于 prog_size 时 lfs_init 直接返回 -22(135 篇踩过一次,cache 300 配 4KB 块也报 -22,因为 cache_size 必须是 read_size 和 prog_size 的整数倍)。RAM 静态占用:cache 256 + prog cache 256 + lookahead 32 + 结构体 632 = 1176B。


四、写账:外部确实快,但别拿”每字节”骗自己

按字节算,外部 NOR 是 2.73us/B(256B 页)到 5.08us/B(64B 编程),内部 Flash 是 26.25us/B(半字串行 52.5us)。差 5 到 10 倍,这个方向没争议。

但记录是 64B 一条,实际付的是这个数:

写入字节 外部典型 外部最坏 内部典型 内部最坏 典型倍数
16 231us 656us 420us 560us 1.8 倍
32 262us 813us 840us 1.12ms 3.2 倍
64 325us 1.13ms 1.68ms 2.24ms 5.2 倍
128 450us 1.75ms 3.36ms 4.48ms 7.5 倍
256 700us 3.00ms 6.72ms 8.96ms 9.6 倍

最坏列要单独看。 64B 的典型快 5.2 倍,最坏只快 1.5 倍——因为外部有 0.5ms 的固定开销和 3ms 的页编程上限,内部是纯线性。越小越快的优势,在最坏情况下会被固定开销吃掉。

一次 lfs_file_sync 是”数据 64B + 元数据提交”两次编程:

配置 典型 最坏
外部 prog_size = 16 588us 1.94ms
外部 prog_size = 64 650us 2.25ms
外部 prog_size = 256 1.02ms 4.12ms
内部 2KB 页 / prog 16 2.52ms 3.36ms

prog 64 比内部快 3.9 倍。但单次 sync 的差别不是重点——三档之间只差 1.7 倍,真正的差别在第五节:多久要做一次 compaction。


五、擦账:45ms 是典型,400ms 是上限,而设计只能按上限

这是全篇最反直觉的一段。按每字节算:

  • 外部 45ms / 4096B = 10.99us/B;内部 20ms / 2048B = 9.77us/B。典型只贵 12%。
  • 外部 400ms / 4096B = 97.66us/B;内部 40ms / 2048B = 19.53us/B。最坏贵 5 倍。

按每条记录摊销(含元数据块那一份):

配置 擦除次数/条 典型 最坏
外部 prog 64 0.03175 1.429ms 12.70ms
外部 prog 256 0.08254 3.714ms 33.02ms
内部 2KB 页 0.04813 0.963ms 1.925ms

最好的一档也是内部的 1.49 倍。 算术很简单:擦除次数/条降到内部的 0.66 倍(4KB 块装的记录多了一倍),单次擦除贵 2.25 倍(45ms 对 20ms),0.66 乘 2.25 就是 1.49。容量的增长追不上擦除时间的增长,这就是 4KB 扇区的固有成本,跟文件系统无关。

然后是真正的问题——compaction 不是一次擦除。littlefs 要擦新块、可能擦旧块、目录分裂还要再擦一块,135 篇现场抓到的 D 工况就是三擦叠加:

路径 典型 最坏
外部 prog 64 136.3ms 1.205s
外部 prog 256 137.8ms 1.212s
内部 2KB 页 63.2ms 124.2ms

典型值 136ms 是内部典型 63ms 的 2.2 倍,最坏值 1.205s 是内部 124ms 的 9.7 倍。

135 篇我记的现场实测是 118ms,现在能解释清楚:它落在内部典型 63ms 和最坏 124ms 之间,说明当时抓到的是一个擦除时间偏长的样本,而不是上限。一个 118ms 的样本让我以为”最坏大概就 120ms”,而 400ms 的上限从来没被触发过——因为它需要硅片的擦除时间落到分布尾部,实验室几百次擦除是撞不到的。

这就是第六节事故的伏笔。


六、事故复盘:IWDG 200ms,第一次 compaction 复位

现场

板子装到现场第七天,复位记录里有 11 条 IWDG 复位,全部集中在上午 9 点到 10 点——整点抄表最密集的时段。复位后系统正常起来,参数回退了一版(丢失最近 3 条未落盘记录),littlefs 挂载成功,没有文件系统损坏。

这个组合很关键:复位 + 文件系统完好 + 只丢缓存里的几条,说明不是介质坏了,是有人在擦除里卡了超过 200ms。

根因

代码是这样的:

watchdog_feed();
lfs_file_sync(&lfs, &f);   /* 里面可能是 3 次擦除 */
watchdog_feed();

w25q_erase() 里用的是阻塞轮询:发 20h(4KB 扇区擦除)+ 24 位地址,然后死等 WIP 清零。等待循环里没有喂狗。 典型 45ms 没事,三次叠加 136ms 也还在 200ms 内,但只要有一次擦除落到 400ms 附近,1.2s 就直接把狗喂崩。

而它一年只来一次——所以实验室三天没事、产线抽检没事、客户现场第七天开始每天一两次。

修法一:在轮询里喂狗(一行)

static int w25q_wait_ready(uint32_t timeout_ms)
{
    uint32_t t0 = tick_ms();
    for (;;) {
        if (!(w25q_rdsr1() & 0x01)) return 0;   /* WIP=0 */
        iwdg_feed();                            /* 关键:等待是"活着的等" */
        if (tick_ms() - t0 > timeout_ms) return LFS_ERR_IO;
    }
}

这是最小改动,也是必须做的一条——不管后面怎么重构,等待循环里不喂狗就是埋雷。但要注意:喂狗只解决”不复位”,不解决”系统停了 1.2 秒”。1kHz 的控制环在这 1.2 秒里丢 1200 个周期,PWM 直接停更。

修法二:擦除改成状态机(架构上唯一正确的做法)

typedef enum { FS_IDLE, FS_ERASING, FS_PROGRAMMING } fs_state_t;

/* 100us 定时器里调用,不阻塞 */
void fs_poll(void)
{
    switch (st) {
    case FS_ERASING:
        if (!(w25q_rdsr1() & 0x01)) { st = FS_IDLE; fs_done_cb(); }
        break;
    case FS_PROGRAMMING:
        if (!(w25q_rdsr1() & 0x01)) { st = FS_IDLE; fs_done_cb(); }
        break;
    default: break;
    }
}

代价是文件系统的所有写操作都变成异步,业务代码要接受”提交完成后回调”。收益是擦除期间 CPU 是空闲的,控制环照跑。

这两条不冲突:状态机是架构,喂狗是保险。先做喂狗止血,再做状态机根治。

轮询本身的代价

RDSR(05h)读一个字节,18MHz 下 977ns。按不同间隔算:

轮询间隔 45ms 擦除要问 总线占用
10us 4500 次 9.77%
50us 900 次 1.95%
100us 450 次 0.98%
500us 90 次 0.20%

50us 是个好折中:擦除结束到发现最多晚 50us,总线占用 2%。但这里有个硬约束:擦除期间器件只认 05h 和 75h,别的命令一律不理。 所以这条 SPI 上不能挂需要实时采样的传感器——挂上去的话,擦除那 45ms 里所有采样都会失败,而且不报错(从机不应答,读回来是 0xFF 或 0x00)。

三条容易踩的细则

  • SPI NOR 没有 R/B# 引脚。 并行 NOR 有专用忙信号,SPI NOR 只有状态寄存器里的 WIP 位。所以”擦除完成中断”这种设计在硬件上不存在,只能定时轮询。想省 CPU 就把轮询放进 100us 定时器,别指望中断。
  • CS 与共用总线。 如果这条 SPI 上还挂着别的从机,各自独立 CS 时互不干扰(忙的只是那片 Flash),但共用 CS 的菊花链不行。参数区建议独占一个 SPI 外设,最差也要独占 CS。
  • 写保护位。 出厂或上次误操作可能把 SR1 的 BP0..BP2 置上,此时擦除不报错但无效(WIP 正常走完,内容不变)。初始化时读一次 SR1/SR2 打日志,这行日志救过我一次。

七、挂载 18.2ms 花在哪:瓶颈从 CPU 换成了总线

内部 Flash 上挂载是 2.4ms,外部变成 18.2ms,7.6 倍。拆开看:

项 内部(4 块 x 2KB) 外部(6 块 x 4KB)
字节搬运 340us(24.1MB/s) 12.00ms(2.05MB/s)
CRC32 680us 2.04ms
tag 解析 1.38ms 4.14ms
合计 2.40ms 18.18ms

占比反过来了:内部是 tag 解析 58%、搬运 14%;外部是搬运 66%、tag 解析 23%、CRC 11%。

135 篇那句”瓶颈是 tag 解析不是 CRC”在外部器件上不成立了。同一个 2.4ms,内部瓶颈在 CPU,外部瓶颈在字节根本没搬进来。结论不能跨介质复用,这是这一节唯一想说的事。

三个应对:

  1. CRC 与传输重叠。 现在这两项是串行算的(18.18ms 里 2.04ms 是纯 CRC)。改成边收边算,能压到 16.1ms。省 11%,不难,值得做。
  2. 别在启动路径上挂载。 18ms 对 72MHz 的 M3 是 130 万个周期,放在 main() 里就是开机多等 18ms;放在低优先级任务里,控制环完全无感。
  3. 挂一次常驻,别挂卸载。 有些代码为了省 RAM 每次读写都 mount/unmount,内部 Flash 时代代价是 2.4ms 看不出来,外部就是每次 18ms。RAM 1176B 换 18ms,这笔账不用算。

如果确实需要更快,唯一的办法是提高总线带宽,见下一节。


八、QSPI 内存映射能不能跑文件系统

先说结论:能跑代码和只读资源,不能跑文件系统的写路径。

带宽前提

我们这块板子是 F103,SPI1 挂在 APB2 72MHz,4 分频 = 18MHz 硬上限,单线有效 2.05MB/s。F103 没有 QSPI 外设,四线只能软件模拟(GPIO 翻转根本到不了 100MHz),所以在这颗 MCU 上”四线”不是选项。

要四线就得换 MCU。H743 的 QUADSPI 104MHz 四线,有效 45.76MB/s,是现在这条路的 22 倍,4KB 读从 2.00ms 降到 0.090ms。但为了 46MB/s 换整颗主控,摊到项目上是几十万的重新认证成本,不划算。 这是最后没换的真实原因,不是技术做不到。

XIP 的三条限制

假设换了 H7,用内存映射(XIP)模式,有三条限制绕不过:

  1. 擦写期间器件忙,取指停摆。 页编程 0.7/3ms、扇区擦除 45/400ms 之内,器件对除 05h 和 75h 以外的命令一律不理。代码在外部 Flash 上跑,这 45ms 就是整机停摆——比内部 Flash 还糟,因为内部至少擦除和取指是同一个阵列,延迟确定;外部是”总线还在、从机不说话”。
  2. Erase Suspend(75h)能救,但控制器不会替你发。 挂起响应 20us 典型 / 50us 最大,挂起后可以读别的扇区。但 QSPI 控制器进入 memory-mapped 模式后是硬件自动发读命令的,它不知道”现在该发 75h”。要用手动挂起,就退回命令模式,XIP 的意义没了。
  3. 代码必须驻留内部 Flash 或 RAM。 擦写例程放 RAM(.RamFunc 段),擦除那 45ms 里不许有任何外部取指。做得到,但要求你把整个写路径(littlefs 的 prog/erase/sync 回调 + SPI 驱动)都搬进 RAM,代码量 3~5KB。

所以我们的分工是:代码和常量放内部 Flash,参数/日志放外部 SPI Flash,外部只做数据不做取指。 这条分工在 F103 上是被 18MHz 逼出来的,在 H7 上也依然成立——因为第 1 条限制不会因为带宽变高而消失。


九、掉电:损失半径从 31 条变成 63 条

擦除窗口的占空比很低:

负载 compaction 次数/天 擦除占空比 100 次/年断电的命中率
96 条/天 1.5 3.1 ppm 0.03%,约 3200 年一次
86400 条/天 33 68.7 ppm 0.69%,约 145 年一次

统计上很安全。但工程上要按”一定命中”设计,因为落到单台设备上的后果是确定的:

  • 擦除中掉电 → 整扇区变随机值。littlefs 靠 CTZ 头和 CRC 判定无效块并跳过,代价是这一块里 63 条记录全没了。内部的损失半径是 31 条,外部是 63 条——擦除单元翻倍,一次掉电的损失也翻倍。
  • 编程中掉电 → NOR 的编程只能把位从 1 写成 0,半编程的页是”部分位已翻”。这看起来可怕,实际上是好消息:内容一定和 CRC 对不上,记录被判无效,不会出现”位翻转但 CRC 侥幸通过”的静默错误。
  • 对比 NAND:NAND 会位翻转、需要 ECC 兜底、坏块要管理、写前必须按页对齐。NOR 的这些”缺点”(擦除慢、单元大)换来的是语义简单——这是选它做参数区的核心理由,不是容量也不是价格。

出厂测试沿用 135 篇那套继电器切 3.3V,但要加一条:每次断电后必须等器件 BUSY 结束(读一次 RDSR 直到 WIP=0)再上电重挂,否则你测的是”带电插拔”而不是”掉电恢复”,前者会把 0xFF 读成有效数据,测出来的通过率是假的。


十、验收清单

  • ☐ block_size 严格等于扇区 4096,且分区起止都在扇区边界
  • ☐ prog_size 不取页大小,取整除页大小的记录粒度(本篇 64)
  • ☐ cache_size >= prog_size,且是 read_size 的整数倍
  • ☐ 编程函数自己拆页,跨页写不会绕回页首覆盖
  • ☐ 抬 CS 之前等 BSY 清零,DMA 路径单独验一遍
  • ☐ 每次 WREN 前确认 WIP=0,WREN 后确认 WEL=1
  • ☐ 初始化时读 SR1/SR2 打日志,确认 BP 位与 QE 位符合预期
  • ☐ 所有等待循环里喂狗,不只是擦写前后各一次
  • ☐ 擦除与编程走状态机,不在控制任务里阻塞
  • ☐ 参数区独占 SPI 或至少独占 CS,擦除期间不做别的采样
  • ☐ compaction 次数打点上报(prog_size 选错时这个数会先涨)
  • ☐ 200 次随机时刻断电,每次等 BUSY 结束后重挂,挂载全部成功

最后两条是这套配置里唯一能提前发现问题的观测点。擦除次数不会告警,compaction 次数会。 一个每天 6.4 次 compaction 的参数区(prog_size = 256 的典型值)和一个每天 1.5 次的(prog_size = 64),在寿命表上差 4 倍,在故障率上差 4 倍,但在线上看起来都”正常工作”。


最后收三句话。第一,外部 Flash 不是内部 Flash 的放大版,它是挂在一条共享总线上的另一个器件。 写快 5 倍、擦最坏慢 5 倍、读慢 11 倍,三个方向不一致,任何”整体更快/更慢”的判断都是错的。

第二,prog_size 是这套系统里性价比最高的一行配置。 从 256 改到 64,元数据密度回到 63 次/块,擦除次数降到 0.66 倍,代价是零行代码。这一行值 3.5 倍寿命,比任何”优化擦写策略”的技巧都值。

第三,400ms 那个上限必须进架构。 它一年可能只来一次,但它来的时候是 1.2 秒。用典型值做设计的人,会在现场第七天拿到一份 IWDG 复位记录,而那份记录里没有任何一行指向 Flash。

工具:drafts/_tools/spi_flash_fs_calc.py(15 个模式、43 项自检断言,纯标准库、全离线)。口径:W25Q64JV 手册的 0.7/3ms 页编程、45/400ms 扇区擦除、100k 次擦写、20us 挂起;内部 Flash 沿用 135 篇的 F103 口径(20/40ms 擦 2KB、52.5/70us 每半字);总线按 SPI1 18MHz、效率 0.91;挂载按 6 个 4KB 块读、CRC 11.76MB/s、tag 解析 0.1725ms/KB(均由 135 篇实测值标定)。编程时间按”固定开销 + 按字节线性”的串行模型,实测会因 SPI 硬件流水略快;与你的器件手册冲突时以手册为准。

文中设备型号与参数为写作口径下的重构,不针对任何具体厂商产品;文内数字均可用配套工具复算,与你的器件数据手册冲突时以数据手册为准。

下一篇预告《参数区双备份与出厂恢复流程》:本篇第五节只说了”擦除单元越大,损失半径越大”,但没回答一个更实际的问题——参数区被写坏了,现场怎么恢复? 双备份 A/B 的有效标志怎么置、什么时候才算”写过”、出厂默认值放哪、恢复流程要不要留后门、以及”用户改过的参数被恢复成默认值”这种事故怎么防。这是 135 篇留下的最后一个空白,也是这套参数系统的收尾。