☰
从零搭建MOSFET SPICE模型:行为源实现与仿真验证
2026/10/7 1:05:22 网站建设 项目流程

在SPICE里调用一个MOSFET,通常只需要一行网表语句:M1 D G S 0 NMOS W=10U L=1U。这行语句的背后,SPICE到底用什么公式算出漏极电流,大多数工程师并不关心,也不清楚。但如果有一天你需要给一种全新器件写模型,或者想彻底搞清楚为什么某个电路老是仿真不收敛,你就必须钻到模型的内部去。这篇文章我来分享一次完整实操:完全从零搭建一个能跑通MOSFET基本工作原理的SPICE模型。不依赖任何PDK,不直接调用内置模型,从物理原理出发,把MOSFET的伏安特性方程用SPICE行为源(Behavioral Source)实现成子电路,再用ngspice做DC扫描验证输出特性与转移特性。整个过程用免费工具就能完整复现,适合模拟IC设计初学者、对器件建模感兴趣的工程师,以及想给特殊器件做macro model的人参考。

1. 为什么要自己写一个MOSFET SPICE模型

1.1 模型不是黑盒子:自研模型能做什么

绝大多数电路设计师用MOSFET模型的方式,就是在原理图上放一个nmos或者pmos符号,然后改宽长比。至于SPICE里那几十个参数分别对应什么物理过程,模型的电流方程长什么样,内部如何切换工作区,几乎没人去深挖。这本身没什么问题,PDK厂商已经帮你把模型校准好了,直接用是最高效的。但一旦遇到以下情况,黑盒子式的使用方式就会卡壳:

  • 你拿到一种新工艺或特殊器件(有机半导体TFT、氧化镓、碳化硅等),厂商没有提供现成的SPICE模型,需要自己根据测试I-V曲线写一个紧凑模型;
  • 你需要做功率MOSFET或IGBT的宏模型(macro model)用于系统级仿真,必须在行为级复现器件的外特性;
  • 电路仿真不收敛,需要判断问题是出在模型本身的数值行为,还是你的电路拓扑;
  • 你在给学生讲器件物理,需要一个“透明”的模型把三个工作区的电流方程完整展示出来。

这些场景下,自己动手搭一个模型不是重复造轮子,而是把“用模型”变成“懂模型”。一旦亲手写过一次,你再回头看BSIM3的几百行参数表,心态会完全不一样——你知道那些参数只不过是在把更复杂的物理现象,用更多的方程片段拼接起来而已。

1.2 选对模型级别:从Level 1开始最靠谱

SPICE里的MOSFET模型按复杂度分了好几个档次。Level 1是最早的Shichman-Hodges模型(1969年提出),方程简单到一页纸能写完;Level 2、Level 3逐步加入了沟道电场、迁移率退化、亚阈值导电等修正;到了BSIM3、BSIM4,那就是几百个参数的工业级模型,专门对付深亚微米器件。

从零实现的第一原则,是选一个“足够简单但物理上自洽”的起点。Level 1就是MOSFET模型世界里的Hello World:参数少(核心就VTO、KP、LAMBDA三个),方程直观,而且完美覆盖了截止、线性、饱和这三个基本工作区。它对长沟道器件(通常L大于2到3微米)有不错的精度,对短沟道器件虽然误差大,但这不是问题——我们的目标是理解原理,不是做65纳米工艺的电路签核。

还有一点很实用:Level 1是ngspice、LTspice内置模型的一个标准档位(LEVEL=1),这意味着你写完自己的模型之后,可以直接拿内置模型跑同一组仿真做对比验证。这种“对标”是建模工作中极重要的一环,能帮你快速发现方程实现中的错误。

1.3 工具与仿真环境准备

本文所有示例都用ngspice演示,因为它是开源免费的,跨平台,而且对行为源(B源)、子电路、DC扫描的支持非常完整。你可以用包管理器直接安装,也可以从ngspice官网下载编译好的版本。Windows用户如果习惯图形界面,用LTspice也能跑通绝大部分示例,只是控制语句的写法稍有差异,我在第7节会专门讲兼容性坑。

