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);
|
||||
|
||||
Reference in New Issue
Block a user