1.嵌入式测试方法论
一个产品如果完整测试流程:
以 RT-Thread 的 PWM 驱动为例:
- 单元测试(白盒):用 Utest + Mock 测试
rt_pwm_set/rt_pwm_enable函数的参数校验、返回值逻辑; - 集成测试(灰盒):调用 PWM 驱动 + 应用层代码,验证能输出预期占空比的 PWM 波;
- 冒烟测试:快速验证 PWM 基础调光功能是否可用;
- 性能测试:测 PWM 频率切换的响应时间、最大输出频率;
- 稳定性测试:连续运行 72 小时 PWM 调光,监控内存 / CPU 无泄漏;
- 可靠性测试:模拟 PWM 引脚短路,测系统是否能检测并保护硬件;
- 系统测试:结合产品需求,测试 PWM 调光的范围、精度、兼容性;
- 回归测试:修改 PWM 驱动代码后,重新执行所有单元 / 集成用例,避免回归 Bug。
快速开发可以使用冒烟测试,验证 “核心功能能跑起来”。
冒烟测试举例:
/* * Copyright (c) 2006-2018, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2020-12-08 wanghaijing first version. */ /* * 程序清单:这是一个 PWM 设备使用例程 * 例程导出了 pwm_sample 命令到控制终端 * 命令调用格式:pwm_sample * 程序功能:通过逻辑分析仪能看到 PH.10 引脚的电平变化。 */ #include <rtthread.h> #include <rtdevice.h> #define PWM_DEV_NAME "pwm5" /* PWM设备名称 */ #define PWM_DEV_CHANNEL 1 /* PWM通道 */ struct rt_device_pwm *pwm_dev; /* PWM设备句柄 */ static int pwm_sample(int argc, char *argv[]) { rt_uint32_t period, pulse, dir; period = 500000; /* 周期为0.5ms,单位为纳秒ns */ dir = 1; /* PWM脉冲宽度值的增减方向 */ pulse = 0; /* PWM脉冲宽度值,单位为纳秒ns */ /* 查找设备 */ pwm_dev = (struct rt_device_pwm *)rt_device_find(PWM_DEV_NAME); if (pwm_dev == RT_NULL) { rt_kprintf("pwm sample run failed! can't find %s device!\n", PWM_DEV_NAME); return RT_ERROR; } /* 设置PWM周期和脉冲宽度默认值 */ rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse); /* 使能设备 */ rt_pwm_enable(pwm_dev, PWM_DEV_CHANNEL); while (1) { rt_thread_mdelay(50); if (dir) { pulse += 5000; /* 从0值开始每次增加5000ns */ } else { pulse -= 5000; /* 从最大值开始每次减少5000ns */ } if (pulse >= period) { dir = 0; } if (0 == pulse) { dir = 1; } /* 设置PWM周期和脉冲宽度 */ rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse); } } /* 导出到 msh 命令列表中 */ MSH_CMD_EXPORT(pwm_sample, pwm sample);2.utest介绍
如果我们要细致的测试代码,建议将utest用起来。
测试流程:
3.utest举例
用我们的代码举例:
1.增加utest组件
2.配置
3.编译
4.编写代码
打开代码后,发现utest和ulog组件已经加入进来了。
新建一个测试文件:
将该代码添加到工程中:
test.c中:
#include "utest.h" #include <rtthread.h> #include <rtdevice.h> #include <board.h> int math_add(int a, int b) { return a + b; } int math_sub(int a, int b) { return a - b; } rt_bool_t math_is_even(int num) { if (num % 2 == 0) { return RT_TRUE; } return RT_FALSE; } #define utest_log_e(...) rt_kprintf("[UTEST ERROR] " __VA_ARGS__) #define utest_log_i(...) rt_kprintf("[UTEST INFO] " __VA_ARGS__) // 通用断言(条件为假则失败) #define utest_assert(condition, msg) \ do { \ if (!(condition)) \ { \ utest_log_e("Assert failed: %s, file: %s, line: %d, msg: %s", \ #condition, __FILE__, __LINE__, msg); \ utest_handle_get()->error = UTEST_FAILED; \ utest_handle_get()->failed_num++; \ return; \ } \ } while (0) // 整数相等断言 #define utest_assert_int(expected, actual, msg) utest_assert((expected) == (actual), msg) /************************ 单个单元测试函数 ************************/ // 测试加法 static void test_unit_add(void) { // 底层断言宏:utest_assert_int(预期值, 实际值, 失败提示) utest_assert_int(5, math_add(2, 3), "2+3 calculation error"); utest_assert_int(0, math_add(-1, 1), "-1+1 calculation error"); utest_assert_int(0, math_add(0, 0), "0+0 calculation error"); } // 测试减法 static void test_unit_sub(void) { utest_assert_int(2, math_sub(5, 3), "5-3 calculation error"); utest_assert_int(-2, math_sub(3, 5), "3-5 calculation error"); utest_assert_int(0, math_sub(0, 0), "0-0 calculation error"); } // 测试偶数判断 static void test_unit_is_even(void) { // 布尔值断言:先转成整数对比(RT_TRUE=1, RT_FALSE=0) utest_assert_int(RT_TRUE, math_is_even(4), "4 should be even"); utest_assert_int(RT_FALSE, math_is_even(5), "5 should be odd"); utest_assert_int(RT_TRUE, math_is_even(0), "0 should be even"); } /************************ 测试用例入口函数 ************************/ // 总测试用例函数(会被 UTEST_TC_EXPORT 注册) static void test_case_math_utils(void) { // 执行单个单元测试(失败则立即返回,不再执行后续) UTEST_UNIT_RUN(test_unit_add); UTEST_UNIT_RUN(test_unit_sub); UTEST_UNIT_RUN(test_unit_is_even); rt_kprintf("All math utility test cases executed successfully!\n"); } /************************ 初始化/清理函数 ************************/ // 测试用例执行前初始化(可选) static rt_err_t test_case_init(void) { rt_kprintf("===== Math utility test case initialization =====\n"); return RT_EOK; } // 测试用例执行后清理(可选) static rt_err_t test_case_cleanup(void) { rt_kprintf("===== Math utility test case cleanup =====\n"); return RT_EOK; } /************************ 注册测试用例 ************************/ // 格式:UTEST_TC_EXPORT(测试用例函数, 用例名, 初始化函数, 清理函数, 超时时间(秒)) UTEST_TC_EXPORT(test_case_math_utils, "math_utils_test", test_case_init, test_case_cleanup, 10);5.测试
串口助手中运行测试用例:
6.总结
本案例为简化演示,把测试代码和被测试的业务代码写在了一起;但实际工作中建议严格分离:单元测试用例要独立抽离出来,不混入主代码。正式量产的产品固件里,还能通过编译配置跳过测试代码的编译,这样既不影响产品代码的轻量化,也能在开发阶段完整验证功能。
4.单元测试用例总结
一个高质量的嵌入式单元测试用例,需全面覆盖以下核心维度,确保代码在各类场景下的可靠性与鲁棒性:
测试维度 | 测试目标 | 示例场景 |
功能正确性 | 验证核心逻辑是否符合预期(最基础) | 加法函数输入 1+2 应返回 3;串口发送字符应能正确出队 |
边界条件 | 验证参数 / 输入的极值场景 | 数组操作测试下标 0 / 最大值 / 最大值 + 1;缓冲区测试满 / 空 / 刚好满的情况 |
异常场景 | 验证错误输入 / 硬件异常时的容错性 | 传 NULL 指针、参数越界、硬件超时、资源申请失败(如内存分配失败) |
性能 / 耗时 | 验证函数执行效率(嵌入式关注实时性) | 算法执行耗时是否低于阈值;中断响应时间是否符合要求 |
资源泄漏 | 验证内存 / 句柄 / 外设资源是否释放 | 动态申请内存后是否释放;打开的串口 / 定时器是否关闭 |
并发 / 重入 | 验证多线程 / 中断下的逻辑正确性 | 多线程读写同一缓冲区;中断中调用的函数是否可重入 |
兼容性 | 验证不同配置 / 硬件版本下的适配性 | 不同编译选项(如 DEBUG 开启 / 关闭);不同芯片型号(如 STM32F1/F4) |
接口契约 | 验证函数入参 / 返回值是否符合约定 | 返回值是否按规范(0 = 成功,负数 = 错误码);入参检查是否生效 |
优秀的嵌入式工程师,核心能力之一便是通过持续打磨单元测试用例、覆盖全维度场景,不断迭代优化代码,从根源上规避嵌入式系统中因边界、异常、并发等问题引发的硬件故障、系统崩溃等风险。
最后再啰嗦一句:一份高质量的嵌入式单元测试用例,从来不是一蹴而就的,而是要在反复校验、补全场景、规避坑点中慢慢打磨;一名优秀的嵌入式工程师,也绝非短期速成,而是靠无数个项目的沉淀、一次次问题的复盘、长期的深耕细作才得以成长。所谓 “35 岁退休” 的说法,放在嵌入式这个重经验、重沉淀、重实战的领域里,简直可笑至极。