实现PVT插补的三次多项式(Hermite cubic)

该文章已生成可运行项目,

PVT(Position–Velocity–Time) 模式里,驱动在收到一段数据

(p0,v0)  →  T  段时长  (p1,v1) (p_0, v_0)\;\xrightarrow[\;T\;]{\text{段时长}}\;(p_1, v_1) (p0,v0)段时长T(p1,v1)

后,“加速度曲线不是直接给的”,而是驱动根据这四个边界条件,在段内自行插补算出来的。最常见实现是用一条 三次多项式(Hermite cubic),也就是:

1) 段内轨迹模型(常见默认)

令段内时间 t∈[0,T]t\in[0,T]t[0,T],驱动构造

p(t)=p0+v0 t+c t2+d t3 p(t)=p_0+v_0\,t+c\,t^2+d\,t^3 p(t)=p0+v0t+ct2+dt3

满足:

p(T)=p1,p˙(0)=v0,p˙(T)=v1 p(T)=p_1,\qquad \dot p(0)=v_0,\qquad \dot p(T)=v_1 p(T)=p1,p˙(0)=v0,p˙(T)=v1

解得(写法给便于工程实现):

A=p1−p0−v0 T,B=v1−v0c=3A−BTT2,d=BT−2AT3 \begin{aligned} A&=p_1-p_0-v_0\,T, \quad B=v_1-v_0\\ c&=\frac{3A-BT}{T^2},\qquad d=\frac{BT-2A}{T^3} \end{aligned} Ac=p1p0v0T,B=v1v0=T23ABT,d=T3BT2A

于是**速度、加速度、加加速度(跃度/jerk)**分别是:

KaTeX parse error: Undefined control sequence: \dddot at position 64: …c+6d\,t,\qquad \̲d̲d̲d̲o̲t̲ ̲p(t)=6d\;(\text…

结论:PVT 段内加速度是线性函数,跃度为常数。也就是说,这段的加速度从段首到段末线性变化

2) 峰值加速度(快速检查是否越限)

因为 p¨(t)\ddot p(t)p¨(t)ttt 线性变化,所以段内最大/最小加速度出现在两端:

astart=p¨(0)=2c,aend=p¨(T)=2c+6dT a_{\text{start}}=\ddot p(0)=2c,\qquad a_{\text{end}}=\ddot p(T)=2c+6dT astart=p¨(0)=2c,aend=p¨(T)=2c+6dT

工程上可以用这两个值与驱动的 amax⁡a_{\max}amax 做比较,预估是否会触发越限。

3) 如果越限,驱动一般会怎么做?

不同厂商策略略有差异,常见有三类(手册会说明):

  1. 时间拉伸(time-scaling):自动增大 TTT(或施加速度覆盖系数)直到满足 amax⁡a_{\max}amax / jmax⁡j_{\max}jmax
  2. 拒收/报错:本段不执行,置故障(常见于严格遵循设定极限的驱动)。
  3. 内部重规划:启用内置 S 曲线/限幅器,实际运行与给定端点等价但段内曲线被“柔化”。

最常见、也最“稳”的方式是 时间拉伸p0,v0,p1,v1p_0,v_0,p_1,v_1p0,v0,p1,v1 不变,只把 TTT 加长一点,三次多项式自然降低所需的峰值加速度/跃度。

4) 与 PT 的差异

  • PT(Position–Time)只有 (p0,p1,T)(p_0,p_1,T)(p0,p1,T)(没有 v1v_1v1 约束),简化实现通常用分段线性:段内速度常数、段边界速度跳变 ⇒ 加速度呈脉冲(不平滑)。一些驱动会内置滤波/限加速度让它更温和,但严格说 PT 不保证速度连续。
  • PVT多一个 v1v_1v1,驱动能构造速度连续的三次多项式 ⇒ 加速度线性、跃度常数,轮廓明显更平滑。

5) 小示例(便于在上位机预估)

给定一段 T=0.02 sT=0.02\,\text{s}T=0.02s(20 ms),p0=1000p_0=1000p0=1000v0=50v_0=50v0=50p1=1200p_1=1200p1=1200v1=60v_1=60v1=60(单位随系统而定):

