产线校准数据注入与防篡改实战:200 台里第 37 台用着上一台的系数——WRP 锁得再死也没用,它锁不住”主语写错”

2026年10月29日 0 By admin

某条 10kV 专线的月线损从 2.8% 跳到 5.9%。这条线上只有一个计量点——三个月前刚换的那台表。

现场用 0.05 级标准表串接比对:B 相电流偏低 3.0%,A、C 两相正常。拆回来我第一件事是读校准区——记录完好,CRC 对,签名对,时间戳是出厂那天,一切合法。

第二件事才是看 sn 字段。上面写着 B2606-0036。这台表的机壳、条码、芯片 UID,全是 0037。

这批一共 200 台,它是第 37 台。工装把上一台的记录写进了这一台。而设备的验签,用的是记录里那个 sn——它拿上一台的身份证,验上一台的签名,当然通过。签名没有失效,它只是验错了人。

这件事我查了三天,最后发现修复点不在工装(提高读 SN 可靠性),在设备(验签时必须绑定自己的 UID)。工装侧要改握手协议(两块固件、两周),设备侧改三行验签(半天)——成本差 20 倍,效果差一个数量级。

一、先把 WRP 的边界划清楚

WRP(Write Protection)是 STM32 选项字节里的写保护位。工程上关于它有三个常见误解,必须先拆掉,后面的账才算得对。

误解 实际 后果
WRP 保护数据不被读出 只禁止编程/擦除,读出照旧 想保密必须叠加 RDP,两件事
WRP 按页保护 按保护组,一组 4KB(2×2KB 页) 160B 的记录要吃掉 4096B
锁了就是安全的 只保护你指定的组 擦除上界写错,等于没锁

第三条最要命,第九节会单独算。

还有一条不算误解、但几乎没人算过的:解锁 WRP 的代价不是”改一个位”,是整片擦除。在 RDP level 1 下清除写保护会触发 mass erase,固件、参数、密钥、校准一起没。返修这件事从”接上标准源重调一遍”变成”重建整台设备”,第十二节有账。

所以 WRP 的定位要摆正:它不是”数据保险箱”,是”一次性写入信封”。信封一旦封上,里面的东西就定了;要改,得把信封撕了——而撕信封的时候,里面的东西也一起没了。

二、种子页放哪:4KB 保护组的税

我先说结论:4KB 里只用了 160B,利用率 3.9%,空耗 3936B。这是 WRP 的税,必须交。

方案 记录长度 保护组 利用率 空耗
Ed25519 签名 160B 4096B 3.9% 3936B
HMAC-SHA256 截断 16B 128B 4096B 3.1% 3968B

512KB 全片 256 页(2KB/页),共 128 个保护组,种子页占 1 个——0.78% 的芯片面积换”出厂后不可改”。这个价我认为值,但它得从别的地方挪出 4KB 来。

补一句:不同容量档的 WRP 位数不一样,能不能精确保护到最后一组,得翻手册里那张对应表——这一步别猜,猜错了就是”以为锁了”。

《Flash 上跑 littlefs 落地实测》 那篇定的分区是 BL 16 + meta 2 + slotA 198 + slotB 198 + FS 96 + 预留 2 = 512KB,里面没有种子页的位置。当时校准参数是塞在参数区里的,靠 scope 白名单保护——那是软件保护,不是物理保护。

现在要挪:

区 旧 新 变化
BL 16KB 16KB 0
meta 2KB 2KB 0
slotA 198KB 198KB 0
slotB 198KB 194KB −4KB
seed 0 4KB +4KB
FS 96KB 96KB 0
预留 2KB 2KB 0
合计 512KB 512KB 0

从 slotB 切,App 176KB,槽余量从 22KB 降到 18KB。18KB 还够一次功能迭代,可以接受。

