Files
STM32F4-Base/scripts/check_bsp_err.py
T
wandering1 d9b887c0af feat(S-5 R4): 返回值不得裸丢弃成为机械检查,并修掉照出的第一处 ECC 静默降级
B 的第一步(加规则)与第二步(修最小最值的那处)。用户裁定"B 按你说的进行"。

**为什么只能项目侧自己做**(两条实测负结果,已写进 §2.27 与 bsp_err.h 边界纪律 4):
· ARMCC v5 对 __attribute__((warn_unused_result)) 不生效(凌晨在 rs485_transmit 上实测:属性
  在位、调用点裸调不接返回值,构建仍 0 错 0 警告);
· cppcheck 也照不出来——按本仓自己的 --enable=warning,style,performance,portability 跑
  gd5f2gq5ue.c,对 14 处丢弃 HAL_SPI_* 返回值、以及"未初始化局部量直接进 P_FAIL 判定"
  **一条不报**(它假定 gd5f_read_status(&status) 会写出参,而失败路径根本不写)。
结论:**丢弃底层传输的返回值,今天没有任何工具会响**。

**R4 上线即照出 37 条存量丢弃**:gd5f2gq5ue.c 29(14 处 HAL + 15 处自家原语)、net_socket.c 8、
ui_net_sink.c 4(net_close)、rs485.c 2、nand_ftl.c 1。按增量拦截老规矩入基线、只减不增——
这是覆盖面扩大新暴露的历史债,不是新增违规;基线 19 → 1 → **38**。

**R4 自己被修三次(逐条记下,都是"规则比我想的更脆")**:
① 首版漏判"裸调用行尾带注释"这种写法(正则的 \s*$ 卡死),而那恰是最常见的"带说明的丢弃"
   → 补可选注释分支,并做 6 条正/反例自检;
② 把 net_set_errno 八处误判成丢弃(它是 setter,返回 int 是状态回声不是调用成败)→ **按真实
   反例把规则收窄**(谓词 + setter 两类豁免),而不是塞进基线洗掉假阳性;
③ must-use 名单只从**头文件**取,gd5f_set_feature 这类 static 原语整个在视野外——而下面那处
   真窟窿恰恰是 static、靠人工才发现 → 名单扩到 .c 内 static 定义(同一套启发式),补完立刻
   又照出 gd5f_bbt_save#1:**运行时标坏后 BBT 落盘结果被丢弃 = 坏块记账可能没持久化**,已入
   基线记为待还,本步不动行为。
另修一处自己的排版误判:R3 原本要求注释块出现字面"返回值",规范 V1.0 允许列对齐写法
「返 回 值:」→ 改为忽略空格匹配(门禁不该规定排版)。

**修掉的第一处真窟窿**:bbt_rebuild 的注释写着"无论扫描成败都须恢复 ECC,否则后续 I/O 在无
ECC 下运行",而它下面 gd5f_set_feature(0xB0,…) + gd5f_wait_busy(100) **返回值全丢**:SPI 一抖
ECC 就保持关闭,且 gd5f_check_ecc() 在 ECC 关闭时恒读"无 ECC 错"——整条链再没有任何信号。
补法复用 init 路径的 E-10(写完回读 B0h 验 ECC_EN 位真生效,"写了但没生效"与写失败同罪),
并把"关 ECC 失败"改成**立即中止**(此时 ECC 仍开着,是安全侧;带着 ECC 去扫 spare 反而会把
厂标好块误判成坏块,"重建 BBT"变成制造坏块)。恢复失败时按 (void)gd5f_bbt_load() 回读盘上
副本,避免 RAM 留着"比盘上更新"的表而下次上电又退回旧表。

