16be01915d
提交后台跑 scripts/ci_local.py 时 end-of-file-fixer 报 Failed 并就地修复: 17 篇补文件末尾换行、1 篇去掉行尾空行。 内容零变化:git diff --ignore-all-space 只有 1 处空行删除;18 篇均在 上一笔流水清理里被改过,恢复补丁时把上一轮的 EOF 修复冲掉了,这一 笔补回。
12 KiB
12 KiB
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 的"哪些函数算返回错误码"是自动取名单,不手写(手写名单必然与代码脱节):
- 头文件里返回
bsp_err_t的公开 API(契约规定bsp_err_t就是错误码); - 返回
int/int32_t且其文档块出现"错误码 / 失败 / BSP_ERR /_FAIL"的函数; - 已知返回
HAL_StatusTypeDef的 HAL 族(HAL_{SPI,I2C,UART}_{Transmit,Receive,TransmitReceive}及其_DMA/_IT变体、HAL_I2C_Mem_{Read,Write}); - 与 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. 限制与盲区(读这篇最重要的部分:绿灯不等于没问题)
- 不管错误码用得对不对。它只判通道类型、命名、有没有文档、有没有被消费。把
BSP_ERR_TIMEOUT用在"参数非法"上,它一声不响——那是语义问题,机器判不了。 - 范围只有
Drivers/BSP。App/**、simulator/**、Src/**不在面内;应用层的同类问题 今天仍然只能靠评审。 consume-result只认单行整句的裸调用(foo(...);一行写完,行尾注释允许)。跨行写的调用 ——参数换行、或;在下一行——按正则形状测不到。这是判据的物理限度,不是漏配。- 名单是启发式:靠"返回类型 + 注释关键词"认函数。注释里不写"错误码/失败"的函数会漏
(假阴性);反过来,把谓词写成
foo_ok()这种不在豁免形状里的名字就会被判违规(假阳性, 见 §7 的两次收窄)。判据依赖命名形状,改名就能绕过规则——这是所有基于正则的门禁共同的洞。 - HAL 族是固定清单(§1 第 3 条)。清单外的返回
HAL_StatusTypeDef的函数(如 FLASH、SD 卡 那批)不在名单里;将来用到就得多补一行正则,属于人工维护的盲区。 err-type/return-doc只看头文件里的声明形状:用宏包出来的 API、函数指针表里的 API、 通过typedef起的函数别名,都不在它的视野里。return-doc的判据是"紧邻注释块里出现『返回值』字样",散文里出现这三个字也会放行。 这不是假想:本仓gd5f_write_enable、gd5f_read_status曾长期"通过",原因竟是它们的 函数说明里顺口写了"返回值"三个字,而不是真有一行返回值说明。一条会因为散文里出现关键词 而放行的规则,等于在奖励运气——已改为忽略空格后匹配(规范 V1.0 允许列对齐写法「返 回 值:」, 门禁不规定排版),但"关键词出现即放行"的性质没有变,只能靠评审看内容。- 基线键带出现序号(
gd5f_read_status#2)。含义是"该文件里该函数的第 2 处丢弃",不是位置。 于是:删掉靠前的丢弃会让后面的序号整体前移,--prune只报"消失的键",不会告诉你某处只是 搬了家;反过来,同函数新增一处丢弃会多出一个新序号 → 会被判新增(这点是好的,实测过)。 - 改标签名 = 改基线键。今天把
R1~R4换成现名时,必须脚本与 38 条基线一起换,中途跑--prune会把改名读成"38 条全还清 + 38 条新账"。同理,今后任何标签改名都是一次原子改动, 不能分两个提交。 - 不提供重试 helper,也不检查重试策略(有上限吗、喂看门狗了吗、跨就绪屏障等待了吗)——
见
Drivers/BSP/bsp_err.h边界纪律 5:一个通用while包装太容易被拿去包住无限等待。
7. 豁免与基线
- 基线在脚本里的
BASELINE常量(不是外部台账文件),只减不增,没有--write。 - 当前在册 38 条(2026-10-23):
·
consume-result37 条 ——gd5f2gq5ue.c28(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.c6(全是net_close)、rs485.c2(rs485_receive_start)、nand_ftl.c1; ·err-type1 条 ——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。)