MATLAB阶跃响应怎么量:别把最后一个采样点当成稳态
曲线"看起来平了"至少包含两个问题:相对于输出真实稳态,动态响应有多快?相对于指令目标,跟踪是否达标?存在静差时,这两个问题会给出不同答案。本文把参考值和观察时长显式传入,避免末样本悄悄改变判断。
环境、运行与真实结果
实测环境:MATLAB R2025b(25.2.0.2998904),Windows PCWIN64,2026-10-19。主实验与独立复核仅依赖基础MATLAB;没有用Python/Octave结果代替MATLAB运行。
关键结果:一阶τ=0.8 s时,上升时间1.75777943548 s;截短至3.5 s后,虽然3.13 s已经入带,却因尾段不足0.5 s而不确认稳定。
把下方完整代码保存为 finite_step_demo.m,在该目录运行:
matlab
r = finite_step_demo;
也可以从该目录执行 matlab -batch "r=finite_step_demo;"。本次实际批处理由总入口调用同一份demo函数;这里给出单篇直接调用方式,未另行声称启动过第二种独立进程。默认在当前源码旁的 results 目录保存PNG、CSV、MAT。
原理、接口与边界
接口 finite_step_metrics(t,y,yinit,target,minHold) 中 target 是调用者明确给出的归一化与误差带参考值;不是自动估计的最终样本。若它是指令目标,得到的是目标跟踪指标;若它是已知输出真实稳态,则得到相对于输出稳态的响应指标。系统存在稳态误差时,两者不能混称。
归一化 z=(y-yinit)/(target-yinit) 同时支持正、负阶跃。t 和 y 必须实数有限、等长,t 从 0 开始且严格递增;零幅度被拒绝;端点差和中间差必须仍为有限数,极端输入导致溢出时返回STEP:Scale错误,不能静默给出指标。第一次已观察到的 10%/90% 上穿在相邻样本间作线性估计。记录开始已经超过某阈值时,该阈值的首次上穿不可知,返回 NaN;若恰好等于阈值,只能记录该观测时刻,不能证明更早历史。
超调为记录中 max(0,max(z)-1)×100%,是采样峰值。2% 判定要求从候选样本起,所有剩余观测样本均在带内,且尾段持续至少 minHold。本例 minHold=0.5 s;confirmedAt 是达到最短观测时长的首个采样时刻。该附加条件是本例约定,不等同 stepinfo 的算法。它不证明样本间连续轨迹或记录结束后的稳定性。稀疏时间戳或丢帧需另行质量控制。
| 场景 | 上升时间/s | 已观察带内尾段起点/s | 解释 |
|---|---|---|---|
| 一阶,τ=0.8,2→5 | 1.75777943548 | 3.130 | 解析上升时间 τ ln9=1.75777966187 |
| 负阶跃,4→1 | 1.75777943548 | 3.130 | 有符号归一化结果一致 |
| 欠阻尼,ζ=0.3,ωn=5 | 0.264272270022 | 2.248 | 采样超调 37.2324095976% |
| 记录截到 3.5 s | 同一阶 | NaN | 候选 3.13 s,尾长仅 0.37 s |
| 错把截断记录最后值当参考 | 1.67303942083 | 2.746 | 会改变指标并产生误导 |
| 仅看 0--5 s / 延长到 8 s | 同一阶 | 3.130 / 6.302 | 6--6.3 s 的后来扰动推翻早期推断 |
新增稳态误差反例 y=2+2.4(1-exp(-t/0.8)):真实最终输出是 4.4,指令目标是 5。以 5 为参考,最终归一化只有 80%,所以 90% 上穿与带内尾段都未达到;以已知真实稳态 4.4 为参考,上升时间约 1.75777943548 s,观测带内尾段起点 3.13 s。两种计算都合理,但回答的问题不同。CSV 中 steady_error_vs_command 和 steady_error_vs_output_final 记录了这一差异。
解析复核覆盖三个时间常数与两个方向;欠阻尼峰值/峰值时间用闭式公式核对;带内尾段另用逐个候选检查整个后缀的方法复核。图 finite_step.png 展示方向、超调、截断与后续扰动;稳态误差反例另见 CSV/MAT。
看图定位错误

