如果你正在用 AI 辅助开发嵌入式固件,让 AI 写过 bootloader hook、烧录流程、验签逻辑,然后把代码推到线上后发现设备行为完全不对、测试全绿但真机 FAIL、或者线上还在跑旧版本。这篇文章就是为你写的。

接下来六天,我把 meta-pass v1.0 的十三次弯路重新梳理了一遍。大多数坑不是技术问题,是同一个原因:任务描述没写清楚什么是「完成」。

文章先给出问题的明确判断,然后用这六天里踩过的真实事例作为证据,最后给出行动建议。


问题的判断

AI 能写出正确的代码,但不会自己判断任务是否结束。

它会编译通过、会跑通测试、会提交代码,但不会去验证线上部署是不是滞后、不会检查二进制里有没有真的把功能链接进去、不会确认测试用的是不是真实数据。

这些判断必须由人来写。写在哪里?写在任务描述里。

这篇文章把十三次弯路提炼成六条原则,作为 meta-pass 项目后续所有需求开发的前置约定。


原则一:验收标准必须有,且必须具体到命令

这个原则回答什么问题:你怎么知道任务做完了?

AI 没有「验收标准」这个概念。它看到「实现 bootloader hook」,就会写代码、跑编译、提交 PR。它不会去想「我怎么知道这事做完了」。

具体 bug

bootloader hook 第一次编译全绿,但检查二进制发现 hook 根本没被链接进去。新建组件目录不触发 cmake 重新配置,组件缺 CMakeLists.txt1。

原始任务描述

实现 bootloader hook 功能,让设备每次开机都回到启动器。验证配置修改已生效。

缺失分析

这条描述没有回答「怎么知道完成了」。AI 收到后会自动执行:写代码 → 编译 → 提交。但它不会主动验证 hook 是否真的进了二进制,也不会检查 sdkconfig 里的 BT 优化配置是否真的生效。

「验证配置修改已生效」是一个模糊的目标描述,不是验收条件。AI 不知道什么叫「生效」,也不知道该查什么。

模板骨架

任务:一句话描述要做什么
验收标准:
  1. 命令级别验证(nm / grep / ls / 串口日志等)
  2. 具体输出预期(=n / =y / 有输出 / 无输出)
  3. 边界条件(文件大小上限、版本号范围等)
  4. 最后一步:跑一次完整流程,确认端到端正常

重写后

我要让设备每次开机都回到启动器,不让它直接进入上次跑的固件。bootloader hook 已经实现了,但要确认两件事。

第一,hook 得真的进了二进制。我在 Mac 上跑 nm 命令看符号表,确认 bootloader_after_init 这个符号存在;然后在 map 文件里搜 hook called,看到记录就对了;最后 QEMU 跑一次,串口日志里出现 hook executed 才算过关。

第二,sdkconfig 里的 BT 和优化配置得真的生效了。跑 grep 命令查 sdkconfig,BT 和编译器优化这两个配置项的值跟我期望的不一样,那就是没生效。另外看一眼固件二进制的大小,不要超过 1.5MB 就行。

以上都过了,这个任务就算完成。


原则二:必须声明环境约束

这个原则回答什么问题:你的代码可以在什么环境下运行?

AI 不知道「环境假设」这个概念。它不会主动检查当前环境是否有残留文件、是否依赖本地状态。它会在任何它能写到的地方放代码,不会自动选择唯一正确的目录。

具体 bug

改完 sdkconfig.defaults 后 idf.py build,IDF 直接复用旧的 sdkconfig 而不重新生成2。有一个测试直接读本地 build/ 目录的构建产物做比对,本地全绿、CI 裸 checkout 后崩3。

原始任务描述

修复这个测试失败的问题。

缺失分析

这条描述没有说明问题发生的场景,也没有说明期望的验证环境。AI 收到后可能在本地 build/ 目录上找原因,而这个问题本质是测试依赖了本地状态,CI 没有这个状态所以崩。

更严重的是,任务描述没有排除错误的环境假设。AI 不知道哪些环境状态是「允许的」、哪些是「不允许的」。

模板骨架

任务:一句话描述要做什么
环境约束:
  - 禁止依赖本地状态(build/、sdkconfig 缓存等)
  - 必须在干净 checkout 环境下运行
  - 测试数据必须能版本化
  - 改完 XXX 之后必须先做 YYY 再运行 ZZZ

重写后

这个测试在本地能过,但在 CI 上崩。原因是测试直接读了 build/ 目录里的东西,CI 没有这个目录。你要改的是让测试从仓库里的固定数据读取,不要依赖本地构建产物。改完之后在干净的 checkout 环境里跑一次确认,别再用本地的 build/ 目录了。


