Files
STM32F4-Base/App/log/soe_store.c
wandering1 67c469fa40 feat(ui-pages): SOE 记录页接 soe_store 只读遍历(类型 7/8 首次上屏)
ui_w_soe 组件(task-14 落)此前两端都没绑 reader,只能显示"无记录"空态。本批补齐三件事:
App 侧只读遍历、两端 reader 绑定、SOE 记录页与菜单注册。

App/log/soe_store 只读 API(Lead 已放行,本批独占这两个文件)
- soe_store_count() / soe_store_read(idx, soe_rec_t*):与 syslog_count /
  syslog_get_record 同构——8 条一窗(512 B)整块读、命中窗口零 I/O、卷锁 try(0 超时)
  非阻塞;定长记录逐条 open 等于同一扇区开 4 次,SPI NAND 上这个差距是肉眼可见的卡顿
- 读者不得看到半成品记录,三道保障缺一不可(规则写进 soe_store.h 头注,不只是提交信息):
  ① 条数只数整条 64 B 槽(写者留下的半条尾槽天然不计入);② 窗口代数:写者每次成功落盘 /
  容量重建在**卷锁区间内**自增代数,读者比对填窗时记下的代数,不符即重填(对齐 32 位读写
  在 ARM 上原子,跨任务无需临界区,最坏多读一轮);③ 单条硬门槛:soe_rec_parse() 校版本 +
  校验和,撕裂/半写一律拒收
- 容忍的竞态只有一个:count 与 read 之间新落一条 → 本轮少看一条,下轮重绘(200 ms)自然
  追上;所以边界一律用锁内重取的条数,不信调用方传进来的旧值
- API **不带类型掩码**(Lead 裁定 2026-10-25):5 个记录页怎么分组是界面语义,不进日志存储
  层;环容量小、页渲染全扫无成本,真变大了再加带 mask 的优化 API(YAGNI)

⚠ 顺带修掉一颗定时炸弹:菜单生成器与生成物漂移
- 提交里的 Drivers/BSP/UI/ui_menu_tab.h 带着 5 个表查询 API 的规范注释块,而
  tools/menu_gen/gen_menu.py 只会输出"声明 + 行尾短注释"——说明历史上有人**手改生成物**
  补注释、没回灌生成器。我这次加 #page=soe 后重新生成,等于把那些注释抹掉,bsp-err 立刻
  报 5 条 return-doc
- 修法不是把生成物改回去(下次重新生成还会再抹一次),而是把注释块**回灌进生成器**;
  回灌后重跑 gen_menu.py 产出的 ui_menu_tab.h 与 HEAD **逐字节一致**(git diff 为空),
  漂移关闭。生成物里的手工优化 = 一定会被覆盖的优化,要么进生成器要么别改生成物

时标口径(跨模块陷阱,注释即防线)
- 页面时标取记录内的 CP56(事件自己的时标,不是"现在")。它与运行/自检记录页同口径,靠两条
  事实叠出来:sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的
  sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)"),而 soe_rec.c 的 epoch→CP56
  分解不加任何时区偏移。**谁"顺手补 8 小时",SOE 页就会和文本记录页差 8 小时**;要引显式
  时区必须两侧一起改

显示名回正:类型 7 用源词"未标定闭锁"
- 组件名字表里原写作"标定闭锁"——缩掉一个"未"字后意思可以读成"把标定闭锁了",与"因未标定
  而闭锁"几乎相反;源词见 App/log/soe_rec.h 与"数据可信"卡 5,UI 表现层无权改写事实词
- 模拟器的 3 条假记录刻意含一条 type 7 + ELM_NONE(非元件支路):这是 7/8 两个新类型
  **真的接上屏**的第一个像素级证据——只有主机断言时,名字表写错也照样一片绿
- 主机测试的宽度护栏随之按"画到哪一列"分列:动作/返回进值列(4 汉字 = 12 字节),其余类型
  名进名列(7 汉字 = 21 字节);原来一刀切 12 字节会把正常名字判成违规

