FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

TileLinkの勉強 (5. Diplomacyがサポートするウィジェットについて(2))

前回の続き。

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

chipyard.readthedocs.io

今回はDiplomacyがサポートするウィジェットについて。AXI4とTileLinkについて様々なウィジェットがサポートされており、これらの部品を組み合わせることで任意のバスを構成していくことになる。

TLWidthWidget

TileLinkの物理的なインタフェースビット幅を変更する。TileLinkインタフェースのビット幅はマネージャにより構成されるが、クライアント側が特定のビット幅を持っていたい場合がある。

引数:

  • innerBeatBytes: Int - クライアント側から見た物理的なビット幅(バイト単位)

使用例:

// マネージャノードがbeatBytesを8に設定している。
// WidthWidgetにより、クライアントはbeatByetsが4に設定されている。
manager.node := TLWidthWidget(4) := client.node

TLFIFOFixer

FIFOドメインを宣言したTileLinkマネージャは、クライアントから到達する、そのドメインへのすべてのFIFOオーダリングなリクエストがリクエストの順番通りに返されることを保証しなければならない。しかし、そのレスポンスの順序を制御するだけでは、同じFIFOドメイン内の他のマネージャからのインタリーブされたレスポンスを制御することができない。FIFOの順序を保証するための責任はTLFIFOFixerにより達成される。

引数:

  • policy: TLFIFOFixer.Policy - (オプション) どのマネージャがTLFIFOFixerにオーダリングを行わせるか?(デフォルト: TLFIFOFixer.all)

policyが取ることのできる引数は以下のとおりである。

  • TLFIFOFixer.all - すべてのマネージャ(FIFOドメイン以外のマネージャも含む)のオーダリングが保証される。
  • TLFIFOFixer.allFIFO - All managers that define a FIFO domain will have ordering guaranteed
  • TLFIFOFixer.allFIFO - FIFOドメインを定義するすべてのマネージャがオーダリングを保証される。
  • TLFIFOFixer.allVolatile - VOLATILE, PUT_EFFECTS, GET_EFFECTSのリージョンタイプを持つすべてのマネージャがオーダリングを保証させる(リージョンタイプについてはManager Nodeを参照すること)。

TLXbar と AXI4Xbar

TileLinkおよびAXI4のクロスバージェネレータであり、TL/AXIクライアントからのリクエストを、マネージャ・スレーブのアドレス定義に基づいてTL/AXIのスレーブに転送する。通常はこのウィジェットは引数無しで生成される。しかし、アービタ内でどのクライアントポートが優先権を手に入れるかなどのアービトレーションのポリシーを変更することができる。デフォルトのポリシーはTLArbiter.roundRobinであるが、優先権洗濯ポリシーを変更したい場合はTLArbiter.lowestIndexFirstに変更することができる。

引数:

All arguments are optional.

すべての引数はオプションである。

  • arbitrationPolicy: TLArbiter.Policy - 使用するアービトレーションのポリシー
  • maxFlightPerId: Int - (AXI4のみ) 同じIDにおいて同時にインフライトになれるIDの数(デフォルト: 7)。
  • awQueueDepth: Int - (AXI4のみ) ライトアドレスキューのサイズ(デフォルト: 2)

使用例:

// lazyモジュールでクロスバーをインスタンス化する。
val tlBus = LazyModule(new TLXbar)

// 単一の入力エッジの接続。
tlBus.node := tlClient0.node
// 複数の入力エッジの接続。
tlBus.node :=* tlClient1.node

// 単一の出力エッジの接続。
tlManager0.node := tlBus.node
// 複数の出力エッジの接続。
tlManager1.node :*= tlBus.node

// クロスバーをlowestIndexFirstのアービトレーションポリシーで宣言する。
// TLArbiterのsingletonを使用しているが、実際にはAXI4である。
val axiBus = LazyModule(new AXI4Xbar(TLArbiter.lowestIndexFirst))

// TLと同様に接続される。
axiBus.node := axiClient0.node
axiBus.node :=* axiClient1.node
axiManager0.node := axiBus.node
axiManager1.node :*= axiBus.node

TLToAXI4 と AXI4ToTL

TileLinkとAXI4プロトコルのコンバータである。TLToAXI4はクライアントにTileLinkを持ち、AXI4スレーブに接続する。AXI4ToTLはAXI4マスターをTileLinkのマネージャに接続する。通常はデフォルトの引数をオーバライドすることはない。

使用例:

axi4slave.node :=
    AXI4UserYanker() :=
    AXI4Deinterleaver(64) :=
    TLToAXI4() :=
    tlclient.node

tlmanager.node :=
    AXI4ToTL() :=
    AXI4UserYanker() :=
    AXI4Fragmenter() :=
    axi4master.node

TLToAXI4コンバータの後には、AXI4Deinterleaver を挿入する必要がある。なぜなら、TLToAXI4コンバータはインタリーブされたリードレスポンスを取り扱うことができないからだ。TLToAXI4コンバータはAXI4のユーザフィールドを使用していくつかの情報を格納する。したがってユーザフィールドの存在しないAXI4ポートに接続する場合にはAXI4UserYankerを接続する必要がある。

AXI4ポートをAXI4ToTLウィジェットに接続する場合、AXI4FragmenterAXI4UserYankerを接続する必要がある。なぜならば、コンバータはユーザフィールドおよび複数ビートのトランザクションを扱うことができないからである。

