FPGA開発日記

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

TileLinkの勉強 (3. Chipyardのリファレンスを読む)

前回の続き。

Chipyardのリファレンスは比較的詳しく書いてあると思うので、この資料を読みながらDiplomacyの勉強をしていこうと思う。

chipyard.readthedocs.io

Diplomacyコネクタ

Diplomacyのグラフは互いにエッジを使って接続されている。Diplomacyのライブラリは4つの演算子を使ってノードを接続している。

  • :=

基本的な演算子である。Chiselの単方向の接続コネクタと同じ記法であるが、意味が異なる。この演算子はDiplomacyノードを接続するための演算子であり、バンドルを接続する演算子ではない。

基本的な接続演算子は、2つのノードを接続するための1つのエッジを生成する。

  • :=*

「クエリ」タイプの接続演算子である。ノード間の複数のエッジにおいて、クライアントノード(演算子の右側)によりエッジの数が決められている場合に、エッジの接続する。

  • :*=

「スター」タイプの接続演算子である。複数のエッジを作成するが、エッジの数はクライアントではなくマネージャ(演算子の左側)により決めらる。複数エッジのマネージャノードに対してネクサスノードを接続する場合に有益である。

  • :*=*

「フレックス」タイプの接続演算子である。 演算子のどちらの側に既知の数のエッジがあるかに基づいて、複数のエッジを作成する。 これは、どちらかの側のノードのタイプが実行時までわからないジェネレーターで使用できる。

RISC-Vのベクトル拡張命令の仕様書日本語版を最新仕様v0.8にアップデートした

結構昔にRISC-V Vector Extensionの仕様を日本語化したものを公開していたのだが、公開して移行全くアップデートしていなかったので最新仕様に対応すべく修正を行った。

前回の公開時の記事はこちら。v.0.7.1をベースに執筆している。

msyksphinz.hatenablog.com

今回は最新仕様のv0.8に基づいて修正を行っている。 RISC-V Vector Extensionの仕様書のタグv0.8を参照しながら執筆を行ったので、中途半端な修正ではなくちゃんと正式リリースに対応しているはずだ。

riscv.github.io

そしてこちらが修正を実施した日本語版。

  • RISC-V “V” Extension 日本語版

RISC-V “V” Extension — riscv-v-spec-japanese ドキュメント

あらかじめ断っておきますが、あくまで趣味で翻訳しているものなので間違いや誤訳はかなり含まれていると思います。 これに関してはDog Foodingというか、自分が使うことになったら修正をかけていくと思う。Pull Requestも大歓迎だけれども。

v0.8での変更点

せっかくなので修正点を要約する。v.0.7.1からv0.8において、主に以下の点において修正が加わっている。

  • vlenbレジスタの導入
  • VL以降のベクトル要素についてはゼロ書き込みの仕様であったものが、「値の変更なし」に変えられている。
  • 全体ベクトルレジスタ・ベクトルロードストア命令が新規追加となる。
  • 4倍ビット幅整数乗算加算命令が新規追加となっている。
  • ビット幅拡張飽和付き累積乗算加算命令が全面削除
  • vfredosum命令の導入。

テール要素の扱いについて

特に大きめの修正点としては「VL以降のベクトル要素についてはゼロ書き込みの仕様であったものが、「値の変更なし」に変えられている。」つまりテール要素に対する書き込みのポリシーに変更が加わっている。

これに関してはかなり議論が行われていたようで、以下の示すような議論の履歴を残すメモも公開されていた。

ただし図が全くないので理解が難しいのだが、自分でいろいろ図を書いて調べてみた。主にレジスタリネーミングのポリシをどのように考えるかというところで実装のしやすさが変わる、というところらしい。

  • ベクトルのテール要素(つまり演算対象ではないベクトルの後ろの方)のアップデートを行わない場合(v0.8の仕様)

この場合、まず空間的にベクトルレジスタが配置されている場合(つまりSIMDみたいな感じかしら)、テール要素をどのように扱おうがオーバヘッドは発生しない。同時に演算が実行されるだけである。

f:id:msyksphinz:20200111232523p:plain:w300

