FPGA開発日記

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

Verilatorの使い方(1. Verilatorの考え方と基本的なシミュレーション実行方法)

f:id:msyksphinz:20200501120616p:plain

Verilatorについて全く知らない人が、どのように使えば良いのかきちんとした文章が世の中に存在していない気がするので、少しまとめてみることにした。VerilatorはフリーでオープンソースVerilogシミュレーションシステムなので、うまく活用すれば強力な武器になる。高額なEDAツールを使わずとも家でハードウェア開発ができるようになる強力なツールだ。

www.veripool.org

RTLシミュレータと言えば、有償のものも含めれば代表的なものは以下のようなものであろうか:

  • Synopsys VCS : 有償。
  • Cadence Xcelium:有償。
  • Mentor QuestaSim:有償

と、ほとんどのツールが有償であるが、Verilatorは無償かつオープンソースである。かつ性能も高いので、オープンソースプロジェクトなどで使うのにはちょうど良い。

予め説明しておくが、Verilatorは良いことばかりではない。なぜ高性能なのか?どういう制約があるのか?昔私のブログで紹介していた。

  • 高速Verilogシミュレータ"Verilator"で「出来ないこと」

msyksphinz.hatenablog.com

個人的には、以下が猛烈に大きい。

全ての遅延記述 (#) は無視される。

event系のイベント (waitなど) はサポートされない。

Unknownステートはサポートされない

前提条件:VerilatorはVerilogをそのままシミュレーションするわけではない

上記の制約も含め、Verilatorの基本的な考え方を説明する。

Verilatorは他の多くのRTLシミュレーションシステムと同様にコンパイル型である。つまり、

という2段階構成を踏む。この際に気をつけなければならないのが上記の制約で、遅延記述やイベント系の構文は全くサポートされない。つまり、テストベンチ系のコードは全くコンパイルすることができないのだ。ほとんどデジタル回路に直結するVerilog記述しか受け付けることができないと考えて良い。

有償のシミュレータと考え方を比較してみる。有償シミュレータはテストすべき回路記述部分(DUT:Design Under Test)とそれを取り囲む形でテストベンチが構成される。DUTとテストベンチは両方Verilogとして記述されている。

しかしVerilatorの場合は違う。DUTはVerilogとして記述されるが、それ以外のテストベンチは殆どの記述が受け付けられないので、C言語を使ってテストベンチを構成する必要がある。これはVerilatorがDUTをC言語プログラムとして変換するため、これをリンクする形でC言語のテストベンチプログラムを用意するという形になる。

f:id:msyksphinz:20200501120052p:plain

カウンタ回路を作ってVerilatorでシミュレーションする

具体的な例を考えて行こう。以下のようなVerilogで記述した回路を考える。en信号でカウントアップする簡単なカウンタだ。

  • counter_4bit.v
module counter
  (
   input logic        clk,
   input logic        reset_n,
   input logic        en,
   output logic [3:0] cnt
   );
always_ff @(posedge clk, negedge reset_n) begin
  if (!reset_n) begin
    cnt <= 4'h0;
  end else begin
    if (en) begin
      cnt <= cnt + 4'h1;
    end
  end
end
endmodule // counter

Verilogでテストベンチを書くのは簡単だが、ここではVerilatorを使う。このカウンタをシミュレーションするVerilator環境を作ってみよう。

使用するVerilatorのバージョンは以下となる。

$ verilator --version
Verilator 4.032 2020-04-04 rev UNKNOWN_REV

まずはこのDUTをコンパイルしてみよう。

$ verilator -cc counter_4bit.v

obj_dirというディレクトリが作成されている。

$ ls -1 obj_dir
Vcounter_4bit.cpp
Vcounter_4bit.h
Vcounter_4bit.mk
Vcounter_4bit__Syms.cpp
Vcounter_4bit__Syms.h
Vcounter_4bit__ver.d
Vcounter_4bit__verFiles.dat
Vcounter_4bit_classes.mk

これらがcounter_4bit.vから生成されたファイル群だ。C言語のコードに変換されていることが分かる。これをシミュレーションするための環境を作っていく。

C++を使ってcounter_4bitのテスト環境を作っていく

次はcounter_4bitのテストベンチ環境を作っていく。説明したとおり、テストベンチはC/C++で記述する必要があり、これが有償シミュレータとの大きな違いだ。このテストベンチの構成方法について、ざっくりとしたテンプレートから示していこうと思う。

// 必要なファイルのインクルード
#include <iostream>
#include <verilated.h>
#include "Vcounter_4bit.h"

int main(int argc, char **argv) {
    // Verilatorのコマンド解析
    // DUTモジュールのインスタンス化
    // DUTモジュールのインタフェースの初期化
    
    // while文 {
    //     クロックを1サイクルずつ進めていく記述
    //     DUTを評価(回路を実行する)記述
    //     DUTインタフェースを評価する処理
    // }
    
    // 終了
}

この流れを見ると、Verilogで記述しているテストベンチと流れは大して変わらないことが分かる。Verilogで記述しているテストベンチを、C/C++で書き直しているだけである。ただし書き直しているといってもVerilog記述のように自然に並列記述をすることはできないため、並列なテスト動作を記述するためには少し工夫する必要がある。

また、while文の内部についても少し考慮が必要だ。DUT内の論理回路の実行は、具体的にはdut->eval();という記述で実行されるが、これはdutが最小時間単位で動いていることを意味する。つまりVerilogで言うtimescale記述のようなものだと考えて良い。クロックとの関係性で言えば、例えば10回分eval();を進めたところで外部クロック信号をトグルする、というような記述を行う。以下のようなイメージだ。

dut->clk = 0;
while (true) {
    dut->eval();
    if (time_counter % 5 == 0) {        // 5 eval()に1回clkをtoggleする。(つまり周期は10 eval()分)
        dut->clk = !dut->clk;
    } 
    time_counter ++;
}

ではさっそくテストベンチを記述する。以下のようになった。

#include <iostream>
#include <verilated.h>
#include "Vcounter_4bit.h"

int time_counter = 0;

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

  Verilated::commandArgs(argc, argv);

  // Instantiate DUT
  Vcounter_4bit *dut = new Vcounter_4bit();

  // Format
  dut->reset_n = 0;
  dut->clk = 0;
  dut->en = 0;

  // Reset Time
  while (time_counter < 100) {
    dut->eval();
    time_counter++;
  }
  // Release reset
  dut->reset_n = 1;

  int cycle = 0;
  while (time_counter < 500) {
    if ((time_counter % 5) == 0) {
      dut->clk = !dut->clk; // Toggle clock
    }
    if ((time_counter % 10) == 0) {
      // Cycle Count
      cycle ++;
    }

    if (cycle % 5 == 0) {
      dut->en = 1;   // Assert En
    } else {
      dut->en = 0;   // Deassert En
    }

    // Evaluate DUT
    dut->eval();

    time_counter++;
  }

  // std::cout << "Final Counter Value = " << dut->cnt << '\n';
  printf("Final Counter Value = %d\n", dut->cnt);

  dut->final();
}

