国内大多数固件团队不写单元测试,不是因为懒,而是因为代码一开始就写错了
立芯单片机
编辑于 2026年06月11日 20:30

我见过太多项目死在最后一公里。功能都做完了,板子也点亮了,联调也跑通了,一上产线——死机、数据错乱、外设行为不符合预期。工程师开始加班排查,逻辑分析仪接上去一层层抓,最后发现是某个边界条件没处理好,一个 uint8_t 溢出了,或者一个状态机在某个罕见时序下走进了不该走的分支。

这种 bug 的修复成本是巨大的。不是说改那一行代码有多难,而是定位它要花三天,复现它要搭一整套环境,验证它要重新过一遍回归。如果这个 bug 在写完那个函数的当天就被一个单元测试拦住了呢?成本几乎为零。

但现实是,国内绝大多数嵌入式团队不写单元测试。 这不是工程师懒,是整个行业的结构性问题。

嵌入式软件和应用层软件有一个根本区别:它跑在真实硬件上。你的代码直接操作寄存器、响应中断、依赖时钟和外设时序。当你打开一个 MCU 的 HAL 库,满眼都是对硬件地址的直接读写。一个典型的 SPI 驱动函数长这样:

你怎么在 PC 上测这个?SPI1->DR 是一个映射到物理地址的寄存器,你的 x86 机器上根本不存在这个地址。很多工程师在这里就放弃了——"嵌入式代码没法脱离硬件跑,所以没法做单元测试。"

这个结论听起来合理,但它是错的。错在把"嵌入式项目"等同于"全部都是硬件操作代码"。

真相是:一个成熟的嵌入式项目,真正直接碰寄存器的代码可能占 10%~20%。剩下的 80% 是什么?协议解析、数据校验、状态机逻辑、配置管理、算法实现、消息队列调度……这些东西和硬件没有半毛钱关系。它们是纯粹的逻辑,完全可以在 PC 上编译、运行、验证。

问题在于,大多数固件项目的代码架构没有把这两层分开。业务逻辑和硬件操作搅在一起,函数里一边算 CRC 一边操作 GPIO,一个文件里既有协议解包也有 DMA 配置。这种代码不是"不能测",是"没法测"——不是测试工具不行,是代码本身的结构让测试无从下手。

所以,谈嵌入式单元测试,第一步不是选框架,是改架构。 分层,是一切可测性的前提。 这个道理其实不新鲜。做过 Linux 驱动的人都知道,内核把硬件抽象层(HAL)和上层逻辑分得很清楚。RTOS 生态里,Zephyr 和 NuttX 也在做类似的事。但在 MCU 裸机项目里,这种分层意识普遍缺失。

具体怎么分?核心思路是:让业务逻辑不知道自己跑在什么硬件上。

举个例子。假设你在做一个传感器数据采集模块,需要从 ADC 读数据、做滤波、判断是否越限、然后通过 UART 上报。一种常见写法是把这些全塞在一个函数里:

这段代码完全不可测。但如果你把它拆成三层:

现在 sensor_process() 是一个纯函数。输入是一个 ADC 原始值和一个滤波器状态,输出是处理结果。你可以在 PC 上写一百个测试用例喂给它:正常值、边界值、溢出值、连续递增序列、突变跳变……每个都能在毫秒级跑完,不需要开发板,不需要仿真器。

用 Unity(嵌入式领域最轻量的 C 测试框架)写测试大概长这样:

这不是什么高深技术。这就是正常的软件工程实践,只是在嵌入式行业被长期忽视了。

再谈一个更现实的问题:AI 生成的代码谁来兜底? 这两年 AI 辅助编程已经渗透到了嵌入式领域。Copilot 能帮你写 HAL 初始化,ChatGPT 能给你生成一个 Modbus RTU 的解析器,Cursor 甚至能帮你补全整个状态机。效率确实提高了——但风险也在悄悄堆积。

AI 生成的代码有一个显著特点:它看起来很对。语法没问题,逻辑主干没问题,甚至注释都写得很规整。但边界条件的处理、异常路径的覆盖、特定硬件时序的约束——这些 AI 经常搞不定。一个 Modbus CRC 校验函数,AI 可能给你生成一个"几乎正确"的版本,在 99% 的帧上都能算对,但在某些特定字节序列上会出错。这种 bug 你人眼 review 很难发现,因为逻辑看起来是通的。

怎么约束 AI?靠 code review 是不够的,因为 AI 的产出速度远超人类 review 的速度。答案是用架构约束它的作用范围,用测试验证它的输出正确性。

架构层面:让 AI 只生成被隔离的业务逻辑模块,不让它碰硬件抽象层和系统初始化。这些模块有清晰的输入输出接口,天然适合被测试覆盖。

测试层面:每一个 AI 生成的函数,必须配套对应的单元测试。而且这个测试最好也让 AI 写——但人来定义测试的 意图。你告诉 AI:"帮我测这个 CRC 函数,覆盖空数据、单字节、最大长度、已知校验对",然后你 review 测试代码,确认它确实验证了你关心的行为。

这形成了一个闭环:AI 写代码,测试约束代码,人约束测试。 说到底,单元测试在嵌入式领域不流行,根本原因不是技术障碍,是组织惯性。大多数团队的验证手段停留在"烧录-上电-看串口-点灯"这种手工模式上。

这在代码量几千行的时候还能撑住,当项目膨胀到几万行、当 AI 开始大规模参与生成代码的时候,手工验证的模式就彻底崩溃了。

工具已经成熟了。Unity + CMock 处理纯 C 项目绑绑有余。C++ 项目可以用 Google Test。跑在 CI 里用 Ceedling 或者 CMake 驱动都行。Mock 硬件依赖用函数指针替换或者链接期替换都是成熟方案。甚至 Renode 这样的全系统仿真器已经能让你在没有硬件的情况下跑集成测试了。

缺的不是工具,是那个把架构拆干净、把测试跑起来的决定。这个决定的投入产出比,随着 AI 生成代码的占比越来越高,只会越来越划算。