1. 从一条曲线说起:GM-ID 到底在解决什么问题
做模拟电路设计的人,对 GM-ID 方法论应该都不陌生。这套方法最早由 F. Silveira 等人在 1996 年提出,核心思想是把 MOS 管的所有小信号参数——跨导 gm、输出电导 gds、特征频率 fT、本征增益 gm/gds——全部表达为归一化电流密度 ID/W 的函数,而这个函数关系只跟工艺本身有关,跟管子的具体尺寸无关。换句话说,只要把某个工艺下 NMOS 和 PMOS 的 GM-ID 曲线族提取出来,后续所有设计都可以在这套曲线上"查表",而不需要反复跑 DC 仿真去试参数。
我刚开始接触这套方法的时候,觉得它有点"多此一举"——Cadence 里直接跑个 DC 仿真,看 gm、gds 的数值不就行了吗?后来做低功耗运放的时候才明白,传统方法的问题在于:你调一个 W/L,gm 变了,ro 也变了,偏置电流也变了,几个变量耦合在一起,你根本不知道当前工作点离最优区域有多远。而 GM-ID 方法把"设计自由度"和"工艺特性"解耦了,你只需要在归一化曲线上选一个工作点,剩下的尺寸和电流自然就确定了。
这套流程的完整链路是:在 Cadence 里搭建测试台,扫描管子的栅压和沟道长度,导出 gm/ID、fT、gm/gds 等参数随 ID/(W/L) 变化的原始数据;然后把数据导入 Matlab,做插值、平滑、绘图,生成可复用的设计曲线族;最后在实际电路设计中,根据增益、带宽、功耗指标反查曲线,确定管子的工作点和尺寸。整条链路里,Cadence 负责"产生数据",Matlab 负责"加工和呈现数据",中间的数据导出环节是最容易出问题的地方。
这篇文章面向的是已经会用 Cadence 跑基本仿真、但还没系统跑过 GM-ID 流程的模拟 IC 设计者,也适合做课程设计或毕业设计、需要快速建立工艺特性认知的同学。我会把整个流程拆开讲,重点放在那些文档里不会写、但实际跑的时候一定会遇到的坑上。
2. 测试台搭建:为什么不能直接用单管 DC 仿真
2.1 单管仿真台的结构设计
很多人第一次跑 GM-ID,直接在 Cadence 里放一个 NMOS,栅极接电压源,漏极接电压源,跑 DC 扫描 VGS,然后看 gm 和 ID。这样做能出数据,但有几个致命问题。
第一个问题是沟道长度调制效应。单管在饱和区,VDS 固定的时候,gds 是有限的,但如果你只扫 VGS 不扫 VDS,你得到的 gm/ID 曲线会包含 VDS 的影响,而 GM-ID 方法论要求的是"本征"特性,也就是 VDS 足够大、gds 影响可以忽略时的值。所以测试台必须包含 VDS 扫描,或者至少把 VDS 固定在足够大的值上(通常取 VDD 的 0.6~0.8 倍),并且要验证在这个 VDS 下管子确实处于强饱和区。
第二个问题是 W 和 L 的取值。GM-ID 曲线是归一化的,理论上跟 W 无关,但 L 必须作为参数扫描。因为不同 L 下,管子的速度、增益、匹配特性都不一样。通常 L 取工艺最小 L 到 10 倍最小 L 之间的若干值,比如 0.18um 工艺取 L = 0.18, 0.24, 0.36, 0.5, 0.8, 1.2, 2.0, 4.0 um 这样一组。W 可以固定一个方便的值,比如 10um,因为归一化之后 W 会被除掉。
第三个问题是体效应。如果你把源极接地、体极也接地,那 VSB = 0,体效应为零。但实际电路中源极电位往往不等于体极电位,所以测试台最好把体极单独接一个电压源,扫描 VSB,这样你还能顺便得到体效应系数随工作点变化的曲线。不过如果只是为了跑基本的 GM-ID 曲线,VSB = 0 也可以接受,只是要在文档里注明。
我常用的测试台结构是这样的:NMOS 的源极和体极接地,栅极接一个 DC 电压源 VG,漏极接一个 DC 电压源 VD。跑 DC 仿真时,扫描 VG 从 0 到 VDD,同时用 parametric analysis 扫描 L 的一组值。仿真器会输出每个 L 下 ID、gm、gds、cgs、cgd 等参数随 VG 变化的曲线。
2.2 仿真器设置里那些容易忽略的选项
Cadence 的 DC 仿真默认设置里,有几个选项会直接影响 GM-ID 数据的质量。
首先是save选项。默认情况下,仿真器只保存节点电压和少量电流,gm、gds 这些派生参数需要显式保存。你需要在 ADE 的 Outputs 里添加gm、gds、cgs、cgd、ft等表达式,或者直接在 netlist 里加.save语句。我一般会在 ADE 里用OP参数保存所有小信号参数,这样跑完 DC 之后可以直接在 Results 里看到。
其次是收敛精度。GM-ID 曲线在亚阈值区变化非常剧烈,ID 从 pA 级到 uA 级跨越好几个数量级,如果仿真器的相对误差设得太大(比如默认的 1e-3),亚阈值区的数据点会非常稀疏,插值之后曲线会失真。我通常把reltol设到 1e-5,abstol设到 1e-15,vntol设到 1e-9。代价是仿真时间变长,但 GM-ID 曲线只需要跑一次,多花几分钟是值得的。
第三是扫描点的密度。DC 扫描默认是线性步长,但 ID 随 VG 是指数变化的,线性步长在亚阈值区会漏掉很多细节。我一般用dec模式,每十倍频程 100 个点,或者直接用log扫描。如果仿真器支持,还可以用adaptive模式,让仿真器根据曲线曲率自动调整步长。
提示:跑完 DC 之后,先别急着导出数据。在 Cadence 里把 gm/ID 对 ID/(W/L) 的曲线画出来看一眼,如果曲线在亚阈值区出现明显的锯齿或跳变,说明扫描点不够密或者收敛精度不够,需要回去调仿真设置。
2.3 参数扫描的组织方式
L 的扫描有两种做法:一种是在 ADE 里用 parametric analysis,一次跑完所有 L;另一种是每个 L 单独跑一次,分别导出数据。前者效率高,但导出的数据文件会包含所有 L 的曲线,后续在 Matlab 里需要按 L 拆分。后者效率低,但数据组织简单。
我倾向于用 parametric analysis,因为 Cadence 的ocean脚本可以一次性把所有 L 的数据导出成结构化的文本文件。具体做法是:在 ADE 里设置好 parametric analysis,跑完之后用ocnPrint命令把每条曲线的数据打印到文件里,文件名里带上 L 的值。这样 Matlab 读进来的时候,直接按文件名分组就行。
如果你用的是spectre仿真器,还可以用mdl或者psf格式导出,数据精度比文本格式高,但 Matlab 读取需要额外的解析工具。我一般用 CSV 格式,虽然精度略低,但通用性好,Matlab 的readmatrix直接就能读。
3. Cadence 数据导出:从波形窗口到 CSV 的完整链路
3.1 为什么直接复制波形数据不可靠
很多教程教你直接在 Cadence 的波形窗口里选中曲线,右键导出 CSV。这个方法能用,但有几个隐患。
第一,波形窗口显示的是"抽稀"之后的数据。Cadence 为了显示流畅,默认会对原始数据做降采样,你看到的曲线可能只有几千个点,而实际仿真可能产生了几十万个点。导出的 CSV 里就是这些降采样后的点,亚阈值区的细节全丢了。
第二,波形窗口的导出功能对多曲线支持不好。如果你一次选中了 8 个 L 的 gm/ID 曲线,导出的 CSV 里所有曲线混在一起,没有明确的 L 标识,后续在 Matlab 里拆分非常麻烦。
第三,波形窗口的导出格式依赖当前显示设置。如果你改了坐标轴范围或者开了对数坐标,导出的数据可能被截断或者做了变换,跟你预期的原始数据不一致。
所以我的做法是:不用波形窗口导出,直接用ocean脚本或者ocnPrint命令从仿真结果数据库里打印原始数据。
3.2 用 Ocean 脚本批量导出数据
Ocean 是 Cadence 的脚本语言,可以自动化仿真和数据处理。下面是我常用的一个导出脚本的骨架:
; 打开仿真结果数据库 openResults("simulation_results/psf") ; 获取所有 L 的列表 L_list = list(0.18 0.24 0.36 0.5 0.8 1.2 2.0 4.0) ; 对每个 L 导出数据 foreach(L L_list ; 选择对应的曲线 selectResult('dc) ; 获取 VG 扫描变量 vg = drGetWaveformXVec("VG") ; 获取 ID、gm、gds 等参数 id = drGetWaveformYVec("ID") gm = drGetWaveformYVec("gm") gds = drGetWaveformYVec("gds") ; 计算归一化电流密度 id_norm = id / (10.0 * L) ; W=10um ; 打印到 CSV 文件 outfile = sprintf(nil "gm_id_L_%g.csv" L) ocnPrint(?output outfile vg id id_norm gm gds ?numberNotation 'engineering ?numSpaces 1) )这个脚本的关键点在于:drGetWaveformXVec和drGetWaveformYVec直接访问原始数据向量,不做任何降采样;ocnPrint的?numberNotation 'engineering选项保证数值以工程记数法输出,避免科学记数法在 Matlab 里解析出错;文件名里带上 L 的值,方便后续分组。
如果你不熟悉 Ocean 脚本,也可以用 ADE 的Results -> Print -> Transient/DC菜单,手动选择每条曲线打印到文件。虽然麻烦一点,但效果一样。
3.3 数据格式的坑:单位、符号和缺失值
Cadence 导出的 CSV 有几个常见的格式问题,不处理的话 Matlab 读进来会出错。
第一个是单位后缀。Cadence 的ocnPrint默认会在数值后面加单位,比如1.234e-5会打印成12.34u。Matlab 的readmatrix不认识u、n、p这些后缀,会直接报错。解决办法是在ocnPrint里加?numberNotation 'scientific,强制用科学记数法输出,不带单位后缀。
第二个是符号问题。有些版本的 Cadence 在打印负数时会在数值和负号之间加空格,比如- 1.234e-5,Matlab 会把它解析成两个数。解决办法是在 Matlab 里用strrep把-替换成-,或者用textscan自定义解析格式。
第三个是缺失值。亚阈值区 ID 非常小,可能低于仿真器的abstol,导致某些点被标记为NaN或者直接缺失。Matlab 读进来之后,这些点会变成NaN,画图的时候曲线会断开。处理办法是在 Matlab 里用fillmissing做插值填充,或者直接用isnan过滤掉这些点。
我一般会在 Matlab 里写一个专门的解析函数,处理这些格式问题:
function data = parse_cadence_csv(filename) % 读取原始文本 fid = fopen(filename, 'r'); raw = textscan(fid, '%s', 'Delimiter', '\n'); fclose(fid); raw = raw{1}; % 找到数据起始行(跳过表头) start_idx = find(~cellfun(@isempty, regexp(raw, '^\s*[-+]?\d', 'once')), 1); raw = raw(start_idx:end); % 替换单位后缀和多余空格 raw = regexprep(raw, '\s+', ' '); raw = strrep(raw, '- ', '-'); % 解析数值 data = zeros(length(raw), 5); for i = 1:length(raw) vals = sscanf(raw{i}, '%f'); if length(vals) >= 5 data(i, :) = vals(1:5); end end % 过滤 NaN 和无效行 data = data(~any(isnan(data), 2), :); end这个函数处理了单位后缀、负号空格、缺失值三个问题,实测下来对大多数 Cadence 导出的 CSV 都能正常工作。
4. Matlab 可视化:从原始数据到可复用设计曲线
4.1 数据清洗:插值、平滑和异常点剔除
Cadence 导出的原始数据,直接画出来往往很难看。亚阈值区的点稀疏,饱和区的点密集,曲线在过渡区会有明显的折角。所以第一步是数据清洗。
插值的目的是把非均匀采样的数据变成均匀采样,方便后续计算和绘图。我一般用interp1做对数插值,因为 ID 跨越多个数量级,线性插值在亚阈值区会严重失真:
% 原始数据:id_norm 和 gm_id % 先取对数,再插值 log_id = log10(id_norm); log_id_uniform = linspace(min(log_id), max(log_id), 1000); gm_id_uniform = interp1(log_id, gm_id, log_id_uniform, 'pchip'); id_norm_uniform = 10.^log_id_uniform;pchip是保形插值,不会像spline那样在数据稀疏区产生过冲,适合 GM-ID 这种单调性较强的曲线。
平滑的目的是去掉仿真器数值噪声带来的毛刺。我一般用smoothdata的gaussian方法,窗口大小取数据点数的 1% 左右。窗口太大会把曲线的真实特征也平滑掉,太小则去噪效果不明显。
异常点剔除主要是处理仿真不收敛导致的跳变点。判断方法是看相邻点的二阶差分,如果某个点的二阶差分超过阈值的 10 倍,就把它标记为异常点,用邻近点的插值替换。
注意:数据清洗的顺序很重要。先剔除异常点,再插值,最后平滑。如果先平滑再插值,异常点会被平滑算法扩散到邻近区域,污染更多数据。
4.2 绘制 GM-ID 曲线族:让曲线自己说话
GM-ID 曲线族的标准画法是:横轴是归一化电流密度 ID/(W/L),对数坐标;纵轴是 gm/ID,线性坐标;每条曲线对应一个 L 值。这样画出来的曲线族,从左到右依次是亚阈值区、 moderate inversion 区、强反型区。
但仅仅画出 gm/ID 是不够的。实际设计中,你还需要看 fT、gm/gds、Cgs 等参数。我一般会画一个 2x2 的子图矩阵:
- 左上:gm/ID vs ID/(W/L),这是最核心的曲线
- 右上:fT vs ID/(W/L),决定管子的速度
- 左下:gm/gds vs ID/(W/L),决定本征增益
- 右下:Cgs/(W*L) vs ID/(W/L),决定寄生电容
这样一张图,基本涵盖了模拟设计需要的所有信息。画图的时候,我会用不同的颜色和线型区分不同的 L,并且在图例里标注 L 的值。颜色用linescolormap,线型用-、--、-.、:循环,保证黑白打印也能区分。
figure('Position', [100 100 1200 900]); L_list = [0.18 0.24 0.36 0.5 0.8 1.2 2.0 4.0]; colors = lines(length(L_list)); line_styles = {'-', '--', '-.', ':'}; for i = 1:length(L_list) subplot(2,2,1); semilogx(id_norm{i}, gm_id{i}, 'Color', colors(i,:), ... 'LineStyle', line_styles{mod(i-1,4)+1}, 'LineWidth', 1.5); hold on; subplot(2,2,2); semilogx(id_norm{i}, ft{i}, 'Color', colors(i,:), ... 'LineStyle', line_styles{mod(i-1,4)+1}, 'LineWidth', 1.5); hold on; % ... 其他子图类似 end subplot(2,2,1); xlabel('I_D/(W/L) [A]'); ylabel('g_m/I_D [1/V]'); title('GM-ID 曲线族'); grid on; legend(arrayfun(@(x) sprintf('L=%.2fum', x), L_list, 'UniformOutput', false), ... 'Location', 'northeast');这张图跑出来之后,你会发现几个有意思的现象。第一,所有 L 的曲线在亚阈值区几乎重合,因为亚阈值区的 gm/ID 理论上只跟工艺有关,跟 L 无关。第二,随着 L 增大,fT 明显下降,因为沟道越长,渡越时间越长。第三,gm/gds 随着 L 增大而增大,因为输出电阻跟 L 成正比。这些现象在教科书上都有,但只有自己跑出来,你才会有直观的感受。
4.3 从曲线到设计:反查工作点的实操方法
有了曲线族,实际设计的时候怎么用?举个例子:你要设计一个单级共源放大器,增益要求 40dB,带宽要求 10MHz,负载电容 1pF,功耗预算 1mW,电源电压 1.8V。
第一步,确定 gm/ID。增益 40dB 意味着 gm/gds = 100。在 gm/gds vs ID/(W/L) 的曲线上,找到 gm/gds = 100 对应的 ID/(W/L) 值。假设这个值是 0.5uA,那么 gm/ID 大约在 15~20 之间(具体值从 gm/ID 曲线上读)。
第二步,确定电流。功耗 1mW,电源 1.8V,总电流约 0.56mA。单级放大器的话,这个电流全部流过输入管。
第三步,确定尺寸。ID = 0.56mA,ID/(W/L) = 0.5uA,所以 W/L = 0.56m / 0.5u = 1120。如果 L 取 0.5um,那么 W = 560um。这个尺寸很大,但为了满足增益和功耗要求,是合理的。
第四步,验证带宽。gm = (gm/ID) * ID = 18 * 0.56m = 10mS。带宽 = gm / (2piCL) = 10m / (2pi1p) = 1.6GHz,远大于 10MHz 的要求。说明带宽不是瓶颈,可以适当降低电流来省功耗。
这个反查过程,如果手动在曲线上读点,效率很低。我一般会在 Matlab 里写一个插值函数,输入目标 gm/ID 或 gm/gds,输出对应的 ID/(W/L) 和 L:
function [id_norm, L_opt] = lookup_gm_id(target_gm_id, gm_id_data, id_norm_data, L_list) % 在所有 L 的曲线中查找最接近目标 gm/ID 的点 min_error = inf; id_norm = NaN; L_opt = NaN; for i = 1:length(L_list) [~, idx] = min(abs(gm_id_data{i} - target_gm_id)); error = abs(gm_id_data{i}(idx) - target_gm_id); if error < min_error min_error = error; id_norm = id_norm_data{i}(idx); L_opt = L_list(i); end end end这个函数虽然简单,但在实际设计中非常实用。你可以把它封装成一个 GUI,滑动条调目标 gm/ID,实时显示对应的 ID/(W/L) 和 L,设计效率会高很多。
5. 踩坑实录:那些让我熬夜的报错和异常
5.1 仿真不收敛:从报错信息到根因定位
跑 GM-ID 扫描的时候,最常见的报错就是DC convergence failure。Cadence 的报错信息通常只告诉你"在某个电压点不收敛",但不会告诉你为什么不收敛。
我的排查链路是这样的:首先看报错点的电压值。如果是在 VG 接近 0 的时候不收敛,那大概率是亚阈值区的数值问题。亚阈值区 ID 极小,仿真器的abstol如果设得太大,电流方程会变成 0 = 0 的退化形式,导致不收敛。解决办法是把abstol从默认的 1e-12 降到 1e-15。
如果是在 VG 接近 VDD 的时候不收敛,那可能是管子进入了线性区,gds 变得很大,节点导纳矩阵的条件数变差。解决办法是加一个小的漏极电阻(比如 1 欧姆),或者把 VDS 设得小一点,避免管子深度进入线性区。
如果是在中间某个点不收敛,那可能是扫描步长太大,仿真器跳过了收敛域。解决办法是把扫描步长减半,或者用adaptive模式。
还有一种情况是仿真器报singular matrix,这通常是因为某个节点没有直流通路。检查测试台,确保每个节点都有到地的电阻或者电流源。
提示:如果反复调仿真设置都不收敛,可以试试先用
dc分析跑一个粗略的扫描,找到不收敛的点,然后在该点附近用dc的sweep模式做精细扫描。有时候不收敛只是局部现象,换个扫描路径就绕过去了。
5.2 数据导出为空:路径、权限和格式的三重检查
跑完仿真,兴冲冲地导出数据,结果 CSV 文件是空的,或者只有表头没有数据。这种情况我遇到过好几次,原因无非三个。
第一,路径不对。Ocean 脚本里的openResults路径是相对于当前工作目录的,如果你在 ADE 里跑仿真,工作目录可能是~/simulation/xxx/psf,但你在终端里跑脚本,工作目录是~,路径就对不上。解决办法是用绝对路径,或者在脚本开头加cd命令切换到正确目录。
第二,权限不够。Cadence 的仿真结果数据库默认是只读的,如果你用ocnPrint往一个没有写权限的目录里写文件,会静默失败,不报错但文件是空的。解决办法是检查目标目录的写权限,或者把输出目录设到/tmp下。
第三,格式不匹配。ocnPrint的?output参数如果指向一个已经存在的文件,默认是追加模式,不是覆盖模式。如果你之前跑过一次,文件里已经有旧数据,新数据会追加在后面,Matlab 读进来会混在一起。解决办法是在脚本开头用delete命令删掉旧文件,或者用?mode 'overwrite'参数强制覆盖。
5.3 曲线异常:从物理直觉反推仿真设置
有时候数据导出来了,画出来的曲线却不对劲。比如 gm/ID 曲线在亚阈值区不是平的,而是往上翘;或者 fT 曲线在某个 L 下突然掉下去。这些异常往往不是数据处理的锅,而是仿真设置有问题。
gm/ID 在亚阈值区往上翘,通常是因为 VDS 太小,管子没有完全进入饱和区,gds 的影响混进了 gm 里。解决办法是把 VDS 调大,或者检查测试台里漏极电压源的设置。
fT 曲线突然掉下去,可能是某个 L 下的仿真没有收敛,数据点全是 NaN,Matlab 插值之后画出来就是一条直线或者跳变。解决办法是回到 Cadence 里检查那个 L 的仿真日志,看有没有收敛警告。
还有一种情况是曲线在 moderate inversion 区出现台阶,这通常是扫描点不够密导致的。解决办法是增加扫描点数,或者用log扫描代替线性扫描。
我一般会在 Matlab 里加一个自动检查函数,对每条曲线做单调性检查和连续性检查,发现异常就打印警告,提示我回去检查仿真设置。这个函数帮我省了很多来回折腾的时间。
6. 效率提升:脚本化、模板化和自动化
6.1 把重复劳动交给脚本
GM-ID 流程里,最耗时的不是仿真本身,而是重复的设置和导出操作。如果你每次都要手动在 ADE 里设参数、跑仿真、导出数据、写 Matlab 脚本,一天下来也跑不了几个工艺角。
我的做法是把整个流程脚本化。Cadence 端用 Ocean 脚本,把测试台搭建、仿真设置、数据导出全部自动化。Matlab 端用一个主脚本,自动读取指定目录下的所有 CSV 文件,做数据清洗、绘图、保存图片。整个流程只需要改几个配置参数,比如工艺库路径、L 的列表、输出目录,然后一键运行。
Ocean 脚本的自动化程度可以做到什么地步?我给你看一个我常用的模板:
; 配置区 lib_name = "my_tech_lib" cell_name = "nmos_gm_id" L_list = list(0.18 0.24 0.36 0.5 0.8 1.2 2.0 4.0) VDD = 1.8 W = 10.0 ; 打开设计 dbOpenCellViewByType(lib_name cell_name "schematic" "" "r") ; 设置仿真环境 design("my_tech_lib" "nmos_gm_id") simulator('spectre) analysis('dc ?saveOppoint t ?sweepVar "VG" ?start 0 ?stop VDD ?step 0.01) ; 参数扫描 foreach(L L_list desVar("L" L) desVar("W" W) run() ; 导出数据 selectResult('dc) ocnPrint(?output sprintf(nil "gm_id_L_%g.csv" L) ?numberNotation 'scientific vg id gm gds cgs cgd) )这个脚本跑一次,就能把所有 L 的数据全部导出。你只需要在 Matlab 端写一个循环,把 CSV 文件读进来,就能出图。
6.2 模板化:让每次新工艺都能快速上手
换一个工艺,GM-ID 流程要重新跑一遍。如果每次都从头搭测试台、写脚本,效率太低。我的做法是做一个模板工程,包含测试台原理图、Ocean 脚本、Matlab 脚本三部分。换工艺的时候,只需要改几个地方:工艺库的名字、管子的模型名、电源电压、L 的列表。其他部分原封不动。
模板工程的关键是参数化。测试台里的管子模型名不要写死,用desVar定义成变量;Ocean 脚本里的路径和文件名用变量拼接;Matlab 脚本里的工艺参数放在一个单独的配置文件里。这样换工艺的时候,只需要改配置文件,不用动核心代码。
我还会在模板里预置几个常用的工艺角,比如 TT、SS、FF、SF、FS。跑 GM-ID 的时候,顺便把几个工艺角都跑一遍,这样后续设计的时候可以直接看工艺角之间的差异,不用等到最后才做 corner 仿真。
6.3 自动化检查:让脚本帮你发现异常
数据导出之后,不要急着画图。先让脚本做一轮自动检查,看看数据有没有问题。我一般会检查这几项:
- 数据点数量是否足够(每个 L 至少 100 个点)
- ID 是否单调递增(随 VG 增大)
- gm/ID 是否在合理范围内(亚阈值区 20~30,强反型区 5~15)
- fT 是否为正数且单调(在强反型区)
- 是否有 NaN 或 Inf
如果任何一项检查不通过,脚本会打印警告,并跳过对应的 L,避免污染后续的绘图和分析。这个自动检查帮我发现了好几次仿真设置的问题,比如某个 L 下 VDS 设错了,导致管子没进饱和区。
7. 从 GM-ID 曲线到实际设计决策
7.1 工作点选择的权衡:增益、速度和功耗
GM-ID 曲线最大的价值,是让你在设计初期就能看清增益、速度、功耗三者之间的权衡关系。
在 gm/ID vs ID/(W/L) 的曲线上,从左到右,gm/ID 从 30 降到 5。gm/ID 越高,意味着同样的电流能获得更高的跨导,也就是更高的增益和更好的噪声性能。但高 gm/ID 对应的是低电流密度,管子工作在亚阈值区或 moderate inversion 区,速度慢,匹配差。
反过来,gm/ID 越低,管子工作在强反型区,速度快,匹配好,但增益低,噪声大。所以工作点的选择,本质上是在"模拟性能"和"速度/匹配"之间找平衡。
我的经验是:对于低功耗、高增益的电路(比如生物医疗信号链的前置放大器),gm/ID 取 20~25,让管子工作在 moderate inversion 区;对于高速电路(比如射频前端),gm/ID 取 8~12,让管子工作在强反型区;对于通用电路,gm/ID 取 15 左右,折中。
7.2 用 gm/gds 曲线确定本征增益上限
gm/gds 曲线告诉你,在给定的 L 和 ID/(W/L) 下,管子的本征增益最大能到多少。这个值决定了单级放大器的增益上限。如果你需要 60dB 的增益,单级放大器肯定做不到(本征增益通常只有 40~50dB),必须用两级或者 cascode 结构。
在 gm/gds vs ID/(W/L) 的曲线上,你会发现一个有趣的现象:gm/gds 随着 ID/(W/L) 减小而增大,在亚阈值区达到最大值。但这个最大值受限于 L,L 越大,gm/gds 越大。所以如果你需要高增益,除了降低电流密度,还可以增大 L。
但增大 L 会降低 fT,影响速度。所以高增益和高速是一对矛盾。GM-ID 曲线让你能定量地看到这个矛盾:L 从 0.18um 增大到 4um,gm/gds 可能从 20 增大到 100,但 fT 可能从 50GHz 降到 2GHz。你需要根据具体指标来选 L。
7.3 从曲线到网表:尺寸计算的完整示例
假设你要设计一个两级运放,第一级是差分输入级,第二级是共源放大级。指标要求:开环增益 80dB,单位增益带宽 50MHz,相位裕度 60 度,负载电容 2pF,功耗 2mW,电源 1.8V。
第一步,分配增益。两级运放的总增益是两级增益之积。假设第一级增益 40dB,第二级增益 40dB。第一级用 cascode 结构,本征增益需要 60dB(留 20dB 余量给反馈);第二级用共源结构,本征增益需要 50dB。
第二步,确定第二级的 gm/ID。第二级需要驱动 2pF 负载,单位增益带宽 50MHz,所以第二级的 gm 至少是 2pi50M*2p = 0.63mS。第二级电流假设 0.5mA,那么 gm/ID = 0.63m/0.5m = 1.26,这个值太低了,说明第二级电流不够。增大电流到 1mA,gm/ID = 0.63,还是太低。实际上,gm/ID 最低也就 5 左右,所以第二级电流至少是 0.63m/5 = 0.126mA。取 0.2mA,gm/ID = 3.15,还是偏低。取 0.5mA,gm/ID = 1.26,不对,我算错了。gm = 0.63mS,ID = 0.5mA,gm/ID = 0.63/0.5 = 1.26 V^-1,这个值确实太低了。正常 gm/ID 在 5~25 之间,所以 ID 应该是 0.63m/5 = 0.126mA 到 0.63m/25 = 0.025mA。取 ID = 0.1mA,gm/ID = 6.3,合理。
第三步,确定第二级的 L。需要本征增益 50dB,也就是 gm/gds = 316。在 gm/gds 曲线上,找到 gm/gds = 316 对应的 L 和 ID/(W/L)。假设 L = 1um 时,gm/gds 最大能到 200,不够;L = 2um 时,gm/gds 最大能到 400,够了。所以 L 取 2um。ID/(W/L) 从曲线上读,假设是 0.1uA,那么 W/L = 0.1m/0.1u = 1000,W = 2000um。这个尺寸很大,但为了高增益,没办法。
第四步,验证。gm = 6.3 * 0.1m = 0.63mS,带宽 = 0.63m/(2pi2p) = 50MHz,刚好满足。增益 = 20*log10(316) = 50dB,满足。
这个计算过程,如果手动在曲线上读点,很繁琐。但如果你把曲线数据存在 Matlab 里,写一个查找函数,几秒钟就能算出来。这就是 GM-ID 方法配合 Matlab 可视化的效率优势。
8. 一些个人体会和后续扩展方向
这套 GM-ID 流程我用了好几年,最大的感受是:它把模拟设计从"试参数"变成了"查曲线"。以前调一个运放,可能要跑几十次 DC 仿真,每次改一个 W 或者 L,看增益和带宽的变化。现在只需要跑一次 GM-ID 扫描,后面所有设计都在曲线上做,效率提升非常明显。
有几个小技巧是我踩过坑之后总结的。第一,跑 GM-ID 的时候,顺便把 Cgs、Cgd、Cdb 这些寄生电容也导出来,后续算带宽和零极点的时候直接用,不用再跑 AC 仿真。第二,L 的列表不要取太多,5~8 个就够了,太多的话曲线太密,看不清。第三,Matlab 绘图的时候,把数据点和拟合曲线分开画,数据点用散点,拟合曲线用实线,这样能看出插值的质量。
后续如果想进一步扩展,可以往几个方向走。一是把 GM-ID 流程和gm/ID设计法结合,做一个自动化的尺寸计算工具,输入指标,输出网表参数。二是把工艺角数据也纳入进来,画一个"工艺角包络",看设计在 worst case 下的裕度。三是把数据导出成 JSON 或者 HDF5 格式,方便和其他工具链集成。
这套流程不复杂,但细节很多。希望这篇总结能帮你少走一些弯路。如果你在跑 GM-ID 的时候遇到什么奇怪的问题,欢迎一起交流。