本次会话先后销项与关闭: - E-61 SD2506 清 INTAF:原实现只读取 CTR1,而按手册 ARST=0 时读操作 不清标志,属空操作;改为读-改-写 CTR1,并按手册订正 CTR1 位宏 (INTAF/INTDF/BLF 位置,ARST 归 CTR3) - E-16 cfg_file:新增 CFG_FILE_MAX_NAME_LEN(段名/键名 ≤15),注册时 拒绝超长键名;s_gen_document 两处 snprintf 改检查式;补 TC-CFG-024 用例(主机单测 149→151 PASSED) - E-23 TC704:PHASE7_MULTI_COUNT 8→5(并发上限 = 8 − 3 个常驻监听), accept 失败分支加网络代次检查(网络重建即重建监听) - E-26(UDP)按“保留 API + 文档定案”关闭;E-30 评估为不必要后关闭; N-4(ACCEPT 决定不实施)关闭 审计文档剩余 5 项 P2 + 2 项监督,经用户裁定“都不重要”后整体删除: - 删除 docs/问题记录/错误码传播审计.md(634 行) - 九类缺陷模式表迁入 docs/编号规范.md §4.2.1(PAT-01~09) - E-xx 转为纯历史编号族:编号规范 §4.1 属主改“无(原属主已删除)”, §4.3 保持冻结;全仓 26 处引用按“保留编号、去悬空文档名”处理 - 顺带修正 N-4 收口遗留:排期详述 C1 才是 E-25/E-27/E-28 的承载处 (审计 §4 曾误写为登记簿);README 两处 C1 旧名改回“net 层跨上下文收口” 一并提交此前遗留的体系层评审报告 2026-10-14 复核 WIP(六维评分复核、 未闭环 Top 5→3、三区水位实测刷新)。 校验:Keil 构建 0 错误 0 警告(Code=212344);check_doc_links --sections-strict 与 --all(142 文档)通过;内存双台账刷新后均 PASS。
40 KiB
pre-commit 自动化代码审查 — 部署与使用说明
生效前提:本机已运行过
pre-commit install(钩子写入.git/hooks/)。 新机器/新开发者进入仓库后需执行一次pre-commit install,详见「新机器启用」。
1. 部署架构
git commit
└─ pre-commit(.git/hooks/pre-commit)
├─ trailing-whitespace 自动修复行尾空格
├─ end-of-file-fixer 自动修复文件尾换行
├─ check-added-large-files 阻止 >1MB 大文件
├─ check-merge-conflict 阻止含冲突标记的文件
├─ check-yaml 校验 YAML 语法
├─ cppcheck C 静态分析(自有代码)
├─ no-mojibake 阻止 U+FFFD 乱码文件
├─ coding-standard C 代码规范检查(规范V1.0 可机械校验项)
├─ doc-links 文档链接存在性(D-3/D-7,.md)
├─ task-table FreeRTOS 任务表一致性(D-2/D-7)
├─ mem-numbers 内存/Flash 数字一致性(D-7 体积档)
├─ lv-conf-sync 两份 lv_conf.h 特性子集一致性(比对生效值)
├─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
├─ watchdog-unit-test 看门狗判定状态机主机侧单测(L0.5)
├─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交)
├─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23)
└─ mem-budget RAM 预算 92% 阈值门禁(N-27 三件套,2026-10-12)
└─ commit-msg(.git/hooks/commit-msg)
└─ gitlint 校验 commit message 格式
两阶段钩子:pre-commit 阶段拦截代码问题,commit-msg 阶段拦截格式问题。任一失败 → commit 中止。
CI 侧的同一套门禁(2026-10-01 T-6):Gitea Actions(
.gitea/workflows/ci.yml)在 push / PR 时 以pre-commit run --all-files复跑全部上述钩子(mem-numbers在无 Keil.map的 runner 上自适应跳过)。 本地提交前想跑同一套判定:& '.venv\Scripts\python.exe' scripts/ci_local.py。详见../测试说明/测试开发标准流程.md§9。
依赖清单
| 依赖 组件 | 安装方式 | 用途 |
|---|---|---|
| pre-commit 4.6.2 | uv tool install pre-commit |
钩子框架 |
| cppcheck 2.21.0 | winget install cppcheck |
C 静态分析引擎 |
| scripts/check_mojibake.py | 随仓库提交 | U+FFFD 乱码检测(纯 Python,无依赖) |
| scripts/check_coding_standard.py | 随仓库提交 | 代码规范检查(纯 Python,无依赖) |
| scripts/check_doc_links.py | 随仓库提交 | 文档链接存在性检查(纯 Python,无依赖) |
| scripts/gen_task_table.py | 随仓库提交 | FreeRTOS 任务表生成/校验(纯 Python,无依赖) |
| scripts/check_mem_numbers.py | 随仓库提交 | 内存/Flash 数字一致性(纯 Python,无依赖) |
| scripts/check_cubemx_survival.py | 随仓库提交 | CubeMX 重生成存活审计(纯 Python,无依赖) |
| scripts/check_test_suite.py | 随仓库提交 | 测试套件开关门禁(纯 Python,无依赖) |
| scripts/check_registration.py | 随仓库提交 | 双端注册反向校验(纯 Python,无依赖) |
| scripts/ram_baseline.py | 随仓库提交 | RAM 预算 92% 门禁 + 趋势台账(纯 Python,无依赖) |
PATH 要求:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需重开终端);装在非 PATH 位置时可用
DTU_CPPCHECK环境变量指向cppcheck.exe完整路径(显式设置但无效 → 硬报错,不静默回退)。
2. 各钩子规则详解
2.1 trailing-whitespace — 行尾空格清理
| 项 | 说明 |
|---|---|
| 触发条件 | 文件任意行末尾含空格或 Tab |
| 处理方式 | 自动修复(直接改写文件,删除行尾空白) |
| 影响范围 | 项目自有代码;豁免 CubeMX / Keil 生成文件(见下方"豁免清单") |
| 典型报错 | Trim Trailing Whitespace → Fixing path/to/file.c |
| 修复方式 | 无需手动修复,钩子已自动改写;重新 git add 即可 |
注意:首次对全库运行时,可能批量修改大量文件的行尾空格。这是正常行为,不会改变代码逻辑。
豁免清单(2026-10-09 定,
trailing-whitespace与end-of-file-fixer共用同一份, 配置里以 YAML 锚点&cubemx-generated定义):Src/*.{c,h}、Inc/*.{c,h}、Drivers/CMSIS/、Drivers/STM32F4xx_HAL_Driver/、Middlewares/、MDK-ARM/*.{uvprojx,uvoptx,sct,map}、.mxproject、*.ioc。 理由:这些文件的内容事实源是.ioc与工具链,每次重生成都按工具自己的风格写回 (行尾空白、文件末尾不带换行)——2026-10-09 实测每次重生成后Src//Inc/下 10+ 个 文件各被补 1 行末尾空行,纯属提交抖动。 ⚠ 只豁免"改写型"钩子:cppcheck/ 乱码 /cubemx-survival/task-table/mem-numbers等只读检查仍覆盖Src/、Inc/(存活审计必须能看到这些文件)。
2.2 end-of-file-fixer — 文件尾换行
| 项 | 说明 |
|---|---|
| 触发条件 | 文件最后一行不是换行符(\n) |
| 处理方式 | 自动修复(在文件尾部追加换行) |
| 影响范围 | 项目自有代码;豁免 CubeMX / Keil 生成文件(同 2.1 豁免清单) |
| 典型报错 | Fix End of Files → Fixing path/to/file.c |
| 修复方式 | 无需手动修复,钩子已自动改写;重新 git add 即可 |
2.3 check-added-large-files — 大文件拦截
| 项 | 说明 |
|---|---|
| 触发条件 | 单个文件 > 1MB(默认阈值) |
| 处理方式 | 阻止提交(需手动修复) |
| 典型报错 | ilerge file added: path/to/file.bin |
| 修复方式 | 确认文件是否应提交;若为构建产物则加入 .gitignore,若为必要大文件用 --no-verify 跳过 |
2.4 check-merge-conflict — 冲突标记拦截
| 项 | 说明 |
|---|---|
| 触发条件 | 文件含 <<<<<<<、=======、>>>>>>> 冲突标记 |
| 处理方式 | 阻止提交 |
| 修复方式 | 手动解决冲突后重新提交 |
2.5 check-yaml — YAML 语法校验
| 项 | 说明 |
|---|---|
| 触发条件 | .yaml / .yml 文件语法错误 |
| 处理方式 | 阻止提交 |
| 检查对象 | 主要校验 .pre-commit-config.yaml 本身 |
| 修复方式 | 修正 YAML 缩进或语法后重新提交 |
2.6 cppcheck — C 静态分析
cppcheck 是开源 C/C++ 静态分析工具,不运行代码,通过语法分析和数据流推理发现潜在缺陷。本项目开启 warning / performance / portability / style 四个类别,任一类别发现问题即阻止提交(--error-exitcode=1)。
| 项 | 说明 |
|---|---|
| 触发条件 | 自有代码 .c 文件(App/、Src/、Inc/、Drivers/BSP/,第三方库除外)含潜在缺陷、冗余代码或可疑写法 |
| 处理方式 | 阻止提交(必须修复) |
| 修复方式 | 按报告修复代码;确属误报用行内抑制 // cppcheck-suppress 或全局 --suppress |
| 典型报错 | [path/to/file.c:123]: (style) variableScope The scope of the variable ... can be reduced |
| 增量速度 | 单文件约 1~2 秒;全库约 5 秒 |
完整规则清单(style 类别 95 项详解)、配置参数、YAML 引号陷阱、误报抑制方法见
docs/代码规范/cppcheck使用说明.md。
2.7 no-mojibake — U+FFFD 乱码拦截
| 项 | 说明 |
|---|---|
| 触发条件 | C 文件含 EF BF BD(U+FFFD 替换符,表示不可逆的编码损坏) |
| 处理方式 | 阻止提交(不可逆,必须修复) |
| 检测原理 | scripts/check_mojibake.py 以二进制读取文件,检测 0xEF 0xBF 0xBD 字节序列 |
| 典型报错 | U+FFFD replacement chars found (encoding corrupted, irreversible!): path/to/file.c |
| 修复方式 | 从 git 历史中找到最后一个干净版本,手动恢复损坏区域 |
⚠️ 编码铁律:U+FFFD 是不可逆损坏(无法从替换符反推原始中文字符)。若发现新文件出现 U+FFFD,必须排查写入方式(禁用 Get-Content / Set-Content 重写中文文件)。
2.8 coding-standard — C 代码规范检查
依据 docs/代码规范/嵌入式C语言代码规范_V1.0.md,用 scripts/check_coding_standard.py 对可机械化校验的规则做提交前拦截(纯 Python 正则扫描,无依赖)。
| 项 | 说明 |
|---|---|
| 触发条件 | 自有代码 .c/.h 文件(App/、Drivers/BSP/,第三方与 CubeMX 生成文件除外)违反下列规则 |
| 处理方式 | 阻止提交(必须修复) |
| 典型报错 | L123 [S3 3.4.1] 宏名须全大写+下划线: net_htonl |
检查项(S 编号与脚本输出一致):
| 编号 | 规范条款 | 规则 |
|---|---|---|
| S1 | 2.3.1 | .h 文件必须有 XXX_H 格式头文件保护宏(禁止 __ 前缀) |
| S2 | 5.2 | 文件开头必须有文件头注释(含 模块名称/作者) |
| S3 | 3.4.1 | #define 宏名必须全大写+下划线+数字 |
| S4 | 3.5.1 | typedef 类型名必须以 _t 结尾 |
| S5 | 6.3.2 | switch 必须包含 default 分支 |
| S6 | 7.5 | 禁用 strcpy/strcat/sprintf/gets/scanf/strtok(改用安全版本) |
| S7 | 6.3.3 | 禁止 goto |
| S8 | 4.1.1 | 禁止 Tab 缩进(4 空格) |
| S9 | 5.3 | 函数定义必须有函数注释(含「函数功能」) |
| S10 | 3.3.1 | 函数名必须全小写(HAL_ 厂商 API 回调名豁免) |
| S11 | 2.3.5 | 禁止相对/绝对路径 include(2026-10-11 细化判定形态:禁的是"用路径找文件"——引号 include 带 /、任意形态带 .. / \ / 绝对路径或盘符;<sys/stat.h> 这类尖括号系统头允许带子目录,由 -I 解析。原实现把任何含 / 的 include 一律判违规,误伤模拟器侧 2 处;全仓库复核:细化后仅这 2 处项目代码判定发生变化,其余 100 处均在已排除的 Lib/lvgl/) |
| S12 | 7.6 | 禁止可变参数函数(...) |
| S13 | 7.7 | 禁止递归函数 |
| S15 | 6.1.4 | 文件作用域指针变量必须初始化为 NULL |
| S16 | 3.2.2 | 文件作用域非 static 全局变量必须 g_ 前缀 |
| S17 | 2.2 | 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过) |
| S18 | 6.3.1 | 禁止 Yoda 条件(字面量/常量必须在运算符右侧) |
| S19 | 4.4.2 | 函数定义左大括号必须与右括号同行 |
不在检查范围(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、注释掉的代码等,仍需人工评审把关。
排除文件(与 cppcheck 钩子范围一致,另加):Src/、Inc/(CubeMX 生成,重新生成即覆盖)、Drivers/BSP/GD5F2GQ5UE/nand_ftl.*(dhara/FatFS 接口适配层,函数名由外部 API 固定)。
2.9 doc-links — 文档链接存在性(D-3/D-7)
用 scripts/check_doc_links.py 校验 Markdown 文档里引用的仓库内路径是否真实存在(纯 Python 标准库,无依赖)。
| 项 | 说明 |
|---|---|
| 触发条件 | 改动任意 .md 文件,且其中链接指向不存在的仓库内路径 |
| 处理方式 | 阻止提交(改路径,或按规范加 [计划] / 状态词) |
| 检查范围 | [文本](相对路径) 链接(相对当前文档解析)+ 反引号内 `docs/xxx.md` 仓库根相对路径 |
| 典型报错 | [示例] docs/架构设计/图形绘制流程.md:73 [markdown 链接] images/RemoDispBus帧格式.svg |
豁免(不算失效):[计划] 前缀;该行含 [示例] 标记(引用报错样例/日志片段时用);引用附近 ±40 字符内的状态词(未创建/尚未创建/已删除/待排期);占位符(*、<模块>、NN_、${...});行内代码 span 里的链接写法( 这类语法示例);外部 URL 与纯锚点;docs/芯片手册/ 等外部资料目录。
排除文件:.opencode/node_modules/、Lib/lvgl/、MDK-ARM/RTE/、docs/芯片手册/。
(simulator/ 已于 2026-10-11 从排除列表移除——其下 3 个 README 是本项目文档,见 §5。)
# 全库扫描(开发时自检)
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
规范依据:
docs/文档规范.md§3(链接规范)、§4(门禁)。
2.10 task-table — FreeRTOS 任务表一致性(D-2/D-7)
用 scripts/gen_task_table.py 以 Src/freertos.c 的 osThreadAttr_t 为唯一事实源,校验 README.md / AGENTS.md 中的任务表是否同步(防止改栈后文档失真——2026-09 曾两张表都比源码落后一大截)。
| 项 | 说明 |
|---|---|
| 触发条件 | 改动 README.md / AGENTS.md / Src/freertos.c,且受管表格区与源码不一致 |
| 处理方式 | 阻止提交;跑 --write 由脚本自动重写表格 |
| 受管区 | <!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py --> ~ <!-- TASK_TABLE:END --> 之间 |
| 典型报错 | [FAIL] 以下文档的任务表与 Src/freertos.c 不一致:README.md |
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --check # 检查(钩子用)
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --write # 自动同步表格
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --diff # 只看期望表格
⚠️ 改栈流程:先改
Src/freertos.c→ 跑--write同步文档 → 提交。钩子报不一致时不要手改表格。 脚本按原换行符 / BOM 状态回写,不会造成整文件行尾漂移。
2.11 mem-numbers — 内存/Flash 数字一致性(D-7 体积档)
用 scripts/check_mem_numbers.py 以 .map 为唯一事实源,校验文档里的内存/Flash 数字。
文档中手抄的 RW_IRAM1 82,048 B / 62.6% 这类数字会随代码变化而失真——实测 2026-09-26 复核时
文档仍写 82,048 / 62.6%,实际 .map 已是 87,040 / 66.4%。
| 项 | 说明 |
|---|---|
| 触发条件 | 改动 docs/内存管理/*.md / App·Src·Inc 源码 / 本脚本 |
| 处理方式 | 阻止提交;受管区跑 --write 自动同步;散句陈旧数字需人工复核 |
| 受管区 | <!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py --> ~ <!-- MEM_SNAPSHOT:END --> |
| 典型报错 | [FAIL] 以下文档的内存快照与 .map 不一致:docs/内存管理/RAM使用分析与优化方案.md |
| 前置 | 需存在 .map(@build 产物);缺失时脚本报 [FAIL] 找不到 .map 事实源 并提示先构建 |
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan # 钩子用(含陈旧散句扫描)
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --write # 同步受管快照
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --diff # 只看期望快照
陈旧数字扫描:从 docs/内存管理/data/ram_report_*.md 历史快照提取「曾为区域用量、现已变化」
的值,正文若仍以现状口吻引用即告警。含「历史/旧/曾/快照/data//实施时」等词的行使行豁免;
讲任务栈/内存池/峰值水位的行也跳过(数字语义不同)。
⚠️ 设计取舍:朴素"扫所有 4~7 位数字"实测 45 处误报(栈配置
4096、堆49,152、 峰值1184都是合法值)。改为"只报历史快照里出现的、当前已变的区域值"后 0 误报。 宁可窄而准——吵的门禁会被绕过。
2.12 cubemx-survival — CubeMX 重生成存活审计
用 scripts/check_cubemx_survival.py 核对"重生成易失清单",防止 CubeMX 重新生成时
静默覆盖手工定制。.ioc 的 ProjectManager.KeepUserCode=true 只保护
/* USER CODE BEGIN/END */ 标记区内的内容——标记之外的一切都会被模板重写,
且多数不报编译错误,只在真出故障时才暴露(2026-09-29 实测一次重生成覆盖 4 处,
含 HardFault_Handler 崩溃采集被打回空 while(1)、两个 FreeRTOS 加固宏被整块删除)。
| 项 | 说明 |
|---|---|
| 触发条件 | 改动 STM32F407-Demo.ioc / Src/*.{c,h} / Inc/*.h / App/util/task_watchdog.* / MDK-ARM/*.uvprojx / 本脚本 |
| 处理方式 | 阻止提交;清单项缺失或基线漂移即 FAIL |
| 检查内容 | 16 项:IWDG 延后启动、崩溃采集×2、栈溢出检测、堆失败钩子、断言改造×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、.ioc 关键项×3 |
| 基线文件 | .cubemx_survival_baseline.json(各项内容哈希;--write 重建) |
| 防误报 | 断言前剥掉 C 注释——本项目多处注释刻意引用历史代码(如"原为 HAL_IWDG_Init(&hiwdg) 直呼"),不过滤会恒定误报 |
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py # 审计报告(人读,含修复提示)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --check # 门禁模式(钩子用)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --report # 只打印报告,不比对基线
推荐工作流:每次 CubeMX 重生成后立刻跑一次(不带参数)——它会把 "被覆盖了哪几处"直接列出并附修复提示,比事后靠故障现象反推快得多。
标记区外的存量以 WARN 提示(
Src/stm32f4xx_it.c的 3 个 Fault 处理函数、Src/freertos.c的栈注释)——这些 CubeMX 不保护,只能人工盯;建议后续 仿照HardFault_Handler自造USER CODE标记区把它们升级为受保护内容。方法论(L1 写
.ioc> L2 搬进USER CODE区 > L3 审计兜底)详见../子系统设计/看门狗设计说明.md§10.2。
2.13 test-suite-switch — 测试套件开关=产品构建
用 scripts/check_test_suite.py 检查 test/board/test_config.h 的 TEST_SUITE_* 开关
是否已全部注释(= 产品构建),防止测试固件被静默提交。
| 项 | 说明 |
|---|---|
| 触发条件 | 改动 test/board/test_config.h,且其中有 TEST_SUITE_* 处于启用状态 |
| 处理方式 | 阻止提交(注释掉开关,或显式放行测试构建) |
| 典型报错 | [FAIL] 默认要求产品构建:请注释掉 test/board/test_config.h 中的 TEST_SUITE_* 开关。 |
| 放行方式 | python scripts/check_test_suite.py --allow-test-build 或 ALLOW_TEST_BUILD=1 git commit ...(放行后仍打 [WARN] 横幅提醒) |
为什么需要这个钩子(2026-10-01 实测):
test_config.h 的 TEST_SUITE_CH395F/STORAGE/GD5F/RTC 决定固件里编入哪个测试套件,
全部注释 = 产品构建——但此前 grep TEST_SUITE scripts/*.py 零命中,没有任何门禁
检查它。实测已提交的 HEAD 里 TEST_SUITE_CH395F 就是开启状态(.map 含 34 处 test 代码),
即仓库里躺着的是测试固件而非产品固件,纯靠开发者记得改回去。
判定实现:先剥掉块注释与行注释再匹配 #define,所以注释掉的开关
(//#define TEST_SUITE_GD5F)不会误判;同文件派生的 TEST_SUITE_SELECTED /
TEST_SUITE_NAME 由白名单排除(它们是开关的推导结果,非用户可选项)。
& '.venv\Scripts\python.exe' scripts/check_test_suite.py # 门禁判定
& '.venv\Scripts\python.exe' scripts/check_test_suite.py --allow-test-build # 显式放行
套件互斥(一次最多启用一个)由
test_config.h:45的#error在编译期兜底; 本钩子在提交期再拦一道(#error只在真正构建时暴露,测试固件仍可能被提交)。
2.14 gitlint — Commit Message 格式
| 项 | 说明 |
|---|---|
| 触发条件 | commit message 标题超 100 字符 / 正文单行超 80 字符 / 其他格式规则 |
| 处理方式 | 阻止提交(commit-msg 阶段) |
| 当前规则 | 宽松:不强制 Conventional Commits,兼容中文标题。配置源是仓库根 .gitlint(不是 hook args):ignore=body-is-missing(不强制写正文)+ [title-max-length] line-length=100;正文行宽走 gitlint 默认 80(未关) |
| 典型报错 | T1 Title exceeds max length (159>100)、B4 Line too long (87>80) |
| 修复方式 | 标题压到 100 字符内(中文长标题极易超限);长说明一律用短标题 + git commit -F <文件> 多行正文,正文每行 ≤80 字符。消息文件必须 UTF-8 无 BOM——用 -F 文件 而非 -m "…",防终端代码页转码破坏中文 |
2.15 lv-conf-sync — 两份 lv_conf.h 特性子集一致性(2026-10-11 加)
| 项 | 说明 |
|---|---|
| 背景 | Lib/lv_conf.h(MCU)与 simulator/lv_conf.h(PC)是两份独立文件,长期漂移:模拟器早已 I1-only,MCU 端却让 11 个 LV_DRAW_SW_SUPPORT_*、5 个未用控件类、LV_USE_THEME_SIMPLE、LV_USE_FLEX/GRID 靠 lv_conf_internal.h 默认值隐式生效 → 固件白链 64,576 B 死代码;更糟的是模拟器 golden 回归跑的是另一套特性子集,验证不到固件实际编译的配置 |
| 触发条件 | Lib/lv_conf.h 或 scripts/check_lv_conf_sync.py 变动 |
| 判定口径 | 比对生效值:文件里没定义的宏,取 Lib/lvgl/src/lv_conf_internal.h 的兜底默认。⚠ 该文件每个选项都先套一层 #ifdef LV_KCONFIG_PRESENT → CONFIG_LV_* 分支不是默认值,脚本取块内非 CONFIG_ 的最后一个字面量(首版按"紧跟 #ifndef 的那行"取,结果整表都是 CONFIG_*,教训) |
| 四类问题 | MISMATCH 生效值不同 → FAIL;INVALID 写了 LVGL 不认的宏名 → FAIL(LV_USE_THEME_BASIC 同类);DUPLICATE 同文件定义两次 → FAIL(后者生效,极易误读);IMPLICIT 一端靠默认值 → 警告(默认值随 LVGL 版本变,建议显式固化) |
| 端专属白名单 | SDL2 驱动 / 演示工程 / include 替换 / 调试辅助(LV_USE_OBJ_NAME 等)/ 断言(固件关、模拟器开)/ LV_USE_SNAPSHOT(模拟器 --screenshot 需要)——见脚本 IGNORE_PREFIX 与 ALLOW_DIFF(后者必须写理由) |
| 典型报错 | FAIL: N 项不一致 + [MISMATCH] LV_USE_OBSERVER MCU=1(默认) vs SIM=0(显式) |
| 修复方式 | 把两端拉到同一生效值(优先显式写死,别依赖默认),或在 ALLOW_DIFF 里登记理由 |
| 触发范围 | Lib/lv_conf.h、simulator/lv_conf.h 或脚本自身变动 → 两端任一改动都会跑。⚠ 早期版本只能由 MCU 端触发,因为顶层 exclude 整目录挡掉了 simulator/(已删,见 §5);pre-commit 4.x 又移除了 always: true(实测对 local 钩子只告警并跳过),所以没有"总是跑"的兜底写法——必须让文件模式覆盖两端 |
| 首次落地实测 | 两端共清掉 26 个 9.3 不存在的选项(LVGL 8 / 9.4 模板残留)、3 处同文件重复定义,并把 10 项 MCU 端靠默认值隐式生效的开关显式固化为 0(含默认 =1 的 LV_USE_OBSERVER / LV_USE_DRAW_PXP / LV_USE_IMAGEBUTTON)。本次清理对镜像体积中性(那些 TU 本就不在 uvprojx,或编成 0 字节空对象),价值在防漂移与可读性 |
2.16 sha256-unit-test — SHA-256 主机侧单测(2026-10-11 加,N-19)
| 项 | 说明 |
|---|---|
| 触发条件 | App/util/sha256.*、test/host/sha256/** 或 test/host/common/unit_test_harness.py 变动 |
| 处理方式 | 阻止提交(单测失败即拦截) |
| 被测对象 | App/util/sha256.c/h(FIPS 180-4 实现,零堆分配)——FTP pwd_hash 校验的地基 |
| 用例 | 13 例:FIPS 180-4 标准向量(空串 / abc / 56 B 补位跨界 / 1 MB)+ hex_eq 非法输入 |
| 为什么需要 | 哈希算错是"正确口令被拒 / 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因;主机侧毫秒级钉死。零平台依赖,无桩 |
| 运行器 | test/host/sha256/run_unit_test.py(MSVC cl.exe,L0.5) |
2.17 ftp-auth-unit-test — FTP 鉴权状态机主机侧单测(2026-10-11 加,N-19)
| 项 | 说明 |
|---|---|
| 触发条件 | Drivers/BSP/NET/lftpd/lftpd_auth.*、App/util/sha256.*、test/host/ftp_auth/** 或 test/host/common/unit_test_harness.py 变动 |
| 处理方式 | 阻止提交(单测失败即拦截) |
| 被测对象 | Drivers/BSP/NET/lftpd/lftpd_auth.c/h(纯函数鉴权状态机,tick 由调用方参数注入,零平台依赖) |
| 用例 | 36 例:连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕 |
| 为什么需要 | 锁定 60 s / 解锁 / 清零 / 回绕是防口令爆破契约核心——板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数设计(tick 注入)就是为让这些分支主机侧毫秒级穷举 |
| 运行器 | test/host/ftp_auth/run_unit_test.py(MSVC cl.exe,L0.5) |
2.18 registration-check — 双端注册反向校验(2026-10-12 加,N-23)
| 项 | 说明 |
|---|---|
| 触发条件 | 任意 .c 文件 + *.uvprojx + CMakeLists.txt + 脚本自身变动(pre-commit 4.x 无 always: true,files: 必须覆盖全部触发面) |
| 处理方式 | 阻止提交(存在新缺口或幽灵注册即拦截);存量缺口走白名单只 [WARN] 不拦 |
| 检查对象 | 扫描 App/、Drivers/BSP/、Src/ 下全部 .c(排除第三方),对照两份注册表:MDK-ARM/STM32F407-Demo.uvprojx 的 <FilePath> + simulator/CMakeLists.txt 的显式源列表(先展开 ${NETCFG_DIR} 等变量) |
| 三类缺口 | ① 存在但未进 uvprojx(固件不编译)② 存在但未进 CMakeLists(模拟器不编译,仅当本应进模拟器时才算)③ 注册了但文件不存在(幽灵注册,改名/删除后残留) |
| 豁免机制 | MCU-only 文件不进模拟器是正确的(模拟器另有 *_sim.c 替身或无对应)。豁免用显式清单 + 每行理由(命名模式 *_mcu.c + 个别点名),不整目录一刀切(N-23 教训正是目录级盲区);唯一目录级豁免 Src/(CubeMX 生成,MCU 专属)→ 见脚本 SIM_EXEMPT |
| 存量白名单 | KNOWN_GAPS 登记 N-23 存量缺口(App/task/rs485_task.c、Drivers/BSP/NET/lftpd/lftpd_log.c,Lead 处置中)→ 命中打 [WARN] 不拦,只对新缺口 FAIL,故钩子可立即上线不拦现有提交;--strict 关白名单供审计 |
| 为什么需要 | 新 .c 必须手动注册两处才会被编译;历史上 rs485_task.c/lftpd_log.c 两处都没注册 = 写了却从不参与任何一端编译的死代码,误导读者以为功能已生效(出处审查报告 N-06 残余) |
| 运行器 | scripts/check_registration.py(纯标准库;退出码 0=一致 / 1=有缺口 / 2=环境错误) |
2.19 mem-budget — RAM 预算 92% 阈值门禁(2026-10-12 加,N-27 三件套)
| 项 | 说明 |
|---|---|
| 触发条件 | 全部自研代码目录(App/ Drivers/BSP/ Src/ Inc/)+ Lib/lv_conf.h + test/board/ + scripts/ram_baseline.* + docs/内存管理/ + 本配置文件 |
| 处理方式 | 阻止提交(RW_IRAM1 / RW_CCM 任一区占比 >92%,或台账受管区与 .map 漂移);无 .map 自适应跳过(同 mem-numbers 在 CI 的行为,盲区登记 docs/任务规划/测试与架构专项.md A-5 残余) |
| 检查逻辑 | 解析 .map 执行区占比 → 超阈值即 FAIL 并附 Top-10 RW+ZI 对象表(定位增量元凶);同时校验 docs/内存管理/RAM预算台账.md 受管区与 .map 一致(漂移 → 提示 --write) |
| 软提醒(不拦) | tsv 末行落后于 .map(忘跑 --update);.map 早于最新源码(忘 @build) |
| 配套三件套 | scripts/ram_baseline.py(门禁/趋势/受管区生成)+ scripts/ram_baseline.tsv(每次 @build 后 --update 追加的趋势基线,--trend 高亮 RW+ZI 增长 >1 KB 的对象)+ docs/内存管理/RAM预算台账.md(子系统配额 + CCM 台账 + 超配额流程) |
| 为什么需要 | 2026-09 体系评审提出的"超阈值即失败 + Top-10 附表"(现沉淀在 docs/问题记录/项目审查与优化建议报告.md §5.5 闭环说明)此前无任何机械检查——mem-numbers 只防"文档数字漂移"不防"占用超阈值";B4/B5 要新建任务与缓冲,红线必须先机械化(登记簿 N-27 前半 / W1-0) |
| 运行器 | scripts/ram_baseline.py --gate(纯标准库,复用 ram_report.py 的 .map 解析口径;退出码 0=通过/跳过 / 1=超阈值或漂移 / 2=解析失败) |
2.20 iec104-unit-test — IEC 60870-5-104 协议栈主机侧单测(2026-10-12 加,B4)
| 项 | 说明 |
|---|---|
| 触发条件 | `App/protocol/iec104/iec104_(asdu |
| 处理方式 | 阻止提交(单测失败即拦截) |
| 被测对象 | App/protocol/iec104/ 三模块(ASDU 编解码 / 点表+数据源 vtable / APCI+链路 FSM)——纯逻辑、tick 注入、发送经回调注入,零 I/O(A-4 约束 1) |
| 用例 | 82 例:CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1-t2-t3 / N(R) 倒退判故障 / 遥控两段式 / 定值越界 PN / C_CS 对时 / 未知类型-地址错误应答 / 周期上送 |
| 为什么需要 | 板端复现这些分支要真主站 + 真等待(t1=15 s、k 窗满要发满 12 帧);link 层 tick 注入 + pts vtable 抽象就是为 L0.5 毫秒级穷举设计;数据源依赖进触发面——测试直接 #include 这些生产源,签名变了立刻暴露 |
| 运行器 | test/host/iec104/run_unit_test.py(MSVC cl.exe,L0.5) |
2.21 freq-unit-test — 频率测量层主机侧单测(2026-10-12 加,B8)
| 项 | 说明 |
|---|---|
| 触发条件 | App/algo/freq.*、App/algo/phasor.*、App/algo/sample_ring.*、test/host/freq/** 或 test/host/common/unit_test_harness.py 变动 |
| 处理方式 | 阻止提交(单测失败即拦截) |
| 被测对象 | App/algo/freq.c/h(L2.5 测频:共轭乘积相位增量 + 解缠绕 + 自适应窗长 K_eff 对齐负序泄漏纹波周期 + 假值钳制带 35~65 Hz + 幅值门 valid=0)——纯合成信号、零 I/O |
| 用例 | 46 断言:J1 扫频 45~55 Hz ≤0.02 Hz(实测 0.0101)/ J2 噪声 1% ≤0.10 Hz / J3 启动收敛 ≤40 ms / J4 阶跃 50→51 ≤120 ms(精度优先于响应,见 App/algo/README.md §3.7)/ J5 浮空 valid=0 不发散 / J6 双跳变钳制恢复 |
| 为什么需要 | 测频精度是 OF/UF 判据的地基;板端无标准源照不出来(规划 §3.3 L3-5 注入验收归 B10);定长窗负序泄漏纹波(0.317 Hz)这类坑只有扫频合成信号才能暴露 |
| 运行器 | test/host/freq/run_unit_test.py(MSVC cl.exe,L0.5) |
2.22 soe-unit-test — SOE 动作记录编解码主机侧单测(2026-10-12 加,B9)
| 项 | 说明 |
|---|---|
| 触发条件 | App/log/soe_rec.*、App/log/soe_store.*、test/host/soe/** 或 test/host/common/unit_test_harness.py 变动 |
| 处理方式 | 阻止提交(单测失败即拦截) |
| 被测对象 | App/log/soe_rec.c/h(64 B 定长 SOE 记录纯编解码:类型/elm_id/IV 位/seq u32/CP56Time2a/epoch_s/value/sum8)——soe_store 板端胶水 SIM_EXEMPT |
| 用例 | 32 断言:编解码往返 / 截断与坏校验和拒绝 / IV 位(无链路时标)/ seq 连续性 / 类型枚举(元件动作·复归、压板出·入、采样坏·复)双类型断言 |
| 为什么需要 | 记录格式是磁盘上的字节级契约——编解码错了要 FTP 下载 + 解码才看得见,板端日志照不出来;与 /log/run.log 同款定长 64 B 语义 |
| 运行器 | test/host/soe/run_unit_test.py(MSVC cl.exe,L0.5) |
3. 日常工作流
3.1 正常提交(秒级完成)
git add .
git commit -m "修复网络超时重连逻辑"
全部钩子自动运行,只检查本次 staged 的文件(增量扫描)。通常 <5 秒完成。
3.2 钩子失败后的处理
# 提交被拦截,查看报错信息
# 修复后重新 add + commit
git add .
git commit -m "修复网络超时重连逻辑"
常见失败场景与处理:
| 钩子 | 失败表现 | 处理步骤 |
|---|---|---|
| trailing-whitespace | Fixing xxx.c → 需要重新 git add |
钩子已自动修复文件,git add 后重新提交 |
| end-of-file-fixer | 同上 | 同上 |
| cppcheck | 报 warning/performance/portability/style | 根据报告修复代码;误报用行内抑制或 --suppress(详见 docs/代码规范/cppcheck使用说明.md) |
| no-mojibake | U+FFFD replacement chars found |
从 git 历史恢复干净版本(不可逆!) |
| coding-standard | L行号 [Sx x.x.x] 规则说明 |
按违规项修复(命名加前缀/大写化、补函数注释、sprintf 改 snprintf 等) |
| gitlint | Title exceeds max length |
精简 commit message 标题 |
3.3 全库扫描(配置变更后)
pre-commit run --all-files
仅在以下场景使用:
- 首次
pre-commit install后确认基线 - 修改
.pre-commit-config.yaml后验证 cppcheck新增 suppress 规则后验证
日常提交不需要跑
--all-files,pre-commit 会自动只检查 staged 文件。
3.4 手动运行单个钩子
pre-commit run cppcheck --all-files # 只跑 cppcheck
pre-commit run trailing-whitespace --files path/to/file.c # 只检查单个文件
pre-commit run no-mojibake --files path/to/file.c
pre-commit run gitlint --hook-stage commit-msg --commit-msg-filename .git/COMMIT_EDITMSG
3.5 临时跳过钩子(紧急情况)
git commit -m "紧急修复" --no-verify
# 或跳过特定钩子
SKIP=cppcheck git commit -m "紧急修复"
⚠️ 不推荐,仅用于紧急修复。跳过后的代码应在下一次提交中补上检查。
4. 新机器启用
# 1. 安装 pre-commit(需要 uv)
uv tool install pre-commit
# 2. 安装 cppcheck(需要 winget)
winget install cppcheck
# 重开终端使 PATH 生效
# 3. 进入仓库,安装钩子
cd D:\Code\DTU Code\STM32F4-Base
pre-commit install
# 4. 首次全库扫描(确认基线)
pre-commit run --all-files
5. 已知问题与注意事项
5.1 cppcheck 找不到
现象:Executable cppcheck not found
原因:cppcheck 安装后 PATH 更新在注册表,已运行的终端不会刷新。
解决:重开终端即可。若需在当前会话临时修复:
$env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Environment]::GetEnvironmentVariable("Path","User")
5.4 clang-format 未启用
原因:全库 222 个 .c/.h 文件大概率不满足 clang-format 默认格式,启用后会批量重写。
计划:Route B —— 全库统一格式化后形成独立基线 commit,再启用 clang-format 钩子。启用时取消 .pre-commit-config.yaml 中的注释即可。
5.5 顶层 exclude 曾整目录豁免 simulator/(2026-10-11 已修)
现象:改 simulator/ 下任何文件(含 simulator/lv_conf.h)的提交一个钩子都不跑——pre-commit run --files simulator/lv_conf.h 里所有钩子都是 (no files to check) Skipped。
根因:顶层 exclude 写了 simulator/。当初是为了挡 SDL2 预编译库、build/、截图产物,但这些已在 .gitignore 里,本就进不了 git 索引;pre-commit 只看索引中的文件,所以那条豁免唯一实际效果是把本项目自研的 21 个 tracked 文件(main.c、sim/*.c|h、tests/*.py、lv_conf.h、dbg_log.h、3 个 README)整体挡在门禁之外。
为什么必须修:模拟器与固件共用同一套 UI 源码,且它是 UI 回归唯一的像素级证据来源——它落在门禁外,等于验证链的地基不受检。
纳入后的实测收益(当天):cppcheck 立刻揪出 sim_syslog.c 一处真实空指针解引用(if (p_epoch == NULL || …) { *p_epoch = 0u; … }——判空分支里紧接着解引用),另有 2 处 localtime_s 未初始化告警、1 处 px 可加 const;coding-standard 报出 61 处(44 处函数左大括号 Allman 风格、8 处缺函数注释、3 处缺文件头注释、2 处 __ 前缀保护宏、2 处 sprintf、2 处 S11 误报),已全部处理。
顺带修掉的规则缺陷:S11 原实现把任何含 / 的 include 判违规,误伤 <sys/stat.h> 这类系统头;已按 include 形态细化,判定见 §2.8。
教训:exclude 只写真正不该进钩子的路径。已被 .gitignore 覆盖的生成物不需要再挡一遍——写上去只会连带豁免同一目录里的自研代码。
6. 文件清单
| 文件 | 位置 | 说明 |
|---|---|---|
.pre-commit-config.yaml |
仓库根 | 钩子配置(33 个钩子,2026-10-12 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 15 个) |
.gitlint |
仓库根 | gitlint 规则(宽松,兼容中文) |
.cubemx_survival_baseline.json |
仓库根 | CubeMX 易失定制基线(哈希;--write 重建) |
scripts/check_mojibake.py |
scripts/ | U+FFFD 检测脚本 |
scripts/check_coding_standard.py |
scripts/ | 代码规范检查脚本(规范 V1.0 可机械校验项) |
scripts/check_doc_links.py |
scripts/ | 文档链接存在性检查(D-3/D-7) |
scripts/gen_task_table.py |
scripts/ | FreeRTOS 任务表生成/校验(D-2/D-7) |
scripts/check_mem_numbers.py |
scripts/ | 内存/Flash 数字一致性(D-7 体积档) |
scripts/check_cubemx_survival.py |
scripts/ | CubeMX 重生成存活审计 |
scripts/check_lv_conf_sync.py |
scripts/ | 两份 lv_conf.h 特性子集一致性(比对生效值 + 无效宏名 + 重复定义) |
scripts/check_test_suite.py |
scripts/ | 测试套件开关门禁(防测试固件静默提交) |
docs/代码规范/嵌入式C语言代码规范_V1.0.md |
docs/代码规范/ | 代码规范原文(S1~S19 规则来源) |
docs/代码规范/cppcheck使用说明.md |
docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) |
docs/文档规范.md |
docs/ | 文档体系规范(链接形态/命名/门禁要求) |
docs/代码规范/pre-commit使用说明.md |
本文档 | 部署与使用说明 |
7. 快速参考卡片
提交代码: git add . && git commit -m "描述"
全库扫描: pre-commit run --all-files
单钩子扫描: pre-commit run <hook-id> --all-files
扫描单个文件: pre-commit run <hook-id> --files <path>
跳过钩子: git commit --no-verify (紧急)
查看已安装钩子: ls .git/hooks/
卸载钩子: pre-commit uninstall
更新钩子版本: pre-commit autoupdate
CubeMX 重生成后立刻执行(检查定制是否被覆盖):
python scripts/check_cubemx_survival.py # 列"被覆盖了哪几处"+修复提示
python scripts/check_cubemx_survival.py --write # 确认无误后重建基线