3. 测试环境搭建:HIL、SIL 与 CI 环境
测试环境搭建,说白了就是给被测代码找个合适的「家」。这个家要是搭错了,后面所有测试结果都不可信。我见过太多团队,花三个月写测试用例,结果环境配错,全部重来。
嵌入式测试环境分三层:软件在环(SIL)、硬件在环(HIL)、持续集成(CI)。它们解决不同阶段的问题,但必须串成一条流水线。
3.1 软件在环(SIL)环境
SIL 环境,就是让嵌入式代码跑在模拟器上。没有真实芯片,没有外设,只有 CPU 指令集的模拟。为什么需要它?因为快。你想想看,编译一次 3 秒,启动模拟器 5 秒,跑完 100 个用例 10 秒。要是每次都用真机,光烧录就得半分钟。
核心原则:SIL 环境只验证逻辑正确性,不验证时序和硬件接口。它负责把「代码写没写对」这件事快速闭环。
3.1.1 常用 SIL 工具
| 工具 | 适用架构 | 启动速度 | 外设模拟 |
|---|---|---|---|
| QEMU | ARM, RISC-V, x86 | 快(< 2s) | 基础 GPIO/UART |
| Renode | ARM, RISC-V | 中等(~5s) | 丰富(I2C, SPI, CAN) |
| Unicorn | ARM, MIPS, x86 | 极快(< 1s) | 无外设 |
| 自家模拟器 | 定制架构 | 取决于实现 | 完全可控 |
我个人习惯用 QEMU 做 ARM Cortex-M 的 SIL 测试。它启动快,社区活跃,踩坑了能找到人问。但要注意,QEMU 的 UART 模拟和真实芯片有差异——我遇到过模拟器上串口收发完全正常,烧到真机上就丢字节。所以 SIL 只能信 80%,剩下 20% 必须交给 HIL。
3.1.2 SIL 环境搭建步骤
- 下载模拟器:apt install qemu-system-arm 或从源码编译
- 准备裸机镜像:编译出 .elf 或 .bin 文件
- 编写启动脚本:指定 CPU 型号、内存大小、外设映射
- 连接测试框架:通过 GDB 或串口重定向与测试用例交互
- 验证基础功能:跑一个 LED 闪烁或串口打印,确认环境通
小技巧:启动脚本里加上 -serial stdio,这样模拟器的串口输出直接打到终端。测试用例可以通过标准输入发送指令,实现半自动化。
3.1.3 SIL 环境避坑
我曾经在 Renode 上搭 SIL 环境,花了两天时间配置外设模型。结果发现 Renode 的 SPI 模拟和真实芯片的时序差了 3 个时钟周期。从那以后我定了个规矩:SIL 只测逻辑,不测时序。凡是跟时间相关的测试,一律标记为「仅 HIL」。
警告:不要用 SIL 环境测试中断响应时间、DMA 传输速率、ADC 采样精度。这些指标在模拟器上毫无意义。
3.2 硬件在环(HIL)环境
HIL 环境,就是把真实硬件接入测试系统。被测设备(DUT)是真实的板子,测试系统通过信号接口(IO、CAN、以太网)给它施加激励,采集响应。说白了,就是让机器代替人插拔线缆、按按钮、读仪表。
我参与过一个汽车 ECU 项目,HIL 环境里跑了 2000 多个测试用例,覆盖了 95% 的故障模式。要是靠人工测,三个月都测不完。
3.2.1 HIL 系统组成
| 组件 | 作用 | 常见选型 |
|---|---|---|
| 实时处理器 | 运行测试模型,控制 IO 时序 | NI PXI, dSPACE SCALEXIO |
| 信号调理板卡 | 电平转换、隔离、滤波 | NI 板卡, 自研调理电路 |
| 故障注入单元 | 模拟短路、断路、过压 | Pickering, 自研继电器矩阵 |
| 负载模拟 | 模拟传感器、执行器 | 电子负载, 电阻箱, 信号发生器 |
| 上位机软件 | 测试管理、数据记录、报告生成 | VeriStand, LabVIEW, Python |
3.2.2 HIL 环境搭建流程
- 定义接口清单:列出 DUT 所有 IO 引脚、通信协议、电气特性
- 设计信号映射:将 DUT 引脚连接到 HIL 板卡通道
- 编写测试模型:用 Simulink 或 Python 模拟被控对象
- 配置故障注入:设计故障场景矩阵(开路、短路、对电源、对地)
- 校准信号质量:用示波器验证 HIL 输出信号的上升沿、幅值、噪声
- 编写自动化脚本:将测试步骤转化为可重复执行的代码
关键指标:HIL 系统的信号延迟必须小于 DUT 控制周期的 1/10。比如 DUT 的 PWM 周期是 1ms,HIL 的响应延迟不能超过 100μs。否则测试结果不可信。
3.2.3 HIL 环境避坑
我曾经在搭建 HIL 时忽略了一个细节:DUT 的 CAN 总线终端电阻是 120Ω,但 HIL 板卡内部已经集成了终端电阻。结果两个电阻并联,变成了 60Ω,CAN 通信时好时坏。查了两天才找到原因。嗯,这里要注意:接口匹配不是软件问题,是硬件问题,必须用万用表量一遍。
警告:HIL 环境上电前,务必用万用表测量所有信号线的电压。DUT 输出 5V,HIL 板卡输入只能承受 3.3V,一上电就烧。我见过不止一次这种事故。
3.3 持续集成(CI)环境
CI 环境,就是把 SIL 和 HIL 串起来的调度中心。代码提交后,CI 自动拉代码、编译、跑 SIL 测试、触发 HIL 测试、生成报告。整个过程不需要人干预。
你想想看,如果没有 CI,开发人员每次提交代码后,要手动编译、手动烧录、手动跑测试、手动记录结果。一天能提交 3 次就了不起了。有了 CI,一天提交 30 次都行。
3.3.1 CI 流水线设计
| 阶段 | 执行内容 | 预期耗时 | 失败处理 |
|---|---|---|---|
| 代码检查 | 静态分析、代码风格、编译 | < 2 min | 阻止合并 |
| SIL 单元测试 | 每个模块的独立测试 | < 5 min | 阻止合并 |
| SIL 集成测试 | 模块间交互测试 | < 10 min | 阻止合并 |
| HIL 冒烟测试 | 关键路径快速验证 | < 15 min | 通知但不阻止 |
| HIL 全量测试 | 所有用例回归 | < 60 min | 生成报告 |
3.3.2 CI 工具选型
我个人推荐 Jenkins 做嵌入式 CI 的主控。为什么?因为它插件生态好,能对接各种硬件触发方式。比如通过串口继电器控制 HIL 设备上电,通过 SSH 远程执行测试脚本。
GitLab CI 也不错,如果你的代码托管在 GitLab 上,可以少维护一套系统。但要注意,GitLab Runner 在 Windows 上跑 HIL 驱动时,权限问题比较多。
小技巧:HIL 设备通常只有一台,多个 CI Job 同时请求会冲突。解决方案是用 Jenkins 的「锁资源」插件,或者自己写一个队列管理脚本。我习惯用 Redis 做分布式锁,简单可靠。
3.3.3 CI 环境搭建步骤
- 部署 CI 服务器:Jenkins 或 GitLab Runner,建议用 Docker 部署
- 配置代码仓库 Webhook:每次 Push 或 Merge Request 触发流水线
- 编写 Jenkinsfile:定义编译、SIL 测试、HIL 测试阶段
- 连接 HIL 设备:通过网口或串口控制 HIL 系统上电、复位
- 配置测试报告:生成 HTML 报告,发送邮件通知
- 设置超时和重试:避免某个 Job 卡死阻塞整个流水线
3.3.4 CI 环境避坑
我曾经遇到一个经典问题:CI 服务器和 HIL 设备不在同一个网段,导致 SSH 连接经常超时。后来加了一台跳板机,才稳定下来。还有一次,HIL 设备长时间运行后内存泄漏,CI 跑着跑着就挂了。解决方案是每次跑完 HIL 测试后,强制重启设备。
警告:CI 环境中的 HIL 设备必须做看门狗保护。如果测试脚本崩溃,看门狗能自动复位设备,避免设备长时间处于未知状态。我见过 HIL 设备因为一个测试用例死循环,连续运行了 8 小时,差点烧了板子。
3.4 三种环境的协同
SIL、HIL、CI 不是孤立的。它们的关系是:CI 调度 SIL 和 HIL,SIL 快速反馈逻辑问题,HIL 验证硬件交互。一个典型的流程是:
- 开发提交代码 → CI 触发
- CI 拉代码、编译、跑 SIL 单元测试(5 分钟出结果)
- SIL 通过后,CI 触发 HIL 冒烟测试(15 分钟出结果)
- HIL 冒烟通过后,CI 触发 HIL 全量回归(1 小时出结果)
- 所有测试通过,CI 生成报告,通知团队
这个流程看起来简单,但落地时有很多细节。比如 SIL 和 HIL 的测试用例如何复用?我的做法是:定义统一的测试接口层。SIL 环境下,测试用例调用模拟器接口;HIL 环境下,测试用例调用真实硬件接口。测试逻辑本身不变。
核心思想:测试用例与执行环境解耦。写一次用例,能在 SIL 和 HIL 上分别执行。这样既保证了测试的一致性,又减少了维护成本。
好了,三种环境的搭建思路和避坑点都讲完了。记住一句话:SIL 求快,HIL 求真,CI 求稳。三者缺一不可。