Files
STM32F4-Base/docs/门禁说明/bsp-err.md
T
wandering1 16be01915d
CI / 静态门禁 (pre-commit) (push) Failing after 46s
CI / 主机侧单测 (MSVC) (push) Failing after 43s
chore(docs): 补齐文件末尾换行(end-of-file-fixer 实测 18 篇)
提交后台跑 scripts/ci_local.py 时 end-of-file-fixer 报 Failed 并就地修复:
17 篇补文件末尾换行、1 篇去掉行尾空行。

内容零变化:git diff --ignore-all-space 只有 1 处空行删除;18 篇均在
上一笔流水清理里被改过,恢复补丁时把上一轮的 EOF 修复冲掉了,这一
笔补回。
2026-10-28 22:12:59 +08:00

12 KiB
Raw Blame History

bsp-err — BSP 错误码与超时命名约定

一句话:它管 Drivers/BSP/** 对外 API 的四件事——用什么类型表达失败、超时宏叫什么、 有没有把返回值写进注释、调用点有没有真的去看返回值。它不管错误码用得对不对(见 §6)。

  • 配置:.pre-commit-config.yaml 第 579 行起(entry: python scripts/check_bsp_err.py、 language: system、pass_filenames: false)。
  • 规则脚本:scripts/check_bsp_err.py(纯标准库;文件头就是规则正文)。
  • 约定正文:Drivers/BSP/bsp_err.h(header-only,无 .c → 不占体积、不需双端注册)。
  • 背景与整改史:原审查报告里该条契约的完整记录已随报告删除,现存依据 = Drivers/BSP/bsp_err.h 与 scripts/check_bsp_err.py 的存量基线。

1. 它在查什么

四项检查,每项一个自描述标签(就是基线键的第二字段,键格式 文件|标签|名字):

标签 判据 拿它挡的具体写法
err-type 公开 API 的返回类型不得是 HAL_StatusTypeDef;也不得是 bool(除非是谓词形状) 用 bool 表达成败——"为什么失败"整条丢掉;用 HAL 枚举——把 HAL 焊进调用方的 include 链
timeout-name #define 出来的 *TIMEOUT* 宏不得取负值 名字叫 TIMEOUT 实际是返回码(本仓曾有 GD5F_BUSY_TIMEOUT = -2、TPAFE5160_BUSY_TIMEOUT = -2,已改)
return-doc 非 void 的公开 API,声明上方紧邻注释块里必须出现"返回值"(匹配时忽略空格) 只写函数功能不写成败含义
consume-result 返回错误码的函数,调用点必须接住返回值或显式 (void) 丢弃;裸调用即违规 HAL_SPI_Transmit(&hspi1, cmd, 3, T); 后面照旧 return OK

consume-result 的"哪些函数算返回错误码"是自动取名单,不手写(手写名单必然与代码脱节):

  1. 头文件里返回 bsp_err_t 的公开 API(契约规定 bsp_err_t 就是错误码);
  2. 返回 int/int32_t 且其文档块出现"错误码 / 失败 / BSP_ERR / _FAIL"的函数;
  3. 已知返回 HAL_StatusTypeDef 的 HAL 族(HAL_{SPI,I2C,UART}_{Transmit,Receive,TransmitReceive} 及其 _DMA/_IT 变体、HAL_I2C_Mem_{Read,Write});
  4. 与 2 同一套启发式,再扫一遍 .c 里的 static 定义——只扫头文件等于只守跨编译单元的边界。

2. 为什么要它

  • 这一层今天六套约定并存:0/−1、自研枚举 CH395F_STATUS_*、芯片寄存器原生码 CH395F_ERR_*(与前一套同模块并存)、HAL_StatusTypeDef、bool,外加第三方 dhara 的 dhara_error_t 与 FatFs 的 FRESULT。真正值钱的不是码名好看,而是"能不能重试" 以前只写在个别注释里——现在 BSP_ERR_CAN_RETRY(e) 把它变成跨模块可判定。
  • 它上线当天就照出真窟窿:consume-result 落地即暴露 37 条存量丢弃,其中一处是 bbt_rebuild 重新打开 ECC 的那两行返回值全丢——SPI 一抖 ECC 就保持关闭,而 ECC 关闭时 ECC 检查恒读"无错",整条链再没有任何信号。修法与运维判据见 docs/测试说明/存储维护与容量恢复.md §4.3。
  • 没有工具会替我们做这件事,两条实测负结果(本门禁存在的正当性全在这两条上): ① __attribute__((warn_unused_result)) 在 ARMCC v5 上完全不生效——属性挂上、调用点 裸调不接返回值,构建仍 0 错 0 警告(在 rs485_transmit 上做过实验); ② cppcheck 也照不出来——按本仓自己的 --enable=warning,style,performance,portability 跑 gd5f2gq5ue.c,对 14 处丢弃 HAL_SPI_* 返回值、以及"未初始化局部量直接进 P_FAIL 判定"一条不报(它假定被调函数会写出参,而失败路径根本不写)。

3. 触发面

files: ^Drivers/BSP/|^scripts/check_bsp_err\.py$|^\.pre-commit-config\.yaml$
pass_filenames: false      # 恒扫全库:要按 git 清单建公开头索引,不能只看暂存的那几个文件
  • 扫描对象来自 git ls-files Drivers/BSP:err-type/timeout-name/return-doc 看 .h (对外 API 面),consume-result 扫 .c(33 个文件)。
  • lftpd/ 排除:它的 226/425/450/550 是 FTP 协议码,不是内部错误码。
  • net_* BSD 兼容层不纳入统一码(用户裁定 2026-10-23,见 Drivers/BSP/bsp_err.h 边界纪律 3): 调用方要的就是"返回字节数 + 每任务 errno 槽"那个形状,模拟器的 Winsock 后端照同一形状实现。 ⚠ 但**"不纳入码"不等于"可以丢弃返回值"**——consume-result 照样管 net_close 这类调用。
  • 因为 files: 覆盖到 .pre-commit-config.yaml 与脚本自身,改门禁必然触发它自己。

4. 报错怎么读

$ python scripts/check_bsp_err.py
BSP 约定:命中 38(在册 38 / 新增 0),基线共 38
输出 意思 做什么
新增 N>0 + [FAIL] 出现了基线之外的新违规 按提示改代码(返回 bsp_err_t / 时长宏带 _MS / 补「返回值:」/ 接住返回值或写 (void))
[提醒] 有 N 条基线已还清 你把存量修掉了 跑 --prune 拿到清单,从脚本的 BASELINE 里删掉那些行,再跑一次确认 0 提醒
命中 0 干净 —
本地跑出与暂存无关的假报警 pre-commit 会 stash 未暂存改动,钩子看到的是"没有你改动"的树 先 git add 再跑钩子(本仓踩过:rs485 试点期被假报一批)

三个只读旗标:--verbose(列出全部命中并标注在册/新增)、--prune(只报已消失、可删的基线)、 --emit-baseline(打印当前全部键,重建基线用)。脚本刻意没有 --write:基线是人审后手改的, 不提供"一条命令把现状洗成合法"的入口。退出码 0 通过 / 1 有新增 / 2 用法或扫描失败。

5. 好处

  • 把"约定"从口头搬到机器上:新代码想沿用旧习惯,提交就被拦,不需要人在评审里记着。
  • 存量与新增分得开:基线让历史债务不阻塞日常工作,"只减不增 + --prune 验收"保证债只会变小。
  • 可审计:--verbose 就是债务清单;每条基线是 文件|标签|名字,能对着代码定位。
  • 零运行时成本:header-only 契约 + 纯标准库脚本,不进固件、不需要构建开关。

6. 限制与盲区(读这篇最重要的部分:绿灯不等于没问题)

  1. 不管错误码用得对不对。它只判通道类型、命名、有没有文档、有没有被消费。把 BSP_ERR_TIMEOUT 用在"参数非法"上,它一声不响——那是语义问题,机器判不了。
  2. 范围只有 Drivers/BSP。App/**、simulator/**、Src/** 不在面内;应用层的同类问题 今天仍然只能靠评审。
  3. consume-result 只认单行整句的裸调用(foo(...); 一行写完,行尾注释允许)。跨行写的调用 ——参数换行、或 ; 在下一行——按正则形状测不到。这是判据的物理限度,不是漏配。
  4. 名单是启发式:靠"返回类型 + 注释关键词"认函数。注释里不写"错误码/失败"的函数会漏 (假阴性);反过来,把谓词写成 foo_ok() 这种不在豁免形状里的名字就会被判违规(假阳性, 见 §7 的两次收窄)。判据依赖命名形状,改名就能绕过规则——这是所有基于正则的门禁共同的洞。
  5. HAL 族是固定清单(§1 第 3 条)。清单外的返回 HAL_StatusTypeDef 的函数(如 FLASH、SD 卡 那批)不在名单里;将来用到就得多补一行正则,属于人工维护的盲区。
  6. err-type/return-doc 只看头文件里的声明形状:用宏包出来的 API、函数指针表里的 API、 通过 typedef 起的函数别名,都不在它的视野里。
  7. return-doc 的判据是"紧邻注释块里出现『返回值』字样",散文里出现这三个字也会放行。 这不是假想:本仓 gd5f_write_enable、gd5f_read_status 曾长期"通过",原因竟是它们的 函数说明里顺口写了"返回值"三个字,而不是真有一行返回值说明。一条会因为散文里出现关键词 而放行的规则,等于在奖励运气——已改为忽略空格后匹配(规范 V1.0 允许列对齐写法「返 回 值:」, 门禁不规定排版),但"关键词出现即放行"的性质没有变,只能靠评审看内容。
  8. 基线键带出现序号(gd5f_read_status#2)。含义是"该文件里该函数的第 2 处丢弃",不是位置。 于是:删掉靠前的丢弃会让后面的序号整体前移,--prune 只报"消失的键",不会告诉你某处只是 搬了家;反过来,同函数新增一处丢弃会多出一个新序号 → 会被判新增(这点是好的,实测过)。
  9. 改标签名 = 改基线键。今天把 R1~R4 换成现名时,必须脚本与 38 条基线一起换,中途跑 --prune 会把改名读成"38 条全还清 + 38 条新账"。同理,今后任何标签改名都是一次原子改动, 不能分两个提交。
  10. 不提供重试 helper,也不检查重试策略(有上限吗、喂看门狗了吗、跨就绪屏障等待了吗)—— 见 Drivers/BSP/bsp_err.h 边界纪律 5:一个通用 while 包装太容易被拿去包住无限等待。

7. 豁免与基线

  • 基线在脚本里的 BASELINE 常量(不是外部台账文件),只减不增,没有 --write。
  • 当前在册 38 条(2026-10-23): · consume-result 37 条 —— gd5f2gq5ue.c 28(14 处丢弃 HAL_SPI_* 状态:HAL_SPI_Transmit×11、 HAL_SPI_Receive×3;14 处丢弃自家原语:gd5f_write_enable×4、gd5f_read_status×3、 gd5f_bbt_save×1、gd5f_block_erase×1、gd5f_page_read×1、gd5f_program_exec×1、 gd5f_program_load×1、gd5f_read_from_cache×1、gd5f_wait_busy×1)、ui_net_sink.c 6(全是 net_close)、rs485.c 2(rs485_receive_start)、nand_ftl.c 1; · err-type 1 条 —— bool ui_content_page_open():要改签名并处理 3 个调用点(三处今天全部 丢弃返回值,即"按 ENTER 开页失败是静默的"),与"补注释"不是一档事,见报告变更记录。
  • 形状白名单(写进规则,不塞基线): · 谓词 is_/has_/can_/should_/need_/valid_——返回值是"提问"而不是"办成没有"; · setter (?:^|_)set_...$——返回的整数是状态回声而不是调用成败(实跑误判出 net_set_errno 八处); · mark_block_bad——今天返回 void,预先豁免,防它将来加错误码返回时把整批调用点变成违规。
  • **原则:规则按真实反例收窄,不拿基线洗假阳性。**本仓第一版 err-type 误判 7 个正当谓词, 改窄而不是入基线;consume-result 两次同类处理见上面两条。

8. 改这个门禁本身

  • 改正则先做正反例自测:写一段一次性 python,把"应当命中"和"不应命中"的样例各列几条, exec 脚本头部拿到正则再断言(今天三次修正则都是这么验的,6 条样例级别即可)。
  • 假阳性只能改规则;把它加进 BASELINE 是洗规则,会把真问题一起豁免掉。
  • 加新的 must-use HAL 族:改 HAL_STATUS_RE,同时补一条正反例自测。
  • 改标签名:脚本与基线一次改完(§6 第 9 条),随后 --emit-baseline 复核数量不变。
  • 门禁自己的注释里不要写只有本次会话懂的代号;要引用规范就写文件.md + 紧邻编号。

9. 变更记录

(本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。)