da36f4ba56
批次六余账之二(代码与脚本面)。四族都是「批次一 / 三c 改了文档、漏了代码」的半改 状态。 - 日志自愈:「L1 文件级自愈」→「文件级自愈」、「L2 错误去重」→「错误去重」(正文 本来就这么叫,层号纯属多余):App/log/soe_store.c、sys_log_mcu.c 三处、 test/host/sys_log_mcu 的测试与假件头共 10 处。 - timeSync:「L3 pending 化」→ 挂起请求 / pending 化——这条口径早写在记忆里, 今天才算全仓一致。App/sync/time_sync.c 3 处。 - CubeMX 防丢失三层优先级:scripts/check_cubemx_survival.py 6 处「由 L3 升级为 L2」「L2 受保护」「L3 兜底」→ 写进 .ioc / 搬进 USER CODE 区 / 审计兜底;变迁史 句子按「换号留事实」改写、日期原样保留。 - 采集链旧层名:App/README.md「采集与算法层(L1/L2/L3)」→「采集与算法层」; protect_task.c/.h「(L3 板端接线,B5)」→「(板端接线,B5)」。 - phasor 断言文案 19 处:改为 ASCII 的 phasor-in ring(C 字面量必须纯 ASCII)。 改前先证明这些串无耦合——全仓只有本文件含它们、harness 只看 returncode 与汇总 标记;改后 phasor 57/57 与基线同数。 - 零行为:剥注释 + 套登记表后与 HEAD 逐字节 / AST 相同;sys_log_mcu 141/141、 phasor 57/57、watchdog 47/47、freq 46/46 均与动手前基线同数。 - 同一四族的文档面(CubeMX 30、采集链 5、菜单层级 2、双写 14)由下一笔收。
389 lines
19 KiB
C
389 lines
19 KiB
C
/*
|
||
* 模块名称: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)) {
|
||
/* 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
|
||
* 的 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;
|
||
}
|