简介:面向控制系统、自动化与信号处理方向的学习者和工程师,这份MATLAB/Simulink基础示例聚焦S平面上传递函数的可视化分析,帮助读者把传递函数、复频率域响应与三维曲面图联系起来理解。压缩包共2个文件,包含1个m脚本和1个txt说明文件,整体约2KB,体量轻巧;m脚本承担传递函数建模、频率范围设置、频率响应计算及三维图形绘制等核心内容,txt文件用于补充使用许可信息。已有1822人学习,说明该主题在课程实验与工程入门中有一定参考需求。读者可借助其中的脚本梳理tf函数用法、S平面零极点与频率响应的关系,并复现传递函数三维曲面图的生成过程,为控制系统动态特性分析和后续Simulink建模打基础。
1. 三张传递函数图,为什么值得单独拆一份脚本
Simulink 的模块拖得再顺,碰到“把这个传递函数的阶跃响应、Bode 图、Nyquist 图都画出来,并且和 Simulink 里跑的波形对上”还是会卡。卡的不是命令拼写,而是不知道时间向量给多密、频率范围取到哪、三张图之间怎么互相印证,以及仿真曲线和理论曲线对不上时先怀疑哪一步。这份 MATLAB 资源就冲这件事来:三个典型传递函数,一套绘图脚本,一个 Simulink 对照模型,把建模、画图、回收仿真数据、定量验证串成一条能照着敲的线。它适合刚补完 Simulink 基础、正准备做控制类课程设计的人,也适合手上有现成模型但说不清稳定裕度从哪儿来的人。
2. 建模入口:tf、zpk、ss 三种写法谁更适合画图
传递函数图能不能画准,八成的锅其实在建模这一步就埋下了。系数顺序写反、零极点该消的没消、时延挂错位置,后面调半天画图参数都是白忙。先把三个被控对象用三种不同写法立起来,后面所有图都基于它们,这样出了问题能快速定位是模型本身歪了还是绘图指令用错了。
2.1 系数向量写法:从分母降幂开始
最常见的入口是tf(num, den),两个行向量分别给分子和分母的系数,顺序必须是降幂,缺项补零。新手翻车最多的地方是分母[1 3 2]被写成[1 2 3],画出来的曲线照样光滑,只是对应的极点位置完全变了,光看图很难察觉。
% 三个典型对象,后面的图都用它们 G1 = tf(1, [1 0.6 1]); % 二阶欠阻尼,zeta=0.3, wn=1 G2 = tf([2 1], [1 3 2]); % 一阶零点配二阶极点 G3 = tf(5, [1 1 0]); % 含积分环节,阶跃下会爬升 % 检查极点位置,确认系数没写错 disp(pole(G1)) % 应为 -0.3 ± 0.9539i disp(zero(G2)) % 应为 -0.5 disp(pole(G2)) % 应为 -1 和 -2pole和zero这两个函数是建模阶段最便宜的后悔药,写完立刻打印一下,比画完图再回头怀疑省事得多。G1的阻尼比 0.3、自然频率 1,是我习惯用来当“有明显超调但不至于振荡到看不清”的参照对象;G2带一个左半平面零点,用来观察零点对相位和超调的拉扯;G3分母常数项为 0,属于典型非真分式之外的另一种特殊情况——积分环节会让低频增益趋于无穷,Bode 图低频段是一条 -20dB/dec 的斜线,正好用来验证频率范围有没有取对。
提示:
tf不会自动约分。如果分子分母有公因式,先跑一遍minreal,否则零极点对消会在 Nyquist 图上伪装成一次包围,给出完全错误的稳定性结论。
2.2 零极点增益与状态空间:什么时候必须换写法
zpk的写法是zpk(z, p, k),直接给零点、极点、增益,从频域角度看反而更直观——Bode 图的每一个转折点对应一个极点或零点,斜率变化一目了然。做频域设计时我一般先用zpk把对象描述清楚,再转成tf交给绘图函数,因为bode、nyquist对两种写法都认,但zpk形式在手工核对转折频率时不容易看错。
% 零极点增益写法:零点 -0.5,极点 -1/-2,增益 2 Gz = zpk(-0.5, [-1 -2], 2); % 转成传递函数形式,核对系数是否与 G2 一致 Gt = tf(Gz); disp(Gt) % 状态空间形式,适合后续做能控能观分析 Gss = ss(G1); disp(size(Gss.A)) % 2x2,二阶系统两个状态ss这条路径在只画三张图时用得不多,但有两种场景绕不开:一是对象本身来自机理建模,天生是状态方程;二是分子阶数接近分母阶数时,tf的数值精度会开始抖动,转成状态空间再算反而更稳。ss(G1)之后可以用tf(Gss)转回来对照,两个方向都跑一遍,系数对得上说明这条链路没有精度损失。
需要留意的是,状态空间转传递函数在极点密集时会掉精度,尤其是高阶级联系统。三张图的场景里二阶、三阶还不至于出问题,但如果后面要把多个模块串起来做闭环,minreal该加还是得加。
2.3 时延与离散采样时间怎么挂上去
连续系统画图不需要额外参数,但一旦对象带传输时延,就必须在建模阶段说清楚,否则 Simulink 和脚本会给出两条不一样的曲线。tf的时延用属性名挂载,不要手工乘一个exp(-s*T),那样得到的模型在step里会被当成无理函数处理,绘图指令会报错或者悄悄降精度。
% 输入时延挂在传递函数属性上 Gd = tf(5, [1 1], 'InputDelay', 0.2); % 离散化:采样时间 0.01 秒,零阶保持器 Gdz = c2d(Gd, 0.01, 'zoh'); % 检查离散前后的直流增益是否一致 disp([dcgain(Gd), dcgain(Gdz)])InputDelay和OutputDelay的区别在闭环里会体现出来:输入时延可以靠前馈补偿,输出时延只能靠反馈预估,画开环三张图时两者形状一样,但接成闭环后相位裕度差得不少。采样时间这个参数很多人随手写,实际上它决定了离散 Bode 图的频率上限——奈奎斯特频率是采样率的一半,超过这个范围画出来的东西没有物理意义。c2d的第三个参数'zoh'对应零阶保持器,和 Simulink 里默认的采样保持行为一致,两边对照时用同一个离散化方法才不会出现“脚本对、仿真错”的假象。
3. 三张图分开画:step、bode、nyquist 的关键参数
模型立住之后才轮到绘图。这三个函数都有一堆默认行为,默认值在演示里够用,一旦要拿图去做判断就必须显式给参数。下面按“时间域—频率域—复平面”的顺序拆,每一张都给出我实际会用的参数组合,以及参数改了之后图会怎么变。
3.1 阶跃响应:时间向量决定你能看到什么
step(G)不带参数时会自动选时间范围,规则是让响应基本到达稳态。这个自动策略在欠阻尼系统上偶尔会截断振荡尾巴,超调量算出来偏小。我一般显式给时间向量,步长取系统最快时间常数的五十分之一左右,既不会糊掉上升沿,也不至于生成几十万个点拖慢绘图。
% 显式时间向量:0 到 15 秒,步长 0.005 t = 0:0.005:15; % 拿响应数据,同时保留时间轴 [y1, t1] = step(G1, t); % 定量指标:超调量、上升时间、调节时间 S1 = stepinfo(G1); fprintf('超调 %.2f%%,上升时间 %.3f s,调节时间 %.3f s\n', ... S1.Overshoot, S1.RiseTime, S1.SettlingTime); % 三个对象画在一张图上对比 figure; step(G1, G2, G3, t); grid on; legend('G1 二阶欠阻尼', 'G2 含零点', 'G3 含积分');时间向量的下界也有讲究。从 0 开始是为了看清初始时刻的斜率,如果对象带时延,起始那段平坦区正好用来验证时延值对不对。stepinfo的调节时间默认按 2% 误差带算,做课程设计时常被要求用 5%,改法是把SettlingTimeThreshold传进去,默认值和你需要的口径不一致时,报出来的数字会差一截。
G3是积分环节,阶跃输入下输出一直线性爬升,stepinfo对它返回的调节时间是NaN,这不是函数坏了,而是这类对象本来就没有稳态。看到NaN先确认对象里是不是有1/s,别急着改代码。
3.2 Bode 图:频率范围与 mag/phase 的输出格式
bode(G)的自动选频会覆盖所有极零点的模长,但对高频衰减特别快的系统,低频段会被压缩得看不清。手工给频率向量是更稳的做法:把所有极点、零点模长取出来,向两端各扩两个数量级,中间用对数等距采样。
% 按极零点模长自动构造频率范围 p = abs(pole(G1)); z = abs(zero(G1)); allc = [p; z]; allc = allc(allc > 0); w = logspace(log10(min(allc))-2, log10(max(allc))+2, 500); % 画幅频和相频 figure; bode(G1, w); grid on; % 取数值:注意 mag 是 1x1xN 三维数组 [mag, phase, wout] = bode(G1, w); mag = squeeze(mag); phase = squeeze(phase); disp([wout(1), 20*log10(mag(1))]) % 低频增益,单位 dBsqueeze这一步是血泪经验。单输入单输出系统的bode输出是三维数组,直接拿去plot会报维度不匹配,或者画出一条莫名其妙的折线。用squeeze压成列向量之后,才能自己算 -3dB 带宽、算穿越频率、做二次加工。
频率范围的另一头是高频。如果对象分子阶数比分母低两阶以上,高频段会以 -40dB/dec 往下掉,logspace上界取太高会让图变成一片空白,取太低又看不到衰减趋势。我一般先按极零点范围画一次,看看曲线形状,再决定要不要把上界再推一个数量级。
margin是这张图的常用搭档,它返回幅值裕度、相位裕度、对应的两个频率:
[Gm, Pm, Wcg, Wcp] = margin(G1); fprintf('幅值裕度 %.2f dB,相位裕度 %.2f 度\n', 20*log10(Gm), Pm);注意Gm是绝对增益不是分贝,忘了取20*log10会得到一个看着像 dB 其实是倍数的数字,这个坑在写报告时特别容易漏。
3.3 Nyquist 图与根轨迹:判断稳定性的两种视角
Nyquist 图的价值在于它能直接回答“闭环稳不稳”,但前提是频率范围给对。默认范围可能漏掉高频段绕向原点的部分,看起来没包围 -1 点,实际上包围了。
% 限定频率范围,避免默认选频截断 figure; nyquist(G1, {0.01, 1000}); grid on; % 根轨迹视角:闭环极点随增益移动 figure; rlocus(G1); grid on; % 闭环阶跃,直接看时域 Gcl = feedback(G1, 1); stepinfo(Gcl)nyquist的花括号参数是频率上下限,单位 rad/s。下限要低于所有极点的模长,上限要高于它们,否则曲线两端会被截断,看起来像一条开口的弧。图上那条 -1 点是否被包围,需要配合pole的实部符号一起看:开环有右半平面极点时,包围方向和常规情况相反,只凭图判断容易得出相反结论。
rlocus从另一个角度回答同一个问题——闭环极点随增益怎么跑。三个对象里G1的根轨迹从两个共轭复极点出发,增益加大后向左半平面弯曲,和feedback之后的阶跃响应超调量变化趋势能对上。这两种视角对照着看,比单独盯着 Nyquist 图猜要可靠得多。
4. 从脚本搬进 Simulink:Transfer Fcn 模块与数据回收
脚本画图快,但课程设计往往要求在 Simulink 里搭出同样的系统并给出波形。两边对不上是最常见的求助场景,问题大多不在模型本身,而在求解器配置和数据回收格式。这一章把“模块参数怎么填、模型怎么用脚本搭、仿真数据怎么回到工作区再画一遍”走完整。
4.1 Transfer Fcn 模块的分子分母系数与求解器
Transfer Fcn 模块的两个参数分别是 Numerator coefficients 和 Denominator coefficients,填法和tf一模一样,降幂向量,用空格或逗号分隔。区别在于这里填的是字符串,脚本里用set_param赋值时要加引号。
求解器的选择直接影响曲线和理论值的贴合度。线性连续系统没有不连续点,用定步长ode4最稳,步长取 0.001 秒,画出来的曲线和step几乎重合。默认的变步长求解器在过零点附近会自动缩步长,虽然精度够,但输出的时间点不均匀,拿去和等间隔的理论曲线对比时会多出插值误差。
% 新建模型并放置模块 mdl = 'tf_three_demo'; new_system(mdl); open_system(mdl); add_block('simulink/Sources/Step', [mdl '/Step'], ... 'Time', '0', 'Before', '0', 'After', '1'); add_block('simulink/Continuous/Transfer Fcn', [mdl '/G1'], ... 'Numerator', '[1]', 'Denominator', '[1 0.6 1]'); add_block('simulink/Sinks/To Workspace', [mdl '/y'], ... 'VariableName', 'yout', 'SaveFormat', 'Structure With Time'); add_block('simulink/Sinks/Scope', [mdl '/Scope']); % 连线 add_line(mdl, 'Step/1', 'G1/1'); add_line(mdl, 'G1/1', 'y/1'); add_line(mdl, 'G1/1', 'Scope/1'); % 求解器配置:定步长 ode4,步长 1ms set_param(mdl, 'StopTime', '15', ... 'Solver', 'ode4', ... 'FixedStep', '0.001', ... 'SaveFormat', 'Structure With Time');'Time'、'Before'、'After'是阶跃源模块的三个参数,分别对应跳变时刻、跳变前取值、跳变后取值。做单位阶跃就把后两者设成 0 和 1。To Workspace的SaveFormat有四种,Structure With Time最通用,里面同时有time和signals.values;Timeseries格式在新版本里更省事,字段名是Time和Data,两种都能用,但同一个项目里最好统一,混着写迟早会取错字段。
注意:Transfer Fcn 模块不允许分子阶数高于分母阶数。如果对象的分子分母同阶,必须先串一个极小时间常数的一阶环节,或者改用状态空间模块,否则仿真直接报错,而且报错信息不会告诉你问题出在阶数上。
4.2 用脚本跑仿真并回收数据
模型搭好之后用sim跑,输出是一个Simulink.SimulationOutput对象,工作区变量从里面按名字取。这一步最容易踩的坑是变量的作用域——To Workspace写到的是仿真输出对象里,不是基础工作区。
% 跑仿真 out = sim(mdl); % 从输出对象里取回数据 yout = out.yout; t_sim = yout.time; y_sim = yout.signals.values; % 和理论阶跃响应叠在一起 [y_ref, t_ref] = step(G1, t_sim); figure; plot(t_sim, y_sim, 'b-', 'LineWidth', 1.5); hold on; plot(t_ref, y_ref, 'r--', 'LineWidth', 1.2); grid on; legend('Simulink 仿真', 'step 理论值'); xlabel('时间 (s)'); ylabel('幅值');两条曲线叠上去之后,差异一般集中在三个位置:起始段的斜率、峰值附近的超调量、稳态值。起始段斜率不对,多半是求解器步长偏大或者阶跃时刻和采样点没对齐;峰值超调量对不上但稳态一致,通常是阻尼比的数值精度问题,把0.6写成0.6001都能在图上看出差别;稳态值不一致则要回头查直流增益,dcgain(G1)和仿真末值应该严格相等。
step的返回时间向量和仿真时间向量长度不同是正常的,两个都是按各自的采样规则生成的。真要逐点做差,得先把其中一条插值到另一条的时间轴上,interp1一行就够,但插值会引入额外误差,做精度对比时不如直接把仿真步长设成和理论时间向量一致。
4.3 三个对象批量对照
单个对象对上了,把三个都跑一遍才算把这条链路走通。手工改模块参数太慢,用set_param在循环里换系数更省事:
cases = { '[1]', '[1 0.6 1]', G1; '[2 1]', '[1 3 2]', G2; '[5]', '[1 1 0]', G3 }; figure; hold on; for k = 1:size(cases, 1) set_param([mdl '/G1'], 'Numerator', cases{k,1}, ... 'Denominator', cases{k,2}); out = sim(mdl); yk = out.yout.signals.values; tk = out.yout.time; plot(tk, yk, 'DisplayName', sprintf('case %d', k)); end grid on; legend('show');循环里每次sim都会重新编译模型,三个对象跑下来几秒钟,比手工点开模块改参数快得多。cases这个元胞数组的第三列存的是理论模型,本来想拿来做自动对比,实际用的时候发现仿真步长和理论时间向量对不齐,自动算误差反而添乱,最后还是靠肉眼叠图。这个取舍看个人习惯,做批量回归时值当,单次验证没必要。
5. 避坑清单:传递函数图对不上的五个现场
下面这几条都是我在改别人模型时反复遇到的,每条按现象、原因、解决三步写,对着排查能省不少时间。
5.1 阶跃曲线起点是斜的,超调量比理论值小一截
现象:step画出来的曲线在 t=0 附近不是从零斜率平滑上升,而是先有一段接近直线的爬坡,峰值也偏低。
原因:时间向量步长太大。自动选时间时,如果系统时间常数很小,采样点会跨过上升沿最快的部分,画出来的折线把峰值削平了。
解决:显式给时间向量,步长取最小时间常数的五十分之一以内。二阶系统的最小时间常数约等于1/(zeta*wn),G1算下来是 3.3 秒量级,但上升沿的细节在 0.1 秒内,步长给 0.005 才够。步长再小到 0.0005 也不会更准,只会让文件变大。
5.2 Bode 图低频段斜率不对,转折点位置偏移
现象:幅频曲线在低频段不是预期的 -20dB/dec 或 0dB/dec,转折点的横坐标和手算的极点模长对不上。
原因:频率范围给错。bode的自动选频在极点模长跨度大时会漏掉中间段,手工给的logspace上下界如果没覆盖所有极零点,曲线两端会被截断,看起来像斜率变了。
解决:先abs(pole(G))和abs(zero(G))把模长列出来,取最小值和最大值各向两端扩两个数量级。G3有个极点正好在原点,模长为 0,构造范围时要把它排除掉,否则log10(0)直接返回-Inf,logspace出来的向量全是NaN。
5.3 Simulink 波形滞后于理论曲线,整体右移
现象:两条曲线形状一致,仿真那条整体晚了一小段,稳态值对得上,就是时间轴错位。
原因:阶跃源的跳变时刻和求解器采样点没对齐,或者对象带了时延但 Transfer Fcn 里没配,Simulink 不会自动从tf的InputDelay属性继承时延。
解决:阶跃源的Time参数设成 0,定步长求解器的步长取时延的十分之一以下;有时延就在模块后面串一个 Transport Delay,延迟值填同一个数。查的时候先看阶跃源本身的输出,确认它在 t=0 就跳了,再往后找。
5.4 Nyquist 图看着没包围 -1 点,闭环其实已经发散
现象:Nyquist 曲线绕开 -1 点,但feedback(G,1)之后阶跃响应发散。
原因:开环有右半平面极点,包围判据的方向要反过来算;或者频率范围下限太高,把低频段那段关键绕行截掉了。
解决:先pole(G)看实部有没有正值,有的话判据要按广义 Nyquist 处理;频率下限压到最小极点模长的十分之一以下。判稳这件事别只信图,pole(feedback(G,1))打印一下闭环极点实部,正的一目了然。
5.5 分子分母同阶,模块报错或曲线在 t=0 出现冲激
现象:Transfer Fcn 模块一跑就报参数非法,或者曲线在 t=0 处有一个极高极窄的尖峰。
原因:分子阶数等于或高于分母阶数,属于非真分式,物理上对应纯微分,Simulink 的连续模块不支持。
解决:串一个时间常数很小的一阶惯性环节1/(0.001*s+1)把它变成真分式,或者改用状态空间模块加D矩阵。前者会引入一点高频衰减,做频域分析时要把这个附加环节的影响算进去,别拿着带滤波的结果去和理论 Bode 图逐点比。
6. 进阶验证:用 stepinfo 与 margin 把图变成数字
图看多了会有错觉,觉得曲线形状对就算对了。真要做判断,得把图上的特征转成数字,再和解析公式对一遍。二阶系统的超调量有闭式解exp(-pi*zeta/sqrt(1-zeta^2)),拿它和stepinfo的输出比,误差应该在千分之一量级;对不上就说明模型不是标准二阶,或者时间向量精度不够。
zeta_list = [0.2 0.4 0.7 1.0]; t = 0:0.002:20; figure; hold on; for k = 1:numel(zeta_list) z = zeta_list(k); G = tf(1, [1 2*z 1]); [y, tt] = step(G, t); plot(tt, y, 'DisplayName', sprintf('\\zeta=%.1f', z)); S = stepinfo(G); theory = exp(-pi*z/sqrt(1-z^2)) * 100; % 理论超调百分比 fprintf('zeta=%.1f 实测 %.2f%% 理论 %.2f%%\n', ... z, S.Overshoot, theory); end grid on; legend('show'); xlabel('时间 (s)'); ylabel('幅值');zeta取 1.0 时理论公式分母为零,会返回NaN或者复数,这是临界阻尼的边界情况,代码里加个判断跳过就行。实测值在zeta接近 1 时会略大于零,因为数值求解没有严格到达临界状态,这个偏差是正常的,别当成 bug。
频率域这边用margin拿到穿越频率和稳定裕度,再做一次扫参,看阻尼比变化时穿越频率怎么移动:
w = logspace(-1, 1, 300); figure; hold on; for k = 1:numel(zeta_list) G = tf(1, [1 2*zeta_list(k) 1]); [Gm, Pm, Wcg, Wcp] = margin(G); bodemag(G, w, 'DisplayName', sprintf('\\zeta=%.1f', zeta_list(k))); fprintf('zeta=%.1f 相位裕度 %.1f 度 穿越频率 %.3f rad/s\n', ... zeta_list(k), Pm, Wcp); end grid on; legend('show');跑出来会看到相位裕度随阻尼比单调上升,穿越频率基本不动——因为自然频率没变,增益也没变。这个结论可以用来反查:如果扫出来的穿越频率跟着阻尼比一起漂,说明对象里混进了别的极点,模型和你以为的不是同一个。
出图归档有个小技巧,exportgraphics比saveas清晰得多,尤其适合把三张图拼进报告:
exportgraphics(gcf, 'bode_sweep.png', 'Resolution', 200);分辨率给 200 是打印和屏幕都够用的折中值,再高文件会大得离谱,插进文档还得压缩。图片文件名带上参数值,比如bode_zeta0.2.png,后面回头找图时不用一个个点开看。
从那以后我每次交图前都强制走一遍“三图叠一组、数字对一遍”的流程:先stepinfo和解析解比,再margin和手算的穿越频率比,两个都对上才敢把图放进报告。图好看不等于对,这是被返工逼出来的习惯。希望帮到你。
本文还有配套的精品资源,点击获取