ただしこれが時間的なベクトルレジスタ配置(つまり、先頭のベクトルから順番に実行していく場合)の場合、テール要素は無視するだけでなく、リネーム元のレジスタの内容をちゃんとコピーしないと「値の保持」にならない。このためのオーバヘッドが発生する。これがベクトルの長さが大きくなるとバカにできなくなる。

f:id:msyksphinz:20200111232945p:plain:w300

ただしやり方としてはいろいろあって、例えば長いベクトルレジスタをいくつかに分割して細かくリネーミングすることで、例えばテール要素はリネームせずに前のレジスタIDを保持することで「値の保持」が実現できるようになる。ただしこの方式の弱点は、リネームテーブルを余計に消費してしまうことである。

f:id:msyksphinz:20200111233012p:plain
テール要素「値保持」の場合、テール要素のグループのみ「リネームしない」ことによりオーバヘッドを削減する。
  • ベクトルのテール要素(つまり演算対象ではないベクトルの後ろの方)をすべてゼロで埋める場合(v0.7.1の仕様)

ベクトルのテールをすべて0で埋める場合には、「ゼロを書き込む」という操作が必要になり結局オーバヘッドが発生する。

f:id:msyksphinz:20200111233251p:plain:w300
テール要素「ゼロ埋め」の場合、テール要素にゼロを書き込むためのコストが発生する。

ただしこれにも解決方法があって、ある程度のグループで「すべての要素がゼロである(というか、テール要素である)」ビットを付加しておき、そのビットが立っている場合には演算を省略するという方式がある。

f:id:msyksphinz:20200111233446p:plain:w400
ゼロ要素ビットが1になっている場合、そのベクトルレジスタグループは演算を省略する。

さらに、リネームをするタイプの演算であれば、やはりベクトルレジスタグループを細かく分割して、テール要素が含まれているベクトルレジスタグループは常に定数ゼロレジスタにリネームするようにしておけば、コストを省略できる。

f:id:msyksphinz:20200111233628p:plain
ゼロ埋め方式でリネームする場合、テール要素は常にゼロベクトルにリネームすることでコストを削減する。

TileLinkの勉強 (2. Chipyardのリファレンスを読む)

前回の続き。

Chipyardのリファレンスは比較的詳しく書いてあると思うので、この資料を読みながらDiplomacyの勉強をしていこうと思う。

chipyard.readthedocs.io

9.1.4. 識別ノード

f:id:msyksphinz:20191221225444p:plain
識別ノード

これまでのノードは入力もしくは出力が定義されていたが、識別ノードは両方を持っている。名前が示しているように、入力と出力を単純に接続するだけである。このノードは複数のノードを複数のエッジをもつ単一のノードに接続するために使用される。例えば、2つのクライアントLazyモジュールがあったとして、これを1つのクライアントノードに接続している。

class MyClient1(implicit p: Parameters) extends LazyModule {
  val node = TLHelper.makeClientNode("my-client1", IdRange(0, 1))

  lazy val module = new LazyModuleImp(this) {
    // ...
  }
}

class MyClient2(implicit p: Parameters) extends LazyModule {
  val node = TLHelper.makeClientNode("my-client2", IdRange(0, 1))

  lazy val module = new LazyModuleImp(this) {
    // ...
  }
}

2つの異なるLazyモジュールをインスタンスして、この2つのノードを1つのノードに見せている。

class MyClientGroup(implicit p: Parameters) extends LazyModule {
  val client1 = LazyModule(new MyClient1)
  val client2 = LazyModule(new MyClient2)
  val node = TLIdentityNode()

  node := client1.node
  node := client2.node

  lazy val module = new LazyModuleImp(this) {
    // Nothing to do here
  }
}

マネージャに対しても同様の処理が可能である。

class MyManager1(beatBytes: Int)(implicit p: Parameters) extends LazyModule {
  val node = TLHelper.makeManagerNode(beatBytes, TLManagerParameters(
    address = Seq(AddressSet(0x0, 0xfff))))

  lazy val module = new LazyModuleImp(this) {
    // ...
  }
}