为什么不在 FS 里切?littlefs 的元数据块是成对的(一对 block 互为备份),切 4KB 等于拆掉一对里的一半,实际必须切 8KB(96→88KB,块数 24→22),代价翻倍。从 app 槽切是 4KB,从 FS 切是 8KB,选前者。

也不建议和 BL 共用一组:BL 那 16KB 正好是 4 个完整组,边界对齐,插一个种子页进去会把 BL 的升级粒度搞乱。

三、160 字节:记录该长什么样

记录格式我改了三版,最后定成这样:

偏移 字段 长度 内容
0 magic 4 CAL1,用于区分”从未写过”和”被擦过”
4 ver 2 记录格式版本,迁移用
6 flags 2 bit0=已闭环,bit1=已锁 WRP
8 sn 16 芯片 96-bit UID 派生,不是手写字符串
24 batch 2 批次号
26 n_coef 1 系数个数,=12
27 pad 1 对齐
28 ts 4 校准时刻,UTC 秒
32 coef 48 12×float32
80 crc32 4 覆盖 0..79
84 sig 64 Ed25519,覆盖 0..83
— 合计 148 对齐 32B → 160B

12 个系数是:三相电压增益 g_u[3]、三相电流增益 g_i[3]、三相相位补偿 φ[3](度)、三相角差补偿 k[3]。g_i 的正常分散范围 0.90~1.10,φ 在 ±0.5° 内。第 37 台需要 g_i[B]=1.000,拿到的是 1.031——差 3.1%,正好对上那条专线的线损缺口。

一条 160B 的记录,2KB 页能放 12 条(1920B)。但校准一辈子只写一次,这个密度毫无意义——记录长度在这里不是容量问题,是”能不能装进一个保护组”的问题。

四、串台:一行容错代码 = 18% 批次风险

工装那段代码长这样(很多家的工装都长这样):

sn = read_sn(retry=1, timeout=300ms)
if sn is None:
    sn = last_sn          # 容错:用上一次的
    log.warning("SN read failed, fallback")

那台 0037 刚上电 1.2 秒,MCU 还在初始化外设,485 不应答。工装重试间隔 50ms,两次都撞在启动窗口里,超时。然后它很体贴地用了上一次的 0036。

日志里确实有一条 WARNING。产线每天 20 多条 WARNING,混着”夹具未夹紧””条码枪重复扫描””气压低”,没人会为一条 SN 告警停线。

这个 p(单台读 SN 失败且回落到缓存)取 1e-3 不算高估:

p n=50 n=200 n=1000
1e-4 0.50% 1.98% 9.52%
3e-4 1.49% 5.82% 25.92%
1e-3 4.88% 18.14% 63.23%
3e-3 13.95% 45.17% 95.04%
1e-2 39.50% 86.60% 100.00%

p=1e-3、n=200 时,18.14% 的批次至少有一台串台,平均 0.2 台/批。每 5.5 批撞一次。

注意这张表的形状:风险与 p 成正比,p 降一个数量级,批次风险也降一个数量级——但永远不是零。你把读 SN 的可靠性做到 1e-6,n=1000 时仍有 0.1% 的批次中招。靠”提高可靠性”这条路,永远走不到 0。

走到 0 的方法是取消缓存路径:读不到就停线,人工补扫。p_escape 恒为 0,与 p 无关。但停线有成本,产线主管不会同意——所以真正的解法在设备端,第五节。

五、”写成功”不等于”写对”:四层验证各抓什么

层 手段 检出率 抓什么 漏什么
L1 编程后回读比对 99.9% 位没写进去、写坏 值本身是错的
L2 CRC32 99.999% 随机位翻转 系统性错值
L3 Ed25519 验签 100% 任何未授权修改 合法的错记录
L4 标准源闭环 100% 配错台、配错相 缓慢漂移

关键在 L3 和 L4 之间那道缝:L1~L3 全部通过,仍然可能是”上一台的正确记录”。因为它是合法签名的、合法的、CRC 对的记录,只是主语错了。