**一条定位纠正(差点把修复记大)**:查链接映射发现 Removing gd5f2gq5ue.o
(i.gd5f2gq5ue_bbt_rebuild), (604 bytes)——该函数在产品镜像里**根本没被链接**(产品构建无调用
者),只由 ENABLE_STORAGE_RECOVERY_TESTS 的恢复构建经 testTask 调用。所以本次修的是**恢复流程
自身的安全**(跑恢复时若 ECC 没恢复,设备会带着"已恢复"的结论在无保护状态下继续跑),不是
现场日常路径;相应地镜像 Code 的 +28 B 也不是它带来的(未逐字节追,线索是
gd5f_private_get_feature 由单调用点变多调用点、内联决策可能变化)。验证因此不能只跑产品构建:
临时打开 TEST_SUITE_STORAGE + ENABLE_STORAGE_RECOVERY_TESTS 编一次(0 错 0 警告且该函数不再
出现在 Removing 列表),随即还原两处开关并重编产品构建核对(test/board 无残留差异)。

**编号门禁把我自己的自造编号拦下了**:我起初用 B-1/B2/B3/B4 标记这批缺陷,id-check 报"未登记
前缀"13 处(且 B2/B3/B4 会与真实里程碑号撞号)→ 全部改写成平实语言。这是那条"注释与文档里
不许用没人检索得到的内部阶段编号"的规则第一次在提交前拦住我,规则是对的。

验证:@build 0 错 0 警告,Code 213,648 → **213,676**、RO 18,860、ER_IROM1 **232,536 B
  / 22.2%**
(ROM 合计 233,724 B;RAM 逐字段不变);ram_report 重生成 + ram_baseline.py --update + 配图常量
同步(mem-numbers Passed);@test **PASS**(到 net init: OK 682 ms、[ERR] 0);R4 两处**探针
实证**(临时加一条裸调用 → 立刻成为新增并 RC=1;还原 → 38/38/0);--prune 点名本次还掉的 2 条
已删;pre-commit run 全绿 15 Passed;check_doc_links --all --sections-strict 147 篇
  0 问题;
id-check --strict 增量拦截通过。

文档:AGENTS.md 与 .pre-commit-config.yaml 的 bsp-err 口径(四条规则、基线 38);
pre-commit使用说明.md §2.27(R4 全量口径 + 两条负结果 + 名单来源与豁免形状 + 单行限度);
bsp_err.h 边界纪律 4;报告 §0.4 / §5.1 / §9 / 附录 A + 变更记录;存储维护与容量恢复.md 新增
§4.3(运维判据:出现 ECC restore FAILED / ECC not enabled 即本次恢复**不算完成**,须断电重启
重走 init,不许接着 nand_ftl_format() 当作已修好;并记明该函数产品镜像不链接)——顺带修该文
结构缺陷:原 ## 5. 变更记录 误插在 §4.1 与 §4.2 之间,会让 §4.2 变成 §5 的子节。
2026-10-23 09:15:41 +08:00

