拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python手绘波特图:电路稳定性分析实用指南

用Python手绘波特图:电路稳定性分析实用指南

带反馈的电路设计,我做了快十年,一个特别深的体会是:波特图不是教科书里的抽象曲线,而是判断电路稳定性最直接的工具。用Python手绘波特图,听起来像是留给学生的作业题,实际上我几乎每个带反馈的硬件项目,动手画板之前都会先用一段小脚本把增益裕度和相位裕度扫一遍。

今天这篇从零开始讲透整个流程:先把手边没有Python、没有绘图库的问题解决掉,再一步步写出图画代码、建立电路模型、读懂幅频和相频曲线,最后用三个真实电路案例把稳定性判断完整走一遍。整个过程不需要Matlab、不需要控制系统工具箱,只用Python加两个最常用的库。适合硬件工程师、电源工程师、嵌入式开发者,也适合正在学自动控制或电路原理的学生。只要你有最基础的Python语法知识,就能跟上。

1. 先回答一个问题:为什么自己折腾波特图而不是直接调库

1.1 波特图在工程里到底解决什么问题

任何一个线性时不变电路,都可以用传递函数描述:输入正弦信号,输出会有多大、会滞后多少。波特图就是把这个"多大"和"滞后多少"分别画成两条曲线,横轴是频率,纵轴分别是增益分贝数和相位角度数。

在负反馈系统里,稳定性问题本质上是一个相位问题。反馈是负反馈的前提,是反馈信号要和输入信号反相抵消。但电路中总有电容、电感,它们会让信号产生相位滞后。一旦某个频率点上,反馈环路的增益刚好是1倍(0dB),同时相位又滞后到了180度,原本的负反馈就变成了正反馈,系统就会振荡。所以判断电路稳定性,核心就是看两个数字:增益降到0dB时,相位离-180度还有多远;相位到-180度时,增益已经跌到0dB以下多少。前者叫相位裕度(PM),后者叫增益裕度(GM)。

1.2 自己写和直接调库的差别

肯定有人问:Python里明明有control库,一个bode()函数就画完了,为什么还要手写?

确实,control.bode()一行出图,我也经常用。但问题在于,很多时候你并不需要整套控制系统工具箱,尤其只是分析一个简单的RC滤波、一个运放补偿网络、一个电源环路,装一大堆依赖反而累赘。手写的真正价值在于:你必须自己把传递函数、频率点、复数计算、分贝换算这些底层逻辑理清楚,而不是丢给黑盒。一旦曲线画出来不符合预期,你知道该往哪查——是模型建错了,还是参数写错了,还是换算公式错了。

而且手写代码特别容易扩展。比如把实测的网分数据导进来,和理论波特图叠在一张图上对比;比如把开环增益和反馈系数分开画,直接观察环路增益。这些用通用库做起来反而绕。

1.3 这篇内容适合谁看

如果你已经有Python环境,前两章可以快速跳过。如果你是完全从零开始,我建议老老实实把环境装好,毕竟后面所有代码都要跑起来才能看到效果。

2. 环境起步:把Python跑起来,别卡在第一步

2.1 Python本体安装的两种方式

我最推荐的方式是去Python官网下载安装包。下载时注意选对版本,建议3.10及以上,太老的版本某些语法和新版库可能会有兼容问题。Windows下安装时有一个关键勾选:Add Python to PATH,务必勾上,不然后面命令行里敲python会提示找不到命令。

安装完成后,打开命令行(Windows下是cmd或PowerShell,macOS/Linux下是终端),输入:

python --version

能输出版本号就说明装好了。如果你电脑上因为某种原因出现了python was not found之类的提示,大概率是PATH没配好,重新跑一下安装程序,勾选PATH选项即可。也可以用py命令代替:

py --version

编辑器方面,vscode配Python插件是最省事的方案。装好Python插件后,打开任意文件夹,新建一个.py文件,右下角选择解释器,就能直接运行。不需要追求复杂的IDE,vscode足够用。

2.2 NumPy和Matplotlib的安装

波特图计算离不开NumPy的数组和复数运算,画图用的是Matplotlib。安装了这两个库,整个流程就通了,不需要其他任何第三方依赖。

在命令行执行:

python -m pip install --upgrade pip python -m pip install numpy matplotlib

这里用python -m pip而不是直接pip,是为了避免电脑上存在多个Python版本时装错位置。装完可以用下面这条命令验证:

python -c "import numpy, matplotlib; print(numpy.__version__, matplotlib.__version__)"

能打印出版本号,环境就绪。如果你用的是虚拟环境(比如venv或conda),记得先激活再安装。

2.3 第一个验证程序

新建一个bode_demo.py文件,先跑通最基本的三行代码:

import numpy as np import matplotlib.pyplot as plt x = np.logspace(0, 5, 500) y = 20 * np.log10(np.abs(1 / (1 + 1j * x / 1000))) plt.semilogx(x, y) plt.xlabel('Frequency (rad/s)') plt.ylabel('Magnitude (dB)') plt.grid(True, which='both') plt.show()

这其实就是一个RC低通滤波器的幅频曲线雏形。如果图像能正常弹出来,说明整个工具链已经通了,下面开始正式写波特图绘制函数。

3. 波特图的理论基础:三个核心点一次讲透

3.1 传递函数、零点和极点

电路分析里,传递函数通常写成H(s),其中s是复频率。一个线性系统在频域的特性,完全由分子多项式和分母多项式的根决定:让分子等于0的点叫零点,让分母等于0的点叫极点。

举一个最简单的一阶RC低通滤波器,输出取电容两端:

[ H(s) = \frac{1}{1 + sRC} ]

当s = -1/(RC)时,分母为0,这就是一个极点。极点对应的角频率ωp = 1/(RC)是这条曲线最重要的转折点。把s = jω代入,就得到了频率响应。

理解零点极点为什么重要,是因为它们直接决定了曲线的形状。系统的幅频特性是每个零点和极点各自贡献的叠加,相频特性也一样。把一个复杂的电路拆成一串零点和极点,分析就变成了搭积木。

3.2 幅值和相位怎么从复数算出来

把s = jω代入传递函数后,H(jω)就是一个复数。比如上面的RC低通:

[ H(j\omega) = \frac{1}{1 + j\omega RC} ]

复数有模长和角度:模长|H|反映增益大小,角度∠H反映相位滞后。波特图的纵轴分别用分贝和度表示:

[ |H|{dB} = 20\log{10}|H|, \quad \phi = \arctan\left(\frac{\text{Im}(H)}{\text{Re}(H)}\right) ]

在Python里,NumPy直接支持复数和复数数组运算。给定一个频率数组omega,1 / (1 + 1j * omega * R * C)就是所有频率点上的复数响应,np.abs(H)取模,np.angle(H, deg=True)取角度。这正是手写波特图最核心的一步:让Python在整个频率轴上扫一遍,逐点算出复数值,然后分别画成两条曲线。

3.3 每个零极点对波特图的贡献规则

这部分决定了你看到曲线时能不能一眼读懂。记忆方法很简单:

  • 一个极点,会让幅频曲线在极点频率之后以**-20dB/dec**的斜率下降;相位会在极点频率前后约两个十倍频程内,从0度逐渐变化到-90度。极点频率处正好是-45度。
  • 一个零点,效果完全反过来:幅频以**+20dB/dec**上升,相位从0度逐渐到+90度。
  • 二阶系统里,共轭复数极点对会有更陡的相位变化:从0度到-180度,而且阻尼比越小,变化越集中、越突然。

正因为如此,系统的阶数越高,相位跑到-180度的可能性越大。一阶系统最多滞后90度,永远不会因为相位问题振荡;二阶系统最多滞后180度,已经很危险;三阶及以上系统,如果增益不够低,几乎必然穿越-180度——这就是为什么"稳定性问题往往是高阶系统的问题"。

4. 手写核心绘图函数:零基础也能读懂代码

4.1 用多项式系数描述传递函数

我选择让绘图函数直接接收分子和分母的多项式系数,这是最贴近电路设计者习惯的写法。比如RC低通H(s) = 1/(1 + sRC),分子系数就是[1],分母系数就是[R*C, 1]。RLC二阶低通H(s) = 1/(LCs² + RCs + 1),分子是[1],分母是[L*C, R*C, 1]。一眼就能把电路参数对应起来,不用自己先算零点极点。