所以 L3 的验签必须这么写:

msg = my_uid_sn() || rec.ver || rec.coef || rec.ts   // 用我自己的 UID
ok  = ed25519_verify(pub, rec.sig, msg)
ok  = ok && (rec.sn == my_uid_sn())                  // 再比一次,防实现写错

而不是:

msg = rec.sn || rec.ver || rec.coef || rec.ts        // ← 用记录自带的

差别只有一个词:主语。前者是”这条记录是不是给我签的”,后者是”这条记录有没有被改过”。后者叫完整性,前者叫授权——两个不同的安全属性,工程上极容易被合并成一个。

my_uid_sn() 从 0x1FFFF7E8 的 96-bit 唯一 ID 派生,取 8 字节转 hex,再补批内序号。不手工录入、不写死、不从外部读。手写字符串的 SN 是这一切的漏洞源头。

改完之后的行为变化:0037 收到 0036 的记录 → 用自己的 UID 算 msg → 验签失败 → 设备报 CAL_INVALID,拒绝进入计量模式 → 产线工位红灯。

故障从”出厂三个月后客户投诉”提前到”产线那台红灯”。 这就是全部的收益。

六、闭环验证:唯一能证明”这一台的系数是这一台的”

L1~L3 都是”数据层面”的验证,只有 L4 是”物理层面”的:接标准源,加已知功率,看设备读回多少。

工位是这么搭的:标准源输出 3×220V、3×5A、φ=0(纯有功),设备读 100 个周波(2 秒),算 P_read,比 P_ref。

  • 标准源准确度 0.05 级
  • 设备重复性实测 0.05%(100 周波平均后)
  • 合成不确定度 √(0.05² + 0.05²) = 0.071%
  • 判据 |e| ≤ 0.2%,等于 2.8 倍合成不确定度
  • 给现场老化留 0.3%(0.5S 级限值 0.5% − 产线 0.2%)

判据 0.2% 不是拍脑袋:卡到 0.1% 的话,2.8σ 变 1.4σ,误判率从 0.5% 涨到 16%,产线一天废十几台好表;卡到 0.3% 又吃掉了现场余量。0.2% 是这两头之间唯一舒服的位置。

第 37 台如果做了 L4:0036 的 g_i[B]=1.031 用在这台上,闭环误差 3.1% ≫ 0.2%,当场判废。不需要任何”更聪明的校验”,只要把物理闭环接上。

12 个系数够不够:线性模型的边界

上面这套是线性模型——一个增益、一个相位,覆盖全量程。实际增益在 5% 和 100% 额定点上会差 0.1%~0.3%,不多,但对 0.5S 级是实打实的余量消耗。

三点校准(5%/20%/100%)每相多两组系数,记录从 12 系数涨到 24 系数 = 196B,对齐后 224B。仍是一页,保护组的税一点都不用多交——4KB 的保护组早就买下了,160B 还是 224B 都是 4KB。这是”按保护组付费”的唯一好处:记录可以随便长,只要不超过 4096B。

我暂时没上三点,因为现场这类 3% 的错跟线性度无关。但接口留着:n_coef=12 走线性,n_coef=24 走分段插值,靠 ver 区分——那两个字节就是为这个留的。

七、平衡负载会把证据擦掉

这里有个检定现场的反直觉细节,值得单独说。

单相增益错 3%,在平衡负载下总误差只体现 1/3:

工况 单相增益误差 总误差 超 0.5S 限值 年电量偏差 年金额
单相满载(不平衡) 3% 3.0% 6.0× 7,920 kWh 4,752 元
平衡负载,一相错 3% 3% 1.0% 2.0× 2,640 kWh 1,584 元
平衡负载,一相错 1% 1% 0.33% 0.7× 880 kWh 528 元

(按 66kW、4,000h/年、0.6 元/kWh 估)

