一、直线插补基础

做运动控制这些年,我接触过不少插补算法。但说来说去,直线插补始终是根基。你想想看,任何复杂的曲线轨迹,最终都要拆解成无数段微小的直线段去逼近。所以,把直线插补吃透了,后面的圆弧、样条插补才能顺手。

1.1 直线插补的数学原理

直线插补的本质是什么?说白了,就是让运动轴协同配合,走出一条直线轨迹。

假设我们要从起点 P0(x0, y0) 走到终点 P1(x1, y1)。在理想情况下,每个时刻的位置应该满足:

(x - x0) / (x1 - x0) = (y - y0) / (y1 - y0) = t

这里的 t 是归一化时间参数,范围从 0 到 1。当 t=0 时在起点,t=1 时到终点。

但实际控制中,我们没法连续输出位置。只能按固定周期(比如 1ms 或 0.5ms)给伺服驱动器发脉冲或位置指令。这就引出了一个问题:如何用离散的指令去逼近连续的直线?

核心矛盾:连续轨迹 vs 离散控制周期。插补算法要解决的就是这个矛盾。

我在早期做激光切割项目时,就遇到过这个问题。当时用的一个开源插补库,走直线时总在终点附近出现微小过冲。查了半天才发现,是插补步长计算时忽略了剩余距离的判断。嗯,这里要注意——插补不是简单的等距分割,必须考虑终点精确到达

1.2 直线插补的两种实现思路

目前主流的直线插补方法,大致分两类:

  • 参考脉冲法:每个周期计算各轴应走的步数,然后发脉冲。DDA 算法就属于这一类。
  • 采样数据法:预先算好整条轨迹的位置序列,运行时直接查表输出。

我个人更偏爱参考脉冲法,尤其是 DDA。为什么?因为它计算量小,适合嵌入式实时系统。你想想看,一个 200MHz 的 MCU,要在 1ms 内完成插补计算、加减速处理、位置输出,留给插补的时间窗口其实很紧张。DDA 只需要几次加法和比较,非常轻量。


二、DDA 直线插补算法详解

DDA,全称 Digital Differential Analyzer,数字微分分析器。这个名字听起来挺唬人,其实原理很简单。

2.1 DDA 的核心思想

DDA 把直线运动看作一个积分过程。每个轴的速度分量是恒定的,位置就是速度对时间的积分。

设直线长度为 L,X 轴增量为 ΔX,Y 轴增量为 ΔY。那么:

  • X 轴速度分量:Vx = ΔX / L
  • Y 轴速度分量:Vy = ΔY / L

每个插补周期,我们累加这些速度分量。当累加值超过 1 时,就输出一个脉冲(或走一步)。

我的经验:DDA 本质上是用累加器模拟了积分过程。你把它想象成一个水桶,每个周期往里倒固定量的水,满了就溢出一杯。这个「溢出」就是脉冲输出。

2.2 DDA 算法的数学推导

设累加器位宽为 N(通常取 8 位或 16 位),最大累加值为 2^N - 1。

每个插补周期 T,各轴的累加值为:

AccX += ΔX
AccY += ΔY

当 AccX ≥ 2^N 时,X 轴输出一个脉冲,AccX -= 2^N。
当 AccY ≥ 2^N 时,Y 轴输出一个脉冲,AccY -= 2^N。

这里有个关键点:ΔX 和 ΔY 必须归一化到累加器位宽范围内。通常的做法是:

ΔX = (x1 - x0) * (2^N) / max(|ΔX|, |ΔY|)
ΔY = (y1 - y0) * (2^N) / max(|ΔX|, |ΔY|)

为什么要除以最大值?为了保证速度最快的轴在每个周期都能输出脉冲,避免丢步。

我曾经踩过的坑:有一次做三轴联动,Z 轴增量很小,ΔZ 算出来只有 3。结果累加器跑了 100 多个周期才溢出一次,导致 Z 轴运动严重滞后。后来我改用 32 位累加器,并适当放大 ΔZ 的量化值,问题才解决。

2.3 DDA 算法的代码实现

下面是一个标准的 DDA 直线插补实现,我用 C 语言风格写:

