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,153 @@
|
||||
/* 掉电不丢区的假实现 —— 一块 RAM,但**按 NOR flash 的规矩来**。
|
||||
|
||||
三条规矩,缺一条就会让 evlog 里最难查的那类错在宿主机上永远复现不出来:
|
||||
1. 写 = **按位与**(`*p &= v`),不是赋值。往没擦过的槽里再写一条记录,
|
||||
真板上得到的是两条记录的与(多半是一条校验不过的垃圾),而**不报错**;
|
||||
2. 擦 = 整个扇区变 0xFF,粒度就是扇区;
|
||||
3. **`sim_hal_reset()` 会把这块区擦干净(= 一块新板子),
|
||||
`sim_hal_reboot()` 不会(= 掉电重启)。** §10.7 的验收「掉电重启后能读回」
|
||||
全靠这两个的区别,别把 store 的清理塞进 reboot 里。
|
||||
|
||||
还多做一件真板会做而假实现容易漏的事:**掉电可以打断一次写**
|
||||
(`sim_store_tear_next_write()`),落进去的是前面若干字节,后面还是 0xFF。
|
||||
这正是 evlog 每条记录都要带校验字节的理由。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_store.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
static uint8_t s_mem[SIM_STORE_SECTORS * SIM_STORE_SECTOR_BYTES];
|
||||
static bool s_inited;
|
||||
static bool s_formatted; /* 这块「flash」出过厂没有(全 0xFF 才是出厂态) */
|
||||
static uint32_t s_writes;
|
||||
static uint32_t s_erases;
|
||||
static uint32_t s_bytes;
|
||||
static int s_fail_in; /* >0:第 n 次写整条失败;0 = 不注入 */
|
||||
static uint32_t s_tear_at; /* >0:下一次写只落这么多字节然后失败 */
|
||||
|
||||
void sim_store_reset_internal(void); /* 只给 sim_hal_reset() 用:连内容一起擦 */
|
||||
void sim_store_reboot_internal(void); /* 只给 sim_hal_reboot() 用:内容留着 */
|
||||
|
||||
static void wipe(void)
|
||||
{
|
||||
memset(s_mem, 0xFF, sizeof(s_mem));
|
||||
s_formatted = true;
|
||||
}
|
||||
|
||||
void sim_store_reset_internal(void)
|
||||
{
|
||||
wipe();
|
||||
s_inited = false;
|
||||
s_writes = 0;
|
||||
s_erases = 0;
|
||||
s_bytes = 0;
|
||||
s_fail_in = 0;
|
||||
s_tear_at = 0;
|
||||
}
|
||||
|
||||
void sim_store_reboot_internal(void)
|
||||
{
|
||||
/* 掉电重启:CPU 侧什么都不剩,flash 里的内容一个字节不变。 */
|
||||
if (!s_formatted) {
|
||||
wipe(); /* 从来没 reset 过的进程:至少给它一块出厂态的 flash */
|
||||
}
|
||||
s_inited = false;
|
||||
s_writes = 0;
|
||||
s_erases = 0;
|
||||
s_bytes = 0;
|
||||
s_fail_in = 0;
|
||||
s_tear_at = 0;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_init(void)
|
||||
{
|
||||
if (!s_formatted) {
|
||||
wipe();
|
||||
}
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void hal_store_geometry(hal_store_geom_t *g)
|
||||
{
|
||||
if (g == NULL) {
|
||||
return;
|
||||
}
|
||||
g->sector_bytes = s_inited ? SIM_STORE_SECTOR_BYTES : 0u;
|
||||
g->sectors = s_inited ? SIM_STORE_SECTORS : 0u;
|
||||
}
|
||||
|
||||
static bool range_ok(uint32_t off, uint32_t len)
|
||||
{
|
||||
if (len > sizeof(s_mem) || off > sizeof(s_mem) - len) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_read(uint32_t off, void *buf, uint32_t len)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (buf == NULL || !range_ok(off, len)) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
memcpy(buf, &s_mem[off], len);
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_write(uint32_t off, const void *buf, uint32_t len)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (buf == NULL || !range_ok(off, len) || (off % 4u) != 0u || (len % 4u) != 0u) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
|
||||
uint32_t n = len;
|
||||
bool fail = false;
|
||||
if (s_fail_in > 0 && --s_fail_in == 0) {
|
||||
return HAL_ERR_BUS; /* 一个字节都没落进去 */
|
||||
}
|
||||
if (s_tear_at > 0u) {
|
||||
n = (s_tear_at < len) ? s_tear_at : len;
|
||||
s_tear_at = 0u;
|
||||
fail = true; /* 掉电:前 n 个字节落进去了,然后就没有然后了 */
|
||||
}
|
||||
|
||||
const uint8_t *src = (const uint8_t *)buf;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
s_mem[off + i] &= src[i]; /* ⚠️ NOR:只能 1→0 */
|
||||
}
|
||||
s_writes++;
|
||||
s_bytes += n;
|
||||
return fail ? HAL_ERR_BUS : HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_erase(uint32_t sector)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (sector >= SIM_STORE_SECTORS) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
memset(&s_mem[sector * SIM_STORE_SECTOR_BYTES], 0xFF, SIM_STORE_SECTOR_BYTES);
|
||||
s_erases++;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ---- 假板子这一侧的控制面 ------------------------------------------------ */
|
||||
|
||||
void sim_store_fail_next_write(int nth) { s_fail_in = nth; }
|
||||
void sim_store_tear_next_write(uint32_t bytes) { s_tear_at = bytes; }
|
||||
uint32_t sim_store_writes(void) { return s_writes; }
|
||||
uint32_t sim_store_erases(void) { return s_erases; }
|
||||
uint32_t sim_store_bytes(void) { return s_bytes; }
|
||||
|
||||
uint8_t sim_store_byte(uint32_t off)
|
||||
{
|
||||
return (off < sizeof(s_mem)) ? s_mem[off] : 0xFFu;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户