diff --git a/App/log/console_log.c b/App/log/console_log.c index 3deaa8f8..fdd4986d 100644 --- a/App/log/console_log.c +++ b/App/log/console_log.c @@ -16,7 +16,7 @@ * 无 stall 日志,纯节奏性饥饿)。修复:发送故障后立即结束本轮 * drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环), * 保证喂狗节奏 ≤ ~300ms。根治(把 USART1 改成只发不收)已于 2026-10-26 - * 由用户裁定不做,本缓解即长期方案;机制详述 docs/子系统设计/控制台单写者日志设计.md §12。 + * 由用户裁定不做,本缓解即长期方案;机制详述 docs/子系统设计/控制台单写者日志设计.md §9。 * 2026-10-02 王建锋 第 8b 轮加固:发送超时判据改用 DWT CYCCNT(时间源无关)。 * 台架第 8a 轮取证(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)定位到:defaultTask * 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 = @@ -68,7 +68,7 @@ * 噪声沿使 RXNE 置位后无人读,下一个事件即锁存 ORE(ORE 只能靠"先读 SR * 再读 DR"清除 ⇒ 永不自愈);引脚悬空只影响发生率,不是必要条件。 * 缓解措施与结论不变,仅改注释;机制详见 - * docs/子系统设计/控制台单写者日志设计.md §12。 + * docs/子系统设计/控制台单写者日志设计.md §9。 */ #include "console_log.h" @@ -493,7 +493,7 @@ uint8_t dbg_console_writer_ready(void) { * 重新初始化,故障时间被单块超时限定。 * ⚠ 2026-10-28 更正:原注释把根因写成"PA10 未接线 / 悬空",用户确认 PA10 一直是接线的; * 充分条件是"RX 使能却不消费 DR",与引脚是否悬空无关 - * (详见 docs/子系统设计/控制台单写者日志设计.md §12)。 + * (详见 docs/子系统设计/控制台单写者日志设计.md §9)。 */ static void con_uart_reinit(void) { __NVIC_ClearPendingIRQ(CON_UART_IRQN); diff --git a/App/task/protect_task.c b/App/task/protect_task.c index 95f588dd..b23c6295 100644 --- a/App/task/protect_task.c +++ b/App/task/protect_task.c @@ -234,7 +234,7 @@ void protect_task(void *argument) { /* 启动锚点行:控制台写者注册后首拍补打(2026-10-10 门控纪律,adcTask * 首帧同款)。写者注册前 con_append 走无锁裸发,与 defaultTask 启动日志 * 逐字节交错——B6 台架实证:本行曾损坏为 "st[art: cycle=220ms elm=70" - * 致 /serial-boot-test 锚点 MISS(控制台单写者日志设计.md §3.4)。 */ + * 致 /serial-boot-test 锚点 MISS(控制台单写者日志设计.md §2.5)。 */ if ((start_logged == 0u) && (dbg_console_writer_ready() != 0u)) { DBG_INFO("protect task start: cycle=%ums elm=%u", (unsigned)PROTECT_TASK_CYCLE_MS, (unsigned)PE_COUNT); diff --git a/Src/freertos.c b/Src/freertos.c index 29d68a35..02ae0fef 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -278,7 +278,7 @@ void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName) * ③ 归还所有权在这里没有下游读者:这一行发完就 NVIC_SystemReset()。 * 对照:**任务态**多段裸发(App/util/task_watchdog.c 的 STALL 行)仍然包 begin/end * —— 那里既不空转(同任务后面还要接着发),也不在断言子树上。这条区分写进 - * docs/子系统设计/控制台单写者日志设计.md §4.4.1,别让下一个人以为是我们漏了。 */ + * docs/子系统设计/控制台单写者日志设计.md §3.4.1,别让下一个人以为是我们漏了。 */ dbg_console_write_raw(msg_head, (unsigned)(sizeof(msg_head) - 1u)); if ((pcTaskName != NULL) && (hook_ptr_ok(pcTaskName) != 0)) { diff --git a/Src/stm32f4xx_it.c b/Src/stm32f4xx_it.c index f05da377..365d7846 100644 --- a/Src/stm32f4xx_it.c +++ b/Src/stm32f4xx_it.c @@ -27,7 +27,7 @@ * 原来这里直接调 HAL_UART_Transmit,于是崩溃路径成了绕过 console_log 行级独占的 * **第三个写者**,并且带两条本仓已实证的失效:① 超时判据是 HAL_GetTick()(TIM7 停摆 * 时永不成立,第 8a/8b 轮台架实证);② 僵尸通道(ORE 锁存)下 HAL 恒快速失败 - * (docs/子系统设计/控制台单写者日志设计.md §12)—— 而 [ERR] 崩溃行正是台架 + * (docs/子系统设计/控制台单写者日志设计.md §9)—— 而 [ERR] 崩溃行正是台架 * 「出现即不通过」的判据来源,快速失败 = 关键证据整条蒸发。 */ #include "console_log.h" /* USER CODE END Includes */ @@ -142,7 +142,7 @@ static void hard_fault_puthex(uint32_t v) * margin 顶到 1,016 < 1,024 ISR 底线)。真正救这条崩溃行的是另外两件事:**发送口归一** * (hard_fault_write 改走 dbg_console_write_raw,三条理由见其函数头)与**去掉 * HAL_Delay**(少一条 tick 依赖)。整行性的真实依据与残余见 - * docs/子系统设计/控制台单写者日志设计.md §4.4.1: + * docs/子系统设计/控制台单写者日志设计.md §3.4.1: * 故障上下文 IPSR != 0,begin() 是**放行**(返回 1 但**不持有**凭证——异常上下文 * 绝不允许等待或失败),所以这里的原子性**不是**靠凭证,而是靠"Fault 优先级 -1、 * 外设中断 ≥5、任务全冻结 ⇒ 没有第二个写者能跑进来"。唯一残留:被抢占任务**已经 diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index f825db27..686574c5 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -885,7 +885,7 @@ ISR 抢占,一抢就撞第 1 条,而后果在屏板侧完全不可见。时 |---|---|---|---| | **USART1** | PA9 / PA10 | 调试口 + **U5 直连 TTL**(ST_TX0/RX0),**MCU 直出、不经任何收发器** | ✅ **屏板链路**(700 kbaud) | | **USART2** | PD5 / PD6 | **RS232#3**(U10 隔离 → U13 TPT3232E → CN1) | 已初始化、固件未占用(日志最终选了 UART5) | -| UART5 | PC12 / PD2 | **RS232#1**(U9 隔离 → U13 → CN1) | ✅ **调试日志**(115200,已迁移、台架在用;端子见 `docs/子系统设计/控制台单写者日志设计.md` §3.4) | +| UART5 | PC12 / PD2 | **RS232#1**(U9 隔离 → U13 → CN1) | ✅ **调试日志**(115200,已迁移、台架在用;台架端口与接线见 `docs/架构设计/硬件规格与能力边界.md` §2) | | UART4 | PC10 / PC11,DE = **PA15** | **RS485#4**(U11 隔离 → U15 → A/B → CN1) | 冻结簇,本项不碰 | | USART3 | P第三方送检验收 / PB11 | U5 直连 TTL(ST_TX2/RX2) | 空闲 | @@ -1315,7 +1315,7 @@ mac=00:00:00:00:00:00 | 项 | 值 | |---|---| -| 端口 | UART5(RS232#1:PC12 TX / PD2 RX,经 U9 隔离 → U13 → CN1 端子),115200 8N1;台架端子接法见 `docs/子系统设计/控制台单写者日志设计.md` §3.4 | +| 端口 | UART5(RS232#1:PC12 TX / PD2 RX,经 U9 隔离 → U13 → CN1 端子),115200 8N1;台架端子接法见 `docs/架构设计/硬件规格与能力边界.md` §2 | | 用途 | printf 日志 + `/serial-boot-test` 采集通道 | | 配置源 | `.serialtest/config.json`(单一配置源) | | 工具 | 仓库内置 Python 串口助手(`.dsh/skills/serial-assistant/`,常驻 daemon + CLI) | diff --git a/docs/产品需求/参考产品复刻明细.md b/docs/产品需求/参考产品复刻明细.md index 8c00c658..9deb2782 100644 --- a/docs/产品需求/参考产品复刻明细.md +++ b/docs/产品需求/参考产品复刻明细.md @@ -795,7 +795,7 @@ tTimer_NoUI 无压无流判据(CN_ > (`CN1.10` 接适配器 TXD、`CN1.12` 接适配器 RXD、`CN1.7` 或 `CN1.8` 接 GND),台架日志口 > 因此由 `COM6` 变为 **`COM3`**,`.serialtest/config.json` 的 `portName` 已同步,`/serial-boot-test` 与 > `serial-assistant` 不必再手工指端口。这些端子是 **RS-232 电平**,TTL 的 CH340 直连不了, -> 用的是 USB 转 RS-232 适配器;接法与端子出处见 `docs/子系统设计/控制台单写者日志设计.md` §3.4。 +> 用的是 USB 转 RS-232 适配器;接法与端子出处见 `docs/架构设计/硬件规格与能力边界.md` §2。 > **这条要在第 16 条动代码之前完成。** > ② **RX 要真的读、ORE 要真的清**——PA10 一直是接线的(需求档 §5.16.2), > 但"RX 使能却不读 `DR`"本身就足以让 ORE 永久锁存。第 11 条实现时必须在 diff --git a/docs/子系统设计/屏板链路设计.md b/docs/子系统设计/屏板链路设计.md index f2406b6f..ca4be03a 100644 --- a/docs/子系统设计/屏板链路设计.md +++ b/docs/子系统设计/屏板链路设计.md @@ -3,7 +3,7 @@ > **模块**:`Drivers/BSP/ScreenLink/`(`sb_proto` 协议层 + `sb_link` 传输层)· `App/algo/sb_led_map`(状态灯折算)· `App/task/ui_task.c`(宿主)。 > **需求与屏板侧契约**(这条链路要做什么、屏板行为逐字节)在 `docs/产品需求/产品需求设计文档.md` §5.16,本文不重抄其表; > 本文是**主机侧实现事实**:接线、串口配置、协议口径、键/灯映射、资源、验证状态与未闭环项。 -> 台架日志口(UART5/RS232#1/CN1 端子)不属本链路,见 `docs/子系统设计/控制台单写者日志设计.md` §3.4。 +> 台架日志口(UART5/RS232#1/CN1 端子)不属本链路,见 `docs/架构设计/硬件规格与能力边界.md` §2。 > **屏板固件事实源**:外部工程 `DTU-LCD`(不在本仓,本链路不改它)。 ## 1. 链路与角色 diff --git a/docs/子系统设计/控制台单写者日志设计.md b/docs/子系统设计/控制台单写者日志设计.md index afae78bb..34115e88 100644 --- a/docs/子系统设计/控制台单写者日志设计.md +++ b/docs/子系统设计/控制台单写者日志设计.md @@ -1,34 +1,13 @@ # 控制台单写者日志设计 -> **版本**:v1.8(已实施;修订史见文末 §11「实施时间线」) -> **日期**:2026-10-30(v1.8:控制台端口由 USART1 改为 **UART5/RS232#1**,全文端口口径同步——物理口与台架端子见本文 §3.1 / §3.4;v1.7 于 2026-10-26:崩溃上报归一进单写者 + 多段裸发收口,道G task-26;v1.6 同日:未注册期裸发加「行级单一所有者」;v1.5 于 2026-10-24;初版 v1.0 于 2026-09-25) -> **状态**:已实施并验证——`/keil-build` 0 错误 0 警告、`/serial-boot-test` 硬件闭环 PASS(2026-09-25,见 §10);设计 2026-09-24 加固实施(当时位于 `Src/usart.c` USER CODE 区),2026-09-25 实现迁出至 `App/log/`(HAL 生成文件不含应用代码);同日写者任务体自 `Src/freertos.c` 迁至 `App/task/default_task.c`(与其余 5 任务文件同址归位) -> **关联文档**:`项目架构总览.md` · `运行与自检记录设计.md`(姊妹模块:持久化日志,同一写者任务) · `docs/测试说明/专项验证/链路延迟测试说明.md`(D6 日志税 / 步骤 8 非阻塞日志,需求出处) · AGENTS.md「平台约束」 +> **模块**:`App/log/console_log.{h,c}`(引擎)+ `App/log/dbg_log.h`(`DBG_*` 宏与行缓冲) +> **通道**:UART5 / RS232#1(PC12 TX / PD2 RX),115200 8N1,轮询发送 +> **写者**:`defaultTask`(全系统唯一) +> **关联**:`运行与自检记录设计.md`(同写者任务的持久化日志)· `项目架构总览.md` · AGENTS.md「平台约束」 -## 1. 背景:实测故障与根因 +## 1. 设计目标与约束 -### 1.1 实测故障(2026-09-24,双证据) - -| # | 症状 | 实测证据 | 后果 | -|---|------|---------|------| -| F1 | 日志行字符级交错 | 崩溃前两行日志逐字符交错:`"[2026-[2026-0909-24 15:2-24 15:27:37:39]…"` | /serial-boot-test 必检标记可能假性丢失、日志不可读 | -| F2 | HardFault(脏指针直接进入 UART) | BFAR=0x10010000(恰为 CCM 末尾+1),PC 落在 `HAL_UART_Transmit` 的缓冲读取处 | 崩溃上报路径自身再崩;被破坏的字符串指针被 UART 逐字节读取 | - -前兆:2026-09-22 链路延迟测试实测**开机连续丢行**(多任务并发 `printf` → 非重入 `fputc`/`HAL_UART_Transmit` 丢整行,~2s 自恢复;`链路延迟测试说明.md` §9.2)——同类竞态的温和形态,当时列为待办(日志非阻塞化)。 - -### 1.2 根因分析(旧路径:`DBG_*` → `printf` → `fputc`) - -| # | 根因 | 机理 | 对应故障 | -|---|------|------|---------| -| C1 | 逐字符发送 | `fputc` 每字符一次 `HAL_UART_Transmit(1 字节)`;两次调用之间可发生任务切换 | F1 交错 | -| C2 | 调用方缓冲直达 UART | `HAL_UART_Transmit` 的缓冲指针是调用方(可能已被破坏)的指针 | F2 HardFault | -| C3 | 多任务并发直触 UART | 轮询 Transmit 的并发保护是 `gState` 状态标志(§3.4):竞争失败方**立即**返回 `HAL_BUSY`(不等待、不重试),返回值被忽略 → 静默丢数据 | 09-22 丢行 | - -HAL 并发语义(本仓库实现实测,`stm32f4xx_hal_uart.c:1135`):轮询版 `HAL_UART_Transmit` **不加** `__HAL_LOCK`(`huart->Lock` 标志仅 IT/DMA 路径使用,`stm32f4xx_hal_def.h:93`);入函数检查 `gState==READY`,全程保持 `BUSY_TX` 直到发完 —— **单次调用是原子的,但调用之间没有排队机制**:并发调用方立即拿到 `HAL_BUSY`。逐字符路径下(C1)"单次调用 = 单字节",原子性退化为字节级(→ F1);整行路径下竞争失败方整行丢失(→ C3)。 - -## 2. 设计目标与约束 - -### 2.1 目标 +### 1.1 目标 | # | 目标 | 达成方式 | |---|------|---------| @@ -38,25 +17,25 @@ HAL 并发语义(本仓库实现实测,`stm32f4xx_hal_uart.c:1135`):轮 | G4 | 内核启动前 / 写者未注册 / ISR / 异常钩子路径不降级 | 回退:有界直发(`dbg_console_write_raw`,≤256B 截断、不等待、不死锁、零内核对象) | | G5 | 缓冲满丢行必须可观测 | 丢弃计数 + 写者刷出 `[ERR] CONSOLE dropped N lines` —— 含 `[ERR]` 模式,`/serial-boot-test` Forbidden 判 FAIL,丢失永不静默 | -### 2.2 约束 +### 1.2 约束 | # | 约束 | 出处 | |---|------|------| -| K1 | 内核启动前禁止创建/使用任何 FreeRTOS/CMSIS-RTOS 对象 | AGENTS.md 平台约束(2026-09-20 启动失败实证根因)→ 本模块内核前纯 BSS/寄存器机制 | +| K1 | 内核启动前禁止创建/使用任何 FreeRTOS/CMSIS-RTOS 对象 | AGENTS.md 平台约束 → 本模块内核前纯 BSS/寄存器机制 | | K2 | 禁止 `malloc/calloc/realloc`(C 堆仅 512B) | AGENTS.md 平台约束(禁 C 库堆分配条)→ 全部状态静态 BSS | -| K3 | HAL 生成文件(`Src/`、`Inc/`)不含应用代码 | 2026-09-25 迁移动机 → 实现位于 `App/log/` | +| K3 | HAL 生成文件(`Src/`、`Inc/`)不含应用代码 | HAL 生成文件只放 CubeMX 内容 → 实现位于 `App/log/` | | K4 | 生产者阻塞时间有界 | 最多 `DBG_CON_WAIT_MS`(200ms),日志不得阻塞业务 | -| K5 | RAM 预算紧张,新增静态内存必须核算 | §8 | +| K5 | RAM 预算紧张,新增静态内存必须核算 | §7 | -## 3. 总体架构 +## 2. 总体架构 -### 3.1 单写者模型 +### 2.1 单写者模型 | 角色 | 实体 | 职责 | |------|------|------| | 生产者 | 全部任务 + 内核前主线程 + ISR | 调用点格式化整行(库 `snprintf`)→ `dbg_console_write_line()` | | 写者 | **defaultTask(唯一)** | 进主循环前 `dbg_console_writer_attach()` 注册为唯一发送者;循环内周期 `dbg_console_drain()` | -| 物理通道 | **UART5(RS232#1:PC12 TX / PD2 RX,经 U9 隔离 → U13 TPT3232E → CN1;115200 8N1)** | 轮询发送(`HAL_UART_Transmit`);端口以 `CON_UART*` 宏收口在 `console_log.c`;DMA2_Stream7 现由屏板链路 USART1 TX 占用,与本模块无关 | +| 物理通道 | **UART5(RS232#1:PC12 TX / PD2 RX,经 U9 隔离 → U13 TPT3232E → CN1;115200 8N1)** | 轮询发送(`HAL_UART_Transmit`);端口以 `CON_UART*` 宏收口在 `console_log.c` | | 唤醒 | `SYS_EVT_CONSOLE_TX`(`sys_evt` bit 6) | **消费型命令位**(`osFlagsWaitAny` 默认消费,与 AGENTS.md 事件位语义一致;区别于就绪位必须 `osFlagsNoClear`) | **与其他日志子系统的关系**(同一"单写者纪律",同一属主任务): @@ -69,7 +48,7 @@ HAL 并发语义(本仓库实现实测,`stm32f4xx_hal_uart.c:1135`):轮 defaultTask 主循环按序处理:控制台排空 → 配置保存执行 → 2s 配置轮询 → LED 心跳 → syslog 落盘 → 栈水位观测——同一任务串行化三类 I/O,任务间零竞争。 -### 3.2 文件清单 +### 2.2 文件清单 | 文件 | 端 | 角色 | |------|-----|------| @@ -77,11 +56,11 @@ defaultTask 主循环按序处理:控制台排空 → 配置保存执行 → 2 | `App/log/console_log.c` | MCU | 实现:环形缓冲 / 生产者 / 写者 / 裸发 / `fputc` | | `Drivers/BSP/dbg_log.h` | 共享 | `#include "console_log.h"`;`DBG_PRINT` 宏绑定 `dbg_console_write_line`(整行一次入环) | | `simulator/dbg_log.h` | 模拟器 | `printf` stub——模拟器不编译本模块(宿主 `printf` 天然串行,无需单写者) | -| `App/task/default_task.c` | MCU | 写者任务体:`dbg_console_writer_attach()` 注册 + 主循环 `dbg_console_drain()`;含存储 bring-up / 配置引擎 / syslog 落盘 / LED / 栈水位(2026-09-25 自 `Src/freertos.c` 迁出) | +| `App/task/default_task.c` | MCU | 写者任务体:`dbg_console_writer_attach()` 注册 + 主循环 `dbg_console_drain()`;含存储 bring-up / 配置引擎 / syslog 落盘 / LED / 栈水位 | | `Src/freertos.c` | MCU | CubeMX 接线:6 任务创建 + netMsgQueue + sys_evt;栈溢出/堆失败钩子裸发(任务名先范围校验再拷入本地缓冲) | | `Src/usart.c` | MCU | 纯 CubeMX 生成(UART 初始化 / MspInit / 句柄声明)——零应用代码 | -### 3.3 拓扑 +### 2.3 拓扑 ```mermaid flowchart LR @@ -102,47 +81,47 @@ flowchart LR P3 -- "有界直发 ≤256B" --> U ``` -### 3.4 台架日志口接线(RS232#1 / CN1) +### 2.4 台架采集口 -- 台架须用 **USB 转 RS-232 适配器**(TTL 的 CH340 直连不了,且 RS-232 的 ±电平可能打坏 `U13` TPT3232E 或适配器): - 适配器 `TXD` → `CN1.10`(板子 `RS232_RX1`)、`RXD` → `CN1.12`(板子 `RS232_TX1`)、`GND` → `CN1.7` 或 `CN1.8`(`CGND`)。 -- 台架串口因此是 **`COM3`**,`.serialtest/config.json` 的 `portName` 已按此配置,`/serial-boot-test` 与 `serial-assistant` 不必手工指端口。 -- RS232#1 **没有 TTL 出口**(`ST_TX1`/`ST_RX1` 只连 `U9` 隔离器与 MCU),唯一外部出口是 CN1 的 RS-232 端子; - `U14` 是 RS232#3 的网口座,本通道用不上。 -- 电平链与引脚见 `docs/架构设计/硬件规格与能力边界.md` §2(UART5 = PC12 TX / PD2 RX,经 U9 隔离 → U13 电平转换 → CN1)。 +- 台架侧端口是 **`COM3`**(USB 转 RS-232 适配器接 CN1 的 RS232#1 端子),`.serialtest/config.json` 的 + `portName` 已按此配置,`/serial-boot-test` 与 `serial-assistant` 不必手工指端口。 +- 端子接法、电平链与引脚归属属硬件面,见 `docs/架构设计/硬件规格与能力边界.md` §2 —— 本文只约定 + 这一路是 115200 8N1 的**只发不读**口(§9 第 1 条即由此而来)。 -### 3.5 并发语义与本设计的配合(实现实测) +### 2.5 并发语义与本设计的配合(实现实测) -写者**已注册**后(稳态):任务上下文唯一调用者 = 写者(`defaultTask`),零竞争;残余窗口 = ISR 直发撞写者发送中(丢失、不挂死,§7)。 +写者**已注册**后(稳态):任务上下文唯一调用者 = 写者(`defaultTask`),零竞争;残余窗口 = ISR 直发撞写者发送中(丢失、不挂死,§6)。 -写者**未注册**期(内核启动 → `defaultTask` `attach`,时长取决于存储 bring-up,通常秒级):`con_append` 走 `dbg_console_write_raw` → `con_tx_bounded`(**寄存器级轮询 TXE/TC,无 `gState` 锁**,见 §4.4)。⚠ **2026-10-10 起此路径无互斥**——旧实现走 `HAL_UART_Transmit`,其 `gState` 检查跨整行保持、竞争失败方立即 `HAL_BUSY`,故当时"只丢不交错";改为寄存器级轮询后该保证**不再成立**:两个任务同时裸发会**逐字节交错**(2026-10-10 台架实测:`adcTask` 首帧日志与 `defaultTask` 的 `[NAND_FTL]` 初始化行交错)。 +写者**未注册**期(内核启动 → `defaultTask` `attach`,时长取决于存储 bring-up,通常秒级):`con_append` 走 `dbg_console_write_raw` → `con_tx_bounded`(**寄存器级轮询 TXE/TC,无 `gState` 锁**,见 §3.4)。`con_tx_bounded` 本身无互斥(寄存器级轮询,无 `HAL` `gState` 检查)——互斥做在它外面一层:整行所有权(§3.4.1)。 | 区间 | 发送者构成 | 并发行为 | |------|-----------|---------| | 内核启动前 | 仅主线程(单线程) | 无并发,直发安全 | -| 写者未注册(内核启动 → attach) | 任何任务都可能记日志(2026-10-26 起这是**常态而非违规**) | **整行独占**(行级单一所有者,§4.4.1):同一时刻只有一个上下文在写 `CON_UART->DR`,抢不到独占的整行转入环形缓冲延后发 → 交错结构性消除,且不丢行 | -| 写者已注册(稳态) | 任务上下文唯一调用者 = 写者 | 任务上下文零竞争;残余窗口 = ISR 直发撞写者发送中(丢失、不挂死,§7) | +| 写者未注册(内核启动 → attach) | 任何任务都可能记日志(**常态,不是违规**) | **整行独占**(行级单一所有者,§3.4.1):同一时刻只有一个上下文在写 `CON_UART->DR`,抢不到独占的整行转入环形缓冲延后发 → 交错结构性消除,且不丢行 | +| 写者已注册(稳态) | 任务上下文唯一调用者 = 写者 | 任务上下文零竞争;残余窗口 = ISR 直发撞写者发送中(丢失、不挂死,§6) | -**纪律(2026-10-10 固化,2026-10-10 / 2026-10-26 两次台架实证其不足)**:周期 / 常驻任务**不得**在写者注册前记日志——用 `dbg_console_writer_ready()`(§5)门控自身日志(`adcTask` 的首帧与 10 s 心跳、`protectTask` 的启动锚点行即按此门控——后者 2026-10-12 台架复测实证:保护任务 接线时启动行未门控,注册前裸发被交错损坏致 `/serial-boot-test` 锚点 MISS,见 §11 变更记录)。内核前代码仍可直接裸发(单线程,无并发)。 -> -> ⚠ **自愿门控挡不住边沿留痕**——它在 2026-10-12(台架复测,启动锚点)与 2026-10-26(未标定闭锁边沿,见 §4.4.1)两次被同一个窗口咬到, -> 因为"这条行要不要留痕"是**业务边沿**决定的,调用方无法预知它是否落在未注册期。2026-10-26 起该窗口改由**机制**兜住 -> (§4.4.1 行级单一所有者);`dbg_console_writer_ready()` 保留,但语义降级为**降延迟**(少让路一次、少占一点环位), -> 不再是防交错的唯一手段——新代码不必再为它改写业务分支。 +**纪律**:周期 / 常驻任务在写者注册前记日志必须用 `dbg_console_writer_ready()`(§4)门控自身日志—— +未注册期任何任务都可能记日志,靠行级单一所有者(§3.4.1)保证不交错;门控的作用只是**降延迟** +(少让路一次、少占一点环位)。启动类锚点行(`adcTask` 首帧与 10 s 心跳、`protectTask` 启动锚点) +按此门控;内核前代码可直接裸发(单线程,无并发)。 -## 4. 详细设计 +**为什么要门控而不是靠机制兜**:一条行要不要留痕由**业务边沿**决定,调用方无法预知自己是否落在未注册期; +两次台架实证(`protectTask` 启动锚点、未标定闭锁边沿行):不门控的边沿行会被交错损坏,致 +`/serial-boot-test` 锚点 MISS。门控 + 行级单一所有者两层在,新代码不必为它改写业务分支。 -### 4.1 环形缓冲 +## 3. 详细设计 + +### 3.1 环形缓冲 | 参数 | 值 | 理由 | |------|-----|------| | 容量 `DBG_CON_RING_SIZE` | 4096B | 2 的幂 → `&(SIZE-1)` 回绕免除法;≈21 整行(192B/行)余量 | -| head/tail | `uint16` + `volatile`;**多生产者安全 / 单写者**(2026-10-24 更正:本表旧写「单生产者」,与本文 §3.1「生产者 = 全部任务 + 内核前主线程 + ISR」互相矛盾,现由代码侧统一——**复核空位 + 推进 head + 整行拷贝收进同一个临界区**,最长 192 B 拷贝在 168 MHz 下约亚微秒;`vTaskDelay` 与 DWT/tick 上界判据一律留在区外,区内绝不睡眠) | head = 生产者写位置(临界区内推进),tail = 写者读位置 | +| head/tail | `uint16` + `volatile`;**多生产者安全 / 单写者**(生产者 = 全部任务 + 内核前主线程 + ISR)。**复核空位 + 推进 head + 整行拷贝收进同一个 `PRIMASK` 窗口**(`CON_CRIT_ENTER/EXIT`,刻意不用 `taskENTER_CRITICAL()`,理由见 §3.4.1),最长 192 B 拷贝在 168 MHz 下约亚微秒;`vTaskDelay` 与 DWT/tick 上界判据一律留在区外,区内绝不睡眠 | head = 生产者写位置(临界区内推进),tail = 写者读位置 | | 已用字节 | `(head - tail) & (SIZE-1)` | 无符号差自然回绕,免锁可读 | | 单行可接收上限 | `SIZE/2 = 2048B` | 保证"一行恒可入环";正常路径受 `DBG_CON_LINE_MAX=192B` 约束,不触发 | | 位置 | BSS(SRAM1,非 CCM) | CCM = FreeRTOS 堆,放 BSS 不占堆;轮询发送不依赖 DMA 可达性(CCM 的 DMA 不可达约束不适用) | -### 4.2 生产者路径(`con_append`) +### 3.2 生产者路径(`con_append`) ```mermaid flowchart TD @@ -154,7 +133,7 @@ flowchart TD D -- 是 --> E["先 dbg_console_drain()
(腾空环形缓冲, 避免自等)"] D -- 否 --> F{"环内空间 ≥ 行长?"} E --> F - F -- 是 --> G["临界区:整行拷贝
(head→环尾 + 回绕两段) + 推进 head"] + F -- 是 --> G["PRIMASK 窗口:整行拷贝
(head→环尾 + 回绕两段) + 推进 head"] F -- 否 --> H["vTaskDelay(1) 等待
最多 200ms (200 tick)"] H -- 出现空间 --> G H -- 超时 --> I["丢弃:s_con_drop++
(写者随后刷 [ERR] CONSOLE dropped N lines)"] @@ -163,12 +142,12 @@ flowchart TD 关键机制: -- **整行原子入环**:拷贝与 head 推进在同一次 `taskENTER_CRITICAL` 内完成 → 写者永不见"半行";临界区时长有界(≤192B 拷贝,微秒级)。 +- **整行原子入环**:拷贝与 head 推进在同一 `PRIMASK` 窗口内完成(`con_ring_try()` 一次试入环,判空位与写在同一窗口)→ 写者永不见"半行";窗口时长有界(≤192B 拷贝,微秒级)。 - **自等待规避**:写者自身记录日志时先 `drain` 腾空(否则"等写者腾出的空间"会等自己,死锁);腾空后入环必立即成功。 -- **丢弃可观测**:丢弃累计 `s_con_drop`,写者每次 drain 后把累计值刷成 `[ERR]` 行(§4.3)——`[ERR]` 触发 `/serial-boot-test` Forbidden 判定,丢行永不静默。 +- **丢弃可观测**:丢弃累计 `s_con_drop`,写者每次 drain 后把累计值刷成 `[ERR]` 行(§3.3)——`[ERR]` 触发 `/serial-boot-test` Forbidden 判定,丢行永不静默。 - **唤醒**:入环后置 `SYS_EVT_CONSOLE_TX` 命令位 → 写者立即被唤醒(零时延);写者循环另有 50ms 超时兜底(事件位异常也不滞留日志)。 -### 4.3 写者路径(`attach` / `drain`) +### 3.3 写者路径(`attach` / `drain`) ```c /* 注册(defaultTask 进主循环前一次,App/task/default_task.c) */ @@ -209,65 +188,44 @@ sequenceDiagram end ``` -### 4.4 裸发(`dbg_console_write_raw`) +### 3.4 裸发(`dbg_console_write_raw`) - 有界:`len > 256 → 256`(截尾);发送走 `con_tx_bounded`(**寄存器级轮询 TXE/TC**,超时判据 = DWT `CYCCNT`,`CON_TX_TIMEOUT_CYC` = 100 ms 对应周期数),**发送全程不持临界区、不等待状态**(这条是硬红线:一行 90 B @115200 约 7.8 ms,把发送包进关中断区等于往启动窗口塞 7.8 ms 中断延迟与最坏喂狗抖动,比交错本身更糟——由主机单测 `TC-CONLOG-015b`/`TC-CONLOG-177` 用「每个上线字节所见临界区深度恒 0」钉住); -- **2026-10-10:DWT 不可用时改走 `con_tx_counted`(指令计数上界 `CON_TX_GUARD_MAX`),不再回退 `HAL_UART_Transmit`**。两个理由:① HAL 的超时判据是 `HAL_GetTick`——断言/停摆现场 tick 未必在走;② `HAL_UART_Transmit → UART_WaitOnFlagUntilTimeout` 这条尾巴会被 armlink 挂进**每个**碰日志的任务的最坏栈链(`configASSERT` 在 FreeRTOS 内部可达 → 三个内核钩子都在链尾),实测每条链多背 64 B; +- **DWT 不可用时走 `con_tx_counted`(指令计数上界 `CON_TX_GUARD_MAX`),不回退 `HAL_UART_Transmit`。**两个理由:① HAL 的超时判据是 `HAL_GetTick`——断言/停摆现场 tick 未必在走;② `HAL_UART_Transmit → UART_WaitOnFlagUntilTimeout` 这条尾巴会被 armlink 挂进**每个**碰日志的任务的最坏栈链(`configASSERT` 在 FreeRTOS 内部可达 → 三个内核钩子都在链尾),实测每条链多背 64 B; - 适用:内核前 / 未注册期、ISR、异常钩子(栈溢出 / 堆失败)、崩溃上报; -- ~~**调用方纪律(2026-10-10 补充)**:写者注册前只允许 `defaultTask` 走此路径~~ → **2026-10-26 起由机制保证**:`con_tx_bounded` 仍无互斥(刻意——见上一条红线),互斥做在**它外面一层**:整行所有权(§4.4.1)。调用方纪律降级为性能建议。 -- 调用方纪律:**只交已知合法的缓冲**——钩子侧先做指针范围校验(Flash / SRAM / CCM 窗口)并拷入本地缓冲再发,TCB 指针不接触 UART(`Src/freertos.c vApplicationStackOverflowHook`,2026-09-24 加固;指针不可信时发占位文本,"上报崩溃的代码不能再崩一次")。2026-10-10 补充:钩子里的 `name[49]/num[16]` 改**函数级 static**(终点路径、打印完即复位,无并发)——放栈上会让这 81 B 出现在每个任务的最坏链上;行号转换改原地反转,去掉 `HAL_Delay`(`con_tx_bounded` 已等 TC)。 +- **互斥不在 `con_tx_bounded` 里**(刻意——见上一条红线),而在它外面一层:整行所有权(§3.4.1)。 +- 调用方纪律:**只交已知合法的缓冲**——钩子侧先做指针范围校验(Flash / SRAM / CCM 窗口)并拷入本地缓冲再发,TCB 指针不接触 UART(`Src/freertos.c vApplicationStackOverflowHook`;指针不可信时发占位文本,"上报崩溃的代码不能再崩一次")。钩子里的 `name[49]/num[16]` 改**函数级 static**(终点路径、打印完即复位,无并发)——放栈上会让这 81 B 出现在每个任务的最坏链上;行号转换改原地反转,去掉 `HAL_Delay`(`con_tx_bounded` 已等 TC)。 -#### 4.4.1 行级单一所有者(2026-10-26 道G task-23,O1 第二个实例收口) +#### 3.4.1 行级单一所有者 -**台架一手证据**(`docs/测试说明/测试数据/boot_log_data/20261026_prod_console_interleave_evidence.txt`,同一镜像 `fw 戳 Oct 26 2026 16:27:01`,排除构建差异): +裸发一行约 7.8 ms,裸发之间必须有互斥,否则两个上下文逐字节交错。机制是非阻塞的**整行所有权**: -| 启动 | 方式 | 结果 | -|---|---|---| -| A 16:28 | ST-LINK 烧录 + 复位 | 行**完好**:`…protection blocked (UA/UN/IA VALID not all set)` | -| B 16:37 | **仅复位** | 行内多出 `[` `2` `0` 三字节:`protection [blocked (UA/UN/IA VALI2D not all s0et)` | -| C 16:44 | **仅复位** | 行内多出 `[` 一字节,位置不同 | +| 环节 | 现状 | +|---|---| +| 取还凭证 | `con_tx_try_claim()` / `con_tx_release()`:只在几条指令的 PRIMASK 临界区里做一次 test-and-set,非阻塞 | +| 三条规则 | ① `IPSR != 0`(异常 / 中断,含四个 Fault 上报)→ **放行且不持有**(日志是唯一遗言,不许失败);② `xTaskGetCurrentTaskHandle() == NULL`(调度器未启动)→ 放行(单执行流,本就无第二写者);③ 同一任务可嵌套持有(`s_con_tx_depth`,drain 轮内的 `[WARN]`/`[ERR]` 留痕行) | +| 抢不到怎么办 | **让路不丢弃**:整行原文转入环形缓冲延后发(≤50 ms 后由写者发出)。前缀与时间戳在 `DBG_*` 调用点就已格式化进调用方缓冲,本模块与 drain 都不补前缀、不重打时间戳 → 延后发出的行与当场直发**逐字节相同** | +| `[ERR]` 口径 | 只有「既抢不到所有权**且**环也放不下」才计入 `s_con_drop`(真溢出)。让路本身不计数——否则门禁常红,等于把静默缺陷换成假失败 | +| drain 也要凭证 | `dbg_console_drain()` 先拿所有权才开轮,拿不到就让位(`tail` 不推进,环里一条不少) | +| 多段裸发收口 | `dbg_console_tx_begin()` / `dbg_console_tx_end()`:非阻塞,拿不到返回 0 而调用方照发。**只用于任务态**(现网唯一实例 = `App/util/task_watchdog.c` 的 STALL 行) | +| 异常态为什么也不持凭证 | `IPSR != 0` 时 `tx_begin()` 返回 1 但并不持有。整行性来自「Fault 优先级 -1、外设中断 ≥5、任务全冻结」⇒ 没有第二个写者能跑进来;唯一残留 = 被抢占任务已在移位寄存器里的 ≤1 个字节,落在故障行**之前**,不影响 `[ERR]` 前缀 grep | +| 为什么绝不排队等锁 | 裸发一行约 7.8 ms,等锁等于把自己的日志节拍压到别人的串口耗时上;`adcTask`(AboveNormal,1 kHz 消费)可能被压住数十拍——比交错更糟 | -**字节账(这是本节最硬的一段证据)**:紧接其后的那一行**恰好缺自己被抢走的那几个字节、且顺序一致**——B 次的下一行原文是 `26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL...`,缺的正是 `[20`;C 次的下一行缺 `[`。串口 framing 错**只会改字节、绝不会让一行变长**,所以多出的字节只能来自另一个写者。而 `[` + `2` `0` 正是 `DBG_*` 行前缀 `"[%s]"` + `sys_clock_get_str()` 输出的 `"2026-…"` 的**头三个字符** —— 两个写者对得上:`[PROT]`(`App/task/protect_task.c` 未标定闭锁边沿 `DBG_INFO`)与 `[NAND_FTL] Initializing NAND FTL...`(`defaultTask` 的 `storage_bring_up()`),二者都在**写者注册之前**裸发(后者由位置决定:`dbg_console_writer_attach()` 排在 bring-up 之后)。三次启动命中两次、只出现在"仅复位"不出现在"烧录"启动 = 时序竞态,与构建无关。 +**行序不是判据**:让路行在流里位置后移(最多一轮 drain,实测毫秒级),且带的是它**自己产生时刻**的时间戳。因此可能出现「后出现的行时间戳更早」——取证要按整行逐字节锚定(每行是否完整),不要锚定相对顺序。 -**修法(选"真正原子"这一支,不是扩缓冲、不是再叠一层自愿门控)**: +**边界与约束**: -- `con_tx_try_claim()` / `con_tx_release()`:**非阻塞**的行所有权。取凭证只在**几条指令的临界区**里做一次 test-and-set;拿到 = 独占整行发送,拿不到 = 当场让路。**三条规则**:① `IPSR != 0`(异常/中断,含四个 Fault 的上报)→ **放行**(不许阻塞,日志是唯一遗言);② `xTaskGetCurrentTaskHandle() == NULL`(调度器未启动)→ **放行**(单执行流,本就无第二写者,早期日志一条不丢);③ 同一任务可嵌套持有(`s_con_tx_depth`)——写者一轮 drain 内还要裸发 `[WARN]`/`[ERR]` 留痕行,那是嵌套持有者。 -- **为什么绝不排队等锁**:裸发一行约 7.8 ms,等锁等于把自己的日志节拍压到别人的串口耗时上;`adcTask`(AboveNormal,1 kHz 消费)可能被压住数十拍 → 比交错更糟。这也是否决"把写者注册点提前"方案(A 案)的同一条理由:提前注册只是把窗口挪短,且要么让 `mount/mkfs` 里面去 drain 串口(把"等串口"塞进存储时序敏感段),要么让生产者在整个 bring-up 期间堵在满环上。 -- **让路 ≠ 丢弃**:抢不到所有权的那条行**整行原文**转入既有环形缓冲延后发。前缀与时间戳在 `DBG_*` **调用点**就已 `snprintf` 进调用方缓冲,本模块与 drain 都不补前缀、不重打时间戳 → **延后发出的行与它当场直发时逐字节相同**(`TC-CONLOG-171`/`TC-CONLOG-178`),取证时不会出现"某条行晚了几百毫秒、时间戳却写着晚的时间"这种证据说谎。 -- **`[ERR]` 仍然是"控制台在丢东西"的唯一信号**:只有「既抢不到所有权**且**环也放不下」才计入既有 `s_con_drop`(真溢出)。按 3 次启动命中 2 次的频率,若把"让路"本身也计 `[ERR]`,门禁会从此常红——那是把静默缺陷换成假失败(`TC-CONLOG-172c`/`TC-CONLOG-172d`/`TC-CONLOG-172e` 分别钉住"真溢出仍红""让路不红""报成才销账")。 -- **顺带堵掉第二个洞**:`dbg_console_drain()` 现在也必须先拿到所有权才开轮,拿不到就让位(`tail` 不推进 → 环里一条不少,≤50 ms 后重发)。旧写法下 attach 就发生在 `defaultTask` 里,完全可能撞上别任务**已发出半行**的裸发,两边逐字节交错。 -- **新增 API**:`dbg_console_tx_begin()` / `dbg_console_tx_end()`(同样非阻塞:拿不到返回 0,调用方照发即可)——给"一条行分多段裸发"的调用方收口用。本模块只提供 API 与主机单测;调用点改造已随 **task-26(2026-10-26)** 落地,但**只落在任务态**:`App/util/task_watchdog.c` 的 STALL 行(正文 + CRLF 共 2 段)包进一次独占,调用点一律写 `(void)dbg_console_tx_begin();`、不拿返回值分支;`Src/freertos.c` 栈溢出/断言钩子与 `Src/stm32f4xx_it.c hard_fault_report` 三处**刻意不包**,理由见下面那条 bullet。 -- **异常态的整行性不靠凭证**(task-26 语义,必须写清免得误读):`IPSR != 0` 时 `dbg_console_tx_begin()` **返回 1 但并不持有凭证**(异常上下文绝不允许等待或失败)。所以四个 Fault 与栈溢出钩子的整行性来自「**Fault 优先级 -1、外设中断 ≥5、任务全冻结** ⇒ 没有第二个写者能跑进来」。唯一残留 = **被抢占任务已在移位寄存器里的那 ≤1 个字节**,它出现在故障行**之前**,不影响 `[ERR]` 前缀可 grep。已明确否决「发送循环逐字节检查让位标志」那类修法——那会把正在发的启动日志行截断,换不来什么。 -- **崩溃上报也归一(task-26)**:`Src/stm32f4xx_it.c` 的 `hard_fault_report`(四个 Fault 共用,原本 **14 段**裸发)**自己调 `HAL_UART_Transmit`**,是绕过本模块行级独占的**第三个写者**;现改为 `dbg_console_write_raw`(**但刻意不包 `tx_begin()/tx_end()`**,见下面两条 bullet)。三条理由(详见该函数 `hard_fault_write` 头):① HAL 超时判据是 `HAL_GetTick()`(TIM7 停摆则永不成立,第 8a/8b 轮实证;正常上限也是 0xFFFF ≈ 65 s),本口是 DWT/指令计数双硬上界;② HAL 状态机在僵尸通道(ORE 锁存)下恒快速失败 ⇒ **`[ERR]` 崩溃行整条蒸发**,而它正是台架「出现即不通过」的判据来源;③ `CON_UART.Instance == NULL` 时本口立即短路,绝不解引用地址 0(机器判据 = `TC-CONLOG-100`/`TC-CONLOG-102`)。同处删除输出后的 `HAL_Delay(200)`:本口等到 TC 才返回,延时冗余,且 `HAL_Delay` 也依赖 TIM7 tick——tick 停摆会**卡在复位之前**(启动段 IWDG 尚未启动,无兜底)。 -- **⚠ 行为变化:让路会改变行序与时间戳顺序(不是回归)**。让路的整行落进环形缓冲,因此它在流里**位置后移**(最多一轮 drain,实测毫秒级)。台架 2026-10-26 复验 5 次(1 次烧录 + 4 次仅复位)稳定观察到 **`[NAND_FTL] Initializing NAND FTL...` 排在 `[PROT] uncalibrated -> protection blocked …` 前一行**;而缺陷形态下的旧一手记录正好相反(PROT 在前,NAND 缺 `[20` 行首,`docs/测试说明/测试数据/boot_log_data/20261026_prod_console_interleave_evidence.txt`)。原因:谁先抢到整行所有权谁立即发完,另一方整行让路延后;常态下 `defaultTask` 的存储 bring-up 行先开火,`protectTask` 的未标定闭锁边沿要等 adcTask 首次给出标志才产生。**所以**:① **行序不是判据**——它是竞态结果,两台设备/两次启动都可能不同;② **时间戳先后也不是判据**——让路行带的是它**自己产生时刻**的时间戳(刻意不重打,见上面判据⑦),于是可能出现"后出现的那一行时间戳更早";③ 正确的判据写法是**按整行逐字节锚定**两行各自是否完整(本次台架就是这么判的,5/5 命中),不要锚定相对顺序、也不要锚定"某行必须在另一行之前"。 +- **绝不在发送路径里用内核临界区**:断言钩子要写控制台,而写控制台若进内核临界区,就形成 `taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed ⇒ dbg_console_write_raw ⇒ 再进临界区` 的静态互递归环;armlink 会把它记进每一条含临界区的任务链(实测每条链 +80 B,`iec104Task` margin 从 1,096 被顶到 1,016 < 1,024 ISR 底线)。故本模块的裸发 / 入环 / 取还凭证**一律用 PRIMASK 窗口**(`CON_CRIT_ENTER/EXIT`,机器判据 = `TC-CONLOG-015` 的 `con_crit_enter == 0`)。判据留给后来人:任何人在 `con_tx_*` / `con_ring_try` / `dbg_console_write_raw` / `dbg_console_drain` 的窗口里再放一处 `taskENTER_CRITICAL()` 或任何含 `configASSERT` 的内核 API,这条环立刻复活;提交前跑 `scripts/stack_report.py`(注明数字来自哪棵树、哪次构建)。 +- **哪些调用点包独占、哪些刻意不包**:任务态多段裸发才包(`task_watchdog.c` STALL 行);`hard_fault_report`(14 段)、`vApplicationStackOverflowHook`(3 段)、`vApplicationAssertFailed`(5 段)**刻意不包**——`IPSR != 0` 时包上是纯空转,保护来自故障优先级与任务冻结;且这三处都挂在 `configASSERT` 子树上,每加一句 armlink 就把成本乘进每条含临界区的任务链。 +- **不做成「整段进环」的原因**:那会让 `storage_bring_up()` 期间串口一个字都不出,而这段正是「死机表现为无任何输出」的窗口——等于把交错换成死机全瞎。现方案保留**胜者实时直发**,只有真撞车才让路一条行。 -- **哪些调用点包整行独占、哪些刻意不包(task-26 定案,不是遗漏)**:**任务态**多段裸发才包(现网唯一实例 = `App/util/task_watchdog.c` 的 STALL 行:既不空转,也不挂在 configASSERT 子树上)。**异常态与终点路径一律不包**——`hard_fault_report`(14 段)、`vApplicationStackOverflowHook`(3 段)、`vApplicationAssertFailed`(5 段):① `IPSR != 0` 时 `tx_begin()` 放行且**不持有**凭证、`tx_end()` 又只对持有者有效 ⇒ 包上是**纯空转**,保护本来就来自"Fault 优先级 -1 + 外设 ≥5 + 任务全冻结";② 这三处都挂在 `configASSERT` 子树上(`vPortEnterCritical/vPortExitCritical` 会进断言钩子),在那上面每加一句,armlink 就把它**乘进每一条含临界区的任务链**——实测包上时 `con_tx_release` 子树 24→152 B、发送环单趟贡献 128→208 B,`defaultTask`/`netTask`/`iec104Task` 三条链各 **+80 B**,`iec104Task` margin 从 1,096 被顶到 **1,016 < 1,024** ISR 底线(栈记账 FAIL);③ 崩溃行发完就 `NVIC_SystemReset()`,"归还所有权"没有下游读者。断言钩子那对 begin/end 还曾只包住"有文件名"一支(else 分支不在窗口内)——半套独比亚没有更误导人。 -- **⚠ 结构性失效模式(这条环是我们自己造的,随时可能复活)**:断言钩子为了留痕要写控制台,而写控制台若要进内核临界区,就形成 `taskEXIT_CRITICAL() ⇒ vPortExitCritical() ⇒ configASSERT() ⇒ vApplicationAssertFailed() ⇒ dbg_console_write_raw ⇒ …⇒ 再进临界区` 这条**静态互递归环**;armlink 不是按运行时报错,而是把它**记进每一条含临界区的任务链**——一次性污染 9 条链的栈记账,且没有任何编译期或运行期告警。为此本模块的裸发/入环/取还凭证一律用 PRIMASK 窗口(`CON_CRIT_ENTER/EXIT`,机器判据 = `TC-CONLOG-015` 的 `con_crit_enter == 0` + `TC-CONLOG-015b` 两种屏蔽水位恒 0)。**判据留给后来人**:任何人在 `con_tx_*` / `con_ring_try` / `dbg_console_write_raw` / `dbg_console_drain` 的窗口里再放一处 `taskENTER_CRITICAL()` 或任何含 `configASSERT` 的内核 API,这条环立刻复活;提交前必须跑 `scripts/stack_report.py`(且**注明数字出自哪棵工作树、哪次构建**——本仓今天两次错误归因都是拿错了树的数字)。反向风险同样要记(task-38):若哪天把 `configASSERT` 裁成空宏,每条链"回收"的 80~90 B 是**静态记账口径变化,不是固件变好**,不得写成优化收益。 -- **本笔没有消除的(别混)**:ORE/NE/FE 脏标志仍在(成因 = 控制台口 RX 使能不消费,§12——当时端口 USART1、现 UART5;用户已裁定不改 RX 配置)。本笔消除的是「崩溃日志整条蒸发」这一类,**不是**「控制台输出可能被脏标志污染」那一类,后者仍挂 §12 既有观察项。**极早期崩溃仍然完全静默**:`MX_UART5_Init()` 之前进 Fault 时本口的 NULL 短路一个字节都不发——旧写法在同场景也不发(`gState != READY` ⇒ `HAL_BUSY`),所以没有变差,但也不能写成「现在能看到早期崩溃」。 -- **实机验证缺口**:崩溃路径不能只靠静态论证(本仓最贵的教训就是「0 警告不代表安全」)。可执行验证设计见下方「验证设计」,**本笔未跑**(台架在别的任务手里)。 - -**为什么不做成"整段进环"**(曾评估):那会让 `storage_bring_up()` 期间串口**一个字都不出**,而这段正是 §「启动看门狗延后」注明的"死机表现为无任何输出"窗口(2026-10-02 那几轮就靠 bring-up 期流式日志取证)——等于把交错换成死机全瞎。现方案保留**胜者实时直发**,只有真撞车才让路一条行。 - -**验证设计(崩溃路径,两腿分工,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 至 TC-CONLOG-172`/`178`(两个生产者并发时行内不得出现别人的字节;`TC-CONLOG-178` 直接复演台架那行原文)。 -- ⚠ **「硬件上多跑几次启动照不出 IPSR」**——产品构建正常启动根本不进异常路径,跑 100 次也只是重复证明"没崩溃"。任何人想推翻或确认本节关于异常态的说法,请改主机侧套件,不要去台架上碰运气。 - -**腿 B — 实机腿:本腿含故意复位(不是常规回归)** -1. **硬前提:触发器必须由代码道提交进仓**(测试宏下编入的一条非法写即可,例如 `*(volatile uint32_t *)0x00000000U = 0U;` + 前置裸发锚点 `[FAULTINJ] arm`);**台架不得自行制造触发条件**——现场不可复现、也无法归因,且会污染"这一轮到底测了什么"的记录。 -2. 判据**只用原子性**,不碰 `[ERR]` 计数:① 崩溃行**一整行**逐字节完整(`[ERR] MEMFAULT PC=… LR=… xPSR=… R0..R3=… CFSR=… HFSR=…` 字段齐全、无邻居行字节插入);② 该行在本次启动日志里**恰好出现一次**(既不丢也不重);③ `CONSOLE dropped` = 0。首个字符应为 `[`;前面若出现 ≤1 个移位寄存器残留字节,记为已知残余,不判失败。 -3. ⚠ **不得在同一条腿要求 `[ERR]` = 0**:崩溃上报本身就以 `[ERR]` 起头,在同一路径上要求「无 `[ERR]`」等于要求这条路径别工作(本次就是这么被台架纠正的)。`[ERR]` 零命中的判据只属于腿 B 之外的**常规回归腿**(3~5 次仅复位不重烧 + 两行整行锚定)。 -4. **故意复位的注入腿不得并入「连续 N 次仅复位不重烧」的计数**:注入会主动 `NVIC_SystemReset()`,混进常规序列会把正常行为判成异常(栈溢出钩子同理)。两条腿分开跑、分开记台账。 -5. `/serial-boot-test` 在注入腿**只在命令行运行时**放宽(故障注入的既有台架口径见 `docs/测试说明/板级测试/实机测试实施指南.md` §4,其中已写明"故障注入用例会复位设备多次 → 确认设备非在用/非已部署"):放宽 `--forbidden` / `--max-boot-count`;**不改仓库配置**(改了就等于把"永不出崩溃日志"设成期望状态)。 - -### 4.5 `fputc` 重定向(MicroLIB `printf`) +### 3.5 `fputc` 重定向(MicroLIB `printf`) `fputc(ch, f)` → `con_append(&b, 1)`:库内部 / 遗留 `printf` 调用同样收敛到单写者路径(注册期逐字节入环;一条完整 `printf` 行 = 环内连续字节,写者按块发出 → 仍行级原子)。 **`printf` 是遗留路径**:逐字节 = 一次临界区 + 一次事件置位;新代码一律用 `DBG_*` 宏(调用点格式化整行、一次入环、一次事件)。两路行为一致,仅效率差异。 -### 4.6 时间参数(实测 / 推导) +### 3.6 时间参数(实测 / 推导) | 参数 | 值 | 推导 | |------|-----|------| @@ -276,31 +234,31 @@ sequenceDiagram | 单行 192B(最坏) | ≈17 ms | `DBG_CON_LINE_MAX` | | 生产者最长等待 | 200 ms(200 × 1ms tick) | ≈9 个发送块的时间,稳态下环恒被及时排空 | | 写者兜底周期 | 50 ms | 事件位失效时日志滞留上限 | -| 实测日志时延 | 事件唤醒级(无丢失) | 2026-09-25 /serial-boot-test:67 行开机日志完整、无交错、无丢行告警 | +| 日志时延 | 事件唤醒级(无丢失) | 环满才等待;稳态下日志随事件位立即发出 | -## 5. API 参考 +## 4. API 参考 全部声明位于 `App/log/console_log.h`(仅 MCU 端;模拟器无对应实现,共享代码勿调用): | 函数 | 签名 | 调用上下文 | 语义 | 备注 | |------|------|-----------|------|------| | `dbg_console_write_line` | `int (const char *line, unsigned len)` | 生产者(任务上下文) | 整行入环 / 直发;0 = 写入字节数,-1 = 参数非法 / 被丢弃 | 截断 ≤192B(保行首时间戳/标记信息) | -| `dbg_console_write_raw` | `void (const char *buf, unsigned len)` | **任意**(ISR / 钩子 / 内核前) | 有界直发 ≤256B;**整行独占发送器**(§4.4.1),抢不到独占则整行原文入环延后发 | 缓冲须已知合法;2026-10-26 起接口上取消"不加锁" | -| `dbg_console_tx_begin` | `uint8_t (void)` | 任务上下文 | 1 = 已取得整行独占(此后本任务多次 `write_raw` 段间无人插入);0 = 别人持有,**调用方照发即可** | 非阻塞、不吞字;给多段裸发的钩子收口用(2026-10-26 加,调用点改造见 task-26) | +| `dbg_console_write_raw` | `void (const char *buf, unsigned len)` | **任意**(ISR / 钩子 / 内核前) | 有界直发 ≤256B;**整行独占发送器**(§3.4.1),抢不到独占则整行原文入环延后发 | 缓冲须已知合法 | +| `dbg_console_tx_begin` | `uint8_t (void)` | 任务上下文 | 1 = 已取得整行独占(此后本任务多次 `write_raw` 段间无人插入);0 = 别人持有,**调用方照发即可** | 非阻塞、不吞字;给多段裸发的钩子收口用(**只用于任务态**,见 §3.4.1) | | `dbg_console_tx_end` | `void (void)` | 与 `tx_begin` 配对 | 归还所有权;非持有者调用为空操作(幂等) | 同任务嵌套按 `s_con_tx_depth` 递减 | | `dbg_console_writer_attach` | `void (void)` | 写者任务、主循环前 | 注册唯一写者 | 仅一次(defaultTask) | -| `dbg_console_writer_ready` | `uint8_t (void)` | 任意(常驻任务) | 写者是否已注册(1=已注册) | 2026-10-10 加:周期任务据此门控日志,避免写者注册前裸发交错(§3.4) | +| `dbg_console_writer_ready` | `uint8_t (void)` | 任意(常驻任务) | 写者是否已注册(1=已注册) | 周期任务据此门控日志,少让路一次(§2.5) | | `dbg_console_drain` | `void (void)` | **仅写者任务** | 排空环并发出 + 刷丢弃计数 | defaultTask 主循环周期调用 | | `dbg_console_dropped` | `uint32_t (void)` | 任意 | 诊断:累计丢弃行数 | 外部读取 | -| `dbg_console_tx_fault_log_take` | `uint8_t (void)` | **仅写者任务** | 取并清"TX 故障待上报"标志 | 2026-10-10 加:drain 内**不得**调 `syslog_post`(会成环),由 defaultTask 在 drain 之外补记 run.log | +| `dbg_console_tx_fault_log_take` | `uint8_t (void)` | **仅写者任务** | 取并清"TX 故障待上报"标志 | drain 内**不得**调 `syslog_post`(会成环),由 defaultTask 在 drain 之外补记 run.log | | `fputc` | `int (int ch, FILE *f)` | `printf` 调用点 | MicroLIB retarget 钩子 → 逐字节 `con_append` | 须为全局符号(C 库链接) | -## 6. 常量 +## 5. 常量 | 常量 | 值 | 定义处 | 含义 | |------|-----|--------|------| | `DBG_CON_LINE_MAX` | 192 | `console_log.h` | 单行文本上限(含结尾 `'\0'`);环形缓冲/入环截断上限;超限截断 | -| `DBG_LINE_BUF_MAX` | = `DBG_CON_LINE_MAX`(深链模块设 **128**) | `dbg_log.h` | **调用点栈上行缓冲尺寸**(2026-10-10 加)。已收紧:`nand_ftl`/`gd5f2gq5ue`/`cfg_file`/`net_cfg`/`net_socket`/`ch395f`/`time_sync`/`default_task`。取值须 ≥ 该模块最长日志行,否则该行被截断 | +| `DBG_LINE_BUF_MAX` | = `DBG_CON_LINE_MAX`(深链模块设 **128**) | `dbg_log.h` | **调用点栈上行缓冲尺寸**。已收紧:`nand_ftl`/`gd5f2gq5ue`/`cfg_file`/`net_cfg`/`net_socket`/`ch395f`/`time_sync`/`default_task`。取值须 ≥ 该模块最长日志行,否则该行被截断 | | `DBG_TS_BUF_MAX` | 24 | `dbg_log.h` | 时间戳文本缓冲(`sys_clock_get_str` 恒写 19 字符 + `'\0'`);与行缓冲**共用一个数组** | | `DBG_CON_RING_SIZE` | 4096 | `console_log.c` | 环形缓冲字节数(必须为 2 的幂) | | `DBG_CON_CHUNK_MAX` | 256 | `console_log.c` | 写者单次发送上限 = 裸发上限 | @@ -309,21 +267,21 @@ sequenceDiagram | `CON_TX_GUARD_MAX` | 20000 | `console_log.c` | DWT 不可用时的**计数**上界(每字节 / TC 各数一次;115200 下一字节约需 3000 次轮询,留 6 倍余量) | | 写者循环超时 | 50 ms | `App/task/default_task.c` | `osEventFlagsWait` 兜底周期 | -## 7. 失效模式与可观测性 +## 6. 失效模式与可观测性 | 失效 | 行为 | 可观测性 | 残余风险 | |------|------|---------|---------| | 缓冲满(日志突发 / 写者卡滞) | 生产者等 200ms 后丢行 | `s_con_drop` 累计 → 写者刷 `[ERR] CONSOLE dropped N lines`(/serial-boot-test FAIL,永不静默) | — | | 写者未注册(内核前) | 单线程直发 | — | 无(无并发) | -| 未注册期其他任务直发 | **已消除**(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] PC=… CFSR=…` 现在是单写者输出;实机验证设计见 §4.4.1 末(待台架窗口) | 极早期(`MX_UART5_Init()` 之前)崩溃仍完全静默(NULL 短路);ORE 脏标志本身未消除(§12) | -| ISR 直发撞写者发送中 | 与写者发送竞争 → 交错/丢失,不挂死(`IPSR != 0` 一律放行,§4.4.1 规则①);异常态没有第二写者(Fault 优先级 -1 + 任务全冻结),残留 ≤1 个移位寄存器字节且落在行**前** | 未计数 | ISR 日志频率极低,best-effort 语义 | +| 未注册期其他任务直发 | 整行独占(§3.4.1):抢不到独占就整行入环延后发,不丢不交错 | 让路不计数;只有"抢不到且环也满"才进 `s_con_drop` → `[ERR] CONSOLE dropped N lines` | 残余 = 让路行在流里位置后移(内容与直发逐字节相同);主机单测 `TC-CONLOG-170 至 TC-CONLOG-179` 钉死 | +| 崩溃上报(四个 Fault) | `hard_fault_report` 的 14 段裸发走 `dbg_console_write_raw`(整行独占**不**用在这里——异常态空转,见 §3.4.1);DWT/指令计数双硬上界,不依赖 tick,ORE 锁存下也不会快速失败 | 崩溃行 `[ERR] PC=… CFSR=…` 现在是单写者输出;实机验证设计见 §3.4.1 末(待台架窗口) | 极早期(`MX_UART5_Init()` 之前)崩溃仍完全静默(NULL 短路);ORE 脏标志本身未消除(§9) | +| ISR 直发撞写者发送中 | 与写者发送竞争 → 交错/丢失,不挂死(`IPSR != 0` 一律放行,§3.4.1 规则①);异常态没有第二写者(Fault 优先级 -1 + 任务全冻结),残留 ≤1 个移位寄存器字节且落在行**前** | 未计数 | ISR 日志频率极低,best-effort 语义 | | 内核状态损坏(栈溢出 / 堆耗尽) | 钩子裸发固定文本 → 复位(栈溢出)/ 仅告警(堆) | `[ERR] STACK OVERFLOW task=…` / `[ERR] MALLOC FAILED …`;任务名范围校验 + 本地拷贝,TCB 指针不入 UART | — | -| 调用方指针损坏(脏指针) | 无法进入 UART(缓冲恒为环 / 模块本地) | 2026-09-24 HardFault 路径结构性消除 | — | -| UART 物理失效(发送卡死 / 超时) | `con_tx_bounded` 在 DWT 上界内返回 `HAL_TIMEOUT` → 该块丢弃 + **UART 重初始化**(僵尸通道自愈,§12) | 2026-10-10 起:drain 内**只置标志** `s_con_tx_fault_log`,由 defaultTask 在 drain 之外 `syslog_post(SYSLOG_RUN, "console tx fault, uart reinit")` 补记 run.log(旧实现直接在 drain 里调 `syslog_post` → `write_line → con_append → drain → syslog_post → write_line` **调用图成环**,静态栈在每个任务上多叠一条日志尾巴) | 已知缺口:丢弃计数与故障计数不分离 | +| 调用方指针损坏(脏指针) | 无法进入 UART(缓冲恒为环 / 模块本地) | 脏指针到不了 UART | — | +| UART 物理失效(发送卡死 / 超时) | `con_tx_bounded` 在 DWT 上界内返回 `HAL_TIMEOUT` → 该块丢弃 + **UART 重初始化**(僵尸通道自愈,§9) | drain 内**只置标志** `s_con_tx_fault_log`,由 defaultTask 在 drain 之外 `syslog_post(SYSLOG_RUN, "console tx fault, uart reinit")` 补记 run.log(旧实现直接在 drain 里调 `syslog_post` → `write_line → con_append → drain → syslog_post → write_line` **调用图成环**,静态栈在每个任务上多叠一条日志尾巴) | 已知缺口:丢弃计数与故障计数不分离 | | `sys_evt` 对象缺失(句柄 NULL) | 唤醒空操作,50ms 兜底 | 至多 50ms 时延 | — | -## 8. 内存预算 +## 7. 内存预算 | 项 | 大小 | 位置 | |----|------|------| @@ -331,164 +289,28 @@ sequenceDiagram | `s_con_head/tail/drop/writer_up/writer_task` | ≈14 B | BSS | | 生产者行缓冲 `__con_line`(`DBG_EMIT` 宏局部) | 默认 192 B + 时间戳 24 B;**深链模块 128 + 24 B**/调用 | 调用者任务栈(入环后即释放,峰值非累积) | -**结论:静态 +4.1KB(SRAM1);FreeRTOS 堆零增量、无新任务/栈、无新内核对象。** 2026-09-25 实测:`heap free=8776B, min_ever=4816B`(CCM 堆不受影响);`__con_line` 192B 峰值在各任务既有栈预算内(任务栈按 2026-09-21 实测峰值 ×1.5 + 1KB ISR 余量定档,见 AGENTS.md 任务栈表;当日栈水位复测 6 任务全 PASS)。 +**结论:静态 +4.1KB(SRAM1);FreeRTOS 堆零增量、无新任务/栈、无新内核对象。** `heap free=8776B, min_ever=4816B`(CCM 堆不受影响);`__con_line` 192B 峰值在各任务既有栈预算内(任务栈按实测峰值 ×1.5 + 1KB ISR 余量定档,见 AGENTS.md 任务栈表)。 -> ⚠️ **2026-10-10 修正"行缓冲不贵"的结论**:行缓冲是**每个日志调用点**在栈上内联的一份数组,会进 armlink 最坏调用链。实测 defaultTask 那条最深链上 **9 个帧各含一份**(192 + 32 B)→ **约 1.5 KB 纯粹是日志行缓冲**,是该项目静态栈链的第一大项。处置 = `dbg_log.h` 新增 `DBG_LINE_BUF_MAX`(深链模块收到 128)+ 时间戳缓冲并入行缓冲(32 → 24),**未抬栈**即把该链 5312 → 2776 B。核算与前后对比见 `docs/内存管理/RAM使用分析与优化方案.md`"2026-10-10 复测"。 +> ⚠️ **行缓冲是静态栈链的第一大项**:行缓冲是**每个日志调用点**在栈上内联的一份数组,会进 armlink 最坏调用链。defaultTask 那条最深链上 **9 个帧各含一份**(192 + 32 B)→ **约 1.5 KB 纯粹是日志行缓冲**。对策 = `dbg_log.h` 的 `DBG_LINE_BUF_MAX`(深链模块收到 128)+ 时间戳缓冲并入行缓冲(32 → 24),该链 5312 → 2776 B。核算见 `docs/内存管理/RAM使用分析与优化方案.md`。 -## 9. 设计决策与否决的替代方案 +## 8. 设计决策与否决的替代方案 | # | 替代方案 | 否决 / 采纳理由 | |---|---------|----------------| -| D1 | 逐字符 `fputc` + `gState` 检查(旧方案) | 原子性退化为字节级 → 字符交错(F1);调用方脏指针直达 UART(F2);并发立即 `HAL_BUSY` → 静默丢行(C3) | +| D1 | 逐字符 `fputc` + `gState` 检查(旧方案) | 原子性退化为字节级 → 字符交错;调用方脏指针直达 UART;并发立即 `HAL_BUSY` → 静默丢行 | | D2 | 整行 `HAL_UART_Transmit` + `gState` 检查 | 消除交错,但不解决脏指针(调用方缓冲仍入 UART);竞争失败方无等待/重试,整行静默丢;调用方阻塞时长不受控(等 UART 物理速率) | | D3 | 专用日志任务 | 违反 G3(RAM 紧张不新增任务/栈);defaultTask 本就是存储子系统唯一写者,单写者纪律收敛到同一任务,且主循环已是 50ms 事件驱动 | | D4 | TX DMA / 中断驱动发送 | 发送率低(115200、日志行稀疏),收益不足;不解决 G2(DMA 同样读调用方缓冲);DMA 缓冲可达性 / 所有权 / 双缓冲复杂度不值 | -| D5 | 环 + 直发双路径(**采纳**) | 注册稳态走环(零丢失、有计数);内核前 / ISR / 钩子走有界直发(G4:不等待、不死锁、零内核对象);未注册期此路径**无互斥**(2026-10-10 起为寄存器级轮询,无 `HAL` `gState`)→ 约定**只有 defaultTask** 记日志,其他常驻任务用 `dbg_console_writer_ready()` 门控(§3.4) | +| D5 | 环 + 直发双路径(**采纳**) | 注册稳态走环(零丢失、有计数);内核前 / ISR / 钩子走有界直发(G4:不等待、不死锁、零内核对象);未注册期此路径**无互斥**(寄存器级轮询,无 `HAL` `gState`)→ 约定**只有 defaultTask** 记日志,其他常驻任务用 `dbg_console_writer_ready()` 门控(§2.4) | | D6 | 每生产者 SPSC 无锁队列 | 多生产者仍需 MPMC;单环 + 临界区整行拷贝更简,生产者临界区 ≤192B 微秒级;事件位复用免新增内核对象 | | D7 | `printf` 保为主路径 | MicroLIB 逐字符回调开销 + 遗留语义;`DBG_*` 宏调用点格式化整行一次入环(一次入环、一次事件) | -## 10. 验证记录 +## 9. 当前限制 -| 日期 | 验证 | 结果 | -|------|------|------| -| 2026-09-24 | HardFault 定位:BFAR=0x10010000(=CCM 末尾+1),PC 在 `HAL_UART_Transmit` 缓冲读取处;崩溃前交错行留证 | 根因确认(F1/F2)→ 单写者加固实施 | -| 2026-09-25 | `@build`(Keil MDK v5 / ARMCC C99) | **0 Error, 0 Warning**(`console_log.c` 注册于 App 组) | -| 2026-09-25 | `@test` 硬件闭环(烧录 → 开机日志采集 → 标记校验) | **PASS**:Required 3/3(`App init OK` / `Network Init` / `net init: OK`);Forbidden 5/5 缺席(`FAIL`/`ERROR`/`HardFault`/`assert`/`[ERR]`,大小写不敏感);67 行开机日志完整、无交错、无丢行 | -| 2026-09-25 | 静态门禁:C 代码规范 V1.0 + U+FFFD 乱码 + cppcheck(warning, performance, portability, style) | 0 违规 | -| 2026-09-25 | 迁移验收:实现迁出 `Src/usart.c`(该文件恢复 478 行纯 CubeMX 形态),双端行为不变 | `@build` 0/0 + `@test` 复测 PASS;引用关系图重生成 | - -## 11. 实施时间线 - -| 日期 | 事件 | -|------|------| -| 2026-09-22 | 链路延迟测试:开机并发 `printf` 丢行(连续 4 行)实证 → S8 非阻塞日志立项(`链路延迟测试说明.md` §10.2) | -| 2026-09-24 | 字符交错 + HardFault 实证 → 单写者加固实施(4KB 环 + defaultTask 唯一发送 + 裸发回退 + `fputc` 收敛);当时位于 `Src/usart.c` USER CODE 区 | -| 2026-09-25 | 迁出至 `App/log/console_log.{h,c}`(HAL 生成文件不含应用代码);`dbg_log.h` 改 `#include "console_log.h"`;Keil 工程 App 组注册;`@build` 0/0、`@test` PASS、引用关系图与本文档同步 | -| 2026-09-25 | 写者任务体迁出:`Src/freertos.c` 的 `StartDefaultTask` 任务体(存储 bring-up / 配置引擎 / 控制台 attach+drain / syslog / LED / 栈水位)整体移入 `App/task/default_task.c`(`default_task_func()`,与其余 5 任务文件同址);`freertos.c` 恢复"接线 + 崩溃钩子"的 CubeMX 形态(719→361 行);行为零变化,`@build` 0/0 + `@test` PASS 复验 | -| 2026-10-02 | **ORE 僵尸通道喂狗节奏保护**(台架 gen-288):round-3 的"单次 drain 预算 250ms"只限单次调用——ORE 锁存在外设软复位后**毫秒级重锁**(PA10 悬空 + RX 使能),背靠背 drain 每轮再耗 100ms 块超时 → 主循环周期拉到 450~500ms → 100ms 节流的喂狗抽样间隙超过 IWDG 最坏容限 1.7s → 停喂 → 误复位(且心跳位全员健康、无 stall 日志,纯**节奏性饥饿**,非死等)。修复:发送故障后立即结束本轮 drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),保证喂狗节奏 ≤ ~300ms | -| 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 与 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 台架复测)。修法 = 非阻塞行所有权(`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 | **纪律盲区台架实证(台架复测)**:`protectTask`(保护任务 接线,台架腿当时未跑)启动锚点行在写者注册前裸发 → 与 defaultTask 启动日志**逐字节交错**(`st[art: cycle=220ms elm=70`,两次开机同形复现 = 启动调度确定性的竞态,非 USB 噪声)→ `@test` 锚点 MISS。修复 = 启动行改 `dbg_console_writer_ready()` 首拍补打(adcTask 首帧同款),`@build` 0/0 + `@test` PASS 复验(锚点完好 +577 ms)。§3.4 纪律示例更新 | -| 2026-10-29 | **端口迁移**:控制台从 USART1 迁到 **UART5(RS232#1,PC12 / PD2,经 U9 → U13 → CN1)**,`console_log.c` 收口成 `CON_UART*` 单点宏(句柄 / IRQn / 时钟 / 重初始化一体);USART1 现仅服务屏板链路(700 kbaud,`屏板链路设计.md`);台架日志口因此为 `COM3`(USB 转 RS-232 适配器 → CN1 端子,`.serialtest/config.json` 已配;接法见 §3.4) | - -## 12. ORE 僵尸通道(2026-10-02 台架实证;当时端口 USART1,现 UART5 同机理) - -**机理**(当时端口 USART1;控制台现走 UART5,同一机理):控制台口使能了 RX 却无人消费 → 线上噪声或对端字节产生帧错误/溢出 → -**ORE(Overrun Error)置位** → HAL 发送路径卡在错误状态。对外设做软复位后 ORE -**毫秒级重锁**,于是每轮 drain 都会耗掉 100ms 量级的块超时。 - -> ⚠ **2026-10-28 更正(用户反馈)**:上面"PA10 悬空、无上拉"是**归因推断,不是实测**—— -> 用户确认 PA10 **一直是接线的**。而 ORE 的**充分条件本来就不依赖引脚悬空**: -> RX 使能了却从不读 `DR`,线上任意一个字节或一个噪声沿使 `RXNE` 置位后无人读, -> 下一个事件即锁存 ORE;ORE 只能靠"先读 SR 再读 DR"清除 ⇒ **永不自愈**。悬空只是提高发生率。 -> -> 因此本节的**观察与缓解措施仍然成立**(ORE 确实锁存、软复位后确实毫秒级重锁、drain 确实被拉长), -> 但**归因需要改写**为"RX 使能却不消费 DR",不是"引脚悬空"。 -> -> 当年那场风暴的真因仍待定:接线正常时更常见的是**对端波特率不匹配**(适配器不是 115200) -> 或适配器 TX 在 USB 掉线时浮空。下次复现先量适配器 TX 的空闲电平与波特率, -> 别再默认"引脚悬空"。 - -**为什么会被误判**:本设计是"心跳位全员健康、无 STALL 日志"的**纯节奏性饥饿**—— -没有任何任务真的死等,只是主循环周期被 drain 拉长到 450~500ms,而喂狗抽样是 100ms -节流的、IWDG 最坏容限仅 1.7s,间歇被拉长就触发停喂。**看日志会以为"系统很健康"**。 - -**已做(缓解)**:单次 drain 250ms 预算(第 3 轮)+ 发送故障后 300ms 冷却(第 6 轮) -+ 超时判据换 DWT(第 8b 轮台架)。 - -**已裁定不做(2026-10-26 用户决定)**:让 USART1 变成**只发不收**(`.ioc` 里不使能 RX、PA10 不再复用为 -`USART1_RX`)确实能从机制上消除 ORE,但它是 `.ioc` 结构性改动 + CubeMX 重生成——要清空全部工作树、 -独占一轮、复跑台架闭环,代价不抵收益。**现行缓解即长期方案**(单次 drain 250 ms 预算 + 发送故障后 -300 ms 冷却 + DWT 时间源无关的超时判据)。2026-10-29 起控制台在 **UART5(RS232#1)**,RX 使能未消费, -本节机理与缓解在新端口原样成立;USART1 的 RX 已由屏板链路(`sb_link`)消费,与本节无关。 - -## 13. 开放问题 - -| # | 项 | 说明 | -|---|----|------| -| O1 | 未注册期裸发无互斥 —— **已处置(2026-10-26,v1.6)** | 启动窗口(内核启动 → attach)`dbg_console_write_raw` 无互斥 → 多任务并发**逐字节交错**。两个台架实例:2026-10-12 台架复测(`protectTask` 启动锚点,靠自愿门控修)与 2026-10-26(`protectTask` 未标定闭锁边沿 × `defaultTask` `[NAND_FTL]` bring-up 行,字节账见 §4.4.1)——**第二个实例证明自愿门控本身不可靠**(边沿留痕无法预知自己落在哪个窗口)。现由机制收口:非阻塞整行独占 + 让路行入环延后发(§4.4.1),`dbg_console_writer_ready()` 降级为延迟优化。曾评估的两个替代方案(提前 attach / 整段进环)均因"把等串口塞进存储时序敏感段"与"死机期全瞎"被否,理由记录在 §4.4.1 末。**残余**:`IPSR != 0` 放行仍可能插入他任务半行(不构成标记漏检,§7);多段裸发钩子的段间缝 → task-26 | -| O2 | `HAL_UART_Transmit` 超时未计数 —— **已处置(2026-10-24,v1.5)** | 三本账已分离:`dbg_console_dropped()` = **真没落地**的数(缓冲满丢的一行,或没能确认送达而出环的一个块);`dbg_console_tx_fault()` = 外设**软复位自愈次数**(语义不变);新增 `dbg_console_tx_timeout()` = 写者排空时看到的**块超时次数**(TXE 与 TC 两条硬上界都算)。**口径边界**:裸发路径(内核前 / ISR / 两条告警行自身)的超时**不进**超时账——裸发没有「块」的概念,混进来读数失去意义。**尚未接线**:本读口暂无生产调用者;候选位置 `App/util/task_watchdog.c` 的 `STALL` 行是**被解析的证据行**,加字段要连解析端与台架判据一起改 → 列为后续项 | -| O3 | `printf` 逐字节开销 | 遗留 `printf` 路径 = 逐字节临界区 + 逐字节事件置位;遗留调用点全部切 `DBG_*` 宏后开销自然消失(不改变机制) | -| O4 | **ORE 僵尸未根治** | §12:现仅有缓解(250ms 预算 / 300ms 冷却 / DWT 超时),机制根源是控制台口(现 UART5)使能 RX 不消费。根治 = `.ioc` 改只发不收 + CubeMX 重生成,**2026-10-26 用户裁定不做** → O4 是**长期现状**而非待办(见 §12) | - -## 14. 相关文档 - -- `项目架构总览.md` —— 六层分层 / 启动时序 / 6 任务(写者任务与就绪屏障位置) -- `运行与自检记录设计.md` —— sys_log 持久化日志(姊妹模块:同写者任务、不同通道) -- `docs/测试说明/专项验证/链路延迟测试说明.md` —— D6 日志税 / 步骤 8 非阻塞日志(需求出处与 09-22 丢行实证) -- `docs/测试说明/流程与规范/自动化测试流程.md` —— `/serial-boot-test` 标记体系(Required / Forbidden 模式,`[ERR]` 语义) -- AGENTS.md —— 平台约束(内核前禁内核对象 / C 堆禁令 / 事件位语义 / 内存预算) - -## 附录:日志静默观察项 × 步骤 8 合并评估(2026-10-25,道F 第二轮 / task-18,零代码改动) - -> 评估任务(**日志静默观察项**,已销项;残余动作是「与步骤 8 合并评估」):该观察项 -> (49s 全系统日志静默,2026-09-22 L3 台架世代号 181)与步骤 8(日志非阻塞化,需求 -> 出处 = `docs/测试说明/专项验证/链路延迟测试说明.md` §9.2/§10.2)的合并性价比判定。 -> 评估纪律 = 结论即交付,不顺手重构;本节结论:**无需最小实现**(不新增代码)。 - -### A.1 步骤 8:目标已由「单写者形态」在**生产者侧**达成 - -步骤 8 要治的是阻塞式日志税(L10 实测 `logtax_line` **4402 µs/行**,`fputc` 逐字节 -@115200 阻塞调用方整行)与并发丢行(实测 `docs/测试说明/专项验证/链路延迟测试说明.md` §9.2: -必检标记被竞态吞)。2026-09-24 单写者 -改造正是同一需求落的方案(§1/§2 G1-G5): - -| 步骤 8 子项(需求出处 `docs/测试说明/专项验证/链路延迟测试说明.md` §10.2 第 9 条) | 原案 | 现状(代码实证) | -|---|---|---| -| `fputc` → 环形缓冲 | 串口 TX 环形 | ✅ 4KB BSS 环 + 事件位唤醒(§4.1/§4.2);生产者 = 亚微秒~µs 级临界区整行入环(≤192B 拷贝),日志税较 4402µs/行**降 3 个量级** | -| TX 中断/DMA | 非阻塞发送 | ❌ 有意不做(轮询单写者,D4);复议时机见 A.2 | -| 溢出丢弃策略 | 待定义 | ✅ 有界等待 200ms(K4)→ 丢弃 + `dropped` 账 → `[ERR] CONSOLE dropped`(永不静默,§4.2/§7) | -| HardFault 冲刷 | 待处理 | ✅ 有界裸发 ≤256B(G4/§4.4,DWT/计数硬上界,不依赖 tick、零内核对象) | - -热路径日志税另已由 D6 单独削掉(删 per-recv 日志,`queue_rt_recv` 5997→1223µs)。 -**结论:步骤 8 按“生产者不阻塞业务”的意图已闭环**;剩余的“阻塞”只在写者任务 -(defaultTask)自身——drain 预算 250ms + 故障冷却 300ms + DWT 硬上界(§11/§12), -**不在业务任务关键链上**,且预算值按“让位 800ms 停喂容忍”定档,属设计内行为。 - -### A.2 TX 中断/DMA(原案后半):维持否决 - -D4 否决理由对现状仍逐条成立(115200 发送率低、日志行稀疏;IT/DMA 引入新内核 -对象、缓冲所有权/双缓冲复杂度、DMA 可达性清单)。三点补充判断: - -1. 把 drain 拖长的真凶是 **ORE 僵尸通道**(当时 USART1 的 PA10 悬空 + RX 使能不消费——现 UART5 同机理见 §12,软复位后毫秒级重锁 → 每轮块超时 - 100ms 级);根治办法(`.ioc` 改只发不收)已于 2026-10-26 裁定不做 → **该形态长期存在**,靠缓解措施兜住; -2. 根治前后数据已可机读:`dbg_console_tx_timeout()`(§13-O2 三本账,v1.5 起) - 就是为“IT/DMA 还剩多少收益”留的量尺; -3. 根治既已裁定不做,TX 路径的形态就是终态:动它不再需要"等根治",但必须拿 `tx_timeout` 的实测收益说话。 - -**结论:不采。**原复议触发条件绑定根治项,已随 2026-10-26 裁定不做而失效;将来若再议 TX 中断/DMA,直接以 `dbg_console_tx_timeout()` 的实测增量为依据。 - -### A.3 日志静默观察项:观察窗收敛到 V5 复跑;“真停滞 vs 无源”判别成立 - -- 判据②(静默 → 可观测)= **已具备**:控制台三本账 `dbg_console_dropped / - dbg_console_tx_fault / dbg_console_tx_timeout` + syslog 的 - `queue full, drop` / `preboot full, drop` / `hold-over drop` - (`App/log/sys_log_mcu.c`);任何“静默丢行”现形为 `[ERR]` → `/serial-boot-test` FAIL。 -- 判据①(复现 49s 并定位):V5 30min 未复现;实测(`docs/测试说明/板级测试/实机测试实施指南.md` §7.2) - 运行期 >10s 静默 24 处**全部落在 e02_probe 窗口、长度 ≈28s**——其 - 同源阻塞机制(`drop_conn` 同步占用 netTask 400-600ms)已随登记式拆链改造修复 - (登记式拆链,`docs/网络/网络自愈架构设计.md` §4.6)。本评估补充一个此前 - 缺位的替代解释:**探针窗内的静默很可能本来就是“无日志源”的正常形态**—— - 探针一轮(挂起 30s + 短暂复连)业务日志天然稀疏,defaultTask 无周期串口输出 - (LED 静默;`[STACK]/[PROF]` 仅新峰值/条件满足才打印),~28s ≈ time_sync - 两条 INFO 之间的静默带,不是排空停滞。 -- **若再现的判别口径**(已具备,无需新增):静默段前后对照三本账与 syslog - 丢弃账——账目零增量且静默后无 `[ERR] CONSOLE dropped` = **无源**(正常, - 不开卡);账目有增量或出现 dropped 行 = **真停滞**(开卡,按 §12 ORE 与 - 冷却路径核对)。 -- **处置建议**:该观察项保留为低风险观察,至拆链修复版(merge 969fc5c)的 V5 - 复跑(判据 = 运行期 `[WD] task stall` 与非开机 `MISS start` 双零,台架由 - lead 排窗)达成后销项;本附录即销项结论落点。排期相应两行与波次行 - 的清理归单写者(lead)。 - -### A.4 评估后的遗留面(全部既有,无新增) - -| 项 | 归属 | 处置 | -|---|---|---| -| ORE 僵尸根治(改只发不收) | §12 / §13-O4 | **2026-10-26 用户裁定不做**;量尺 `dbg_console_tx_timeout()` 保留,将来重评直接用 | -| `tx_timeout` 读口接线(STALL 证据行加字段) | §13-O2 | 涉 `App/util/task_watchdog.c` 与被解析证据行的台架判据,非本卡写域;维持 O2“后续项” | -| 未注册期裸发互斥 / `printf` 遗留开销 | §13-O1/O3 | 现状 = 纪律已落地,开机日志密度上升再议 | -| 日志静默观察项 | 曾登记(已销项) | 见 A.3:V5 复跑双零后销项(归 lead) | +| 限制 | 现状 | +|---|---| +| 控制台口 RX 使能但不消费 | UART5 RX 使能,固件从不读 `DR` → 线上任意字节或噪声沿即锁存 ORE,且 ORE 只能靠「先读 SR 再读 DR」清除、**永不自愈**。缓解 = 单次 drain 250 ms 预算 + 发送故障后 300 ms 冷却 + DWT 周期计数硬上界(时间源无关,TIM7 停摆也不影响)。根治 = `.ioc` 改只发不收 + CubeMX 重生成,**已裁定不做**(结构性改动 + 独占一轮台架,代价不抵收益),故按长期现状对待。观察要点:`sys_evt`/`ipc` 与 CPU 均健康、心跳位全绿,唯一症状是主循环周期被 drain 拉长到 450–500 ms,与「任务死等」同形不同因 | +| 极早期崩溃完全静默 | `MX_UART5_Init()` 之前进 Fault,本口 NULL 短路一个字节都不发 | +| ISR 直发与写者发送竞争 | `IPSR != 0` 一律放行(不允许阻塞),best-effort:可能与写者发送中的块交错、也可能丢,但不会挂死;本口 ISR 日志频率极低 | +| 发送块超时未进 STALL 证据行 | `dbg_console_tx_timeout()` 读口已存在,但 `App/util/task_watchdog.c` 的 STALL 行尚未接该字段——接进去要连解析端与台架判据一起改 | +| `printf` 逐字节开销 | 遗留路径 = 逐字节临界区 + 逐字节事件置位;新代码一律 `DBG_*` 宏,遗留调用点切完后开销自然消失 | diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 7eb11211..527b5b71 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -473,7 +473,7 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗 |---|---|---| | R1 | **启动段无保护**(复位 → `task_wd_start()` 约数秒) | 接受;表现为"无输出 + LED 不闪",人工辨识。要覆盖需"短超时 + 手动多喂",复杂度收益比不划算 | | R2 | **LSI 精度差** | 已按 47 kHz 最坏值校验,停喂到复位仍留 ~900 ms | -| R3 | **喂狗节奏饥饿** | 控制台 drain 曾把主循环拉长导致误复位(2026-10-02 台架日志代次 288)。现缓解 = drain 单次 250 ms 预算 + 发送故障 300 ms 冷却;**结构根治(USART1 改只发不收)2026-10-26 已裁定不做 → 现行缓解即长期方案**(机制详述 `控制台单写者日志设计.md` §12) | +| R3 | **喂狗节奏饥饿** | 控制台 drain 曾把主循环拉长导致误复位(2026-10-02 台架日志代次 288)。现缓解 = drain 单次 250 ms 预算 + 发送故障 300 ms 冷却;**结构根治(USART1 改只发不收)2026-10-26 已裁定不做 → 现行缓解即长期方案**(机制详述 `控制台单写者日志设计.md` §9) | | R4 | **NAND 真实卡死时 FTL 直接喂狗** | 设备存活但存储功能冻结(与 ORE 自愈同类边界)。若要求"NAND 死也必须复位",需给 `nand_ftl_wd_feed()` 加次数/时限上限 | | R5 | **栈溢出与断言在复位原因上不可区分**(都是 `SFTRSTF`) | 可接受;串口分别打 `[ERR] STACK OVERFLOW` / `[ERR] ASSERT FAILED`,`selftest.log` 侧另有记录 | | R6 | **`RCC_CSR` 读后即清** | 已在 `main.c` 统一处理;漏清会让下次软复位读回上次标志 | @@ -493,7 +493,7 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗 看门狗是本项目**第一次**把 CubeMX 生成流程引入可靠性关键路径,由此沉淀出与看门狗本身无关的通用方法论。`AGENTS.md`「平台约束」指向本节,改动 `.ioc`/`Src/`/`Inc/` 前请先读。 -> **2026-10-26(道G task-26)一句话登记,机制不在本文复述**:四个 Fault 与栈溢出 / 断言钩子的**崩溃上报发送口已归一**进控制台单写者(`dbg_console_write_raw` + 一次整行独占,不再自行调用 `HAL_UART_Transmit`),机制正文与残余见 `docs/子系统设计/控制台单写者日志设计.md` §4.4.1。本节只关心一件事:这些实现仍须整体待在 `Src/freertos.c` 的 `USER CODE BEGIN 4/5` 区与 `Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 0` 区(§10.2 的「搬进 USER CODE 区」条),改完跑 `scripts/check_cubemx_survival.py`(§10.3)。同族另一笔只登记指针:断言钩子与临界区在控制台路径上互达会构成**幻影环**,armlink 的 `In Cycle` 记账随环形状波动(同一 port.o 三棵树实测每链差 0~80 B,休眠≠解耦),三树取证、量化表与「configASSERT 裁空宏 ≠ 优化收益」的钉死句见 `docs/内存管理/RAM使用分析与优化方案.md` §4.0.7(道G task-38)。 +> **2026-10-26(道G task-26)一句话登记,机制不在本文复述**:四个 Fault 与栈溢出 / 断言钩子的**崩溃上报发送口已归一**进控制台单写者(`dbg_console_write_raw` + 一次整行独占,不再自行调用 `HAL_UART_Transmit`),机制正文与残余见 `docs/子系统设计/控制台单写者日志设计.md` §3.4.1。本节只关心一件事:这些实现仍须整体待在 `Src/freertos.c` 的 `USER CODE BEGIN 4/5` 区与 `Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 0` 区(§10.2 的「搬进 USER CODE 区」条),改完跑 `scripts/check_cubemx_survival.py`(§10.3)。同族另一笔只登记指针:断言钩子与临界区在控制台路径上互达会构成**幻影环**,armlink 的 `In Cycle` 记账随环形状波动(同一 port.o 三棵树实测每链差 0~80 B,休眠≠解耦),三树取证、量化表与「configASSERT 裁空宏 ≠ 优化收益」的钉死句见 `docs/内存管理/RAM使用分析与优化方案.md` §4.0.7(道G task-38)。 ### 10.1 覆盖史(三次重生成) diff --git a/docs/架构设计/硬件规格与能力边界.md b/docs/架构设计/硬件规格与能力边界.md index 64634e34..450033a2 100644 --- a/docs/架构设计/硬件规格与能力边界.md +++ b/docs/架构设计/硬件规格与能力边界.md @@ -35,7 +35,7 @@ | **USART1(U5 直连 TTL)** | UART | PA9 TX / PA10 RX | **700000 8N1 → 屏板链路**(TTL 直连、无 DE,`../产品需求/产品需求设计文档.md` §5.16.2);PA10 双向均已接线;日志已迁 UART5 | | **UART4(RS-485)** | UART | PC10 TX / PC11 RX / **PA15 DE** | 半双工通信(V0.1 轮换后 RS485#4 = UART4,`../硬件平台/V0.1_网表详细分析报告.md` §4.2) | | **USART2(RS-232 #3)** | UART | PD5 TX / PD6 RX | 经 U10 隔离 → U13 TPT3232E → CN1;固件目前未占用(日志实际走 UART5/RS232#1) | -| **UART5(RS-232 #1)** | UART | PC12 TX / PD2 RX | 经 U9 隔离 → U13 → CN1;**日志口**(115200,`/serial-boot-test` 采集通道;台架端子 `../子系统设计/控制台单写者日志设计.md` §3.4) | +| **UART5(RS-232 #1)** | UART | PC12 TX / PD2 RX | 经 U9 隔离 → U13 → CN1;**日志口**(115200,`/serial-boot-test` 采集通道) | | **状态 LED ×6** | GPIO | PC5 / PB0 / PB1 / PB2 / PF11 / PF12 | **低电平点亮** | | SWD | — | PA13 SWDIO / PA14 SWCLK | V0.1 已修复 V0.0 的 SWCLK 引脚缺陷 | | 调试口 | — | CN2(XH-3A,1=SWDIO/2=SWCLK/3=GND) | V0.1 由 H2 排针改为 XH 端子 | diff --git a/docs/测试说明/专项验证/模拟器差距清单.md b/docs/测试说明/专项验证/模拟器差距清单.md index 2c745f14..a330a485 100644 --- a/docs/测试说明/专项验证/模拟器差距清单.md +++ b/docs/测试说明/专项验证/模拟器差距清单.md @@ -51,7 +51,7 @@ | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | |---|---|---|---|---| -| **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `/serial-boot-test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计.md` §4.1 | +| **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `/serial-boot-test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计.md` §3.1 | | **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计.md` §3.3 | | **无应用回调(空操作)** | 模拟器不注册段回调,`cfg_file_poll` 重载后回调为空 | 各段回调真正执行(如网络重配置) | "配置变更 → 触发回调 → 副作用"链路在模拟器上**只验证到写盘**,回调逻辑不覆盖 | `/serial-boot-test`(配置保存 → UI 生效) | | **「清除记录」两端时序不同** | `syslog_request_clear()` 在调用内**同步**删除宿主文件(`sim_syslog.c` 单线程,没有卷锁、没有队列),返回后状态立刻是 `OK`/`FAIL`,`SYSLOG_CLEAR_WAIT` 永不出现 | 非写者上下文只置标志,真正的 `f_unlink` 排在 defaultTask 的 `syslog_drain()` 尾部(卷锁 try:拿不到锁就顺延,见 `App/log/sys_log_mcu.c`) | 「按了按钮 → 下一拍才见结果」这条异步链、卷占用顺延、以及删除与写者重建的交错,在模拟器上**结构上不存在**;UI 页面若把 WAIT 当失败,模拟器上看不出来 | 主机单测 `test/host/sys_log_mcu/`(TC-SYSLOG-110..122:请求侧零盘触点 / 落地只在 drain / 卷占用顺延 / 幂等 / 不误删;TC-SYSLOG-133/133b:清除成功投一条操作记录、清 oper 自己时不投)+ 台架装置维护页目检 | diff --git a/docs/测试说明/专项验证/链路延迟测试说明.md b/docs/测试说明/专项验证/链路延迟测试说明.md index 7659e2f5..4aff0547 100644 --- a/docs/测试说明/专项验证/链路延迟测试说明.md +++ b/docs/测试说明/专项验证/链路延迟测试说明.md @@ -709,7 +709,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 9. **日志非阻塞化:已实施·形态变更(生产者侧 2026-09-24 单写者;2026-10-25 与日志静默观察项合并评估收口)**: `fputc` → 环形缓冲**已落地**(4KB 环 + 单写者 + 事件位;4402µs/行 → µs 级入环, 溢出 = 有界 200ms + dropped 账 + `[ERR]` 留痕;冲刷 = 有界裸发,见 - `../../子系统设计/控制台单写者日志设计.md` §4);**TX 中断/DMA 维持否决** + `../../子系统设计/控制台单写者日志设计.md` §8);**TX 中断/DMA 维持否决** (该文档 D4),原绑定的复议条件(根治项落地后实测 `tx_timeout`)已随 2026-10-26 裁定不做而失效——日志静默观察项 × 日志单写者化合并评估全文 结论见同文档文末附录; 10. **S9** 客户端侧(DTU-RemtoeLCD):按键后立即取帧、不传 PNG(直接发 1bpp/差量 + canvas 绘制)、 @@ -723,7 +723,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 + 中断/DMA,溢出丢弃策略需定义)。 **现状(2026-10-25)**:根治已随单写者落地——行级原子 = 临界区整行入环、单写者发出, 交错/丢行结构性消除;「写者注册前」窗口为裸发例外,纪律 = `dbg_console_writer_ready()` - 门控(`../../子系统设计/控制台单写者日志设计.md` §3.4)。 + 门控(`../../子系统设计/控制台单写者日志设计.md` §2.5)。 --- diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index 846506b4..eb8b110c 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -507,7 +507,7 @@ uint16_t ch395f_link_change_count(void); | 6 | `App/log/soe_store.c` 函数头 ④ | 注释写"打开/写入结构性错误 → `unlink` 重试一次(同 run.log)",实现里 `f_open` 失败**直接 return -1、不重试**(只有 `f_write` 失败才重试);且 `run.log` 侧 `s_append_rec` 的重试码集合还含 `FR_INVALID_NAME`/`FR_NO_PATH` → 两套自愈集合不一致。属"注释夸大实现"还是"实现少一半"待裁 | TC-SOESTORE-022 | | 7 | `App/cfg/cfg_file_store_mcu.c:50` | 函数头写"未挂载时 `f_open` 返回 `FR_NO_SYSTEM`"——**`FRESULT` 里没有这个码**(`Lib/FatFs/ff.h` 共 20 个码,未挂载实际是 `FR_NOT_READY`/`FR_NO_FILESYSTEM`)。只在注释里不参与编译,危害是后人照它写判据会写出永不成立的分支 | —(注释面) | | 8 | `App/log/console_log.c:317` vs `:285` | 冷却起点用 `s_con_cool_until = (uint32_t)HAL_GetTick()`,而判据拿 `!= 0U` 当哨兵 → 故障恰好落在 `HAL_GetTick()==0` 那一拍(开机头 1 ms,或 49.7 天回绕到 0)时**冷却等于没记**,下一轮 drain 立刻再耗一次 100 ms 块超时。撞的正是控制台域那条已登记的"背靠背块超时拖长主循环 → 喂狗节奏饥饿"。建议:另用一个"在冷却"标志,别拿 tick 值当哨兵 | TC-CONLOG-081 / 082(按现状钉) | -| 9 | `App/log/console_log.c:394~412` | `con_append` 的**空位判据在 `taskENTER_CRITICAL` 之外**(check-then-act)。生产者按 `docs/子系统设计/控制台单写者日志设计.md` §3.1 是"全部任务 + 内核前主线程 + ISR",而同文档 §4.1 的 head/tail 行写作"单生产者/单写者"——**两处口径互斥,后者需要更正**。多生产者可在环 >95% 满时同时过判据 → `head` 各自推进后合计越过容量 → `con_used()` 回绕变小 → 写者少发一段,被覆盖的行**既不打 `[ERR]` 也不进 `dropped`**(静默损坏;索引恒取模,不会越界)。**这条没有用例、只报推断**:单线程宿主构造不出真抢占点(把注入点挂在 `taskENTER_CRITICAL` 上已在区内,不算抢占点) | —(无用例,仅登记) | +| 9 | `App/log/console_log.c` 入环判据 | 已收敛:`con_append` 的"复核空位 + 整行拷贝 + 推进 head"现在同处一个 `PRIMASK` 窗口(`con_ring_try()`),check-then-act 的抢占点不存在了;生产者口径也统一写在 `docs/子系统设计/控制台单写者日志设计.md` §3.1(多生产者 / 单写者),与原登记时两处口径互斥的情况不符。**环 >95% 满时多生产者同时过判据**这条推断随之失效,无需再单独立项;`con_used()` 回绕变小只剩"写者少发一段"这一个观察面 | —(已收敛,留作回归时核对窗口边界) | | 10 | `App/log/console_log.c:181~183` + `:306~331` | `huart1.Instance == NULL`(HAL 早期 / 断言-故障上报路径的专用短路,返回 `HAL_ERROR`)在 drain 里被当成**僵尸通道**走 `con_uart_reinit()` → `MX_USART1_UART_Init()`(含 MspInit 的 RCC/GPIO 重配)。当前接线顺序(`MX_USART1_UART_Init` 早于 `dbg_console_writer_attach`)挡住了,但"未初始化"与"通道卡死"共用一条自愈支路,attach 时机一旦被提前就会在 HAL 未就绪时重配外设 | TC-CONLOG-103(钉现状) | ### 第 1 轮修复处置(2026-10-24,同日;生产码改动 + 用例翻正向) diff --git a/docs/测试说明/板级测试/实机测试实施指南.md b/docs/测试说明/板级测试/实机测试实施指南.md index cb646fbe..63ed57a1 100644 --- a/docs/测试说明/板级测试/实机测试实施指南.md +++ b/docs/测试说明/板级测试/实机测试实施指南.md @@ -10,7 +10,7 @@ > **约定**:本文事项完成后**先销上游登记**,再把结论写进正文对应小节(本文不再记流水,过程查 `git log`); > 结论与证据沉淀进对应模块文档/问题记录(2026-10-02 台架窗口的完整取证链—— > IWDG PR/RLR 静默错配 / dhara resume 回滚 / ORE 僵尸通道——见 `看门狗设计说明.md` 2026-10-02 变更记录、 -> `STM32F4-Base存储架构说明.md` §5.4.1、`控制台单写者日志设计.md` §12;修复已随 `930215f` 提交)。 +> `STM32F4-Base存储架构说明.md` §5.4.1、`控制台单写者日志设计.md` §9;修复已随 `930215f` 提交)。 --- @@ -467,7 +467,7 @@ CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创 | **A2** 网络重建失败恢复 | ✅ 已闭环(正常路径零回归;故障分支见「网络负向场景台架验证」) | `网络/Net层阻塞模型架构设计.md` | | **A3** CH395F 字节命令超时 | ✅ 已闭环(正常路径零回归;故障分支待专项窗口) | — | | **A4** `remo_send` 返回值 | ✅ 已闭环(正常路径零回归;推流压测待专项窗口) | — | -| 2026-10-02 台架取证链(IWDG PR/RLR 错配 / dhara resume 回滚 / ORE 僵尸通道) | ✅ 已修 | `子系统设计/看门狗设计说明.md` 2026-10-02 变更记录 · `架构设计/STM32F4-Base存储架构说明.md` §5.4.1 · `子系统设计/控制台单写者日志设计.md` §12;提交 `930215f` | +| 2026-10-02 台架取证链(IWDG PR/RLR 错配 / dhara resume 回滚 / ORE 僵尸通道) | ✅ 已修 | `子系统设计/看门狗设计说明.md` 2026-10-02 变更记录 · `架构设计/STM32F4-Base存储架构说明.md` §5.4.1 · `子系统设计/控制台单写者日志设计.md` §9;提交 `930215f` | > **真机判定汇总**(2026-10-03,本机 MCU,新固件全闭环 `@test` PASS 7/7):A1 ✅ PASS(含配置持久化); > A2/A3/A4 正常路径零回归;芯片级失败分支 / 短写 / 拔线负向场景待专项台架窗口(判据见 §7.1)。