简介面向控制理论学习者、自动化专业学生与控制工程师ILC_Simulink压缩包提供一套基于Simulink的迭代学习控制ILC仿真模型用于理解并验证周期性任务中的无静差跟踪思路。压缩包共14个文件以slx/slxc模型、Stateflow相关文件、m脚本、mat数据及xml配置为主整体约66KB可直接在MATLAB/Simulink环境中打开研读。已有3981人学习下载说明其在ILC入门与课程实践中有一定参考价值。模型围绕传递函数被控对象展开包含误差计算、增益校正、饱和限幅与迭代更新等环节读者可通过多周期仿真观察输出曲线调整学习增益和停止条件进而复现无静差跟踪过程并用于论文复现、算法对比或工程原型验证。1. 一轮一轮地纠偏Simulink ILC 到底补的是哪块短板做重复轨迹跟踪的人大概率都遇到过一个别扭现象同一段 S 形曲线伺服每跑一遍都在同一个拐角处滞后几十毫秒PID 参数调到振荡边缘也就压到这儿了。原因不复杂——常规反馈控制器只为“当前这一遍”的误差负责跑完就把这一遍的经验丢干净了。迭代学习控制Iterative Learning ControlILC做的事很朴素把上一轮每个采样点上的误差记下来折算成前馈修正量叠到下一轮的控制输入里让同一段轨迹跑第 10 遍时明显比第 1 遍更贴合参考。Simulink ILC 这套组合在工程上出现频率高是因为 Simulink 天然按“一次仿真等于一轮实验”来组织数据把迭代轴和时间轴拆开处理非常顺手被控对象、参考轨迹、采样周期全都是可视化的改一个增益重跑一遍比在纯脚本里手搓离散递推直观得多。这份ILC_Simulink.rar面向的就是这个场景重复性轨迹跟踪的学习律搭建、多轮迭代驱动、收敛性验证。适合已经会用 Simulink 搭基础闭环、但一上 ILC 就发现“第二轮和第一轮一模一样”“跑到第八轮突然发散”的人。2. ILC 的数学骨架更新律、相对阶和收敛条件怎么对上号在动手拖模块之前先把三件事想清楚更新律长什么样、它为什么能收敛、增益该从哪个数量级起调。这三件事没想明白模型搭得再漂亮也只是在调玄学。2.1 从误差序列到控制序列更新律到底在算什么设定很干净系统在固定时间区间[0, T]上重复运行第 k 轮的控制输入记作u_k(t)输出y_k(t)参考轨迹y_d(t)每轮不变误差e_k(t) y_d(t) - y_k(t)。最基础的 P 型更新律写成u_{k1}(t) u_k(t) Γ · e_k(t)Γ是学习增益量纲上是“误差到控制”的转换系数。这个式子里没有当前轮的反馈全是前一轮的记忆所以 ILC 天然是前馈结构反馈回路负责鲁棒性和抗扰ILC 支路负责把重复性的、每轮都一模一样的偏差一点点磨掉。两者是叠加关系不是替代关系——把 PID 拆了只留 ILC第一轮就会被初值偏差带跑偏。对线性时不变对象G把更新律代进去能推出误差的递推关系e_{k1} (I - G·Γ) · e_k收敛的充要条件是这个算子的谱半径小于 1SISO 情况下退化成“在每一个频点上|1 - G(jω)Γ(jω)| 1”。这个式子看着简单但它解释了两个最常见的翻车一是G·Γ在高频段衰减得不够快1 - GΓ的模反而趋近甚至超过 1高频噪声被逐轮放大二是对象本身带相位滞后采样延迟、执行器滞后、滤波器某个频段上GΓ的相角转到 180 度附近1 - GΓ直接变成大于 1 的实数。所以增益不是越大越好它有一个非常明确的上界。从工程角度看I - GΓ这个形式还告诉你一件事ILC 的收敛速度是逐频点不同的。低频段G增益大1 - GΓ容易被压得很小收敛快高频段G衰减1 - GΓ接近 1几乎不收敛。这就导致一个典型现象——前几轮误差曲线下降很快然后卡在一个“地板”上不动了。这个地板不是算法不收敛是高频段本来就没被学习到得靠 Q 滤波器或者相位补偿来处理。2.2 三种常用学习律的适用边界原版 P 型不是万能的实际项目里能见到的学习律大体是下面这几类选型的关键在于被控对象的相对阶和相位特性。学习律更新式相对阶匹配典型场景主要风险P 型u_{k1}(i) u_k(i) Γ·e_k(i)相对阶 1且延迟可控采样快、对象干净的伺服轴高频段增益被放大D 型u_{k1}(i) u_k(i) Γ·(e_k(i) - e_k(i-1))/Ts相对阶 12对象含积分、低频漂移明显差分放大测量噪声PD 型上面两项加权相加通用拿不准时的默认选择两个增益耦合调参周期长带遗忘因子u_{k1} (1-α)u_k Γ·e_k通用存在非重复扰动、初值每轮不同稳态留残差收敛不到零高阶用前 m 轮的u加权组合非最小相位、滞后大柔性机构、大惯量传动内存翻倍调参维度爆炸P 型之所以经常“看着能用但收不干净”是因为它的高频段几乎不衰减。数字实现里通常还要额外塞一个 Q 滤波器零相位低通把高频段强行压下去代价是牺牲一点稳态精度。D 型在对象带积分环节时表现更好因为微分把低频的大增益抵消掉了但差分对噪声极其敏感采样周期一大就直接废掉。PD 型是最稳的折中代价是多一个要调的参数。带遗忘因子的写法值得单独提一句。它的作用是让 ILC 不要死记硬背每一轮的细节每轮都把历史控制量打个折u_{k1}(i) (1 - α)·u_k(i) Γ·e_k(i)α通常取 0.05 到 0.2。好处是非重复性扰动不会被无限累积坏处是稳态误差收敛不到零会停在一个与α成正比的残差上。要不要用取决于你的场景是“重复性扰动占主导”还是“每轮都有随机干扰”。2.3 学习增益与采样周期的匹配增益初值的取法有个经验公式先测出对象的直流增益g给一个单位阶跃看稳态输出然后取Γ 0.1 / g起步。这个初值通常偏保守收敛慢但不会炸跑十几轮看趋势再往上加。如果第 5 轮左右误差范数开始抬头立刻减半别硬撑。采样周期和每轮采样点数这两个参数经常被忽视但它们决定了 ILC 能不能对齐。ILC 的前提是“第 k 轮的第 i 个采样点和第 k1 轮的第 i 个采样点对应同一个时刻”采样点数一旦每轮不同整个记忆就错位了。参数含义典型取值调整方向Γ学习增益0.1 / g起步收敛慢就加抬头就减半Ts采样周期≤ 上升时间的 1/50越小越准内存线性增长N每轮采样点数T_end / Ts 1必须固定不能随轮次变Q低通截止频率0.2 ~ 0.4 × 1/(2Ts)控制量发抖就往下压α遗忘因子0.05 ~ 0.2抗非重复扰动就加大Ts的选取还有一层隐含约束采样周期必须显著小于对象的相位滞后时间常数否则1 - GΓ的相角在高频段提前转过去增益再小也压不住。常见做法是让Ts至少比执行器响应时间小一个数量级做不到就老老实实加 Q 滤波器。提示先用一组保守参数小增益、强滤波把整条链路跑通、把曲线画出来再逐项放开。反过来做九成时间都花在猜“到底是模型搭错了还是增益太大了”。3. 在 Simulink 里搭一条 ILC 回路两条实现路线和它们的取舍搭 ILC 模型有两条主流路线差别在于“迭代的记忆存在哪里”。路线 A 把记忆放在模型内部的 MATLAB Function 块里用persistent变量缓存路线 B 把记忆放在基础工作区用脚本在外层循环反复调用sim()。两条路线各有明确的适用场景混着用就是自找麻烦。3.1 模型骨架与信号流向不管走哪条路线模型的骨架是一样的。一条标准的前馈加反馈结构参考轨迹Repeating Sequence Stair或Signal Editor输出y_d求和点y_d - y得到误差e反馈控制器PID Controller输出u_fbILC 前馈支路输出u_ilc求和点u u_fb u_ilc送进被控对象被控对象Discrete State-Space块A/B/C/D四个矩阵直接填记录误差e、控制量u、输出y都勾上 “Log signal data”被控对象建议用Discrete State-Space而不是连续传递函数块。原因是离散状态空间每步的更新顺序明确x(k1) Ax(k) Bu(k)y(k) Cx(k) Du(k)只要D是零矩阵y(k)就只依赖u(k-1)从结构上就不会出现代数环。换成连续的Transfer Fcn块求解器一改就可能冒出代数环报错。求解器选Fixed-step、类型discrete (no continuous states)固定步长填Ts。这一条是硬要求不是建议——变步长求解器会让每轮输出的采样点数抖动ILC 的记忆直接错位。误差信号进 ILC 块之前需要一个采样点序号。用Counter Limited上限设N-1输出 0 到N-1加 1 之后就是 1 到N。计数器在每个采样步自增模型重置时归零正好和 ILC 的索引对上。3.2 路线 AMATLAB Function 块加 persistent 缓存这条路线的优点是模型自包含点一次运行就能看到多轮效果配合Fast Restart或者手动重跑适合快速试参数。核心代码是一个 MATLAB Function 块function u ilc_p_type(e, idx, N, Gamma, u0, reset) %#codegen % P 型迭代学习律u_k(i) u_{k-1}(i) Gamma * e_k(i) % e : 当前采样点的跟踪误差标量 % idx : 当前采样点序号 1..N来自 Counter Limited 加 1 % N : 每轮固定采样点数必须与计数器上限一致 % Gamma : 学习增益标量 % u0 : 基线前馈可以接 0也可以接 PD 的输出 % reset : 大于 0.5 时清空历史缓存换轨迹或重跑前必须拉高一次 persistent Uhist if isempty(Uhist) Uhist zeros(N, 1); end if reset 0.5 Uhist zeros(N, 1); end i min(max(round(idx), 1), N); % 索引保护防止越界 u u0 Uhist(i) Gamma * e; % 前馈 本轮修正 Uhist(i) Uhist(i) Gamma * e; % 只缓存学习出来的那一部分 end几个关键点值得说明。persistent Uhist在两次仿真之间不会自动清零这是路线 A 最大的坑也是后面避坑章节要重点讲的东西reset端口就是给这个用的换参考轨迹或者改增益重跑之前必须拉高一次。u0端口单独留出来是为了让反馈控制器和 ILC 前馈解耦——u0接 PID 输出ILC 只负责叠加学习量调参的时候可以单独把Gamma置零验证反馈回路是否正常。i的钳位看着多余实际上很有用。Counter Limited在模型重置那一步可能输出一个越界的值不做保护就是数组越界报错而且报错信息指向 MATLAB Function 块内部排查起来相当费劲。模块的采样时间设成Ts和模型基采样时间一致。如果这个块被放在触发子系统里触发周期必须严格等于Ts否则idx和实际时刻对不上。路线 A 的局限也很明显persistent变量在sim()多次调用之间是否保留取决于有没有开Fast Restart、有没有重新编译模型行为不够可控。而且它只能缓存控制量误差序列拿不出来画不了收敛曲线。所以路线 A 适合“坐在电脑前手动点运行试参数”不适合做批量实验。3.3 路线 B脚本外循环加 logsout 回传推荐需要画收敛曲线、需要跑几十轮做对比、需要把不同增益的结果放进同一张图就得走路线 B。模型里删掉 ILC 的 MATLAB Function 块换成一个From Workspace块读Uff误差信号照常勾选 log剩下的全在脚本里做。%% ILC 多轮迭代驱动脚本 model ilc_model; Ts 1e-3; T_end 2.0; N round(T_end / Ts) 1; % 每轮固定采样点数 Niter 30; Gamma 0.35; alpha 0; % 遗忘因子0 表示不用 Uff zeros(N, 1); % 第 0 轮前馈 Eall zeros(N, Niter); Uall zeros(N, Niter); t_grid (0:N-1) * Ts; % 统一时间栅格 for k 1:Niter in Simulink.SimulationInput(model); in in.setVariable(Uff_in, Uff); in in.setVariable(Gamma_in, Gamma); % 若模型内还有增益块 out sim(in); ts out.logsout.get(e_log).Values; e_k ts.Data(:); % 对齐到固定栅格防止求解器输出点数抖动 e_k interp1(ts.Time, e_k, t_grid, linear, extrap); Eall(:, k) e_k; Uff (1 - alpha) * Uff Gamma * e_k; % 外循环更新前馈 Uall(:, k) Uff; end这段脚本的逻辑是每一轮把当前的前馈序列Uff灌进模型跑完一次仿真从logsout里取出误差时间序列重采样到固定栅格然后用更新律算出下一轮的前馈。Simulink.SimulationInput的好处是它会在仿真前后自动处理模型状态比直接改assignin加sim干净得多也不会污染基础工作区。interp1那一行是防弹衣。理论上固定步长求解器的输出点数应该严格等于N但实际跑起来经常差几个点——logging decimation、模型里有使能子系统导致某些步没记录、To Workspace的采样设置不对都会让长度对不上。一旦长度对不上Eall(:, k) e_k就直接报维度错误。用interp1强制重采样到t_grid顺手把这个问题按死。Uff的更新写在循环里而不是模型里好处是所有中间结果都在工作区想画什么图画什么图。代价是不能用parfor——第 k1 轮严格依赖第 k 轮的结果并行化在这里没有意义。3.4 验收先确认“单轮没问题”再谈迭代很多人一上来就跑 30 轮看到误差不降就怀疑 ILC 写错了其实问题往往在第一轮的反馈回路本身就不过关。建议加一个前置检查把Gamma置零、Uff全零跑单轮看三件事。第一跟踪误差的量级是否合理。如果第一轮误差就是参考幅值的百分之几十说明反馈参数根本没调好ILC 再强也救不回来。第二控制量u是否在物理范围内有没有顶到限幅。ILC 会逐轮累加控制量第一轮就饱和的话后面只会越累加越离谱。第三误差曲线的形状是否每轮可重复——这一条没法单轮验证但可以跑两轮Gamma 0的仿真两轮误差曲线应该完全重合。如果两轮就不重合说明模型里有随机源或者状态没复位ILC 的前提已经不成立了。注意logsout的默认格式是Dataset取数据要写out.logsout.get(信号名).Values.Data。如果改成Array格式时间列会丢interp1就没法用了。这个设置在模型配置参数的 Data Import/Export 里。4. 避坑与排查五类让 ILC 白跑的翻车现场下面这五条都是实际调过的坑每一条都按“现象 → 原因 → 解决”写清楚对着现象找就行。4.1 第二轮误差和第一轮完全重合persistent 变量没清现象跑完一轮再跑一轮把两次的误差曲线叠在一起两条线一模一样差值恒等于零。控制量也没有任何变化看起来像是 ILC 根本没起作用。原因persistent变量在 MATLAB Function 块里的生命周期跨越了多次sim()调用。开了Fast Restart之后模型不重新编译persistent变量保持上一次的值有些版本的 Simulink 在正常模式下也会保留取决于模型的Start simulation配置。结果就是第二轮开始时Uhist里存的还是上一轮结束时的值而reset端口又没被拉高缓存从来没被清过。解决两个层面同时处理。模型层面把reset端口的驱动做出来——可以用一个Step块在t 0时给一个窄脉冲或者干脆在模型里加一个Initial Condition触发的复位逻辑。脚本层面每次sim()之前在基础工作区执行clear ilc_p_type或者直接关掉Fast Restartset_param(model, FastRestart, off)。最省事的做法是干脆走路线 B把状态放在基础工作区生命周期完全可控。4.2 误差向量长度对不上变步长求解器埋的雷现象脚本跑到第 3 轮或者第 7 轮报Unable to perform assignment because the size of the left side is 2001 and the size of the right side is 1998。改小Niter有时候能过有时候过不了看起来毫无规律。原因求解器不是固定步长或者模型里有使能子系统、零交叉检测、事件触发之类的机制导致某些采样步没有被记录。logsout只记录实际计算出来的点变步长情况下点数天然会抖。另一种可能是To Workspace块或者logsout的decimation参数被改过每几个点抽一个长度自然对不上。解决三步走。第一求解器强制设成Fixed-stepdiscrete步长直接填Ts这一条没有商量余地。第二interp1重采样到固定栅格把长度问题在数据层面抹平。第三加一行assert做兜底assert(numel(e_k) 2, 误差序列为空检查 logsout 信号名是否拼错); e_k interp1(ts.Time, e_k, t_grid, linear, extrap); assert(numel(e_k) N, 重采样后长度仍不匹配检查 Ts 与 t_grid 定义);assert看着土但它能在出问题的那一轮直接把行号打出来比跑到第 20 轮才发现维度炸了强太多。4.3 前 7 轮收敛第 8 轮突然发散增益和被控对象增益的乘积越界现象RMS 误差从第 1 轮到第 7 轮单调下降降得很漂亮第 8 轮开始抬头第 12 轮控制量顶到限幅第 15 轮直接数值爆炸。曲线形状是典型的指数上升。原因|1 - G(jω)Γ|在某个频段上超过了 1。最常见的是高频段G在高频衰减Γ是个常数GΓ趋近于 01 - GΓ趋近于 1理论上只是临界不收敛。但只要对象带一点相位滞后采样保持本身就有半个采样的延迟这个频段的1 - GΓ模就会略大于 1误差被逐轮放大。前几轮看不出来因为低频段的收敛速度远快于高频段的发散速度两条曲线交叉的那一刻就是“突然”发散的那一轮。解决先减半Γ确认趋势反转再决定要不要上 Q 滤波器。Q 滤波器的做法是在更新律里串一个零相位低通% 零相位低通截止频率 wc采样 Ts wc 2 * pi * 0.35 / (2 * Ts); [b, a] butter(2, wc / (pi / Ts)); e_f filtfilt(b, a, e_k); % 零相位不引入额外滞后 Uff Uff Gamma * e_f;filtfilt的关键是零相位——普通的filter会引入相位滞后而相位滞后恰恰是导致发散的那个东西用带滞后的滤波器去修滞后等于没修。另外可以顺手加一个幅值限幅Uff max(min(Uff, u_max), -u_max)至少保证数值不炸给排查留时间。4.4 每个采样点的初值都不一样被控对象状态没复位现象误差曲线在t 0那个点的值每轮都不一样而且看起来像是随机的。后面的曲线形状倒是正常就是这个起点漂。原因被控对象里有积分器或者连续状态跑完一轮之后状态停在终值上下一轮从终值开始跑初值自然不对。Fast Restart会明确保留终态这是它的设计目的即使是普通模式如果模型配置里勾了Load initial state或者Save final state也会出同样的问题。用Transfer Fcn之类的连续块时更隐蔽因为状态藏在求解器内部模型上看不见。解决每轮开始前显式复位。走路线 B 的话Simulink.SimulationInput默认就是从模型初始条件开始只要不勾Fast Restart就没问题。走路线 A 的话给积分器加一个Initial Condition的外部复位端口用一个Step块在t 0触发或者用set_param(model, LoadInitialState, off)显式关掉状态加载。离散状态空间的x0直接填在块参数里不会有这个问题这也是推荐用Discrete State-Space的原因之一。4.5 跑到几十轮以后内存吃满日志数据没有预分配现象前 20 轮跑得飞快之后越来越慢Niter设到 200 的时候 MATLAB 直接卡死。任务管理器里 MATLAB 的内存占用一路往上爬。原因Eall和Uall没有预分配每次Eall(:, k) e_k都在重新分配内存MATLAB 会复制整个数组。200 轮 × 2001 点 × 8 字节看着不多但复制次数是平方级的。另一种情况是所有信号都勾了 log包括中间变量、控制器内部状态每轮几百个信号全存下来几十轮就上 GB。解决Eall zeros(N, Niter)这行预分配必须在循环外不能省。日志只勾必要的三个信号误差e、控制量u、输出y。如果确实要跑几百轮只保留范数序列rms_e和最近 5 轮的完整数据历史数据用完就清。这些都属于基本功但在 ILC 这种“跑很多次”的场景里不做就会翻车。提示排查顺序建议从“单轮是否正常”开始再看“两轮是否可重复”最后才看“多轮是否收敛”。反过来排查八成会把模型配置问题误判成算法问题。5. 收敛性验证与 Q 滤波器把「感觉收敛了」变成一张对数坐标曲线判断 ILC 有没有真的收敛不能靠肉眼看 Scope。把每一轮的误差序列算成两个标量画在对数坐标上趋势一目了然。%% 收敛性指标计算与可视化 rms_e sqrt(mean(Eall.^2, 1)); % 每轮的 RMS 误差 sup_e max(abs(Eall), [], 1); % 每轮的无穷范数 idx_k 1:Niter; figure; semilogy(idx_k, rms_e, o-, idx_k, sup_e, s--, LineWidth, 1.2); xlabel(迭代轮次 k); ylabel(误差范数); legend(RMS, L_\infty, Location, northeast); grid on; % 单调性检查任何一轮抬头都要报警 bad find(diff(rms_e) 0, 1, first); if ~isempty(bad) fprintf(第 %d 轮误差抬头rms 从 %.4g 升到 %.4g\n, ... bad, rms_e(bad), rms_e(bad1)); endrms_e反映的是整体能量sup_e反映的是最坏点。这两个指标经常不同步rms_e一路下降但sup_e卡住不动说明大部分点收敛得很好但有个别点始终压不下去通常是参考轨迹的拐角处或者有非重复扰动的位置。反过来sup_e下降而rms_e反弹说明整体在变差但峰值被压住了往往是滤波器把某个频段的能量搬到了别处。验收标准我一般定三条rms_e在 15 轮内下降一个数量级以上sup_e的最大值不超过参考幅值的 5%从第 10 轮开始连续 5 轮的rms_e波动小于 1%。三条都过才算真的收敛而不是“看起来像收敛”。Q 滤波器的参数不能拍脑袋定。我的做法是先不加滤波让rms_e一直跑到抬头那一轮把那一轮的误差序列做 FFT看能量堆在哪个频段然后把Q的截止频率定在那个频段的 0.6 倍左右。这样既能压住发散又不会把有用的低频信息一起砍掉。截止频率定好之后Gamma通常还能往上抬一点因为高频段被压住之后增益的上界主要由低频决定。还有一个容易忽略的技巧给学习增益加一个随采样点变化的时间权重。轨迹起始段和结束段往往是瞬态增益给大了容易冲中间段稳态可以给大一些。写成Gamma_vec Gamma * w(i)w是个从 0.5 平滑过渡到 1 再回到 0.5 的窗代码上只是把标量换成向量但收敛的平滑度改善很明显。最后说个血泪经验。我早期的习惯是改完参数直接跑 30 轮看曲线“差不多”就收工结果换个参考轨迹全部推倒重来。后来改成三步强制走一遍第一步Gamma 0跑单轮确认反馈回路干净第二步Gamma取保守值跑 5 轮确认rms_e单调下降第三步才放开Gamma和Niter做完整实验。这三步加起来不到两分钟但省下来的返工时间远超这个数。收敛曲线不是给别人看的报告是自己判断“参数还能不能再推一点”的唯一依据别省。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?