首页 / 驱动算法 / 高级篇

静摩擦远大于动摩擦系统的电机驱动优化

为什么电机一启动会"咔哒"跳一下?为什么低速总是一顿一顿像"爬行"?当静摩擦(Stribeck)远大于动摩擦时,低速爬行、启动死区、定位超调、极限环会接踵而至。本文从摩擦机理讲起,给出辨识方法,以及摩擦前馈、高频微振、扰动观测器、位置环防死区四类工程补偿方案,配交互式爬行抑制仿真器。

1. 什么是静摩擦?为什么它远大于动摩擦?

把一台带减速器/丝杠的伺服轴用手轻轻推:推不动——需要先克服一个"门槛"力,一旦动起来,维持运动反而轻松很多。这个"门槛"就是静摩擦(Static Friction)Fs,运动起来后需要克服的恒定阻力叫库仑动摩擦(Coulomb Friction)Fc,再加上与速度成正比的部分叫粘滞摩擦 b·v

在精密伺服系统里,静摩擦往往明显大于动摩擦。原因有三:

典型比值 Fs/Fc = 1.5 ~ 4,带预紧减速器、油封的轴甚至可达 5~10。

💡 Stribeck 曲线:从静止到运动的完整摩擦画像

摩擦不是从 Fs 直接跳到 Fc,而是要经过一段"随速度增大摩擦先下降"的过渡。这段负斜率区域(0 → vs)就是所有爬行问题的物理根源。工程上常用指数形式描述:

