实战项目二:嵌入式模块测试、模拟硬件接口、集成测试
好,咱们进入第二个实战项目。这个项目我特别喜欢,因为它特别“接地气”。说白了,就是教你如何在电脑上,把那些依赖硬件的嵌入式模块给测透了。你想想看,做嵌入式最头疼的是什么?硬件还没回来,或者硬件不稳定,代码怎么测?
我个人习惯,在项目启动的第一周,就把模拟硬件接口的测试框架搭好。这样等硬件一回来,我的软件已经跑过好几轮了。嗯,这招帮我省了无数个加班的夜晚。
项目背景:一个温控模块
咱们要测的是一个温控模块。它负责读取温度传感器,控制风扇转速,并通过串口上报数据。硬件接口包括:
- 一个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;
}
第三步:集成测试
模块测试通过后,就该集成测试了。集成测试测的是模块之间的交互。说白了,就是看整个温控流程能不能跑通。
我建议用“场景驱动”的方式写集成测试。比如:
// 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;
}
知识体系:嵌入式测试的核心逻辑
下面这张图,是我自己总结的嵌入式测试分层模型。你看一眼就明白了。
你看,从下往上,测试范围越来越大,但测试速度越来越慢。我个人习惯,模块测试要占到70%以上,集成测试20%,硬件回归测试10%。这个比例是我在多个项目中摸索出来的,比较实用。
避坑指南
最后,分享几个我踩过的坑:
- 模拟层太“完美”:我曾经把模拟I2C写得特别完美,从不报错。结果上硬件后,传感器偶尔通信失败,代码直接崩溃。后来我在模拟层里加了随机错误注入,才把这个问题提前暴露出来。
- 忽略时序问题:模拟层跑得飞快,但真实硬件有延迟。比如GPIO从拉高到稳定需要几微秒。我建议在模拟层里加一些微小的延时,模拟真实情况。
- 集成测试用例太少:只测了“温度正常-风扇正常”这一个场景。实际上,温度传感器断线、风扇堵转、串口缓冲区满,这些异常场景都要覆盖到。
好了,这个实战项目就讲到这里。你回去可以试着把温控模块的代码写出来,然后按照我说的三层结构搭测试。相信我,等你习惯了这种测试方式,你会爱上它的——因为bug会少很多,而且定位问题快得惊人。
公众号:蓝海资料掘金营,微信deep3321