四畳半テクノポリス

コロナのストレスで気が狂い、D進した院生

PythonでVerilogもSPICEも同時に生成しLSIの設計テストを効率化した

HDL (SystemVerilog/Verilog/VHDL/Chisel/etc.) Advent Calendar 2021 12月14日

Veriloggenとは

VeriloggenPythonVerilogを組み立てる内部DSLです。スレッドの合成やステートマシンのようなリッチな機能も持っていますが、Chiselとの最大違いはVerilogとの直交性の高さだと感じています。Chiselがscalaの文法で論理回路を書くのに対して、Veriloggenでの回路設計はPythonの使って深層学習フレームワークでネットワークを組み立てる感覚に近いと感じています。 なので、Python上でコードを書いていても、常に生成されるVerilogを意識しながら書くことになり、生成されるコードはVerilogとしても読みやすいものとなります。また、過去に書いた回路をVeriloggenで書き直すことも容易です。

github.com

研究で使っている用途

Verilogは未定義ワイヤや結線ミスが発生しやすく、デバッグに長い時間を取られてしまいがちなので、私は普段のFPGAでの開発におけるほとんどのコードをVeriloggenで書いています。 本記事ではFPGAでの回路開発以外でVeriloggenが大活躍している、LSIの設計やテストでの利用について書きます。

Verilogのテストベンチ生成時のPWLの生成

当たり前ですがデジタル回路とはHightとLowに変化するという点を除けば基本的にアナログ回路と同様です。LSIの設計の教科書を見ると、AND回路やOR回路はトランジスタの組み合わせで構成されています。 さらにXOR回路などもANDやNOTやOR等の基本的なロジック回路の組み合わせではなくトランジスタで構成され、クロスラッチといったトリッキーな回路で構成されることがあります。特にSRAMなどはプレチャージなどの動作を行う必要からアナログ的な性格が強い回路です。 しかし、デジタル回路はシーケンシャルに振る舞うため、検証においてはアナログ回路とは異なるアプローチが必要となります。論理的に考えられる状態や実用時に用いるデータを元に、テストケースを生成して検証するソフトウェアテスト的な手法が必要となります。

このような検証を行うためにVeriloggenVerilogのシミュレーションを生成しつつ、SPICEのPWL(Piece-Wise Linear)区分線形のグラフのパターンも同時生成することで、Verilog上のデジタル回路のモデルとSPICE上のモデルの実行結果を比較可能になります。このテストのフローが次の図です。

f:id:toriten1024:20211214222127p:plain
検証フロー


SPICEモデルの生成

今回は例としてLTspiceのライブラリに含まれているDフリップフロップ挙動がVerilogで記述されたDフリップフロップと一致するかのテストを行います。 SPICE上のDフリップフロップは次の回路になります。Dフリップフロップのすべての入力ピンに電圧原が接続された状態です。LTspiceではシミュレーション実行時にnetファイルというシミュレーションを行うためのスクリプトが生成されます。

f:id:toriten1024:20211214150302p:plain
LTspiceの編集画面

この時のシミュレーション用のスクリプトが次のコードになります。

* 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_counterself.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で実行した結果

f:id:toriten1024:20211214145242p:plain
生成SPICEモデルの実行結果

同様にVerilogのシミュレーションも実行

iverilog -s DFF_ctrl DFF.v -o DFF.o
vvp DFF.o

実行結果をGtkWaveで表示したもの。

f:id:toriten1024:20211214230655p:plain
VerilogによるDフリップフロップのシミュレーション結果
おおよそ同じ結果になってますね。実際の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")