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:
2026-10-27 05:10:11 +08:00
parent 48a8ef0564
commit b1494eb10c
11 changed files with 324 additions and 37 deletions
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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 次指令计数、
+1 -1
View File
@@ -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 时才跑)在这条路径上永远不会触发,
+91
View File
@@ -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);