MATLAB控制系统仿真源码:传递函数到PID与状态空间实践 简介《MATLAB与控制系统仿真实践第3版》配套源码压缩包面向学习控制系统仿真、自动控制原理与MATLAB/Simulink实践的高校学生和工程师用于对照教材实例完成建模、分析、控制器设计与仿真验证。包内共390个文件约1.41MB以255个.m脚本为核心辅以.mdl与.slx Simulink模型、.mat数据、.fig图形及Java/JAR依赖文件覆盖根轨迹、频率响应、状态空间、PID/LQR、系统辨识、优化调参等典型实验便于按章节复现与修改。已有394人学习下载。源码保留教材示例的模型与脚本结构可直接运行并观察输出响应也可作为课程实验、专题练习与算法对比的参考帮助读者从公式推导过渡到可执行仿真积累排错与参数调试经验。1. 控制系统仿真源码的价值在于参数能被改课本上的控制系统仿真例子大多停在一条干净的曲线超调刚好 16.3%调节时间刚好 4 秒稳定裕度刚好是正的。真到手上把被控对象的时间常数改一下曲线立刻发散把采样周期挪一挪离散化结果就和连续域对不上。这套《MATLAB与控制系统仿真实践》第 3 版的配套源码最实在的地方在于它把传递函数建模、时域指标提取、根轨迹与频域校核、PID 与状态空间设计这些环节拆成了一堆能直接改参数的 .m 脚本和 .slx 模型。它不是电子书是代码包。适合刚学完自控原理、想把纸面计算变成可复现实验的学生也适合工作里要快速验证一个被控对象、又懒得从零搭框架的工程师。下面按「先看结构、再跑时域、再校频域、最后避坑」的顺序拆一遍。2. 解压之后先做什么目录盘点、工具箱依赖与路径清理很多人的第一反应是双击某个看起来像主程序的 .m 文件然后被一片红色报错劝退。控制类源码包里脚本之间的调用关系通常很深一个主脚本往上能牵出十几个函数文件直接 run 的翻车概率极高。稳妥的顺序是先盘清文件构成再确认工具箱齐全最后把路径整理干净再跑第一个验证脚本。2.1 用一次遍历弄清包里有什么解压到一个不含中文和空格的路径比如D:\ctrl_sim_src。中文路径在旧版本 MATLAB 里会让load、save、sim时不时出玄学问题没必要在这里赌运气。然后在你自己的工程目录下新建一个盘点脚本root D:\ctrl_sim_src; % 解压根目录按自己的实际路径改 d dir(fullfile(root, **, *.*)); % 递归列出所有文件含子目录 d d(~[d.isdir]); % 去掉目录项只保留文件 ext strings(numel(d), 1); for k 1:numel(d) [~, ~, e] fileparts(d(k).name); % 取扩展名 ext(k) lower(string(e)); end summary(categorical(ext)) % 按扩展名统计数量这段代码只做一件事告诉你这个包的主体是脚本还是模型。控制系统类源码包一般由三类文件构成——.m函数与脚本、.slx或.mdl模型、.mat数据。如果.m数量占大头说明它以命令行仿真为主重点看函数签名和调用链如果.slx数量可观说明相当一部分实验是用 Simulink 搭的得先确认你的版本能打开。参数上唯一需要注意的是fullfile(root, **, *.*)这种递归写法要求 MATLAB R2016b 及以上更老的版本得用genpath配合循环。盘完点之后建议再做一次函数依赖的粗筛在命令窗口执行which -all tf之类的抽查意义不大更有效的是把疑似主脚本逐个用matlab.codetools.requiredFilesAndProducts扫一遍看它到底拉了哪些文件、依赖哪些工具箱。这一步花三五分钟能省掉后面半小时的「函数不存在」排查。2.2 工具箱依赖报错里最多的一类控制系统仿真离不开 Control System Toolbox这一点没有替代方案。tf、ss、zpk、step、bode、margin、rlocus、place、lqr全部来自它。Simulink 部分是另一套Simulink Control Design 则负责线性化分析。装完 MATLAB 不等于装全了这些尤其在只勾选了基础组件的安装方式下。prods ver; names string({prods.Name}); need [Control System Toolbox, Simulink, ... Simulink Control Design, Optimization Toolbox]; for n need hit any(strcmpi(names, n)); fprintf(%-32s %s\n, n, string(hit)); end逻辑很简单ver返回结构体数组取出Name字段转成字符串数组逐个比对。输出里出现false的那一行就是后面必然报错的地方。Optimization Toolbox不是必需但如果源码里有基于fmincon或lsqnonlin做参数寻优的实验缺了它那部分脚本就会中断可以先跳过也可以装齐。参数说明一点strcmpi用不区分大小写比对避免不同版本里工具箱名称的大小写差异导致漏判。版本兼容是第二个坑。.slx是向上兼容、向下不兼容的格式用 R2023a 存的模型在 R2018a 里打不开反过来可以。如果你的 MATLAB 版本明显低于包的产生年份优先跑.m脚本部分模型文件单独用高版本机器导出成低版本再回来。.mat文件同理save时的-v7.3格式在很老的版本里读不了。2.3 路径清理为什么我每次都从 restoredefaultpath 开始控制类源码包常见的问题是函数名与工具箱函数重名比如包里自带一个margin.m或step.m一旦它抢在工具箱前面进了搜索路径调用行为就会变得莫名其妙——不报错但结果全错这才是最难查的。% 动搜索路径之前先把当前工作区的变量清掉避免同名变量遮蔽函数 clear; close all; clc; restoredefaultpath; % 回到 MATLAB 出厂路径 root D:\ctrl_sim_src; addpath(genpath(root)); % 把整个包加进来 rehash toolboxcache; % 刷新函数缓存让新路径立即生效 % 确认关键函数来自工具箱而不是包内同名文件 fprintf(tf - %s\n, which(tf)); fprintf(bode- %s\n, which(bode));restoredefaultpath会把你自己加的路径全部清掉所以后面必须紧跟addpath否则你会连自己的工具箱路径一起丢掉。rehash toolboxcache这一步很多人省结果是新加的路径要等下次重启 MATLAB 才完全生效调试时极其迷惑。最后的which检查是最便宜的后悔药如果输出的是包内路径而不是toolbox\control\...说明存在同名覆盖把包内那个文件改名或者从路径里剔除问题就解决了。顺带一提如果你同时装了多个版本的 MATLAB别用savepath把包路径永久写进全局配置那会让另一个版本的 MATLAB 也去加载它。用一次性的addpath就够了每次开工程重建路径麻烦几秒省掉整晚的排查。2.4 第一个验证脚本三行代码确认环境可用路径和环境都确认之后不要急着碰复杂实验先跑一个最小闭环s tf(s); G 1 / (s^2 2*0.5*s 1); % 阻尼比 0.5、自然频率 1 rad/s 的标准二阶 info stepinfo(G); fprintf(超调 %.2f%%, 上升时间 %.3f s, 调节时间 %.3f s\n, ... info.Overshoot, info.RiseTime, info.SettlingTime); step(G);tf(s)这种写法要求 Control System Toolbox 正常加载能跑通说明依赖没问题后面的脚本出错就是脚本自身的事范围立刻缩小。阻尼比 0.5 对应的理论超调约 16.3%如果输出偏离这个数一个量级以上基本可以判定有同名函数覆盖或者路径顺序异常。这一步我一般写进工程根目录的check_env.m以后换机器第一件事就是跑它。3. 从传递函数到时域把「曲线看着还行」变成能对照的数字控制系统仿真的第一个分水岭是你能不能脱离肉眼看曲线用数值判断一个系统的好坏。源码里那些时域实验脚本核心就两件事把被控对象写成 MATLAB 能认的模型对象再从响应里把指标提出来。3.1 tf、zpk、ss 三种表达与互转的边界同一个系统可以用三种方式表达传递函数tf、零极点增益zpk、状态空间ss。源码里通常会混用因为不同分析任务适配不同表达——频域分析用tf最直观根轨迹用zpk便于看零极点状态空间设计则必须用ss。num [1]; den [1 1 1]; % s^2 s 1注意降幂排列 G_tf tf(num, den); G_zpk zpk(G_tf); % 转零极点形式 G_ss ss(G_tf); % 转状态空间自动选可控标准型 % 反向验证转回 tf 应和原模型一致 G_back tf(G_ss); fprintf(互转误差 %.3e\n, norm(G_back.num{1} - G_tf.num{1}));这里最容易出错的是den的排列顺序必须是降幂从 s² 的系数写起中间缺项用 0 补齐。少写一个 0 不会报错只会让你仿真一个完全不同的系统。第二个要注意的点是ss转换选用的标准型——MATLAB 默认给出的是可控标准型状态变量的物理意义与源码里手推的未必一致。如果你要做极点配置或者 LQR必须先确认A矩阵的维度与源码中定义的输入输出通道对应关系否则算出来的增益矩阵 K 用上去后输出会完全对不上。zpk形式还有个额外好处可以直接读出增益。源码里经常出现的500/(s(s5)(s20))这种开环传递函数写成zpk([], [0 -5 -20], 500)比tf([500], [1 25 100 0])更不容易算错多项式展开。3.2 指标不要只信 stepinfo自己算一遍更稳stepinfo好用但它有默认假设稳态值为 1或者由FinalValue推断调节时间用 2% 误差带。源码里有些实验的稳态值不是 1或者教材用的是 5% 误差带直接对比就会出现「我的结果和书上对不上」的情况。[y, t] step(G_tf, 0:0.001:20); % 显式给时间向量采样间隔 1 ms yfinal y(end); % 实际稳态值 % 调节时间最后一次进入 2% 误差带并停留的时间 band 0.02 * abs(yfinal); idx find(abs(y - yfinal) band, 1, last); ts t(idx 1); % 上升时间10% 到 90% 稳态值 i10 find(y 0.1*yfinal, 1, first); i90 find(y 0.9*yfinal, 1, first); tr t(i90) - t(i10); % 超调量 Mp (max(y) - yfinal) / abs(yfinal) * 100; fprintf(ts%.3f s, tr%.3f s, Mp%.2f%%\n, ts, tr, Mp);逻辑上find(..., 1, last)找的是最后一次超出误差带的位置这个位置加一个采样点就是调节时间这比从头扫更稳因为有些高阶系统会反复进出误差带从前往后找会低估。参数上有两个可调项时间向量的步长决定指标的分辨率1 ms 对绝大多数慢系统足够快系统要降到 0.1 ms误差带系数0.02可以改成0.05与教材对齐。另外要注意step(G_tf, t)这种显式时间向量的调用方式比自己造阶跃信号再lsim更省事也更不容易在初值上踩坑。3.3 二阶系统参数扫描震荡和速度是此消彼长的源码里通常会有一段二阶系统的参数扫描但多数写得很简略。实际做的时候把阻尼比 ζ 和自然频率 ωn 拉成网格观察指标变化比单看几条曲线有说服力得多。zeta 0.1:0.1:1.0; % 阻尼比 wn 0.5:0.5:5.0; % 自然频率 rad/s MPa zeros(numel(zeta), numel(wn)); TSa zeros(numel(zeta), numel(wn)); for i 1:numel(zeta) for j 1:numel(wn) G tf(wn(j)^2, [1, 2*zeta(i)*wn(j), wn(j)^2]); info stepinfo(G); MPa(i,j) info.Overshoot; TSa(i,j) info.SettlingTime; end end % 输出一张可直接贴进文档的表 T array2table(MPa, RowNames, zeta string(zeta), ... VariableNames, wn string(wn)); disp(T);双重循环里每次重建tf对象开销不大但如果你把 ωn 的网格加密到 100 个点循环次数就到 1000 次耗时会有感。常见的做法是预分配矩阵上面已经做了而不是在循环里MPa [MPa; ...]动态扩维后者在大网格下会让运行时间翻好几倍。结果解读上ζ 从 0.1 涨到 0.7超调从 70% 左右掉到 5% 以下但这个过程不是线性的ωn 只影响时间轴缩放对超调百分比几乎没有影响。这条结论在源码的注释里未必写但把上表打出来就能一眼看到。实际调参时先按超调约束定下 ζ再按响应速度定 ωn比一次改两个参数高效得多。4. 根轨迹与频域校核增益是算出来的不是试出来的时域曲线好看不代表系统稳定裕度够。源码里频域那一部分讲的是同一个系统的另一面。这部分最容易出现的问题是学生拿rlocus画一张图看曲线不过虚轴就说稳定却说不清具体留了多少相位裕度。4.1 根轨迹把 rlocfind 换成可复现的扫描rlocfind是个交互式函数要用鼠标点图上一点脚本化和批量跑都不方便结果也没法进版本管理。我一般用增益向量直接扫num 1; den conv([1 0], conv([1 5], [1 20])); % s(s5)(s20) sys tf(num, den); K logspace(-1, 3, 2000); % 增益从 0.1 到 1000对数分布 CL feedback(sys, 1); % 先确认闭环结构此处为负反馈单位反馈 poles zeros(numel(K), 3); for i 1:numel(K) cl_i feedback(K(i) * sys, 1); poles(i,:) pole(cl_i).; % 每次取三个闭环极点 end % 找出首次出现右半平面极点实部 0的增益 crossIdx find(max(real(poles), [], 2) 0, 1, first); if isempty(crossIdx) fprintf(扫描范围内未失稳\n); else fprintf(临界增益约为 %.2f\n, K(crossIdx)); end核心思路是临界增益就是闭环极点实部第一次变正的那个 K。用对数分布logspace而不是线性步进是因为临界点附近的增益跨度往往有好几个数量级线性扫描容易跨过去。2000 个点对三个极点的系统是秒级完成如果系统阶次高可以把点数降到 500再在临界区间内用二分法细化。参数上要注意feedback的符号默认是负反馈如果你要研究正反馈得加第三个参数1。这个细节在复杂框图上出错极隐蔽——闭环极点会全部跑到右半平面你会以为系统本身不稳其实是反馈符号写反了。这也是我说的「血泪经验」凡是和理论结果差太远的仿真先回头看一眼反馈的符号。4.2 Bode 与稳定裕度margin 的三个返回值怎么读[Gm, Pm, Wcg, Wcp] margin(sys); fprintf(幅值裕度 %.2f dB %.2f rad/s\n, 20*log10(Gm), Wcg); fprintf(相位裕度 %.2f deg %.2f rad/s\n, Pm, Wcp); bode(sys); grid on; margin(sys); % 直接在图上标注便于贴报告返回值有四个幅值裕度Gm是倍数不是分贝所以打印时要20*log10相位裕度Pm单位是度Wcg是相位穿越频率Wcp是幅值穿越频率。源码里经常能看到把Gm直接当分贝打印的写法结果差一个对数变换数值看着像那么回事结论却是错的。判据要记牢闭环稳定要求Pm 0且Gm 1即分贝值 0。只满足一个不够。很多讲义上强调相位裕度是因为大部分系统先撞相位关但幅值裕度不足导致的失稳来得更突然几乎不给调整时间。4.3 批量校核allmargin 与结果落表当一个系统有多个工作点或者源码包里给了好几组对象参数逐个margin再肉眼比对太低效。allmargin返回结构体适合批量处理sysList {tf(1,[1 1 1]), tf(10,[1 3 3 1]), tf(1, conv([1 0],[1 2]))}; rows {}; for i 1:numel(sysList) m allmargin(sysList{i}); s struct(ID, i, Stable, m.Stable, ... GainMargin_dB, 20*log10(min([m.GainMargin, Inf])), ... PhaseMargin_deg, min([m.PhaseMargin, Inf])); rows{end1} s; %#okSAGROW end T struct2table([rows{:}]); disp(T);allmargin的Stable字段是它自己判的闭环稳定性可以直接用不必再手动feedback后查极点。GainMargin和PhaseMargin都是向量因为多回路系统可能有多个穿越点工程上一般取最小值作为瓶颈所以代码里套了min。这里用Inf兜底是为了处理没有任何穿越点的系统否则min会返回空值导致表构造失败。把这张表存成.csv或者直接贴进报告比截几张 Bode 图更能说明问题。我的习惯是每次改完控制器参数重跑一遍这个脚本把前后两次的表并排看增益掉了多少、相位补了多少一清二楚。5. 避坑与排查五个把我从「能跑」打回「不能用」的细节跑通和跑对是两件事。以下五个问题几乎每个刚接触控制系统仿真的人都会撞上一两个。5.1 现象、原因与解法现象一脚本第一行就报Undefined function tf。原因不是代码写错而是 Control System Toolbox 没装或者没在当前 license 里。解法是先跑 2.2 节那段依赖检查确认工具箱存在存在还报错才去查是不是restoredefaultpath之后忘了addpath。还有一种少见情况包内自带一个叫tf.m的文件把工具箱函数遮蔽了用which tf一看路径就露馅。现象二stepinfo算出的调节时间和教材差一大截。原因通常有两个教材用 5% 误差带而 MATLAB 默认 2%或者系统直流增益不是 1stepinfo推断的稳态值和你以为的不一样。解法是显式指定stepinfo(G, SettlingTimeThreshold, 0.05)同时先看dcgain(G)是多少。这个坑特别容易误导人因为曲线形状看着完全正常只有数字对不上。现象三Nyquist 图看着包围了 -1但margin说系统稳定。原因是对 Nyquist 判据的理解偏差——判稳看的是对 -1 点的包围圈数和开环右半平面极点数不是「曲线有没有经过 -1 左边」。多数情况下allmargin的Stable字段更可信因为它按传递函数直接判闭环极点。解法是不要只看图把feedback后的pole打出来实部全负就是稳没有歧义。现象四Simulink 模型打不开或者一跑就报求解器步长相关错误。打不开多半是版本问题.slx不能降级打开需要在高版本里导出为低版本格式。求解器报错一般是变步长求解器遇到了刚性系统或者模型里有代数环。解法是把 Solver 从ode45换成ode15s或ode23tb固定步长场景下把步长降到系统最小时间常数的十分之一以下。现象五c2d离散化之后仿真结果和连续域差得离谱。原因是采样时间选得太大。经验规则是采样频率至少是系统带宽的 10 到 20 倍工程上常取Ts 1/(20*fbw)。另一个原因是离散化方法zoh适合带零阶保持器的实际系统tustin更适合做频域特性保持两者在低频段差别不大在高频段能差出十几个百分点。解法是先用bandwidth估计闭环带宽再定Ts别拍脑袋给个 0.01。5.2 一份十分钟走完的自查顺序顺序检查项判定方式常见结论1路径是否干净which tf指向 toolbox指向包内则同名覆盖2工具箱是否齐ver中有 Control System Toolbox缺失则整章脚本不可用3模型是否同版本能open_system无警告报错则版本不兼容4反馈符号是否正确feedback默认负反馈正反馈会误判失稳5采样时间是否合理Ts 1/(20*fbw)过大则离散失真按这个顺序走九成的「跑不对」都能定位。真正难查的是第一项和第四项因为它们都不报错只给你一个错误的结果。我的习惯是每接手一个新包先把前四项过一遍再开始看具体实验内容看着啰嗦实际能省掉大量来回。6. 把示例改成自己的模板PID 与状态空间落地的最小闭环源码包最大的用途不是照着跑一遍而是抽出一套能复用的骨架。我通常做两件事把 PID 整定脚本改成接受任意G的函数把状态空间部分改成能直接套自己对象的模板。function [C, info] pid_tune(G, mode) % 极简 PID 整定mode zn 用 Ziegler-Nichols 临界比例度法 [~, ~, Wcg, ~] margin(G); % 相位穿越频率 Ku 1 / abs(evalfr(G, 1j*Wcg)); % 临界增益 Tu 2*pi / Wcg; % 临界振荡周期 switch mode case zn % 经典 Z-N 参数 Kp 0.6*Ku; Ti 0.5*Tu; Td 0.125*Tu; end C pid(Kp, Kp/Ti, Kp*Td); % 并联型 PID info stepinfo(feedback(C*G, 1)); end这段代码的关键在evalfr(G, 1j*Wcg)它在相位穿越频率处取开环幅值倒数就是临界增益。注意Wcg可能为空说明开环相位永远到不了 -180°此时 Z-N 法不适用要么先加积分环节要么换用基于模型的整定方法。状态空间部分同理place(A, B, p)要求系统完全能控先rank(ctrb(A,B))确认阶数再用lqr(A, B, Q, R)时Q 取单位阵、R 取 1 是不错的起点然后按「调 Q 加快响应、调 R 省控制能量」的方向微调。从那以后我每次拿到新的控制系统源码包都强制走一遍「路径检查 → 最小验证 → 指标自算 → 裕度落表」这四步哪怕包看起来很简单也不跳。希望帮到你。本文还有配套的精品资源点击获取