class MyManager2(beatBytes: Int)(implicit p: Parameters) extends LazyModule {
  val node = TLHelper.makeManagerNode(beatBytes, TLManagerParameters(
    address = Seq(AddressSet(0x1000, 0xfff))))

  lazy val module = new LazyModuleImp(this) {
    // ...
  }
}

class MyManagerGroup(beatBytes: Int)(implicit p: Parameters) extends LazyModule {
  val man1 = LazyModule(new MyManager1(beatBytes))
  val man2 = LazyModule(new MyManager2(beatBytes))
  val node = TLIdentityNode()

  man1.node := node
  man2.node := node

  lazy val module = new LazyModuleImp(this) {
    // Nothing to do here
  }
}

クライアントとマネージャのグループを接続したい場合、以下のように接続できる。

class MyClientManagerComplex(implicit p: Parameters) extends LazyModule {
  val client = LazyModule(new MyClientGroup)
  val manager = LazyModule(new MyManagerGroup(8))

  manager.node :=* client.node

  lazy val module = new LazyModuleImp(this) {
    // Nothing to do here
  }
}

:=*演算子の意味についてはDiplomacy コネクタの章を確認してほしい。つまり、複数のエッジを持つノードをたがいに接続したのである。識別ノードのエッジは順番に接続され、従ってこの場合はclient1.nodemanager1.nodeに接続され、client2.nodemanager2.nodeに接続される。

識別ノードの入力ノードと出力ノードの数は一致していなければならない。そうでなければ、エラボレーション時にエラーが発生する。

9.1.5. アダプタノード

f:id:msyksphinz:20191221225509p:plain
アダプタノード

識別ノードと同様に、アダプタノードは複数の入力を受け付け、同じだけの出力を生成する。しかし識別ノードと異なるのはアダプタノードは単純にノードを接続するわけではない。入力と出力の論理と物理的なインタフェースを変更し、メッセージを変更することができる。RocketChipはアダプタのライブラリをDiplomaticウィジェットで提供している。

アダプタノードを自分で作成する必要性はあまりないが、必要であれば以下のようにして作成できる。

val node = TLAdapterNode(
  clientFn = { cp =>
    // ..
  },
  managerFn = { mp =>
    // ..
  })

clientFn は入力のTLClientPortParameters を受け付ける関数であり、出力のための該当するパラメータを生成する。managerFn は出力のTLManagerPortParameters を受け付ける関数であり、入力のための該当するパラメータを生成する。

9.1.6. ネクサスノード

f:id:msyksphinz:20191221225530p:plain
ネクサスノード

ネクサスノードはアダプタノードと似ているが出力インタフェースが異なる。しかし同様に出力ノードの数と入力ノードの数は異なっていても良い。このノードタイプはTileLinkのクロスバージェネレータを提供しているTLXbarウィジェットに使用される。このノードを手動で作成する必要性はあまりないが、必要に応じて以下のように生成できる。

val node = TLNexusNode(
  clientFn = { seq =>
    // ..
  },
  managerFn = { seq =>
    // ..
  })

アダプタノードのコンストラクタと似ているが、単一のパラメータオブジェクトを渡して単一の結果を返すのではなく、複数のパラメータを受け取る関数を取る。さらに、入力シーケンスの数と出力シーケンスの数は同一でなくても良い。

TileLinkの勉強 (1. Chipyardのリファレンスを読む)

SiFive社のCPUおよびRocketChipには、バスとしてTileLinkおよびDiplomacyが使用されている。

TileLinkはバスプロトコルなので良いとして、Diplomacyの理解は非常に難解だ。私もまだ完全に理解できていない。

Chipyardのリファレンスは比較的詳しく書いてあると思うので、この資料を読みながらDiplomacyの勉強をしていこうと思う。

chipyard.readthedocs.io

9. TileLinkとDiplomacyリファレンス

TileLinkはRocketChipおよび他のChipyardジェネレータで使用されているキャッシュコヒーレンスとメモリプロトコルである。これらは、キャッシュ、メモリ、周辺機器、DMAデバイスなどの異なるモジュールとどのように互いに通信を行うかを規定している。

RocketChipのTilelinkの実装はDiplomacyとよばれる、Chiselジェネレータ内で構成情報を交換して行われる2段階のエラボレーション方式を使用している。Diplomacyの詳細に関しては、Cook, Terpstra, Leeの論文を参考にすること。

