Files
STM32F4-Base/App/log
wandering1 1d4b6a17ce fix(控制台): 未注册期裸发加"整行独占",让路整行入环不丢,修掉 O1 第二实例
台架一手证据(boot_log_data/20261026_prod_console_interleave_evidence.txt):两次
「仅复位不重烧」启动里,[PROT] 未标定 INFO 行内分别多出 3 个和 1 个字节,而紧邻其后
的 [NAND_FTL] 行正好缺自己行首同样数量的字节("[20" 与 "[")——串口 framing 错不会
让行变长,字节按原序出现在邻居里 ⇒ 两个写者并发逐字节写 USART1->DR。写者对:
protectTask 的未标定闭锁边沿 × defaultTask 的 storage_bring_up(),两者都落在
dbg_console_writer_attach() 之前(attach 排在存储 bring-up 之后),该窗口
s_con_writer_up == 0 ⇒ 任何任务的整行都走零互斥裸发。这是控制台单写者设计说明在册
O1 的**第二个实例**;2026-10-12 那次只补了"自愿门控",而自愿门控挡不住业务边沿留痕
(业务无法预知某条边沿落在哪个窗口),所以这次被同一窗口第二次咬到。

修法是**非阻塞整行独占**:con_tx_try_claim/release 只在几条指令的临界区里
test-and-set,绝不把 7.8 ms 的逐字节发送包进关中断区、绝不等待;IPSR 上下文与调度
器未启动两种情况显式放行(崩溃遗言必须能出去;早期本就单执行流,一条不丢);同任务
按深度嵌套。抢不到独占的那条行**原文入环**延后发——前缀与时间戳在 DBG_* 调用点就
格式化进调用方缓冲,本模块与 drain 都不补前缀、不重打时间戳,所以延后发出的行与当场
直发逐字节相同;[ERR] 只在「抢不到独占且环也放不下」时才红,按 3 次启动命中 2 次的
频率,让路若计 [ERR] 就会把静默缺陷换成常红门禁。顺带堵掉第二处洞:drain 每轮先取
凭证,拿不到就让位且 tail 不推进(旧写法 attach 可以撞上别人已发出的半行)。
新导出 dbg_console_tx_begin()/tx_end() 供 task-26 收口故障钩子的多段裸发。

否决过的两条路写进文档 §4.4.1:提前 attach 会把等串口塞进 mount/mkfs 时序敏感段
或让生产者在整个 bring-up 里堵在满环上;整段进环会让「无看门狗窗口」死机时一个字都
不出(2026-10-02 那几轮正是靠 bring-up 期流式日志取证的)。

判据:test/host/console_log 127 → 152 全绿(M 族 TC-CONLOG-170..179 共 +25,既有
127 条零删除)。唯一换钉法的是 TC-CONLOG-015——旧断言钉的正是缺陷机理本身(裸发 0
次临界区 = 零互斥),现改钉「恰 2 次几条指令的临界区 + 每个上线字节都在临界区之外 +
零等待」;硬红线由假 USART 在每个上线字节处记录临界区深度水位恒 0 来机器守住。四处
注入反证各自转红 11 / 5 / 1 / 3 条并逐字节还原复跑;覆盖率 console_log.c 199/200 行
→ 256/257 行、16/16 → 21/21 函数。邻域复跑:task_prof 27/27、sys_log_mcu 137/137、
iec104_serv 37/37。CONLOG 游标抬到 179。

台架复验判据(**本笔尚未上机**,下一轮请台架跑):连续 3~5 次「仅复位不重烧」启动
日志 [ERR] 零命中,且 [PROT] 未标定与 [NAND_FTL] Initializing 两行都完整。
成本:.bss +5 B、Flash 估 +100~150 B,零新增 DBG_* / 零新增缓冲 / 零堆 / 零内核对象。
文档:控制台单写者日志设计说明.md 升 v1.6(O1 改「已处置」含两实例与字节账、新增
§4.4.1、§3.4 并发区间、§4.4 取消「不加锁」口径、§7 失效表、§11 变更记录、API 表)。
2026-10-26 19:34:29 +08:00
..

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/子系统设计/控制台单写者日志设计说明.md
  • docs/子系统设计/运行与自检记录设计说明.md
  • FatFs 陷阱:AGENTS.md「平台约束」