3、测试环境搭建:HIL、SIL 与 CI 环境配置
说到测试环境搭建,我见过不少团队在这上面栽跟头。硬件还没回来,软件没法测;软件写好了,硬件环境又迟迟配不好。说白了,测试环境就是你的“战场”,战场没铺好,仗怎么打?
我个人习惯把嵌入式测试环境分成三层:软件在环(SIL)、硬件在环(HIL)和持续集成(CI)。这三层不是替代关系,而是互补关系。你想想看,如果每次改个逻辑都要烧录到真板上跑,那效率得多低?反过来,如果全程只跑模拟器,最后联调时发现硬件时序对不上,那又得返工。
3.1 软件在环(SIL)环境搭建
SIL 环境,说白了就是把你的嵌入式代码放在 PC 上跑,用模拟器代替真实硬件。我在项目中遇到过最典型的情况:硬件开发板还没到货,但算法团队已经催着要验证控制逻辑了。这时候 SIL 就是救命稻草。
3.1.1 交叉编译与模拟器选择
嵌入式代码要在 PC 上跑,第一步就是交叉编译。你得把目标平台(比如 ARM Cortex-M4)的代码,编译成 x86 架构的可执行文件。常用的工具有:
- QEMU: 全系统模拟器,能模拟完整的 MCU 外设。我建议用它来做裸机或 RTOS 的 SIL 验证。
- Unicorn: 轻量级 CPU 模拟器,适合只验证指令逻辑的场景。跑单元测试时特别好用。
- Renode: 专为嵌入式设计的模拟框架,支持多核、外设建模,还能和 CI 集成。
嗯,这里要注意:模拟器再逼真也不是真硬件。外设时序、中断延迟、ADC 采样抖动这些,模拟器是模拟不出来的。所以 SIL 只适合验证“功能逻辑”,不适合验证“时序性能”。
3.1.2 测试桩与 Mock 层
SIL 环境里最头疼的问题是什么?是硬件依赖。你的代码里肯定有 GPIO_WritePin()、UART_SendByte() 这些硬件操作函数。在 PC 上跑,这些函数根本不存在。
我的做法是:在代码中抽象出一层硬件抽象层(HAL),SIL 测试时把 HAL 替换成 Mock 桩函数。举个例子:
// 原 HAL 函数(真实硬件)
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) {
// 操作寄存器
GPIOx->BSRR = GPIO_Pin << (PinState ? 0 : 16);
}
// Mock 桩函数(SIL 环境)
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) {
// 记录调用日志,不操作真实硬件
printf("[MOCK] GPIO_WritePin: port=%p, pin=%d, state=%d\n", GPIOx, GPIO_Pin, PinState);
// 更新模拟状态机
mock_gpio_state[GPIO_Pin] = PinState;
}
3.2 硬件在环(HIL)环境搭建
HIL 环境,就是把真实硬件接入测试系统。我记得第一次搭 HIL 环境时,光接线就花了两天。后来总结出一套标准流程,效率才提上来。
3.2.1 HIL 硬件拓扑
一个典型的 HIL 环境包含以下组件:
| 组件 | 作用 | 常见选型 |
|---|---|---|
| 被测设备(DUT) | 运行目标固件的真实硬件板卡 | STM32F4 开发板、树莓派、自研板卡 |
| 信号仿真器 | 模拟传感器输入、执行器负载 | NI PXI、dSPACE、自研信号板 |
| 数据采集卡 | 捕获 DUT 的 GPIO、PWM、ADC 输出 | NI DAQ、USB-6210、逻辑分析仪 |
| 上位机控制软件 | 运行测试脚本,控制仿真流程 | Python + PyVISA、LabVIEW |
| 电源管理模块 | 控制 DUT 上下电、监测电流 | 可编程电源(如 Keysight E36312A) |
我曾经踩过一个坑:信号仿真器和 DUT 的参考地没接好,导致共模电压漂移,ADC 读数一直不准。查了三天才发现是地线问题。所以 HIL 环境搭建时,接地和屏蔽一定要优先处理好。
3.2.2 HIL 测试脚本框架
HIL 测试的核心是“自动化”。你不能每次都手动按按钮、看波形。我习惯用 Python 写测试脚本,通过串口或以太网控制 DUT,同时用数据采集卡读取响应。
import pyvisa
import serial
import time
class HILTestFixture:
def __init__(self, dut_port, daq_address):
self.dut = serial.Serial(dut_port, 115200, timeout=1)
self.daq = pyvisa.ResourceManager().open_resource(daq_address)
def set_sensor_input(self, voltage):
"""通过 DAQ 输出模拟传感器电压"""
self.daq.write(f"SOUR:VOLT {voltage}")
time.sleep(0.01) # 等待 DUT 采样
def read_dut_output(self):
"""从 DUT 串口读取响应"""
self.dut.write(b"GET_STATUS\n")
return self.dut.readline().decode().strip()
def test_overvoltage_protection(self):
"""测试过压保护功能"""
self.set_sensor_input(5.5) # 输入 5.5V(超过阈值)
response = self.read_dut_output()
assert "PROTECTION_ACTIVE" in response, "过压保护未触发!"
print("[PASS] 过压保护测试通过")
# 运行测试
fixture = HILTestFixture("COM3", "USB0::0x1234::0x5678::INSTR")
fixture.test_overvoltage_protection()
3.3 持续集成(CI)环境配置
CI 环境,就是把 SIL 和 HIL 测试串起来,每次代码提交都自动跑一遍。我见过很多团队,CI 只跑编译检查,不跑测试。那其实叫“持续编译”,不叫“持续集成”。
3.3.1 CI 流水线结构
一个完整的嵌入式 CI 流水线,我建议分三个阶段:
- 静态检查阶段: 代码风格检查(clang-format)、静态分析(cppcheck)、编译检查。这个阶段跑得最快,5 分钟内必须出结果。
- SIL 测试阶段: 在 QEMU 或 Unicorn 上运行单元测试和集成测试。这个阶段跑得稍慢,但 15 分钟内应该能完成。
- HIL 测试阶段: 连接真实硬件,运行回归测试。这个阶段最慢,但最关键。我建议只在合并到主分支时触发。
为什么会这样设计?因为反馈速度很重要。如果每次提交都要等 HIL 跑完(可能 30 分钟),开发者会疯掉的。所以把快速检查放在前面,慢速检查放在后面。
3.3.2 CI 工具选型与配置
嵌入式 CI 的工具有很多,我比较推荐 Jenkins 和 GitLab CI。Jenkins 插件生态丰富,GitLab CI 配置简洁。下面是一个 GitLab CI 的 .gitlab-ci.yml 示例:
stages:
- static_check
- sil_test
- hil_test
static_check:
stage: static_check
script:
- cppcheck --enable=all --suppress=missingInclude src/
- clang-format --dry-run --Werror src/*.c
only:
- merge_requests
sil_test:
stage: sil_test
script:
- make SIL=1
- ./build/sil_test_runner
artifacts:
paths:
- build/test_results.xml
reports:
junit: build/test_results.xml
hil_test:
stage: hil_test
script:
- python3 scripts/hil_runner.py --config config/hil_board1.json
tags:
- hil-runner # 指定有硬件连接的 runner
only:
- main
3.4 三层环境的关系与数据流
为了让你更直观地理解这三层环境的关系,我画了一张结构图:
从这张图你能看到,数据流是单向的:代码提交触发 CI,CI 先调度 SIL 测试,SIL 通过后再调度 HIL 测试。这样设计的好处是:尽早发现低级错误,避免浪费昂贵的 HIL 资源。
3.5 环境搭建的常见问题与对策
最后,我总结几个我在项目中遇到过的典型问题,希望能帮你少走弯路:
| 问题 | 现象 | 对策 |
|---|---|---|
| SIL 测试通过,HIL 测试失败 | 模拟器上逻辑正确,真板上跑飞 | 检查 HAL 层抽象是否完整,特别是中断和 DMA 部分 |
| HIL 测试结果不稳定 | 同一测试有时通过有时失败 | 检查时序问题,增加等待时间,使用示波器抓波形 |
| CI 流水线跑太久 | 一次提交等 40 分钟才出结果 | 拆分测试用例,把冒烟测试和全量回归分开跑 |
| 硬件资源冲突 | 两个 CI 任务同时抢同一块板卡 | 给每个硬件板卡配专用 runner,用 tags 隔离 |
好了,测试环境搭建这部分就讲到这里。记住:SIL 保逻辑,HIL 验硬件,CI 做串联。三层环境不是一次性搭完就完事的,需要根据项目进展持续优化。你想想看,如果每次迭代都花半天时间在环境问题上,那还怎么快速交付?