HOME

@pgate1
投稿日 2021年12月10日 更新日 2021年12月10日

Verilator と Visual Studio で検証したい

Merry Xmas! HDL Advent Calender 2021 10日目だね。

本記事では、Verilator と Microsoft Visual Studio C++(MSVC)での検証環境をご紹介。

はじめに

VerilogHDL / SystemVerilog の検証環境として、 Verilator&C++コンパイラの環境も公知になってきた。 特に個人で無償使用できる検証環境としては実行速度最速かと (ただし回路規模が大きいとコンパイル時間が長くなるけど)。 FPGA向け開発では検証を素早く行いたいし、信号の観測性の良さはデバグの肝だからね。

参考: FPGA開発日記:Verilatorの使い方(1. Verilatorの考え方と基本的なシミュレーション実行方法)

Verilatorから生成されたC++コード(Verilatedコード)をコンパイルするのに、 多くはGCCやClangが使用される。 ただ、まれに Visual Studio を使う人もおるじゃろ? おらんか、いるとするじゃろ? 泣くなよ、やめよっかこの話
やめないけど。
…で、個人的には Visual Studio で作ったリファレンスモデルに、 検証対象のHDLデザインを組み込んで検証したいケースがあるわけ。 そこで、Visual Studio でVerilatedコードを使うためのちょっとしたコツをここに記す。

例として、SNES on FPGA でやったスーファミ互換自作PPU(Picture Processing Unit)単体の検証例を示す。

環境

VerilatorでC++を生成

Verilator v4.216のSource code (tar.gz)をダウンロードし、Cygwinにインストール。
$ tar zxvf verilator-4.216.tar.gz
$ cd verilator-4.216
$ autoconf
$ ./configure
$ make -j `nproc`
$ make test
$ make install
$ verilator --version
Verilator 4.216 2021-12-05 rev UNKNOWN.REV
検証対象をVerilogHDLからC++に変換。
$ verilator --cc ppu_top.v --compiler msvc --public --l2-name v
--compiler msvc
MSVC用のチューニングでC++コードを生成する。

--public
全内部信号をトップに引き出しpublic指定で宣言する。 テストベンチコードから容易に読み書き可能になるが、 場合によってはクロックが誤ってシミュレートされる可能性があるらしい。 これはお好みで。

--l2-name <value>
信号名の先頭文字列として<value>を使用する。これもお好みで。

参考:verilator Arguments

Visual Studio でコンパイル

C++空のプロジェクトを作成し、 プロジェクトの[ソースファイル]に[既存の項目を追加]として以下を追加。
プロジェクトのプロパティで [VC++ディレクトリ]→[インクルードディレクトリ]にVerilatorのincludeディレクトリを追加。

エラー回避

このままコンパイルしても怒られる。

C:\cygwin64\usr\local\share\verilator\include\verilatedos.h(230,1): fatal error C1189: #error:  "Verilator requires a C++11 or newer compiler"

これは文字通り、VerilatorがC++11以上を要求しているため。
verilatedos.h
:
#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__) || defined(VL_CPPCHECK)
:
#else
# error "Verilator requires a C++11 or newer compiler"
#endif
:

しかし Visual Studio の既定では __cplusplus が 199711L となっている。 そこで、プロジェクトのプロパティ [C/C++]→[コマンドライン]の[追加のオプション]に/Zc:__cplusplusを追加。
これで __cplusplus が 201402L になりコンパイルが通る。

参考:/Zc:__cplusplus (更新された __cplusplus マクロの有効化)

テストベンチについて

トップモジュールの入出力信号はVppu_topのメンバ変数としてアクセスできる。 それ以外の内部信号は、--publicで生成されたVppu_top___024root.hにメンバ変数としてpublic指定で宣言されており、 トップモジュールからはrootp経由でアクセスできる。

参考:Connecting to Verilated Models

あとは基本的なVerilator向けのテストベンチコードなので一部省略して貼っておく。 動作としては、リファレンスモデルで取得しておいたPPUレジスタのスナップショットを読み込み、 VerilatedしたPPUの各レジスタやメモリに設定する。 シミュレーションを実行し、描画されたピクセルデータを画像として保存し、表示して確認する。
ppu_sim.cpp
#include "Vppu_top.h"
#include "Vppu_top___024root.h" // publicオプションで生成される