如果遇到用时间常数形式给出的传递函数H(s) = A0 / ((1 + s/ω1)(1 + s/ω2)),我专门写了一个小函数,把一系列极点角频率展开成多项式系数。这是很有用的辅助工具,后面三极点放大器的例子会用到。

4.2 频率轴扫描和复数值计算

横轴用对数刻度,这是波特图最重要的特征。因为电路的兴趣频段通常跨越好几个数量级,从几赫兹到几兆赫兹,线性坐标根本没法看。我用np.logspace生成对数均匀分布的频率点,计算对应的角频率ω = 2πf,然后用np.polyval对分子分母多项式求值:

import numpy as np import matplotlib.pyplot as plt def eval_transfer(b, a, omega): """计算 s = j*omega 时的频率响应 H(jw)。b、a 是多项式系数,按 s 降幂排列。""" s = 1j * np.asarray(omega, dtype=float) H = np.polyval(b, s) / np.polyval(a, s) return H

np.polyval可以一次性对数组求值,效率很高。返回的H是一个复数数组,包含了每个频率点的增益和相位信息。

4.3 绘图和参考线标注

绘图我用两个上下排列的子图:上面画幅频,下面画相频。两条参考线必须画:幅频的0dB线(增益为1的位置)和相频的-180度线(负反馈变正反馈的临界位置)。这两条线是后面判断稳定性的基准。

def bode_manual(b, a, fmin=0.1, fmax=10_000_000.0, points=1000, unwrap=True): f = np.logspace(np.log10(fmin), np.log10(fmax), points) omega = 2.0 * np.pi * f H = eval_transfer(b, a, omega) mag_db = 20.0 * np.log10(np.abs(H) + 1e-12) phase_deg = np.angle(H, deg=True) if unwrap: phase_deg = np.unwrap(phase_deg, period=360) fig, (ax_mag, ax_phase) = plt.subplots(2, 1, figsize=(10, 7), sharex=True) ax_mag.semilogx(f, mag_db, lw=1.8) ax_mag.axhline(0, color='k', lw=0.8) ax_mag.grid(True, which='both', alpha=0.3) ax_mag.set_ylabel('Magnitude (dB)') ax_phase.semilogx(f, phase_deg, lw=1.8) ax_phase.axhline(-180, color='r', ls='--', lw=0.8) ax_phase.grid(True, which='both', alpha=0.3) ax_phase.set_ylabel('Phase (deg)') ax_phase.set_xlabel('Frequency (Hz)') return f, mag_db, phase_deg

注意两处细节:幅频计算时我加了1e-12,避免极低频或高频处数值刚好为0时取对数报错;相位计算后用了np.unwrap(phase_deg, period=360),防止相位从-180度跳到+180度产生视觉上的折线。这个细节后面会专门展开。

4.4 完整代码与基本调用

把eval_transfer和bode_manual合在一起,整个绘图部分约40行。调用方式非常直观:

# 一阶RC低通:R=1k, C=1uF,极点频率约159Hz R = 1e3 C = 1e-6 f, mag, phase = bode_manual([1], [R * C, 1], fmin=0.1, fmax=100e3, points=600) plt.show()

只要能把电路的传递函数列出来,画波特图就是这一行的事。但要注意,这个函数画的是开环或特定传递函数的频率响应,判断稳定性时需要分析的是整个反馈环路的环路增益。

5. 案例一:一阶RC低通,先把整个流程跑通

5.1 电路模型与极点计算

第一个案例故意选最简单的RC低通,目的是把"从电路到传递函数再到波特图"的流程跑通。

R=1kΩ,C=1μF,RC=1ms,极点角频率:

[ \omega_p = \frac{1}{RC} = 1000 \text{ rad/s}, \quad f_p = \frac{1000}{2\pi} \approx 159 \text{ Hz} ]

传递函数:

[ H(s) = \frac{1}{1 + s / 1000} ]

5.2 绘制结果解读

运行代码后,你会看到幅频曲线在159Hz之前是一条水平线(0dB),159Hz之后开始以-20dB/dec的斜率下降。在转折频率处,实际增益是-3dB。相频曲线在159Hz处正好是-45度,低于十倍频程时接近0度,高于十倍频程时趋近-90度。

