FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

CMakeによりVerilogのビルドを管理するための調査(1. CMakeによるビルド環境の構築)

ブログのコメントで、CMakeを使ってVerilogのプロジェクトを扱うためにはどのようにすれば良いのか、という話を頂いた。

確かに、CMakeがサポートしているのはC++とか、ソフトウェアのプロジェクトばかりだ。 Verilogをプロジェクトをサポートするためにはどのようにすれば良いのだろうか。 まずは、単純にググってみるところから始めよう。

VerilogのデザインをCMakeで管理することによるメリット

Makefile作成の自動化

まずこれは当然だが、Makeによるビルドを自動化することができる。QuestaとかVivado Simulator,Vcsなどのコンパイル型のシミュレータは、ビルドのフローはソフトウェアのそれと大して変わらない。 従って、コマンドとオプションさえ適切に与えてやれば、CMakeでプロジェクト管理することによってMakefileの生成を自動化することができる。

CTestによるテスト結果の管理

これはCMakeというよるみツールセットと連携することによるメリットだが、CTestとうまく連携させることによりVerilogのシミュレーション結果を管理することができる。 さらに、これは最近知ったのだが、CDashというツールを利用すればリグレッション結果を綺麗な図として表示させることができるようだ。これも時間があれば挑戦してみたい。

CDash

www.kitware.com

Vivado SimulatorによるビルドとリグレッションをCMakeで自動化してみる

既存技術の調査

とりあえずググってみると、以下のようなプロジェクトを見つけた。

github.com

ああ、そうか。 add_custom_commandがあるんだった!CMakeのサポートしていない言語でも、add_custom_commandがあるからそれで自由に依存関係を作れば良い!

Vivado Simulatorの環境をCMakeで作成

とりあえず、フリーのシミュレータとしてVivado Simulatorを選択した。Verilogの場合生成する物が明確なオブジェクトファイルではなくデータベースなどになり、C++と比べて依存関係の記述がちょっと難しくなる。 またツール毎に生成物が異なるので注意が必要だ。

ここでは、単純な4ビット加算器とそれをテストするためのテストベンチを用意してみた。

  • adder.v
`timescale 100ps/100ps

module adder
  (
   input wire [ 3: 0] in_A,
   input wire [ 3: 0] in_B,
   output wire [ 4: 0] out_C
   );

  assign out_C = in_A + in_B;

endmodule // adder
  • test_adder.v
`timescale 1ps/1ps

module test_adder;
  reg [ 3: 0] in_A;
  reg [ 3: 0] in_B;
  wire [ 4: 0] out_C;

  adder
    adder_0
      (/*AUTOINST*/
       // Outputs
       .out_C                           (out_C[4:0]),
       // Inputs
       .in_A                            (in_A[3:0]),
       .in_B                            (in_B[3:0]));


  initial begin
    for (in_A = 0; in_A < 10; in_A=in_A+1) begin
      for (in_B = 0; in_B < 10; in_B=in_B+1) begin
        #100;
        $display ("in_A=%01x, in_B=%01x, out_C=%02x", in_A, in_B, out_C);
      end
    end
    $finish;
  end

endmodule // test_adder

依存関係を記述する

add_custom_commandにより記述していくのだが、僕がCMakeにあまり詳しくないことまり、あまりうまく記述できていない。 本来ならばファイルリストを用意して、単純に生成コマンドを入れていくだけなのだが、、、

下記のような依存関係にしてみた。

adder_test.v → xsim.dir/work/test_adder.sdb  (xvlog.batで生成)
adder.v → xsim.dir/work/adder.sdb (xvlog.batで生成)
xsim.dir/work/test_adder.sdb, xsim.dir/work/adder.sdb → test_adder (xelab.batで生成)

これをCMakeで記述するのだが、正直これはあまり綺麗ではない。Verilogファイルが増えたらその分add_custom_commandも増やさいないといけない! 上記の先駆者のプロジェクトでは、for文を使ってうまく書いているが、これを見習うべきだな。。。

っていうか、Verilog用のサポートを自分で記述してCMakeにpull requestしてみればいいんじゃないか? なんちて。

CMakeLists.txt

cmake_minimum_required(VERSION 3.1)

project (test_adder)

