A11:事件日志(环形缓冲 + 落盘)

事件从 evlog_put*() 进一个 256 条的 RAM 环(临界区里两条指令,**不阻塞、
不碰 flash**),由最低优先级的 t_log 攒够 16 条或最老那条等满 1 s 时批量写进
新的 `evlog` 分区(17 扇区 × 4 KB = **4335 条**)。A10 的事件出口原样接上,
判据那一层一个字没动。

八条实现约定(详见需求文档 §10.7):

· 落盘不用 NVS 也不用 SPIFFS,是一块裸 data 分区 —— NVS 的页式 GC 会把
  「写一条事件」变成一次不可预测的搬移。HAL 因此多出第六样 hal_store;
· 17 个扇区不是 16:每扇区要拿一个 16 B 的槽当扇区头,4 KB 只装 255 条,
  16 扇区 = 4080,差 16 条不到 §10.7 要求的 4096;
· 序号 = flash 里的位置(epoch × 255 + 槽号),不是自增计数器 —— 重启接着数
  不用存计数器;代价是没落盘的事件还没有序号,A12 读之前要先 flush;
· 入队走关中断的临界区,**不是第三把锁**:入队方是最高优先级的 t_safety,
  停在一把 t_log 持着的锁上等于让最低优先级的任务决定巡检节拍;
· 丢事件可以,悄悄丢不行 —— 缓冲满了盖最旧的,但补一条 LOG_EV_LOST 记着丢了几条;
· 每条记录带 CRC-8(见下),写失败就放弃该扇区剩余部分,被放弃的那段在序号
  空间里是个洞,读回时给一条空记录占位(跳过的话上位机游标会卡住);
· 每次上电记一条 LOG_EV_BOOT:时间戳上电从 0 计,没有它两次上电的记录会穿插。

事件类型的数字是 flash 格式的一部分,只许往末尾加;safety_ev_t → log_ev_t
是显式 switch 不是加偏移。§10.7 里还没人产生的六类号已占住(A12/A15/A16 去填)。

