PythonとVeriloggenのデータフローライブラリでパイプライン回路をお手軽に設計する
Veriloggen 0.5.0をリリースしました。Python 3.5をサポートしたり、遅延評価・合成の仕組みを入れたり、かなり意欲的な更新です。
最近、GoogleのTensorFlowが流行っていますね。データフローですね。そこで、今回の目玉は、パイプライン回路をお手軽に設計できる、データフローライブラリ(lib.dataflow)です。
データフローライブラリを使えば、、制御信号(ready, valid)を持つ、RTLでの設計が面倒なパイプライン回路をPythonだけで設計することができます。しかも、データフローの可視化にも対応しています。
更に、シミュレーションライブラリ(lib.simulation)を使えば、Pythonだけでそのまま回路シミュレーションもできます。
では早速試してみましょう!
準備(ダウンロード&インストール)
以前の記事を参考にインストールしてください。 Pythonのライブラリ一式に加えて、Icarus Verilogが別途インストールされている必要があります。
早速データフローを書いてみる
veriloggen/examples/dataflow_example/ にある例を参考にデータフローモデルで回路を書いてみましょう。
ヘッダー部
Veriloggenを含む各種ライブラリをインポートします。
from __future__ import absolute_import from __future__ import print_function import sys import os from veriloggen import *
回路本体 入出力ポート
今回は x*c+y (Mutiply Add)を計算するデータフローパイプラインを作ります。
ModuleオブジェクトのmがVerilogのモジュール定義を管理します。データフローで書けるんじゃないの?と思うと思いますが、lib.dataflowもあくまでVeriloggenのライブラリの一つなので、Verilogのソースコードの一部としてデータフローを高い抽象度で組み立てていきます。
x, vx, rxがそれぞれ入力xのdata・valid・readyの信号に対応します。入力y、出力zも同様です。入力cは定数を入力するためのポートなので、valid, readyがありません。
def mkMultAdd(): m = Module('multadd') clk = m.Input('CLK') rst = m.Input('RST') # data in X x = m.Input('x', 32) vx = m.Input('vx') rx = m.Output('rx') # data in Y y = m.Input('y', 32) vy = m.Input('vy') ry = m.Output('ry') # constant c = m.Input('c', 32) # data out Z z = m.Output('z', 32) vz = m.Output('vz') rz = m.Input('rz')
回路本体 データフロー本体
lib.Dataflowオブジェクトのdfがデータフローの定義を管理します。データフローを追加するVerilogモジュールとデータフロー名のプレフィックス、クロック信号・リセット信号を引数で渡します。
まず、通常のVerilogの信号からデータフロー変数を作ります。df.input()メソッドに、データフロー変数を構成する生の信号を渡します。データフロー変数pxは、入力xのデータと制御信号で定義しています。入力yのデータフロー変数pyも同様です。
# dataflow manager df = lib.Dataflow(m, 'df', clk, rst) # input -> dataflow variable px = df.input(x, valid=vx, ready=rx) py = df.input(y, valid=vy, ready=ry)
次に、実際に演算を定義しましょう。まず、x*cを定義します。入力xのデータフロー変数pxに定数cを掛け合わせます。データフロー管理オブジェクトdfをメソッドとして呼び出し、演算の定義を引数で渡します。データフローに基づく演算結果もデータフロー変数になるため、dfメソッド呼び出しは新しいデータフロー変数を返します。
次にyの加算を追加し、x*c+yを定義しましょう。pzは pxc(px*cの結果)とpy(入力yのデータフロー変数)との積で定義されています。
# dataflow definitions
pxc = df(px * c)
pz = df(pxc + py)
そして、データフロー変数pzを生のVerilog信号に接続して、データフロー変数の世界からVerilogの世界に戻ります。 pz.output()メソッドに接続先のVerilog信号を渡せば、自動的に接続されます。
最後にmake_always()で順序回路としてデータフロー回路を出力します。ついでに、データフローの定義をdraw_graph()メソッドで可視化しましょう。これを使うにはpygraphvizのインストールが必要です。
# dataflow variable -> output pz.output(z, valid=vz, ready=rz) # generate always statement df.make_always() # draw dataflow graph in png try: df.draw_graph() except: print('Dataflow graph could not be generated.', file=sys.stderr) return m
テストベンチを含めた記述全体
上記の回路本体だけではVerilogシミュレータでの検証ができないので、シミュレーション用コードを付加した、コード全体を以下に記します。
from __future__ import absolute_import from __future__ import print_function import sys import os from veriloggen import * def mkMultAdd(): m = Module('multadd') clk = m.Input('CLK') rst = m.Input('RST') # data in X x = m.Input('x', 32) vx = m.Input('vx') rx = m.Output('rx') # data in Y y = m.Input('y', 32) vy = m.Input('vy') ry = m.Output('ry') # constant c = m.Input('c', 32) # data out Z z = m.Output('z', 32) vz = m.Output('vz') rz = m.Input('rz') # dataflow manager df = lib.Dataflow(m, 'df', clk, rst) # input -> dataflow variable px = df.input(x, valid=vx, ready=rx) py = df.input(y, valid=vy, ready=ry) # dataflow definitions pxc = df(px * c) pz = df(pxc + py) # dataflow variable -> output pz.output(z, valid=vz, ready=rz) # generate always statement df.make_always() # draw dataflow graph in png try: df.draw_graph() except: print('Dataflow graph could not be generated.', file=sys.stderr) return m def mkTest(): m = Module('test') # target instance madd = mkMultAdd() # copy paras and ports params = m.copy_params(madd) ports = m.copy_sim_ports(madd) clk = ports['CLK'] rst = ports['RST'] x = ports['x'] vx = ports['vx'] rx = ports['rx'] y = ports['y'] vy = ports['vy'] ry = ports['ry'] c = ports['c'] z = ports['z'] vz = ports['vz'] rz = ports['rz'] uut = m.Instance(madd, 'uut', params=m.connect_params(madd), ports=m.connect_ports(madd)) reset_done = m.Reg('reset_done', initval=0) reset_stmt = [] reset_stmt.append( reset_done(0) ) reset_stmt.append( x(0) ) reset_stmt.append( y(0) ) reset_stmt.append( c(8) ) reset_stmt.append( vx(0) ) reset_stmt.append( vy(0) ) lib.simulation.setup_waveform(m, uut) lib.simulation.setup_clock(m, clk, hperiod=5) init = lib.simulation.setup_reset(m, rst, reset_stmt, period=100) nclk = lib.simulation.next_clock init.add( Delay(1000), reset_done(1), nclk(clk), Delay(10000), Systask('finish'), ) x_count = m.TmpReg(32, initval=0) y_count = m.TmpReg(32, initval=0) z_count = m.TmpReg(32, initval=0) xfsm = lib.FSM(m, 'xfsm', clk, rst) xfsm.add(vx(0)) xfsm.goto_next(cond=reset_done) xfsm.add(vx(1)) xfsm.add(x.inc(), cond=rx) xfsm.add(x_count.inc(), cond=rx) xfsm.goto_next(cond=AndList(x_count==10, rx)) xfsm.add(vx(0)) xfsm.make_always() yfsm = lib.FSM(m, 'yfsm', clk, rst) yfsm.add(vy(0)) yfsm.goto_next(cond=reset_done) yfsm.add(vy(1)) yfsm.add(y.add(2), cond=ry) yfsm.add(y_count.inc(), cond=ry) yfsm.goto_next(cond=AndList(y_count==10, ry)) yfsm.add(vy(0)) yfsm.make_always() zfsm = lib.FSM(m, 'zfsm', clk, rst) zfsm.add(rz(0)) zfsm.goto_next(cond=reset_done) zfsm.goto_next() zinit= zfsm.current() zfsm.add(rz(1), cond=vz) zfsm.goto_next(cond=vz) for i in range(10): zfsm.add(rz(0)) zfsm.goto_next() zfsm.goto(zinit) zfsm.make_always() m.Always(Posedge(clk))( If(reset_done)( If(AndList(vx, rx))( Systask('display', 'x=%d', x) ), If(AndList(vy, ry))( Systask('display', 'y=%d', y) ), If(AndList(vz, rz))( Systask('display', 'z=%d', z) ) ) ) return m if __name__ == '__main__': test = mkTest() verilog = test.to_verilog('tmp.v') print(verilog) sim = lib.simulation.Simulator(test) rslt = sim.run() print(rslt) #sim.view_waveform()
スクリプト実行
では、上記のコードを実行してみましょう。test.pyなど、好きな名前で上記のコードを保存して、実行します。
python test.py
すると、Verilogの長いソースコードが表示された後、シミュレーション結果が表示されたはずです。
スクリプトを実行したディレクトリに"tmp.v"というVerilogのファイルができているので、中身を確認しましょう。その中に、以下の様な回路本体の記述があるはずです。
Veriloggenのコードでは、演算の関係だけを定義したのですが、実際に生成される回路は、入力が有効かどうかを示すvalidビットや、出力側で現在の値を受理できるかどうかを示すreadyビットでパイプラインの動作が制御される必要があるため、その制御ロジックが自動で追加されています。これは手では書きたくないですね・・・。
module multadd ( input CLK, input RST, input [(32 - 1):0] x, input vx, output rx, input [(32 - 1):0] y, input vy, output ry, input [(32 - 1):0] c, output [(32 - 1):0] z, output vz, input rz ); assign rx = (_df_ready_0 || (!_df_valid_0)); assign ry = (_df_ready_1 || (!_df_valid_1)); reg [(32 - 1):0] _df_data_0; reg _df_valid_0; wire _df_ready_0; assign _df_ready_0 = (_df_ready_2 || (!_df_valid_2)); reg [(32 - 1):0] _df_data_1; reg _df_valid_1; wire _df_ready_1; assign _df_ready_1 = (_df_ready_2 || (!_df_valid_2)); reg [(32 - 1):0] _df_data_2; reg _df_valid_2; wire _df_ready_2; assign _df_ready_2 = (_df_ready_3 || (!_df_valid_3)); reg [(32 - 1):0] _df_data_3; reg _df_valid_3; wire _df_ready_3; assign _df_ready_3 = rz; assign z = _df_data_3; assign vz = _df_valid_3; always @(posedge CLK) begin if(RST) begin _df_data_0 <= 0; _df_valid_0 <= 0; _df_data_1 <= 0; _df_valid_1 <= 0; _df_data_2 <= 0; _df_valid_2 <= 0; _df_data_3 <= 0; _df_valid_3 <= 0; end else begin if(((vx && rx) && (_df_ready_0 || (!_df_valid_0)))) begin _df_data_0 <= (x * c); end if((_df_ready_0 || (!_df_valid_0))) begin _df_valid_0 <= (vx && rx); end if(((vy && ry) && (_df_ready_1 || (!_df_valid_1)))) begin _df_data_1 <= y; end if((_df_ready_1 || (!_df_valid_1))) begin _df_valid_1 <= (vy && ry); end if((((_df_valid_0 && _df_ready_0) && (_df_valid_1 && _df_ready_1)) && (_df_ready_2 || (!_df_valid_2)))) begin _df_data_2 <= (_df_data_0 + _df_data_1); end if((_df_ready_2 || (!_df_valid_2))) begin _df_valid_2 <= ((_df_valid_0 && _df_ready_0) && (_df_valid_1 && _df_ready_1)); end if(((_df_valid_2 && _df_ready_2) && (_df_ready_3 || (!_df_valid_3)))) begin _df_data_3 <= _df_data_2; end if((_df_ready_3 || (!_df_valid_3))) begin _df_valid_3 <= (_df_valid_2 && _df_ready_2); end end end endmodule
データフロー定義の可視化
定義したデータフローがどのようなものか、ソースコードだけではイメージしづらいので、可視化しましょう。 Pygraphvizが正しくインストールされていれば、すでに df.draw_graph() でデータフロー定義の可視化結果が画像として生成されているので、それを開きます。今回の例ではout.pngです。
長方形がデータフロー変数、楕円が演算子、三角形がVerilogの生信号、台形がデータフロー変数を定義するVerilogの生信号の組をそれぞれ表しています。
Verilog信号のx, vx, rxでデータフロー変数が定義されており、それとcを掛け合わせたものが_df_data_0となっています。そしてそれと、Verilog信号y, vy, ryで定義されるデータフロー変数の1ステージ分遅れた値が足し合わされ、_df_data_2、_df_data_3と伝搬していき、出力zに接続されています。
途中、データフロー変数が自動で追加されるのは、入力された世代(段数)が同じ値ごとに演算を適用するためです。入力からの段数が違うもの同士を演算する場合、段数が少ない方に調整用のデータフロー変数が自動で追加されます。Verilogでの設計の場合には、時刻・段数が違う場合には設計者が明示的にレジスタを挟む必要がありますが、Veriloggenのデータフローライブラリの場合には、自動的に調整を行ってくれます。
シミュレーション
先ほどのスクリプト実行で表示されたシミュレーション結果は以下の通りのはずです。 今回の例ではc=8なので、xの10倍の値がzから出力されていればOKです。
VCD info: dumpfile uut.vcd opened for output. x= 1 y= 2 x= 2 y= 4 x= 3 y= 6 x= 4 y= 8 z= 10 x= 5 y= 10 z= 20 x= 6 y= 12 z= 30 x= 7 y= 14 z= 40 x= 8 y= 16 z= 50 x= 9 y= 18 z= 60 x= 10 y= 20 z= 70 z= 80 z= 90 z= 100
テキストではわかりにくいので、波形で確認しましょう。GTKwaveをインストールした上で、上記スクリプトの最終行"#sim.view_waveform()"のコメントアウトを削除して、再実行すればよいGTKwaveで波形が表示されます。もしくは、以下の様に直接GTKwaveを起動しても良いです。
gtkwave --giga uut.vcd &
図の様に、x, yの入力に応じて、zの値が変化していることが確認できます。上記のシミュレーションパターンでは、出力zのready信号rzを意図的にぱたぱた変化させ、連続的にデータを受信できないようなケースを検証しています。rzの変化に応じて、パイプライン全体が制御されているのがわかります。
まとめ
Veriloggenのデータフローライブラリを使うと、お手軽に高性能なデータフローパイプライン回路がPythonだけで開発できます。データフロー定義の可視化もできます。FPGAなどのハードウェアアクセレータで高い性能・電力効率を達成するには、高い稼働率を持つ演算パイプラインを構築することが非常に重要です。そのような場合にも、Veriloggenを使えば、既存のHDLよりも少ない労力で、既存の高位合成よりも高い性能のハードウェアを設計することができます。
Pythonベースの高位合成コンパイラPolyphonyを試してみた
はじめに
高位合成友の会の第3回が12/8に開催されるらしいです。 (僕はPythonでのハードウェアメタプログラミングの話をします。)
プログラムによると、PolyphonyというPythonベースの高位合成コンパイラの発表があるらしいです。Pythonで高位合成コンパイラを作っている自分としては、これは試さずにいられない!ということで、早速試してみました。
ダウンロード
公式Webからダウンロードしましょう。将来的にはオープンソースになるようですが、現在(11月12日)はzip化された実行形式のみが利用可能です。
http://www.sinby.com/PolyPhony/we-need-your-feedback.html
実行権限が必要なので、chmodしましょう。
chmod 755 polyphony
コンパイル対象のPythonのソースコードを準備する
今回は、PyCoRAMのtestsにある、フィボナッチ数列を求める回路の記述を利用しました。以下のように、最低限のコードにしましょう。ここでは名前は"test.py"としましょう。
def fib(v): if v <= 0: return 0 if v == 1: return 1 r0 = 0 r1 = 1 for i in range(v-1): prev_r1 = r1 r1 = r0 + r1 r0 = prev_r1 return r1
コンパイル
先ほどダウンロードしたpolyphonyを実行しましょう。"-o"オプションで出力ファイル名のprefixが指定出来るようです。
./polyphony -o fib test.py
生成されたVerilog HDLコードをチェックする
"fib.v"という名前で、以下のようなコードが出力されているはずです。
ステートマシンをベースとした回路が生成されています。"READY", "ACCEPT", "VALID"で動作を制御するようですね。
module fib ( input signed [31:0] fib_IN0, input CLK, input RST, input fib_READY, input fib_ACCEPT, output reg signed [31:0] fib_OUT0, output reg fib_VALID ); //localparams localparam fib_grp_top0_INIT = 0; localparam fib_grp_top0_S0 = 1; localparam fib_grp_top0_S1 = 2; localparam fib_grp_top0_S2 = 3; localparam fib_grp_top0_S8 = 4; localparam fib_grp_top0_FINISH = 5; localparam fib_grp_ifthen1_S1 = 6; localparam fib_grp_ifthen3_S2 = 7; localparam L1_grp_top0_S0 = 8; localparam L1_grp_top0_S1 = 9; localparam L1_grp_forbody5_S2 = 10; localparam L1_grp_bridge6_S0 = 11; localparam L1_grp_bridge6_S3 = 12; //internal regs reg /*unsigned*/ [3:0] fib_state; reg signed [31:0] i; reg signed [31:0] prev_r1; reg signed [31:0] r0; reg signed [31:0] r1; reg signed [31:0] t10; //internal wires wire cond11; wire cond8; wire cond9; //functions //sub module instances //assigns assign cond8 = (fib_IN0 <= 0); assign cond9 = (fib_IN0 == 1); assign cond11 = (i < t10); always @(posedge CLK) begin if (RST) begin fib_OUT0 <= 0; fib_VALID <= 0; fib_state <= fib_grp_top0_INIT; end else begin //if (RST) case(fib_state) fib_grp_top0_INIT: begin if (fib_READY == 1) begin fib_VALID <= 0; fib_state <= fib_grp_top0_S0; end end fib_grp_top0_S0: begin if (cond8) begin fib_state <= fib_grp_ifthen1_S1; end else begin fib_state <= fib_grp_top0_S1; end end fib_grp_top0_S1: begin if (cond9) begin fib_state <= fib_grp_ifthen3_S2; end else begin fib_state <= fib_grp_top0_S2; end end fib_grp_top0_S2: begin r0 <= 0; r1 <= 1; i <= 0; fib_state <= L1_grp_top0_S0; end fib_grp_top0_S8: begin fib_OUT0 <= r1; fib_state <= fib_grp_top0_FINISH; end fib_grp_top0_FINISH: begin fib_VALID <= 1; if (fib_ACCEPT == 1) begin fib_state <= fib_grp_top0_INIT; end end fib_grp_ifthen1_S1: begin fib_OUT0 <= 0; fib_state <= fib_grp_top0_S1; end fib_grp_ifthen3_S2: begin fib_OUT0 <= 1; fib_state <= fib_grp_top0_S2; end L1_grp_top0_S0: begin t10 <= (fib_IN0 - 1); fib_state <= L1_grp_top0_S1; end L1_grp_top0_S1: begin if (cond11) begin fib_state <= L1_grp_forbody5_S2; end else begin fib_state <= fib_grp_top0_S8; end end L1_grp_forbody5_S2: begin prev_r1 <= r1; r1 <= (r0 + r1); fib_state <= L1_grp_bridge6_S0; end L1_grp_bridge6_S0: begin i <= (i + 1); fib_state <= L1_grp_bridge6_S3; end L1_grp_bridge6_S3: begin r0 <= prev_r1; fib_state <= L1_grp_top0_S0; end endcase end end endmodule
まとめ
Pythonで手軽にハードウェア設計ができるのは素敵ですね! 自分以外にもPythonで高位合成に取り組んでいる方がいるのも嬉しいです。
PyCoRAMの高位合成エンジンも単体で切り出して、普通の高位合成コンパイラとして利用できるようにしようかなぁ、と思いました。
VeriloggenとPythonでハードウェアのRTLシミュレーションをする
Veriloggenをいろいろ更新し,Version 0.4.3をリリースしました. github.com
同時にPyverilogも1.0.1をリリースしました.テスト周りを補強しリファクタリングを行った安定版です. github.com
準備: PyverilogとVeriloggenのインストール
安定版のインストールなら,pipでのインストールが簡単でおすすめです. メインの開発環境が汚れるのが心配な方はvirtualenvを使いましょう. virtualenvを使う場合には,以下の手順でインストールできます.
Python以外のソフトウェアとしてIcarus verilog (iverilog) が必要です. 例えばUbuntuなら,
sudo apt-get install iverilog
とすればイントールできるでしょう. Pythonのパッケージでは,Jinja2が別途必要ですが,Veriloggen/Pyverilogのインストールと同時に自動的にインストールされるはずです. また,テストにはpytestとpytest-pythonが必要なので,一緒にインストールしましょう.
mkdir testdir cd testdir virtualenv --python=python3 . source bin/activate pip install pyverilog pip install veriloggen pip install pytest pytest-pythonpath
1: Veriloggenを使ってPython上でハードウェアを書いてみる
今回はまず,HDLにおけるHello, world!として,LEDをチカチカ光らせる回路を作りましょう. 適当なエディタで新しいファイル(led.pyなど)を作り,以下のコードを書きましょう. 順序回路を管理するライブラリSeqを利用して,一定周期毎にLEDの値がインクリメントされる回路を作っています. シミュレーション用にdisplay文(Cでいうprintf)を挿入しています.
import sys import os from veriloggen import * def mkLed(): m = Module('blinkled') interval = m.Parameter('INTERVAL', 16) clk = m.Input('CLK') rst = m.Input('RST') led = m.OutputReg('LED', 8, initval=0) count = m.Reg('count', 32, initval=0) seq = lib.Seq(m, 'seq') seq.add( Systask('display', 'LED:%d count:%d', led, count) ) seq.add( count(count + 1), cond=count<interval-1 ) seq.add( count(0), cond=count==interval-1 ) seq.add( led(led + 1), cond=count==interval-1 ) seq.make_always(clk, rst) return m
2. シミュレーション用のコード(テストベンチ)を書いてみる
上記のコードだけではシミュレーションができないので,シミュレーション用のコードを書きましょう. 上記と同じファイルに追記しましょう.
m.copy_params()とm.copy_sim_ports()で,パラメータ定義・ポート定義をblinkledモジュールからコピーしています. これで面倒なポート生成を自動化できます.
lib.simulation.setup_waveform()で波形生成の設定,lib.simulation.setup_clock()でクロック信号の生成,lib.simulation.setup_reset()でリセット信号の設定をそれぞれ行っています.
def mkTest(): m = Module('test') # target instance led = mkLed() # copy paras and ports params = m.copy_params(led) ports = m.copy_sim_ports(led) clk = ports['CLK'] rst = ports['RST'] uut = m.Instance(led, 'uut', params=m.connect_params(led), ports=m.connect_ports(led)) lib.simulation.setup_waveform(m, uut) lib.simulation.setup_clock(m, clk, hperiod=5) init = lib.simulation.setup_reset(m, rst, m.make_reset(), period=100) init.add( Delay(1000), Systask('finish'), ) return m
3. main部を書いて実行してみる
最後に,上記で定義したメソッドを使ってハードウェアとテストベンチを生成しましょう.そして,シミュレーションをしてみましょう.
まず,上記のファイルに以下のコードを追加しましょう.以下のコードはそのスクリプトが主体となって起動される場合のみに実行されます.
if __name__ == '__main__': test = mkTest() verilog = test.to_verilog() print(verilog) sim = lib.simulation.Simulator(test) rslt = sim.run() print(rslt)
mkTest()でテストベンチを作成しています.mkTest()内では,mkLed()を呼び出しLEDハードウェアを生成しています.ハードウェアの階層構造は自動的に解析されますので,mkTest()でテストベンチハードウェアを生成するだけで,LEDハードウェアも生成されます.
次にlib.simulation.Simulator()でシミュレータの設定を作成します.シミュレーション対象のオブジェクトを引数で渡します. そして,sim.run()でシミュレータを起動して,RTLシミュレーションを実行します. 現在の実装ではIcarus Verilogのみに対応しています.
python3 led.py
すると,display文によってLEDの値とcountの値が表示されています.
LED: x count: x ... LED: x count: x LED: 0 count: 0 LED: 0 count: 1 LED: 0 count: 2 LED: 0 count: 3 LED: 0 count: 4 LED: 0 count: 5 LED: 0 count: 6 LED: 0 count: 7 LED: 0 count: 8 LED: 0 count: 9 LED: 0 count: 10 LED: 0 count: 11 LED: 0 count: 12 LED: 0 count: 13 LED: 0 count: 14 LED: 0 count: 15 LED: 1 count: 0 LED: 1 count: 1 LED: 1 count: 2 LED: 1 count: 3 ...
シミュレーションの実体は裏でiverilogを起動しているだけなのですが,別に起動する必要がなくて手間が減りました. また実行結果をstr形式で取得できるので,ハードウェア構成のテストの自動化も簡単になりました. 今後はcocotbなどと連携して,Pythonそのものでシミュレータの制御をできるようにもしたいですね.
おまけ 波形を見る
システムにGTKwaveがインストールされていれば,シミュレーション結果を波形で観測することができます. 実行スクリプトに"sim.view_waveform()"の一行を追加しましょう.
if __name__ == '__main__': test = mkTest() verilog = test.to_verilog() print(verilog) sim = lib.simulation.Simulator(test) rslt = sim.run() print(rslt) sim.view_waveform()
そしてもう一度実行してみましょう.
python3 led.py
GTKwaveが起動し波形が観測できます.
まとめ
Veriloggenを使うと,Pythonだけでハードウェアモデリングとシミュレーションができます.
VeriloggenはHDLの構文木オブジェクトをどうするかをPythonの機能を使って定義します.一方で,同じPythonをベースとしたRTL設計ツールのMyHDLやPyMTLは,メソッド定義などのPythonの構文木を,自前のパーサーでHDLに変換します.Pythonの文法で直接RTLモデリングをしたいだけであれば,後者の方がより簡単です.
Veriloggenはソースコードそのものでハードウェアの振る舞いを定義するためのツールではなく,「どのようにRTLを組み立てるか」のルールを書くためのツールです.そのため,いろいろな構成のハードウェアをパラメータを変えて自動生成したり,高位合成系のバックエンドとして使う,などの,メタプログラミング的な使い方に適しています.
Veriloggenで条件・遅延付き代入を含むVerilog HDLのステートマシンを作る
引き続きVeriloggenのお話です.今回はVeriloggenのFSMライブラリの条件付き代入や遅延付き代入を紹介します.
VeriloggenはPythonでVerilog HDLのソースコードを組み立てることができるライブラリです. 今回の例はGitHubのここにあります.
lib.fsmでステートマシンを組み立てる
m = Module('blinkled') fsm = lib.FSM(m, 'fsm')
ステートマシン (FSM) を作るにはlib.FSMを使うのが便利です.1つめの引数はModuleオブジェクト,2つめはFSM名のプレフィックスです.
valid = m.Reg('valid', initval=0) fsm.add( valid(1) ) fsm.goto_next()
FSMオブジェクトは状態を管理しており,addメソッドを呼び出すことにより,その状態での代入を追加することができます.addメソッドを複数回呼び出すことで,複数の代入を追加することもできます.goto_nextメソッドを呼び出すことで,状態番号を1インクリメントし,次の状態に遷移します.この例は以下の様なVerilogのコードに対応します.
case(fsm) // ... fsm_1: begin valid <= 1; fsm <= fsm_2; end // ... endcase
条件付き代入
各状態での代入には以下のように条件を付加することができます.この例では,ready==1が成立するときのみvalidに1が代入されます.同様に,goto_nextメソッドにも遷移条件を付加することができます.
fsm.add( valid(1), cond=(ready==1) ) fsm.goto_next(cond=(ready==1))
遅延付き代入
各状態での代入には更に遅延を付加することができます.以下の例では,delay=1を指定しているので,1サイクル後にvalidに0が代入されます.
fsm.add( valid(0), delay=1 )
keepを指定することで,代入を繰り返すことができます.以下の例では,keep=3を指定しているので,3サイクル連続でvalidに1が代入されます.
fsm.add( valid(1), keep=3) # 以下と等価 fsm.add( valid(1), delay=0) fsm.add( valid(1), delay=1) fsm.add( valid(1), delay=2)
条件付き代入と遅延付き代入・繰り返し代入は併用することができます.遅延付きの場合の代入条件は即座に評価され保存されます.そして,指定されたサイクル数の遅延の後,評価済みの条件が成立していたのであれば,代入を行います.keepが指定されている場合にも条件は予め評価され保存されます.
fsm.add( valid(0), cond=(ready==1), delay=1 )
以下のように,同一時刻で遅延付き代入と通常の代入など,複数の代入が競合する場合にはどうなるでしょうか.VeriloggenのFSMライブラリは,代入の指示が新しい方が優先されます.以下の例では,Aの代入とBの代入が競合しますが,Bが優先されます.
fsm.add( valid(1) ) fsm.add( valid(0), delay=1) # A fsm.goto_next() fsm.add( valid(1) ) # B fsm.add( valid(0), delay=1) fsm.goto_next()
これと条件付き代入を併用することで,BRAMを使ったFIFOからの読み出しなどが簡単に書けます.以下の例では,emptyが0のときにdeqに1を代入し,読み出しリクエストを発行します.そしてその次のサイクルでdeqを0にします.更に読み出した値を2サイクル後にdst_valに代入します.
fsm.add( deq(1), cond=Not(empty) ) fsm.add( deq(0), delay=1 ) fsm.add( dst_val(rdata), delay=2 ) fsm.goto_next(cond=Not(empty))
Verilog HDLでこのような処理を書く場合には,以下のように,現在の状態と条件を元に,明示的にdeq=0やdst_val=rdataなどの処理を未来の状態に追加しなければならないため,記述が煩雑になりがちです.Veriloggenの場合には,遅延を気にせず,現在の状態とそれに対応する処理を書けば良いため,記述がシンプルになります.
case(fsm) // ... fsm_1: begin if(!empty) begin deq <= 1; fsm <= fsm_2; end end fsm_2: begin deq <= 0; // other operations fsm <= fsm_3; end fsm_3: begin dst_val <= rdata; // other operations fsm <= fsm_4; end // ... endcase
Veriloggenを用いたFSMと各種代入のサンプルコード
現在のVeriloggenはInitial文および関連する文法をサポートしているので,テストベンチも書くことができます.Pythonだけでハードウェアのメタプログラミング設計もできるし,検証ができるので,ちょっと楽しい.
import sys import os from veriloggen import * def mkLed(): m = Module('blinkled') clk = m.Input('CLK') rst = m.Input('RST') ready = m.Input('ready') valid = m.OutputReg('valid', initval=0) count = m.Reg('count', width=32, initval=0) fsm = lib.FSM(m, 'fsm') for i in range(2): fsm.goto_next() # assert valid, then de-assert at the next cycle fsm.add( valid(1) ) fsm.add( valid(0), delay=1 ) for i in range(2): fsm.goto_next() # assert valid and go to the next state if a condition is satisfied now # then de-assert at the next cycle with the same condition fsm.add( valid(1), cond=(ready==1) ) fsm.add( valid(0), cond=(ready==1), delay=1 ) fsm.goto_next(cond=(ready==1)) for i in range(2): fsm.goto_next() # condition alias c = AndList((count >= 16), (ready==1)) # assert valid 1 cycle later if a condition is satisfied now # then de-assert 3 cycles later with the same condition for i in range(4): fsm.add( valid(1), cond=c, delay=1, keep=2) fsm.add( valid(0), cond=c, delay=3 ) fsm.goto_next(cond=c) # build always statement m.Always(Posedge(clk))( If(rst)( m.make_reset(), ).Else( count(count + 1), fsm.make_case() )) return m def mkTest(): m = Module('test') clk = m.Reg('CLK') rst = m.Reg('RST') ready = m.Reg('ready', initval=0) valid = m.Wire('valid') uut = m.Instance(mkLed(), 'uut', #ports=(('CLK', clk), ('RST', rst), ('ready', ready), ('valid', valid))) ports=connect_same_name(clk, rst, ready, valid)) lib.simulation.setup_waveform(m, uut) lib.simulation.setup_clock(m, clk, hperiod=5) init = lib.simulation.setup_reset(m, rst, m.make_reset(), period=100) init.add( [ lib.simulation.next_clock(clk) for i in range(8) ], ready(1), Delay(1000), Systask('finish'), ) return m if __name__ == '__main__': test = mkTest() verilog = test.to_verilog('tmp.v') print(verilog)
生成されるVerilog HDLコード
module test ( ); reg CLK; reg RST; reg ready; wire valid; blinkled uut ( .CLK(CLK), .RST(RST), .ready(ready), .valid(valid) ); initial begin $dumpfile("uut.vcd"); $dumpvars(0, uut); end initial begin CLK = 0; forever begin #5 CLK = (!CLK); end end initial begin RST = 0; ready = 0; #100; RST = 1; #100; RST = 0; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; ready = 1; #1000; $finish; end endmodule module blinkled ( input CLK, input RST, input ready, output reg valid ); reg [(32 - 1):0] count; reg [(32 - 1):0] fsm; localparam fsm_init = 0; localparam fsm_1 = 1; localparam fsm_2 = 2; reg [(32 - 1):0] _d1_fsm; localparam fsm_3 = 3; localparam fsm_4 = 4; reg _fsm_cond_4_1_0; localparam fsm_5 = 5; localparam fsm_6 = 6; localparam fsm_7 = 7; reg _fsm_cond_7_1_1; reg [(32 - 1):0] _d2_fsm; reg _fsm_cond_7_2_2; reg _fsm_cond_7_2_3; reg [(32 - 1):0] _d3_fsm; reg _fsm_cond_7_3_4; reg _fsm_cond_7_3_5; reg _fsm_cond_7_3_6; localparam fsm_8 = 8; reg _fsm_cond_8_1_7; reg _fsm_cond_8_2_8; reg _fsm_cond_8_2_9; reg _fsm_cond_8_3_10; reg _fsm_cond_8_3_11; reg _fsm_cond_8_3_12; localparam fsm_9 = 9; reg _fsm_cond_9_1_13; reg _fsm_cond_9_2_14; reg _fsm_cond_9_2_15; reg _fsm_cond_9_3_16; reg _fsm_cond_9_3_17; reg _fsm_cond_9_3_18; localparam fsm_10 = 10; reg _fsm_cond_10_1_19; reg _fsm_cond_10_2_20; reg _fsm_cond_10_2_21; reg _fsm_cond_10_3_22; reg _fsm_cond_10_3_23; reg _fsm_cond_10_3_24; localparam fsm_11 = 11; always @(posedge CLK) begin if(RST) begin valid <= 0; count <= 0; fsm <= fsm_init; _d1_fsm <= fsm_init; _fsm_cond_4_1_0 <= 0; _fsm_cond_7_1_1 <= 0; _d2_fsm <= fsm_init; _fsm_cond_7_2_2 <= 0; _fsm_cond_7_2_3 <= 0; _d3_fsm <= fsm_init; _fsm_cond_7_3_4 <= 0; _fsm_cond_7_3_5 <= 0; _fsm_cond_7_3_6 <= 0; _fsm_cond_8_1_7 <= 0; _fsm_cond_8_2_8 <= 0; _fsm_cond_8_2_9 <= 0; _fsm_cond_8_3_10 <= 0; _fsm_cond_8_3_11 <= 0; _fsm_cond_8_3_12 <= 0; _fsm_cond_9_1_13 <= 0; _fsm_cond_9_2_14 <= 0; _fsm_cond_9_2_15 <= 0; _fsm_cond_9_3_16 <= 0; _fsm_cond_9_3_17 <= 0; _fsm_cond_9_3_18 <= 0; _fsm_cond_10_1_19 <= 0; _fsm_cond_10_2_20 <= 0; _fsm_cond_10_2_21 <= 0; _fsm_cond_10_3_22 <= 0; _fsm_cond_10_3_23 <= 0; _fsm_cond_10_3_24 <= 0; end else begin count <= (count + 1); _d1_fsm <= fsm; _d2_fsm <= _d1_fsm; _d3_fsm <= _d2_fsm; case(_d3_fsm) fsm_7: begin if(_fsm_cond_7_3_6) begin valid <= 0; end end fsm_8: begin if(_fsm_cond_8_3_12) begin valid <= 0; end end fsm_9: begin if(_fsm_cond_9_3_18) begin valid <= 0; end end fsm_10: begin if(_fsm_cond_10_3_24) begin valid <= 0; end end endcase case(_d2_fsm) fsm_7: begin if(_fsm_cond_7_2_3) begin valid <= 1; end _fsm_cond_7_3_6 <= _fsm_cond_7_3_5; end fsm_8: begin if(_fsm_cond_8_2_9) begin valid <= 1; end _fsm_cond_8_3_12 <= _fsm_cond_8_3_11; end fsm_9: begin if(_fsm_cond_9_2_15) begin valid <= 1; end _fsm_cond_9_3_18 <= _fsm_cond_9_3_17; end fsm_10: begin if(_fsm_cond_10_2_21) begin valid <= 1; end _fsm_cond_10_3_24 <= _fsm_cond_10_3_23; end endcase case(_d1_fsm) fsm_2: begin valid <= 0; end fsm_4: begin if(_fsm_cond_4_1_0) begin valid <= 0; end end fsm_7: begin if(_fsm_cond_7_1_1) begin valid <= 1; end _fsm_cond_7_2_3 <= _fsm_cond_7_2_2; _fsm_cond_7_3_5 <= _fsm_cond_7_3_4; end fsm_8: begin if(_fsm_cond_8_1_7) begin valid <= 1; end _fsm_cond_8_2_9 <= _fsm_cond_8_2_8; _fsm_cond_8_3_11 <= _fsm_cond_8_3_10; end fsm_9: begin if(_fsm_cond_9_1_13) begin valid <= 1; end _fsm_cond_9_2_15 <= _fsm_cond_9_2_14; _fsm_cond_9_3_17 <= _fsm_cond_9_3_16; end fsm_10: begin if(_fsm_cond_10_1_19) begin valid <= 1; end _fsm_cond_10_2_21 <= _fsm_cond_10_2_20; _fsm_cond_10_3_23 <= _fsm_cond_10_3_22; end endcase case(fsm) fsm_init: begin fsm <= fsm_1; end fsm_1: begin fsm <= fsm_2; end fsm_2: begin valid <= 1; fsm <= fsm_3; end fsm_3: begin fsm <= fsm_4; end fsm_4: begin if((ready == 1)) begin valid <= 1; end _fsm_cond_4_1_0 <= (ready == 1); if((ready == 1)) begin fsm <= fsm_5; end end fsm_5: begin fsm <= fsm_6; end fsm_6: begin fsm <= fsm_7; end fsm_7: begin _fsm_cond_7_1_1 <= ((count >= 16) && (ready == 1)); _fsm_cond_7_2_2 <= ((count >= 16) && (ready == 1)); _fsm_cond_7_3_4 <= ((count >= 16) && (ready == 1)); if(((count >= 16) && (ready == 1))) begin fsm <= fsm_8; end end fsm_8: begin _fsm_cond_8_1_7 <= ((count >= 16) && (ready == 1)); _fsm_cond_8_2_8 <= ((count >= 16) && (ready == 1)); _fsm_cond_8_3_10 <= ((count >= 16) && (ready == 1)); if(((count >= 16) && (ready == 1))) begin fsm <= fsm_9; end end fsm_9: begin _fsm_cond_9_1_13 <= ((count >= 16) && (ready == 1)); _fsm_cond_9_2_14 <= ((count >= 16) && (ready == 1)); _fsm_cond_9_3_16 <= ((count >= 16) && (ready == 1)); if(((count >= 16) && (ready == 1))) begin fsm <= fsm_10; end end fsm_10: begin _fsm_cond_10_1_19 <= ((count >= 16) && (ready == 1)); _fsm_cond_10_2_20 <= ((count >= 16) && (ready == 1)); _fsm_cond_10_3_22 <= ((count >= 16) && (ready == 1)); if(((count >= 16) && (ready == 1))) begin fsm <= fsm_11; end end endcase end end endmodule
シミュレーション結果
ちゃんと遅延付き代入や条件付き代入が動作している.
PythonとVeriloggenで既存のVerilogモジュールを読み込んで改造する
前回に引き続きVeriloggenの話.今回は, read_verilog_module(), read_verilog_module_str() を使って,Verilog HDLで書かれた既存のハードウェア構成を取り込んで,更に改変する方法についてまとめます.
VeriloggenはPythonでVerilog HDLのソースコードを組み立てることができるライブラリです. 今回の例はGitHubのここにあります.
既存のVerilog HDLのソースコードをインポートする
from veriloggen import * led_v = '''\ module blinkled # ( parameter WIDTH = 8 ) ( input CLK, input RST, output reg [WIDTH-1:0] LED ); reg [32-1:0] count; always @(posedge CLK) begin if(RST) begin count <= 0; end else begin if(count == 1023) begin count <= 0; end else begin count <= count + 1; end end end always @(posedge CLK) begin if(RST) begin LED <= 0; end else begin if(count == 1023) begin LED <= LED + 1; end end end endmodule ''' modules = read_verilog_module_str(led_v) m = modules['blinkled']
read_verilog_module_str(code)を使うと,テキスト形式のVerilog HDLのソースコードをVeriloggenの内部形式(Module)に変換して取り込むことができる. 辞書形式でモジュール一覧が返ってくるので,モジュール名をキーにしてモジュールに参照できる.
modules = read_verilog_module('led.v') m = modules['blinkled']
既存のVerilog HDLのファイル(.v)を取り込むにはread_verilog_module(*filename)を使う.返り値はテキストの場合と同様.
インポートしたVerilogモジュールを改造する
もちろんインポートしたモジュールをそのまま利用して,インスタンスを作成したりできる.Veriloggenではさらに,インポートしたモジュールを元に,拡張や変更がPythonのモジュールをいじくり回すだけでできる.
def mkLed(): modules = read_verilog_module_str(led_v) m = modules['blinkled'] # change the module name m.name = 'modified_led' # add new statements enable = m.Input('enable') busy = m.Output('busy') old_statement = m.always[0].statement[0].false_statement m.always[0].statement[0].false_statement = If(enable)(*old_statement) m.Assign( busy(m.variable['count'] < 1023) ) return m
この例ではまず,取り込んだblinkledモジュールの名前を'modified_led'に変更している.これはテキストの置換でできるくらいに簡単.
Veriloggenならではの機能としては,Verilog HDLのソースコードに後から,ポートを追加したり,回路を追加したりできる点がある.例では,入力ポートの'enable'や出力ポートの'busy'を追加している.そしてalways文の定義を変えたり,assign文を追加したりしている.具体的には,上で示したblinkledの1つ目のalways文でcountをインクリメントしているのだが,その条件にenableを追加している.加えて,countの値を使ってbusyを定義するassign文を追加している.
インポートした時点でModuleオブジェクトはユーザーがPythonで組み立てたModuleと全く同じなため,一から組み立てるのと同じように,m.Reg()やm.Always()などで信号や回路を後から自由に追加できる.簡単♪
Veriloggenのソースコード
import sys import os import collections from veriloggen import * led_v = '''\ module blinkled # ( parameter WIDTH = 8 ) ( input CLK, input RST, output reg [WIDTH-1:0] LED ); reg [32-1:0] count; always @(posedge CLK) begin if(RST) begin count <= 0; end else begin if(count == 1023) begin count <= 0; end else begin count <= count + 1; end end end always @(posedge CLK) begin if(RST) begin LED <= 0; end else begin if(count == 1023) begin LED <= LED + 1; end end end endmodule ''' def mkLed(): modules = read_verilog_module_str(led_v) m = modules['blinkled'] # change the module name m.name = 'modified_led' # add new statements enable = m.Input('enable') busy = m.Output('busy') old_statement = m.always[0].statement[0].false_statement m.always[0].statement[0].false_statement = If(enable)(*old_statement) m.Assign( busy(m.variable['count'] < 1023) ) return m if __name__ == '__main__': led_module = mkLed() led_code = led_module.to_verilog() print(led_code)
出力されるVerilog HDLのソースコード
module modified_led # ( parameter WIDTH = 8 ) ( input CLK, input RST, output reg [(((WIDTH - 1) + 1) - 1):0] LED, input enable, output busy ); reg [(((32 - 1) + 1) - 1):0] count; always @(posedge CLK) begin if(RST) begin count <= 0; end else if(enable) begin if((count == 1023)) begin count <= 0; end else begin count <= (count + 1); end end end always @(posedge CLK) begin if(RST) begin LED <= 0; end else begin if((count == 1023)) begin LED <= (LED + 1); end end end assign busy = (count < 1023); endmodule
入力Verilogコードと比べて,if(enable) という条件やassign busy = ... という定義が追加されているのがわかる.
何が嬉しいのか?
基本となる回路をVerilog HDLを書いておいて,それを特定のルールでチューニングしたりするのが自動できるようになります.あとはある回路をプロトタイピング用の小さなRTLに自動で変換したり,自作の高位合成処理系のコード生成器の一部として使ったりもできると思います.是非お試しください.
PythonとVeriloggenでソーティングネットワークを書いてみる
@miyox氏がSynthesijer.Scalaでソーティングネットワークを自動生成していたので,Veriloggenでも試してみた.
VeriloggenはPythonでVerilog HDLのソースコードを組み立てるフレームワークです.
基本的な構成はSynthesijer.Scala版とほぼ同じだけど,Veriloggenの方がVerilog HDLの文法に近く抽象化が少ない.
比較器
_i = [0] def mk_pair(): s = m.Wire('small_' + str(_i[0]), width) l = m.Wire('large_' + str(_i[0]), width) _i[0] += 1 return s, l def prim_net(a, b): s, l = mk_pair() m.Assign(s( Cond(a < b, a, b) )) # small m.Assign(l( Cond(a < b, b, a) )) # large return s, l
mk_pair()で比較用のwire変数を作成し,prim_net()でassignで代入する.比較器を組み合わせ回路として構成する.
ネットワーク1段分
def chain_net(regs, fsm, e): x = regs[0] for i in range(e): s, l = prim_net(x, regs[i+1]) fsm.add( regs[i](s) ) x = l fsm.add( regs[e](x) ) for i in range(e + 1, len(regs)): fsm.add( regs[i](regs[i]) ) fsm.goto_next()
chain_netでパイプラインステージ1段分の回路を組み立てる.Veriloggenはlib.fsmというステートマシンライブラリがあるのでそれを使う.
fsm自体は状態遷移機械を管理するオブジェクトである.そのadd()メソッドを呼び出すことで,各状態での変数の代入を行う.
fsm.goto_next()を呼び出すと,状態変数がひとつ増加し,その状態へ遷移するコードが追加される.fsm.goto_next()を使わずに次の状態に進めたい場合には,代入と同時にfsm.set_next()を呼び出すし,その後,fsm.inc()を呼び出せばよい.
組み立て
# build up fsm = lib.FSM(m, 'fsm') idle = fsm.current() # init state fsm.add(*[registers[i](inputs[i]) for i in range(numports)], cond=kick) fsm.add(busy(1), cond=kick) fsm.goto_next(cond=kick) # connect network for i in range(numports): chain_net(registers, fsm, numports-i-1) # finalize fsm.add(busy(0)) fsm.goto(idle)
fsmは状態遷移を管理するオブジェクト.fsm.current()で現在の状態遷移のラベルが取得できる.
まず,kickがアサートされたら,register*にinput*をコピーし,同時にbusyをアサートする.fsm.add()の名前付き引数でcond=を指定すると,代入する条件が指定出来る.If(cond)( hoge ) として条件を追加することのシンタックスシュガーである.
そして次の状態へ遷る.fsm.goto_next()の名前付き引数でcond=を指定すると,次の状態に遷移する条件を追加できる.指定しない場合やNoneを指定した場合には,無条件で遷移する.
あとは,chain_net()を規定回数呼び出して,ネットワーク全体を組み立てる.最後に,busyをデアサートして,idle状態に遷移して終了. fsm.goto_next()を呼び出すと,状態変数が自動的に1増えるが,fsm.goto()の場合には増えない.
ソーティングネットワークのソースコード全体
import sys import os from veriloggen import * nclk = lib.simulation.next_clock def mkSort(numports=4): m = Module('sort') width = m.Parameter('WIDTH', 32) clk = m.Input('CLK') rst = m.Input('RST') inputs = [ m.Input('input_' + str(i), width) for i in range(numports) ] outputs = [ m.Output('output_' + str(i), width) for i in range(numports) ] kick = m.Input('kick') busy = m.OutputReg('busy') registers = [ m.Reg('registers_' + str(i), width) for i in range(numports) ] for i in range(numports): m.Assign(outputs[i](registers[i])) _i = [0] def mk_pair(): s = m.Wire('small_' + str(_i[0]), width) l = m.Wire('large_' + str(_i[0]), width) _i[0] += 1 return s, l def prim_net(a, b): s, l = mk_pair() m.Assign(s( Cond(a < b, a, b) )) # small m.Assign(l( Cond(a < b, b, a) )) # large return s, l def chain_net(regs, fsm, e): x = regs[0] for i in range(e): s, l = prim_net(x, regs[i+1]) fsm.add( regs[i](s) ) x = l fsm.add( regs[e](x) ) for i in range(e + 1, len(regs)): fsm.add( regs[i](regs[i]) ) fsm.goto_next() # build up fsm = lib.FSM(m, 'fsm') idle = fsm.current() # init state fsm.add(*[registers[i](inputs[i]) for i in range(numports)], cond=kick) fsm.add(busy(1), cond=kick) fsm.goto_next(cond=kick) # connect network for i in range(numports): chain_net(registers, fsm, numports-i-1) # finalize fsm.add(busy(0)) fsm.goto(idle) init = [ busy(0) ] + [ r(0) for r in registers ] + [ fsm.set_init() ] # import assignment into always statement m.Always(Posedge(clk))( If(rst)( init ).Else( fsm.make_case() )) return m def mkSimSort(numports=4): m = Module('simsort') width = m.Parameter('WIDTH', 32) clk = m.Reg('CLK') rst = m.Reg('RST') inputs = [ m.Reg('input_' + str(i), width) for i in range(numports) ] outputs = [ m.Wire('output_' + str(i), width) for i in range(numports) ] kick = m.Reg('kick') busy = m.Wire('busy') uut = m.Instance(mkSort(numports), 'uut', (width,), [clk, rst] + inputs + outputs + [kick, busy]) lib.simulation.setup_waveform(m, uut) lib.simulation.setup_clock(m, clk) lib.simulation.setup_reset(m, rst) m.Initial( [ ip(100 - i) for i, ip in enumerate(inputs) ], kick(0), Wait(rst), nclk(clk), Wait(Not(rst)), nclk(clk), nclk(clk), nclk(clk), kick(1), nclk(clk), kick(0), ) m.Initial( Delay(100), Wait(kick), nclk(clk), Wait(busy), nclk(clk), Wait(Not(busy)), nclk(clk), Systask('finish'), ) return m if __name__ == '__main__': sort = mkSimSort() verilog = sort.to_verilog('tmp.v') print(verilog)
Veriloggenはまだテストベンチが生成できないので,テストベンチはVerilog HDLで書いた.面倒だった.
現在,テストベンチもVeriloggenで書けます.上記のmkSimSortを参照.
module test; initial begin $dumpfile("uut.vcd"); $dumpvars(0, uut); end parameter WIDTH = 32; reg CLK; reg RST; reg [(WIDTH - 1):0] input_0; reg [(WIDTH - 1):0] input_1; reg [(WIDTH - 1):0] input_2; reg [(WIDTH - 1):0] input_3; wire [(WIDTH - 1):0] output_0; wire [(WIDTH - 1):0] output_1; wire [(WIDTH - 1):0] output_2; wire [(WIDTH - 1):0] output_3; reg kick; wire busy; sort uut( CLK, RST, input_0, input_1, input_2, input_3, output_0, output_1, output_2, output_3, kick, busy ); initial begin CLK = 0; forever #5 CLK = ~CLK; end initial begin RST = 0; kick = 0; input_0 = 4; input_1 = 3; input_2 = 2; input_3 = 1; #100; RST = 1; #100; RST = 0; @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; kick = 1; @(posedge CLK); #1; kick = 0; #500; $finish; end endmodule
実行結果とシミュレーション結果
生成されるVerilog HDLのコードはこんな感じ.なお現在のPyverilog.ast_code_generatorはちゃんと整形されたコードを生成します.
module simsort # ( parameter WIDTH = 32 ) ( ); reg CLK; reg RST; reg [(WIDTH - 1):0] input_0; reg [(WIDTH - 1):0] input_1; reg [(WIDTH - 1):0] input_2; reg [(WIDTH - 1):0] input_3; wire [(WIDTH - 1):0] output_0; wire [(WIDTH - 1):0] output_1; wire [(WIDTH - 1):0] output_2; wire [(WIDTH - 1):0] output_3; reg kick; wire busy; sort #( WIDTH ) uut ( CLK, RST, input_0, input_1, input_2, input_3, output_0, output_1, output_2, output_3, kick, busy ); initial begin $dumpfile("uut.vcd"); $dumpvars(0, uut); end initial begin CLK = 0; forever begin #5 CLK = (!CLK); end end initial begin RST = 0; #100; RST = 1; #100; RST = 0; end initial begin input_0 = 100; input_1 = 99; input_2 = 98; input_3 = 97; kick = 0; wait(RST); @(posedge CLK); #1; wait((!RST)); @(posedge CLK); #1; @(posedge CLK); #1; @(posedge CLK); #1; kick = 1; @(posedge CLK); #1; kick = 0; end initial begin #100; wait(kick); @(posedge CLK); #1; wait(busy); @(posedge CLK); #1; wait((!busy)); @(posedge CLK); #1; $finish; end endmodule module sort # ( parameter WIDTH = 32 ) ( input CLK, input RST, input [(WIDTH - 1):0] input_0, input [(WIDTH - 1):0] input_1, input [(WIDTH - 1):0] input_2, input [(WIDTH - 1):0] input_3, output [(WIDTH - 1):0] output_0, output [(WIDTH - 1):0] output_1, output [(WIDTH - 1):0] output_2, output [(WIDTH - 1):0] output_3, input kick, output reg busy ); reg [(WIDTH - 1):0] registers_0; reg [(WIDTH - 1):0] registers_1; reg [(WIDTH - 1):0] registers_2; reg [(WIDTH - 1):0] registers_3; assign output_0 = registers_0; assign output_1 = registers_1; assign output_2 = registers_2; assign output_3 = registers_3; reg [(32 - 1):0] fsm; localparam fsm_init = 0; localparam fsm_1 = 1; wire [(WIDTH - 1):0] small_0; wire [(WIDTH - 1):0] large_0; assign small_0 = (((registers_0 < registers_1))? registers_0 : registers_1); assign large_0 = (((registers_0 < registers_1))? registers_1 : registers_0); wire [(WIDTH - 1):0] small_1; wire [(WIDTH - 1):0] large_1; assign small_1 = (((large_0 < registers_2))? large_0 : registers_2); assign large_1 = (((large_0 < registers_2))? registers_2 : large_0); wire [(WIDTH - 1):0] small_2; wire [(WIDTH - 1):0] large_2; assign small_2 = (((large_1 < registers_3))? large_1 : registers_3); assign large_2 = (((large_1 < registers_3))? registers_3 : large_1); localparam fsm_2 = 2; wire [(WIDTH - 1):0] small_3; wire [(WIDTH - 1):0] large_3; assign small_3 = (((registers_0 < registers_1))? registers_0 : registers_1); assign large_3 = (((registers_0 < registers_1))? registers_1 : registers_0); wire [(WIDTH - 1):0] small_4; wire [(WIDTH - 1):0] large_4; assign small_4 = (((large_3 < registers_2))? large_3 : registers_2); assign large_4 = (((large_3 < registers_2))? registers_2 : large_3); localparam fsm_3 = 3; wire [(WIDTH - 1):0] small_5; wire [(WIDTH - 1):0] large_5; assign small_5 = (((registers_0 < registers_1))? registers_0 : registers_1); assign large_5 = (((registers_0 < registers_1))? registers_1 : registers_0); localparam fsm_4 = 4; localparam fsm_5 = 5; always @(posedge CLK) begin if(RST) begin busy <= 0; registers_0 <= 0; registers_1 <= 0; registers_2 <= 0; registers_3 <= 0; fsm <= fsm_init; end else begin case(fsm) fsm_init: begin if(kick) begin registers_0 <= input_0; registers_1 <= input_1; registers_2 <= input_2; registers_3 <= input_3; end if(kick) begin busy <= 1; end if(kick) begin fsm <= fsm_1; end end fsm_1: begin registers_0 <= small_0; registers_1 <= small_1; registers_2 <= small_2; registers_3 <= large_2; fsm <= fsm_2; end fsm_2: begin registers_0 <= small_3; registers_1 <= small_4; registers_2 <= large_4; registers_3 <= registers_3; fsm <= fsm_3; end fsm_3: begin registers_0 <= small_5; registers_1 <= large_5; registers_2 <= registers_2; registers_3 <= registers_3; fsm <= fsm_4; end fsm_4: begin registers_0 <= registers_0; registers_1 <= registers_1; registers_2 <= registers_2; registers_3 <= registers_3; fsm <= fsm_5; end fsm_5: begin busy <= 0; fsm <= fsm_init; end endcase end end endmodule
動作波形はこんな感じ.
ちゃんとソートされている.
感想
Veriloggen,あんまり書きやすくないかも・・・.精進します. PythonなのにSynthesijer.Scalaよりもソースコードが長くなったのもイマイチ.
PyMTLを使ってLEDチカチカ回路を作ってみる
ACM/IEEE MICRO-48で発表されたPyMTLを試してみたので,その使い方をまとめておきます.
PyMTLはPython上でBehavior level, Cycle level, Register transfer levelの3種類の抽象度でハードウェアをモデリングすることができるフレームワークです.
ここでは,Verilog HDLなどと同じRTLで回路を記述し,Verilog HDLのソースコードを生成してみます.
GitHubにあるPyMTLプロジェクトのREADMEとISCA2015でのチュートリアルのスライドを参考にして進めます.
PyMTLとVerilatorのインストール
あらかじめpython2.7とvirtualenvをインストールしておく.
pip install virtualenv
作業用ディレクトリを作成する.
mkdir pymtl
PATHの設定等を記述したスクリプトを用意する.setup.shという名前で作成する.
VERILATOR=~/pymtl/verilator/ source ./bin/activate export PATH=$VERILATOR/bin/:$PATH export PYMTL_VERILATOR_INCLUDE_DIR=$VERILATOR/include export VERILATOR_ROOT=$VERILATOR
反映させる.
source setup.sh
PyMTLとVerilatorをインストールする.
cd pymtl virtualenv --python=python2.7 . git clone https://github.com/cornell-brg/pymtl.git pip install --editable ./pymtl git clone http://git.veripool.org/git/verilator cd verilator/ autoconf export VERILATOR_ROOT=`pwd` ./configure make cd ..
PyMTLのテスト
cd pymtl mkdir build cd build py.test .. --verbose --tb=line py.test .. --verbose --test-verilog cd ../
LEDチカチカハードウェアをPyMTLで書いてみる
サンプルディレクトリを作成し,定義本体用ファイルとテスト用ファイルを用意する.
mkdir -p my_examples/led cd my_examples/led touch ledRTL.py touch ledRTL_test.py
定義本体のledRTL.pyはこんな感じ.
from pymtl import * class LedRTL( Model ): def __init__( s ): s.led = OutPort( 8 ) s.count = Wire( 32 ) s.led_count = Wire( 8 ) @s.tick_rtl def seq(): if s.count == 1023: s.count.next = 0 s.led_count.next = s.led_count + 1 else: s.count.next = s.count + 1 @s.combinational def comb(): s.led.value = s.led_count
テストのledRTL_test.pyはこんな感じ.
from pymtl import * from ledRTL import LedRTL def test_simple( test_verilog ): model = LedRTL() if test_verilog: model = TranslationTool( model ) model.elaborate()
実行してみる.
py.test ledRTL_test.py --test-verilog
そうすると,こんな感じのVerilog HDLのソースコードが同じディレクトリに生成されている.
//----------------------------------------------------------------------------- // LedRTL_0x791afe0d4d8c //----------------------------------------------------------------------------- // dump-vcd: False `default_nettype none module LedRTL_0x791afe0d4d8c ( input wire [ 0:0] clk, output reg [ 7:0] led, input wire [ 0:0] reset ); // register declarations reg [ 31:0] count; reg [ 7:0] led_count; // PYMTL SOURCE: // // @s.tick_rtl // def seq(): // if s.count == 1023: // s.count.next = 0 // s.led_count.next = s.led_count + 1 // else: // s.count.next = s.count + 1 // logic for seq() always @ (posedge clk) begin if ((count == 1023)) begin count <= 0; led_count <= (led_count+1); end else begin count <= (count+1); end end // PYMTL SOURCE: // // @s.combinational // def comb(): // s.led.value = s.led_count // logic for comb() always @ (*) begin led = led_count; end endmodule // LedRTL_0x791afe0d4d8c
想像した通りの回路がVerilog HDLで記述されている.
PyMTLの実装について
ハードウェアの各変数は,Modelクラスを継承したクラスのオブジェクトとして保持されています. Verilog HDLソースコードを生成する際には,Model継承クラスが持つ変数とその変数名の辞書をobj.__dict__を使って取得し,その中からハードウェア変数のPythonソースコード中の変数名を取得して,Verilog HDLでの変数名として利用しているようです.なるほど,かっこいい!