A=1200−1000−50⋅0.02=200−1=199B=60−50=10c=3⋅199−10⋅0.020.022≈597−0.24×10−4≈1.492×106d=10⋅0.02−2⋅1990.023≈0.2−3988×10−6≈−4.97×107 \begin{aligned} A&=1200-1000-50\cdot0.02=200-1=199\\ B&=60-50=10\\ c&=\frac{3\cdot199-10\cdot0.02}{0.02^2}\approx \frac{597-0.2}{4\times10^{-4}}\approx 1.492\times 10^6\\ d&=\frac{10\cdot0.02-2\cdot199}{0.02^3}\approx \frac{0.2-398}{8\times10^{-6}}\approx -4.97\times 10^7\\ \end{aligned} ABcd=12001000500.02=2001=199=6050=10=0.0223199100.024×1045970.21.492×106=0.023100.0221998×1060.23984.97×107

于是

astart=2c≈2.98×106,aend=2c+6dT≈2.98×106+6(−4.97×107)(0.02)≈−2.0×106 a_{\text{start}}=2c\approx 2.98\times10^6,\quad a_{\text{end}}=2c+6dT\approx 2.98\times10^6+6(-4.97\times10^7)(0.02)\approx -2.0\times10^6 astart=2c2.98×106,aend=2c+6dT2.98×106+6(4.97×107)(0.02)2.0×106

(数值只是演示量级:说明段越短、位移/速度变化越大 ⇒ 需要的加速度越大,因此合理选 TTT 很关键。)

实战建议:在上位机按上述公式先行估算峰值加速度/跃度;若超限,增大段长 TTT 或减小端点速度差 ∣v1−v0∣|v_1-v_0|v1v0、位移量 ∣p1−p0∣|p_1-p_0|p1p0

6) 有些驱动提供更高级的插补器

部分品牌在 PVT 内部直接使用 五次多项式(quintic)/S 曲线(可同时约束 amax⁡a_{\max}amaxjmax⁡j_{\max}jmax),甚至把 a0,a1a_0,a_1a0,a1 也当边界条件(但这已超出标准 PVT 的最小约束集)。遇到这类驱动,你会在手册里看到:“PVT 插补为 jerk-limited / S-curve”,加速度会是分段线性连续非线性,比三次更“柔”。


一句话总结

  • 标准 PVT:驱动用三次 Hermite插补,加速度在段内线性变化、跃度常数;是否越限取决于 (p0,p1,v0,v1,T)(p_0,p_1,v_0,v_1,T)(p0,p1,v0,v1,T)
  • 工程做法:上位机先按公式估峰值 → 若超限就拉长 TTT 或减小速度/位移跨度;或选用支持 S 曲线/时间缩放 的驱动。

例子:100ms内移动10mm

错误思路:10 mm / 100 ms → 每 1 ms 递增 0.1 mm
这种做法本质上是恒速线性插值,等价于 PT(Position-Time),不是严格的 PVT

  • 这样做的速度是常数 100 mm/s,加速度为 0(理想数学上起止瞬间需要无穷大加速度跳变)。
  • 真实伺服会自己“圆滑”起止,导致线性位置和电机实际轨迹不一致、冲击也更大。

如果要实现“驱动内部 PVT 插补”,标准做法是用一段三次多项式(Hermite cubic),同时满足段首/段末的位置和速度p0,v0p_0,v_0p0,v0p1,v1p_1,v_1p1,v1 ,用时 TTT)。这样速度连续、加速度线性变化(jerk 常数)


一段 PVT 的通用公式(落地可用)

给定:p0,v0p_0,v_0p0,v0p1,v1p_1,v_1p1,v1 ,段时长 TTT

令段内时间 t∈[0,T]t\in[0,T]t[0,T],取

p(t)=p0+v0t+c t2+d t3 p(t)=p_0+v_0 t + c\,t^2 + d\,t^3 p(t)=p0+v0t+ct2+dt3

满足边界条件可解得(记 A=p1−p0−v0T,  B=v1−v0A=p_1-p_0-v_0 T,\;B=v_1-v_0A=p1p0v0T,B=v1v0):

c=3A−BTT2,d=BT−2AT3. c=\frac{3A-BT}{T^2},\qquad d=\frac{BT-2A}{T^3}. c=T23ABT,d=T3BT2A.

由此:

