逐点比较法(一):基本原理与判别函数、进给方向与偏差计算、终点判别方法

各位同学,今天咱们正式进入插补算法的核心内容。逐点比较法,这个名字听起来有点绕,但说白了,它就像你走路时每走一步都要低头看看脚下——「我走偏了没有?该往哪边调整?」

我在刚入行做数控系统时,第一个接触的插补算法就是它。那时候没有现在这么多高级芯片,全靠逻辑电路硬算。你想想看,一个简单的直线插补,CPU 要在一个中断周期内完成偏差计算、方向判断、脉冲输出……嗯,逐点比较法就是那个时代的经典产物。

基本原理:走一步,看一步

逐点比较法的核心思想只有八个字:边运动,边比较,边修正。

具体来说,刀具每走一步,系统就计算一下当前位置与理想轨迹之间的偏差。如果偏差为正,说明刀具偏在轨迹的一侧;如果偏差为负,说明偏在另一侧。然后根据偏差符号,决定下一步往哪个方向走。

核心逻辑闭环:

  1. 偏差判别:当前点偏离理想轨迹了吗?
  2. 进给控制:该往哪个方向走一步?
  3. 偏差计算:走完这一步,新偏差是多少?
  4. 终点判断:到终点了没?没到就回到第1步。

这四步循环往复,直到刀具到达终点。说白了,就是「走一步看一步」的闭环控制。我当年调试第一台三轴雕刻机时,就是用这个逻辑手写了一段直线插补,结果跑出来的轨迹锯齿感很明显——后来才发现是终点判断的精度没处理好。这个坑后面我会细说。

判别函数:如何量化「偏了」?

要判断刀具是否偏离轨迹,首先得有个数学工具。对于直线插补,我们引入一个判别函数 F。

假设我们要插补一条从原点 O(0,0) 到终点 A(xe, ye) 的直线。当前刀具位置为 P(xi, yi)。那么判别函数定义为:

F = x_e * y_i - y_e * x_i

这个公式怎么理解?其实它就是向量 OP 与 OA 的叉积。叉积的正负,反映了 P 点在 OA 的哪一侧。

  • F = 0:刀具正好在直线上(理想情况)
  • F > 0:刀具在直线上方(或左侧,取决于坐标系定义)
  • F < 0:刀具在直线下方(或右侧)

你可能会问:「为什么非要用叉积?用距离不行吗?」

原因很简单——叉积只有乘法和减法,没有开方,没有除法。在早期的数控系统里,CPU 算个开方要几十个周期,而乘减运算一个周期就能搞定。这就是工程上的取舍:用计算量换精度。

进给方向:偏差说了算

有了判别函数 F,进给方向就很好定了。以第一象限直线为例:

判别函数 F 进给方向 说明
F ≥ 0 +X 方向 刀具偏上,向 X 正方向拉回
F < 0 +Y 方向 刀具偏下,向 Y 正方向拉回

为什么 F≥0 时走 +X?你想想看,当 F>0 时,刀具在直线上方,说明 Y 方向走多了,下一步就该多走 X,把偏差拉回来。这就是逐点比较法的精髓——每次只修正一个方向,用阶梯折线逼近理想直线。

我的经验: 实际项目中,进给方向判断要特别注意象限切换。第一象限和第三象限的符号逻辑是相反的。我曾经在写四象限通用插补时,漏了一个符号取反,结果刀具在第三象限直接往反方向跑……那次差点把工件干废。

偏差计算:递推才是王道

每次走完一步,都要重新计算 F 值。但如果每次都按原始公式 F = xe * yi - ye * xi 硬算,那就太笨了——每次都要做两次乘法,太浪费。

这里有个漂亮的优化:递推公式。

假设当前偏差为 Fi,走一步后的新偏差为 Fi+1:

  • 如果走 +X 方向:xi+1 = xi + 1,y 不变
    Fi+1 = xe * yi - ye * (xi + 1)
    = (xe * yi - ye * xi) - ye
    = Fi - ye
  • 如果走 +Y 方向:yi+1 = yi + 1,x 不变
    Fi+1 = xe * (yi + 1) - ye * xi
    = (xe * yi - ye * xi) + xe
    = Fi + xe

看到了吗?递推之后,每次只需要做一次加法或减法!乘法消失了。这在早期的 8 位单片机上是质的飞跃。

// 第一象限直线插补递推核心代码
int F = 0;          // 初始偏差
int x = 0, y = 0;   // 当前位置
int xe = 100, ye = 80; // 终点坐标

while (!isFinished(x, y, xe, ye)) {
    if (F >= 0) {
        // 走 +X
        x++;
        F -= ye;    // 递推:F_new = F_old - ye
        outputPulseX();
    } else {
        // 走 +Y
        y++;
        F += xe;    // 递推:F_new = F_old + xe
        outputPulseY();
    }
}

注意: 递推公式只适用于单位步长(每次走 1 个脉冲当量)。如果你的系统步长不是 1,记得把 xe、ye 换成对应的脉冲数。我曾经见过有人直接把物理坐标代进去,结果递推出来的偏差完全不对——因为单位没统一。

终点判别:别走过头了

终点判别看似简单,但坑不少。常用的方法有三种:

方法 原理 优缺点
总步数法 总步数 N = xe + ye,每走一步减 1 简单可靠,最常用
坐标比较法 判断 x == xe && y == ye 直观,但容易因累积误差无法精确到达
偏差归零法 判断 F == 0 且到达终点附近 精度高,但实现复杂

我个人最推荐总步数法。为什么?因为它在原理上就保证了「走够步数就停」,不会因为累积误差而多走或少走。你想想看,如果终点是 (100, 80),总步数就是 180 步。不管中间怎么偏,走完 180 步,刀具一定在终点附近——误差最多一个脉冲当量。

终点判别的避坑指南:

  • 总步数法要小心整数溢出——如果 xe+ye 超过 65535(16 位),记得用 32 位变量。
  • 坐标比较法要加一个「容差窗口」:|x - xe| ≤ 1 && |y - ye| ≤ 1,防止因舍入误差永远到不了。
  • 偏差归零法只适用于直线终点恰好在网格点上时,否则 F 永远不会归零。

完整流程:一张图看懂

下面我用一张 SVG 流程图,把逐点比较法的完整循环展示出来。你可以把它当作调试时的「路线图」。

逐点比较法完整流程图 逐点比较法完整流程(第一象限直线) 开始 初始化:F=0, x=0, y=0, N=xe+ye N == 0 ? (终点到?) 是 结束 否 F ≥ 0 ? 是 走 +X:F -= ye 否 走 +Y:F += xe 更新坐标:N-- 循环 说明:每走一步,先判终点,再判偏差,递推计算新偏差,直到总步数归零。

这张图把整个循环串起来了。你调试的时候,可以对照这个流程,一步步看变量怎么变。我当年调试时,就是在纸上画了这么一张图,然后手动模拟了十几步,才彻底搞懂递推的规律。

小结:逐点比较法的灵魂

回顾一下,逐点比较法的核心就三件事:

  • 判别函数:用叉积判断刀具在轨迹的哪一侧
  • 递推计算:用加减法代替乘法,大幅降低计算量
  • 终点判别:总步数法最稳妥,别在终点判断上省变量