3. 逐点比较法(二):直线插补的推导与实现、圆弧插补的推导与实现、不同象限的处理
上一章我们聊了逐点比较法的基本思路——说白了就是“走一步看一步,偏了就纠”。这一章咱们把它落到实处,把直线和圆弧的插补逻辑完整推一遍,再把不同象限的坑填上。
我个人习惯把逐点比较法叫做“最朴素的插补算法”,因为它不需要三角函数,不需要矩阵运算,全靠加减法和符号判断就能干活。早期数控系统里,这玩意儿是绝对的主力。
3.1 直线插补的推导
先看第一象限的直线。假设起点在原点 O(0,0),终点是 E(Xe,Ye),当前刀具在 P(Xi,Yi)。
判断点在直线哪一侧,我用的是“偏差函数”这个概念:
F = Xi * Ye - Yi * Xe
为什么是这个式子?你想想看,如果 P 点在直线上,那么三角形 OPE 的面积就是零。这个 F 值其实就是叉积的 2 倍,正负号直接告诉你点在直线的哪一侧。
核心判断逻辑:
- F = 0:点在直线上,走 +X 方向
- F > 0:点在直线上方,走 +X 方向(逼近直线)
- F < 0:点在直线下方,走 +Y 方向(逼近直线)
每次走一步后,偏差函数怎么更新?这里有个小技巧——不用重新算乘法,用递推公式就行。
如果走了 +X 步(Xi+1 = Xi + 1):
F_new = (Xi+1) * Ye - Yi * Xe = F_old + Ye
如果走了 +Y 步(Yi+1 = Yi + 1):
F_new = Xi * Ye - (Yi+1) * Xe = F_old - Xe
嗯,这里要注意:递推公式里只有加减法,没有乘法。这就是逐点比较法能在老式步进电机系统上跑得飞快的原因。
实战经验:我在调试一台老式铣床时遇到过一个问题——终点判断。光靠 F 值归零是不够的,因为可能刚好跨过终点。我的做法是加一个计数器,记录 X 和 Y 方向的总步数,走完就停。
3.2 直线插补的实现
下面给一个 C 风格的伪代码实现。注意,这里只处理第一象限,后面再扩展。
void line_interp(int Xe, int Ye) {
int F = 0;
int step = Xe + Ye; // 总步数
int x = 0, y = 0;
while (step > 0) {
if (F >= 0) {
// 走 +X
x++;
F -= Ye; // 注意:这里减 Ye,不是加
printf("Step +X, pos(%d,%d)\n", x, y);
} else {
// 走 +Y
y++;
F += Xe;
printf("Step +Y, pos(%d,%d)\n", x, y);
}
step--;
}
}
避坑指南:我曾经在递推公式的符号上栽过跟头。F >= 0 时走 +X,F 要 减 Ye;F < 0 时走 +Y,F 要 加 Xe。这个符号和偏差函数的定义方式有关,不同教材可能正负相反,但逻辑必须自洽。
3.3 圆弧插补的推导
圆弧插补比直线复杂一些。以第一象限逆圆为例,圆心在原点,起点 S(Xs,Ys),终点 E(Xe,Ye),当前点 P(Xi,Yi)。
偏差函数用点到圆心的距离平方差来定义:
F = Xi² + Yi² - R²
其中 R 是圆弧半径。判断逻辑:
- F = 0:点在圆弧上
- F > 0:点在圆外
- F < 0:点在圆内
对于逆圆,进给方向是:
- F >= 0:点在圆外或圆上,走 -X 方向(向圆内靠)
- F < 0:点在圆内,走 +Y 方向(向圆外靠)
递推公式同样可以简化。走 -X 步(Xi-1 = Xi - 1):
F_new = (Xi-1)² + Yi² - R² = F_old - 2*Xi + 1
走 +Y 步(Yi+1 = Yi + 1):
F_new = Xi² + (Yi+1)² - R² = F_old + 2*Yi + 1
你看,递推公式里只有加减法和乘 2(左移一位),没有平方运算。这就是逐点比较法的精髓——用递推把复杂计算变成简单操作。
3.4 圆弧插补的实现
void arc_interp_ccw(int Xs, int Ys, int Xe, int Ye) {
int R2 = Xs*Xs + Ys*Ys; // 半径平方
int F = 0;
int x = Xs, y = Ys;
int step = abs(Xe - Xs) + abs(Ye - Ys); // 近似总步数
while (step > 0) {
if (F >= 0) {
// 走 -X
x--;
F -= 2*x + 1; // 注意:x 已经减了,所以是 2*x+1
printf("Step -X, pos(%d,%d)\n", x, y);
} else {
// 走 +Y
y++;
F += 2*y + 1; // y 已经加了,所以是 2*y+1
printf("Step +Y, pos(%d,%d)\n", x, y);
}
step--;
}
}
个人经验:圆弧插补的终点判断比直线麻烦。我习惯用“当前点与终点的坐标差”来判断是否到达,而不是单纯依赖步数计数器。因为圆弧路径可能不是单调的,步数估算容易出错。
3.5 不同象限的处理
不同象限的处理,说白了就是“符号变换”。直线和圆弧在不同象限的进给方向、偏差更新符号都会变化。
先看直线插补的象限处理。下表总结了四个象限的进给方向:
| 象限 | F >= 0 进给方向 | F < 0 进给方向 | F >= 0 偏差更新 | F < 0 偏差更新 |
|---|---|---|---|---|
| 第一象限 | +X | +Y | F - Ye | F + Xe |
| 第二象限 | -X | +Y | F + Ye | F - Xe |
| 第三象限 | -X | -Y | F + Ye | F - Xe |
| 第四象限 | +X | -Y | F - Ye | F + Xe |
你发现规律了吗?其实只要把终点坐标的绝对值代入第一象限的算法,然后根据象限决定正负方向就行。我习惯的做法是:先把终点坐标取绝对值,算出步数,再根据象限映射到实际方向。
圆弧插补的象限处理更复杂一些,因为圆弧有顺圆和逆圆之分,还有跨象限的情况。下面这张图展示了第一象限逆圆和顺圆的进给方向对比:
跨象限的圆弧处理,我个人的做法是“分段处理”。把圆弧按象限边界(0°、90°、180°、270°)切成若干段,每段单独调用对应象限的插补逻辑。这样代码清晰,不容易出错。
曾经踩过的坑:有一次做跨象限圆弧插补,我偷懒没分段,直接用统一的递推公式。结果在象限边界附近出现了“抖动”——刀具在边界上来回走,就是不往前走。后来老老实实分段处理,问题就解决了。
3.6 本章小结
逐点比较法的直线和圆弧插补,核心就三点:
- 偏差函数:直线用叉积,圆弧用距离平方差
- 递推公式:把乘法和平方变成加减法
- 象限映射:用绝对值统一处理,再映射到实际方向
这套方法虽然看起来“土”,但在资源受限的嵌入式系统里,它比 DDA(数字微分分析器)更稳定,比数据采样法更简单。我到现在做低成本的步进电机控制方案,首选还是逐点比较法。