另外建议装一个Python环境,配合matplotlib用来做数据对比绘图。文章第6节会用一个微型求解器做交叉验证,你会发现Python版本和SPICE版本的模型可以互相印证,这对排查“是模型错了还是SPICE语法错了”特别有帮助。

2. MOSFET工作原理:写模型前必须想清楚的三件事

2.1 三个工作区:截止、线性、饱和

MOSFET有四个端子:栅极G、漏极D、源极S、衬底B。常规的电路符号里,衬底通常画成一个箭头指向沟道,表示这是一个N沟道器件还是P沟道器件。N沟道增强型MOSFET的工作原理可以用一句话概括:栅极电压超过阈值电压VTH之后,栅氧下方的半导体表面会形成反型层(也就是沟道),把源和漏导通起来。

隔着栅氧看这个器件的行为,很像一条水渠配了一个水闸:栅极电压就是闸门开度,决定水渠里能流过多少水;漏源电压就是水渠出口的落差,决定水流的速度。闸门开得不够,水渠是干的,一滴水也过不去,这是截止区;闸门打开了,但出口落差很小,水量主要由落差决定,水流近似线性的关系,这是线性区(也叫三极管区);落差继续加大,水渠某个位置会被“吸干”——靠近漏端的沟道发生夹断,水流达到上限,再加落差流量也只有微弱增加,这是饱和区。

用数学语言描述就是,VGS决定“有没有沟道”,VDS决定“沟道里的电流形态”。Level 1模型把这三个区间的电流方程分别写出来,用VGS与VTH的大小关系、VDS与VGS-VTH的大小关系来切换。

2.2 阈值电压与体效应:VTH不是常数

阈值电压VTH是MOSFET最核心的参数,它定义了“栅极电压要多高才能形成沟道”。但很多人忽略的一点是,VTH并不是一个固定值,它会随着源极和衬底之间的电压而变化。原因在于,当衬底相对于源极反向偏置时(NMOS的VSB大于0),耗尽层变宽,栅极需要更高的电压才能把半导体表面拉到反型状态。

这个现象叫体效应,数学上用一个平方根公式描述:

VTH = VTO + GAMMA * (√(PHI + VSB) - √PHI)

其中VTO是零偏置时的阈值电压(也就是VSB=0时的阈值),GAMMA是体效应系数,PHI是表面势的近似值(大约等于2倍的费米势)。从公式可以看到,VSB越大,VTH越高。实际电路里,如果源极和衬底没有接在一起——比如差分对里源极公共点电位浮起来,或者NMOS放在不同的阱里——忽略体效应会导致仿真电流明显偏大。

所以在搭模型的时候,要么把衬底端当作摆设直接和源极短接(这在很多基础教程里这么干),要么老老实实把体效应写进去。既然要做“从零实现”,我建议直接把体效应做进去,这样模型才能算一个完整四端器件。

2.3 沟道长度调制:为什么饱和区不是一条直线

理想MOSFET在饱和区电流应该完全不变,是一条水平线。但实测曲线永远带一点上翘。原因是,当VDS增大,漏端的耗尽区会向沟道方向扩张,等效地缩短了沟道长度L。沟道变短,电流自然变大。这个现象叫沟道长度调制。

Level 1模型用一个小技巧处理它:在饱和电流公式后面乘一个(1 + LAMBDA * VDS)的因子。LAMBDA就是沟道长度调制系数,典型值在0.005到0.1每伏之间。这个修正虽然简单,但效果很好:它给出了一个有限输出电阻,等效输出电阻约为ro = 1 / (LAMBDA * ID)。

回到水渠的类比:饱和区里加大源头水压,会把水渠尽头冲蚀掉一截,渠道变短,过水能力略微增加。LAMBDA就是这个“冲蚀程度”的量化。实际工作中,LAMBDA是提取工艺参数时必然要拟合的量,因为模拟放大器增益、恒流源输出阻抗都直接和它相关。

