逐点比较法(一):直线插补原理与偏差计算

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

我最早接触这个算法是在一台老式的开环数控系统上。那时候没有伺服闭环,步进电机每转一圈就丢几步,但逐点比较法硬是靠简单的加减法把轨迹精度控制在了一个脉冲当量以内。嗯,今天我们就把它拆开看看。

核心思想:步步为营

想象你站在一张网格纸上,要从起点 O(0,0) 走到终点 A(4,3)。你不能斜着走,只能横着或竖着迈步。逐点比较法的策略就是:

  1. 每走一步,算一下当前位置与理想直线的偏差
  2. 根据偏差的正负,决定下一步往 X 还是 Y 方向走
  3. 走到终点就停

你想想看,这不就是人在迷宫里摸墙走的逻辑吗?只不过我们用数学公式代替了手感。

核心结论:逐点比较法每步只做一次加减法和一次符号判断,计算量极小,非常适合嵌入式实时系统。我在一个 8 位 MCU 上跑过,主频只有 8MHz,照样能驱动三轴联动。

偏差计算公式推导

设直线起点 O(0,0),终点 A(Xe, Ye)。当前刀具位于 P(Xi, Yi)。

我们需要一个值 Fi,能反映 P 点在 OA 线的哪一侧。怎么算?

先写出直线 OA 的方程:

Y / X = Ye / Xe   →   Xe·Y - Ye·X = 0

把当前点 P 的坐标代进去,定义偏差:

Fi = Xe·Yi - Ye·Xi

这个 Fi 的符号就是关键:

  • Fi = 0:P 正好在直线上
  • Fi > 0:P 在直线上方(对于第一象限)
  • Fi < 0:P 在直线下方

为什么会这样?你拿第一象限的线试一下:如果 P 偏上,Yi 偏大,Fi 自然为正。反过来也一样。

我的习惯:推导公式时别死记硬背。你就记住「叉积」两个字——Fi 其实就是向量 OA 和 OP 的叉积的 Z 分量。叉积的正负代表旋转方向,正好对应点在线的哪一侧。

进给方向判断

有了偏差 Fi,进给规则就很简单了。还是以第一象限为例:

偏差 Fi 含义 进给方向 下一步坐标
Fi ≥ 0 点在线上或上方 +X 方向 Xi+1 = Xi + 1, Yi+1 = Yi
Fi < 0 点在线下方 +Y 方向 Xi+1 = Xi, Yi+1 = Yi + 1

规则背后的直觉:偏上了就往右拉,偏下了就往上提。每一步都在纠正偏差,最终走出一条逼近直线的折线。

我曾经踩过的坑:注意 Fi = 0 时归入 Fi ≥ 0 的情况,走 +X。如果你把 Fi = 0 单独处理成走 +Y,终点判断会出问题。因为终点 (Xe, Ye) 的偏差 Fi = 0,按 +X 走一步就超出 Xe 了,而按 +Y 走会多走一步。这个小细节让我的第一版代码在终点处多抖了一下。

递推计算:告别乘法

每次都用 Fi = Xe·Yi - Ye·Xi 算一遍?太慢了。数控系统每毫秒要走好几步,乘法吃不消。

我们用递推。假设当前点 P(Xi, Yi),偏差 Fi:

  • 若 Fi ≥ 0,走 +X:
    Xi+1 = Xi + 1
    Yi+1 = Yi
    Fi+1 = Xe·Yi - Ye·(Xi + 1) = Fi - Ye
  • 若 Fi < 0,走 +Y:
    Xi+1 = Xi
    Yi+1 = Yi + 1
    Fi+1 = Xe·(Yi + 1) - Ye·Xi = Fi + Xe

看到了吗?每次更新偏差只需要一次加减法。Fi+1 = Fi - Ye 或 Fi+1 = Fi + Xe。Ye 和 Xe 是终点坐标,提前算好存起来就行。