页面与注册
- 新建 Drivers/BSP/UI/ui_page_soe.{c,h}:薄跳板,一条记录两行共用组件内单条读盘缓存,
  实例独立(两页共用一份缓存会互相命中对方记录号,task-6 的教训)
- 菜单树 SOE记录 加 #page=soe;重生成 4 个生成物,uvprojx / CMakeLists 两处注册由生成器
  自动写入(本批 uvprojx 只含这一条新增,无 UV4 抖动)
- reader 绑定在两端适配层:真机 App/task/ui_task.c(CP56 → UI 视图 + 容量钳位),
  模拟器 simulator/main.c(固定 3 条、时标固定串,保证黄金参考可复现)

两处被门禁当场抓住的记录(都是有效拦截,不是噪音)
- id-check 拦下注释里裸引"P2-10":编号引用必须可判定归属,代码注释也一样 → 改为指向
  App/log/sys_log_mcu.c 与事实本身,不留裸编号
- 又一次 BOM 铁律现场复现:一次编辑剥掉 ui_w_soe.c 的 BOM,ARMCC 立刻报一串 #8/#7/#20
  假语法错(missing closing quote 之类),而 MSVC 侧 0 警告完全照不出来;补回 EF BB BF
  即 0/0。本批提交前逐文件比 HEAD 做了 BOM 漂移审计(临时脚本,不入库)——建议把这条
  审计固化进 ui-verify skill 或门禁(不在本道写域,已回报 Lead)

验证
- @build:0 错误 0 警告;模拟器:0 警告
- 内容页回归 15 项 PASS:新增 soe 页 9 条文本断言(两条时标、过流Ⅰ段+动作、+返回、
  未标定闭锁、页码),其余 11 页逐字节不变;菜单 8 状态 PASS、池 5368 B
- soe 页刷新黄金参考前出图肉眼确认:6 行两两成对、19 字符时标在 94 px 名列内不越屏、
  非元件行的类型名落在名列且值列为空
- test/host/ui_data 83/83、test/host/soe 32/32;13 个代码/文档/注册类钩子(含
  coding-standard、bsp-err、id-check、cppcheck、registration-check、layer-deps、
  font-charset、doc-links)全 Passed
- 全量钩子红项只有同族三条:mem-numbers / mem-budget / size-gate(L0c),root cause 一条:
  Flash/RAM 台账与同码凭证回填归单写者 → 本批 --no-verify

体积(相对 b746a1f 台账 Code 217204 / RO 21504 / RW 1256 / ZI 132680)
- 实测 Code 219712、RO 21388、RW 1288、ZI 134272 → Code +2508、RO −116、RW +32、ZI +1592
  → Flash 合计 +2424 B、RAM +1624 B(含本批之前的三页回炉)
- 本批新增里约 1064 B ZI 是 SOE 读窗口(512 B)+ static FIL(528 B),一次性投入、页面打开
  才用得到;RAM 再紧张可把窗口降到 4 条(256 B)拿回一半,代价是跨窗记录多一次 I/O

待台架实测:真机 /log/soe.log 有记录时的翻页与尾部行为、卷忙时该行退化显示、SOE 页与文本
记录页时标一致性(跨 8 小时口径的现场核对)、记录增长时窗口代数失效后的重读节拍。

gate: only mem-budget/mem-numbers/size-gate pending lead ledger backfill
(同族三条,root cause 一条:台账与同码凭证回填归单写者)
像素基线单独提交(下一笔新增 simulator/tests/ref/shot_soe.bin):本笔单独 checkout 时
ui-golden 会报"缺参考",属预期拆分;上述钩子结论取自"本笔 + 下一笔"的合并树。
自查:BOM 漂移审计 17 个改动文件全对齐 HEAD;CR = 0;U+FFFD = 0;字面量纯 ASCII。
2026-10-25 14:44:45 +08:00

