Files
STM32F4-Base/test/host/console_log/console_log_test.c
T
wandering1 5a806d8a26 fix(log,net): 处置主机单测照出的 10 条现状,撤销 1 条误报
用户裁定「全部修」。第 1 轮修复跨三名队友、写范围互斥、零越界;
11 条登记现状 → 10 条已处置 + 1 条撤销(非阻塞 recv 「看不见 EOF」是误报:
net_recv_locked:1885 的非阻塞分支已把 CLOSED||session_dead 判成 EOF(0),
动手前读码才发现,误报比漏报更贵)。

存储与日志(App/log/sys_log_mcu.c、soe_store.c、cfg_file_store_mcu.c、sys_log.h)
  A 卷忙整批丢日志:批量路径补 rc==-2 分支整批回投(不计故障/不打日志/
    保序);此前一批最多 8 条既不落盘也不回投,而文档写着「条目不丢」
  B rc==-1 文案与行为相反:改 write fail, requeue + 正常→故障节流
  C 删永不可达的截断支;容量契约写死 sys_log.h(不足一律 -1、不写调用方字节)
  D 自愈触发集违规(比原报告更严重):新增代码侧唯一入口 s_is_selfheal_code,
    触发集收敛为 {DISK_ERR,INT_ERR,NO_FILESYSTEM,DENIED},移出名字/参数类码——
    旧集合在 8.3 同名同因下每轮删一次自己的历史日志且永不自愈,正是存储架构
    §10.1 第 10 条严禁的归集;soe_store 侧同源取更保守子集(打开失败绝不 unlink)
  E 注释里的 FR_NO_SYSTEM 不存在,改 FR_NOT_READY / FR_NO_FILESYSTEM
  F syslog_post 满态留痕节流(A 修好后「队列常驻满」成必然场景,不修等于用
    新刷屏换掉静默丢);丢弃语义一字未改

NET(net_socket.c/.h、net_types.h、net_socket_sim.c、lftpd.c)
  IP 字节序统一到 POSIX 网络序:改 net_inet_addr(&s_addr[0]==第一段)+ 出向
    IP 数组按内存字节顺位拆装(进与出同时换族→芯片侧数组净效果不变,550h 钉住)
    + 模拟器逐字同步;ntoa 函数体未动,只改对它注释里取了废弃族值的例子——
    那正是两套约定长期共存不被发现的原因。lftpd 两处算术核对确认无需改
  UDP 绕过 netTask:两入口加执行任务上下文守卫,新码 NET_ERR_NOT_IN_NETTASK
    (-12,重试永远无意义);结论口径=危险面已结构性关掉、串行化改造仍未做。
    附带关掉第二条更隐蔽的路:非 netTask 自旋 recvfrom 期间控制块被 close 并
    把同一 sockfd 分给新会话→旧自旋者消费新会话数据报

控制台(console_log.c/.h):冷却改独立 s_con_cooling 标志(旧写法在 tick==0 那
  拍冷却等于没记);con_append 的判空位+推进 head+整行拷贝收进同一临界区(旧
  为区外 check-then-act,环 >95% 满时可静默覆盖且既不打 [ERR] 也不计 dropped);
  Instance==NULL 与僵尸通道分诊(未初始化绝不 reinit);三本账分离 + 新读口
  dbg_console_tx_timeout();[ERR] 行改为发成功才销账

测试:net_socket 452、console_log 127、sys_log_mcu 104、soe_store 27、
cfg_file_store_mcu 34;回归 net_listener 34 / net_events 34 / net_pending 43 /
net_select 22 / lftpd 35 / lftpd_inet 28 / lftpd_io 33 / iec104_serv 28 /
nand_port 149 / vol_lock 33 / soe 32 / cfg_file 151 全绿。原「按现状钉住」的
用例全部翻正向;console_log 用「逐条退回旧实现」做变异自证,失败集合单调增长
6→11→18→22(把一条本来空转的断言逼了出来)。
游标抬升:TC-NETSOCK-545→560、TC-SYSLOG-100→107、TC-CONLOG-135→167。

文档:控制台设计说明 §4.1 口径与 §13-O2 销项 + v1.5;运行与自检记录 §4.4/§4.5
四条;存储架构 §10.1 第 10 条推论(指向 s_is_selfheal_code);TRAP-NET-21 后续
条目;BSD 指南新增 §9.0 s_addr 唯一约定;覆盖率口径 §9「第 1 轮修复处置」表 +
后续项 5 条 + 台架复验 7 条 + 新坑 8 条 + 变更记录。

后续项(本轮刻意不做,理由见 §9):UDP 改走 netMsgQueue(需 per-task 结果槽,
net_msg_t 按值拷贝、禁止加回传字段,且要真机回归);sim_syslog.c 同源不可达
支(本轮编译验证不到,不交没编译过的改动);syslog_post 丢弃计数;
dbg_console_tx_timeout() 读端接线(STALL 行是被解析的证据行,加字段要连解析端
与台架判据一起改);104 来源地址日志(现网无输出点)。

gate: @build 0 errors 0 warnings(Code=213720 RO=18860 RW=1204 ZI=131500)。
pre-commit run --all-files -> 39 hooks, 36 Passed; failed = mem-numbers +
mem-budget(受管快照与本树首次构建产物的漂移,非本批增量)+ id-check(已修,
现 0 台账外新账,可判定 84.63% > 基线 82.6%)。实测水位 RW_IRAM1 57.4%、
RW_CCM 87.9%,均低于 92% 红线;RAM/Flash 本批增量为数个 static 变量。
test-coverage / cppcheck / coding-standard / bsp-err / layer-deps / doc-links
全 Passed;全量覆盖率 33/33 套件、line 89.3%、func 92.3%、逐文件下降 0。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 19:43:56 +08:00

1198 lines
53 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 参数校验与截断 --\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(&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 回绕两段拷贝位置正确(前 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 + 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 在环里(本块被丢)");
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_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-080 到点即清冷却标志、放行本轮排空,且没有新增故障"
"(cool_until 只存时刻,不再被拿来当哨兵清零)");
/* 修复后的正向断言:冷却与否看独立标志,不再拿 tick 值当哨兵 */
reset_all();
fault_setup(0U, 0); /* 故障恰好落在 HAL_GetTick()==0 那一拍 */
CHECK(s_con_cooling == 1U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-081 tick==0 那拍的故障照样进入冷却(独立标志,不撞 0 哨兵)");
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 同一 tick 上再来一轮被冷却挡住:不再白耗一次块超时");
con_tick_now = DBG_CON_FAULT_COOL_MS - 1U;
writer_drain();
CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-083 冷却 299ms 仍未到点");
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 到点后清冷却并放行:全程没有第二次故障");
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 超时判据的真实后果:半块 / 已上线仍判失败 --\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..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 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(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 重定向(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 && con_ring_store[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) 同样拒绝");
}
/* ============ 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;
}