PythonでVerilogもSPICEも同時に生成しLSIの設計テストを効率化した
HDL (SystemVerilog/Verilog/VHDL/Chisel/etc.) Advent Calendar 2021 12月14日
Veriloggenとは
VeriloggenはPythonでVerilogを組み立てる内部DSLです。スレッドの合成やステートマシンのようなリッチな機能も持っていますが、Chiselとの最大違いはVerilogとの直交性の高さだと感じています。Chiselがscalaの文法で論理回路を書くのに対して、Veriloggenでの回路設計はPythonの使って深層学習フレームワークでネットワークを組み立てる感覚に近いと感じています。 なので、Python上でコードを書いていても、常に生成されるVerilogを意識しながら書くことになり、生成されるコードはVerilogとしても読みやすいものとなります。また、過去に書いた回路をVeriloggenで書き直すことも容易です。
研究で使っている用途
Verilogは未定義ワイヤや結線ミスが発生しやすく、デバッグに長い時間を取られてしまいがちなので、私は普段のFPGAでの開発におけるほとんどのコードをVeriloggenで書いています。 本記事ではFPGAでの回路開発以外でVeriloggenが大活躍している、LSIの設計やテストでの利用について書きます。
Verilogのテストベンチ生成時のPWLの生成
当たり前ですがデジタル回路とはHightとLowに変化するという点を除けば基本的にアナログ回路と同様です。LSIの設計の教科書を見ると、AND回路やOR回路はトランジスタの組み合わせで構成されています。 さらにXOR回路などもANDやNOTやOR等の基本的なロジック回路の組み合わせではなくトランジスタで構成され、クロスラッチといったトリッキーな回路で構成されることがあります。特にSRAMなどはプレチャージなどの動作を行う必要からアナログ的な性格が強い回路です。 しかし、デジタル回路はシーケンシャルに振る舞うため、検証においてはアナログ回路とは異なるアプローチが必要となります。論理的に考えられる状態や実用時に用いるデータを元に、テストケースを生成して検証するソフトウェアテスト的な手法が必要となります。
このような検証を行うためにVeriloggenでVerilogのシミュレーションを生成しつつ、SPICEのPWL(Piece-Wise Linear)区分線形のグラフのパターンも同時生成することで、Verilog上のデジタル回路のモデルとSPICE上のモデルの実行結果を比較可能になります。このテストのフローが次の図です。
SPICEモデルの生成
今回は例としてLTspiceのライブラリに含まれているDフリップフロップ挙動がVerilogで記述されたDフリップフロップと一致するかのテストを行います。 SPICE上のDフリップフロップは次の回路になります。Dフリップフロップのすべての入力ピンに電圧原が接続された状態です。LTspiceではシミュレーション実行時にnetファイルというシミュレーションを行うためのスクリプトが生成されます。
この時のシミュレーション用のスクリプトが次のコードになります。
* C:\path\to\project\Draft1.asc A2 N002 0 N003 N001 N004 ~Q Q 0 DFLOP V§D N002 0 PULSE(0 1v 0 0b 0n 2n 4n) V§CLK N003 0 PWL(0 1v 1n 1V 1.1n 0V 2n 0V 3.1n 1V 4n 1V 4.1n 0V 5n 0V 5.1n 1V 2.1 0V) V1 N001 0 PWL(0 0 0.1n 0 0.1001n 1v 0.5n 1v 0.5001n 0) V§CLR N004 0 PWL(0 0 10n 0V 10.001n 1V) .tran 20n .backanno .end
それぞれのVから始まっている行は電源であり、それぞれの電源のある経過時間における電圧PWLは以下のフォーマットで記述可能です。
V[電源名] [線名] 0 PWL([経過時間] [電圧] [経過時間] [電圧] [経過時間] [電圧]... )
PythonコードによるPWLの生成
いよいよVeriloggenによってVerilogのテストベンチとSPICEのコードを同時に生成するプログラムを作っていきます。ちょっと長いので区切って説明しますね。
Dフリップフロップの制御クラス
def __init__(self,name = "DFF_ctrl"): self.module = vg.Module(name) self.CLK = self.module.Reg("CLK") #input CLK self.D = self.module.Reg("D") #input Data self.CLR = self.module.Reg("CLR") #reset_signal self.PRE = self.module.Reg("PRE") #input Pre #VerilogのDフリップフロップモジュールの読み出し dff_mod = make_dff_module() dff_inst = Submodule(self.module, dff_mod, name = "DFF_inst", arg_ports=( ("CLK",self.CLK), ("D",self.D), ("CLR",self.CLR), ) ) self.step = 1 #リセット信号のダミー self.DUMMY = self.module.Reg(name + "_" +"DUMMY") self.timming = vg.simulation.setup_reset(self.module, reset=self.DUMMY,period = self.step, polarity="low") self.timeline_counter = 0 #経過時間のカウンター self.timetable = {} #時間ごとに変化するレジスタの値を記録 self.width_talbe = {} #レジスタごとのビットサイズを記録
テスト対象の回路を制御するためのテストベンチを生成するプログラムをクラスとして定義しました。 このクラスはテスト対象のモジュール接続される信号をレジスタとして保持しています。 また、SPICEを生成するために、信号の変化の時系列情報を記録するリストも所持しています。
遅延と値の更新
値の更新に関する関数群です。本来であれば継承などを使ってオーバーロードなどで(pythonにはないけど)で実装するべきだと思います。
def delay(self, delay_time): self.timeline_counter += delay_time return Delay(delay_time) def update(self, Reg): #spiceファイルのためにレジスタ毎にタイミングテープるを生成する if str(Reg.left) not in self.timetable: #レジスタがtimetableに記録されてない場合追加 self.timetable[str(Reg.left)] = [] self.width_talbe[str(Reg.left)] = Reg.left.width self.timetable[str(Reg.left)].append((self.timeline_counter, (Reg.right))) else: #レジスタの値をアップデータする。 self.timetable[str(Reg.left)].append((self.timeline_counter, self.timetable[str(Reg.left)][-1][1] )) #spiceは折れ線でデータを宣言するので0.005ずらして新しい値を追加する self.timetable[str(Reg.left)].append((self.timeline_counter+0.005, (Reg.right))) return Reg
Verilogでは#10
というように表現される遅延ですが、VeriloggenではDelay
メソッドで表現されます。今回はSPICE上での遅延も記録しないといけないので、DFlipFlop_module
クラスのdelay
メソッドでラップしてクラスメンバに記録を追加します。
update
メソッドも同様に、値の書き換えが行われる際に、最後のdelay
で更新されたtimeline_counter
が指している時刻に値の変化を記録します。
これらの関数はVeriloggenの遅延や値の更新を引数としてとり、timeline_counter
等に記録したうえでそのまま戻り値として素通りさせます、よって、次の節:シーケンス操作のようにVeriloggenのsimulationに追加する際に一度関数の中をくぐらせて使います。
シーケンス操作
#CLRにパルスを送りDFFをクリアする def clear_seq(self, init_value = 0): self.timming.add( self.update(self.CLR(0)), self.delay(self.step), self.update(self.CLR(1)), self.delay(self.step), self.update(self.CLR(0)), ) #引数valueの値にDFFの出力を変化させる def write(self,value = 0 ): self.timming.add( #negative self.update(self.CLK(0)), self.update(self.D(value)), self.delay(self.step), #positive self.update(self.CLK(1)), self.delay(self.step) ) pass
デジタル回路の操作はアドレスをセットしてクロックを立ち上げたり、SPI等のビット列のシリアル化して送信など決まったパターンを取ります。こういった操作シーケンスの生成を関数化しておくことで、アドレス走査などのテストの記述が楽になります。
Dフリップフロップの操作として
の二つのシーケンスをメソッドとして定義しています。
SPICEとしての出力
def dump_as_spice(self, vpositive="1V"): spice_file = "" for item in self.timetable: #1bitレジスタの時 if(self.width_talbe[str(item)] == None): spice_file += "V{} {} 0 PWL(".format(item, item) for change in self.timetable[item]: change_bit = 0 if(change[1] == 0)else vpositive spice_file += "{}n {} ".format(change[0], change_bit) spice_file += ")\n" else:#マルチビットなレジスタの時 for i in range(self.width_talbe[str(item)]): spice_file += "V{}\<{}\> {}\<{}\> 0 PWL(".format(item, i, item, i) for change in self.timetable[item]: change_str = '{:016b}'.format(change[1]) change_bit = change_str[::-1][i] change_bit = 0 if(change_bit == "0")else vpositive spice_file += "{}n {} ".format(change[0],change_bit ) spice_file += ")\n" spice_file += "\n" return spice_file def save_as_spice(self, filename = "sim.net"): #回路本体が格納されているnetファイル path = "./template.net" f = open(path) header = f.read() #netファイルにPWLによる信号の変動と、シミュレーションの最大時間を追加する save_data = header.format(self.dump_as_spice(),self.timeline_counter+10 ) with open(filename, mode = "w") as save_file: save_file.write(save_data)
Verilogのテストベンチでの値の変化はself.timeline_counter
、self.timetable
に記録され、この変化をSPICEのPWLに変換します。Pythonで生成しているのはテストベンチだけであり、Dフリップフロップ自体はあくまでもSPICEモデルを利用するため、SPICEファイルのテンプレートが必要となります。上で示したSPICEのnet
ファイルをベースに作ったテンプレートは以下の通りです。
* C:\path\to\project\Draft1.asc A2 D 0 CLK PRE CLR ~Q Q 0 DFLOP {} .tran {}n .backanno .end
操作
#main sequence dff_control = dff_control_module() dff_control.reset() dff_control.write(1) dff_control.write(0) dff_control.write(1) dff_control.write(0) dff_control.write(1) dff_control.clear_seq() dff_control.close()
Dフリップフロップの読み書きの操作です。1と0を交互に書き込んだ後CLRを1にしてリセットします。
SPICEとVerilogのシミュレーション
生成されたnetファイルに対して以下のコマンドを実行
&'..\path\to\LTspice\XVIIx64.exe' -b .\DFF.net
生成されたSPICEのコードをLTspiceで実行した結果
同様にVerilogのシミュレーションも実行
iverilog -s DFF_ctrl DFF.v -o DFF.o vvp DFF.o
実行結果をGtkWaveで表示したもの。 おおよそ同じ結果になってますね。実際のLSIの設計で使用するモデルでは、FETや配線の寄生コイルや寄生容量を製造プロセスに合わせてより詳細に計算するので、信号の立ち上がりや立下りが滑めるなどもう少し調整が必要になる筈です。
感想
Veriloggenを扱った記事としてはかなり邪道な内容となってしまいました。そのことはここで謝っておきます。 この記事が原因でVeriloggenに関して誤解を招くと申し訳ないのですが、基本的にこの記事は私の個人的なVeriloggenの利用について述べた記事なので、正しい情報、詳細な情報は公式のGithubなどをご覧ください。 また、お金が潤沢にあるのであればVerilogA等のミックスドシグナルなシミュレータを使うべきだと思います(近年はオープンソースなVerilog-Aのシミュレータなども登場してきているようですね)。
このプログラムは以下の二つの理由から開発されました。
①SPICEでデジタル回路のシミュレーションを行う上で、手作業でPWLを書くことに限界を感じた。
②半導体のテスタであるCloudTestingLabがVerilogのシミュレーション結果のファイルであるvcdファイルを使ってテストパターンを生成するため、入 力・出力信号共に正しい挙動をするvcdファイルが欲しかった。
これらの目標はおおよそ達成することができたと思います。少なくとも私の開発はとても楽になりました。Veriloggenを開発してくださった高前田 伸也先生に感謝を申し上げます。
また、近年MakeLSIなどホビーで半導体を製造することが流行っていますが、トランジスタを並べてデジタル回路を開発する場面があれば、お役に立つかもしれません。 長々とありがとうございました。
付録
全体のコード
import veriloggen as vg from veriloggen.core.module import Module from veriloggen.core.submodule import Submodule from veriloggen.core.vtypes import Delay, If, Int, Negedge, Posedge, Initial, Integer, Reg, Sensitive , Subst import sys def addtimescale(fname): with open(fname) as f: l = f.readlines() l.insert(0,"`timescale 1ns/1ps \n") with open(fname, mode='w') as f: f.writelines(l) #制御対象の回路のコントローラをモジュールとして宣言 def make_dff_module(): m = vg.Module("dff") CLK = m.Input("CLK") CLR = m.Input("CLR") D = m.Input("D") Q = m.OutputReg("Q") _Q = m.Output("_Q") _Q.assign(~Q) m.Always( Posedge(CLK), Posedge(CLR) )( If(CLR == 1)( Q(0) ).Else( Q(D) ) ) return m class dff_control_module(): def __init__(self,name = "DFF_ctrl"): self.module = vg.Module(name) #input CLK self.CLK = self.module.Reg("CLK") #input Data self.D = self.module.Reg("D") #reset_signal self.CLR = self.module.Reg("CLR") #input Pre self.PRE = self.module.Reg("PRE") #VerilogのDフリップフロップモジュールの読み出し dff_mod = make_dff_module() dff_inst = Submodule(self.module, dff_mod, name = "DFF_inst", arg_ports=( ("CLK",self.CLK), ("D",self.D), ("CLR",self.CLR), ) ) self.step = 1 #リセット信号のダミー self.DUMMY = self.module.Reg(name + "_" +"DUMMY") self.timming = vg.simulation.setup_reset(self.module, reset=self.DUMMY,period = self.step, polarity="low") #時系列のカウンター self.timeline_counter = 0 #時間ごとに変化するレジスタの値を記録 self.timetable = {} #レジスタごとのビットサイズを記録 self.width_talbe = {} def delay(self, delay_time): self.timeline_counter += delay_time return Delay(delay_time) def update(self, Reg): #spiceファイルのためにレジスタ毎にタイミングテープるを生成する if str(Reg.left) not in self.timetable: #レジスタがtimetableに記録されてない場合追加 self.timetable[str(Reg.left)] = [] self.width_talbe[str(Reg.left)] = Reg.left.width self.timetable[str(Reg.left)].append((self.timeline_counter, (Reg.right))) else: #レジスタの値をアップデータする。 self.timetable[str(Reg.left)].append((self.timeline_counter, self.timetable[str(Reg.left)][-1][1] )) #spiceは折れ線でデータを宣言するので0.005ずらして新しい値を追加する self.timetable[str(Reg.left)].append((self.timeline_counter+0.005, (Reg.right))) return Reg def reset(self, init_value = 0): self.timming.add( self.update(self.D(0)), self.update(self.CLK(0)), self.update(self.CLR(0)), self.update(self.PRE(0)), self.delay(self.step), ) def clear_seq(self, init_value = 0): self.timming.add( self.update(self.CLR(0)), self.delay(self.step), self.update(self.CLR(1)), self.delay(self.step), self.update(self.CLR(0)), ) def write(self,value = 0 ): self.timming.add( #negative self.update(self.CLK(0)), self.update(self.D(value)), self.delay(self.step), #positive self.update(self.CLK(1)), self.delay(self.step) ) pass def close(self): self.timming.add( vg.Systask('finish'), ) vg.simulation.setup_waveform(self.module, self.module.get_vars()) def dump_as_spice(self, vpositive="1V"): spice_file = "" for item in self.timetable: #1bitレジスタの時 if(self.width_talbe[str(item)] == None): spice_file += "V{} {} 0 PWL(".format(item, item) for change in self.timetable[item]: change_bit = 0 if(change[1] == 0)else vpositive spice_file += "{}n {} ".format(change[0], change_bit) spice_file += ")\n" else:#マルチビットなレジスタの時 for i in range(self.width_talbe[str(item)]): spice_file += "V{}\<{}\> {}\<{}\> 0 PWL(".format(item, i, item, i) for change in self.timetable[item]: change_str = '{:016b}'.format(change[1]) change_bit = change_str[::-1][i] change_bit = 0 if(change_bit == "0")else vpositive spice_file += "{}n {} ".format(change[0],change_bit ) spice_file += ")\n" spice_file += "\n" return spice_file def save_as_spice(self, filename = "sim.net"): #回路本体が格納されているnetファイル path = "./template.net" f = open(path) header = f.read() #netファイルにPWLによる信号の変動と、シミュレーションの最大時間を追加する save_data = header.format(self.dump_as_spice(),self.timeline_counter+10 ) with open(filename, mode = "w") as save_file: save_file.write(save_data) def addtimescale(fname): with open(fname) as f: l = f.read() l = "`timescale 1ns/1ps \n"+l l = l.replace("uut.vcd",sys.argv[0].split(".")[0]+".vcd") with open(fname, mode='w') as f: f.writelines(l) #main sequence dff_control = dff_control_module() dff_control.reset() dff_control.write(1) dff_control.write(0) dff_control.write(1) dff_control.write(0) dff_control.write(1) dff_control.clear_seq() dff_control.close() #save as spice print(sys.argv[0].split(".")[0]+".net") dff_control.save_as_spice(sys.argv[0].split(".")[0]+".net") #save as verilog verilog = dff_control.module.to_verilog(sys.argv[0].split(".")[0]+".v") addtimescale(sys.argv[0].split(".")[0]+".v")