シンプルなTileLinkのウィジェットを追加する方法の概要については、アクセラレータデバイスを追加するセクションを参考にすること。このセクションでは、RocketChipにより提供されているTilelinkおよびDiplomacyの機能について説明する。

TileLink 1.7プロトコルの詳細な仕様についてはSiFiveのウェブサイトを参考にすること。

9.1. TileLinkのノードタイプ

DiplomacyはSoC内の異なるコンポーネントをDirected Acyclic Graphのノードとして表現する。TileLinkのノードは異なるタイプを持つことができる。

9.1.1. クライアントノード

f:id:msyksphinz:20191221224954p:plain
クライアントノード

TileLinkのクライアントはTileLinkのトランザクションを、まずはAチャネルを通じてリクエストを発行し、Dチャネルを通じてレスポンスを受け取る。クライアントの種類がTL-Cであれば、ProbeリクエストをBチャネルを通じて受け取り、ReleaseコマンドをCチャネルを通じて発行し、Eチャネルを通じてGrantメッセージを発行する。

RocketChip/ChipyardのL1キャッシュ及びDMAデバイスはクライアントノードである。

TileLinkクライアントを追加するためには、testchippipから、LazyModuleにTLHelperオブジェクトを以下のように追加することで実現できる:

class MyClient(implicit p: Parameters) extends LazyModule {
  val node = TLHelper.makeClientNode(TLClientParameters(
    name = "my-client",
    sourceId = IdRange(0, 4),
    requestFifo = true,
    visibility = Seq(AddressSet(0x10000, 0xffff))))

  lazy val module = new LazyModuleImp(this) {
    val (tl, edge) = node.out(0)

    // 残りのコードをここに追加する。
  }
}

name引数はDiplomacyグラフの中でのノードの名前を識別する。この引数はTLClientParametersの中で唯一必須の引数である。

SourceI引数はこのクライアントが使用するソースIDの範囲を指定する。ここでは[0, 4)までのIDを使用しているので、このクライアントは同時に4つまでのリクエストをインフライトに発行することができる。各リクエストはsourceフィールドにおいてそれぞれ異なる値を設定される。このフィールドのデフォルト値はIdRange(0, 1)であり、これは単一のリクエストのみがインフライトすることができることを意味する。

requestFifo引数はBooleanのオプションでありデフォルトではfalseである。この引数をtrueに設定すると、クライアントノードはダウンストリームのマネージャに対してレスポンスをFIFOの順序で送ることを要求する(つまり、当該リクエストが送信されるのと同じ順番にリクエストが返ってくることを期待する)

visibility 引数はクライアントがアクセスするアドレス領域を指定する。デフォルトの値はすべてのアドレスを含んでいる。例えば、1つのアドレス領域AddressSet(0x10000, 0xffff)を指定した場合、クライアントは0x10000から0x1fffffまでの領域のみアクセスできる。通常はこの設定は使用しないが、ダウンストリームのクロスバージェネレータがハードウェアを最適化するために使用することができる。つまり、クライアントとマネージャのアービトレーションにおいて、visibilityが重ならないアドレス領域におけるクロスバーの最適化を実施することができる。

Lazyモジュールの実装において、node.outという表記はbundle/edgeのペアのリストを取得するために使用する。TLHelperを使用しているならば、単一のクライアントエッジのみが指定され、単一のペアのみが返される。

tlバンドルはChiselのハードウェアバンドルであり、このモジュールのIOポートを接続する。このバンドルにはTL-ULおよびTL-UHの場合2種類のTileLinkチャネルのDecoupleバンドルが含まれており、TL-Cの場合は5種類である。ここまでで、TileLinkにメッセージを送受信するために必要なハードウェアを接続するための準備が整った。

edgeオブジェクトには、Diplomacyグラフのエッジが示されている。エッジにはTileLinkのエッジオブジェクトメソッドに示された様々な便利な関数が含まれている。

9.1.2. マネージャノード

