Files
STM32F4-Base/test/host/console_log/console_log_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 21:00:17 +08:00

1198 lines
55 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 上下文的无锁直发、缓冲满有界等待与
* 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、
* huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算。
*
* 为什么要主机侧单测(而不是只用台架):
* 本模块的风险全在"异常时刻"——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(round 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 全绿;每一步的失败集合都是上一步的超集,说明四组断言各自
* 咬住自己的那条语义,没有互相顶包。
* 也就是说:这四组断言不是"写在假件规则上"的自证,而是真能咬住生产语义的。
*
* 故意没测(逐行说明见交付报告):
* - 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_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;
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--;
}
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;
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;
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)");
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)");
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_crit_enter == 1U && con_crit_exit == 1U && con_crit_max_depth == 1U,
"TC-CONLOG-026 one line takes exactly one critical-section level (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 (round 8b motivation) --\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_crit_hook = crit_second_producer;
con_dwt_step = SystemCoreClock / 4U; /* 复核不过就直接判上界,不空等 */
rc = dbg_console_write_line(first, 60U);
con_crit_hook = 0;
con_ring_probe_on = 0;
con_dwt_step = 0U;
used_after = used_now();
CHECK(s_inject_hits == 1U && con_crit_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");
}
/* ===================== 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();
printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
fflush(stdout);
return (s_failed == 0) ? 0 : 1;
}