Verilatorを使ってみた
Verilatorの使用感を知りたかったので、初めて使ってみた。そのときの調査や作業の記録を残しておく。
はじめに
Verilatorとは何か
Verilatorは、VerilogまたはSystemVerilogで記述した論理回路のためのシミュレータである。より厳密には、VerilogやSystemVerilogの記述を直接シミュレートするのではなく、記述をC++コードに変換してからC++コードとしてコンパイルし、シミュレータを生成する。
主な特徴は下記のとおり。
- 無償
- 他のシミュレータよりも高速 (とされている)
- シミュレーションのためにはC++コードを書く必要がある。VerilogやSystemVerilogだけではテストベンチ(設計物を検証するための環境)を構成できない
- VerilogやSystemVerilogの一部の機能はサポートされない。論理合成対象の記述はほぼサポートされているが、合成不能な検証用の記述にはサポート外のものがある
類似する他のツールと比べてどうか
有償のVerilogシミュレータで広く使われているのは、CadenceのXcelium・SynopsysのVCS・SiemensのModelSim/Questaのいわゆるbig 3である。また、無償のものに Icalus Verilog がある。
VerilatorリポジトリのREADMEの What Verilator Does と Performance に、他のツールとの比較についておおよそ下記のように書かれている。
- VerilogやSystemVerilogの様々な機能を使いたい場合には、Verilatorは向いていない。他のシミュレータを使ったほうがよい
- Verilatorは、高速なシミュレーションに向いている
- 有償の高速なシミュレータ(前述のbig 3など)と同等またはそれ以上の速度で動作する
- 無償のシミュレータである Icalus Verilog はインタプリタ方式なので、コンパイル方式であるVerilatorのほうが100倍ほど高速である
必要なOSとツール
Linux・MacOS・Windowsならいずれでも動作する様子。整備が進んでいるのはLinuxで、多くのLinuxディストリビューションが標準パッケージを提供している(参照: Versions for verilator)。
シミュレータの生成にはC++コンパイラが必要。
試作したもの
概要
基本を理解できるよう、簡単なカウンタを題材にテストベンチを組み、シミュレーションを実行して波形ダンプを出力するところまで試行した。
試作したものは verilator-example に置いた。
使用したOSとツール
手元で使用したOSとツールは下記のとおり。
- Debian 12 (bookworm) for amd64
- Verilator 5.006
- Gcc 12.2.0
- GNU Make 4.3
- GTKWave 3.3.118
VerilatorやGccなどはいずれもDebian標準のものを使用した。
参考にした例
試作を始めるにあたり、最初に例を探した。主に下記を参考にした。
-
Verilator manual の Examples に載っている3つの例
- Example Create-Binary Execution
- もっともシンプルな例。Verilog記述のみ・C++記述なしでメッセージを表示する
- Example C++ Execution
- Verilog記述とC++記述を使ってメッセージを表示する
- C++記述からVerilogモジュールを扱う手順の基礎がわかる
- クロック信号などが登場しないため、この例だけだと論理回路の扱い方まではわからない
- Example SystemC Execution
- C++ではなくSystemCを使っていること以外は、Example C++ Executionと同様
- Example Create-Binary Execution
- Verilatorリポジトリの examples/ ディレクトリ内のいくつかの例
- make_hello_c/
- 前述の Example C++ Execution とおおよそ同内容
- make_tracing_c/
- DUT(Design Under Test; テスト対象の設計物)であるVerilog記述のカウンタをテストベンチから制御し、波形ダンプ付きで動作させる
- テストベンチにはC++とVerilogの両方を使用
- DUTにはクロックとリセットを含む入出力ポートがあり、それをC++記述から制御しているので、テストベンチの基本的な組み方がわかる
- Verilatorの呼び出し手順などはMakefileに記述
- make_tracing_sc/
- C++ではなくSystemCを使っていること以外は make_tracing_c と同様。DUTはVerilogのまま
- cmake_tracing_c/
- make_tracing_c のDUTとテストベンチをmakeコマンドではなく CMake で扱うための CMakeLists.txt が置かれている
- make_hello_c/
C++とSystemCのどちらを使うのかは、用途と好みに応じて選べばよい様子だった。おそらく下記のような考え方になると思うが、SystemCは馴染みがなかったので、この試作ではC++を使うことにした。
- C++を選ぶと、C++のすべての機能を制約なく使えるため(VerilatorではVerilog記述のほうをC++側に合わせる構造なのでC++記述に対しては制約がかからない)、自由度が高い。C++から呼び出せるものなら、ライブラリでも他言語でも何でも使えるはず
- SystemCに慣れていて、SystemCが扱える範囲のことができれば十分なら、SystemCのほうが扱いやすいかもしれない
MakeとCMakeのどちらを使うのかも、好みで選べばよさそうだったので、ここではMakeを使うことにした。
構成
下図のような階層構成とした。
前述の make_tracing_c と同様に、テストベンチは sim_main (C++記述) と top (SystemVerilog記述) の2階層構成にした。C++記述のみの単一階層構成ではないため、SystemVerilogのほうが扱いやすいテストベンチ機能はSystemVerilog側に配置できる。
また、ディレクトリ構成とファイル配置は下記のようにした。src/ にはDUTを、sim/ にはテストベンチを置いた。
- src/
- Counter.sv
- sim/
- Makefile
- sim_main.cpp
- top.sv
sim/ にMakefileを置いて、このディレクトリでmakeコマンドを実行するとシミュレーションをおこなえるようにした。
DUTの記述
DUTであるカウンタのソースコードは下記のとおり。
// A simple up-counter module
module Counter #(
parameter int unsigned WIDTH = 1
) (
input logic clk,
input logic rst_n,
input logic enb,
output logic [WIDTH-1:0] count,
output logic carryout
);
always_ff @(posedge clk or negedge rst_n) begin
if (~rst_n) begin
count <= 0;
end else if (enb) begin
count <= count + 1;
end
end
assign carryout = (enb && count == '1);
endmodule
- WIDTH パラメータはカウンタのビット幅
- clk はクロック入力、rst_n は負論理の非同期リセット入力
- enb 入力が 1 のとき、カウント値出力 count はインクリメントされる
- enb 入力が 1 で count が最大値のとき、carryout には 1 が出力される
テストベンチの記述
テストベンチは上位層の sim_main.cpp と下位層の top.sv という2階層構成にしたので、役割分担を決める必要があった。ここでは下記のようにした。
- sim_main.cpp
- Verilatorのために必須の処理を書く
- それ以外には、下記の責務のみを担う
- タイムスケールを設定
- クロック信号と非同期リセット信号を制御
- top.sv
- sim_main.cpp が扱わない残りのすべてのテストベンチの責務を担う
- DUTのインスタンスを設置
- DUTの入力信号を制御し動作させる。ここでは、シミュレーションが開始されたら enb 入力を 1 にして、カウンタの値を進めさせる
- DUTの出力信号を観測し、終了条件が満たされたらシミュレーションを完了させる。ここでは、carryout 出力が 1 になったら
$finish
を実行する - 波形ダンプを出力
- sim_main.cpp が扱わない残りのすべてのテストベンチの責務を担う
sim_main.cpp の内容は下記のとおり。Verilatorリポジトリの examples/make_tracing_c/sim_main.cpp を参考にして作成した。
// A C++ wrapper of the top module
#include <memory>
#include "verilated.h"
#include "Vtop.h"
int main(int argc, char **argv) {
// Create a VerilatedContext
std::unique_ptr<VerilatedContext> contextp{new VerilatedContext};
contextp->commandArgs(argc, argv);
// Create the Verilated model of the top module
std::unique_ptr<Vtop> top{new Vtop{contextp.get(), "top"}};
// Setup the context
contextp->timeunit(-9); // Set timeunit to 1ns
contextp->timeprecision(-9); // Set timeprecision to 1ns
contextp->traceEverOn(true); // Allow traces
// Run
top->clk = 0;
top->rst_n = !0;
while (!contextp->gotFinish()) {
top->clk = !(top->clk);
if (!top->clk) {
top->rst_n = !(contextp->time() > 0 && contextp->time() < 20);
}
top->eval();
contextp->timeInc(5);
}
// Finalize
top->final();
return 0;
}
-
// Create a VerilatedContext
の箇所-
VerilatedContext
を生成し、contextp
という変数に保持 -
contextp
にはstd::unique_ptr
を使うため、明示的な解放は不要
-
-
// Create the Verilated model...
の箇所- topモジュールのインスタンスを生成し、
top
という変数に保持
- topモジュールのインスタンスを生成し、
-
// Setup the context
の箇所-
contextp
に指示し、timeunit を 1ns に、timeprecision を 1ns に設定-
verilated.cpp の
VerilatedContext::Serialized::Serialized()
に、timeunit関数やtimeprecision関数が扱う値のデフォルト値は -12 で、意味は picosecond と書いてある - 値を +1 すると単位時間は10倍になるようなので、ここで与えている
-9
は nanosecond を意味する - 指定が意図どおりかどうかは、波形ダンプで確認できる
-
verilated.cpp の
- 波形ダンプ出力を許可 (実際の出力開始は top.sv が指示する)
-
-
// Run
の箇所-
top->clk
(クロック信号) とtop->rst_n
(非同期リセット信号) に初期値を設定してから、シミュレーション実行のwhileループを開始。ループ内では下記の順に処理する- シミュレーション終了指示(SystemVerilog記述での
$finish
の実行など)の有無を判別し、指示があったならループ処理を終了する -
top->clk
を反転させる - クロック立ち下がりのタイミングで非同期リセットを制御する。絶対時刻
contextp->time()
(単位は1ns) が0より大きく20より小さい区間でのみアサートし(負論理なので値は0)、さもなければネゲートする - topモジュールを評価する
- 絶対時刻を 5ns 進める
- シミュレーション終了指示(SystemVerilog記述での
- 波形ダンプで見たときのクロック信号が下記を満たすようにするために、クロック制御・topモジュール評価・絶対時刻の加算をこのソースコードのような順序にしている
- 時刻 0ns から波形ダンプ開始
- クロック周期は 10ns (クロック周波数は100MHz)
- 時刻 0ns、10ns、20ns ... でクロックが立ち上がる
- 非同期リセットの変化タイミングをクロックの立ち上がりからずらすために、クロックの立ち下がりを使っている。実物の回路にはこういったことをする必要があるが、Verilatorには必要ない。ここでは、波形ダンプを見たときに人間が見慣れた波形になるようこうしている
-
-
// Finalize
の箇所- Verilatorが求める完了処理をおこなってからプログラムを終了させる
top.sv の内容は下記のとおり。DUTに合わせて書いたものなので、examples/make_tracing_c/top.v との類似点は少ない。
// The top module in SystemVerilog
module top (
input logic clk,
input logic rst_n
);
// The configuration of the DUT
localparam int unsigned DUT_WIDTH = 4;
// The DUT
logic dut_enb;
// verilator lint_off UNUSEDSIGNAL
logic [DUT_WIDTH-1:0] dut_count;
// verilator lint_on UNUSEDSIGNAL
logic dut_carryout;
Counter #(
.WIDTH(DUT_WIDTH)
) dut (
.clk (clk),
.rst_n (rst_n),
.enb (dut_enb),
.count (dut_count),
.carryout(dut_carryout)
);
// The test scenario
typedef enum logic [1:0] {
STATE_INITIAL,
STATE_RUNNING,
STATE_FINISHED
} state_e;
state_e state;
always_ff @(posedge clk or negedge rst_n) begin
if (~rst_n) begin
state <= STATE_INITIAL;
end else if (state == STATE_INITIAL) begin
state <= STATE_RUNNING;
end else if (state == STATE_RUNNING && dut_carryout) begin
state <= STATE_FINISHED;
end else if (state == STATE_FINISHED) begin
$finish();
end
end
assign dut_enb = (state == STATE_RUNNING);
// Enable tracing if `+trace` argument specified
initial begin
if ($test$plusargs("trace") != 0) begin
$display("Start tracing to dump.vcd...");
$dumpfile("dump.vcd");
$dumpvars();
end
end
endmodule
- topモジュールの概形
- clk (クロック) と rst_n (負論理の非同期リセット) の2つの入力ポートを持つ。他にポートはない
- パラメータを持たない
-
// The configuration of the DUT
の箇所- ローカルパラメータ
DUT_WIDTH
にDUTのカウンタのビット幅として4を設定
- ローカルパラメータ
-
// The DUT
の箇所- DUTのインスタンスを置いている
- DUTの
WIDTH
パラメータには、DUT_WIDTH
の値を渡している -
// verilator lint_off UNUSEDSIGNAL
と// verilator lint_on UNUSEDSIGNAL
は、Verilatorへの警告抑制指示。このテストベンチではDUTの count 出力ポートを受けとる信号 dut_count を使わないため、dut_count の宣言をこの指示で囲まないと、Verilatorでのコンパイル時に警告が出てエラーになる
-
// The test scenario
の箇所- DUTの入力ポート(enb)に与える信号の制御と、DUTの出力ポート(carryout)からの信号を監視してシミュレーションの終了を判定する部分
- Verilatorに合わせて、タイミング記述(
@(posedge clk)
や#1
など)は使わず、合成可能なステートマシンを使っている。下記の順序で状態遷移する
(Verilator 5からタイミング記述がサポートされたが、ここでは使っていない)- 初期状態は
STATE_INITIAL
。enb は 0 - 非同期リセットが解除されたら
STATE_RUNNING
に遷移。enb を 1 にする - carryout が 1 になるまでは
STATE_RUNNING
に留まる。enb は 1 のまま - carryout が 1 になったら
STATE_FINISHED
に遷移。enb を 0 にする - その次サイクルで
$finish
を実行し、シミュレーションを終了させる
- 初期状態は
-
// Enable tracing if `+trace` argument specified
の箇所- シミュレータ起動時のコマンドライン引数に
+trace
が指定されていたら、dump.vcd
というファイルにVCD形式の波形ダンプを出力する - VCD(Value Change Dump)はVerilog標準の波形ダンプ形式
- シミュレータ起動時のコマンドライン引数に
Makefileの内容と動かし方
sim/ に置いたMakefileの内容は下記のとおり。
# Sources and targets
SOURCES = sim_main.cpp top.sv Counter.sv
SIMULATOR = ./obj_dir/Vtop
DUMPFILE = dump.vcd
CLEANFILES = obj_dir $(DUMPFILE)
vpath %.sv ../src
# Commands
VERILATOR = verilator
# Flags for commands
VERILATOR_FLAGS = --cc --exe --build -j 0 -Wall --trace
SIMULATOR_FLAGS = +trace
# Rules
.PHONY: all build run clean
all: run;
run: $(DUMPFILE);
build: $(SIMULATOR);
$(SIMULATOR): $(SOURCES)
$(VERILATOR) $(VERILATOR_FLAGS) $^
$(DUMPFILE): $(SIMULATOR)
$(SIMULATOR) $(SIMULATOR_FLAGS)
clean:
-rm -fr $(CLEANFILES)
- カレントディレクトリを sim/ にして、make を引数なしで実行すると、シミュレータのビルドとシミュレーションの実行をまとめておこなう
- ビルドとシミュレーションを分けて実行する手順は下記のようにしている
-
make build
: シミュレータ(./obj_dir/Vtop)を生成する -
make run
: シミュレーションを実行し、波形ダンプ dump.vcd を生成する
-
- Verilatorによるコンパイルでは、分割コンパイルはせずにすべてのソースコードをまとめて与えている。コンパイル時の引数
VERILATOR_FLAGS
に並べているものは下記のとおり-
--cc
: VerilogやSystemVerilogの記述をC++記述へと変換する -
--exe
: 実行可能バイナリを生成する -
--build
: C++記述への変換に続けて、C++コンパイルなどのビルドをおこなう -
-j 0
: 並列にビルドする。並列度は自動選択 -
-Wall
: すべての警告を有効にする -
--trace
: VCD形式の波形ダンプ生成を可能にする (C++変換時に生成コードを埋め込む)
-
- シミュレーション実行時の引数
SIMULATOR_FLAGS
は下記のとおり-
+trace
: 波形ダンプの生成を指示する
-
カレントディレクトリを sim/ にして make
を実行すると、例えば下記のようにシミュレータが生成され、シミュレーションが実行される。
$ cd sim
$ make
verilator --cc --exe --build -j 0 -Wall --trace sim_main.cpp top.sv ../src/Counter.sv
make[1]: Entering directory '.../verilator-example/sim/obj_dir'
...
make[1]: Leaving directory '.../verilator-example/sim/obj_dir'
./obj_dir/Vtop +trace
Start tracing to dump.vcd...
- top.sv:41: Verilog $finish
問題なく実行できた場合は、波形ダンプファイル dump.vcd が生成される。
波形ダンプの様子
得られた波形ダンプ dump.vcd をGTKWaveで表示したときの様子は下記のとおり。Signals
の信号名は、いずれも top モジュール内でのもの。
テストベンチおよびDUTが意図どおりに動作していることが、下記のように確認できた。
- テストベンチ
- clk の周期は 10ns。波形ダンプは 0ns から開始されていて、信号の立ち上がりは 0ns・10ns・20ns... に位置している
- rst_n は、5ns から 25ns の間にアサートされている
- dut_enb (DUTのenb入力) は、非同期リセット解除後の最初のサイクル(clk の立ち上がり)から 1 になっている
- dut_carryout (DUTのcarryout出力) が 1 になった次のサイクルで波形ダンプが完了している
- DUT
- enb入力が 1 であれば、dut_count (DUTのcount出力) がインクリメントされている
扱わなかったこと
Verilatorの基本的な使い方がわかれば十分だったので、この試作では下記を扱わなかった。
- FST形式の波形ダンプ (FST: Fast Signal Trace)
- Verilatorがサポートしている波形ダンプの形式は、VCDとFSTの2つ
- 試作で使用したVCDは、Verilog標準という利点があるが、サイズが大きいことが問題。長時間シミュレーションには向かない
- FSTはGTKWaveが定めたもので(参照: Supported Formats - GTKWave Documentation)、VCDよりもサイズが小さいことが利点。Verilog標準ではないが、GTKWaveで波形を見る分には問題ない
- Lintツールとしての使用
- Verilatorはlint(構文チェック)機能を備えているので、いずれは試したい
- C++コードでの動作チェックや検証
- この試作では、波形ダンプを使ってテストベンチやDUTの動作を確認した。波形ダンプでの確認は、人間による目視を伴うため、バグ探しなどには向いているが、仕様違反がないことを網羅的に検証するような用途には向いていない
- 検証では、何らかのプログラムを使って仕様違反などを判別するのがよい。他のVerilogシミュレータを用いた検証であれば、Verilog記述でチェッカーを書いたり、DUTの内部状態をファイルにダンプしてから別途チェックしたりするが、Verilatorではそれに加えてC++によるチェックが使える (SystemVerilogのDPI: Direct Programming Interfaceなどを使えば他のシミュレータでもC++を使えるので、それほど特殊ではないが)
- コードカバレッジの取得と内容確認
- 実践的な検証で十分なコードカバレッジを達成しようとすると、長時間シミュレーションになりうるので、Verilator向きの用途になる。そういう状況になったら調べることになりそう
補足
VerilogとSystemVerilogの機能のサポート状況
VerilatorがVerilogとSystemVerilogのどの機能をサポートしているのかは、最新のVerilatorに対してであれば、下記に記載されている。
いくらか大まかだが、下記のように考えればよい様子。
- 合成可能な記述の扱い
- 大抵のものはサポートされている
- タイミング記述の扱い (参照: Input Languages > Time)
- Verilator 5からは、
--timing
を指定すれば、一部のタイミング記述を使用できる
- Verilator 5からは、
- 不定値の扱い (参照: Input languages > Unknown States)
- Verilatorは基本的に0と1の2状態のみを扱うシミュレータなので、Verilogが定める4状態(0、1、不定x、ハイインピーダンスz)を正しくは扱えない。が、不定値を明示的に使うなどしても、エラーにはならない
- よって、不定値やハイインピーダンスが使われると、シミュレーションは実行できるが、Verilog仕様とは異なるふるまいをする
- それ以外の合成不能な記述の扱い
- 公式の examples が使っているものは問題なく使える。例えば initial、
$display
、$time
、$finish
、波形ダンプ関連のディレクティブ、$test$plusargs(...)
など - それ以外のディレクティブの扱われ方は、Imput languages > Language Limitations で確認できる
- 公式の examples が使っているものは問題なく使える。例えば initial、
Verilator 5を使って試作したソースコードはVerilator 4でもそのまま動くのか?
試作したもの一式をUbuntu 22.04 LTS (on Docker) の Verilator 4.038 で動かしてみようとした。が、sim_main.cpp のコンパイルが後述の2箇所で失敗したため、その時点で作業を中断した。
Verilator 5を使って書いたものをVerilator 4に対応させることは、あまり考えないほうがよさそうだった。
VerilatedContext
クラスが存在しない
Verilatorの v4.038タグ の examples/make_tracing_c/sim_main.cpp を見てみると、VerilatedContext
というクラスを使っておらず、代わりに Verilated::commandArgs
関数や Verilated::traceEventOn
関数を使っていた。
Verilatorの中を調べる(その3) - Vengineerの妄想 によれば、VerilatedContext
クラスは v4.200 から導入されたようだ。v4.200 と、その直前のバージョンである v4.110 の examples/make_tracing_c/sim_main.cpp を見比べると、確かにそうなっていた。
あれこれ探してみると、Verilator manualのVerilated and VerilatedContextに Verilated
と VerilatedContext
の位置づけについて下記のように書いてあった。
-
Verilated::commandArgs
などの呼び出しは、デフォルトのグローバル(スレッド)コンテキストに適用される - 複数のコンテキストを扱う場合には
Verilated::commandArgs
などの呼び出しを使うべきではない。代わりに、適切な VerilatedContext オブジェクトを選んでそのメソッド呼び出しを使うべきである
v4.200 以降でも v4.110 までと同一の Verilated::commandArgs
関数などの呼び出しはサポートされているようである。v5.006 の include/verilated.h には、v4.110 のものに似た Verilated クラスが定義されていた。
contextp->time() と contextp->timeInc() 相当のものが存在しない
VerilatedContext
問題の回避方法がわかったので、sim_main.cpp の contextp->...
となっている箇所を Verilated::...
に置き換えた。すると今度は Verilated::time()
と Verilated::timeInc()
という関数が存在しないというエラーになった。v4.110 の include/verilated.h にこれらの関数が定義されていないので、実際に使えないということだろう。
参考文献
- Verilator本家
- Verilatorの使い方 - FPGA開発日記
- Verilatorやってみた!〜OpenCVでテストベンチを書いてみた〜
- Verilatorの中を調べる - Vengineerの妄想
Discussion