TileLinkのマネージャはクライアントのAチャネルからリクエストを受け取り、Dチャネルにレスポンスを返す。マネージャノードはTLHelperを使用して以下のようにして作成できる。

f:id:msyksphinz:20191221225033p:plain
マネージャノード
class MyManager(implicit p: Parameters) extends LazyModule {
  val device = new SimpleDevice("my-device", Seq("tutorial,my-device0"))
  val beatBytes = 8
  val node = TLHelper.makeManagerNode(beatBytes, TLManagerParameters(
    address = Seq(AddressSet(0x20000, 0xfff)),
    resources = device.reg,
    regionType = RegionType.UNCACHED,
    executable = true,
    supportsArithmetic = TransferSizes(1, beatBytes),
    supportsLogical = TransferSizes(1, beatBytes),
    supportsGet = TransferSizes(1, beatBytes),
    supportsPutFull = TransferSizes(1, beatBytes),
    supportsPutPartial = TransferSizes(1, beatBytes),
    supportsHint = TransferSizes(1, beatBytes),
    fifoId = Some(0)))

  lazy val module = new LazyModuleImp(this) {
    val (tl, edge) = node.in(0)
  }
}

The makeManagerNode method takes two arguments. The first is beatBytes, which is the physical width of the TileLink interface in bytes. The second is a TLManagerParameters object.

makeManagerNode メソッドは2つの引数を取る。1つ目はbeatBytesであり、これはTileLinkインタフェースの物理的な幅をバイト単位で返す。2番目の引数はTLManagerParametersオブジェクトである。

TLManagerParametersにおける必須の引数はaddressのみであり、これはマネージャが管理するアドレスの領域を指定する。この情報はクライアントからのリクエストをルーティングするために使用される。この例では、マネージャは0x20000から0x20fffまでの領域からのリクエストを受け取る。AddressSet の2番目の引数はマスクであり、サイズではない。したがってこのアドレス領域の設定は2の累乗の範囲内で行わなければならない。そうでなければ、アドレッシングの設定は設定の意図とは異なるように動作してしまう。

2番目の引数はresourcesであり、Deviceオブジェクトから値を設定する。この場合にはSimpleDeviceを指定している。この引数はエントリをBootROM上に書き込まれるデバイスツリーに追加したい場合には必須である。デバイスツリーの情報はLinuxのドライバにより読み込まれる。SimpleDeviceに設定されている2つの引数は名前およびデバイスツリーのエントリの互換性に関する情報のリストである。このマネージャでは、デバイスツリーの情報は以下のようになる:

L12: my-device@20000 {
    compatible = "tutorial,my-device0";
    reg = <0x20000 0x1000>;
};

次の引数はregionTypeである。この引数はマネージャのキャッシュの動作について示している。この引数は、以下の7つの内どれかの値を設定できる:

  1. CACHED - 中間エージェントはその領域におけるキャッシュされたコピーを保持する。
  2. TRACKED - この領域は別のマスターによりキャッシュされるが、コヒーレンスの機能は提供される。
  3. UNCACHED - この領域はキャッシュされない。しかし可能ならばキャッシュされるべきである。
  4. IDEMOTENT - 最近Putされた内容を返すが、内容はキャッシュされるべきではない。
  5. VOLATILE - 内容はPut操作無しで変更される可能性があるが、PutおよびGetでは副作用は発生しない。
  6. PUT_EFFECTS - Putにより副作用が発生するため、コマンドを複合したり、遅延させるべきではない。
  7. GET_EFFECTS - Getは副作用を発生させるため、投機的に発行させるべきではない。

次はexecutable引数であり、CPUがフェッチ命令をこのマネージャから発行することができるかを示している。デフォルトではfalseであり、MMIOなどの周辺機器では設定すべきである。

次の6つの引数はsupportから始まり、異なるAチャネルのメッセージタイプについて、受付可能かを決めている。メッセージタイプの定義については、TileLinkエッジオブジェクトのメソッドを参照のこと。TransferSizes のケースクラスには、各メッセージタイプについてマネージャが受け付けることのできる論理サイズがバイト単位で指定されている。これは内包的な領域であり、すべての論理サイズは2の累乗である必要がある。したがってこの場合には、マネージャは1, 2, 4, 8バイトのリクエストを受け付けることができる。