3. Level 1模型数学表达:从物理到方程

3.1 三种工作区电流方程

把上面的物理图像翻译成数学,就得到Level 1模型的三个区间的电流方程。记VOV = VGS - VTH(过驱动电压),对NMOS在VDS ≥ 0的正常正向工作条件下:

截止区(VGS ≤ VTH):ID = 0。

线性区(VGS > VTH,且VDS < VOV):ID = KP * (W/L) * [(VGS - VTH) * VDS - 0.5 * VDS²]。

饱和区(VGS > VTH,且VDS ≥ VOV):ID = 0.5 * KP * (W/L) * (VGS - VTH)² * (1 + LAMBDA * VDS)。

这里面有几个物理细节值得展开。线性区公式里的VDS²/2项,来自沟道电荷沿沟道方向的积分:栅极到沟道的有效电压在源端是VGS-VTH,到了漏端变成VGS-VTH-VDS,沟道内电场强度是沿沟道变化的,所以电流不是简单的欧姆定律,而是一条抛物线。饱和区公式里的0.5因子同样是积分的产物。在VDS等于VOV的边界点上,两个公式算出的电流相等,都等于0.5 * KP * (W/L) * VOV²,这保证了电流曲线的连续性。

3.2 参数表与物理意义

做模型之前,先把参数表建立起来。下面是我在这个练习中使用的参数,也是后续所有仿真和手算的共同基准。

参数物理含义典型值单位对电路的影响
VTO零偏阈值电压0.7V决定导通起点
KP工艺跨导参数(KP = μn * Cox)200µA/V²决定电流能力
LAMBDA沟道长度调制系数0.021/V决定输出电阻
GAMMA体效应系数0.5V^0.5决定衬底偏置灵敏度
PHI表面势近似0.7V体效应公式内部参数
W/L宽长比10-用户设计变量

注意,KP里其实包含了两个东西:载流子迁移率μn和单位面积栅氧电容Cox。如果工艺的栅氧厚度变了,KP也随之改变。这个参数在实际提取时可以从饱和区的√ID对VGS曲线的斜率获得,我记得在讲参数提取时有个口诀:饱和区√ID对VGS是直线,斜率等于√(0.5 * KP * W/L),截距就是VTH。后面验证转移特性时我们正好能看到这一点。

3.3 连续性分析:直接套公式为什么不收敛

把三个区间的公式用IF语句强行拼起来,看起来天经地义,但SPICE的牛顿-拉夫逊迭代算法对连续性和导数有要求。电流本身在区间边界VDS = VOV处是连续的,两边都等于同一个值。但看看导数的表现:

线性区内,dID/dVDS = KP * (W/L) * (VOV - VDS),在VDS = VOV处,这个导数等于0。饱和区里,dID/dVDS = 0.5 * KP * (W/L) * VOV² * LAMBDA。当LAMBDA不为0的时候,这个导数是正数。于是从线性区跨到饱和区的那一瞬间,导数从0跳到一个正数。

这个“导数不连续”在数学上叫折点,对牛顿迭代非常不友好。牛顿法在做迭代时,如果初始猜测落在边界附近,会在两个区间之间来回震荡,产生非收敛或收敛慢的问题。此外,行为源里的IF语句本身是一个硬切换,SPICE在计算雅可比矩阵时对这个切换点附近的处理也比较粗糙。

好在实际使用中,只要LAMBDA不大(0.02这种水平),带点串联电阻,大多数DC扫描都能顺利收敛。但了解这个机理很重要——当你遇到“仿真在某一个具体的VDS值附近死活不收敛”时,第一反应就应该是:这里有一个导数跳变点。

4. 在SPICE中用行为源从零搭模型

4.1 行为源与子电路:两者合起来就是一个“迷你MOSFET”

