✌️

Verilatorを使ってみた

2025/01/13に公開

Verilatorの使用感を知りたかったので、初めて使ってみた。そのときの調査や作業の記録を残しておく。

https://github.com/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 manualExamples に載っている3つの例
    • Example Create-Binary Execution
      • もっともシンプルな例。Verilog記述のみ・C++記述なしでメッセージを表示する
    • Example C++ Execution
      • Verilog記述とC++記述を使ってメッセージを表示する
      • C++記述からVerilogモジュールを扱う手順の基礎がわかる
      • クロック信号などが登場しないため、この例だけだと論理回路の扱い方まではわからない
    • Example SystemC Execution
      • C++ではなくSystemCを使っていること以外は、Example C++ 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 が置かれている

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であるカウンタのソースコードは下記のとおり。

Counter.sv
// 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 の内容は下記のとおり。Verilatorリポジトリの examples/make_tracing_c/sim_main.cpp を参考にして作成した。

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 という変数に保持
  • // Setup the context の箇所
    • contextp に指示し、timeunit を 1ns に、timeprecision を 1ns に設定
      • verilated.cppVerilatedContext::Serialized::Serialized() に、timeunit関数やtimeprecision関数が扱う値のデフォルト値は -12 で、意味は picosecond と書いてある
      • 値を +1 すると単位時間は10倍になるようなので、ここで与えている -9 は nanosecond を意味する
      • 指定が意図どおりかどうかは、波形ダンプで確認できる
    • 波形ダンプ出力を許可 (実際の出力開始は top.sv が指示する)
  • // Run の箇所
    • top->clk (クロック信号) と top->rst_n (非同期リセット信号) に初期値を設定してから、シミュレーション実行のwhileループを開始。ループ内では下記の順に処理する
      1. シミュレーション終了指示(SystemVerilog記述での $finish の実行など)の有無を判別し、指示があったならループ処理を終了する
      2. top->clk を反転させる
      3. クロック立ち下がりのタイミングで非同期リセットを制御する。絶対時刻 contextp->time() (単位は1ns) が0より大きく20より小さい区間でのみアサートし(負論理なので値は0)、さもなければネゲートする
      4. topモジュールを評価する
      5. 絶対時刻を 5ns 進める
    • 波形ダンプで見たときのクロック信号が下記を満たすようにするために、クロック制御・topモジュール評価・絶対時刻の加算をこのソースコードのような順序にしている
      • 時刻 0ns から波形ダンプ開始
      • クロック周期は 10ns (クロック周波数は100MHz)
      • 時刻 0ns、10ns、20ns ... でクロックが立ち上がる
    • 非同期リセットの変化タイミングをクロックの立ち上がりからずらすために、クロックの立ち下がりを使っている。実物の回路にはこういったことをする必要があるが、Verilatorには必要ない。ここでは、波形ダンプを見たときに人間が見慣れた波形になるようこうしている
  • // Finalize の箇所
    • Verilatorが求める完了処理をおこなってからプログラムを終了させる

top.sv の内容は下記のとおり。DUTに合わせて書いたものなので、examples/make_tracing_c/top.v との類似点は少ない。

top.sv
// 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からタイミング記述がサポートされたが、ここでは使っていない)
      1. 初期状態は STATE_INITIAL。enb は 0
      2. 非同期リセットが解除されたら STATE_RUNNING に遷移。enb を 1 にする
      3. carryout が 1 になるまでは STATE_RUNNING に留まる。enb は 1 のまま
      4. carryout が 1 になったら STATE_FINISHED に遷移。enb を 0 にする
      5. その次サイクルで $finish を実行し、シミュレーションを終了させる
  • // Enable tracing if `+trace` argument specified の箇所
    • シミュレータ起動時のコマンドライン引数に +trace が指定されていたら、dump.vcd というファイルにVCD形式の波形ダンプを出力する
    • VCD(Value Change Dump)はVerilog標準の波形ダンプ形式

Makefileの内容と動かし方

sim/ に置いたMakefileの内容は下記のとおり。

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 モジュール内でのもの。

GTKWave-screenshot

テストベンチおよび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 を指定すれば、一部のタイミング記述を使用できる
  • 不定値の扱い (参照: 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 で確認できる

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 VerilatedContextVerilatedVerilatedContext の位置づけについて下記のように書いてあった。

  • 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 にこれらの関数が定義されていないので、実際に使えないということだろう。

参考文献

Discussion