AI 生成代码几秒钟,为什么验证却要半个月?这个问题我琢磨了很久,直到自己亲手用 Copilot 和 Claude Code 写完一版蓝牙协议栈的移植代码,编译通过、静态检查通过,然后烧到板子上第一轮就翻车,我才真正意识到:在嵌入式这个行当里,“代码能跑”和“代码能用”之间的距离,比大多数人想象的要远得多。
先说结论:AI 生成代码本身不是问题,问题在于嵌入式软件对“正确性”的定义比互联网软件严苛得多。你写一个 Web 后端,逻辑错了顶多报个 500,重启一下就好;但在嵌入式里,一个指针越界可能直接踩掉中断向量表,一个时序竞争可能让电机在没有任何报错的情况下突然反转。更不用提那些跑在量产设备上的固件,一旦出了问题,轻则返厂,重则出安全事故。所以,AI 几秒钟生成的代码,我们花半个月去验证,这笔账不仅划算,而且是必须的。
这篇文章我不会聊太多概念层面的“AI 赋能嵌入式”,那个太虚。我想花点时间,从一个实际做嵌入式软件验证的人的角度,拆一拆这套验证体系到底由哪些环节组成,每个环节为什么绕不开,以及你真正落地时会踩到哪些坑。
1. AI 生成代码的“快”,恰恰是验证压力的来源
1.1 同样的代码,放在不同语境里,风险完全不同
如果你让 AI 写一段 Python 脚本处理 Excel 表格,生成之后你肉眼扫一眼,再跑两个样例数据,基本就敢用了。但同样一段逻辑,如果用 C 语言跑在 Cortex-M 内核的 MCU 上,你需要确认的东西多一个量级:
- 这段代码用了多少栈?最坏情况下会不会溢出?
- 有没有隐式的动态内存分配?在长期运行的设备里,堆碎片会不会导致内存耗尽?
- 中断上下文里能不能安全调用这些函数?有没有不可重入的风险?
- 编译器会不会因为优化选项把某个 volatile 变量的访问给优化掉?
- 目标芯片的内存映射、外设寄存器地址、总线等待周期,代码是否都考虑到了?
任何一个问题没有验证清楚,这段 AI 生成的代码即使语法完美、逻辑自洽,也等于一颗定时炸弹。
1.2 生成速度越快,代码审查的负担越重
我见过不少团队,引入 AI 编程工具之后,代码合并请求(MR)的数量翻了好几倍。以前一个人一天写 200 行 C 代码已经不少了,现在 AI 十分钟就能生成 200 行,而且看起来很像模像样。但问题是,代码量上去了,审查的人没变,审查的时间也没变。
这就会造成一个很实际的问题:审查者开始走马观花。我看过太多“AI 生成的代码,逻辑上似乎没问题,但风格完全不是团队风格”的 MR 被轻易放过去,因为代码实在太长了,人眼根本盯不过来。等到集成测试或者路试阶段出了问题,再回头排查,成本已经是生成阶段的几十倍。
所以我在团队里定了一个规矩:AI 生成的代码,必须由另一名工程师逐行走查,并且要写走查记录。这条规矩看着很笨,但它反而让团队形成了正向循环——AI 生成得快,审查也跟得上,整体质量才没有崩掉。
1.3 “快”不代表“正确”,一句注释也可能决定生死
还有一个容易被忽略的地方:AI 生成的代码里,注释和代码之间可能存在严重的不一致。我们团队有一次排查一个非常隐蔽的 bug,函数名和变量名都叫req_send_timeout,逻辑看起来也合理,但实际行为却完全不是超时控制的逻辑。后来发现,AI 是根据一个给普通工程师看的注释生成这个函数的,而注释描述的场景跟这个模块的实际需求差了十万八千里。
这种事不是个例。AI 对自然语言的理解是统计性的,它并不知道你的硬件手册里那句话说“此寄存器写 1 清零”意味着什么。所以,代码审查的时候,我特别提醒团队:不要只看代码本身,要把注释和代码对照起来看,更要把 AI 参考的那段文档或者需求描述拿出来看。验证的不只是代码,还有代码背后那个逻辑链。
2. 嵌入式代码验证的特殊性:编译通过只是万里长征第一步
2.1 静态分析和编译警告是底线,不是亮点
很多刚从互联网转过来的工程师有个习惯:编译通过、没有 warning,就觉得代码已经写完了。但在嵌入式场景里,这充其量只是“代码上了手术台,还没开刀”而已。
我们常规的静态检查至少包括几个层次:
- 编译器开
-Wall -Wextra -Werror,把警告直接变成错误,强制消除隐患; - 用
cppcheck或者clang-tidy做语法层之外的分析,检查未初始化变量、空指针解引用、资源泄漏等; - 用
MISRA C:2012规则集做合规检查,特别是对指针操作、类型转换、控制流这几大类做约束。
AI 生成的代码,在 MISRA 规则下往往能一次性通过率不高。比如,AI 很喜欢用memcpy做结构体拷贝,但如果目标是嵌入式系统,尤其是安全性要求比较高的场景,这会直接违反 MISRA 规则,因为这种拷贝方式对内存对齐和结构体中间可能存在的 padding 太敏感了。
静态分析这块,我的建议是:不要用默认参数直接跑,一定要针对你的目标编译器、目标架构、目标工程定制规则集。否则静态分析的结果会像 Google 翻译一样——语法上挑不出刺,但读起来总觉得哪里不对劲。
2.2 单元测试在嵌入式里为什么这么重
做嵌入式单元测试比做纯软件单元测试要麻烦,原因是硬件依赖。你不能指望每个函数都能在 PC 上跑起来,很多代码直接访问寄存器、操作外设,底层就是硬件,根本没有一个标准的运行时环境提供这些 API。
我们这边的做法是引入硬件抽象层(HAL),把代码里所有跟硬件相关的访问都封装成可替换的接口。这样,面向 PC 的单元测试就可以注入一个 mock 的 HAL,模拟出各种各样寄存器值、中断标志位和时序状态,把逻辑层测个底朝天。
举一个我们实际测过的例子:AI 帮我们生成了一段基于状态机的按键扫描逻辑,带 20ms 软件消抖和长按、短按判断。代码逻辑本身没有语法问题,但跑单元测试时发现,状态机在“按下 15ms 后释放再立刻按下”这种极端时序下,会进入一个未定义状态,所有后续按键事件全部失效。这种问题,靠人读代码也能发现,但靠人读代码的时间足够 AI 再生成十几版代码了。单元测试在这里的价值,是用机器取代了人眼去做这种最细粒度、最机械化、最不带感情色彩的检查。
2.3 覆盖率到底要看哪一行,有几个真实指标
我们内部定的覆盖率指标不是硬性的“90% 行覆盖”,而是拆成三个维度看:
| 指标 | 关注点 | 实际意义 |
|---|---|---|
| 行覆盖率 (Line Coverage) | 每行代码是否被执行 | 防止伪代码、死代码混入 |
| 分支覆盖率 (Branch Coverage) | 每个条件分支的 true/false 是否都跑到 | 防止边界值判断出错 |
| MC/DC 覆盖率 | 每个条件独立影响结果 | 关键安全逻辑必须做到 |
MC/DC 这一项,AI 生成的代码通常很难达标。原因也不难理解,AI 生成代码的思路是“把看起来对的逻辑写出来”,并不会主动构造测试用例去论证每个条件独立作用的场景。所以,AI 生成代码之后,我们往往要人工补一大批 MC/DC 用例,这也是验证周期被拉长的重要原因之一。
提示:如果你们的产品要做功能安全认证(比如 IEC 61508、ISO 26262),MC/DC 覆盖率不是可选项,而是硬性指标。这一块没有捷径可走,只能老老实实把用例堆出来。
3. 从编译到上路:分层验证体系到底怎么设计
3.1 分层验证的整体思路
嵌入式 AI 生成代码的验证体系,我倾向于用一个四层金字塔来描述:
- 静态分析层:不运行代码,只做语法、语义、风格、规则检查,筛掉明显的问题。
- 动态单元测试层:在 PC 或模拟器上运行逻辑代码,用 mock 屏蔽硬件依赖,验证纯逻辑正确性。
- 硬件在环测试层(HIL):把代码烧到真实芯片或者开发板上,通过调试器、逻辑分析仪、信号发生器等设备,验证代码在真实硬件上的表现。
- 整机路试层:把设备放到实际使用环境里,跑完整的场景,验证整个系统的协同行为、长期稳定性、异常恢复能力。
每一层都有明确的入口条件和出口条件,达不到标准不允许进入下一层。这个体系不复杂,但很多团队跳层,导致问题被不断往后推。
3.2 静态分析层:入口条件与实操清单
入口条件其实很简单:代码能过编译,能过定制化的静态分析规则集。但出口条件我们要严格一些:所有高优先级告警清零,中优先级告警必须有明确解释和责任人签字。不允许出现“先放着以后再说”这种情况。
实操时你会发现,AI 生成的代码在静态分析层最容易出的问题不是逻辑错误,而是风格不一致和“过度工程”。
- 风格不一致:AI 可能在同一份代码里混用
uint8_t和unsigned char,混用typedef struct和直接struct,看起来不优雅,但不至于出 bug。可一旦团队维护者习惯了某种风格,这种不一致就会像牛皮癣一样,反复出现。 - 过度工程:AI 可能给一个 simple 变量生成了三个不同的 getter/setter,还加了个带互斥锁的单例模式。在嵌入式里,这种过度设计不仅浪费 Flash,还增加静态分析的复杂度。
所以,静态分析阶段的关注点应该是:筛掉影响安全和可维护性的硬伤,再花少量时间处理一致的代码风格。不要指望 AI 自动遵守团队的 MISRA 配置,这个只能靠规则集约束。
3.3 单元测试层:怎么让 AI 生成的代码可测性好
AI 生成的代码,我们拿回来直接写单测,往往会发现两个问题:一是函数内部直接调用了硬件 API,二是函数之间耦合度太高,没法单独摘出来测。
这时候不能硬着头皮去测,要先做一点简单的重构,把硬件依赖隔离出去。比如:
// 改造前 uint8_t read_button_state(void) { return (uint8_t)(GPIOA->IDR & GPIO_PIN_0); } // 改造后 static uint8_t (*read_gpio_pin)(void) = platform_read_gpio_pin; uint8_t read_button_state(void) { return read_gpio_pin(); }这样,单测时只需要把read_gpio_pin替换成 mock 函数,就能在 PC 上模拟任意按键状态序列。这个改造点很小,但对可测性的提升是质的改变。
我们在做单测时,还特别注意构造“破坏性数据”。比如,某个函数接受一个结构体指针,AI 生成的实现里默认指针是有效的。我们的单测就会专门传一个 NULL 进去,传一个只初始化了一半的结构体进去,甚至传一个故意把 size 字段改大的结构体进去,看代码会不会崩溃。这种测试思路对 AI 生成代码尤其重要,因为 AI 对“前提条件”的理解往往过于理想化。
3.4 硬件在环层:AI 代码第一次接触真实硬件
如果说前两层还在“软件”范畴里打转,那么从 HIL 开始,你就要面对真实的物理世界了。这一层最常见的问题包括:
- 时序问题:AI 生成的代码在 PC 上跑得飞快,但烧到真实的 MCU 上,由于总线频率、Flash 等待周期、外设响应时间不一样,原先逻辑上合理的时序假设可能完全不成立。比如,两条连续执行的 I2C 操作之间,如果没有足够的延时,实际硬件上就可能出现 ACK 丢失。
- 寄存器操作互斥:AI 在一个功能里使能了某个外设中断,在另一个功能里关闭了全局中断。单测时它们各跑各的,完全没问题;但在 HIL 阶段,两个功能交替执行,就可能在中断关闭期间错过了重要事件。
- 低功耗模式:很多 AI 生成的代码根本没考虑低功耗。你让它写一个定时采集传感器数据的任务,它可能生成一个
while(1) { read_sensor(); delay_ms(1000); }。这在开发板上没问题,但如果是电池供电设备,这种写法功耗直接上天,设备半天就没电了。
HIL 阶段我建议配备逻辑分析仪和示波器,不要只看程序跑没跑通,要对着时序图看每一个信号边沿。AI 代码在这方面是个迷,它生成的外设初始化顺序可能与芯片手册推荐顺序不同,但这种差异往往不会立刻报错,而是在长期运行或某个特定触发条件下才暴露出来。
3.5 整机路试层:验证体系里最像“路试”的环节
整机路试放在最后,但它其实是最接近用户真实使用的一个阶段。我们通常会把设备放在典型应用场景里,连续跑 72 到 168 个小时。这期间除了采集日志,还会定期注入一些异常事件,例如:
- 多次上下电,模拟用户频繁开关机;
- 通信链路拔插,模拟信号不稳定场景;
- 外设热插拔,模拟使用过程中接入不同附件;
- 电源电压拉偏,模拟电池电量波动。
AI 生成的代码在这种长期运行测试中暴露的问题往往比较集中:内存泄漏和状态机漂移。内存泄漏好理解,就是不断重复某个操作后,可用内存逐渐减少;状态机漂移比较隐蔽,是状态变量被修改到了未定义值,导致整个系统进入一个既不是正常运行也不是异常处理的“鬼打墙”状态。
路试阶段发现的 bug,修复起来往往不难,但定位过程非常折磨人。因为你要从几千条日志上下文里,找到那条与故障强相关的蛛丝马迹。我们现在的做法是,在代码里预埋足够的日志埋点,特别是状态机跳转的地方,务必把“从哪来到哪去”打印得清清楚楚。这招对 AI 生成代码尤其重要,因为你对它的逻辑不熟悉,没有日志几乎没法推断它当时在想什么。
4. 容易被掩盖的缺陷类型:AI 代码专属“暗坑”盘点
4.1 静态时序分析缺失,导致“看起来对、跑起来乱”
先说一个比较典型的:AI 生成的代码在处理外设寄存器时,通常直接写赋值语句。但嵌入式硬件有一个重要特性——某些寄存器在写入后,需要等待几个时钟周期才能读到正确值,而有些寄存器需要先写某个特定的解锁序列才能访问。AI 不会知道这些硬件手册里的细节,所以它生成的代码,逻辑上完全正确,但在真实硬件上就会时不时地抽风。
我们在验证体系里加入了静态时序检查,虽然不会完全自动化,但会让工程师在走查代码时对照芯片手册,把“寄存器操作后是否需要延时”这一项作为必查点。尤其是 AI 生成的初始化序列,每个外设的初始化步骤都要核对一遍。这项工作很枯燥,但它能省下后面 HIL 阶段的大量排障时间。
4.2 大小端问题和结构体打包问题
嵌入式平台千差万别,ARM 基本都是小端,但有些 MCU 是大端,或者支持字节序切换。AI 生成代码时,默认情况下它对端序的假设可能是错误的,特别是在处理通信协议、文件系统、Flash 存储这类跟外部数据打交道的模块。
举个实际例子:AI 帮我们生成了一段用于解析 Modbus 报文的代码,它在解析寄存器地址时直接用了*(uint16_t*)&buf[offset]这种强转指针的方式。在一台 x86 的 PC 上跑单测完全没问题,因为 x86 是小端。但放到一个支持字节序切换的 MCU 上,只要工程师在配置里把字节序设成了大端,这段 AI 代码解析出来的所有数据都是错乱的。
验证体系里处理这类问题,最直接的手段是:在单测阶段就通过 CMake 或 Makefile 生成两套配置,一套小端、一套大端,跑同一批测试用例。这样可以在代码进入硬件之前,先把端序相关的隐患揪出来。
4.3 中断上下文里的非法操作
这应该是嵌入式场景下 AI 生成代码最危险的“坑”之一。AI 生成的代码里,如果某个函数被中断处理程序调用,而这个函数内部又有阻塞式延时、printf、动态内存申请等操作,整个系统的实时性就会崩塌。
我们团队还撞到过一次特别隐蔽的情况:AI 生成的中断处理函数里,调用了另一个模块的查询函数,导致几个中断之间的优先级反转,系统间歇性卡死。这种问题在单测的时候是完全测不出来的,因为单测跑在普通线程上下文里,没有中断优先级的概念。
验证这一类问题的办法,是在代码走查时画一张“中断调用树”,人工确认每个中断服务函数直接或间接调用了哪些函数,逐一排查是否包含不安全的操作。这个工作靠人来做确实繁琐,但目前也没有完全自动化的工具能替代,至少我还没遇到。
4.4 错误处理分支过于理想化
AI 生成代码时,对错误处理分支的构造往往比较草率。它们倾向于生成“如果条件不满足就返回错误码”这类标准结构,但错误码本身怎么定义、上层怎么处理、是否需要重试、是否需要状态转换,AI 经常是不管的。
这就导致一种很尴尬的局面:异常路径上报了错误码,但系统不知道该拿这个错误码怎么办,最终只能处于一个半死不活的状态。所以我们在验证 AI 生成的代码时,会专门做一轮错误注入测试,把每一个错误码可能对应的场景都模拟一遍,看系统能不能按要求降级、重试或者安全停机。只有把错误路径也验证过,才敢说这段代码“能上路”。
5. 把验证流程固化到工具链:让 AI 生成的每一行代码都被“自动盯着”
5.1 从 AI 聊天窗到代码仓库的必经之路
我经常看到一些工程师在 AI 工具里生成完代码,复制粘贴到工程目录里就完事了。这种做法在嵌入式开发里真的要改一改。AI 工具只是一个生产代码的“输入设备”,它输出的代码必须像有人写的一样,走完版本管理、持续集成、自动化测试这一整套流程。
我们在内部是这样操作的:AI 生成代码,先放到一个单独的分支,然后自动触发 CI。CI 里至少包含这些步骤:
- 拉取最新代码,编译多个目标平台(ARM Cortex-M、RISC-V、x86 模拟器);
- 运行静态分析套件;
- 编译并运行 PC 端的单元测试,生成覆盖率报告;
- 用
west build或者cmake生成烧录镜像; - 对 HIL 环境的测试固件进行自动烧录,并运行冒烟测试用例。
只要以上任何一步失败,这个 AI 生成的分支就不允许合并到主干。这样做的好处是,即使 AI 生成的代码质量不稳定,也不会直接污染共享的开发主干。
5.2 用测试用例集反向约束 AI 生成
这是我在实际使用中摸索出来非常有用的经验:在让 AI 生成代码之前,先把测试用例写好,用测试用例去约束 AI 的生成结果。换句话说,把“验证”前置到“生成”阶段。
比如,我需要让 AI 生成一个温度传感器的驱动。我不会直接说“帮我写一个读温度的驱动”,而是先把测试用例列出来:
TEST_CASE(read_temperature_mock_i2c_returns_valid_value) { mock_i2c_set_read_data(0x1A, 0x07D0); // 25.0 度 assert_true(fabs(read_temperature() - 25.0) < 0.1); } TEST_CASE(read_temperature_i2c_nack_returns_error) { mock_i2c_set_nack(0x1A); assert_equal(read_temperature(), TEMP_I2C_ERROR); }然后把这段测试代码作为需求描述的一部分发给 AI,让它在满足测试用例的前提下实现逻辑。这样生成的代码,至少在一开始就具备可测试性,不会出现“拿到手不知道怎么测”的尴尬场景。
5.3 需要纳入版本管理的,不止是代码,还有测试向量和真机日志
很多团队把版本管理范围限定在源代码和文档,但嵌入式验证体系的产物远不止这些。测试向量、测试固件、压测记录、真机日志、硬件配置版本,这些都是应该进入版本管理体系的。AI 生成的代码在某个版本能通过验证,但在下一个版本可能因为硬件微调、编译器升级等因素挂掉,这时候你要能回溯当时的验证环境,否则排查起来会非常痛苦。
我们现在的做法是,在每次验证有结论时,生成一份验证报告,内容包括代码版本、编译器版本、目标板硬件版本、测试工具版本、覆盖率和结论签核人,把这个报告提交到一个单独的仓库里。这样,任何一次 AI 生成代码引入的验证记录都是可追溯的,将来出问题也不至于扯皮。
6. 验证体系的投入产出比:算清楚这笔账
6.1 不要只看测试花费的时间,要用全生命周期视角去看
AI 生成代码时,你省下的是“写代码”的时间;但如果你跳过或者压缩验证环节,将来在量产现场、售后维护阶段花掉的时间会成倍地找回来。我们团队有次在一个简单的外设驱动上依赖了 AI 生成的代码,省了不到半天;结果在整机路试阶段发现通信偶发失败,定位加修复加回归,前后花了两周。这件事之后,团队就形成了一条潜规则:AI 生成的代码,可以帮你减少写代码的时间,但验证时间不得压缩。
6.2 在哪些环节投入,性价比最高
根据我们的经验,如果时间有限,优先把资源花在这三块:
| 优先级 | 验证环节 | 理由 |
|---|---|---|
| 高 | 静态分析 | 成本最低,能筛掉 70% 的明显问题 |
| 高 | 单元测试 | 能在不依赖硬件的情况下提前暴露逻辑缺陷 |
| 中 | 硬件在环测试 | 发现单测覆盖不了的时序和硬件交互问题 |
| 中低 | 整机路试 | 真实场景最会暴露问题,但耗时最长,最好通过自动巡检减少人力 |
这个优先级不是固定的。如果你的产品对安全要求极高,整机路试的权重必须提高;如果只是内部原型验证,静态分析和单元测试足够,路试可以简化。
6.3 验证体系不是一次性建设,要跟着 AI 使用频率迭代
最后提醒一点:验证体系不能建一次就完事。AI 生成代码的使用频率越高,验证体系就要做得越重。反过来,验证体系跑出来的数据,也要反过来喂给 AI 生成的流程——哪些错误模式反复出现,就把它固化到静态分析规则和单测用例里。这样,验证体系和 AI 生成之间,会形成一个类似于“从实践中学习”的正循环。
我们内部现在有一个“AI 生成代码缺陷库”,每发现一个新类型的 bug,就整理成一条规则,定期补充到静态分析配置和代码走查清单里。几个月下来,AI 生成的代码质量肉眼可见地提升,这不是因为 AI 变聪明了,而是因为验证体系把 AI 生成的“劣质行为”约束住了。
最后说点实际的
我一直觉得,AI 生成代码这件事,放在嵌入式领域的正确使用方式不是“替代工程师”,而是“给工程师配了个写代码速度极快的实习生”。这个实习生可能基础不错,但不懂你们的产品、不懂芯片手册的细节、不懂老设备里踩过的那些坑。你能不能让这个“实习生”发挥价值,完全取决于你愿不愿意花时间验证它的工作成果。
验证体系听起来是个“麻烦事”,但把 AI 生成代码和验证体系当作一个整体来看,你就会发现,几秒钟的生成带来的速度红利,只有配上扎实的验证环节,才能真正转化为项目交付效率。否则,那些被“省”下来的时间,早晚会加倍地花在定位 bug 和售后救火上。