这个图形本身就是"一个极点"的标准模板。后面看任何复杂曲线,脑子里都要能拆成这种基本模板的叠加。

5.3 从RC看波特图的实际手感

一阶RC系统永远不会振荡,因为相位最多只能到-90度,根本够不到-180度。但它教会我们两件事:

第一,转折频率是设计带宽的关键。比如你做一个信号调理电路,想要100kHz以内平坦增益,极点就必须放到100kHz以上,同时还要考虑这个极点给更高频段带来的衰减是否够用。

第二,增益和相位是成对出现的,不可能只要衰减不要延迟。所有电容、电感在提供滤波的同时,都在消耗相位裕度。这个代价在单极点时不明显,在多极点系统中会集中暴露。

6. 案例二:RLC二阶系统,谐振峰该怎么读

6.1 从RLC推导传递函数与极点

第二个案例换成RLC串联谐振电路,输出取电容两端。L=10mH,C=1μF,R=10Ω。

传递函数:

[ H(s) = \frac{1}{LCs^2 + RCs + 1} ]

代入参数:

[ LC = 10 \times 10^{-3} \times 1 \times 10^{-6} = 10^{-8}, \quad RC = 10 \times 10^{-6} = 10^{-5} ]

固有角频率:

[ \omega_0 = \frac{1}{\sqrt{LC}} = \sqrt{10^{8}} = 10000 \text{ rad/s}, \quad f_0 \approx 1591 \text{ Hz} ]

品质因数:

[ Q = \frac{1}{R}\sqrt{\frac{L}{C}} = \frac{100}{10} = 10 ]

Q值高达10,说明这是一个低损耗、高谐振的系统。计算极点时,判别式(RC)² - 4LC为负,得到一对共轭复数极点:

[ s = -500 \pm j9987 ]

6.2 不同阻尼下波特图变化

绘制出的幅频曲线和RC低通完全不同:在1600Hz附近会有一个明显的谐振峰,峰值高度约20dB,因为|H|max ≈ Q。相位曲线也不再是缓慢下降到-90度,而是在谐振频率附近急剧从0度跌到-180度附近。

如果把R分别改成100Ω和200Ω,Q值会降到1和0.5。Q=1时谐振峰基本消失,系统接近临界阻尼;Q=0.5时系统过阻尼,没有任何过冲,相位下降也平缓得多。用下面这段代码可以直观对比:

L, C = 10e-3, 1e-6 for R in (10, 100, 200): f, mag, phase = bode_manual([1], [L * C, R * C, 1], fmin=1, fmax=1e6, points=1000) plt.semilogx(f, mag, label=f'R={R}') plt.grid(True, which='both') plt.legend() plt.show()

我把三条曲线叠在一起时,最大的感受是:阻尼是二阶系统的命根子。没有足够的阻尼,相位变化的剧烈程度远超直觉,这对反馈回路的设计是致命的。

6.3 谐振峰与品质因数的工程含义

谐振峰在滤波器设计里意味着带通选择性和电压增益,但在电源和反馈电路里往往意味着噪声放大和稳定性隐患。比如EMI滤波器如果阻尼不够,谐振峰处的增益会把某些频率的干扰放大,而不是滤掉。处理办法一般是加阻尼电阻或降低Q值。

另外一个容易被忽视的点是:这个RLC二阶网络本身的0dB穿越点约在2245Hz,此处相位已经接近-172度,离-180度只有约8度。作为开环滤波器它当然不会"振荡",但如果它出现在负反馈环路里,就等于给环路贡献了一个相位裕度损失极大的极点对,整体稳定性需要重新审视。无源元件本身不是不稳定源,但它会让你的有源反馈环更容易不稳定。

7. 案例三:三极点放大器与相位补偿

7.1 建立三极点放大器模型

前两个案例都属于不自激的系统,现在来看一个真正需要担心的场景。

假设我们设计了一个运算放大器构成的电压反馈级,它的开环增益A0=1000(60dB),有三个极点:

  • 第一个极点在f1=1kHz
  • 第二个极点在f2=100kHz
  • 第三个极点在f3=1MHz