检定台默认跑平衡负载。1.0% 的误差,超 0.5S 限值 2 倍——检定员的判断是”这台需要修调”,不是”这台装错了系数”。于是他重写一遍系数,复测合格,出具报告。

串台的证据,被”修调”这个动作覆盖了。 记录里留下的是那次返工重烧之后的合法重校准,签名齐全,时间戳合理。

这次能查到,靠的是运气:它是 10kV 专线上唯一的计量点,3% 全暴露在线损上。同样一台表如果装在 200 台分户表里,3% 摊到总电量上只剩 0.015%——比线损的正常波动还小两个数量级,永远查不出来。

所以顺序必须是:产线闭环(L4)→ 锁 WRP → 出厂检定。检定不能当兜底,它会把证据洗掉。

八、什么时候锁:锁前锁后的顺序

写 160B
  → 回读逐字节比对           (L1)
  → CRC32                    (L2)
  → 验签(用自己的 UID)       (L3)
  → 标准源闭环                (L4)  ← 不合格在这里停,别往下走
  → 锁 WRP(写选项字节)
  → 复位
  → 读回选项字节,逐位比对
  → 再验签 + 再闭环一次

三个容易漏的点:

一、锁之前必须闭环完。 锁完再发现系数错,就得解锁,而解锁 = 整片擦除(第十二节)。顺序反了,返修成本从 5 分钟变 23.4 分钟。

二、改选项字节是整页擦除再写。 选项字节那一页里同时住着 RDP、WRP、用户选项字节。你只想改 WRP,但硬件是”擦掉整页,再写回全部”。所以必须:先读出全部选项字节 → 只改 WRP 位 → 整体写回 → 读回逐位比对。只写一个字节的实现,会把 RDP 顺手改成 0(无保护)。

三、锁完必须再验一次,而且要比对选项字节。 比对的是”读回的 WRP 位图 == 期望位图 && RDP == 期望值”。不相等就判废,不要”再锁一次”——第二次写可能把第一次的错掩盖成另一个错,而你失去了判断依据。

九、锁了之后怎么升级:擦除白名单

WRP 锁的是种子页,升级擦的是 app 槽,两者不重叠——理论上是这样。

实际上《STM32 Bootloader 与 IAP 远程升级实战》 里那个擦除循环,上界写的是 FLASH_END:

for (addr = APP_ADDR; addr < FLASH_END; addr += PAGE) flash_erase(addr);
  • APP_ADDR = 0x08004000
  • SEED_ADDR = 0x0807F000
  • FLASH_END = 0x08080000

正确上界是 SEED_ADDR,擦 246 页。写成 FLASH_END 就多擦 2 页——正好是种子页。

而且 WRP 这时候也救不了:WRP 锁的是”禁止编程/擦除”,擦除操作会直接失败返回错误码。如果 bootloader 不检查返回值(绝大多数不检查),它会以为擦成功了,继续往下走,最后跳进 app。

症状:升级后校准区全 0xFF → app 用默认系数(g=1.0、φ=0)→ 误差 0.5%~2% → 不报错、不告警、不重启,就是慢慢算不准。

怎么区分”被擦过”和”从未写过”:

现象 被擦过 从未写过
magic 0xFFFFFFFF 0xFFFFFFFF
ts 0xFFFFFFFF 0xFFFFFFFF
同批其他设备 有正常记录 全都没有
工单记录 有校准时间 无

单看这台设备分不出来,得靠批内比对 + 工单。这也是我坚持 magic 用 CAL1 而不是 0xFFFFFFFF 的原因——出厂就该有值。

硬一点的做法:擦除函数只接受白名单区间,越界直接 assert。编译期用 static_assert(SEED_ADDR % PAGE == 0),运行时用 if (addr >= SEED_ADDR) return ERR_RANGE。别指望下一个人记得改常量。

十、HMAC 还是签名:密钥放在谁手里

137 篇 结尾留的坑是”HMAC token 的密钥怎么在产线和现场之间分配”。我算完账,结论是这个分配问题不存在,因为不该用 HMAC。

