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:
+244
-35
@@ -31,6 +31,26 @@
|
||||
* adcTask(B1)等周期任务此时落日志会与 defaultTask 的存储初始化
|
||||
* 日志字节级交错(台架实测 [ADC_TASK] 首帧行与 [NAND_FTL] 行交错)。
|
||||
* 提供就绪查询供周期任务门控自身日志,不改直发路径本身。
|
||||
* 2026-10-26 王建锋 **O1 第二个实例(台架实证)修复:裸发路径加「行级单一所有者」**。
|
||||
* 台架同镜像两次启动(fw 戳 Oct 26 2026 16:27:01,排除构建差异):16:28 那次
|
||||
* protectTask 的未标定 INFO 完好,16:37 那次同一行变成
|
||||
* "protection [blocked (UA/UN/IA VALI2D not all s0et)" —— 多出的 3 个字节是
|
||||
* '[' '2' '0',而 DBG 行前缀恰是 '[' + sys_clock_get_str() 的 "2026-…",
|
||||
* **即另一条行的行首三个字符**;串口 framing 错只会改字节、不会让行变长,
|
||||
* 故只可能是另一个写者在同一行内插入。机理 = 写者注册前(内核启动 ->
|
||||
* default_task.c 的 dbg_console_writer_attach,中间隔着整段 storage_bring_up)
|
||||
* 所有任务的整行都走 dbg_console_write_raw 逐字节轮询 USART1->DR,**零互斥**
|
||||
* (docs/子系统设计/控制台单写者日志设计说明.md 在册 O1;20261012_B6 那次
|
||||
* "st[art:" 同一形态)。修法 = con_tx_try_claim()/con_tx_release() 一对
|
||||
* **非阻塞**所有权(只在几条指令的临界区里做 test-and-set,绝不把 7.8 ms
|
||||
* 的发送包进临界区、绝不关中断、绝不等待);抢不到所有权的那条行**不丢**,
|
||||
* 落进既有环形缓冲延后发(竞争失败不该变成丢行,更不该变成 [ERR] 假红——
|
||||
* 那个窗口每 3 次启动命中 2 次),只有环也放不下才计入既有 dropped 账
|
||||
* (真溢出仍然看得见)。顺带堵掉第二处洞:**attach 恰好撞上别人已发出的半行**
|
||||
* ——dbg_console_drain 现在也必须先拿到所有权才开轮,拿不到就让位
|
||||
* (tail 不推进,环里一条不少,下轮 ≤50 ms 后重发,这条路径永不丢)。
|
||||
* 另导出 dbg_console_tx_begin()/tx_end() 供"一条行分多段裸发"的调用方收口
|
||||
* (Src/freertos.c 故障钩子是下一张卡),本模块不改任何调用方文件。
|
||||
* 2026-10-24 王建锋 主机单测(test/host/console_log)照出的四条一并修复:
|
||||
* 冷却改判独立标志 s_con_cooling(旧写法拿 tick 值当"是否在冷却"哨兵,
|
||||
* 故障恰好落在 HAL_GetTick()==0 那一拍时冷却等于没记);
|
||||
@@ -66,7 +86,10 @@
|
||||
* ① 行级原子:整行在一次临界区内拷入环形缓冲,写者按块一次 HAL_UART_Transmit
|
||||
* 发出 → 行与行不可能再交错(UART 只有这一个调用点);
|
||||
* ② 指针可控:进 UART 的缓冲恒为本文件的环形缓冲,调用方脏指针再也到不了 UART;
|
||||
* ③ 写者未注册(内核启动早期/裸机)或中断上下文:回退直接阻塞发送(无并发或必须裸发)。
|
||||
* ③ 写者未注册(内核启动早期/裸机)或中断上下文:回退直接发送,但**整行独占**
|
||||
* 由 con_tx_try_claim() 保证(道G task-23:未注册期原来就是"多个任务都直发、
|
||||
* 零互斥",O1 两个实例都出在这里);抢不到独占的那条行不丢,落环形缓冲延后发,
|
||||
* 与当场直发逐字节相同。ISR / 调度器未启动两种上下文放行(不许等)。
|
||||
* 约束:不新增任务、不动态分配;环形缓冲为 BSS(SRAM1,非 CCM),无 DMA 可达性冲突。
|
||||
*/
|
||||
#define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */
|
||||
@@ -110,12 +133,29 @@ 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; /* 写者任务句柄(自等保护用) */
|
||||
/* 行级单一所有者(道G task-23,O1 第二实例):USART1 数据寄存器同一时刻只允许
|
||||
* 一个上下文在逐字节轮询写入。NULL = 空闲。
|
||||
* ⚠ 这不是"锁",是**所有权凭证**:拿不到就当场让路,绝不排队等待(见
|
||||
* con_tx_try_claim 函数头:裸发一行 @115200 约 7.8 ms,把它包进临界区等于给
|
||||
* 启动窗口塞进 7.8 ms 的中断延迟与最坏喂狗抖动,比交错本身更糟)。 */
|
||||
static volatile TaskHandle_t s_con_tx_owner;
|
||||
static volatile uint8_t s_con_tx_depth; /* 同一任务重入深度(drain 轮内的裸发留痕行) */
|
||||
|
||||
/* con_tx_try_claim 的三种结果(区分开是必要的:**放行**的那些上下文没有持有凭证,
|
||||
* 绝不能在释放时把别人的所有权清掉) */
|
||||
#define CON_TX_NO 0u /* 别人正持有整行 -> 调用方让路(不阻塞) */
|
||||
#define CON_TX_BYPASS 1u /* 放行:本上下文不许等也无需凭证(ISR / 调度器未启动) */
|
||||
#define CON_TX_HELD 2u /* 已持有:调用方发完必须 con_tx_release */
|
||||
|
||||
/*
|
||||
* 函数功能:计算环形缓冲已用字节数(head-tail 差值回绕取模,无锁)
|
||||
* 入口参数:无
|
||||
* 返回值:已用字节数 uint16_t
|
||||
*/
|
||||
static uint8_t con_ring_try(const char *p_buf, uint16_t len);
|
||||
/* 定义在下方 con_append 旁;裸发让路路径要用,故前置声明
|
||||
* (ARMCC 对隐式声明报警告,本仓红线是 0 警告) */
|
||||
|
||||
static uint16_t con_used(void) {
|
||||
return (uint16_t)((uint16_t)(s_con_head - s_con_tail) & (DBG_CON_RING_SIZE - 1U));
|
||||
}
|
||||
@@ -217,19 +257,153 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次、不加锁)
|
||||
* 函数功能:尝试取得"USART1 整行发送所有权"(**非阻塞**,道G task-23)
|
||||
* 返回值:CON_TX_HELD 已持有(发完必须 con_tx_release)/ CON_TX_BYPASS 放行且不记账
|
||||
* / CON_TX_NO 别人正持有整行,调用方须让路
|
||||
* 限定条件:任意上下文(内部只有一次几条指令的临界区,绝不睡眠、绝不长期关中断)
|
||||
* 函数说明:三条规则各有硬理由,改动前请连读 docs/子系统设计/控制台单写者日志设计说明.md
|
||||
* 的「行级单一所有者」一节:
|
||||
* ① IPSR != 0(异常/中断,含四个 Fault 的崩溃上报)-> **放行**:这些上下文不许阻塞,
|
||||
* 且一发生就要复位,日志是唯一遗言,绝不允许在这里"让路"掉。已知残余(登记在同一
|
||||
* 篇文档):ISR 可以打断某任务已发出半行的裸发,两侧仍可能交错——但 Fault 行以
|
||||
* "[ERR]" 起头且整段在同一次 con_tx_bounded 内,标记校验按字节 grep 仍能命中,
|
||||
* 不构成漏检;把钩子改成"一条行一次发完"是另一张卡的活(API 已备:tx_begin/tx_end)。
|
||||
* ② 调度器未启动(xTaskGetCurrentTaskHandle() == NULL,osKernelStart 之前)-> **放行**:
|
||||
* 此刻全系统只有一个执行流,本就没有第二个写者;早期启动日志一条都不该丢。
|
||||
* ③ 已调度且空闲 -> 临界区里"看 + 认领"(几条指令),拿到即整行独占;已被持有 ->
|
||||
* 立刻返回 CON_TX_NO,**不排队**:裸发一行 @115200 约 7.8 ms,等锁等于把自己的
|
||||
* 日志节拍压到别人的串口耗时上,adcTask(AboveNormal,1 kHz 消费)可能被压住
|
||||
* 数十拍,比交错更糟。
|
||||
*/
|
||||
static uint8_t con_tx_try_claim(void) {
|
||||
TaskHandle_t me;
|
||||
uint8_t got = 0U;
|
||||
|
||||
if (__get_IPSR() != 0U) {
|
||||
return (uint8_t)CON_TX_BYPASS;
|
||||
}
|
||||
me = xTaskGetCurrentTaskHandle();
|
||||
if (me == NULL) {
|
||||
return (uint8_t)CON_TX_BYPASS;
|
||||
}
|
||||
if (s_con_tx_owner == me) {
|
||||
if (s_con_tx_depth < 255U) {
|
||||
s_con_tx_depth++;
|
||||
}
|
||||
return (uint8_t)CON_TX_HELD;
|
||||
}
|
||||
taskENTER_CRITICAL();
|
||||
if (s_con_tx_owner == NULL) {
|
||||
s_con_tx_owner = me;
|
||||
s_con_tx_depth = 1U;
|
||||
got = 1U;
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
return (got != 0U) ? (uint8_t)CON_TX_HELD : (uint8_t)CON_TX_NO;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:归还整行发送所有权(与 con_tx_try_claim 成对)
|
||||
* 入口参数:held - con_tx_try_claim 的返回值(只有 CON_TX_HELD 才真正归还)
|
||||
* 返回值:无
|
||||
* 限定条件:与取锁同上下文;CON_TX_BYPASS / CON_TX_NO 一律空操作——放行的那些上下文
|
||||
* 从未持有凭证,误清会把别人正持有的整行放开,等于把这次修复改没
|
||||
* 函数说明:同任务嵌套按深度递减(写者一轮 drain 内还会再裸发 [WARN]/[ERR] 留痕行,
|
||||
* 那是嵌套持有者,最外层释放时才清 owner)
|
||||
*/
|
||||
static void con_tx_release(uint8_t held) {
|
||||
TaskHandle_t me;
|
||||
|
||||
if (held != (uint8_t)CON_TX_HELD) {
|
||||
return;
|
||||
}
|
||||
me = xTaskGetCurrentTaskHandle();
|
||||
if (s_con_tx_owner != me) {
|
||||
return;
|
||||
}
|
||||
if (s_con_tx_depth > 1U) {
|
||||
s_con_tx_depth--;
|
||||
return;
|
||||
}
|
||||
taskENTER_CRITICAL();
|
||||
if (s_con_tx_owner == me) {
|
||||
s_con_tx_owner = NULL;
|
||||
s_con_tx_depth = 0U;
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把"一条行分几段裸发"的调用方收成一个整行独占者(道G task-23)
|
||||
* 返回值:1 - 已独占(此后本任务每次 dbg_console_write_raw 都直接发,段间无人插入)
|
||||
* 0 - 别人正持有;调用方**照发即可**(本函数不改变任何调用方的既有语义,
|
||||
* 绝不阻塞、绝不吞字,只是段间仍可能被插——调用方自己权衡)
|
||||
* 限定条件:任务上下文(ISR 内调用会返回 1 但不记账,见 con_tx_try_claim ①)
|
||||
* 函数说明:给多段裸发的诊断/崩溃钩子用(Src/freertos.c 的钩子把一条行拆成
|
||||
* head/name/":"/" (resetting)" 等多次直发,段与段之间就是插字节的位置)。
|
||||
* 配对使用:tx_begin() -> 若干次 write_raw -> tx_end()。嵌套安全(按深度计)。
|
||||
*/
|
||||
uint8_t dbg_console_tx_begin(void) {
|
||||
uint8_t held = con_tx_try_claim();
|
||||
|
||||
return (held == (uint8_t)CON_TX_NO) ? 0U : 1U;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:结束一次整行独占(与 dbg_console_tx_begin 成对)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:只在 dbg_console_tx_begin() 之后调用;未持有时为空操作(幂等)
|
||||
*/
|
||||
void dbg_console_tx_end(void) {
|
||||
con_tx_release((uint8_t)CON_TX_HELD);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次;**整行独占发送器**)
|
||||
* 入口参数:buf - 文本缓冲;len - 字节数(超上限截断)
|
||||
* 限定条件:串口已初始化;仅用于写者未注册、中断/异常上下文、崩溃上报
|
||||
* 返回值:无
|
||||
* 限定条件:串口已初始化;写者未注册、中断/异常上下文、崩溃上报、诊断转储
|
||||
* 函数说明:走 con_tx_bounded(DWT 硬上界),tick 停摆时仍能在有界时间内返回。
|
||||
* 道G task-23 起本函数是裸发的**唯一漏斗**(所有裸发调用点都经过它,所以调用方
|
||||
* 文件一个都不用改):
|
||||
* · 拿到所有权 -> 整行一次发完,中途不会有别人的字节插进来;
|
||||
* · 拿不到 -> **本行不丢**,改投既有环形缓冲延后发。竞争失败不该变成丢行,更不该
|
||||
* 变成 [ERR]:台架那个未注册窗口每 3 次启动命中 2 次,按"丢弃 + [ERR]"处理只会
|
||||
* 把静默缺陷换成常红的门禁([ERR] 是 @test 的 Forbidden 标记)。
|
||||
* 入环的是**同一批字节原文**,本模块与 drain 都不补前缀、不重打时间戳(DBG_* 的
|
||||
* 前缀在调用点就已 snprintf 进调用方缓冲)-> 延后发出去的行与它当场直发时
|
||||
* 逐字节相同(判据⑦),取证时不会出现"证据说谎";
|
||||
* · 环也放不下 -> 这才计入既有 s_con_drop(真溢出,仍然可见,不静默);
|
||||
* · ISR / 调度器未启动 -> 放行直发(con_tx_try_claim ①②)。
|
||||
*/
|
||||
void dbg_console_write_raw(const char *buf, unsigned len) {
|
||||
uint8_t held;
|
||||
|
||||
if ((buf == NULL) || (len == 0U)) {
|
||||
return;
|
||||
}
|
||||
if (len > DBG_CON_CHUNK_MAX) {
|
||||
len = DBG_CON_CHUNK_MAX;
|
||||
}
|
||||
|
||||
held = con_tx_try_claim();
|
||||
if (held == (uint8_t)CON_TX_NO) {
|
||||
if (con_ring_try(buf, (uint16_t)len) != 0U) {
|
||||
/* 延后发:写者空出来就排出去(未注册时也一样——注册后第一轮 drain 就发)。
|
||||
* 唤醒事件只在事件组已建时发(内核前 sys_evtHandle 还是 NULL) */
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX);
|
||||
}
|
||||
} else {
|
||||
s_con_drop++; /* 让了路又放不下环 = 真丢,进既有那本账 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
(void)con_tx_bounded((const uint8_t *)buf, (uint16_t)len);
|
||||
con_tx_release(held);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -281,6 +455,7 @@ static void con_uart_reinit(void) {
|
||||
void dbg_console_drain(void) {
|
||||
uint16_t used;
|
||||
uint32_t t0;
|
||||
uint8_t held;
|
||||
|
||||
if (s_con_writer_up == 0U) {
|
||||
return;
|
||||
@@ -289,6 +464,16 @@ void dbg_console_drain(void) {
|
||||
return; /* 重入保护:写者自记录不触发嵌套 drain */
|
||||
}
|
||||
|
||||
/* 整行所有权(道G task-23):本函数按块直写 USART1->DR,若此刻别的应用上下文
|
||||
* 还在未注册期那条裸发里发到自己一半(attach 就发生在 defaultTask 里,与别的
|
||||
* 任务裸发**可以**前后脚撞上),两边就会逐字节交错——那正是本卡要修的形态。
|
||||
* 拿不到就**本轮让位**:tail 不推进 => 环里一条不少,主循环 ≤50 ms 后再试,
|
||||
* 所以这条路径**永不丢弃**,也就不该在这里加任何计数(加了就是把让位谎报成丢行)。 */
|
||||
held = con_tx_try_claim();
|
||||
if (held == (uint8_t)CON_TX_NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_con_draining = 1U;
|
||||
t0 = (uint32_t)HAL_GetTick();
|
||||
|
||||
@@ -301,6 +486,7 @@ void dbg_console_drain(void) {
|
||||
if (s_con_cooling != 0U) {
|
||||
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
|
||||
s_con_draining = 0U;
|
||||
con_tx_release(held); /* 让位也要归还凭证,否则别人永远进不来 */
|
||||
return;
|
||||
}
|
||||
s_con_cooling = 0U;
|
||||
@@ -370,7 +556,10 @@ void dbg_console_drain(void) {
|
||||
|
||||
s_con_draining = 0U;
|
||||
|
||||
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失) */
|
||||
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失)。
|
||||
* 账上有两种成因,都算"真没落地":① 环满等不到空位(con_append);② 抢不到整行
|
||||
* 所有权**且**环也放不下(dbg_console_write_raw,道G task-23)。正常让路不入这本账,
|
||||
* 所以 [ERR] 行至今仍是"控制台在丢东西"的唯一信号,没被稀释。 */
|
||||
if (s_con_drop != 0U) {
|
||||
char note[64];
|
||||
uint32_t d = s_con_drop;
|
||||
@@ -385,6 +574,7 @@ void dbg_console_drain(void) {
|
||||
/* 只有**真发出去了才销账**(2026-10-24 一并修正):旧写法先把计数清零
|
||||
* 再裸发,通道不可用/僵尸时那一发也失败 -> 丢弃证据被一起冲掉,[ERR] 行
|
||||
* 永远出不来("永不静默"变成"永不显示")。留到下一轮或串口恢复后补发。 */
|
||||
/* 所有权仍在手里(本凭证覆盖整轮,含这条留痕行) */
|
||||
if (con_tx_bounded((const uint8_t *)note, (uint16_t)n) == HAL_OK) {
|
||||
/* 按"本次报出去的这条"扣账而不是清零:期间生产者新丢的行留下轮报。
|
||||
* 递减只发生在这一处(单写者 + s_con_draining 重入保护),生产者只增,
|
||||
@@ -393,6 +583,53 @@ void dbg_console_drain(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
con_tx_release(held);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:尝试把一整行原文放进环形缓冲(**只试一次,绝不等待**)
|
||||
* 入口参数:p_buf - 整行文本;len - 字节数(调用方已钳位)
|
||||
* 返回值:1 - 已入环(head 已推进);0 - 空位不够,什么都没写
|
||||
* 限定条件:任务上下文(内部是一次几条指令至多拷满 192B 的临界区,不睡眠)
|
||||
* 函数说明:两个调用方共用同一份判据,避免长出"两套入环语义":
|
||||
* ① con_append 的常规路径(外面套有界等待,等写者腾位);
|
||||
* ② dbg_console_write_raw 抢不到整行所有权时的让路路径(**不套等待**:延后发一条行
|
||||
* 可以,把调用者压住 200ms 就是另一场事故)。
|
||||
* 复核空位 + 整行拷贝 + 推进 head **必须在同一个临界区内**(2026-10-24 修复):旧写法
|
||||
* 先出区判 free_space >= len 再进区写,两个生产者可在环 >95% 满时同时过判据,head
|
||||
* 合计越过容量 -> con_used() 回绕变小 -> 后写的行覆盖尚未发出的旧行,且既不报错也不
|
||||
* 计丢弃(静默损坏)。等待(vTaskDelay/DWT 判据)刻意留在区外。
|
||||
* ⚠ 入环的是调用方给的**字节原文**:本函数与 drain 都不补前缀、不重打时间戳
|
||||
* (DBG_* 的前缀在调用点就已 snprintf 进调用方缓冲)——延后发与当场直发逐字节
|
||||
* 相同,这是 task-23 判据⑦的结构保证。
|
||||
*/
|
||||
static uint8_t con_ring_try(const char *p_buf, uint16_t len)
|
||||
{
|
||||
uint8_t fitted = 0U;
|
||||
|
||||
taskENTER_CRITICAL();
|
||||
if ((uint16_t)((DBG_CON_RING_SIZE - 1U) - con_used()) >= 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)p_buf[i];
|
||||
}
|
||||
for (i = first; i < len; i++) {
|
||||
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
|
||||
(uint8_t)p_buf[i];
|
||||
}
|
||||
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
|
||||
fitted = 1U;
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
return fitted;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -433,37 +670,9 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
}
|
||||
|
||||
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) {
|
||||
/* 判据与拷贝都在 con_ring_try 的那一个临界区里完成(函数头有为什么);
|
||||
* 本函数只负责"等多久"——等待一律在区外,区内绝不睡眠 */
|
||||
if (con_ring_try(buf, len) != 0U) {
|
||||
break;
|
||||
}
|
||||
if (have_dwt != 0U) {
|
||||
|
||||
+15
-3
@@ -15,6 +15,9 @@
|
||||
* 修改记录:
|
||||
* 2026-09-24 王建锋 创建(自 Src/usart.c USER CODE 区迁出:
|
||||
* HAL 库生成文件不得包含应用代码)
|
||||
* 2026-10-26 王建锋 道G task-23:裸发路径加「行级单一所有者」(O1 第二实例),
|
||||
* 新增 dbg_console_tx_begin()/dbg_console_tx_end() 供"一条行分多段裸发"
|
||||
* 的调用方收口;dbg_console_write_raw() 的"不加锁"三个字从接口上取消
|
||||
*/
|
||||
|
||||
#ifndef CONSOLE_LOG_H
|
||||
@@ -32,15 +35,22 @@ extern "C" {
|
||||
* dbg_console_write_line - 生产者入口:调用方已格式化好的整行文本一次入环形缓冲
|
||||
* (刻意**不用**自研可变参数函数——规范 S12 7.6 禁止;格式化在
|
||||
* DBG_PRINT 宏内用库 snprintf 完成)
|
||||
* dbg_console_write_raw - 异常/早期路径:有界单次直发(不加锁)
|
||||
* dbg_console_write_raw - 异常/早期路径:有界单次直发;**整行独占发送器**
|
||||
* (道G task-23:拿不到独占就把整行原文转入环形缓冲延后发,
|
||||
* 与当场直发逐字节相同——不补前缀、不重打时间戳;
|
||||
* 连环都放不下才计入 dbg_console_dropped())
|
||||
* dbg_console_writer_attach- 写者任务(defaultTask)注册自己为唯一发送者
|
||||
* dbg_console_writer_ready - 写者是否已注册(1=已注册)。供**周期性任务**在写者就绪前
|
||||
* 抑制日志用:写者未注册时 con_append 走无锁直发路径
|
||||
* (仅假设 defaultTask 在日志),其余任务此时落日志会与
|
||||
* defaultTask 的存储初始化日志字节级交错(2026-10-10 台架实测)
|
||||
* dbg_console_drain - 写者任务排空缓冲并发送(单次有界;TX 失败自动软复位自愈)
|
||||
* dbg_console_dropped - 真没落地的丢弃数:缓冲满丢的一行,或一个没能确认送达
|
||||
* 而出环的块(诊断;[ERR] CONSOLE dropped 行报的就是它)
|
||||
* dbg_console_dropped - 真没落地的丢弃数:① 缓冲满丢的一行;② 一个没能确认
|
||||
* 送达而出环的块;③ 抢不到整行所有权**且**环也放不下的裸发
|
||||
* (道G task-23)。诊断;[ERR] CONSOLE dropped 行报的就是它。
|
||||
* ⚠ 抢不到所有权但成功让路入环的那条行**不入本账**(它没丢,
|
||||
* 只是晚一点发)——这本账必须保持"红得有意义",因为
|
||||
* [ERR] 是 @test 的 Forbidden 标记)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断,语义不变)
|
||||
* dbg_console_tx_timeout - 写者排空时观察到的块超时次数(TXE / TC 两条硬上界都算)。
|
||||
* 三本账分开:看日志有没有在丢 -> dropped;看外设自愈了几次
|
||||
@@ -55,6 +65,8 @@ extern "C" {
|
||||
#define DBG_CON_LINE_MAX 192U /* 单行文本上限(含结尾 '\0',超出截断) */
|
||||
int dbg_console_write_line(const char *line, unsigned len);
|
||||
void dbg_console_write_raw(const char *buf, unsigned len);
|
||||
uint8_t dbg_console_tx_begin(void); /* 1=已独占一条行;0=别人持有(调用方照发即可) */
|
||||
void dbg_console_tx_end(void); /* 与 tx_begin 成对;未持有时空操作(幂等) */
|
||||
void dbg_console_writer_attach(void);
|
||||
uint8_t dbg_console_writer_ready(void);
|
||||
void dbg_console_drain(void);
|
||||
|
||||
+42
-10
@@ -1,7 +1,7 @@
|
||||
# 控制台单写者日志设计说明
|
||||
|
||||
> **版本**:v1.5(已实施;修订史见文末「修订记录」)
|
||||
> **日期**:2026-10-24(v1.5;初版 v1.0 于 2026-09-25)
|
||||
> **版本**:v1.6(已实施;修订史见文末「修订记录」)
|
||||
> **日期**:2026-10-26(v1.6:未注册期裸发加「行级单一所有者」,O1 第二个实例收口;v1.5 于 2026-10-24;初版 v1.0 于 2026-09-25)
|
||||
> **状态**:已实施并验证——`@build` 0 错误 0 警告、`@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`(姊妹模块:持久化日志,同一写者任务) · `测试说明/链路延迟测试说明.md`(D6 日志税 / 步骤 8 非阻塞日志,需求出处) · AGENTS.md「平台约束」
|
||||
|
||||
@@ -111,10 +111,15 @@ flowchart LR
|
||||
| 区间 | 发送者构成 | 并发行为 |
|
||||
|------|-----------|---------|
|
||||
| 内核启动前 | 仅主线程(单线程) | 无并发,直发安全 |
|
||||
| 写者未注册(内核启动 → attach) | **约定只有 `defaultTask` 记日志** | 遵守约定 → 无并发;**其他任务此时记日志 → 逐字节交错**(裸发无互斥) |
|
||||
| 写者未注册(内核启动 → attach) | 任何任务都可能记日志(2026-10-26 起这是**常态而非违规**) | **整行独占**(行级单一所有者,§4.4.1):同一时刻只有一个上下文在写 `USART1->DR`,抢不到独占的整行转入环形缓冲延后发 → 交错结构性消除,且不丢行 |
|
||||
| 写者已注册(稳态) | 任务上下文唯一调用者 = 写者 | 任务上下文零竞争;残余窗口 = ISR 直发撞写者发送中(丢失、不挂死,§7) |
|
||||
|
||||
**纪律(2026-10-10 固化)**:周期 / 常驻任务**不得**在写者注册前记日志——用 `dbg_console_writer_ready()`(§5)门控自身日志(`adcTask` 的首帧与 10 s 心跳、`protectTask` 的启动锚点行即按此门控——后者 2026-10-12 B6 台架实证:B5 接线时启动行未门控,注册前裸发被交错损坏致 `@test` 锚点 MISS,见 §11 变更记录)。内核前代码仍可直接裸发(单线程,无并发)。
|
||||
**纪律(2026-10-10 固化,2026-10-10 / 2026-10-26 两次台架实证其不足)**:周期 / 常驻任务**不得**在写者注册前记日志——用 `dbg_console_writer_ready()`(§5)门控自身日志(`adcTask` 的首帧与 10 s 心跳、`protectTask` 的启动锚点行即按此门控——后者 2026-10-12 B6 台架实证:B5 接线时启动行未门控,注册前裸发被交错损坏致 `@test` 锚点 MISS,见 §11 变更记录)。内核前代码仍可直接裸发(单线程,无并发)。
|
||||
>
|
||||
> ⚠ **自愿门控挡不住边沿留痕**——它在 2026-10-12(B6,启动锚点)与 2026-10-26(未标定闭锁边沿,见 §4.4.1)两次被同一个窗口咬到,
|
||||
> 因为"这条行要不要留痕"是**业务边沿**决定的,调用方无法预知它是否落在未注册期。2026-10-26 起该窗口改由**机制**兜住
|
||||
> (§4.4.1 行级单一所有者);`dbg_console_writer_ready()` 保留,但语义降级为**降延迟**(少让路一次、少占一点环位),
|
||||
> 不再是防交错的唯一手段——新代码不必再为它改写业务分支。
|
||||
|
||||
## 4. 详细设计
|
||||
|
||||
@@ -197,12 +202,36 @@ sequenceDiagram
|
||||
|
||||
### 4.4 裸发(`dbg_console_write_raw`)
|
||||
|
||||
- 有界:`len > 256 → 256`(截尾);发送走 `con_tx_bounded`(**寄存器级轮询 TXE/TC**,超时判据 = DWT `CYCCNT`,`CON_TX_TIMEOUT_CYC` = 100 ms 对应周期数),不持临界区、不等待状态;
|
||||
- 有界:`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;
|
||||
- 适用:内核前 / 未注册期、ISR、异常钩子(栈溢出 / 堆失败)、崩溃上报;
|
||||
- **调用方纪律(2026-10-10 补充)**:`con_tx_bounded` **无互斥**(寄存器级轮询,无 `HAL` 的 `gState` 锁)→ 写者注册前**只允许 `defaultTask`**(存储 bring-up 阶段)走此路径;其他常驻任务须用 `dbg_console_writer_ready()` 门控日志(见 §3.4)。
|
||||
- ~~**调用方纪律(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)。
|
||||
|
||||
#### 4.4.1 行级单一所有者(2026-10-26 道G task-23,O1 第二个实例收口)
|
||||
|
||||
**台架一手证据**(`docs/测试说明/boot_log_data/20261026_prod_console_interleave_evidence.txt`,同一镜像 `fw 戳 Oct 26 2026 16:27:01`,排除构建差异):
|
||||
|
||||
| 启动 | 方式 | 结果 |
|
||||
|---|---|---|
|
||||
| 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 | **仅复位** | 行内多出 `[` 一字节,位置不同 |
|
||||
|
||||
**字节账(这是本节最硬的一段证据)**:紧接其后的那一行**恰好缺自己被抢走的那几个字节、且顺序一致**——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 之后)。三次启动命中两次、只出现在"仅复位"不出现在"烧录"启动 = 时序竞态,与构建无关。
|
||||
|
||||
**修法(选"真正原子"这一支,不是扩缓冲、不是再叠一层自愿门控)**:
|
||||
|
||||
- `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 与主机单测;**调用点改造另卡跟进**(`Src/freertos.c` 栈溢出/堆失败钩子把一条行拆成 head/name/":"/" (resetting)" 多次直发,段与段之间仍是可插入的缝 → task-26)。
|
||||
- **已知残余(不在本卡修)**:`IPSR != 0` 的放行仍可能打断某任务已发出半行。判断依据:Fault 上报以 `[ERR]` 起头且整段在同一次 `con_tx_bounded` 内,标记校验按**字节** grep 仍能命中,不构成漏检;且该路径随后即 `NVIC_SystemReset()`。
|
||||
|
||||
**为什么不做成"整段进环"**(曾评估):那会让 `storage_bring_up()` 期间串口**一个字都不出**,而这段正是 §「启动看门狗延后」注明的"死机表现为无任何输出"窗口(2026-10-02 那几轮就靠 bring-up 期流式日志取证)——等于把交错换成死机全瞎。现方案保留**胜者实时直发**,只有真撞车才让路一条行。
|
||||
|
||||
### 4.5 `fputc` 重定向(MicroLIB `printf`)
|
||||
|
||||
`fputc(ch, f)` → `con_append(&b, 1)`:库内部 / 遗留 `printf` 调用同样收敛到单写者路径(注册期逐字节入环;一条完整 `printf` 行 = 环内连续字节,写者按块发出 → 仍行级原子)。
|
||||
@@ -227,7 +256,9 @@ sequenceDiagram
|
||||
| 函数 | 签名 | 调用上下文 | 语义 | 备注 |
|
||||
|------|------|-----------|------|------|
|
||||
| `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,不等待、不加锁 | 缓冲须已知合法 |
|
||||
| `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_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_drain` | `void (void)` | **仅写者任务** | 排空环并发出 + 刷丢弃计数 | defaultTask 主循环周期调用 |
|
||||
@@ -255,8 +286,8 @@ sequenceDiagram
|
||||
|------|------|---------|---------|
|
||||
| 缓冲满(日志突发 / 写者卡滞) | 生产者等 200ms 后丢行 | `s_con_drop` 累计 → 写者刷 `[ERR] CONSOLE dropped N lines`(@test FAIL,永不静默) | — |
|
||||
| 写者未注册(内核前) | 单线程直发 | — | 无(无并发) |
|
||||
| 未注册期其他任务直发 | **逐字节交错**(`con_tx_bounded` 无互斥,非旧 `HAL_BUSY`) | 交错行可致 `@test` 标记失配(2026-10-10 台架实证) | 已固化纪律:周期任务用 `dbg_console_writer_ready()` 门控(§3.4 / §9-D5) |
|
||||
| ISR 直发撞写者发送中 | 与写者发送竞争 → 丢失,不挂死 | 未计数 | ISR 日志频率极低,best-effort 语义 |
|
||||
| 未注册期其他任务直发 | **已消除**(2026-10-26):整行独占,抢不到就整行入环延后发(§4.4.1) | 让路不计数;只有"抢不到且环也满"才进 `s_con_drop` → `[ERR] CONSOLE dropped N lines` | 残余 = 让路行在流里位置后移(内容与直发逐字节相同);主机单测 `TC-CONLOG-170..179` 钉死 |
|
||||
| ISR 直发撞写者发送中 | 与写者发送竞争 → 交错/丢失,不挂死(`IPSR != 0` 一律放行,§4.4.1 规则①) | 未计数 | Fault 行以 `[ERR]` 起头且整段在同一次 `con_tx_bounded` 内 → 按字节 grep 仍命中,不构成标记漏检;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` **调用图成环**,静态栈在每个任务上多叠一条日志尾巴) | 已知缺口:丢弃计数与故障计数不分离 |
|
||||
@@ -308,6 +339,7 @@ sequenceDiagram
|
||||
| 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 | **O1 第二个实例 → 未注册期裸发加「行级单一所有者」(道G task-23)**:台架同镜像两次"仅复位"启动,`[PROT]` 未标定闭锁边沿行内分别多出 `[20`(3 字节)与 `[`(1 字节),而紧随其后的 `[NAND_FTL] Initializing NAND FTL...` 行恰好缺同样那几个字节的**行首**(字节账与三次启动命中两次的频率见 §4.4.1)→ 判据"framing 错不会让行变长"锁定为**两个写者并发裸发**,即 O1 的第二个实例(第一个是 2026-10-12 B6)。修法 = 非阻塞行所有权(`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..179`,含逐字节相同判据),四处生产码注入反证各自转红(取消独占 11 红 / 让路改静默丢 5 红 / 非属主可归还 1 红 / 把发送包进临界区 3 红)。§3.4 / §4.4 / §7 / §13-O1 同步 |
|
||||
| 2026-10-12 | **纪律盲区台架实证(B6)**:`protectTask`(B5 接线,台架腿当时未跑)启动锚点行在写者注册前裸发 → 与 defaultTask 启动日志**逐字节交错**(`st[art: cycle=220ms elm=70`,两次开机同形复现 = 启动调度确定性的竞态,非 USB 噪声)→ `@test` 锚点 MISS。修复 = 启动行改 `dbg_console_writer_ready()` 首拍补打(adcTask 首帧同款),`@build` 0/0 + `@test` PASS 复验(锚点完好 +577 ms)。§3.4 纪律示例更新 |
|
||||
|
||||
## 12. ORE 僵尸通道(2026-10-02 台架实证)
|
||||
@@ -332,7 +364,7 @@ sequenceDiagram
|
||||
|
||||
| # | 项 | 说明 |
|
||||
|---|----|------|
|
||||
| O1 | 未注册期裸发无互斥 | 启动窗口(内核启动 → attach)`dbg_console_write_raw` 无互斥 → 多任务并发会**逐字节交错**(非旧 `HAL_BUSY` 丢整行)。2026-10-10 已固化"周期任务用 `dbg_console_writer_ready()` 门控"纪律;若未来开机日志密度上升,可升级为"内核前静态待发行 + attach 后统一补发"或给裸发加轻量互斥 |
|
||||
| O1 | 未注册期裸发无互斥 —— **已处置(2026-10-26,v1.6)** | 启动窗口(内核启动 → attach)`dbg_console_write_raw` 无互斥 → 多任务并发**逐字节交错**。两个台架实例:2026-10-12 B6(`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 超时),机制根源是 USART1 使能了 RX。根治 = `.ioc` 改只发不收 + CubeMX 重生成,**2026-10-26 用户裁定不做** → O4 是**长期现状**而非待办(见 §12) |
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
* 模块名称:控制台单写者日志(console_log)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 App/log/console_log.c 的单写者契约(G1~G5 / K1~K5,见
|
||||
* docs/子系统设计/控制台单写者日志设计说明.md):环形缓冲入环与分块发送、
|
||||
* 行级原子、192B 截断、写者未注册/ISR 上下文的无锁直发、缓冲满有界等待与
|
||||
* 行级原子、192B 截断、写者未注册/ISR 上下文的直发(未注册期自 2026-10-26
|
||||
* 起是**整行独占**,不再是"无锁":M 族 TC-CONLOG-170..179 钉"别的写者插不进
|
||||
* 一行、让路行逐字节等同直发、让位不丢、真溢出才 [ERR]")、缓冲满有界等待与
|
||||
* 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、
|
||||
* huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算。
|
||||
* huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算、
|
||||
* 整行所有权(行级单一所有者,道G task-23)。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只用台架):
|
||||
* 本模块的风险全在"异常时刻"——UART 僵尸通道卡死、TIM7 停摆导致 uwTick 不往前走、
|
||||
@@ -63,7 +66,16 @@
|
||||
* 四条全退回 -> 红 22 条(再红 TC-CONLOG-140/141/143/145)。
|
||||
* 改回修复版即 127/127 全绿;每一步的失败集合都是上一步的超集,说明四组断言各自
|
||||
* 咬住自己的那条语义,没有互相顶包。
|
||||
* 也就是说:这四组断言不是"写在假件规则上"的自证,而是真能咬住生产语义的。
|
||||
* 2026-10-26(道G task-23,行级单一所有者)另做四处注入,逐个单独注入、各自归因、
|
||||
* 每次改完逐字节还原(152 条基线复跑绿):
|
||||
* 取消整行独占(退回"无条件直发") -> 红 11 条(含 178 = 台架原文形态那条行);
|
||||
* 让路改成静默丢弃(不入环也不计数) -> 红 5 条(170b/170c/171/178/179b);
|
||||
* 归还所有权时不比属主(两道卫兵都拆) -> 红 1 条(179c;只拆外层里层仍守住时
|
||||
* 测试全绿是**正确结果**,不是漏测);
|
||||
* 把整段发送包进临界区(关中断 7.8ms) -> 红 3 条(015/015b/177 与 176)。
|
||||
* ⚠ 顺带记一条注入方法论:第一版"取消整行独占"的锚点在 raw 与 drain 里各出现一次,
|
||||
* 被写入前的唯一性守卫挡下——模糊锚点会一改变两个地方,等于同时测两个变异。
|
||||
* 也就是说:这些断言不是"写在假件规则上"的自证,而是真能咬住生产语义的。
|
||||
*
|
||||
* 故意没测(逐行说明见交付报告):
|
||||
* - console_log.c:383 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。
|
||||
@@ -115,6 +127,9 @@ int con_uart_txe_ready_left;
|
||||
int con_uart_tc_after;
|
||||
int con_uart_tc_stuck;
|
||||
int con_uart_pending;
|
||||
uint32_t con_uart_tx_hook_at; /* 道G task-23:第 N 个字节上线后触发一次 */
|
||||
void (*con_uart_tx_hook)(void);
|
||||
uint32_t con_uart_crit_at_tx_max; /* 上线字节所见最大临界区深度(硬红线水位) */
|
||||
|
||||
uint32_t con_dwt_step;
|
||||
int con_dwt_ctrl_sticky;
|
||||
@@ -307,7 +322,16 @@ static void reset_all(void)
|
||||
s_con_writer_up = 0U;
|
||||
s_con_dwt_up = 0U;
|
||||
s_con_writer_task = 0;
|
||||
/* 道G task-23 的整行所有权凭证:跨场景必须显式清,否则上一场景留下的持有者会让
|
||||
* 下一场景的裸发全部走环,症状正是"单独跑绿、连跑偶红" */
|
||||
s_con_tx_owner = 0;
|
||||
s_con_tx_depth = 0U;
|
||||
|
||||
/* 一次性把手先归零:上一条用例装填的钩子如果留在这儿,会把"第二个生产者"塞进
|
||||
* 本条用例的某个随机字节之后(那种红比真缺陷还难查) */
|
||||
con_uart_tx_hook_at = 0U;
|
||||
con_uart_tx_hook = 0;
|
||||
con_uart_crit_at_tx_max = 0U;
|
||||
con_uart_reset();
|
||||
con_dwt_reset();
|
||||
|
||||
@@ -528,8 +552,21 @@ static void tc_unattached(void)
|
||||
CHECK(tx_is(0U, "hello"), "TC-CONLOG-012 direct send reaches the wire verbatim (never went through the ring)");
|
||||
CHECK(used_now() == 0U, "TC-CONLOG-013 direct send does not enter the ring (head == tail)");
|
||||
CHECK(con_evt_sets == 0U, "TC-CONLOG-014 direct send does not set SYS_EVT_CONSOLE_TX (no writer to wake yet)");
|
||||
CHECK(con_crit_enter == 0U,
|
||||
"TC-CONLOG-015 direct send uses zero critical sections = the 2026-10-10 byte-interleaving root cause (not whole-line loss)");
|
||||
/* ⚠ 本条判据在道G task-23(2026-10-26)**换了钉法**,这里留全过程:旧写法断言
|
||||
* "裸发用 0 次临界区",钉住的正是当时的**缺陷机理本身**(零互斥 => 多个任务可以
|
||||
* 在同一行里互相插字节;台架 20261012_B6 的 "st[art:" 与本次 protectTask 未标定
|
||||
* 行内多出 "[" "2" "0" 三字节都是它)。修复后裸发要取"整行所有权",取与还各一次、
|
||||
* 每次只有几条指令的 test-and-set,所以计数从 0 变 2。
|
||||
* 换来两条更硬的判据(缺一条就该红),断言总数只增不减:
|
||||
* ① 每个上线字节都发生在临界区**之外**(con_uart_crit_at_tx_max == 0)——整行
|
||||
* 独占绝不允许把 @115200 约 7.8 ms 的发送包进关中断区,那等于往启动窗口塞
|
||||
* 7.8 ms 中断延迟与最坏喂狗抖动,比交错本身更糟(Lead 红线);
|
||||
* ② 全程零次 vTaskDelay:拿不到所有权就当场让路,不排队(见 TC-CONLOG-170 族)。
|
||||
*/
|
||||
CHECK((con_crit_enter == 2U) && (con_crit_exit == 2U) && (con_crit_max_depth == 1U),
|
||||
"TC-CONLOG-015 direct send takes exactly two few-instruction critical sections (claim + return) and never nests them");
|
||||
CHECK((con_uart_crit_at_tx_max == 0U) && (con_delay_calls == 0U),
|
||||
"TC-CONLOG-015b whole-line ownership puts every wire byte outside any critical section and never waits");
|
||||
|
||||
reset_all();
|
||||
memset(big, 'B', sizeof(big));
|
||||
@@ -1171,6 +1208,179 @@ static void tc_check_then_act(void)
|
||||
"TC-CONLOG-167 no producer-side ring access ever happened outside a critical section");
|
||||
}
|
||||
|
||||
/* ===================== M 整行所有权(道G task-23,O1 第二个实例) ===================== */
|
||||
|
||||
/* 装进"A 行发送中途"的第二个生产者:换任务句柄后再裸发一行,精确模拟真实抢占
|
||||
* —— A(s_task_other)此刻持有整行所有权,B(s_task_writer)必须让路。
|
||||
* 这是本套唯一能把"两个生产者并发时,写进 DR 的字节流里不得出现另一条行的字节"
|
||||
* 做成必然失败断言的把手:修复后是**非阻塞**设计,永远不会调 vTaskDelay,所以既有的
|
||||
* con_delay_hook 在这条路径上根本不会被触发(拿它当注入点会静默地什么都测不到)。
|
||||
* 台架一手证据:docs/测试说明/boot_log_data/20261026_prod_console_interleave_evidence.txt
|
||||
* —— PROT 行内多出 [ 2 0 三字节,紧接其后的 NAND_FTL 行恰好缺这三个字节的行首。 */
|
||||
/* 台架 16:37 那次被插字的原文行(boot_log_data/20261026_prod_console_interleave_evidence.txt
|
||||
* OCCURRENCE 1 去掉三个外来字节之后的样子)。M 族拿它当"真实行长与真实前缀"的样本,
|
||||
* 免得用例自己造一个不像现场的理想行。 */
|
||||
static const char s_prot_line[] =
|
||||
"[2026-10-26 16:37:07][PROT] uncalibrated -> protection blocked"
|
||||
" (UA/UN/IA VALID not all set)\r\n";
|
||||
|
||||
static void s_tx_second_writer(void)
|
||||
{
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
dbg_console_write_raw("BBBBBB", 6U);
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
}
|
||||
|
||||
static void tc_ownership(void)
|
||||
{
|
||||
char fill[DBG_CON_LINE_MAX];
|
||||
uint32_t i;
|
||||
|
||||
printf("-- TC-CONLOG-170..179 whole-line ownership (O1 instance 2, bench byte account) --\n");
|
||||
|
||||
/* 170 A 行发到第 4 个字节时被 B 抢占:A 逐字节完整,B 整行让路入环 */
|
||||
reset_all();
|
||||
con_uart_tx_hook_at = 4U;
|
||||
con_uart_tx_hook = s_tx_second_writer;
|
||||
dbg_console_write_raw("AAAAAAAA", 8U);
|
||||
CHECK(tx_is(0U, "AAAAAAAA") && (con_uart_tx_n == 8U),
|
||||
"TC-CONLOG-170 the line already on its way out stays byte-intact when a second producer preempts mid-line");
|
||||
CHECK(used_now() == 6U,
|
||||
"TC-CONLOG-170b the preempted producer books its whole line into the ring instead of spraying bytes into another line");
|
||||
CHECK((dbg_console_dropped() == 0U) && (con_delay_calls == 0U),
|
||||
"TC-CONLOG-170c yielding is neither a drop nor a wait (this is what the earlier whole-line-loss variant would have cost)");
|
||||
|
||||
/* 171 判据⑦:延后发出去的内容 = 当场直发本会产出的同一批字节(同时间戳/单前缀/单 CRLF) */
|
||||
con_uart_tx_hook = 0;
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
dbg_console_writer_attach();
|
||||
writer_drain();
|
||||
CHECK((con_uart_tx_n == 14U) && tx_is(8U, "BBBBBB") && (used_now() == 0U),
|
||||
"TC-CONLOG-171 the deferred line reaches the wire byte-identical to immediate sending (no second prefix, no re-stamped time, single CRLF)");
|
||||
|
||||
/* 172 抢不到所有权 + 环也放不下 -> 才计真丢弃(真溢出必须仍然看得见) */
|
||||
reset_all();
|
||||
(void)memset(fill, 0x58, sizeof(fill));
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
dbg_console_writer_attach();
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
CHECK(dbg_console_tx_begin() == 1U, "TC-CONLOG-172 a task can take whole-line ownership while the wire is free");
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
for (i = 0U; i < 21U; i++) {
|
||||
(void)dbg_console_write_line(fill, sizeof(fill)); /* 21*192=4032,环可用 4095 -> 只剩 63 字节 */
|
||||
}
|
||||
CHECK((used_now() == (21U * (uint32_t)DBG_CON_LINE_MAX)) && (con_uart_tx_n == 0U),
|
||||
"TC-CONLOG-172b the writer yields the whole round while another task owns the wire: nothing sent, nothing dequeued");
|
||||
dbg_console_write_raw(fill, 70U);
|
||||
CHECK((dbg_console_dropped() == 1U) && (con_uart_tx_n == 0U)
|
||||
&& (used_now() == (21U * (uint32_t)DBG_CON_LINE_MAX)),
|
||||
"TC-CONLOG-172c only a line that can neither own the wire nor fit the ring counts as a real drop, and it never half-sends");
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
dbg_console_tx_end();
|
||||
writer_drain();
|
||||
CHECK((con_uart_tx_n >= (21U * (uint32_t)DBG_CON_LINE_MAX))
|
||||
&& tx_contains("[ERR] CONSOLE dropped 1"),
|
||||
"TC-CONLOG-172d once ownership frees up the deferred round goes out complete, and the true overflow surfaces as [ERR]");
|
||||
/* 172e 报出去就销账(既有语义,本卡不许退化):账清零 + 不重复报同一笔 */
|
||||
CHECK((dbg_console_dropped() == 0U) && (tx_count("[ERR] CONSOLE dropped 1") == 1U),
|
||||
"TC-CONLOG-172e the drop ledger is settled only after the [ERR] note actually left the wire, and it is not reported twice");
|
||||
|
||||
/* 173 写者一轮 drain 被所有权挡住:让位 != 丢 */
|
||||
reset_all();
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
(void)dbg_console_tx_begin();
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
dbg_console_writer_attach();
|
||||
(void)dbg_console_write_line("L1\r\n", 4U);
|
||||
CHECK((con_uart_tx_n == 0U) && (used_now() == 4U),
|
||||
"TC-CONLOG-173 while another task owns the wire the writer sends nothing and keeps every byte queued");
|
||||
writer_drain();
|
||||
CHECK((con_uart_tx_n == 0U) && (used_now() == 4U) && (dbg_console_dropped() == 0U),
|
||||
"TC-CONLOG-173b yielding a round is not a drop: the tail never advances, so no ledger moves");
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
dbg_console_tx_end();
|
||||
CHECK(s_con_tx_owner == 0,
|
||||
"TC-CONLOG-173c ownership is really returned (a leaked token would push every later line into the ring forever)");
|
||||
writer_drain();
|
||||
CHECK(tx_is(0U, "L1\r\n") && (used_now() == 0U) && (dbg_console_dropped() == 0U),
|
||||
"TC-CONLOG-173d the deferred round goes out complete as soon as the wire is free");
|
||||
|
||||
/* 174 同任务嵌套(写者一轮里再裸发留痕行)不得自我丢弃 */
|
||||
reset_all();
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
(void)dbg_console_tx_begin();
|
||||
dbg_console_write_raw("SEGA", 4U);
|
||||
dbg_console_write_raw("SEGB", 4U);
|
||||
dbg_console_tx_end();
|
||||
CHECK((con_uart_tx_n == 8U) && tx_is(0U, "SEGASEGB") && (dbg_console_dropped() == 0U)
|
||||
&& (used_now() == 0U),
|
||||
"TC-CONLOG-174 nested raw writes by the owner go out at once, in order, with zero drops");
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
dbg_console_write_raw("NEXT", 4U);
|
||||
CHECK((con_uart_tx_n == 12U) && tx_is(8U, "NEXT") && (used_now() == 0U),
|
||||
"TC-CONLOG-174b after tx_end the next task owns the wire again (no permanent ring detour)");
|
||||
|
||||
/* 175 异常上下文:放行,不等、不计丢弃(崩溃遗言必须能出去) */
|
||||
reset_all();
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
(void)dbg_console_tx_begin();
|
||||
con_ipsr = 1U;
|
||||
dbg_console_write_raw("[ERR] HARD", 10U);
|
||||
CHECK((con_uart_tx_n == 10U) && tx_is(0U, "[ERR] HARD"),
|
||||
"TC-CONLOG-175 an exception context sends at once even while a task owns the wire (crash logs are the last words)");
|
||||
CHECK((dbg_console_dropped() == 0U) && (used_now() == 0U)
|
||||
&& (s_con_tx_owner == (TaskHandle_t)&s_task_other),
|
||||
"TC-CONLOG-175b the bypass neither counts a drop nor touches the owner token (that would free a line in flight)");
|
||||
con_ipsr = 0U;
|
||||
|
||||
/* 176 调度器未启动:单执行流,不取凭证、不开临界区、一条不丢 */
|
||||
reset_all();
|
||||
con_task_self = 0;
|
||||
(void)dbg_console_tx_begin();
|
||||
dbg_console_write_raw("EARLY", 5U);
|
||||
CHECK((con_uart_tx_n == 5U) && tx_is(0U, "EARLY") && (dbg_console_dropped() == 0U)
|
||||
&& (s_con_tx_owner == 0) && (con_crit_enter == 0U),
|
||||
"TC-CONLOG-176 before the scheduler starts, the single execution stream sends directly, taking no token and no critical section");
|
||||
|
||||
/* 177 硬红线:整行独占绝不能把 @115200 约 7.8 ms 的发送包进关中断区 */
|
||||
reset_all();
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
dbg_console_writer_attach();
|
||||
(void)dbg_console_write_line(s_prot_line, (unsigned)strlen(s_prot_line));
|
||||
writer_drain();
|
||||
CHECK((con_uart_tx_n == (uint32_t)strlen(s_prot_line)) && (con_uart_crit_at_tx_max == 0U)
|
||||
&& tx_is(0U, s_prot_line),
|
||||
"TC-CONLOG-177 every byte reaches the wire with interrupts enabled (ownership never wraps the transmit)");
|
||||
|
||||
/* 178 台架原文形态复演:那条被插进 [20 的 PROT 行,现在要么完整、要么整条让路 */
|
||||
/* 逐字节照抄台架那一行的原文(含时间戳前缀与 CRLF),长度交给 strlen 现算:
|
||||
* 手数字节数自己就踩过一次坑(把 93 数成 39,用例先红的是我的算术) */
|
||||
reset_all();
|
||||
con_uart_tx_hook_at = (uint32_t)strlen(s_prot_line) - 30U;
|
||||
con_uart_tx_hook = s_tx_second_writer;
|
||||
dbg_console_write_raw(s_prot_line, (unsigned)strlen(s_prot_line));
|
||||
CHECK((con_uart_tx_n == (uint32_t)strlen(s_prot_line)) && tx_is(0U, s_prot_line)
|
||||
&& (used_now() == 6U) && (dbg_console_dropped() == 0U),
|
||||
"TC-CONLOG-178 the exact bench shape (that PROT line, interrupted mid-transmit) comes out unmangled: the [20 insertion is structurally gone");
|
||||
|
||||
/* 179 begin 失败不吞字、不越权归还 */
|
||||
reset_all();
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
(void)dbg_console_tx_begin();
|
||||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||||
CHECK(dbg_console_tx_begin() == 0U,
|
||||
"TC-CONLOG-179 tx_begin reports failure instead of blocking when another task owns the wire");
|
||||
dbg_console_write_raw("FREE", 4U);
|
||||
CHECK((con_uart_tx_n == 0U) && (used_now() == 4U) && (dbg_console_dropped() == 0U),
|
||||
"TC-CONLOG-179b a failed begin still delivers the caller bytes (deferred, never swallowed)");
|
||||
dbg_console_tx_end();
|
||||
CHECK(s_con_tx_owner == (TaskHandle_t)&s_task_other,
|
||||
"TC-CONLOG-179c a non-owner tx_end cannot release someone else ownership (that would re-open this very hole)");
|
||||
con_task_self = (TaskHandle_t)&s_task_other;
|
||||
dbg_console_tx_end();
|
||||
CHECK(s_con_tx_owner == 0, "TC-CONLOG-179d the real owner releases normally");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
int main(void)
|
||||
{
|
||||
@@ -1189,6 +1399,7 @@ int main(void)
|
||||
tc_counted_paths();
|
||||
tc_three_ledgers();
|
||||
tc_check_then_act();
|
||||
tc_ownership();
|
||||
|
||||
printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
@@ -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