Files
STM32F4-Base/Drivers/BSP/app_assert.h
T
wandering1 56893709fe feat(S12): 模块级轻量断言宏 APP_ASSERT + net_socket 状态机不变量试点 2 处
规格:docs/任务规划/测试与架构专项.md §2.1(模块级轻量断言宏,必须零生产开销)。
本提交只做道C-1:新增断言宏 + 2 处试点,不开 net 层大口子、不新增 test/host 目录。

改动
- 新增 Drivers/BSP/app_assert.h:APP_ASSERT 两级裁剪。
  · 关闭态(默认,产品构建):编译期消失、零符号零代码;条件仍进 sizeof 的
    不求值上下文,写错变量名/比较类型会当场报错,不会出现"裁掉=从没被编译器看过"。
  · 打开态:失败交 configASSERT 用的同一个钩子 vApplicationAssertFailed()
    (裸控制台打 [ERR] ASSERT FAILED file:line 后软复位);宿主端(模拟器 /
    主机单测)落 stderr + abort(),非零退出码不会被一键回归静默吞掉。
  - 开关 APP_ASSERT_ENABLE 默认注释,策略与 ENABLE_DESTRUCTIVE_TESTS 同级。
  - 纯宏、无函数、无数据,不新增 .c → 不涉及双端注册(.h 靠 include 路径找到)。
- Drivers/BSP/NET/net_socket.c:2 处试点,都落在"函数头写了限定条件、函数体
  一项都没检查"的缺口上。
  · net_close_locked:控制块必须落在 s_net_socks 数组内且 in_use——此前给出
    越界或已释放指针会一路走到 memset(sizeof(net_sock_t)),直接踩脏 fd 表。
  · net_drop_conn_locked:进入时必须 in_use + TCP 流 + 会话处于 ESTABLISHED
    或 CONNECTING——函数体无条件置 CLOSED 并登记重建,将来绕过
    NET_MSG_DROP_CONN 分派门的新调用路径会拆掉一个健康监听器(症状与 N-30
    一族同源难查),断言把这条状态机不变量钉成机器可查。
  两处均 netTask 上下文、非中断、非高频;条件纯读无副作用;既有错误返回分支
  一条都没改(断言不替代错误码,见 app_assert.h 第四节纪律)。
- test/host/net_socket/run_unit_test.py:include_dirs 补 Drivers/BSP 一行,使该
  套单测编 net_socket.c 时用真 app_assert.h,不造桩——假件与真头漂移正是本
  runner「常量闸」要防的那类自洽假绿;stub 目录仍在最前,同名头命中不受影响。
- Drivers/BSP/README.md:根级文件表补 app_assert.h 一行。

两处有意偏离示意稿(理由已写进头注释)
1) 断言点不再额外发一条 DBG_ERROR:钩子本身已打印 file:line,而每条 DBG_*
   调用点约 90 B Flash + 216 B 栈(AGENTS「日志不是免费的」);断言点又常在
   「内核状态已不可信」的现场,走裸控制台比走日志行缓冲稳。
2) 头落 Drivers/BSP(与 dbg_log.h、bsp_err.h 同级)而非示意稿的 App/util:它是
   纯宏叶子能力,落 App/util 会让 BSP 新增一条反向 include App 层头的边,而
   layer-deps 存量基线只减不增、加不进去;落 Drivers/BSP 已在 MCU 与模拟器双端
   include 路径内 → 双端零工程文件改动(uvprojx / CMakeLists 一字未动)。

验证(工作树 _wt/S12-assert,分支 task/S12-assert)
- @build 产品态:BUILD_OK,0 错误 0 警告。
- 体积与 RAM:Code=213780 RO=18860 RW=1204 ZI=131852,与 scripts/ram_baseline.tsv
  的 2026-10-24 基线行完全相同 → Flash/RAM 增量 0 B;.map 中 app_assert 相关
  符号 0 条(规格完成判据"无新增符号")。gate 全绿,无需 mem-budget 例外。
- 打开态实测(临时取消注释编译,跑完已还原成注释态):0 错 0 警,
  Code=213916(2 处断言共 +136 B ≈ 68 B/处,无每调用点栈代价),RW/ZI 不变。
