Files
STM32F4-Base/test/host/console_log/console_log_test.c
T
wandering1 b1494eb10c feat(固件): 标定值语义 setter + 卷锁 fail-open 自检留痕 + 控制台编号写法整理
三批同笔的理由:它们是同一份权威构建(Linker run 04:5x,Code=233,900 / RW=1,536)里的内容,中间态从未
单独编译过,拆开提任何一段都没有对应的构建凭证。按道分述如下。

**① 算法层(ui-maint):写值与置脏成对,不留可单独调用的置脏口**。新增 run_par_cal_k_set() /
run_par_cal_b_set(),被拒时副本不动也不置脏(= 不承诺持久化);104 的 run_par_104_write() k/b 两支
同批改调 setter,函数内自己碰 s_dirty 的两处删净——**消除两份置脏逻辑才是本段的目的**。刻意**没有**新增
公开 run_par_mark_dirty():作者的理由我采纳并原文保留——「一旦存在就一定有人只调它」,与今天删掉的零消费
者接口是同一失效机理。另新增合法域唯一出口 run_par_db_range()、粗守上限 RUN_PAR_K_MAX_UV、毫→工程换算
单点 s_milli_to_eng(),并改正了头文件里「除以 1000 唯一在此」那条在加 db_range 后会变假陈述的说法。
**顺带满足 task-43 第①项**:adc_cal 落盘口的缺口由本段 setter 解决(写值必置脏),task-43 剩余项 = 可诊断
性与文档。

**② 日志层(log-maint):卷锁 fail-open 的 defaultTask 侧边沿留痕**。syslog_drain() 尾部紧跟既有
s_quiet_
drop 差值块之后读 vol_lock_get_stats(),水位新增才算一个 episode、每上电一条,投 SYSLOG_SELFTEST,文本
vollock failopen=%lu。三条硬约束都落了且**有牙**:失败调用点零改动(wdev.log 自己也要这把非递归卷锁);
不打错级、**不打 [ERR]**(断言里 g_sys_err_count == 0 就是机器判据,把「改成 DBG_ERROR」注入后四条用例
全红);锁层只自增、唯一写者消费。作者一度按我的要求准备造 obs_first_ms/obs_last_ms 字段,我撤了那条
要求——记录自带 ts 就是被观测时刻,**再造一个时间戳字段等于造第二个「看起来像发生时刻」的东西**。
成本 .bss +48 B、**零栈增量**(缓冲全静态),已由本笔权威构建的 after 表证实:9 条链全 PASS,
iec104Task 1,464 / margin 1,096、defaultTask 3,040 与提交态逐项一致。

**③ 控制台与中断(log-maint,纯注释/文档)**:b623a406 之后的编号写法整理,把裸形态号改为可校验形式,
零代码行为改动。

实测:@build 0 错误 0 警告;run_par 67/67、sys_log_mcu 141/141、console_log 套件零回归
覆盖率只增不减
(sys_log_mcu.c 410/413 = 99.3%、函数 22/22);cppcheck / coding-standard /
doc-links / bom / mojibake /
nested-comment 全 0;编号五项式错误差值 0、不可判定绝对数 **1,156**(上限 1,166 未动,本笔没吃余量)。
台账随本笔回填:ram_baseline --write 与 mem-numbers --write 同批(RW_IRAM1 80,920 B 61.7%
/
RW_CCM 57,376 B 87.9% / ER_IROM1 260,004 B 24.8%)。
2026-10-27 05:10:11 +08:00