维度 HMAC-SHA256 Ed25519
验证成本 62µs 38.9ms(625×)
记录长度 128B 160B(都是 1 页)
密钥持有终端 12 台工装 1 台签发机
5 年内至少一次泄露 70.2% 2.5%(28.4×)

(验证成本按 M3@72MHz 估:HMAC 128B×35 cycles/B;Ed25519 验签 2.8M cycles。泄露面按 12 台工装、单台年泄露 2%、服务器年 0.5% 估,都是假设值,换参数改三个常量重跑。)

HMAC 的致命问题不是慢,是它要求验证方持有密钥。设备要验 HMAC,就得把密钥烧进固件;工装要签发,也得持有同一个密钥。12 台工装 × 5 年,70% 概率至少泄露一台。密钥一旦泄露,伪造能力就是全局的。

签名把这件事翻过来了:工装不持有任何长期密钥。它只是个搬运工——按 SN 向签发服务器要 160B 记录,拿到什么就写什么,写完不知道里面是什么。

那工装被逆向、被偷、被员工拷走,能拿到什么?公钥 + 一批已经签好的 160B 记录。伪造不出新记录。

断网怎么办?按工单预签发 200 条,工装本地缓存。这里就回到第四节那个”缓存”——但这次的缓存是安全的:每条记录都绑了设备 UID,错配必然验签失败。同一个”缓存”动作,在 HMAC 方案里是系统性缺陷,在签名方案里是无害优化。

这就是”绑定主体”的复利:它不只修了一个 bug,它让一整类优化变得安全。

十一、私钥不可撤销:泄露半径与两个公钥槽

上一节把签名说得太好,得补一句反面:签名私钥泄露的后果比 HMAC 严重。

  • HMAC 密钥泄露:12 台工装重刷,4 小时,完事。
  • 签名私钥泄露:在役设备全部要换公钥——而公钥在固件里,换公钥 = 一次全量固件升级。

所以私钥的管理标准要高一个等级:

密钥 存放 轮换 泄露影响
K_master(签名私钥) 离线 HSM / 不联网 永不 全部设备可被签发假校准
Key_pub 设备固件(只读) 随固件 仅验证,无风险

工程上留两条缝:

一、设备保留 2 个公钥槽 + key_id。 记录里带 key_id,设备按 id 选公钥验证。第二槽的用途是预置下一把公钥:先随一次常规固件升级把新公钥塞进去,等现场铺开了再启用新私钥——而不是换钥那天才发现所有在役设备都验不过。

二、威胁模型要写清楚,别自己吓自己。 假校准的收益是什么?不是”控制设备”,是让电表少计 3%。这是个商业欺诈模型,攻击者是有动机改系数的人(用电大户、想省检定费的集成商),不是远程黑客。他拿不到你的私钥,他能做的是:现场拆机改 flash(WRP 挡住)、伪造一条记录(签名挡住)、或者买通产线(这条挡不住,任何技术都挡不住)。

把威胁模型写成”防国家级攻击者”,会导致过度设计(白盒密码、反调试壳),而《固件防逆向与混淆实战》 已经算过:Chow 白盒 704KB 是 flash 的 138%,裸机上根本装不下。

校准这条线上,WRP + 签名 + UID 绑定,已经覆盖了全部有经济动机的攻击面。

复检到期:有效期不能在设备里判

0.5S 级工业表一般 5~6 年一检(按 6 年估,具体看当地规程)。记录里只有 ts,没有”有效期”字段——到期时间 = ts + 6 年,算在上位机,不算在设备。

原因很直接:设备判到期就得读 RTC,而 RTC 会被改、会掉电、会在换电池时归零。让设备用自己的时钟判断自己该不该被检定,等于让被告当法官。

设备的职责只有一条:把 ts 如实报上去,且这个 ts 不可改(改就要重签,重签要私钥)。判定、告警、停结算,全在上位机。