KaTeX parse error: Undefined control sequence: \dddot at position 84: …c+6dt,\\ j(t)&=\̲d̲d̲d̲o̲t̲ ̲p(t)=6d\;\;(\te…

性质:速度连续;加速度在段内线性变化;jerk 为常数。


代入例子(做对比)

目标:从 p0=0p_0=0p0=0p1=10 mmp_1=10\text{ mm}p1=10 mmT=0.1 sT=0.1\text{ s}T=0.1 s

  • 若做 PT/恒速:每 1 ms 加 0.1 mm ⇒ v=100 mm/sv=100\text{ mm/s}v=100 mm/s,但起止瞬间加速度跳变(不建议作为驱动内部的标准插补)。

  • 若做 PVT(更合理),常见两种边界速度设法:

    1. 起停都为 0v0=v1=0v_0=v_1=0v0=v1=0

      • A=10,  B=0⇒c=300.01=3000,  d=−200.001=−20000A=10,\;B=0\Rightarrow c=\frac{30}{0.01}=3000,\; d=\frac{-20}{0.001}=-20000A=10,B=0c=0.0130=3000,d=0.00120=20000(单位按 mm、s)。
      • 峰值加速度出现在端点:∣amax⁡∣=∣2c∣=6000 mm/s2|a_{\max}|=|2c|=6000\text{ mm/s}^2amax=2c=6000 mm/s2(另一端为 −6000-60006000)。
      • 速度在中点达峰值,vmax⁡≈150 mm/sv_{\max}\approx150\text{ mm/s}vmax150 mm/s(平均速仍是 100)。
    2. 保持段内基本恒速:选 v0=v1=100 mm/sv_0=v_1=100\text{ mm/s}v0=v1=100 mm/s(更像匀速段,但两端仍会有有限的加速度过渡,而非脉冲)。


约束与整定(很重要)

  • 加速度/jerk 限制(驱动或机械允许的上限)

    • v0=v1=0 时,端点加速度幅值 ∣a∣max=6 ΔpT2|a|_\text{max}=\dfrac{6\,\Delta p}{T^2}amax=T26Δp

      • 反推 最小 TT≥6 Δpamax⁡T \ge \sqrt{\dfrac{6\,\Delta p}{a_{\max}}}Tamax6Δp
    • jerk 幅值 ∣j∣=∣6d∣=∣6(BT−2A)T3∣|j|=|6d|= \left|\dfrac{6(BT-2A)}{T^3}\right|j=6d=T36(BT2A)

  • 超限怎么办:做 time-scaling(把本段 TTT 按比例放大),或减小 ∣p1−p0∣|p_1-p_0|p1p0∣v1−v0∣|v_1-v_0|v1v0;必要时改用 S 曲线/五次多项式(可同时约束 aaajjj)。


固件实现步骤(驱动侧)

  1. 接收段:得到 (p1,v1,T)(p_1,v_1,T)(p1,v1,T),内部保存上一段末端 (p0,v0)(p_0,v_0)(p0,v0)

  2. 计算系数:按上式求 c,dc,dc,d

  3. 限幅检查:用端点加速度 a(0)=2c,  a(T)=2c+6dTa(0)=2c,\;a(T)=2c+6dTa(0)=2c,a(T)=2c+6dT∣j∣=∣6d∣|j|=|6d|j=6d 校验上限;如超限 → 拉长 T 或告警。

  4. 按内部高速周期(如 1 kHz)采样

    tk=k Δt(Δt=1 ms),pk=p(tk). t_k = k\,\Delta t\quad(\Delta t=1\text{ ms}),\quad p_k = p(t_k). tk=kΔt(Δt=1 ms),pk=p(tk).

    pkp_kpk 送入位置环(或把 v(tk)v(t_k)v(tk) 送入速度环)。

  5. 段切换:t 到 TTT 时切入下一段,保证上一段末端与下一段起点(含 vvv)一致。

如果你一定要“1 ms 一点”,那是 上位 PVT 规划 + 驱动内部再细分 的冗余;驱动侧仍然以多项式生成 1 ms 目标,不是简单 0.1 mm 线性累加。


何时用线性(PT)也能接受?

  • 轨迹要求不高、速度较低;或由上位机已经做了充分的 S 曲线限加速度,驱动只需“按时到点”。
  • 否则,更推荐 PVT 的三次插补(速度连续、冲击更小)。

小结

  • “每 1 ms 加 0.1 mm”=PT/恒速是标准的 PVT
  • PVT 正确做法:用三次多项式满足 (p0,v0)→(p1,v1)(p_0,v_0)\to(p_1,v_1)(p0,v0)(p1,v1)TTT速度连续、加速度线性;配合加速度/jerk 限制与时间缩放,机械更安全。

C/C++ 内联函数

20 kHz 位置环(50 µs 周期)非常适合在驱动内部做 PVT(三次 Hermite)插补。思路是:段内跃度 jjj 恒定,所以可以用一个常加加速度(jerk 常数)的离散递推在 50 µs 中断里高效“滚动”出 p,v,ap,v,ap,v,a

  • 段开始一次性算好三次系数 c,dc,dc,d、端点加速度 a(0),a(T)a(0),a(T)a(0),a(T)jerk j=6dj=6dj=6d

  • 中断里不用幂函数,仅做加减乘

    a←a+j Δtv←v+a Δt+12j Δt2p←p+v Δt+12a Δt2+16j Δt3 \begin{aligned} a &\leftarrow a + j\,\Delta t \\ v &\leftarrow v + a\,\Delta t + \tfrac{1}{2} j\,\Delta t^2 \\ p &\leftarrow p + v\,\Delta t + \tfrac{1}{2} a\,\Delta t^2 + \tfrac{1}{6}j\,\Delta t^3 \end{aligned} avpa+jΔtv+aΔt+21jΔt2p+vΔt+21aΔt2+61jΔt3

  • 最后一步“对齐段末”用闭式公式消除累计误差。

  • STM32F405 有单精度 FPU,float 足够;段初始化时也可以用 double 再转 float。


代码(可直接用在 50 µs 定时中断)

// pvt_hermite_fast.h
#pragma once
#include <math.h>
#include <stdbool.h>
#include <stdint.h>

typedef float  pvt_real;     // F405 有单精度 FPU,float 最快
typedef double pvt_real64;   // 段初始化可用 double 提升一次性精度

typedef struct {
  // 边界
  pvt_real p0, v0, p1, v1;
  pvt_real T;          // 段时长(可能因限幅被拉长)

  // 三次系数与派生量
  pvt_real c, d;       // p(t) = p0 + v0 t + c t^2 + d t^3
  pvt_real a0;         // 段首加速度 = 2c
  pvt_real j;          // 常数 jerk = 6d

  // 运行状态
  pvt_real t;          // 已运行时间
  pvt_real p, v, a;    // 当前设定 p/v/a
  bool active;

  // 采样常量(减少乘法次数)
  pvt_real dt, dt2, dt3;

  // 限幅
  pvt_real a_max;      // 若<=0 则不限制
  pvt_real j_max;      // 若<=0 则不限制
} PVT_Seg;

/*—— 一次性计算 c,d ——*/
static inline void pvt_compute_cd64(pvt_real64 p0,pvt_real64 v0,
                                    pvt_real64 p1,pvt_real64 v1,
                                    pvt_real64 T,
                                    pvt_real64* c, pvt_real64* d)
{
  pvt_real64 A = (p1 - p0) - v0*T;   // p1 - p0 - v0 T
  pvt_real64 B = (v1 - v0);
  *c = (3.0*A - B*T) / (T*T);
  *d = (B*T - 2.0*A) / (T*T*T);
}

/*—— 按 a_max / j_max 需要拉长时间 ——*/
static inline pvt_real pvt_timescale_for_limits(pvt_real a0, pvt_real aT, pvt_real j,
                                                pvt_real a_max, pvt_real j_max)
{
  pvt_real s = 1.0f;
  if (a_max > 0.0f) {
    pvt_real a_need = fmaxf(fabsf(a0), fabsf(aT));
    if (a_need > a_max) s = fmaxf(s, sqrtf(a_need / a_max));   // a ~ 1/T^2
  }
  if (j_max > 0.0f) {
    pvt_real j_need = fabsf(j);
    if (j_need > j_max) s = fmaxf(s, cbrtf(j_need / j_max));   // j ~ 1/T^3
  }
  return s;
}

/*—— 启动一段(设定 50us 周期等)——*/
static inline void pvt_start(PVT_Seg* s,
                             float p0, float v0, float p1, float v1,
                             float T,      // 例如 0.02f = 20ms
                             float a_max,  // mm/s^2 或 脉冲/s^2;<=0 表示不限制
                             float j_max,  // mm/s^3;<=0 表示不限制
                             float dt)     // 例如 50e-6f
{
  if (T <= 0.0f) T = 1e-3f;
  if (dt <= 0.0f) dt = 50e-6f;

  s->p0=p0; s->v0=v0; s->p1=p1; s->v1=v1; s->T=T;
  // 用 double 求一次 c,d
  pvt_real64 c64,d64;
  pvt_compute_cd64(p0, v0, p1, v1, T, &c64, &d64);
  float c = (float)c64, d = (float)d64;

  float a0 = 2.0f*c;
  float aT = 2.0f*c + 6.0f*d*T;
  float j  = 6.0f*d;

  // 可选:自动拉长时间
  float s_scale = pvt_timescale_for_limits(a0,aT,j, a_max,j_max);
  if (s_scale > 1.0f) {
    s->T = T * s_scale;
    pvt_compute_cd64(p0, v0, p1, v1, s->T, &c64, &d64);
    c = (float)c64; d = (float)d64;
    a0 = 2.0f*c;
    aT = 2.0f*c + 6.0f*d*s->T;
    j  = 6.0f*d;
  }

  s->c=c; s->d=d; s->a0=a0; s->j=j;

  // 初始化运行状态(t=0 用精确端点)
  s->t = 0.0f;
  s->p = p0;
  s->v = v0;
  s->a = a0;

  s->dt  = dt;
  s->dt2 = dt*dt * 0.5f;        // 0.5 dt^2
  s->dt3 = dt*dt*dt / 6.0f;     // (1/6) dt^3

  s->a_max = a_max;
  s->j_max = j_max;
  s->active = true;
}

/*—— 中断里每 50us 调一次;返回 false 表示段已结束 ——*/
static inline bool pvt_step_50us(PVT_Seg* s, float* p_ref, float* v_ref, float* a_ref)
{
  if (!s->active) return false;

  // 若剩余时间不足一个 dt,直接跳到段末避免累积误差
  float t_left = s->T - s->t;
  if (t_left <= s->dt) {
    s->p = s->p1;
    s->v = s->v1;
    s->a = 2.0f*s->c + 6.0f*s->d*s->T;  // 端点加速度
    s->t = s->T;
    s->active = false;
  } else {
    // 常数 jerk 递推:先更新加速度,再更新速度与位置
    s->a += s->j * s->dt;
    s->v += s->a * s->dt + s->j * s->dt2;
    s->p += s->v * s->dt + s->a * s->dt2 + s->j * s->dt3;
    s->t += s->dt;
  }

  if (p_ref) *p_ref = s->p;
  if (v_ref) *v_ref = s->v;
  if (a_ref) *a_ref = s->a;
  return s->active;
}

用法示例(20 ms 一段,ISR 里 50 µs 推算设定值):

// 全局/任务里准备一段
PVT_Seg seg;
void start_one_segment(void) {
  // 例:从 0mm,0 到 10mm,0,用时 20ms;限制 a_max/j_max(按机械设)
  pvt_start(&seg, 0.0f, 0.0f, 10.0f, 0.0f,
            0.020f,    // T = 20ms
            6000.0f,   // a_max
            1.0e6f,    // j_max
            50e-6f);   // dt = 50us (20kHz)
}

// 20kHz 定时器中断(位置环)
void TIMx_UP_IRQHandler(void)
{
  float p, v, a;
  if (!pvt_step_50us(&seg, &p, &v, &a)) {
    // 段结束:这里切下一段(把上一段 p1,v1 当下一段 p0,v0)
    // pvt_start(&seg, ... 下一段 ...);
  }

  // 将 p 作为位置给定送入位置环(或 v 给速度环)
  // controller_set_position_ref(p);
  // 其余:前馈、限幅、抗饱和等…
}

关键参数与工程建议

  • 段长 vs. 内环周期:20 ms/段 × 20 kHz = 400 步/段,非常平滑。高速段可 10 ms/段。
  • 时间对齐:保证段长 TTT1 ms 的整数倍(多数驱动/规划器假设 1 ms 内插)。内部 50 µs 只是控制环采样,更细。
  • 限幅:上面 pvt_start() 自带 time-scaling,避免超过 amax⁡a_{\max}amax / jmax⁡j_{\max}jmax
  • FPU/编译:用 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -Ofast;中断里避免 powf() 这类重函数。
  • 数值收尾:最后一步直接用端点值“对齐”,避免累计误差渗漏到下一段。
  • 多轴同步:多轴一起调用 pvt_start(),共用同一时钟;段边界统一触发即可天然同步。
  • 前瞻缓冲上位机侧仍建议提前喂 ≥150–200 ms 数据,以便容错和流畅衔接。
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值