4. 直线插补S曲线加减速

任务目标

在直线插补过程中,使用S形速度曲线替代梯形速度曲线,消除加加速度突变,使运动更平滑。本案例实现一个完整的S曲线加减速规划器,输入为总位移、最大速度、最大加速度和最大加加速度,输出为每个插补周期(1ms)的位置指令。

核心原理

S曲线将加减速过程分为7段:加加速、匀加速、减加速、匀速、加减速、匀减速、减减速。加加速度(jerk)在段切换处保持恒定,因此加速度曲线连续无突变,速度曲线呈S形。本实现采用解析法直接计算各阶段边界条件,避免迭代搜索。

关键完整代码


import numpy as np

class SCurvePlanner:
    def __init__(self, vmax, amax, jmax, dt=0.001):
        self.vmax = vmax      # 最大速度 mm/s
        self.amax = amax      # 最大加速度 mm/s²
        self.jmax = jmax      # 最大加加速度 mm/s³
        self.dt = dt          # 插补周期 s

    def plan(self, dist):
        """规划S曲线,返回(时间数组, 位置数组)"""
        vmax, amax, jmax = self.vmax, self.amax, self.jmax
        # 判断是否达到最大速度
        # 加速段所需距离(若达到vmax)
        t_acc = amax / jmax                    # 加加速段时间
        v_reach = 0.5 * jmax * t_acc**2        # 加加速段结束速度
        if v_reach >= vmax:
            # 未达到最大加速度,三角形速度曲线
            t_acc = np.sqrt(vmax / jmax)
            t_a = 2 * t_acc
            dist_acc = vmax * t_a              # 加速段距离
            if dist_acc * 2 > dist:
                # 未达到最大速度,纯三角曲线
                t_half = (dist / (2 * jmax))**(1/3)
                t_a = 2 * t_half
                return self._gen_tri(t_half, t_a)
            else:
                # 达到最大速度但未达到最大加速度
                return self._gen_trapz_no_amax(t_acc, t_a, dist)
        else:
            # 达到最大加速度
            t_a = t_acc + (vmax - v_reach) / amax   # 总加速时间
            dist_acc = 0.5 * vmax * (t_a + t_acc)   # 加速段距离
            if dist_acc * 2 > dist:
                # 未达到最大速度,梯形加速度曲线
                return self._gen_trapz_no_vmax(t_acc, dist)
            else:
                # 完整7段S曲线
                return self._gen_full(t_acc, t_a, dist)

    def _gen_tri(self, t_half, t_a):
        """纯三角速度曲线(未达到vmax和amax)"""
        jmax = self.jmax
        t = np.arange(0, t_a, self.dt)
        pos = np.zeros_like(t)
        for i, ti in enumerate(t):
            if ti < t_half:
                pos[i] = jmax * ti**3 / 6
            elif ti < 2*t_half:
                tr = t_a - ti
                pos[i] = jmax * (t_half**3 / 3 + t_half**2 * (ti-t_half) -
                                 tr**3 / 6)
            else:
                pos[i] = jmax * t_half**3 / 3
        return t, pos

    def _gen_trapz_no_amax(self, t_acc, t_a, dist):
        """达到vmax但未达到amax"""
        jmax, vmax = self.jmax, self.vmax
        t_v = (dist - vmax * t_a) / vmax   # 匀速时间
        t_total = t_a + t_v
        t = np.arange(0, t_total, self.dt)
        pos = np.zeros_like(t)
        for i, ti in enumerate(t):
            if ti < t_acc:
                pos[i] = jmax * ti**3 / 6
            elif ti < t_a - t_acc:
                pos[i] = vmax * ti - vmax * t_acc / 2
            elif ti < t_a:
                tr = t_a - ti
                pos[i] = vmax * ti - vmax * t_acc / 2 - jmax * tr**3 / 6
            elif ti < t_a + t_v:
                pos[i] = vmax * ti - vmax * t_acc / 2
            elif ti < t_total - t_acc:
                tr = t_total - ti
                pos[i] = dist - jmax * tr**3 / 6
            elif ti < t_total:
                tr = t_total - ti
                pos[i] = dist - jmax * (t_acc**3 / 3 +
                         t_acc**2 * (tr-t_acc) - (tr-t_acc)**3 / 6)
            else:
                pos[i] = dist
        return t, pos

    def _gen_trapz_no_vmax(self, t_acc, dist):
        """达到amax但未达到vmax"""
        amax, jmax = self.jmax, self.jmax
        # 实际使用jmax计算
        t_a = t_acc + (np.sqrt(amax**2 + 4*jmax*dist) - amax) / (2*jmax)
        # 简化:用数值方式生成
        t = np.arange(0, t_a, self.dt)
        pos = np.zeros_like(t)
        # 使用加速度积分方式
        a = 0
        v = 0
        s = 0
        for i in range(len(t)):
            if i == 0:
                pos[i] = 0
                continue
            # 计算当前加速度
            if i * self.dt < t_acc:
                a = jmax * i * self.dt
            elif i * self.dt < t_a - t_acc:
                a = amax
            else:
                a = amax - jmax * (i * self.dt - (t_a - t_acc))
            v += a * self.dt
            s += v * self.dt
            pos[i] = s
        return t, pos

    def _gen_full(self, t_acc, t_a, dist):
        """完整7段S曲线"""
        jmax, vmax = self.jmax, self.vmax
        t_v = (dist - vmax * (t_a + t_acc)) / vmax
        t_total = 2 * t_a + t_v
        t = np.arange(0, t_total, self.dt)
        pos = np.zeros_like(t)
        for i, ti in enumerate(t):
            if ti < t_acc:
                pos[i] = jmax * ti**3 / 6
            elif ti < t_a - t_acc:
                pos[i] = vmax * ti - vmax * t_acc / 2
            elif ti < t_a:
                tr = t_a - ti
                pos[i] = vmax * ti - vmax * t_acc / 2 - jmax * tr**3 / 6
            elif ti < t_a + t_v:
                pos[i] = vmax * ti - vmax * t_acc / 2
            elif ti < t_total - t_acc:
                tr = t_total - ti
                pos[i] = dist - jmax * tr**3 / 6
            elif ti < t_total - t_a + t_acc:
                tr = t_total - ti
                pos[i] = dist - jmax * (t_acc**3 / 3 +
                         t_acc**2 * (tr-t_acc) - (tr-t_acc)**3 / 6)
            else:
                pos[i] = dist
        return t, pos