int main()
{
    Vppu_top* ppu_top = new Vppu_top();
    Vppu_top___024root* ppu = ppu_top->rootp; // 内部信号へアクセスしやすいように

    // リセット
    ppu_top->p_reset = 1;
    ppu_top->eval();
    ppu_top->m_clock = 1;
    ppu_top->eval();
    ppu_top->p_reset = 0;
    ppu_top->eval();
    ppu_top->m_clock = 0;
    ppu_top->eval();

    // PPUスナップショットデータの読み込み
    FILE* ifp = fopen("../snapshot/rockman.dat", "rb");
    unsigned char* buf = new unsigned char[0x20000];

    while (!feof(ifp)) {
        fread(buf, 1, 11, ifp);

        // レジスタの値を設定
        if (strncmp((char*)buf, "PPU:", 4) == 0) {
            fread(buf, 1, atoi((char*)buf + 4), ifp);
            ppu->v__DOT__ppu1__DOT__bg_mode = buf[0];
            ppu->v__DOT__ppu1__DOT__f_bg3_pri = (buf[0]==1) & buf[1];
            ppu->v__DOT__ppu1__DOT__n_brightness = buf[2];
            :
            省略
            :
        }
        // VRAMの値を設定
        if (strncmp((char*)buf, "VRA:", 4) == 0) {
            unsigned short* vram = new unsigned short[32768];
            fread(vram, 2, atoi((char*)buf + 4) >> 1, ifp);
            for (int i = 0; i < 32768; i++) {
                ppu->v__DOT__vramA__DOT__cells[i] = vram[i] & 0xFF;
                ppu->v__DOT__vramB__DOT__cells[i] = (vram[i] >> 8) & 0xFF;
            }
            delete[] vram;
        }
    }

    fclose(ifp);
    delete[] buf;
    printf("read snapshot ok.\n");

    ppu_top->eval();

    unsigned char** image = new unsigned char*[480];
    for (int i = 0; i < 480; i++) image[i] = new unsigned char[512 * 3];

    // シミュレーション実行
    for (int x = 0, y = 0;;) {
        ppu_top->m_clock = 0;
        ppu_top->eval();
        ppu_top->m_clock = 1;
        ppu_top->eval();

        // 描画結果の取得
        if(ppu->v__DOT__ppu2__DOT__scanline >= 0 && 
           ppu->v__DOT__ppu2__DOT__scanline < 240 && 
           ppu_top->VIDEO_enable)
        {
            image[y][x * 3    ] = ppu_top->cR << 3;
            image[y][x * 3 + 1] = ppu_top->cG << 3;
            image[y][x * 3 + 2] = ppu_top->cB << 3;

            x++;
            if (x == 512) {
                x = 0;
                memcpy(image[y + 1], image[y], 512 * 3);
                y += 2;
                if (y == 480) break;
            }
        }
    }

    ppu_top->final();
    delete ppu_top;

    // 描画結果をPPMフォーマットで出力
    FILE* ofp = fopen("out.ppm", "w");
    fprintf(ofp, "P3\n512 480 255\n");
    for (int y = 0; y < 480; y++) {
        for (int x = 0; x < 512 * 3; x += 3) {
            fprintf(ofp, "%d %d %d\n", image[y][x], image[y][x+1], image[y][x+2]);
        }
    }
    fclose(ofp);

    for (int i = 0; i < 480; i++) delete[] image[i];
    delete[] image;

    // PPMの表示(あらかじめ画像ビューワに関連付けしておく)
    system("cmd /c out.ppm");

    return 0;
}

実行すると描画結果が出る。かっこいい。

windows.hを使う場合

verilated_types.h の VlURNG min() が競合するようなので、 以下のようにverilatorヘッダファイルを先にincludeすること。
includeの例
#include "VR3000A.h"
#include "VR3000A___024root.h"

#include<windows.h>
#include<windowsx.h>

コンパイル時間の短縮

Visual Studio の基本的な使い方になるけど、 [インクリメンタルビルドの有効化]はもちろん、 [複数プロセッサによるコンパイル有効化]でコンパイル時間を Debugビルドで約70%短縮、Releaseビルドで約30%短縮できる。

あと、Verilatorの使い方として本来なら、 アクセスしたい信号のみHDL記述中に/* verilator public */ を追記しpublic指定するのが速度を落とさない作法のようだ。 ただ、私の場合は検証対象のVerilogHDLに手を加えないスタイルの環境 (VerilogHDLは中間言語とみなしSFLから生成)でもあるので、 --publicですべての信号を引き出している。 わざわざアクセスしたい信号を選別するのも面倒だし、 テストベンチからアクセスしない変数はコンパイルで上手いこと処理されるのを期待して。

WSL2 と Visual Studio 2022 で試してみる

もうWSL2もあるし Visual Studio 2022 もリリースされたし、 Visual Studio 2022 ならWSL2内のC++コードにそのままアクセスできるかもしれないので試してみた。

WSL2でのVerilatorのコンパイルと、VerilogHDLからC++への変換はもちろん問題ない。 しかし Visual Studio 2022 でのコンパイルは、 なぜか標準Cライブラリ(x86)へのパスが通ってなくてあきらめた。 あとWSL2内のC++コードにアクセスするのにも別途設定が必要のようだ。 またそのうち試してみよう。

おわりに

Verilatedコードを Visual Studio でコンパイル&ビルドするコツを記した。

今回まとめて説明してしまっているけど、 --publicは Visual Studio によらずGCC向けでももちろん有効なオプションだし、 今回のPPU描画テスト程度なら WSL2 & VS Code 環境でもなんら問題ない。

Visual Studio を使う訴求力を求めるなら、Windowsアプリとして作ったリファレンスに、 HDLのCPUをVerilatedして組み込んで検証するケースなら少しは分かってもらえるかな… ありえんか、やめよっかこの話。


©2021 pgate1