333 lines
15 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""BSP 错误码与超时命名约定检查(报告 S-5 的机械检查那一半)。
为什么要有这个脚本:约定写在 docs 里不会自己生效。同一层的驱动今天并存六套返回约定,
而"能不能重试"只写在个别注释里;叫 *_TIMEOUT 的东西里既有真毫秒(单位靠不统一的 _MS 后缀表达)、
也有**返回码**。没有机械检查,新代码只会继续按各自旧习惯长。
三条规则(只看 Drivers/BSP 下的公开头文件 = 对外 API 面;lftpd 按第三方排除):
R1 公开 API 不得返回 HAL_StatusTypeDef / bool
bool 把"为什么失败"整条丢掉;HAL 枚举把 HAL 焊进调用方的 include 链。
R2 *_TIMEOUT 宏不得取负值
名字叫 TIMEOUT 却是返回码,读代码的人会当成时长;超时应返回 BSP_ERR_TIMEOUT。
R3 非 void 的公开 API 必须在紧邻注释块里写「返回值:」
约定要能被读到,才算约定(也才谈得上审查)。
R4 返回错误码的函数,调用点必须**接住返回值**或**显式 (void) 丢弃**
名单自动取,不手写:① 返回 bsp_err_t 的公开 API(契约规定它就是错误码);
② 返回 int/int32_t 且文档块出现"错误码 / 失败 / BSP_ERR"的函数(覆盖 GD5F 私有码族);
③ 已知返回 HAL_StatusTypeDef 的 HAL 传输/读写族。为什么单列:实测 ARMCC v5 不认
warn_unused_result、cppcheck 也不报这类丢弃(都在本仓实跑验证过)——**丢弃底层传输
的返回值,今天没有任何工具会响**。已知限度:只认单行整句的裸调用,跨行写的暂测不到。
增量拦截(与 layer-deps / id-check 同构):BASELINE 钉住存量,只减不增,没有 --write。
修完一条跑 --prune,它会点名"这条已消失,请从 BASELINE 删除"。
退出码:0 = 通过;1 = 有新增违规;2 = 用法或扫描错误。
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
EXCLUDE = ("lftpd/",)
# 存量基线(2026-10-23 清点):"文件|规则|名字"。修掉一条删一行,不要往里加。
BASELINE = frozenset(
line.strip() for line in """
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Receive#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Receive#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Receive#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#10
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#11
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#4
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#5
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#6
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#7
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#8
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#9
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_bbt_save#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_block_erase#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_page_read#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_program_exec#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_program_load#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_from_cache#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_status#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_status#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_status#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_wait_busy#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#4
Drivers/BSP/GD5F2GQ5UE/nand_ftl.c|R4|gd5f_write_enable#1
Drivers/BSP/RS485/rs485.c|R4|rs485_receive_start#1
Drivers/BSP/RS485/rs485.c|R4|rs485_receive_start#2
Drivers/BSP/UI/ui_content_page.h|R1|ui_content_page_open
Drivers/BSP/UI/ui_net_sink.c|R4|net_close#1
Drivers/BSP/UI/ui_net_sink.c|R4|net_close#2
Drivers/BSP/UI/ui_net_sink.c|R4|net_close#3
Drivers/BSP/UI/ui_net_sink.c|R4|net_close#4
Drivers/BSP/UI/ui_net_sink.c|R4|net_close#5
Drivers/BSP/UI/ui_net_sink.c|R4|net_close#6
""".splitlines() if line.strip()
)
DEFINE_RE = re.compile(r"^\s*#define\s+([A-Z][A-Z_0-9]*TIMEOUT[A-Z_0-9]*)\s+(-[0-9]+)",
re.MULTILINE);
# bool 用在"提问"上是合法的(is_editable / has_children / is_active…);只有拿 bool 当
# **错误通道**才是问题。用名字形状区分,避免把 7 个正当谓词误判成违规、再拿基线去洗规则。
PREDICATE_RE = re.compile(r"(^|_)(is|has|can|should|need|valid|active|open|empty)_|" +
r"_(is|has|can|valid)$")
DECL_RE = re.compile(r"^\s*(?:extern\s+)?([A-Za-z_][A-Za-z_0-9_ ]*[ ]+\**)"
r"([a-z_][a-z_0-9]*)\s*\([^;{]*\)\s*;", re.MULTILINE);
def tracked_headers():
"""git 跟踪的 BSP 公开头文件(排除第三方 lftpd)。"""
out = subprocess.run(["git", "ls-files", "--", "Drivers/BSP"],
cwd=str(ROOT), capture_output=True, text=True)
if out.returncode != 0:
print("git ls-files 失败:", out.stderr.strip())
raise SystemExit(2)
files = []
for rel in out.stdout.split():
rel = rel.replace("\\", "/")
if not rel.endswith(".h"):
continue
if any(part in rel for part in EXCLUDE):
continue
files.append(rel)
return sorted(files)
def doc_block_before(lines, idx):
"""取声明上方紧邻的注释块文本(向上直到 /* 或空行为止)。"""
buf = []
j = idx - 1
while j >= 0:
t = lines[j].strip()
if t.endswith("*/") or t.startswith("*") or t.startswith("/*"):
buf.append(t)
if t.startswith("/*"):
break
j -= 1
continue
break
return "\n".join(buf)
HAL_STATUS_RE = re.compile(r"^HAL_(?:SPI|I2C|UART)_(?:Transmit|Receive|TransmitReceive)"
r"(?:_DMA|_IT)?$|^HAL_I2C_Mem_(?:Read|Write)$")
BARE_CALL_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z_0-9]*)\s*\(.*\);"
r"(?:\s*/\*.*?\*/)?\s*$") # 行尾注释不影响判定
# 两类形状不算"丢弃错误码",也不许塞进基线——同 R1 谓词白名单的理由:**规则该按真实
# 反例收窄,而不是拿基线去洗假阳性**:
# · 谓词:返回值是"提问"而不是"办成没有"(is_/has_/can_/should_/need_/valid_);
# · setter:形如 xxx_set_yyy(...) 的写入口,它返回的 int 是状态回声而不是调用成败
# (实跑误判出 net_set_errno 八处——它只是把值写进调用方自己的 errno 槽)。
ALLOW_BARE_RE = re.compile(
r"(?:(?:^|_)(?:is|has|can|should|need|valid)_)|(?:^|_)set_[a-z0-9_]+$"
r"|(?:^|_)mark_block_bad$")
def must_use_names():
"""自动取"返回错误码"的函数名(不手写名单,避免名单与代码脱节)。
两条来源用**同一套启发式**:① 头文件里的公开声明;② .c 里的 static 定义(只从头文件
取会漏掉 static——实测 gd5f_set_feature 等就是 static 且返回错误码,上面提到的 ECC 那两行丢弃
正是这类,靠人工才发现)。
"""
names = {}
for hdr in tracked_headers():
try:
htext = (ROOT / hdr).read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", hdr, exc)
raise SystemExit(2)
hlines = htext.splitlines()
for m in DECL_RE.finditer(htext):
ret = m.group(1).replace("const", "").strip()
hname = m.group(2)
flat = ret.replace(" ", "")
hdoc = doc_block_before(hlines, htext[:m.start()].count(chr(10)))
if flat.endswith("bsp_err_t"):
names.setdefault(hname, "返回 bsp_err_t")
elif flat in ("int", "int32_t") and re.search("错误码|失败|BSP_ERR|_FAIL", hdoc):
names.setdefault(hname, "返回 int 且文档声明会失败")
for src_c in tracked_sources():
try:
ctext = (ROOT / src_c).read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", src_c, exc)
raise SystemExit(2)
clines = ctext.splitlines()
for cname, cidx in static_int_defs(ctext):
cdoc = doc_block_before(clines, cidx)
if re.search("错误码|失败|BSP_ERR|_FAIL", cdoc):
names.setdefault(cname, ".c 内 static 且文档声明会失败")
return names
def tracked_sources():
"""git 跟踪的 BSP 源文件(排除第三方 lftpd)。"""
out = subprocess.run(["git", "ls-files", "--", "Drivers/BSP"],
cwd=str(ROOT), capture_output=True, text=True)
if out.returncode != 0:
print("git ls-files 失败:", out.stderr.strip())
raise SystemExit(2)
files = []
for rel in out.stdout.split():
rel = rel.replace(chr(92), "/")
if not rel.endswith(".c"):
continue
if "lftpd/" in rel:
continue
files.append(rel)
return sorted(files)
def static_int_defs(text):
"""从 .c 里挑出"返回 int/int32_t 的 static 函数定义"(不引正则,按形状扫)。
为什么需要:只从**头文件**取 must-use 名单会漏掉 static 函数——实测
gd5f_set_feature / gd5f_private_get_feature / gd5f_private_bbt_find_best 都是 static,
它们同样返回错误码,却在名单之外,等于门禁只守跨编译单元的边界、看不见编译
单元内部的丢弃(ECC 那两行就是靠人工发现的,不是靠这条规则)。
"""
hits = []
lines = text.splitlines()
brace = chr(123)
for idx, line in enumerate(lines):
t = line.strip()
if not t.endswith(brace):
continue
if "(" not in t or t.startswith("if") or t.startswith("for"):
continue
if not (t.startswith("static ") or " static " in t):
continue
head = t.split("(")[0]
words = head.replace("static", " ").split()
if len(words) < 2 or words[0] not in ("int", "int32_t"):
continue
hits.append((words[-1], idx))
return hits
def scan_r4(must_use):
"""R4:Drivers/BSP 下的 .c 里,对 must-use 函数的"裸调用"(不接、也不显式 (void))。"""
files = tracked_sources()
hits = []
for rel in files:
try:
text = (ROOT / rel).read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", rel, exc)
raise SystemExit(2)
seen = {}
for line in text.splitlines():
t = line.strip()
if t.startswith("*") or t.startswith("//") or t.startswith("/*"):
continue
m = BARE_CALL_RE.match(line)
if not m:
continue
name = m.group(1)
if name in ("if", "while", "for", "switch", "return", "sizeof", "else"):
continue
if name not in must_use and not HAL_STATUS_RE.match(name):
continue
if ALLOW_BARE_RE.search(name):
continue
seen[name] = seen.get(name, 0) + 1
hits.append((rel, "R4", "%s#%d" % (name, seen[name])))
return hits
def scan():
hits = []
for rel in tracked_headers():
try:
text = (ROOT / rel).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", rel, exc)
raise SystemExit(2)
lines = text.splitlines()
for m in DEFINE_RE.finditer(text):
hits.append((rel, "R2", m.group(1)))
for m in DECL_RE.finditer(text):
ret = m.group(1).replace("const", "").strip()
name = m.group(2)
flat = ret.replace(" ", "");
if name in ("void", "return", "if", "while", "sizeof", "typedef", "static"):
continue
is_bool = flat.endswith("bool") or flat.endswith("_Bool")
if flat.endswith("HAL_StatusTypeDef") or (is_bool and not PREDICATE_RE.search(name)):
hits.append((rel, "R1", name))
if flat == "void":
continue
idx = text[:m.start()].count("\n")
# 忽略空格:规范 V1.0 允许列对齐写法「返 回 值:」,那只是一种排版选择,
# 要求"必须写成不空格的形式"等于让门禁去规定排版,会把合规注释判成违规。
marker = doc_block_before(lines, idx).replace(" ", "").replace("\u3000", "")
if "返回值" not in marker:
hits.append((rel, "R3", name))
hits.extend(scan_r4(must_use_names()))
return hits
def main():
ap = argparse.ArgumentParser(description="BSP 错误码/超时命名约定(增量拦截)")
ap.add_argument("--verbose", action="store_true", help="列出全部命中与归类")
ap.add_argument("--prune", action="store_true", help="只报已消失、可从 BASELINE 删除的条目")
ap.add_argument("--emit-baseline", action="store_true",
help="打印当前全部命中的键(建基线用)")
args = ap.parse_args()
keys = {"%s|%s|%s" % h for h in scan()}
if args.emit_baseline:
for key in sorted(keys):
print(key)
print("# 共 %d 条" % len(keys))
return 0
gone = sorted(set(BASELINE) - keys)
fresh = sorted(keys - set(BASELINE))
if args.prune:
if not gone:
print("[prune] 基线 %d 条全部仍在" % len(BASELINE))
for key in gone:
print("[prune] 这条已消失,请从 BASELINE 删除:", key)
return 0
if args.verbose:
for key in sorted(keys):
print(" [%s] %s" % ("基线" if key in BASELINE else "新增", key))
print("BSP 约定:命中 %d(在册 %d / 新增 %d),基线共 %d"
% (len(keys), len(keys & set(BASELINE)), len(fresh), len(BASELINE)))
if fresh:
print("[FAIL] 新增违反 BSP 约定的条目:")
for key in fresh:
print(" -", key)
print(' 出路:① 返回 bsp_err_t(Drivers/BSP/bsp_err.h,0=成功、负值=失败,')
print(' 可重试判定用 BSP_ERR_CAN_RETRY);② 时长宏一律 *_MS 且必须是时间,')
print(' 若将来出现按循环次数计时的等待,改名 *_LOOP 并注释写明非时间量;')
print(' ③ 返回码不许叫 *_TIMEOUT。')
return 1
if gone:
print("[提醒] 有 %d 条基线已还清,请用 --prune 列出并从 BASELINE 删除" % len(gone))
return 0
if __name__ == "__main__":
raise SystemExit(main())