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 表)。
This commit is contained in:
@@ -107,20 +107,54 @@ extern int con_uart_txe_ready_left; /* >=0 = 只准就绪 N 次
|
||||
extern int con_uart_tc_after; /* 前 N 次 TC 读不就绪,之后就绪 */
|
||||
extern int con_uart_tc_stuck; /* 1 = TC 永不就绪 */
|
||||
extern int con_uart_pending; /* 1 = 有个字节已进 DR 待收 */
|
||||
/* 前置声明:con_uart_tx_put 要看当前临界区深度(定义在本文件下方的④内核入口段,
|
||||
* 这里必须先声明,否则 C89 隐式声明在 ARMCC/MSVC 上都是错误或告警) */
|
||||
uint32_t con_crit_depth(void);
|
||||
|
||||
/* —— 道G task-23 新增的两个把手(判据说明见 con_uart_tx_put 内注释)—— */
|
||||
extern uint32_t con_uart_tx_hook_at; /* 第 N 个字节上线后触发一次(0=不触发) */
|
||||
extern void (*con_uart_tx_hook)(void); /* 触发时跑的函数(测试装填) */
|
||||
extern uint32_t con_uart_crit_at_tx_max; /* 所有上线字节里见过的最大临界区深度 */
|
||||
|
||||
void con_uart_reset(void);
|
||||
|
||||
/*
|
||||
* 上线一个字节 = 记进字节日志,并在此处给测试两个观察/介入点(道G task-23):
|
||||
* ① con_uart_crit_at_tx_max:本字节上线时的临界区深度水位。整行所有权只能靠一次
|
||||
* 几条指令的 test-and-set 取得,**绝不允许把 7.8 ms 的发送包进关中断区**(那等于
|
||||
* 把中断延迟与最坏喂狗抖动塞进启动窗口,比交错本身更糟)——本水位恒 0 就是这条
|
||||
* 硬红线的机器证据(判据见 console_log_test.c 的 M 族 TC-CONLOG-170..179)。
|
||||
* ② con_uart_tx_hook/_at:在"第 N 个字节上线之后"跑一遍测试装填的函数,用来精确
|
||||
* 模拟**发送任务在两个字节之间被抢占**。行级所有权是"拿不到就当场让路"的非阻塞
|
||||
* 设计,所以既有 con_delay_hook(vTaskDelay 时才跑)在这条路径上永远不会触发,
|
||||
* 必须另有塞入点,否则"两个生产者并发时不得出现另一条行的字节"这条判据没法做
|
||||
* 成必然失败的断言(只能靠静态论证,那不算交付)。
|
||||
* 钩子一次性:进入即摘钩,防递归;钩子里再发的字节照常记账。
|
||||
*/
|
||||
static void con_uart_tx_put(uint8_t b) {
|
||||
uint32_t depth;
|
||||
|
||||
if (con_uart_tx_n + 1U >= CON_UART_TX_MAX) {
|
||||
printf("[FATAL] fake USART: tx log overflow (>=%u)\n", CON_UART_TX_MAX);
|
||||
printf("[FATAL] fake uart: wire log overflow (>=%u bytes)\n", (unsigned)CON_UART_TX_MAX);
|
||||
exit(2);
|
||||
}
|
||||
con_uart_tx[con_uart_tx_n++] = b;
|
||||
/* 每次都在尾部补一个 NUL:让 strstr 一类字符串函数不可能读过缓冲末尾 */
|
||||
/* NUL 结尾(tx 是二进制缓冲,但让 CHECK 的 %s 打印能读) */
|
||||
con_uart_tx[con_uart_tx_n < CON_UART_TX_MAX ? con_uart_tx_n : (CON_UART_TX_MAX - 1U)] = 0U;
|
||||
con_uart_cur_len++;
|
||||
}
|
||||
|
||||
depth = con_crit_depth();
|
||||
if (depth > con_uart_crit_at_tx_max) {
|
||||
con_uart_crit_at_tx_max = depth;
|
||||
}
|
||||
if ((con_uart_tx_hook != 0) && (con_uart_tx_hook_at != 0U)
|
||||
&& (con_uart_tx_n == con_uart_tx_hook_at)) {
|
||||
void (*h)(void) = con_uart_tx_hook;
|
||||
|
||||
con_uart_tx_hook_at = 0U; /* 一次性:先摘钩再跑,钩子内部的发送不再触发 */
|
||||
h();
|
||||
}
|
||||
}
|
||||
/* 收走「上次就绪之后写进 DR 的字节」:它此刻才真正上线 */
|
||||
static void con_uart_take_byte(void) {
|
||||
if (con_uart_pending == 0) {
|
||||
|
||||
Reference in New Issue
Block a user