最後の引数はfifoId 設定であり、FIFOドメインにマネージャが入っているかを示している。この引数をNoneに設定(デフォルトである)すると、マネージャはレスポンスの順序を保証しない。fifoIdを設定すると、同じfifoId が設定された他のすべてのマネージャと共有され、つまりそのFIFOドメイン内のクライアントからのリクエストはすべて同じ順序で帰ってくることを意味する。

9.1.3. レジスタノード

f:id:msyksphinz:20191221225054p:plain
レジスタノード

マネージャノードを直接指定してTileLinkリクエストを処理する論理をすべて書いたとしても、通常はレジスタノードを使用した方が簡単である。このタイプのノードはregmapメソッドを提供し制御/状態レジスタを自動的に生成しTileLinkプロトコルを処理するための論理も生成する。レジスタノードの使用方法について野依詳細についてはレジスタノードを参照のこと。

「驚異の量子コンピュータ 宇宙最強マシンへの挑戦」を読んだ

量子コンピュータに関する本は久しぶりに読んだ。最近量子コンピュータ関係はあまり手を付けることができていなかったので復習の意味を込めて読了。 読み物として非常に面白かった。 量子コンピュータの発展の歴史と、その基礎的な技術と考え方について初心者にもわかりやすいように説明してある。

f:id:msyksphinz:20200108002622p:plain
f:id:msyksphinz:20200108002732p:plain

量子コンピュータの領域について、非常に幅広く書いてある。ブレイクスルーがどの段階で起きたのか、どのような方式が研究されており、どのような方式がまた主流なのか、など。 基本的な量子コンピュータの知識についても復習になった。エンタングルメントとか完全に忘れていた。各種量子ゲートの演算など、どのような方式があり、どのような量子アルゴリズムが提案されているかなどが復習できた。

また量子アニーリングやD-Wave社やベンチャー企業なのがこぞって量子コンピュータの開発に参戦しているなど、現在の状況を俯瞰されている。

著者の藤井啓祐先生の講演は一度だけ聞きに行ったことがある。確かその講演に触発されて量子コンピュータシミュレータの開発を始めたのだった。 その時の成果物はBitBucketのプライベートリポジトリにお蔵入りになっているのだけれども、またメンテナンスして公開できればいいなと思っている。

XMLとStemsを使ってVerilatorのRTLシミュレーション結果をGtkWaveでアノテーション表示する方法

VerilatorはVerilogコンパイルしてシミュレーションするためのツールだが、波形ログとしてはVCDやFSTなどを出力することができる。VCDはかなり昔から使われている波形保存のためのフォーマット、FSTはVCDと違って圧縮されたより高速アクセスを実現するためのフォーマットだ。

  • VCD : Value Change Dump
    • 波形ファイル。バイナリ圧縮されていないので中身を目視で読むことができる。
  • FST
    • Fast Signal Trace. バイナリ圧縮された波形フォーマット形式。

これに加えて、VerilatorはXMLファイルを出力することができる。XMLファイルにはVerilogの階層構造などの情報が格納されており、Verilogファイルの解析結果などがXML形式で出力される。

$ verilator --version
Verilator 4.008 2018-12-01 rev UNKNOWN_REV
$ verilator --help
        --x-initial-edge            Enable initial X->0 and X->1 edge triggers
        --xml-only                  Create XML parser output
         -y <dir>                   Directory to search for modules

これを使えば、GtkWaveにて波形を表示する際にVerilogファイルとの関係を表示しつつ、現在の信号の値をVerilogファイルにアノテーションしながら表示することができるようだ。やり方を調査してみよう。

  • 使用するVerilator : 4.008 2018-12-01 (後述するが Verilator Version 3系 (3.924 2018-06-12)とはXMLのフォーマットが違うので注意)
  • 使用するGtkWave : v3.3.103

今回は、TileLinkの単体テスト用に使っている環境を使用する。

f:id:msyksphinz:20200107233825p:plain
XMLとStemsを使ったRTLアノテーションのフロー

VerilatorでVerilogの階層構造をXMLで出力する

