みねっちょのマイコン関係ブログ

組込開発系フリーソフトやハードの情報発信ブログ

WSL で 無償の Verilog な サイクルベースシミュレータ Verilator を使う

サイト内 Google 検索:


最終更新: 2021-05-02

Verilator は、Verilog HDL をサイクルベース方式で論理シミュレーションする無償の設計ツールです。近年、RISC-V アーキテクチャ「Rocket」チップの設計で使用され注目を集めました。一般的なイベント ドリブン方式と比べ非常に高速ですが、特有の制約事項もあります。

本記事では、従来の Verilog HDL 使用者が出来るだけ違和感無く移行出来る様、VerilogC++ 記述のテストベンチの具体例を WSL の Ubuntu-20.04 上で実行して解説し、結果をイベント ドリブン方式の無償な論理シミュレータ Icarus Verilog と比較します。

目次:


 


前提条件:

  • Windows 10 を使用していること
  • Microsoft Store から WSL の Ubuntu-18.04 以降をインストールしていること

Verilator とは:

Verilator は、Verilog HDL 記述をサイクルベース シミュレーションする無償の設計ツールです。一般的な論理シミュレータはイベント ドリブン方式ですが、Verilator はサイクルベース方式なため高速です。Verilator は、Verilog HDL 記述を一旦 C++ または System-C に変換した後に、OS のネイティブな実行可能形式に C++ を使ってコンパイルします。イベント ドリブン方式な商用の3大シミュレータ (の旧世代製品 *1 ) と比べても相当に速いと言われています。
公式サイトは次の通りです。
www.veripool.org


 


制約事項:

非常に速度メリットの大きい Verilator ですが、サイクルベース方式ゆえの制約事項も有ります。本記事では、従来の イベント ドリブン方式の Verilog HDL シミュレータ使用経験者が、出来るだけ労力少なく Verilator に移行できる事を目指します。

ザックリ言うと、Verilator には次の様な制約事項が有ります。

  1. 遅延記述「#」を受け付けない
  2. 最上位階層は C++ 言語 (もしくは SystemC) で記述せねばならない
  3. 論理合成可能な HDL しかシミュレーション出来ない
  4. リセットされていない回路の初期値がゼロになる
  5. VCD や FST ダンプを行うには、新しいバージョン (恐らく 4.2 以降) を使用するか、トップの C++ (もしくは SystemC) への記述が必要

このうち、1 と 2 の部分で、同期設計の回路であっても、テストベンチの修正が必要となります。しかし、後述の通りテストベンチの Verilog HDL の書き方を少し工夫すれば、通常の Verilog シミュレータ用記述の殆どがそのまま使用可能となります。


 


インストール手順:

Verilator のインストールには、Ubuntu 上で次のコマンドを実行します。尚、「perl-doc」パッケージは、Verilator コマンドライン引数で「verilator --help」を指定してヘルプを見る時に必要となります。
比較の為、Icarus Verilog (iverilog) もインストールしておきます。

sudo apt update
sudo apt install verilator
sudo apt install perl-doc
sudo apt install iverilog


 


テストベンチの例:

さっそく、テストベンチの記述例を紹介します。Verilator には、最上位階層を C++ 言語で記述せねばならないという制約が有るため、テストベンチを2分割し、下位階層の Verilog HDL 記述部と、それを補う上位階層の必要最低限の C++ 記述部とに分けます。

Verilog のままで良い下位階層のテストベンチ:

まずは、Verilator においても Verilog HDL 記述のまま実行可能な下位階層のテストベンチ部です。

この Verilog テストベンチ中には、一切の遅延記述「#」が含まれていない事に注目してください。リセットやシミュレーション停止の指示は、全てサイクル カウント数に基づいて行われています。尚、私の個人的な嗜好で GUI による波形ダンプは行っておらず、テキストでシミュレーション結果を表示します。テキストダンプはクロックの立ち上がりの直前で行うのが、同期式回路での一般的な手法です。

「cat」から「EOF」までを Ubuntu のターミナル上にコピー&ペーストすると「bench.v」というファイルが出来ます。

cat <<"EOF" > bench.v
`default_nettype none

module bench();

  reg           clk = 1, dumptrig = 0;

  parameter     RSTCOUNT = 10, ENDCOUNT = 30;

  time          count = 0;
  reg           nrst = 1;

  /* Reset and Simulation Finish */
  always @(posedge clk) begin
    casez (count)
      0        : nrst <= 0;
      RSTCOUNT : nrst <= 1;
      ENDCOUNT : $finish;
    endcase
    count <= count + 1;
  end

  /* Design Under Test */
  dut u_dut (.clk(clk), .nrst(nrst));

  /* ROM Code Load */
  initial begin
    $readmemh("main.v", u_dut.insrom);
  end

  /* Text Dump */
  initial begin
    $display("--------------------");
    $display("   C     A     D    ");
    $display("   O  n  D     A    ");
    $display("   U  R  D     T    ");
    $display("   N  S  R     A    ");
    $display("   T  T <=> <======>");
    $display("--------------------");
  end
  always @(posedge dumptrig) begin
    $write("%4d: %1h", count, nrst);
    $write(" %3h %8h", u_dut.addr_reg, u_dut.romout);
    $display;
  end

endmodule

`default_nettype wire
EOF

