第三章 环境搭建:交叉编译工具链配置、模拟器环境(QEMU)搭建、硬件在环(HIL)环境准备

好,咱们进入第三章。这一章说白了就是「把家伙事儿备齐」。我见过太多人,框架设计得天花乱坠,结果环境搭了一星期还没跑起来。嗯,咱们不干那种事。

自动化测试框架要落地,离不开三样东西:交叉编译工具链模拟器环境硬件在环环境。这三者各有各的用处,也各有各的坑。我一个一个说。

3.1 交叉编译工具链配置

嵌入式开发,说白了就是在PC上写代码,编译成目标芯片能跑的机器码。这个「编译成目标芯片能跑的机器码」的过程,就叫交叉编译。

我个人习惯用 ARM GCC 工具链。为什么?因为免费、稳定、社区活跃。你想想看,如果团队里每个人装个IAR都要找License,那自动化框架还怎么推广?

3.1.1 安装与验证

以ARM Cortex-M系列为例,我通常这样配置:

# 下载ARM GCC工具链
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2

# 解压到指定目录
tar -xjf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 -C /opt/

# 配置环境变量
export PATH=$PATH:/opt/gcc-arm-none-eabi-10.3-2021.10/bin

验证是否装好了?跑一行命令:

arm-none-eabi-gcc --version

能看到版本号,就说明成了。

我的小技巧: 别把工具链直接装系统目录。我习惯放 /opt 下,然后用软链接管理版本。这样切换工具链版本时,改个链接就行,不用动环境变量。

3.1.2 编写测试用的Makefile

自动化测试框架里,编译环节必须可重复、可脚本化。我一般写一个通用的Makefile模板:

# 工具链定义
CROSS_COMPILE = arm-none-eabi-
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy

# 编译参数
CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -O0 -g
LDFLAGS = -T link.ld -nostartfiles

# 目标文件
TARGET = test_firmware.elf
BIN = test_firmware.bin

# 源文件
SRCS = main.c test_runner.c uart_driver.c
OBJS = $(SRCS:.c=.o)

all: $(BIN)

$(BIN): $(TARGET)
	$(OBJCOPY) -O binary $< $@

$(TARGET): $(OBJS)
	$(LD) $(LDFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f $(OBJS) $(TARGET) $(BIN)
注意: 我曾经因为忘记加 -nostartfiles 参数,导致编译出来的固件在QEMU上跑不起来。折腾了两天才发现是启动文件冲突。嗯,这个坑你们别踩。

3.2 模拟器环境(QEMU)搭建

QEMU这东西,说白了就是个「软件里的硬件」。你可以在PC上模拟出一块STM32开发板,跑你的固件。好处是什么?快、便宜、可重复

我做过一个项目,团队20个人,每人配一块开发板要两万多。后来全用QEMU做CI测试,成本几乎为零。你想想看,这账怎么算都划算。

3.2.1 安装QEMU

以Ubuntu为例:

sudo apt-get install qemu-system-arm

验证安装:

qemu-system-arm --version

3.2.2 启动一个模拟的STM32

我习惯用 STM32F4-Discovery 的板级支持。启动命令长这样:

qemu-system-arm \
  -machine stm32f4-discovery \
  -kernel test_firmware.bin \
  -serial stdio \
  -nographic

这里有几个参数要解释一下:

  • -machine:指定模拟的板子型号
  • -kernel:加载你的固件二进制文件
  • -serial stdio:把串口输出重定向到终端
  • -nographic:不启动图形界面,纯命令行模式

核心思路: 自动化测试框架里,QEMU要能通过脚本启动、运行、关闭。我一般封装成一个函数:

function run_qemu() {
    local firmware=$1
    local timeout=$2

    qemu-system-arm \
      -machine stm32f4-discovery \
      -kernel $firmware \
      -serial stdio \
      -nographic &
    local pid=$!

    sleep $timeout
    kill $pid 2>/dev/null
}

3.2.3 模拟外设的注意事项

QEMU不是万能的。它模拟了GPIO、UART、Timer这些基本外设,但像ADC、DAC、USB这些复杂外设,模拟得并不完整。

我曾经踩过一个坑:在QEMU上跑ADC采样测试,结果一直返回0。查了半天才发现,QEMU的ADC模型根本没实现。嗯,所以我的建议是:逻辑功能用QEMU测,硬件相关功能留到HIL测

3.3 硬件在环(HIL)环境准备

HIL,说白了就是「真刀真枪干」。你的固件跑在真实芯片上,测试脚本通过调试接口(比如JTAG/SWD)跟它交互。

为什么需要HIL?因为有些bug只在真实硬件上出现。比如时序问题、电源噪声、外设互斥……这些在QEMU里根本复现不了。

3.3.1 硬件连接方案

我常用的HIL架构是这样的:

组件 作用 推荐型号
目标板 运行被测固件 STM32F4-Discovery
调试器 烧录、调试、控制 J-Link / ST-Link
上位机 运行测试脚本 Linux PC
串口线 日志输出、命令交互 USB转TTL

3.3.2 自动化烧录与复位

自动化测试里,烧录和复位必须脚本化。我用 OpenOCD 来做这件事:

# 烧录固件
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
  -c "program test_firmware.elf verify reset exit"

# 复位并暂停
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
  -c "init; reset halt; exit"
我的经验: 写一个Python脚本封装OpenOCD命令,用subprocess模块调用。这样测试框架里只需要调用一个函数:flash_firmware("test_firmware.elf"),干净利落。

3.3.3 串口日志采集

HIL测试里,串口是最重要的「眼睛」。我一般用 pyserial 库来采集日志:

import serial
import time

def collect_uart_log(port="/dev/ttyUSB0", baud=115200, timeout=10):
    ser = serial.Serial(port, baud, timeout=1)
    log = []
    start = time.time()

    while time.time() - start < timeout:
        if ser.in_waiting:
            line = ser.readline().decode("utf-8", errors="ignore").strip()
            log.append(line)
            # 检测测试结束标志
            if "TEST_DONE" in line:
                break

    ser.close()
    return log
注意: 串口线一定要用带屏蔽的。我曾经因为用了根劣质串口线,测试结果时好时坏,排查了三天才发现是线的问题。嗯,这种低级错误,犯一次就够了。

3.4 三种环境的对比与选择

最后,我做个总结。这三种环境各有各的定位:

  • 交叉编译工具链:基础中的基础。没有它,你连固件都编不出来。
  • QEMU模拟器:适合做CI回归测试、逻辑功能验证。速度快、成本低。
  • HIL硬件在环:适合做硬件相关测试、压力测试、边界测试。真实可靠,但成本高。

我个人建议的测试策略是:80%的测试用例跑在QEMU上,20%的关键用例跑在HIL上。这样既保证了测试覆盖率,又控制了成本和时间。

好,环境搭建这部分就讲到这里。下一章咱们开始讲测试用例的设计——那才是自动化测试框架的灵魂。