27 个用例;23 条注错逐个跑过,21 条变红。第一版校验和用异或,漏掉的正是最常见
的那条:payload 全 0 的记录被掉电切在第 8 个字节上时,后 8 个字节留在 0xFF,
而 8 个 0xFF 的异或恰好是 0 —— 半条的校验和与整条一模一样,扫描把半条当成
一条真事件。换成 CRC-8 之后拦住。另外两个只在这里才踩得到的坑:「掉电重启」
不能用 sim_hal_reset()(那是换新板子,flash 一起擦,测试永远绿),为此加了
sim_hal_reboot();「读到洞就跳过」这个错被测试里 static 数组的零初始化掩盖过
一次,缓冲要先填成非零。

Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-07 09:20:02 +08:00
共同撰写人 Claude Opus 5
父节点 273c53347e
当前提交 18d3d69856
共修改 21 个文件,包含 2025 行新增和 103 行删除
+46
查看文件
@@ -664,6 +664,52 @@ ESP32.KEY_SER → U21 → U22 → U33 (先移入的字节落在链尾 U
必记事件:eFuse FAULT / PG 变化、SR-5 触发关路、SW-8 反灌断 VBUS、过温降载、
每次重启序列(软 / 硬、谁下的命令)、越界命令被拒、I²C 器件失联、风扇停转。
> **A11 已落地**(2026-09-07,`firmware/main/evlog.{h,c}` + 新增的
> `hal_store`(掉电不丢区)+ `partitions.csv` 的 `evlog` 分区)。落地时定下来的八条:
>
> 1. **落盘不用 NVS,也不用 SPIFFS,是一块裸的 data 分区。** NVS 是键值表
> (4096 条记录 = 4096 个键,页式 GC 会把「写一条事件」变成一次不可预测的搬移),
> SPIFFS 的掉电一致性自己就是一整章。evlog 本身已经是环形缓冲,它要的只是
> 「擦一个扇区 / 写一段字节」—— HAL 因此多出第六样 `hal_store`。
> 2. **容量是 17 个扇区 = 4335 条,不是 16 个。** 每个扇区要拿一个 16 B 的槽
> 当扇区头(epoch),于是 4 KB 只装得下 255 条,16 扇区 = 4080,
> **差 16 条不到本表要求的 4096**。8 MB flash 上多这 4 KB 没有代价。
> 3. **序号 = flash 里的位置**(`epoch × 255 + 槽号`),不是一个自增计数器。
> 重启之后接着数不用把计数器存起来,掉电也不会倒退。代价是**没落盘的事件
> 还没有序号** ⇒ `events` 命令(10.8)要先 `evlog_flush()` 再读。
> 反过来做(入队时发号)会在缓冲溢出丢事件时留下一段永远补不上的空号,
> 上位机那边的现象是「拉日志卡在某个序号上不动」。
> 4. **入队在临界区里做完,不是加第三把锁**(§10.3 的共享资源只有两个)。
> 入队方是 `t_safety`(最高优先级、断电之后调),停在一把 `t_log` 持着的锁上
> 就等于让最低优先级的任务决定巡检的节拍。关中断的临界区里**只改内存**:
> 时间戳与校验都在外面算完。
> 5. **丢事件可以,悄悄丢不行。** RAM 缓冲(256 条)满了盖最旧的 ——「最新的一条
> 永远进得去」——但被盖掉的会在下一次落盘时留下一条 `LOG_EV_LOST`,
> 值是丢了多少条。一份会安静少掉几行的日志比没有日志更糟:排查的人会拿它
> 当完整的时间线看。flash 写失败丢掉的那一批同样补这条。
> 6. **每条记录带一个 CRC-8,而且只能是 CRC-8。** NOR 的写会被掉电打断在任意
> 字节上,半条记录在字节层面跟真记录长得一模一样。第一版用异或和,
> 漏掉的正是最常见的那条:`bus_mv` 与 `current_ua` 都是 0 的记录被切在第 8 个
> 字节上时,后 8 个字节留在 0xFF,而 **8 个 0xFF 的异或恰好是 0** ——
> 半条的校验和与整条一模一样。
> 7. **写失败之后放弃这个扇区的剩余部分。** 接着写的话,重启扫描会停在那半条上,
> 把后面写进去的全部当成不存在。代价是最多浪费 254 个槽,换来的是
> 「扫描停在哪 = 日志到哪为止」这条不变量永远成立。被放弃的那段在序号空间里
> 是个洞,`evlog_read()` 给它一条空记录**占住位置** —— 跳过它的话上位机的
> 游标会卡在洞上。
> 8. **每次上电记一条 `LOG_EV_BOOT`。** 本表写着「上电从 0 计」,所以时间戳会
> 跳回去;没有这条记录的话,两次上电的事件在时间轴上互相穿插,
> 而日志里看不出中间重启过。
>
> 事件类型的**数字是 flash 格式的一部分**(改一个值就等于把跑了半年的历史记录
> 重新解释了一遍,而且读出来仍然是合法记录、没有一处报错),所以 `log_ev_t`
> 只许在末尾追加;`safety_ev_t` → `log_ev_t` 是一个显式的 switch,不是加偏移。
> 上表里还没人产生的那几类(eFuse FAULT / PG 变化 / 过温降载 / 重启序列 /
> 命令被拒 / I²C 失联 / 风扇停转)**号已经占住**,各归各的包去填。
>
> 27 条 ctest 用例;23 条「一定会被写出来的错」逐个注进去跑过,**21 条变红**。
> 剩下两条抓不到,理由见 `firmware/HANDOFF.md`。
### 10.8 上位机接口 ✅ 已定稿(2026-09-06 用户拍板:方案 C)
**这一节回答的问题**:SW-1 的触发条件写的是「`adb` 探活连续失败 N 次」,