1d4b6a17ce
台架一手证据(boot_log_data/20261026_prod_console_interleave_evidence.txt):两次 「仅复位不重烧」启动里,[PROT] 未标定 INFO 行内分别多出 3 个和 1 个字节,而紧邻其后 的 [NAND_FTL] 行正好缺自己行首同样数量的字节("[20" 与 "[")——串口 framing 错不会 让行变长,字节按原序出现在邻居里 ⇒ 两个写者并发逐字节写 USART1->DR。写者对: protectTask 的未标定闭锁边沿 × defaultTask 的 storage_bring_up(),两者都落在 dbg_console_writer_attach() 之前(attach 排在存储 bring-up 之后),该窗口 s_con_writer_up == 0 ⇒ 任何任务的整行都走零互斥裸发。这是控制台单写者设计说明在册 O1 的**第二个实例**;2026-10-12 那次只补了"自愿门控",而自愿门控挡不住业务边沿留痕 (业务无法预知某条边沿落在哪个窗口),所以这次被同一窗口第二次咬到。 修法是**非阻塞整行独占**:con_tx_try_claim/release 只在几条指令的临界区里 test-and-set,绝不把 7.8 ms 的逐字节发送包进关中断区、绝不等待;IPSR 上下文与调度 器未启动两种情况显式放行(崩溃遗言必须能出去;早期本就单执行流,一条不丢);同任务 按深度嵌套。抢不到独占的那条行**原文入环**延后发——前缀与时间戳在 DBG_* 调用点就 格式化进调用方缓冲,本模块与 drain 都不补前缀、不重打时间戳,所以延后发出的行与当场 直发逐字节相同;[ERR] 只在「抢不到独占且环也放不下」时才红,按 3 次启动命中 2 次的 频率,让路若计 [ERR] 就会把静默缺陷换成常红门禁。顺带堵掉第二处洞:drain 每轮先取 凭证,拿不到就让位且 tail 不推进(旧写法 attach 可以撞上别人已发出的半行)。 新导出 dbg_console_tx_begin()/tx_end() 供 task-26 收口故障钩子的多段裸发。 否决过的两条路写进文档 §4.4.1:提前 attach 会把等串口塞进 mount/mkfs 时序敏感段 或让生产者在整个 bring-up 里堵在满环上;整段进环会让「无看门狗窗口」死机时一个字都 不出(2026-10-02 那几轮正是靠 bring-up 期流式日志取证的)。 判据:test/host/console_log 127 → 152 全绿(M 族 TC-CONLOG-170..179 共 +25,既有 127 条零删除)。唯一换钉法的是 TC-CONLOG-015——旧断言钉的正是缺陷机理本身(裸发 0 次临界区 = 零互斥),现改钉「恰 2 次几条指令的临界区 + 每个上线字节都在临界区之外 + 零等待」;硬红线由假 USART 在每个上线字节处记录临界区深度水位恒 0 来机器守住。四处 注入反证各自转红 11 / 5 / 1 / 3 条并逐字节还原复跑;覆盖率 console_log.c 199/200 行 → 256/257 行、16/16 → 21/21 函数。邻域复跑:task_prof 27/27、sys_log_mcu 137/137、 iec104_serv 37/37。CONLOG 游标抬到 179。 台架复验判据(**本笔尚未上机**,下一轮请台架跑):连续 3~5 次「仅复位不重烧」启动 日志 [ERR] 零命中,且 [PROT] 未标定与 [NAND_FTL] Initializing 两行都完整。 成本:.bss +5 B、Flash 估 +100~150 B,零新增 DBG_* / 零新增缓冲 / 零堆 / 零内核对象。 文档:控制台单写者日志设计说明.md 升 v1.6(O1 改「已处置」含两实例与字节账、新增 §4.4.1、§3.4 并发区间、§4.4 取消「不加锁」口径、§7 失效表、§11 变更记录、API 表)。
1409 lines
68 KiB
C
1409 lines
68 KiB
C
/*
|
||
* 模块名称:控制台单写者日志(console_log)主机侧单元测试
|
||
* 模块功能:在 PC 上离线验证 App/log/console_log.c 的单写者契约(G1~G5 / K1~K5,见
|
||
* docs/子系统设计/控制台单写者日志设计说明.md):环形缓冲入环与分块发送、
|
||
* 行级原子、192B 截断、写者未注册/ISR 上下文的直发(未注册期自 2026-10-26
|
||
* 起是**整行独占**,不再是"无锁":M 族 TC-CONLOG-170..179 钉"别的写者插不进
|
||
* 一行、让路行逐字节等同直发、让位不丢、真溢出才 [ERR]")、缓冲满有界等待与
|
||
* 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、
|
||
* huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算、
|
||
* 整行所有权(行级单一所有者,道G task-23)。
|
||
*
|
||
* 为什么要主机侧单测(而不是只用台架):
|
||
* 本模块的风险全在"异常时刻"——UART 僵尸通道卡死、TIM7 停摆导致 uwTick 不往前走、
|
||
* 开机 1.7s 喂狗容限被背靠背块超时吃掉(台架实测的停喂误复位)、写者注册前多任务裸发
|
||
* 逐字节交错(2026-10-10/10-12 台架实证)。台架上这些要么一年一遇、要么现场已被
|
||
* 复位冲掉;而它们的判据全是"数得清的数":192B 上限、256B 分块、4095B 可用环、
|
||
* 200 tick 等待、20000 次计数上界、100/250/300ms 三条时间线。把它们做成断言,是这台
|
||
* 设备上唯一能把这几条线钉死的办法(板端套件只能"在真机上跑一遍")。
|
||
*
|
||
* 桩的边界(细节见 stub/stm32f4_fake.h 头注释):
|
||
* console_log.h / sys_events.h(含真实 SYS_EVT_CONSOLE_TX 位)用**真实头**;
|
||
* usart.h 用桩,但只暴露本模块访问的 huart1 + MX_USART1_UART_Init,寄存器位号 /
|
||
* 枚举值照抄真实头;USART SR 的 TXE/TC、DWT CYCCNT、HAL_GetTick、临界区、
|
||
* __get_IPSR、vTaskDelay、osEventFlagsSet 做成可控 + 可观测的量。
|
||
* ⚠ 本文件**刻意不定义**任何 syslog_* / DBG_* 符号:console_log.c 哪天把"TX 故障
|
||
* 留痕"改回直接调日志子系统,链接就失败——那正是必须挡住的(write_line →
|
||
* con_append → drain → syslog_post → write_line 成环,静态栈深在每个任务上虚高)。
|
||
*
|
||
* 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 127 号 / 127 条断言):
|
||
* 000..006 宿主 printf 与本模块 fputc 的隔离 + 参数校验与 192B 截断
|
||
* 010..018 写者未注册 = 无锁直发(不入环、不置事件位、零临界区、不等待、256B 有界)
|
||
* 020..038 注册后走环形:入环不发送、按 256B 分块、多生产者不交错、跨环尾两段拷贝、
|
||
* 入环长度钳 RING/2
|
||
* 040..044 写者自记录先排空(不自等)/ drain 重入保护 / 未注册时 drain 让位
|
||
* 050..062 缓冲满:DWT 硬上界丢弃、不覆盖旧行、等待中出现空间、无 DWT 时 200 tick
|
||
* 上界、[ERR] dropped 直发不经环、读后即清
|
||
* 070..088 TX 僵尸通道:软复位自愈序列、故障即止、WARN 只一次、tx_fault_log_take
|
||
* 读后清、300ms 冷却两端、tick==0 也进冷却(哨兵撞值修好后为正向断言)、
|
||
* 半块 / 已上线仍判失败
|
||
* 090..106 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读
|
||
* HAL_GetTick(第 8b 轮台架动机)、huart1.Instance == NULL 短路两路 +
|
||
* "未初始化绝不 reinit"(误自愈修好后为正向断言)+ 证据不蒸发
|
||
* 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR
|
||
* 仍不等待)
|
||
* 120..122 drain 单次 250ms 预算与下轮续排(让位喂狗但不丢字节)
|
||
* 130..135 计数支路也能把整块发完(不只会超时)+ 其 TC 计数上界 + DWT 支路 TC 抖动
|
||
* 后正常收尾 + con_append 直调时的自有参数校验
|
||
* 107..108 冷却截止 tick 跨 49.7 天回绕两端(无符号差值),改用独立标志后仍成立
|
||
* 140..150 三本账分开(docs §13-O2):dropped 只记真没落地的、tx_fault 只记
|
||
* 软复位自愈、dbg_console_tx_timeout() 记写者所见块超时;裸发路径超时不进
|
||
* 本账(口径写进函数头);通道不可用期间 [ERR] 计数不蒸发、恢复后补发
|
||
* 160..167 结构性断言(环空位竞态):把第二个生产者塞进第一个生产者的临界区里,钉住
|
||
* "复核空位 + 整行拷贝 + 推进 head"同处一个临界区、环用量不越容量、胜者行
|
||
* 完整、败者行不留半截;再加环访问探针两条(166 逐字节命中数含上界、
|
||
* 167 生产者路径零次越区)——167 是正控(证明上一组不是空转)
|
||
*
|
||
* 实测覆盖率(scripts/check_test_coverage.py --module console_log,/Od + /Zi 口径):
|
||
* App/log/console_log.c **199/200 行 = 99.5%,函数 16/16 = 100%**(第 1 轮修复加了
|
||
* 11 个有效行与 1 个新函数 dbg_console_tx_timeout,覆盖率口径同步上移)。
|
||
*
|
||
* 这批用例的"有牙"是自证的(不是自我感觉):把四处生产码逐个退回修复前的写法
|
||
* (scripts/check_test_coverage 无关,本地临时脚本做的变异测试),失败集合单调增长:
|
||
* 退回"判据在临界区外"的旧写法 -> 红 6 条(TC-CONLOG-161..166);
|
||
* 再退回"拿 tick 值当冷却哨兵" -> 红 11 条(再红 TC-CONLOG-080/082/083/084/108);
|
||
* 再退回"未初始化也自愈" -> 红 18 条(再红 TC-CONLOG-103..106/144/148/149);
|
||
* 四条全退回 -> 红 22 条(再红 TC-CONLOG-140/141/143/145)。
|
||
* 改回修复版即 127/127 全绿;每一步的失败集合都是上一步的超集,说明四组断言各自
|
||
* 咬住自己的那条语义,没有互相顶包。
|
||
* 2026-10-26(道G task-23,行级单一所有者)另做四处注入,逐个单独注入、各自归因、
|
||
* 每次改完逐字节还原(152 条基线复跑绿):
|
||
* 取消整行独占(退回"无条件直发") -> 红 11 条(含 178 = 台架原文形态那条行);
|
||
* 让路改成静默丢弃(不入环也不计数) -> 红 5 条(170b/170c/171/178/179b);
|
||
* 归还所有权时不比属主(两道卫兵都拆) -> 红 1 条(179c;只拆外层里层仍守住时
|
||
* 测试全绿是**正确结果**,不是漏测);
|
||
* 把整段发送包进临界区(关中断 7.8ms) -> 红 3 条(015/015b/177 与 176)。
|
||
* ⚠ 顺带记一条注入方法论:第一版"取消整行独占"的锚点在 raw 与 drain 里各出现一次,
|
||
* 被写入前的唯一性守卫挡下——模糊锚点会一改变两个地方,等于同时测两个变异。
|
||
* 也就是说:这些断言不是"写在假件规则上"的自证,而是真能咬住生产语义的。
|
||
*
|
||
* 故意没测(逐行说明见交付报告):
|
||
* - console_log.c:383 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。
|
||
* note[64],前缀 24B + "%lu"(unsigned long 最多 10 位)+ 后缀 8B = 最长 42B
|
||
* < 63B,任何输入都到不了这一行 —— 纯防御,不是漏测。
|
||
* - 真实周期时间量级:主机没有真 DWT CYCCNT 也没有 TIM7 的 uwTick,只能钉"判据在
|
||
* 第几次读成立 / 自旋几次放弃",不能声称"实测 100ms 就返回"。
|
||
* - ISR 与写者同时裸发的真总线交错(docs §13-O1):宿主单线程,造不出两个执行流。
|
||
* - osEventFlagsWait 唤醒写者与 50ms 兜底:写者任务体在 App/task/default_task.c。
|
||
* - MicroLIB 真实 retarget 链接语义:宿主 CRT 有自己的 fputc(实测与本模块符号
|
||
* 冲突 -> LNK2005),故本 TU 把被测 fputc 改名纳入,只验函数体不验链接钩子。
|
||
* - 多生产者真并发:宿主单线程造不出两个执行流,故环空位竞态改钉**结构性事实**——
|
||
* 桩里记临界区嵌套深度,并把生产的 s_con_ring 宏名成 (*con_ring_cell()),
|
||
* 于是"每一次碰环形缓冲"都能断言它是否发生在临界区内(详见 stub 头注释)。
|
||
* 这不是"模拟并发",是把"判据与推进之间不出区"这句话本身变成可执行断言。
|
||
*
|
||
* 通过:全部 TC PASS(main 返回 0)。
|
||
* 入口:int main(void),返回 0=PASS / 1=FAIL;跑飞保险 exit(2)。
|
||
*
|
||
* 创建日期:2026-10-24
|
||
*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdint.h>
|
||
|
||
/* 平台假件先纳入:它给出 USART/DWT/HAL/内核入口的宿主等价声明(stub 目录排第一) */
|
||
#include "stm32f4_fake.h"
|
||
|
||
/* ===================== 假件全局量(声明见 stub/stm32f4_fake.h) ===================== */
|
||
uint32_t SystemCoreClock = 168000000U; /* 与 MCU 端一致(168 MHz) */
|
||
|
||
USART_TypeDef g_con_uart;
|
||
CoreDebug_Type g_con_core_debug;
|
||
DWT_Type g_con_dwt;
|
||
UART_HandleTypeDef huart1;
|
||
|
||
uint32_t con_uart_tx_n;
|
||
uint8_t con_uart_tx[CON_UART_TX_MAX];
|
||
uint32_t con_uart_block_n;
|
||
uint32_t con_uart_block_len[CON_UART_BLOCK_MAX];
|
||
uint32_t con_uart_cur_len;
|
||
uint32_t con_uart_txe_reads;
|
||
uint32_t con_uart_tc_reads;
|
||
int con_uart_txe_stuck;
|
||
int con_uart_txe_after;
|
||
int con_uart_txe_ready_left;
|
||
int con_uart_tc_after;
|
||
int con_uart_tc_stuck;
|
||
int con_uart_pending;
|
||
uint32_t con_uart_tx_hook_at; /* 道G task-23:第 N 个字节上线后触发一次 */
|
||
void (*con_uart_tx_hook)(void);
|
||
uint32_t con_uart_crit_at_tx_max; /* 上线字节所见最大临界区深度(硬红线水位) */
|
||
|
||
uint32_t con_dwt_step;
|
||
int con_dwt_ctrl_sticky;
|
||
uint32_t con_dwt_reads;
|
||
|
||
uint32_t con_tick_now;
|
||
uint32_t con_tick_step;
|
||
uint32_t con_tick_reads;
|
||
|
||
static char s_task_writer; /* 假写者任务句柄(取地址当不透明句柄) */
|
||
static char s_task_other; /* 假"别的任务"句柄 */
|
||
TaskHandle_t con_task_self;
|
||
|
||
uint32_t con_delay_calls;
|
||
void (*con_delay_hook)(void);
|
||
|
||
uint32_t con_crit_enter;
|
||
uint32_t con_crit_exit;
|
||
uint32_t con_crit_max_depth;
|
||
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;
|
||
/* 道G task-23 的整行所有权凭证:跨场景必须显式清,否则上一场景留下的持有者会让
|
||
* 下一场景的裸发全部走环,症状正是"单独跑绿、连跑偶红" */
|
||
s_con_tx_owner = 0;
|
||
s_con_tx_depth = 0U;
|
||
|
||
/* 一次性把手先归零:上一条用例装填的钩子如果留在这儿,会把"第二个生产者"塞进
|
||
* 本条用例的某个随机字节之后(那种红比真缺陷还难查) */
|
||
con_uart_tx_hook_at = 0U;
|
||
con_uart_tx_hook = 0;
|
||
con_uart_crit_at_tx_max = 0U;
|
||
con_uart_reset();
|
||
con_dwt_reset();
|
||
|
||
con_tick_now = 0U;
|
||
con_tick_step = 0U;
|
||
con_tick_reads = 0U;
|
||
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
con_delay_calls = 0U;
|
||
con_delay_hook = 0;
|
||
con_crit_hook = 0;
|
||
|
||
con_crit_enter = 0U;
|
||
con_crit_exit = 0U;
|
||
con_crit_max_depth = 0U;
|
||
s_crit_depth = 0U;
|
||
|
||
memset(con_ring_store, 0, sizeof(con_ring_store));
|
||
con_ring_probe_on = 0;
|
||
con_ring_probe_hits = 0U;
|
||
con_ring_zero_depth_hits = 0U;
|
||
con_ring_draining = 0;
|
||
|
||
con_ipsr = 0U;
|
||
|
||
sys_evtHandle = (osEventFlagsId_t)&s_evt_obj;
|
||
con_evt_sets = 0U;
|
||
con_evt_flags = 0U;
|
||
|
||
con_nvic_clears = 0U;
|
||
con_rcc_disables = 0U;
|
||
con_rcc_enables = 0U;
|
||
s_mx_init_calls = 0U;
|
||
s_mx_repair = 0;
|
||
|
||
huart1.Instance = &g_con_uart;
|
||
huart1.gState = HAL_UART_STATE_READY;
|
||
huart1.RxState = HAL_UART_STATE_READY;
|
||
huart1.ErrorCode = HAL_UART_ERROR_NONE;
|
||
}
|
||
|
||
/* ===================== 观察辅助 ===================== */
|
||
static uint32_t used_now(void)
|
||
{
|
||
return con_used();
|
||
}
|
||
|
||
static const char *tx_at(uint32_t skip)
|
||
{
|
||
return (skip >= con_uart_tx_n) ? "" : (const char *)&con_uart_tx[skip];
|
||
}
|
||
|
||
static int tx_is(uint32_t skip, const char *s)
|
||
{
|
||
size_t n = strlen(s);
|
||
|
||
if ((size_t)con_uart_tx_n < (size_t)skip + n) {
|
||
return 0;
|
||
}
|
||
return memcmp(tx_at(skip), s, n) == 0;
|
||
}
|
||
|
||
/* 断言 [0, n) 全部是同一个字符(回绕场景下按字符比对,不依赖 NUL 结尾) */
|
||
static int tx_prefix_all(char c, uint32_t n)
|
||
{
|
||
uint32_t i;
|
||
|
||
if (con_uart_tx_n < n) {
|
||
return 0;
|
||
}
|
||
for (i = 0U; i < n; i++) {
|
||
if (con_uart_tx[i] != (uint8_t)c) {
|
||
return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
static int tx_at_is(char c, uint32_t skip, uint32_t n)
|
||
{
|
||
uint32_t i;
|
||
|
||
if (con_uart_tx_n < skip + n) {
|
||
return 0;
|
||
}
|
||
for (i = 0U; i < n; i++) {
|
||
if (con_uart_tx[skip + i] != (uint8_t)c) {
|
||
return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* 在已上线字节里找一段(tx 是二进制缓冲,不依赖 NUL 结尾) */
|
||
static int tx_contains(const char *needle)
|
||
{
|
||
size_t nl = strlen(needle);
|
||
uint32_t i;
|
||
|
||
if ((size_t)con_uart_tx_n < nl) {
|
||
return 0;
|
||
}
|
||
for (i = 0U; (size_t)i + nl <= (size_t)con_uart_tx_n; i++) {
|
||
if (memcmp(&con_uart_tx[i], needle, nl) == 0) {
|
||
return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static uint32_t tx_count(const char *needle)
|
||
{
|
||
size_t nl = strlen(needle);
|
||
uint32_t i;
|
||
uint32_t hits = 0U;
|
||
|
||
if ((size_t)con_uart_tx_n < nl) {
|
||
return 0U;
|
||
}
|
||
for (i = 0U; (size_t)i + nl <= (size_t)con_uart_tx_n; i++) {
|
||
if (memcmp(&con_uart_tx[i], needle, nl) == 0) {
|
||
hits++;
|
||
}
|
||
}
|
||
return hits;
|
||
}
|
||
|
||
/* 写者任务上下文里排空(生产契约:drain 只允许写者任务调) */
|
||
static void writer_drain(void)
|
||
{
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
con_ring_draining++; /* 此刻读环的是写者,不计入生产者违规 */
|
||
dbg_console_drain();
|
||
con_ring_draining--;
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
}
|
||
|
||
/* 后台排空(不改当前任务句柄):模拟"生产者等一轮,写者腾出了空间" */
|
||
static void bg_drain_hook(void)
|
||
{
|
||
con_ring_draining++;
|
||
dbg_console_drain();
|
||
con_ring_draining--;
|
||
}
|
||
|
||
/* 把环填到只剩 want_free 字节可用:走真实入环路径(顺带踩到回绕)。
|
||
* 填充期间把 DWT 上界调大——万一算差一位也不会卡在等待里出不来。 */
|
||
static void fill_ring(uint32_t want_free)
|
||
{
|
||
char pad[DBG_CON_LINE_MAX];
|
||
uint32_t i;
|
||
|
||
memset(pad, 'Z', sizeof(pad));
|
||
con_dwt_step = SystemCoreClock / 4U;
|
||
for (i = 0U; i < 40U; i++) {
|
||
uint32_t free_space = (uint32_t)((DBG_CON_RING_SIZE - 1U) - used_now());
|
||
uint32_t n;
|
||
|
||
if (free_space <= want_free) {
|
||
break;
|
||
}
|
||
n = free_space - want_free;
|
||
if (n > DBG_CON_LINE_MAX) {
|
||
n = DBG_CON_LINE_MAX;
|
||
}
|
||
if (dbg_console_write_line(pad, n) != (int)n) {
|
||
break;
|
||
}
|
||
}
|
||
con_dwt_step = 0U;
|
||
s_con_drop = 0U; /* 填充过程中的边角丢弃不算场景断言 */
|
||
}
|
||
|
||
/* 注册写者(defaultTask),随后把"当前任务"换回别的任务 */
|
||
static void attach_writer(void)
|
||
{
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
dbg_console_writer_attach();
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
}
|
||
/* ============ TC-CONLOG-000..006 参数校验与截断 ============ */
|
||
static void tc_params(void)
|
||
{
|
||
char buf[DBG_CON_LINE_MAX + 128U];
|
||
|
||
printf("-- TC-CONLOG-000..006 argument checks and truncation --\n");
|
||
reset_all();
|
||
printf("(probe) this line goes through the host CRT\n");
|
||
fflush(stdout);
|
||
CHECK(con_uart_tx_n == 0U && used_now() == 0U,
|
||
"TC-CONLOG-000 host printf does not pass through this module fputc (else test output would be swallowed into the ring)");
|
||
|
||
reset_all();
|
||
con_uart_txe_stuck = 1; /* 通道卡死:任何字节都不该上线 */
|
||
CHECK(dbg_console_write_line(0, 5U) == -1, "TC-CONLOG-001 write_line(NULL) returns -1");
|
||
CHECK(dbg_console_write_line("abc", 0U) == -1, "TC-CONLOG-002 write_line(len=0) returns -1");
|
||
dbg_console_write_raw(0, 5U);
|
||
dbg_console_write_raw("abc", 0U);
|
||
CHECK(con_uart_tx_n == 0U, "TC-CONLOG-003 both NULL and len=0 never touch the UART");
|
||
CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-004 invalid arguments are not counted as drops");
|
||
|
||
reset_all();
|
||
memset(buf, 'A', sizeof(buf));
|
||
CHECK(dbg_console_write_line(buf, sizeof(buf)) == (int)DBG_CON_LINE_MAX,
|
||
"TC-CONLOG-005 a line longer than DBG_CON_LINE_MAX is truncated to 192 (return value is the delivered byte count)");
|
||
CHECK(con_uart_tx_n == DBG_CON_LINE_MAX, "TC-CONLOG-006 after truncation exactly 192 bytes reach the wire");
|
||
}
|
||
|
||
/* ============ TC-CONLOG-010..018 写者未注册 = 无锁直发 ============ */
|
||
static void tc_unattached(void)
|
||
{
|
||
char big[DBG_CON_CHUNK_MAX + 64U];
|
||
|
||
printf("-- TC-CONLOG-010..018 writer not attached: lock-free direct send --\n");
|
||
reset_all();
|
||
CHECK(dbg_console_writer_ready() == 0U, "TC-CONLOG-010 writer_ready() == 0 before attach");
|
||
CHECK(dbg_console_write_line("hello", 5U) == 5, "TC-CONLOG-011 before attach the whole line is sent directly, returning its length");
|
||
CHECK(tx_is(0U, "hello"), "TC-CONLOG-012 direct send reaches the wire verbatim (never went through the ring)");
|
||
CHECK(used_now() == 0U, "TC-CONLOG-013 direct send does not enter the ring (head == tail)");
|
||
CHECK(con_evt_sets == 0U, "TC-CONLOG-014 direct send does not set SYS_EVT_CONSOLE_TX (no writer to wake yet)");
|
||
/* ⚠ 本条判据在道G task-23(2026-10-26)**换了钉法**,这里留全过程:旧写法断言
|
||
* "裸发用 0 次临界区",钉住的正是当时的**缺陷机理本身**(零互斥 => 多个任务可以
|
||
* 在同一行里互相插字节;台架 20261012_B6 的 "st[art:" 与本次 protectTask 未标定
|
||
* 行内多出 "[" "2" "0" 三字节都是它)。修复后裸发要取"整行所有权",取与还各一次、
|
||
* 每次只有几条指令的 test-and-set,所以计数从 0 变 2。
|
||
* 换来两条更硬的判据(缺一条就该红),断言总数只增不减:
|
||
* ① 每个上线字节都发生在临界区**之外**(con_uart_crit_at_tx_max == 0)——整行
|
||
* 独占绝不允许把 @115200 约 7.8 ms 的发送包进关中断区,那等于往启动窗口塞
|
||
* 7.8 ms 中断延迟与最坏喂狗抖动,比交错本身更糟(Lead 红线);
|
||
* ② 全程零次 vTaskDelay:拿不到所有权就当场让路,不排队(见 TC-CONLOG-170 族)。
|
||
*/
|
||
CHECK((con_crit_enter == 2U) && (con_crit_exit == 2U) && (con_crit_max_depth == 1U),
|
||
"TC-CONLOG-015 direct send takes exactly two few-instruction critical sections (claim + return) and never nests them");
|
||
CHECK((con_uart_crit_at_tx_max == 0U) && (con_delay_calls == 0U),
|
||
"TC-CONLOG-015b whole-line ownership puts every wire byte outside any critical section and never waits");
|
||
|
||
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 --\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");
|
||
}
|
||
|
||
/* ===================== M 整行所有权(道G task-23,O1 第二个实例) ===================== */
|
||
|
||
/* 装进"A 行发送中途"的第二个生产者:换任务句柄后再裸发一行,精确模拟真实抢占
|
||
* —— A(s_task_other)此刻持有整行所有权,B(s_task_writer)必须让路。
|
||
* 这是本套唯一能把"两个生产者并发时,写进 DR 的字节流里不得出现另一条行的字节"
|
||
* 做成必然失败断言的把手:修复后是**非阻塞**设计,永远不会调 vTaskDelay,所以既有的
|
||
* con_delay_hook 在这条路径上根本不会被触发(拿它当注入点会静默地什么都测不到)。
|
||
* 台架一手证据:docs/测试说明/boot_log_data/20261026_prod_console_interleave_evidence.txt
|
||
* —— PROT 行内多出 [ 2 0 三字节,紧接其后的 NAND_FTL 行恰好缺这三个字节的行首。 */
|
||
/* 台架 16:37 那次被插字的原文行(boot_log_data/20261026_prod_console_interleave_evidence.txt
|
||
* OCCURRENCE 1 去掉三个外来字节之后的样子)。M 族拿它当"真实行长与真实前缀"的样本,
|
||
* 免得用例自己造一个不像现场的理想行。 */
|
||
static const char s_prot_line[] =
|
||
"[2026-10-26 16:37:07][PROT] uncalibrated -> protection blocked"
|
||
" (UA/UN/IA VALID not all set)\r\n";
|
||
|
||
static void s_tx_second_writer(void)
|
||
{
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
dbg_console_write_raw("BBBBBB", 6U);
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
}
|
||
|
||
static void tc_ownership(void)
|
||
{
|
||
char fill[DBG_CON_LINE_MAX];
|
||
uint32_t i;
|
||
|
||
printf("-- TC-CONLOG-170..179 whole-line ownership (O1 instance 2, bench byte account) --\n");
|
||
|
||
/* 170 A 行发到第 4 个字节时被 B 抢占:A 逐字节完整,B 整行让路入环 */
|
||
reset_all();
|
||
con_uart_tx_hook_at = 4U;
|
||
con_uart_tx_hook = s_tx_second_writer;
|
||
dbg_console_write_raw("AAAAAAAA", 8U);
|
||
CHECK(tx_is(0U, "AAAAAAAA") && (con_uart_tx_n == 8U),
|
||
"TC-CONLOG-170 the line already on its way out stays byte-intact when a second producer preempts mid-line");
|
||
CHECK(used_now() == 6U,
|
||
"TC-CONLOG-170b the preempted producer books its whole line into the ring instead of spraying bytes into another line");
|
||
CHECK((dbg_console_dropped() == 0U) && (con_delay_calls == 0U),
|
||
"TC-CONLOG-170c yielding is neither a drop nor a wait (this is what the earlier whole-line-loss variant would have cost)");
|
||
|
||
/* 171 判据⑦:延后发出去的内容 = 当场直发本会产出的同一批字节(同时间戳/单前缀/单 CRLF) */
|
||
con_uart_tx_hook = 0;
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
dbg_console_writer_attach();
|
||
writer_drain();
|
||
CHECK((con_uart_tx_n == 14U) && tx_is(8U, "BBBBBB") && (used_now() == 0U),
|
||
"TC-CONLOG-171 the deferred line reaches the wire byte-identical to immediate sending (no second prefix, no re-stamped time, single CRLF)");
|
||
|
||
/* 172 抢不到所有权 + 环也放不下 -> 才计真丢弃(真溢出必须仍然看得见) */
|
||
reset_all();
|
||
(void)memset(fill, 0x58, sizeof(fill));
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
dbg_console_writer_attach();
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
CHECK(dbg_console_tx_begin() == 1U, "TC-CONLOG-172 a task can take whole-line ownership while the wire is free");
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
for (i = 0U; i < 21U; i++) {
|
||
(void)dbg_console_write_line(fill, sizeof(fill)); /* 21*192=4032,环可用 4095 -> 只剩 63 字节 */
|
||
}
|
||
CHECK((used_now() == (21U * (uint32_t)DBG_CON_LINE_MAX)) && (con_uart_tx_n == 0U),
|
||
"TC-CONLOG-172b the writer yields the whole round while another task owns the wire: nothing sent, nothing dequeued");
|
||
dbg_console_write_raw(fill, 70U);
|
||
CHECK((dbg_console_dropped() == 1U) && (con_uart_tx_n == 0U)
|
||
&& (used_now() == (21U * (uint32_t)DBG_CON_LINE_MAX)),
|
||
"TC-CONLOG-172c only a line that can neither own the wire nor fit the ring counts as a real drop, and it never half-sends");
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
dbg_console_tx_end();
|
||
writer_drain();
|
||
CHECK((con_uart_tx_n >= (21U * (uint32_t)DBG_CON_LINE_MAX))
|
||
&& tx_contains("[ERR] CONSOLE dropped 1"),
|
||
"TC-CONLOG-172d once ownership frees up the deferred round goes out complete, and the true overflow surfaces as [ERR]");
|
||
/* 172e 报出去就销账(既有语义,本卡不许退化):账清零 + 不重复报同一笔 */
|
||
CHECK((dbg_console_dropped() == 0U) && (tx_count("[ERR] CONSOLE dropped 1") == 1U),
|
||
"TC-CONLOG-172e the drop ledger is settled only after the [ERR] note actually left the wire, and it is not reported twice");
|
||
|
||
/* 173 写者一轮 drain 被所有权挡住:让位 != 丢 */
|
||
reset_all();
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
(void)dbg_console_tx_begin();
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
dbg_console_writer_attach();
|
||
(void)dbg_console_write_line("L1\r\n", 4U);
|
||
CHECK((con_uart_tx_n == 0U) && (used_now() == 4U),
|
||
"TC-CONLOG-173 while another task owns the wire the writer sends nothing and keeps every byte queued");
|
||
writer_drain();
|
||
CHECK((con_uart_tx_n == 0U) && (used_now() == 4U) && (dbg_console_dropped() == 0U),
|
||
"TC-CONLOG-173b yielding a round is not a drop: the tail never advances, so no ledger moves");
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
dbg_console_tx_end();
|
||
CHECK(s_con_tx_owner == 0,
|
||
"TC-CONLOG-173c ownership is really returned (a leaked token would push every later line into the ring forever)");
|
||
writer_drain();
|
||
CHECK(tx_is(0U, "L1\r\n") && (used_now() == 0U) && (dbg_console_dropped() == 0U),
|
||
"TC-CONLOG-173d the deferred round goes out complete as soon as the wire is free");
|
||
|
||
/* 174 同任务嵌套(写者一轮里再裸发留痕行)不得自我丢弃 */
|
||
reset_all();
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
(void)dbg_console_tx_begin();
|
||
dbg_console_write_raw("SEGA", 4U);
|
||
dbg_console_write_raw("SEGB", 4U);
|
||
dbg_console_tx_end();
|
||
CHECK((con_uart_tx_n == 8U) && tx_is(0U, "SEGASEGB") && (dbg_console_dropped() == 0U)
|
||
&& (used_now() == 0U),
|
||
"TC-CONLOG-174 nested raw writes by the owner go out at once, in order, with zero drops");
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
dbg_console_write_raw("NEXT", 4U);
|
||
CHECK((con_uart_tx_n == 12U) && tx_is(8U, "NEXT") && (used_now() == 0U),
|
||
"TC-CONLOG-174b after tx_end the next task owns the wire again (no permanent ring detour)");
|
||
|
||
/* 175 异常上下文:放行,不等、不计丢弃(崩溃遗言必须能出去) */
|
||
reset_all();
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
(void)dbg_console_tx_begin();
|
||
con_ipsr = 1U;
|
||
dbg_console_write_raw("[ERR] HARD", 10U);
|
||
CHECK((con_uart_tx_n == 10U) && tx_is(0U, "[ERR] HARD"),
|
||
"TC-CONLOG-175 an exception context sends at once even while a task owns the wire (crash logs are the last words)");
|
||
CHECK((dbg_console_dropped() == 0U) && (used_now() == 0U)
|
||
&& (s_con_tx_owner == (TaskHandle_t)&s_task_other),
|
||
"TC-CONLOG-175b the bypass neither counts a drop nor touches the owner token (that would free a line in flight)");
|
||
con_ipsr = 0U;
|
||
|
||
/* 176 调度器未启动:单执行流,不取凭证、不开临界区、一条不丢 */
|
||
reset_all();
|
||
con_task_self = 0;
|
||
(void)dbg_console_tx_begin();
|
||
dbg_console_write_raw("EARLY", 5U);
|
||
CHECK((con_uart_tx_n == 5U) && tx_is(0U, "EARLY") && (dbg_console_dropped() == 0U)
|
||
&& (s_con_tx_owner == 0) && (con_crit_enter == 0U),
|
||
"TC-CONLOG-176 before the scheduler starts, the single execution stream sends directly, taking no token and no critical section");
|
||
|
||
/* 177 硬红线:整行独占绝不能把 @115200 约 7.8 ms 的发送包进关中断区 */
|
||
reset_all();
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
dbg_console_writer_attach();
|
||
(void)dbg_console_write_line(s_prot_line, (unsigned)strlen(s_prot_line));
|
||
writer_drain();
|
||
CHECK((con_uart_tx_n == (uint32_t)strlen(s_prot_line)) && (con_uart_crit_at_tx_max == 0U)
|
||
&& tx_is(0U, s_prot_line),
|
||
"TC-CONLOG-177 every byte reaches the wire with interrupts enabled (ownership never wraps the transmit)");
|
||
|
||
/* 178 台架原文形态复演:那条被插进 [20 的 PROT 行,现在要么完整、要么整条让路 */
|
||
/* 逐字节照抄台架那一行的原文(含时间戳前缀与 CRLF),长度交给 strlen 现算:
|
||
* 手数字节数自己就踩过一次坑(把 93 数成 39,用例先红的是我的算术) */
|
||
reset_all();
|
||
con_uart_tx_hook_at = (uint32_t)strlen(s_prot_line) - 30U;
|
||
con_uart_tx_hook = s_tx_second_writer;
|
||
dbg_console_write_raw(s_prot_line, (unsigned)strlen(s_prot_line));
|
||
CHECK((con_uart_tx_n == (uint32_t)strlen(s_prot_line)) && tx_is(0U, s_prot_line)
|
||
&& (used_now() == 6U) && (dbg_console_dropped() == 0U),
|
||
"TC-CONLOG-178 the exact bench shape (that PROT line, interrupted mid-transmit) comes out unmangled: the [20 insertion is structurally gone");
|
||
|
||
/* 179 begin 失败不吞字、不越权归还 */
|
||
reset_all();
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
(void)dbg_console_tx_begin();
|
||
con_task_self = (TaskHandle_t)&s_task_writer;
|
||
CHECK(dbg_console_tx_begin() == 0U,
|
||
"TC-CONLOG-179 tx_begin reports failure instead of blocking when another task owns the wire");
|
||
dbg_console_write_raw("FREE", 4U);
|
||
CHECK((con_uart_tx_n == 0U) && (used_now() == 4U) && (dbg_console_dropped() == 0U),
|
||
"TC-CONLOG-179b a failed begin still delivers the caller bytes (deferred, never swallowed)");
|
||
dbg_console_tx_end();
|
||
CHECK(s_con_tx_owner == (TaskHandle_t)&s_task_other,
|
||
"TC-CONLOG-179c a non-owner tx_end cannot release someone else ownership (that would re-open this very hole)");
|
||
con_task_self = (TaskHandle_t)&s_task_other;
|
||
dbg_console_tx_end();
|
||
CHECK(s_con_tx_owner == 0, "TC-CONLOG-179d the real owner releases normally");
|
||
}
|
||
|
||
/* ===================== main ===================== */
|
||
int main(void)
|
||
{
|
||
tc_params();
|
||
tc_unattached();
|
||
tc_ring();
|
||
tc_writer_self();
|
||
tc_full_drop();
|
||
tc_tx_fault();
|
||
tc_tx_cool();
|
||
tc_tx_half();
|
||
tc_dwt();
|
||
tc_null_uart();
|
||
tc_fputc();
|
||
tc_budget();
|
||
tc_counted_paths();
|
||
tc_three_ledgers();
|
||
tc_check_then_act();
|
||
tc_ownership();
|
||
|
||
printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n",
|
||
s_passed, s_total, s_failed);
|
||
fflush(stdout);
|
||
return (s_failed == 0) ? 0 : 1;
|
||
}
|