实战项目二:嵌入式模块测试、模拟硬件接口、集成测试

好,咱们进入第二个实战项目。这个项目我特别喜欢,因为它特别“接地气”。说白了,就是教你如何在电脑上,把那些依赖硬件的嵌入式模块给测透了。你想想看,做嵌入式最头疼的是什么?硬件还没回来,或者硬件不稳定,代码怎么测?

我个人习惯,在项目启动的第一周,就把模拟硬件接口的测试框架搭好。这样等硬件一回来,我的软件已经跑过好几轮了。嗯,这招帮我省了无数个加班的夜晚。

项目背景:一个温控模块

咱们要测的是一个温控模块。它负责读取温度传感器,控制风扇转速,并通过串口上报数据。硬件接口包括:

  • 一个I2C温度传感器(地址0x48)
  • 一个GPIO控制的风扇(高电平开启)
  • 一个UART串口(115200, 8N1)

目标很明确:在没有任何真实硬件的情况下,把整个模块的逻辑测通。

第一步:模拟硬件接口

模拟硬件接口,说白了就是“造假”。但造假也要造得像。我建议你为每个硬件外设写一个模拟层。

先看I2C模拟:

// mock_i2c.h
#ifndef MOCK_I2C_H
#define MOCK_I2C_H

#include <stdint.h>

// 模拟I2C读写函数
int mock_i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);
int mock_i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);

// 设置模拟温度值(测试用)
void mock_i2c_set_temperature(float temp_celsius);

#endif
// mock_i2c.c
#include "mock_i2c.h"

static float simulated_temperature = 25.0f;

int mock_i2c_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len) {
    // 只模拟温度传感器
    if (dev_addr != 0x48) return -1;
    
    // 假设温度寄存器是0x00,返回两个字节
    if (reg_addr == 0x00 && len == 2) {
        int16_t raw = (int16_t)(simulated_temperature * 100.0f);
        data[0] = (raw >> 8) & 0xFF;
        data[1] = raw & 0xFF;
        return 0;
    }
    return -1;
}

int mock_i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len) {
    // 温度传感器通常不需要写操作,直接返回成功
    return 0;
}

void mock_i2c_set_temperature(float temp_celsius) {
    simulated_temperature = temp_celsius;
}
我的小技巧:模拟层一定要提供“注入”接口。比如上面的mock_i2c_set_temperature,这样测试用例就能随意控制温度值,模拟各种边界情况。

GPIO和UART的模拟也是同理。GPIO就维护一个状态变量,UART就搞个环形缓冲区。我曾经在一个项目里,因为UART模拟层没做好,导致集成测试时死活收不到数据,查了两天才发现是模拟层的缓冲区溢出处理写错了。嗯,从那以后我特别重视模拟层的健壮性。

第二步:模块测试

模块测试,就是单独测温控模块的每个函数。咱们的温控模块大概长这样:

// temp_control.h
#ifndef TEMP_CONTROL_H
#define TEMP_CONTROL_H

#include <stdint.h>

void temp_control_init(void);
float temp_control_read_temperature(void);
void temp_control_set_fan_speed(uint8_t speed); // 0-100
uint8_t temp_control_get_fan_speed(void);
void temp_control_report_status(char *buffer, uint16_t buf_size);

#endif

测试用例怎么写?我习惯用“三段式”:准备、执行、验证。

// test_temp_control.c
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include "temp_control.h"
#include "mock_i2c.h"
#include "mock_gpio.h"
#include "mock_uart.h"

void test_read_temperature_normal(void) {
    printf("测试:正常读取温度...\n");
    
    // 准备:设置模拟温度为30.5度
    mock_i2c_set_temperature(30.5f);
    temp_control_init();
    
    // 执行
    float temp = temp_control_read_temperature();
    
    // 验证
    assert(temp > 30.0f && temp < 31.0f);
    printf("通过!温度 = %.2f°C\n", temp);
}

void test_fan_speed_high_temperature(void) {
    printf("测试:高温时风扇全速...\n");
    
    // 准备:设置模拟温度为85度(高温)
    mock_i2c_set_temperature(85.0f);
    temp_control_init();
    
    // 执行
    temp_control_read_temperature();
    temp_control_set_fan_speed(100); // 假设逻辑是温度>80时全速
    
    // 验证
    uint8_t speed = temp_control_get_fan_speed();
    assert(speed == 100);
    printf("通过!风扇速度 = %d%%\n", speed);
}

void test_report_status_format(void) {
    printf("测试:状态报告格式...\n");
    
    mock_i2c_set_temperature(45.2f);
    temp_control_init();
    temp_control_read_temperature();
    temp_control_set_fan_speed(60);
    
    char buffer[64];
    temp_control_report_status(buffer, sizeof(buffer));
    
    // 验证格式:应该包含温度和速度信息
    assert(strstr(buffer, "Temp") != NULL);
    assert(strstr(buffer, "Fan") != NULL);
    printf("通过!报告内容:%s\n", buffer);
}