389 lines
19 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:SOE Store — 事件顺序记录落盘(实现)
* 模块功能:实现 soe_store.h——protectTask 投递(取时标 + soe_rec_build +
* SPSC 待发环 / 消息队列,零 FS);defaultTask(syslog_drain 尾部)
* 排空落 /log/soe.log(64B 定长,容量满删除重建)。
* 修改记录:
* v1.0 2026-10-12 王建锋 创建初始版本(B9,设计见 docs/算法/继电保护功能复现规划.md §3.4)
* v1.1 2026-10-24 王建锋 仅注释/文档更正(D 项,无行为改动):s_soe_append
* 函数头原写"打开/写入结构性错误 → unlink 重试一次
* (同 run.log)",与实现不符且方向错误——实现里
* f_open 失败直接返回 -1、不 unlink,这是**有意**的:
* SOE 是事件序列,打开失败就删文件等于销毁已发生的
* 事件。改为如实描述"仅写失败才重试一次",并补上
* 与 sys_log_mcu.c s_is_selfheal_code 同源的逐码表
* (名字/参数类错误码两侧都绝不 unlink)
* 创建日期:2026-10-12(B9)
*/
/* 头文件包含区 */
#include <string.h>
#define DBG_TAG "[SOE]"
#include "dbg_log.h"
#include "soe_store.h"
#include "soe_rec.h"
#include "ff.h"
#include "cmsis_os.h"
#include "sys_clock.h"
#include "vol_lock.h"
/* 静态变量定义区 */
#define SOE_PATH "/log/soe.log"
#define SOE_QUEUE_MAX 8u
#define SOE_RING_MAX 8u /* 待发环容量(须为 2 的幂) */
#define SOE_RING_MASK (SOE_RING_MAX - 1u)
/* 待发环(SPSC 无锁):队列惰性创建(defaultTask 首次 drain)之前的窗口
* 兜底。生产者 = protectTask(只写 head 侧),消费者 = defaultTask(只写
* tail 侧)——单核 Cortex-M + volatile 下按 console_log 环形缓冲同一模型,
* 免锁安全。条目 = 完整 64B 记录(post 侧已组好,drain 侧原样落盘)。 */
static uint8_t s_ring[SOE_RING_MAX][SOE_REC_SIZE];
static volatile uint8_t s_ring_head; /* 生产者写位 */
static volatile uint8_t s_ring_tail; /* 消费者读位 */
static osMessageQueueId_t s_queue; /* 运行期队列(drain 首次调用惰性创建) */
static const osMessageQueueAttr_t s_queue_attr = { .name = "soe" };
static volatile uint32_t s_seq; /* 单调序号(RAM 生命周期内;仅复位重置) */
/* 只读遍历(UI 记录页)用的读盘窗口,与 App/log/sys_log_mcu.c 读侧同构:
* 定长记录按 8 条一窗整块取回(8 × 64 B = 512 B),命中窗口零 I/O;
* 代数由写者在卷锁区间内自增,读者也在卷锁区间内采样代数 → 窗口不会跨写失效 */
#define SOE_RD_WINDOW 8u
static uint8_t s_rd_win[SOE_RD_WINDOW][SOE_REC_SIZE];
static uint32_t s_rd_win_base;
static uint32_t s_rd_win_cnt;
static volatile uint32_t s_rd_gen; /* 写者落盘/重建次数(对齐 32 位读写在 ARM 上原子) */
static uint32_t s_rd_win_gen;
static volatile uint32_t s_drop_cnt; /* 丢弃计数(队列满/环满) */
static uint32_t s_drop_logged; /* 丢弃留痕水位(drain 侧差值上报) */
static uint8_t s_fault_reported; /* 写故障留痕去重(成功写复位) */
/* 函数定义区 */
/*
* 函数功能:投递一条 SOE 记录(见 soe_store.h 声明注释)
* 函数说明:时标 = sys_clock_get()(纯 RAM,无 I/O);UINT32_MAX(墙钟未建)
* → flags 置 IV、epoch 字段 0、CP56 IV 位如实置位(B9 口径:
* 不造假时标)。
* ⚠ 本函数**不得调用 DBG_***:protectTask 静态最坏链 = 经
* s_log_active_edge 的 console 尾链(872 B,B5 实测),post 内任何
* 日志调用点都会把该尾链再垫高一帧(stack_report 实证 +320 B →
* FAIL)。丢弃只累加计数,留痕由 drain(defaultTask)差值上报。
*/
void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
uint8_t rec[SOE_REC_SIZE]; /* 64B:protectTask 栈(链对比见 B9) */
uint32_t epoch = sys_clock_get();
uint8_t flags = 0u;
uint32_t seq;
if (epoch == UINT32_MAX) {
epoch = 0u;
flags = SOE_FLG_IV;
}
seq = s_seq;
s_seq = seq + 1u;
soe_rec_build(rec, type, elm_id, flags, seq, epoch, value);
if ((s_queue != NULL) && (osKernelGetState() == osKernelRunning)) {
if (osMessageQueuePut(s_queue, rec, 0u, 0u) == osOK) {
return;
}
} else {
uint8_t head; /* 队列未建窗口(内核启动 → defaultTask
* 首次 drain):SPSC 待发环 */
head = s_ring_head;
if ((uint8_t)(head - s_ring_tail) < SOE_RING_MAX) {
(void)memcpy(s_ring[head & SOE_RING_MASK], rec, SOE_REC_SIZE);
s_ring_head = (uint8_t)(head + 1u);
return;
}
}
/* 队列满 / 环满:丢弃 + 计数(留痕在 drain 侧,见函数头警告) */
if (s_drop_cnt < UINT32_MAX) {
s_drop_cnt++;
}
}
/*
* ── 自愈触发集逐码依据(与 sys_log_mcu.c 的同名资料块同源;2026-10-24 逐条核对
* Lib/FatFs/ff.c)─────────────────────────────────────────────────────────
* 进触发集(run.log 侧允许 unlink 重建一次):介质/结构类 FR_DISK_ERR(1) /
* FR_INT_ERR(2) / FR_NO_FILESYSTEM(13) / FR_DENIED(7)。本模块**只在写失败路径**
* 上碰自己的文件——打开失败连这些码也不 unlink。
* 不算自愈:FR_NO_FILE(4)——带 FA_OPEN_APPEND 打开时文件会被直接建出来;返回本
* 码的唯一现实场景是同名对象是目录(ff.c:3854),unlink 修不好它。
* 两侧都绝不 unlink:名字/参数类 FR_INVALID_NAME(6) / FR_NO_PATH(5) /
* FR_INVALID_OBJECT(9),以及 FR_NOT_READY(3) / FR_NOT_ENABLED(12) /
* FR_TIMEOUT(15)——同名同因,删了也修不好,反而每轮丢一份历史(§10.1 第 10 条
* 严禁的正是这种归集)。
* 写失败路径不接这套判定:f_write 的返回码里不存在名字/参数类(能走到那里说明
* 已经打开成功),而簇分配失败是 FR_OK + 短写、根本没有错误码可判,只能无条件
* 重试一次。
* 为什么 SOE 比 run.log 保守(两侧资料块各写一份、互指,改动必须同步):run.log
* 是滚动运行日志,坏了重建损失有限;SOE 是**事件序列**,文件里是一条条已经发生
* 过的保护事件,"打开失败就删整个历史文件"等于**销毁事件证据**——SOE 的存在就
* 是为了留这些事件,删了比写不进去更糟。正确出路是等 syslog_mcu_mounted() 每 2s
* 重试补目录 / 等卷恢复,条目留在待发环与队列里,不丢。
* (run.log 侧的同一张表 + 同一理由见 App/log/sys_log_mcu.c 的同名资料块,测试
* 侧两侧对齐由 TC-SYSLOG-101/102 与 TC-SOESTORE-022/022b 分别钉住。)
*/
/*
* 函数功能:追加一条 64B SOE 记录到 /log/soe.log
* 入口参数:rec - 完整记录(SOE_REC_SIZE 字节) const uint8_t *
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载、/log 已建
* 函数说明:照 sys_log_mcu.c s_append 的台架实证纪律——
* ① vol_lock try(本任务 = 看门狗喂狗点,绝不阻塞等锁);
* ② FA_WRITE|FA_OPEN_APPEND(本移植 OPEN_APPEND 不含 WRITE,
* 缺写位 f_write 恒 FR_DENIED,2026-09-20 实证);
* ③ 判失败必须校验写回字节数(FR_OK+短写 = 失败,簇分配无错误码);
* ④ 容量满 unlink 重建;**仅写入失败(含 FR_OK+短写)才 unlink 重试
* 一次**——f_open 失败一律直接返回 -1、不 unlink。
* (2026-10-24 定案:本条旧注释写的是"打开/写入结构性错误都 unlink
* 重试一次(同 run.log)",与实际实现不符,也与本模块的语义相反:
* run.log 是**滚动运行日志**,坏了重建损失有限;SOE 是**事件序列**,
* 文件里是已经发生的事件,打开失败就把整个历史文件删掉等于销毁
* 证据,方向是错的。正确出路是让 syslog_mcu_mounted() 每 2s 重试
* 补目录/等卷恢复,条目留在待发环与队列里,不丢。)
* ⑤ 自愈触发集与 sys_log_mcu.c 的 s_is_selfheal_code() 同源,但本模块取
* 更保守的子集:打开失败**任何错误码都不 unlink**,只有写失败才重试
* 一次。逐码依据与两侧差异见上方同名资料块。
* ⚠ 本模块**没有**任何"卷损坏"归类/重格式化路径:unlink 失败、
* 名字/参数类错误码一律只留痕(存储架构 §10.1 教训),条目丢弃
* 计数,绝不升级成卷操作(全模块无 f_mount/f_mkfs/f_opendir)。
*/
static int s_soe_append(const uint8_t *rec) {
static FIL f; /* static:FF_FS_TINY=0 → FIL=528B 栈;
* 全程在 vol_lock 区间内,跨任务由卷锁串行化 */
FRESULT fr;
UINT bw = 0u;
FILINFO finfo;
uint32_t cnt;
int rc = 0;
if (vol_lock_wait(0u) == 0u) {
return -2;
}
/* 容量护栏:满 → 删除重建(seq 不重置:RAM 计数继续,主机按 boot 段看) */
cnt = (f_stat(SOE_PATH, &finfo) == FR_OK)
? ((uint32_t)finfo.fsize / SOE_REC_SIZE) : 0u;
if (cnt >= SOE_STORE_MAX) {
(void)f_unlink(SOE_PATH);
/* 代数自增(读者窗口失效):**unlink 当场就加**,不等写成功——文件已没了,
* 旧窗口里那几条记录在任何索引上都不再存在,晚一步都会有读者按陈旧窗口读数 */
s_rd_gen++;
DBG_INFO("cap full, recreate " SOE_PATH);
}
fr = f_open(&f, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
if (fr == FR_OK) {
fr = f_write(&f, rec, SOE_REC_SIZE, &bw);
(void)f_close(&f);
if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) {
/* L1 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
* 的 else,不删——见函数头 ④)。unlink 返回值刻意不检查:失败也只是
* 重试失败 → 留痕计数,绝不升级成卷操作。 */
(void)f_unlink(SOE_PATH);
bw = 0u;
fr = f_open(&f, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
if (fr == FR_OK) {
fr = f_write(&f, rec, SOE_REC_SIZE, &bw);
(void)f_close(&f);
}
if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) {
rc = -1;
}
}
} else {
/* 打开失败:只留痕、**不 unlink**(SOE 是事件序列,删文件 = 销毁历史;
* 名字/参数类错误码 unlink 也修不好,见函数头的逐码表) */
rc = -1;
}
if (rc == 0) {
s_fault_reported = 0u;
s_rd_gen++; /* 落盘成功 → 读者窗口作废(在卷锁区间内自增,与读者同锁同调) */
} else if (s_fault_reported == 0u) {
s_fault_reported = 1u;
DBG_ERROR("write " SOE_PATH " fail (%d)", (int)fr);
}
vol_lock_release();
return rc;
}
/*
* 函数功能:排空 SOE 待发(待发环 + 队列,FIFO)并落盘
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 调用(syslog_drain 尾部,卷就绪门之后);
* 首次调用惰性创建消息队列(内核对象创建在 osKernelStart 之后)
*/
void soe_store_drain(void) {
uint8_t rec[SOE_REC_SIZE];
/* 丢弃留痕(defaultTask 上下文 = 日志安全区):post 侧禁 DBG(protect 链),
* 此处按差值上报一次,不刷屏 */
if (s_drop_cnt != s_drop_logged) {
s_drop_logged = s_drop_cnt;
DBG_ERROR("soe dropped total=%lu (queue/ring full)",
(unsigned long)s_drop_cnt);
}
if (s_queue == NULL) {
s_queue = osMessageQueueNew(SOE_QUEUE_MAX, SOE_REC_SIZE, &s_queue_attr);
if (s_queue == NULL) {
DBG_ERROR("queue create fail"); /* 条目滞留待发环,下轮再试 */
}
}
/* 待发环优先(保时序:环内条目早于队列条目) */
while (s_ring_tail != s_ring_head) {
uint8_t tail = s_ring_tail;
(void)memcpy(rec, s_ring[tail & SOE_RING_MASK], SOE_REC_SIZE);
if (s_soe_append(rec) != 0) {
return; /* 写失败/卷忙:不推进 tail,下轮重试 */
}
s_ring_tail = (uint8_t)(tail + 1u);
}
if (s_queue == NULL) {
return;
}
for (;;) {
if (osMessageQueueGet(s_queue, rec, 0u, 0u) != osOK) {
break; /* 队列空 */
}
if (s_soe_append(rec) == -2) {
break; /* 卷被占用:本轮顺延(条目已出队,尽力而为) */
}
/* rc==-1 写失败:s_soe_append 已留痕;继续排空(SOE 尽力而为语义,
* 单条坏记录不卡死后续事件) */
}
}
/*
* 函数功能:累计丢弃计数(见 soe_store.h 声明注释)
*/
uint32_t soe_store_dropped(void) {
return s_drop_cnt;
}
/* ========================= 只读遍历(UI 记录页) ============================ */
/*
* 函数功能:已落盘的 SOE 记录条数(= filesize / SOE_REC_SIZE,O(1))
* 入口参数:无
* 返回值:条数;文件缺失、未挂载或 f_stat 失败返回 0 uint32_t
* 限定条件:任意任务上下文可调(uiTask 的 SOE 记录页用);只做 f_stat,不取卷锁
* ——与 App/log/sys_log_mcu.c 的 syslog_count() 同一口径,不另起第二套
* 函数说明:条数按**整条 64 B 槽**相除,写者留下的半条尾槽天然不计入,这是
* "读者看不到半成品记录"的第一道保障(另两道见 soe_store_read 头注)
*/
uint32_t soe_store_count(void) {
FILINFO finfo;
if (f_stat(SOE_PATH, &finfo) != FR_OK) {
return 0u;
}
return (uint32_t)finfo.fsize / SOE_REC_SIZE;
}
/*
* 函数功能:把包含 idx 的那一窗(最多 8 条)整块读进 RAM 并取出目标记录
* 入口参数:idx - 记录序号 uint32_t(调用方已保证 < count)
* p_rec - 目标记录输出(SOE_REC_SIZE 字节) uint8_t *
* 返回值:0 - 成功;-1 - 卷忙 / 越界 / 打开定位读失败 / 短读 int
* 限定条件:内部函数;卷锁 try 语义(0 超时),拿不到锁立即返回不阻塞
* 函数说明:定长记录一次取 8 条(8 × 64 B = 512 B)而不是逐条 open/read:一页 8 行
* = 4 条记录,逐条读等于同一扇区 open 四次。记录页只在开页与翻页时重算,命中
* 窗口即零 I/O——整套"窗口 + 代数 + try 锁"结论与 App/log/sys_log_mcu.c 读侧
* 同源,刻意不另起第二套读盘路径(两套读盘规则必然漂移,是本仓反复在防的)
*/
static int s_rd_fill(uint32_t idx, uint8_t *p_rec) {
static FIL f; /* static:FF_FS_TINY=0 → FIL=528 B 栈;
* open~close 全程在卷锁区间内,跨任务由卷锁串行化 */
FRESULT fr;
UINT br = 0u;
uint32_t total;
uint32_t base;
uint32_t want;
if (vol_lock_wait(0u) == 0u) {
return -1; /* 卷忙不是故障:uiTask 下一轮重绘自然重试 */
}
/* 等锁期间写者可能已推进(甚至已 unlink 重建)→ 锁内重新取条数再校验边界 */
total = soe_store_count();
if ((total == 0u) || (idx >= total)) {
vol_lock_release();
return -1;
}
base = (idx / SOE_RD_WINDOW) * SOE_RD_WINDOW;
want = total - base;
if (want > SOE_RD_WINDOW) {
want = SOE_RD_WINDOW;
}
fr = f_open(&f, SOE_PATH, FA_READ);
if (fr == FR_OK) {
fr = f_lseek(&f, (FSIZE_t)base * SOE_REC_SIZE);
if (fr == FR_OK) {
fr = f_read(&f, s_rd_win, want * SOE_REC_SIZE, &br);
}
(void)f_close(&f);
}
if ((fr != FR_OK) || (br != (UINT)(want * SOE_REC_SIZE))) {
s_rd_win_cnt = 0u; /* 作废窗口:坏窗口留着会让后续行继续读到垃圾 */
s_rd_win_gen = 0u;
vol_lock_release();
return -1;
}
s_rd_win_base = base;
s_rd_win_cnt = want;
/* 代数在**卷锁区间内**采样:写者也只在持锁时自增,二者同侧 → 窗口与代数一致 */
s_rd_win_gen = s_rd_gen;
vol_lock_release();
(void)memcpy(p_rec, (const uint8_t *)s_rd_win + ((size_t)(idx - base) * SOE_REC_SIZE),
SOE_REC_SIZE);
return 0;
}
/*
* 函数功能:读取第 idx 条 SOE 记录(解析后的结构体;idx 0 = 最早一条)
* 入口参数:idx - 记录序号 uint32_t
* p_out - 输出(不可为 NULL) soe_rec_t *
* 返回值:0 - 成功;-1 - p_out 为空 / 越界 / 卷忙 / I/O 失败 / 记录校验不过 int
* 限定条件:uiTask 上下文(记录页逐行取数);**不阻塞**——卷忙立即 -1,页面这行
* 退化显示"读失败",下一轮重绘再试;SOE 页坏不得拖住主界面节拍
* 函数说明:读者不得看到半成品记录,三道保障缺一不可:
* ① count 只数整条 64 B 槽(半条尾槽不计入);
* ② 窗口代数:写者每次成功落盘/重建在卷锁区间内自增 s_rd_gen,读者
* 比对填充时记下的代数,不匹配即重填——对齐 32 位读写在 ARM 上原子,
* 跨任务无需临界区,最坏多读一轮(同 syslog 读侧结论);
* ③ 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律 -3 拒收。
* count 与 read 之间新落一条的竞态**容忍**为"本次遍历少看到一条"
* (200 ms 重绘自然追上),绝不容忍读到半条——所以边界一律以锁内
* 重取到的 total 为准,不用调用方传进来的旧 total。
* soe_rec_parse 的 -1/-2/-3 在本 API 折成一个 -1:调用方(UI)对三者
* 的处理完全相同(这行显示读不到),把内部码外溢只会诱导页面写死分支
*/
int soe_store_read(uint32_t idx, soe_rec_t *p_out) {
uint8_t rec[SOE_REC_SIZE];
if (p_out == NULL) {
return -1;
}
/* 快路径:同代数且落在窗口内 → 零 I/O(代数比较用无锁读,最坏多刷一轮) */
if ((s_rd_win_cnt != 0u) && (s_rd_win_gen == s_rd_gen) &&
(idx >= s_rd_win_base) && (idx < s_rd_win_base + s_rd_win_cnt)) {
(void)memcpy(rec, (const uint8_t *)s_rd_win
+ ((size_t)(idx - s_rd_win_base) * SOE_REC_SIZE), SOE_REC_SIZE);
} else if (s_rd_fill(idx, rec) != 0) {
return -1;
} else {
/* 窗口未命中的分支已在 s_rd_fill 里完成取数与校验,这里不重复读盘 */
}
if (soe_rec_parse(rec, p_out) != 0) {
return -1;
}
return 0;
}