2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Using Verilator with CMake

Posted at

はじめに

15日なら暇になるだろでカレンダーに入れたけれど結局年の瀬に書く人になってしまいました. まぁ書かないより良いのでヨシ!

自己紹介

普段は京都大学集積回路工学の研究室の隅っこで準同系暗号をやってるます. せっかく集積回路の研究室なんだからハードウェア高速化やりたいなぁと思ったのがこの記事の内容を学ぶ羽目になった理由です. HDL, 書かないで済むならそれが一番(もちろんアナログでどうにかせいと言っているわけではなくCPUで済む場合とか)なので引き返せる人は引き返したほうが良いと思うんですが, まぁこの記事を見に来るところまで着てたらもうダメかもしれないですね......

Verilatorは銀の弾丸ではない

さてこの記事の想定読者は,C++で普段プログラムを書いていて, さぁてHDLで何か書くからシミュレーション・テスト環境, できれば速いやつが欲しいなぁみたいな3年くらい前の私です. まず結論から言うと, Verilatorが使える状況は実は結構限定的です. まず第一にVerilatorは期待しているほど速くないです. SynopsysのVCSが使える人はそっちを使ったほうが良いでしょう. Icarus Verilogよりは速いと思うので失望するほど遅くもないですが. 第二に0か1でない値が扱えません. 例えばtristate bufferが入るような回路を書きたい場合には致命的です. 初期化とかがundefinedな場合に勝手に0にされてバグに気づかないとか(実際なった)とかもあります. そういう意味でもVCSが使えるならそっちが良いですし, 今考えると実はIcarus Verilogと両方でシミュレーションができるのが良かったんじゃないかっていう気もします. 第三に遅延が扱えません. まぁFPGAくらいなら困らないんですが, ASICとかだとP&R後に遅延を考慮したシミュレーションをしないと正しく性能評価できないので, これは結構致命的だったりします. VCSだとそれも扱えるのにVerilatorより速かったりするのでやっぱVCSが使えるならVCS使っといたほうが多分良いです.

じゃあVerilatorっていつ使うの

そんな否定的なことを書いておいてこんな記事を書いているわけですから当然Verilatorを使うケースはあるわけです. まず第一にはC++でテストベンチが自然にかけることがあります. いやDPIとか使うとVCSでも呼び出せるっちゃ呼び出せるはずなのですが, VerilatorではVerilogを食わすと単にheaderとC++のソースファイルが出てくる形なので, HDL特有のあれこれとかを真面目に学習しなくても扱うことができます. つまり比較的学習コストが低いです. C++でコードを普段書いているなら参照実装になるものもC++でしょうからテストベンチからそれらを直接呼び出せるのでその意味でも便利です. 第二にOpen Sourceです. まぁVCS云々って上に書いてたんですが, 集積回路の専門の人間でもなければそんな物アクセスできるわけがないので, そういう環境ではベターな選択肢です. FPGAベンダの無償供給するシミュレータとかに比べれば合成とかもないのもあってセットアップ含めずっと高速(な場合が多い)です. 第三に可搬性が高いです. まぁOpen Sourceって言うことからも来ては居るんですが, インストールというほどのインストールがほぼ存在しないのでどこでも簡単に使えます. やろうと思えばCIでHDLのテストするとかできると思います. まぁIcarus Verilogも簡単なのでそことの優位性ではないですが, Cadenceのツールとかに比べたら天地の差じゃないでしょうか. (個人的な恨みが入っています)

タイトルのCMakeってどういう話