原则三:验证必须覆盖部署状态和真实数据

这个原则回答什么问题:你怎么知道上线的东西是对的?

AI 没有「部署状态」和「真实数据」这两个概念。它修完代码、跑完测试、提交 PR,就会认为任务完成。它不会去检查线上版本对不对,也不会去确认测试用的是不是真实固件镜像。

具体 bug

BUG-03 排查三天4:仓库代码修复为「单写尾扇区」流程,但线上 https://meta-pass.pages.dev/ 还是旧版(pre-dbbd091),执行三写流程把签名扇区擦掉了。真机表现为「签名固件显示未签名」。

宿主集成测试对合成镜像验签全 PASS,但真机行为与预期不符5。合成 fixture 只能验证逻辑正确性,不能验证真实二进制格式、真实密钥链、真实设备行为。

原始任务描述

修复这个签名验证失败的 bug。

缺失分析

这条描述没有要求验证部署状态,也没有指定测试数据的要求。AI 收到后可能只做两件事:改代码 → 跑测试。但它不会检查线上版本对不对,也不会用真实固件做验证。

「修复签名验证失败的 bug」是一个结果描述,不是验收标准。AI 不知道「修复」意味着什么。

模板骨架

任务:一句话描述要做什么
验收标准:
  1. 代码修复
  2. 部署验证(curl 比对线上文件与仓库源文件)
  3. 真机复现(在实体设备上跑一遍完整流程)
  4. 数据要求(必须用真实数据,不能用合成数据)

重写后

这个 bug 是在真机上发现的:用了新版本的安装脚本之后,签名固件在设备上显示为未签名。你先修代码,然后做两件事。

第一,重新部署到线上环境,部署完用 curl 抓取线上的 JS 文件,跟仓库里的源文件对比一下,确认部署上去的是最新代码。

第二,在真机上跑一遍完整流程,串口日志里能看到签名校验通过才算修完。

另外,验证的时候用真实的固件镜像,不要用合成的测试数据。合成数据结构和真机不一样,合成数据能通过不代表真机能过。


原则四:协议修改必须先读对端源码

这个原则回答什么问题:改客户端之前,你需要知道什么?

AI 不知道协议的对端长什么样。它只看自己负责的代码,不会去想对端可能期望什么。它不会主动去读服务端源码,也不会主动去对照参照实现。

具体 bug

esptool-js 不读 MD5 digest 帧,备份必败。前三轮补丁(分块大小、重试次数、超时设置)全无效6。后来逐行读 stub 源码才发现:handle_flash_read 在数据帧发完后无条件追加 16 字节 MD5 digest,但 esptool-js 从来不读这一帧6。残帧留在传输缓冲里,每次 readFlash 调用都毒化下一次响应,协议错位累积导致备份必败。

原始任务描述

修一下备份功能,现在经常失败。

缺失分析

这条描述只说了现象,没说根因。AI 收到后会进入「盲目优化」模式:调大分块大小、加重试、延长时间。这三个方向单独看都是合理的,但在协议错位的前提下,任何优化都是徒劳。

三轮补丁花了三周,真正解决问题只花了一天:读完 stub 源码,写出协议交互时序图,然后对照 esptool.py 的参照实现补上了 MD5 digest 帧的读取和校验6。

模板骨架

任务:一句话描述要做什么
前置步骤:
  1. 读对端源码(确认帧格式、协议行为)
  2. 对照参照实现(看官方库怎么处理)
  3. 写出协议交互时序图
  4. 用 mock 服务端测试客户端
执行步骤:
  根据协议理解进行修改

重写后

备份功能不稳定,我先试了几个方向:调大了分块大小、加了重试、延长了超时,都没用。后来我去看了固件侧的 stub 源码,发现 handle_flash_read 在每个数据帧后面都会追加 16 字节的 MD5 digest,但 esptool-js 完全不读这一帧。这 16 个字节留在缓冲里,每次都把下一个响应搞乱,所以备份必然失败。

你要修的话,先去读 components/stub/src/esp_flash_hal.c 里的 handle_flash_read,搞清楚帧格式,然后对照 esptool.py 的实现,把 MD5 digest 帧的读取逻辑补上。不要在没有搞清楚对端协议的情况下直接改客户端代码。


原则五:部署物必须有版本追踪和单一来源

这个原则回答什么问题:你怎么知道线上是哪个版本?你怎么知道代码只有一个副本?