到期后的行为也要定死:不锁表(不能停用户的电),置”需复检”标志上报 EMS,同时标记该表数据不用于结算。这三件事分开做,别合成一个”过期即停”。

十二、返修悖论:解锁 = 整片擦除 = RDP 归零

WRP 最贵的账不在产线,在返修。

步骤 耗时 说明
解锁 → 等效整片擦除 7.7s RDP 归零,固件与密钥一起没
重烧固件(双槽) 15s 130 篇 的双槽布局
重新注入密钥 10min 130 篇 的产线注入工装
重校准(标准源闭环) 5min 同产线工位
拆装与复测 8min 开壳、灌胶、老化复测
合计 23.4min 不锁 WRP 只要 5min,4.7×

擦除本身只占 7.7 秒(1%)。贵的是擦完之后要重建的一切——这是所有”安全换便利”决策里最容易被低估的一项。

而这里有个真正的悖论:

解锁 WRP 触发 mass erase,RDP 一起归零。所以返修过的设备,比出厂设备更容易被读出固件。

出厂时 RDP=1、WRP=锁;返修后如果流程漏了”擦完立即恢复 RDP+WRP”,这台设备就是 RDP=0 的裸片。而这个”立即恢复”的动作,通常发生在产线之外的维修点——安全链上最薄的一环,恰好是最没纪律的一环。

三种处理方式,我选第三种:

  1. 现场解锁返修 —— 不选,安全链断在最不可控的地方。
  2. 维修点解锁 —— 还是不选,除非维修点有产线级工装和密钥。
  3. 板级替换:维修点只换板,坏板回厂统一解锁、重烧、重校准。

第三种把”解锁”这个高危动作收敛回产线,代价是备件板库存。一块 4 层板成本 30 元,一次安全事故的成本不是 30 元能比的。

软件封印不能自证

铅封拆了能看出来,WRP 解了看不出来。

设备端的现实:解锁 → 改系数 → 重锁,读回来的选项字节和记录跟原厂一模一样。WRP 位还是 1、ts 还是出厂时间(如果没人动它)、签名还是原厂那份(如果系数没变)——没有任何一个字节能证明”这中间被打开过”。

这是 WRP 方案的根本局限,绕不过去。能补的只有三条:

手段 能证明什么 代价
一次性计数页(每次解锁 +1) 解锁次数 解锁触发整片擦除,计数页也没了
上位机台账(SN → 工单 → 复检时间) 谁、何时、做了什么 依赖流程纪律,不进设备
铅封号绑定进记录 物理封印未动 需人工录入,多一道错源

第一条看着最像技术解,实际是死路——整片擦除会把计数器一起擦掉,计数器活不过它要记录的那个事件。用被保护对象记录对它的攻击,这个套娃没有出口。

第二条才是真解,但它不在设备里,在流程里。工程上必须接受一件事:WRP 保证”出厂后没被改”,保证不了”从来没被改”。前者是技术问题,后者是管理问题。

这也是我坚持 ts 不可改的原因:改 ts 就要重签,重签要私钥,而新 ts 会跟工单对不上。用签名把台账逼回流程——技术做不到的事,让流程的痕迹留在技术上。

十三、产线节拍:瓶颈从来不在 Flash

单台最坏路径实测:

步骤 耗时 占比
读 SN(含 1 次重试) 600.0ms 10.2%
擦种子页(最坏 40ms) 40.0ms 0.7%
编程 160B(80 半字) 5.6ms 0.1%
回读比对 + CRC 2.0ms 0.0%
锁 WRP + 选项字节 + 复位 160.1ms 2.7%
锁后自检(验签一次) 38.9ms 0.7%
闭环:标准源稳定 3000.0ms 51.2%
闭环:100 周波 @50Hz 2000.0ms 34.1%
闭环:计算与写工单 10.0ms 0.2%
合计 5.86s 100%

