简介:ILC_Simulink.rar 是面向控制工程学习者与研究者的 Simulink 迭代学习控制仿真模型,围绕周期性或重复性任务中的无静差跟踪问题,提供从被控对象传递函数搭建到 ILC 误差计算、反馈校正与迭代更新策略实现的完整示例,适合高校课程设计、控制算法入门复现与科研仿真验证。压缩包共 14 个文件,约 66KB,以 7 个 mat 数据文件、2 个 xml 配置文件和 slx/slxc 模型文件为主,并含 m 脚本、autosave 等辅助文件,其中模型文件承载 ILC 主流程,mat 数据与 m 脚本便于加载、绘图和批处理。已有 3981 人学习,说明该主题在控制理论学习中具有一定关注度。读者可借助模型观察多周期迭代后的输出曲线与误差收敛过程,理解误差反馈、增益调整、饱和限幅等环节对跟踪性能的影响,并据此开展参数对比与算法改进练习。
1. ILC_Simulink.rar 拆包:迭代学习控制为什么值得放进 Simulink 跑
Simulink ILC 这套东西,第一次接触的人容易把它当成某种高级 PID,名字里带“学习”,听起来像加了自适应。实际把 ILC_Simulink.rar 拆开你会发现核心结构很简单:一条被控对象、一块保存上一轮控制量的内存、一条按批次更新的学习律。它的价值在于处理“同一段轨迹反复执行”的场景:机械臂重复抓放、伺服台按固定指令来回走、试验台按同一曲线跑完一个周期。这类系统的误差不是随机的,而是每轮几乎一模一样地重复出现,反馈控制压不下去的相位滞后部分,恰好能用上一轮的经验补上。Simulink 在这里的角色不是画图工具,而是把时间域和迭代域两个维度同时摆进一个能反复重跑的环境。这份资源适合已经会用 Simulink 搭基础回路、但还没系统跑过迭代学习的人。
2. 迭代域怎么塞进时间域:三种 Simulink 结构与选型
2.1 先把 t 轴和 k 轴分开,否则后面全是白费功夫
迭代学习控制的基本形式非常短,P 型就一行:
u_{k+1}(t) = u_k(t) + Γ · e_k(t) e_k(t) = r(t) - y_k(t), t ∈ [0, T]k是迭代批次,t是一次试验内部的时间。这条式子能成立,靠的是一个前提:每次试验的初始状态、参考轨迹、试验时长完全一致。系统把上一轮“哪里欠了、哪里超了”整段记下来,下一轮一开始就把这份补偿量整体加到输入上。
它和反馈控制的分工很清楚。反馈在同一次试验内部实时纠偏,对抗的是当下的扰动;ILC 在批次之间做前馈修正,对抗的是“每次都一样”的重复性误差。真正难压的是后者,因为相位滞后决定了反馈增益不能给大,而 ILC 不需要实时性,它可以把整段误差倒着用。
新手翻车最多的地方,是把两件事混在一起做。有人试图在一个 Simulink 模型里用反馈的思路实现 ILC,加个 Memory 块、加个 Unit Delay,结果跑出来发现只有第一个采样点在学习。原因是Unit Delay延的是一个采样周期Ts,而你需要延的是整个试验长度T。迭代域的“上一拍”是上一轮试验,不是上一个采样点,这个区别不搞清楚,模型怎么调都不会收敛。
所以结构选型的第一原则是:迭代循环必须明确落在某处,要么在脚本里,要么在框图里的 For Iterator 中,不能靠 Memory 块猜。
2.2 三种落地结构:脚本驱动、For Iterator、MATLAB Function
实际能跑通的结构就三种,各自的边界很清晰。
| 结构 | 迭代域在哪 | 适合场景 | 调试难度 | 典型坑 |
|---|---|---|---|---|
MATLAB 脚本 +sim()循环 | 脚本的for循环 | 算法验证、参数扫描、教学复现 | 低 | 状态未复位、base 工作区污染 |
| For Iterator 子系统 | 框图内部 | 定点部署、代码生成、HIL | 中 | Memory 块初值、迭代间信号宽度 |
| MATLAB Function / S-Function 块 | 块内persistent变量 | 复杂学习律、非线性、带约束更新 | 中高 | persistent 变量跨次仿真残留 |
脚本驱动是最推荐先用的一种。它把“迭代”这件事放回 MATLAB 里,你能用fprintf打日志、能用dbstop断点、能随时改学习增益重跑,调试体验比在框图里翻信号好太多。代价是每次sim()都有启动开销,模型大的时候慢。
For Iterator 结构把整个迭代过程放进一次仿真,k变成了框图里的一个循环变量。好处是能生成代码、能上硬件,坏处是中间变量全在信号线上,出错时你只能靠 Scope 一个个看。而且迭代之间传递u_k需要 Memory 块或者Data Store Memory,初值设错就会第一轮就发散。
MATLAB Function 块适合学习律本身复杂的情况,比如带饱和、带死区、带自适应增益。需要注意的是persistent变量在同一个 MATLAB 会话里跨多次sim()调用是保留的,第二次跑仿真时它带着上一次的残留值,表现出来就是“改了参数但结果没变”。稳妥做法是在块里加一个复位输入端口,每轮开始时清零。
2.3 从零搭一条 P 型 ILC 回路
先说模型该长什么样。被控对象用一个 State-Space 块或者 Transfer Fcn 块,输入端口接In1,输出接Out1,中间不要塞任何积分器复位逻辑。控制量u_k从工作区读,用From Workspace块,数据格式设成带时间的结构体或者直接给[t, u]两列矩阵。
配置参数里几项必须改:
- Solver 选固定步长(
ode4或离散),Fixed-step size 设成Ts。变步长求解器会在每次试验里给出不同长度的输出,误差数组和参考数组长度对不上,后面norm直接报错。 - Data Import/Export 里的
Final states和Initial state都不要勾。勾了Final states,Simulink 会把上一轮结束时的状态存下来,下一轮默认接着跑,这时候你的“重复性前提”就没了,误差会卡在一个值上再也不降。 Save output打开,输出格式选 Dataset,名字记成yout。
模型准备好之后,主循环脚本这样写:
%% ilc_p_type.m —— P 型迭代学习控制主循环 clear; clc; mdl = 'ilc_plant'; % 被控对象模型名,需与 .slx 文件名一致 T = 10; % 单次试验时长 (s) Ts = 0.001; % 采样周期 (s) N = round(T/Ts) + 1; % 采样点数 Kmax = 50; % 最大迭代次数 Gamma = 0.6; % 学习增益 Qwin = 5; % 误差滑动平均窗口(点数),1 表示不滤波 t = (0:N-1)' * Ts; r = sin(2*pi*0.5*t); % 期望轨迹,写入 base 供模型引用 u = zeros(N,1); % 第 0 轮控制输入全零 err = zeros(Kmax,1); % 每轮的误差 2-范数 set_param(mdl,'LoadInitialState','off'); % 每次从零初值起跑 set_param(mdl,'SaveFinalState','off'); % 不把终态带回下一轮 set_param(mdl,'StopTime',num2str(T)); for k = 1:Kmax assignin('base','u_k',u); % 当前轮控制量 out = sim(mdl); % 跑一次试验 y = out.yout; % 取输出 e = r - y; % 本轮跟踪误差 err(k) = norm(e,2); u = u + Gamma * e; % P 型更新律 if Qwin > 1 u = movmean(u, Qwin); % 简易低通,压制高频抖 end fprintf('iter %3d ||e||_2 = %.6e\n', k, err(k)); end figure; semilogy(1:Kmax, err, 'o-'); xlabel('迭代次数 k'); ylabel('||e_k||_2'); grid on;代码里有几个地方值得单独说。
assignin('base','u_k',u)把当前轮控制量写进 base 工作区,模型里的From Workspace块就是从这里取数。如果改用SimulationInput对象,这一步可以省掉,但初学阶段用assignin更直观,方便你在命令行里plot(u_k)看波形。
set_param那两行是整个脚本的关键。LoadInitialState关掉,保证每轮从x(0)=0起跑;SaveFinalState关掉,保证不把终态写回模型。这两行漏掉一行,误差曲线就会在某个值附近来回波动,看起来像“收敛到一个精度极限了”,实际上是初值污染。
e = r - y对长度有要求。r是N×1,y从 Dataset 里取出来也是N×1,前提是模型的StopTime正好是Ts的整数倍。如果T=10、Ts=0.001,N=10001,这没问题;如果Ts设成0.0007,长度会差一两个点,r - y直接报维度不匹配。我一般会在循环里加一句assert(numel(y)==N),出错早发现。
movmean(u, Qwin)是最简陋的 Q 滤波器,用滑动平均替代真正的低通。它能压住高频抖,但会引入一点点相位滞后,只是因为窗口小(5 个点、5 ms),实际影响可以接受。要做得更严谨,见第 3 章的零相位滤波。
Gamma的正负号不能乱给。第一轮跑完先看一眼err(1),如果第二轮比第一轮大,先把Gamma取反试一次。符号对不对,取决于被控对象的直流增益是正还是负,这个后面还会展开。
3. 学习律不是只有 P:D 型、PD 型与 Q 滤波器的取法
3.1 相对阶决定你该用 P 还是 D
选学习律类型的依据是相对阶,不是个人喜好。
相对阶 0 的对象,也就是传递函数分子分母同阶、存在直接馈通项(D ≠ 0),P 型就能收敛。判据是|1 - Γ·G(jω)| < 1对所有频率成立,落实到操作上就是:先看G(0)的符号,Γ取同号,幅值从小到大慢慢推。
相对阶 1 的对象,也就是G(s)分子比分母低一阶,P 型在学习增益偏大的时候很容易把高频噪声放大。原因是误差直接反馈,而对象对输入的响应滞后了一个采样周期,这个滞后在学习律里变成了正反馈。常见做法是换成 D 型:
u_{k+1}(t) = u_k(t) + Γ · ė_k(t)D 型相当于给误差加了个微分,把滞后带来的相位差补回来。代价是微分对噪声敏感,如果输出里有测量噪声,ė_k会被放得很大。这时必须配 Q 滤波器一起用。
相对阶 ≥ 2 的对象最难处理。P 型和 D 型都很难直接收敛,需要相位超前补偿或者高阶学习律。如果手上的模型是二阶以上又想做 ILC,我的建议是先加一个内环反馈把对象“压”成一阶等效,再在外层做学习,这比硬调高阶学习律省事得多。
实践里最稳的起步方式:先用 P 型跑通,把Γ从0.1开始按 0.1 递增,记录每一档跑 20 轮之后err(20)的值。找到err(20)开始不降反升的那个Γ,取它的 0.5 到 0.6 倍作为工作值。留余量不是为了保守,是因为实际系统里总有建模误差,仿真上刚好收敛的参数搬到实物上大概率发散。
3.2 Q 滤波器:为什么 ILC 可以用“非因果”的实现
Q 滤波器在 ILC 里的作用是把学习律在高频段的增益压到 1 以下。理论上|1 - Γ·G|在中低频小于 1 就够收敛,但高频段只要有一点点超过 1,噪声就会在那个频率上被逐轮放大,最后表现为误差曲线上的锯齿,怎么调增益都去不掉。
Q 滤波器的形式是:
u_{k+1}(t) = Q(z) · [ u_k(t) + Γ · e_k(t) ]Q(z)一般是一个低通滤波器。关键在于:ILC 是离线按批次算的,整个e_k(t)数组完整地躺在内存里,所以可以用filtfilt做零相位滤波。前向一遍、反向一遍,相位正好抵消,等效幅频特性是|Q(jω)|²。
这一点是 ILC 相对于实时控制的一个“特权”。实时控制里你不可能知道未来的信号,任何滤波都会掉相位;ILC 里你手里有整段轨迹,所以掉相位这件事可以直接绕过。很多人第一次看到 ILC 论文里用非因果滤波会觉得不合理,其实是因为下意识把它当成了在线算法。
零相位滤波的代价是它对首尾样本的处理会有边界效应。filtfilt默认用反射填充,试验首尾那几十个点会被削一下,如果参考轨迹在首尾有大跳变,那一段的误差会偏大。处理办法是把滤波器的阶数压低(二阶巴特沃斯足够),或者手动给首尾做对称延拓再滤波。
截止频率怎么定:先画|1 - Γ·G(jω)|的波特图,看它在哪个频率开始超过 1,把Q的截止频率设在那个频率的 0.7 倍左右。不要凭感觉设,设高了等于没滤,设低了会把有用的中频学习能力砍掉,表现为误差收敛到某个值就下不去了。
3.3 一套能直接改参数的 PD + 零相位滤波实现
把 D 型和 Q 滤波合起来,主循环改写如下:
%% ilc_pd_q.m —— PD 型学习律 + 零相位 Q 滤波 Kp = 0.55; % 比例学习增益 Kd = 0.004; % 微分子项系数,量纲上要除以 Ts fc = 40; % Q 滤波器截止频率 (Hz) Fs = 1/Ts; [bq, aq] = butter(2, fc/(Fs/2)); % 二阶巴特沃斯,归一化截止频率 u = zeros(N,1); err = zeros(Kmax,1); for k = 1:Kmax assignin('base','u_k',u); out = sim(mdl); y = out.yout; e = r - y; de = [0; diff(e)] / Ts; % 后向差分,长度保持 N u = u + Kp*e + Kd*de; % PD 更新 u = filtfilt(bq, aq, u); % 零相位滤波,不掉相位 err(k) = norm(e,2); fprintf('iter %3d ||e||_2 = %.6e max|e| = %.6e\n', ... k, err(k), max(abs(e))); endde = [0; diff(e)]/Ts这一行有两个细节。前面补一个 0 是为了让de和e长度一致,补 0 相当于假设初始时刻误差变化率为零,这个假设在e(1)≈0的时候成立。除以Ts是把差分换算成变化率,Kd的物理量纲由你自己定,但一旦定了,改Ts之后Kd要跟着调,否则学习强度会变。
filtfilt有个硬性要求:信号长度必须大于3×(滤波器阶数×2),二阶滤波器大概是 12 个点以上。N一般都在几百上千,不成问题。但如果你做的是短周期试验(比如只有 20 个采样点),filtfilt会报错,这时候只能退回去用filter或者直接不滤。
max|e|这个指标建议一起打印。只看 2-范数有时候会骗人:某几个采样点误差特别大但整体能量在降,||e||_2好看,实际跟踪效果很差。两个指标一起看,能早一点发现局部发散。
最后补一个常用的改动:遗忘因子。
alpha = 0.02; % 遗忘因子,0 表示不遗忘 u = (1-alpha)*u + Kp*e + Kd*de; % 带遗忘的更新加了遗忘因子之后,学习律不再是纯积分形式,对非重复性扰动有了抑制能力,代价是纯重复性误差的收敛精度会略微下降。alpha取 0.01 到 0.1 之间,小于 0.01 基本等于没加,大于 0.1 会明显拖慢收敛速度。
4. 批量仿真怎么组织:sim 驱动、数据落地与误差范数
4.1 sim() 调用的几个必须设对的参数
sim的调用方式直接决定跑得快不快、数据取得对不对。
最朴素的是out = sim(mdl),模型参数全部靠set_param提前设好。这种方式的问题在于每次调用都会重新解析一遍模型,模型大的时候启动开销能占到总耗时的三成以上。
推荐用SimulationInput对象复用:
%% 用 SimulationInput 复用配置,减少重复解析开销 simIn = Simulink.SimulationInput(mdl); simIn = simIn.setModelParameter('StopTime', num2str(T), ... 'LoadInitialState','off', ... 'SaveFinalState','off', ... 'SaveOutput','on', ... 'OutputSaveName','yout'); for k = 1:Kmax simIn = simIn.setVariable('u_k', u); % 变量注入,不碰 base 工作区 out = sim(simIn); y = out.yout; % ... 更新律 endsetVariable的好处是把变量注入到仿真自己的工作区,不污染 base。之前用assignin的时候,跑完一轮u_k就留在那儿了,如果两套实验交替跑,很容易拿错数据。
SaveOutput和OutputSaveName决定out.yout里有什么。如果不显式设,新版 MATLAB 默认把输出存成out.yout,但老版本可能是out.get('yout'),跨版本迁移的时候会报错。稳妥做法是显式设一遍。
还有两个参数容易被忽略。ReturnWorkspaceOutputs在新版里默认是on,不用管;SaveSimulationMetadata建议关掉,它会往输出里塞一堆诊断信息,跑几百轮之后内存会涨得很快。
4.2 数据落地:Dataset、To Workspace 与采样对齐
输出数据有三种存法,各有用处。
- Dataset(
logsout):结构化最好,支持多信号、支持信号名索引,缺点是取数要out.logsout.get('y').Values.Data,多一层转换。 - To Workspace 块 + Array 格式:最快,直接是
N×1数组,缺点是模型里每加一个观测信号就要加一个块,模型会变臃肿。 - To Workspace 块 + Timeseries 格式:带时间轴,方便
resample,缺点是每次取数都要.Data。
我一般用 Dataset 存主输出y,用 Array 存几个调试用的中间量(比如控制量u本身、误差e),跑完一轮直接把 Array 变量覆盖掉,不留历史。
采样对齐是这里最容易出错的地方。模型输出的长度由求解器和StopTime共同决定,理想情况下是N = T/Ts + 1,但有三种情况会让它偏离:
一是模型里有Transport Delay块,延迟不是Ts的整数倍,求解器会插值出额外的点。二是用了变步长求解器,输出点由求解器自己定,长度不可控。三是StopTime写成了'10.0'而Ts=0.001,浮点累加误差会让最后一个点丢或多。第三种最常见,解决办法是StopTime直接写num2str(N*Ts - Ts)而不是num2str(T),用采样点数反推时长。
每次跑完加一句断言:
assert(numel(y) == N, 'sprintf' + "输出长度 %d 与预期 %d 不一致", numel(y), N);这种断言很粗暴,但能帮你在一堆数值问题里第一时间定位到“是长度错了”还是“是算法不收敛”。
4.3 收敛曲线与参数扫描脚本
单次实验跑完,看semilogy(1:Kmax, err)这条曲线够不够。判断标准有两条:误差是否整体下降,下降是否在最后几轮趋于平缓。如果曲线是一条水平线,说明学习增益太小或者符号反了;如果曲线先降后升,说明增益太大,高频段已经开始放大。
参数扫描用两层循环,把Γ和Q截止频率一起扫:
%% 参数扫描:Γ × fc,记录每个组合的最终误差 Gammas = [0.2 0.4 0.6 0.8 1.0]; fcs = [20 40 80 160]; result = zeros(numel(Gammas), numel(fcs)); for gi = 1:numel(Gammas) for fi = 1:numel(fcs) Gamma = Gammas(gi); [bq, aq] = butter(2, fcs(fi)/(Fs/2)); u = zeros(N,1); for k = 1:30 % 每个组合跑 30 轮够看趋势 assignin('base','u_k',u); out = sim(mdl); e = r - out.yout; u = filtfilt(bq, aq, u + Gamma*e); end result(gi, fi) = norm(e, 2); % 记录最后一轮的误差 end end imagesc(fcs, Gammas, log10(result)); set(gca,'YDir','normal'); colorbar; xlabel('Q 滤波器截止频率 (Hz)'); ylabel('学习增益 \Gamma');这个扫描脚本跑起来不快,四乘五一共 20 个组合、每组 30 轮,就是 600 次仿真。模型小的话几分钟能出结果,模型大的话建议先把Kmax降到 15 轮,看趋势够了再精细调。log10(result)是为了让色阶拉开,误差跨两三个数量级的时候线性色阶看不出差别。
扫描结果里通常会出现一个“平原”区域:Γ在中间某段、fc在中间某段,误差都很低。取平原中心而不是边缘,这是留余量的另一种做法。
5. 排查清单:ILC 仿真跑不出收敛的五种现场
5.1 误差降到某个值就卡住,怎么加迭代都不动
现象:err曲线前 10 轮降得很快,之后变成一条水平线,||e||_2卡在 1e-3 量级不再下降,加大Γ也没用。
原因:九成是初值污染。模型里Final states没关,或者Initial state被勾上了,导致每轮试验的起点不是x(0)=0,而是上一轮的终点。这样误差里混进了一个与k相关的常量偏置,学习律一直在补偿这个偏置,压不到真正的跟踪误差上。
解决:set_param(mdl,'LoadInitialState','off')和set_param(mdl,'SaveFinalState','off')两行都加上。改完先在循环里打印y(1),正确情况下每轮的y(1)应该完全一样,差一点点都说明状态没复位干净。另外检查模型里有没有Integrator块设了初始条件、有没有Data Store Memory带着上一轮的值。
5.2 第一轮就发散,误差指数级涨到溢出
现象:err(1)正常,err(2)比err(1)大两三倍,之后每轮翻倍,十几轮之后变成Inf或者NaN。
原因:学习增益符号反了,或者学习律类型和对象相对阶不匹配。符号反了最好判断,把Γ取负再跑一次就能确认。更隐蔽的是类型不匹配:给一个相对阶 1 的对象硬上 P 型,Γ小的时候能勉强收敛,稍大一点就发散,看起来像“增益上限很低”,实际上是选错了学习律。
解决:先做符号测试,Γ取+0.1和-0.1各跑 5 轮,看哪个在降。符号确认之后如果增益上限仍然很低(比如只能给到 0.05),换成 D 型再试。判断相对阶的方法很简单:看传递函数分子分母的阶数差,或者给对象一个阶跃输入,看输出是不是立刻有响应。立刻有响应是相对阶 0,滞后一个采样周期是相对阶 1。
5.3 误差曲线整体在降,但上面叠加了高频锯齿
现象:semilogy出来的曲线趋势是向下的,但每个点附近都有明显的抖动,max|e|和||e||_2差了一到两个数量级,控制量u的波形看起来像噪声。
原因:没有 Q 滤波器,或者截止频率设得太高。ILC 的更新律本质上是积分环节,对高频段的增益是|1 - Γ·G|,只要这个值在高频超过 1,噪声就会逐轮放大。另一种可能是微分项Kd给得太大,把输出里的数值噪声放大了。
解决:先加 Q 滤波器,截止频率从对象带宽的 0.7 倍开始试,逐步往上推,找到锯齿刚开始出现的那个频率再退回来。用filtfilt而不是filter,避免滤波本身引入相位滞后。如果加了滤波还有锯齿,把Kd降到原来的三分之一试一次。做完这两步之后,把u的波形画出来看,正常的u应该是光滑的,有明显的锯齿说明滤波没到位。
5.4 跑到几十轮之后越来越慢,内存一直涨
现象:前 20 轮每轮 2 秒,到 60 轮之后每轮变成 8 秒,任务管理器里 MATLAB 的内存占用线性上升,最后可能报Out of memory。
原因:每次sim()都返回一个完整的SimulationOutput对象,里面装着全部信号、元数据、诊断信息。如果这些对象被存进数组(比如outs(k) = out),内存就是Kmax倍增长。另一种情况是模型开了SaveSimulationMetadata,每轮都往输出里塞一份模型快照。
解决:循环里只取需要的字段,取完就把out覆盖掉,不要存整个对象。要留历史数据的话,只留y、u、err这几个N×1数组。set_param(mdl,'SaveSimulationMetadata','off')也加上。如果确实需要保留每一轮的完整输出,改成存到.mat文件里,跑完一轮save一次然后clear,不要全堆在内存里。
5.5 报错说维度不匹配,但怎么看模型都对
现象:e = r - y这一行报Matrix dimensions must agree,打印出来numel(r)是 10001,numel(y)是 10000 或者 10002。
原因:StopTime和Ts不是严格的整数倍关系。T=10、Ts=0.001的时候,浮点运算下10/0.001可能算出9999.9999,round之后是 10000,但模型实际输出了 10001 个点。也可能是模型里有代数环或者过零检测,求解器多输出一个点。
解决:StopTime不要直接写T,改成num2str((N-1)*Ts),用采样点数反推时长。然后在循环开头加断言,长度不符时立刻停下来,别让它跑到e = r - y才报错。如果断言一直不过,把out.yout的Time字段打出来看最后一个时间点是多少,正常应该正好等于(N-1)*Ts,多一点点或少一点点都能看出来。
注意:这五条里第 1 条和第 2 条占实际翻车案例的一大半。每次新建模型,我会先把状态复位那两行
set_param写上,再写别的,顺序反了后面排查会很痛苦。
6. 收敛性验证:把时域范数和频域界对起来做交叉检查
时域看||e_k||_2只能告诉你“在降”,不能告诉你“还能降多少、余量有多大”。补一个频域检查,心里才有底。
做法是从模型里把被控对象的线性模型提出来,linmod或者直接用手上的状态空间矩阵都行,然后画|1 - Γ·G(jω)|的幅频曲线:
%% 频域收敛判据检查 [Ac,Bc,Cc,Dc] = linmod(mdl); % 在当前工作点线性化 G = ss(Ac,Bc,Cc,Dc); w = logspace(-1, 3, 500); M = 1 - Gamma * squeeze(freqresp(G, w)); % 注意 G 的符号约定 figure; semilogx(w, abs(M), 'LineWidth', 1.2); hold on; yline(1, 'r--'); xlabel('\omega (rad/s)'); ylabel('|1 - \Gamma G(j\omega)|'); grid on;曲线全部压在 1 以下才算合格。如果有某一段鼓起来超过 1,那一段对应的频率就是误差会放大的频段,表现出来就是u的波形上出现那个频率的纹波。这时候要么降Γ,要么加 Q 滤波器把那段压下去。
交叉检查的价值在于:时域误差在降、频域曲线却有一段超过 1,说明你的仿真里有些环节没被linmod捕获(饱和、限幅、量化、延迟),实机上这些环节会起作用,仿真上的收敛不可信。反过来,频域全在 1 以下但时域不收敛,说明模型配置有问题,八成还是状态没复位。
最后说一个我常用的技巧:找临界增益。把Γ从工作值开始每次乘 1.2 往上推,跑到发散为止,记下发散前那一档的Γ_max。实际工作值取0.5 × Γ_max到0.6 × Γ_max。这个比例不是拍脑袋,是因为建模误差和采样抖动通常会把有效增益往上抬 30% 到 50%,留一半余量刚好覆盖。
从那以后我每次调完 ILC 参数,都会把时域误差曲线和频域|1 - Γ·G|曲线并排放在一个 figure 里截图存档,跑实物之前先看一眼这两张图对不对得上。对不上就别上机,省钱省时间。希望帮到你。
本文还有配套的精品资源,点击获取