TLROM

The TLROM widget provides a read-only memory that can be accessed using TileLink. Note: this widget is in the freechips.rocketchip.devices.tilelink package, not the freechips.rocketchip.tilelink package like the others.

TLROMウィジェットはTileLink経由でアクセスすることのできるRead-Onlyのメモリである。このウィジェットfreechips.rocketchip.devices.tilelinkパッケージに含まれており、freechips.rocketchip.tilelinkではないことに注意が必要である。

引数:

  • base: BigInt - メモリのベースアドレス
  • size: Int - バイト単位でのメモリサイズ
  • contentsDelayed: => Seq[Byte] - ROMのバイト内容を生成するための関数。
  • executable: Boolean - (オプション) CPUがこのROMの内容をフェッチすることができるかを示す(デフォルト: true)
  • beatBytes: Int - (オプション) バイト単位でのインタフェースのビット幅。(デフォルト: 4)
  • resources: Seq[Resource] - (オプション) デバイスツリーに接続されるリソースのシーケンス

使用例:

val rom = LazyModule(new TLROM(
  base = 0x100A0000,
  size = 64,
  contentsDelayed = Seq.tabulate(64) { i => i.toByte },
  beatBytes = 8))
rom.node := TLFragmenter(8, 64) := client.node

サポートされる操作:

TLROMは単一ビートの読み込みのみサポートされる。複数ビートの読み込みを行う場合、TLFragmenterをROMの前に配置する必要がある。

TLRAM と AXI4RAM

The TLRAM and AXI4RAM widgets provide read-write memories implemented as SRAMs.

TLRAMとAXI4RAMウィジェットSRAMとして実装されるRead-Writeメモリである。

引数:

  • address: AddressSet - RAMがカバーするアドレス範囲。
  • cacheable: Boolean - (オプション) RAMのコンテンツをキャッシュできるかを示す(デフォルト: true)。
  • executable: Boolean - (オプション) RAMのコンテンツが命令としてフェッチできるかを示す(デフォルト: true)
  • beatBytes: Int - (オプション) TL/AXI4インタフェースの幅をバイト単位で示す(デフォルト: 4)。
  • atomics: Boolean - (オプション, TileLinkのみ) RAMがアトミック操作をサポートするかを示す(デフォルト: false)。

使用例:

val xbar = LazyModule(new TLXbar)

val tlram = LazyModule(new TLRAM(
  address = AddressSet(0x1000, 0xfff)))

val axiram = LazyModule(new AXI4RAM(
  address = AddressSet(0x2000, 0xfff)))

tlram.node := xbar.node
axiram := TLToAXI4() := xbar.node

サポートされている操作:

TLRAMはTL-ULリクエストの単一ビートのみをサポートしている。atomicsをtrueに設定すると、LogicalとArithmeticの操作をサポートする。複数ビートのRead/Writeを行いたい場合はTLFragmenterを接続すること。

AXI4RAMはAXI4-Liteの操作のみをサポートするため、複数ビートのRead/Writeおよび最大サイズよりも小さなRead/Writeはサポートされない。フルのAXI4プロトコルを使用したい場合は AXI4Fragmenterを使用すること。

TileLinkの勉強 (4. Diplomacyがサポートするウィジェットについて)

前回の続き。

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

chipyard.readthedocs.io

今回はDiplomacyがサポートするウィジェットについて。AXI4とTileLinkについて様々なウィジェットがサポートされており、これらの部品を組み合わせることで任意のバスを構成していくことになる。

Diplomaticウィジェット

RocketChipはDiplomaticなTileLinkとAXI4のウィジェットをライブラリとして提供している。共通部品として最も多く使用されるウィジェットを以下に示す。TileLinkのウィジェットfreechips.rocketchip.tilelinkから入手できる。また、AXI4のウィジェットfreechips.rocketchip.amba.axi4から入手できる。

TLBuffer

TileLinkのトランザクションをバッファリングするためのウィジェットである。TL-C以外の場合は2つのキューをインスタンス化し、TL-Cの場合は5つのキューをインスタンス化するだけである。各チャンネルのキューを柔軟に設定するために、freechips.rocketchip.diplomacy.BufferParamsをコンストラクタに渡すことができる。case classの引数は以下のとおりである。

  • depth: Int - キューのエントリ数
  • flow: Boolean - Trueの場合、Valid信号は組み合わせ回路で構成されるためエンキューと同じサイクルで消費される。
  • pipe: Boolean - Trueの場合、Ready信号は組み合わせ回路で構成されるため、1エントリのキューは最大のレートで使用される。

コンストラクタにはInt型の整数のみを渡すことができる。BufferParamsオブジェクトの代わりに整数を渡すことによって、整数で渡されたdepthのキューが生成され、flowpipeはfalseに設定される。

また、以下のあらかじめ定義されたBufferParamsオブジェクトを使用することができる。

  • BufferParams.default = BufferParams(2, false, false)
  • BufferParams.none = BufferParams(0, false, false)
  • BufferParams.flow = BufferParams(1, true, false)
  • BufferParams.pipe = BufferParams(1, false, true)

引数:

4種類のコンストラクタがある。引数が0, 1, 2, 5つ取るバリエーションである。

0個の引数を持つコストラクタはすべてのチャネルでBufferParams.defaultを使用する。

