输入缓慢变化时,系统可能跟得上;变化很快时,输出往往变小且滞后。频率响应把这种差异按频率展开,帮助解释反馈环路的稳定余量。先读传递函数与极点。
用一条正弦探测系统
对稳定 LTI 对象,施加 u(t)=Asinωt,初始瞬态衰减后,输出为
y(t)=A∣P(jω)∣sin[ωt+argP(jω)].
幅值比告诉我们振幅如何变化,相位告诉我们输出在周期中的偏移。频率 ω 用 rad/s,普通频率 f=ω/(2π) 用 Hz。热对象 P(s)=50/(100s+1) 有
∣P(jω)∣=1+(100ω)250,argP(jω)=−arctan(100ω).
当 ω=0.01 rad/s时,幅值比为 50/2≈35.36,相位为−45°。这不是“所有信号都固定延迟某个时间”:相位与频率的关系决定了不同频率分量的相对变化。
从对象转向环路增益
稳定性分析关注闭环分母 1+L(s),其中 L=PC。取归一化示例
L(s)=s+12e−τds.
这里对象的时间常数是1 s,与前面的100 s温控参数不同;这样可以直接看清延迟的影响。无延迟时闭环极点为−3。纯延迟的频率响应为 e−jωτd,幅值始终1,相位减少 ωτd 弧度。
增益交越频率满足 ∣L(jωc)∣=1,所以 ωc=3≈1.732 rad/s。无延迟时该处相位为−60°,相位裕度为120°。加入延迟后:
PM=120∘−ωcτdπ180∘.
τd=0.5 s时约70.4°;首次到达零裕度的延迟约1.209 s。在这个开环稳定、单增益交越的特定模型中,这是首次失稳边界。它不能作为任意环路的通用延迟公式。
正在呈现知识画面
频率响应与稳定裕度 · 实验环路L(s)=2e^(−τs)/(s+1)。虚线竖标记为增益交越√3 rad/s;纯延迟保持幅值、减少相位。正弦图展示环路本身的稳态响应,不是闭环阶跃。
{"id":"control-06","title":"频率响应与稳定裕度 · 实验","summary":"环路L(s)=2e^(−τs)/(s+1)。虚线竖标记为增益交越√3 rad/s;纯延迟保持幅值、减少相位。正弦图展示环路本身的稳态响应,不是闭环阶跃。","height":1050,"html":"<p class=\"intro\" data-i18n=\"scope\"></p><div class=\"controls\"><div class=\"control\"><label for=\"parameter-0\" data-i18n=\"delayControl\"></label><output id=\"value-0\" for=\"parameter-0\"></output><input type=\"range\" id=\"parameter-0\" data-parameter=\"0\" min=\"0\" max=\"1.5\" step=\"0.05\" value=\"0.5\"/></div><div class=\"control\"><label for=\"parameter-1\" data-i18n=\"probe\"></label><output id=\"value-1\" for=\"parameter-1\"></output><input type=\"range\" id=\"parameter-1\" data-parameter=\"1\" min=\"0.1\" max=\"5\" step=\"0.1\" value=\"1.7\"/></div></div><div class=\"tools\"><button id=\"reset\" data-i18n=\"reset\"></button></div><div id=\"plots\"></div><div id=\"metrics\"></div><p id=\"status\" role=\"status\" aria-live=\"polite\"></p>","css":".plotarea{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px}.yticks{display:flex;flex-direction:column;justify-content:space-between;padding:3px 0;font-size:13px;font-variant-numeric:tabular-nums;color:var(--muted)}*{box-sizing:border-box}.intro{margin:0 0 16px;color:var(--muted);line-height:1.65}.controls{display:grid;gap:14px}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center}.control label,.control > span{font-size:16px;line-height:1.5}.control input{grid-column:1/-1;width:100%;min-height:28px}.control output{font-variant-numeric:tabular-nums}.control:has(.presets){grid-template-columns:1fr}.control .presets{grid-column:1/-1;display:flex;flex-wrap:wrap;width:100%;min-width:0}.tools{margin:14px 0}.tools button{font:inherit;padding:8px 14px;min-height:42px}.chart{margin:22px 0}.chart h3{font-size:16px;margin:0 0 6px;line-height:1.5}.chart svg{display:block;width:100%;height:160px;overflow:hidden}.scale,.axes{font-size:14px;color:var(--muted);font-variant-numeric:tabular-nums}.axes{display:flex;justify-content:space-between;align-items:start;gap:8px;margin-top:6px}.axes span:nth-child(2){text-align:center;flex:1;min-width:0}.legend{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:8px;font-size:15px}.legend span{display:inline-flex;align-items:center;gap:7px}.legend i{display:inline-block;width:22px;flex-shrink:0}#metrics{border-top:1px solid var(--rule);padding-top:12px;display:grid;gap:8px}#metrics>div{display:flex;justify-content:space-between;gap:16px;font-size:15px;line-height:1.5}#metrics strong{font-weight:600;font-variant-numeric:tabular-nums;flex-shrink:0}#status{font-size:14px;color:var(--muted);min-height:3em;margin:14px 0 0}button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}@media(max-width:420px){.chart svg{height:145px}#metrics>div{flex-wrap:wrap;gap:3px 12px}}","js":"const MODEL_ID=6, DEFAULTS=[0.5, 1.7], METRICS=[\"crossover\", \"phaseMargin\", \"amplitude\"];\n/* Deterministic teaching models. Time and integration are independent of rendering. */\nfunction controlModel(id, p) {\n const clip=(x,a,b)=>Math.max(a,Math.min(b,x));\n const rk4=(x,t,h,f)=>{const a=f(x,t),b=f(x.map((v,i)=>v+h*a[i]/2),t+h/2),c=f(x.map((v,i)=>v+h*b[i]/2),t+h/2),d=f(x.map((v,i)=>v+h*c[i]),t+h);return x.map((v,i)=>v+h*(a[i]+2*b[i]+2*c[i]+d[i])/6);};\n const result={plots:[],metrics:{}};\n const series=(name,points)=>({name,points});\n const graph=(title,curves,extra={})=>result.plots.push({title,curves,...extra});\n if(id===1){\n const amb=p[0], kp=p[1], times=Array.from({length:401},(_,i)=>i*2), temps=[], actions=[];\n for(let mode=0;mode<3;mode++){\n let eq=mode===0?amb+20:mode===1?(amb+20+50*kp*40)/(1+50*kp):40;\n let tau=mode===1?100/(1+50*kp):100;\n const temp=times.map(t=>[t,t<=100?40:eq+(40-eq)*Math.exp(-(t-100)/tau)]);\n temps.push(series(['open','feedback','feedforward'][mode],temp));\n actions.push(series(['open','feedback','feedforward'][mode],temp.map(([t,y])=>[t,t<100?.4:mode===0?.4:mode===1?.4+kp*(40-y):(40-amb)/50])));\n }\n graph('temperature',temps,{event:100});graph('action',actions,{event:100});\n result.metrics={openFinal:amb+20,feedbackFinal:(amb+20+50*kp*40)/(1+50*kp),feedforwardFinal:40};\n } else if(id===2){\n const cap=p[0], loss=p[1], tau=cap/loss, eq=20+400/loss;\n const curve=Array.from({length:401},(_,i)=>{const t=i*2;return[t,eq+(20-eq)*Math.exp(-t/tau)];});\n graph('temperature',[series('temperature',curve),series('equilibrium',[[0,eq],[800,eq]])]);\n graph('netpower',[series('netpower',curve.map(([t,y])=>[t,400-loss*(y-20)]))]);\n result.metrics={tau,equilibriumFinal:eq,at100:eq+(20-eq)*Math.exp(-100/tau)};\n } else if(id===3){\n const z=p[0],w=p[1];let x=[0,0],ys=[],vs=[];const dt=.01;\n for(let j=0;j<=2000;j++){const t=j*dt;if(j%5===0){ys.push([t,x[0]]);vs.push([t,x[1]]);}x=rk4(x,t,dt,a=>[a[1],w*w*(1-a[0])-2*z*w*a[1]]);}\n graph('response',[series('response',ys),series('target',[[0,1],[20,1]])]);graph('velocity',[series('velocity',vs)]);\n result.metrics={zeta:z,omega:w,overshoot:z>0&&z<1?100*Math.exp(-Math.PI*z/Math.sqrt(1-z*z)):0,peakTime:z>0&&z<1?Math.PI/(w*Math.sqrt(1-z*z)):null,finalWindow:ys.at(-1)[1]};\n } else if(id===4 || id===8){\n const wind=id===8,mode=wind?p[0]:p[0],kp=wind?.04:p[1],ki=wind?.001:p[2],kd=!wind&&mode===2?1:0,tt=wind?p[1]:20,dt=p[3]||.2;\n let x=[40,0,40];const yy=[],req=[],act=[],pp=[],ii=[],dd=[];\n const target=t=>wind?(t<200?80:40):40;\n const ambient=t=>wind?20:(t<100?20:10);\n const read=(a,t)=>{const e=target(t)-a[0],P=kp*e,I=wind||mode>0?a[1]:0,D=-kd*(a[0]-a[2])/5,uc=.4+P+I+D,u=wind?clip(uc,0,1):uc;return{e,P,I,D,uc,u};};\n for(let j=0;j<=Math.round(800/dt);j++){\n const t=j*dt,r=read(x,t);\n if(j%Math.max(1,Math.round(2/dt))===0){yy.push([t,x[0]]);req.push([t,r.uc]);act.push([t,r.u]);pp.push([t,r.P]);ii.push([t,r.I]);dd.push([t,r.D]);}\n // Event occurs at a step boundary. Freeze exogenous inputs across this step.\n const ref=target(t+1e-8),amb=ambient(t+1e-8);\n x=rk4(x,t,dt,a=>{const e=ref-a[0],P=kp*e,I=wind||mode>0?a[1]:0,D=-kd*(a[0]-a[2])/5,uc=.4+P+I+D,u=wind?clip(uc,0,1):uc;return[(1000*u-20*(a[0]-amb))/2000,(wind||mode>0)?ki*e+(wind&&mode===1?(u-uc)/tt:0):0,(a[0]-a[2])/5];});\n }\n graph('temperature',[series('temperature',yy),series('target',wind?[[0,80],[200,80],[200,40],[800,40]]:[[0,40],[800,40]])],{event:wind?200:100});\n graph('action',[series('request',req),series('applied',act)],{event:wind?200:100});\n graph('components',[series('proportional',pp),series('integral',ii),series('derivative',dd)],{event:wind?200:100});\n result.metrics={final:yy.at(-1)[1],integral:ii.at(-1)[1],maxRequest:Math.max(...req.map(x=>x[1])),minRequest:Math.min(...req.map(x=>x[1])),at300:yy.find(x=>Math.abs(x[0]-300)<.01)?.[1],physical:req.every(x=>x[1]>=0&&x[1]<=1)};\n } else if(id===5){\n const ki=p[0],b=.03,c=.5*ki,disc=b*b-4*c;\n const roots=disc>=0?[[-b/2+Math.sqrt(disc)/2,0],[-b/2-Math.sqrt(disc)/2,0]]:[[-b/2,Math.sqrt(-disc)/2],[-b/2,-Math.sqrt(-disc)/2]];\n graph('poleplane',[series('poles',roots)],{scatter:true,xrange:[-.035,.005],yrange:[-.04,.04]});\n result.metrics={real1:roots[0][0],imag1:roots[0][1],real2:roots[1][0],imag2:roots[1][1],discriminant:disc};\n } else if(id===6){\n const delay=p[0],omega=p[1],wc=Math.sqrt(3),mag=[],phase=[];\n for(let i=0;i<=240;i++){const l=-2+i/80,w=10**l;mag.push([l,20*Math.log10(2/Math.hypot(1,w))]);phase.push([l,(-Math.atan(w)-w*delay)*180/Math.PI]);}\n graph('magnitude',[series('loop',mag),series('zeroDb',[[-2,0],[1,0]])],{logx:true,event:Math.log10(wc)});\n graph('phase',[series('loop',phase),series('minus180',[[-2,-180],[1,-180]])],{logx:true,event:Math.log10(wc)});\n const amp=2/Math.hypot(1,omega),ph=-Math.atan(omega)-omega*delay,points=Array.from({length:241},(_,i)=>i*4*Math.PI/240/omega);\n graph('sine',[series('input',points.map(t=>[t,Math.sin(omega*t)])),series('output',points.map(t=>[t,amp*Math.sin(omega*t+ph)]))]);\n result.metrics={crossover:wc,phaseMargin:120-wc*delay*180/Math.PI,amplitude:amp,phase:ph*180/Math.PI,criticalDelay:2*Math.PI/(3*wc)};\n } else if(id===7){\n const ts=p[0],delay=p[1],a=Math.exp(-ts),b=1-a,K=2;let x=1,prev=1;const continuous=[],samples=[],us=[];\n for(let k=0;k*ts<12-1e-8;k++){\n const t=k*ts,u=-K*(delay?prev:x);samples.push([t,x]);\n for(let j=0;j<=12;j++){const h=Math.min(j*ts/12,12-t);if(h<0)break;continuous.push([t+h,Math.exp(-h)*x+(1-Math.exp(-h))*u]);if(t+h>=12)break;}\n us.push([t,u],[Math.min(t+ts,12),u]);const next=a*x+b*u;prev=x;x=next;\n }\n graph('state',[series('state',continuous),series('samples',samples)],{dots:1});graph('action',[series('held',us)]);\n const disc=a*a-4*b*K,roots=delay?(disc>=0?[(a+Math.sqrt(disc))/2,(a-Math.sqrt(disc))/2]:[Math.sqrt(b*K),Math.sqrt(b*K)]):[a-b*K];\n result.metrics={coefficient:a-b*K,radius:Math.max(...roots.map(Math.abs)),period:ts,delay,initial:1};\n } else if(id===9){\n const delay=p[0],window=p[1],clear=p[2];let q=0,n=2,command=2,pending=[],history=[],changes=0,cost=0,maxQ=0;const qs=[],ns=[],cs=[],ds=[],ls=[];\n for(let t=0;t<=240;t++){\n while(pending.length&&pending[0].at<=t)n=pending.shift().value;\n const rate=t>=20&&t<100?70:20;let desired=Math.min(12,Math.max(1,Math.ceil((rate+q/clear)/10)));\n if(t%10===0){history.push({t,value:desired});history=history.filter(x=>x.t>=t-window);let next=desired<command?Math.max(...history.map(x=>x.value)):desired;\n if(next!==command){command=next;changes++;if(delay===0)n=command;else pending.push({at:t+delay,value:command});}}\n qs.push([t,q]);ns.push([t,n]);cs.push([t,command]);ds.push([t,desired]);ls.push([t,rate]);maxQ=Math.max(q,maxQ);\n if(t<240){cost+=n;q=Math.max(0,q+rate-10*n);}\n }\n graph('backlog',[series('backlog',qs)]);graph('replicas',[series('available',ns),series('command',cs),series('suggested',ds)]);graph('rate',[series('arrival',ls),series('capacity',ns.map(([t,n])=>[t,n*10]))]);\n result.metrics={maxBacklog:maxQ,instanceSeconds:cost,changes,finalBacklog:q};\n } else if(id===10){\n const alpha=p[0],noise=p[1],dt=p[2]||.005;let x=[1,0,0,0];const pos=[],est=[],err=[],vel=[],ev=[],u=[];\n for(let j=0;j<=Math.round(10/dt);j++){\n const t=j*dt;if(j%Math.max(1,Math.round(.025/dt))===0){pos.push([t,x[0]]);est.push([t,x[2]]);err.push([t,x[1]-x[3]]);vel.push([t,x[1]]);ev.push([t,x[3]]);u.push([t,-2*x[2]-3*x[3]]);}\n x=rk4(x,t,dt,(a,t)=>{const input=-2*a[2]-3*a[3],innovation=a[0]+noise*Math.sin(30*t)-a[2];return[a[1],input,a[3]+2*alpha*innovation,input+alpha*alpha*innovation];});\n }\n graph('position',[series('actual',pos),series('estimate',est)]);graph('velocity',[series('actual',vel),series('estimate',ev)]);graph('action',[series('action',u)]);\n result.metrics={finalPosition:pos.at(-1)[1],finalVelocityError:err.at(-1)[1],peakAction:Math.max(...u.map(x=>Math.abs(x[1]))),observerPole:-alpha};\n } else if(id===11){\n const horizon=p[0],rho=p[1],disturb=p[2],inputs=[0,-.5,.5,-1,1];\n const optimize=x=>{let best={cost:Infinity,path:[]};\n function walk(state,depth,cost,path){if(cost>best.cost+1e-10)return;if(depth===horizon){if(cost<best.cost-1e-10)best={cost,path};return;}for(const u of inputs){const next=state+u;walk(next,depth+1,cost+next*next+rho*u*u,[...path,u]);}}\n walk(x,0,0,[]);return best;};\n let x=2.5;const first=optimize(x),pred=[[0,x]],real=[[0,x]],actions=[];let px=x;\n first.path.forEach((u,i)=>{px+=u;pred.push([i+1,px]);});\n for(let k=0;k<12;k++){const plan=optimize(x),u=plan.path[0];actions.push([k,u],[k+1,u]);x+=u;if(k===2)x+=disturb;real.push([k+1,x]);}\n graph('prediction',[series('realized',real),series('firstPrediction',pred)],{event:3});graph('action',[series('action',actions)]);\n result.metrics={firstAction:first.path[0],firstCost:first.cost,final:x,horizon,candidates:5**horizon};\n }\n return result;\n}\nif(typeof module!=='undefined')module.exports={controlModel};\n\nconst $=s=>document.querySelector(s), colors=['var(--accent)','#b77818','#269282'],dash=['','7 4','2 4'];\nconst number=x=>Number.isFinite(x)?viz.number(x,Number.isInteger(x)?0:Math.abs(x)<.01&&x!==0?5:3):'—';\nfunction plot(parent,spec){\n const all=spec.curves.flatMap(s=>s.points),xx=all.map(p=>p[0]),yy=all.map(p=>p[1]);\n const zero=['action','components','rate','netpower','backlog','replicas'].includes(spec.title);\n let lo=zero?Math.min(0,...yy):Math.min(...yy),hi=zero?Math.max(0,...yy):Math.max(...yy),xmin=Math.min(...xx),xmax=Math.max(...xx);\n if(hi===lo)hi=lo+1;if(xmin===xmax)xmax=xmin+1;\n const pad=(hi-lo)*.08;lo-=pad;hi+=pad;\n if(spec.xrange)[xmin,xmax]=spec.xrange;if(spec.yrange)[lo,hi]=spec.yrange;\n const sx=x=>8+(x-xmin)/(xmax-xmin)*584,sy=y=>8+(hi-y)/(hi-lo)*144;\n const section=document.createElement('section');section.className='chart';\n const title=document.createElement('h3');title.textContent=viz.t(spec.title);section.append(title);\n const scale=document.createElement('div');scale.className='scale';scale.textContent=viz.t('vertical')+' '+number(lo)+' … '+number(hi);section.append(scale);\n const ns='http://www.w3.org/2000/svg',svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 600 160');svg.setAttribute('preserveAspectRatio','none');svg.setAttribute('role','img');svg.setAttribute('aria-label',viz.t(spec.title));\n function node(name,attrs){const n=document.createElementNS(ns,name);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,v);svg.append(n);return n;}\n for(let j=0;j<=4;j++)node('line',{x1:8,x2:592,y1:8+j*36,y2:8+j*36,stroke:'var(--rule)','stroke-width':1});\n if(lo<=0&&hi>=0)node('line',{x1:8,x2:592,y1:sy(0),y2:sy(0),stroke:'var(--muted)','stroke-width':1});\n if(spec.scatter)node('line',{x1:sx(0),x2:sx(0),y1:8,y2:152,stroke:'var(--muted)','stroke-width':1});\n if(spec.event!==undefined&&spec.event>=xmin&&spec.event<=xmax)node('line',{x1:sx(spec.event),x2:sx(spec.event),y1:8,y2:152,stroke:'var(--muted)','stroke-dasharray':'3 3'});\n spec.curves.forEach((s,i)=>{if(spec.scatter||spec.dots===i){for(const [x,y] of s.points)node('circle',{cx:sx(x),cy:sy(y),r:spec.scatter?5:2.8,fill:colors[i%3]});}else{let pts=s.points;if(spec.title==='replicas')pts=pts.flatMap((p,j)=>j?[[p[0],pts[j-1][1]],p]:[p]);node('path',{d:pts.map(([x,y],j)=>(j?'L':'M')+sx(x).toFixed(2)+' '+sy(y).toFixed(2)).join(' '),fill:'none',stroke:colors[i%3],'stroke-width':2,'stroke-dasharray':dash[i%3],'vector-effect':'non-scaling-stroke'});}});\n const area=document.createElement('div');area.className='plotarea';const ticks=document.createElement('div');ticks.className='yticks';for(const v of [hi,(hi+lo)/2,lo]){const label=document.createElement('span');label.textContent=number(v);ticks.append(label);}area.append(ticks,svg);section.append(area);\n const axes=document.createElement('div');axes.className='axes';const fmt=x=>spec.logx?number(10**x):number(x);\n for(const text of [fmt(xmin),viz.t(spec.scatter?'realAxis':spec.logx?'frequencyAxis':MODEL_ID===11?'stepAxis':'timeAxis'),fmt(xmax)]){const a=document.createElement('span');a.textContent=text;axes.append(a);}section.append(axes);\n const legend=document.createElement('div');legend.className='legend';spec.curves.forEach((s,i)=>{const el=document.createElement('span'),sw=document.createElement('i');sw.style.borderTop=`3px ${i===0?'solid':i===1?'dashed':'dotted'} ${colors[i%3]}`;el.append(sw,document.createTextNode(viz.t(s.name)));legend.append(el);});section.append(legend);parent.append(section);\n}\nfunction parameterValue(el){\n const pressed=el.matches('.presets')&&el.querySelector('[aria-pressed=\"true\"]');\n return +(pressed?pressed.value:el.value);\n}\nfunction setParameter(el,value){\n if(el.matches('.presets'))[...el.querySelectorAll('button')].forEach(b=>b.setAttribute('aria-pressed',String(+b.value===value)));\n else el.value=value;\n}\nfunction update(){\n const values=[...document.querySelectorAll('[data-parameter]')].map(parameterValue);\n document.querySelectorAll('input[data-parameter]').forEach(e=>$('#value-'+e.dataset.parameter).textContent=number(+e.value));\n const r=controlModel(MODEL_ID,values);document.body.dataset.result=JSON.stringify(r.metrics);document.body.dataset.parameters=JSON.stringify(values);\n $('#plots').replaceChildren();r.plots.forEach(s=>plot($('#plots'),s));$('#metrics').replaceChildren();\n for(const [k,v]of Object.entries(r.metrics)){if(!METRICS.includes(k))continue;const line=document.createElement('div'),label=document.createElement('span'),value=document.createElement('strong');label.textContent=viz.t(k);value.textContent=typeof v==='boolean'?viz.t(v?'yes':'no'):number(v);line.append(label,value);$('#metrics').append(line);}\n $('#status').textContent=viz.t('updated');\n}\ndocument.querySelectorAll('input[data-parameter]').forEach(e=>e.addEventListener('input',update));\ndocument.querySelectorAll('.presets[data-parameter]').forEach(group=>group.addEventListener('click',e=>{\n const button=e.target.closest('button');if(!button||!group.contains(button))return;\n [...group.querySelectorAll('button')].forEach(b=>b.setAttribute('aria-pressed',String(b===button)));\n update();\n}));\n$('#reset').addEventListener('click',()=>{document.querySelectorAll('[data-parameter]').forEach((e,i)=>setParameter(e,DEFAULTS[i]));update();});update();\n","audio":false,"strings":{"amplitude":"所选频率的幅值比","crossover":"增益交越 / rad·s⁻¹","delayControl":"纯延迟 / s","frequencyAxis":"角频率 / rad·s⁻¹(对数轴)","input":"输入","loop":"L(jω)","magnitude":"环路幅值 / dB","minus180":"−180°","no":"否","output":"输出","phase":"环路相位 / °","phaseMargin":"相位裕度 / °","probe":"探测角频率 / rad·s⁻¹","realAxis":"实部 / s⁻¹;纵轴:虚部 / s⁻¹","reset":"重置实验","scope":"环路L(s)=2e^(−τs)/(s+1)。虚线竖标记为增益交越√3 rad/s;纯延迟保持幅值、减少相位。正弦图展示环路本身的稳态响应,不是闭环阶跃。","sine":"所选频率的正弦稳态","stepAxis":"离散步 k","timeAxis":"时间 / s","updated":"已按当前参数重新计算完整轨迹。","vertical":"纵轴范围","yes":"是","zeroDb":"0 dB"}}
改变频率,看正弦相位差;改变延迟,看幅值曲线保持不变而相位继续下降。增益交越处的标记连接 Bode 图与裕度数值。相位在图中保持展开,不把越过−180°的值折回到正角度。
Bode 图、增益裕度和 Nyquist
Bode 图把幅值 20log10∣L∣ 和相位画在对数频率轴上。0 dB对应幅值1,−6.02 dB约对应幅值0.5。环路增益应无量纲;若画带单位的对象增益,需要说明归一化或单位基准。
在相位交越频率 ωπ 处,相位为−180°,增益裕度为 1/∣L(jωπ)∣,换成 dB 为 −20log10∣L(jωπ)∣。某些环路没有有限相位交越,工具可能报告无穷增益裕度;这不代表系统能承受任意未建模延迟。
更完整的 Nyquist 判据跟踪 L(s) 在围住右半平面的轮廓上的像,利用其对−1点的绕行与开环右半平面极点数量判断闭环右半平面极点。它源于对 1+L(s) 应用辩值原理;使用时必须明确轮廓方向以及虚轴极点如何绕开。
开环含不稳定极点、多次交越或复杂结构时,不能仅凭一个“正相位裕度”判断内部稳定。基础篇用单交越例子建立直觉;完整判据需要检查整个频率轨迹与开环极点。
带宽与抗扰为什么存在取舍?
提高交越频率往往让闭环更快,但固定延迟在更高频率造成更大相位损失;传感器噪声、未建模高频模态也更容易进入动作。带宽应明确指对象、开环交越还是闭环的某个阈值频率,三者不是同一个量。
不要把“相位裕度越大越好”当作单目标优化。要同时满足响应速度、扰动抑制、控制幅度与鲁棒性要求。约束与鲁棒性会用灵敏度函数明确各条信号通路。
自检
给 L=2/(s+1) 加0.5 s纯延迟,会改变增益交越频率吗?
参考思路
不会,因为纯延迟幅值为1,ωc仍为3。但相位裕度从120°降至约70.4°,说明幅值不变仍可能损害稳定性。
工具输出 gm=2,它是2 dB吗?
参考思路
不是。若返回的是线性增益倍率2,对应20log102≈6.02 dB。还要核对交越频率返回值的含义,不能按变量名猜测。
延伸
python-control margin 文档明确区分增益裕度、以度表示的相位裕度及两种交越频率;多交越情形应检查完整结果,而不仅摘取一个标量。