上記で説明したとおりであり、

  1. dutとしてcounter_4bitモジュールをインスタンス化する。
  2. reset_n / clk / en を初期化する。
  3. 100単位時間ほどリセット状態を保持する。
  4. リセットをリリースし、シミュレーション開始する。
  5. 5単位時間に1回clkをトグルする。
  6. 5サイクルに1回en信号を有効化する。単位時間毎にAssetを変更してしまうと一瞬でDeassertされてしまうので注意。
  7. 毎単位時間にdut->eval()を呼び出しDUTを評価する。
  8. 所定時間シミュレーションを実行すると終了し、最後に結果dut->cntを出力する。

ではこれをシミュレーション環境を準備してみよう。以下のように実行する。

$ verilator --cc counter_4bit.v -exe tb_counter_4bit.cpp
$ cd obj_dir
$ make -C obj_dir -f Vcounter_4bit.mk

obj_dir/Vcounter_4bitというバイナリが生成されていることが分かる。これがシミュレーション本体だ。ではさっそく実行してみよう。

$ ./obj_dir/Vcounter_4bit
Final Counter Value = 8

問題なくカウンタが動作していることが分かった。ここまででVerilatorの基礎は完了だ。以降では、波形のダンプ方法、様々なVerilatorの機能について紹介していく。