shtaxxx日記

コンピュータアーキテクチャについて研究している研究者の日記や技術紹介

PythonとVeriloggenのデータフローライブラリでパイプライン回路をお手軽に設計する

Veriloggen 0.5.0をリリースしました。Python 3.5をサポートしたり、遅延評価・合成の仕組みを入れたり、かなり意欲的な更新です。

github.com

最近、GoogleTensorFlowが流行っていますね。データフローですね。そこで、今回の目玉は、パイプライン回路をお手軽に設計できる、データフローライブラリ(lib.dataflow)です。

データフローライブラリを使えば、、制御信号(ready, valid)を持つ、RTLでの設計が面倒なパイプライン回路をPythonだけで設計することができます。しかも、データフローの可視化にも対応しています。

更に、シミュレーションライブラリ(lib.simulation)を使えば、Pythonだけでそのまま回路シミュレーションもできます。

では早速試してみましょう!

準備(ダウンロード&インストール)

以前の記事を参考にインストールしてください。 Pythonのライブラリ一式に加えて、Icarus Verilogが別途インストールされている必要があります。

shtaxxx.hatenablog.com

早速データフローを書いてみる

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です。

f:id:sxhxtxa:20151117013146p:plain

長方形がデータフロー変数、楕円が演算子、三角形が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の変化に応じて、パイプライン全体が制御されているのがわかります。

f:id:sxhxtxa:20151117012915p:plain

まとめ

Veriloggenのデータフローライブラリを使うと、お手軽に高性能なデータフローパイプライン回路がPythonだけで開発できます。データフロー定義の可視化もできます。FPGAなどのハードウェアアクセレータで高い性能・電力効率を達成するには、高い稼働率を持つ演算パイプラインを構築することが非常に重要です。そのような場合にも、Veriloggenを使えば、既存のHDLよりも少ない労力で、既存の高位合成よりも高い性能のハードウェアを設計することができます。

Pythonベースの高位合成コンパイラPolyphonyを試してみた

はじめに

高位合成友の会の第3回が12/8に開催されるらしいです。 (僕はPythonでのハードウェアメタプログラミングの話をします。)

hls.connpass.com

プログラムによると、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"としましょう。

github.com

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上でハードウェアを書いてみる

サンプルのコードがexamplestestsにあります.

今回はまず,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のみに対応しています.

では,作成したPythonスクリプトを実行してみましょう.

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が起動し波形が観測できます.

f:id:sxhxtxa:20151104003228p:plain

まとめ

Veriloggenを使うと,Pythonだけでハードウェアモデリングとシミュレーションができます.

VeriloggenはHDLの構文木オブジェクトをどうするかをPythonの機能を使って定義します.一方で,同じPythonをベースとしたRTL設計ツールのMyHDLやPyMTLは,メソッド定義などのPython構文木を,自前のパーサーでHDLに変換します.Pythonの文法で直接RTLモデリングをしたいだけであれば,後者の方がより簡単です.

Veriloggenはソースコードそのものでハードウェアの振る舞いを定義するためのツールではなく,「どのようにRTLを組み立てるか」のルールを書くためのツールです.そのため,いろいろな構成のハードウェアをパラメータを変えて自動生成したり,高位合成系のバックエンドとして使う,などの,メタプログラミング的な使い方に適しています.

Veriloggenで条件・遅延付き代入を含むVerilog HDLのステートマシンを作る

引き続きVeriloggenのお話です.今回はVeriloggenのFSMライブラリの条件付き代入や遅延付き代入を紹介します.

VeriloggenはPythonVerilog HDLのソースコードを組み立てることができるライブラリです. 今回の例はGitHubここにあります.

github.com

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

シミュレーション結果

f:id:sxhxtxa:20150827002429p:plain

ちゃんと遅延付き代入や条件付き代入が動作している.

PythonとVeriloggenで既存のVerilogモジュールを読み込んで改造する

前回に引き続きVeriloggenの話.今回は, read_verilog_module(), read_verilog_module_str() を使って,Verilog HDLで書かれた既存のハードウェア構成を取り込んで,更に改変する方法についてまとめます.

VeriloggenはPythonVerilog HDLのソースコードを組み立てることができるライブラリです. 今回の例はGitHubここにあります.

github.com

既存の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はPythonVerilog HDLのソースコードを組み立てるフレームワークです.

github.com

このソースコード一式は,ここにあります.

基本的な構成は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

動作波形はこんな感じ.

f:id:sxhxtxa:20150819010943p:plain

ちゃんとソートされている.

感想

Veriloggen,あんまり書きやすくないかも・・・.精進します. PythonなのにSynthesijer.Scalaよりもソースコードが長くなったのもイマイチ.

PyMTLを使ってLEDチカチカ回路を作ってみる

ACM/IEEE MICRO-48で発表されたPyMTLを試してみたので,その使い方をまとめておきます.

PyMTLはPython上でBehavior level, Cycle level, Register transfer levelの3種類の抽象度でハードウェアをモデリングすることができるフレームワークです.

github.com

ここでは,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での変数名として利用しているようです.なるほど,かっこいい!