1つの引数を持つコンストラクタはすべてのチャネルでBufferParamsオブジェクトを使用する。

2つの引数を持つコンストラクタは、それぞれ以下のように引数が適用される。

  • ace: BufferParams - A, C, Eチャネルに使用されるパラメータである。
  • bd: BufferParams - B, Dチャネルに使用されるパラメータである。

5つの引数を持つコンストラクタはそれぞれ以下のように引数が適用される。

  • a: BufferParams - Aチャネルに使用されるパラメータである。
  • b: BufferParams - Bチャネルに使用されるパラメータである。
  • c: BufferParams - Cチャネルに使用されるパラメータである。
  • d: BufferParams - Dチャネルに使用されるパラメータである。
  • e: BufferParams - Eチャネルに使用されるパラメータである。

使用例:

// デフォルトの設定
manager0.node := TLBuffer() := client0.node

// 暗黙的にチャネル当たり8エントリのキューを挿入する。
manager1.node := TLBuffer(8) := client1.node

// Aチャネルではデフォルト設定を使用し、Dチャネルではパイプを使用する。
manager2.node := TLBuffer(BufferParams.default, BufferParams.pipe) := client2.node

// AチャネルとDチャネルにキューを挿入する。
manager3.node := TLBuffer(
  BufferParams.default,
  BufferParams.none,
  BufferParams.none,
  BufferParams.default,
  BufferParams.none) := client3.node

AXI4Buffer

TLBufferと同様だがAXI4用である。BufferParamsオブジェクトを引数に取る。

引数:

TLBufferと同様に、AXI4Bufferにも4種類のコンストラクタがある。引数が0, 1, 2, 5つ取るバリエーションである。

0個の引数を持つコストラクタはすべてのチャネルでBufferParams.defaultを使用する。

1つの引数を持つコンストラクタはすべてのチャネルでBufferParamsオブジェクトを使用する。

2つの引数を持つコンストラクタは、それぞれ以下のように引数が適用される。

  • aw: BufferParams - AR, AW, Wチャネルに使用されるパラメータである。
  • br: BufferParams - B, Rチャネルに使用されるパラメータである。

5つの引数を持つコンストラクタはそれぞれ以下のように引数が適用される。

  • aw: BufferParams - AWチャネルに使用されるパラメータである。
  • w: BufferParams - Wチャネルに使用されるパラメータである。
  • b: BufferParams - Bチャネルに使用されるパラメータである。
  • ar: BufferParams - ARチャネルに使用されるパラメータである。
  • r: BufferParams - Rチャネルに使用されるパラメータである。

使用例:

// デフォルト設定
slave0.node := AXI4Buffer() := master0.node

// 暗黙的にチャネル当たり8エントリのキューを挿入する
slave1.node := AXI4Buffer(8) := master1.node

// AW/W/ARチャネルではデフォルト設定を使用し、B/Rチャネルではパイプを使用する。
slave2.node := AXI4Buffer(BufferParams.default, BufferParams.pipe) := master2.node

// AW/B/ARチャネルに1エントリのキューを挿入し、W/Rチャネルにキューを挿入する。
// Single-entry queues for aw, b, and ar but two-entry queues for w and r
slave3.node := AXI4Buffer(1, 2, 1, 1, 2) := master3.node

AXI4UserYanker

個のウィジェットはユーザフィールドを持つAXIポートを受け取り、そのユーザフィールドを取り払う。ARとAWリクエストチャネルのユーザフィールドは内部のキューによりARID/AWIDに関連付けられて保持される。このユーザフィールドはレスポンス時に付け加えられる。

引数:

  • capMaxFlight: Option[Int] - (任意) インフライトなIDを保持することのできるリクエストの数。None(デフォルト)の場合、UserYankerはインフライトなリクエストの最大数までサポートする。

使用例:

nouser.node := AXI4UserYanker(Some(1)) := hasuser.node

AXI4Deinterleaver

異なるIDを持つ複数ビートのAXIリードレスポンスはインタリーブされる可能性がある。このウィジェットはスレーブからのリードレスポンスを並び替え、すべてのビートが単一のトランザクションになるように調整する。

引数:

使用例:

interleaved.node := AXI4Deinterleaver() := consecutive.node

TLFragmenter

TLFragmenterウィジェットはTileLinkインタフェースの最大論理転送サイズを縮小するために、大きな大きなトランザクションをより小さなトランザクションに分割する。

引数:

  • minSize: Int - 接続されるマネージャの最小転送サイズ
  • maxSize: Int - Fragmenterが適用された後の最大転送サイズ
  • alwaysMin: Boolean - (オプション)すべてのリクエストをminSizeに分割するか(そうでない場合、マネージャでサポートされる最大サイズに分割する) (デフォルトではfalse)。
  • earlyAck: EarlyAck.T - (オプション)複数ビートのPutコマンドにおいて、最初のビートでAcknowledgeするか、最後のビットでAckowledgeするか?取りうる値は以下である(デフォルト : EarlyAck.None)
    • EarlyAck.AllPuts - 常に最初のビートでAckowledgeを返す。
    • EarlyAck.PutFulls - PutFullの場合は最初のビートでAckowledgeを返す。そうでなければ最後のビートでAckowledgeを返す。
    • EarlyAck.None - 常に最後のビートでAckowledgeを返す。
  • holdFirstDenied: Boolean - (optional) Allow the Fragmenter to unsafely combine multibeat Gets by taking the first denied for the whole burst. (default: false)

