4、开发环境搭建:基于 GCC + Makefile 的测试环境、交叉编译工具链配置、模拟器与硬件在环测试环境
说实话,很多做嵌入式测试的朋友,一上来就问我:「用什么工具好?」
我的回答往往是——先把 GCC 和 Makefile 玩明白。工具链这东西,说白了就是个「翻译官」。你写的 C 代码,它帮你翻译成目标芯片能跑起来的机器码。我见过太多人,IDE 用得飞起,但一换平台就抓瞎。嗯,今天咱们就把这层窗户纸捅破。
4.1 基于 GCC + Makefile 的本地测试环境
先聊本地环境。你想想看,如果每次改一行代码都要烧录到板子上跑,那效率得多低?我个人的习惯是:先在 PC 上用 GCC 编译跑一遍单元测试,确认逻辑没问题了,再往目标平台上搬。
GCC 的好处是啥?跨平台、免费、生态好。咱们做单元测试,最常用的组合就是 GCC + Makefile + CUnit(或者 Check 库)。
一个典型的 Makefile 长这样:
# 单元测试 Makefile 示例
CC = gcc
CFLAGS = -Wall -Wextra -g -O0 --coverage
LDFLAGS = -lcunit --coverage
SRC = $(wildcard src/*.c)
TEST_SRC = $(wildcard test/*.c)
OBJ = $(SRC:.c=.o) $(TEST_SRC:.c=.o)
test_all: $(OBJ)
$(CC) -o $@ $^ $(LDFLAGS)
./test_all
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o test_all *.gcda *.gcno *.gcov
.PHONY: test_all clean
--coverage 选项。这样跑完测试后,用 gcov 工具就能看到每行代码的覆盖率。我在项目中遇到过好几次,自认为测得很全了,结果 gcov 一跑,发现有个异常分支根本没走到。
编译和运行一步到位:
$ make test_all
gcc -Wall -Wextra -g -O0 --coverage -c src/uart_driver.c -o src/uart_driver.o
gcc -Wall -Wextra -g -O0 --coverage -c test/test_uart.c -o test/test_uart.o
gcc -o test_all src/uart_driver.o test/test_uart.o -lcunit --coverage
./test_all
Suite: UART Driver Suite
Test: test_uart_init ... PASSED
Test: test_uart_send ... PASSED
Test: test_uart_receive ... PASSED
Test: test_uart_error_handling ... PASSED
Run Summary: 4/4 passed
4.2 交叉编译工具链配置
好,本地测完了,接下来要往目标芯片上搬。这时候就需要交叉编译工具链了。
交叉编译是啥?说白了就是「在 PC 上编译,生成 ARM/MIPS/RISC-V 等架构的可执行文件」。我最早接触交叉编译是在做 STM32 项目时,用的是 arm-none-eabi-gcc。
配置步骤其实不复杂:
- 下载工具链:从芯片厂商官网或 ARM 官方下载。比如
gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 - 解压到指定目录:我习惯放在
/opt/toolchains/下 - 设置环境变量:把
bin目录加到PATH里 - 验证安装:跑一下
arm-none-eabi-gcc --version
一个交叉编译的 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 -O2 -g
LDFLAGS = -T linker.ld -nostartfiles
SRC = main.c uart_driver.c gpio_driver.c
OBJ = $(SRC:.c=.o)
TARGET = firmware.elf
BIN = firmware.bin
all: $(BIN)
$(TARGET): $(OBJ)
$(LD) $(LDFLAGS) -o $@ $^
$(BIN): $(TARGET)
$(OBJCOPY) -O binary $< $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o $(TARGET) $(BIN)
flash: $(BIN)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program $(BIN) 0x08000000 verify reset exit"
.PHONY: all clean flash
关键点:
-mcpu=cortex-m4指定 CPU 型号,别搞错了。我见过有人用 M0 的芯片配 M4 的编译选项,烧进去直接跑飞-mthumb指定 Thumb 指令集,ARM 芯片基本都用这个-T linker.ld链接脚本,定义了代码段、数据段、堆栈的地址布局
你可能会问:「链接脚本怎么写?」嗯,这个确实是个技术活。最简单的办法是拿芯片厂商提供的示例改。我刚开始做的时候,自己手写了一个,结果堆栈地址算错了,程序一跑就进 HardFault。后来老老实实拿 ST 官方的改,省心多了。
4.3 模拟器与硬件在环测试环境
环境搭好了,代码也交叉编译通过了。但问题来了——你不能每次都烧录到板子上测吧?尤其是团队开发,板子就那么几块,大家抢着用。这时候就需要模拟器和硬件在环(HIL)测试了。
4.3.1 模拟器(QEMU)
QEMU 是个好东西。它可以模拟完整的 ARM/MIPS 等嵌入式系统。我常用它来做集成测试前的「预检」。
启动一个 QEMU 模拟的 STM32 开发板:
qemu-system-arm \
-M stm32vldiscovery \
-kernel firmware.bin \
-nographic \
-serial mon:stdio
这样你就能在 PC 上看到串口输出了。配合 gdb 还能单步调试。我在项目中遇到过一个问题:某个中断处理函数在板子上跑得好好的,但 QEMU 里就是触发不了。后来发现是 QEMU 对那个外设的模拟不完整。所以记住——模拟器通过不代表板子上能过,但模拟器挂了,板子上大概率也挂。
4.3.2 硬件在环(HIL)测试
HIL 测试,说白了就是把真实的硬件和测试脚本连起来,自动化跑测试。我参与过一个车载项目,HIL 环境是这样的:
| 组件 | 作用 | 我用的方案 |
|---|---|---|
| 测试主机 | 运行测试脚本,发送指令 | Python + pytest |
| 通信接口 | 连接主机和目标板 | UART / JTAG / Ethernet |
| 目标板 | 运行被测固件 | STM32F407 开发板 |
| 信号采集 | 捕获 GPIO 波形、ADC 值等 | 逻辑分析仪 + pySerial |
一个简单的 HIL 测试脚本片段:
import serial
import time
def test_uart_echo():
"""测试 UART 回环功能"""
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
test_data = b'Hello HIL Test!'
ser.write(test_data)
time.sleep(0.1)
response = ser.read(len(test_data))
assert response == test_data, f"Echo failed: {response}"
ser.close()
print("UART echo test PASSED")
if __name__ == '__main__':
test_uart_echo()
4.4 三种环境的对比与选择
最后做个总结。这三种环境各有各的用途,我一般这样安排:
| 环境 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| GCC + Makefile 本地环境 | 单元测试、逻辑验证 | 速度快、调试方便 | 无法验证硬件相关代码 |
| 交叉编译环境 | 生成目标固件 | 真实反映目标平台行为 | 需要额外配置工具链 |
| 模拟器(QEMU) | 集成测试预检 | 无需硬件、可重复性好 | 外设模拟不完整 |
| 硬件在环(HIL) | 系统级验证、回归测试 | 最真实、最可靠 | 需要硬件资源、速度慢 |
我个人建议的流程是:本地写代码 → GCC 单元测试 → 交叉编译 → QEMU 预检 → HIL 全量回归。这样既能保证效率,又能确保质量。
嗯,环境搭建这部分就聊到这儿。下一章咱们开始讲具体的测试用例设计方法,到时候会用到今天搭的环境。记得把 Makefile 和工具链配好,别到时候手忙脚乱。