1476 lines
72 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_log)主机侧单元测试
* 模块功能:在 PC 上离线验证 App/log/console_log.c 的单写者契约(G1~G5 / K1~K5,见
* docs/子系统设计/控制台单写者日志设计说明.md):环形缓冲入环与分块发送、
* 行级原子、192B 截断、写者未注册/ISR 上下文的直发(未注册期自 2026-10-26
* 起是**整行独占**,不再是"无锁":M 族 TC-CONLOG-170 至 TC-CONLOG-179 钉"别的写者插不进
* 一行、让路行逐字节等同直发、让位不丢、真溢出才 [ERR]")、缓冲满有界等待与
* 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、
* huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算、
* 整行所有权(行级单一所有者,道G task-23)。
*
* 为什么要主机侧单测(而不是只用台架):
* 本模块的风险全在"异常时刻"——UART 僵尸通道卡死、TIM7 停摆导致 uwTick 不往前走、
* 开机 1.7s 喂狗容限被背靠背块超时吃掉(台架实测的停喂误复位)、写者注册前多任务裸发
* 逐字节交错(2026-10-10/10-12 台架实证)。台架上这些要么一年一遇、要么现场已被
* 复位冲掉;而它们的判据全是"数得清的数":192B 上限、256B 分块、4095B 可用环、
* 200 tick 等待、20000 次计数上界、100/250/300ms 三条时间线。把它们做成断言,是这台
* 设备上唯一能把这几条线钉死的办法(板端套件只能"在真机上跑一遍")。
*
* 桩的边界(细节见 stub/stm32f4_fake.h 头注释):
* console_log.h / sys_events.h(含真实 SYS_EVT_CONSOLE_TX 位)用**真实头**;
* usart.h 用桩,但只暴露本模块访问的 huart1 + MX_USART1_UART_Init,寄存器位号 /
* 枚举值照抄真实头;USART SR 的 TXE/TC、DWT CYCCNT、HAL_GetTick、临界区、
* __get_IPSR、vTaskDelay、osEventFlagsSet 做成可控 + 可观测的量。
* ⚠ 本文件**刻意不定义**任何 syslog_* / DBG_* 符号:console_log.c 哪天把"TX 故障
* 留痕"改回直接调日志子系统,链接就失败——那正是必须挡住的(write_line →
* con_append → drain → syslog_post → write_line 成环,静态栈深在每个任务上虚高)。
*
* 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 127 号 / 127 条断言):
* 000..006 宿主 printf 与本模块 fputc 的隔离 + 参数校验与 192B 截断
* 010..018 写者未注册 = 无锁直发(不入环、不置事件位、零临界区、不等待、256B 有界)
* 020..038 注册后走环形:入环不发送、按 256B 分块、多生产者不交错、跨环尾两段拷贝、
* 入环长度钳 RING/2
* 040..044 写者自记录先排空(不自等)/ drain 重入保护 / 未注册时 drain 让位
* 050..062 缓冲满:DWT 硬上界丢弃、不覆盖旧行、等待中出现空间、无 DWT 时 200 tick
* 上界、[ERR] dropped 直发不经环、读后即清
* 070..088 TX 僵尸通道:软复位自愈序列、故障即止、WARN 只一次、tx_fault_log_take
* 读后清、300ms 冷却两端、tick==0 也进冷却(哨兵撞值修好后为正向断言)、
* 半块 / 已上线仍判失败
* 090..106 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读
* HAL_GetTick(第 8b 轮台架动机)、huart1.Instance == NULL 短路两路 +
* "未初始化绝不 reinit"(误自愈修好后为正向断言)+ 证据不蒸发
* 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR
* 仍不等待)
* 120..122 drain 单次 250ms 预算与下轮续排(让位喂狗但不丢字节)
* 130..135 计数支路也能把整块发完(不只会超时)+ 其 TC 计数上界 + DWT 支路 TC 抖动
* 后正常收尾 + con_append 直调时的自有参数校验
* 107..108 冷却截止 tick 跨 49.7 天回绕两端(无符号差值),改用独立标志后仍成立
* 140..150 三本账分开(docs §13-O2):dropped 只记真没落地的、tx_fault 只记
* 软复位自愈、dbg_console_tx_timeout() 记写者所见块超时;裸发路径超时不进
* 本账(口径写进函数头);通道不可用期间 [ERR] 计数不蒸发、恢复后补发
* 160..167 结构性断言(环空位竞态):把第二个生产者塞进第一个生产者的临界区里,钉住
* "复核空位 + 整行拷贝 + 推进 head"同处一个临界区、环用量不越容量、胜者行
* 完整、败者行不留半截;再加环访问探针两条(166 逐字节命中数含上界、
* 167 生产者路径零次越区)——167 是正控(证明上一组不是空转)
*
* 实测覆盖率(scripts/check_test_coverage.py --module console_log,/Od + /Zi 口径):
* App/log/console_log.c **199/200 行 = 99.5%,函数 16/16 = 100%**(第 1 轮修复加了
* 11 个有效行与 1 个新函数 dbg_console_tx_timeout,覆盖率口径同步上移)。
*
* 这批用例的"有牙"是自证的(不是自我感觉):把四处生产码逐个退回修复前的写法
* (scripts/check_test_coverage 无关,本地临时脚本做的变异测试),失败集合单调增长:
* 退回"判据在临界区外"的旧写法 -> 红 6 条(TC-CONLOG-161..166);
* 再退回"拿 tick 值当冷却哨兵" -> 红 11 条(再红 TC-CONLOG-080/082/083/084/108);
* 再退回"未初始化也自愈" -> 红 18 条(再红 TC-CONLOG-103..106/144/148/149);
* 四条全退回 -> 红 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`。
* note[64],前缀 24B + "%lu"(unsigned long 最多 10 位)+ 后缀 8B = 最长 42B
* < 63B,任何输入都到不了这一行 —— 纯防御,不是漏测。
* - 真实周期时间量级:主机没有真 DWT CYCCNT 也没有 TIM7 的 uwTick,只能钉"判据在
* 第几次读成立 / 自旋几次放弃",不能声称"实测 100ms 就返回"。
* - ISR 与写者同时裸发的真总线交错(docs §13-O1):宿主单线程,造不出两个执行流。
* - osEventFlagsWait 唤醒写者与 50ms 兜底:写者任务体在 App/task/default_task.c。
* - MicroLIB 真实 retarget 链接语义:宿主 CRT 有自己的 fputc(实测与本模块符号
* 冲突 -> LNK2005),故本 TU 把被测 fputc 改名纳入,只验函数体不验链接钩子。
* - 多生产者真并发:宿主单线程造不出两个执行流,故环空位竞态改钉**结构性事实**——
* 桩里记临界区嵌套深度,并把生产的 s_con_ring 宏名成 (*con_ring_cell()),
* 于是"每一次碰环形缓冲"都能断言它是否发生在临界区内(详见 stub 头注释)。
* 这不是"模拟并发",是把"判据与推进之间不出区"这句话本身变成可执行断言。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL;跑飞保险 exit(2)。
*
* 创建日期:2026-10-24
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* 平台假件先纳入:它给出 USART/DWT/HAL/内核入口的宿主等价声明(stub 目录排第一) */
#include "stm32f4_fake.h"
/* ===================== 假件全局量(声明见 stub/stm32f4_fake.h) ===================== */
uint32_t SystemCoreClock = 168000000U; /* 与 MCU 端一致(168 MHz) */
USART_TypeDef g_con_uart;
CoreDebug_Type g_con_core_debug;
DWT_Type g_con_dwt;
UART_HandleTypeDef huart1;
uint32_t con_uart_tx_n;
uint8_t con_uart_tx[CON_UART_TX_MAX];
uint32_t con_uart_block_n;
uint32_t con_uart_block_len[CON_UART_BLOCK_MAX];
uint32_t con_uart_cur_len;
uint32_t con_uart_txe_reads;
uint32_t con_uart_tc_reads;
int con_uart_txe_stuck;
int con_uart_txe_after;
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;
uint32_t con_dwt_reads;
uint32_t con_tick_now;
uint32_t con_tick_step;
uint32_t con_tick_reads;
static char s_task_writer; /* 假写者任务句柄(取地址当不透明句柄) */
static char s_task_other; /* 假"别的任务"句柄 */
TaskHandle_t con_task_self;
uint32_t con_delay_calls;
void (*con_delay_hook)(void);
uint32_t con_crit_enter;
uint32_t con_crit_exit;
uint32_t con_crit_max_depth;
/* PRIMASK 版关中断窗口的账(道G task-26:本模块自己的取还/入环窗口用它,不再走内核
* 临界区 —— 那会把 configASSERT 环接进每一条含临界区的任务链,栈记账 9 链各 +80 B) */
uint32_t con_pm_enter;
uint32_t con_pm_exit;
uint32_t con_pm_max_depth;
uint32_t con_pm_at_tx_max;
void (*con_pm_hook)(void);
static uint32_t s_pm_depth;
static uint32_t s_crit_depth;
/* 环形缓冲实体 + 探针(钩子本体在 stub/stm32f4_fake.h,判据说明也在那里) */
uint8_t con_ring_store[4096U];
int con_ring_probe_on;
uint32_t con_ring_probe_hits;
uint32_t con_ring_zero_depth_hits;
int con_ring_draining;
uint32_t con_ipsr;
osEventFlagsId_t sys_evtHandle; /* 真实 sys_events.h 声明的全局句柄 */
static char s_evt_obj; /* 假事件组对象(非 NULL 即可) */
uint32_t con_evt_sets;
uint32_t con_evt_flags;
uint32_t con_nvic_clears;
uint32_t con_rcc_disables;
uint32_t con_rcc_enables;
static uint32_t s_mx_init_calls;
static int s_mx_repair; /* 1 = 重初始化把僵尸通道治好(模型行为) */
/* ===================== 假件实现 ===================== */
uint32_t HAL_GetTick(void)
{
uint32_t v = con_tick_now;
con_tick_reads++;
if (con_tick_reads > 2000000U) {
printf("[FATAL] fake HAL_GetTick called %lu times (runaway)\n",
(unsigned long)con_tick_reads);
exit(2);
}
con_tick_now += con_tick_step;
return v;
}
TaskHandle_t xTaskGetCurrentTaskHandle(void)
{
return con_task_self;
}
void vTaskDelay(TickType_t ticks)
{
(void)ticks;
con_delay_calls++;
if (con_delay_calls > 5000U) {
printf("[FATAL] fake vTaskDelay called %lu times (runaway)\n",
(unsigned long)con_delay_calls);
exit(2);
}
if (con_delay_hook != 0) {
con_delay_hook();
}
}
void (*con_crit_hook)(void);
void con_crit_push(void)
{
con_crit_enter++;
s_crit_depth++;
if (s_crit_depth > con_crit_max_depth) {
con_crit_max_depth = s_crit_depth;
}
if (con_crit_hook != 0) {
con_crit_hook(); /* 深度已记好,注入体看到的是"已在区内" */
}
}
uint32_t con_crit_depth(void)
{
return s_crit_depth;
}
void con_crit_pop(void)
{
con_crit_exit++;
if (s_crit_depth == 0U) {
printf("[FATAL] critical section exit without enter\n");
exit(2);
}
s_crit_depth--;
}
/* PRIMASK 窗口桩:与上面的内核临界区平行(生产侧 __get_PRIMASK/__disable_irq/
* __set_PRIMASK 展开到这里)。成对建模即可——生产侧把 PRIMASK 存在**局部**变量里
* 并成对写回,所以嵌套时深度自然回升;未配对就退出说明生产代码写错了窗口。 */
void con_pm_push(void)
{
con_pm_enter++;
s_pm_depth++;
if (s_pm_depth > con_pm_max_depth) {
con_pm_max_depth = s_pm_depth;
}
if (con_pm_hook != 0) {
con_pm_hook(); /* 深度已记好:注入体看到的是"已在区内" */
}
}
void con_pm_pop(void)
{
con_pm_exit++;
if (s_pm_depth == 0U) {
printf("[FATAL] PRIMASK window exit without enter (unbalanced CON_CRIT_*)\n");
exit(2);
}
s_pm_depth--;
}
uint32_t con_pm_depth(void)
{
return s_pm_depth;
}
osStatus_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags)
{
(void)ef_id;
con_evt_sets++;
con_evt_flags = flags;
return (osStatus_t)flags;
}
/* 真实版是 CubeMX 生成的 HAL_UART_Init 封装;桩按脚本决定"软复位到底治没治好" */
void MX_USART1_UART_Init(void)
{
s_mx_init_calls++;
huart1.Instance = &g_con_uart;
huart1.gState = HAL_UART_STATE_READY;
huart1.ErrorCode = HAL_UART_ERROR_NONE;
if (s_mx_repair != 0) {
con_uart_txe_stuck = 0;
con_uart_txe_after = 0;
con_uart_txe_ready_left = -1;
con_uart_tc_after = 0;
con_uart_tc_stuck = 0;
}
}
void con_uart_reset(void)
{
memset(&g_con_uart, 0xFF, sizeof(g_con_uart)); /* SR 恒全 1:就绪与否由钩子决定 */
con_uart_tx_n = 0U;
con_uart_block_n = 0U;
con_uart_cur_len = 0U;
con_uart_txe_reads = 0U;
con_uart_tc_reads = 0U;
con_uart_txe_stuck = 0;
con_uart_txe_after = 0;
con_uart_txe_ready_left = -1;
con_uart_tc_after = 0;
con_uart_tc_stuck = 0;
con_uart_pending = 0;
}
void con_dwt_reset(void)
{
memset(&g_con_dwt, 0, sizeof(g_con_dwt));
memset(&g_con_core_debug, 0, sizeof(g_con_core_debug));
con_dwt_step = 0U;
con_dwt_ctrl_sticky = 1;
con_dwt_reads = 0U;
}
/* ===================== 断言 ===================== */
static int s_total, s_passed, s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码(原样纳入生产源) =====================
* ⚠ 唯一一处生产符号改名:**宿主 CRT 自己占用了 fputc 这个全局符号**(实测
* libucrt.lib(fputc.obj) 与本 TU 冲突 -> LNK2005 / LNK1169,ARMCC + MicroLIB 上
* 没有这件事,那里 fputc 正是留给用户实现的 retarget 钩子)。所以把被测函数临时
* 改名后再纳入,函数体与行覆盖统计一字未动,测试直接调 con_fputc_under_test 验它。
* 顺带回答"测试自己的 printf 会不会被这个钩子吞掉":不会——宿主 CRT 用的是内部
* _fputc_nolock/_output 那条链,不解析用户态 fputc;TC-CONLOG-000 再钉一次。 */
#define fputc con_fputc_under_test
#include "console_log.c"
#undef fputc
/* ===================== 复位:生产 static + 假件,一次不漏 ===================== */
static void reset_all(void)
{
/* 生产模块的 static(10 个数据项 + 1 个任务句柄)——漏一个就会跨场景污染 */
memset(s_con_ring, 0, sizeof(s_con_ring));
s_con_head = 0U;
s_con_tail = 0U;
s_con_drop = 0U;
s_con_tx_fault = 0U;
s_con_tx_fault_log = 0U;
s_con_cool_until = 0U;
s_con_cooling = 0U;
s_con_tx_timeout = 0U;
s_con_draining = 0U;
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();
con_tick_now = 0U;
con_tick_step = 0U;
con_tick_reads = 0U;
con_task_self = (TaskHandle_t)&s_task_other;
con_delay_calls = 0U;
con_delay_hook = 0;
con_crit_hook = 0;
con_crit_enter = 0U;
con_crit_exit = 0U;
con_crit_max_depth = 0U;
s_crit_depth = 0U;
/* PRIMASK 窗口一样要清:漏一项就是"单独跑绿、连跑偶红"(上一轮的教训) */
con_pm_enter = 0U;
con_pm_exit = 0U;
con_pm_max_depth = 0U;
con_pm_at_tx_max = 0U;
con_pm_hook = 0;
s_pm_depth = 0U;
memset(con_ring_store, 0, sizeof(con_ring_store));
con_ring_probe_on = 0;
con_ring_probe_hits = 0U;
con_ring_zero_depth_hits = 0U;
con_ring_draining = 0;
con_ipsr = 0U;
sys_evtHandle = (osEventFlagsId_t)&s_evt_obj;
con_evt_sets = 0U;
con_evt_flags = 0U;
con_nvic_clears = 0U;
con_rcc_disables = 0U;
con_rcc_enables = 0U;
s_mx_init_calls = 0U;
s_mx_repair = 0;
huart1.Instance = &g_con_uart;
huart1.gState = HAL_UART_STATE_READY;
huart1.RxState = HAL_UART_STATE_READY;
huart1.ErrorCode = HAL_UART_ERROR_NONE;
}
/* ===================== 观察辅助 ===================== */
static uint32_t used_now(void)
{
return con_used();
}
static const char *tx_at(uint32_t skip)
{
return (skip >= con_uart_tx_n) ? "" : (const char *)&con_uart_tx[skip];
}
static int tx_is(uint32_t skip, const char *s)
{
size_t n = strlen(s);
if ((size_t)con_uart_tx_n < (size_t)skip + n) {
return 0;
}
return memcmp(tx_at(skip), s, n) == 0;
}
/* 断言 [0, n) 全部是同一个字符(回绕场景下按字符比对,不依赖 NUL 结尾) */
static int tx_prefix_all(char c, uint32_t n)
{
uint32_t i;
if (con_uart_tx_n < n) {
return 0;
}
for (i = 0U; i < n; i++) {
if (con_uart_tx[i] != (uint8_t)c) {
return 0;
}
}
return 1;
}
static int tx_at_is(char c, uint32_t skip, uint32_t n)
{
uint32_t i;
if (con_uart_tx_n < skip + n) {
return 0;
}
for (i = 0U; i < n; i++) {
if (con_uart_tx[skip + i] != (uint8_t)c) {
return 0;
}
}
return 1;
}
/* 在已上线字节里找一段(tx 是二进制缓冲,不依赖 NUL 结尾) */
static int tx_contains(const char *needle)
{
size_t nl = strlen(needle);
uint32_t i;
if ((size_t)con_uart_tx_n < nl) {
return 0;
}
for (i = 0U; (size_t)i + nl <= (size_t)con_uart_tx_n; i++) {
if (memcmp(&con_uart_tx[i], needle, nl) == 0) {
return 1;
}
}
return 0;
}
static uint32_t tx_count(const char *needle)
{
size_t nl = strlen(needle);
uint32_t i;
uint32_t hits = 0U;
if ((size_t)con_uart_tx_n < nl) {
return 0U;
}
for (i = 0U; (size_t)i + nl <= (size_t)con_uart_tx_n; i++) {
if (memcmp(&con_uart_tx[i], needle, nl) == 0) {
hits++;
}
}
return hits;
}
/* 写者任务上下文里排空(生产契约:drain 只允许写者任务调) */
static void writer_drain(void)
{
con_task_self = (TaskHandle_t)&s_task_writer;
con_ring_draining++; /* 此刻读环的是写者,不计入生产者违规 */
dbg_console_drain();
con_ring_draining--;
con_task_self = (TaskHandle_t)&s_task_other;
}
/* 后台排空(不改当前任务句柄):模拟"生产者等一轮,写者腾出了空间" */
static void bg_drain_hook(void)
{
con_ring_draining++;
dbg_console_drain();
con_ring_draining--;
}
/* 把环填到只剩 want_free 字节可用:走真实入环路径(顺带踩到回绕)。
* 填充期间把 DWT 上界调大——万一算差一位也不会卡在等待里出不来。 */
static void fill_ring(uint32_t want_free)
{
char pad[DBG_CON_LINE_MAX];
uint32_t i;
memset(pad, 'Z', sizeof(pad));
con_dwt_step = SystemCoreClock / 4U;
for (i = 0U; i < 40U; i++) {
uint32_t free_space = (uint32_t)((DBG_CON_RING_SIZE - 1U) - used_now());
uint32_t n;
if (free_space <= want_free) {
break;
}
n = free_space - want_free;
if (n > DBG_CON_LINE_MAX) {
n = DBG_CON_LINE_MAX;
}
if (dbg_console_write_line(pad, n) != (int)n) {
break;
}
}
con_dwt_step = 0U;
s_con_drop = 0U; /* 填充过程中的边角丢弃不算场景断言 */
}
/* 注册写者(defaultTask),随后把"当前任务"换回别的任务 */
static void attach_writer(void)
{
con_task_self = (TaskHandle_t)&s_task_writer;
dbg_console_writer_attach();
con_task_self = (TaskHandle_t)&s_task_other;
}
/* ============ TC-CONLOG-000..006 参数校验与截断 ============ */
static void tc_params(void)
{
char buf[DBG_CON_LINE_MAX + 128U];
printf("-- TC-CONLOG-000..006 argument checks and truncation --\n");
reset_all();
printf("(probe) this line goes through the host CRT\n");
fflush(stdout);
CHECK(con_uart_tx_n == 0U && used_now() == 0U,
"TC-CONLOG-000 host printf does not pass through this module fputc (else test output would be swallowed into the ring)");
reset_all();
con_uart_txe_stuck = 1; /* 通道卡死:任何字节都不该上线 */
CHECK(dbg_console_write_line(0, 5U) == -1, "TC-CONLOG-001 write_line(NULL) returns -1");
CHECK(dbg_console_write_line("abc", 0U) == -1, "TC-CONLOG-002 write_line(len=0) returns -1");
dbg_console_write_raw(0, 5U);
dbg_console_write_raw("abc", 0U);
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-003 both NULL and len=0 never touch the UART");
CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-004 invalid arguments are not counted as drops");
reset_all();
memset(buf, 'A', sizeof(buf));
CHECK(dbg_console_write_line(buf, sizeof(buf)) == (int)DBG_CON_LINE_MAX,
"TC-CONLOG-005 a line longer than DBG_CON_LINE_MAX is truncated to 192 (return value is the delivered byte count)");
CHECK(con_uart_tx_n == DBG_CON_LINE_MAX, "TC-CONLOG-006 after truncation exactly 192 bytes reach the wire");
}
/* ============ TC-CONLOG-010..018 写者未注册 = 无锁直发 ============ */
static void tc_unattached(void)
{
char big[DBG_CON_CHUNK_MAX + 64U];
printf("-- TC-CONLOG-010..018 writer not attached: lock-free direct send --\n");
reset_all();
CHECK(dbg_console_writer_ready() == 0U, "TC-CONLOG-010 writer_ready() == 0 before attach");
CHECK(dbg_console_write_line("hello", 5U) == 5, "TC-CONLOG-011 before attach the whole line is sent directly, returning its length");
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)");
/* ⚠ 本条判据在道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 族)。
*
* ⚠ **第二次换钉**(道G task-26,2026-10-26):上面那次只是把数字从 0 挪到 2,
* 钉子仍然钉在**实现细节**(用的是哪一种关中断原语)而不是行为上。本笔把裸发与
* 入环路径的窗口从内核临界区换成 PRIMASK(原因与实测的"9 条任务链各 +80 B"写在
* App/log/console_log.c 的 CON_CRIT_ENTER 注释里:taskEXIT_CRITICAL ⇒
* vPortExitCritical ⇒ configASSERT ⇒ 断言钩子 ⇒ 回到本模块,armlink 把这条环算进
* 每一条含临界区的任务链)。判据随之搬成三条**等价且更强**的行为断言:
* ① 窗口次数与嵌套深度不变(取 + 还各一次、深度 1)=> 独占语义一点没丢;
* ② 整条裸发路径 **0 次内核临界区调用** => 这同时就是"那条 configASSERT 环不
* 成立"的机器证据,不靠任何人的口头保证;
* ③ 上线字节看到的**两种**屏蔽深度水位都为 0 => "发送绝不包进关中断区"这条红线
* 换了原语之后照旧有牙。断言条数:本段由 2 条变 3 条,只增不减。
*/
CHECK((con_pm_enter == 2U) && (con_pm_exit == 2U) && (con_pm_max_depth == 1U)
&& (con_crit_enter == 0U),
"TC-CONLOG-015 direct send takes exactly two few-instruction PRIMASK windows (claim + return), never nests them, and calls the kernel critical API zero times (that is what keeps the configASSERT cycle out of every task's worst-case stack)");
/* ⚠ 编号**不退役**:015b 这个号生产注释(App/log/console_log.c 的 CON_CRIT_ENTER
* 一处、Src/stm32f4xx_it.c 的 hard_fault_write 一处)与本文档都在引用,一旦把它换成
* 新号,那些引用立刻变成悬空号(今天已经被这类问题咬过两次)。所以这里保持
* 015b = "上线字节的屏蔽水位恒 0"这条红线(换了原语,语义照旧),新加的"绝不等待"
* 另起 015c。 */
CHECK((con_uart_crit_at_tx_max == 0U) && (con_pm_at_tx_max == 0U),
"TC-CONLOG-015b every byte reaches the wire with interrupts enabled under BOTH masking primitives (the ~7.8 ms transmit is never inside a critical window)");
CHECK(con_delay_calls == 0U,
"TC-CONLOG-015c whole-line ownership never waits: contention yields on the spot instead of queueing");
reset_all();
memset(big, 'B', sizeof(big));
dbg_console_write_raw(big, sizeof(big));
CHECK(con_uart_tx_n == DBG_CON_CHUNK_MAX, "TC-CONLOG-016 write_raw is bounded: only 256 bytes are sent");
reset_all();
con_uart_txe_after = 3; /* 前 3 次不就绪(TXE 抖动,未超上界) */
CHECK(dbg_console_write_line("CD", 2U) == 2, "TC-CONLOG-017 after TXE jitter the whole line still goes out");
CHECK(tx_is(0U, "CD") && con_delay_calls == 0U,
"TC-CONLOG-018 direct send neither waits on tick nor enters the ring (premise for ISR and crash hooks)");
}
/* ============ TC-CONLOG-020..037 注册后走环形缓冲 ============ */
static void tc_ring(void)
{
char line[DBG_CON_LINE_MAX];
static char oversize[3000]; /* 超长入环的源(>= 钳位后的 2048B) */
uint32_t i;
printf("-- TC-CONLOG-020..027 after attach: producers enter the ring, writer sends --\n");
reset_all();
attach_writer();
CHECK(dbg_console_writer_ready() == 1U, "TC-CONLOG-020 writer_ready() == 1 after attach");
CHECK(s_con_writer_task == (TaskHandle_t)&s_task_writer,
"TC-CONLOG-021 attach records exactly the caller task handle (basis of the self-wait guard)");
CHECK(dbg_console_write_line("netTask up\r\n", 12U) == 12, "TC-CONLOG-022 a successful ring entry returns the line length");
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-023 producers never touch the UART");
CHECK(used_now() == 12U, "TC-CONLOG-024 the whole line enters the ring at once (used = 12)");
CHECK(con_evt_sets == 1U && con_evt_flags == SYS_EVT_CONSOLE_TX,
"TC-CONLOG-025 after entering the ring the real event bit SYS_EVT_CONSOLE_TX (bit6) wakes the writer");
CHECK(con_pm_enter == 1U && con_pm_exit == 1U && con_pm_max_depth == 1U
&& con_crit_enter == 0U,
"TC-CONLOG-026 one line takes exactly one PRIMASK window at depth one and no kernel critical section (implementation basis of line atomicity)");
writer_drain();
CHECK(tx_is(0U, "netTask up\r\n"), "TC-CONLOG-027 writer drain: wire bytes are byte-identical to what entered the ring");
printf("-- TC-CONLOG-028..032 256B chunking and no interleaving between producers --\n");
reset_all();
attach_writer();
memset(line, 'C', sizeof(line));
for (i = 0U; i < 5U; i++) {
(void)dbg_console_write_line(line, sizeof(line));
}
writer_drain();
CHECK(con_uart_block_n == 4U, "TC-CONLOG-028 960B is split into 4 blocks by DBG_CON_CHUNK_MAX");
CHECK(con_uart_block_len[0] == 256U && con_uart_block_len[1] == 256U
&& con_uart_block_len[2] == 256U && con_uart_block_len[3] == 192U,
"TC-CONLOG-029 block sizes 256/256/256/192 (the single-send ceiling is really enforced)");
CHECK(con_uart_tx_n == 960U && con_uart_txe_reads == 960U,
"TC-CONLOG-030 every byte is loaded into DR exactly once (write when ready, no resend)");
reset_all();
attach_writer();
con_task_self = (TaskHandle_t)&s_task_other;
(void)dbg_console_write_line("[A] 11111\r\n", 11U);
con_task_self = (TaskHandle_t)&s_task_writer; /* 写者自己也记一行 -> 先排空 */
(void)dbg_console_write_line("[B] 22222\r\n", 11U);
con_task_self = (TaskHandle_t)0; /* 连"无句柄"也算一个生产者 */
(void)dbg_console_write_line("[C] 33333\r\n", 11U);
writer_drain();
CHECK(tx_is(0U, "[A] 11111\r\n[B] 22222\r\n[C] 33333\r\n"),
"TC-CONLOG-031 three producers, one line each -> the wire still shows three intact lines (G1 line atomicity)");
CHECK(strstr((const char *)&con_uart_tx[5], "[B] 22222") != 0,
"TC-CONLOG-032 no line boundary was broken (no line spliced into another)");
printf("-- TC-CONLOG-033..037 ring wrap: two-segment copy + chunking by contiguous run --\n");
reset_all();
attach_writer();
s_con_head = 4090U;
s_con_tail = 4090U;
(void)dbg_console_write_line("0123456789ABCDEF", 16U);
CHECK(s_con_head == 10U && used_now() == 16U,
"TC-CONLOG-033 a wrapped line leaves head at 10 (4090+16-4096)");
CHECK(memcmp(&con_ring_store[4090], "012345", 6) == 0
&& con_ring_store[0] == (uint8_t)'6' && con_ring_store[9] == (uint8_t)'F'
&& con_ring_store[10] == 0U,
"TC-CONLOG-034 both wrap segments land correctly (first 6B fill to the ring end, remaining 10B start at 0)");
writer_drain();
CHECK(con_uart_block_n == 2U && con_uart_block_len[0] == 6U
&& con_uart_block_len[1] == 10U,
"TC-CONLOG-035 drain chunks by contiguous run: 6 + 10 (never reads past the ring end)");
CHECK(tx_is(0U, "0123456789ABCDEF"), "TC-CONLOG-036 the wrapped line reaches the wire with correct content and order");
reset_all();
attach_writer();
/* 源缓冲必须给到 3000B:con_append 只钳长度,不会钳读取范围,
* 拿 192B 的栈数组当 2048B 源就是越界读(插桩跑直接 0xC0000005) */
memset(oversize, 'D', sizeof(oversize));
CHECK(con_append(oversize, (uint16_t)3000U) == 0, "TC-CONLOG-037 calling con_append directly with an over-long buffer still succeeds");
CHECK(used_now() == (DBG_CON_RING_SIZE / 2U),
"TC-CONLOG-038 ring entry length is clamped to RING/2=2048 (so any line can always eventually enter)");
}
/* ============ TC-CONLOG-040..042 写者自记录 / 重入 / 未注册 drain ============ */
static void tc_writer_self(void)
{
char line[DBG_CON_LINE_MAX];
uint32_t i;
printf("-- TC-CONLOG-040..042 writer self-logging drains first / drain re-entry guard --\n");
reset_all();
attach_writer();
memset(line, 'E', sizeof(line));
for (i = 0U; i < 21U; i++) { /* 21*192 = 4032,只剩 63B 可用 */
(void)dbg_console_write_line(line, sizeof(line));
}
con_task_self = (TaskHandle_t)&s_task_writer; /* 写者自己记一行 */
CHECK(dbg_console_write_line("F", 1U) == 1, "TC-CONLOG-040 a writer logging about itself loses no line");
CHECK(con_delay_calls == 0U,
"TC-CONLOG-041 writer self-logging drains before entering: it never waits (waiting for itself would deadlock)");
CHECK(con_uart_tx_n == 4032U && used_now() == 1U && dbg_console_dropped() == 0U,
"TC-CONLOG-042 old lines all reach the wire, the new line enters the ring, zero drops");
reset_all();
attach_writer();
(void)dbg_console_write_line("hold", 4U);
s_con_draining = 1U; /* 模拟正处于 drain 之内 */
writer_drain();
CHECK(con_uart_tx_n == 0U && used_now() == 4U,
"TC-CONLOG-043 drain re-entry guard: a nested call yields at once (writer self-logging causes no recursive drain)");
reset_all();
(void)dbg_console_write_line("early\r\n", 7U); /* 未注册 -> 已直发 */
con_uart_tx_n = 0U;
writer_drain();
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-044 with no writer attached drain does nothing");
}
/* ============ TC-CONLOG-050..062 缓冲满 / 丢弃 / 可观测 ============ */
static void tc_full_drop(void)
{
printf("-- TC-CONLOG-050..053 ring full: DWT hard ceiling drops without overwriting old lines --\n");
reset_all();
attach_writer();
fill_ring(10U); /* 只剩 10B 可用 */
con_dwt_step = SystemCoreClock / 4U; /* 一次判据 = 250ms > 200ms 上界 */
CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1,
"TC-CONLOG-050 ring full + DWT ceiling reached -> this line is dropped (returns -1)");
CHECK(dbg_console_dropped() == 1U, "TC-CONLOG-051 drop counter +1");
CHECK(con_delay_calls == 0U,
"TC-CONLOG-052 with DWT the cycle criterion is used, no need to burn 200 ticks first (K4)");
CHECK(s_con_head == 4085U && used_now() == 4085U && con_uart_tx_n == 0U,
"TC-CONLOG-053 a drop does not advance head and the producer does not send itself (no half write, no overwrite of pending lines)");
printf("-- TC-CONLOG-054..056 space appears while waiting -> line enters the ring --\n");
reset_all();
attach_writer();
fill_ring(10U);
con_dwt_step = 0U; /* 时间静止:只能靠写者腾地方 */
con_delay_hook = bg_drain_hook; /* 每等一轮 = 写者排空一次 */
CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == 20,
"TC-CONLOG-054 after one wait round space frees up -> the whole line enters the ring");
CHECK(con_delay_calls == 1U && dbg_console_dropped() == 0U,
"TC-CONLOG-055 only one wait round happened and it is not counted as a drop");
con_delay_hook = 0;
CHECK(con_uart_tx_n == 4085U, "TC-CONLOG-056 it really was the writer that freed the space (old lines reached the wire)");
printf("-- TC-CONLOG-057..059 no-DWT branch: wait ceiling = DBG_CON_WAIT_MS ticks --\n");
reset_all();
con_dwt_ctrl_sticky = 0; /* 调试单元被关 -> DWT 使能失败 */
attach_writer();
fill_ring(10U);
CHECK(s_con_dwt_up == 0U, "TC-CONLOG-057 s_con_dwt_up stays 0 when DWT enable fails");
CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1,
"TC-CONLOG-058 without DWT a drop still happens (criterion falls back to tick counting, never waits forever)");
CHECK(con_delay_calls == DBG_CON_WAIT_MS && dbg_console_dropped() == 1U,
"TC-CONLOG-059 the no-DWT wait ceiling is exactly DBG_CON_WAIT_MS(200) vTaskDelay calls");
printf("-- TC-CONLOG-060..062 drop observability: the [ERR] line is sent raw, not through the ring --\n");
reset_all();
attach_writer();
fill_ring(10U);
con_dwt_step = SystemCoreClock / 4U;
(void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); /* 丢 1 行 */
con_dwt_step = 0U;
writer_drain();
CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines"),
"TC-CONLOG-060 the drain tail flushes [ERR] CONSOLE dropped 1 lines (loss is never silent, @test marks FAIL)");
CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-061 the counter is cleared once flushed (next round does not repeat the warning)");
CHECK(used_now() == 0U,
"TC-CONLOG-062 the warning goes out raw: not through the ring -> no con_append->drain recursion");
}
/* ============ TC-CONLOG-070..081 TX 僵尸通道:自愈 / 冷却 / 半块 ============ */
/* 公共前置:注册写者 + 把环填满 + 让通道卡死 */
static void fault_setup(uint32_t tick0, int repair)
{
attach_writer();
fill_ring(0U);
con_tick_now = tick0;
con_uart_txe_stuck = 1;
con_dwt_step = SystemCoreClock / 4U; /* 一次判据就跨过 CON_TX_TIMEOUT_CYC */
s_mx_repair = repair;
writer_drain();
}
static void tc_tx_fault(void)
{
printf("-- TC-CONLOG-070..074 soft reset did not heal: stop at the fault + trace only via flag --\n");
reset_all();
fault_setup(5000U, 0);
CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-070 send timeout -> tx_fault counter +1");
CHECK(s_mx_init_calls == 1U && con_nvic_clears == 1U
&& con_rcc_disables == 1U && con_rcc_enables == 1U,
"TC-CONLOG-071 full self-heal sequence: clear pending -> gate off clock -> gate on clock -> re-init");
CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX,
"TC-CONLOG-072 stop at the fault: only the current 256B block is lost, the rest waits in the ring (yields to the watchdog)");
CHECK(con_uart_tx_n == 0U,
"TC-CONLOG-073 while the channel is unhealed even that WARN cannot get out (raw send is bounded too, no retry)");
CHECK(dbg_console_tx_fault_log_take() == 1U && dbg_console_tx_fault_log_take() == 0U,
"TC-CONLOG-074 the trace flag is read-then-clear: the writer uses it to append run.log outside drain (breaks the call graph cycle)");
printf("-- TC-CONLOG-075..077 soft reset healed: the WARN goes out exactly once --\n");
reset_all();
con_tick_now = 7000U;
attach_writer();
(void)dbg_console_write_line("X", 1U);
con_uart_txe_ready_left = 0; /* 一装载就卡死 */
con_dwt_step = SystemCoreClock / 4U;
s_mx_repair = 1; /* 重初始化把通道治好 */
writer_drain();
CHECK(tx_contains("[WARN] CONSOLE tx fault"), "TC-CONLOG-075 the first fault WARN line reaches the wire");
CHECK(used_now() == 0U && s_con_cool_until == 7000U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-076 the faulted block is dropped (already out of the ring, not replayable) + cooldown deadline is the tick then");
con_uart_txe_ready_left = -1;
con_dwt_step = 0U;
writer_drain(); /* tick 未推进 -> 仍在冷却 */
CHECK(s_con_cool_until == 7000U && s_mx_init_calls == 1U,
"TC-CONLOG-077 during cooldown drain yields at once: no retry, no second self-heal");
}
static void tc_tx_cool(void)
{
printf("-- TC-CONLOG-078..084 + 107..108 cooldown both ends / tick==0 / wrap --\n");
reset_all();
fault_setup(7000U, 1); /* 治好的那次故障 */
CHECK(s_con_cool_until == 7000U && used_now() == 3839U,
"TC-CONLOG-078 3839B remain in the ring after the fault (this block was dropped)");
con_dwt_step = 0U;
con_uart_txe_stuck = 0;
con_tick_now = 7299U; /* 差 1ms */
writer_drain();
CHECK(s_con_cool_until == 7000U && used_now() == 3839U && s_mx_init_calls == 1U,
"TC-CONLOG-079 1ms short of the deadline: the ring does not move and no repeated self-heal (cooldown is the pacing guard)");
con_tick_now = 7300U; /* 到点 */
writer_drain();
CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-080 at the deadline the flag clears, this round drains, and no new fault appears"
" (cool_until only stores a deadline and is no longer used as a clear-on-zero sentinel)");
/* 修复后的正向断言:冷却与否看独立标志,不再拿 tick 值当哨兵 */
reset_all();
fault_setup(0U, 0); /* 故障恰好落在 HAL_GetTick()==0 那一拍 */
CHECK(s_con_cooling == 1U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-081 a fault in the tick==0 round still enters cooldown (own flag, no zero sentinel collision)");
writer_drain(); /* tick 仍是 0 */
CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U
&& used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX,
"TC-CONLOG-082 another round on the same tick is blocked by cooldown: no second wasted block timeout");
con_tick_now = DBG_CON_FAULT_COOL_MS - 1U;
writer_drain();
CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-083 299ms is still inside the cooldown");
con_uart_txe_stuck = 0;
con_tick_now = DBG_CON_FAULT_COOL_MS; /* 到点(且回绕起点正好是 0) */
writer_drain();
CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-084 after the deadline the cooldown clears and passes through: no second fault anywhere");
reset_all();
attach_writer();
fill_ring(0U);
s_con_cooling = 1U; /* 在不在冷却只由这个标志说 */
s_con_cool_until = 0xFFFFFFFFU; /* 截止时刻记在回绕点前 */
con_tick_now = 100U; /* 现在刚回绕到 100 */
writer_drain();
CHECK(s_con_cooling == 1U && used_now() == 4095U,
"TC-CONLOG-107 wrap-around deadline still cools (unsigned delta 101 < 300)");
con_tick_now = 401U; /* 无符号差值 402 >= 300 */
writer_drain();
CHECK(s_con_cooling == 0U && used_now() == 0U,
"TC-CONLOG-108 past the wrapped deadline the flag clears and the ring empties");
}
static void tc_tx_half(void)
{
printf("-- TC-CONLOG-085..088 real consequences of the timeout criterion: half block / already sent yet failed --\n");
reset_all();
attach_writer();
(void)dbg_console_write_line("ABCDEFGH", 8U);
con_uart_tc_stuck = 1; /* 字节都进了 DR,只有 TC 不置位 */
con_dwt_step = SystemCoreClock / 4U;
writer_drain();
CHECK(tx_is(0U, "ABCDEFGH"), "TC-CONLOG-085 TC stuck: all 8 bytes were handed to the shifter");
CHECK(dbg_console_tx_fault() == 1U && con_uart_block_n == 0U,
"TC-CONLOG-086 already on the wire but the whole block still counts as failed (docs section 13-O2: drop and fault books were not separated)");
reset_all();
attach_writer();
(void)dbg_console_write_line("ABCDEFGH", 8U);
con_uart_txe_ready_left = 3; /* 只挤得出 3 个字节 */
con_dwt_step = SystemCoreClock / 4U;
writer_drain();
CHECK(tx_is(0U, "ABC"), "TC-CONLOG-087 TXE stuck mid-block -> only the 3 bytes already written to DR reach the wire");
CHECK(dbg_console_tx_fault() == 1U && used_now() == 0U,
"TC-CONLOG-088 a half block takes the same self-heal branch and leaves the ring (trailing 5 bytes die with the block)");
}
/* ============ TC-CONLOG-090..099 DWT 使能 / 指令计数上界 ============ */
static void tc_dwt(void)
{
printf("-- TC-CONLOG-090..093 lazy DWT enable and skip-when-ready --\n");
reset_all();
(void)dbg_console_write_line("ok\r\n", 4U); /* 走一次直发 -> 顺带使能 DWT */
CHECK(s_con_dwt_up == 1U, "TC-CONLOG-090 con_dwt_ensure sets s_con_dwt_up after a successful enable");
CHECK((g_con_core_debug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0U,
"TC-CONLOG-091 DEMCR.TRCENA really gets written (bit number matches core_cm4.h)");
CHECK((g_con_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U,
"TC-CONLOG-092 DWT.CTRL.CYCCNTENA really gets written");
CHECK(tx_is(0U, "ok\r\n") && con_dwt_reads == 4U,
"TC-CONLOG-093 with the channel ready the send loop reads no further CYCCNT (3 from ensure + 1 for t0)");
printf("-- TC-CONLOG-094..097 DWT enable failure -> con_tx_counted instruction ceiling --\n");
reset_all();
con_dwt_ctrl_sticky = 0; /* CYCCNTENA 写不进去 */
con_uart_txe_stuck = 1;
dbg_console_write_raw("Q", 1U);
CHECK(s_con_dwt_up == 0U, "TC-CONLOG-094 DWT enable failure -> the counted-guard branch is taken");
CHECK(con_uart_txe_reads == CON_TX_GUARD_MAX,
"TC-CONLOG-095 each byte spins at most CON_TX_GUARD_MAX(20000) times then gives up (bounded, not a dead wait)");
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-096 if the block is not out within the counted ceiling the whole block is abandoned");
CHECK(con_dwt_reads == 3U,
"TC-CONLOG-097 the counted branch never reads CYCCNT (independent of DWT, hence of any time source)");
printf("-- TC-CONLOG-098..099 the timeout criterion does not use HAL_GetTick --\n");
reset_all();
con_uart_txe_stuck = 1; /* tick 冻结在 0,模拟 TIM7 停摆 */
con_dwt_step = SystemCoreClock / 10U; /* 一次判据正好 = 100ms 上界 */
dbg_console_write_raw("Q", 1U);
CHECK(con_tick_reads == 0U && con_uart_tx_n == 0U,
"TC-CONLOG-098 it exits even when tick never advances: one raw send reads HAL_GetTick zero times");
reset_all();
con_uart_txe_stuck = 1;
con_dwt_step = SystemCoreClock / 4U;
dbg_console_write_raw("Z", 1U);
CHECK(con_uart_txe_reads == 1U,
"TC-CONLOG-099 the DWT branch declares timeout on the first spin (the ceiling really works, never burns 20000)");
}
/* ============ TC-CONLOG-100..106 huart1.Instance == NULL 短路 + 未初始化分诊 ============ */
static void tc_null_uart(void)
{
printf("-- TC-CONLOG-100..106 USART1 not initialised: yield, never reinit, keep proof --\n");
reset_all();
huart1.Instance = 0;
dbg_console_write_raw("boom", 5U);
CHECK(con_uart_tx_n == 0U && con_uart_txe_reads == 0U,
"TC-CONLOG-100 with Instance==NULL not one register is touched (no dereference of address 0)");
CHECK(con_dwt_reads == 3U,
"TC-CONLOG-101 DWT is still lazily enabled before the short circuit (ensure stays ahead of the NULL check)");
reset_all();
huart1.Instance = 0;
CHECK(dbg_console_write_line("early", 5U) == 5,
"TC-CONLOG-102 the unattached path (pre-kernel) survives a missing UART and still returns the line length");
/* 修复后的正向断言:未初始化 != 僵尸通道,绝不重配外设 */
reset_all();
attach_writer();
(void)dbg_console_write_line("abc", 3U);
huart1.Instance = 0; /* 写者起来了但串口还没初始化 */
s_mx_repair = 1;
writer_drain();
CHECK(s_mx_init_calls == 0U && con_rcc_disables == 0U && con_nvic_clears == 0U,
"TC-CONLOG-103 Instance==NULL never triggers a peripheral soft reset (no RCC/GPIO reconfig before HAL is ready)");
CHECK(dbg_console_tx_fault() == 0U && s_con_cooling == 0U,
"TC-CONLOG-104 not treated as a zombie channel: tx_fault and cooldown stay untouched");
CHECK(dbg_console_dropped() == 1U && used_now() == 0U,
"TC-CONLOG-105 the block that left the ring but never hit the wire counts as dropped");
huart1.Instance = &g_con_uart; /* 串口随后初始化好 */
con_uart_tx_n = 0U;
writer_drain();
CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines") && dbg_console_dropped() == 0U,
"TC-CONLOG-106 drop evidence survives the outage and is flushed once the UART is back");
}
/* ============ TC-CONLOG-110..115 fputc 重定向 ============ */
static void tc_fputc(void)
{
printf("-- TC-CONLOG-110..115 fputc retarget (legacy printf path) --\n");
reset_all();
CHECK(con_fputc_under_test('Q', stdout) == 'Q', "TC-CONLOG-110 fputc returns the character unchanged (C library convention)");
CHECK(tx_is(0U, "Q") && used_now() == 0U,
"TC-CONLOG-111 writer not attached: printf bytes go out directly (same as the old behaviour)");
reset_all();
attach_writer();
(void)con_fputc_under_test('R', stdout);
CHECK(con_uart_tx_n == 0U && used_now() == 1U && con_ring_store[0] == (uint8_t)'R',
"TC-CONLOG-112 after attach fputc also funnels into the single-writer path (byte by byte into the ring)");
writer_drain();
CHECK(tx_is(0U, "R"), "TC-CONLOG-113 the writer sends those printf bytes");
reset_all();
attach_writer();
con_ipsr = 12U; /* 假异常号:任意非 0 */
CHECK(dbg_console_write_line("ISR", 3U) == 3, "TC-CONLOG-114 writing a whole line from interrupt context returns the line length");
CHECK(tx_is(0U, "ISR") && used_now() == 0U && con_delay_calls == 0U,
"TC-CONLOG-115 interrupt context always sends raw: no ring entry, no waiting (else it sleeps inside the ISR)");
reset_all();
attach_writer();
fill_ring(0U); /* 环全满 */
con_ipsr = 5U;
con_dwt_step = SystemCoreClock / 4U;
CHECK(dbg_console_write_line("0123456789", 10U) == 10,
"TC-CONLOG-116 ring completely full + interrupt context: still sends directly (an ISR never enters the drop wait)");
CHECK(dbg_console_dropped() == 0U && con_delay_calls == 0U,
"TC-CONLOG-117 this path loses no line and records no drop (K1: pre-kernel and ISR depend on no kernel object)");
}
/* ============ TC-CONLOG-120..122 drain 250ms 预算 ============ */
static void tc_budget(void)
{
printf("-- TC-CONLOG-120..122 single drain budget 250ms (yields to the watchdog poll) --\n");
reset_all();
attach_writer();
fill_ring(0U); /* 4095B = 16 块 */
con_tick_step = 130U; /* 每次读 tick 走 130ms */
writer_drain();
CHECK(con_uart_block_n == 2U, "TC-CONLOG-120 one round sends only 2 blocks then yields on budget (not because it finished)");
CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - (2U * DBG_CON_CHUNK_MAX),
"TC-CONLOG-121 when it yields, 3583B remain in the ring (next round continues, nothing lost)");
con_tick_step = 0U;
writer_drain(); /* 预算内发完余下 3583B */
CHECK(used_now() == 0U && con_uart_tx_n == (DBG_CON_RING_SIZE - 1U),
"TC-CONLOG-122 the next round sends the remaining 3583B too: yielding on budget loses no bytes (4095 total)");
}
/* ============ TC-CONLOG-130..135 计数支路的成功路径 / TC 抖动 / 直调 NULL ============ */
static void tc_counted_paths(void)
{
printf("-- TC-CONLOG-130..132 DWT unavailable -> con_tx_counted can also finish a block (not only time out) --\n");
reset_all();
con_dwt_ctrl_sticky = 0; /* CYCCNTENA 落不住 */
con_tick_now = 9000U; /* tick 冻结:证明与本支路无关 */
dbg_console_write_raw("AB", 2U);
CHECK(s_con_dwt_up == 0U && tx_is(0U, "AB"),
"TC-CONLOG-130 the counted branch sends the whole block and returns success (fallback path did not regress)");
CHECK(con_uart_tc_reads == 1U && con_dwt_reads == 3U && con_tick_reads == 0U,
"TC-CONLOG-131 the counted branch waits no tick and reads no CYCCNT (zero external time source)");
reset_all();
con_dwt_ctrl_sticky = 0;
con_uart_tc_stuck = 1; /* 字节能进 DR,TC 永不置位 */
dbg_console_write_raw("CD", 2U);
CHECK(tx_is(0U, "CD") && con_uart_tc_reads == CON_TX_GUARD_MAX,
"TC-CONLOG-132 with TC stuck the counted branch also gives up after at most CON_TX_GUARD_MAX spins");
printf("-- TC-CONLOG-133..135 DWT branch TC jitter / con_append direct-call argument checks --\n");
reset_all();
con_dwt_step = 1U; /* 时间走得极慢:不会误判超时 */
con_uart_tc_after = 2; /* 前两轮 TC 不就绪 */
dbg_console_write_raw("EF", 2U);
CHECK(tx_is(0U, "EF") && con_uart_tc_reads == 3U,
"TC-CONLOG-133 TC jitter (below the ceiling) still ends as a successful block: the TC loop exits normally");
reset_all();
CHECK(con_append(0, 3U) == -1, "TC-CONLOG-134 con_append(NULL) must block it itself (not only relying on the caller)");
CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) is rejected the same way");
}
/* ============ TC-CONLOG-140..150 三本账分开(docs §13-O2) ============ */
static void tc_three_ledgers(void)
{
uint32_t f0;
uint32_t v0;
printf("-- TC-CONLOG-140..150 dropped / tx_fault / tx_timeout: three separate books --\n");
/* TC 卡死:8 个字节都交给了发送器 -> 只涨超时账 + 自愈账,丢弃账不动 */
reset_all();
attach_writer();
(void)dbg_console_write_line("ABCDEFGH", 8U);
con_uart_tc_stuck = 1;
con_dwt_step = SystemCoreClock / 4U;
writer_drain();
CHECK(dbg_console_tx_timeout() == 1U,
"TC-CONLOG-140 TC timeout is booked on the new timeout ledger");
CHECK(dbg_console_dropped() == 1U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-141 timed-out block is no longer silently swallowed: dropped and self-heal each move by one");
/* 缓冲满丢行:只算丢弃,与 UART 无关 */
reset_all();
attach_writer();
fill_ring(10U);
con_dwt_step = SystemCoreClock / 4U;
(void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U);
CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U
&& dbg_console_tx_fault() == 0U,
"TC-CONLOG-142 buffer-full drop touches only the dropped ledger (no UART involved)");
/* 口径:只有写者排空看到的块才进超时账;裸发路径(内核前 / ISR / 告警行)
* 没有"块"的概念,它的超时不混进来(否则运维读数失去意义)。
* 两条硬上界(DWT 与指令计数)都算,这一点用同一断言在两处覆盖。 */
reset_all();
con_dwt_ctrl_sticky = 0;
con_uart_txe_stuck = 1;
dbg_console_write_raw("Q", 1U);
CHECK(dbg_console_tx_timeout() == 0U,
"TC-CONLOG-143 raw-path timeout stays out of the block-timeout ledger (documented scope)");
reset_all();
attach_writer();
con_dwt_ctrl_sticky = 0; /* DWT 用不了 -> 计数上界支路 */
con_uart_txe_stuck = 1;
(void)dbg_console_write_line("Q", 1U);
writer_drain();
CHECK(dbg_console_tx_timeout() == 1U,
"TC-CONLOG-150 the counted-guard upper bound is booked on the same ledger as the DWT one");
/* 通道不可用:既不是超时也不是自愈,只算丢弃(分诊与分账的交点) */
reset_all();
attach_writer();
(void)dbg_console_write_line("xyz", 3U);
huart1.Instance = 0;
writer_drain();
CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U
&& dbg_console_tx_fault() == 0U,
"TC-CONLOG-144 dead channel (not initialised): only the dropped ledger moves");
/* 145:两轮都坏 -> 超时账与自愈账各自累计(互不吞并) */
reset_all();
attach_writer();
fill_ring(0U);
s_con_tx_fault = 1U; /* 预置:让首轮那条 WARN 留痕不发,
* 否则通道没治好时它自己也吃一次超时 */
con_uart_txe_stuck = 1;
con_dwt_step = SystemCoreClock / 4U;
s_mx_repair = 0;
f0 = dbg_console_tx_fault();
v0 = dbg_console_tx_timeout();
writer_drain();
con_tick_now = 400U; /* 出冷却,再来一轮 */
writer_drain();
CHECK((dbg_console_tx_timeout() - v0) == 2U,
"TC-CONLOG-145 timeout ledger grows once per timed-out block across rounds");
CHECK((dbg_console_tx_fault() - f0) == 2U,
"TC-CONLOG-146 self-heal ledger grows on its own, independent of the timeout ledger");
CHECK(dbg_console_tx_timeout() == dbg_console_tx_timeout(),
"TC-CONLOG-147 the timeout getter is a pure read (no clear-on-read semantics)");
/* 148/149:通道不可用期间 [ERR] 告警不能蒸发(丢的证据必须留得住) */
reset_all();
attach_writer();
fill_ring(10U);
con_dwt_step = SystemCoreClock / 4U;
(void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); /* 缓冲满丢 1 行 */
con_dwt_step = 0U;
huart1.Instance = 0; /* 告警那一发也发不出去 */
writer_drain();
CHECK(dbg_console_dropped() >= 1U,
"TC-CONLOG-148 drop count survives while the channel is unavailable (evidence no longer washed away)");
huart1.Instance = &g_con_uart;
con_uart_tx_n = 0U;
writer_drain();
CHECK(tx_contains("[ERR] CONSOLE dropped") && dbg_console_dropped() == 0U,
"TC-CONLOG-149 once the UART is back the pending drop count is reported and then cleared");
}
/* ============ TC-CONLOG-160..165 check-then-act 的结构性钉法(环空位竞态) ========= */
static char s_inject_buf[64];
static uint32_t s_inject_left;
static uint32_t s_inject_hits;
/* 第二个生产者:被塞进"第一个生产者的临界区"里执行。旧写法里判据在区外,
* 第一个生产者复核时看不到这次写入 -> head 合计越过容量;新写法复核在区内,
* 一定看得到 -> 第一个生产者改为等待/丢弃,绝不覆盖未发出的旧行。 */
static void crit_second_producer(void)
{
if (s_inject_left == 0U) {
return;
}
s_inject_left--;
s_inject_hits++;
(void)con_append(s_inject_buf, 60U);
}
static void tc_check_then_act(void)
{
char first[64];
uint32_t used_after;
int rc;
printf("-- TC-CONLOG-160..165 re-check must stay inside the critical section --\n");
reset_all();
attach_writer();
fill_ring(100U); /* 只剩 100B 可用:两个 60B 塞不下 */
memset(first, 'X', sizeof(first));
memset(s_inject_buf, 'Y', sizeof(s_inject_buf));
s_inject_left = 1U;
s_inject_hits = 0U;
con_ring_probe_hits = 0U;
con_ring_zero_depth_hits = 0U;
con_ring_probe_on = 1; /* 探针只在本次调用期间计数 */
con_pm_hook = crit_second_producer; /* 窗口换 PRIMASK 之后注入点跟着搬 */
con_dwt_step = SystemCoreClock / 4U; /* 复核不过就直接判上界,不空等 */
rc = dbg_console_write_line(first, 60U);
con_pm_hook = 0;
con_ring_probe_on = 0;
con_dwt_step = 0U;
used_after = used_now();
CHECK(s_inject_hits == 1U && con_pm_max_depth == 2U,
"TC-CONLOG-160 the second producer ran with the region already held (depth two nesting)");
CHECK(rc == -1 && dbg_console_dropped() == 1U,
"TC-CONLOG-161 the first producer re-checked inside the region, found space gone and dropped instead of overwriting");
CHECK(used_after == (DBG_CON_RING_SIZE - 1U) - 40U,
"TC-CONLOG-162 ring usage stayed within capacity: 3995B + one 60B line, not 4115B wrapped past the end");
writer_drain(); /* 末尾还会跟一条 [ERR] dropped */
CHECK(con_uart_tx_n >= used_after && tx_contains("[ERR] CONSOLE dropped 1"),
"TC-CONLOG-163 everything that survived the race is delivered, plus the drop warning");
CHECK(tx_prefix_all('Z', (DBG_CON_RING_SIZE - 1U) - 100U)
&& tx_at_is('Y', (DBG_CON_RING_SIZE - 1U) - 100U, 60U),
"TC-CONLOG-164 old 3995 bytes intact then the winner 60B line intact, loser absent (no silent overwrite)");
CHECK(!tx_contains("XXXXXX"),
"TC-CONLOG-165 the dropped line never made it onto the wire half-written");
/* 环访问探针(把生产的 s_con_ring 名成 (*con_ring_cell()),每次碰环都进钩子,
* 钩子本体见 stub 头注释)。167 是纯正控:证明探针真数到了逐字节访问、不是零命中
* 空转。166 的下界是正控、上界则是判据——旧写法里"复核"发生在区外,败者那一份 60B
* 也会真写进环(合计约 120 次),所以本条在退回旧写法时同样会红。 */
/* 期望次数 = 胜者那一行的逐字节写入(60B);败者复核失败后一个字节都没写,
* 这正是"不覆盖未发出旧行"的直接体现,所以命中数恰好只有 60 多几次。 */
CHECK(con_ring_probe_hits >= 60U && con_ring_probe_hits < 120U,
"TC-CONLOG-166 ring probe counted exactly the winner line byte writes (loser wrote nothing)");
CHECK(con_ring_zero_depth_hits == 0U,
"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 to TC-CONLOG-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) && (con_pm_enter == 0U),
"TC-CONLOG-176 before the scheduler starts, the single execution stream sends directly, taking no token and opening neither kind of critical window");
/* 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)
{
tc_params();
tc_unattached();
tc_ring();
tc_writer_self();
tc_full_drop();
tc_tx_fault();
tc_tx_cool();
tc_tx_half();
tc_dwt();
tc_null_uart();
tc_fputc();
tc_budget();
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);
fflush(stdout);
return (s_failed == 0) ? 0 : 1;
}