2021-04-23: else-if の連続を casez に変更


 


C++ への書き直しが必要な上位階層のテストベンチ:

上記の下位階層の Verilog HDL には、「clk」信号と「dumptrig」信号をドライブしている部分がありません。代わりに「reg」宣言部で初期値を設定しています。これらの信号をドライブする部分は、2分割したうちのもう一方の上位階層のテストベンチに記載してあります。

上位階層テストベンチを Verilog HDL で記載する場合には次の通りとなります。この Verilog HDL は C++ソースコードに1対1で記述し直せる様に工夫しています。

「cat」から「EOF」までを Ubuntu のターミナル上にコピー&ペーストすると「frame.v」というファイルが出来ます。

cat <<"EOF" > frame.v
`default_nettype none

`timescale 1ns/1ps
`define PERIOD 1000

module frame();

  /* Command Line Arguments */
  // parameter argv = 1'bz;

  /* Testbench */
  bench u_bench();

  /* Clock and DumpTrigger */
  always begin
    u_bench.clk = 1;
    #(`PERIOD/2-1);
    u_bench.dumptrig = 0;
    #1;
    u_bench.clk = 0;
    #(`PERIOD/2-1);
    u_bench.dumptrig = 1;
    #1;
  end

endmodule

`default_nettype wire
EOF

2021-04-10: always 文中の処理をノンブロッキングからブロッキングに訂正, 2021-04-11: timescale 修正
2021-04-25: PERIOD を parameter から `define とし 100 → 1000

always 文の中でクロック信号「clk」の1サイクル分の記述をしています。シミュレーション結果のテキスト ダンプ用トリガー信号「dumptrig」信号の立ち上がりは、「clk」信号の立ち上がり直前としています。
この always 文中の記述は、上から順に1行ずつ逐次処理しますので、「ブロッキング代入文」つまり「=」で記述せねばなりません。対立概念である並列処理は、「ノンブロッキング代入文」つまり「<=」です。通常、フリップ フロップ (FF) を記述する場合には「ノンブロッキング代入文」を、組み合わせ論理回路を記述する場合や上記の様に順次処理を記載する場合には「ブロッキング代入文」を使用します。尚、「ブロッキング処理」と「ノンブロッキング処理」というのは、Verilog HDL の文法に限らならい、情報処理プロセスの一般の用語です。

上記の HDL を Verilator 用の C++ソースコードに記述し直すと、次の通りとなります。

下位テストベンチを呼び出す部分は C++ 言語でのオブジェクトに置き換えます。また、always 文中の遅延記述「#」の部分を「eval()」関数に置き換えます。尚、「verilated.h」というヘッダファイルは「/usr/share/verilator/include/」ディレクトリの下にインストールされています。

「cat」から「EOF」までを Ubuntu のターミナル上にコピー&ペーストすると「cframe.cpp」というファイルが出来ます。

cat <<"EOF" > cframe.cpp
#pragma onece
#include <iostream>
#include "verilated.h"
#include "obj_dir/Vbench.h"

int main(int argc, char* argv[]) {

  /* Command Line Arguments */
  // Verilated::commandArgs(argc, argv);

  /* Testbench */
  Vbench* u_bench = new Vbench();

  /* Clock and DumpTrigger */
  while (!Verilated::gotFinish()) {
    u_bench->bench__DOT__clk = 1;
    u_bench->eval();
    u_bench->bench__DOT__dumptrig = 0;
    u_bench->eval();
    u_bench->bench__DOT__clk = 0;
    u_bench->eval();
    u_bench->bench__DOT__dumptrig = 1;
    u_bench->eval();
  }

  /* Closing Operation */
  u_bench->final();
  delete u_bench;
  return 0;
}
EOF

2021-04:25: u_bench->final(); と return 0; 追加. 2021-05-02: delete u_bench; 追加

比較のため左右に並べると次の通りです。 f:id:minettyo:20210420000831j:plain


 


シミュレーションの実行:

作成したテストベンチを使って、WSL 上で実際にシミュレーションを流してみます。比較の為、サイクルベース方式の Verilator とイベント ドリブン方式の Icarus Verilog の双方でシミュレーションを実行してみます。

DUT (Design Under Test) として次のソースコードを使用します。これは、パイプライン ROM の記述です。

「cat」から「EOF」までを ubuntu のターミナル上にコピー&ペーストすると「dut.v」というファイルが出来ます。