add_custom_command (OUTPUT xsim.dir/work/test_adder.sdb
  COMMAND /cygdrive/c/Xilinx/Vivado/2015.4/bin/xvlog.bat test_adder.v
  DEPENDS test_adder.v
)

add_custom_command (OUTPUT xsim.dir/work/adder.sdb
  COMMAND /cygdrive/c/Xilinx/Vivado/2015.4/bin/xvlog.bat adder.v
  DEPENDS adder.v
)

add_custom_target (test_adder
  COMMAND /cygdrive/c/Xilinx/Vivado/2015.4/bin/xelab.bat -debug typical test_adder -s top_sim
  DEPENDS xsim.dir/work/adder.sdb xsim.dir/work/test_adder.sdb
)

Makefileの作成

cmakeを叩くと、ちゃんとMakefileが生成された。

$ cmake .
-- Configuring done
-- Generating done
-- Build files have been written to: /home/masayuki/work/xvlog_test

ただし、custom_targetを使っているのでmakeにオプションを付ける必要がある。

$ make help
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... test
... edit_cache
... rebuild_cache
... test_adder

$ make test_adder
[ 50%] Generating xsim.dir/work/test_adder.sdb
INFO: [VRFC 10-2263] Analyzing Verilog file "C:/usr/work/xvlog_test/test_adder.v" into library work
INFO: [VRFC 10-311] analyzing module test_adder
[100%] Generating xsim.dir/work/adder.sdb
INFO: [VRFC 10-2263] Analyzing Verilog file "C:/usr/work/xvlog_test/adder.v" into library work
INFO: [VRFC 10-311] analyzing module adder
Vivado Simulator 2015.4
Copyright 1986-1999, 2001-2015 Xilinx, Inc. All Rights Reserved.
Running: C:/Xilinx/Vivado/2015.4/bin/unwrapped/win64.o/xelab.exe -debug typical test_adder -s top_sim
Multi-threading is on. Using 6 slave threads.
Starting static elaboration
Completed static elaboration
Starting simulation data flow analysis
Completed simulation data flow analysis
Time Resolution for simulation is 1ps
Compiling module work.adder
Compiling module work.test_adder
Built simulation snapshot top_sim
[100%] Built target test_adder

ビルドは完了した!今度は、CTestによるテストパタンを追加してみる。

ソフトウェアとVerilogを連携させたいならば、DPI-Cがオススメ

Verilog-HDLの環境に、プログラムのhexファイルだとか、ソフトウェアで生成したデータであるとか、あるいはVerilogのシミュレーション自身をソフトウェアにより制御したいときには、DPI-Cがオススメだ。

www.kumikomi.net

DPI-Cを使うことにより、VerilogのタスクをC/C++から呼び出すことができたり、C/C++の関数をVerilog-HDLからタスクとして呼び出すことができるようになる。 大概のシミュレータ(VCS/Modelsim/NCVerilog)でもサポートされており、基本的なC++との通信ツールとして確立されている。 もう一つ、VPIという規格があるのだが、こちらは、VerilogからC/C++を呼び出すのみであり、C/C++からVerilogを読み出すことはできない。この分、DPI-Cの方が便利であると言える。 VPIはVerilog-HDLシミュレータとしてはVeritakがサポートされている。こちらも、便利なので時々利用している。シミュレータによって使い分ける感じかな。

Verilog Procedural Interface - Wikipedia, the free encyclopedia

この機能を使って、バイナリファイルをVerilog-HDLのブロックRAMにロードする機能を実装した。 バイナリファイルといっても、hexが記述してあるテキストファイルなのだが、これをC/C++で読み込む。この読み込み関数がVerilog-HDLのブロックRAMを書き換える機能を呼び出し、hexをロードするという訳だ。

Verilog-HDL : hexファイルを読み出すC/C++の関数を呼び出し
C/C++ : hexファイルをオープン、ロードし、整形しVerilog-HDLのタスクを呼び出し
Verilog-HDL : 関数からデータを受け取って、BlockRAMへ書き出し

とりあえず、8ビットずつしかBlockRAMへ書き込む関数が用意できなかったため、いちいちシフトしてBlockRAMへ書き込むようにしている。

task DPI_task_writeByte;
    input [31: 0] addr;
    input [ 7: 0] data;
