Files
STM32F4-Base/App/log/soe_store.c
T
wandering1 5a806d8a26 fix(log,net): 处置主机单测照出的 10 条现状,撤销 1 条误报
用户裁定「全部修」。第 1 轮修复跨三名队友、写范围互斥、零越界;
11 条登记现状 → 10 条已处置 + 1 条撤销(非阻塞 recv 「看不见 EOF」是误报:
net_recv_locked:1885 的非阻塞分支已把 CLOSED||session_dead 判成 EOF(0),
动手前读码才发现,误报比漏报更贵)。

存储与日志(App/log/sys_log_mcu.c、soe_store.c、cfg_file_store_mcu.c、sys_log.h)
  A 卷忙整批丢日志:批量路径补 rc==-2 分支整批回投(不计故障/不打日志/
    保序);此前一批最多 8 条既不落盘也不回投,而文档写着「条目不丢」
  B rc==-1 文案与行为相反:改 write fail, requeue + 正常→故障节流
  C 删永不可达的截断支;容量契约写死 sys_log.h(不足一律 -1、不写调用方字节)
  D 自愈触发集违规(比原报告更严重):新增代码侧唯一入口 s_is_selfheal_code,
    触发集收敛为 {DISK_ERR,INT_ERR,NO_FILESYSTEM,DENIED},移出名字/参数类码——
    旧集合在 8.3 同名同因下每轮删一次自己的历史日志且永不自愈,正是存储架构
    §10.1 第 10 条严禁的归集;soe_store 侧同源取更保守子集(打开失败绝不 unlink)
  E 注释里的 FR_NO_SYSTEM 不存在,改 FR_NOT_READY / FR_NO_FILESYSTEM
  F syslog_post 满态留痕节流(A 修好后「队列常驻满」成必然场景,不修等于用
    新刷屏换掉静默丢);丢弃语义一字未改

NET(net_socket.c/.h、net_types.h、net_socket_sim.c、lftpd.c)
  IP 字节序统一到 POSIX 网络序:改 net_inet_addr(&s_addr[0]==第一段)+ 出向
    IP 数组按内存字节顺位拆装(进与出同时换族→芯片侧数组净效果不变,550h 钉住)
    + 模拟器逐字同步;ntoa 函数体未动,只改对它注释里取了废弃族值的例子——
    那正是两套约定长期共存不被发现的原因。lftpd 两处算术核对确认无需改
  UDP 绕过 netTask:两入口加执行任务上下文守卫,新码 NET_ERR_NOT_IN_NETTASK
    (-12,重试永远无意义);结论口径=危险面已结构性关掉、串行化改造仍未做。
    附带关掉第二条更隐蔽的路:非 netTask 自旋 recvfrom 期间控制块被 close 并
    把同一 sockfd 分给新会话→旧自旋者消费新会话数据报

控制台(console_log.c/.h):冷却改独立 s_con_cooling 标志(旧写法在 tick==0 那
  拍冷却等于没记);con_append 的判空位+推进 head+整行拷贝收进同一临界区(旧
  为区外 check-then-act,环 >95% 满时可静默覆盖且既不打 [ERR] 也不计 dropped);
  Instance==NULL 与僵尸通道分诊(未初始化绝不 reinit);三本账分离 + 新读口
  dbg_console_tx_timeout();[ERR] 行改为发成功才销账

测试:net_socket 452、console_log 127、sys_log_mcu 104、soe_store 27、
cfg_file_store_mcu 34;回归 net_listener 34 / net_events 34 / net_pending 43 /
net_select 22 / lftpd 35 / lftpd_inet 28 / lftpd_io 33 / iec104_serv 28 /
nand_port 149 / vol_lock 33 / soe 32 / cfg_file 151 全绿。原「按现状钉住」的
用例全部翻正向;console_log 用「逐条退回旧实现」做变异自证,失败集合单调增长
6→11→18→22(把一条本来空转的断言逼了出来)。
游标抬升:TC-NETSOCK-545→560、TC-SYSLOG-100→107、TC-CONLOG-135→167。

文档:控制台设计说明 §4.1 口径与 §13-O2 销项 + v1.5;运行与自检记录 §4.4/§4.5
四条;存储架构 §10.1 第 10 条推论(指向 s_is_selfheal_code);TRAP-NET-21 后续
条目;BSD 指南新增 §9.0 s_addr 唯一约定;覆盖率口径 §9「第 1 轮修复处置」表 +
后续项 5 条 + 台架复验 7 条 + 新坑 8 条 + 变更记录。

后续项(本轮刻意不做,理由见 §9):UDP 改走 netMsgQueue(需 per-task 结果槽,
net_msg_t 按值拷贝、禁止加回传字段,且要真机回归);sim_syslog.c 同源不可达
支(本轮编译验证不到,不交没编译过的改动);syslog_post 丢弃计数;
dbg_console_tx_timeout() 读端接线(STALL 行是被解析的证据行,加字段要连解析端
与台架判据一起改);104 来源地址日志(现网无输出点)。

gate: @build 0 errors 0 warnings(Code=213720 RO=18860 RW=1204 ZI=131500)。
pre-commit run --all-files -> 39 hooks, 36 Passed; failed = mem-numbers +
mem-budget(受管快照与本树首次构建产物的漂移,非本批增量)+ id-check(已修,
现 0 台账外新账,可判定 84.63% > 基线 82.6%)。实测水位 RW_IRAM1 57.4%、
RW_CCM 87.9%,均低于 92% 红线;RAM/Flash 本批增量为数个 static 变量。
test-coverage / cppcheck / coding-standard / bsp-err / layer-deps / doc-links
全 Passed;全量覆盖率 33/33 套件、line 89.3%、func 92.3%、逐文件下降 0。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 19:43:56 +08:00

257 lines
13 KiB
C
Raw 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 生命周期内;仅复位重置) */
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);
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;
} 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;
}