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
注意: 本地 GCC 环境只能验证「逻辑正确性」。如果你的代码依赖硬件寄存器操作,那本地测试就得用 mock 或者 stub 来模拟。我曾经踩过这个坑——本地全绿,烧到板子上就崩,最后发现是寄存器地址写错了。

4.2 交叉编译工具链配置

好,本地测完了,接下来要往目标芯片上搬。这时候就需要交叉编译工具链了。

交叉编译是啥?说白了就是「在 PC 上编译,生成 ARM/MIPS/RISC-V 等架构的可执行文件」。我最早接触交叉编译是在做 STM32 项目时,用的是 arm-none-eabi-gcc

配置步骤其实不复杂:

  1. 下载工具链:从芯片厂商官网或 ARM 官方下载。比如 gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
  2. 解压到指定目录:我习惯放在 /opt/toolchains/
  3. 设置环境变量:把 bin 目录加到 PATH
  4. 验证安装:跑一下 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()
避坑指南: 我曾经在 HIL 测试中遇到一个诡异的问题——脚本跑 10 次,有 1 次会超时。查了两天才发现是串口缓冲区没清空,上一次测试的残留数据干扰了结果。所以每次测试前,记得先清空缓冲区。

4.4 三种环境的对比与选择

最后做个总结。这三种环境各有各的用途,我一般这样安排:

环境 适用场景 优点 缺点
GCC + Makefile 本地环境 单元测试、逻辑验证 速度快、调试方便 无法验证硬件相关代码
交叉编译环境 生成目标固件 真实反映目标平台行为 需要额外配置工具链
模拟器(QEMU) 集成测试预检 无需硬件、可重复性好 外设模拟不完整
硬件在环(HIL) 系统级验证、回归测试 最真实、最可靠 需要硬件资源、速度慢

我个人建议的流程是:本地写代码 → GCC 单元测试 → 交叉编译 → QEMU 预检 → HIL 全量回归。这样既能保证效率,又能确保质量。

嗯,环境搭建这部分就聊到这儿。下一章咱们开始讲具体的测试用例设计方法,到时候会用到今天搭的环境。记得把 Makefile 和工具链配好,别到时候手忙脚乱。