Ryuz's tech blog

FPGAなどの技術ブログ

C++の動作モデル書いてみた(主にVerilator用)

はじめに

前回に続くネタです。これ自体は特に Verilator に依存しているわけではなく汎用のC++ライブラリとして記述してるのでいろいろできると思うのですが、ひとまずは Verilator と C++ モデルを接続動作させるのに使っています。

何を作っているのか

作っているもの自体はこちらになります。

要はVerilatorにC++で書いたモデルを接続できる程度のイベントキューを自作してみようという試みです。 実際シミュレーターには詳しくないのでこれであっているのかわかりませんが今のところうまく動いています。

(本物のシミュレーターであれば信号変化に紐づけた追跡もしないとモジュール相互で変数を介してハンドシェークするのは難しいのだと思いますが、Verilatorに一方的にC++をぶら下げるだけであれば時間イベントの管理だけで事足りそうですし、あまり余計な処理を入れてVerilatorの持ち味を殺してしまうともったいないですので安直に行きたいと思います。)

Verilator 自体はイベントキューを持っていませんので、メインループはユーザー側にあります。

なので一旦、Verilator は置いておいて、C++でイベントキューを管理するクラスと、それにいろいろC++で作ったモデルをノードとして登録していける仕組みをつくりました。そこにVerilatorで生成したモジュールをさらに1つのノードとして追加すれば C++ モデルと Verilog モデルが共存して動作できるという発想です。

イメージ図

何ができるのか

各ノードは Nodeクラスから派生して作りますが、

  • 自分のイベントの登録
  • 他イベントを含む、イベント前後での信号監視や値変更の機会提供

が主な機能になります。

Node の基本クラスは

class Node
{
    friend Manager;

protected:
    bool    m_active = false;    // 活性化フラグ

    virtual sim_time_t  InitialProc(Manager* manager) { return 0; }     // シミュレーション開始時に一度呼ばれる
    virtual void        FinalProc(Manager* manager) {}                  // シミュレーション終了時に一度呼ばれる
    virtual sim_time_t  EventProc(Manager* manager) { return 0; }       // イベント処理に呼ばれる
    virtual void        PrefetchProc(Manager* manager) {};              // 値フェッチの為に何かのイベント(他人のイベント含む)前に呼ばれる
    virtual bool        CheckProc(Manager* manager) { return false; }   // 自分に関連する事象変化が起こっていないかのチェックに呼ばれる
    virtual void        DumpProc(Manager* manager) {}                   // 波形ダンプタイミングで呼ぶ(主に Verilator用)
    virtual void        ThreadProc(Manager* manager) {}                 // 別スレッドの処理
};

といった感じですで、 InitialProc() や EventProc() の戻り値で0以外を渡すとその時刻後にイベントが登録されるようにしています。

イメージとしては、

always @(posedge clk)
   c <= a + b;

をやりたいときは、

  • PrefetchProc() で clk, a, b の値を取り込む
  • CheckProc で @posedge clk を監視して該当していれば true を返して活性状態を知らせる
  • 活性化するとEventProc()が呼ばれてくるので事前に取り込んでおいた a と b を足して c に代入する

というような実装にすればいいようにしたつもりです。

どんな感じになるのか

まずはクロックリセットですが

  • 一定時間後に値を一回だけ変えるノード(リセットを生成)
  • 一定時間毎に値を反転させるノード(クロック生成)

を作ればいいわけです。 これは簡単なのですがその際に制御対象の型をテンプレート記述することでVerilatorに依存せずに何でも指定可能になります。今回のクラスはすべてスマートポインタでラッピングしてますが

template<typename Tp>
std::shared_ptr< ResetNode<Tp> > ResetNode_Create(Tp* signal_reset, double time, bool active_high=true)
{
    return ResetNode<Tp>::Create(signal_reset, time, active_high);
}

が ResetNode の生成にかかわる部分でして、型に依存せずに signal_reset に弄りたい値のポインタを渡せるわけです。 これで verilated.h をインクルードすることなく、verilator にも利用可能なクラスが出来上がります。

で、この辺は信号数が多くなると構造体化したくなり、そのときに C++17 が使えると、いちいち明示的に指定しなくても型推論してくれるので便利だったりします。

ビデオ入出力のモデルを作ってみた

これは前回の通りですが、C++ なら OpenCV が使えるので、jpg, png, bmp などなんでも読み書き出来てとても便利です。

AXI4 Stream Video 信号の生成

加えて、C++いろんな乱数モデルを持ってますので、シードを指定したうえで再現性良く乱数を使っていくことも容易ですね。下記の例では AXIバスの valid や ready に乱数ウェイトを入れて、ハンドシェーク系のバグ出しをやりやすくしたものです。

AXI4 の valid/ready に乱数ウェイトを入れる

おわりに

いろいろな制約はあるものの Verilator の素晴らしいまでのシミュレーション速度の速さと、C++でテストベンチを書けるメリットを最大限活かせれば Verilog ライフはまだまだ便利になるんじゃないかなと感じた次第です。 あと、「C++でテストベンチを書ける」と書いてますが、当然 Verilog 側にも書くことができます。特に task なんかは C++ で書こうとするととても面倒なので(将来のC++20とかでコルーチンが言語仕様に入ってくると扱いやすそう)、C++Verilogの良いところ取りでテストベンチが作れるといいのではないかと思いました。