4. 逐点比较法直线插补:从原理到代码实现

逐点比较法,这个名字听起来有点绕。说白了,就是让刀具或画笔,每走一步都回头看一眼:我现在在线的哪一侧?偏了没有?然后决定下一步往哪走。

我最早接触这个算法,是在一台老式的步进电机雕刻机上。那时候没有现成的运动控制卡,全靠单片机逐点算、逐点跑。你别说,虽然现在有更高级的算法,但逐点比较法的思路,至今仍是很多嵌入式运动控制的基础。

4.1 逐点比较法的核心思想

想象一下,你要在纸上画一条从原点 (0,0) 到点 (5,3) 的直线。你不能一笔画过去,只能一格一格地走。怎么走才能最接近那条理想的直线?

逐点比较法的答案很简单:每走一步,就计算当前位置与理想直线的偏差,然后根据偏差的方向,决定下一步是走 X 方向还是 Y 方向。

这个过程可以拆成四个步骤,我习惯叫它“四步循环”:

  1. 偏差判别:判断当前点相对于理想直线的位置(在线上、线上方还是线下方)。
  2. 进给控制:根据偏差结果,决定向 X 或 Y 方向走一步。
  3. 偏差计算:走到新位置后,重新计算新的偏差值。
  4. 终点判别:检查是否走到了终点,没到就继续循环。

核心逻辑:逐点比较法不是“算好路径再走”,而是“走一步、看一步、算一步”。这种实时反馈的特性,让它非常适合硬件资源有限的嵌入式系统。

下面这张图,把整个流程串起来了。你可以对照着看,每一步的逻辑都很清晰。

逐点比较法直线插补四步循环 起点 (0,0) ① 偏差判别 F ≥ 0 还是 F < 0? ② 进给控制 F≥0 走+X,F<0 走+Y ③ 偏差计算 F = F - Ye 或 F = F + Xe ④ 终点判别 总步数减完没? 到达终点 未到终点,继续循环

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 终点判别:别跑过头了

光知道往哪走还不够,还得知道什么时候停。终点判别有几种常见做法:

  1. 总步数法:我最常用的一种。先算出总共需要走多少步:N = |Xe| + |Ye|。每走一步,N 减 1。N 减到 0,就说明到终点了。
  2. 坐标比较法:每走一步,比较当前坐标和终点坐标。都相等了,就停。这个方法需要比较两个坐标值,稍微麻烦一点。

我个人推荐总步数法。为什么?因为它只需要一个计数器,减到零就触发中断,简单可靠。我在一个项目中用过坐标比较法,结果因为比较逻辑写错了,导致电机多走了好几步,把工件给撞了……从那以后,我基本都用总步数法。

注意:总步数法要求 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 语句处理四个象限,代码量没增加多少,但适用性广了很多。

嗯,逐点比较法直线插补的内容,到这里就差不多了。从原理到公式,从递推到代码,每一步我都尽量结合自己的实际经验来讲。希望你能感受到,这个算法虽然年代久远,但它的思想——用简单的加减法实现实时的轨迹控制——至今仍然很有价值。