22. 插补算法测试:单元测试框架、边界条件测试、压力测试
做运动控制这么多年,我踩过最大的坑是什么?
不是算法设计,不是RTOS调度,而是——测试没做到位。
你想想看,插补算法在仿真环境里跑得再好,一上真实电机就出问题。为什么?因为测试覆盖不全。边界条件没测,压力没扛住,单元测试形同虚设。今天我就把这三块内容掰开了讲,全是实战经验。
22.1 单元测试框架怎么搭?
我个人习惯用C语言 + Unity测试框架。轻量、嵌入式友好、没有依赖地狱。
先看一个最简单的直线插补单元测试:
#include "unity.h"
#include "interpolator.h"
void setUp(void) {}
void tearDown(void) {}
void test_LineInterpolation_Basic(void) {
Point start = {0.0f, 0.0f, 0.0f};
Point end = {100.0f, 0.0f, 0.0f};
float feedrate = 100.0f; // mm/s
Interpolator interp;
interpolator_init(&interp, &start, &end, feedrate);
// 验证总距离
TEST_ASSERT_FLOAT_WITHIN(0.001f, 100.0f, interp.total_length);
// 验证插补方向向量
TEST_ASSERT_FLOAT_WITHIN(0.001f, 1.0f, interp.dir.x);
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.0f, interp.dir.y);
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.0f, interp.dir.z);
// 验证插补周期
TEST_ASSERT_EQUAL_UINT32(1000, interp.period_us);
}
这里要注意几个点:
- 浮点数比较用WITHIN宏,别用EQUAL。浮点精度问题我吃过亏,曾经一个0.0001的误差导致电机抖动半小时。
- 每个测试函数只测一个逻辑。我见过有人一个函数测10个东西,出错了都不知道是哪个条件触发的。
- setUp/tearDown里做资源初始化。特别是RTOS环境,信号量、队列这些要重置。
22.2 边界条件测试——最容易翻车的地方
我曾经在一个项目中,圆弧插补在90度边界处偶尔会多走一个脉冲。查了三天,发现是角度计算时atan2的边界处理没做好。
边界条件测试,说白了就是找算法的「死穴」。我总结了几类必测场景:
| 边界类型 | 测试场景 | 预期行为 |
|---|---|---|
| 零长度 | 起点==终点 | 不产生任何插补点 |
| 单轴运动 | 仅X轴移动 | Y/Z轴输出恒为0 |
| 最大速度 | 进给率=系统上限 | 不溢出、不丢步 |
| 最小步长 | 移动距离<1个脉冲当量 | 舍入处理或报错 |
| 圆弧半径 | 半径=0 | 退化为点或报错 |
| 象限切换 | 圆弧跨象限 | 平滑过渡,无突变 |
看一个圆弧跨象限的测试代码:
void test_ArcInterpolation_CrossQuadrant(void) {
Point center = {0.0f, 0.0f, 0.0f};
Point start = {100.0f, 0.0f, 0.0f}; // 第一象限
Point end = {0.0f, -100.0f, 0.0f}; // 第四象限
float radius = 100.0f;
ArcInterpolator arc;
arc_interpolator_init(&arc, ¢er, &start, &end, radius, ARC_CW);
Point output;
float last_angle = 0.0f;
int step_count = 0;
while (arc_interpolator_step(&arc, &output) == 0) {
// 验证角度单调递减(CW方向)
float current_angle = atan2f(output.y, output.x);
if (step_count > 0) {
TEST_ASSERT_TRUE(current_angle <= last_angle + 0.001f);
}
last_angle = current_angle;
step_count++;
}
// 验证终点精度
TEST_ASSERT_FLOAT_WITHIN(0.01f, end.x, output.x);
TEST_ASSERT_FLOAT_WITHIN(0.01f, end.y, output.y);
}
22.3 压力测试——让算法「喘不过气」
压力测试不是简单的跑循环。我把它分成三个维度:
- 时间压力:插补周期被压缩到极限
- 空间压力:连续插补段数量巨大
- 资源压力:RTOS任务被频繁抢占
先看时间压力测试。我一般这样设计:
void stress_test_TimingPressure(void) {
// 设置极短插补周期:100us
interpolator_set_period(100);
Point start = {0, 0, 0};
Point end = {10000, 5000, 2000}; // 长距离
Interpolator interp;
interpolator_init(&interp, &start, &end, 500.0f);
uint32_t start_time = micros();
uint32_t max_delta = 0;
uint32_t min_delta = 0xFFFFFFFF;
Point output;
int ret;
do {
uint32_t t0 = micros();
ret = interpolator_step(&interp, &output);
uint32_t t1 = micros();
uint32_t delta = t1 - t0;
if (delta > max_delta) max_delta = delta;
if (delta < min_delta) min_delta = delta;
// 检查是否超时
TEST_ASSERT_TRUE(delta < 100); // 必须在100us内完成
} while (ret == 0);
// 输出统计信息
printf("Max step time: %u us\n", max_delta);
printf("Min step time: %u us\n", min_delta);
printf("Avg step time: %u us\n", (start_time - micros()) / step_count);
}
空间压力测试更狠。我曾在项目中连续下发5000段微线段(每段1-2mm),看插补缓冲区会不会溢出:
void stress_test_SpacePressure(void) {
const int SEGMENT_COUNT = 5000;
Point segments[SEGMENT_COUNT];
// 生成连续微线段
for (int i = 0; i < SEGMENT_COUNT; i++) {
segments[i].x = i * 1.5f;
segments[i].y = sinf(i * 0.1f) * 10.0f;
segments[i].z = 0;
}
// 逐个送入插补器
for (int i = 0; i < SEGMENT_COUNT - 1; i++) {
int ret = interpolator_load_segment(&segments[i], &segments[i+1], 100.0f);
TEST_ASSERT_EQUAL(0, ret); // 不能返回缓冲区满
}
// 验证所有段都被正确消费
TEST_ASSERT_EQUAL(SEGMENT_COUNT - 1, interpolator_consumed_count());
}
核心观点: 压力测试不是为了证明「能跑」,而是为了找到「什么时候会崩」。我习惯在压力测试中故意制造最坏情况——比如在插补任务执行到一半时,用高优先级任务抢占CPU。
22.4 测试自动化与持续集成
手动测试一次两次还行,天天测谁也受不了。我建议把测试集成到CI流水线里:
- 每次提交代码:自动跑单元测试 + 边界条件测试
- 每晚构建:跑完整压力测试(耗时可能30分钟以上)
- 版本发布前:在目标硬件上跑全量测试
这里给个Makefile片段,一键运行所有测试:
test: unit_test boundary_test stress_test
unit_test:
@echo "Running unit tests..."
@./build/test_runner --suite unit
boundary_test:
@echo "Running boundary tests..."
@./build/test_runner --suite boundary
stress_test:
@echo "Running stress tests (this may take a while)..."
@./build/test_runner --suite stress --iterations 10000
22.5 测试覆盖率——别被数字骗了
很多人追求100%行覆盖率,但说实话,行覆盖率高不代表测试质量高。
我见过一个项目,行覆盖率95%,但边界条件一个没测。为什么?因为所有测试用例都走的是「正常路径」,异常分支根本没触发。
我建议关注三个指标:
- 分支覆盖率:每个if-else、switch-case都跑到
- 边界覆盖率:每个边界条件都有对应测试用例
- 突变测试:故意改一个逻辑(比如把+改成-),看测试能不能发现
突变测试听起来高大上,其实实现很简单。我写了个小脚本,自动修改源码中的运算符,然后跑测试。如果测试没报错,说明那行代码的测试是「假阳性」。
# 突变测试示例:把 + 改成 -
sed -i 's/current_pos += step;/current_pos -= step;/' interpolator.c
make test
# 如果测试通过,说明步进累加的测试不充分!
22.6 知识体系总览
下面这张图是我对插补算法测试的完整理解,画出来方便你对照:
这张图把测试体系分成了三层:底层是单元测试,中间是边界条件和压力测试,上层是自动化集成。右侧是质量度量指标,用来衡量测试到底做没做到位。
嗯,最后说一句心里话:测试不是负担,是护身符。我见过太多项目因为测试不到位,在现场出问题,然后花几倍的时间去排查。与其那样,不如一开始就把测试做扎实。
你想想看,一个插补算法写出来可能只要两天,但测试和调优可能要两周。这笔账,值得算。
总结三个关键动作:
- 单元测试用框架,别手写printf调试
- 边界条件列成表格,逐条验证
- 压力测试找最坏情况,别只跑理想场景