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]>
这个提交包含在:
@@ -0,0 +1,59 @@
|
||||
/* 掉电不丢的一块字节区(需求 §10.7 的「落盘」)—— HAL 的第六样东西。
|
||||
|
||||
计划 §4.1 写着「HAL 只抽象四样」,§10.3 的两把锁是第五样。这里的第六样同样
|
||||
要给个理由:§10.7 要求事件**在上位机不在线时也记得下来**,而「记得下来」在
|
||||
ESP32-S3 上就是往 NOR flash 的一个分区里写。事件日志本身(环形、批量、
|
||||
按序号增量拉取)是与平台无关的逻辑,宿主机上必须跑得到;能跑不到的只有
|
||||
「怎么擦一个扇区、怎么写一段字节」这三行。抽的就是这三行。
|
||||
|
||||
---- 抽象的是 NOR flash,不是文件 ----------------------------------------
|
||||
接口上带着 NOR 的两条物理事实,因为**上层绕不开它们**:
|
||||
1. **写只能把 1 变成 0。** 同一个位置写两遍不是覆盖,是按位与。
|
||||
所以 sim 的实现也是按位与 —— 真板上「以为覆盖成功了」的那种错,
|
||||
在一个 memcpy 式的假实现里一次也复现不出来。
|
||||
2. **擦除的粒度是扇区**(4 KB),擦完全是 0xFF。
|
||||
还有一条没写进接口但同样要记住的:擦一次就少一次寿命(十万量级)。
|
||||
§10.7 的量级是「每天几十条」,17 个扇区轮着用,一辈子擦不了几轮。
|
||||
|
||||
⚠️ **不做 wear leveling、不做坏块管理、不做文件系统。** 这块区只有一个用户
|
||||
(evlog),它自己就是个环形缓冲,天然轮流擦全部扇区。套一层 SPIFFS 反而
|
||||
多出一堆掉电一致性问题 —— 那正是这一节最不想要的东西。 */
|
||||
#ifndef HAL_STORE_H
|
||||
#define HAL_STORE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint32_t sector_bytes; /* 擦除粒度。ESP32-S3 的 NOR 是 4096 */
|
||||
uint32_t sectors; /* 这块区有几个扇区 */
|
||||
} hal_store_geom_t;
|
||||
|
||||
/* 幂等。ESP 侧是找到 `evlog` 分区;找不到返回 HAL_ERR_STATE
|
||||
(分区表里漏了这一项 —— 那是编译期就该发现的错,不是硬件故障)。 */
|
||||
hal_err_t hal_store_init(void);
|
||||
|
||||
/* 没 init 时两个字段都是 0。调用方拿它算容量,所以**不许猜**这两个数。 */
|
||||
void hal_store_geometry(hal_store_geom_t *g);
|
||||
|
||||
/* 读。off/len 以字节计,越界返回 HAL_ERR_ARG。 */
|
||||
hal_err_t hal_store_read(uint32_t off, void *buf, uint32_t len);
|
||||
|
||||
/* 写。⚠️ **按位与语义**(见上):往没擦过的地方写,得到的是两次内容的与,
|
||||
而且**不报错**。想覆盖就得先擦整个扇区。
|
||||
off 与 len 都要求 4 字节对齐(esp_flash 的要求,sim 里同样拦)。 */
|
||||
hal_err_t hal_store_write(uint32_t off, const void *buf, uint32_t len);
|
||||
|
||||
/* 擦一个扇区(整块变 0xFF)。sector 是索引,不是字节偏移。 */
|
||||
hal_err_t hal_store_erase(uint32_t sector);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_STORE_H */
|
||||
在新工单中引用
屏蔽一个用户