最終更新: 2021-05-02
Verilator は、Verilog HDL をサイクルベース方式で論理シミュレーションする無償の設計ツールです。近年、RISC-V アーキテクチャ「Rocket」チップの設計で使用され注目を集めました。一般的なイベント ドリブン方式と比べ非常に高速ですが、特有の制約事項もあります。
本記事では、従来の Verilog HDL 使用者が出来るだけ違和感無く移行出来る様、Verilog と C++ 記述のテストベンチの具体例を WSL の Ubuntu-20.04 上で実行して解説し、結果をイベント ドリブン方式の無償な論理シミュレータ Icarus Verilog と比較します。
目次: Verilator は、Verilog HDL 記述をサイクルベース シミュレーションする無償の設計ツールです。一般的な論理シミュレータはイベント ドリブン方式ですが、Verilator はサイクルベース方式なため高速です。Verilator は、Verilog HDL 記述を一旦 C++ または System-C に変換した後に、OS のネイティブな実行可能形式に C++ を使ってコンパイルします。イベント ドリブン方式な商用の3大シミュレータ (の旧世代製品 *1 ) と比べても相当に速いと言われています。 非常に速度メリットの大きい Verilator ですが、サイクルベース方式ゆえの制約事項も有ります。本記事では、従来の イベント ドリブン方式の Verilog HDL シミュレータ使用経験者が、出来るだけ労力少なく Verilator に移行できる事を目指します。 ザックリ言うと、Verilator には次の様な制約事項が有ります。 このうち、1 と 2 の部分で、同期設計の回路であっても、テストベンチの修正が必要となります。しかし、後述の通りテストベンチの Verilog HDL の書き方を少し工夫すれば、通常の Verilog シミュレータ用記述の殆どがそのまま使用可能となります。 Verilator のインストールには、Ubuntu 上で次のコマンドを実行します。尚、「perl-doc」パッケージは、Verilator コマンドライン引数で「verilator --help」を指定してヘルプを見る時に必要となります。 さっそく、テストベンチの記述例を紹介します。Verilator には、最上位階層を C++ 言語で記述せねばならないという制約が有るため、テストベンチを2分割し、下位階層の Verilog HDL 記述部と、それを補う上位階層の必要最低限の C++ 記述部とに分けます。 まずは、Verilator においても Verilog HDL 記述のまま実行可能な下位階層のテストベンチ部です。 この Verilog テストベンチ中には、一切の遅延記述「#」が含まれていない事に注目してください。リセットやシミュレーション停止の指示は、全てサイクル カウント数に基づいて行われています。尚、私の個人的な嗜好で GUI による波形ダンプは行っておらず、テキストでシミュレーション結果を表示します。テキストダンプはクロックの立ち上がりの直前で行うのが、同期式回路での一般的な手法です。 「cat」から「EOF」までを Ubuntu のターミナル上にコピー&ペーストすると「bench.v」というファイルが出来ます。 2021-04-23: else-if の連続を casez に変更 上記の下位階層の Verilog HDL には、「clk」信号と「dumptrig」信号をドライブしている部分がありません。代わりに「reg」宣言部で初期値を設定しています。これらの信号をドライブする部分は、2分割したうちのもう一方の上位階層のテストベンチに記載してあります。 上位階層テストベンチを Verilog HDL で記載する場合には次の通りとなります。この Verilog HDL は C++ のソースコードに1対1で記述し直せる様に工夫しています。 「cat」から「EOF」までを Ubuntu のターミナル上にコピー&ペーストすると「frame.v」というファイルが出来ます。
2021-04-10: always 文中の処理をノンブロッキングからブロッキングに訂正, 2021-04-11: timescale 修正 always 文の中でクロック信号「clk」の1サイクル分の記述をしています。シミュレーション結果のテキスト ダンプ用トリガー信号「dumptrig」信号の立ち上がりは、「clk」信号の立ち上がり直前としています。 上記の HDL を Verilator 用の C++ のソースコードに記述し直すと、次の通りとなります。 下位テストベンチを呼び出す部分は C++ 言語でのオブジェクトに置き換えます。また、always 文中の遅延記述「#」の部分を「eval()」関数に置き換えます。尚、「verilated.h」というヘッダファイルは「/usr/share/verilator/include/」ディレクトリの下にインストールされています。 「cat」から「EOF」までを Ubuntu のターミナル上にコピー&ペーストすると「cframe.cpp」というファイルが出来ます。 2021-04:25: u_bench->final(); と return 0; 追加. 2021-05-02: delete u_bench; 追加 比較のため左右に並べると次の通りです。
作成したテストベンチを使って、WSL 上で実際にシミュレーションを流してみます。比較の為、サイクルベース方式の Verilator とイベント ドリブン方式の Icarus Verilog の双方でシミュレーションを実行してみます。 DUT (Design Under Test) として次のソースコードを使用します。これは、パイプライン ROM の記述です。 「cat」から「EOF」までを ubuntu のターミナル上にコピー&ペーストすると「dut.v」というファイルが出来ます。
2021-03-31: Adr Inc の2項目 bit 幅を1に修正。addr_reg の下位 2 bit に連接で 2'b0 代入に修正。 ROM コードには、次のファイル「main.v」を使用します。これは、【こちらの別記事】で作成した ARM 命令用の32ビット幅 ROM コード「main.v」です。アドレスは、32ビット ワード単位となっている事に注意してください。 「cat」から「EOF」までを ubuntu のターミナル上にコピー&ペーストすると「main.v」というファイルが出来ます。 これらのファイルを Verilator で実行するには、次のコマンドを Ubuntuで実行します。シェルスクリプト化する前提で記載しています。 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 します。 10行目の「./obj_dir/Vbench」で「make」コマンドにより生成された実行可能形式バイナリを実行します。次の結果が得られる筈です。左が Verilator によるシミュレーション結果、右が後述する Icarus Verilog によるシミュレーション結果です。 シミュレーションしている回路はパイプライン ROM なので、入力したアドレスに対しデータが1クロック遅れて出力されます。
同じシミュレーションを Icarus Verilog で実行するには、Ubuntu 上で次のコマンドを実行します。 上図の2つのシミュレーション結果を比較すると、非同期リセットを実行する前の結果が違うかと思います。Verilator ではリセットされていないレジスタの値は「0」になっていますが、Icarus Verilog では「X」(不定値、Unknown) になっています。 これらの特性を良く理解すれば、高速な Verilog シミュレーションを行えます。
前提条件:
Verilator とは:
公式サイトは次の通りです。
www.veripool.org
制約事項:
インストール手順:
比較の為、Icarus Verilog (iverilog) もインストールしておきます。sudo apt update
sudo apt install verilator
sudo apt install perl-doc
sudo apt install iverilog
テストベンチの例:
Verilog のままで良い下位階層のテストベンチ:
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
C++ への書き直しが必要な上位階層のテストベンチ:
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-25: PERIOD を parameter から `define とし 100 → 1000
この always 文中の記述は、上から順に1行ずつ逐次処理しますので、「ブロッキング代入文」つまり「=」で記述せねばなりません。対立概念である並列処理は、「ノンブロッキング代入文」つまり「<=」です。通常、フリップ フロップ (FF) を記述する場合には「ノンブロッキング代入文」を、組み合わせ論理回路を記述する場合や上記の様に順次処理を記載する場合には「ブロッキング代入文」を使用します。尚、「ブロッキング処理」と「ノンブロッキング処理」というのは、Verilog HDL の文法に限らならい、情報処理プロセスの一般の用語です。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
シミュレーションの実行:
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-04-22: addr_reg のビット幅定義から下位 2 bit を削除。
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 での実行:
/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
Icarus Verilog での実行:
rm a.out
iverilog frame.v bench.v dut.v
./a.out
Verilator と Icarus Verilog のシミュレーション結果の比較:
また、Verilog の「$finish」コマンドを受け付けた直後、Verilator は1サイクル実行してから停止しますが、Icarus Verilog では直ぐに停止しています。