- 宿主 MSVC 三态实测:关闭态断言不求值但条件仍做类型检查(错误标识符报
  C2065);打开态真条件不触发、假条件 stderr 打 [ASSERT] FAILED 后 abort;
  net_socket 主机单测 452/452 通过(用真头,开关默认关)。
- 门禁实跑:cppcheck(warning/perf/portability/style)、no-mojibake、coding-standard、
  layer-deps、bsp-err、id-check、doc-links、registration-check、mem-budget 全 0。
- 台架 @test 未跑:bench 锁归 Lead;产品态本改动零行为差异,无需烧录回归。

遗留
- 打开态未在台架上真跑一轮(需 bench 锁 + 一个故意造假的注入点):待台架实测。
- 文档销项(测试与架构专项.md §2.1、并行分道方案.md 对应行、AGENTS 任务表)
  归主工作树统一处理,本分支未碰 docs/任务规划/**。
2026-10-25 03:04:07 +08:00

130 lines
7.1 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.
/*
* 模块名称:模块级轻量断言宏(APP_ASSERT)
* 模块功能:给自研代码里"不该发生"的**内部不变量**一个统一出口——编译期整体
* 裁剪,生产构建零符号零开销;打开后与 configASSERT 共用同一个失败钩子。
* 规格出处:docs/任务规划/测试与架构专项.md §2.1。该节的代码块自称"示意,非最终
* 实现",本文件即落地实现;与示意稿的两处差异见下方"落地差异"。
* 落地差异(相对 §2.1 示意稿,均为有意):
* 1. 断言点**不再额外发一条 DBG_ERROR**:钩子本身就打印 file:line(断言唯一有
* 价值的现场信息),而每条 DBG_* 调用点约 90 B Flash + 216 B 栈(AGENTS
* 「日志不是免费的」);断言点又常处在"内核状态已不可信"的现场,走裸控制台
* 比走日志行缓冲更稳。
* 2. 本头落在 Drivers/BSP 根目录(与 dbg_log.h、bsp_err.h 同级),不是示意稿写的
* App/util:它是纯宏叶子能力(无状态、无数据、不依赖任何上层服务),BSP 与
* App 两侧都要用。放 App/util 会让 BSP 层新增一条"反向 include App 层头"的边,
* 而 layer-deps 门禁的存量基线只减不增,加不进去。落在 Drivers/BSP 已在 MCU
* (uvprojx IncludePath)与模拟器(simulator/CMakeLists.txt 的
* include_directories)双端 include 路径内 → **双端零工程文件改动**;主机侧
* 单测也因此直接用真头、不再造桩——假件与真头漂移正是"常量闸"要防的那类
* 自洽假绿(见 test/host/net_socket/run_unit_test.py 的注释)。
* ⚠ 本注释块内禁止出现"斜杠紧跟星号"的连写(Markdown 粗体收尾在路径末尾时最容易
* 撞上,例如目录通配那种写法):ARMCC 报 #9-D nested comment is not allowed,
* 而 @build 的验收口径是 0 错误 **0 警告**(2026-10-25 首编即中招,已改)。
* 适用平台:STM32F407ZGTx(ARMCC v5,C99);PC 模拟器与主机侧单测(MSVC)同标准
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* 2026-10-25 王建锋 创建(S12;试点 = Drivers/BSP/NET/net_socket.c 状态机不变量 2 处)
*/
#ifndef APP_ASSERT_H
#define APP_ASSERT_H
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================================
* 一、编译期裁剪分级:两态(规格只给"生产关 / 调试开",不设中间档)
* ==========================================================================*/
/*
* 开关:**默认关闭 = 产品构建**。
* 打开方式 = 取消下一行注释。策略与 test/board/test_config.h 的
* ENABLE_DESTRUCTIVE_TESTS 同级:默认必须关闭,严禁以打开态提交。
* ⚠ 关闭态是 @build 的验收口径:"0 错误 0 警告" 且 .map 无新增符号
* (规格 §2.1 完成判据)。本宏不新增任何 .c,故不涉及双端注册。
*/
//#define APP_ASSERT_ENABLE
/* ============================================================================
* 二、打开态:失败交"与 configASSERT 同一个出口"
* ==========================================================================*/
#ifdef APP_ASSERT_ENABLE
#if defined(__arm__) || defined(__CC_ARM)
/*
* MCU 端:Inc/FreeRTOSConfig.h 的 configASSERT 用的就是这个钩子——实现见
* Src/freertos.c 的 USER CODE Application 区(裸控制台打
* "[ERR] ASSERT FAILED <file>:<line>" 后 NVIC_SystemReset())。
* 应用层断言复用同一钩子 → 台架上"断言是怎么死的"只有一种形态,不必再造
* 第二条复位路径。这里**不** include FreeRTOSConfig.h:本头必须能被看不见
* RTOS 的编译单元包含(模拟器 / 主机侧单测就是这种)。声明与
* FreeRTOSConfig.h 原文同签名——重复声明合法,真漂移了由链接器兜底。
*/
extern void vApplicationAssertFailed(const char *file, long line);
#define APP_ASSERT_FAIL(p_file, line) vApplicationAssertFailed((p_file), (long)(line))
#else
/*
* 宿主端(PC 模拟器 / 主机侧单测):没有 RTOS、也没有可复位的外设看门狗,
* 正确形态是"现场写 stderr 然后 abort()"——非零退出码,任何一键回归都会
* 判失败,不会被"静默继续"把断言吞成一条日志。
*/
#include <stdio.h>
#include <stdlib.h>
#define APP_ASSERT_FAIL(p_file, line) do { \
(void)fprintf(stderr, "[ASSERT] FAILED %s:%d\r\n", (p_file), (int)(line)); \
(void)fflush(stderr); \
abort(); \
} while (0)
#endif /* __arm__ || __CC_ARM */
/*
* 断言宏(打开态):条件为假 → 交 APP_ASSERT_FAIL 处置。
* 包成 do { } while (0) 以适配"单条语句"位置;条件只求值一次。
*/
#define APP_ASSERT(cond) do { \
if (!(cond)) { \
APP_ASSERT_FAIL(__FILE__, __LINE__); \
} \
} while (0)
/* ============================================================================
* 三、关闭态(默认):代码消失,但表达式的类型检查保留
* ==========================================================================*/
#else /* !APP_ASSERT_ENABLE */
/*
* 与示意稿的 ((void)0) 同样"零代码零符号",但用 sizeof 把条件留在编译期的
* 类型检查里:sizeof 的操作数是**不求值上下文**(不生成代码、不产生副作用),
* 于是断言写错变量名 / 写错比较类型时,生产构建也会当场报错——否则"生产裁掉
* 断言"等于"这条表达式从没被编译器看过",要等到哪天有人打开开关才炸。
* 附带好处:只在断言里出现的变量不会被 ARMCC 判成"set but never used"。
*/
#define APP_ASSERT(cond) do { (void)sizeof(!!(cond)); } while (0)
#endif /* APP_ASSERT_ENABLE */
/* ============================================================================
* 四、使用纪律(规格 §2.1 的三条 + 落地补两条;错用是常态,故随头常驻)
* ==========================================================================*/
/*
* 1. **不得**在中断上下文或高频路径使用——打开态每次失败都要打印并复位;
* 本宏服务"逻辑上不可能",不是"偶尔会失败"。
* 2. **不得**替代正常错误返回:外部输入、对端数据、芯片返回值一律走错误码
* (约定见 Drivers/BSP/bsp_err.h 与 net_* 的返回契约)。断言只写内部不变量
* ——即"为假就说明我们自己写错了"的状态机 / 指针来源 / 字段配对关系。
* 3. 生产构建关闭(与 ENABLE_DESTRUCTIVE_TESTS 同级):要开就在本地或测试构建
* 里打开,**跑完必须还原成注释态**。
* 4. 断言条件**不得带副作用**:关闭态不求值、打开态才求值,带副作用等于让行为
* 随开关漂移——这是本宏唯一真正危险的误用形态。
* 5. 台架口径:MCU 钩子打印以 [ERR] 开头 → 真机上断言一旦触发,@test 必然判本次
* 失败,这是刻意的(AGENTS「Forbidden 标记含 [ERR]」/ 出现 Fault 即不通过)。
*/
#ifdef __cplusplus
}
#endif
#endif /* APP_ASSERT_H */