使用例:

val beatBytes = 8
val blockBytes = 64

single.node := TLFragmenter(beatBytes, blockBytes) := multi.node

axi4lite.node := AXI4Fragmenter() := axi4full.node

Additional Notes

  • TLFragmenterは PutFull, PutPartial, LogicalData, Get, Hint に対して変更を加える。
  • TLFragmenterはArithmeticDataはそのまま通過させる(alwaysMinが有効の場合はminSizeまで縮小させる)
  • TLFragmenterはacqureコマンドを変更できない(livelockを発生させる可能性がある)。したがって、両サイドにキャッシュを配置するのは危険である。

AXI4Fragmenter

AXI4FragmenterはTLFragmenterと似ているが、TileLinkではなく複数ビートのAXI4トランザクションを分割する。これはAXI4からAXI4-Liteへの効率的な変換の機能も持っている。このウィジェットのコンストラクタは引数を取らない。

使用例:

axi4lite.node := AXI4Fragmenter() := axi4full.node

TLSourceShrinker

マネージャが管理するソースIDは、通常は接続されるクライアントの情報により計算される。いくつかの場合には、ソースのIDの数を固定したいときがある。例えばTileLinkのポートをVerilogブラックボックスにエクスポートしたいときなどが挙げられる。このような問題を解決するために、クライアントがより多くのソースIDを必要とする場合にはTLSourceShrinkerを使用できる。

引数:

  • maxInFlight: Int - The maximum number of source IDs that will be sent from the TLSourceShrinker to the manager.
  • maxInFlight: Int - TLSourceShrinkerからマネージャに転送されるソースIDの最大数。

使用例:

// クライアントノードはおそらくソースIDを16以上持っている。
// マネージャノードはIDを16個までしか管理できない。
manager.node := TLSourceShrinker(16) := client.node

AXI4IdIndexer

TLSourceShrinkerのAXI4番である。スレーブAXI4インタフェースのAWID/ARIDのビット数を制限する。AXI4ポートを外部インタフェース・もしくはブラックボックスに接続する場合に有益である。

引数:

  • idBits: Int - スレーブインタフェース上のIDビット数

使用例:

// マスターノードはおおそらく16以上のIDを持っている。
// スレーブノードは4つのIDまでしか管理できない。
slave.node := AXI4IdIndexer(4) := master.node

注意:

AXI4IDIndexerはスレーブインタフェースにuserフィールドを追加し、マスターリクエストのIDをフィールドにストアする。userフィールドを持たないAXI4インタフェースに接続する場合、AXI4UserYankerを使用する必要がある。

FIRRTLに入門する (24. FIRRTLにおけるメモリの生成を調査する)

