MOSモデルの作り方
(original created by arugak)
(added comments by akita11)
ここでは、pyEDAを使ってBSIM3というモデルを作ってみます。
最初に、pyEDA付属のサンプルデータで処理を試してみます。
その後、実測データを読み込んで、テクノロジノードや製造プロセスに合わせて初期値や処理手順を調整してみます。(この内容はまだ書かれてない)
pyEDAとは?
pythonで実装されたBSIM3のIVモデル、それを使ったMOSモデル作成サンプルが含まれる
MOSモデル作成のしくみ
測定データによく合うモデルパラメータをコンピュータで探します。
内部的には、scipyの非線形最小二乗ソルバ―です。目的関数はpyEDAのBSIM3モデルを、ヤコビ行列はpyEDAの自動微分ルーチンを利用します。 最後に容量のパラメータを追加して、チェックして完成です。
モデル作成で一番厄介なのは、IVカーブです。 ややこしい部分をpyEDAで処理します。
準備
(※この部分はakita11が追記)
まずPythonをインストールします。2.x系と3.x系がありますが、pyEDAは2.x系が必要です。
Windowsでは、Anaconda等のディストリビューションを利用するのが便利です。Anacondaを利用する場合、追加的なパッケージは必要ありません。 Linuxでは、多くの場合、Pythonはあらかじめインストールされています。必要なパッケージは以下ですが、これも既に入っている場合が多いです。
matplotlib
numpy
scipy
準備(オプション)
一連の作業は、簡単なpythonのコードを書いていく形態なので、少し凝ったことをする際にはデバッガが利用できるIDEを準備すると便利です。Anacondaを利用する場合には、Spyderが付いています。筆者(=arugak)はPyCharmというのを使ってみました。お好みで。 モデルのチェックのために、ngspiceを実行できるようにすると良いです。よく使われるLTSpiceは、例えばvth, gm, gds, cgg等のインスタンスパラメータ(インスタンスごとに計算された変数)を出力することが出来ないからです。 チュートリアル1 --- pyEDA付属サンプルの実行
GitHubからpyEDAを取得します。オリジナルにいくつか修正を加えたものです。ブラウザからはこちら オリジナルに対する修正点は、オリジナルのコードのうち、現在のpythonで動作しない箇所をなおしたことと、明らかな間違いを修正したことだけです。そのため、ここで実行するサンプルは、オリジナルに含まれる測定データとフィッティング手順を使って、まず実行してみるという内容です。
% git clone git@github.com:arugak/pyEDA.git
サンプルのディレクトリへ移動します
% cd pyEDA/app/compact
ここにあるファイルは、以下の通りです。
code:a.sh
.
|-- BSIM3v3Param.py --- パラメータの初期値ファイル
|-- MOSp35Data.py --- データ読み込みルーチン
|-- data
| `-- level49.dat --- 測定データのサンプル
`-- fitLevel49.py --- 処理手順のスクリプト
使用する測定データは、data/level49.datです。5つのデバイスサイズに対してそれぞれ、Ids-VgsとIds-Vdsの2つのブロック(ヘッダとテーブルの対)があり、合計10ブロックになっています。(※akita11追記:ExcelやCSV形式などのデータから、この形式に変換する必要があるため、必要に応じてスクリプトなどを書いていきます)
code: data/level49.dat
$ Aurora File: n15x15.gtr.au.dat Date: Misc:
$ Atem File: n15x15.gtr
VARIABLE W = 1.5e-05
VARIABLE L = 1.5e-05
VARIABLE T = 25
VARIABLE REGION = 0
TABLE VGS VBS VDS ID
0.000000e+00 0.000000e+00 1.000000e-01 7.000000e-13
1.000000e-01 0.000000e+00 1.000000e-01 6.100000e-13
2.000000e-01 0.000000e+00 1.000000e-01 3.100000e-12
...
パラメータの初期値が書いてあるファイルは、BSIM3v3Param.pyです。param0はpythonの辞書型変数です。キーはパラメータ名、値は数値またはタプルです。タプルの意味は、(初期値, 下限, 上限)で、パラメータの探索範囲を指定しています。
code:BSIM3v3Param.py
param0={
...
'NCH': 1.7e+17 ,
...
'VTH0': (0.7, 0.1, 1.),
...
}
処理手順は、fitLevel49.pyにあります。下記のstep10関数は、パラメータVTH0, U0, UA, UBを探すものです。データソースは"IdVg_LW_lin_b0"なるものをつかいます。step10の後ろには、大きい数字のstepもあり、これは後の工程です。処理は、番号の小さいstepから順に、少しずつパラメータを決めながら進みます。
code:fitLevel49.py
...
def step10(self):
fit = MOS_IV_Fit(self.param, targets, 'Step 1')
fit.setDataSource(self.IdVg_LW_lin_b0)
result,err = fit.doFit()
...
先ほどの"IdVg_LW_lin_b0"なるデータソースは、MOSp35Data.pyで作成されています。ここでは、data/level49.datをロードして、'n15x15.gtr'というテーブルのvbs=0, vds=0.1の条件に一致するものを抽出して、IdVg_LW_lin_b0という名前のデータソースを作っています。
code: MOSp35Data.py
...
self.loadAuroraFile('data/level49.dat')
...
# Idvg, long/wide, linear region, zero Vb
dsrc = MOS_IV_FitData('Idvg, long/wide, linear region, zero Vb')
dsrc.addCurve(ds.mosID(), ds.getCurve('Vgs', {'Vbs':0., 'Vds':0.1}, 'Id'))
self.IdVg_LW_lin_b0 = dsrc
...
環境変数PYTHONPATHをセットします(こんな特殊なパッケージをインストールするのは嫌だ。ということで)
% export PYTHONPATH=../../
ファイルの出力先ディレクトリoutputを作って実行してみます。
% mkdir output
% python fitLevel49.py
しばらくたつと終了して、最後にはモデルパラメータのごときものが表示されます。これが、サンプルの抽出ルーチンが探し当てたモデルパラメータです。outputディレクトリ内には、各stepにおける実行結果のプロットがpng形式で出力されています。また、各ステップで決定されたパラメータは、output/MOSp35Proj.save.10などにバイナリで保存されています。バイナリの実体は、pickle化されたファイルです。
code:a.sh
...
final params:
A0: 0.884886 A1: -0.0520993 A2: 0.87865 ACDE: 1
AF: 1 AGS: 0.136035 ALPHA0: 0 ALPHA1: 0
AT: 33000 B0: 4.7945E-07 B1: 3.31225E-06 BETA0: 30
...
自分のオリジナルモデルを作成する際には、抽出手順(fitLevel49.py)や初期値(BSIM3v3Param.py)を所望の処理をするように修正します。
MOSの特性は、製造プロセスやテクノロジノードによって異なります。また、nmosとpmosでは、振る舞いがだいぶ違います。そのため、あらゆる場面で通用するフィッティング手順というのは、たぶん存在しません。測定データをもとに、少しずつ合わせこんでゆく他ないように(筆者は)予想しています。
チュートリアル2 --- 操作のいろいろ
全てのステップを実行すると時間がかかります。そこで、特定のステップだけ実行したい場合には、fitLevel49.pyのproj.run()を、proj.run(start,stop)のように書き換えます。startに指定したステップより以前の結果が既に存在する場合は、以前のステップで決定されたパラメータが初期値として読み込まれます。
code:fitLevel49.py
proj.run(20, 20)
ステップの特定範囲、例えば20から130を指定するときは下記のようにします。
code:fitLevel49.py
proj.run(20, 130)
注意が必要なのは、下記2つは意味が違うことです。startに0が設定されていない場合には、初期値ファイルBSIM3v3Param.pyを読み込みません。これはわかりにくいのですが、そういう仕様です。
code:fitLevel49.py
proj.run(0, 10) --- 初期値ファイルを読み込む
proj.run(10, 10) --- 初期値ファイルは読み込まない
フィッティングを行わずにプロットだけしてみたいときは、データソースに0をかけます。データソースに対する乗算記号(*)は、データソースの重みに対する乗算です。つまり0をかけると、データソースの重みが無いことになって、何もせずにプロットだけして終わります。
code: fitLevel49.py
fit.setDataSource(0 * self.IdVg_LW_lin_b0)
複数のデータソースを対象に処理したい場合は、加算記号(+)で書きます。下記は、IdVg_LW_lin_baの後ろに、重みを15倍したIdVg_LN_lin_baが追加されて、データソースにセットされます。+演算子は、足し算というより、appendのイメージです。
code:fitLevel49.py
fit.setDataSource(self.IdVg_LW_lin_ba + 15.0 * self.IdVg_LN_lin_ba)
実測データを使ったMOSモデルフィッティングの例