AI 不知道「版本追踪」和「单一来源」的概念。它不会主动在部署物里嵌入版本号,不会主动检查线上版本是否与仓库一致,也不会在代码存放位置上做选择。

具体 bug

排查「签名验证不上」三天,最后发现线上安装页是旧版本7。同源码本地构建与 CI 构建哈希不同,因为 ESP-IDF 默认嵌入编译时间戳 CONFIG_APP_COMPILE_TIME_DATE8。导致 plays 市场固件与 GitHub Release 固件同版本两个字节流。

dev 副本与发布副本必然漂移,第二次 writeFlash 擦掉了已写的签名9。dev 副本 blobOffset 传分区大小而非镜像长度,name blob 写到槽位末尾外 4056 字节(slot 0 写进 cardid NVS 分区),双写擦掉 MSIG9。

原始任务描述

修一下安装页的 bug,然后部署到线上。

缺失分析

这条描述没有规定版本追踪要求,也没有规定代码存放约束。AI 收到后会修代码、部署,但它不会在部署后检查版本号对不对,也不会注意代码应该放在哪个目录。

dev 副本漂移的本质是:同一份规范维护了两份副本,任何一方的修改都不会同步到另一方9。结构性修复是 server.mjs 直接服务规范的 install-slot/ 目录,杜绝开发副本漂移9。

模板骨架

任务:一句话描述要做什么
版本追踪要求:
  - 固件:git describe 版本号嵌入二进制
  - 网页:首行日志打印部署时的 git SHA
  - 发布后用 curl 抓取响应头确认版本号
代码存放约束:
  - 所有代码只放在单一目录
  - dev server 和部署都从这个目录读取
  - 禁止在别处维护副本

重写后

安装页有个 bug,修完部署到线上之后,我要确认两件事。

第一,部署上去的是最新代码。部署完用 curl 看响应头里的版本号,跟仓库里的 git describe 对上。如果线上版本号比仓库旧,说明部署滞后了,要重推。

第二,代码只有一个来源。所有 install-slot 相关的文件都放在 install-slot 目录下,dev server 和 Pages 部署都从这个目录读取,禁止在别处维护副本。如果有人改了别的地方,那一定是漏了同步。

另外,固件的二进制里要嵌入 git 版本号,这样刷完设备之后从串口能看出来跑的是哪个版本。


原则六:设备行为类任务必须有真机证据

这个原则回答什么问题:你怎么知道设备真的按预期工作了?

AI 没有「真机证据」这个概念。它会用逻辑推理「应该能工作」,但不会去验证。它会写「理论上应该工作」,但不会提供串口日志或屏幕截图。

具体 bug

各种推断「应该可以」,但设备行为完全不符合预期10。宿主测试全绿,真机 FAIL,两者之间的 gap 需要字节级回放和真实验签器才能定位11。

宿主测试对合成镜像验签全 PASS,但真机签名验证失败5。根因是签名脚本读的 image_len 字段语义偏差(从文件大小读而非从镜像头读),这个偏差在合成 fixture 的固定结构中恰好不暴露,但在真实镜像中必然触发5。

原始任务描述

修一下签名验证的逻辑,现在设备上不认。

缺失分析

这条描述只说了现象,没有规定验证方式。AI 收到后可能只改代码、跑宿主测试,然后提交 PR。但它不会在真机上验证,也不会提供串口日志作为证据。

「应该可以」不等于「确实可以」。逻辑正确不等于设备行为正确。嵌入式系统的行为受 Flash 布局、协议时序、硬件状态等多重因素影响,任何一环与预期不符,设备行为就会偏离。

模板骨架

任务:一句话描述要做什么
验证要求:
  1. 必须提供串口日志或屏幕截图作为证据
  2. 禁止只写「理论上应该工作」
  3. 任务完成后,在实体设备上跑一次完整流程

重写后

签名验证的逻辑改了之后,宿主测试全绿,但真机不认。原因是签名脚本读 image_len 的方式有问题:它从文件大小读,而不是从镜像头读。合成测试数据用的固定结构刚好不暴露这个问题,但真机固件的头部结构和合成数据不一样,真机就跑不出来。

你修完之后,不要只看宿主测试。拿一个真实的固件镜像,在真机上跑一遍完整流程,把串口日志截下来发给我看。没有串口日志或者截图,这个任务不算完成。另外,如果改动涉及设备行为,禁止只写「理论上应该可以」这种话,必须用真机证据说话。


总结:三条元原则

把这六条归纳成三条元原则:

元原则一:任务描述必须自洽 AI 不可能从上下文推断出未写明的要求。所有约束、所有验证标准、所有环境假设,必须全部写在任务描述里。

元原则二:任务描述必须可执行 每一条验收标准都必须能被一条命令或一个观测动作验证。不能写「看起来正确」「应该没问题」这种无法判断的表述。

元原则三:任务描述必须覆盖 AI 的盲区 AI 会写代码,但不会验证部署、不会检查线上版本、不会判断协议对齐。这些盲区必须由人写进任务描述。


交付:测试验证脚手架 Skill

把六条原则提炼成一个可复用的 skill 文件。以后每次让 AI 写嵌入式固件的任务描述前,先用这个 skill 生成模板,再填入具体信息。

# skill: firmware-task-scaffold
# 用途:从模糊任务描述推导出完整的验收模板
# 用法:输入模糊描述 → 输出带模板骨架的任务描述

task_template:
  title: "任务标题(一句话描述要做什么)"
  description: "任务背景和问题现象"

  acceptance_criteria:
    - command: "nm build/meta-pass.elf | grep <symbol>"
      expected: "有输出"
    - command: "grep -r <keyword> build/meta-pass.map"
      expected: "有记录"
    - command: "QEMU 跑一次,串口日志包含 <message>"
      expected: "看到预期日志"
    - command: "grep CONFIG_<X> build/sdkconfig"
      expected: "=n 或 =y"
    - command: "ls -la build/meta-pass.bin"
      expected: "大小 < 1.5MB"

  environment_constraints:
    - "禁止依赖本地 build/ 目录"
    - "禁止依赖网络可达性"
    - "必须在干净 checkout 环境下运行"
    - "测试数据必须能版本化"

  verification_steps:
    - step: 1
      action: "代码修复"
    - step: 2
      action: "重新部署到线上"
    - step: 3
      action: "curl 比对线上文件与仓库源文件"
    - step: 4
      action: "在真机上跑完整流程"
    - step: 5
      action: "提供串口日志或截图"

  data_requirements:
    - "必须用真实固件镜像,不用合成数据"
    - "测试数据必须版本化"

  deployment_checks:
    - "固件嵌入 git describe 版本号"
    - "网页首行日志打印部署时 git SHA"
    - "发布后用 curl -I 确认版本号"

  code_placement:
    - "所有 install-slot 代码只放在 install-slot/ 目录"
    - "dev server 和 Pages 部署都从这个目录读取"
    - "禁止在别处维护副本"

使用方式:

  1. 收到模糊任务描述后,先对照 skill 中的模板骨架逐项检查缺失项
  2. 缺失项填入具体命令、预期输出、约束条件
  3. 输出一份完整的任务描述,包含验收标准、环境约束、验证步骤

参考


  1. 编译通过 ≠ 功能在里面来源:docs/assets/meta-pass-v1-retrospective.zh_CN.md 教训 2。 ↩︎

  2. sdkconfig 屏蔽数据:两次构建尺寸差 21%(1,283,232 vs 1,024,608 字节)。来源:nmem memory 09741cfa(2026-09-13)。 ↩︎

  3. 测试依赖本地 build/ 来源:docs/assets/meta-pass-v1-retrospective.zh_CN.md 教训 4。 ↩︎

  4. BUG-03 排查耗时三天。来源:docs/assets/handoff-unsigned-rootcause.zh_CN.md §0。 ↩︎

  5. 真实镜像验证必要性来源:docs/assets/handoff-unsigned-rootcause.zh_CN.md §2。 ↩︎ ↩︎ ↩︎

  6. esptool-js 不读 MD5 digest 帧根因:来源:docs/development/engineering/debugging-workflow.zh_CN.md §1 第四轮。 ↩︎ ↩︎ ↩︎

  7. 线上安装页滞后原因:来源:docs/assets/handoff-unsigned-rootcause.zh_CN.md §3.1。 ↩︎

  8. 哈希不一致原因:ESP-IDF 默认嵌入编译时间戳 CONFIG_APP_COMPILE_TIME_DATE。来源:nmem memory 17fcbeb8(2026-09-11)。 ↩︎

  9. dev 副本漂移根因:来源:nmem memory c158ba6f(2026-09-15)。 ↩︎ ↩︎ ↩︎ ↩︎

  10. 真机证据 > 推断来源:docs/assets/meta-pass-v1-retrospective.zh_CN.md 工程习惯。 ↩︎

  11. 宿主与真机结论相悖时的排查方法:docs/assets/handoff-unsigned-rootcause.zh_CN.md §0。 ↩︎