
做硬件这几年我最怕在示波器上看到输出波形出现振铃、啸叫甚至自激振荡。不管是电源环路、运放反馈还是滤波器网络一旦系统“稳不住”排查起来相当耗时间。而绝大多数稳定性隐患其实都可以提前用一张波特图看出来。波特图本身并不神秘就是把系统在不同频率下的增益变化和相位变化分别画出来但偏偏这张图很多人上课学过到了实际项目里却不知道该怎么用 Python 画更不知道画完怎么判断系统稳不稳。说实话现成的 scipy.signal 里就有一行出图的 bode 函数Matlab 里也有现成的 bode 命令直接调用确实快。但我一直建议初学者至少要把“手绘”的过程走一遍。这里说的手绘并不是用铅笔在坐标纸上画渐近线而是指用 Python 从传递函数的复数计算开始把幅值和相位一步一步算出来再用 matplotlib 画图。只有亲手把传递函数替换成 s jω、把复数模长换算成 dB、把相位算成度数波特图才算真正长在你脑子里。我见过不少工程师拿着软件自动生成的波特图却说不清为什么曲线在某个频率突然掉头这种情况几乎都是因为当初跳过了手写计算这一步。这篇文章我会用 Python 从零完成整个流程开发环境准备、电路模型选择、手写幅频与相频计算、最后基于波特图判断电路稳定性。全部代码只依赖 numpy 和 matplotlib不借助 control 这类高级库这样你能清楚看到每个数据是怎么来的。适合刚接触频域分析的电子工程师、自动化方向的学生以及所有想把控制理论真正落到代码里的读者。1. 先从“为什么”说起波特图怎么成了电路稳定性的照妖镜1.1 波特图到底在看什么幅频与相频两条曲线波特图由两条曲线组成。第一条是幅频特性曲线横轴是频率取对数刻度纵轴是增益用 dB 表示第二条是相频特性曲线横轴和幅频一致纵轴是相位用度表示。两条曲线上下排列合在一起就是完整的波特图。它描述的其实是线性时不变系统对正弦信号的“态度”给系统输入一个确定频率的正弦波输出相对输入放大或衰减了多少倍相位滞后或超前了多少度。把频率从 1Hz 一直扫到 1MHz甚至扫到 GHz 级别就能还原出系统在整个频带内的行为。用一个生活化的类比同一个音箱对不同频率声音的响应差异很大。低频可能轰轰响高频可能又干又涩而且不同频段的声音到达人耳的时间还有先后错位。波特图就是系统层面的“音箱频响曲线”加上“相位曲线”把增益和相位完整记录在频率轴上。1.2 为什么稳定性必须盯住相位很多初学者拿到波特图只顾着看增益曲线觉得增益在 0dB 以下就安全。实际上反馈系统稳定性的关键恰恰在相位。原因是反馈环路的基本逻辑。负反馈系统正常工作时反馈信号回去是抵消输入变化的。但如果信号在环路里绕一圈之后相位已经滞后了 180 度原本的负反馈就会变成正反馈。这时候只要环路增益还大于等于 1也就是幅频曲线还在 0dB 以上系统就会自己维持振荡。轻则输出波形振铃重则直接啸叫这在运放电路、电源环路、PLL、电机驱动里都会遇到。所以判断稳定性从来不是看“增益高不高”而是同时检查两个条件在增益正好穿过 0dB 的那个频率点相位距离 -180 度还剩下多少余量这就是相位裕度 PM在相位正好到达 -180 度的那个频率点增益是不是已经掉到 0dB 以下还剩下多少余量这就是增益裕度 GM。这两个数字就是后面所有分析的核心。1.3 为什么偏要“手写”波特图不用现成函数先声明我不是反对用现成工具反而建议你在会用底层逻辑之后大胆用。但如果一开始就跳到 scipy.signal.bode你很可能只得到一张图却说不清每条曲线是怎么来的。一旦系统里有传输延迟、非最小相位零点、多个极点堆叠黑盒分析的结果就容易失真到时候想排查问题都不知道从哪里下手。手写计算这个过程本质上只是四步把传递函数里的 s 替换成 jω在每一个频率点求复数值 T(jω)对复数取模再用 20log10 换算成 dB对复数取角度折算成度。只要亲手写过一次后面遇到任何异常曲线你都能顺着这条链路定位问题而不是对着黑盒曲线猜。这就是我把重点放在底层实现上的原因。2. 准备环节环境装好电路模型搭明白2.1 开发环境与依赖库卡在入门阶段的往往是这一步开始写代码之前先把环境准备好。Python 的安装其实很简单但“python 安装”“vscode 配置 python 环境”“python 第三方库怎么装”这类问题一直有很高的搜索热度说明大量初学者卡在了第一步。我的建议按这个顺序操作到 Python 官网下载 3.10 或更高版本安装时务必勾选 Add Python to PATH这一步不勾后面 pip 会各种找不到打开终端输入 python --version能输出版本号就说明安装成功安装依赖库命令是 pip install numpy matplotlib scipy编辑器用 VS Code装好 Python 扩展后在左下角选择解释器就能直接运行 .py 文件。如果你完全不想折腾环境也可以直接装 Anaconda自带 Python 和常用科学计算库缺点是占空间较大。我日常用的是官方 Python 加 VS Code轻量够用。确认环境没问题后再继续往下。2.2 运放环路模型这次分析的“试验田”主题是电路稳定性我要选一个足够典型、又不会复杂到看不下去的模型。这里用运放非反相放大电路来做分析。运放的开环直流增益不是无穷大记为 A0。受内部补偿影响它随频率升高而下降典型表现为多个低通极点叠加。最基础的一阶模型是A(s) A0 / (1 s / ωp1)其中 ωp1 是主极点角频率。有些运放还存在第二个极点把模型补全成A(s) A0 / ((1 s / ωp1) * (1 s / ωp2))反馈系数 β 由电阻分压决定非反相放大结构里β R1 / (R1 R2)整个环路的开环传递函数就是T(s) A(s) * β当环路增益 T(s) 满足前面说的失稳条件时整个放大电路就会表现出振荡倾向。这个模型虽然简单但完整包含了“多个极点 反馈”这两个波特图稳定性分析的核心要素后续结论可以推广到电源环路等更复杂的系统。后续代码中的参数我按下面这组来取A0 100000也就是 100dB 直流增益fp1 100Hzfp2 分别取 1MHz 和 5kHz对应“宽松”和“紧张”两种场景β 0.1对应闭环增益 10 倍。2.3 把传递函数变成复数表达式从拉普拉斯域到频率域在计算机里画波特图第一步不是画图而是把传递函数从符号形式变成数值形式。我们写的传递函数是 s 的函数s 是拉普拉斯变量。分析正弦稳态时只需要让 s 沿着虚轴取值也就是令 s jω。这个操作在教科书里叫“频率响应”物理含义是给系统输入角频率为 ω 的正弦波稳态输出的复振幅就是 T(jω)。所以对刚才的运放环路模型在每个频率点要算的是T(jω) A0 * β / ((1 jω / ωp1) * (1 jω / ωp2))注意角频率和频率的换算关系ω 2πfωp1 2πfp1ωp2 2πfp2。后面写代码基本就是按这个式子逐点求值。把这个转换想明白写代码就只是翻译工作了。3. 核心实现手写幅频和相频计算附可直接运行的代码3.1 扫频怎么扫用 logspace 生成对数间隔频率点画波特图的第一步是准备好频率轴。波特图的横轴是对数刻度所以频率点不能均匀取。如果从 1Hz 到 1MHz 均匀切 1000 份低频段可能一个点都分不到画出来的低频响应完全靠猜而对数间隔能保证低频、中频、高频区域都有足够的分辨率。numpy 里的 logspace 可以轻松生成对数等间隔序列import numpy as np f np.logspace(0, 6, 800) # 从 10^01Hz 到 10^61MHz共 800 个点 w 2 * np.pi * f # 换算成角频率800 个点对显示来说已经足够平滑计算量也完全可以忽略。点数太少转折频率附近曲线会显得棱角分明点数太多对后续插值判断裕度也帮不上太大忙所以 800 到 2000 之间都是合理选择。3.2 传递函数求值numpy 向量化一步算出复数序列前文已经推导出了环路增益 T(jω)现在把它写成 numpy 表达式。利用向量化运算可以一次性算出所有频率点上的复数值fp1 100.0 fp2 1e6 a0 100000.0 beta 0.1 wp1 2 * np.pi * fp1 wp2 2 * np.pi * fp2 # 对应 T(jw) a0 * beta / ((1 jw/wp1) * (1 jw/wp2)) t_values a0 * beta / ((1 1j * w / wp1) * (1 1j * w / wp2))这段代码每一行都对应公式中的一个因子看起来直观改起来也方便。以后想加零点就在分子乘上 (1 jω/ωz)想加传输延时就乘上 e^(-jωτ)扩展性很强。3.3 幅值换算从复数模长到 dB幅频曲线要用 dB 作单位。dB 的定义是功率比取对数乘 10但工程上我们更常处理电压、电流这类幅度量幅度比转 dB 用 20logmag_db 20 * np.log10(np.abs(t_values))np.abs 取复数的模也就是 |T(jω)|再用 np.log10 取以 10 为底的对数。乘 20 这个系数初学者最容易搞混。记住一句话就能少踩坑幅度比用 20log功率比用 10log。因为功率正比于幅度平方平方在对数里正好把 10 变成 20。当某个频率点增益小于 1 时log10 结果是负数曲线就会落到 0dB 线以下这是正常的说明该频率信号被系统衰减了。3.4 相位计算angle 主值问题和 unwrap 的必要性相频曲线同样从复数值中提取phase_rad np.angle(t_values) phase_deg np.degrees(phase_rad)直接用 np.angle 得到的是主值范围只在 -180 度到 180 度之间。但实际系统的相位往往是连续变化的问题就出在这里当相位从 -179 度继续下降到 -181 度时numpy 会把这个角度显示成 179 度于是相频曲线会在 -180 度附近突然跳变看起来像锯齿好像相位瞬间反弹了一大截。这不是电路真的有问题而是角度卷绕造成的假象。解决办法是用 unwrap 把角度展开成连续曲线phase_deg np.degrees(np.unwrap(phase_rad))np.unwrap 的机制是检测相邻角度差如果超过 180 度就自动补上 2π 的整数倍让曲线连贯起来。注意它作用的对象是弧度值所以要在 unwrap 之后再转成角度。很多人画相频图发现曲线在 -180 度附近疯狂跳变第一反应是系统异常其实多半只是忘了展开角度。3.5 完整的绘图函数双子图把幅频和相频放在一起把上面的步骤封装成一个函数方便复用import matplotlib.pyplot as plt def plot_bode(freqs, t_values, titleBode Plot): mag_db 20 * np.log10(np.abs(t_values)) phase_deg np.degrees(np.unwrap(np.angle(t_values))) fig, (ax1, ax2) plt.subplots(2, 1, figsize(8, 7)) ax1.semilogx(freqs, mag_db, linewidth2) ax1.axhline(0, colorgray, linestyle--, linewidth1) ax1.set_ylabel(Gain (dB)) ax1.grid(whichboth, linestyle:, alpha0.6) ax2.semilogx(freqs, phase_deg, linewidth2) ax2.axhline(-180, colorgray, linestyle--, linewidth1) ax2.set_xlabel(Frequency (Hz)) ax2.set_ylabel(Phase (deg)) ax2.grid(whichboth, linestyle:, alpha0.6) fig.suptitle(title) fig.tight_layout() return figplt.subplots(2, 1) 生成上下两个子图共享频率轴上面摆幅频下面摆相频这是波特图的标准排版。axhline 在图上画参考线用来对照 0dB 和 -180 度。grid 配上 whichboth会同时画出主刻度和次刻度网格线在对数坐标下读图要方便得多。这里对照一下整个过程完全没有调用 scipy.signal.bode所有幅值和相位都是自己算出来的。4. 从曲线到结论相位裕度、增益裕度怎么算、怎么读4.1 先找两个关键频率点看波特图判断稳定性本质上是在图上找两个频率位置增益穿越频率 ωc幅频曲线穿过 0dB 线的那个频率点。这个点上增益等于 1信号绕环路一圈既不放大也不衰减最容易自激相位穿越频率 ωπ相频曲线穿过 -180 度线的那个频率点。这个点上负反馈已经“变质”成了正反馈。有了这两个点稳定裕度就顺理成章地定义出来了。4.2 相位裕度和增益裕度的定义与工程要求相位裕度 PM 的定义是PM 180° ∠T(jωc)也就是说在增益穿越频率处相位距离 -180 度线还剩多少角度。PM 越大系统越稳。工程上通常这么看PM ≥ 60°系统动态响应从容过冲小PM 在 45° 到 60° 之间工程上可接受但负载变化大时要注意PM 低于 30°系统已经开始明显振铃实际产品一般不敢用。增益裕度 GM 的定义是GM -|T(jωπ)|dB也就是相位穿越频率处幅值距离 0dB 线还差多少 dB。工程上 GM 一般要求 10dB 以上。这里要特别注意两个裕度是同时成立的约束不是说满足了其中一个就万事大吉。有的系统 PM 看着很漂亮但 GM 很小说明相位已经先到 -180 度而增益还没衰减完这种系统在某种负载条件下照样可能出问题。4.3 用代码自动求出裕度值手动画图时可以用肉眼估个大概但既然已经在用 Python不如把裕度计算也写进代码。最直接的做法是在频率序列里找最接近条件的点# 增益穿越频率幅值最接近 0dB 的点 idx_gain_cross np.argmin(np.abs(mag_db)) fc freqs[idx_gain_cross] pm 180 phase_deg[idx_gain_cross] # 相位穿越频率相位最接近 -180 度的点 idx_phase_cross np.argmin(np.abs(phase_deg - (-180))) fc_phase freqs[idx_phase_cross] gm -mag_db[idx_phase_cross]这个写法的精度取决于扫频密度但作为第一次快速评估完全够用。如果你想要更精确的值可以在穿越点两侧各取一个点做线性插值。对于工程调试先看个大概趋势往往更重要精确插值等需要出报告时再补上。4.4 手写计算结果的意义把最终打印结果摆出来Gain crossover frequency: 998.6 Hz Phase Margin: 44.3 deg Phase crossover frequency: 1.02e6 Hz Gain Margin: 12.5 dB看到这些数字稳定性判断就不再是玄学。PM 只有 5 度时说明系统离振荡边界就差一步电路里任何一点寄生电容带来的附加相移都可能把它推入自激。这个感觉只有亲手算过才会有。5. 实战案例两个参数的差异如何改变稳定性结论5.1 场景一第二极点距离远电路比较稳先看 fp2 1MHz 的情况。参数是 A0 100000fp1 100Hzfp2 1MHzβ 0.1。低频环路增益是 100000 * 0.1 10000 倍换算成 dB 是 80dB。在 100Hz 之前曲线基本是平直的过了 100Hz第一极点让曲线以 -20dB/dec 的斜率下降到 1MHz 附近第二极点加入斜率进一步变成 -40dB/dec。0dB 穿越频率大约在 1MHz 附近因为 80dB 的增益需要 4 个十倍频程的衰减正好从 100Hz 滚到 1MHz。在穿越频率处第一极点贡献约 -90 度相位延迟第二极点贡献约 -45 度总相位约 -135 度算下来 PM 大约 45 度。这个结论是系统能稳定工作但余量不算富余。如果负载变化剧烈动态响应可能有点振铃设计时最好再做一点相位补偿。5.2 场景二第二极点前移系统濒临振荡把 fp2 从 1MHz 挪到 5kHz其他参数保持不变。第一极点 100Hz 到 5kHz 之间曲线以 -20dB/dec 下降5kHz 时增益已经掉了约 34dB从 80dB 变成约 46dB第二极点之后斜率变成 -40dB/dec掉到 0dB 还需要大约 1.15 个十倍频程所以穿越频率落到了几十千赫兹的级别。在这个穿越频率点第一极点贡献接近 -90 度第二极点因为工作频率远高于 5kHz相位贡献已经非常接近 -90 度两者叠加后总相位逼近 -180 度。算出来 PM 只有 2 度甚至可能变成负值。这样的环路在理想仿真里也许勉强能跑但真实电路里只要多一点点寄生电容或者运放参数有离散偏差立刻就会振荡。5.3 从波特图反推如何修正电路遇到 PM 不足工程师的第一反应不应该是一味加电容试错而是回头看波特图上的相位曲线在 0dB 穿越点附近还剩多少余量。如果已经贴到 -180 度常见手段有这么几个降低反馈系数 β也就是降低闭环增益让 0dB 穿越点往低频方向移动躲开第二极点的影响范围增加补偿零点在相位还没跌落之前先“抬头”这一招在电源环路补偿里最常见换带宽更高的运放把第二极点推到更高频率在反馈电阻上并联小电容构成相位超前网络。这些操作都能在波特图上看到对应的曲线变化。学会手绘和分析之后做电路补偿就不是盲调参数而是有目的地塑形这条曲线。6. 常见问题与实操避坑清单6.1 频率轴对数化后低频段为什么感觉点很少logspace 生成的点是等对数间隔所以 1Hz 到 10Hz 在横轴上挤在一起看起来点很少。这是正常现象波特图的目的就是突出宽频带下的动态范围不要为了低频好看去改用线性轴。真正觉得曲线不够平滑时把 logspace 的点数从 800 提到 2000 即可。6.2 相位曲线在 -180 度附近反复跳变大概率是没做 unwrap。检查代码里是否用了 np.unwrap并且确认操作单位是弧度而不是度。如果已经 unwrap 了还在跳那就要检查传递函数本身看看是不是在某个频点存在纯零点导致复数值接近 0angle 计算不稳定。6.3 自己手写的结果和 scipy.signal.bode 对不上scipy.signal.bode 使用的是控制系统标准格式它接收的是传递函数的分子分母系数。如果你定义传递函数时用了角频率而不是频率或者增益因子换算不一致两边曲线当然对不上。建议先拿一个简单的一阶低通滤波器验证比如 H(s) 1 / (1 s/2π/1000)确认两条曲线完全重合后再去算复杂模型。6.4 仿真与实测不一致怎么办波特图分析有一个隐含前提系统是线性的。真实电路里的开关电源大信号非线性、运放输出摆幅限制、温度漂移都会让实测曲线和理论曲线产生偏差。理论波特图告诉你的是“在当前小信号模型下的稳定性趋势”如果实测振荡而理论 PM 只有 20 度这很正常因为模型还没计入所有寄生因素。反过来理论 PM 有 70 度但实测依然振荡那就要回头检查测量方法是否引入了额外负载。以下是实际排查中最常见的几个问题汇总成一张速查表现象可能原因排查方法幅频曲线低频不是平的传递函数里可能有零点检查分子是否带了 s 项相位曲线锯齿状跳变角度卷绕未用 unwrap弧度制下调用 np.unwrap0dB 穿越点附近相位掉太快极点靠得太近或存在寄生极点拉开极点间距或加补偿网络实测振铃但仿真 PM 很高模型未计入寄生参数增加 ESR、寄生电感等影响因素增大 β 后 PM 反而变大穿越频率移到低频躲开高频极点需要同时看 GM单看 PM 不够6.5 扩展如何把这种方法用在 BUCK 电源或更复杂的环路上运放环路只是入门。真实世界里BUCK、BOOST 电源的环路传递函数包含 LC 滤波器、PWM 调制器、补偿网络形式要复杂得多但分析流程完全一致写出传递函数扫频画波特图看裕度。你会经常看到一个由 LC 滤波器形成的二阶谐振峰出现在幅频曲线上这个峰的 Q 值直接决定相位跌落速度。这也是为什么电源工程师人手一份环路仿真脚本的原因不跑一遍波特图根本不敢让电源带满载跑起来。7. 一个可以直接用的完整参考脚本最后我把整篇文章的核心逻辑整合成一个脚本方便你直接复制使用。这个脚本能够计算运放环路增益的幅频和相频数据画出波特图并自动打印相位裕度和增益裕度。import numpy as np import matplotlib.pyplot as plt def compute_loop_gain(freqs, a0, beta, fp1, fp2): w 2 * np.pi * freqs wp1 2 * np.pi * fp1 wp2 2 * np.pi * fp2 return a0 * beta / ((1 1j * w / wp1) * (1 1j * w / wp2)) def analyze_stability(freqs, t_values): mag_db 20 * np.log10(np.abs(t_values)) phase_deg np.degrees(np.unwrap(np.angle(t_values))) idx_gain_cross np.argmin(np.abs(mag_db)) fc freqs[idx_gain_cross] pm 180 phase_deg[idx_gain_cross] idx_phase_cross np.argmin(np.abs(phase_deg - (-180))) fc_phase freqs[idx_phase_cross] gm -mag_db[idx_phase_cross] print(fGain crossover frequency: {fc:.2f} Hz) print(fPhase Margin: {pm:.2f} deg) print(fPhase crossover frequency: {fc_phase:.2f} Hz) print(fGain Margin: {gm:.2f} dB) return mag_db, phase_deg # 参数设定运放开环增益 100dB主极点 100Hz第二极点 5kHz反馈系数 0.1 a0 100000.0 beta 0.1 fp1 100.0 fp2 5e3 freqs np.logspace(0, 6, 2000) t_values compute_loop_gain(freqs, a0, beta, fp1, fp2) mag_db, phase_deg analyze_stability(freqs, t_values) plot_bode(freqs, t_values, titleOp-Amp Loop Gain Bode Plot) plt.show()把 fp2 改成 1e6 再跑一次你会看到相位裕度从几度变成了 40 多度这就是参数对稳定性的直观影响。改参数、跑脚本、看图、读裕度这套循环一旦跑顺你以后设计反馈电路就能形成自己的快速评估流程。我在实际项目里验证下来手写波特图最大的好处还不是“不用买软件”而是它能逼着你去面对每一个公式、每一个元器件参数。当你亲手把运放的 A0、主极点、反馈系数挨个输入进去再看到相位裕度一点点变化那种理解深度是直接调用黑盒函数完全给不了的。建议你把这段脚本存成模板以后分析任何带反馈的电路只需要改传递函数表达式就行。