test(host): P2 覆盖率批次,5 套主机单测与共享假 FatFs

P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界):
  net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片)
  sys_log_mcu 75 条 / 310-313=99.0%、12-12
  console_log 104 条 / 188-189=99.5%、15-15
  cfg_file_store_mcu 34 条 / 97-97=100%、10-10
  soe_store 26 条 / 78-79=98.7%、4-4
合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。

账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29,
恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母
4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件
全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对:
下降 0 个文件。

基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只
定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED
可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开
一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正
两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04),
另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。
存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、
nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。

编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27,
先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK;
id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。

文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、
12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。
本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。

10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、
net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示
全部修复,另开一轮(含测试字面量统一纯 ASCII)。

gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers
Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱);
mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0
(纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。
This commit is contained in:
2026-10-24 17:54:02 +08:00
parent 4ac869058d
commit 794906cdc8
46 changed files with 10821 additions and 39 deletions
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""SOE 落盘胶水层(soe_store)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/soe_store/soe_store_test.c(以 #include 纳入**生产源**
App/log/soe_store.c 与 App/log/soe_rec.c,并纳入共享假卷
test/host/common/fatfs_fake.c),在 PC 上离线跑 post 侧零 I/O 契约、环/队列分流与
丢弃计数、drain 侧的卷锁 try / FA_WRITE|FA_OPEN_APPEND / 写回字节数校验 / 容量满
重建 / 故障留痕去重,失败即非 0 退出。工具链与判定委托
test/host/common/unit_test_harness.py。
⚠ 与 test/host/soe/ 是两码事:那套测的是 soe_rec.c 的**编解码**(纯逻辑,零 FS);
本套测 soe_store.c 的**落盘胶水**。目录、模块名、TC 号族都分开
(TC 模块码用 SOESTORE,SOE 已被存量占用)。
为什么要主机侧单测(而不是只用台架):
1. post 侧"一条 DBG 都不许打"是 protectTask 的**栈预算硬约束**(函数头原话:
该任务静态最坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 里加一条
日志就 +320 B → stack_report FAIL)。真机上忘这条不会崩,只会某次最坏路径
把栈打穿;只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。
2. 环写失败不推进 tail(不丢)而队列写失败丢已出队那一条——**两条通道不对称**,
台架要恰好撞上"FTP 正占卷"才看得到一次。
3. 短写(FR_OK + 写回不足)、容量满重建、故障留痕去重都要故障注入才成立。
4. 陷阱二(名字/参数类错误码严禁升级成卷操作)只有假卷能把 FR_INVALID_NAME 稳定
喂进自愈路径,并断言"邻居文件与 /log 目录一根毛都没动"。
桩的边界:ff.h / sys_clock.h / vol_lock.h / soe_rec.h / soe_store.h 用**真实头**
(FRESULT、FA_*、sys_clock_get 签名一改即编译期报错);main.h 是桩,只为截断
sys_clock.h -> sd2506.h -> main.h -> stm32f4xx_hal.h 这条链(真实 main.h 第一行
就拉整棵 HAL);cmsis_os.h / dbg_log.h 是本目录桩。FatFs 由
test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**,
避免被卷进别人的构建)。sys_clock_* / vol_lock_* / 消息队列强符号桩在本测试单元里。
用法(仓库根目录):
& '.venv\Scripts\python.exe' test/host/soe_store/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、
docs/算法/继电保护功能复现规划.md §3.4(L4-4/L4-5,B9)、
docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项
创建日期:2026-10-24
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 顺序敏感:stub 必须最先(覆盖 cmsis_os.h / dbg_log.h / main.h),
# 其余全是真实头目录;假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例)。
BUILD = TestBuild(
name="SOE Store Tests",
src=os.path.join(HERE, "soe_store_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "test", "host", "common"),
os.path.join(ROOT, "App", "log"),
os.path.join(ROOT, "App", "util"),
os.path.join(ROOT, "App", "fs"),
os.path.join(ROOT, "Drivers", "BSP"), # bsp_err.h(sd2506.h 依赖)
os.path.join(ROOT, "Drivers", "BSP", "SD2506"),
os.path.join(ROOT, "Lib", "FatFs"), # 真实 ff.h / ffconf.h
HERE,
],
summary_marker="=== SOE Store Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+708
View File
@@ -0,0 +1,708 @@
/*
* 模块名称:SOE 落盘胶水层(soe_store)主机侧单元测试(L0.5)
* 模块功能:在 PC 上离线验证 App/log/soe_store.c 的四条对外契约与它自己的
* "照 sys_log_mcu 的台架实证纪律"注释:
* - post 侧**零文件系统、零日志**(protectTask 栈预算 + DoD 硬约束);
* - 环 / 队列两条待发通道的分流与"丢弃只计数";
* - drain 侧的 vol_lock try、FA_WRITE|FA_OPEN_APPEND、写回字节数校验、
* 容量满 unlink 重建、故障留痕去重;
* - **名字/参数类错误码绝不升级成卷操作**(存储架构 §10.1 第 10 条)。
*
* 被测对象:真实源文件(#include 原样纳入,不改生产代码):
* App/log/soe_store.c —— 本模块主体
* App/log/soe_rec.c —— 64B 记录构建(纯逻辑,零 OS,直接用真件;
* test/host/soe 已单独覆盖其编解码,本套不重测)
* 假件与桩:
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明)
* stub/dbg_log.h / stub/cmsis_os.h / stub/main.h —— 本目录桩
* sys_clock / vol_lock 的可控强符号实现写在本文件里
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. **post 侧禁 DBG 是栈预算硬约束**(soe_store.c 函数头原话:protectTask 静态最
* 坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 内任何日志调用点都会
* 再垫高一帧,stack_report 实测 +320 B → FAIL)。真机上"忘了这条"不会崩,只会
* 在某次最坏路径上把栈打穿 → 只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。
* 2. **环 / 队列的丢弃语义不对称**:环写失败**不推进 tail**(下轮重试、条目不丢),
* 队列条目已出队、卷忙就丢那一条。真机上要恰好撞上"FTP 正在占卷"才看得到,
* 而"到底丢不丢"是运维读 SOE 时能不能信任序号连续的前提。
* 3. **短写 = 失败**(FR_OK + 写回字节数不足,簇分配没有错误码)与**满则重建**
* 这两条只在介质坏块 / 容量见底时发作;假卷可以把"只写回 63 字节"直接喂进来。
* 4. 写故障留痕的去重(s_fault_reported)决定了长跑时 /log 与串口会不会被同一条
* 故障刷屏——真机要复现"失败→恢复→再失败"这个序列极慢。
*
* 覆盖点(TC 号族 = SOESTORE(三位序号);SOE 已被存量 test/host/soe 占用):
* —— post 侧(零 I/O 契约)——
* TC-SOESTORE-001 post 3 条只写环:零 FatFs 调用、零卷锁调用、零日志
* TC-SOESTORE-002 墙钟未建立(UINT32_MAX)→ flags 带 IV 且 epoch 字段落 0(不造假时标)
* TC-SOESTORE-003 seq 上电内单调 +0123(丢弃也占号:号不跳 = 假象)
* TC-SOESTORE-004 环满(8 条)→ 第 9 条丢弃 + 计数,不阻塞、不崩、**不打日志**
* TC-SOESTORE-005 内核运行 + 队列已建 → 走队列,环不再写入
* TC-SOESTORE-006 队列满 → 丢弃 + 计数(与环满同一条可观测出口)
* TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不再回绕
* —— drain 侧:落盘与打开模式 ——
* TC-SOESTORE-010 首次 drain 惰性建队列;1 条落盘 = 64 字节且可被 soe_rec_parse 解回
* TC-SOESTORE-011 打开模式必须是 FA_WRITE|FA_OPEN_APPEND;对照保真:只给
* FA_OPEN_APPEND 的句柄 f_write 恒返 FR_DENIED(陷阱一,本仓实证根因)
* TC-SOESTORE-012 多条落盘:一条一次 open/write/close(本模块**没有**批量合并——
* 与 sys_log 的 N-24 不同),FIFO 顺序、seq 递增
* TC-SOESTORE-013 open 与 close 严格配对(脏句柄不跨调用残留)
* —— drain 侧:故障注入 ——
* TC-SOESTORE-014 短写 63B → 判失败 → unlink 重建 → 重试成功 → 最终恰好 64 字节
* TC-SOESTORE-015 连续两次短写 → 返回 -1;留痕恰好一次(去重);恢复后再失败才再留痕
* TC-SOESTORE-016 卷忙(vol_lock_wait(0)=0)→ 返回 -2;**不碰 FatFs、不 release**
* TC-SOESTORE-017 环条目在写失败时不推进 tail → 下轮重试不丢、且不重复落盘
* TC-SOESTORE-018 队列条目已出队:卷忙只丢当前一条,剩余留在队列(不对称现状钉住)
* TC-SOESTORE-019 容量满(1024 条)→ unlink 重建 + INFO 留痕,新文件恰好 64 字节
* TC-SOESTORE-020 丢弃留痕按差值上报一次,不刷屏
* TC-SOESTORE-021 队列创建失败(返回 NULL)→ 条目滞留环、下轮再试,不崩
* —— 陷阱二:名字/参数类错误码不得升级成卷操作 ——
* TC-SOESTORE-022 f_open 返 FR_INVALID_NAME → 只留痕返回 -1:**不 unlink**、
* 不碰 /log 目录、条目全部保留(自愈只针对自己的文件)
* TC-SOESTORE-023 写路径反复失败时 f_unlink 次数有界(每轮 <=1),目录与邻居文件不动
* TC-SOESTORE-024 假卷严格 8.3 校验的保真自检:".fschk" -> 名字/盘符类拒绝码,
* 而生产路径 SOE_PATH 必过(钉住"改名即静默写失败"的约束)
*
* 覆盖率台账(--module soe_store 自量):App/log/soe_store.c 78/79 行 = 98.7%、
* 函数 4/4。唯一未覆盖行 soe_store.c:202 = drain 里 for (;;) 的收尾大括号,
* 该循环只能靠 break 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。
*
* 本套照出的现状(只登记、不改生产代码;详见交 Lead 的报告):
* soe_store.c 的函数头第 ④ 条写"打开/写入结构性错误 → unlink 重试一次(同 run.log)",
* 但实现里 **f_open 失败直接 return -1、不 unlink 重试**(只有 f_write 失败才重试)。
* run.log 侧(sys_log_mcu.c s_append_rec)则对 FR_NO_FILE/FR_NO_PATH/FR_INVALID_NAME
* 等一整套码做 unlink+重开。两套并不一致 —— 是注释夸大了实现,还是实现漏了一半,
* 需生产侧裁定;本套只用 TC-SOESTORE-022 把现状钉成"改了会响"。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ff.h" /* 真实 Lib/FatFs/ff.h:FRESULT / FA_* / FIL 原汁 */
#include "fatfs_fake.h" /* 假卷:类型与常量都取自上面那份真头 */
#include "cmsis_os.h" /* 桩:消息队列 + 内核状态(签名照 cmsis_os2.h) */
#include "dbg_log.h" /* 桩:DBG_* -> 计数 */
#include "sys_clock.h" /* 真实头(App/util):签名一改即编译期报错 */
#include "vol_lock.h" /* 真实头(App/fs) */
#include "soe_rec.h"
#include "soe_store.h"
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 假件与生产源码纳入 ===================== */
#include "fatfs_fake.c" /* 假 FatFs 实现 */
#include "soe_rec.c" /* 真实 64B 记录构建(纯逻辑) */
#include "soe_store.c" /* 被测主体 */
/* ===================== dbg_log 桩计数(stub/dbg_log.h 声明) ===================== */
int g_soe_err_count;
int g_soe_info_count;
int g_soe_debug_count;
/* ===================== 假墙钟 ===================== */
static uint32_t s_clock_now = 1760234567u; /* 2025-10-12 前后,CP56 年域内 */
static int s_clock_valid = 1;
uint32_t sys_clock_get(void)
{
return (s_clock_valid != 0) ? s_clock_now : UINT32_MAX;
}
/* sys_clock.h 里声明、本模块不调用的接口:给出空实现,纯为可链接 */
int sys_clock_init(void) { return 0; }
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *p_time)
{
(void)unix_secs; (void)p_time; return 0;
}
int sys_clock_set_unix(uint32_t unix_secs) { (void)unix_secs; return 0; }
int sys_clock_get_time(sd2506_time_t *p_time) { (void)p_time; return -1; }
uint32_t HAL_GetTick(void) { return 0u; }
char *sys_clock_get_str(char *buf, size_t buf_size)
{
(void)buf; (void)buf_size;
return NULL;
}
/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */
static int s_lock_busy_left; /* >0:接下来 N 次 vol_lock_wait 返回"忙"(try 失败)
* 用计数器而不是按序号取值的脚本数组——脚本数组的下标
* 会被场景里前几次 drain 消耗掉,注入看着设了其实没生效 */
static int s_lock_waits;
static int s_lock_releases;
static int s_lock_held;
static int s_lock_seq_err; /* 未持锁却 release = 锁纪律被写反 */
static void lock_busy_next(int n)
{
s_lock_busy_left = n;
}
static void locks_clear(void)
{
s_lock_busy_left = 0;
s_lock_waits = 0;
s_lock_releases = 0;
s_lock_held = 0;
s_lock_seq_err = 0;
}
int vol_lock_init(void) { return 0; }
int vol_lock_wait(uint32_t timeout_ms)
{
int r;
(void)timeout_ms;
/* 跑飞保险:调用数越界直接判死,不让死循环拖挂门禁 */
if (s_lock_waits > 20000) {
(void)fprintf(stderr, "[FATAL] vol_lock_wait call guard tripped\n");
exit(2);
}
r = (s_lock_busy_left > 0) ? 0 : 1;
if (s_lock_busy_left > 0) {
s_lock_busy_left--;
}
s_lock_waits++;
if (r != 0) {
s_lock_held = 1;
}
return r;
}
void vol_lock_release(void)
{
s_lock_releases++;
if (s_lock_held == 0) {
s_lock_seq_err++; /* 没拿到锁就释放:fail-open 语义被写反的证据 */
}
s_lock_held = 0;
}
/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */
#define MQ_SLOTS 16u
#define MQ_MAXMSG 64u /* SOE 记录 64B;syslog 侧条目 12B,同一池够用 */
typedef struct {
int used; /* 槽位有无消息 */
uint8_t data[MQ_MAXMSG];
} mq_slot_t;
static mq_slot_t s_q_pool[MQ_SLOTS];
static uint32_t s_q_head; /* 出队下标 */
static uint32_t s_q_cnt;
static uint32_t s_q_cap; /* osMessageQueueNew 给的 msg_count */
static uint32_t s_q_msgsz;
static int s_q_new_calls;
static int s_q_new_fail; /* 1 = osMessageQueueNew 返 NULL */
static int s_q_get_calls;
static int s_q_put_calls;
static int s_q_create_fail_left; /* 预留:连续失败注入(当前用 0/1) */
static void queue_clear(void)
{
(void)memset(s_q_pool, 0, sizeof(s_q_pool));
s_q_head = 0u;
s_q_cnt = 0u;
s_q_cap = SOE_QUEUE_MAX;
s_q_msgsz = SOE_REC_SIZE;
s_q_new_calls = 0;
s_q_new_fail = 0;
s_q_create_fail_left = 0;
s_q_get_calls = 0;
s_q_put_calls = 0;
}
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
const osMessageQueueAttr_t *attr)
{
(void)attr;
s_q_new_calls++;
if ((s_q_new_fail != 0) || (s_q_create_fail_left > 0)) {
if (s_q_create_fail_left > 0) {
s_q_create_fail_left--;
}
return NULL;
}
s_q_cap = msg_count;
s_q_msgsz = msg_size;
s_q_head = 0u;
s_q_cnt = 0u;
/* 句柄本身没有意义:返回一个稳定非空指针即可 */
return (osMessageQueueId_t)&s_q_pool[0];
}
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
uint8_t msg_prio, uint32_t timeout)
{
uint32_t tail;
(void)mq_id; (void)msg_prio; (void)timeout;
if (s_q_put_calls++ > 20000) {
(void)fprintf(stderr, "[FATAL] osMessageQueuePut guard tripped\n");
exit(2);
}
if ((msg_ptr == NULL) || (s_q_cnt >= s_q_cap) || (s_q_msgsz > MQ_MAXMSG)) {
return osErrorResource; /* 队列满:真实 CMSIS 在 timeout=0 时的返回 */
}
tail = (s_q_head + s_q_cnt) % MQ_SLOTS;
(void)memcpy(s_q_pool[tail].data, msg_ptr, s_q_msgsz);
s_q_pool[tail].used = 1;
s_q_cnt++;
return osOK;
}
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
uint8_t *msg_prio, uint32_t timeout)
{
(void)mq_id; (void)msg_prio; (void)timeout;
if (s_q_get_calls++ > 20000) {
(void)fprintf(stderr, "[FATAL] osMessageQueueGet guard tripped\n");
exit(2);
}
if ((msg_ptr == NULL) || (s_q_cnt == 0u)) {
return osErrorResource; /* 队列空 */
}
(void)memcpy(msg_ptr, s_q_pool[s_q_head].data, s_q_msgsz);
s_q_pool[s_q_head].used = 0;
s_q_head = (s_q_head + 1u) % MQ_SLOTS;
s_q_cnt--;
return osOK;
}
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id)
{
(void)mq_id;
return s_q_cnt;
}
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id)
{
(void)mq_id;
s_q_head = 0u;
s_q_cnt = 0u;
return osOK;
}
/* ===================== 内核状态开关 ===================== */
static osKernelState_t s_kernel_state = osKernelInactive;
osKernelState_t osKernelGetState(void)
{
return s_kernel_state;
}
/* ===================== 复位与辅助 ===================== */
static void reset_all(void)
{
ff_fake_reset();
locks_clear();
queue_clear();
s_clock_now = 1760234567u;
s_clock_valid = 1;
s_kernel_state = osKernelInactive;
g_soe_err_count = 0;
g_soe_info_count = 0;
g_soe_debug_count = 0;
/* 生产模块的 static 在场景之间会残留:逐位显式清,漏一次会红一片
*(test/host/lftpd_io 与 nand_port 同一教训) */
(void)memset(s_ring, 0, sizeof(s_ring));
s_ring_head = 0u;
s_ring_tail = 0u;
s_queue = NULL;
s_seq = 0u;
s_drop_cnt = 0u;
s_drop_logged = 0u;
s_fault_reported = 0u;
/* 生产语义前置:/log 由 defaultTask 挂载时建好,本模块假定它在(soe_store.h:
* "由 syslog_drain 在卷就绪门之后挂接") */
(void)ff_fake_mkdir_direct("/log");
}
/* 用假卷 + 真 soe_rec_parse 校验落盘内容:路径常量取自生产宏 SOE_PATH */
static int read_rec(uint32_t idx, soe_rec_t *p_out)
{
uint8_t raw[SOE_REC_SIZE];
if (ff_fake_fetch(SOE_PATH, raw, idx * SOE_REC_SIZE, SOE_REC_SIZE) != SOE_REC_SIZE) {
return 0;
}
return (soe_rec_parse(raw, p_out) == 0) ? 1 : 0;
}
/* ===================== TC-SOESTORE-001..007:post 侧 ===================== */
static void tc_post_side(void)
{
soe_rec_t out;
uint32_t i;
int ok;
printf("-- TC-SOESTORE-001..007 post 侧(零 I/O 契约)--\n");
/* 001:post 3 条只写环,绝不碰盘、绝不碰卷锁、绝不打日志 */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 0x1111u);
soe_store_post((uint8_t)SOE_T_ELM_RETURN, 2u, 0x2222u);
soe_store_post((uint8_t)SOE_T_PLATE_IN, SOE_ELM_NONE, 1u);
ok = (s_ring_head == 3u) && (s_ring_tail == 0u) && (s_drop_cnt == 0u);
for (i = 0u; i < (uint32_t)FF_FAKE_OP_N; i++) {
if (g_ff_obs.calls[i] != 0) {
ok = 0;
}
}
CHECK(ok && (s_lock_waits == 0) && (g_soe_err_count == 0) && (g_soe_info_count == 0),
"TC-SOESTORE-001 post 零 FatFs / 零卷锁 / 零日志(protectTask 栈预算)");
/* 002:墙钟未建立 -> IV + epoch 字段 0(不造假时标) */
reset_all();
s_clock_valid = 0;
soe_store_post((uint8_t)SOE_T_SAMPLE_BLOCK, 1u, 0xF0u);
soe_store_drain(); /* 环 -> 卷:不 drain 就还在 RAM 里 */
ok = read_rec(0u, &out);
CHECK(ok && (out.flags & SOE_FLG_IV) != 0u && (out.epoch_s == 0u)
&& (out.cp56[2] & 0x80u) != 0u && (out.value == 0xF0u),
"TC-SOESTORE-002 无墙钟 -> flags.IV + epoch=0 + CP56 IV 位(B9 口径)");
/* 003:seq 单调;丢弃的那条也占号(真机丢号 = 主机看到的号不连续是**如实**) */
reset_all();
for (i = 0u; i < 9u; i++) {
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
}
ok = (s_seq == 9u) && (s_drop_cnt == 1u);
CHECK(ok, "TC-SOESTORE-003 seq 单调递增,丢弃也占号(号不跳才是假象)");
/* 004:环满丢弃不阻塞、不崩、**不打日志**(留痕归 drain 侧) */
reset_all();
for (i = 0u; i < 9u; i++) {
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
}
CHECK((s_ring_head == 8u) && (s_drop_cnt == 1u)
&& (g_soe_err_count == 0) && (g_soe_info_count == 0),
"TC-SOESTORE-004 环满丢弃:只计数,post 侧零 DBG(栈尾链 +320B 教训)");
/* 005:内核运行 + 队列已建 -> 走队列,环不再写 */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); /* 先落一条,drain 会建队列 */
s_kernel_state = osKernelRunning;
soe_store_drain(); /* 建队列 + 落第 1 条 */
s_ring_head = 0u; /* 观察用:清环位 */
s_ring_tail = 0u;
soe_store_post((uint8_t)SOE_T_ELM_RETURN, 1u, 2u);
CHECK((s_ring_head == 0u) && (s_q_cnt == 1u) && (s_q_msgsz == SOE_REC_SIZE),
"TC-SOESTORE-005 队列已建 + 内核运行 -> 条目进队列、环不再写");
/* 006:队列满 -> 丢弃计数(与环满同一条可观测出口) */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
s_kernel_state = osKernelRunning;
soe_store_drain(); /* 建队列 */
(void)osMessageQueueReset(s_queue);
for (i = 0u; i < SOE_QUEUE_MAX + 2u; i++) {
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
}
CHECK((s_q_cnt == SOE_QUEUE_MAX) && (s_drop_cnt == 2u),
"TC-SOESTORE-006 队列满 -> 丢弃并计数(soe_store_dropped 同一出口)");
/* 007:计数饱和不回绕 */
reset_all();
s_drop_cnt = UINT32_MAX;
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
CHECK((s_drop_cnt == UINT32_MAX) && (soe_store_dropped() == UINT32_MAX),
"TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不回绕");
}
/* ===================== TC-SOESTORE-010..021:drain 落盘与故障注入 ===================== */
static void tc_drain_paths(void)
{
soe_rec_t out;
uint32_t i;
int ok;
int opens_before;
int writes_before;
printf("-- TC-SOESTORE-010..021 drain 落盘 / 故障注入 --\n");
/* 010:首次 drain 惰性建队列 + 1 条 64B 落盘且能解析回来 */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 3u, 0xABCDu);
soe_store_drain();
ok = (s_q_new_calls == 1) && (s_q_msgsz == SOE_REC_SIZE)
&& (ff_fake_size(SOE_PATH) == SOE_REC_SIZE);
CHECK(ok && read_rec(0u, &out) && (out.type == (uint8_t)SOE_T_ELM_ASSERT)
&& (out.elm_id == 3u) && (out.value == 0xABCDu) && (out.seq == 0u),
"TC-SOESTORE-010 惰性建队列 + 64B 定长落盘 + 记录可解回");
/* 011:打开模式必须带 FA_WRITE —— 陷阱一 */
reset_all();
soe_store_post((uint8_t)SOE_T_PLATE_OUT, SOE_ELM_NONE, 0u);
soe_store_drain();
ok = (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND));
/* 同一断言的"为什么必须带写位"保真对照:只给 FA_OPEN_APPEND 的句柄 f_write
* 必须拿 FR_DENIED——假件如果哪天放行,这条会红,整套陷阱一断言随之失效 */
{
static FIL probe;
static uint8_t payload[SOE_REC_SIZE];
UINT bw = 0u;
FRESULT fr2;
fr2 = f_open(&probe, SOE_PATH, FA_OPEN_APPEND);
{
FRESULT fr3 = f_write(&probe, payload, SOE_REC_SIZE, &bw);
CHECK(ok && (fr2 == FR_OK) && (fr3 == FR_DENIED) && (bw == 0u),
"TC-SOESTORE-011 模式必为 FA_WRITE|FA_OPEN_APPEND;缺写位 f_write 恒 FR_DENIED");
}
(void)f_close(&probe);
}
/* 012:多条 = 一条一次 open/write/close(本模块刻意没有批量合并)+ FIFO */
reset_all();
for (i = 0u; i < 4u; i++) {
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)(i + 1u), 100u + i);
}
soe_store_drain();
ok = (ff_fake_size(SOE_PATH) == 4u * SOE_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 4)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 4);
if (ok) {
for (i = 0u; i < 4u; i++) {
if (!read_rec(i, &out) || (out.seq != i) || (out.value != 100u + i)) {
ok = 0;
}
}
}
CHECK(ok, "TC-SOESTORE-012 逐条 open/write/close(无批量合并)+ FIFO 顺序正确");
/* 013:open/close 严格配对 */
reset_all();
soe_store_post((uint8_t)SOE_T_SAMPLE_RESUME, SOE_ELM_NONE, 0u);
soe_store_drain();
CHECK(g_ff_obs.calls[FF_FAKE_OP_CLOSE] == g_ff_obs.calls[FF_FAKE_OP_OPEN],
"TC-SOESTORE-013 open/close 配对(脏句柄不跨调用残留)");
/* 014:短写判失败 -> unlink 重建 -> 重试成功 -> 恰好 64 字节 */
reset_all();
ff_fake_inject_write_short(SOE_PATH, SOE_REC_SIZE - 1u, 1);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 7u, 0x555Au);
soe_store_drain();
CHECK((ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] >= 1)
&& (g_ff_obs.last_write_bw == SOE_REC_SIZE)
&& (g_soe_err_count == 0),
"TC-SOESTORE-014 FR_OK+短写被当失败处理(不校验字节数就会静默收下 63B)");
/* 015:连续**两轮** drain 都失败 -> 留痕只许 1 次(一轮 = 首写 + 自愈重试
* = 2 次 f_write,所以注入 4 次零字节短写才够撑两轮) */
reset_all();
ff_fake_inject_write_short(SOE_PATH, 0u, 4);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
soe_store_drain(); /* 第 1 轮失败:留痕 1 次,环不推进 */
ok = (g_soe_err_count == 1) && (s_ring_tail == 0u) && (s_fault_reported == 1u);
soe_store_drain(); /* 第 2 轮仍失败:不得再留痕 */
CHECK(ok && (g_soe_err_count == 1) && (s_ring_tail == 0u),
"TC-SOESTORE-015 写故障留痕去重(防串口与 selftest 被同一条故障刷屏)");
/* 015b:成功一次复位去重位,之后再失败才会重新留痕 */
/* 015b:失败(留痕1) -> 恢复(去重位复位) -> 再失败(必须留痕2)。
* 少了"成功复位"这一步,一次真机坏块就会让后续所有写故障永久静默。 */
reset_all();
ff_fake_inject_write_short(SOE_PATH, 0u, 2);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
soe_store_drain(); /* 失败 -> 留痕 1 */
soe_store_drain(); /* 注入耗尽 -> 成功 -> 复位 */
ok = (g_soe_err_count == 1) && (s_fault_reported == 0u) && (s_ring_tail == 1u);
ff_fake_inject_write_short(SOE_PATH, 0u, 2);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u);
soe_store_drain(); /* 又失败 -> 留痕 2 */
CHECK(ok && (g_soe_err_count == 2) && (s_fault_reported == 1u),
"TC-SOESTORE-015b 恢复后再失败必须重新留痕(漏复位 = 永久静默)");
/* 016:卷忙 -> -2:不碰 FatFs、不 release、环不推进 */
reset_all();
lock_busy_next(1);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
soe_store_drain();
ok = (s_lock_waits == 1) && (s_lock_releases == 0) && (s_lock_seq_err == 0);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
&& (s_ring_tail == 0u) && (g_soe_err_count == 0),
"TC-SOESTORE-016 卷忙 = 顺延:零 FatFs、零 release、不计故障(喂狗点纪律)");
/* 017:写失败时环不推进 tail -> 下轮重试、既不丢也不重复 */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 0x77u);
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
soe_store_drain(); /* open 失败 -> -1,tail 不动 */
ok = (s_ring_tail == 0u) && (ff_fake_size(SOE_PATH) == 0u ||
ff_fake_exists(SOE_PATH) == 0);
soe_store_drain(); /* 下轮成功 */
CHECK(ok && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
&& read_rec(0u, &out) && (out.elm_id == 5u),
"TC-SOESTORE-017 环条目写失败不推进 tail:不丢、下轮也不重复落");
/* 018:队列条目已出队 -> 卷忙只丢当前一条,剩余留在队列(现状不对称) */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
s_kernel_state = osKernelRunning;
soe_store_drain(); /* 建队列 */
(void)osMessageQueueReset(s_queue);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 10u, 10u);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 11u, 11u);
opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN]; /* 前面那次 drain 已开过盘 */
writes_before = g_ff_obs.calls[FF_FAKE_OP_WRITE];
lock_busy_next(1);
soe_store_drain();
CHECK((s_q_cnt == 1u)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == writes_before)
&& (s_lock_waits >= 1) && (s_lock_seq_err == 0) && (s_q_get_calls >= 2),
"TC-SOESTORE-018 队列路径卷忙:只丢已出队那一条、本轮零 FatFs(与环路径不对称)");
/* 019:容量满 -> unlink 重建 + INFO 留痕,新文件恰好 64 字节 */
reset_all();
CHECK(ff_fake_mkfile_size(SOE_PATH, (uint32_t)SOE_STORE_MAX * SOE_REC_SIZE) == 0,
"TC-SOESTORE-019 前置:铺 1024 条(64KB)假记录");
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 9u, 0x99u);
soe_store_drain();
CHECK((g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
&& (g_soe_info_count == 1) && read_rec(0u, &out) && (out.elm_id == 9u),
"TC-SOESTORE-019 满则删除重建:新文件只剩本次这一条");
/* 020:丢弃留痕按差值上报一次,不刷屏 */
reset_all();
for (i = 0u; i < SOE_RING_MAX + 3u; i++) {
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); /* 环 8 + 丢 3 */
}
soe_store_drain(); /* 差值上报 1 条 */
soe_store_drain(); /* 无新丢弃 -> 不再报 */
CHECK((s_drop_cnt == 3u) && (s_drop_logged == 3u) && (g_soe_err_count == 1),
"TC-SOESTORE-020 丢弃留痕按差值上报一次(defaultTask 侧才是日志安全区)");
/* 021:队列创建失败 -> 条目滞留环、下轮再试、不崩 */
/* 021:osMessageQueueNew 返回 NULL —— 生产代码里环在队列**之前**排空,
* 所以条目不丢;留痕一次,内核已运行时后续 post 仍走环(s_queue 为空),
* 下一轮再建队列。真机上 heap_4 见底就是这种形态。 */
reset_all();
s_kernel_state = osKernelRunning;
s_q_new_fail = 1;
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 4u, 4u);
soe_store_drain();
ok = (s_queue == NULL) && (g_soe_err_count == 1)
&& (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 5u); /* 队列没建成 -> 仍进环 */
CHECK(ok && (s_q_cnt == 0u) && (s_ring_head == 2u),
"TC-SOESTORE-021 队列创建失败:环路径照常落盘不丢条目 + 留痕一次");
s_q_new_fail = 0;
soe_store_drain();
CHECK((s_queue != NULL) && (s_ring_tail == 2u)
&& (ff_fake_size(SOE_PATH) == 2u * SOE_REC_SIZE) && (g_soe_err_count == 1),
"TC-SOESTORE-021b 堆恢复后队列补建、滞留条目全部落盘");
}
/* ===================== TC-SOESTORE-022..024:陷阱二 —— 名字/参数类错误码 ===================== */
static void tc_name_error_not_volume(void)
{
uint32_t i;
int ok;
printf("-- TC-SOESTORE-022..024 名字/参数类错误码严禁升级成卷操作 --\n");
/* 022:FR_INVALID_NAME 只让本条失败,**不得** unlink 别人、不得动目录 */
reset_all();
(void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 2);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
soe_store_drain(); /* open 失败 -> -1 */
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
&& (ff_fake_exists("/log") == 2)
&& (ff_fake_exists("/log/run.log") == 1)
&& (s_ring_tail == 0u) && (g_soe_err_count == 1);
CHECK(ok, "TC-SOESTORE-022 FR_INVALID_NAME -> 只留痕失败;不 unlink、目录与邻居文件不动");
/* 023:反复失败的自愈上限:每轮最多 unlink 一次,条目与卷都不被吞 */
reset_all();
(void)ff_fake_mkfile("/log/run.log", NULL, 0u);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u);
for (i = 0u; i < 5u; i++) {
ff_fake_inject_write_short(SOE_PATH, 0u, 2); /* 每轮两次短写都失败 */
soe_store_drain();
}
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(5 * 1))
&& (ff_fake_exists("/log") == 2) && (ff_fake_exists("/log/run.log") == 1)
&& (s_ring_tail == 0u);
ff_fake_inject_write_short(SOE_PATH, 0u, 0); /* 清掉不再需要的注入 */
(void)ok;
CHECK(ok, "TC-SOESTORE-023 长跑反复失败:unlink 有界(<=1/轮),绝不升级为整卷动作");
/* 024:假卷严格 8.3 校验的保真自检 —— 生产路径必过、点开头必被拒 */
reset_all();
{
static FIL probe;
FRESULT fr_ok;
FRESULT fr_dot;
FRESULT fr_colon;
fr_ok = f_open(&probe, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
(void)f_close(&probe);
fr_dot = f_open(&probe, "/log/.fschk", FA_WRITE | FA_CREATE_ALWAYS);
if (fr_dot == FR_OK) {
(void)f_close(&probe);
}
fr_colon = f_open(&probe, "1:/log/soe.log", FA_WRITE | FA_OPEN_APPEND);
if (fr_colon == FR_OK) {
(void)f_close(&probe);
}
CHECK((fr_ok == FR_OK)
&& ((fr_dot == FR_INVALID_NAME) || (fr_dot == FR_INVALID_DRIVE))
&& (fr_colon == FR_INVALID_DRIVE),
"TC-SOESTORE-024 假件保真:/log/soe.log 过 8.3,.fschk 与坏盘符被拒");
}
/* 生产路径本身合规的正向断言:改名成非 8.3 会让上面那条保真自检变红 */
reset_all();
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
soe_store_drain();
CHECK((strcmp(g_ff_obs.last_open_path, "/LOG/SOE.LOG") == 0)
&& (g_ff_obs.last_open_rc == (int)FR_OK),
"TC-SOESTORE-024b SOE_PATH 在严格 8.3 卷上确实打得开(改名=静默写失败的防线)");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== SOE Store Tests ===\n");
tc_post_side();
tc_drain_paths();
tc_name_error_not_volume();
printf("\n=== SOE Store Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+91
View File
@@ -0,0 +1,91 @@
/*
* 模块名称:主机侧单测桩 - cmsis_os.h(soe_store 套件专用)
* 模块功能:给 App/log/soe_store.c 一个主机可编译的 CMSIS-RTOS V2 **消息队列 +
* 内核状态**最小子集。
*
* ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。
* 真实 Middlewares/.../CMSIS_RTOS_V2/cmsis_os.h 第 126/127/155 行会拉
* FreeRTOS.h + task.h + cmsis_os2.h(整棵内核 + portmacro),PC 上编不动。
*
* 保真点(逐条对照真实头,签名一字不改;改动真实头即编译期报错):
* - osMessageQueueId_t = void * (cmsis_os2.h:207)
* - osMessageQueueAttr_t 六字段同序同型 (cmsis_os2.h:272~279)
* ——soe_store.c 用 .name 指定成员初始化,字段名也必须一致
* - osStatus_t osMessageQueuePut(id, const void *, uint8_t, uint32_t) (:689)
* - osStatus_t osMessageQueueGet (id, void *, uint8_t *, uint32_t) (:697)
* - osMessageQueueId_t osMessageQueueNew(uint32_t, uint32_t, const attr *)(:676)
* - osKernelState_t osKernelGetState(void) (cmsis_os2.h:298)
* - osOK = 0 / osErrorResource = -3 (cmsis_os2.h:177/180)
* - osKernelRunning = 2 (cmsis_os2.h:63)
* 本模块只用到内核状态的两个值,其余按真实枚举原样列出(少列会让"顺手改了
* 状态判定"这类漂移编不出来)。
*
* 队列行为由测试单元给**真 FIFO + 满/空语义**的强符号实现(不是恒返 osOK 的空壳):
* soe_store_post 的"队列满就丢弃并计数"、drain 的"取空即停"全靠这两条才测得出。
*
* 创建日期:2026-10-24
*/
#ifndef CMSIS_OS_H
#define CMSIS_OS_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 不透明句柄(真实 cmsis_os2.h:195~207 全是 void *) */
typedef void *osMessageQueueId_t;
typedef void *osThreadId_t;
typedef void *osTimerId_t;
typedef void *osEventFlagsId_t;
typedef void *osMutexId_t;
typedef void *osSemaphoreId_t;
/* 状态码(cmsis_os2.h:176~186) */
typedef enum {
osOK = 0,
osError = -1,
osErrorTimeout = -2,
osErrorResource = -3,
osErrorParameter = -4,
osErrorNoMemory = -5,
osErrorISR = -6
} osStatus_t;
/* 内核状态(cmsis_os2.h:60~68) */
typedef enum {
osKernelInactive = 0,
osKernelReady = 1,
osKernelRunning = 2,
osKernelLocked = 3,
osKernelSuspended = 4,
osKernelError = -1
} osKernelState_t;
/* 消息队列属性(cmsis_os2.h:272~279,字段名与顺序原样) */
typedef struct {
const char *name;
uint32_t attr_bits;
void *cb_mem;
uint32_t cb_size;
void *mq_mem;
uint32_t mq_size;
} osMessageQueueAttr_t;
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
const osMessageQueueAttr_t *attr);
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
uint8_t msg_prio, uint32_t timeout);
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
uint8_t *msg_prio, uint32_t timeout);
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id);
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id);
osKernelState_t osKernelGetState(void);
#ifdef __cplusplus
}
#endif
#endif /* CMSIS_OS_H */
+30
View File
@@ -0,0 +1,30 @@
/*
* 模块名称:主机侧单测桩 - dbg_log.h(soe_store 套件专用)
* 模块功能:DBG_* 换成计数,让"post 侧一条日志都不许打、drain 侧按差值只打一次"
* 成为可断言的行为。
*
* ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。
*
* 为什么这条计数在本模块**尤其**重要(App/log/soe_store.c 的函数头原话):
* soe_store_post() 由 protectTask 调用,该任务的静态最坏链 = 经 s_log_active_edge
* 的 console 尾链(872 B,B5 实测),post 里任何日志调用点都会再垫高一帧
* (stack_report 实测 +320 B → FAIL)。所以"post 零 DBG"不是风格问题,
* 是栈预算硬约束;真实宏展开成 snprintf + 192 B 行缓冲,只有把宏换成计数
* 才能在主机上把这条钉住。
*
* 创建日期:2026-10-24
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
/* 计数定义在 soe_store_test.c */
extern int g_soe_err_count;
extern int g_soe_info_count;
extern int g_soe_debug_count;
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_soe_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_soe_debug_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_soe_err_count++; } while (0)
#endif /* DBG_LOG_H */
+34
View File
@@ -0,0 +1,34 @@
/*
* 模块名称:主机侧单测桩 - main.h(soe_store 套件专用)
* 模块功能:截断 App/util/sys_clock.h -> Drivers/BSP/SD2506/sd2506.h -> main.h
* -> Inc/main.h -> stm32f4xx_hal.h 这条 include 链。
*
* ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。
* 真实 Inc/main.h 第一行就 #include "stm32f4xx_hal.h"(整棵 HAL + CMSIS),
* PC 上编不动;本桩与 test/host/sys_clock/stub/main.h 同一招(该套件已验证
* "真实 sys_clock.h + 真实 sd2506.h + 桩 main.h"这条组合可行),好处是
* **sys_clock.h / sd2506.h 保持真实**:sys_clock_get() 的签名一改即编译期报错,
* 不必像 test/host/time_sync 那样连 sys_clock.h 一起桩掉。
*
* 创建日期:2026-10-24
*/
#ifndef __MAIN_H
#define __MAIN_H
#include <stddef.h> /* size_t:真实 sys_clock.h 的 get_str 原型要用 */
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* HAL 毫秒时基(MCU 端 = TIM7 中断累加)。本模块不读它,声明在此只为让
* 真实 main.h 的消费者(sd2506.h / sys_clock.h)在 PC 上也拿得到同一符号。 */
uint32_t HAL_GetTick(void);
#ifdef __cplusplus
}
#endif
#endif /* __MAIN_H */