int main(void) {
    test_read_temperature_normal();
    test_fan_speed_high_temperature();
    test_report_status_format();
    
    printf("\n所有模块测试通过!\n");
    return 0;
}
核心要点:模块测试要覆盖正常路径、边界路径和异常路径。比如温度传感器读失败怎么办?风扇速度设为200%怎么办?这些都要测。

第三步:集成测试

模块测试通过后,就该集成测试了。集成测试测的是模块之间的交互。说白了,就是看整个温控流程能不能跑通。

我建议用“场景驱动”的方式写集成测试。比如:

// test_integration.c
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include "temp_control.h"
#include "mock_i2c.h"
#include "mock_gpio.h"
#include "mock_uart.h"

void test_full_control_cycle(void) {
    printf("集成测试:完整控制周期...\n");
    
    // 场景1:低温启动
    mock_i2c_set_temperature(20.0f);
    temp_control_init();
    
    float temp = temp_control_read_temperature();
    assert(temp < 25.0f);
    
    // 风扇应该低速或关闭
    uint8_t speed = temp_control_get_fan_speed();
    assert(speed < 30);
    printf("  低温阶段:温度=%.1f, 风扇=%d%%\n", temp, speed);
    
    // 场景2:温度升高
    mock_i2c_set_temperature(55.0f);
    temp = temp_control_read_temperature();
    temp_control_set_fan_speed(70);
    
    speed = temp_control_get_fan_speed();
    assert(speed >= 70);
    printf("  中温阶段:温度=%.1f, 风扇=%d%%\n", temp, speed);
    
    // 场景3:高温报警
    mock_i2c_set_temperature(90.0f);
    temp = temp_control_read_temperature();
    temp_control_set_fan_speed(100);
    
    speed = temp_control_get_fan_speed();
    assert(speed == 100);
    
    // 检查串口是否发送了报警信息
    char report[64];
    temp_control_report_status(report, sizeof(report));
    assert(strstr(report, "ALARM") != NULL || strstr(report, "HIGH") != NULL);
    printf("  高温阶段:温度=%.1f, 风扇=%d%%, 报告=%s\n", temp, speed, report);
    
    printf("集成测试通过!\n");
}

int main(void) {
    test_full_control_cycle();
    printf("\n所有集成测试通过!\n");
    return 0;
}
注意:集成测试的用例不要太多,5-8个核心场景就够了。我曾经见过有人写了50个集成测试用例,结果每次跑都要10分钟,最后大家都不跑了。集成测试贵精不贵多。

知识体系:嵌入式测试的核心逻辑

下面这张图,是我自己总结的嵌入式测试分层模型。你看一眼就明白了。

嵌入式测试分层模型 硬件模拟层 (Mock Layer) 模拟I2C、GPIO、UART等硬件外设行为 模块测试层 (Unit Test Layer) 单独测试每个函数/模块的功能正确性 集成测试层 (Integration Test Layer) 测试模块间交互、完整业务流程 硬件回归测试 (Hardware Regression) 测试金字塔 底层多,上层少 底层快,上层慢

你看,从下往上,测试范围越来越大,但测试速度越来越慢。我个人习惯,模块测试要占到70%以上,集成测试20%,硬件回归测试10%。这个比例是我在多个项目中摸索出来的,比较实用。

避坑指南

最后,分享几个我踩过的坑:

  • 模拟层太“完美”:我曾经把模拟I2C写得特别完美,从不报错。结果上硬件后,传感器偶尔通信失败,代码直接崩溃。后来我在模拟层里加了随机错误注入,才把这个问题提前暴露出来。
  • 忽略时序问题:模拟层跑得飞快,但真实硬件有延迟。比如GPIO从拉高到稳定需要几微秒。我建议在模拟层里加一些微小的延时,模拟真实情况。
  • 集成测试用例太少:只测了“温度正常-风扇正常”这一个场景。实际上,温度传感器断线、风扇堵转、串口缓冲区满,这些异常场景都要覆盖到。
我的建议:每次写完模拟层,先跑一个“冒烟测试”——就是最核心的3-5个用例。如果冒烟测试都过不了,别往下写了,先修模拟层。

好了,这个实战项目就讲到这里。你回去可以试着把温控模块的代码写出来,然后按照我说的三层结构搭测试。相信我,等你习惯了这种测试方式,你会爱上它的——因为bug会少很多,而且定位问题快得惊人。


公众号:蓝海资料掘金营,微信deep3321