200 台 19.5 分钟。单台 5.86s,占 30 秒节拍的 19.5%——放得进产线。

看占比:闭环验证 86%,Flash 操作 1%。为了省 40ms 的擦除去换更快的编程算法,是把 1% 的项优化 30%——0.3% 的收益,风险和调试成本远大于此。

另一个反直觉的点:验签 38.9ms 只付一次。WRP 保证了种子页之后不会被改,所以验签结果缓存在 RAM 里,运行期不再验。如果没锁 WRP,你就得每次启动都验(启动预算从 180.4ms 涨到 219.3ms,130 篇 的账),或者接受”运行中可能被改”的风险。

WRP 的价值不只是防篡改,它还把”每次验签”变成”一次验签” ——38.9ms 从启动路径上消失了。这是很少被提到的协同效应。

十四、triage:现场八个症状

症状 判据 动作
验签失败 签名对不上 拒绝使用,进服务模式;不要静默回落默认值
缺记录(全 0xFF) magic 与 ts 全 FF 查 bootloader 擦除上界,不是现场故障
平衡负载误差 1% 超 0.5S 两倍 先查台号绑定,可能是上一台的系数
单相误差 3%、其余正常 增益单相偏 同上 + 查相序映射
所有设备 CRC 相同 记录逐字节一致 工装用了固定值,批次召回
ts 晚于出厂日期 时间戳倒挂 WRP 未生效或被解锁过
校准在但误差随时间漂 闭环复测超差 器件老化,走返修流程(板级替换)
WRP 位=0 但记录完好 选项字节被清 解锁过一次,按返修设备处理

第一行那条”不要静默回落默认值”,值得展开一句:默认系数(g=1.0、φ=0)能让设备继续跑,误差 0.5%~2%,用户看不出来。看起来很”友好”,实际上是把一次可检测的故障变成了一次不可检测的计量偏差。校准失效必须是硬故障,不能是降级。

十五、清单

  • ☐ 验签的 msg 用设备自己的 UID 派生 sn,不用记录里的 sn;验完再比一次字段
  • ☐ sn 从 96-bit UID 派生,不手工录入、不写死常量
  • ☐ 校准记录独占一个 4KB WRP 保护组;从 app 槽切 4KB,不从 littlefs 切 8KB
  • ☐ 顺序:写 → 回读 → CRC → 验签 → 闭环 → 锁 → 复位 → 读回选项字节逐位比对 → 再验
  • ☐ 改选项字节必须”读全部 → 改一位 → 写全部”,不能单字节写
  • ☐ 擦除上界是常量 SEED_ADDR,不是 FLASH_END;擦除函数加越界 assert
  • ☐ 工装不持有任何长期密钥,只搬运服务器签好的 160B 记录
  • ☐ 私钥离线/HSM;设备留 2 个公钥槽 + key_id
  • ☐ 校准缺失进服务模式,不静默回落默认值
  • ☐ 维修点只换板不解锁,坏板回厂统一处理

这篇的所有数字都在 drafts/_tools/cal_injection_wrp_calc.py 里(rec/wrp/layout/cross/err/prog/line/ver/hmac/key/repair/erase/triage/table 十四种模式,selftest 34 项全过)。换平台、换页大小、换批量,改开头几个常量重跑就有新账。

(文中平台参数按 STM32F103 高容量 2KB 页、72MHz 推演;Ed25519 验签 2.8M 周期、HMAC 35 cycles/B、工装与服务器泄露率均为工程假设,换器件请以实测为准;动 WRP 之前先备份选项字节,解锁会连固件一起擦。)

下一篇预告:《相序、CT 极性与缺相:接线错误的自诊断》——校准系数对、签名对、闭环也过,但现场 CT 装反了,负荷为 0 时读数是负的;这类错误不会让任何一级校验失败,只会让电量悄悄变少或者变多。校准这条线往下走一步,就到接线了。