feat(固件): 标定值语义 setter + 卷锁 fail-open 自检留痕 + 控制台编号写法整理
三批同笔的理由:它们是同一份权威构建(Linker run 04:5x,Code=233,900 / RW=1,536)里的内容,中间态从未 单独编译过,拆开提任何一段都没有对应的构建凭证。按道分述如下。 **① 算法层(ui-maint):写值与置脏成对,不留可单独调用的置脏口**。新增 run_par_cal_k_set() / run_par_cal_b_set(),被拒时副本不动也不置脏(= 不承诺持久化);104 的 run_par_104_write() k/b 两支 同批改调 setter,函数内自己碰 s_dirty 的两处删净——**消除两份置脏逻辑才是本段的目的**。刻意**没有**新增 公开 run_par_mark_dirty():作者的理由我采纳并原文保留——「一旦存在就一定有人只调它」,与今天删掉的零消费 者接口是同一失效机理。另新增合法域唯一出口 run_par_db_range()、粗守上限 RUN_PAR_K_MAX_UV、毫→工程换算 单点 s_milli_to_eng(),并改正了头文件里「除以 1000 唯一在此」那条在加 db_range 后会变假陈述的说法。 **顺带满足 task-43 第①项**:adc_cal 落盘口的缺口由本段 setter 解决(写值必置脏),task-43 剩余项 = 可诊断 性与文档。 **② 日志层(log-maint):卷锁 fail-open 的 defaultTask 侧边沿留痕**。syslog_drain() 尾部紧跟既有 s_quiet_ drop 差值块之后读 vol_lock_get_stats(),水位新增才算一个 episode、每上电一条,投 SYSLOG_SELFTEST,文本 vollock failopen=%lu。三条硬约束都落了且**有牙**:失败调用点零改动(wdev.log 自己也要这把非递归卷锁); 不打错级、**不打 [ERR]**(断言里 g_sys_err_count == 0 就是机器判据,把「改成 DBG_ERROR」注入后四条用例 全红);锁层只自增、唯一写者消费。作者一度按我的要求准备造 obs_first_ms/obs_last_ms 字段,我撤了那条 要求——记录自带 ts 就是被观测时刻,**再造一个时间戳字段等于造第二个「看起来像发生时刻」的东西**。 成本 .bss +48 B、**零栈增量**(缓冲全静态),已由本笔权威构建的 after 表证实:9 条链全 PASS, iec104Task 1,464 / margin 1,096、defaultTask 3,040 与提交态逐项一致。 **③ 控制台与中断(log-maint,纯注释/文档)**:b623a406 之后的编号写法整理,把裸形态号改为可校验形式, 零代码行为改动。 实测:@build 0 错误 0 警告;run_par 67/67、sys_log_mcu 141/141、console_log 套件零回归 覆盖率只增不减 (sys_log_mcu.c 410/413 = 99.3%、函数 22/22);cppcheck / coding-standard / doc-links / bom / mojibake / nested-comment 全 0;编号五项式错误差值 0、不可判定绝对数 **1,156**(上限 1,166 未动,本笔没吃余量)。 台账随本笔回填:ram_baseline --write 与 mem-numbers --write 同批(RW_IRAM1 80,920 B 61.7% / RW_CCM 57,376 B 87.9% / ER_IROM1 260,004 B 24.8%)。
This commit is contained in:
+103
-24
@@ -77,10 +77,21 @@ static uint8_t s_dirty;
|
||||
|
||||
/* 内部函数实现区 */
|
||||
|
||||
/*
|
||||
* 函数功能:毫单位定点 → 工程量浮点(**本模块唯一换算点**)
|
||||
* 入口参数:milli - 毫单位 内 int32_t
|
||||
* 返 回 值:工程量 内 float
|
||||
* 限定条件:仅本文件内使用(消费侧一律走 run_par_deadband()/run_par_db_range(),
|
||||
* 不许各自再写一次 /1000——那才是"第二真源"最常见的样子)
|
||||
*/
|
||||
static float s_milli_to_eng(int32_t milli) {
|
||||
return (float)milli / 1000.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:死区点量程上限(毫单位)
|
||||
* 入口参数:idx - 0..8 uint8_t
|
||||
* 返 回 值:上限;idx 越界返 0 长(一切值皆拒) 内 int32_t
|
||||
* 返 回 值:上限;idx 越界返 0 内 int32_t
|
||||
*/
|
||||
static int32_t s_db_cap_milli(uint8_t idx) {
|
||||
if (idx < 4u) {
|
||||
@@ -156,7 +167,29 @@ float run_par_deadband(uint8_t idx) {
|
||||
if (idx >= RUN_PAR_DB_N) {
|
||||
return 0.0f;
|
||||
}
|
||||
return ((float)s_db.db[idx]) / 1000.0f; /* 毫→工程唯一换算点 */
|
||||
return s_milli_to_eng(s_db.db[idx]); /* 毫→工程:唯一换算点 = s_milli_to_eng */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取第 idx 路死区的合法域(工程量)——**合法域的唯一出口**
|
||||
* 入口参数:idx - 0..8 uint8_t;lo/hi - 输出下限/上限(工程量 V/A/Hz) float *
|
||||
* 返回值:0 - 已写出;-1 - idx 越界或指针为空(调用方不得回退成自造默认域) int
|
||||
* 限定条件:任意上下文(纯常量读,无锁、无 I/O)
|
||||
* 函数说明:为什么公开这个口:UI 整定页要给数字编辑器设 min/max,而这些上限的
|
||||
* 真源是本模块的 RUN_PAR_DB_MAX_*(电压 50V / 电流 1A / 频率 5Hz,
|
||||
* "额定 × 二档裕量")。页面里抄一份就是第二真源——判据「不新增第二份
|
||||
* 真源」最典型的复发形态是"顺手把常量写进界面代码"。因此本函数的
|
||||
* 消费者是装配层 App/app/ui_data_feed.c 的注入表实现,不是页面本身。
|
||||
* ⚠ 给的是**粗守域**(挡误写),不是工况域;真提交仍由 s_db_check
|
||||
* 同源守卫裁(104 选择段与 UI 编辑器共用同一批常量,不会漂移)。
|
||||
*/
|
||||
int run_par_db_range(uint8_t idx, float *lo, float *hi) {
|
||||
if ((idx >= RUN_PAR_DB_N) || (lo == NULL) || (hi == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
*lo = 0.0f;
|
||||
*hi = s_milli_to_eng(s_db_cap_milli(idx));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -198,19 +231,74 @@ int run_par_104_read(uint32_t addr, float *out) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址写(两段式;k/b 直通 adc_cal_set——**run_par 不存
|
||||
* 函数功能:写某路系数 k(µV/码)**并在同一函数内置脏**——整定的语义写入口
|
||||
* 入口参数:sem - 电气语义通道 adc_semantic_t;k_uv - 系数(µV/码,>0) float
|
||||
* 返回值:0 - RAM 副本已写 + 已置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:整定路径上下文(iec104Task 的 104 执行段;UI 经 ui_data_yt_ops_t 由
|
||||
* 装配层 App/app/ui_data_feed.c 转进来)。勿在 adcTask 调用
|
||||
* 函数说明:**为什么"写值 + 置脏"必须长在一个函数里**(lead 裁②,2026-10-27):
|
||||
* 置脏是持久化承诺,写值是 RAM 事实,两者拆开就会出现"屏上已改、盘上
|
||||
* 永不落、且没有任何错误返回"这种无人报错的状态——本仓今天已实测到它的
|
||||
* 既有形态(绕过本模块直调 adc_cal_set() 的写者不置脏,值永远不落盘,
|
||||
* 另立 task-43 处置)。所以这里不提供单独的 mark_dirty 公开口:它一旦
|
||||
* 存在就一定有人只调它(同"零消费者的状态查询口"那条教训)。
|
||||
* 零点 b 走 RMW 取**当前生效值**(经合法带哨兵,天然满足再写校验)。
|
||||
* 粗守(0 < k ≤ RUN_PAR_K_MAX_UV)只为挡误写,真带在 adc_cal 执行段裁:
|
||||
* 两层各说各的,页面上"合法域内仍被拒(ERR)"就是执行段拒的,不伪装。
|
||||
*/
|
||||
int run_par_cal_k_set(adc_semantic_t sem, float k_uv) {
|
||||
int32_t k_q16;
|
||||
int32_t b_codes;
|
||||
|
||||
/* 通道号不在这里判:adc_cal_set 自带合法性与合法带裁决(越界一律 -1),
|
||||
* 本层再判一次就是第二真源。粗守只挡误写:0 < k ≤ RUN_PAR_K_MAX_UV */
|
||||
if (!isfinite(k_uv) || !(k_uv > 0.0f) || (k_uv > RUN_PAR_K_MAX_UV)) {
|
||||
return -1;
|
||||
}
|
||||
k_q16 = (int32_t)lrintf(k_uv * 65536.0f);
|
||||
b_codes = (int32_t)lrintf(adc_cal_b_codes(sem));
|
||||
if (adc_cal_set(sem, k_q16, b_codes) != 0) {
|
||||
return -1; /* 执行段拒:副本未动,脏位也不置 = 不承诺任何持久化 */
|
||||
}
|
||||
s_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写某路零点偏移 b(码值)并置脏(与 k 同款语义,粗守 ±1e6 码)
|
||||
* 入口参数:sem - 电气语义通道 adc_semantic_t;b_codes - 零点(码值) int32_t
|
||||
* 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:同 run_par_cal_k_set()
|
||||
* 函数说明:k 侧 RMW 用当前生效值;±8192 真带由 adc_cal 执行段裁(粗守目的是
|
||||
* 挡误写不是限工况)。本函数的消费者 = 104 的 0x8048~ 分支(改造后),
|
||||
* 不是"为将来预留"的口
|
||||
*/
|
||||
int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes) {
|
||||
int32_t k_q16;
|
||||
|
||||
if (((float)b_codes < -1.0e6f) || ((float)b_codes > 1.0e6f)) {
|
||||
return -1;
|
||||
}
|
||||
k_q16 = (int32_t)lrintf(adc_cal_k_uv(sem) * 65536.0f);
|
||||
if (adc_cal_set(sem, k_q16, b_codes) != 0) {
|
||||
return -1;
|
||||
}
|
||||
s_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址写(两段式;k/b 经本模块语义 setter——**run_par 不存
|
||||
* 系数**,系数唯一真源 = [adc_cal]/[adc_cal_ofs],lead 裁②a)
|
||||
* 返 回 值:0 接受 / -1 拒绝 / -2 非本段(调用方落 prot_set) int
|
||||
* 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b 的 adc_cal 合法带
|
||||
* 由执行段裁决——IEC104 允许执行段 PN,两段结果可不一致,注释在此
|
||||
* 明说防"选择过了怎么还 PN"的现场困惑);k/b 单侧写 RMW 用**当前
|
||||
* 生效值**做另一侧(生效值经合法带哨兵,天然满足再写校验)。
|
||||
* 明说防"选择过了怎么还 PN"的现场困惑)。k/b 两支自 2026-10-27 起一律
|
||||
* 走 run_par_cal_k_set() / run_par_cal_b_set():写值与置脏在同一个函数
|
||||
* 里发生,本函数不再自己碰 s_dirty(lead 裁②:消除两份置脏逻辑)。
|
||||
*/
|
||||
int run_par_104_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint8_t slot; /* 每个命中分支先赋值再使用(无未init读路径) */
|
||||
int32_t k_q16;
|
||||
int32_t b_codes;
|
||||
int rc;
|
||||
|
||||
if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) {
|
||||
int32_t milli;
|
||||
@@ -234,29 +322,20 @@ int run_par_104_write(uint32_t addr, float v, uint8_t select) {
|
||||
if (select != 0u) {
|
||||
return 0; /* 粗守过了 ≠ 执行必过:合法带裁决在执行段(见函数头) */
|
||||
}
|
||||
k_q16 = (int32_t)lrintf(v * 65536.0f);
|
||||
b_codes = (int32_t)lrintf(adc_cal_b_codes((adc_semantic_t)slot));
|
||||
rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes);
|
||||
if (rc == 0) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return rc;
|
||||
/* 写值 + 置脏收进语义 setter(粗守 0<k≤上限 也在里面,与本支
|
||||
* 原先那条逐字同值守卫一致,行为不变)*/
|
||||
return run_par_cal_k_set((adc_semantic_t)slot, v);
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) {
|
||||
slot = (uint8_t)(addr - RUN_PAR_IOA_B0);
|
||||
if (!isfinite(v) || (v < -1000000.0f) || (v > 1000000.0f)) {
|
||||
return -1; /* 粗守 ±1e6 码;±8192 真带由 adc_cal 执行段裁 */
|
||||
if (!isfinite(v)) {
|
||||
return -1; /* NaN/Inf 不得进 lrintf(该转换是 UB) */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0;
|
||||
}
|
||||
b_codes = (int32_t)lrintf(v);
|
||||
k_q16 = (int32_t)lrintf(adc_cal_k_uv((adc_semantic_t)slot) * 65536.0f);
|
||||
rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes);
|
||||
if (rc == 0) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return rc;
|
||||
/* 同款语义 setter:粗守 ±1e6 码在 setter 内,±8192 真带由 adc_cal 执行段裁 */
|
||||
return run_par_cal_b_set((adc_semantic_t)slot, (int32_t)lrintf(v));
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
+51
-1
@@ -33,6 +33,14 @@
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 地址映射 + 直通
|
||||
* adc_cal + B7 式脏标志落盘请求
|
||||
* v1.1 2026-10-27 DTU 项目组 道F task-7(遥测整定两页)配套:新增语义
|
||||
* setter run_par_cal_k_set / run_par_cal_b_set
|
||||
* (写值 + 置脏成对;104 的 k/b 两支同批改调它,
|
||||
* 消除两份置脏逻辑)+ 合法域唯一出口
|
||||
* run_par_db_range() + 系数粗守上限
|
||||
* RUN_PAR_K_MAX_UV;毫→工程换算收进模块内单点
|
||||
* s_milli_to_eng()(顺带把 task-43 第①项所需的
|
||||
* "写值必置脏"这一半满足掉)
|
||||
*/
|
||||
|
||||
#ifndef RUN_PAR_H
|
||||
@@ -40,6 +48,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_cal.h" /* adc_semantic_t:公开语义 setter 的形参类型(同层 algo→algo,方向不逆)*/
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -48,6 +57,11 @@ extern "C" {
|
||||
#define RUN_PAR_DB_N 9u
|
||||
#define RUN_PAR_CH_N 8u
|
||||
|
||||
/* 系数 k 的**粗守**上限(µV/码):只为挡误写(如把 mV 当 µV 填),不是工况域;
|
||||
* 真合法带在 adc_cal 执行段裁(µV/码 ×[0.5,2.0]),两层各说各的、互不掩盖。
|
||||
* 公开它是因为 UI 整定页要给编辑器设上限——上限在这里,就不许页面再抄一份。*/
|
||||
#define RUN_PAR_K_MAX_UV 1.0e9f
|
||||
|
||||
/*
|
||||
* 函数功能:运行参数初始化——副本置工厂默认 + 注册 [run_par] 段进 cfg 引擎
|
||||
* 入口参数:无
|
||||
@@ -64,7 +78,9 @@ void run_par_init(void);
|
||||
* 入口参数:idx - 0..7 = 语义通道序(UA..IN),8 = 频率 uint8_t
|
||||
* 返 回 值:死区 V / A / Hz;idx 越界返 0.0(调用方按"无死区"保守处理) float
|
||||
* 限定条件:任意上下文(int32 单字读原子;104 COV 每拍调用)
|
||||
* 函数说明:毫单位 → 浮点的换算点(除以 1000 唯一在此,勿在消费侧另写)。
|
||||
* 函数说明:毫单位 → 浮点的换算在本模块内也只有一处(run_par.c 的
|
||||
* s_milli_to_eng(),run_par_deadband 与 run_par_db_range 共用);
|
||||
* 消费侧一律取本模块给的工程量,不要再各自除 1000。
|
||||
*/
|
||||
float run_par_deadband(uint8_t idx);
|
||||
|
||||
@@ -105,6 +121,40 @@ int run_par_104_read(uint32_t addr, float *out);
|
||||
*/
|
||||
int run_par_104_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
/*
|
||||
* 函数功能:写某路系数 k(µV/码)**并在同一函数内置脏**——整定的语义写入口
|
||||
* 入口参数:sem - 电气语义通道(adc_semantic_t 枚举序) adc_semantic_t;
|
||||
* k_uv - 系数(µV/码,须 >0 且 <= RUN_PAR_K_MAX_UV) float
|
||||
* 返回值:0 - RAM 副本已写且已置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:整定路径上下文(iec104Task 的 C_SE 执行段;UI 侧由装配层
|
||||
* App/app/ui_data_feed.c 经 ui_data_yt_ops_t 转进来)。勿在 adcTask 调用
|
||||
* 函数说明:**写值与置脏成对,不许拆开**(lead 裁②):拆开就等于"屏上已改、
|
||||
* 盘上永不落、且没有任何错误返回"。因此本模块**不提供**公开的
|
||||
* mark_dirty——单独调它等于空承诺;零点 b 由本函数 RMW 取当前生效值。
|
||||
*/
|
||||
int run_par_cal_k_set(adc_semantic_t sem, float k_uv);
|
||||
|
||||
/*
|
||||
* 函数功能:写某路零点偏移 b(码值)并置脏(与 k 同款语义,粗守 ±1e6 码)
|
||||
* 入口参数:sem - 电气语义通道 adc_semantic_t;b_codes - 零点(码值) int32_t
|
||||
* 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:同 run_par_cal_k_set()
|
||||
* 函数说明:k 侧 RMW 取当前生效值;±8192 真带由 adc_cal 执行段裁。消费者 =
|
||||
* 104 的 0x8048~ 分支(改造后走这里),不是为将来预留的口
|
||||
*/
|
||||
int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes);
|
||||
|
||||
/*
|
||||
* 函数功能:取第 idx 路死区的合法域(工程量)——**合法域的唯一出口**
|
||||
* 入口参数:idx - 0..8 uint8_t;lo/hi - 输出下限/上限(V/A/Hz) float *
|
||||
* 返回值:0 - 已写出;-1 - idx 越界或指针空(调用方不得回退成自造默认域) int
|
||||
* 限定条件:任意上下文(纯常量读)
|
||||
* 函数说明:存在的唯一理由:UI 整定页要给编辑器设 min/max,而这些上限的真源是
|
||||
* 本模块的 RUN_PAR_DB_MAX_*;页面抄一份就是第二真源。给的是**粗守域**,
|
||||
* 真提交仍由与 104 选择段同源的守卫裁,两条路不会漂移到两套数字。
|
||||
*/
|
||||
int run_par_db_range(uint8_t idx, float *lo, float *hi);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘脏标志探查(非消费)
|
||||
* 入口参数:无
|
||||
|
||||
@@ -168,7 +168,7 @@ static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护
|
||||
* 窗口可能被更高优先级上下文再次进入,共享量会被它改写、退出时就恢复错了。
|
||||
* ⚠ 只允许包住"几条指令"的量级,**绝不允许**包住串口发送本身:一行 90 B @115200
|
||||
* 约 7.8 ms,把它关进中断等于给启动窗口塞 7.8 ms 中断延迟与最坏喂狗抖动
|
||||
* (红线与机器判据见 con_tx_bounded 函数头与主机单测 TC-CONLOG-015b/177)。
|
||||
* (红线与机器判据见 con_tx_bounded 函数头与主机单测 TC-CONLOG-015b 与 TC-CONLOG-177)。
|
||||
*/
|
||||
#define CON_CRIT_ENTER(pm_var) do { (pm_var) = __get_PRIMASK(); __disable_irq(); } while (0)
|
||||
#define CON_CRIT_EXIT(pm_var) do { __set_PRIMASK(pm_var); } while (0)
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.8 2026-10-27 DTU 项目组 道G task-40:syslog_drain() 尾部按**边沿**补投一条
|
||||
* 卷锁 fail-open(零互斥那一态)自检记录。锁层只自增计数不产日志
|
||||
* (唯一能写的通道自己也要这把**非递归**卷锁),留痕必须由
|
||||
* 唯一写者做;一个 episode(= 一次上电)只投一条,不打
|
||||
* DBG 错级、不打 [ERR]([ERR] 是"设备出故障"的唯一信号
|
||||
* 通道,塞进可观测性事件就变成第二个信号源——与 task-23
|
||||
* "日志让路不计 [ERR]"同一根判据)。
|
||||
* 成本:.bss +48 B(3 枚计数/标志 + 静态快照 + 静态行缓冲),
|
||||
* **零栈增量**(缓冲全静态;前后两表见 task-40 交付)。
|
||||
* v1.7 2026-10-26 王建锋 「记录 5 页」的数据面(task-10):syslog_id_t 尾部追加
|
||||
* SYSLOG_ACCIDENT / ALARM / START / OPER / YK 五枚,本文件
|
||||
* s_paths[]/s_caps[] 同步到七项(**必须与枚举同序**,越界
|
||||
@@ -162,6 +171,33 @@ static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃
|
||||
static volatile uint32_t s_quiet_drop; /* quiet 通道累计丢弃条数 */
|
||||
static uint32_t s_quiet_reported; /* drain 侧已上报水位(唯一写者私有) */
|
||||
|
||||
/* 卷锁 fail-open(零互斥那一态)的边沿留痕(道G task-40,2026-10-27)
|
||||
* ------------------------------------------------------------------
|
||||
* 锁层只**自增计数**、绝不产日志:唯一能把事写下来的通道(/log/wdev.log 等)自己也要
|
||||
* 取这把卷锁,而卷锁**非递归**——被卷忙挡住时既写不出东西,也不许在失败分支里再去取
|
||||
* 同一把锁(App/fs/vol_lock.h「可观测性」段 + task-17 查出的事实)。所以留痕由唯一写者
|
||||
* (defaultTask)在这里按**边沿**补投,投的是自检记录。
|
||||
* ⚠ episode = 一次上电,只投一条。fail-open 的成因是"互斥体从未创建成功"(vol_lock.c
|
||||
* 的那一支),一旦成立,后面每一次取锁都零互斥;若"计数每前进一次报一条"
|
||||
* 就等于每轮 drain 一条记录 + 一次 NAND 写——诊断自己变成负载,正是上面 s_quiet_drop
|
||||
* 那条纪律要避免的事(同款先例另有 App/fs/vol_idle_sync.c 的 s_vol_sync_fail_latch)。
|
||||
* ⚠ **不打 DBG 错级、不打 [ERR]**:[ERR] 在本仓是"设备出故障"的唯一信号通道(@test 的
|
||||
* Forbidden 标记也依赖它,四个 Fault 的崩溃日志都以 [ERR] 开头)。把一个可观测性事件
|
||||
* 塞进 [ERR],它就同时不再是故障、也不再是禁令,两边一起失去意义——与 task-23 里
|
||||
* "日志让路不计 [ERR]"是同一根判据。机器判据 = TC-SYSLOG-137 里的 g_sys_err_count == 0。
|
||||
* 时刻:本模块在这里**不读 tick**(读一次 tick 就给 defaultTask 的 drain 最坏链多挂一个
|
||||
* callee,而这条链的余量我们一直在省着花)。记录自带的 ts 字段就是"**被观测到的时刻**"
|
||||
* ——不是发生时刻,粒度 = 一轮 drain;这正是 vol_lock.h 第②条告诫要求消费方自己说清的
|
||||
* 那件事,所以这里不另造 obs_first_ms/obs_last_ms 字段(那只会造出第二个"看起来像发生
|
||||
* 时刻"的东西)。要"何时开始忙"的精确窗口,请读 vol_lock_get_stats() 的现值。
|
||||
* 成本:3 枚计数/标志 + 一份静态快照 + 一行静态文本 ≈ 48 B .bss,**零栈增量**
|
||||
* (缓冲全静态,照 App/util/task_prof.c 的 s_prof_line 先例;前后 stack_report 两表
|
||||
* 见 task-40 交付)。 */
|
||||
static volatile uint32_t s_vl_failopen_seen; /* drain 侧上次读到的 fail-open 计数 */
|
||||
static uint8_t s_vl_reported; /* 本 boot 已留痕(1 = 本 episode 不再投) */
|
||||
static vol_lock_stats_t s_vl_now; /* 静态快照,不走调用方栈 */
|
||||
static char s_vl_note[32]; /* 记录文本(落盘按 SYSLOG_DESC_MAX=28 截断) */
|
||||
|
||||
/* 日志读写放大收口(2026-10-15,读侧):8 条滑动读窗口 + 写者代数。
|
||||
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
|
||||
* 预算余量见 docs/内存管理/RAM预算台账.md)。s_rd_gen 由写者自增、读者比对,
|
||||
@@ -843,6 +879,22 @@ void syslog_drain(void) {
|
||||
s_quiet_reported = s_quiet_drop;
|
||||
DBG_ERROR("quiet drop total=%lu", (unsigned long)s_quiet_drop);
|
||||
}
|
||||
/* 卷锁 fail-open(零互斥那一态)边沿补投一条自检记录(约束与理由全在上面那组静态量旁边)。
|
||||
* vol_lock_get_stats() 不取锁、不关中断、不阻塞:卷被长期占住时正是最需要读它的
|
||||
* 时刻,观测口若再去抢同一把锁,就把观测做成了故障的一部分。
|
||||
* 必须排在 s_clear_service() 之前吗?不必——两者无先后关系(清除的是记录文件,
|
||||
* 本条是**新增**一条自检记录);但必须排在下面 soe_store_drain() 之前,因为本函数
|
||||
* 这条支路不持卷锁,而 soe_store_drain() 自己 try 锁(沿用本函数既有纪律)。 */
|
||||
vol_lock_get_stats(&s_vl_now);
|
||||
if (s_vl_now.failopen != s_vl_failopen_seen) {
|
||||
s_vl_failopen_seen = s_vl_now.failopen; /* 水位每轮都追:别让下一轮误判"新增" */
|
||||
if (s_vl_reported == 0U) {
|
||||
s_vl_reported = 1U; /* 一个 episode 一条(= 一次上电) */
|
||||
(void)snprintf(s_vl_note, sizeof(s_vl_note), "vollock failopen=%lu",
|
||||
(unsigned long)s_vl_failopen_seen);
|
||||
syslog_post(SYSLOG_SELFTEST, s_vl_note);
|
||||
}
|
||||
}
|
||||
/* 「清除记录」的落地半边——必须在上面的条目排空**之后**(顺序是判据的一部分:
|
||||
* 先删后写会把本轮刚排空的旧记录又落回去,count 不归零 = 没实际效果的按钮),
|
||||
* 也在 soe_store_drain() 之前:本函数自带卷锁 try,绝不在此持锁状态下调用。
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ static void hard_fault_puthex(uint32_t v)
|
||||
* 残余风险(照实登记,别当成已消除):
|
||||
* - **极早期崩溃仍然静默**:若在 MX_USART1_UART_Init() 之前进 Fault,本口的
|
||||
* `huart1.Instance == NULL` 短路会直接返回、一个字节都不发(换来的是"绝不解引用
|
||||
* 地址 0",见 hard_fault_write ③;该短路有机器判据 TC-CONLOG-100/102)。旧写法在
|
||||
* 地址 0",见 hard_fault_write ③;该短路有机器判据 TC-CONLOG-100 与 TC-CONLOG-102)。旧写法在
|
||||
* 同样场景下也不发(gState==RESET 不等于 READY ⇒ HAL_BUSY),**并没有因为本次改动
|
||||
* 变差**,但也别写成"早期崩溃现在能看到"。
|
||||
* - **ORE/NE/FE 脏标志本身没被消除**:本笔只是不再经由 HAL 状态机,从而消除了
|
||||
|
||||
@@ -242,7 +242,7 @@ sequenceDiagram
|
||||
**验证设计(崩溃路径,两腿分工,2026-10-26 台架更正后定稿)**:这条路径不能只靠静态论证(本仓最贵的教训就是「0 警告不代表安全」),但**两腿各证各的,别互相顶包**。
|
||||
|
||||
**腿 A — 主机侧套件承担全部关于 IPSR 与异常态整行性的主张**(已落地、可重复、确定性证据):
|
||||
- 假 USART + `con_ipsr` + 「每个上线字节的临界区深度水位」+ 一次性注入把手(在第 N 个字节上线后跑第二个生产者),把「Fault 上下文照发、不等、不丢、不越权归还」做成必然失败的断言:`TC-CONLOG-175` / `TC-CONLOG-175b`(IPSR 直发且不动别人的凭证)、`TC-CONLOG-174` / `TC-CONLOG-174b`(一次独占内多段连发不自我丢弃、`tx_end` 后别人能接手)、`TC-CONLOG-015` / `TC-CONLOG-015b` / `TC-CONLOG-177`(发送全程临界区深度恒 0,即"绝不把整行包进关中断区"这条硬红线)、`TC-CONLOG-170..172`/`178`(两个生产者并发时行内不得出现别人的字节;`TC-CONLOG-178` 直接复演台架那行原文)。
|
||||
- 假 USART + `con_ipsr` + 「每个上线字节的临界区深度水位」+ 一次性注入把手(在第 N 个字节上线后跑第二个生产者),把「Fault 上下文照发、不等、不丢、不越权归还」做成必然失败的断言:`TC-CONLOG-175` / `TC-CONLOG-175b`(IPSR 直发且不动别人的凭证)、`TC-CONLOG-174` / `TC-CONLOG-174b`(一次独占内多段连发不自我丢弃、`tx_end` 后别人能接手)、`TC-CONLOG-015` / `TC-CONLOG-015b` / `TC-CONLOG-177`(发送全程临界区深度恒 0,即"绝不把整行包进关中断区"这条硬红线)、`TC-CONLOG-170 至 TC-CONLOG-172`/`178`(两个生产者并发时行内不得出现别人的字节;`TC-CONLOG-178` 直接复演台架那行原文)。
|
||||
- ⚠ **「硬件上多跑几次启动照不出 IPSR」**——产品构建正常启动根本不进异常路径,跑 100 次也只是重复证明"没崩溃"。任何人想推翻或确认本节关于异常态的说法,请改主机侧套件,不要去台架上碰运气。
|
||||
|
||||
**腿 B — 实机腿:本腿含故意复位(不是常规回归)**
|
||||
@@ -306,7 +306,7 @@ sequenceDiagram
|
||||
|------|------|---------|---------|
|
||||
| 缓冲满(日志突发 / 写者卡滞) | 生产者等 200ms 后丢行 | `s_con_drop` 累计 → 写者刷 `[ERR] CONSOLE dropped N lines`(@test FAIL,永不静默) | — |
|
||||
| 写者未注册(内核前) | 单线程直发 | — | 无(无并发) |
|
||||
| 未注册期其他任务直发 | **已消除**(2026-10-26):整行独占,抢不到就整行入环延后发(§4.4.1) | 让路不计数;只有"抢不到且环也满"才进 `s_con_drop` → `[ERR] CONSOLE dropped N lines` | 残余 = 让路行在流里位置后移(内容与直发逐字节相同);主机单测 `TC-CONLOG-170..179` 钉死 |
|
||||
| 未注册期其他任务直发 | **已消除**(2026-10-26):整行独占,抢不到就整行入环延后发(§4.4.1) | 让路不计数;只有"抢不到且环也满"才进 `s_con_drop` → `[ERR] CONSOLE dropped N lines` | 残余 = 让路行在流里位置后移(内容与直发逐字节相同);主机单测 `TC-CONLOG-170 至 TC-CONLOG-179` 钉死 |
|
||||
| 崩溃上报(四个 Fault)原走独立 HAL 发送口 | **已归一**(2026-10-26 task-26):`hard_fault_report` 的 14 段裸发改走 `dbg_console_write_raw`(整行独占**不**用在这里——异常态空转,见 §4.4.1);ORE 快速失败(= 证据整条蒸发)与 `HAL_GetTick`/`HAL_Delay` 的 tick 依赖随之消失 | 崩溃行 `[ERR] <TAG> PC=… CFSR=…` 现在是单写者输出;实机验证设计见 §4.4.1 末(待台架窗口) | 极早期(`MX_USART1_UART_Init()` 之前)崩溃仍完全静默(NULL 短路);ORE 脏标志本身未消除(§12) |
|
||||
| ISR 直发撞写者发送中 | 与写者发送竞争 → 交错/丢失,不挂死(`IPSR != 0` 一律放行,§4.4.1 规则①);异常态没有第二写者(Fault 优先级 -1 + 任务全冻结),残留 ≤1 个移位寄存器字节且落在行**前** | 未计数 | ISR 日志频率极低,best-effort 语义 |
|
||||
| 内核状态损坏(栈溢出 / 堆耗尽) | 钩子裸发固定文本 → 复位(栈溢出)/ 仅告警(堆) | `[ERR] STACK OVERFLOW task=…` / `[ERR] MALLOC FAILED …`;任务名范围校验 + 本地拷贝,TCB 指针不入 UART | — |
|
||||
@@ -360,8 +360,8 @@ sequenceDiagram
|
||||
| 2026-10-02 | **round 8b 加固**:发送超时判据改用 DWT `CYCCNT`(时间源无关)。原 `HAL_UART_Transmit` 的 `HAL_GetTick() - Tickstart` 依赖 TIM7 的 `uwTick`,TIM7 一旦停摆超时永不成立 → 写者死等 TXE → 唯一喂狗者停摆 → IWDG 复位。裸发送与缓冲满等待两处均换成 DWT 周期计数硬上界(DWT 使能失败时退回旧 HAL 行为);另加 `huart1.Instance == NULL` 短路,防早期/断言路径解引用 NULL |
|
||||
| 2026-10-10 | **新增 `dbg_console_writer_ready()` + 未注册期纪律固化**:裸发为寄存器级轮询(无 `gState`)→ 未注册期多任务并发**逐字节交错**(台架 `adcTask` 首帧日志与 `[NAND_FTL]` 行交错实证)。新增 `dbg_console_writer_ready()` 供周期任务门控;`adcTask` 首帧/心跳日志据此门控(§3.4 / §4.4) |
|
||||
| 2026-10-25 | **N-2 × S8 合并评估收口(评估任务,零代码)**:S8 生产者侧判定已达成、TX IT/DMA 维持否决并绑定 S-3 复议条件、N-2 收敛到 V5 复跑双零判据——全文见文末附录 |
|
||||
| 2026-10-26 | **崩溃上报归一 + 多段裸发收口(道G task-26,接 task-23 第二段)**:`Src/stm32f4xx_it.c hard_fault_report`(四个 Fault 共用,14 段裸发)原**自行调用 `HAL_UART_Transmit`** ⇒ 绕过本模块行级独占的**第三个写者**,并带两条已实证失效:HAL 超时依赖 `HAL_GetTick()`(TIM7 停摆永不成立)与僵尸通道下恒快速失败(**关键 `[ERR]` 证据整条蒸发**)。现改 `dbg_console_write_raw`(**不包** `tx_begin()/tx_end()`,理由见 §4.4.1);`hard_fault_report` 与栈溢出钩子输出后的 `HAL_Delay(200)` 一并删除(本口已等 TC;少一条 tick 依赖,避免卡在复位之前)。同类收口只落在任务态:`App/util/task_watchdog.c` STALL 行 2 段包进一次独占;`Src/freertos.c` 栈溢出钩子(3 段)与断言钩子(5 段)以及 `hard_fault_report`(14 段)**不包**(异常态空转 + 代价 ×9 条任务链,见 §4.4.1)。**语义**:异常上下文 `tx_begin()` 放行不持有,故障行整行性靠 Fault 优先级与任务冻结,残留 ≤1 个移位寄存器字节(落在行前,不影响 `[ERR]` grep)。**未消除**:ORE/NE/FE 脏标志、极早期(串口未初始化)崩溃静默。主机侧复用既有判据 `TC-CONLOG-100/102`(NULL 短路)与 `TC-CONLOG-174`(一次独占内多段连发不自我丢弃),未新增用例;实机验证设计见 §4.4.1 末。**行序口径**:让路行入环后位置后移,台架 5/5 观察到 `[NAND_FTL]` 稳定排在 `[PROT]` 前一行(旧缺陷形态相反)⇒ 行序与时间戳先后**都不再是判据**,只能按整行逐字节锚定(§4.4.1) |
|
||||
| 2026-10-26 | **O1 第二个实例 → 未注册期裸发加「行级单一所有者」(道G task-23)**:台架同镜像两次"仅复位"启动,`[PROT]` 未标定闭锁边沿行内分别多出 `[20`(3 字节)与 `[`(1 字节),而紧随其后的 `[NAND_FTL] Initializing NAND FTL...` 行恰好缺同样那几个字节的**行首**(字节账与三次启动命中两次的频率见 §4.4.1)→ 判据"framing 错不会让行变长"锁定为**两个写者并发裸发**,即 O1 的第二个实例(第一个是 2026-10-12 B6)。修法 = 非阻塞行所有权(`con_tx_try_claim`/`con_tx_release`,只在几条指令的临界区里 test-and-set;发送全程**不关中断**)+ 让路整行原文入环延后发(不丢、不补前缀、不重打时间戳)+ drain 每轮先取凭证(堵掉"attach 撞上别人半行")+ `[ERR]` 只在"抢不到且环也满"时红。新增 `dbg_console_tx_begin/tx_end`(多段裸发收口,调用点改造 → task-26)。主机单测 127 → **152** 条(M 族 `TC-CONLOG-170..179`,含逐字节相同判据),四处生产码注入反证各自转红(取消独占 11 红 / 让路改静默丢 5 红 / 非属主可归还 1 红 / 把发送包进临界区 3 红)。§3.4 / §4.4 / §7 / §13-O1 同步 |
|
||||
| 2026-10-26 | **崩溃上报归一 + 多段裸发收口(道G task-26,接 task-23 第二段)**:`Src/stm32f4xx_it.c hard_fault_report`(四个 Fault 共用,14 段裸发)原**自行调用 `HAL_UART_Transmit`** ⇒ 绕过本模块行级独占的**第三个写者**,并带两条已实证失效:HAL 超时依赖 `HAL_GetTick()`(TIM7 停摆永不成立)与僵尸通道下恒快速失败(**关键 `[ERR]` 证据整条蒸发**)。现改 `dbg_console_write_raw`(**不包** `tx_begin()/tx_end()`,理由见 §4.4.1);`hard_fault_report` 与栈溢出钩子输出后的 `HAL_Delay(200)` 一并删除(本口已等 TC;少一条 tick 依赖,避免卡在复位之前)。同类收口只落在任务态:`App/util/task_watchdog.c` STALL 行 2 段包进一次独占;`Src/freertos.c` 栈溢出钩子(3 段)与断言钩子(5 段)以及 `hard_fault_report`(14 段)**不包**(异常态空转 + 代价 ×9 条任务链,见 §4.4.1)。**语义**:异常上下文 `tx_begin()` 放行不持有,故障行整行性靠 Fault 优先级与任务冻结,残留 ≤1 个移位寄存器字节(落在行前,不影响 `[ERR]` grep)。**未消除**:ORE/NE/FE 脏标志、极早期(串口未初始化)崩溃静默。主机侧复用既有判据 `TC-CONLOG-100 与 TC-CONLOG-102`(NULL 短路)与 `TC-CONLOG-174`(一次独占内多段连发不自我丢弃),未新增用例;实机验证设计见 §4.4.1 末。**行序口径**:让路行入环后位置后移,台架 5/5 观察到 `[NAND_FTL]` 稳定排在 `[PROT]` 前一行(旧缺陷形态相反)⇒ 行序与时间戳先后**都不再是判据**,只能按整行逐字节锚定(§4.4.1) |
|
||||
| 2026-10-26 | **O1 第二个实例 → 未注册期裸发加「行级单一所有者」(道G task-23)**:台架同镜像两次"仅复位"启动,`[PROT]` 未标定闭锁边沿行内分别多出 `[20`(3 字节)与 `[`(1 字节),而紧随其后的 `[NAND_FTL] Initializing NAND FTL...` 行恰好缺同样那几个字节的**行首**(字节账与三次启动命中两次的频率见 §4.4.1)→ 判据"framing 错不会让行变长"锁定为**两个写者并发裸发**,即 O1 的第二个实例(第一个是 2026-10-12 B6)。修法 = 非阻塞行所有权(`con_tx_try_claim`/`con_tx_release`,只在几条指令的临界区里 test-and-set;发送全程**不关中断**)+ 让路整行原文入环延后发(不丢、不补前缀、不重打时间戳)+ drain 每轮先取凭证(堵掉"attach 撞上别人半行")+ `[ERR]` 只在"抢不到且环也满"时红。新增 `dbg_console_tx_begin/tx_end`(多段裸发收口,调用点改造 → task-26)。主机单测 127 → **152** 条(M 族 `TC-CONLOG-170 至 TC-CONLOG-179`,含逐字节相同判据),四处生产码注入反证各自转红(取消独占 11 红 / 让路改静默丢 5 红 / 非属主可归还 1 红 / 把发送包进临界区 3 红)。§3.4 / §4.4 / §7 / §13-O1 同步 |
|
||||
| 2026-10-12 | **纪律盲区台架实证(B6)**:`protectTask`(B5 接线,台架腿当时未跑)启动锚点行在写者注册前裸发 → 与 defaultTask 启动日志**逐字节交错**(`st[art: cycle=220ms elm=70`,两次开机同形复现 = 启动调度确定性的竞态,非 USB 噪声)→ `@test` 锚点 MISS。修复 = 启动行改 `dbg_console_writer_ready()` 首拍补打(adcTask 首帧同款),`@build` 0/0 + `@test` PASS 复验(锚点完好 +577 ms)。§3.4 纪律示例更新 |
|
||||
|
||||
## 12. ORE 僵尸通道(2026-10-02 台架实证)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 运行/自检记录与待机显示设计说明
|
||||
|
||||
> **版本**:v1.2(§4.3 已随七类记录更新;v1.1 的原始设计口径不变)
|
||||
> **版本**:v1.3(§4.4 新增"卷锁 fail-open 边沿补投自检记录"一条,道G task-40;v1.2 为 §4.3 随七类记录更新,v1.1 的原始设计口径不变)
|
||||
> **日期**:2026-09-14(v1.2 更新 2026-10-26)
|
||||
> **状态**:步骤 1–9 全部完成——双端构建 0/0、`--logtest` PASS、ui-verify 五门禁全绿;真机 @test 待上电回归(§8.2)
|
||||
> **关联文档**:`项目架构总览.md` · `STM32F4-Base存储架构说明.md` · `系统墙钟时间维护说明.md` · `UI设计/内容页架构.md`(如存在)· `docs/UI设计/mockups/`(待机显示页面.jpg / 运行记录页.jpg / 自检记录页.jpg / 密码页面实际机器的示例图片.jpg)
|
||||
@@ -112,7 +112,7 @@
|
||||
| 文件 | 记录类型(= `syslog_id_t`) | 上限 | 满时策略 |
|
||||
|---|------|------|---------|
|
||||
| `/log/run.log` | `SYSLOG_RUN` 上电/重启与运行事件 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 |
|
||||
| `/log/selftest.log` | `SYSLOG_SELFTEST` 自检故障(有问题才记) | **512 条(32KB)** | 同上 |
|
||||
| `/log/selftest.log` | `SYSLOG_SELFTEST` 自检故障(有问题才记;来源含**卷锁 fail-open 边沿留痕**,见 §4.4 最后一条) | **512 条(32KB)** | 同上 |
|
||||
| `/log/accid.log` | `SYSLOG_ACCIDENT` 事故(跳闸型动作) | **512 条(32KB)** | 同上 |
|
||||
| `/log/alarm.log` | `SYSLOG_ALARM` 保护告警 | **1024 条(64KB)** | 同上 |
|
||||
| `/log/start.log` | `SYSLOG_START` 保护启动(越限电平) | **1024 条(64KB)** | 同上 |
|
||||
@@ -177,6 +177,21 @@ defaultTask(唯一写者,挂载成功后):
|
||||
- **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。
|
||||
- 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。
|
||||
- **自愈触发集分类(2026-10-24,代码侧唯一入口 `s_is_selfheal_code()`)**:只有"删了这个文件真能修好"的介质/结构类码才触发 unlink 重建——`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`;**名字/参数类严禁归入**(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`,另显式排除 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT`)。同名同因下 unlink 必然同样失败,旧集合的真实后果是**每轮排空删一次自己的历史日志且永远修不好**,违反 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条。`f_write` 之后的"失败/短写 → unlink 重试一次"**不接**本判定(能走到那里说明已打开成功;而簇分配失败是 `FR_OK` + 短写、根本没有码可判,必须无条件重试)。钉住:TC-SYSLOG-101 / 101b / 102 / 103;`App/log/soe_store.c` 侧同源但取**更保守子集**(TC-SOESTORE-022 / 022b)。
|
||||
- **卷锁 fail-open(零互斥那一态)边沿补投一条自检记录(2026-10-27 道G task-40)**:锁层只自增计数、
|
||||
绝不产日志——唯一能落盘的通道(`/log/wdev.log` 等)自己也要取这把**非递归**卷锁,被卷忙
|
||||
挡住时既写不出、也不许在失败分支里再取同一把锁(机理与四态语义的唯一出处是
|
||||
`App/fs/vol_lock.h` 的「可观测性」段,本文不复述)。因此留痕由唯一写者在 `syslog_drain()`
|
||||
尾部按**边沿**补投:判据是"计数比上一轮读到的大了",不是"计数大于零"(后者是电平,每轮一条
|
||||
= 每轮一次 NAND 写,诊断自己变成负载);**一个 episode(= 一次上电)只投一条**,重新上电
|
||||
复位 latch 后故障复发仍会再投(否则第二次开机起永久静默)。文本形如
|
||||
`vollock failopen=3`(累计值就是证据,不带态号),落 `selftest.log`。
|
||||
⚠ 三条约束(与 task-23「日志让路不计 `[ERR]`」同一根判据):① 不在失败调用点就地打日志;
|
||||
② 不打 DBG 错级、不打 `[ERR]`——`[ERR]` 是"设备出故障"的唯一信号通道(`@test` 的 Forbidden
|
||||
也依赖它),塞进可观测性事件就同时不再是故障、不再是禁令;③ 记录自带的时间戳是
|
||||
**被观测到的时刻**(粒度 = 一轮 `syslog_drain()`),不是发生时刻,所以本模块刻意不再另造
|
||||
`obs_first_ms`/`obs_last_ms` 字段去"看起来更精确"。主机判据:`TC-SYSLOG-137` 至
|
||||
`TC-SYSLOG-137d`(首投一条并带累计值 / 同 boot 不再投 / 计数不变不投 / 重新上电可再投),
|
||||
其中 `g_sys_err_count == 0` 就是"不打错级"的机器证据。
|
||||
- **留痕节流分两层、各自独立抑制位**:drain 侧"整批回投"只在正常→故障那一次报(`write fail, requeue %lu entries`,落盘成功一轮复位;旧文案写作 "drop N entries" 与实际行为相反——整批其实一条没丢);post 侧 `queue full, drop` / `preboot full, drop` 各一位(**丢弃语义不变,满则丢照旧**,只压留痕次数)。节流之后**没有丢弃计数**——要计数属新增对外可见信息,另列后续项。
|
||||
- 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义,FatFS 系统锁保护单次调用。
|
||||
- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 模块功能:在 PC 上离线验证 App/log/console_log.c 的单写者契约(G1~G5 / K1~K5,见
|
||||
* docs/子系统设计/控制台单写者日志设计说明.md):环形缓冲入环与分块发送、
|
||||
* 行级原子、192B 截断、写者未注册/ISR 上下文的直发(未注册期自 2026-10-26
|
||||
* 起是**整行独占**,不再是"无锁":M 族 TC-CONLOG-170..179 钉"别的写者插不进
|
||||
* 起是**整行独占**,不再是"无锁":M 族 TC-CONLOG-170 至 TC-CONLOG-179 钉"别的写者插不进
|
||||
* 一行、让路行逐字节等同直发、让位不丢、真溢出才 [ERR]")、缓冲满有界等待与
|
||||
* 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、
|
||||
* huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算、
|
||||
@@ -1303,7 +1303,7 @@ static void tc_ownership(void)
|
||||
char fill[DBG_CON_LINE_MAX];
|
||||
uint32_t i;
|
||||
|
||||
printf("-- TC-CONLOG-170..179 whole-line ownership (O1 instance 2, bench byte account) --\n");
|
||||
printf("-- TC-CONLOG-170 to TC-CONLOG-179 whole-line ownership (O1 instance 2, bench byte account) --\n");
|
||||
|
||||
/* 170 A 行发到第 4 个字节时被 B 抢占:A 逐字节完整,B 整行让路入环 */
|
||||
reset_all();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
用本机 MSVC 编译 test/host/console_log/console_log_test.c(以 #include 纳入生产源
|
||||
App/log/console_log.c),在 PC 上离线验证单写者环形缓冲的全套契约:192B 行截断、
|
||||
256B 分块、跨环尾两段拷贝、写者未注册/ISR 上下文的直发(未注册期自 2026-10-26 起
|
||||
为**整行独占**:抢不到独占的整行原文入环延后发,M 族 TC-CONLOG-170..179 负责钉)、
|
||||
为**整行独占**:抢不到独占的整行原文入环延后发,M 族 TC-CONLOG-170 至 TC-CONLOG-179 负责钉)、
|
||||
缓冲满两条上界
|
||||
(DWT 周期 / 200 tick)与丢弃计数、[ERR] 告警行不经环(不递归)、TX 僵尸通道软复位
|
||||
自愈 + 故障即止 + 300ms 冷却(含无符号回绕)、DWT 使能失败退回 20000 次指令计数、
|
||||
|
||||
@@ -127,7 +127,7 @@ void con_uart_reset(void);
|
||||
* ① con_uart_crit_at_tx_max:本字节上线时的临界区深度水位。整行所有权只能靠一次
|
||||
* 几条指令的 test-and-set 取得,**绝不允许把 7.8 ms 的发送包进关中断区**(那等于
|
||||
* 把中断延迟与最坏喂狗抖动塞进启动窗口,比交错本身更糟)——本水位恒 0 就是这条
|
||||
* 硬红线的机器证据(判据见 console_log_test.c 的 M 族 TC-CONLOG-170..179)。
|
||||
* 硬红线的机器证据(判据见 console_log_test.c 的 M 族 TC-CONLOG-170 至 TC-CONLOG-179)。
|
||||
* ② con_uart_tx_hook/_at:在"第 N 个字节上线之后"跑一遍测试装填的函数,用来精确
|
||||
* 模拟**发送任务在两个字节之间被抢占**。行级所有权是"拿不到就当场让路"的非阻塞
|
||||
* 设计,所以既有 con_delay_hook(vTaskDelay 时才跑)在这条路径上永远不会触发,
|
||||
|
||||
@@ -179,6 +179,13 @@
|
||||
* TC-SYSLOG-134 quiet 的入参守卫:坏 id / NULL 既不投也不计丢弃(参数错 != 容量故障)
|
||||
* TC-SYSLOG-135 启动前数组满同样计数、同样静默(另一条腿,与 F 项的两个通道分区一致)
|
||||
* TC-SYSLOG-136 同内容在七类之间**逐字节相同**:记录体不编码类型 = "文件即类型"的机器表达
|
||||
* TC-SYSLOG-137 卷锁 fail-open(零互斥那一态)第一次前进:补投**一条**自检记录、字段带累计值、
|
||||
* 且 g_sys_err_count 恒 0(不打 DBG 错级、不打 [ERR]——道G task-40)
|
||||
* TC-SYSLOG-137b 同一 episode(一次上电)内该计数继续前进:不再投第二条(诊断不许变成
|
||||
* 负载:每轮一条 = 每轮一次 NAND 写,与 s_quiet_drop 那条纪律同源)
|
||||
* TC-SYSLOG-137c 计数没前进:多轮 drain 一条都不投(边沿判据,不是电平判据)
|
||||
* TC-SYSLOG-137d 重新上电(reset_all)后计数再前进:又投一条——证明 latch 是"每 boot
|
||||
* 一条"而不是"永远只有一条",否则故障复发就永久静默
|
||||
*
|
||||
* 覆盖率台账(`scripts/check_test_coverage.py --module sys_log_mcu` 本机实跑):
|
||||
* App/log/sys_log_mcu.c **403/406 行 = 99.3%、函数 22/22**。
|
||||
@@ -421,8 +428,24 @@ static void locks_clear(void)
|
||||
s_last_wait_to = 0xFFFFFFFFu; /* 哨兵:从未调用过就不等于 timeout=0 */
|
||||
}
|
||||
|
||||
/* 卷锁观测计数假件(道G task-40):生产侧 syslog_drain() 每轮读一次,判"边沿"。
|
||||
* failopen 由测试自己抬(模拟 osMutexNew 失败那一支),其余三态给固定值——本卡
|
||||
* 只消费 fail-open 那一态,其余三态各有属主(App/fs/vol_lock.h 的四态语义不得合并)。 */
|
||||
static uint32_t s_vl_failopen_fake;
|
||||
|
||||
int vol_lock_init(void) { return 0; }
|
||||
|
||||
void vol_lock_get_stats(vol_lock_stats_t *p_out)
|
||||
{
|
||||
if (p_out == NULL) {
|
||||
return;
|
||||
}
|
||||
p_out->acquired = 11u;
|
||||
p_out->try_timeout = 2u;
|
||||
p_out->blk_timeout = 0u;
|
||||
p_out->failopen = s_vl_failopen_fake;
|
||||
}
|
||||
|
||||
int vol_lock_wait(uint32_t timeout_ms)
|
||||
{
|
||||
int r;
|
||||
@@ -599,6 +622,11 @@ static void reset_all(void)
|
||||
* 否则上一个场景的丢弃会让本场景第一条留痕被当成"已经报过" */
|
||||
s_quiet_drop = 0u;
|
||||
s_quiet_reported = 0u;
|
||||
/* 卷锁 fail-open 留痕(道G task-40):latch 与水位、假件计数都得归零,
|
||||
* 否则上一个场景"已经报过"会让本场景的边沿静默(同上面那条教训) */
|
||||
s_vl_failopen_seen = 0u;
|
||||
s_vl_reported = 0u;
|
||||
s_vl_failopen_fake = 0u;
|
||||
(void)memset(s_rd_win, 0, sizeof(s_rd_win));
|
||||
s_rd_win_id = -1;
|
||||
s_rd_win_base = 0u;
|
||||
@@ -2408,6 +2436,68 @@ static void tc_kinds(void)
|
||||
CHECK(ok, "TC-SYSLOG-136 a record body does not encode its kind: same content is byte-identical across all seven kinds");
|
||||
}
|
||||
|
||||
/* ============ TC-SYSLOG-137 卷锁 fail-open 边沿留痕(道G task-40) ============ */
|
||||
static void tc_vollock_note(void)
|
||||
{
|
||||
uint8_t rec[SYSLOG_REC_SIZE];
|
||||
uint32_t c0;
|
||||
uint32_t c1;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-SYSLOG-137 volume-lock fail-open: one self-test record per boot, never [ERR] --\n");
|
||||
|
||||
/* 137 边沿第一次前进:补投一条,字段带累计值,且全程不打错级/[ERR]。
|
||||
* 注意投递与落盘分在两轮 drain(投递发生在 drain 尾部,本轮批次已取走),
|
||||
* 所以这里显式排两轮,别把"本轮没落盘"误读成"没投"。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain(); /* 建队列;此刻计数为 0,不该有任何留痕 */
|
||||
c0 = syslog_count(SYSLOG_SELFTEST);
|
||||
g_sys_err_count = 0;
|
||||
s_vl_failopen_fake = 1u;
|
||||
syslog_drain(); /* 读到 1 != 上次 0 -> 投递 */
|
||||
ok = (syslog_count(SYSLOG_SELFTEST) == c0);
|
||||
syslog_drain(); /* 落盘 */
|
||||
c1 = syslog_count(SYSLOG_SELFTEST);
|
||||
(void)memset(rec, 0, sizeof(rec));
|
||||
(void)raw_at(P_SELFTEST, c0, rec);
|
||||
ok = ok && (c1 == c0 + 1u)
|
||||
&& (memcmp(rec + SYSLOG_DESC_OFFS, "vollock failopen=1", 18u) == 0)
|
||||
&& (rec[SYSLOG_DESC_OFFS + 18u] == 0u)
|
||||
&& (g_sys_err_count == 0);
|
||||
CHECK(ok, "TC-SYSLOG-137 the first fail-open edge posts exactly one self-test record carrying the cumulative count, with zero error-level traces");
|
||||
|
||||
/* 137b 同一 episode 内继续前进:不得再投(每轮一条 = 每轮一次 NAND 写) */
|
||||
s_vl_failopen_fake = 5u;
|
||||
syslog_drain();
|
||||
syslog_drain();
|
||||
CHECK((syslog_count(SYSLOG_SELFTEST) == c1) && (g_sys_err_count == 0),
|
||||
"TC-SYSLOG-137b a growing fail-open count inside the same boot posts no second record (the diagnostic must not become load)");
|
||||
|
||||
/* 137c 电平不变:多轮 drain 一条都不投——边沿判据而非电平判据 */
|
||||
syslog_drain();
|
||||
syslog_drain();
|
||||
CHECK((syslog_count(SYSLOG_SELFTEST) == c1) && (g_sys_err_count == 0),
|
||||
"TC-SYSLOG-137c an unchanged count posts nothing across further drains (edge-triggered, not level-triggered)");
|
||||
|
||||
/* 137d 重新上电:latch 复位,故障复发必须还能出声(否则"每 boot 一条"会退化成
|
||||
* "永远一条",第二次开机就永久静默) */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
c0 = syslog_count(SYSLOG_SELFTEST);
|
||||
s_vl_failopen_fake = 2u;
|
||||
syslog_drain();
|
||||
syslog_drain();
|
||||
ok = (syslog_count(SYSLOG_SELFTEST) == c0 + 1u);
|
||||
(void)memset(rec, 0, sizeof(rec));
|
||||
(void)raw_at(P_SELFTEST, c0, rec);
|
||||
ok = ok && (memcmp(rec + SYSLOG_DESC_OFFS, "vollock failopen=2", 18u) == 0);
|
||||
CHECK(ok, "TC-SYSLOG-137d after a reboot the latch re-arms, so a repeat fail-open is audible again (one record per boot, not one per lifetime)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
@@ -2427,6 +2517,7 @@ int main(void)
|
||||
tc_post_throttle();
|
||||
tc_clear();
|
||||
tc_kinds();
|
||||
tc_vollock_note();
|
||||
|
||||
printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
Reference in New Issue
Block a user