上記の通り、XML形式のVerilogファイルの階層構造情報を出力したい場合、Verilatorのコンパイルオプションに--xml-onlyを付け加える。

  • Makefile
tilelink-xml: TestHarness.sv verilator_bin
        mkdir -p $(generated_dir_debug)/$(long_name)
        $(VERILATOR) $(VERILATOR_FLAGS) --xml-only -Mdir $(generated_dir_debug)/$(long_name) \
        -o $(abspath $(sim_dir))/$@ $(verilog) $(cppfiles) -LDFLAGS "$(LDFLAGS)" \
        -CFLAGS "-I$(generated_dir_debug) -include $(model_header_debug)"

これでVerilatorによるコンパイル時にXMLファイルが生成される。今回のテスト環境ではgenerated-src-debug/freechips.rocketchip.unittest.TLManyMasterManySlaveTestConfig/VTestHarness.xmlが生成された。

  • generated-src-debug/freechips.rocketchip.unittest.TLManyMasterManySlaveTestConfig/VTestHarness.xml
<?xml version="1.0" ?>
<!-- DESCRIPTION: Verilator output: XML representation of netlist -->
<verilator_xml>
  <files>
    <file id="e" filename="/home/msyksphinz/work/riscv/chisel-development/chisel-hw/TestHarness.sv" language="1800-2017"/>
      ...
  <cells>
    <cell fl="e21582" name="TestHarness" submodname="TestHarness" hier="TestHarness">
      <cell fl="e21590" name="UnitTestSuite" submodname="UnitTestSuite" hier="TestHarness.UnitTestSuite">
        <cell fl="e21517" name="tests_0" submodname="TLOriginalRAMSimpleTest" hier="TestHarness.UnitTestSuite.tests_0">
          <cell fl="e21442" name="SimpleTimer" submodname="SimpleTimer" hier="TestHarness.UnitTestSuite.tests_0.SimpleTimer"/>
...

xml2stemsでstemsファイルに変換する

次にstemsファイルというものを生成する。stemsファイルはGtkWaveでアノテーションを行うためのファイルで、これはXMLファイルから生成する必要がある。これにはxml2stemsというGtkWaveに付属のコマンドを使用するのだが、このコマンドが曲者で、Verilator 4.x系の生成するXMLファイルに対応していない。Verilator 3.x系を使えば問題ないと思うのだが、どうも<module ...>内に指定される要素の数の違いによりxml2stemsが正しくファイルを生成できない様子だった。

とりあえず、GtkWave 3.3.103内のcontrib/xml2stems/xml2stems.ccを改造することで事なきを得た。

diff contrib/xml2stems/xml2stems.cc contrib/xml2stems/xml2stems.cc.orig
76c76
<                                                       if(numqt == 8) break;
---
>                                                       if(numqt == 6) break;
81c81
<                                       if(numqt == 8)
---
>                                       if(numqt == 6)
85c85
<                                                       numqt = 6;
---
>                                                       numqt = 4;
90c90
<                                       if(numqt == 6)
---
>                                       if(numqt == 4)

要するにXMLファイルの要素数が違うのでエラーが発生していたのだが、正直xml2stemsの実装も目に余るひどさなので、XMLファイルを解析するためのライブラリなどを使って書き直したいところだ。

という訳でxml2stemsを使ってgenerated-src-debug/freechips.rocketchip.unittest.TLManyMasterManySlaveTestConfig/VTestHarness.xmlをstemsに変換すると以下のようになる。

xml2stems -V generated-src-debug/freechips.rocketchip.unittest.TLManyMasterManySlaveTestConfig/VTestHarness.xml VTestHarness.stems
  • VTestHarness.stems