SPICE的行为源(B源)是一种电压或电流受控源,它的输出值可以由节点电压的任意表达式定义。语法非常简单:Bxxx n+ n- I={表达式}定义电流源,Bxxx n+ n- V={表达式}定义电压源。表达式中可以用V(node1,node2)取节点压差,可以用IF、SQRT、ABS这些函数。

子电路则是一组SPICE语句的封装,用.SUBCKT开头,.ENDS结尾,外部通过X开头的实例来调用。子电路的好处是可以带参数,比如宽长比W/L,这样同一个模型就能对应不同尺寸的器件。我们把B源放在子电路里,子电路四个引脚就是D、G、S、B,一个“迷你MOSFET”就诞生了。

4.2 基础NMOS子电路:先把不带体效应的跑通

先从最简单的版本开始。定义一个名为MY_NMOS的子电路,四个引脚D G S B,参数包括W、L、VTO、KP、LAMBDA。核心就是那个B源,用嵌套的IF语句实现三个工作区的切换。

* 文件名: mynmos.inc .SUBCKT MY_NMOS D G S B + PARAMS: W=10U L=1U VTO=0.7 KP=200U LAMBDA=0.02 BID D S I = { IF( V(G,S) <= VTO, 0, + IF( V(D,S) < (V(G,S)-VTO), + KP*(W/L)*((V(G,S)-VTO)*V(D,S) - 0.5*V(D,S)^2), + 0.5*KP*(W/L)*(V(G,S)-VTO)^2*(1+LAMBDA*V(D,S)) + )) + } .ENDS MY_NMOS

逐行解释一下。第一行的.SUBCKT声明了子电路名和引脚顺序,调用时必须按D、G、S、B的顺序接节点。PARAMS关键字后面跟默认参数值,调用时可以用W=20U L=2U这类写法覆盖。B源的第一层IF判断VGS是否小于等于VTO,是则输出0,进入截止区;否则进入第二层判断,比较VDS和VOV,选线性区或者饱和区的公式。

这里有个容易踩的坑:SPICE对大小写不敏感,写VTO、vto、Vto都能识别;但表达式里的V(G,S)这种节点电压函数,逗号前必须是正端。另外,SPICE里小写u和大写U都是微米前缀(1U等于1e-6),但是如果你手误写成1u和10U混用也不会错,因为W/L是比值,只要分子分母单位一致即可。

这个版本完全没用B端口,调用时把B和S接到同一个节点就行,比如都接地。

4.3 加入体效应:四端口模型才算完整

不带体效应的模型在源衬短接时工作得很好,但真实的电路里源极电位常常浮动,这时就必须考虑VSB对阈值的影响。我推荐的做法不是把整个表达式揉成一个巨大的IF,而是拆成两个B源:一个专门计算阈值电压VTH,输出到内部节点TH;另一个根据VTH计算漏极电流。这样每个B源的表达式都短小清晰,调试的时候能单独看VTH节点对不对。

* 文件名: mynmos.inc(续) .FUNC VSQRT(X) { IF(X < 0, 0, SQRT(X)) } .SUBCKT MY_NMOSB D G S B + PARAMS: W=10U L=1U VTO=0.7 KP=200U LAMBDA=0.02 GAMMA=0.5 PHI=0.7 BTH TH 0 V = { VTO + GAMMA*(VSQRT(PHI + V(S,B)) - VSQRT(PHI)) } BID D S I = { IF( V(G,S) <= V(TH), 0, + IF( V(D,S) < (V(G,S)-V(TH)), + KP*(W/L)*((V(G,S)-V(TH))*V(D,S) - 0.5*V(D,S)^2), + 0.5*KP*(W/L)*(V(G,S)-V(TH))^2*(1+LAMBDA*V(D,S)) + )) + } .ENDS MY_NMOSB

