Files
STM32F4-Base/test/host/console_log/console_log_test.c
T
wandering1 794906cdc8 test(host): P2 覆盖率批次,5 套主机单测与共享假 FatFs
P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界):
  net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片)
  sys_log_mcu 75 条 / 310-313=99.0%、12-12
  console_log 104 条 / 188-189=99.5%、15-15
  cfg_file_store_mcu 34 条 / 97-97=100%、10-10
  soe_store 26 条 / 78-79=98.7%、4-4
合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。

账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29,
恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母
4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件
全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对:
下降 0 个文件。

基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只
定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED
可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开
一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正
两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04),
另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。
存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、
nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。

编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27,
先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK;
id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。

文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、
12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。
本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。

10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、
net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示
全部修复,另开一轮(含测试字面量统一纯 ASCII)。

gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers
Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱);
mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0
(纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。
2026-10-24 17:54:02 +08:00

923 lines
40 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-*,属主登记处 = 本文件;实占 104 号 / 104 条断言):
* 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..104 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读
* HAL_GetTick(round 8b 动机)、huart1.Instance == NULL 短路两路
* 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR
* 仍不等待)
* 120..122 drain 单次 250ms 预算与下轮续排(让位喂狗但不丢字节)
* 130..135 计数支路也能把整块发完(不只会超时)+ 其 TC 计数上界 + DWT 支路 TC 抖动
* 后正常收尾 + con_append 直调时的自有参数校验
*
* 实测覆盖率(scripts/check_test_coverage.py --module console_log,/Od + /Zi 口径):
* App/log/console_log.c **188/189 行 = 99.5%,函数 15/15 = 100%**。
*
* 故意没测(逐行说明见交付报告):
* - console_log.c:352 唯一未覆盖行: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 改名纳入,只验函数体不验链接钩子。
*
* 通过:全部 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;
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_push(void)
{
con_crit_enter++;
s_crit_depth++;
if (s_crit_depth > con_crit_max_depth) {
con_crit_max_depth = 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_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_enter = 0U;
con_crit_exit = 0U;
con_crit_max_depth = 0U;
s_crit_depth = 0U;
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;
}
/* 在已上线字节里找一段(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;
dbg_console_drain();
con_task_self = (TaskHandle_t)&s_task_other;
}
/* 后台排空(不改当前任务句柄):模拟"生产者等一轮,写者腾出了空间" */
static void bg_drain_hook(void)
{
dbg_console_drain();
}
/* 把环填到只剩 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 参数校验与截断 --\n");
reset_all();
printf("(probe) 这行走宿主 CRT\n");
fflush(stdout);
CHECK(con_uart_tx_n == 0U && used_now() == 0U,
"TC-CONLOG-000 宿主 printf 不经过本模块 fputc(否则测试输出会被吞进环)");
reset_all();
con_uart_txe_stuck = 1; /* 通道卡死:任何字节都不该上线 */
CHECK(dbg_console_write_line(0, 5U) == -1, "TC-CONLOG-001 write_line(NULL) 返回 -1");
CHECK(dbg_console_write_line("abc", 0U) == -1, "TC-CONLOG-002 write_line(len=0) 返回 -1");
dbg_console_write_raw(0, 5U);
dbg_console_write_raw("abc", 0U);
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-003 两路 NULL/0 都不碰 UART");
CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-004 参数非法不计入丢弃");
reset_all();
memset(buf, 'A', sizeof(buf));
CHECK(dbg_console_write_line(buf, sizeof(buf)) == (int)DBG_CON_LINE_MAX,
"TC-CONLOG-005 超过 DBG_CON_LINE_MAX 的行按 192 截断(返回值即交付字节数)");
CHECK(con_uart_tx_n == DBG_CON_LINE_MAX, "TC-CONLOG-006 截断后恰好上线 192 字节");
}
/* ============ TC-CONLOG-010..018 写者未注册 = 无锁直发 ============ */
static void tc_unattached(void)
{
char big[DBG_CON_CHUNK_MAX + 64U];
printf("-- TC-CONLOG-010..018 写者未注册:无锁直发 --\n");
reset_all();
CHECK(dbg_console_writer_ready() == 0U, "TC-CONLOG-010 attach 之前 writer_ready() == 0");
CHECK(dbg_console_write_line("hello", 5U) == 5, "TC-CONLOG-011 未注册时整行直发,返回行长");
CHECK(tx_is(0U, "hello"), "TC-CONLOG-012 直发内容原样上线(没经过环形缓冲)");
CHECK(used_now() == 0U, "TC-CONLOG-013 直发不入环(head == tail)");
CHECK(con_evt_sets == 0U, "TC-CONLOG-014 直发不置 SYS_EVT_CONSOLE_TX(还没写者可唤醒)");
CHECK(con_crit_enter == 0U,
"TC-CONLOG-015 直发零临界区 = 2026-10-10 逐字节交错的根因(不是丢整行)");
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 有界:只发 256 字节");
reset_all();
con_uart_txe_after = 3; /* 前 3 次不就绪(TXE 抖动,未超上界) */
CHECK(dbg_console_write_line("CD", 2U) == 2, "TC-CONLOG-017 TXE 抖动后仍发全");
CHECK(tx_is(0U, "CD") && con_delay_calls == 0U,
"TC-CONLOG-018 直发既不等 tick 也不入环(ISR/崩溃钩子可用的前提)");
}
/* ============ 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 注册后:生产者入环、写者发送 --\n");
reset_all();
attach_writer();
CHECK(dbg_console_writer_ready() == 1U, "TC-CONLOG-020 attach 之后 writer_ready() == 1");
CHECK(s_con_writer_task == (TaskHandle_t)&s_task_writer,
"TC-CONLOG-021 attach 记下的正是调用方的任务句柄(自等保护的依据)");
CHECK(dbg_console_write_line("netTask up\r\n", 12U) == 12, "TC-CONLOG-022 入环成功返回行长");
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-023 生产者绝不碰 UART");
CHECK(used_now() == 12U, "TC-CONLOG-024 整行一次入环(used = 12)");
CHECK(con_evt_sets == 1U && con_evt_flags == SYS_EVT_CONSOLE_TX,
"TC-CONLOG-025 入环后置真实事件位 SYS_EVT_CONSOLE_TX(bit6)唤醒写者");
CHECK(con_crit_enter == 1U && con_crit_exit == 1U && con_crit_max_depth == 1U,
"TC-CONLOG-026 整行入环恰好一层临界区(行级原子的实现依据)");
writer_drain();
CHECK(tx_is(0U, "netTask up\r\n"), "TC-CONLOG-027 写者排空:上线字节与入环内容逐字节相同");
printf("-- TC-CONLOG-028..032 分块 256B 与多生产者不交错 --\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 按 DBG_CON_CHUNK_MAX 切成 4 块");
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 块长 256/256/256/192(单次发送上限真被执行)");
CHECK(con_uart_tx_n == 960U && con_uart_txe_reads == 960U,
"TC-CONLOG-030 每字节恰好装载一次 DR(就绪即写,没有重复发)");
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 三个生产者各写一行 -> 上线仍是三行连续原文(G1 行级原子)");
CHECK(strstr((const char *)&con_uart_tx[5], "[B] 22222") != 0,
"TC-CONLOG-032 行边界没被打断(没有任何一行被插进另一行中间)");
printf("-- TC-CONLOG-033..037 跨环尾:两段拷贝 + 按连续段分块 --\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 跨环尾入环后 head 回绕到 10(4090+16-4096)");
CHECK(memcmp(&s_con_ring[4090], "012345", 6) == 0
&& s_con_ring[0] == (uint8_t)'6' && s_con_ring[9] == (uint8_t)'F'
&& s_con_ring[10] == 0U,
"TC-CONLOG-034 回绕两段拷贝位置正确(前 6B 填到环尾、余 10B 从 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 按连续段分块:6 + 10(绝不把环中间当连续段读越界)");
CHECK(tx_is(0U, "0123456789ABCDEF"), "TC-CONLOG-036 回绕行的上线内容与顺序都对");
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 直调 con_append 超长仍成功");
CHECK(used_now() == (DBG_CON_RING_SIZE / 2U),
"TC-CONLOG-038 入环长度钳在 RING/2=2048(保证任何一行都恒可入环)");
}
/* ============ 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 写者自记录先排空 / drain 重入保护 --\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 写者自记录不丢行");
CHECK(con_delay_calls == 0U,
"TC-CONLOG-041 写者自记录先排空再入环:绝不进等待(否则就是等自己 -> 死锁)");
CHECK(con_uart_tx_n == 4032U && used_now() == 1U && dbg_console_dropped() == 0U,
"TC-CONLOG-042 老行全部上线、新行入环,零丢弃");
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 重入保护:嵌套调用直接让位(写者自记录不触发递归 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 写者未注册时 drain 不做任何事");
}
/* ============ TC-CONLOG-050..062 缓冲满 / 丢弃 / 可观测 ============ */
static void tc_full_drop(void)
{
printf("-- TC-CONLOG-050..053 缓冲满:DWT 硬上界丢弃且不覆盖旧行 --\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 环满 + DWT 上界到 -> 丢这一行(返回 -1)");
CHECK(dbg_console_dropped() == 1U, "TC-CONLOG-051 丢弃计数 +1");
CHECK(con_delay_calls == 0U,
"TC-CONLOG-052 有 DWT 时用周期判据,不必先烧 200 个 tick(K4)");
CHECK(s_con_head == 4085U && used_now() == 4085U && con_uart_tx_n == 0U,
"TC-CONLOG-053 丢弃不推进 head、也不生产者自己发(没半写、更没覆盖未发出的旧行)");
printf("-- TC-CONLOG-054..056 等待中出现空间 -> 入环成功 --\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 等一轮后腾出空间 -> 整行入环成功");
CHECK(con_delay_calls == 1U && dbg_console_dropped() == 0U,
"TC-CONLOG-055 只等了一轮,且不算丢弃");
con_delay_hook = 0;
CHECK(con_uart_tx_n == 4085U, "TC-CONLOG-056 腾地方的正是写者(老行已上线)");
printf("-- TC-CONLOG-057..059 无 DWT 支路:等待上界 = DBG_CON_WAIT_MS 个 tick --\n");
reset_all();
con_dwt_ctrl_sticky = 0; /* 调试单元被关 -> DWT 使能失败 */
attach_writer();
fill_ring(10U);
CHECK(s_con_dwt_up == 0U, "TC-CONLOG-057 DWT 使能失败时 s_con_dwt_up 保持 0");
CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1,
"TC-CONLOG-058 无 DWT 时仍会丢(判据退回 tick 计数,不会无限等)");
CHECK(con_delay_calls == DBG_CON_WAIT_MS && dbg_console_dropped() == 1U,
"TC-CONLOG-059 无 DWT 的等待上界恰为 DBG_CON_WAIT_MS(200) 次 vTaskDelay");
printf("-- TC-CONLOG-060..062 丢弃可观测:[ERR] 行直发不经环 --\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 排空末尾刷出 [ERR] CONSOLE dropped 1 lines(丢行永不静默,@test 判 FAIL)");
CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-061 刷完即清零(下一轮不重复告警)");
CHECK(used_now() == 0U,
"TC-CONLOG-062 告警行走裸发:不入环 -> 不会 con_append->drain 递归");
}
/* ============ 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 软复位没治好:故障即止 + 留痕只能靠标志 --\n");
reset_all();
fault_setup(5000U, 0);
CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-070 发送超时 -> tx_fault 计数 +1");
CHECK(s_mx_init_calls == 1U && con_nvic_clears == 1U
&& con_rcc_disables == 1U && con_rcc_enables == 1U,
"TC-CONLOG-071 自愈完整一遍:清 pending -> 断时钟 -> 供时钟 -> 重初始化");
CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX,
"TC-CONLOG-072 故障即止:只丢当前 256B 块,其余留环里等下轮(让位喂狗)");
CHECK(con_uart_tx_n == 0U,
"TC-CONLOG-073 通道没治好时连那条 WARN 也上不了线(裸发同样有界、不重试)");
CHECK(dbg_console_tx_fault_log_take() == 1U && dbg_console_tx_fault_log_take() == 0U,
"TC-CONLOG-074 留痕标志读后清:写者据此在 drain 之外补记 run.log(断调用图成环)");
printf("-- TC-CONLOG-075..077 软复位治好了:WARN 上线且只一次 --\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 首次故障的 WARN 行成功上线");
CHECK(used_now() == 0U && s_con_cool_until == 7000U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-076 故障块丢弃(行已出环不可重放)+ 冷却截止记为当时 tick");
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 冷却期内 drain 直接让位:不重试、不自愈第二次");
}
static void tc_tx_cool(void)
{
printf("-- TC-CONLOG-078..084 300ms 冷却两端 + 回绕 + tick==0 现状 --\n");
reset_all();
fault_setup(7000U, 1); /* 治好的那次故障 */
CHECK(s_con_cool_until == 7000U && used_now() == 3839U,
"TC-CONLOG-078 故障后剩 3839B 在环里(本块被丢)");
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 仍在冷却:环不动、不重复自愈(冷却即节奏保护)");
con_tick_now = 7300U; /* 到点 */
writer_drain();
CHECK(s_con_cool_until == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-080 到点即清冷却、放行本轮排空,且没有新增故障");
reset_all();
fault_setup(0U, 0);
CHECK(s_con_cool_until == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-081 现状:tick==0 时记出的 cool_until 撞上 0 哨兵 = 等于没冷却");
writer_drain();
CHECK(dbg_console_tx_fault() == 2U
&& used_now() == (DBG_CON_RING_SIZE - 1U) - 2U * DBG_CON_CHUNK_MAX,
"TC-CONLOG-082 上一条的后果:下一轮立刻再耗一次块超时(节奏保护在这一 tick 失效)");
reset_all();
attach_writer();
fill_ring(0U);
s_con_cool_until = 0xFFFFFFFFU; /* 上轮故障记在回绕点 */
con_tick_now = 100U; /* 现在刚回绕到 100 */
writer_drain();
CHECK(s_con_cool_until == 0xFFFFFFFFU && used_now() == 4095U,
"TC-CONLOG-083 跨 49.7 天回绕仍判在冷却内(无符号差值 101 < 300)");
con_tick_now = 401U; /* 无符号差值 402 >= 300 */
writer_drain();
CHECK(s_con_cool_until == 0U && used_now() == 0U,
"TC-CONLOG-084 回绕后到点 -> 清冷却并把 4095B 全排空");
}
static void tc_tx_half(void)
{
printf("-- TC-CONLOG-085..088 超时判据的真实后果:半块 / 已上线仍判失败 --\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 卡死:8 字节都已上线");
CHECK(dbg_console_tx_fault() == 1U && con_uart_block_n == 0U,
"TC-CONLOG-086 已上线仍整块判失败(docs §13-O2:丢弃/故障两本账没分开的现状)");
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 中途卡死 -> 只有已写进 DR 的 3 字节上线");
CHECK(dbg_console_tx_fault() == 1U && used_now() == 0U,
"TC-CONLOG-088 半块同样走自愈分支,整块出环(尾部 5 字节随块作废)");
}
/* ============ TC-CONLOG-090..099 DWT 使能 / 指令计数上界 ============ */
static void tc_dwt(void)
{
printf("-- TC-CONLOG-090..093 DWT 惰性使能与就绪即走 --\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 成功使能后置 s_con_dwt_up");
CHECK((g_con_core_debug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0U,
"TC-CONLOG-091 真的写了 DEMCR.TRCENA(位号与 core_cm4.h 一致)");
CHECK((g_con_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U,
"TC-CONLOG-092 真的写了 DWT.CTRL.CYCCNTENA");
CHECK(tx_is(0U, "ok\r\n") && con_dwt_reads == 4U,
"TC-CONLOG-093 通道就绪时发送循环没再读 CYCCNT(3 次 ensure + 1 次取基准 t0)");
printf("-- TC-CONLOG-094..097 DWT 使能失败 -> con_tx_counted 计数上界 --\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 使能失败 -> 走计数上界支路");
CHECK(con_uart_txe_reads == CON_TX_GUARD_MAX,
"TC-CONLOG-095 每字节最多自旋 CON_TX_GUARD_MAX(20000) 次即放弃(有界,非死等)");
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-096 计数上界内没发出去就整块放弃");
CHECK(con_dwt_reads == 3U,
"TC-CONLOG-097 计数支路一次都不读 CYCCNT(不依赖 DWT,也就不依赖时间源)");
printf("-- TC-CONLOG-098..099 超时判据不依赖 HAL_GetTick(round 8b 的动机) --\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 tick 完全不走也能退出:裸发一次都没读 HAL_GetTick");
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 DWT 支路第一次自旋就判超时(上界真的起作用,不会拖满 20000 次)");
}
/* ============ TC-CONLOG-100..104 huart1.Instance == NULL 短路 ============ */
static void tc_null_uart(void)
{
printf("-- TC-CONLOG-100..104 USART1 未初始化:安全让位,不再触发二次故障 --\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 Instance==NULL 时一次寄存器都不碰(不解引用地址 0)");
CHECK(con_dwt_reads == 3U,
"TC-CONLOG-101 仍然先惰性使能 DWT 再短路(ensure 在判空之前,顺序照旧)");
reset_all();
huart1.Instance = 0;
CHECK(dbg_console_write_line("early", 5U) == 5,
"TC-CONLOG-102 未注册路径(内核前)撞上无串口也不崩、仍返回行长");
reset_all();
attach_writer();
(void)dbg_console_write_line("abc", 3U);
huart1.Instance = 0; /* 写者起来了但串口被毁 */
s_mx_repair = 1;
writer_drain();
CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U && huart1.Instance != 0,
"TC-CONLOG-103 未初始化也被当作僵尸通道自愈:重初始化把句柄装回来");
con_dwt_step = SystemCoreClock / 4U;
writer_drain(); /* 仍在冷却 -> 让位 */
CHECK(used_now() == 0U, "TC-CONLOG-104 自愈那一轮已把出环字节丢掉,没留下脏数据");
}
/* ============ TC-CONLOG-110..115 fputc 重定向 ============ */
static void tc_fputc(void)
{
printf("-- TC-CONLOG-110..115 fputc 重定向(printf 遗留路径) --\n");
reset_all();
CHECK(con_fputc_under_test('Q', stdout) == 'Q', "TC-CONLOG-110 fputc 原样返回字符(C 库约定)");
CHECK(tx_is(0U, "Q") && used_now() == 0U,
"TC-CONLOG-111 未注册写者:printf 的字节直发(与旧行为一致)");
reset_all();
attach_writer();
(void)con_fputc_under_test('R', stdout);
CHECK(con_uart_tx_n == 0U && used_now() == 1U && s_con_ring[0] == (uint8_t)'R',
"TC-CONLOG-112 注册后 fputc 也收敛到单写者路径(逐字节入环)");
writer_drain();
CHECK(tx_is(0U, "R"), "TC-CONLOG-113 写者把 printf 字节发出");
reset_all();
attach_writer();
con_ipsr = 12U; /* 假异常号:任意非 0 */
CHECK(dbg_console_write_line("ISR", 3U) == 3, "TC-CONLOG-114 中断上下文写整行返回行长");
CHECK(tx_is(0U, "ISR") && used_now() == 0U && con_delay_calls == 0U,
"TC-CONLOG-115 中断上下文一律裸发:不入环、不等待(否则在 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 环全满 + 中断上下文:仍直发成功(ISR 绝不进丢弃等待)");
CHECK(dbg_console_dropped() == 0U && con_delay_calls == 0U,
"TC-CONLOG-117 这条路径既不丢行也不记丢弃(K1:内核前/ISR 不依赖任何内核对象)");
}
/* ============ TC-CONLOG-120..122 drain 250ms 预算 ============ */
static void tc_budget(void)
{
printf("-- TC-CONLOG-120..122 单次 drain 预算 250ms(让位喂狗轮询) --\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 一轮只发 2 块就因预算让位(不是发完)");
CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - (2U * DBG_CON_CHUNK_MAX),
"TC-CONLOG-121 让位时余下 3583B 留在环里(下轮续排,不丢)");
con_tick_step = 0U;
writer_drain(); /* 预算内发完余下 3583B */
CHECK(used_now() == 0U && con_uart_tx_n == (DBG_CON_RING_SIZE - 1U),
"TC-CONLOG-122 下轮把余下的 3583B 也发完:预算让位不丢字节(累计 4095)");
}
/* ============ TC-CONLOG-130..135 计数支路的成功路径 / TC 抖动 / 直调 NULL ============ */
static void tc_counted_paths(void)
{
printf("-- TC-CONLOG-130..132 DWT 不可用 -> con_tx_counted 也能发全(不只会超时) --\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 计数支路把整块发完并返回成功(回退路径没退化)");
CHECK(con_uart_tc_reads == 1U && con_dwt_reads == 3U && con_tick_reads == 0U,
"TC-CONLOG-131 计数支路既不等 tick 也不再读 CYCCNT(零外部时间源依赖)");
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 TC 卡死时计数支路同样最多自旋 CON_TX_GUARD_MAX 次即放弃");
printf("-- TC-CONLOG-133..135 DWT 支路 TC 抖动 / con_append 直调参数校验 --\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 抖动(未超上界)后仍算整块成功:TC 循环能正常退出");
reset_all();
CHECK(con_append(0, 3U) == -1, "TC-CONLOG-134 con_append(NULL) 自己也要挡住(不只靠上层)");
CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) 同样拒绝");
}
/* ===================== main ===================== */
int main(void)
{
/* 先验一次宿主 CRT 与本模块 fputc 的关系(被 tc_params 记成 TC-CONLOG-000) */
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();
printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
fflush(stdout);
return (s_failed == 0) ? 0 : 1;
}