さてとりあえずVerilatorの良し悪しの話っぽいことはしたので, それでもVerilatorを使いたいっていう人に向けての情報をここから書いていきます. 想定読者はC++を書いている人々なのできっとCMakeを使っているんじゃないかと思います. まぁmakeで頑張っている人はverilatorのCLIコマンドをそのまま叩いてどうにかすると思うので困らないでしょうし......CMakeに対応しているって言うことは当然makefileを吐いてくれる機能もあります. 他のパッケージマネージャっぽい何かたちは対応する方法を私も知りません. 個人の感想としてはCMakeがまぁBetter than Nothingじゃないんですかね.
Verilatorには公式でCMakeに対する対応が入っているのでDocumentがあります. まぁこれを見てもうわかる人はここでブラウザバックを押すと良いと思います. 私は毎度新しいソフトウェアのCMakeを見るたびにそれでこれはどうやって使うんだって混乱するタイプの人間なので表現の違う情報があると助かるかなぁと思ってこの記事を書いています. この記事ではVerilatorをいい感じに使う上で私が思っている知見も書いておくのでそっちのほうは役に立ったりするかもしれません.

Verilatorのインストール

Ubuntu(WSL2を含む)を使っているそこのあなたは一応aptで入れることができます. ただaptのやつはVerilatorが精力的に開発されてることもあってかなり古いことが多いです. 私としてはソースからのインストールをおすすめします.
これに関しては公式のDocumentで迷うことはないでしょう. 単に依存パッケージを入れてsudo make install -jで終わりです. Xyceみたいに大量のオプションを指定しないといけないとかはないです. (個人的な恨みが入っています)

Gtkwaveのインストール

これは別に古くても困らないのでaptで問題ないです. 波形ビューアーなのでVerilatorを使うのに必須なわけではないですが, 一応波形を見ようと思ったらこれを使うと思うので触れておきます. もちろんSynopsysのWave Viewerとかでもかまいませんが.

sudo apt install gtkwave

VerilatorのCMakeでの使い方

Example Verilogファイルの準備

さて本題ですが, さすがに何かしらのexampleがないと説明が宙に浮いてしまうのでexampleを用意することにします.

私はほとんどVerilogを直接は書かない人間なのでChisel3でexampleを用意しようと思います. 今回は乱数生成器のxoshiro128**を例にとりましょう.

import chisel3._
import chisel3.util._

class xoshiro128() extends Module{
    val io = IO(new Bundle{
        val seed = Input(UInt(128.W))
        val seedwrite = Input(Bool())
        val out = Output(UInt(32.W))
    })

    val s = Reg(Vec(4,UInt(32.W)))
    io.out := ((s(1)*5.U)(31,0)).rotateLeft(7)*9.U
    val t = (s(1) << 9)(31,0)
    s(0) := s(0) ^ s(1) ^ s(3)
    s(1) := s(0) ^ s(1) ^ s(2)
    s(2) := s(0) ^ s(2) ^ t
    s(3) := (s(1) ^ s(3)).rotateLeft(11)

    when(io.seedwrite){
        for(i <- 0 until 4){
            s(i) := io.seed(32*(i+1)-1,32*i)
        }
    }
}

object xoshiroTop extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new xoshiro128())
}

これをVerilogに変換すると以下のようになります.
今回は結構単純な回路なのでVerilogとしても読める程度のVerilogが吐かれました.
このVerilogファイルがxoshiro128.vとして保存されているとします.