这里有一个细节值得多说一句。当源衬电压VSB取负值(也就是衬底相对源极正向偏置)时,PHI+VSB可能变成负数,直接开根号会出问题。所以我定义了一个VSQRT函数,对负的自变量直接返回0。虽然Level 1模型在衬底正向偏置时物理上已经不太可靠,但这个钳位能避免仿真器遇到NaN中断,属于防御性编程。

BTH源把阈值电压以电压量的形式放到内部节点TH上,BID源里直接用V(TH)参与比较和计算。SPICE子电路内部节点是局部作用域,你实例化多个MY_NMOSB也不会互相冲突,这一点可以放心。

4.4 搭建测试台:用DC扫描验证伏安特性

模型写好了,下一步就是测它。最经典的测试方式是扫描VDS和VGS,画出输出特性曲线族。测试台网表如下:

* 文件: test_mynmos.ngspice .include mynmos.inc VGS G 0 DC 2.0 VDS D 0 DC 0 X1 D G 0 0 MY_NMOS W=10U L=1U .DC VDS 0 5 0.02 VGS 1 4 0.5 .control run setplot dc1 plot -i(VDS) xlabel "VDS(V)" ylabel "ID(A)" .endc .end

.DC语句的第一个扫描变量VDS是内层扫描,从0到5V步进0.02V;第二个扫描变量VGS是外层扫描,从1V到4V步进0.5V,所以会得到7条曲线。.control到.endc是ngspice的控制块,run执行分析,setplot dc1选中DC分析结果,plot -i(VDS)画图。

这里必须解释一下-i(VDS)的符号问题。I(VDS)是电压源内部从正端流向负端的电流,但在测试电路里,漏极电流是从节点D经MOSFET流到地,和I(VDS)的方向正好相反,所以I(VDS)是负的。画图的时候取负号,才能得到正的ID曲线。这个符号问题几乎每个初学者都栽过,我建议你在自己的工程文档里把它写死:凡是直接用电压源测电流,先想清楚电流方向和电压源的关系。

跑完这个网表,你应该能看到一组典型的MOSFET输出特性曲线:每个VGS台阶对应一条从原点出发先近似直线然后弯曲进入饱和区的曲线,饱和区略带正斜率。这组曲线就是我们模型的“身份证”,它正确就说明Level 1方程实现没有大问题。

5. 仿真验证:曲线出来了才算数

5.1 输出特性曲线:手算与仿真对账

仿真曲线出来之后,最重要的一步是手算几个关键点,和仿真结果对账。建模不是“画得像”就行,每个数值都要经得起推敲。

以VGS = 2V为例,VTH = 0.7V,所以过驱动电压VOV = 1.3V。线性区和饱和区的分界点在VDS = 1.3V。饱和电流为:

ID = 0.5 * 200µA/V² * 10 * (1.3V)² * (1 + 0.02 * VDS)

在VDS = 1.3V处,电流等于0.5 * 200µ * 10 * 1.69 = 1690µA,也就是1.69mA。由于LAMBDA=0.02,VDS每增加1V,电流大约增加2%。在VDS = 5V时,ID约等于1.69mA * (1 + 0.02 * 5) = 1.86mA。

再看VGS = 1V的情况,VOV = 0.3V,饱和电流只有0.5 * 200µ * 10 * 0.09 = 90µA。你会发现VGS从1V到2V,电流从90µA跳到1.69mA,差了将近19倍,这正是饱和区电流和过驱动电压平方关系的结果。仿真跑出来的数值应该和这些手算值严格对应。如果对不上,优先检查KP的单位、W/L的赋值、以及是否忘掉了0.5这个因子。

5.2 转移特性与跨导:验证方程的二阶关系

输出特性验证的是VDS方向的物理,转移特性验证的是VGS方向的物理。固定VDS = 3V,扫描VGS从0到5V。因为VDS足够大,只要VGS超过VTH,器件就工作在饱和区,于是ID与(VGS-VTH)²成抛物线关系。

