做机器人动力学与控制这些年,我踩过最多的坑不是模型有多复杂,而是同一段轨迹跑一万遍,误差看上去还是那一副样子。六轴工业机器人在码垛线上重复同一个搬运循环,单次轨迹跟踪误差波形几乎一模一样;协作机器人在打磨工位上走同一条环形路径,每一圈的力波动都能对上号;移动机器人巡检一圈,某些路段的横向偏差总比别处大。这类现象背后其实是同一件事:误差里有一大块是周期性的,周期等于任务周期。只要任务在重复,重复控制(Repetitive Control)就有用武之地。这篇笔记想把重复控制从"听起来很美"拉到"能写进控制器里跑起来",把内模原理的来龙去脉、延迟链长度怎么算、Q滤波器和相位补偿怎么调、嵌入式里环形缓冲区怎么写、跑起来不收敛该怎么查,一条条捋清楚。适合已经写过位置环PID、想再往上抬一截轨迹精度的朋友,也适合刚接触学习类控制、被"1/(1-z^-N)"绕晕过的同学。
1. 为什么重复控制能干PID干不了的活
1.1 从一条码垛循环说起:误差为什么是"复印"出来的
先看一个真实场景。某条码垛线,单次循环周期T=8 s,机器人从取件点抓箱、抬升、横移、下放,回到原点。位置环用1 kHz的伺服周期,级联PID的位置环带宽大概15 Hz。跑下来你会发现,末端在横移换向那一段的跟踪误差最大能到0.6 mm,而且每一箱的误差曲线形状高度一致,差别只在几十微米量级。
这个"高度一致"不是巧合。误差的成因基本固定:换向时的减速机背隙、关节惯量耦合导致的动态滞后、轨迹规划里加速度不连续带来的柔性冲击、重力补偿在姿态变化区间内的偏差。这些扰动都与关节位置、速度、姿态时间函数绑定,而关节运动又是周期性的。换句话说,扰动d(t)满足d(t+T)=d(t)。
PID看到这种周期扰动会怎么处理?它只能靠积分项一点点"熬"。积分器的带宽很低,典型位置环积分时间常数在几十毫秒量级,对8 s周期里占0.2 s的换向段,积分来不及建立足够输出就已经过去了,下一个周期再重来一遍。结果就是稳态误差被压在某个水平上不能再降。你可以试着把积分增益拉大,代价是超调和低频振荡,机械系统的柔性模态很快就会被激发出来。
重复控制的思路完全不同:既然扰动是周期的,那我为什么不让控制器"记住"上一个周期的补偿量,这一周期原样补回去?**它本质上是在控制器里内置一个周期信号的模型,用"以周期为单位的记忆"取代"以时间为单位的积分"。**这就是它能把误差压到微米级的原因,也是它和普通反馈控制的根本差别。
1.2 重复控制和迭代学习控制到底差在哪
初学者最容易混的就是这两个。名字都带"学习",都用上一周期的信息,都能做到周期信号的零稳态误差,但工程属性和使用条件完全不同。
| 维度 | 重复控制 RC | 迭代学习控制 ILC |
|---|---|---|
| 运行时序 | 实时在线,沿时间轴连续运行 | 逐批次离线/半离线,一个周期结束后更新 |
| 控制律形式 | u(z)=k_r·Q(z)z^-N/(1-Q(z)z^-N)·e(z) | u_{k+1}(t)=u_k(t)+L·e_k(t) |
| 存储需求 | 一个周期长度的延迟链,实时读写 | 一个周期的误差和输入数组,周期末整段更新 |
| 初始条件 | 无要求,随时可切入 | 要求每批次初始条件严格一致 |
| 对非周期成分 | 不敏感,不学习 | 可能被误学进去,污染下一批输入 |
| 典型场景 | 连续回转、往复运动、恒周期作业 | 数控加工单件轮廓、注塑批次、固定轨迹测试 |
具体说,ILC是"跑完一遍,复盘,改进下一遍",周期内不做修改;RC是"一边跑一边用N拍之前的信息修正现在",从头到尾都在闭环里。这个差别决定了RC能塞进伺服驱动器的实时任务里,而ILC一般跑在运动控制器上层的轨迹层,需要任务级的同步信号。
我在项目里的选择原则很简单:**如果任务是连续、周期固定、不允许暂停,用RC;如果任务是单次长轨迹、开头可以从静止起步、能容忍批次之间迭代,用ILC。**两者并不互斥,我见过在点胶设备上先跑ILC做前几批的轮廓标定,稳定后切成RC维持,效果比单用一种更干净。
注意:RC需要任务周期严格固定。如果单次循环时间在7.8 s到8.3 s之间抖,延迟链长度就没法固定,抑制效果会明显打折。这个坑后面第5章会专门讲。
1.3 内模原理:把一台"周期信号发生器"塞进控制回路
内模原理(Internal Model Principle)这句话我第一次读的时候觉得像绕口令:要使系统对某类信号实现零稳态误差,控制器内必须包含该信号的生成模型。翻译成工程师语言就是——你想干掉什么信号,就得在控制器里造一台能产生这个信号的装置。
周期信号怎么"造"出来?不用振荡器,一个延迟加正反馈就够了。信号输出后延迟T秒,再原样送回输入端,形成一个自循环,输出端就永远是这个周期波形。数学上它的传递函数是1/(1-e^(-Ts)),离散域写成1/(1-z^-N),其中N=T/Ts是延迟拍数。
这个环节的频率响应很关键。在ω=2πk/T(k=0,1,2,...)这些频率上,e^(-jωT)=1,分母为0,增益趋于无穷。把这样一个"无穷大增益"的环节并联到误差通道上,闭环灵敏度函数S=1/(1+PC)在这些谐波频率上被强行压到零——理论上,任务周期所有整数倍谐波处的稳态误差都能被消掉,一个都不漏。
这就是RC的诱人之处:它不是一个频点一个频点地去补,而是一口气把整个谐波栅栏全部覆盖。相比之下,如果你想用多个陷波器或者谐振控制器去覆盖前20次谐波,需要20个二阶环节,参数整定到崩溃。RC只需要一个延迟链。
当然,理论上的"无穷大增益"在工程上必须打折扣。原因很直接:**你在建模时假设的对象模型永远不精确,高频段更是离谱。**在那些地方还保留无穷大增益,等于给一个相位已经不清不楚的回路灌能量,结果就是啸叫或者极限环。所以实际实现里一定要引入Q滤波器和相位补偿,把无穷大增益"削平"并把相位拉回安全区。这两件事是RC从教科书走到机柜里的关键,下一章详细拆。
2. 重复控制器的结构拆解与参数设计
2.1 插入式结构:为什么不做成独立控制器
RC有三代结构演化:直接型、插入式(plug-in)、以及各种改进型。工程上用得最多的是插入式,因为它不动你原有的反馈环。
插入式的做法是:保留原来的级联PID或状态反馈控制器C(z),在控制量上再并一路u_rc,输入取位置误差e,输出加到C的输出上。这样原有闭环的稳定性和动态响应基本不变,RC只是在这个基础上做周期误差的"精修"。换句话说,如果RC调砸了,你顶多是性能不升,而不是整机失控——这一点对上线很关键。
插入式结构的灵敏度函数可以推一下,过程不复杂。设原闭环灵敏度S=1/(1+PC),从插入点到输出的传递(补灵敏度)为T_w=PC/(1+PC)=P·S。RC的传递函数为R(z)=k_r·Q(z)z^-N/(1-Q(z)z^-N)。加入并联通道后,新的误差传递函数是:
S_rc(z) = S(z)·(1 - Q(z)z^-N) / (1 - Q(z)z^-N + k_r·z^-N·T_w(z))
分母就是新闭环的特征多项式,稳定性全看它的零点是不是都在单位圆内。这个式子建议你用MATLAB扫一遍频响画出来,比死记公式有用得多。我第一次推的时候纠结了半天Q滤波器到底放分子还是分母,后来直接对着这幅图看——分子上那个(1-Qz^-N)决定了在高频段S_rc会回落到S本身,也就是高频段RC自动"下班",把活交回给原来的PID,这个性质非常符合工程直觉。
2.2 Q滤波器:把无穷大增益拉回地面的那只手
理想的1/(1-z^-N)在高频段的增益仍然是无穷大(只要频率落在谐波点上),现实中做不到也没必要。Q(z)的作用就是给这个正反馈回路加一点"泄漏",让增益在高频段衰减下来。
常见的Q有三种写法,我给你一张对比表:
| Q的形式 | 表达式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 常数 | Q=q,q∈[0.9,0.99] | 实现最简单,一个乘法 | 全频段均匀泄漏,低频精度也受损 | 快速验证、低精度场合 |
| 一阶低通 | Q(z)=(1-a)/(1-a·z^-1) | 高频衰减平缓,相位滞后小 | 参数少,调节余量有限 | 大多数伺服位置环 |
| 零相位低通 | 移动平均/对称FIR | 无相位滞后 | 需要未来数据,额外引入延迟 | 对相位敏感的场合 |
| 双二阶 | 巴特沃斯/切比雪夫IIR | 过渡带可控,衰减陡 | 相位滞后大,整定复杂 | 高频模态密集的对象 |
实用建议:**先用常数Q跑通,确定N和k_r量级,再换成低通。**很多人一上来就上双二阶,结果相位补偿算不准,怎么调都不收敛,最后怀疑人生。
Q的截止频率怎么定?我的经验是取反馈闭环带宽的1/2到1/3,同时不要超过对象模型可信频段的上限。举个例子,位置环带宽20 Hz,那Q的截止频率大概取7到10 Hz。这个选择看起来保守,但换来的鲁棒性是实打实的:机械负载变化、关节摩擦随温度漂移、末端负载变化,都不会让系统瞬间翻车。
代价也很明确——截止频率以上的谐波抑制不了,误差里会残留高频分量。这就要看你更在乎什么。我在打磨项目中就把截止频率压到5 Hz,宁可残留一点高频纹波,也不让力控环和RC打架导致工件表面出现周期性划痕。
2.3 相位补偿:最容易翻车的一个拍数
z^-N这个纯延迟在频域上是线性相位,ω越大滞后越多,一圈就是2π的整数倍。要让它和被控对象T_w的相位对齐,就得在通道里插一个超前环节z^m,m是超前拍数。
m怎么算?别用试凑法硬试,有办法。步骤是:
- 画出T_w(z)在0到奈奎斯特频率的伯德图。
- 找出你打算抑制的谐波频段,一般取基频的3到20次谐波,记这个频段为中心频率ω_c。
- 读出T_w在ω_c处的相位滞后φ(度)。
- 用公式 m ≈ (φ/360°) / (ω_c·Ts) 算出拍数,四舍五入取整。
举个实际数字。T_w在ω_c/2π=150 Hz处滞后38°,Ts=1 ms,那么ω_c·Ts=2π×150×0.001=0.942 rad,φ=38°=0.663 rad,m=0.663/0.942≈0.70,取整m=1。也就是插入一拍超前。
这里有个细节值得说:m必须取整数拍,因为离散系统里没有"半拍"这种东西(除非你做分数延迟近似,后面第4章会讲)。所以相位补偿天生就是粗粒度的,这也是为什么Q滤波器必须存在——它帮你把补偿不到位的那些频段的增益压下去。
实操心得:如果你算出来的m超过2,说明T_w在关心频段的相位滞后太大,硬补偿会把高频段相位抬起来反而有害。这时候正确的做法是降低Q的截止频率,缩小RC的"作业范围",而不是加大m。
2.4 奇次谐波与多周期结构:两条进阶路线
标准RC的延迟链长度是N,对应基频周期T。但对于半周期对称的信号,比如往复运动的去程和回程波形镜像对称,可以用奇次谐波重复控制:延迟链长度改成N/2,只抑制1、3、5……次谐波。
好处很实在:延迟链短一半,收敛速度快一倍(因为信息反馈的周期短了),内存占用减半,相位补偿也更容易做。代价是只对奇次谐波有效,偶次谐波指望不上。我在一个往复式点胶设备上用过这个方法,周期内的对称性非常好,实测收敛到稳定只需要4个循环,标准RC要8个循环才稳住。
另一条路线是多周期并联:一个任务里同时存在两个不同的周期成分,比如主轴旋转周期1.2 s加上送料周期0.4 s,就挂两条延迟链,各自N值不同,输出相加。这条路要小心,两条链的增益会叠加,稳定性条件变成两个约束的联立,一般做法是把每条链的增益降到原来的60%左右再并联,然后逐条增加。我个人的经验是能不做就不做,优先级排序上,先想办法把非主周期那部分用前馈或者机械方式消掉,比在控制器里硬堆两条链划算。
3. 参数整定、稳定性判据与复合控制分工
3.1 延迟拍数N的换算与插入点的选择
N=T/Ts,看起来是最简单的一步,实际上藏着几个容易忽略的坑。
第一个坑是必须取整数。如果T=8 s、Ts=1 ms,N=8000,完美。但如果Ts不是整除关系,比如位置环周期是0.3 ms,T=8 s,N=26666.67,那就有0.67拍的误差。0.67拍听起来很小,实际上相当于在回路里引入了一个随频率变化的相位误差,在接近奈奎斯特频率时这个误差能到几十度。处理办法有两种:一是把采样周期调到能整除,二是做分数延迟近似(拉格朗日插值或一阶全通),后者的相位特性在高频段会退化,需要重新扫频确认。
第二个坑是Ts到底取哪个环的周期。这取决于你把RC插在哪一层:
| 插入点 | 典型周期 | 可抑制谐波上限 | 模型可信度 | 说明 |
|---|---|---|---|---|
| 位置环输出 | 1 ms | 数百Hz | 高 | 主流选择,模型稳定,落地容易 |
| 速度环输出 | 125-250 μs | 1 kHz级 | 中 | 需考虑速度环自身动态,建模工作量大 |
| 电流环输出 | 60-125 μs | 数千Hz | 低 | 带宽极高但对象随温度、磁饱和变化,慎用 |
我自己的选择是默认插在位置环。原因很朴素:位置环的对象模型(从力矩指令到关节位置)在几十赫兹范围内用二阶加延迟就能拟合得不错,而电流环虽然带宽高,但电感参数随电流变化、死区补偿非线性、母线电压波动,模型一会儿一个样,RC这种对模型相位敏感的方法放进去就是在刀尖上跳舞。
第三个坑是N的内存换算。N=8000的float32缓冲区是32 KB,双精度64 KB,对现在的控制器不算什么。但如果你的任务是长周期低速巡检,比如移动机器人绕仓库一圈90 s,电流环周期100 μs,N=900000,float32就是3.6 MB,这时候就要考虑用定点的或者压缩存储,甚至把RC放到上层慢环去做。
3.2 稳定性条件怎么用才不会误判
工程上最常用的判据形式是这样的:对全部频率ω,要求
|Q(e^{jωTs}) - k_r·e^{jωmTs}·T_w(e^{jωTs})| < 1
这个条件的来源是把新闭环特征多项式在单位圆上做小增益估计。说白了就是:Q滤波器漏出去的那部分增益,减去RC补偿通道贡献的增益,两者的差必须始终小于1。
怎么用?画一张频响图,横轴频率,纵轴是上面那个复数的模值,如果全程都在1以下,稳。这条曲线有个专门的名字叫重构谱(regeneration spectrum),是我们整定时最常盯的一张图。它有三个用处:
- 判断稳定裕量。曲线上距离1最近的那个点,就是最危险的频段,工程上一般留0.15到0.25的余量,也就是峰值不超过0.75到0.85。
- 定位振荡频率。如果系统振起来,实测振荡频率往这张图上一对,就知道是哪个频段裕量不够。
- 指导参数调整。k_r增大,曲线整体上移;Q截止降低,曲线在高频下压;m变化,曲线形状在低频段改变。
不同教材对稳定性条件的写法有差别,有的写成|1-k_r·T_w·z^m|<1不带Q,有的把Q放分子,第一次看很容易绕进去。我的建议是:别纠结公式形式,直接对着S_rc特征多项式的根去算,或者更省事——用MATLAB直接扫频画重构谱,实测最可靠。
3.3 k_r的整定流程:从小往大走,随时准备退
k_r是RC通道的总增益,直接决定抑制速度和稳定裕量。整定流程我总结成五步:
- k_r先设为0。确认原有闭环跑起来和平时一样,误差曲线对得上基线。这一步是排除法,避免后面分不清是RC的问题还是原系统的问题。
- 设k_r=0.05,跑三个周期。看误差有没有缓慢下降的趋势。如果完全没有下降,说明N或m错了,先回去检查这两个参数。如果有下降但很慢,正常。
- 逐步加倍到0.1、0.2、0.4,每档跑五个周期。观察两个指标:误差是否继续下降,机械是否有可听的高频啸叫。
- 找到临界点后回退40%。比如0.4开始有轻微啸叫,实际取0.24。
- 做边界工况测试。空载、满载、不同速度、不同姿态各跑一遍,确认任何工况下都不振。
这套流程看起来笨,但能省掉大量反复。我见过太多人直接上0.8,第一次跑就振,然后怀疑是算法写错了,其实只是增益给大了。
注意:k_r的可取值范围和Q的截止频率强耦合。Q截止调高,k_r必须降。别指望两个都往上提。
3.4 和级联PID的分工:谁管什么,边界在哪
这里必须讲清楚职责划分,否则容易出现"两个控制器抢活干"的情况,表现为低频段来回拉扯、系统响应变粘。
分工原则是:**PID负责直流、低频动态和突变瞬态的响应;RC只负责周期性的、稳态的误差成分。**具体讲:
- PID的积分项负责消除非周期的稳态偏差,比如负载变化引起的常值偏移。
- PID的比例和微分负责响应指令突变、应对外部冲击。
- RC负责周期内固定位置的重复误差,比如某个关节在特定角度的减速机背隙、某个方向上的重力补偿残差。
这就引出一个重要的设计约束:任务基频应该明显低于反馈环带宽,最好低于带宽的1/4到1/3。原因是RC的抑制范围主要在Q截止频率以内,而Q截止又低于反馈带宽。如果任务基频已经接近带宽了,RC能管的谐波次数只剩下两三次,性价比很低。
算两个例子对比一下。码垛机器人循环8 s,基频0.125 Hz,反馈带宽20 Hz,Q截止10 Hz,可抑制前80次谐波,余量非常充足,RC能充分发挥。而高速点胶机循环0.4 s,基频2.5 Hz,同样带宽20 Hz、Q截止10 Hz,只能抑制前4次谐波,效果就有限了。这时候要么提高反馈带宽(换更高刚度的机械结构、更高带宽的驱动器),要么干脆考虑前馈补偿或者ILC,别在RC上死磕。
4. 从仿真验证到嵌入式落地
4.1 MATLAB/Simulink里的快速搭建与频域验证
先讲怎么在MATLAB里把重构谱扫出来,这是所有参数整定前的必修课。
% 重复控制重构谱扫描脚本 Ts = 1e-3; % 位置环周期 1ms T_task = 8.0; % 任务周期 8s N = round(T_task/Ts); % 延迟拍数 8000 kr = 0.28; % 重复控制增益 m = 1; % 相位超前拍数 % 被控对象:用二阶加延迟近似位置环等效模型 wn = 2*pi*12; % 自然频率 12Hz zeta = 0.9; % 阻尼比 Gs = tf(wn^2, [1, 2*zeta*wn, wn^2]); Gz = c2d(Gs, Ts, 'zoh'); % 原反馈控制器(简化PD+积分形式) Cz = pid(2.5, 6.0, 0.002, Ts); S = feedback(1, Gz*Cz); Tw = Gz*S; % 补灵敏度,插入点到输出 % 重构谱 w = logspace(-2, log10(pi/Ts), 5000); Q = 0.95; % 先用常数Q z = exp(1j*w*Ts); num = kr * (z.^m) .* squeeze(freqresp(Tw, w)); reg = abs(Q - num); % 重构谱模值 semilogx(w/(2*pi), reg, 'LineWidth', 1.6); hold on; yline(1, 'r--', 'LineWidth', 1.2); yline(0.85, 'g--', 'LineWidth', 1.2); xlabel('Frequency (Hz)'); ylabel('|Regeneration Spectrum|'); grid on; legend('|Q - k_r T_w z^m|', '稳定边界', '推荐余量');跑完这张图,如果曲线在10 Hz以内都低于0.85,10 Hz以上因为Q的衰减也压下去了,那这组参数就有戏。重点是看10 Hz到100 Hz这一段有没有鼓包——这一段最容易因为相位补偿不到位而顶起来。
Simulink里搭结构更直观:原反馈环外面并一路,由"Delay Line"(用Integer Delay,长度N)、"Q Filter"(离散低通)、"Phase Comp"(Integer Delay,长度m,注意方向)和增益组成。注意Simulink的Integer Delay在反馈回路上需要设置初值为0,同时勾选"Prevent algebraic loop"。
4.2 Python验证:把算法先跑通再上机
上机前我一般用Python先写一版仿真,方便快速改参数。下面这个类是最小可用实现:
import numpy as np class RepetitiveController: """插入式重复控制器(常数Q版本)""" def __init__(self, N, kr=0.28, q=0.95, q_alpha=None): self.N = int(N) self.kr = float(kr) self.q = float(q) # q_alpha 为 None 时用常数Q,否则用一阶低通 self.q_alpha = q_alpha self.buf = np.zeros(self.N, dtype=np.float32) self.idx = 0 self.lp_state = 0.0 def reset(self): """任务周期不匹配或重新计时时调用,清空延迟链""" self.buf[:] = 0.0 self.idx = 0 self.lp_state = 0.0 def _q_filter(self, x): if self.q_alpha is None: return self.q * x a = self.q_alpha self.lp_state = a * self.lp_state + (1.0 - a) * x return self.lp_state def step(self, e): """输入当前周期位置误差,输出重复控制补偿量""" delayed = self.buf[self.idx] # w[n-N] qd = self._q_filter(delayed) # Q(z) * w[n-N] self.buf[self.idx] = e + qd # w[n] = e[n] + Q*w[n-N] self.idx += 1 if self.idx >= self.N: self.idx = 0 return self.kr * qd # u_rc[n] = k_r * Q * w[n-N] def soft_start(self, cycle, cycles_to_full=5): """前几个周期线性爬升增益,抑制启动冲击""" scale = min(1.0, (cycle + 1) / float(cycles_to_full)) return self.kr * scale这段代码里有几个点值得单独说。
buf[self.idx] = e + qd这一句是整个算法的灵魂,它对应的是w[n]=e[n]+Q·w[n-N]。注意写入的是e加了反馈量,不是直接写e,这是正反馈内模的体现。很多初学者在这里直接写buf[idx]=e,结果变成了一个纯粹的延迟器,完全没有学习效果,然后疑惑为什么误差不收敛。我第一次写也错了这个地方,调试了两小时才发现。
返回值用的是kr * qd而不是kr * w,因为输出应该是延迟N拍之后再滤波的量。这个符号方向要和你在控制量上的叠加方式匹配,如果发现系统一上来就发散,第一件事就是把返回值的符号取反试试。
reset()方法很重要。当任务周期有变化、或者设备重新开始一个作业批次时,必须清空延迟链,否则上一批次的残留信息会污染这一批次的开头。
4.3 嵌入式C实现:一个环形缓冲区搞定
上到DSP或者伺服驱动器里,核心就是一个环形缓冲加状态更新,代码量极小:
/* 重复控制器 - 嵌入式实现 */ #define RC_N 8000 /* 延迟拍数:8s / 1ms */ #define RC_KR 0.28f /* 重复控制增益 */ #define RC_Q_ALPHA 0.20f /* 一阶低通系数 */ static float rc_buf[RC_N]; static uint32_t rc_idx = 0; static float rc_lp_state = 0.0f; static float rc_gain_scale = 0.0f; /* soft-start 用 */ void rc_init(void) { uint32_t i; for (i = 0; i < RC_N; i++) { rc_buf[i] = 0.0f; } rc_idx = 0; rc_lp_state = 0.0f; rc_gain_scale = 0.0f; } /* 每个位置环周期调用一次,e为位置误差,返回补偿量 */ float rc_step(float e) { float delayed = rc_buf[rc_idx]; float qd, out; /* Q(z): 一阶低通,抑制高频无穷大增益 */ rc_lp_state = RC_Q_ALPHA * rc_lp_state + (1.0f - RC_Q_ALPHA) * delayed; qd = rc_lp_state; /* 内模更新:w[n] = e[n] + Q * w[n-N] */ rc_buf[rc_idx] = e + qd; rc_idx++; if (rc_idx >= RC_N) { rc_idx = 0; } out = RC_KR * rc_gain_scale * qd; /* 保护:输出限幅,防止异常情况下冲击执行机构 */ if (out > 5.0f) out = 5.0f; if (out < -5.0f) out = -5.0f; return out; } /* 每完成一个任务周期调用一次,实现软启动 */ void rc_cycle_tick(void) { if (rc_gain_scale < 1.0f) { rc_gain_scale += 0.2f; /* 5个周期爬到满增益 */ if (rc_gain_scale > 1.0f) { rc_gain_scale = 1.0f; } } }几点说明。rc_buf占32 KB,如果是C2000系列那种RAM紧张的芯片,可以改用Q15定点,或者把N减小(比如把RC放到速度环或者更慢的环上,Ts增大,N自然变小)。rc_gain_scale实现的是软启动,这个后面第5章还会展开讲,它能显著降低第一个周期的冲击。输出限幅是必须的,因为延迟链初值为零的瞬间,或者任务周期突然变化时,可能有异常大的补偿量冒出来。
关于浮点还是定点:如果目标芯片有FPU,直接用float最省事,数值范围也宽;没有FPU的话用Q15,注意累加时要做饱和处理,我在一个老平台上用Q15时,缓冲区溢出导致补偿量绕回负值,出现了非常诡异的方向性偏差,查了很久才定位到饱和问题。
4.4 分数延迟与内存占用:两个绕不开的工程细节
前面提过N必须是整数拍,如果T/Ts不是整数怎么办?两种处理方式。
第一种是拉格朗日插值。把延迟拆成整数部分加一个0到1之间的分数,分数部分用相邻几个样本做多项式插值。三阶拉格朗日插值在低频段相位精度很好,但在接近奈奎斯特频率时相位误差会明显增大,所以用了分数延迟之后,Q的截止频率要相应下调,重构谱要重新扫一遍。
第二种是一阶全通滤波器。用一个一阶全通环节逼近分数延迟,它的幅频特性恒为1,只调相位。优点是计算量小,缺点是相位逼近只在低频有效,而且参数对采样周期敏感。
我个人的做法是优先想办法让T/Ts变成整数,比如微调位置环周期,或者把RC放到慢一点的环上。实在调不了,用三阶拉格朗日,补偿精度一般够用。
内存方面再啰嗦一句。除了缓冲区本身,还要考虑缓存命中率的问题。8 KB的缓冲区在大部分DSP的L1缓存里放不下,每次读写都是访存操作,这对1 ms周期的任务影响不大,但如果周期压到100 μs就要算一下了。我们做过测算,N=8000的float缓冲区,在200 MHz的C2000上单次读写大约消耗1到2 μs,占100 μs周期的1%到2%,可以接受。真正要担心的是把这个环塞进125 μs甚至更快的任务里,那时候要考虑用DMA搬运或者把缓冲区做成双缓冲结构。
5. 常见问题与排查技巧实录
5.1 启动瞬间抖一下、或者要跑十几圈才收敛
这是最常见的两个抱怨,成因不同,处理方式也不同。
启动抖动的根源是延迟链初值为零。第一个周期里,缓冲区全是0,RC输出为0,所以第一圈基本没啥效果;但到了第N拍之后,缓冲区开始有内容了,而此时这些内容还是"半成型"的,可能包含较大的误差值,突然被乘以k_r回灌进控制量,就会有一次冲击。处理办法是软启动:前5到8个周期把k_r从0线性爬升到目标值,让延迟链里的内容慢慢积累成型。上面C代码里的rc_gain_scale就是这个用途,实测能把这个冲击压掉七成以上。
收敛慢通常有两个原因。一是k_r给得太保守,为了安全起见设了0.05,那确实要跑很久。二是任务周期的对称性没利用上,本来可以用奇次谐波结构(N/2)但没有用。还有一个容易被忽略的原因:误差信号里混入了非周期成分(比如随机的摩擦力波动、外部扰动),RC会把它们也往延迟链里塞,导致学习方向被污染。这时候应该在误差通道前面加一个梳状滤波器或者带通滤波器,只让周期成分进去。
实操心得:如果任务周期短(比如0.5 s以内),我一般直接上奇次谐波结构,收敛速度快一倍,代价很小。
5.2 高频啸叫:怎么快速定位到具体频段
啸叫是RC调试中最典型的故障,听到的时候千万别硬扛,立刻把k_r降到一半再排查。
定位方法很直接:**录音或者用加速度计测振,做一次频谱分析,找到振荡的主频。**然后拿这个频率去和重构谱对照。有三种常见情况:
- 主频接近Q截止频率。说明Q的过渡带不够陡,或者k_r在这个频段余量不足。处理办法是降低Q截止,或者把Q换成更高阶的滤波器。
- 主频在Q截止频率以上。这说明问题不在RC本身,而是原来的反馈环被RC的相位补偿带偏了,或者机械模态被激发。检查一下m是不是取大了。我遇到过一次,m从1改到2之后500 Hz出现啸叫,改回1就干净了。
- 主频等于任务基频或低次谐波。这说明RC的增益太大,整个回路在基频附近产生了极限环。降k_r。
判断"是不是RC引起的"有个简单办法:把k_r清零,看啸叫是否消失。消失就是RC,不消失就是原系统或者机械问题。
5.3 周期对不上:误差降了一半就下不去了
这个坑真的很常见,而且不容易发现,因为现象很"温和"——误差确实下降了,但下降到某个水平就卡住,怎么调参数都没用。
原因就是实际任务周期和设定的N不匹配。常见来源有:PLC发出的启动信号有抖动、传送带速度有波动、工件位置有偏差导致每次循环的实际路径长度不同。这种偏差可能只有1%到2%,但累积到N=8000拍上就是80到160拍的误差,相位完全错位,抑制效果大打折扣。
处理办法是让RC自己找周期。具体做法是用一个同步触发信号(比如工件到位的光电开关、主轴的编码器零位脉冲)去复位延迟链指针,而不是单纯依赖固定的N值。如果拿不到硬件触发,可以用软件做周期估计:检测误差信号的自相关峰值位置,或者用一个简单的锁相环跟踪基频。
我在一个纸箱码垛项目里就遇到过这个问题。原来的做法是用PLC的定时器触发,定时器精度是10 ms,周期8 s意味着0.125%的误差,看起来很小,但实测误差只能降到原来的60%。后来改成用光电开关的信号去复位指针,误差直接降到20%以下。硬件触发永远比软件估计可靠,能用就用。
5.4 问题速查表
把上面这些整理成一张表,方便对照。
| 现象 | 可能原因 | 排查顺序 | 处理方法 |
|---|---|---|---|
| 完全没效果 | N或m算错;符号写反 | 先查N=T/Ts;再把输出符号取反试 | 修正延迟拍数;确认正反馈方向 |
| 第一周期冲击大 | 延迟链初值为零后突变 | 看前两个周期的误差波形 | 加soft-start;预填缓冲区 |
| 收敛极慢 | k_r过小;存在非周期扰动 | 检查k_r取值;看误差的周期一致性 | 提高k_r;误差通道加带通 |
| 高频啸叫 | Q截止过高;m取大;k_r过大 | 先清零k_r确认因果 | 降Q截止;m回调;降k_r |
| 降一半卡住 | 任务周期与N不匹配 | 对比触发信号与实际周期 | 用硬件触发复位指针;周期估计 |
| 某些频点无改善 | 该频段重构谱超过1 | 扫重构谱找鼓包 | 调整m或降Q截止 |
| 负载变化后失效 | 模型相位随工况变化 | 满载/空载分别扫频 | 降k_r留余量;Q截止下压 |
| 输出缓慢漂移 | 积分项与RC打架 | 观察低频段的响应 | 限制RC在低频段的贡献 |
6. 重复控制的扩展方向与个人实践体会
6.1 几条值得尝试的扩展路线
RC本身的结构很简洁,但它的可扩展性相当好,我试过几条路子,简单说说。
和迭代学习控制混用。前几批用ILC做轮廓标定,把学到的补偿量作为RC延迟链的初始内容塞进去,这样RC一上来就是"有经验"的状态,省掉了收敛过程。这个思路在换型频繁的产线上特别有用,新工件的第一批就能跑出稳定批次的精度。
在移动机器人巡检路径上的应用。移动机器人绕固定路线巡检,一圈的时间基本固定,横向偏差里有一大块是周期性的——比如某段地面的起伏、某个转弯处的转向滞后。把RC架在横向控制器的输出上,周期取一圈的时间(几十秒),N值会很大,需要放在慢环上,周期取外环的位置环周期。实测下来,重复路段的横向偏差能压掉一半以上。这里的坑是周期会随电池电量、地面摩擦变化而漂移,所以必须配合周期估计。
在恒力打磨、恒力矩应用里做力补偿。打磨工具头的力波动如果有周期性(比如工件本身的轮廓周期、砂带接头的周期),也可以用RC做力环的周期补偿。但要注意力环的带宽一般比位置环低,Q截止要相应下压,而且力信号噪声大,误差通道必须加重滤波,否则RC会把噪声学进去。
多周期并联。前面说过,能用其他方法解决就不要用这个。如果非要用,我建议先把主周期那条链调稳,跑一周确认没问题,再加第二条链,且第二条链的k_r先取主链的一半,慢慢往上加。
6.2 我在实际项目里踩过的几个具体坑
说几个具体的,可能比抽象的原则更有用。
符号方向搞反。这个太常见了。数学上推导出来的形式是正反馈内模,但具体到你叠加控制量的位置,可能整体需要取负号。判断方法很简单:跑一个周期,如果误差幅度明显变大甚至发散,立刻停机取反。我一般会在第一次上机时把k_r设到0.02这种极小值,先看趋势,确认方向对了再往上加。
缓冲区索引溢出。用uint32_t做索引时,rc_idx++之后如果不及时回绕,会一直涨到溢出,回绕到0的位置就乱了。正确写法是先加后判断,或者用rc_idx = (rc_idx + 1) % RC_N。后者在编译器优化下通常也能变成条件判断,性能差异不大,可读性更好。
采样周期改了忘了改N。这个坑最隐蔽。调试过程中如果改了位置环周期(比如从1 ms改成0.5 ms),N必须同步乘以2,否则周期对不上,表现就是"降一点然后卡住"。我现在的习惯是在代码里用宏把T_task和Ts都定义出来,N由编译器算,避免手算出错。
忘记处理任务结束时的收尾。设备停机、急停、手动干预之后,延迟链里还是旧的误差数据,下次启动如果不reset,开头几拍会用错误的数据去补偿。现在的做法是在任务开始前、以及任何异常恢复之后都调用一次rc_init()。
把RC当成万能药。有一段时间我总想着用RC把所有周期误差都吃掉,结果发现有些误差的根源根本不是控制器能解决的——比如减速机背隙是机械间隙,你在控制量上补多少,它都会先走完这段空程再响应;比如结构共振,你在某个频点加大补偿,反而把模态激得更厉害。**正确的态度是:先把机械和传动的问题解决掉,RC是锦上添花,不是雪中送炭。**我见过最夸张的案例,一台设备因为联轴器松动导致周期性误差,折腾了两周的控制参数,最后拧紧一颗螺丝全解决了。
调参日志比调参本身重要。这是我做了几年之后才养成的习惯:每次改参数,记录下k_r、Q截止、m、任务周期、以及误差的RMS和峰值,画成趋势图。很多时候你以为在优化,其实是在原地打转,回头看日志才发现三小时前那组参数已经是最优的了。
跑到最后你会发现,重复控制最迷人的地方不在那行公式,而在于它把"记忆"这件事引入了一个本来只会"即时反应"的控制器。延迟链里存的是上一周期的误差,本质上是让系统多了一个时间维度上的自我参照。这个视角打开之后,很多控制问题会变得不一样——你会发现有些看起来是"响应慢"的问题,其实是"没有记住上次发生了什么";有些看起来是"模型不准"的问题,其实是"没有把周期性成分和随机成分分开处理"。我现在拿到一个新工位,第一件事是先跑十几遍同样的轨迹,把误差曲线叠在一起看。如果它们能重合,那这个工位就值得上重复控制,而且大概率能把精度往上抬一截。