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 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
App/log/ — 日志子系统
两类日志:持久化运行日志(
sys_log,落 NAND)与控制台日志(console_log,走串口)。 两者都遵循单写者模型,避免多任务交错输出。
文件一览
| 文件 | 作用 |
|---|---|
sys_log.h |
日志接口(平台无关):定长 64B 记录,七类各一个 /log 文件(run / selftest / accid / alarm / start / oper / yk) |
sys_log_mcu.c |
sys_log 的 MCU 端后端实现(NAND + FatFs) |
console_log.c/h |
控制台单写者日志:多任务日志经队列汇入唯一写者,避免串口输出交错 |
soe_rec.c/h |
SOE 记录纯编解码(B9):64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
soe_store.c/h |
SOE 落盘胶水(B9):/log/soe.log,protectTask 投递(零文件系统)→ defaultTask 排空落盘 |
关键机制
syslog(运行/自检记录)
- 唯一写者 = defaultTask;其它任务只投递,不直接写盘
- 单记录定长 64 B,循环写;待机页可显示记录
- 内核启动前的日志走无锁静态缓冲
s_preboot[8]暂存(因为内核启动前不能建队列), 内核启动后由syslog_drain惰性创建真队列并排空 - ⚠️ 写盘必须校验
f_write返回的字节数(该 FatFs 移植在簇分配失败时返回FR_OK+ 短写, 不报错误码);追加写必须FA_WRITE|FA_OPEN_APPEND(FA_OPEN_APPEND单独用不含写位) - 清除记录 = 请求 / 落地分离:非写者上下文(uiTask 等)只能
syslog_request_clear(id)置一个标志(零盘触点、零卷锁),真正的f_unlink在 defaultTask 的syslog_drain()尾部执行——顺序也是判据的一部分:排在条目排空之后,否则本轮刚排空的旧记录会被 写回去、syslog_count()不归零。结果读syslog_clear_state(id)(NONE/OK/WAIT/FAIL),事实复核读syslog_count()。 落地函数syslog_clear_apply(id)是 MCU 后端专属(模拟器不实现 → 共享代码误调在 链接期就报错):卷锁一律 try,卷占用返 -1 且不动原文件;幂等只认FR_NO_FILE,FR_NO_PATH如实返 -1 且不做任何卷级动作(不补目录、不重格式化) - 删除的作用域 = 上表七个记录文件(
syslog_request_clear(id)逐 id 独立请求);取证文件wdev.log与soe.log各有各的属主与通道,不在本 API 作用域,想清它们要另开 API。 清除成功会往操作记录里落一条clear <文件>——但清oper.log自己时刻意不投, 否则"我清除了记录"这条会把刚删空的文件又写成 1 条,直接砸掉「删除后syslog_count()归零」这条判据(TC-SYSLOG-133b钉住) - ⚠ 宿主后端(
simulator/sim/sim_syslog.c)与 MCU 端判据相同、时序不同:模拟器单线程, 七类对应工作目录下七份宿主文件(sim_accid.log等),syslog_post_quiet()在本端 等价于syslog_post()(宿主进程没有那条栈硬线,也没有队列与丢弃);syslog_request_clear()调用内就删完了,因此WAIT态在模拟器上永不出现 (同syslog_post在两端"有队列 / 直写无队列"的差异)
五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做)
决策一:记录类型就是 syslog_id_t 本身,一类记录 = /log 下一个严格 8.3 短名的文件。
64B 定长记录体里没有类型字段位:[0..18] 时标 / [19] 空格 / [20..29] epoch /
[30] 空格 / [31..58] 描述 / 尾部补零,这个布局被 UI 记录页与主机解码工具按固定偏移
读了了很久(run.log / selftest.log 是既有文件,既有读者)。塞类型字节要么挪走 epoch
(上电/重启分类就断),要么挪走描述(屏上少字),且两端读者同批改——所以
"这条记录是什么类型"由它在哪个文件里回答。TC-SYSLOG-136 把这条钉成机器断言:
同一条内容在七类之间逐字节相同,记录体不编码类型。
决策二:不动 soe_type_t(二进制 SOE 通道保持现有 8 型)。 加 SOE 类型必须同步 UI 侧
ui_w_soe.c 的类型名表(那是另一位队友的写域),而 SOE 页与这五页本就是两套读者
(二进制解码器 vs 文本列表)。别把新类型顺手加进 SOE:那会同时造出
"同一个事件两个真源"。
| 记录 | 日志 ID / 文件 | 容量上限 | 写入点 | 触发判据 |
|---|---|---|---|---|
| 事故 | SYSLOG_ACCIDENT / accid.log |
512 | App/task/protect_task.c:181 |
asserted 上升沿且该元件跳闸投退项 = 1 |
| 保护告警 | SYSLOG_ALARM / alarm.log |
1024 | 同文件 184 |
asserted 上升沿且跳闸投退项 = 0 或该元件无此项 |
| 保护启动 | SYSLOG_START / start.log |
1024 | 同文件 176 |
picked 上升沿(越限电平,早于/等于动作) |
| 操作 | SYSLOG_OPER / oper.log |
512 | App/protocol/iec104/iec104_pts.c:574/602、App/task/default_task.c:242、App/log/sys_log_mcu.c:646(宿主 sim_syslog.c:245) |
强制复归受理 / 软压板投退落地 / 定值真落盘成功 / 清除记录成功 |
| 遥控 | SYSLOG_YK / yk.log |
256 | App/protocol/iec104/iec104_pts.c:573/576/582/586/596/598/609 |
104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 |
- "哪些元件算跳闸"只有一个真源:定值表
g_prot_set_meta里的PS_*_TRIP(枚举注释 即"0 告警 / 1 跳闸")+ 实例表k_pe[]的trip_ctl列。分类入口prot_elm_record_kind()(App/algo/prot_elm.c)里没有第二份元件名单, 没有该投退项的五项(OV/UV/3U0/OF/UF)填PROT_ELM_NO_TRIP=0xFF→ 恒告警;TC-PROTELEM-015逐元钉住"逐项独立 / 改定值即改类别 / 无此项者不受别人定值影响"。 - 描述只用 ROM 常量或
prot_elm_name()直传:所有写入点零snprintf、零拼接。 最长一条 20 字节 <SYSLOG_DESC_MAX(28);TC-IEC104-030逐条量长度(超 28 会被 投递侧静默截断,屏上少几个字,没人会去数)。 - ⚠️ 两个投递口,按调用者的栈预算选:
syslog_post()的两个"满态丢弃留痕"分支 内联着日志行缓冲(每处约 216 B 栈),因此syslog_post_quiet()给静态最坏链吃紧的 任务用(protectTask、iec104Task 必须用它),丢弃只累加syslog_post_dropped()、 由唯一写者 defaultTask 在syslog_drain()里按差值补一条留痕——同soe_store_post当年那条872 → 1192 B FAIL的实证纪律。收/丢判据两个口完全一致。 - 读侧契约(给 task-11):
syslog_count(id)+syslog_get_record(id, idx, ts, ts_len, desc, desc_len)与运行/自检记录同一枚 reader,"最近 20 条"= 从count-20起读;count()==0就是空态,页面显示"无记录"。不要在共享代码里调 MCU 专属的syslog_clear_apply()(模拟器不实现 → 链接期报错是故意的);清除一律走syslog_request_clear()+syslog_clear_state(),并把WAIT当正常态看(模拟器端 永不出现WAIT,见下条)。
console_log(控制台单写者)
- 多任务的生产者 → 单写者消费者,消除任务间日志行交错
- 设计详见
docs/子系统设计/控制台单写者日志设计说明.md
soe(SOE 事件顺序记录,B9)
- 记录 = 64 B 定长二进制(
soe_rec.h布局),落/log/soe.log; 与 run.log 分文件——run.log 是文本记录、有 UI/主机读取方,二进制 SOE 混入会破坏读取器;分文件保持"定长 64B + 条数=fsize/64 + 满删重建 + FTP 下载 + 主机解码"同一套先例 - 生产者 = protectTask(元件动作/返回、软压板投退、采样闭锁/恢复六类
边沿):
soe_store_post()纯 RAM(取时标 + 组记录 + 队列/待发环), 零 NAND I/O;⚠️ post 内禁DBG_*——会把 protectTask 静态最坏链 垫高一帧(实证 872→1192 B FAIL),丢弃留痕由 drain(defaultTask)差值上报 - 唯一写者 = defaultTask:挂
syslog_drain()尾部(继承其卷就绪门), 50 ms 节拍 → SOE 落盘延迟 ≤50 ms;写盘纪律全套照 syslog(卷锁 try /FA_WRITE|FA_OPEN_APPEND/ 校验字节数 / 满删重建 / 失败留痕去重), 绝不把名字/参数类错误码归入卷损坏触发集 - 时标口径:sys_clock 秒域时基 → CP56 ms 域 = 秒×1000(亚秒恒 0, 不造假);墙钟未建 → IV 位如实置位;seq 仅上电生命周期内单调 (容量重建不重置,仅复位重置——主机解码按 boot 段分段断言)
- 主机侧:
test/host/soe/decode_soe.py解码 + 断言(校验和/seq 单调/ CP56↔epoch UTC 交叉);⚠️ 交叉校验用calendar.timegm,不是本地时区的dt.timestamp()(+08 机差 28800 s,首跑实证坑)
相关文档
docs/子系统设计/控制台单写者日志设计说明.mddocs/子系统设计/运行与自检记录设计说明.md- FatFs 陷阱:
AGENTS.md「平台约束」