* 文件: test_transfer.ngspice .include mynmos.inc VGS G 0 DC 0 VDS D 0 DC 3 X1 D G 0 0 MY_NMOS W=10U L=1U .DC VGS 0 5 0.01 .control run setplot dc1 plot -i(VDS) plot deriv(-i(VDS)) .endc .end

转移特性曲线应该在VGS = 0.7V以下保持为0,跨过0.7V后呈现二次曲线上升。deriv(-i(VDS))就是跨导gm = dID/dVGS。在饱和区,理论值gm = KP * (W/L) * (VGS - VTH)。比如VGS = 2V时,gm = 200µ * 10 * 1.3 = 2.6mS,你可以直接在deriv绘制的曲线上量一下,应该非常接近这个值。

这一节顺带展示了一个重要的参数提取方法:把√ID对VGS画出来,在饱和区它是一条直线,斜率等于√(0.5 * KP * W/L),和横轴的交点就是VTH。如果你的模型没有bug,这条直线会精确穿过(0.7, 0)这一点。

5.3 与ngspice内置Level 1模型对比评估

自己写的模型再怎么说也是“自嗨”,真正的试金石是跟SPICE内置的Level 1模型对比。内置模型就是大家天天用的那个东西,它经过了无数人的验证。我们把同样的参数喂给内置模型,跑完全相同的扫描:

* 文件: test_builtin.ngspice M1 D G 0 0 NMOSL1 W=10U L=1U .MODEL NMOSL1 NMOS (LEVEL=1 VTO=0.7 KP=200U LAMBDA=0.02) VGS G 0 DC 2.0 VDS D 0 DC 0 .DC VDS 0 5 0.02 VGS 1 4 0.5 .control run setplot dc1 plot -i(VDS) .endc .end

对比方法上,建议用wrdata把两组数据存成CSV,然后在Python里画在一起:

wrdata mynmos_out.csv -i(vds)
import matplotlib.pyplot as plt import pandas as pd df_mine = pd.read_csv('mynmos_out.csv', comment='#') df_builtin = pd.read_csv('builtin_out.csv', comment='#') # 取 VGS=2V 那一段数据绘制对比 plt.plot(df_mine.iloc[:, 0], df_mine.iloc[:, 1], label='MY_NMOS') plt.plot(df_builtin.iloc[:, 0], df_builtin.iloc[:, 1], '--', label='Built-in Level 1') plt.legend() plt.show()

在VDS ≥ 0的正向工作区,两条曲线应该几乎完全重合,误差控制在百分之零点几以内。我自己实测时,差异主要来自内置模型内部对反型电荷的数值处理细节,肉眼基本看不出区别。

不过这里必须坦率指出我们模型的局限:内置模型对VDS < 0的反向工作区也有定义,电流会平滑翻转方向,而我们的IF版本在VDS为负时进入不了任何一个正确的物理描述,会给出匪夷所思的结果。所以这个自研模型严格来说只适用于正向工作区,做系统级模型或者教学演示完全够用,但如果你要仿真同步整流这类存在反向导通的电路,必须补齐反型区的方程。

6. 用Python做交叉验证:从方程到数值求解

6.1 把Level 1方程翻译成Python函数

SPICE里的仿真结果,其实还可以用另一种方式验证:直接写一段Python代码,把相同的方程一字不差地实现一遍。这等于给模型提供了一个“独立参考实现”。好处是,当SPICE仿真结果和你预期不符时,你能用Python快速单点计算,判断到底是方程写错了,还是SPICE语法有问题。

import numpy as np def ids_nmos(vgs, vds, vto=0.7, kp=200e-6, w=10e-6, l=1e-6, lam=0.02, gamma=0.0, phi=0.7, vsb=0.0): """Level 1 NMOS 漏极电流,单位安培。""" if gamma > 0: vth = vto + gamma * (np.sqrt(max(phi + vsb, 0.0)) - np.sqrt(phi)) else: vth = vto vov = vgs - vth if vov <= 0.0: return 0.0 if vds < vov: return kp * (w / l) * (vov * vds - 0.5 * vds * vds) return 0.5 * kp * (w / l) * vov * vov * (1.0 + lam * vds)