module xoshiro128(
  input          clock,
  input          reset,
  input  [127:0] io_seed, // @[src/main/scala/xoshiro128.scala 5:16]
  input          io_seedwrite, // @[src/main/scala/xoshiro128.scala 5:16]
  output [31:0]  io_out // @[src/main/scala/xoshiro128.scala 5:16]
);
  reg [31:0] s_0; // @[src/main/scala/xoshiro128.scala 11:16]
  reg [31:0] s_1; // @[src/main/scala/xoshiro128.scala 11:16]
  reg [31:0] s_2; // @[src/main/scala/xoshiro128.scala 11:16]
  reg [31:0] s_3; // @[src/main/scala/xoshiro128.scala 11:16]
  wire [34:0] _io_out_T = s_1 * 3'h5; // @[src/main/scala/xoshiro128.scala 12:21]
  wire [31:0] _io_out_T_4 = {_io_out_T[24:0],_io_out_T[31:25]}; // @[src/main/scala/xoshiro128.scala 12:44]
  wire [35:0] _io_out_T_5 = _io_out_T_4 * 4'h9; // @[src/main/scala/xoshiro128.scala 12:47]
  wire [40:0] _t_T = {s_1, 9'h0}; // @[src/main/scala/xoshiro128.scala 13:19]
  wire [31:0] t = _t_T[31:0]; // @[src/main/scala/xoshiro128.scala 13:24]
  wire [31:0] _s_0_T = s_0 ^ s_1; // @[src/main/scala/xoshiro128.scala 14:18]
  wire [31:0] _s_0_T_1 = _s_0_T ^ s_3; // @[src/main/scala/xoshiro128.scala 14:25]
  wire [31:0] _s_1_T_1 = _s_0_T ^ s_2; // @[src/main/scala/xoshiro128.scala 15:25]
  wire [31:0] _s_2_T = s_0 ^ s_2; // @[src/main/scala/xoshiro128.scala 16:18]
  wire [31:0] _s_2_T_1 = _s_2_T ^ t; // @[src/main/scala/xoshiro128.scala 16:25]
  wire [31:0] _s_3_T = s_1 ^ s_3; // @[src/main/scala/xoshiro128.scala 17:19]
  wire [31:0] _s_3_T_3 = {_s_3_T[20:0],_s_3_T[31:21]}; // @[src/main/scala/xoshiro128.scala 17:37]
  assign io_out = _io_out_T_5[31:0]; // @[src/main/scala/xoshiro128.scala 12:12]
  always @(posedge clock) begin
    if (io_seedwrite) begin // @[src/main/scala/xoshiro128.scala 19:23]
      s_0 <= io_seed[31:0]; // @[src/main/scala/xoshiro128.scala 21:18]
    end else begin
      s_0 <= _s_0_T_1; // @[src/main/scala/xoshiro128.scala 14:10]
    end
    if (io_seedwrite) begin // @[src/main/scala/xoshiro128.scala 19:23]
      s_1 <= io_seed[63:32]; // @[src/main/scala/xoshiro128.scala 21:18]
    end else begin
      s_1 <= _s_1_T_1; // @[src/main/scala/xoshiro128.scala 15:10]
    end
    if (io_seedwrite) begin // @[src/main/scala/xoshiro128.scala 19:23]
      s_2 <= io_seed[95:64]; // @[src/main/scala/xoshiro128.scala 21:18]
    end else begin
      s_2 <= _s_2_T_1; // @[src/main/scala/xoshiro128.scala 16:10]
    end
    if (io_seedwrite) begin // @[src/main/scala/xoshiro128.scala 19:23]
      s_3 <= io_seed[127:96]; // @[src/main/scala/xoshiro128.scala 21:18]
    end else begin
      s_3 <= _s_3_T_3; // @[src/main/scala/xoshiro128.scala 17:10]
    end
  end
endmodule

このVerilogファイルを使ってテストをしていくことを考えることとします.

C++テストベンチの用意

Verilogファイルを用意したので次はテストベンチを用意しましょう.
xoshiro128**には公式のC実装があるのでこれを参照実装としてテストを行います. 最初に全体のコードを示します. このファイルはcpptest.cppとして保存されているとします.

#include <verilated.h>
#include <verilated_fst_c.h>
#include <Vxoshiro128.h>
#include<iostream>
#include<random>

//----
/*  Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org)

To the extent possible under law, the author has dedicated all copyright
and related and neighboring rights to this software to the public domain
worldwide. This software is distributed without any warranty.

See <http://creativecommons.org/publicdomain/zero/1.0/>. */

#include <stdint.h>

/* This is xoshiro128** 1.1, one of our 32-bit all-purpose, rock-solid
   generators. It has excellent speed, a state size (128 bits) that is
   large enough for mild parallelism, and it passes all tests we are aware
   of.

   Note that version 1.0 had mistakenly s[0] instead of s[1] as state
   word passed to the scrambler.

   For generating just single-precision (i.e., 32-bit) floating-point
   numbers, xoshiro128+ is even faster.

   The state must be seeded so that it is not everywhere zero. */


static inline uint32_t rotl(const uint32_t x, int k) {
	return (x << k) | (x >> (32 - k));
}


static uint32_t s[4];

uint32_t next(void) {
	const uint32_t result = rotl(s[1] * 5, 7) * 9;

	const uint32_t t = s[1] << 9;

	s[2] ^= s[0];
	s[3] ^= s[1];
	s[1] ^= s[2];
	s[0] ^= s[3];

	s[2] ^= t;

	s[3] = rotl(s[3], 11);

	return result;
}
//----

void clock(Vxoshiro128 *dut, VerilatedFstC* tfp){
  static uint time_counter = 0;
  dut->eval();
  tfp->dump(1000*time_counter);
  time_counter++;
  dut->clock = !dut->clock;
  dut->eval();
  tfp->dump(1000*time_counter);
  time_counter++;
  dut->clock = !dut->clock;
}

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

    //generatros
    std::random_device seed_gen;
    std::default_random_engine engine(seed_gen());
    std::uniform_int_distribution<uint32_t> seeddist(0, std::numeric_limits<uint32_t>::max());
    for(int i = 0; i < 4; i++) s[i] = seeddist(engine);

    Verilated::commandArgs(argc, argv);
    Vxoshiro128 *dut = new Vxoshiro128();
    Verilated::traceEverOn(true);
    VerilatedFstC* tfp = new VerilatedFstC;
    dut->trace(tfp, 100);  // Trace 100 levels of hierarchy
    tfp->open("simx.fst");

    // Format
    dut->reset = 1;
    dut->clock = 0;
    dut->io_seedwrite = 0;

    // Reset
    clock(dut, tfp);

    //Release reset
    dut->reset = 0;

    for(int i = 0; i < 4; i++) dut->io_seed[i] = s[i];
    dut->io_seedwrite = 1;
    clock(dut, tfp);
    dut->io_seedwrite = 0;

    for(int test = 0; test < 100; test++){
      std::cout<<std::hex<<s[0]<<":"<<s[1]<<":"<<s[2]<<":"<<s[3]<<std::endl;;
      const uint32_t res = next();
      if(dut->io_out != res){
        std::cout<<test<<":"<<":"<<dut->io_out<<":"<<res<<std::endl;
        dut->final();
        tfp->close();
        exit(1);
      }
      clock(dut, tfp);
    }
    std::cout<<"PASS"<<std::endl;
}