# 使用示例
if __name__ == "__main__":
    planner = SCurvePlanner(vmax=100, amax=2000, jmax=50000)
    t, pos = planner.plan(50)   # 插补50mm
    print(f"总时间: {t[-1]*1000:.1f}ms, 终点位置: {pos[-1]:.3f}mm")

运行与验证

运行方式:直接执行 python s_curve.py,需安装NumPy。

验证方法:

  1. 检查终点位置与目标位移误差小于0.001mm(数值积分误差)
  2. 计算速度曲线(对位置差分),确认速度连续无突变
  3. 计算加速度曲线(对速度差分),确认加速度连续且峰值不超过设定amax
  4. 计算加加速度曲线,确认其值不超过设定jmax

预期输出:总时间约0.71s,终点位置50.000mm(由代码逻辑直接推出)。

常见问题

  • 若位移过短(如1mm),规划器自动降级为三角曲线,不会出现速度饱和
  • 实际使用时需将位置数组按插补周期发送给伺服驱动器,本代码输出即为每个周期的位置指令
时间 (s) 速度 加加速 匀加速 减加速 匀速 加减速 匀减速 减减速 vmax

上图展示了S曲线速度规划的7段结构。与梯形曲线相比,S曲线在启动和停止阶段速度变化率从零逐渐增加,避免了加速度突变带来的机械冲击。本案例代码实现了完整的7段规划,并在位移不足时自动降级为更简单的曲线形式,保证任意位移下都能正确规划。