传递函数:

[ H(s) = \frac{1000}{(1 + s/\omega_1)(1 + s/\omega_2)(1 + s/\omega_3)} ]

用辅助函数展开分母系数:

def factors_to_poly(freqs): """连乘 (1 + s/freq_i),返回按 s 降幂排列的多项式系数。""" coeff = [1.0] for f in freqs: coeff = np.convolve(coeff, [1.0 / f, 1.0]) return coeff A0 = 1000 p1, p2, p3 = 2*np.pi*1e3, 2*np.pi*100e3, 2*np.pi*1e6 a = factors_to_poly([p1, p2, p3]) b = [A0] f, mag, phase = bode_manual(b, a, fmin=100, fmax=10e6, points=1000) plt.show()

7.2 不补偿为什么危险

绘制结果会清楚显示,增益从1kHz处开始以-20dB/dec滚降,100kHz后变成-40dB/dec,1MHz后变成-60dB/dec。让我手动估算一下0dB穿越点:

1kHz到100kHz之间,增益从60dB降到20dB(两个十倍频程,-40dB)。之后以-40dB/dec继续降,20dB降到0dB还需要0.5个十倍频程,所以0dB穿越频率约在316kHz附近。

在这个频率点,第一个极点早已贡献了接近-90度的相移,第二个极点贡献约-72度,第三个极点贡献约-17度,总相位已经非常接近-180度。这意味着,当环路增益等于1倍时,反馈已经几乎完全变成正反馈,相位裕度趋近于0,系统临界振荡。实际做出来一定会啸叫或者振铃。

7.3 加零点后相位裕度的改善

解决办法是在反馈网络里引入一个零点,让相位在穿越点附近往回拉。比如在误差放大器的反馈电阻上并联一个电容,构成一个零点。假设零点频率设在fz=100kHz:

[ H(s) = \frac{1000(1 + s/\omega_z)}{(1 + s/\omega_1)(1 + s/\omega_2)(1 + s/\omega_3)} ]

分子系数变为[A0/z1, A0]:

z1 = 2*np.pi*100e3 b_comp = [A0 / z1, A0] f2, mag2, phase2 = bode_manual(b_comp, a, fmin=100, fmax=10e6, points=1000) plt.show()

加的这个零点在100kHz处开始提供+20dB/dec的上升斜率,同时贡献正向相位。在316kHz的0dB穿越点附近,零点大约贡献了+72度的相位,把原本趋近-180度的总相位拉回到约-107度,相位裕度瞬间提升到70度以上。这个设计就非常稳妥了。

选零点频率时的经验是:零点放在0dB穿越频率的三分之一到十分之一之间,能获得足够的相位提升,又不会明显抬高高频增益、引入噪声。放太靠近穿越点,相位还没提上来;放太低,高频噪声会被放大。

8. 自动判稳:用代码算出增益裕度和相位裕度

8.1 判断逻辑

人眼从图上读裕度虽然可行,但不够精确,尤其像三极点放大器这种相位裕度接近0的临界情况,必须量化。自动化判稳的逻辑很直接:

  • 相位裕度PM:找到幅频曲线穿越0dB的频率点(增益穿越频率),读取该频率的相位,计算它与-180度的差值。
  • 增益裕度GM:找到相频曲线穿越-180度的频率点(相位穿越频率),读取该频率的增益,计算它与0dB的差值。

工程上一般要求PM≥45度、GM≥10dB才算稳妥。如果元件有误差、温度有漂移,这几个度数的余量是必要的。

8.2 手写compute_margins函数

基于上面的逻辑,写一个自动判稳函数:

def compute_margins(f, mag_db, phase_deg): # 增益穿越频率:幅度最接近 0dB 的点 idx_gc = int(np.argmin(np.abs(mag_db))) f_gc = f[idx_gc] phase_gc = phase_deg[idx_gc] pm = phase_gc - (-180.0) # 相位穿越频率:依次找 phase = -180 的穿越点 cross_indices = np.where(np.diff(np.sign(phase_deg + 180.0)))[0] gm_values = [] gm_freqs = [] for i in cross_indices: i = int(i) frac = (0.0 - (phase_deg[i] + 180.0)) / ((phase_deg[i + 1] + 180.0) - (phase_deg[i] + 180.0)) f_cross = f[i] + frac * (f[i + 1] - f[i]) mag_cross = mag_db[i] + frac * (mag_db[i + 1] - mag_db[i]) gm_values.append(-mag_cross) gm_freqs.append(f_cross) if gm_values: idx_min = int(np.argmin(gm_values)) gm = gm_values[idx_min] f_gm = gm_freqs[idx_min] else: gm = np.inf f_gm = None return pm, f_gc, gm, f_gm

