5. 圆弧插补实现:逐点比较法圆弧插补、象限处理、终点判断

各位同学,今天我们来聊聊圆弧插补。说实话,直线插补大家可能都熟悉,但圆弧插补才是真正考验功力的地方。我当年刚入行时,第一次在数控系统上跑圆弧,结果画出来的不是圆,是个歪歪扭扭的椭圆——那场面,至今难忘。

圆弧插补的核心思想,说白了就是「用折线逼近圆弧」。你想想看,我们的电机只能走直线段,但目标轨迹是圆弧,怎么办?那就用无数小直线段去逼近它。逐点比较法就是其中一种经典算法,它通过判断当前点与理想圆弧的位置关系,决定下一步往哪个方向走。

5.1 逐点比较法的基本原理

逐点比较法这个名字很直白——每走一步,就比较一下当前位置和理想圆弧的偏差。根据偏差的正负,决定下一步的进给方向。

我们先定义偏差函数。对于圆心在原点的圆弧,假设当前点为 (xi, yi),理想圆弧半径为 R,那么偏差为:

F = x_i² + y_i² - R²

这个 F 值就是我们的判断依据:

  • F = 0:点在圆弧上,完美
  • F > 0:点在圆弧外侧,需要向圆内方向走
  • F < 0:点在圆弧内侧,需要向圆外方向走

嗯,这里要注意:不同象限的进给方向是不一样的。我们待会儿详细说。

5.2 第一象限圆弧插补

我们先从最简单的第一象限逆圆开始讲。假设圆弧起点在 (x₀, y₀),终点在 (xₑ, yₑ),圆心在原点。

对于第一象限逆圆,进给规则是这样的:

  • F ≥ 0:向 -X 方向走一步(因为点在圆外或圆上,需要向圆内走)
  • F < 0:向 +Y 方向走一步(点在圆内,需要向圆外走)

每次走完一步,都要更新偏差值。我习惯用递推公式来计算,这样效率高:

// 向 -X 方向走一步
x_{i+1} = x_i - 1
F_{i+1} = F_i - 2*x_i + 1

// 向 +Y 方向走一步
y_{i+1} = y_i + 1
F_{i+1} = F_i + 2*y_i + 1

为什么用递推?因为直接计算 x² + y² - R² 涉及乘法,在嵌入式平台上比较耗时。递推公式只用加减法,快得多。我在一个 STM32F4 的项目上做过测试,递推方式比直接计算快了将近 3 倍。

5.3 象限处理——这才是真正的坑

好了,第一象限搞定了。但圆弧可能跨越多个象限,对吧?比如从第二象限走到第四象限。这时候,每个象限的进给方向都不一样。

我给大家总结一下四个象限的进给规则:

象限 圆弧方向 F ≥ 0 进给方向 F < 0 进给方向
第一象限 逆圆 -X +Y
第一象限 顺圆 -Y +X
第二象限 逆圆 +Y +X
第二象限 顺圆 -X +Y
第三象限 逆圆 +X -Y
第三象限 顺圆 +Y -X
第四象限 逆圆 -Y -X
第四象限 顺圆 +X -Y

看着有点晕是吧?我有个记忆技巧:逆圆时,F≥0 总是往圆心方向走。你想想看,第一象限圆心在原点,所以往 -X 走;第二象限圆心也在原点,但此时点在左边,往圆心走就是 +Y... 嗯,顺着这个思路去推,就不容易记混。

核心要点:象限切换时,偏差递推公式不变,但进给方向要根据当前象限重新判断。我曾经见过有人把递推公式也改了,结果画出来的圆弧在象限边界处出现跳变。

5.4 终点判断——别多走一步

圆弧插补的终点判断比直线要复杂一些。直线可以用总步数来判断,但圆弧的步数不是固定的——它取决于你走的路径。

我常用的方法有两种:

  1. 坐标比较法:每走一步,判断当前坐标是否到达终点。但要注意,由于是逐点逼近,可能不会精确落在终点上。所以一般用「当前坐标与终点坐标的差值小于一个步长」作为判断条件。
  2. 总步数法:预先计算圆弧需要走的总步数。对于 1/4 圆弧,步数就是 |xₑ - x₀| + |yₑ - y₀|。每走一步计数减一,减到零就结束。

我个人更推荐总步数法。为什么?因为坐标比较法在跨象限时容易出问题——你想想看,如果圆弧从第一象限走到第二象限,x 坐标先减小后增大,用坐标比较就麻烦了。

小技巧:总步数 = |xₑ - x₀| + |yₑ - y₀| 这个公式只适用于 90° 圆弧。对于任意角度,需要根据圆弧的弧长和步长来计算。我在项目中一般用弧长除以步长,再向上取整。

5.5 代码实现框架

下面我给出一个 RTOS 任务实现的框架。这个任务在 FreeRTOS 上跑过,效果还不错。

void ArcInterpolationTask(void *pvParameters)
{
    ArcParams_t *arc = (ArcParams_t *)pvParameters;
    int32_t x = arc->startX;
    int32_t y = arc->startY;
    int32_t F = x*x + y*y - arc->R*arc->R;
    uint32_t totalSteps = arc->totalSteps;
    
    while(totalSteps > 0)
    {
        // 判断当前象限
        uint8_t quad = GetQuadrant(x, y);
        
        // 根据象限和偏差决定进给方向
        if(F >= 0)
        {
            // 向圆心方向走
            switch(quad)
            {
                case 1: x--; break;  // -X
                case 2: y++; break;  // +Y
                case 3: x++; break;  // +X
                case 4: y--; break;  // -Y
            }
            F = F - 2*x + 1;  // 注意:这里假设向X方向走
        }
        else
        {
            // 向圆外方向走
            // ... 类似处理
        }
        
        // 输出脉冲
        OutputStepPulse(x, y);
        
        totalSteps--;
        
        // 让出CPU,给其他任务运行机会
        vTaskDelay(1);
    }
    
    // 插补完成
    vTaskDelete(NULL);
}

注意:上面的代码为了简洁,省略了象限判断和进给方向的完整逻辑。实际项目中,你需要把 8 种情况(4 象限 × 2 方向)都处理到。另外,递推公式中的 2*x 和 2*y 可以用左移操作优化,即 2*x = x << 1。

5.6 知识体系总览

下面这张图展示了圆弧插补的核心逻辑,我建议你把它记在脑子里:

圆弧插补逐点比较法知识体系 圆弧插补 偏差计算 F = x² + y² - R² 象限处理 4象限 × 2方向 = 8种情况 终点判断 总步数法 / 坐标比较法 递推公式优化 F ≥ 0 / F < 0 判断 进给方向表 象限切换处理 步数预计算 跨象限终点判断 RTOS任务实现 FreeRTOS / RT-Thread 任务调度

这张图把圆弧插补的三大核心模块串起来了。你从「偏差计算」出发,得到 F 值;然后根据「象限处理」决定进给方向;每走一步都要做「终点判断」。这三个模块在 RTOS 任务中循环执行,直到插补完成。

最后说一句,圆弧插补的调试比直线麻烦很多。我曾经在一个项目上,因为象限边界处理少了一个条件,导致圆弧在 90° 位置出现了一个小台阶。排查了整整两天才找到问题。所以,象限边界处的逻辑一定要仔细验证,最好用仿真工具先跑一遍。