性能对比:直接公式法每步需要 2 次乘法和 1 次减法;递推法只需要 1 次加法。在 100kHz 插补频率下,递推法每秒节省约 20 万次乘法运算——这对老平台是质的飞跃。

终点判断

走多少步停下来?总步数 N = Xe + Ye。每走一步,计数器减 1。减到 0 就停。

为什么不是 √(Xe² + Ye²)?因为逐点比较法走的是曼哈顿路径,总步数就是 X 方向步数加 Y 方向步数。终点 (Xe, Ye) 恰好是 X 和 Y 各自走完的步数。

我建议:终点计数器用有符号整数。如果因为干扰多走了一步,计数器变成负数,也能及时刹停。我在一台老机床上遇到过步进电机丢步导致过冲的情况,负数判断救了我一命。

完整流程与 SVG 图解

把上面所有环节串起来,就是逐点比较法直线插补的完整流程:

逐点比较法直线插补流程图(第一象限) 开始 初始化:X=0, Y=0, F=0, N=Xe+Ye N > 0? 结束 F ≥ 0? 走 +X:F = F - Ye 走 +Y:F = F + Xe N ≤ 0 N > 0 F ≥ 0 F < 0 说明:每走一步,N 减 1;F 的更新只用加减法,无需乘法

代码实现:C 语言版

下面是一个完整的逐点比较法直线插补函数。我习惯把终点坐标和当前坐标都放在结构体里,方便扩展成多轴。

typedef struct {
    int Xe, Ye;   // 终点坐标(相对起点)
    int X, Y;     // 当前位置
    int F;        // 当前偏差
    int N;        // 总步数计数器
} LineInterpolator;

void line_init(LineInterpolator *li, int Xe, int Ye) {
    li->Xe = Xe;
    li->Ye = Ye;
    li->X  = 0;
    li->Y  = 0;
    li->F  = 0;
    li->N  = Xe + Ye;
}

// 返回 0:插补结束;返回 1:还有步数
int line_step(LineInterpolator *li, int *dx, int *dy) {
    if (li->N <= 0) return 0;

    if (li->F >= 0) {
        // 走 +X
        *dx = 1;
        *dy = 0;
        li->X += 1;
        li->F -= li->Ye;
    } else {
        // 走 +Y
        *dx = 0;
        *dy = 1;
        li->Y += 1;
        li->F += li->Xe;
    }

    li->N--;
    return 1;
}

使用建议:line_step 返回的 dx, dy 可以直接送给步进电机驱动器。如果你用梯形加减速,把 dx, dy 塞进速度规划队列就行。我在一个激光雕刻项目里就是这么干的,效果很稳。

各象限的进给规则

上面只讲了第一象限。其他象限怎么办?其实很简单——根据终点所在的象限,把 X 和 Y 的进给方向做镜像即可。

象限 终点符号 F ≥ 0 进给 F < 0 进给
一 Xe>0, Ye>0 +X +Y
二 Xe<0, Ye>0 -X +Y
三 Xe<0, Ye<0 -X -Y
四 Xe>0, Ye<0 +X -Y

偏差更新公式在所有象限都一样:F ≥ 0 时 F = F - |Ye|,F < 0 时 F = F + |Xe|。绝对值保证了递推的一致性。

我曾经犯过的错:在第二象限直接用了 F = F - Ye,忘了 Ye 是正数,结果偏差越算越偏。后来我统一用绝对值做递推,进给方向单独用符号表控制,代码清晰多了。

精度分析

逐点比较法的最大偏差不超过一个脉冲当量。为什么?因为每走一步都在纠正,偏差 Fi 的绝对值永远不会超过 max(|Xe|, |Ye|)。换算成几何距离,轨迹与理想直线的垂直距离 ≤ 1 个步长。

对于大多数开环系统,这个精度足够了。如果你需要亚微米级精度,那就得考虑数据采样法或时间分割法——那是后面章节的内容。

一句话总结:逐点比较法用最简单的加减法,实现了脉冲当量级的轨迹精度。它不完美,但在资源受限的嵌入式系统里,它是最可靠的选择之一。