穿越时的频率和增益都用了线性插值,所以结果不依赖采样的分辨率,精度完全够用。

8.3 对三极点案例跑结果

把第7章的三极点案例放进compute_margins,可以量化出:

参数未加零点加零点(fz=100kHz)
0dB穿越频率约316kHz约316kHz
相位裕度PM接近0度约70度以上
相位穿越频率约307kHz远高于3MHz
增益裕度GM约-11dB(负值,不稳定)很大(>20dB)

未补偿时GM是负的,说明增益在相位到达-180度时仍然高于0dB,系统处于不稳定区域。加零点后,相位穿越被推到很远的频率,那时的增益已经衰减到远低于0dB,裕度充足。

有趣的是,如果对这个三极点系统跑compute_margins,你还可能遇到一种情况:相位根本没有穿越-180度,GM返回无穷大。这时不要高兴太早,说明系统在分析频段内相位问题不突出,但你仍然要检查相位裕度是否足够,以及频段范围是否覆盖了真正可能出问题的区域。

9. 我用这套方法踩过的坑和一点使用建议

9.1 频率范围和采样点数别太吝啬

频率范围必须覆盖所有转折频率和穿越点。我一般把下限放到第一个转折频率的十分之一以下,上限放到0dB穿越频率的十倍以上。比如三极点放大器的0dB穿越在316kHz,我的扫描上限至少要放到3MHz,最好到10MHz,这样第三个极点(1MHz)对相位的贡献才完整显现。

点数方面,500点起步,1000点足够。点太少,平滑度和穿越点定位都会变差;点太多,计算量增大,但绘图效率完全跟得上。对数均匀分布的点数,覆盖6个数量级时用1000点,相邻点之间的倍数约为10^(6/1000)≈1.014,足够看到谐振峰等细节。

9.2 相位unwrap千万别忽略

np.angle返回的相位范围是-180度到+180度。如果真实相位已经到了-270度(三极点系统完全可能),不处理的话曲线会在-180度处突然跳到+180度再继续下降,图上会出现一条假折线。用np.unwrap(phase_deg, period=360)可以把相位轨迹延展成单调的连续曲线,自动判稳函数才不会被折线误导。这个坑我第一次踩的时候,花了很长时间才意识到曲线折返不是电路特性,而是角度表示的周期性问题。

9.3 非最小相位系统别照搬稳定性判据

我的代码和工程经验都基于最小相位系统——也就是所有零点和极点都在左半平面。如果系统里有右半平面零点(比如Boost变换器的电流模式控制中会出现),波特图还能画,但稳定性判据就不能简单看增益裕度和相位裕度了。右半平面零点会让相位进一步恶化,必须结合奈奎斯特图或更完整的频域判据来判断。用这套脚本做开关电源补偿时,一定先确认你的控制对象是不是最小相位系统。

9.4 波特图够用,但别只信波特图

波特图告诉你的是一些电路参数和拓扑能否构成一个带宽合理、裕度充足的系统。但真实电路里还有寄生参数、温度漂移、元件容差、非线性效应,这些在理想传递函数里都不存在。所以我的习惯是:设计阶段用这套Python脚本快速筛选参数的合理性,样机出来后再用网络分析仪或者瞬态响应波形验证一次。画波特图帮你排除明显不稳定的设计,而不是替代实测。

最后再分享一个实用小技巧:电路参数在设计过程中改来改去很常见,与其每次手动改代码里的数字,不如把电阻、电容值定义成变量,然后写一个循环,把不同参数下的波特图叠在一起对比。这个习惯能让你瞬间看清楚哪些元件对相位裕度最敏感,对后续调试的帮助非常大。

返回列表