++ module TestHarness file /home/msyksphinz/work/riscv/chisel-development/chisel-hw/TestHarness.sv lines 20192 - 20192
++ module TOP file (VerilatorTop) lines 1 - 1
++ comp TestHarness type TestHarness parent TOP
++ comp UnitTestSuite type UnitTestSuite parent TestHarness
++ module UnitTestSuite file /home/msyksphinz/work/riscv/chisel-development/chisel-hw/TestHarness.sv lines 20111 - 20111
++ comp tests_0 type TLManyMasterManySlaveTest parent UnitTestSuite
++ module TLManyMasterManySlaveTest file /home/msyksphinz/work/riscv/chisel-development/chisel-hw/TestHarness.sv lines 20034 - 20034
++ comp SimpleTimer type SimpleTimer parent TLManyMasterManySlaveTest
++ comp lazy_dut type TLManyMasterManySlave parent TLManyMasterManySlaveTest
++ module SimpleTimer file /home/msyksphinz/work/riscv/chisel-development/chisel-hw/TestHarness.sv lines 1 - 1
...

これで必要なファイルはすべて整った。Verilatorでシミュレーションを実行してFSTファイルを生成し、GtkWaveで確認してみよう。

gtkwave -t VTestHarness.stems simx.fst
f:id:msyksphinz:20200107233903p:plain
XMLとStemsを使ったRTLアノテーションの様子。rtlbrowseを使ってRTLとの一致を確認できる。

上手く行ったようだ。これでデバッグをより捗らせることができる。

FIRRTLに入門する (21. 多次元配列及び構造体に関する処理の検討(3))

https://raw.githubusercontent.com/freechipsproject/firrtl/master/doc/images/firrtl_logo.svg?sanitize=true

前回までの多次元構造体の処理を見直して、もう少し簡単になるんじゃないかということで作り直した。基本的な考え方は以下の通りだ。

  • SubField(構造体のメンバへのアクセス)が発生すると、WSubFieldの構造を取り除き、WRefと足し込む名前(ex.name)を記憶して次に進む。
  • WSubIndexおよびWSubAccessが発生すると、そのままの形を保持する。
  • WRefの場合、これまでにため込んだ構造体のWSubFieldの値を足し込む。

以下のようなソースコードになった。

  • src/main/scala/firrtl/passes/LowerTypes.scala
  def lowerTypesExp(memDataTypeMap: MemDataTypeMap,
    info: Info, mname: String, subfield_name: String = "")(e: Expression): Expression = {
    e match {
      case e: WRef => {
        val new_subfield_name = subfield_name match {
          case "" => e.name
          case x => e.name + "_" + x
        }
        e.copy(name = new_subfield_name)
      }
      case e: WSubAccess => {
        val exps = lowerTypesExp(memDataTypeMap, info, mname, subfield_name)(e.expr)
        WSubAccess(exps, e.index, e.tpe, flow(e))
      }
      case e: WSubField => {
...
          case _ => {
            val new_subfield_name = subfield_name match {
              case "" => e.name
              case x => e.name + "_" + x
            }
            lowerTypesExp(memDataTypeMap, info, mname, new_subfield_name)(e.expr)
          }
      case e: WSubIndex => {
...
          case _ => {
            val exps = lowerTypesExp(memDataTypeMap, info, mname, subfield_name)(e.expr)
            WSubIndex(exps, e.value, e.tpe, flow(e))
          }

以下の2つのテストをコンパイルする。WSubIndexWSubAccessを使うケースだ。どちらもWSubFieldを3階層使用している。

  • VecBundle.fir
  module VecBundle8  :
    input in: { a : { b : { c : UInt<8>[4] } [8] } [16] }[32]
    output out : { b : UInt<8>[4] }
    out.b <= in[0].a[1].b[2].c

  module VecBundle9  :
    input in: { a : { b : { c : UInt<8>[4] } [8] } [16] }[32]
    input sel1: UInt<5>
    input sel2: UInt<4>
    input sel3: UInt<3>
    output out : { b : UInt<8>[4] }
    out.b <= in[sel1].a[sel2].b[sel3].c
  module VecBundle8 :
    input in_a_b_c : UInt<8>[4][8][16][32]
    output out_b : UInt<8>[4]
  
    out_b <= in_a_b_c[0][1][2]

  module VecBundle9 :
    input in_a_b_c : UInt<8>[4][8][16][32]
    input sel1 : UInt<5>
    input sel2 : UInt<4>
    input sel3 : UInt<3>
    output out_b : UInt<8>[4]
  
    out_b <= in_a_b_c[sel1][sel2][sel3]

上手くできた。この配列分解の検討は、とりあえずここまでかな。