はじめに
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.h
とVxoshiro128.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
を通して直接指定することも可能です.
最後に
過去の自分ならうれしいけれど改めて書くと思ったより当たり前では?ってなるいつものやつになっていますが, ちょっとでも役に立つと幸いです.