4. 逐点比较法直线插补:从原理到代码实现
逐点比较法,这个名字听起来有点绕。说白了,就是让刀具或画笔,每走一步都回头看一眼:我现在在线的哪一侧?偏了没有?然后决定下一步往哪走。
我最早接触这个算法,是在一台老式的步进电机雕刻机上。那时候没有现成的运动控制卡,全靠单片机逐点算、逐点跑。你别说,虽然现在有更高级的算法,但逐点比较法的思路,至今仍是很多嵌入式运动控制的基础。
4.1 逐点比较法的核心思想
想象一下,你要在纸上画一条从原点 (0,0) 到点 (5,3) 的直线。你不能一笔画过去,只能一格一格地走。怎么走才能最接近那条理想的直线?
逐点比较法的答案很简单:每走一步,就计算当前位置与理想直线的偏差,然后根据偏差的方向,决定下一步是走 X 方向还是 Y 方向。
这个过程可以拆成四个步骤,我习惯叫它“四步循环”:
- 偏差判别:判断当前点相对于理想直线的位置(在线上、线上方还是线下方)。
- 进给控制:根据偏差结果,决定向 X 或 Y 方向走一步。
- 偏差计算:走到新位置后,重新计算新的偏差值。
- 终点判别:检查是否走到了终点,没到就继续循环。
核心逻辑:逐点比较法不是“算好路径再走”,而是“走一步、看一步、算一步”。这种实时反馈的特性,让它非常适合硬件资源有限的嵌入式系统。
下面这张图,把整个流程串起来了。你可以对照着看,每一步的逻辑都很清晰。
4.2 偏差计算公式的由来
公式怎么来的?我当初学的时候,老师直接扔了个公式出来,搞得我一头雾水。后来自己推导了一遍,发现其实很简单。
假设我们要插补第一象限的直线,起点在原点 O(0,0),终点是 E(Xe, Ye)。对于任意一点 P(Xi, Yi),我们可以用直线方程来判断它的位置:
直线 OE 的方程是:Xe * Y - Ye * X = 0
把 P 点坐标代进去,就得到了偏差值 F:
F = Xe * Yi - Ye * Xi
这个 F 值能告诉我们什么?
- F = 0:P 点正好在直线上。
- F > 0:P 点在直线上方(对于第一象限直线来说)。
- F < 0:P 点在直线下方。
有了这个判断,进给方向就很好定了:
| 偏差 F | 当前位置 | 进给方向 | 说明 |
|---|---|---|---|
| F ≥ 0 | 线上或线上方 | +X 方向 | 向直线靠拢 |
| F < 0 | 线下方 | +Y 方向 | 向直线靠拢 |
一个小技巧:我习惯把 F 的符号和进给方向记成“大走 X,小走 Y”。F≥0 说明当前点偏上(Y 大了),那就走 X 方向去追;F<0 说明偏下(X 大了),就走 Y 方向去追。这样记不容易搞混。
4.3 递推计算:避免重复算乘法
每次都用 F = Xe * Yi - Ye * Xi 算一遍,虽然可行,但太慢了。尤其是在老式的 8 位单片机上,乘法指令很费时间。
所以,实际工程中用的是递推公式。什么意思?就是利用上一步的 F 值,加上一个简单的加减法,就能得到下一步的 F 值。
推导过程很简单,我直接说结论:
- 走 +X 后:新坐标 (Xi+1, Yi),新偏差
F_new = F_old - Ye - 走 +Y 后:新坐标 (Xi, Yi+1),新偏差
F_new = F_old + Xe
你看,每次只需要做一次加法或减法。这在硬件上几乎不花时间。
为什么能这样算? 因为走 X 方向时,Yi 不变,Xi 增加 1,代入原公式一减,Ye 就出来了。走 Y 方向同理。说白了,就是把乘法转化成了累加。
4.4 终点判别:别跑过头了
光知道往哪走还不够,还得知道什么时候停。终点判别有几种常见做法:
- 总步数法:我最常用的一种。先算出总共需要走多少步:
N = |Xe| + |Ye|。每走一步,N 减 1。N 减到 0,就说明到终点了。 - 坐标比较法:每走一步,比较当前坐标和终点坐标。都相等了,就停。这个方法需要比较两个坐标值,稍微麻烦一点。
我个人推荐总步数法。为什么?因为它只需要一个计数器,减到零就触发中断,简单可靠。我在一个项目中用过坐标比较法,结果因为比较逻辑写错了,导致电机多走了好几步,把工件给撞了……从那以后,我基本都用总步数法。
注意:总步数法要求 Xe 和 Ye 都是整数(以脉冲当量为单位)。如果你的终点坐标是浮点数,需要先做取整处理。取整带来的误差,通常在一个脉冲当量以内,对于大多数应用是可以接受的。
4.5 软件实现:C 语言代码示例
说了这么多,不如直接看代码。下面是一个完整的逐点比较法直线插补函数,适用于第一象限。
// 逐点比较法直线插补(第一象限)
// 输入:终点坐标 (Xe, Ye),单位:脉冲数
void line_interpolate(int Xe, int Ye) {
int step_total = Xe + Ye; // 总步数
int F = 0; // 初始偏差
int X = 0, Y = 0; // 当前位置
while (step_total > 0) {
// 第一步:偏差判别
if (F >= 0) {
// 第二步:进给 +X
X++;
// 第三步:偏差计算
F = F - Ye;
} else {
// 第二步:进给 +Y
Y++;
// 第三步:偏差计算
F = F + Xe;
}
// 第四步:终点判别
step_total--;
// 输出脉冲(实际项目中这里会调用硬件驱动函数)
// motor_step_x(X);
// motor_step_y(Y);
}
}
这段代码看起来很简单,对吧?但实际工程中,有几个坑需要注意:
- 象限处理:上面的代码只适用于第一象限。其他象限需要根据 Xe、Ye 的符号,调整进给方向。比如第二象限,X 方向是负向走。
- 速度控制:这个循环跑得飞快。如果不加延时,电机会以最高速度运行,可能会丢步。我一般会在循环里加一个定时器等待,或者用硬件 PWM 来控制脉冲频率。
- 溢出问题:如果 Xe 和 Ye 很大(比如几万),F 值可能会超出 int 范围。建议用 long 或者做限幅处理。
我的调试习惯:在正式跑电机之前,我会先用 printf 把每一步的 X、Y、F 值打印出来,画在纸上或者用 Excel 描点。看一眼轨迹就知道算法对不对,比直接上电机安全多了。
4.6 各象限的进给方向总结
不同象限的进给方向不一样,但规律是一样的:F≥0 走 X 方向,F<0 走 Y 方向。只是 X 和 Y 的正负要根据象限来定。
| 象限 | 终点坐标符号 | F ≥ 0 进给 | F < 0 进给 |
|---|---|---|---|
| 第一象限 | (+Xe, +Ye) | +X | +Y |
| 第二象限 | (-Xe, +Ye) | -X | +Y |
| 第三象限 | (-Xe, -Ye) | -X | -Y |
| 第四象限 | (+Xe, -Ye) | +X | -Y |
你想想看,其实不管哪个象限,核心逻辑都一样。只要把 Xe 和 Ye 的绝对值代入偏差计算,进给方向根据象限符号表查一下就行。我曾经写过一个通用的插补函数,用一个 switch 语句处理四个象限,代码量没增加多少,但适用性广了很多。
嗯,逐点比较法直线插补的内容,到这里就差不多了。从原理到公式,从递推到代码,每一步我都尽量结合自己的实际经验来讲。希望你能感受到,这个算法虽然年代久远,但它的思想——用简单的加减法实现实时的轨迹控制——至今仍然很有价值。