Files
STM32F4-Base/App/log/console_log.c
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +08:00

564 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:Console Single-Writer Logging — 控制台单写者日志(USART1)实现
* 模块功能:控制台单写者日志的环形缓冲 + 唯一写者发送实现(API 见 console_log.h);
* 兼载 MicroLIB printf 底层 fputc 重定向
* 适用平台:STM32F407ZGTx(仅 MCU 端)
* 作者:王建锋
* 创建日期:2026-09-24
* 修改记录:
* 2026-09-24 王建锋 创建(自 Src/usart.c USER CODE 区迁出,实现逻辑不变,
* 仅变更文件位置:HAL 库生成文件不得包含应用代码)
* 2026-10-02 王建锋 僵尸通道喂狗节奏保护(台架日志代次 288 实证):第 3 轮的
* "单次 drain 预算 250ms" 只限单次调用——ORE 锁存在外设软复位后
* 毫秒级重锁(PA10 悬空 + RX 使能),背靠背 drain 每轮再耗 100ms
* 块超时 → 主循环周期拉到 450~500ms → 100ms 节流的喂狗抽样间隙
* 超过 IWDG 最坏容限 1.7s → 停喂 → 误复位(且心跳位全员健康、
* 无 stall 日志,纯节奏性饥饿)。修复:发送故障后立即结束本轮
* drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),
* 保证喂狗节奏 ≤ ~300ms。根治(把 USART1 改成只发不收)已于 2026-10-26
* 由用户裁定不做,本缓解即长期方案;机制详述 docs/子系统设计/控制台单写者日志设计说明.md §12。
* 2026-10-02 王建锋 第 8b 轮加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
* 台架第 8a 轮取证(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)定位到:defaultTask
* 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 =
* HAL_UART_Transmit 的超时判据 `HAL_GetTick() - Tickstart` 依赖
* TIM7 的 uwTick —— TIM7 一旦停摆,超时永不成立 → 写者死等 TXE →
* 唯一喂狗者停摆 → IWDG 复位。本改动给"裸发送"与"缓冲满等待"两处
* 都换成 DWT 周期计数硬上界(DWT 使能失败时退回旧 HAL 行为)。
* 另加 USART1 未初始化(huart1.Instance == NULL)短路:防止在
* HAL_Init 早期/断言-故障上报路径上解引用 NULL 读写地址 0。
* 2026-10-10 王建锋 新增 dbg_console_writer_ready() 查询:写者未注册时
* con_append 走无锁直发(前提是"此窗口只有 defaultTask 记日志"),
* adcTask(B1)等周期任务此时落日志会与 defaultTask 的存储初始化
* 日志字节级交错(台架实测 [ADC_TASK] 首帧行与 [NAND_FTL] 行交错)。
* 提供就绪查询供周期任务门控自身日志,不改直发路径本身。
* 2026-10-24 王建锋 主机单测(test/host/console_log)照出的四条一并修复:
* 冷却改判独立标志 s_con_cooling(旧写法拿 tick 值当"是否在冷却"哨兵,
* 故障恰好落在 HAL_GetTick()==0 那一拍时冷却等于没记);
* con_append 的"复核空位 + 整行拷贝 + 推进 head"收进同一临界区(旧写法
* 判据在区外,多生产者可在环将满时同时过判据 -> head 越过容量把未发出
* 的旧行静默覆盖,既不报错也不计丢弃);
* drain 失败分诊:Instance==NULL(未初始化)不再走外设软复位自愈(那会
* 在 HAL 未就绪时重配 RCC/GPIO),改记 dropped;僵尸通道仍自愈;
* 三本账分开(docs §13-O2):新增 dbg_console_tx_timeout(),dropped 只记
* 真丢的,tx_fault 仍只记软复位自愈次数;[ERR] dropped 行改为"发成功才
* 销账",通道不可用期间不再把丢弃证据一起冲掉。
*/
#include "console_log.h"
#include <stdio.h>
#include "usart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os.h"
#include "sys_events.h"
/* 刻意不 include "sys_log.h":日志子系统不得回调本模块(调用图成环 →
* armlink 静态栈深在每个任务上虚高,见 dbg_console_drain 故障路径注释)。
* TX 故障留痕改由写者任务在 drain 之外补记 */
/* ===== 控制台单写者日志(2026-09-24 加固,HardFault 定位结论驱动)=====
* 背景:旧路径 DBG_* → printf → fputc **每字符一次** HAL_UART_Transmit。HAL 的锁只保证
* "单字节"原子,任务切换发生在字符之间 → 两行日志逐字符交错(2026-09-24 崩溃前
* 实测:"[2026-[2026-0909-24 15:2-24 15:27:37:39]…");更严重的是字符串指针一旦
* 被破坏,脏指针会直接进入 UART 缓冲遍历(同日 HardFault:BFAR=0x10010000 恰为
* CCM 末尾+1,PC 落在 HAL_UART_Transmit 的缓冲读取处)。
* 架构:**defaultTask 是唯一串口发送者**;其余任务/上下文只把文本写入环形缓冲。
* ① 行级原子:整行在一次临界区内拷入环形缓冲,写者按块一次 HAL_UART_Transmit
* 发出 → 行与行不可能再交错(UART 只有这一个调用点);
* ② 指针可控:进 UART 的缓冲恒为本文件的环形缓冲,调用方脏指针再也到不了 UART;
* ③ 写者未注册(内核启动早期/裸机)或中断上下文:回退直接阻塞发送(无并发或必须裸发)。
* 约束:不新增任务、不动态分配;环形缓冲为 BSS(SRAM1,非 CCM),无 DMA 可达性冲突。
*/
#define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */
#define DBG_CON_CHUNK_MAX 256U /* 写者单次 HAL_UART_Transmit 字节上限 */
#define DBG_CON_WAIT_MS 200U /* 缓冲满时生产者最长等待(ms),仍满则丢弃并计数 */
#define DBG_CON_TX_TIMEOUT_MS 100U /* 单次发送超时:256B 块 @115200 标称 22ms;僵尸通道限时 + 自愈 */
#define DBG_CON_DRAIN_BUDGET_MS 250U /* 单次 drain 连续发送上限:让位喂狗轮询(缺失容忍 800ms) */
#define DBG_CON_FAULT_COOL_MS 300U /* 故障后发送冷却:ORE 锁存毫秒级重锁,背靠连发无益
且拖长主循环周期 → 喂狗节奏饥饿(台架日志代次 288 实证);
冷却期内 drain 只让位、不发送,下轮重试 */
/* ==== 第 8b 轮:与时间源无关的硬上界(DWT CYCCNT)====
* HAL_UART_Transmit 的超时判据是 `HAL_GetTick() - Tickstart > Timeout`,而 HAL 时基
* = TIM7 的 uwTick。TIM7 停摆 → 超时永不成立 → 写者死等 TXE → 唯一喂狗者停摆 →
* IWDG 误复位(第 8a 轮台架现场)。改用 DWT CYCCNT(内核硬件周期计数,不依赖任何
* 中断/uwTick)做判据;DWT 使能失败时退回 HAL_UART_Transmit(旧行为,功能不退化)。 */
#define CON_TX_TIMEOUT_CYC (SystemCoreClock / 10U) /* 发送硬上界 = 100ms 对应周期数 */
/*
* DWT 不可用时的**指令计数**上界(替代 HAL_UART_Transmit 回退,2026-10-10)。
* 为什么不再回退 HAL:① HAL_UART_Transmit 的超时判据是 HAL_GetTick——断言/停摆
* 现场 tick 未必在走,等待无上界;② 它的调用链 `HAL_UART_Transmit →
* UART_WaitOnFlagUntilTimeout` 会被 armlink 挂进**每个**碰日志的任务的最坏链
* (configASSERT 在 FreeRTOS 内部可达 → 三个内核钩子都在链尾),实测每条链多背
* 64 B 栈。计数上界同样有界、零外部依赖。
* 取值:115200 bps 下一字节 87 µs;轮询循环 ~5 周期 → 每字节约 3000 次迭代,
* 取 20000 留 6 倍余量(单字节卡死最多数毫秒级即放弃,不无限自旋)
*/
#define CON_TX_GUARD_MAX 20000u
#define CON_APPEND_WAIT_CYC (SystemCoreClock / 5U) /* 缓冲满等待硬上界 = 200ms 对应周期数 */
static uint8_t s_con_ring[DBG_CON_RING_SIZE];
static volatile uint16_t s_con_head; /* 生产者写入位置 */
static volatile uint16_t s_con_tail; /* 写者读取位置 */
static volatile uint32_t s_con_drop; /* 真没落地的丢弃数(一次一行/一块) */
static volatile uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */
static volatile uint8_t s_con_tx_fault_log; /* TX 故障留痕待上报(由写者任务在 drain 之外补记 run.log) */
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(只存时刻,不再兼作哨兵) */
static volatile uint8_t s_con_cooling; /* 1 = 正在故障冷却(独立标志:tick 恰好等于 0 的那一拍也必须算在冷却) */
static volatile uint32_t s_con_tx_timeout; /* 发送块超时次数(写者排空所见,与丢弃账分开) */
static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护用) */
/*
* 函数功能:计算环形缓冲已用字节数(head-tail 差值回绕取模,无锁)
* 入口参数:无
* 返回值:已用字节数 uint16_t
*/
static uint16_t con_used(void) {
return (uint16_t)((uint16_t)(s_con_head - s_con_tail) & (DBG_CON_RING_SIZE - 1U));
}
/*
* 函数功能:惰性使能 DWT 周期计数器(时间源无关的硬上界基准)
* 入口参数:无
* 返回值:无
* 限定条件:任意上下文可调(幂等;纯寄存器写,不创建任何内核对象)
* 函数说明:CoreDebug->DEMCR.TRCENA=1 → DWT->CYCCNT=0 → DWT->CTRL.CYCCNTENA=1;
* 使能成功置 s_con_dwt_up=1(供超时判据选择),失败保持 0(退回 HAL 超时)。
* 在 Cortex-M4 上 DWT 恒存在;失败仅可能出现在调试单元被硬件关闭时。
*/
static void con_dwt_ensure(void) {
if (s_con_dwt_up != 0U) {
return;
}
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U) {
s_con_dwt_up = 1U;
}
}
/*
* 函数功能:DWT 不可用时的计数上界发送(每字节/TC 各数 CON_TX_GUARD_MAX 次)
* 入口参数:u - USART 寄存器基址(调用方已判非 NULL) USART_TypeDef *
* p_buf - 待发数据;len - 字节数
* 返回值:HAL_OK - 已发出;HAL_TIMEOUT - 计数上界内未完成(本块放弃)
* 限定条件:USART1 已初始化
* 函数说明:刻意不用 HAL_UART_Transmit——理由见 CON_TX_GUARD_MAX 处注释
* (tick 依赖不可信 + 其调用链污染所有任务的最坏栈链)
*/
static HAL_StatusTypeDef con_tx_counted(USART_TypeDef *u, const uint8_t *p_buf, uint16_t len) {
uint32_t guard;
uint16_t i;
for (i = 0U; i < len; i++) {
guard = 0U;
while ((u->SR & USART_SR_TXE) == 0U) {
if (++guard >= CON_TX_GUARD_MAX) {
return HAL_TIMEOUT;
}
}
u->DR = (uint16_t)p_buf[i];
}
guard = 0U;
while ((u->SR & USART_SR_TC) == 0U) {
if (++guard >= CON_TX_GUARD_MAX) {
return HAL_TIMEOUT;
}
}
return HAL_OK;
}
/*
* 函数功能:有界裸发送(不依赖 uwTick / HAL 超时)
* 入口参数:p_buf - 待发数据;len - 字节数(调用方已限 ≤ DBG_CON_CHUNK_MAX)
* 返回值:HAL_OK - 已全部写入发送寄存器;HAL_TIMEOUT - 有界上界内未完成
* 限定条件:USART1 已初始化
* 函数说明:寄存器级轮询 TXE/TC,超时判据用 DWT CYCCNT(CON_TX_TIMEOUT_CYC);
* DWT 未使能时走 con_tx_counted(指令计数上界,2026-10-10 取代
* HAL_UART_Transmit 回退)。本函数只做 TX,
* 不触碰 RX/错误标志——僵尸通道自愈由调用方按返回值处理。
* ⚠ USART1 未初始化(huart1.Instance == NULL,如 HAL_Init 早期或
* 断言/故障上报路径)时立即返回 HAL_ERROR:否则会解引用 NULL 去读写
* 地址 0(flash 别名区)→ 在故障处理里再触发故障(2026-10-02 台架实测)。
*/
static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
USART_TypeDef *u;
uint32_t t0;
uint16_t i;
con_dwt_ensure();
if (huart1.Instance == NULL) {
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */
}
if (s_con_dwt_up == 0U) {
return con_tx_counted(huart1.Instance, p_buf, len);
}
u = huart1.Instance;
t0 = DWT->CYCCNT;
for (i = 0U; i < len; i++) {
while ((u->SR & USART_SR_TXE) == 0U) {
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
return HAL_TIMEOUT; /* 有界:本块放弃(记账由调用方按返回值做) */
}
}
u->DR = (uint16_t)p_buf[i];
}
while ((u->SR & USART_SR_TC) == 0U) {
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
return HAL_TIMEOUT; /* TC 超时:字节已交给发送器,调用方分账 */
}
}
return HAL_OK;
}
/*
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次、不加锁)
* 入口参数:buf - 文本缓冲;len - 字节数(超上限截断)
* 限定条件:串口已初始化;仅用于写者未注册、中断/异常上下文、崩溃上报
* 函数说明:走 con_tx_bounded(DWT 硬上界),tick 停摆时仍能在有界时间内返回。
*/
void dbg_console_write_raw(const char *buf, unsigned len) {
if ((buf == NULL) || (len == 0U)) {
return;
}
if (len > DBG_CON_CHUNK_MAX) {
len = DBG_CON_CHUNK_MAX;
}
(void)con_tx_bounded((const uint8_t *)buf, (uint16_t)len);
}
/*
* 函数功能:注册控制台写者(defaultTask 在进入主循环前调用一次)
* 函数说明:注册后生产者改走环形缓冲;未注册时直接阻塞发送(内核启动早期)
*/
void dbg_console_writer_attach(void) {
s_con_writer_task = xTaskGetCurrentTaskHandle();
s_con_writer_up = 1U;
}
/*
* 函数功能:查询控制台写者是否已注册(1=已注册,此后日志走环形缓冲行级原子路径)
* 返回值:s_con_writer_up(volatile 读,任意任务上下文可调)
* 限定条件:无
* 函数说明:供**周期性任务**在写者就绪前抑制自身日志。写者未注册时 con_append 走
* dbg_console_write_raw 无锁直发(设计前提是"此窗口只有 defaultTask 记日志"),
* 其余任务此时落日志会与 defaultTask 的存储初始化日志逐字节交错
* (2026-10-10 台架:adcTask 首帧日志与 [NAND_FTL] 初始化行交错)。
*/
uint8_t dbg_console_writer_ready(void) {
return s_con_writer_up;
}
/*
* 函数功能:USART1 僵尸通道自愈(清错误锁存 + 整外设重初始化)
* 函数说明:RX 悬空噪声(PA10 未接线)锁存的 ORE/NE/FE/PE 使 HAL 轮询 TX 恒快速失败
* (控制台静默、系统不复位);发送器卡死状态则使 TXE 等待耗满超时(冻结唯一
* 写者 → 看门狗误复位,2026-10-02 台架实证)。外设软复位清锁存与卡死状态后
* 重新初始化,故障时间被单块超时限定
*/
static void con_uart_reinit(void) {
__NVIC_ClearPendingIRQ(USART1_IRQn);
__HAL_RCC_USART1_CLK_DISABLE();
__HAL_RCC_USART1_CLK_ENABLE();
huart1.gState = HAL_UART_STATE_RESET;
huart1.RxState = HAL_UART_STATE_RESET;
huart1.ErrorCode = HAL_UART_ERROR_NONE;
(void)MX_USART1_UART_Init();
}
/*
* 函数功能:排空环形缓冲(仅写者任务调用)
* 函数说明:按块一次 con_tx_bounded 发出(DWT 硬上界,不依赖 uwTick);每块完成后
* 再推进读指针(临界区保护);发送失败 → 外设软复位自愈(丢当前块);
* 单次调用最多连续发 DBG_CON_DRAIN_BUDGET_MS 后让位主循环,保证看门狗轮询
* 不被僵尸通道饿死
*/
void dbg_console_drain(void) {
uint16_t used;
uint32_t t0;
if (s_con_writer_up == 0U) {
return;
}
if (s_con_draining != 0U) {
return; /* 重入保护:写者自记录不触发嵌套 drain */
}
s_con_draining = 1U;
t0 = (uint32_t)HAL_GetTick();
/* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送——
* 本轮 drain 直接让位主循环,保住喂狗节奏(日志代次 288 停喂误复位的
* 直接诱因 = 背靠连发的块超时链)。是否在冷却看 s_con_cooling 独立标志,
* **不再拿 tick 值当哨兵**(2026-10-24 实测:故障恰好落在 tick==0 那一拍时,旧写法
* 记出的 cool_until==0 等于没记冷却,下一轮立刻再耗一次 100ms 块超时)。
* 无符号差值跨 49.7 天回绕安全 */
if (s_con_cooling != 0U) {
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
s_con_draining = 0U;
return;
}
s_con_cooling = 0U;
}
while ((used = con_used()) != 0U) {
uint16_t tail;
uint16_t contig;
uint16_t n;
HAL_StatusTypeDef st;
tail = s_con_tail;
contig = (uint16_t)(DBG_CON_RING_SIZE - tail); /* 到环尾的连续段 */
n = (used < contig) ? used : contig;
if (n > DBG_CON_CHUNK_MAX) {
n = DBG_CON_CHUNK_MAX;
}
st = con_tx_bounded(&s_con_ring[tail], n);
taskENTER_CRITICAL();
s_con_tail = (uint16_t)((uint16_t)(s_con_tail + n) & (DBG_CON_RING_SIZE - 1U));
taskEXIT_CRITICAL();
if (st != HAL_OK) {
if (st == HAL_ERROR) {
/* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL,
* 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事:
* 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配
* RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。
* 本块字节确实一个都没上线 -> 计入 dropped(只记真丢的),
* 末尾的 [ERR] dropped 行负责留痕;本轮到此为止(失败即止)。 */
s_con_drop++;
break;
}
/* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁,
* 继续背靠连发只会把主循环耗在块超时链上);本块丢弃
* (行已出环,不可重放),本块在超时账与丢弃账上各记一笔 */
s_con_tx_timeout++; /* 新增账:块超时次数(TXE 与 TC 两条硬上界
* 都算,口径 = 写者排空时观察到的块超时) */
con_uart_reinit();
s_con_tx_fault++;
s_con_drop++; /* 确认不了的块算真丢(一次丢弃事件),
* 旧写法只涨 tx_fault -> 账上看不出日志在丢,
* docs §13-O2 记的正是这条 */
s_con_cool_until = (uint32_t)HAL_GetTick();
s_con_cooling = 1U;
if (s_con_tx_fault == 1U) {
static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n";
/* 刻意**不在这里**调 syslog_post():syslog_post → dbg_console_write_line
* → con_append → dbg_console_drain 会与本函数构成**调用图成环**。运行时
* 有 s_con_draining 重入标志挡着,不会真递归;但 armlink 静态调用图会把
* 整条日志尾巴在每个任务的最坏链上多叠一层(实测 6 个任务的最坏链末尾
* 全是这个环)→ 栈深数字虚高、真实风险反而被掩盖。
* 改法:只置标志,由写者任务(defaultTask 主循环,本就是 syslog 唯一
* 写者)在 drain **之外**补记 run.log(见 dbg_console_tx_fault_log_take) */
s_con_tx_fault_log = 1U;
(void)dbg_console_write_raw(note, (unsigned)sizeof(note) - 1U);
}
break; /* 故障即止:本轮 drain 到此为止,让位主循环 */
}
if ((uint32_t)(HAL_GetTick() - t0) >= DBG_CON_DRAIN_BUDGET_MS) {
break; /* 预算用尽:让位主循环(看门狗轮询),下轮续排 */
}
}
s_con_draining = 0U;
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失) */
if (s_con_drop != 0U) {
char note[64];
uint32_t d = s_con_drop;
int n;
n = snprintf(note, sizeof(note), "\r\n[ERR] CONSOLE dropped %lu lines\r\n",
(unsigned long)d);
if (n > 0) {
if (n > (int)sizeof(note) - 1) {
n = (int)sizeof(note) - 1;
}
/* 只有**真发出去了才销账**(2026-10-24 一并修正):旧写法先把计数清零
* 再裸发,通道不可用/僵尸时那一发也失败 -> 丢弃证据被一起冲掉,[ERR] 行
* 永远出不来("永不静默"变成"永不显示")。留到下一轮或串口恢复后补发。 */
if (con_tx_bounded((const uint8_t *)note, (uint16_t)n) == HAL_OK) {
/* 按"本次报出去的这条"扣账而不是清零:期间生产者新丢的行留下轮报。
* 递减只发生在这一处(单写者 + s_con_draining 重入保护),生产者只增,
* 故 s_con_drop >= d 恒成立,无需再写下溢分支(写了也永远测不到)。 */
s_con_drop -= d;
}
}
}
}
/*
* 函数功能:把一段文本放入控制台(写者已注册)/直接发出(未注册或中断上下文)
* 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped)
* 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行;
* "复核空位 -> 整行拷贝 -> 推进 head"是一个原子的临界区动作;
* 环满等待(vTaskDelay / DWT 判据)一律在临界区之外,区内绝不睡眠。
* 写者自身记录日志时先排空缓冲,避免自等。
* 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)——
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(第 8b 轮加固)。
*/
static int con_append(const char *buf, uint16_t len) {
uint32_t waited = 0U;
uint32_t d0 = 0U;
uint8_t have_dwt = 0U;
if ((buf == NULL) || (len == 0U)) {
return -1;
}
if ((s_con_writer_up == 0U) || (__get_IPSR() != 0U)) {
dbg_console_write_raw(buf, len);
return 0;
}
if (len > (DBG_CON_RING_SIZE / 2U)) {
len = (uint16_t)(DBG_CON_RING_SIZE / 2U); /* 超长保护(正常路径不会触发) */
}
if ((xTaskGetCurrentTaskHandle() == s_con_writer_task) && (s_con_draining == 0U)) {
dbg_console_drain(); /* 写者自己记录:先腾空,避免自等(drain 进行中则跳过,防嵌套) */
}
con_dwt_ensure();
if (s_con_dwt_up != 0U) {
d0 = DWT->CYCCNT;
have_dwt = 1U;
}
for (;;) {
uint16_t free_space;
int fitted = 0;
/* 复核空位 + 整行拷贝 + 推进 head **必须在同一个临界区内**(2026-10-24 修复):
* 旧写法先出区判 free_space >= len 再进区写,两个生产者可在环 >95% 满时同时过
* 判据,head 合计越过容量 -> con_used() 回绕变小 -> 后写的行覆盖尚未发出的旧行,
* 且既不报错也不计丢弃(静默损坏)。本区最长拷 192B(168MHz 亚微秒级),相对
* 115200 波特下一行约 16ms 可忽略;等待(vTaskDelay/DWT 判据)刻意留在区外。 */
taskENTER_CRITICAL();
free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - con_used());
if (free_space >= len) {
uint16_t head = s_con_head;
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
uint16_t i;
if (first > len) {
first = len;
}
for (i = 0U; i < first; i++) {
s_con_ring[head + i] = (uint8_t)buf[i];
}
for (i = first; i < len; i++) {
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
(uint8_t)buf[i];
}
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
fitted = 1;
}
taskEXIT_CRITICAL();
if (fitted != 0) {
break;
}
if (have_dwt != 0U) {
if ((uint32_t)(DWT->CYCCNT - d0) >= CON_APPEND_WAIT_CYC) {
s_con_drop++;
return -1;
}
} else if (waited >= DBG_CON_WAIT_MS) {
s_con_drop++;
return -1;
}
vTaskDelay(1);
waited++;
}
if (sys_evtHandle != NULL) {
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX); /* 唤醒写者 */
}
return 0;
}
/*
* 函数功能:把一行已格式化的文本交给单写者控制台(生产者入口,**非可变参数**)
* 入口参数:line - 整行文本(含结尾换行);len - 字节数
* 返回值:写入字节数;-1 - 参数非法或被丢弃
* 函数说明:整行一次入环形缓冲 → 行级原子(不会与他行交错);调用方缓冲随即可复用。
* 刻意不用自研可变参数函数(规范 S12 7.6);格式化由 DBG_PRINT 宏内
* 库 snprintf 在调用点完成。
*/
int dbg_console_write_line(const char *line, unsigned len) {
if ((line == NULL) || (len == 0U)) {
return -1;
}
if (len > DBG_CON_LINE_MAX) {
len = DBG_CON_LINE_MAX; /* 截断(丢尾,保开头的标记信息) */
}
if (con_append(line, (uint16_t)len) != 0) {
return -1;
}
return (int)len;
}
/*
* 函数功能:已丢弃的日志行数(诊断用)
*/
uint32_t dbg_console_dropped(void) {
return s_con_drop;
}
/*
* 函数功能:已发生的 TX 故障(僵尸通道自愈)次数(诊断用)
*/
uint32_t dbg_console_tx_fault(void) {
return s_con_tx_fault;
}
/*
* 函数功能:已发生的发送块超时次数(诊断用)
* 返回值:块超时累计次数 uint32_t
* 限定条件:无(任意上下文可读,含 ISR)
* 函数说明:三本账分开(docs/子系统设计/控制台单写者日志设计说明.md §13-O2)——
* dbg_console_dropped() = **真没落地**的丢弃数(缓冲满丢的一行,或一个
* 没能确认送达而出环的块;[ERR] 行报的就是它);
* dbg_console_tx_fault() = 外设软复位**自愈次数**(语义不变);
* dbg_console_tx_timeout() = 本账:写者排空时观察到的**块超时**次数
* (TXE 与 TC 两条硬上界都算)。
* 裸发路径(内核前 / ISR / 两条告警行自身)的超时不记本账——那些上下文里
* 没有"块"的概念,也只可能发生在启动极早期,混进来只会让运维读数失去意义。
* UART 物理失效但字节已发出的情形只涨本账,不污染另两本。 */
uint32_t dbg_console_tx_timeout(void) {
return s_con_tx_timeout;
}
/*
* 函数功能:取走"控制台 TX 故障待上报"标志(读后清)
* 返回值:1 - 需要补记一条 run.log 证据,0 - 无
* 限定条件:**写者任务**(defaultTask)在 dbg_console_drain() 之外调用
* 函数说明:断调用图成环用,见 dbg_console_drain 故障路径注释
*/
uint8_t dbg_console_tx_fault_log_take(void) {
uint8_t pending = s_con_tx_fault_log;
s_con_tx_fault_log = 0U;
return pending;
}
/*
* 函数功能:MicroLIB printf 底层重定向(库内部/遗留 printf 调用)
* 函数说明:同样只把字节交给单写者路径;未注册写者时直发(与旧行为一致)
*/
int fputc(int ch, FILE *f) {
uint8_t b = (uint8_t)ch;
(void)f;
(void)con_append((const char *)&b, 1U);
return ch;
}