ヘッダファイル

まず理解しておくべきはincludeすべきヘッダーファイルですが, 御覧の通りここではverilated.h, verilated_fst_c.hVxoshiro128.hというヘッダーが読み込まれています.

#include <verilated.h>
#include <verilated_fst_c.h>
#include <Vxoshiro128.h>

verilated.hはverilatorを利用するには必ず読み込む必要があるヘッダーファイルです.verilated_fst_c.hはFSTファイル形式で波形データを書き出す際に必要なヘッダファイルです. Verilatorでは出力としてFSTとVCDをサポートしていますが, FSTのほうがバイナリ形式で圧縮されているため, 仕様ツールの互換性として問題がなければFSTを使うと良いでしょう. どちらを利用するかはCMakeList.txtでverilatorにオプションとして渡すので後ほど指定方法を説明します. Vxoshiro128.hはverilatorが生成するヘッダーファイルで, 名前は与えるVerilogファイルに応じて変動します. 基本的には与えたファイル名の先頭にVを付けた名前になります.

初期設定

Verilatorでテストを行うには必ずこんな感じの初期設定が必要です. 念のためですが, dutはDevice Under Testの略です.

    Verilated::commandArgs(argc, argv);
    Vxoshiro128 *dut = new Vxoshiro128();
    Verilated::traceEverOn(true);
    VerilatedFstC* tfp = new VerilatedFstC;
    dut->trace(tfp, 100);  // Trace 100 levels of hierarchy
    tfp->open("simx.fst");