F(v) = Fc·sgn(v) + b·v + (Fs − Fc)·e−(v/vs·sgn(v)

vs 称 Stribeck 速度,是摩擦从"静"过渡到"动"的特征速度,一般很小(百分之几到零点几 rad/s)。v=0 时上式等于 Fs,v→∞ 时趋向 b·v 主导的粘滞段。

摩擦分量 表达式 对系统的影响
静摩擦 Fs 启动死区、"突跳"、定位保持时的死区
库仑动摩擦 Fc·sgn(v) 方向切换时扭矩阶跃(象限切换毛刺)
粘滞摩擦 b·v 高速增益损失,速度-转矩曲线斜率
Stribeck 过渡 (Fs−Fc)·e−(v/vs 低速负阻尼 → 爬行(stick-slip)

2. 静摩擦 >> 动摩擦带来哪些问题?

⚠️ 关键认知:爬行是"负阻尼"失稳,不是增益不足

Stribeck 负斜率段相当于给系统注入负阻尼:速度扰动会被放大而不是被衰减。所以"调大 PI 增益硬压"往往压不住爬行,增益大了甚至更糟(进入极限环)。正确思路是让运行点离开负斜率区——用前馈把静摩擦"提前垫掉",或用 dither 让系统始终处于微运动、绕开静摩擦粘滞。这是本文全部方案的核心逻辑。

3. 摩擦辨识:先测出 Fs、Fc、b、vs

要补偿,先得把摩擦参数测出来。工程上四类方法各有适用场景:

方法一:多速匀速扫描(测库仑 + 粘滞)

让电机以多个不同速度匀速运行(避开极低速区),记录每个速度下的稳态电流 → 转矩。低速段(远离 vs 影响)的截距就是 Fc,斜率就是 b:

库仑与粘滞参数

T(v) = Fc·sgn(v) + b·v  ⟹  多组 (v, T) 线性拟合得 Fc 与 b

方法二:静摩擦临界转矩测量(测 Fs

位置锁定在零速,以极慢的速率线性增大 iq 指令(如每 100ms 加 0.1% 额定),记录转子刚好开始转动时的转矩,即为 Fs。多测几次取平均,减小随机性。

方法三:低速 Stribeck 扫描(测 vs 与过渡形状)

在 vs 附近取多点(如 0.5、1、2、5、10、20 × 0.01 rad/s)做匀速扫描,把 F(v) 曲线画出来,负斜率段的范围就是 vs 的尺度,也能量出过渡形状与指数项幅值。

方法四:位置锁定转矩斜坡(最高精度基准)

和齿槽转矩标定类似,用位置环把转子锁在固定角度,记录维持位置所需的 iq,多角度扫描。适合做出厂标定基准,但耗时,一般只做研发验证。

✅ 标定小贴士

① 方向要分开测:正反向摩擦往往不对称(重力、装配),前馈必须分方向存参;② 摩擦随温度、磨损漂移(油温上升 Fs 明显下降),产品级要做定期重标定在线自适应;③ 零速附近的 sgn(v) 会抖动,测量和补偿都要加死区/滞回

4. 补偿方案总览

方案 原理 对爬行/死区效果 代价 适用场景
摩擦前馈 按指令速度预补 F̂(v)(含静摩擦 boost) 显著,直接消除死区 需标定参数,参数漂移需自适应 所有带速度环/位置环的伺服
Stribeck 前馈查表 把实测 F(v) 曲线存表按速度查 高,含过渡段 表内存、需标定 爬行敏感的高精度轴
高频微振 dither 叠加高频小幅正弦,绕开静摩擦粘滞 中,等效降低 Fs 噪声/磨损/发热 低速持续运行、不允许明显爬行的场合
扰动观测器 DOB 把摩擦当扰动观测并前馈 中高,自适应 受观测器带宽限制 摩擦难建模、需跟随漂移
位置环防死区 到位前先给突破扭矩 + 积分限幅 消除极限环 需调阈值 定位/到位保持类应用

实际产品通常是前馈为主 + DOB 兜底 + 位置环防超调的组合,分工明确不重复。

5. 方案一:摩擦前馈(最直接、性价比最高)

核心思想:把摩擦当成一个"与速度相关、可预知"的确定性扰动,在扭矩指令上提前补上,让闭环只负责残余误差。摩擦前馈三件套:

摩擦前馈扭矩(分方向)

Tff(vref) = F̂c·sgn(vref) + b̂·vref + Boost(vref)

💡 为什么 Boost 要补 Fs 而不是 Fc

启动瞬间系统处于"静止"状态,要突破的是 Fs。若前馈只补 Fc,剩下的 (Fs−Fc) 还得靠 PI 积分去攒,攒满又"突跳"——死区只是被缩小没被消除。把低速段前馈补到 F̂s,突破发生在积分还很小的时候,启动平滑无突跳。速度超过 vs 后 Boost 退出,回到动态值。

参数误差容忍度:前馈参数有 ±20% 误差仍能显著改善(残余由 PI 兜底)。只有方向搞反才会帮倒忙——所以分方向标定、分方向补偿是铁律。

6. 方案二:高频微振(Dither)——不标定也能"润滑"

在扭矩/位置指令上叠加一个高频小幅正弦(或方波),让轴始终处于微幅往复运动,处于"动摩擦域"而绕开静摩擦粘滞:

Dither 注入

Tcmd = Tctrl + Ad·sin(2π·fd·t),  Ad ≈ (0.1~0.3)·Fs

对比项 摩擦前馈 Dither 微振
是否需要标定 需要(Fs/Fc/b/vs 不需要
对启动突跳 彻底消除(boost) 大幅减轻
附加噪声/磨损 有(需权衡)
参数漂移鲁棒性 需自适应 天然鲁棒
推荐组合 主方案 低速段补充/无法标定时的替代

7. 方案三:扰动观测器把摩擦"看"出来

摩擦本质是一个确定性扰动,可以用扰动观测器(DOB)/ 扩张状态观测器(ESO)在带宽内估计出来再前馈,不需要精确摩擦模型:

一阶摩擦扰动观测器

τ̂fric(k+1) = τ̂fric(k) + L·( ω̇meas·J − (Te − τ̂fric) )  (L 为观测器增益,等价低通滤波观测

8. 方案四:位置环防死区与防超调

定位类应用(机械臂、云台、转台、点胶)还要专门处理"到位"环节:

  1. 突破扭矩(break-away move):当位置误差进入死区(< 摩擦死区对应的角度)时,先发一个持续短时(如 50~100ms)的 F̂s 级扭矩"撬动"轴,再回到伺服——避免"想动动不了、攒够了又冲过头"
  2. 积分分离 / 变增益:位置误差很小时退出积分或降低增益,防止积分在死区里继续累积造成过冲
  3. 到位保持:到位后保持一个略大于 F̂s·sgn(位置误差) 的保持扭矩,防止被外力"吸走"又回不来(deadband 抖动)
  4. 速度前馈:位置环叠加速度前馈,让运动指令不再依赖积分"蹭"过去,从源头减小对摩擦的敏感

9. 交互式演示:摩擦模型仿真器与爬行抑制

下面这个仿真器对一个"速度环 + Stribeck 摩擦"系统做实时仿真。你可以调节静/动摩擦比、指令速度,开关摩擦前馈与 dither,直观对比爬行与抑制效果。左侧是 Stribeck 摩擦曲线及当前运行点,右侧是速度随时间的变化。

Stribeck 爬行仿真:摩擦前馈 / Dither 的抑制效果
速度波动峰峰值 -- · 停滞占比 --% · 前馈 --

观察要点:

10. C 语言实现

10.1 摩擦前馈(含静摩擦 boost,分方向)

/* 摩擦前馈:库仑 + 粘滞 + 静摩擦 boost(分方向) */ typedef struct { float fs_pos, fs_neg; /* 静摩擦 Fs(正/反向),N·m */ float fc_pos, fc_neg; /* 库仑摩擦 Fc(正/反向) */ float b; /* 粘滞系数,N·m·s/rad */ float vs; /* Stribeck 速度,rad/s */ } friction_param_t; /* 按指令速度计算摩擦前馈扭矩(分方向取参) */ float friction_ff(const friction_param_t *fp, float vref) { const float s = (vref >= 0.0f) ? 1.0f : -1.0f; const float a = fabsf(vref); const float fs = (vref >= 0.0f) ? fp->fs_pos : fp->fs_neg; const float fc = (vref >= 0.0f) ? fp->fc_pos : fp->fc_neg; float ff; if (a < 0.01f) return 0.0f; /* 零速死区,避免方向抖动 */ if (a < fp->vs) { /* 低速:静摩擦 boost */ ff = fs * s; } else { /* 正常:库仑 + Stribeck + 粘滞 */ float stribeck = (fs - fc) * expf(-(a * a) / (fp->vs * fp->vs)); ff = (fc + stribeck) * s + fp->b * vref; } return ff; } /* 速度环内使用:te_ref = te_pi + friction_ff(&fp, speed_ref); */

10.2 高频微振 Dither

/* 高频微振 dither:叠加到扭矩指令,绕开静摩擦粘滞 */ static float dith_phase = 0.0f; static const float DITHER_F = 120.0f; /* Hz,避开机械谐振 */ static const float DITHER_A = 0.18f; /* 幅值系数,× Fs */ float dither_add(float fs, float dt) { dith_phase += 2.0f * 3.14159265f * DITHER_F * dt; if (dith_phase > 6.2831853f) dith_phase -= 6.2831853f; return DITHER_A * fs * sinf(dith_phase); } /* 使用:te_cmd = te_pi + ff + (dither_on ? dither_add(fs, dt) : 0.0f); */

10.3 扰动观测器估计残余摩擦

/* 一阶扰动观测器:估计"前馈之后"的残余摩擦/扰动 */ typedef struct { float tau_hat; /* 扰动估计,N·m */ float L; /* 观测器增益,需按带宽整定 */ float J; /* 转动惯量,kg·m² */ } fric_dob_t; float dob_update(fric_dob_t *d, float te, float w_meas, float dt) { /* w_dot ≈ (te - tau_hat)/J;观测残差 = w_dot_meas - w_dot_model */ float w_dot_meas = (w_meas - d->w_prev) / dt; float w_dot_model = (te - d->tau_hat) / d->J; d->tau_hat += d->L * (w_dot_meas - w_dot_model) * dt; /* 积分校正 */ d->w_prev = w_meas; return d->tau_hat; } /* 使用:te_ref = te_pi + friction_ff(&fp, w_ref) + dob_update(&dob, te_actual, w_meas, dt); */ /* 注意:dob 与模型前馈"分工",只观测残余,避免同一扰动被补偿两次 */

11. 摩擦补偿注入到 FOC 级联的哪个位置?

前面几节讲了"补什么",这一节回答工程落地时的第一个问题:补偿扭矩加在哪一层、按什么信号生成?FOC 级联通常分四层:位置环 → 速度环 → 电流环 → 逆变器。摩擦是一个转矩层的扰动,所以所有与摩擦相关的补偿都围绕"速度环输出的转矩指令 Tref"做文章,而不是进电流环。

FOC 级联与摩擦补偿注入点 位置环 P 速度环 PI Σ 电流环 PI 逆变器 电机+负载 θ_ref ω_ref T_ref u_αβ θ θ_enc 编码器反馈 摩擦前馈 ff(v_ref) Dither A_d·sin DOB 残余 τ̂_dob 启动撬动 到位保持(位置环) 转矩指令层注入(速度环输出处) 位置环侧生成(启动/到位阶段) 编码器反馈
注入点生成信号补偿内容说明
转矩指令 Σ指令速度 vref摩擦前馈 T̂ff(vref)用指令速度而非实测速度,避免反馈噪声被前馈放大
转矩指令 Σ时间 tDither Ad·sin(2πfdt)高频小幅,绕开静摩擦粘滞
转矩指令 Σ实测速度 ω、电磁转矩 TeDOB 残余 τ̂dob吃掉前馈未覆盖的残余与漂移
位置环侧位置误差 eθ启动撬动 / 到位保持死区内发短时突破扭矩 + 保持扭矩
💡 为什么不在电流环里补摩擦?

电流环带宽(通常 1~5 kHz)远高于摩擦的动态过程,它只负责"让实际电流快速等于参考电流"。摩擦表现为转矩层扰动,在电流环内部补只会引入相位滞后和电流纹波。正确做法是:摩擦前馈 / Dither / DOB 全部加在速度环输出的 T_ref 上,电流环保持"跟随"职责不变;位置环负责启动/到位阶段的特殊动作。

⚠️ 叠加顺序与抗饱和

推荐叠加顺序:Tref = TPI + T̂ff + Ad·sin + τ̂dob + 撬动/保持。前馈加在 PI 输出之后、转矩限幅之前;PI 积分仍按"不包含前馈"的限幅做抗饱和,否则前馈会把积分"顶"进饱和区。注意前馈本身也会推高 T_ref,转矩限幅要留出余量。

12. LuGre 动态摩擦模型:静态 Stribeck 不够用的时候

前文的 Stribeck 模型是静态/稳态摩擦:给定速度 v 就对应一个摩擦扭矩 F(v)。但实验会发现:微动(几个微米内)时轴像一个弹簧一样有"预滑移"位移,撤力后会部分回弹;往返微动还有滞回。这些静态模型解释不了,但对纳米级定位、高精度微动是致命的。LuGre 模型用一根"鬃毛"(bristle)的弹性形变 z 描述微观接触:

LuGre 动态摩擦模型

dz/dt = v − σ0·|v|/g(v)·z   (鬃毛形变动态)
g(v) = Fc + (Fs − Fc)·e−(v/vs   (Stribeck 包络)
τfric = σ0·z + σ1·dz/dt + b·v   (输出 = 刚度 + 阻尼 + 粘滞)

三个核心参数:σ0(鬃毛刚度,决定预滑移位移大小)、σ1(鬃毛阻尼,抑制预滑移振荡)、b(粘滞)。稳态时 dz/dt=0 → zss = g(v)·sgn(v)/σ0,代回输出即退化为静态 Stribeck 曲线,两者自洽。

场景静态 StribeckLuGre
宏观匀速 / 加减速足够不必要(增复杂度)
微动 / 预滑移(< 100 µm)无法描述弹性位移必须(σ0 弹性)
往返微动滞回无法描述必须
低速精密跟踪(含零点穿越)大致可用更精确
✅ 工程简化:两段式处理

产品里很少直接跑完整 LuGre 微分方程(参数敏感、在线辨识难)。更稳的做法是两段式:① 预滑移区(|v| < vp,位移极小)把摩擦当刚度为 σ0 的弹簧,在位置环里等效成一个"微小弹性负载";② 滑移区退化为静态 Stribeck 前馈。这样既保留预滑移物理,又不引入复杂微分状态,工程上最实用。

13. 摩擦参数在线自适应(RLS)

标定一次就完事了吗?不行——油温升高 30 °C,Fs 可能下降 30% 以上;磨损、负载变化也会让摩擦漂移。所以产品级方案需要在线辨识,让前馈参数跟着实际走。

13.1 思路:正常运行中"顺便"辨识

在 |ω| > vs 的运动段,摩擦 ≈ Fc·sgn(ω) + b·ω,这是关于参数线性的,可用带遗忘因子的递推最小二乘(RLS)在线估计:

RLS 递推(2 参数,回归量 φ = [sgn(ω), ω])

yobs = Te − J·ω̇  (观测摩擦扭矩)
e = yobs − φTθ̂  →  θ̂ += K·e,  K = P·φ / (λ + φTPφ)
P = (P − K·φTP) / λ  (λ 遗忘因子,0.90 ~ 0.995)

13.2 Fs 怎么在线更新?

Fs 出现在"静止→运动"的突破瞬间,RLS 的线性模型覆盖不到。工程上常用突破事件采样:检测到"速度从 0 突破到运动"的瞬间,记录此刻的转矩幅值 |T|,它就是 Fs 的一个样本,做低通平滑更新:

Fs 突破采样更新

s ← F̂s + α·(|Tbreak| − F̂s), α ≈ 0.1 ~ 0.3,仅突破事件发生时

机器人每次从静止启动都是一次"免费"的 Fs 采样,跑几轮就收敛。方向要分开(正反向 Fs 通常不对称)。

13.3 与 DOB 的分工

RLS 是慢自适应(秒级收敛),DOB 是快估计(带宽内瞬时),二者天然互补:RLS 慢慢修正前馈模型参数,DOB 吃掉前馈没补上的瞬时残余。这样既不会像"纯 DOB"那样受带宽限制,也不会像"纯前馈"那样参数漂移后失配。完整 C 实现见第 16 节。

14. 复合前馈:摩擦 + 齿槽 + 重力

真实机器人关节 / 精密转台上,影响转矩的确定性扰动远不止摩擦:齿槽转矩(随角度周期变化,见第 3、9 篇)、重力力矩(随角度变化)、弹簧 / 缆绳张力(随角度或位移)。它们叠加在一起,任何一个单独补偿都会"按下葫芦浮起瓢"。工程上把它们汇总成一个复合转矩前馈

复合前馈(按信号源分工)

Tff(θ, vref) = Tfric(vref) + Tcog(θ) + Tgrav(θ) + Tspring(θ)
Tref = TPI + Tff + Ad·sin(2πfdt) + τ̂dob

扰动源依赖变量前馈形式与本文 / 前文关系
摩擦速度 vrefStribeck 查表 / 模型 + boost本文第 5 节
齿槽角度 θ角度查表 / 谐波注入第 3、9 篇
重力角度 θcos / sin 函数或角度查表机器人关节标配
弹簧 / 缆绳角度 θ刚度 × 位移查表多连杆 / 腱驱
残余未知DOB 兜底本文第 7 节 + 第 07 篇
⚠️ 叠加的坑:重复补偿与饱和

别重复补:摩擦 / 齿槽 / 重力各自独立前馈,但 DOB 观测的是"总残余",不要再给每个源各配一个 DOB——否则同一扰动被补两遍。② 转矩饱和:几路前馈加在一起可能瞬时超过电流 / 转矩限幅,必须统一限幅,且让 PI 积分知道限幅(抗饱和)。③ 角度相位:齿槽 / 重力前馈用指令角度还是实测角度?跟随误差大时用指令角度(含前馈位置)+ 相位校正,避免反馈噪声。

15. Fs/Fc 特别大(5~10)系统的专用策略

带预紧谐波减速器、油封、或涡轮蜗杆自锁机构的轴,Fs/Fc 可达 5~10。此时启动突跳更猛、到位极限环更大、爬行更顽固,常规方案要加强

  1. 启动"撬动"(break-away jolt):静止启动时先发一个 1.2~1.5 × F̂s、持续 5~30 ms 的短时扭矩脉冲"撬"动轴,立即回落。用"主动猛突破"代替"PI 积分慢慢攒",启动时间从几百 ms 降到几 ms,且无突跳过冲
  2. 到位防极限环:位置误差进入死区后,先给一次撬动扭矩快速过死区,然后切到伺服保持——避免"攒积分 - 冲过头 - 反向攒"的极限环
  3. 位置保持扭矩:到位后保持一个 ≈ F̂s × 安全系数(0.9~1.1) × sgn(eθ) 的扭矩,防止被外力"吸走"。注意保持扭矩不要超过 Fs 太多,否则低速微调时又会粘滞
  4. 速度前馈必配:大 Fs 时位置环若只靠积分"蹭"过死区必然极限环,必须叠加速度前馈 + 位置环输出限幅
  5. 加大死区与滞回:速度反馈噪声相对更大,sgn 滞回带要覆盖噪声幅值,防止前馈方向高频翻转
  6. 方向解锁:正反向 Fs 严重不对称(如重力叠加)时,反向启动要单独给"方向解锁"扭矩,不能共用正向前馈
  7. Dither 幅值同步加大:dither 幅值取 Fs 的 10%~30%,Fs 大时绝对幅值也大,要确认不会激励谐振 / 产生可闻噪声(必要时改用方波 dither 提高效率)
策略常规 Fs/Fc = 2~4大 Fs/Fc = 5~10
启动方式前馈 boost 即可必须撬动脉冲(1.2~1.5 × Fs
到位积分分离 + 突破扭矩撬动 + 保持扭矩 + 速度前馈
前馈死区0.5~1% vs加大滞回带防抖动
Dither 幅值0.1~0.3 Fs按 0.1~0.3 Fs,但校验谐振 / 噪声
⚠️ 大 Fs 系统的机械警示

Fs/Fc 特别大往往提示机械预紧过度(润滑不良、密封过紧、预载过大)。算法可以"撬"过去,但长期大扭矩撬动会加速磨损、发热,甚至使电机长期处于接近堵转的大电流。务必先做机械排查(前文要点 1),把 Fs 本身降下来才是治本。

16. 完整可编译工程代码:friction_comp 模块

把前文所有方案落成一个可直接移植的 C 模块:摩擦前馈(分方向 + boost)+ Dither + RLS 在线自适应 + DOB 残余 + 启动撬动 / 到位保持,注入点为速度环输出的 T_ref。头文件与源文件如下,直接加入工程即可使用。

16.1 friction_comp.h

/** * @file friction_comp.h * @brief 摩擦补偿模块(静摩擦 >> 动摩擦伺服系统) * —— 摩擦前馈(含静摩擦boost) + Dither + RLS在线辨识 * + DOB残余 + 启动撬动/到位保持 * @note 注入点:速度环输出(转矩指令 T_ref); * 启动撬动/保持扭矩由位置环侧生成,同样汇入 T_ref * @version v1.0 */ #ifndef __FRICTION_COMP_H #define __FRICTION_COMP_H #include <stdint.h> #ifdef __cplusplus extern "C" { #endif /* ---------- 摩擦静态参数(正反向分开) ---------- */ typedef struct { float fs_pos; /* 静摩擦 Fs(正向),N·m */ float fs_neg; /* 静摩擦 Fs(反向),N·m */ float fc_pos; /* 库仑摩擦 Fc(正向),N·m */ float fc_neg; /* 库仑摩擦 Fc(反向),N·m */ float b_pos; /* 粘滞系数 b(正向),N·m·s/rad */ float b_neg; /* 粘滞系数 b(反向),N·m·s/rad */ float vs; /* Stribeck 速度,rad/s */ } friction_param_t; /* ---------- RLS 在线辨识状态 ---------- */ typedef struct { float p_hat[2]; /* 估计参数 [Fc, b] */ float cov[3]; /* 协方差矩阵 P(2x2 上三角存储) */ float lambda; /* 遗忘因子 0.90 ~ 0.995 */ } friction_rls_t; /* ---------- 一阶扰动观测器 ---------- */ typedef struct { float tau_hat; /* 残余扰动估计,N·m */ float L; /* 观测器增益 */ float J; /* 转动惯量,kg·m² */ float w_prev; /* 上一拍速度,rad/s */ } friction_dob_t; /* ---------- 启动撬动 / 到位保持 ---------- */ typedef struct { float jolt_torque; /* 撬动扭矩幅值,一般 = Fs x 1.2~1.5 */ float jolt_time; /* 撬动持续时间,s */ float hold_torque; /* 到位保持扭矩,一般 ~= Fs */ float deadband; /* 位置死区,rad */ } friction_jolt_t; /* ---------- 补偿器总状态 ---------- */ typedef struct { friction_param_t param; friction_rls_t rls; friction_dob_t dob; friction_jolt_t jolt; float dither_freq; /* dither 频率,Hz */ float dither_amp_k; /* dither 幅值系数(xFs) */ uint8_t ff_enable; /* 摩擦前馈使能 */ uint8_t dither_enable; /* dither 使能 */ uint8_t adapt_enable; /* 在线自适应使能 */ float dt; /* 控制周期,s */ float phase; /* dither 相位累加器 */ uint32_t jolt_cnt; /* 撬动倒计时 */ uint8_t jolt_active; /* 撬动进行中标志 */ } friction_comp_t; /* ---------- API ---------- */ void friction_comp_init(friction_comp_t *fc, float dt); float friction_ff(friction_comp_t *fc, float v_ref); float friction_dither(friction_comp_t *fc); float friction_dob_update(friction_comp_t *fc, float te, float w_meas); void friction_rls_update(friction_comp_t *fc, float tau_f_obs, float w_meas); float friction_comp_process(friction_comp_t *fc, float v_ref, float w_meas, float te, float pos_err); #ifdef __cplusplus } #endif #endif /* __FRICTION_COMP_H */

16.2 friction_comp.c

/** * @file friction_comp.c * @brief 摩擦补偿模块实现 * @usage 速度环 ISR 内每周期调用一次: * float T_ref = pi_out * + friction_comp_process(&fc, v_ref, w_meas, te_actual, pos_err); * 其中 te_actual 取电机实际电磁转矩(= iq * Kt)。 */ #include "friction_comp.h" #include <math.h> #define FRIC_M2PI 6.283185307179586f #define FRIC_FF_DEAD 0.01f /* 前馈速度死区,rad/s,防方向抖动 */ #define FRIC_RLS_WS 0.02f /* RLS 辨识最小速度,rad/s */ static float fric_sign(float x) { return (x >= 0.0f) ? 1.0f : -1.0f; } /* 初始化:标称参数由上层配置,这里只初始化状态量 */ void friction_comp_init(friction_comp_t *fc, float dt) { fc->dt = dt; fc->phase = 0.0f; fc->jolt_active = 0; fc->jolt_cnt = 0; /* RLS 初值:以标称参数一半起步,协方差取大加快收敛 */ fc->rls.p_hat[0] = 0.5f * 0.5f * (fc->param.fc_pos + fc->param.fc_neg); fc->rls.p_hat[1] = 0.5f * 0.5f * (fc->param.b_pos + fc->param.b_neg); fc->rls.cov[0] = 10.0f; fc->rls.cov[1] = 0.0f; fc->rls.cov[2] = 10.0f; fc->rls.lambda = 0.985f; fc->dob.tau_hat = 0.0f; fc->dob.w_prev = 0.0f; } /* 摩擦前馈:库仑 + 粘滞 + 低速静摩擦 boost,分方向取参 */ float friction_ff(friction_comp_t *fc, float v_ref) { const friction_param_t *p = &fc->param; float s, a, fs, fc_, b, ff, st; if (!fc->ff_enable) return 0.0f; a = fabsf(v_ref); if (a < FRIC_FF_DEAD) return 0.0f; /* 死区防抖 */ s = fric_sign(v_ref); fs = (v_ref >= 0.0f) ? p->fs_pos : p->fs_neg; fc_= (v_ref >= 0.0f) ? p->fc_pos : p->fc_neg; b = (v_ref >= 0.0f) ? p->b_pos : p->b_neg; if (a < p->vs) { ff = fs * s; /* 静摩擦 boost */ } else { st = (fs - fc_) * expf(-(a * a) / (p->vs * p->vs)); ff = (fc_ + st) * s + b * v_ref; } return ff; } /* 高频微振 dither:幅值 = 幅值系数 x Fs */ float friction_dither(friction_comp_t *fc) { const friction_param_t *p = &fc->param; float fs_avg; if (!fc->dither_enable) return 0.0f; fs_avg = 0.5f * (p->fs_pos + p->fs_neg); fc->phase += FRIC_M2PI * fc->dither_freq * fc->dt; if (fc->phase > FRIC_M2PI) fc->phase -= FRIC_M2PI; return fc->dither_amp_k * fs_avg * sinf(fc->phase); } /* 一阶扰动观测器:估计"前馈之后"的残余扰动 */ float friction_dob_update(friction_comp_t *fc, float te, float w_meas) { friction_dob_t *d = &fc->dob; float w_dot_meas = (w_meas - d->w_prev) / fc->dt; float w_dot_model = (te - d->tau_hat) / d->J; d->tau_hat += d->L * (w_dot_meas - w_dot_model) * fc->dt; d->w_prev = w_meas; return d->tau_hat; } /* RLS 在线辨识 [Fc, b]:仅运动态喂数据,结果慢速写回前馈参数 */ void friction_rls_update(friction_comp_t *fc, float tau_f_obs, float w_meas) { friction_param_t *p = &fc->param; float *P = fc->rls.cov; float *ph = fc->rls.p_hat; float lam = fc->rls.lambda; float phi[2], y_pred, e, Pphi[2], den, K[2], Pn[3], k; if (!fc->adapt_enable) return; if (fabsf(w_meas) < FRIC_RLS_WS) return; /* 静止段不辨识 */ phi[0] = fric_sign(w_meas); phi[1] = w_meas; y_pred = phi[0] * ph[0] + phi[1] * ph[1]; e = tau_f_obs - y_pred; Pphi[0] = P[0] * phi[0] + P[1] * phi[1]; Pphi[1] = P[1] * phi[0] + P[2] * phi[1]; den = lam + phi[0] * Pphi[0] + phi[1] * Pphi[1]; K[0] = Pphi[0] / den; K[1] = Pphi[1] / den; ph[0] += K[0] * e; ph[1] += K[1] * e; /* 协方差更新 P = (I - K*phi')*P / lam */ Pn[0] = ((1.0f - K[0] * phi[0]) * P[0] - K[0] * phi[1] * P[1]) / lam; Pn[1] = ((1.0f - K[0] * phi[0]) * P[1] - K[0] * phi[1] * P[2]) / lam; Pn[2] = (-K[1] * phi[0] * P[1] + (1.0f - K[1] * phi[1]) * P[2]) / lam; P[0] = Pn[0]; P[1] = Pn[1]; P[2] = Pn[2]; /* 慢速写回前馈参数(分方向),k 为每拍更新率 */ k = 0.002f; if (w_meas >= 0.0f) { p->fc_pos += k * (ph[0] - p->fc_pos); p->b_pos += k * (ph[1] - p->b_pos); } else { p->fc_neg += k * (ph[0] - p->fc_neg); p->b_neg += k * (ph[1] - p->b_neg); } } /* 启动撬动 + 到位保持 */ static float friction_breakaway(friction_comp_t *fc, float pos_err) { float push = 0.0f; /* ① 进入位置死区 -> 发短时撬动扭矩 */ if (!fc->jolt_active) { if (fabsf(pos_err) < fc->jolt.deadband) { fc->jolt_active = 1; fc->jolt_cnt = (uint32_t)(fc->jolt.jolt_time / fc->dt); } } if (fc->jolt_active) { push = fric_sign(pos_err) * fc->jolt.jolt_torque; if (fc->jolt_cnt > 0) fc->jolt_cnt--; if (fc->jolt_cnt == 0) fc->jolt_active = 0; } /* ② 已到位:保持扭矩防"被吸走"(带死区防抖) */ if (fabsf(pos_err) < fc->jolt.deadband * 0.5f) { push += fric_sign(pos_err) * fc->jolt.hold_torque; } return push; } /* 综合入口:速度环 ISR 内每周期调用 */ float friction_comp_process(friction_comp_t *fc, float v_ref, float w_meas, float te, float pos_err) { /* 先算角加速度,dob_update 会更新 w_prev,顺序不能反 */ float w_dot = (w_meas - fc->dob.w_prev) / fc->dt; float ff = friction_ff(fc, v_ref); float dth = friction_dither(fc); float dob = friction_dob_update(fc, te, w_meas); float push = friction_breakaway(fc, pos_err); /* 观测摩擦扭矩 ~= te - J*w_dot,喂给 RLS(前馈后的残余观测) */ friction_rls_update(fc, te - fc->dob.J * w_dot, w_meas); return ff + dth + dob + push; }

16.3 集成示例

/* ============================================================ * 使用示例:速度环 ISR(10 kHz)内的集成 * ============================================================ */ static friction_comp_t g_fric; void motor_ff_init(void) { /* 标称参数:来自第 3 节辨识,之后由 RLS 在线校正 */ g_fric.param.fs_pos = 1.50f; g_fric.param.fs_neg = 1.45f; g_fric.param.fc_pos = 0.60f; g_fric.param.fc_neg = 0.55f; g_fric.param.b_pos = 0.02f; g_fric.param.b_neg = 0.02f; g_fric.param.vs = 0.05f; /* Stribeck 速度 */ g_fric.dob.J = 8.0e-4f; /* 惯量(惯量辨识获得) */ g_fric.dob.L = 400.0f; /* 观测器增益(按带宽整定) */ g_fric.jolt.jolt_torque = 1.3f * g_fric.param.fs_pos; /* 撬动 */ g_fric.jolt.jolt_time = 0.05f; g_fric.jolt.hold_torque = 0.9f * g_fric.param.fs_pos; /* 保持 */ g_fric.jolt.deadband = 0.002f; /* 2 mrad */ g_fric.dither_freq = 120.0f; /* 避开机械谐振 */ g_fric.dither_amp_k = 0.20f; g_fric.ff_enable = 1; g_fric.dither_enable = 0; /* 默认关,按需开 */ g_fric.adapt_enable = 1; /* 在线自适应 */ friction_comp_init(&g_fric, 0.0001f); } void speed_isr_10k(void) { float w_cmd = speed_cmd(); /* 速度指令 rad/s */ float v_ref = speed_loop(); /* 速度环 PI 输出 */ float w_meas = speed_fb(); /* 实测速度 rad/s */ float te_act = iq_fb() * KT; /* 实际电磁转矩 N·m */ float pos_err = position_err(); /* 位置环误差 rad */ /* 摩擦补偿总量叠加到转矩指令(电流环之前) */ float T_ref = v_ref + friction_comp_process(&g_fric, w_cmd, w_meas, te_act, pos_err); torque_apply(T_ref); /* 送电流环参考 */ }
✅ 移植清单

① 速度环 ISR 内每周期调用一次 friction_comp_process,把返回值加到 T_ref;② 按第 3 节辨识结果初始化 param 与 dob.J / dob.L;③ 需要在线自适应就把 adapt_enable = 1,否则置 0 只做静态前馈;④ dither 频率按机械谐振实测选取,默认先关。

17. 扩展演示:摩擦参数在线辨识(RLS)仿真器

下面这个仿真器验证第 13 节的自适应方案:系统带未知的 Fs / Fc / b,指令做正弦往复运动(提供辨识激励)。左侧是 RLS 对 Fc、b 的在线收敛曲线(以及 Fs 突破采样的收敛),右侧对比自适应前馈(RLS 校正)与无前馈(纯 PI)两种配置的速度跟踪——直观看到前馈把速度环从"独自扛摩擦"中解放出来。初始估计故意设错(标称一半),观察它如何自己"学"回来。

摩擦参数在线辨识(RLS)与自适应前馈
Fĉ = -- · b̂ = -- · Fŝ = -- · 自适应 --

观察要点:

18. 工程要点与常见误区

要点 1:先查机械,再谈算法

爬行严重时先检查润滑、密封圈抱紧、导轨/减速器预紧、皮带张力。机械原因(如润滑不良导致的异常大 Fs)靠算法只能"掩盖"不能"解决",还会加剧磨损。算法补偿是"锦上添花",不是"救火"。

要点 2:前馈方向是铁律

摩擦前馈方向必须与运动方向一致,且正反向参数分开存。方向一旦反了,前馈非但不抵消反而加倍助长——这是摩擦前馈唯一会"帮倒忙"的地方。

要点 3:参数漂移要兜底

Fs 随油温、磨损显著变化(油温升高 Fs 下降 30% 以上不罕见)。产品级方案要定期重标定或让 DOB/自适应算法跟随漂移,否则前馈会逐渐失配。

要点 4:dither 频率避开机械谐振

dither 频率若落在机械一阶谐振附近,会把微振放大成可闻共振甚至啸叫。上电前用扫频确认谐振峰,再选 fd(一般取远低于最低谐振的 50~200 Hz,或高于谐振但不过激)。

要点 5:前馈与 DOB 分工不重复

模型前馈吃掉"已知"摩擦,DOB 只观测"残余"。若同一份摩擦被两者各补一次,会造成过补偿和振荡。建议 DOB 的输入取"实际扭矩 − 前馈",输出再接回。

误区 1:加大增益能压住爬行

爬行是 Stribeck 负阻尼失稳,加大 PI 增益往往更糟(进入极限环)。应先让运行点离开负斜率区(前馈/dither),再谈增益。

误区 2:sgn 函数直接用在零速

速度反馈在零速附近来回抖动,直接用 sgn(v) 会让前馈方向高频翻转,相当于注入噪声。必须加速度死区 + 滞回(如 |v|<0.01 时前馈置 0,并保持上次方向直到越过滞回带)。

误区 3:位置环积分不抗饱和

位置误差一直在死区里,积分持续累积 → 超过 Fs 后爆发 → 冲过头 → 反向再来……极限环。要积分分离/变增益 + 积分限幅

误区 4:前馈只在速度环补一次

位置环(尤其点到点运动)也要配合:到位突破扭矩、保持扭矩、速度前馈。只做速度环前馈,位置到位阶段仍会被静摩擦死区卡住。

19. 小结

  1. 静摩擦 >> 动摩擦的根源在微观咬合与工程预紧,典型 Fs/Fc = 1.5~4,预紧减速器可到 5~10
  2. 危害:启动死区与突跳、低速爬行(Stribeck 负阻尼)、定位极限环、象限切换毛刺、低速轮廓误差
  3. 辨识:多速匀速扫描(Fc、b)+ 临界转矩斜坡(Fs)+ 低速 Stribeck 扫描(vs),正反向分开
  4. 四类补偿:摩擦前馈(主方案,含静摩擦 boost)、dither 微振(无标定降级)、扰动观测器(跟随漂移)、位置环防死区(到位/保持)
  5. 爬行的本质是负阻尼失稳,先让运行点离开 Stribeck 负斜率区,再谈增益整定
  6. 成败关键:方向、死区/滞回、参数漂移自适应、前馈与 DOB 分工、dither 避谐振
  7. 注入位置:摩擦前馈 / Dither / DOB 全加在速度环输出 T_ref,电流环只管跟随;位置环负责撬动与保持
  8. 模型进阶:LuGre 动态模型解释预滑移 / 滞回,工程用"预滑移弹簧 + 滑移 Stribeck"两段式简化
  9. 在线自适应:RLS 辨识 Fc / b + 突破采样辨识 Fs + DOB 兜底,参数随温度 / 磨损漂移不再怕
  10. 复合前馈:摩擦(速度相关)+ 齿槽 / 重力(角度相关)按信号源分工叠加,DOB 兜底总残余
  11. 大 Fs/Fc(5~10)系统:启动撬动、到位保持、速度前馈、方向解锁、加大滞回

下一篇我们回到角度反馈的精度问题——磁编码器非线性校准,理解磁编角度误差从哪来、对 FOC 影响多大,以及如何把读数校准准。