ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Chisel Bootcamp - 幕間(1) - ChiselのAPI - Decoupled

スポンサーリンク

前回の記事ではChisel BootcampはModule3.2の練習問題でRISC-VのISA向けのレジスタファイルを作成した。

www.tech-diningyo.info

今日はModule3.2とModule3.3の間にある幕間の章でChiselの標準ライブラリについて紹介していく。 最初はDecoupledから。

Module 3.2幕間: Chiselの標準ライブラリ

モチベーション

いつもどおりモチベーションを引用。

Chiselは全てを再利用するので、標準ライブラリとして提供するのはRTLの相互運用を促進するインターフェースやハードウェアのブロックとして一般的に使われるものとするのが理にかなっている。

Chiselチートシート

ここで紹介する標準ライブラリの機能は以前にも紹介したChiselのチートシートに載っている。

とりあえずチートシートに載っているものをざっと列挙してみよう。このページでも紹介していくが、Chisel3のAPIへのリンクも貼ってあるのでそちらも一緒に見てみると良いかも。

Decoupled : 標準のReady-Validインターフェース

まずはDecoupledIOから。これはChiselが提供している一般的に使用されるインターフェースの一つでデータ転送のためのready-validインターフェースの機能を提供するものだ。

とりあえず使い方は以下の感じらしい。

val myChiselData = UInt(8.W)
// or any Chisel data type, such as Bool(), SInt(...), or even custom Bundles
val myDecoupled = Decoupled(myChiselData)

実際どういうこと??とも思うので試してみる。

class MyDecoupledIO extends Module {
    val io = IO(new Bundle {
        val dcp = Decoupled(UInt(8.W))
    })
    
    val ctr = RegInit(2.U(2.W))
    val r = RegInit(0.U(8.W))
    
    val valid = (ctr === 0.U)
    val update = io.dcp.ready && valid

    when (update) {
        r := r + 1.U
    }
    
    when (ctr === 0.U) {
        when (update) {
            ctr := 2.U
        }
    }.otherwise {
        ctr := ctr - 1.U
    }
    
    io.dcp.valid := valid
    io.dcp.bits := r
}

回路的には何も意味が無い回路となっており、ctrが0になった際にvalidが上がって、readyが入ってくるとctrrが更新されるだけとなる。

とりあえずDecoupledを使う際に大事なのは、以下の点。

  • DecoupledはIOの一種で宣言すると、以下の3つの信号が宣言される
    • valid : bitsが有効であることを示す出力信号
    • ready : 対向のモジュールがデータを受け取れるかどうかを示す信号
    • bits: 出力データ

valid/readyを使ったデータのやり取りを行う際には、各信号向けにxx_valid/xx_ready/xx_dataとか書くよりはこっちを使ったほうが、シンプルに書けるはず。

で、上記のなんの意味もない回路ではあるが実際にRTLを生成すると以下の出力が得られる。

回路の中身よりは、宣言したDecoupled

  • io_dcp_ready
  • io_dcp_valid
  • io_dcp_bits

マッピングされていることが大事。

module decoupled_io( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input        io_dcp_ready, // @[:@6.4]
  output       io_dcp_valid, // @[:@6.4]
  output [7:0] io_dcp_bits // @[:@6.4]
);
  reg [1:0] ctr; // @[cmd21.sc 6:22:@8.4]
  reg [31:0] _RAND_0;
  reg [7:0] r; // @[cmd21.sc 7:20:@9.4]
  reg [31:0] _RAND_1;
  wire  valid; // @[cmd21.sc 9:22:@10.4]
  wire  update; // @[cmd21.sc 10:31:@11.4]
  wire [8:0] _T_17; // @[cmd21.sc 13:16:@13.6]
  wire [7:0] _T_18; // @[cmd21.sc 13:16:@14.6]
  wire [7:0] _GEN_0; // @[cmd21.sc 12:19:@12.4]
  wire [1:0] _GEN_1; // @[cmd21.sc 17:23:@19.6]
  wire [2:0] _T_23; // @[cmd21.sc 21:20:@24.6]
  wire [2:0] _T_24; // @[cmd21.sc 21:20:@25.6]
  wire [1:0] _T_25; // @[cmd21.sc 21:20:@26.6]
  wire [1:0] _GEN_2; // @[cmd21.sc 16:24:@18.4]
  assign valid = ctr == 2'h0; // @[cmd21.sc 9:22:@10.4]
  assign update = io_dcp_ready & valid; // @[cmd21.sc 10:31:@11.4]
  assign _T_17 = r + 8'h1; // @[cmd21.sc 13:16:@13.6]
  assign _T_18 = _T_17[7:0]; // @[cmd21.sc 13:16:@14.6]
  assign _GEN_0 = update ? _T_18 : r; // @[cmd21.sc 12:19:@12.4]
  assign _GEN_1 = update ? 2'h2 : ctr; // @[cmd21.sc 17:23:@19.6]
  assign _T_23 = ctr - 2'h1; // @[cmd21.sc 21:20:@24.6]
  assign _T_24 = $unsigned(_T_23); // @[cmd21.sc 21:20:@25.6]
  assign _T_25 = _T_24[1:0]; // @[cmd21.sc 21:20:@26.6]
  assign _GEN_2 = valid ? _GEN_1 : _T_25; // @[cmd21.sc 16:24:@18.4]
  assign io_dcp_valid = valid;
  assign io_dcp_bits = r;

  always @(posedge clock) begin
    if (reset) begin
      ctr <= 2'h2;
    end else begin
      if (valid) begin
        if (update) begin
          ctr <= 2'h2;
        end
      end else begin
        ctr <= _T_25;
      end
    end
    if (reset) begin
      r <= 8'h0;
    end else begin
      if (update) begin
        r <= _T_18;
      end
    end
  end
endmodule

最後に実装時の注意点が3つ書いてあったので、それを載せておく。

  1. readyvalidは互いが組み合わせの論理とならないようにする。さもないと、組み合わせのループによって合成が出来ないという結果になってしまう。
  2. またreadyは対向がデータを受け取ることが出来るかどうか、validはモジュール内に送信するデータがあるかどうか、という点にのみ依存するようにする。
  3. トランザクションが完了したあと(次のクロックサイクル)でデータは更新されるようにする。

ということで今日はここまで。次からは最初に載せた各APIの例を見ていくことにする。