2、测试环境搭建:交叉编译工具链、模拟器与开发板、测试框架选择

好,咱们进入第二章。测试环境搭建,说白了就是给你的模型找个「家」。这个家安不好,后面所有测试都是空中楼阁。我见过太多团队,模型在 PC 上跑得飞起,一上板子就崩,最后发现是工具链版本不对。嗯,咱们今天就把这事彻底捋清楚。

2.1 交叉编译工具链:打通 PC 与嵌入式设备的桥梁

嵌入式设备资源有限,你不可能直接在板子上写代码、编译。所以咱们得在强大的 PC 上编译,生成目标设备能运行的二进制文件。这就是交叉编译。

工具链的核心三件套:

  • 编译器:比如 arm-linux-gnueabihf-gcc。名字里就藏着目标架构、操作系统、ABI 等信息。
  • 链接器:负责把目标文件链接成可执行文件。
  • 库文件:目标设备上的 C/C++ 运行时库,比如 libc、libstdc++。

我个人习惯,拿到一块新开发板,第一件事就是去芯片厂商官网下载官方提供的工具链。为什么?因为官方版本经过充分验证,踩坑最少。我曾经图省事,用了一个社区版的工具链,结果浮点运算结果总差一点点,排查了整整两天,最后换回官方版就解决了。

避坑指南:

我曾经在项目里遇到过一个诡异问题:模型推理结果在板子上和 PC 上不一致。后来发现是工具链的浮点 ABI 设置不对。ARM 有 hard-float 和 soft-float 之分,选错了,浮点参数传递方式就变了,结果自然对不上。

工具链安装示例(以 ARM Cortex-A 系列为例):

# 下载官方工具链
wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz

# 解压到指定目录
tar -xf gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz -C /opt/

# 设置环境变量
export PATH=/opt/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/bin:$PATH

# 验证安装
arm-none-linux-gnueabihf-gcc --version

你想想看,工具链装好后,怎么验证它能不能用?我一般会写一个最简单的 Hello World,然后静态编译,放到板子上跑。如果这一步通了,后面就顺了。

2.2 模拟器与开发板:测试的两条腿

开发板贵,而且数量有限。模拟器就是你的「虚拟开发板」,可以在 PC 上模拟目标设备的 CPU、内存、外设。但模拟器终究是模拟的,性能、时序、外设行为都和真实硬件有差距。

什么时候用模拟器?

  • 早期功能验证:比如模型推理逻辑对不对。
  • 单元测试:跑 Google Test 用例,验证算法模块。
  • 回归测试:每次代码提交后,快速跑一遍。

什么时候必须上开发板?

  • 性能测试:推理延迟、内存带宽、功耗。
  • 外设交互:摄像头、显示屏、传感器。
  • 长时间稳定性测试:跑 72 小时,看会不会死机。

我的经验:

我建议团队里至少配 3 块开发板。一块给开发人员日常调试,一块给测试人员跑自动化测试,一块作为备用。别问我为什么知道——有一次主力板子烧了,项目直接停了两天。

常用模拟器对比:

模拟器 适用架构 特点 启动速度
QEMU ARM, RISC-V, x86 等 开源,支持全系统模拟 中等
Renode ARM, RISC-V 支持多核,调试能力强 较快
Zephyr 模拟器 ARM, x86 专为 Zephyr RTOS 优化

说白了,模拟器帮你快速迭代,开发板帮你做最终验证。两条腿走路,才稳当。

2.3 测试框架选择:Google Test vs Catch2

测试框架,就是帮你写测试用例、组织测试、生成报告的工具。嵌入式 C/C++ 项目里,最常用的就是 Google Test 和 Catch2。

Google Test(gtest):

  • Google 出品,文档齐全,社区活跃。
  • 支持参数化测试、死亡测试、值参数化。
  • 集成到 CMake 非常方便。

Catch2:

  • 单头文件库,部署简单。
  • 使用 BDD 风格,测试用例可读性强。
  • 支持 Section,可以嵌套测试场景。

我个人的选择标准:

  • 如果团队里有人用过 gtest,那就用 gtest。学习成本最低。
  • 如果项目是全新的,我倾向于 Catch2。因为它部署简单,而且 BDD 风格写出来的测试用例,产品经理都能看懂。
  • 如果项目对二进制大小有严格限制,Catch2 的单头文件方式更友好。

注意:

在嵌入式设备上跑单元测试,内存是个大问题。我曾经在只有 64KB RAM 的 MCU 上跑 gtest,结果堆栈溢出。后来改用 Catch2,并且把测试用例拆成多个小文件,才搞定。所以,选框架之前,先看看你的目标设备有多少内存。

Google Test 示例(交叉编译):

// test_model.cpp
#include <gtest/gtest.h>
#include "model_runner.h"

TEST(ModelTest, InferenceResult) {
    ModelRunner runner;
    float input[10] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
    float output[5];
    
    runner.load_model("model.tflite");
    runner.run_inference(input, output);
    
    // 验证输出是否在预期范围内
    for (int i = 0; i < 5; i++) {
        EXPECT_NEAR(output[i], expected_output[i], 1e-5);
    }
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Catch2 示例(交叉编译):

// test_model.cpp
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
#include "model_runner.h"

TEST_CASE("Model inference produces correct results", "[model]") {
    ModelRunner runner;
    float input[10] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
    float output[5];
    
    runner.load_model("model.tflite");
    runner.run_inference(input, output);
    
    SECTION("Output values are within tolerance") {
        for (int i = 0; i < 5; i++) {
            REQUIRE(output[i] == Approx(expected_output[i]).epsilon(1e-5));
        }
    }
}

你可能会问,这两个框架在嵌入式上怎么选?其实核心就三点:内存占用、编译速度、社区支持。我个人建议,如果项目规模大、团队人多,选 gtest;如果项目小、追求快速上手,选 Catch2。

总结一下:

测试环境搭建,不是装个工具链就完事了。你得想清楚:

  1. 工具链版本和芯片架构是否匹配?
  2. 模拟器和开发板怎么分工?
  3. 测试框架能不能在目标设备上跑起来?

这三个问题想明白了,你的测试环境就稳了。后面写测试用例、跑自动化,都会顺风顺水。

好,这一章就到这里。下一章咱们聊聊「测试用例设计:从单元测试到集成测试」,到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。