第十三章 多段连续插补:路径预读、拐角处理、速度衔接

各位同学,今天我们来聊聊运动控制里一个非常核心的话题——多段连续插补。

说实话,单段插补其实没什么难度。给一段直线,算好起点终点,插补过去就完事了。但实际工程中,谁会让机床走一段就停一下?那效率太低了。真正的工业场景,是成百上千段小线段连续执行,中间不能有明显的停顿感。

我当年刚入行时,就踩过这个坑。客户反馈说我们的机床走复杂轮廓时,每到一个拐角就“抖”一下,加工出来的表面全是振纹。后来一查,就是速度衔接没处理好。从那以后,我对多段连续插补的敬畏心就上来了。

13.1 路径预读:提前看清前方的路

什么叫路径预读?说白了,就是让控制器提前“看”到后面几段甚至几十段的路径信息。

为什么要预读?因为单段决策是短视的。你只看到眼前这一段,就不知道前面有个急转弯,结果速度冲上去了,到拐角才发现刹不住车。

我个人习惯的做法是:在RTOS中维护一个环形缓冲区,专门存放预读的路径段。这个缓冲区的大小,直接决定了预读的深度。

预读深度的选择原则:
  • 预读太少(比如2-3段):拐角处理效果差,速度波动大
  • 预读太多(比如50-100段):内存占用大,实时性下降
  • 经验值:一般取8-16段,兼顾效果和性能

预读的任务在RTOS中通常是一个中等优先级的任务。为什么不是最高优先级?因为插补输出才是硬实时,预读可以稍微慢一点,但不能阻塞插补。

// 预读任务伪代码
void LookAheadTask(void *param) {
    while(1) {
        // 检查缓冲区是否还有空位
        if(ring_buffer_has_space()) {
            // 从G代码解析器取下一段
            PathSegment seg = GetNextSegment();
            // 计算该段的几何信息
            CalculateSegmentInfo(&seg);
            // 放入预读缓冲区
            ring_buffer_push(&seg);
        }
        // 让出CPU,别占着不放
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

这里有个细节:预读任务不能和插补任务共用同一个缓冲区,否则会出现数据竞争。我一般用双缓冲或者无锁队列来解决。

13.2 拐角处理:不是所有角都要减速

拐角处理是多段连续插补的难点。你想想看,两段直线相交,如果直接按路径走,速度方向突变,必然产生冲击。

但问题是,是不是所有拐角都要减速?当然不是。

我把它分为三类:

拐角类型 特征 处理策略
小角度拐角(< 30°) 方向变化不大 几乎不减速,直接平滑过渡
中等角度拐角(30°-120°) 有明显方向变化 适当减速,插入圆弧过渡
大角度拐角(> 120°) 近乎折返 必须大幅减速,甚至停止

嗯,这里要注意:拐角角度的计算,不是简单的向量点积。我遇到过一个问题——当两段线段长度差异很大时,角度计算会失真。比如一段很长,一段很短,短的那段几乎可以忽略,但角度算出来却很大。

我的解决方案是:在计算拐角时,引入一个“有效长度”的概念。如果某段长度小于阈值,就把它合并到前一段中再计算角度。

避坑指南: 我曾经因为没处理好短线段合并,导致一个五轴机床在加工叶轮时频繁抖动。后来花了三天时间排查,才发现是预读时把一段0.1mm的短线段当成了独立段处理,拐角减速逻辑被错误触发。

13.3 速度衔接:让速度曲线平滑如丝

速度衔接,说白了就是让速度在段与段之间平滑变化,不要有突变。

常见的速度衔接策略有三种:

  1. 直接停止法:每段结束都减速到0,再加速。简单但效率低。
  2. 拐角减速法:只在拐角处减速,直线段保持高速。效率高,但拐角处仍有冲击。
  3. S形速度规划法:用S形曲线连接前后段的速度。效果最好,但计算量大。

我个人最推荐的是第三种。虽然计算量大,但在现代MCU上完全扛得住。关键是,S形曲线能保证加速度连续,没有冲击。

来看一个实际的速度衔接计算:

// 速度衔接计算
typedef struct {
    float v_start;    // 起始速度
    float v_end;      // 结束速度
    float v_max;      // 最大允许速度
    float acc_max;    // 最大加速度
    float jerk_max;   // 最大加加速度
} VelocityProfile;

// 计算S形速度衔接
void CalculateSProfile(VelocityProfile *profile) {
    // 先判断能否直接衔接
    float dv = profile->v_end - profile->v_start;
    float t_acc = fabs(dv) / profile->acc_max;
    
    // 如果时间太短,说明速度变化不大,直接线性过渡
    if(t_acc < 0.001f) {
        profile->v_end = profile->v_start;
        return;
    }
    
    // 否则,生成S形曲线参数
    // 这里省略了具体的S形曲线计算细节
    // 实际项目中需要查表或迭代计算
}

为什么S形曲线好?因为它考虑了加加速度(Jerk)。加速度的变化率如果太大,机械结构会感受到冲击。S形曲线让加速度也平滑变化,机床跑起来就像在丝绸上滑行。

注意: 速度衔接不是越平滑越好。如果路径很短,强行做S形曲线反而会导致速度还没提上去就要减速,效率更低。我的经验是:当路径长度小于某个阈值时,直接使用梯形速度规划,不做S形。

13.4 整体架构:预读+拐角+衔接的协同

这三个模块不是孤立的。预读为拐角处理提供数据,拐角处理为速度衔接提供约束,速度衔接最终决定插补输出的速度。

我画了一张图,展示它们之间的关系:

多段连续插补核心模块协同图 路径预读模块 环形缓冲区管理 拐角处理模块 角度计算与过渡策略 速度衔接模块 S形曲线/梯形规划 提供路径数据 约束速度 插补输出模块 实时生成位置指令 RTOS调度层:任务优先级管理、同步与通信

从图中可以看到,预读模块是数据源头,它把解析好的路径段喂给拐角处理模块。拐角处理模块根据角度信息,计算出每个拐角处的允许速度。这个速度约束再传给速度衔接模块,最终生成平滑的速度曲线。

在RTOS中,这三个模块可以设计成三个独立的任务:

  • 预读任务:优先级中等,负责从G代码缓冲区取数据
  • 拐角处理任务:优先级较高,因为它的输出直接影响插补
  • 速度衔接任务:优先级最高,因为它要实时计算插补点的速度

当然,实际项目中也可以把拐角处理和速度衔接合并成一个任务,减少任务切换开销。这取决于你的MCU性能和实时性要求。

我的经验: 在Cortex-M4上,我通常把预读任务放在200Hz,拐角处理放在500Hz,速度衔接放在1kHz。这样既能保证实时性,又不会让CPU满载。如果MCU性能更强,可以适当提高频率。

好了,关于多段连续插补的核心内容就讲到这里。路径预读让你看得远,拐角处理让你走得稳,速度衔接让你跑得顺。三者缺一不可。

记住,运动控制不是理论游戏,是实打实的工程实践。每一个参数的选择,背后都有血的教训。希望你们在实际项目中,能少走一些我当年走过的弯路。