左上在物理量坐标比较正负阶跃;右上用归一化进度比较一阶与欠阻尼响应;左下突出截断记录的带内尾长不足0.5 s;右下说明后来的扰动能推翻短记录给出的观察结论。图中"已确认"仅指已记录样本与最短尾长条件,不意味着未来永久稳定。
接入自己的采样日志时,先找阶跃触发点并把t归零,确认时间严格递增;指定初值和参考终值,不从任意末点估计。若原始日志含缺样,不要简单删除后仍称完整连续记录;本例拒绝非有限值,数据质量应在计算前独立处理。记录开始已越过阈值时,不倒推看不到的上穿时刻。
完整可复制代码
以下代码包含全部局部函数、实验输入、断言与出图,不依赖另外下载的辅助脚本。不要把局部函数单独从外部调用;修改数据时在同一demo入口中调用它。
matlab
function r = finite_step_demo(outDir)
% Finite-record step metrics with explicit initial/target values; base MATLAB.
if nargin<1,outDir=fullfile(fileparts(mfilename('fullpath')),'results');end
if ~exist(outDir,'dir'),mkdir(outDir);end
t=(0:0.002:8)';tau=0.8;holdTime=0.5;
z1=1-exp(-t/tau);
zeta=0.3;wn=5;wd=wn*sqrt(1-zeta^2);
z2=1-exp(-zeta*wn*t).*(cos(wd*t)+zeta/sqrt(1-zeta^2)*sin(wd*t));
positive=2+3*z1;negative=4-3*z1;ringing=2+3*z2;
short=(t<=3.5);prefix=(t<=5);
disturbed=z1;disturbed(t>=6 & t<=6.3)=disturbed(t>=6 & t<=6.3)+0.06;
cases={'first_order','underdamped','negative_step','truncated', ...
'wrong_last_as_target','prefix_before_disturbance','extended_after_disturbance', ...
'steady_error_vs_command','steady_error_vs_output_final'};
metrics={finite_step_metrics(t,positive,2,5,holdTime), ...
finite_step_metrics(t,ringing,2,5,holdTime), ...
finite_step_metrics(t,negative,4,1,holdTime), ...
finite_step_metrics(t(short),positive(short),2,5,holdTime), ...
finite_step_metrics(t(short),positive(short),2,positive(find(short,1,'last')),holdTime), ...
finite_step_metrics(t(prefix),2+3*disturbed(prefix),2,5,holdTime), ...
finite_step_metrics(t,2+3*disturbed,2,5,holdTime), ...
finite_step_metrics(t,2+2.4*z1,2,5,holdTime), ...
finite_step_metrics(t,2+2.4*z1,2,4.4,holdTime)};
analyticRise=tau*log(9);analyticSettling=-tau*log(0.02);
analyticOvershoot=100*exp(-zeta*pi/sqrt(1-zeta^2));
assert(abs(metrics{1}.riseTime-analyticRise)<1e-5,'STEP:AnalyticRise','First-order rise differs.');
assert(metrics{1}.observedSettling>=analyticSettling && ...
metrics{1}.observedSettling-analyticSettling<=0.0021,'STEP:AnalyticSettle','Sampled settling differs.');
assert(abs(metrics{2}.overshootPercent-analyticOvershoot)<0.002,'STEP:Overshoot','Sampled peak differs.');
assert(abs(metrics{3}.riseTime-metrics{1}.riseTime)<1e-12 && ...
metrics{3}.observedSettling==metrics{1}.observedSettling,'STEP:Direction','Negative step normalization differs.');
assert(isnan(metrics{4}.observedSettling),'STEP:Truncation','Insufficient observation must be unresolved.');
assert(~isnan(metrics{5}.observedSettling) && ~isinf(metrics{5}.observedSettling),'STEP:FalseTarget','Last-sample target trap missing.');
assert(metrics{7}.observedSettling>metrics{6}.observedSettling+2,'STEP:Future','Later departure was not exposed.');
assert(isnan(metrics{8}.t90) && isnan(metrics{8}.observedSettling), ...
'STEP:TrackingError','An 80-percent final response never reaches command-based 90 percent.');
assert(abs(metrics{9}.riseTime-analyticRise)<1e-5 && metrics{9}.observedSettling==3.13, ...
'STEP:OutputFinal','Known true output final value changes the metric meaning.');
rows=cell(numel(cases),9);
for k=1:numel(cases)
a=metrics{k};rows(k,:)={cases{k},a.t10,a.t90,a.riseTime,a.overshootPercent, ...
a.candidateStart,a.observedSettling,a.confirmedAt,a.observedTailDuration};
end
tab=cell2table(rows,'VariableNames',{'Case','T10','T90','RiseTime','OvershootPercent', ...
'CandidateStart','ObservedSettling','ConfirmedAt','TailDuration'});
writetable(tab,fullfile(outDir,'finite_step_metrics.csv'));
fig=figure('Visible','off','Color','w','Position',[80 80 1150 850]);
subplot(2,2,1);plot(t,positive,'b-',t,negative,'m-');grid on;
xlabel('Time from step (s)');ylabel('Measured value');
legend('2 to 5','4 to 1');title('Explicit endpoints support both directions');
subplot(2,2,2);plot(t,z1,'b-',t,z2,'r-');hold on;
plot([0 8],[0.98 0.98],'k:',[0 8],[1.02 1.02],'k:');grid on;
xlabel('Time (s)');ylabel('Normalized progress');legend('First order','Underdamped','2% band','Location','best');
title('Rise crossings and sampled overshoot');
subplot(2,2,3);plot(t,z1,'b-');hold on;
plot(t(short),z1(short),'m-','LineWidth',1.5);
plot([2.5 4.2],[0.98 0.98],'k:');xlim([2.5 4.2]);ylim([0.95 1.01]);grid on;
xlabel('Time (s)');ylabel('Normalized progress');
title('At 3.5 s, in-band tail is shorter than 0.5 s');
subplot(2,2,4);plot(t,disturbed,'b-');hold on;
plot([0 8],[0.98 0.98],'k:',[0 8],[1.02 1.02],'k:');grid on;
xlim([3 8]);ylim([0.96 1.08]);xlabel('Time (s)');ylabel('Normalized progress');
title('A passed prefix cannot prove future stability');
set(findall(fig,'Type','axes'),'FontSize',10);
print(fig,fullfile(outDir,'finite_step.png'),'-dpng','-r150');close(fig);
r=struct('tau',tau,'samplePeriod',0.002,'minimumObservation',holdTime, ...
'analyticRise',analyticRise,'analyticSettling',analyticSettling, ...
'analyticOvershootPercent',analyticOvershoot,'cases',{cases},'metrics',{metrics});
save(fullfile(outDir,'finite_step.mat'),'r','t','positive','negative','ringing','disturbed','tab');
fprintf('STEP PASS rise=%.12g analytic=%.12g settling=%.12g overshoot=%.12g\n', ...
metrics{1}.riseTime,analyticRise,metrics{1}.observedSettling,metrics{2}.overshootPercent);
fprintf('STEP truncated=%.12g wrong_target=%.12g prefix=%.12g extended=%.12g\n', ...
metrics{4}.observedSettling,metrics{5}.observedSettling,metrics{6}.observedSettling,metrics{7}.observedSettling);
fprintf('STEP steady_error command_t90=%.12g command_settle=%.12g output_final_rise=%.12g output_final_settle=%.12g\n', ...
metrics{8}.t90,metrics{8}.observedSettling,metrics{9}.riseTime,metrics{9}.observedSettling);
end
function s = finite_step_metrics(t,y,yinit,target,minHold)
% target is the explicit normalization/band reference. If it is a command,
% these are tracking metrics. If it is a known true output final value,
% these are response metrics. Never silently replace it by the last sample.
% A sampled, finite-record contract, not a claim about unobserved future time.
validateattributes(t,{'double'},{'real','vector','finite','nonempty'});
validateattributes(y,{'double'},{'real','vector','finite','nonempty'});
validateattributes(yinit,{'double'},{'real','scalar','finite'});
validateattributes(target,{'double'},{'real','scalar','finite'});
validateattributes(minHold,{'double'},{'real','scalar','finite','positive'});
t=t(:);y=y(:);
assert(numel(t)==numel(y) && numel(t)>=2 && t(1)==0 && all(diff(t)>0), ...
'STEP:Time','Require equal lengths, t(1)=0 and strictly increasing sample times.');
amplitude=target-yinit;
assert(amplitude~=0,'STEP:ZeroAmplitude','A zero-amplitude step has no normalized metrics.');
assert(~isnan(amplitude) && ~isinf(amplitude),'STEP:Scale','Endpoint difference must remain finite.');
delta=y-yinit;
assert(all(~isnan(delta) & ~isinf(delta)),'STEP:Scale','Measurement differences must remain finite.');
z=delta/amplitude;
assert(all(~isnan(z) & ~isinf(z)),'STEP:Scale','Normalization must remain finite.');
t10=first_upcross(t,z,0.1);t90=first_upcross(t,z,0.9);
inside=abs(z-1)<=0.02;lastOutside=find(~inside,1,'last');
candidate=NaN;observed=NaN;confirmed=NaN;tail=0;
if isempty(lastOutside),start=1;else,start=lastOutside+1;end
if start<=numel(t)
candidate=t(start);tail=t(end)-candidate;
if tail>=minHold
observed=candidate;
j=find(t>=candidate+minHold,1,'first');confirmed=t(j);
end
end
s=struct('referenceMeaning','Explicit reference supplied by caller; command tracking and true output final response are distinct.', ...
'initial',yinit,'target',target,'t10',t10,'t90',t90, ...
'riseTime',t90-t10,'overshootPercent',100*max(0,max(z)-1), ...
'candidateStart',candidate,'observedSettling',observed,'confirmedAt',confirmed, ...
'observedTailDuration',tail,'recordEnd',t(end),'minimumObservation',minHold, ...
'interpretation','All remaining observed samples are within 2%; future behavior is unproven.');
end
function tc = first_upcross(t,z,level)
% Linear crossing estimate; no extrapolation when the record starts above it.
tc=NaN;
if z(1)>level,return;end
if z(1)==level,tc=t(1);return;end
k=find(z(1:end-1)<level & z(2:end)>=level,1,'first');
if ~isempty(k),tc=t(k)+(level-z(k))*(t(k+1)-t(k))/(z(k+1)-z(k));end
end
本次如何交叉核对
一阶模型用τln9和−τln0.02核对;欠阻尼超调用闭式公式核对;观察带内尾段还用另一种"逐个候选检查整个后缀"的算法复核。缺交点、零幅度、非法时间与非有限输入均有拒绝/未确认检查。正负方向都做了对照;最终补丁又加入端点差及中间差溢出的两项拒绝测试,重跑后正常实验结果保持不变。
这些是实际运行后的数值交叉检查;公开代码中的断言保证核心实验可直接复现。输出里的NaN表示该指标不能由当前记录确认,不应替换成0。
相关入口与复现问题
只把记录截短,哪些指标还能报告,哪些必须返回未确认?
官方资料
MathWorks stepinfo用于核对初值/终值和常见10%--90%、2%定义;本例未调用stepinfo,不要求Control System Toolbox,附加最短观察尾长也不等同它的全部算法。
资料核查于2026-10-19;当前官方页面可能显示较新版本,本文运行环境以实际R2025b记录为准。