
做机器人动力学与控制这些年我踩过最多的坑不是模型有多复杂而是同一段轨迹跑一万遍误差看上去还是那一副样子。六轴工业机器人在码垛线上重复同一个搬运循环单次轨迹跟踪误差波形几乎一模一样协作机器人在打磨工位上走同一条环形路径每一圈的力波动都能对上号移动机器人巡检一圈某些路段的横向偏差总比别处大。这类现象背后其实是同一件事误差里有一大块是周期性的周期等于任务周期。只要任务在重复重复控制Repetitive Control就有用武之地。这篇笔记想把重复控制从听起来很美拉到能写进控制器里跑起来把内模原理的来龙去脉、延迟链长度怎么算、Q滤波器和相位补偿怎么调、嵌入式里环形缓冲区怎么写、跑起来不收敛该怎么查一条条捋清楚。适合已经写过位置环PID、想再往上抬一截轨迹精度的朋友也适合刚接触学习类控制、被1/(1-z^-N)绕晕过的同学。1. 为什么重复控制能干PID干不了的活1.1 从一条码垛循环说起误差为什么是复印出来的先看一个真实场景。某条码垛线单次循环周期T8 s机器人从取件点抓箱、抬升、横移、下放回到原点。位置环用1 kHz的伺服周期级联PID的位置环带宽大概15 Hz。跑下来你会发现末端在横移换向那一段的跟踪误差最大能到0.6 mm而且每一箱的误差曲线形状高度一致差别只在几十微米量级。这个高度一致不是巧合。误差的成因基本固定换向时的减速机背隙、关节惯量耦合导致的动态滞后、轨迹规划里加速度不连续带来的柔性冲击、重力补偿在姿态变化区间内的偏差。这些扰动都与关节位置、速度、姿态时间函数绑定而关节运动又是周期性的。换句话说扰动d(t)满足d(tT)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_{k1}(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)其中NT/Ts是延迟拍数。这个环节的频率响应很关键。在ω2πk/Tk0,1,2,...这些频率上e^(-jωT)1分母为0增益趋于无穷。把这样一个无穷大增益的环节并联到误差通道上闭环灵敏度函数S1/(1PC)在这些谐波频率上被强行压到零——理论上任务周期所有整数倍谐波处的稳态误差都能被消掉一个都不漏。这就是RC的诱人之处它不是一个频点一个频点地去补而是一口气把整个谐波栅栏全部覆盖。相比之下如果你想用多个陷波器或者谐振控制器去覆盖前20次谐波需要20个二阶环节参数整定到崩溃。RC只需要一个延迟链。当然理论上的无穷大增益在工程上必须打折扣。原因很直接**你在建模时假设的对象模型永远不精确高频段更是离谱。**在那些地方还保留无穷大增益等于给一个相位已经不清不楚的回路灌能量结果就是啸叫或者极限环。所以实际实现里一定要引入Q滤波器和相位补偿把无穷大增益削平并把相位拉回安全区。这两件事是RC从教科书走到机柜里的关键下一章详细拆。2. 重复控制器的结构拆解与参数设计2.1 插入式结构为什么不做成独立控制器RC有三代结构演化直接型、插入式plug-in、以及各种改进型。工程上用得最多的是插入式因为它不动你原有的反馈环。插入式的做法是保留原来的级联PID或状态反馈控制器C(z)在控制量上再并一路u_rc输入取位置误差e输出加到C的输出上。这样原有闭环的稳定性和动态响应基本不变RC只是在这个基础上做周期误差的精修。换句话说如果RC调砸了你顶多是性能不升而不是整机失控——这一点对上线很关键。插入式结构的灵敏度函数可以推一下过程不复杂。设原闭环灵敏度S1/(1PC)从插入点到输出的传递补灵敏度为T_wPC/(1PC)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的形式表达式优点缺点适用场景常数Qqq∈[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^mm是超前拍数。m怎么算别用试凑法硬试有办法。步骤是画出T_w(z)在0到奈奎斯特频率的伯德图。找出你打算抑制的谐波频段一般取基频的3到20次谐波记这个频段为中心频率ω_c。读出T_w在ω_c处的相位滞后φ度。用公式 m ≈ (φ/360°) / (ω_c·Ts) 算出拍数四舍五入取整。举个实际数字。T_w在ω_c/2π150 Hz处滞后38°Ts1 ms那么ω_c·Ts2π×150×0.0010.942 radφ38°0.663 radm0.663/0.942≈0.70取整m1。也就是插入一拍超前。这里有个细节值得说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的换算与插入点的选择NT/Ts看起来是最简单的一步实际上藏着几个容易忽略的坑。第一个坑是必须取整数。如果T8 s、Ts1 msN8000完美。但如果Ts不是整除关系比如位置环周期是0.3 msT8 sN26666.67那就有0.67拍的误差。0.67拍听起来很小实际上相当于在回路里引入了一个随频率变化的相位误差在接近奈奎斯特频率时这个误差能到几十度。处理办法有两种一是把采样周期调到能整除二是做分数延迟近似拉格朗日插值或一阶全通后者的相位特性在高频段会退化需要重新扫频确认。第二个坑是Ts到底取哪个环的周期。这取决于你把RC插在哪一层插入点典型周期可抑制谐波上限模型可信度说明位置环输出1 ms数百Hz高主流选择模型稳定落地容易速度环输出125-250 μs1 kHz级中需考虑速度环自身动态建模工作量大电流环输出60-125 μs数千Hz低带宽极高但对象随温度、磁饱和变化慎用我自己的选择是默认插在位置环。原因很朴素位置环的对象模型从力矩指令到关节位置在几十赫兹范围内用二阶加延迟就能拟合得不错而电流环虽然带宽高但电感参数随电流变化、死区补偿非线性、母线电压波动模型一会儿一个样RC这种对模型相位敏感的方法放进去就是在刀尖上跳舞。第三个坑是N的内存换算。N8000的float32缓冲区是32 KB双精度64 KB对现在的控制器不算什么。但如果你的任务是长周期低速巡检比如移动机器人绕仓库一圈90 s电流环周期100 μsN900000float32就是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_r0.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 HzQ截止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.8510 Hz以上因为Q的衰减也压下去了那这组参数就有戏。重点是看10 Hz到100 Hz这一段有没有鼓包——这一段最容易因为相位补偿不到位而顶起来。Simulink里搭结构更直观原反馈环外面并一路由Delay Line用Integer Delay长度N、Q Filter离散低通、Phase CompInteger Delay长度m注意方向和增益组成。注意Simulink的Integer Delay在反馈回路上需要设置初值为0同时勾选Prevent algebraic loop。4.2 Python验证把算法先跑通再上机上机前我一般用Python先写一版仿真方便快速改参数。下面这个类是最小可用实现import numpy as np class RepetitiveController: 插入式重复控制器常数Q版本 def __init__(self, N, kr0.28, q0.95, q_alphaNone): 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, dtypenp.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_full5): 前几个周期线性爬升增益抑制启动冲击 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就要算一下了。我们做过测算N8000的float缓冲区在200 MHz的C2000上单次读写大约消耗1到2 μs占100 μs周期的1%到2%可以接受。真正要担心的是把这个环塞进125 μs甚至更快的任务里那时候要考虑用DMA搬运或者把缓冲区做成双缓冲结构。5. 常见问题与排查技巧实录5.1 启动瞬间抖一下、或者要跑十几圈才收敛这是最常见的两个抱怨成因不同处理方式也不同。启动抖动的根源是延迟链初值为零。第一个周期里缓冲区全是0RC输出为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%但累积到N8000拍上就是80到160拍的误差相位完全错位抑制效果大打折扣。处理办法是让RC自己找周期。具体做法是用一个同步触发信号比如工件到位的光电开关、主轴的编码器零位脉冲去复位延迟链指针而不是单纯依赖固定的N值。如果拿不到硬件触发可以用软件做周期估计检测误差信号的自相关峰值位置或者用一个简单的锁相环跟踪基频。我在一个纸箱码垛项目里就遇到过这个问题。原来的做法是用PLC的定时器触发定时器精度是10 ms周期8 s意味着0.125%的误差看起来很小但实测误差只能降到原来的60%。后来改成用光电开关的信号去复位指针误差直接降到20%以下。硬件触发永远比软件估计可靠能用就用。5.4 问题速查表把上面这些整理成一张表方便对照。现象可能原因排查顺序处理方法完全没效果N或m算错符号写反先查NT/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 msN必须同步乘以2否则周期对不上表现就是降一点然后卡住。我现在的习惯是在代码里用宏把T_task和Ts都定义出来N由编译器算避免手算出错。忘记处理任务结束时的收尾。设备停机、急停、手动干预之后延迟链里还是旧的误差数据下次启动如果不reset开头几拍会用错误的数据去补偿。现在的做法是在任务开始前、以及任何异常恢复之后都调用一次rc_init()。把RC当成万能药。有一段时间我总想着用RC把所有周期误差都吃掉结果发现有些误差的根源根本不是控制器能解决的——比如减速机背隙是机械间隙你在控制量上补多少它都会先走完这段空程再响应比如结构共振你在某个频点加大补偿反而把模态激得更厉害。**正确的态度是先把机械和传动的问题解决掉RC是锦上添花不是雪中送炭。**我见过最夸张的案例一台设备因为联轴器松动导致周期性误差折腾了两周的控制参数最后拧紧一颗螺丝全解决了。调参日志比调参本身重要。这是我做了几年之后才养成的习惯每次改参数记录下k_r、Q截止、m、任务周期、以及误差的RMS和峰值画成趋势图。很多时候你以为在优化其实是在原地打转回头看日志才发现三小时前那组参数已经是最优的了。跑到最后你会发现重复控制最迷人的地方不在那行公式而在于它把记忆这件事引入了一个本来只会即时反应的控制器。延迟链里存的是上一周期的误差本质上是让系统多了一个时间维度上的自我参照。这个视角打开之后很多控制问题会变得不一样——你会发现有些看起来是响应慢的问题其实是没有记住上次发生了什么有些看起来是模型不准的问题其实是没有把周期性成分和随机成分分开处理。我现在拿到一个新工位第一件事是先跑十几遍同样的轨迹把误差曲线叠在一起看。如果它们能重合那这个工位就值得上重复控制而且大概率能把精度往上抬一截。