b623a406ad
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。
—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。
—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。
—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。
—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。
—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。
产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
122 lines
6.2 KiB
C
122 lines
6.2 KiB
C
/*
|
||
* 模块名称:Volume Lock(卷锁)— FreeRTOS 实现
|
||
* 模块功能:osMutex 串行化 NAND FatFs 卷访问(设计/使用约定见 vol_lock.h)
|
||
* 适用平台:STM32F407ZGTx + FreeRTOS
|
||
* 作者:王建锋
|
||
* 创建日期:2026-10-02
|
||
* 修改记录:
|
||
* v1.0 2026-10-02 王建锋 创建
|
||
* v1.1 2026-10-26 DTU 项目组 道G task-17:① 新增 4 个观测计数(T-a/T2/T3/T4,
|
||
* 共 16 B 静态)与 vol_lock_get_stats() 只读口 —— 不取锁、
|
||
* 零外部调用、零局部变量,13 处调用点一字未动;
|
||
* ② 修 :38「返回值」注释与代码相反的错(fail-open 原被
|
||
* 归给 0,实际返 1);③ 刻意不记时刻:时刻要读 tick,
|
||
* 等于给 defaultTask 取锁链添帧,改由消费方观测并自盖
|
||
* (命名 obs_first_ms/obs_last_ms,见 vol_lock.h 告诫 ②)
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include "vol_lock.h"
|
||
|
||
#include "cmsis_os.h"
|
||
|
||
/* 静态变量定义区 */
|
||
static osMutexId_t s_lock; /* 卷串行化互斥锁(vol_lock_init 创建) */
|
||
|
||
/* 观测计数(道G task-17「卷锁 5 态可观测性」)—— 本模块唯一的新增状态,共 16 B。
|
||
* 为什么计数必须在锁层、而且只有计数没有别的:
|
||
* ① 13 处调用点各自 return、历史上零痕;在每个调用点各写一份 = 13 份真源。
|
||
* ② 不能在这里打日志或投记录:唯一能落盘的通道(/log/wdev.log、run.log)自己也要
|
||
* 取这把卷锁,而卷锁**非递归** —— 被卷忙挡住时既写不出东西,还会自取死锁。
|
||
* ③ 也不读 tick 记时刻:那会给 defaultTask 的取锁链添一个 callee 与几字节帧,而
|
||
* 这条链的栈余量正是要省着花的;"什么时候开始忙"交给消费方观测(口径见 vol_lock.h
|
||
* 的告诫 ②:观测时刻不等于发生时刻,别写成首次发生)。
|
||
* 并发:多个任务可同时自增同一个计数,竞态下少计不会多计(判据是"是否 > 0 / 是否还
|
||
* 在增长",不是精确值),所以不为此上临界区 —— 上临界区反倒把观测做成了故障本身。
|
||
* volatile:跨任务读写且无锁保护,禁止把读侧重复读优化掉。 */
|
||
static volatile uint32_t s_st_acquired; /* T-a 真取到互斥 */
|
||
static volatile uint32_t s_st_try_timeout; /* T2 try(timeout=0)失败:卷被别人占住 */
|
||
static volatile uint32_t s_st_blk_timeout; /* T3 分片等待超时:ftpTask 正常退让 */
|
||
static volatile uint32_t s_st_failopen; /* T4 锁不存在却返 1:谎报成功那一态 */
|
||
|
||
/* 函数定义区 */
|
||
|
||
/*
|
||
* 函数功能:创建卷串行化互斥锁(幂等,重复调用安全)
|
||
* 入口参数:无 void
|
||
* 返回值:0 - 成功(已创建或已存在);-1 - osMutexNew 失败 int
|
||
* 限定条件:可在内核启动后任意时刻调用;本实现不检查是否已启动调度器
|
||
*/
|
||
int vol_lock_init(void) {
|
||
if (s_lock != NULL) {
|
||
return 0; /* 幂等 */
|
||
}
|
||
s_lock = osMutexNew(NULL);
|
||
return (s_lock != NULL) ? 0 : -1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:获取卷锁(首次调用自动 vol_lock_init)
|
||
* 入口参数:timeout_ms - 等待上限(毫秒);0 = try 语义(不阻塞) uint32_t
|
||
* 返回值:1 - 已取得互斥,**或锁不存在的 fail-open(返 1 但此刻并未串行化)**;
|
||
* 0 - 未取得(timeout=0 的 try 失败,或 timeout>0 的分片等待超时) int
|
||
* 限定条件:返回 0 时调用方**不得** vol_lock_release;fail-open 是为保证
|
||
* 看门狗喂狗点(defaultTask)绝不能因等锁而阻塞
|
||
* 函数说明:⚠ 原 v1.0 这行「返回值」把 fail-open 归给 0,与本函数 :46 的代码和
|
||
* TC-VOLLOCK-008 都相反(道G task-17 修正)。"返 1"有两种成因,调用方
|
||
* 无需各自分辨(它们只关心能不能干活),但整机的判据必须看
|
||
* vol_lock_get_stats().failopen —— 累计 > 0 = 本 boot 至少发生过一次
|
||
* "无互斥读写卷"。本函数只自增计数:不打日志、不投记录、不读 tick、
|
||
* 不新增局部变量(理由见上面计数块)。
|
||
*/
|
||
int vol_lock_wait(uint32_t timeout_ms) {
|
||
if (s_lock == NULL) {
|
||
(void)vol_lock_init();
|
||
if (s_lock == NULL) {
|
||
s_st_failopen++; /* T4:返 1 但零互斥 —— 谎报成功那一态 */
|
||
return 1; /* 锁创建失败:fail-open(不阻塞,保持启动段可用) */
|
||
}
|
||
}
|
||
if (osMutexWait(s_lock, timeout_ms) == osOK) {
|
||
s_st_acquired++; /* T-a:真持锁 */
|
||
return 1;
|
||
}
|
||
if (timeout_ms == 0u) {
|
||
s_st_try_timeout++; /* T2:压根没等(喂狗点/绘制路径只有这一态会发生) */
|
||
} else {
|
||
s_st_blk_timeout++; /* T3:等了一会儿没等到(分片退让),语义与 T2 不同 */
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:释放卷锁
|
||
* 入口参数:无 void
|
||
* 返回值:无 void
|
||
* 限定条件:仅在 vol_lock_wait() 返回 1 后调用;锁未创建时静默返回
|
||
*/
|
||
void vol_lock_release(void) {
|
||
if (s_lock != NULL) {
|
||
(void)osMutexRelease(s_lock);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读卷锁观测计数(契约与消费方告诫见 vol_lock.h 的函数头)
|
||
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 vol_lock_stats_t *
|
||
* 返 回 值:无 void
|
||
* 限定条件:不取卷锁、不关中断、不阻塞;**零外部调用、零局部变量**,因此不改变
|
||
* 任何调用方的栈最坏链(这条是本卡选型的结论,不要"顺手优化"掉)
|
||
* 函数说明:逐字段拷而非整体赋值 —— 每个字段都是某个真实值,不会读到半更新的字;
|
||
* 四元组之间不保证同一瞬间(诊断够用,理由见 .h 的告诫 ①)
|
||
*/
|
||
void vol_lock_get_stats(vol_lock_stats_t *p_out) {
|
||
if (p_out == NULL) {
|
||
return;
|
||
}
|
||
p_out->acquired = s_st_acquired;
|
||
p_out->try_timeout = s_st_try_timeout;
|
||
p_out->blk_timeout = s_st_blk_timeout;
|
||
p_out->failopen = s_st_failopen;
|
||
}
|