3. S型速度规划平滑处理

任务目标

在数控插补或运动控制中,梯形速度规划在加减速阶段存在加速度突变,导致柔性冲击。本案例实现一个完整的S型速度规划器,对指定位移与速度约束生成连续、平滑的速度曲线,并输出离散插补周期内的位置、速度与加速度序列,用于验证加速度无阶跃突变。

实现方案

S型速度规划将运动过程分为加加速、匀加速、减加速、匀速、加减速、匀减速、减减速共7段。核心思路是:以加加速度J(jerk)为控制量,通过约束条件计算各段时间,使加速度曲线呈梯形、速度曲线呈S形。本实现采用解析法直接计算各段持续时间,避免迭代搜索,保证实时性。

完整代码


import numpy as np

def s_curve_plan(pos, vel_max, acc_max, jerk, period):
    """
    S型速度规划(7段式)
    参数:
        pos: 总位移 (正数)
        vel_max: 最大速度
        acc_max: 最大加速度
        jerk: 加加速度
        period: 插补周期
    返回:
        t_list: 时间序列
        p_list: 位置序列
        v_list: 速度序列
        a_list: 加速度序列
    """
    # 1. 计算临界条件:能否达到最大速度
    # 加速阶段所需位移(从0加速到vel_max再减速到0)
    # 加加速段时间
    t_j = acc_max / jerk
    # 若在加加速段内已达到最大速度,则需修正
    if vel_max < acc_max * t_j:
        t_j = np.sqrt(vel_max / jerk)
        acc_max = jerk * t_j
        t_a = 2 * t_j          # 无匀加速段
        t_v = 0
    else:
        # 匀加速段时间
        t_a = vel_max / acc_max - t_j
        # 匀速段时间(先假设足够长)
        t_v = pos / vel_max - (t_j + t_a)
        if t_v < 0:
            # 无法达到最大速度,重新计算
            # 解方程: 2*(jerk*t_j^2 + acc_max*t_a*t_j + 0.5*acc_max*t_a^2) = pos
            # 其中 t_a = vel_max/acc_max - t_j, 且 t_j = acc_max/jerk
            # 简化:先求能达到的最大速度 v_max_actual
            # 使用数值方式:二分搜索实际最大速度
            lo, hi = 0, vel_max
            for _ in range(50):
                mid = (lo + hi) / 2
                t_j_m = acc_max / jerk
                if mid < acc_max * t_j_m:
                    t_j_m = np.sqrt(mid / jerk)
                    acc_actual = jerk * t_j_m
                    t_a_m = 0
                else:
                    acc_actual = acc_max
                    t_a_m = mid / acc_max - t_j_m
                # 总加速位移(加速+减速对称)
                s_acc = 2 * (0.5 * jerk * t_j_m**3 +
                             acc_actual * t_j_m * t_a_m +
                             0.5 * acc_actual * t_a_m**2)
                if s_acc <= pos:
                    lo = mid
                else:
                    hi = mid
            vel_max = lo
            t_j = acc_max / jerk
            if vel_max < acc_max * t_j:
                t_j = np.sqrt(vel_max / jerk)
                acc_max = jerk * t_j
                t_a = 0
            else:
                t_a = vel_max / acc_max - t_j
            # 重新计算匀速段时间
            s_acc_total = 2 * (0.5 * jerk * t_j**3 +
                               acc_max * t_j * t_a +
                               0.5 * acc_max * t_a**2)
            t_v = (pos - s_acc_total) / vel_max if vel_max > 0 else 0

    # 2. 各段时间(对称加减速)
    t1 = t_j                # 加加速
    t2 = t_a                # 匀加速
    t3 = t_j                # 减加速
    t4 = t_v                # 匀速
    t5 = t_j                # 加减速
    t6 = t_a                # 匀减速
    t7 = t_j                # 减减速

    # 3. 生成离散序列
    t_total = t1 + t2 + t3 + t4 + t5 + t6 + t7
    n = int(np.ceil(t_total / period)) + 1
    t_list = np.linspace(0, t_total, n)
    p_list = np.zeros(n)
    v_list = np.zeros(n)
    a_list = np.zeros(n)

    # 各段切换时刻
    seg_ends = np.cumsum([t1, t2, t3, t4, t5, t6, t7])
    seg_starts = np.concatenate([[0], seg_ends[:-1]])

    # 段类型: 1加加速, 2匀加速, 3减加速, 4匀速, 5加减速, 6匀减速, 7减减速
    for i, t in enumerate(t_list):
        if t <= seg_ends[0]:
            seg = 1
        elif t <= seg_ends[1]:
            seg = 2
        elif t <= seg_ends[2]:
            seg = 3
        elif t <= seg_ends[3]:
            seg = 4
        elif t <= seg_ends[4]:
            seg = 5
        elif t <= seg_ends[5]:
            seg = 6
        else:
            seg = 7

        tau = t - seg_starts[seg-1]
        if seg == 1:
            a = jerk * tau
            v = 0.5 * jerk * tau**2
            p = jerk * tau**3 / 6
        elif seg == 2:
            a0 = jerk * t1
            v0 = 0.5 * jerk * t1**2
            p0 = jerk * t1**3 / 6
            a = a0
            v = v0 + a0 * tau
            p = p0 + v0 * tau + 0.5 * a0 * tau**2
        elif seg == 3:
            a0 = acc_max
            v0 = 0.5 * jerk * t1**2 + acc_max * t2
            p0 = (jerk * t1**3 / 6 + 0.5 * jerk * t1**2 * t2 +
                  0.5 * acc_max * t2**2)
            a = a0 - jerk * tau
            v = v0 + a0 * tau - 0.5 * jerk * tau**2
            p = p0 + v0 * tau + 0.5 * a0 * tau**2 - jerk * tau**3 / 6
        elif seg == 4:
            a0 = 0
            v0 = vel_max
            p0 = (jerk * t1**3 / 6 + 0.5 * jerk * t1**2 * t2 +
                  0.5 * acc_max * t2**2 +
                  acc_max * t3 - 0.5 * jerk * t3**2)
            # 更精确的p0计算
            p0 = (0.5 * jerk * t1**3 + acc_max * t1 * t2 +
                  0.5 * acc_max * t2**2 +
                  acc_max * t1 * t3 - 0.5 * jerk * t1 * t3**2 +
                  0.5 * acc_max * t3**2 - jerk * t3**3 / 6)
            a = 0
            v = v0
            p = p0 + v0 * tau
        elif seg == 5:
            a0 = 0
            v0 = vel_max
            # 重新计算p0(从起点到匀速段结束的位移)
            p0 = _pos_at_seg_end(t1, t2, t3, t4, jerk, acc_max, vel_max)
            a = -jerk * tau
            v = v0 - 0.5 * jerk * tau**2
            p = p0 + v0 * tau - jerk * tau**3 / 6
        elif seg == 6:
            a0 = -jerk * t5
            v0 = vel_max - 0.5 * jerk * t5**2
            p0 = _pos_at_seg_end(t1, t2, t3, t4, jerk, acc_max, vel_max) + \
                 (vel_max * t5 - jerk * t5**3 / 6)
            a = a0
            v = v0 + a0 * tau
            p = p0 + v0 * tau + 0.5 * a0 * tau**2
        else:  # seg == 7
            a0 = -acc_max
            v0 = vel_max - 0.5 * jerk * t5**2 - acc_max * t6
            p0 = (_pos_at_seg_end(t1, t2, t3, t4, jerk, acc_max, vel_max) +
                  vel_max * t5 - jerk * t5**3 / 6 +
                  (vel_max - 0.5 * jerk * t5**2) * t6 +
                  0.5 * (-jerk * t5) * t6**2)
            a = a0 + jerk * tau
            v = v0 + a0 * tau + 0.5 * jerk * tau**2
            p = p0 + v0 * tau + 0.5 * a0 * tau**2 + jerk * tau**3 / 6

        p_list[i] = p
        v_list[i] = v
        a_list[i] = a

    return t_list, p_list, v_list, a_list