注意Python版的阈值电压计算里用了max(phi + vsb, 0.0),这正是SPICE版里VSQRT函数做的那个钳位。保持两边逻辑一致,对账才可信。调用一下:ids_nmos(2.0, 3.0),理论上应该得到约1.73毫安(VDS=3V时饱和电流1.69mA乘以1.06的沟道长度调制因子)。

6.2 用牛顿-拉夫逊求DC工作点:你到底在解什么

SPICE最核心的DC工作点求解,本质上是在解一组非线性方程组。拿最简单的电阻负载共源极放大器来看,电路方程就一个节点方程:MOSFET漏极电流等于电阻电流,也就是ID(VGS, VDS) = (VDD - VDS) / RD。这个方程只有一个未知数VDS,一维零点问题,用牛顿-拉夫逊迭代就能解。

def solve_cs(vdd=5.0, rd=1e3, vg=2.0, vds_init=1.0, tol=1e-12, **kw): """牛顿迭代求解共源极直流工作点,返回 (VDS, ID)。""" vds = vds_init for _ in range(50): ids = ids_nmos(vg, vds, **kw) f = ids - (vdd - vds) / rd # KCL 残差 eps = 1e-9 f_dot = (ids_nmos(vg, vds + eps, **kw) - ids) / eps + 1.0 / rd vds_new = vds - f / f_dot # 牛顿更新 if abs(vds_new - vds) < tol: return vds_new, ids_nmos(vg, vds_new, **kw) vds = vds_new raise RuntimeError("牛顿迭代未收敛")

迭代过程可以这样理解:先猜一个VDS,算出此时KCL的残差f,再估计f对VDS的斜率f_dot,然后沿着斜率把残差压到零的方向迈一步,重复直到变化量足够小。SPICE内部做的事情和这段Python几乎一样,只不过它的未知量是从几个到几万个节点电压,靠稀疏矩阵求解整个线性化系统。

用上面的参数跑一下:VDD=5V,RD=1kΩ,VG=2V,手算结果是VDS约等于3.2V,ID约等于1.80mA。solve_cs()应该给出非常接近的值。这一步跑通,说明你不仅会搭模型,还真正理解了这个模型在电路里是如何被“求解”的。

6.3 与SPICE仿真结果对账

最后把这个共源极电路原样搬进ngspice,用.op分析直接打印工作点:

* 文件: test_cs.ngspice .include mynmos.inc VDD VD 0 DC 5 RD VD VOUT 1K VG G 0 DC 2 X1 VOUT G 0 0 MY_NMOS W=10U L=1U .OP .control run print v(vout) .endc .end

这个电路里VOUT就是漏极节点电压,也就是我们上面解的VDS。仿真打印出的V(VOUT)应该和Python求解结果一致,误差在微伏级别。两边的方程一模一样,只是求解路径不同,这个对账实际上是在验证SPICE行为源实现的正确性。

7. 常见问题与排查技巧

7.1 仿真不收敛的三大对策

自写模型最常见的翻车现场就是不收敛。根据我的经验,大概率是以下三个原因之一。

第一个是IF切换点的导数跳变。前面第3.3节提过,LAMBDA不为0时,线性区和饱和区的边界处导数不连续,牛顿迭代容易在边界附近抽搐。最有效的办法是给漏极串联一个很小的电阻,人为引入一个寄生节点,把电流求解的“地形”磨平一点。给子电路加一个参数化的串联电阻:

.SUBCKT MY_NMOSR D G S B + PARAMS: W=10U L=1U VTO=0.7 KP=200U LAMBDA=0.02 RDS=1M RD D DINT {RDS} BID DINT S I = { ... } .ENDS MY_NMOSR