...
        if ($unsigned(addr[31: 0]) >= 32'hbfc0_0000 && $unsigned(addr[31: 0]) <= 32'hbfc0_0fff) begin
            // Startup ROM
            addr_offset = addr - 32'hbfc0_0000;
            addr4 = (addr_offset[31: 4]);
            addr4_index = (addr_offset[ 3: 2]);
            inst = DUT.ram_START.bram_0.regs[addr4];
            DUT.ram_START.bram_0.regs[addr4] = (inst[127:0] & (~(8'hff << {addr_offset[ 3: 0], 3'b000}))) |
                                               (data[ 7: 0] << {addr_offset[ 3: 0], 3'b000});

この関数DPI_task_writeByteはC/C++から呼び出されるタスクであり、引数としてアドレス、バイトデータを渡される。 アドレス範囲によって書き込まれるBlockRAMが違うので最初にアドレス判定を行い、オフセットを抜く (addr_offset)。 次に、対象となるBlockRAMのラインをとりあえず読み出し、そのラインに対して対象のバイト列をマージする。 そして最後に、再度ラインに書き込みを行い、BlockRAMを更新する。

とまあ、上記の解説はDPI-Cとは関係無いけれども、Verilog-HDLだけでは面倒な、複雑な処理でもDPI-Cを混ぜればうまくこなすことができる。オススメ!

github.com

環境構築からシミュレーションまでを自動化する

現在のPulsar-2プロジェクトは、リポジトリをcloneしてから実行するまでがいろいろコマンドを叩かなくてはならなくて大変なので、クローンしてから自動的に実行できるまでMakefileを最適した。

QuestaSimでシミュレーションを開始するまでにやらなければならないのは、

  1. リポジトリのクローン
  2. ISSから生成されるデコーダVerilogファイルの生成
  3. QuestaSimディレクトリの構築
  4. QuestaSimディレクトリでのコンパイル
  5. DPI-Cのプロジェクトのコンパイル

となっている。これらをそれぞれ叩いていると忘れてしまうので、一気通貫できるように工夫しよう。

リポジトリのクローン

リポジトリはサブリポジトリをふんだんに利用した構成になっており、一度cloneしただけでは全てのファイルが取得されない。 なので、submodule updateを利用してサブモジュールを全て取得するのだが、

git clone https://github.com/msyksphinz/pulsar-2.git --recursive
cd pulsar-2
git checkout pulsar2_core
git submodule update --init --recursive

最後のsubmodule update --init --recursiveは、サブリポジトリの初期化と再帰的なクローンを全てやってくれる優れものだ。これを利用すれば、上記のコマンドだけでまずは全てのファイルをcloneしてくることができる。

Verilogファイルを自動生成する

シミュレーションの環境は、./sim/questa_pulsar2_top で実行するのだが、その前に、デコーダと制御回路はissがデコードテーブルから自動生成するものを利用するため、まずはRubyのコマンドを叩いてそれらを実行する必要がある。 Makefileで実行するとき、多段Makeを利用すれば簡単にディレクトリを移動できるが、今のところissのsrcディレクトリに専用のMakefileを作っていないため、ディレクトリを跨いたコマンドの実行をMakefileから実行しないといけない。 rubyのオプションを調べると、実行時のディレクトリを指定できるオプションがあった。

mips_ctrl:
        ruby -C../../iss/swimmer_riscv/src ./gen_verilog_dec.rb

実際に上記のコマンドを叩いているのは、./sim/questa_pulsar2_top ディレクトリなのだが、上記のrubyスクリプトはディレクトリを../../iss/swimmer_riscv/srcまで移動して実行したのと同じになる。

QuestaSimディレクトリの構築

Questaの環境構築をするために、vlib, vmapなどのコマンドをMakefileで記述して環境を構築する。そしてコンパイルコマンドも実行するようにしておく。

DPI-Cのプロジェクトのコンパイル

DPI-Cを利用するためのライブラリも同時に構築する。これにより、コンパイルの終了と同時にDPI-Cのライブラリをビルドするディレクトリ(../../iss/swimmer_riscv/build_dpi/)まで移動してビルドをする。

ここまでの仕事をmake一発でできるような環境を構築しておいた。

github.com

あとは、make run_coremark を叩けばサンプルとしてcoremarkを実行できる。

フェッチのリクエストとQueueの実装

仕事が忙しいので、少しずつ実装している。命令フェッチを発行して、命令を取得してくるとそれをQueueに投入する部分だ。 基本的に128bitで命令をフェッチし、まずはアドレスバスの応答が返ってくる。それはif_queueに貯められ、IDが保存される。 このIDを参照して、データバスからの応答と一致するとデータを受け取る、という訳だ。 データチャネルとアドレスチャネルが同時に到着する場合、if_queueには貯めこまれることはなく、すぐに命令キャッシュへの保存と、命令バッファへの転送が行われる。 まだアドレスチャネルとデータチャネルをずらして制御するモデルを作っていないので何とも言えないが、そのあたりもちゃんと実装する必要があるなあ。

github.com

フェッチ部の実装とパイプトレースの出力

ようやくシミュレーションが出来るようになったので、どんどん追加していく。 端子の規則が出来ていなくて、余計な信号が出来てしまっていた。

github.com

例えば、EmacsVerilogモードにて、クロックの名前がCPU_CLK, リセットの名前がCPU_RESETとなっていた場合、接続したいモジュールの信号の欄で、

/*AUTOWIRE*/

moduleA
u_moduleA
  (/*AUTOINST*/);

moduleB
u_moduleB
  (/*AUTOINST*/);

として、C-x, C-aを入力すると自動的に接続してくれる。

wire conect_A;

moduleA
u_moduleA
  (.CLK(CLK),
   .connect_A(connect_A));

moduleB
u_moduleB
  (.CLK(CLK)
   .connect_A(connect_A));

端子名が同じで、同一階層の中に入力ポートと出力ポートがあれば自動的に接続してくれる。また、AUTOWIREを持っていれば自動的に配線を定義してくれる。 両方とも出力になっていれば、自動的にoutputポート、両方とも入力になっていれば、自動的にinputポートを作ってくれる。

これ、配線ミスや接続し忘れを防ぐのにかなり便利だ。例えば、端子名を間違っており、実は接続できていないかった場合には、接続先が無いため、勝手にinput/outputポートを作られる。 そうすると、上の階層までポートが上がってしまい、最終的に接続先が無くなって未接続となるわけだ。

これで、やっとCPUのパイプトレースが出るようになった。ただし、まだフェッチが完了してからパイプラインの内部信号に不定が載ったままのため、次のデコードステージに進んでくれない。 これもどんどん見ていこう。

デバッグ環境の構築

最近仕事が忙しいので、殆ど家でプログラミングが出来ていないが、少しずつでも進めていこう。 CPUの設計をする際に、パイプラインの検証をするためのパイプラインロガーを接続していこう。

github.com

ロガーとしては、実行した命令を表示するinst_loggerと、パイプラインの進行具合を表示するpipe_loggerを持っている。 inst_loggerはISSと突き合せる検証用、pi_loggerは命令発行やリザベーションステーションの空き具合を調査するための 性能検証用だ。

これらのロガーをbindで接続し、まずはログを表示できるようにする。 まだ接続しただけなので、何も表示されないが、命令を実行するようになるといろいろ情報が分かってくるだろう。

github.com

bind test_pulsar2_top.DUT pipe_logger #(.renid_w(renid_w), .phyreg_w(phyreg_w)) pipe_logger_0 ();
bind test_pulsar2_top.DUT inst_logger #(.renid_w(renid_w), .phyreg_w(phyreg_w)) inst_logger_0 ();

DPI-Cを使うための調査(DPIのライブラリをリンクして使...おうと思ったが)

msyksphinz.hatenablog.com

QuestaSimのバージョンとか、ライブラリのバージョン、あるいは32ビットのライブラリか64ビットのライブラリかによって、使用するコンパイラも変えなければならないが、利用している環境が32ビットモードしか持っていないかったので、Cygwinの32ビット版を急遽インストールしてlibbfdをビルドし直した。 実現したいことは、QuestaSimからlibbfdにアクセスして、バイナリから情報を引き出しつつシミュレーションをすることだ。

github.com

とりあえず、ライブラリをダイナミックリンクライブラリとしてビルドするところまでは出来た。 しかし、実際にこのライブラリを組み込んでシミュレーションを流そうとすると、ライブラリの呼び出しのところで固まってしまった。

もうちょっと調査する必要がありそうだなあ。。。