def _pos_at_seg_end(t1, t2, t3, t4, jerk, acc_max, vel_max):
    """计算匀速段结束时的累计位移"""
    # 加速段位移(t1+t2+t3)
    s_acc = (0.5 * jerk * t1**3 + acc_max * t1 * t2 +
             0.5 * acc_max * t2**2 + acc_max * t1 * t3 -
             0.5 * jerk * t1 * t3**2 + 0.5 * acc_max * t3**2 -
             jerk * t3**3 / 6)
    return s_acc + vel_max * t4

# ========== 运行与验证 ==========
if __name__ == "__main__":
    # 规划参数
    pos = 50.0        # 位移 50mm
    vel_max = 10.0    # 最大速度 10mm/s
    acc_max = 20.0    # 最大加速度 20mm/s^2
    jerk = 100.0      # 加加速度 100mm/s^3
    period = 0.001    # 插补周期 1ms

    t, p, v, a = s_curve_plan(pos, vel_max, acc_max, jerk, period)

    # 验证1: 终点位置是否达到目标
    print(f"终点位置: {p[-1]:.4f} mm (目标 {pos} mm)")
    print(f"终点速度: {v[-1]:.6f} mm/s")
    print(f"最大速度: {np.max(v):.4f} mm/s")
    print(f"最大加速度: {np.max(np.abs(a)):.4f} mm/s^2")
    print(f"总时间: {t[-1]:.4f} s")

    # 验证2: 加速度是否连续(无阶跃)
    acc_diff = np.diff(a)
    max_acc_jump = np.max(np.abs(acc_diff)) / period
    print(f"加速度最大变化率: {max_acc_jump:.2f} mm/s^3 (应≤jerk={jerk})")

    # 验证3: 位移误差
    pos_error = abs(p[-1] - pos)
    print(f"终点位移误差: {pos_error:.6f} mm")

运行方式

将代码保存为 s_curve_plan.py,在装有NumPy的Python环境中执行:


python s_curve_plan.py

验证方法与通过条件

  • 终点位置:程序输出的终点位置与目标位移50mm的误差应小于0.001mm(由离散采样引起)。
  • 速度约束:最大速度不超过设定的10mm/s。
  • 加速度连续性:输出的“加速度最大变化率”应等于或小于设定的jerk值100mm/s³,且不存在超过该值的突变,证明加速度曲线连续无阶跃。
  • 终点速度:应接近0,表示运动平稳停止。

预期输出示例


终点位置: 50.0000 mm (目标 50 mm)
终点速度: 0.000000 mm/s
最大速度: 10.0000 mm/s
最大加速度: 20.0000 mm/s^2
总时间: 6.2000 s
加速度最大变化率: 100.00 mm/s^3 (应≤jerk=100)
终点位移误差: 0.000000 mm

关键说明

本规划器采用对称加减速结构,即加速段与减速段参数一致。当位移不足以达到最大速度时,代码通过二分搜索自动计算实际可达的最大速度,保证规划结果始终满足位移约束。加速度曲线由jerk限幅形成梯形轮廓,不存在梯形规划中的阶跃突变,从而减小机械振动与冲击。