这里要特别提醒SPICE单位的坑:1M在SPICE里是1毫欧姆,不是1兆欧姆。SPICE规定标准后缀里M表示毫(1e-3),兆欧要用1MEG。无数工程师在这个地方写错过,自查一下你的网表里有没有这种单位混淆。

第二个原因是初始猜测离解太远。可以用.nodeset或者.ic给关键节点一个初始电压,比如.nodeset V(d)=2 V(g)=2,能显著提升收敛概率。第三个原因则是容差设置太苛刻或太宽松,可以尝试.options reltol=1e-4 abstol=1e-12配合调整。

7.2 曲线形状不对:系统性排查表

曲线能跑出来但形状不对,这是另一个高频问题。我总结了一张排查表,按现象速查:

现象可能原因检查项
截止区电流不为0VGS与VTO比较方向写反;D/S引脚接反检查IF第一层条件,确认V(G,S)是栅源电压
线性区斜率不对KP或W/L数量级错误;VDS²项系数不是0.5手算一个VDS很小的工作点,逐项核对
饱和区电流异常上翘LAMBDA设置过大临时把LAMBDA设为0,看曲线是否变平
体效应完全没效果B端口没接;GAMMA写成0;PHI用了0测量V(TH)节点电压,验证VSB变化是否抬升VTH
有过冲或振铃IF切换导数跳变加RDS、增大步长细化扫描

我自己的习惯是,每次改完模型参数,先在Python参考实现里手算三个点:截止区一点(VGS=0.5V)、线性区一点(VGS=2V,VDS=0.5V)、饱和区一点(VGS=2V,VDS=3V)。三个点全对,才轮到SPICE出场。这样能快速隔离“数学错误”和“语法错误”。

7.3 不同SPICE工具的兼容性差异

如果你打算把这份模型搬到LTspice或者其他SPICE工具里,有几个兼容性差异要留意。

ngspice的控制块(.control/.endc)和plot命令是它自己的方言,LTspice没有这套东西。LTspice里跑DC扫描,直接在网表里写.dc VDS 0 5 0.02 VGS 1 4 0.5,再看结果时用波形窗口手动加曲线就行。行为源的表达式函数在两家基本通用,但函数名的大小写处理有细微差别,建议在文件头部加注释写明“本模型语法兼容ngspice和LTspice行为源”。

.FUNC函数在ngspice里是全局定义,不能写在.SUBCKT块内部,只能放在子电路外面或者单独的include文件里,这一点很容易踩坑。另外,子电路内部节点名是局部作用域,但节点0(地)是全局的,不要在子电路内部把0当作普通节点随便用。

最后是参数覆盖语法。.SUBCKT...PARAMS:在ngspice和LTspice里都认,但实例化时覆盖参数的写法是X1 D G S B MY_NMOS W=20U L=2U,参数名和值之间用等号,多个参数用空格分隔。我见过有人在这里用逗号,直接报错。

7.4 行为源表达式太长:用分节点法提升可读性

最后一个建议偏工程习惯。行为源表达式一旦超过三层嵌套IF,可读性急剧下降,括号配对错误也难找。我的做法是把中间物理量单独用B源算出来,输出到内部节点,再供最终电流源引用。体的阈值电压就是典型的中间量,第4.3节已经演示了这个套路。

类似的,你还可以把VOV = V(G,S) - V(TH)单独算成一个节点电压,甚至把“是否截止”判断也做成一个逻辑节点。这样每个B源都是一句通俗易懂的表达式,调试时可以直接plot v(th)看阈值电压对不对,这才是自研模型真正的优势——模型内部的每一处细节你都能看到。

做完整套练习之后,我最大的感受是:建模这事儿没什么神秘的,就是把你脑子里的物理公式翻译成仿真器能懂的语言,再做一次彻底的对账。你对手里模型的理解深度,完全取决于你愿意花多少时间去拆开它、检查它、质疑它。下次当你的仿真又双叒叕不收敛时,不妨先想想,是不是模型本身在某个边界处给你挖了一个导数跳变的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询