https://raw.githubusercontent.com/freechipsproject/firrtl/master/doc/images/firrtl_logo.svg?sanitize=true
$ sbt assembly && ./utils/bin/firrtl -td regress -i ./regress/ICache.fir -X sverilog -ll trace 2>&1 | tee ICache.log
======== Starting Transform CheckInitialization$ ========
Exception in thread "main" firrtl.passes.PassExceptions:
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.data[1] <= mux(refill_done, mux(_T_321[1], _T_304[1], VOID), VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.mask[0] <= mux(refill_done, _GEN_7, VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.data[3] <= mux(refill_done, mux(_T_321[3], _T_304[3], VOID), VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.mask[3] <= mux(refill_done, _GEN_10, VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.data[0] <= mux(refill_done, mux(_T_321[0], _T_304[0], VOID), VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.data[2] <= mux(refill_done, mux(_T_321[2], _T_304[2], VOID), VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.mask[2] <= mux(refill_done, _GEN_9, VOID)
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[ICache.scala 97:25] : [module ICache]  Reference tag_array is not fully initialized.
   : tag_array._T_328.mask[1] <= mux(refill_done, _GEN_8, VOID)

いろいろ調査した結果、どうもSRAMを最初に展開しておかないといけないのが問題らしい。つまり、以下のようなFIRコードを考える。

circuit VecMem :
  module VecMem :
    input clock : Clock

    input in: UInt<32>
    input wr_en: UInt<1>
    input rd_en: UInt<1>

    input addr: UInt<6>
    input data: UInt<1>

    output rdata : UInt<32>[4]

    smem array : UInt<32>[4][64]

    when rd_en :
      read mport rdata_tmp = array[addr], clock
    when wr_en :
      write mport wdata = array[addr], clock

    rdata <= rdata_tmp

このときにExpandConnect()においてIsInvalidの場合、ちゃんと展開しておかなければならない。

  • src/main/scala/firrtl/passes/Passes.scala
object ExpandConnects extends Pass {
    ...
           case sx: DefMemory => flows(sx.name) = SourceFlow; sx
           case sx: DefNode => flows(sx.name) = SourceFlow; sx
           case sx: IsInvalid =>
             // create_expsからcreate_exps_connectに置き換える。
             val invalids = create_exps_connect(sx.expr).flatMap { case expx =>
                flow(set_flow(expx)) match {
                   case DuplexFlow => Some(IsInvalid(sx.info, expx))
                   case SinkFlow => Some(IsInvalid(sx.info, expx))

これでFIRRTLを実行してみる。

sbt assembly && ./utils/bin/firrtl -td regress -i regress/VecMem.fir -X sverilog -ll trace 2>&1 | tee VecMem.log

ExpandConnect()実行前。

...
    array.wdata.data is invalid
    array.wdata.mask is invalid
...

ExpandConnect()実行後。

    array.wdata.data[0] is invalid
    array.wdata.data[1] is invalid
    array.wdata.data[2] is invalid
    array.wdata.data[3] is invalid
    array.wdata.mask[0] is invalid
    array.wdata.mask[1] is invalid
    array.wdata.mask[2] is invalid
    array.wdata.mask[3] is invalid

このように展開しておく。これで一応エラーなく実行できるようになったようだ。

  logic [31:0][3:0] array [0:63];
...
  assign array_rdata_tmp_addr = array_rdata_tmp_addr_pipe_0;
  assign array_rdata_tmp_data = array[array_rdata_tmp_addr];
...

ただしこれでもまだおかしいところがある。また解析して作り直そう。

FIRRTLに入門する (23. 多次元配列の出力時のループ展開の調整(2))

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

前回まででループ展開の問題に対してある程度解決の目途が立った。しかしまだ調整できていない部分がある。

ExpandWhesによりデフォルト値が挿入されるのだが、これはなぜかというとポートであるoutと記述とout.a[0], out.a[1], out.a[2], out.a[3]がずれているため、デフォルト値が挿入されているようだ。

circuit VecExtract :
  module VecExtract :
    input in : UInt<32>[4][4][4]
    input sel1 : UInt<2>
    input sel2 : UInt<2>
    output out : { a : UInt<32>[4]}
  
    out.a[0] <= in[sel1][sel2][0]
    out.a[1] <= in[sel1][sel2][1]
    out.a[2] <= in[sel1][sel2][2]
    out.a[3] <= in[sel1][sel2][3]

からの、

    skip
    out.a <= VOID
    out.a[0] <= in[sel1][sel2][0]
    out.a[1] <= in[sel1][sel2][1]
    out.a[2] <= in[sel1][sel2][2]
    out.a[3] <= in[sel1][sel2][3]

out.aにVOIDが挿入されてしまう問題。これは、output out : { a : UInt<32>[4]}が展開されないため出力のoutが使用されていないと勘違いされてしまい、これにより余計に挿入されてしまう文である。

これを解決するために色々試行錯誤したのだが、結局自前のcreate_expsを挿入してポート部分の配列は展開して対処することにした。

  • src/main/scala/firrtl/passes/ExpandWhens.scala
  /** Returns all references to all Female leaf subcomponents of a reference */
  private def getFemaleRefs(n: String, t: Type, g: Flow): Seq[Expression] = {
    val exps = create_exps_port(n, t, g)  // create_expsの代わりにcreate_exps_portを使用する
    val ret = exps.flatMap { case exp =>
...
        

create_exps_portの実装は以下のようにした。要するにTypeに応じて生成するExpressionを切り替える。

  def create_exps_port(n: String, t: Type, g: Flow): Seq[Expression] = {
    val base_wref = WRef(n, t, ExpKind, g)
    val base_exp = create_exps(base_wref)
    t match {
      case (_: GroundType) => base_exp
      case t: BundleType =>
        t.fields.flatMap(f => create_exps_connect(WSubField(base_wref, f.name, f.tpe, times(g, f.flip))))
      case t: VectorType => (0 until t.size).flatMap(i => create_exps_connect(WSubIndex(base_wref, i, t.tpe, g)))
    }
  }

これにより以下のように配列が展開される。

circuit VecArray :
  module VecArray :
    input in : UInt<8>[2][4][8]
    input sel : UInt<3>
    output out : UInt<8>[2][4]

    out[0][0] <= in[sel][0][0]
    out[0][1] <= in[sel][0][1]
    out[1][0] <= in[sel][1][0]
    out[1][1] <= in[sel][1][1]
    out[2][0] <= in[sel][2][0]
    out[2][1] <= in[sel][2][1]
    out[3][0] <= in[sel][3][0]
    out[3][1] <= in[sel][3][1]

また、以下のような配列に対しても正常に展開可能だ。

  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 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[0] <= in_a_b_c[sel1][sel2][sel3][0]
    out_b[1] <= in_a_b_c[sel1][sel2][sel3][1]
    out_b[2] <= in_a_b_c[sel1][sel2][sel3][2]
    out_b[3] <= in_a_b_c[sel1][sel2][sel3][3]

FIRRTLに入門する (23. 多次元配列の出力時のループ展開の調整)

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

前回の実装で、配列に関する実装は以下のようになっていた。以下のFIRコードをコンパイルすると、Verilogファイルが生成されるのだが、

  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 VecBundle9(
  input logic  [7:0] in_a_b_c [4][8][16][32],
  input logic  [4:0] sel1               ,
  input logic  [3:0] sel2               ,
  input logic  [2:0] sel3               ,
  output logic [7:0] out_b [4]
);
  assign out_b = in_a_b_c[sel1][sel2][sel3];
endmodule

これは実はよくない。なぜなら、[7:0] out_b [4]というベクタに対してそのまま代入してしまっている。これは展開して接続した方が確実だ。

for (int i = 0; i < 4; i=i+1) begin
    assign out_b[i] = in_a_b_c[sel1][sel2][sel3][i];
end

しかし、今のところFIRではfor文はサポートしていないので、とりあえず手っ取り早い方法としては全部展開してしまうという方法がある。これでも、すべての多次元配列を展開されるよりはよっぽどマシだ。

assign out_b[0] = in_a_b_c[sel1][sel2][sel3][0];
assign out_b[1] = in_a_b_c[sel1][sel2][sel3][1];
assign out_b[2] = in_a_b_c[sel1][sel2][sel3][2];
assign out_b[3] = in_a_b_c[sel1][sel2][sel3][3];

という訳で上記のように最も先頭に当たる配列を展開するためにはどのようにすればよいのか検討する。

まず、通常のFIRRTLの変換プロセスにおいて、これらの配列の展開を行っているのはExpandConnectsというPassだ。

  • ExpandConnects実行前
circuit VecExtract :
  module VecExtract :
    input in : UInt<32>[4][4][4]
    input sel1 : UInt<2>
    input sel2 : UInt<2>
    output out : { a : UInt<32>[4]}
  
    out.a <= in[sel1][sel2]
  • ExpandConnects実行後
circuit VecExtract :
  module VecExtract :
    input in : UInt<32>[4][4][4]
    input sel1 : UInt<2>
    input sel2 : UInt<2>
    output out : { a : UInt<32>[4]}
  
    out.a[0] <= in[sel1][sel2][0]
    out.a[1] <= in[sel1][sel2][1]
    out.a[2] <= in[sel1][sel2][2]
    out.a[3] <= in[sel1][sel2][3]

現在の私の実装ではこれが実現されていない。なぜかというと、ExpandConnectsの中で実装されている

  • src/main/scala/firrtl/passes/Passes.scala
object ExpandConnects extends Pass {
  def run(c: Circuit): Circuit = {
    def expand_connects(m: Module): Module = {
...
          case sx: Connect =>
            val locs = create_exps(sx.loc)
            val exps = create_exps(sx.expr)
            Block(locs.zip(exps).map { case (locx, expx) =>
               to_flip(flow(locx)) match {
                  case Default => Connect(sx.info, locx, expx)
                  case Flip => Connect(sx.info, expx, locx)
               }
            })

create_exposはさんざん改造したので分かるのだが、create_expsは配列を展開しない。したがってこのままになっているのだ。

  • src/main/scala/firrtl/Utils.scala
  def create_exps(e: Expression): Seq[Expression] = e match {
    case ex: Mux =>
      val e1s = create_exps(ex.tval)
...
    case ex => ex.tpe match {
...
      case t: VectorType => Seq(ex)
    }
  }

したがって、これを配列の数だけ展開するためのcreate_exps_connectを作成する。というか、元に戻しただけである。

  def create_exps_connect(e: Expression): Seq[Expression] = e match {
    case ex: Mux =>
      val e1s = create_exps_connect(ex.tval)
...
    case ex => ex.tpe match {
      case (_: GroundType) => Seq(ex)
...
      case t: VectorType => (0 until t.size).flatMap(i => create_exps_connect(WSubIndex(ex, i, t.tpe,flow(ex))))
    }
  }

これに従って、上記のexpand_connects()を書き換える。create_expsの所をcreate_exps_connectに置き換えるだけである。

...
          case sx: Connect =>
            val locs = create_exps_connect(sx.loc)
            val exps = create_exps_connect(sx.expr)
            Block(locs.zip(exps).map { case (locx, expx) =>
...

これでとりあえずできた。

circuit VecExtract :
  module VecExtract :
    input in : UInt<32>[4][4][4]
    input sel1 : UInt<2>
    input sel2 : UInt<2>
    output out : { a : UInt<32>[4]}
  
    out.a[0] <= in[sel1][sel2][0]
    out.a[1] <= in[sel1][sel2][1]
    out.a[2] <= in[sel1][sel2][2]
    out.a[3] <= in[sel1][sel2][3]

ただし、問題はこの後だ。ExpandWhesによりデフォルト値が挿入されるのだが、これはなぜかというとポートであるoutと記述とout.a[0], out.a[1], out.a[2], out.a[3]がずれているため、デフォルト値が挿入されているようだ。

    skip
    out.a <= VOID
    out.a[0] <= in[sel1][sel2][0]
    out.a[1] <= in[sel1][sel2][1]
    out.a[2] <= in[sel1][sel2][2]
    out.a[3] <= in[sel1][sel2][3]

これにより、VOIDの記述が残ってしまいエラーが発生してしまう。

======== Starting Transform InferTypes$ ========
Exception in thread "main" firrtl.FirrtlInternalException: Internal Error! Please file an issue at https://github.com/ucb-bar/firrtl/issues
...
    at firrtl.stage.FirrtlMain.main(FirrtlStage.scala)
Caused by: scala.MatchError: WVoid (of class firrtl.WVoid$)
    at firrtl.passes.InferTypes$.infer_types_e$1(InferTypes.scala:26)
...

色々試行錯誤しているが上手い解決方法が思いつかない。もう少し実装を観察する。

FIRRTLに入門する (22. 多次元配列の構造体に関するSystemVerilog出力の修正)

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

前回までで多次元構造体配列について、とりあえずエラーが出ずにVerilogが生成されるようになったが、生成されたSystemVerilogのコードは明らかにおかしい。

circuit VecBundle1 :
  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 logic  [7:0][3][7][15][31] in_a_b_c,
  output logic [7:0][3]            out_b
);
  assign out_b = in_a_b_c[0][1][2];
endmodule

ビット幅の指定がおかしくなっているのと、ベクトル配列の指定がおかしい。理想としては以下だ。

module VecBundle8(
    input logic  [7:0] in_a_b_c[4][8][16][32],
    output logic [7:0] out_b[4]
);
  assign out_b = in_a_b_c[0][1][2];
endmodule

SystemVerilogの生成については、SystemVerilogEmitterが担当している。そして、SystemVerilogEmitter内のstringifyが型の出力を担当している。

    // Turn ports into Seq[String] and add to portdefs
    def build_ports(): Unit = {
...
    }
    def emit_systemverilog(): DefModule = {
      build_netlist(m.body)
      build_ports()
...

build_ports()でポートタイプを作り上げているのだが、それを2つに分割する。つまり、以下の2つの分割するわけだ。

    input logic  [7:0] in_a_b_c[4][8][16][32],
//               ~~~~~         ~~~~~~~~~~~~~~
//                 |                  `----------- stringify_vec()
//                 `------------------------------ stringify()
      // Turn types into strings, all ports must be GroundTypes
      val tpes = m.ports map {
        case Port(_, _, _, tpe: GroundType) => stringify(tpe)
        case Port(_, _, _, tpe: VectorType) => stringify(tpe)
        case port: Port => error(s"Trying to emit non-GroundType Port $port")
      }

      // Turn types into strings, all ports must be GroundTypes
      val tpes_vec = m.ports map {
        // case Port(_, _, _, tpe: GroundType) => stringify_vec(tpe)
        case Port(_, _, _, tpe: GroundType) => ""
        case Port(_, _, _, tpe: VectorType) => stringify_vec(tpe)
        case port: Port => error(s"Trying to emit non-GroundType Port $port")
      }

stringify_vec()VectorTypeの型を再帰的に探していき、VectorTypeのたびに"[$wx]"を出力するという寸法である。

  def stringify_vec(tpe: VectorType): String = {
    val elem_type = tpe.tpe match {
      case (t: VectorType) => {
        val wx = sv_bitWidth(tpe)
        val field_str = if (wx > 0) s"[$wx]" else ""
        val str_element = tpe.tpe match {
          case tpe_elem: VectorType => stringify_vec(tpe_elem)
        }
        str_element + field_str
      }
      case (_: UIntType | _: SIntType | _: AnalogType) => {
        val wx = sv_bitWidth(tpe)
        val vec_str = if (wx > 0) s"[$wx]" else ""
        vec_str
      }
      case ClockType | AsyncResetType | AsyncResetNType => ""
      case _ => throwInternalError(s"trying to write unsupported type in the Verilog Emitter: $tpe")
    }
    elem_type
  }

最後に、stringify_vec()の結果をポート宣言に追加する。

      // dirs are already padded
      val pad_to_port    = padToMax(tpes)
      val pad_to_portvec = padToMax(tpes_vec)
      val pad_vec = pad_to_port.zip(pad_to_portvec)
      (dirs, pad_vec, m.ports).zipped.toSeq.zipWithIndex.foreach {
        case ((dir, (tpe, tpe_vec), Port(info, name, _, _)), i) =>
...
          if (i != m.ports.size - 1) {
            portdefs += Seq(dir, " ", tpe, " ", name, " ", tpe_vec, ",", info)
          } else {
            portdefs += Seq(dir, " ", tpe, " ", name, " ", tpe_vec, info)
          }
...

これにより、上記のFIRRTLは以下のようになる。

./utils/bin/firrtl -td regress -i ./regress/VecBundle.fir -X sverilog -ll trace 2>&1 | tee VecBundle.log
module VecBundle8(
  input logic  [7:0] in_a_b_c [4][8][16][32],
  output logic [7:0] out_b [4]
);
  assign out_b = in_a_b_c[0][1][2];
endmodule

ある程度形になってきた。まだ配列を正常に処理できていないと思うけど、とりあえず形になってきた。

ついでに、以下のようなFIRのテストを動かしてみた。

  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 VecBundle9(
  input logic  [7:0] in_a_b_c [4][8][16][32],
  input logic  [4:0] sel1               ,
  input logic  [3:0] sel2               ,
  input logic  [2:0] sel3               ,
  output logic [7:0] out_b [4]
);
  assign out_b = in_a_b_c[sel1][sel2][sel3];
endmodule

良さそうだ。

TileLinkのDiplomacyを使った実際のデザインを試してみる

TileLinkはDiplomacyという方式を使って実装されており、それを理解するのは大変だ。実際に触ってみるのが一番良い気がする。 という訳で、RocketChipの中でTileLinkのユニットテストを行っているデザインを抽出して実行してみた。

使用したのは、RocketChipの以下のリビジョンだ。

commit d12c7497ced7436872f2486e5ab6f0021dc094c0 (HEAD, tag: v1.2.2, upstream/1.2.x)
Author: Jim Lawson <ucbjrl@berkeley.edu>
Date:   Wed Dec 18 12:38:41 2019 -0800

    Remove redundant scm definition in pomExtra.
    This causes Sonatype/Nexus to fail with:
    ```
    Invalid POM: /edu/berkeley/cs/rocketchip_2.12/1.2.2/rocketchip_2.12-1.2.2.pom: Parsing Error: Duplicated tag: 'scm' (position: START_TAG seen ...&lt;/developers>\n &lt;scm>... @37:10)
    ```

調べてみると、TileLinkを使ったデザインで、以下のコンフィグレーションがシンプルで良い気がする。

ag "extend Config"
...
../src/main/scala/unittest/Configs.scala:149:class TLSimpleUnitTestConfig extends Config(new WithTLSimpleUnitTests ++ new WithTestDuration(10) ++ new BaseSubsystemConfig)
...

このコンフィグレーションを確認してみる。TLSimpleUnitTestConfigというのは以下のような構成となっている。

  • src/main/scala/unittest/Configs.scala
class WithTLSimpleUnitTests extends Config((site, here, up) => {
  case UnitTests => (q: Parameters) => {
    implicit val p = q
    val txns = 100 * site(TestDurationMultiplier)
    val timeout = 50000 * site(TestDurationMultiplier)
    Seq(
      // Module(new TLUserTest(               txns=   txns, timeout=timeout)),
      Module(new TLRAMSimpleTest(1,        txns=15*txns, timeout=timeout)),
      Module(new TLRAMSimpleTest(4,        txns=15*txns, timeout=timeout)),
      Module(new TLRAMSimpleTest(16,       txns=15*txns, timeout=timeout)),
      Module(new TLRAMZeroDelayTest(4,     txns=15*txns, timeout=timeout)),
      Module(new TLRAMHintHandlerTest(     txns=15*txns, timeout=timeout)),
      Module(new TLFuzzRAMTest(            txns= 3*txns, timeout=timeout)),
      Module(new TLRR0Test(                txns= 3*txns, timeout=timeout)),
      Module(new TLRR1Test(                txns= 3*txns, timeout=timeout)),
      Module(new TLRAMRationalCrossingTest(txns= 3*txns, timeout=timeout)),
      Module(new TLRAMAsyncCrossingTest(   txns= 5*txns, timeout=timeout)),
      Module(new TLRAMAtomicAutomataTest(  txns=10*txns, timeout=timeout)),
      Module(new TLRAMECCTest(8, 4,        txns=15*txns, timeout=timeout)),
      Module(new TLRAMECCTest(4, 1,        txns=15*txns, timeout=timeout)),
      Module(new TLRAMECCTest(1, 1,        txns=15*txns, timeout=timeout)) ) }
})

これだけたくさんテストをすると時間がかかるので、一本だけ抽出する。

      Module(new TLRAMSimpleTest(1,        txns=15*txns, timeout=timeout)),

このTLRAMSimpleTestというのは、何をするテストだろうか。

  • src/main/scala/tilelink/SRAM.scala
class TLRAMSimpleTest(ramBeatBytes: Int, txns: Int = 5000, timeout: Int = 500000)(implicit p: Parameters) extends UnitTest(timeout) {
  val dut = Module(LazyModule(new TLRAMSimple(ramBeatBytes, txns)).module)
  io.finished := dut.io.finished
}
class TLRAMSimple(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
  val fuzz = LazyModule(new TLFuzzer(txns))
  val model = LazyModule(new TLRAMModel("SRAMSimple"))
  val ram  = LazyModule(new TLRAM(AddressSet(0x0, 0x3ff), beatBytes = ramBeatBytes))

  ram.node := TLDelayer(0.25) := model.node := fuzz.node

  lazy val module = new LazyModuleImp(this) with UnitTestModule {
    io.finished := fuzz.module.io.finished
  }
}

インスタンスされているのは、

  • fuzz : TLFuzzer ランダムなリクエストを発生する。
  • model : TLRAMModel("SRAMSimple") RAMモデル。
  • ram : TLRAM(AddressSet(0x0, 0x3ff), beatBytes = ramBeatBytes) 実際のRAM。

これらを順に接続していく。

  ram.node := TLDelayer(0.25) := model.node := fuzz.node

なんでRAMを2つ接続しているのか良く分からないが...

この時のTLRAMSimpleTestのオプションとして以下が設定されている。

  • ramBeatBytes = 1
  • txns = 15 * txns = 15 * 100 * site(TestDurationMultiplier) = 15 * 100 * 10 = 15000

となっているのだが、面倒なのでかなり短縮しよう。15本くらいリクエストを出せば十分だろう。

実際にコンフィグレーションを生成してみる。

make debug CONFIG=TLSimpleUnitTestConfig PROJECT=freechips.rocketchip.unittest

生成できたようだ。実行してみよう。

# dummyは本当はELFを指定するのだが使わないのでdummyとしている。
./emulator-freechips.rocketchip.unittest-TLSimpleUnitTestConfig-debug +verbose -voutput/wave.vcd dummy
Started UnitTest TLRAMSimpleTest
SRAMSimple G  0x1f7 - 0x1f7
SRAMSimple g  0x1f7 := 0xcd, undefined (uninitialized or prior overlapping puts)
SRAMSimple G  0x0fb - 0x0fb
...
SRAMSimple G  0x122 - 0x122
SRAMSimple g  0x122 := 0x5c, undefined (uninitialized or prior overlapping puts)
*** PASSED *** Completed after 1069 cycles
f:id:msyksphinz:20191222012001p:plain
GTKWaveでの確認結果。

一応正しくデザインが生成できたようだ。もう少し解析してみよう。