Files
STM32F4-Base/test/host/console_log/stub/stm32f4_fake.h
T
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。

—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。

—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。

—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。

—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。

—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。

产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

375 lines
20 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.
/*
* 模块名称:主机侧单测桩 - 假 HAL/CMSIS 寄存器面 + 内核入口(console_log 模块专用)
* 模块功能:让 App/log/console_log.c 在 PC 上编起来,并把它依赖的硬件/内核面做成
* **可脚本控制**的模型:
* ① USART1:TXE/TC 可注入「就绪 / 永远不就绪(僵尸通道)/ 前 N 次不就绪」;
* 写进 DR 的字节被收进一条上线字节日志,并按「一次 TC 就绪 = 一块」记块长。
* ② DWT CYCCNT:每次访问推进 con_dwt_step 个周期(= 给时间定价),
* CTRL 的 CYCCNTENA 可脚本成「写不进去」以走 con_tx_counted 支路。
* ③ HAL_GetTick:显式量 + 每次读自动步进。
* ④ 内核入口:临界区计数、当前任务句柄、vTaskDelay(可挂回调 + 跑飞保险)、
* __get_IPSR、osEventFlagsSet、NVIC/RCC 宏。
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建(MCU 走真实 stm32f4xx*.h)。
*
* 为什么必须桩(换机复核依据):真实链是 usart.h -> main.h -> stm32f4xx_hal.h ->
* stm32f4xx.h -> core_cm4.h,里面是 ARMCC 内联汇编与 __IO 寄存器绝对地址
* (0xE0001000 一类),宿主 MSVC 既编不动也访问不了。本桩把同样的位语义搬到普通
* 结构体上,字段名 / 位号 / 枚举值一律照真实头抄,生产代码写法一字不改即可编译:
* USART_TypeDef.SR/DR、USART_SR_TXE=bit7、USART_SR_TC=bit6(stm32f4xx_hal_uart.h)
* DWT_Type.CTRL/CYCCNT + DWT_CTRL_CYCCNTENA_Msk=bit0、CoreDebug_Type.DEMCR +
* CoreDebug_DEMCR_TRCENA_Msk=bit24(core_cm4.h)
* HAL_StatusTypeDef{OK=0,ERROR=1,BUSY=2,TIMEOUT=3}(stm32f4xx_hal_def.h)
* HAL_UART_STATE_RESET=0x00、HAL_UART_ERROR_NONE=0x00、USART1_IRQn=7
* UART_HandleTypeDef 只搬本模块访问的 4 个字段(Instance/gState/RxState/ErrorCode),
* 余下(Init/Lock/...)刻意不搬:抄半份 HAL 结构只会更快与真实头漂移。
*
* ★ 两个手法(本桩唯一的技巧点,改之前先读懂):
* a) 「读寄存器位」没法用结构体成员钩(u->SR 是成员访问,宏替换不了成员名),但生产
* 代码的判据写作 (u->SR & USART_SR_TXE),而 **USART_SR_TXE 是本桩的宏** —— 把它
* 定义成函数调用,于是每次读该位都进 con_uart_read_txe():在这里计数、判跑飞、按
* 脚本决定就绪与否。SR 成员恒为全 1,就绪完全由钩子返回值决定(返回掩码=就绪,
* 返回 0=不就绪,(全1 & 0)==0 让生产代码继续等)。TC 同理。
* b) DWT 同理:#define DWT (con_dwt_touch(), &g_con_dwt) —— 每次 DWT-> 访问先走一遍
* touch()(CYCCNT += step、计数判跑飞、按需让 CYCCNTENA 位落不住),再把地址给出去。
* 真实硬件 CYCCNT 每周期 +1,这里「每次访问 +step」是同一个模型、只是步长交给测试。
* 于是 CON_TX_TIMEOUT_CYC = SystemCoreClock/10 这类编译期表达式**不必改生产代码**
* 就能在 1 次自旋内成立(step 取 = 上界值即可)。
* c) DR 的写不做钩子(写就是写进真成员)。「哪个字节真正上线了」由 a) 的就绪事件推定:
* 生产代码每写一个 DR 之前必然刚读到一次就绪的 TXE(两个发送循环都是这个结构),
* 所以「上次就绪之后写进 DR 的那个字节」= 此刻离开发送寄存器上线的字节 ——
* 与真实「TXE 空 -> 装载 -> 移位」时序一致,不是桩自己发明的规则。
* 由此还能照出超时路径的真实后果:TXE 卡死时最后那个字节**没有**被写进 DR,
* 也就不会出现在上线日志里(半个块丢失、丢在哪个字节可查)。
*
* 创建日期:2026-10-24
*/
#ifndef STM32F4_FAKE_H
#define STM32F4_FAKE_H
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
/* ===== 与真实头同值的位 / 枚举(来源见文件头) ===== */
#define USART_SR_TXE_Msk (0x0080U) /* USART1->SR bit7 发送寄存器空 */
#define USART_SR_TC_Msk (0x0040U) /* USART1->SR bit6 发送完成 */
#define DWT_CTRL_CYCCNTENA_Msk (0x00000001U)
#define CoreDebug_DEMCR_TRCENA_Msk (0x01000000U)
#define USART1_IRQn 7
#define HAL_UART_STATE_RESET 0x00U
#define HAL_UART_STATE_READY 0x20U
#define HAL_UART_ERROR_NONE 0x00U
typedef enum {
HAL_OK = 0x00U,
HAL_ERROR = 0x01U,
HAL_BUSY = 0x02U,
HAL_TIMEOUT = 0x03U
} HAL_StatusTypeDef;
/* 主机没有真周期钟。CON_TX_TIMEOUT_CYC = SystemCoreClock/10 与
* CON_APPEND_WAIT_CYC = SystemCoreClock/5 都是编译期表达式,取值原样保留
* 168 MHz(与 MCU 端 SystemCoreClock 一致),时间快慢由 con_dwt_step 决定。 */
extern uint32_t SystemCoreClock;
/* ------------------------------------------------------------------ */
/* ① 假 USART1 */
/* ------------------------------------------------------------------ */
typedef struct {
volatile uint32_t SR;
volatile uint32_t DR;
} USART_TypeDef;
typedef struct {
USART_TypeDef *Instance;
volatile uint32_t gState;
volatile uint32_t RxState;
volatile uint32_t ErrorCode;
} UART_HandleTypeDef;
extern USART_TypeDef g_con_uart; /* 寄存器实体(单 TU,非 static) */
#define CON_UART_TX_MAX 20000U /* 上线字节日志容量 */
#define CON_UART_BLOCK_MAX 64U /* 块长记录容量 */
extern uint32_t con_uart_tx_n; /* 已上线字节数 */
extern uint8_t con_uart_tx[CON_UART_TX_MAX];
extern uint32_t con_uart_block_n; /* 已完成的块数 */
extern uint32_t con_uart_block_len[CON_UART_BLOCK_MAX];
extern uint32_t con_uart_cur_len; /* 当前块已上线字节数 */
extern uint32_t con_uart_txe_reads;
extern uint32_t con_uart_tc_reads;
extern int con_uart_txe_stuck; /* 1 = TXE 永不就绪(僵尸通道) */
extern int con_uart_txe_after; /* 前 N 次 TXE 读**不**就绪,之后就绪 */
extern int con_uart_txe_ready_left; /* >=0 = 只准就绪 N 次,之后卡死 */
extern int con_uart_tc_after; /* 前 N 次 TC 读不就绪,之后就绪 */
extern int con_uart_tc_stuck; /* 1 = TC 永不就绪 */
extern int con_uart_pending; /* 1 = 有个字节已进 DR 待收 */
/* 前置声明:con_uart_tx_put 要看当前临界区深度(定义在本文件下方的④内核入口段,
* 这里必须先声明,否则 C89 隐式声明在 ARMCC/MSVC 上都是错误或告警) */
uint32_t con_crit_depth(void);
uint32_t con_pm_depth(void); /* PRIMASK 版关中断窗口深度(前置声明理由同上) */
/* —— 道G task-23 新增的两个把手(判据说明见 con_uart_tx_put 内注释)—— */
extern uint32_t con_uart_tx_hook_at; /* 第 N 个字节上线后触发一次(0=不触发) */
extern void (*con_uart_tx_hook)(void); /* 触发时跑的函数(测试装填) */
extern uint32_t con_uart_crit_at_tx_max; /* 所有上线字节里见过的最大临界区深度 */
extern uint32_t con_pm_at_tx_max; /* 同上,但量的是 PRIMASK 关中断窗口深度
* (道G task-26 之后裸发路径用它,这里先
* 声明:con_uart_tx_put 在本文件上方) */
void con_uart_reset(void);
/*
* 上线一个字节 = 记进字节日志,并在此处给测试两个观察/介入点(道G task-23):
* ① con_uart_crit_at_tx_max:本字节上线时的临界区深度水位。整行所有权只能靠一次
* 几条指令的 test-and-set 取得,**绝不允许把 7.8 ms 的发送包进关中断区**(那等于
* 把中断延迟与最坏喂狗抖动塞进启动窗口,比交错本身更糟)——本水位恒 0 就是这条
* 硬红线的机器证据(判据见 console_log_test.c 的 M 族 TC-CONLOG-170..179)。
* ② con_uart_tx_hook/_at:在"第 N 个字节上线之后"跑一遍测试装填的函数,用来精确
* 模拟**发送任务在两个字节之间被抢占**。行级所有权是"拿不到就当场让路"的非阻塞
* 设计,所以既有 con_delay_hook(vTaskDelay 时才跑)在这条路径上永远不会触发,
* 必须另有塞入点,否则"两个生产者并发时不得出现另一条行的字节"这条判据没法做
* 成必然失败的断言(只能靠静态论证,那不算交付)。
* 钩子一次性:进入即摘钩,防递归;钩子里再发的字节照常记账。
*/
static void con_uart_tx_put(uint8_t b) {
uint32_t depth;
if (con_uart_tx_n + 1U >= CON_UART_TX_MAX) {
printf("[FATAL] fake uart: wire log overflow (>=%u bytes)\n", (unsigned)CON_UART_TX_MAX);
exit(2);
}
con_uart_tx[con_uart_tx_n++] = b;
/* NUL 结尾(tx 是二进制缓冲,但让 CHECK 的 %s 打印能读) */
con_uart_tx[con_uart_tx_n < CON_UART_TX_MAX ? con_uart_tx_n : (CON_UART_TX_MAX - 1U)] = 0U;
con_uart_cur_len++;
depth = con_crit_depth();
if (depth > con_uart_crit_at_tx_max) {
con_uart_crit_at_tx_max = depth;
}
/* 换用 PRIMASK 窗口之后,"发送不许被关中断包住"这条红线由这个水位证明:
* 任一上线字节看到的屏蔽深度必须恒 0。内核临界区水位 + PRIMASK 水位**两个都
* 为 0**,才叫"整行独占没把 @115200 约 7.8 ms 的发送关进中断"。 */
if (con_pm_depth() > con_pm_at_tx_max) {
con_pm_at_tx_max = con_pm_depth();
}
if ((con_uart_tx_hook != 0) && (con_uart_tx_hook_at != 0U)
&& (con_uart_tx_n == con_uart_tx_hook_at)) {
void (*h)(void) = con_uart_tx_hook;
con_uart_tx_hook_at = 0U; /* 一次性:先摘钩再跑,钩子内部的发送不再触发 */
h();
}
}
/* 收走「上次就绪之后写进 DR 的字节」:它此刻才真正上线 */
static void con_uart_take_byte(void) {
if (con_uart_pending == 0) {
return;
}
con_uart_pending = 0;
con_uart_tx_put((uint8_t)(g_con_uart.DR & 0xFFU));
}
static uint32_t con_uart_read_txe(void) {
con_uart_take_byte();
con_uart_txe_reads++;
if (con_uart_txe_reads > 200000U) {
printf("[FATAL] fake USART: TXE read %lu times (runaway)\n",
(unsigned long)con_uart_txe_reads);
exit(2);
}
if ((con_uart_txe_stuck != 0) || (con_uart_txe_after > 0)) {
if (con_uart_txe_after > 0) {
con_uart_txe_after--;
}
return 0U;
}
if (con_uart_txe_ready_left >= 0) {
if (con_uart_txe_ready_left == 0) {
return 0U; /* 配额用尽 -> 从这一字节起通道卡死 */
}
con_uart_txe_ready_left--;
}
con_uart_pending = 1;
return USART_SR_TXE_Msk;
}
static uint32_t con_uart_read_tc(void) {
con_uart_take_byte();
con_uart_tc_reads++;
if (con_uart_tc_reads > 200000U) {
printf("[FATAL] fake USART: TC read %lu times (runaway)\n",
(unsigned long)con_uart_tc_reads);
exit(2);
}
if (con_uart_tc_stuck != 0) {
return 0U;
}
if (con_uart_tc_after > 0) { /* TC 抖动:等几轮才置位 */
con_uart_tc_after--;
return 0U;
}
if (con_uart_block_n >= CON_UART_BLOCK_MAX) {
printf("[FATAL] fake USART: block log overflow (>=%u)\n", CON_UART_BLOCK_MAX);
exit(2);
}
con_uart_block_len[con_uart_block_n++] = con_uart_cur_len;
con_uart_cur_len = 0U;
return USART_SR_TC_Msk;
}
/* ★手法 a):判据里的掩码宏定义成函数调用 -> 每次读该位都进钩子 */
#define USART_SR_TXE (con_uart_read_txe())
#define USART_SR_TC (con_uart_read_tc())
/* ------------------------------------------------------------------ */
/* ② 假 DWT / CoreDebug */
/* ------------------------------------------------------------------ */
typedef struct {
volatile uint32_t CTRL;
volatile uint32_t CYCCNT;
} DWT_Type;
typedef struct {
volatile uint32_t DEMCR;
} CoreDebug_Type;
extern DWT_Type g_con_dwt;
extern CoreDebug_Type g_con_core_debug;
extern uint32_t con_dwt_step; /* 每次 DWT 访问推进的周期数 */
extern int con_dwt_ctrl_sticky; /* 0 = CYCCNTENA 写不进去 */
extern uint32_t con_dwt_reads;
void con_dwt_reset(void);
/* ★手法 b):每次访问先「花时间」,再决定是否让使能位落得住 */
static void con_dwt_touch(void) {
con_dwt_reads++;
if (con_dwt_reads > 200000U) {
printf("[FATAL] fake DWT: accessed %lu times (runaway)\n",
(unsigned long)con_dwt_reads);
exit(2);
}
g_con_dwt.CYCCNT += con_dwt_step;
if (con_dwt_ctrl_sticky == 0) {
g_con_dwt.CTRL &= ~DWT_CTRL_CYCCNTENA_Msk;
}
}
#define DWT (con_dwt_touch(), &g_con_dwt)
#define CoreDebug (&g_con_core_debug)
/* ------------------------------------------------------------------ */
/* ③ HAL 时基(真实原型 uint32_t HAL_GetTick(void),stm32f4xx_hal.c) */
/* ------------------------------------------------------------------ */
extern uint32_t con_tick_now;
extern uint32_t con_tick_step; /* 每次读之后自动推进的 ms(0 = 不动) */
extern uint32_t con_tick_reads;
uint32_t HAL_GetTick(void);
/* ------------------------------------------------------------------ */
/* ④ 内核入口 */
/* ------------------------------------------------------------------ */
typedef void *TaskHandle_t;
typedef uint32_t TickType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
extern TaskHandle_t con_task_self; /* xTaskGetCurrentTaskHandle 的返回值 */
TaskHandle_t xTaskGetCurrentTaskHandle(void);
extern uint32_t con_delay_calls; /* vTaskDelay 次数 = 真的等过 */
extern void (*con_delay_hook)(void); /* 每次 delay 跑一遍(可模拟别的任务排空) */
void vTaskDelay(TickType_t ticks);
extern uint32_t con_crit_enter;
extern uint32_t con_crit_exit;
void con_crit_push(void);
void con_crit_pop(void);
uint32_t con_crit_depth(void);
/* 进区回调:环空位竞态那条正向用例靠它把"第二个生产者"精确塞进第一个生产者的临界区里
* (旧写法里判据在区外,塞进去的写入不会被复核到;新写法复核在区内,必然看到)。
* 只在 con_crit_push 记完深度之后调用,不改任何深度值。 */
extern void (*con_crit_hook)(void);
#define taskENTER_CRITICAL() con_crit_push()
#define taskEXIT_CRITICAL() con_crit_pop()
/* —— PRIMASK 版关中断窗口(生产侧 CON_CRIT_ENTER/EXIT 展开后落到这三个原语)——
* "关没关中断"不许靠人嘴说、也不许靠我保证,这三个把手让用例能机器断言:
* ① 取环写环 / 清属主确实发生在关中断窗口内(环探针 + 深度水位);
* ② 整条裸发路径 0 次内核临界区调用(con_crit_enter == 0)——这条同时是
* "taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ 断言钩子 ⇒ 回本模块"
* 那条环**不成立**的证据(那条环在静态调用图上会被算进每一条含临界区的任务链,
* 实测三条链各 +80 B,见 console_log.c 的 CON_CRIT_ENTER 注释);
* ③ 每个上线字节看到的屏蔽深度恒 0(发送绝不关中断)。
* 深度按"成对 push/pop"建模:生产侧保存的是**局部** PRIMASK 副本并成对写回,
* 所以嵌套时深度自然回升,注入钩子看到的也是"已在区内"。 */
extern uint32_t con_pm_enter; /* __disable_irq 次数 */
extern uint32_t con_pm_exit; /* __set_PRIMASK 次数 */
extern uint32_t con_pm_max_depth; /* 曾达到的最大嵌套深度 */
extern uint32_t con_pm_at_tx_max; /* 任一上线字节所见屏蔽深度水位(必须恒 0) */
extern void (*con_pm_hook)(void); /* 进窗回调:把"第二个生产者"塞进关中断窗口里 */
void con_pm_push(void);
void con_pm_pop(void);
#define __get_PRIMASK() ((uint32_t)(con_pm_depth() != 0U ? 1U : 0U))
#define __disable_irq() con_pm_push()
#define __set_PRIMASK(v) con_pm_pop()
/* 环形缓冲实体改由测试持有:生产代码里的 s_con_ring 被宏名成 (*con_ring_cell()),
* 于是**每一次对环的取址访问都进这个钩子** —— 这是本轮唯一能把"整行拷贝发生在
* 临界区内、而读环发送发生在临界区外"这件事直接断言出来的办法(生产侧的空位判据
* 与拷贝之间没有任何函数调用可插桩,靠调用深度而不是靠时序观察来钉结构性事实)。
* 钩子只记事实不改行为:返回的就是那块 4096B 存储。 */
/* 注意:**不要**在这里定义 DBG_CON_RING_SIZE(那是生产 console_log.c 的宏,
* 重复定义会撞 C4005);环形存储用字面量 4096U,与生产的数组维度一致即兼容。 */
extern uint8_t con_ring_store[4096U];
extern int con_ring_probe_on; /* 只在被测那一次调用期间开启 */
extern uint32_t con_ring_probe_hits; /* 探针开启期间被索引取址的次数(证明真碰了环) */
extern uint32_t con_ring_zero_depth_hits;/* 其中发生在临界区之外(深度 0)的次数 */
/* 写者读环发本来就在临界区外(正确行为),探针不该把它算成违规:由测试在进入
* drain 的前后翻转这个标志(等价于把生产侧的 s_con_draining 递进来) */
extern int con_ring_draining;
/* 用老式无原型定义,与生产那行 static 声明展开后的形态一致(避免 C4255/C4273) */
static uint8_t (*con_ring_cell())[4096U] {
if (con_ring_probe_on != 0) {
con_ring_probe_hits++;
/* 两种关中断窗口都算"在区内":内核临界区(BASEPRI;写者 drain 推 tail 那一处
* 仍是它)与本模块自己的 PRIMASK 窗口(入环拷贝 / 取还所有权)。换原语之后
* 判据含义不变:**生产者绕过任何关中断窗口碰环**才算竞态回来了。 */
if ((con_crit_depth() == 0U) && (con_pm_depth() == 0U) && (con_ring_draining == 0)) {
con_ring_zero_depth_hits++; /* 生产者绕过临界区碰环 = 竞态回来了 */
}
}
return &con_ring_store;
}
#define s_con_ring (*con_ring_cell())
/* 真实 __get_IPSR 是 core_cmFunc.h 的静态内联;这里做成宏读一个可控量 */
extern uint32_t con_ipsr; /* 0 = 线程模式;!=0 = 中断/异常上下文 */
#define __get_IPSR() (con_ipsr)
/* 事件组:原型与真实 cmsis_os2.h 逐字一致 */
typedef void *osEventFlagsId_t;
typedef uint32_t osStatus_t;
#define osFlagsWaitAny 0x00000000u
#define osFlagsNoClear 0x00000001u
extern uint32_t con_evt_sets;
extern uint32_t con_evt_flags;
osStatus_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags);
/* NVIC / RCC:真实版是寄存器写。自愈路径的判据 = 重初始化做了几回、次序对不对 */
extern uint32_t con_nvic_clears;
extern uint32_t con_rcc_disables;
extern uint32_t con_rcc_enables;
#define __NVIC_ClearPendingIRQ(irqn) do { (void)(irqn); con_nvic_clears++; } while (0)
#define __HAL_RCC_USART1_CLK_DISABLE() do { con_rcc_disables++; } while (0)
#define __HAL_RCC_USART1_CLK_ENABLE() do { con_rcc_enables++; } while (0)
#endif /* STM32F4_FAKE_H */