// DDA 直线插补,单步执行
int dda_line_step(DDA_State *state) {
    int pulse_x = 0, pulse_y = 0;

    // 累加各轴增量
    state->acc_x += state->delta_x;
    state->acc_y += state->delta_y;

    // 判断是否溢出
    if (state->acc_x >= state->max_val) {
        state->acc_x -= state->max_val;
        pulse_x = 1;
    }
    if (state->acc_y >= state->max_val) {
        state->acc_y -= state->max_val;
        pulse_y = 1;
    }

    // 更新已走步数
    state->steps_done++;
    if (state->steps_done >= state->total_steps) {
        return 1;  // 插补完成
    }
    return 0;
}

这段代码看起来简单,但实际工程中要考虑几个细节:

  • 累加器初始值可以设为 max_val/2,这样第一个脉冲不会延迟
  • 终点判断要用总步数,而不是累加器值
  • 多轴联动时,每个轴独立累加,互不干扰

三、DDA 算法的优缺点分析

任何算法都有两面性。DDA 用了这么多年,自然有它的道理,但也有明显的短板。

3.1 DDA 的优点

优点 说明
计算量极小 每个周期只需几次加法和比较,适合低端 MCU
实现简单 核心代码不到 20 行,调试方便
易于扩展 从两轴扩展到六轴,只需增加累加器
实时性好 计算时间固定,不会出现抖动

我记得有一次给一个老式雕刻机做改造,主控芯片是 STM32F103,72MHz 主频。用 DDA 算法,1ms 插补周期内还能腾出 60% 的 CPU 时间做 IO 扫描和通信。换成其他算法,估计就扛不住了。

3.2 DDA 的缺点

缺点 说明
精度受位宽限制 8 位累加器误差约 0.4%,16 位约 0.0015%
速度波动 脉冲输出不均匀,可能导致低速抖动
加减速困难 原生 DDA 不支持速度规划,需额外处理
终点误差 累加器余数可能导致终点位置偏差

避坑指南:我曾经在一个精密点胶项目中用了 8 位 DDA,结果发现点胶轨迹末端总是偏 0.1mm。后来换成 16 位累加器,并在终点做了一次余数补偿,精度才达标。所以,位宽选择要结合机械精度要求,别一味追求省资源。

3.3 DDA 的改进方向

针对 DDA 的缺点,工业界有一些成熟的改进方案:

  • 高精度 DDA:用 32 位甚至 64 位累加器,配合浮点运算
  • 带速度规划的 DDA:在插补前先算好速度曲线,动态调整 ΔX、ΔY
  • 余数补偿:插补结束时,把累加器余数按比例分配到各轴
  • 双缓冲 DDA:一个缓冲区做插补计算,另一个做脉冲输出,消除抖动

我个人在实际项目中,通常采用「16 位 DDA + 余数补偿 + 梯形加减速」的组合。这个方案在大多数通用运动控制场景下,都能达到 0.01mm 以内的精度,而且代码量控制在 200 行以内。


四、本章知识体系

下面这张图,是我梳理的直线插补与 DDA 算法的知识结构。你可以把它当作一个快速索引:

直线插补与DDA算法知识体系 直线插补基础 DDA算法详解 优缺点分析 数学原理 离散化方法 核心思想 数学推导 优点列表 缺点列表 代码实现(累加器、溢出判断、终点检测) 改进方向:高精度、速度规划、余数补偿 工程实践:16位DDA + 余数补偿 + 梯形加减速 图1:直线插补与DDA算法知识体系结构图

这张图把本章的核心内容串了起来。你可以看到,从直线插补基础出发,到 DDA 算法的原理和实现,再到优缺点分析和改进方向,最后落地到工程实践。每个环节都有对应的知识点。


好了,直线插补和 DDA 算法就聊到这里。下一章我们会深入圆弧插补,看看 DDA 如何扩展到圆弧轨迹。到时候你会发现,核心思想其实是一样的——积分累加,溢出输出。掌握了这个本质,其他插补算法学起来就快了。

一个小建议:如果你手头有开发板,不妨把上面的 DDA 代码跑一遍。用示波器看 X 轴和 Y 轴的脉冲输出,观察它们的时间关系。亲眼看到脉冲序列走出 45 度斜线,比看一百遍公式都管用。


公众号:蓝海资料掘金营,微信deep3321