最初の行は見ての通りコマンドライン引数が渡せるのですが, この機能は私は使ったことがないので何ができるのかは知りません. 2行目は実際にverilatorが生成した与えたVerilogと等価なシミュレータを実体化しています. 3行目以降は必須ではないのですが, 波形を生成するための設定です. tfpが何の略かははっきりわかってないのですがたぶんTrace File Pointerじゃないでしょうか. 3行目では波形をtraceするかどうかのオプションをtrueにしています. 4行目では波形を書き込むファイル関連のオプションを指定するオブジェクトを実体化しています. ここではFSTを使っていますが, VCDを使う場合は4行目以降のFSTの文字列をVCDに変えれば大体動きます. 5行目はtop moduleから100階層潜った分までを出力する設定です. たぶん今回のデザインだと100もないですしここの数字は実際の使用用途に合わせたお好みで設定するといいと思います. 最後の行はsimx.fstというファイルに書き込めという設定になっています. simxはSimulation eXecutionの略で適当に私がつけただけです.

Wireの値の指定

初期化しているところを取ってきますが, 32bit以下の値であればこんな感じでメンバで値を書き込むことができます.

    // Format
    dut->reset = 1;
    dut->clock = 0;
    dut->io_seedwrite = 0;

Verilogファイルを見ると今回対象にしているモジュールはio_seedが128bit幅になっています. このような32bitを超える信号は32bitずつに分割された配列になってアクセスできます.

    for(int i = 0; i < 4; i++) dut->io_seed[i] = s[i];

Clockの評価について

Verilatorではevalを呼ぶと組み合わせ回路としての評価が行われるという形なので, clockを1周期評価するにはクロックを反転して評価するのを2回する必要があります. よく行われる操作なのでここでは関数を切り出しています. 波形を書き出すには明示的にdump関数を呼ぶ必要があるので, それもここで処理しています. カウンタを1000倍しているのは気分です. そういえば過去にここのカウンタをがあふれて大量のエラーメッセージがでたことがあるのでやらないほうがいいかもしれません.

void clock(Vxoshiro128 *dut, VerilatedFstC* tfp){
  static uint time_counter = 0;
  dut->eval();
  tfp->dump(1000*time_counter);
  time_counter++;
  dut->clock = !dut->clock;
  dut->eval();
  tfp->dump(1000*time_counter);
  time_counter++;
  dut->clock = !dut->clock;
}

CMakeLists.txtの用意

さてやっと本題のCMakeの話をします. 以下にCMakeLists.txtを示します.

project(xoshiro-test CXX C)
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "-march=native -O3")
find_package(verilator)
add_executable(cpptest cpptest.cpp)
verilate(cpptest SOURCES xoshiro128.v THREADS 2 TRACE_FST)

見ての通り重要なのは最後の3行だけですね.
find_packageはまぁ必要なのはそれはそうだというのは分かると思います. 公式のdocumentどおりにインストールしていればちゃんと見つけられるはずです.
最後から2行目はCMakeの一般的記法ですが, 最後のverialteで参照するのでそれよりは先に必要です.
重要なのは最後のverilateです. ここでは最低限の例として3つのオプションを指定しています.

SOURCESではVerilogソースファイルを指定します. トップモジュールになるファイルを指定するので, 名前を使ってトップモジュールが解決されます. もしファイル名と違うトップモジュールを指定したい場合はTOP_MODULEオプションを使って指定します.

THREADSではシミュレーション実行時の使用スレッド数を指定します. これは回路をパーティションする操作を含むため, 多すぎるとエラーが出ます.

TRACE_FSTはFSTファイルで波形をダンプすることを指定します. ここをTRACE_VCDにするとVCDファイルで波形を出すことができます.

もしCMakeとしてwrapされていないオプションが使いたい場合はVERILATOR_ARGSを通して直接指定することも可能です.

最後に

過去の自分ならうれしいけれど改めて書くと思ったより当たり前では?ってなるいつものやつになっていますが, ちょっとでも役に立つと幸いです.

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?