1d96fc9f87
- 新增 App/log/soe_rec.{c,h}:64B 定长 SOE 记录纯编解码(CP56Time2a
时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS
依赖,主机 L0.5 覆盖(32 断言,含与 iec104 CP56 解码器交叉互证)
- 新增 App/log/soe_store.{c,h}:protectTask 边沿投递(取时标 + 组记录 +
SPSC 待发环/消息队列,零文件系统);defaultTask 经 syslog_drain 尾部
排空落 /log/soe.log(卷锁 try / FA_WRITE|FA_OPEN_APPEND / 校验字节数 /
容量满 unlink 重建,全套照 run.log 实证纪律;无卷损坏归类路径)
- protect_task.c:元件动作/返回、软压板投退、采样闭锁/恢复六类边沿
→ soe_store_post;post 内禁 DBG_*(protect 静态最坏链 872 B 不变,
改前/改后 stack_report 九任务逐字节一致),丢弃留痕挪 drain 侧
- iec104:单点 YX 自发改 M_SP_TB_1(30) 带 CP56 时标(总召/周期保持
M_SP_NA_1,单测两种都断言);link cfg 新增 epoch_ms 时标源回调
(NULL/无效 → IV 位如实置位);serv 绑 sys_clock(秒域时基,
ms 域 = 秒×1000,不造假亚秒)
- 主机单测:test/host/soe/(32 断言)+ iec104 94→97(TB 时标/IV);
iec104_master.py 解析 type 30(B6/B7 流不回归);decode_soe.py
台架解码断言工具(seq 单调/校验和/UTC 交叉)
- 注册:uvprojx + check_registration SIM_EXEMPT 点名(MCU-only 胶水)
- 文档(我方属主区):`App/log/README.md` 增 soe 节(64B 记录/单写者/
post 禁 DBG/UTC 交叉坑);`docs/子系统设计/IEC104上行设计说明.md`
§1 范围行 + §3.2 时标注 + §7 台架行 + §8 第 2 条销项 + 变更记录行
台架腿(bench 锁 B9,release --bench-pass):run_boot_test PASS;
--b7-pre 21/21、--b7-post 13/13(B6/B7 回归 + 确定性动作边沿);
自发 YX 上送 M_SP_TB_1 带 ts(11:00:51 动作 / 11:00:53 复归);
FTP 下载 /log/soe.log 30 条记录 decode PASS(seq 0..28 单调、
ELM_ASSERT 14 条、UTC 交叉一致);定值/压板已恢复默认。
台账 +1088 B(SPSC 环 512 + 静态 FIL 528 + 计数器)随合并由 Lead
回填(ram_baseline --write + check_mem_numbers --write)。
gate: only mem-budget/mem-numbers pending lead ledger backfill
(pre-commit --all-files: 29 Passed / 2 Failed / 0 Skipped;
hygiene self-check OK: no BOM/CRLF/mojibake/trailws/non-ASCII literal)
sync 追平 v2-on-ui(B8 freq + N33 抬栈)后复跑:@build 0/0
(Code=209492 RW=1104 ZI=154184);stack_report 九任务全 PASS、
protectTask 872 B/57% 不变;ci_local L0.5 15 runner 951/951
(freq 46 / soe 32 / iec104 97 / prot_set 90 / prot_elm 47)。
顺带修 protect_task.c 陈旧注释:定值巡检 31 项 → 40 项(B8 后)。
211 lines
8.5 KiB
C
211 lines
8.5 KiB
C
/*
|
||
* 模块名称:SOE Store — 事件顺序记录落盘(实现)
|
||
* 模块功能:实现 soe_store.h——protectTask 投递(取时标 + soe_rec_build +
|
||
* SPSC 待发环 / 消息队列,零 FS);defaultTask(syslog_drain 尾部)
|
||
* 排空落 /log/soe.log(64B 定长,容量满删除重建)。
|
||
* 创建日期: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++;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:追加一条 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 重建;打开/写入结构性错误 → unlink 重试一次
|
||
* (L1 文件级自愈,同 run.log)。
|
||
* ⚠ 本模块**没有**任何"卷损坏"归类/重格式化路径:unlink 失败、
|
||
* 名字/参数类错误码一律只留痕(存储架构 §10.1 教训),条目丢弃
|
||
* 计数,绝不升级成卷操作。
|
||
*/
|
||
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 文件级自愈:陈旧/损坏条目 → 删除重建、重试一次(同 run.log;
|
||
* 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 {
|
||
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;
|
||
}
|