cat <<"EOF" > dut.v
`default_nettype none

module dut
  (input clk, input nrst);

  reg   [31:0]  insrom[0:2**10-1];
  reg   [31:0]  romout;
  reg   [11:2]  addr_reg;
  wire  [11:2]  addr;

  /* Pipeline ROM */
  always @(posedge clk or negedge nrst) begin
    if (!nrst) addr_reg[11:2] <= 10'b0;
    else addr_reg[11:2] <= addr[11:2];
    romout[31:0] <= insrom[addr_reg[11:2]][31:0];
  end

  /* Address Incrementor */
  assign addr[11:2] = addr_reg[11:2] + 1'b1;

endmodule

`default_nettype wire
EOF

2021-03-31: Adr Inc の2項目 bit 幅を1に修正。addr_reg の下位 2 bit に連接で 2'b0 代入に修正。
2021-04-22: addr_reg のビット幅定義から下位 2 bit を削除。

ROM コードには、次のファイル「main.v」を使用します。これは、【こちらの別記事】で作成した ARM 命令用の32ビット幅 ROM コード「main.v」です。アドレスは、32ビット ワード単位となっている事に注意してください。

「cat」から「EOF」までを ubuntu のターミナル上にコピー&ペーストすると「main.v」というファイルが出来ます。

cat <<"EOF" > main.v
@000000 e3a0d201 eb000003 e1a00000 e1a00000
@000004 e1a00000 e1a00000 e24dd008 e3a03003
@000008 e58d3000 e3a03004 e58d3004 e59d0000
@00000c e59d3004 e0800003 e28dd008 e12fff1e
@000010 00000000 00000000 00000000 00000000
EOF


 


Verilator での実行:

これらのファイルを Verilator で実行するには、次のコマンドを Ubuntuで実行します。シェルスクリプト化する前提で記載しています。

/bin/rm obj_dir/Vbench
verilator -Wall -Wno-SYNCASYNCNET --cc bench.v dut.v --exe cframe.cpp
cd obj_dir
export CXX='g++'
export CXXFLAGS='-O2'
export LINK='g++'
export LDFLAGS=
make -e -f Vbench.mk
cd ..
./obj_dir/Vbench

1行目の「rm」は、古い実行形式バイナリが残って居た場合の削除用 (トップのみ) です。

2行目が Verilator を使って Verilog HDL から C++ソースコードへ変換する部分です。変換されたソースコードは、「obj_dir」という名前のディレクトリが自動作成され、その下に置かれます。「-Wall」と「-Wno-SYNCASYNCNET」オプションは、全ての Warning をエラーとして扱うが、非同期リセットだけは例外とするものです。「--cc」オプションとそれに続く Verilog のファイル名は、指定された Verilog ソースコードC++ 言語へ変換する指示です。SystemC へ変換する場合には「--sc」と指示します。「--exe」オプションとそれに続く C++ のファイル名で、最上位階層のテストベンチを指定します。

3行目で、自動作成されたディレクトリに下ります。

4行目から7行目は、「-e」オプション付きの「make」コマンドに渡す環境変数です。最適化オプション「-O2」を指定しています。

8行目の「make」コマンドで、Verilator により自動生成された Makefile (Verilog 記述の最上位階層の module 名の先頭にVが付き、拡張子が「.mk」) を使って、自動生成された C++ソースコードを Make します。
f:id:minettyo:20210331031141j:plain:w300

10行目の「./obj_dir/Vbench」で「make」コマンドにより生成された実行可能形式バイナリを実行します。次の結果が得られる筈です。左が Verilator によるシミュレーション結果、右が後述する Icarus Verilog によるシミュレーション結果です。

シミュレーションしている回路はパイプライン ROM なので、入力したアドレスに対しデータが1クロック遅れて出力されます。 f:id:minettyo:20210331031207j:plain

Icarus Verilog での実行:

同じシミュレーションを Icarus Verilog で実行するには、Ubuntu 上で次のコマンドを実行します。

rm a.out
iverilog frame.v bench.v dut.v
./a.out


 


Verilator と Icarus Verilog のシミュレーション結果の比較:

上図の2つのシミュレーション結果を比較すると、非同期リセットを実行する前の結果が違うかと思います。Verilator ではリセットされていないレジスタの値は「0」になっていますが、Icarus Verilog では「X」(不定値、Unknown) になっています。
また、Verilog の「$finish」コマンドを受け付けた直後、Verilator は1サイクル実行してから停止しますが、Icarus Verilog では直ぐに停止しています。

これらの特性を良く理解すれば、高速な Verilog シミュレーションを行えます。


 


【論理回路設計の目次へ戻る】   【WSL 関係の目次へ戻る】


*1:Cacence 社 Incisive (NC-Verilog)、Synopsys 社 VCS、Mentor Graphics 社 ModelSim