Files
STM32F4-Base/App/log/soe_store.c
T
wandering1 1d96fc9f87 feat(soe): B9 动作记录 + 时标 SOE(L4-4/L4-5)+ 104 M_SP_TB_1 自发
- 新增 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 后)。
2026-10-12 19:38:13 +08:00

211 lines
8.5 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 定长,容量满删除重建)。
* 创建日期: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;
}