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

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

Chiselの文法 - 入門編 〜その6:ChiselのVec〜

スポンサーリンク

前回の続きでChiselの文法入門編その6
今回はChiselにおける拡張型であるVecBundleの2つについてまとめて書こうを思ったのですが、思いの外膨らんだので、Vecメインにしました。

Chisel入門編〜その6:Vec〜

その2でChiselの”型”として、以下を紹介しましたが実はChiselにはまだ幾つか”型”が存在しています。
今回のその6ではその残りの型の中からVecBundleについての概要を紹介し、主にVecの使い方についてをまとめます。
一言で書くとBundleVecは共にChiselの基本の型を束ねる目的で使用される拡張型といえる存在です。
Verlog HDL及びSystem Verilogで言うと、以下のようなものに置き換えることが可能です。

  • Vec - メモリ配列
  • Bundle - 構造体

VecBundleの位置づけ

これについては以下の画像を見てもらうと分かりやすいと思います。

f:id:diningyo-kpuku-jougeki:20190602144130p:plain

ご覧いただいたとおりでBool/UIntといった型はBitsクラスを継承しているのに対して、Vec/BundleはそれぞれAggregate/Recordクラスを継承したものになっています。
これらのクラスは内部にData型のインスタンスSeqListMapに入れて管理しており、複数のData型をまとめて扱うことが出来るようになっています。
これだけだとScalaSeq等と一緒じゃん。。となるのですがVecは実際の回路上で動的に変化する要素に対して使えるところに大きな違いがあります。 では使い方についてを見ていこうと思います。

Vec

冒頭に記載した通りVecVerilog HDLにおけるメモリ配列に相当する概念です。
すぐ上に書いたとおりScalaだとSeqと似たデータ型ですが、Chiselにおいてはこの2つは、以下のように評価のタイミングが異なっています。

  • ScalaSeq : Chiselのエラボレート時に評価され、ハードウェア要素が固定される。
val addr = Wire(UInt(2.W))
val a = Seq.fill(4)(RegInit(0.U(32.W)))
// 以下はエラーになる:
//   → Seqで宣言したRegInitの配列の要素参照に
//      Chiselのハードウェア要素であるaddrを使っているため
a(addr) := io.wrdata
  • ChiselのVec : 回路の動作中の信号を使って評価される。
val addr = Wire(UInt(2.W))
val a = Reg(Vec(4, UInt(32.W)))
a(addr) := io.wrdata

早い話がChiselのVecを使うと通常の論理回路におけるアドレス信号等の信号を使ってVecで宣言したChiselのハードウェア要素に動的にアクセスが可能になるということです。
使い分けとしては以下のように考えると良いと思います。

  • Scalaのcollection型:エラボレート時に回路構成が決まるもの(例えばマルチプレクサの入力数とか、フィルタの段数とか)
  • ChiselのVec:論理回路の動作中に他のChiselのハードウェア要素に応じて参照する場所が変化するもの(レジスタファイルのアドレスとか)

Vecの宣言

基本形

Vecの宣言は以下のような形になります。

val a = Vec(<要素数>, <Chiselの型のインスタンス>)

素数10で8bitのUIntVecを宣言すると以下のような形になります。

val a = Vec(10, UInt(8.W))

なお、VecもChiselの”ハードウェア要素”ではなくChiselの"型"になるので、実際にハードウェアとして作成する論理回路の一部にするためには”ハードウェア要素”でラッピングする必要があります。
上記の”要素数10で8bitのUIntVec”をハードウェアとしてインスタンスする場合は以下のような感じです。

  • Wireの場合
val wireVecA = Wire(Vec(10, UInt(8.W)))
  • Regの場合
val wireVecA = Reg(Vec(10, UInt(8.W)))

Chiselを書き始めた時はこの区別がついてなかったので、以下のようにVecを外側にしてエラーが発生しまくってました。。。

  • 注意:以下はエラーになる例です
val wireVecA = Vec(10, Wire(UInt(8.W))) // エラー

エラーメッセージは前回紹介したコレ↓

[error] (run-main-5) chisel3.core.Binding$ExpectedChiselTypeException:
vec type 'chisel3.core.UInt@a' must be a Chisel type, not hardware

初期値が必要な場合

さて上記でVecの宣言の仕方は分かったと思いますが、ここで以下のような場合はどうするの??と思った方がいるのではないでしょうか?

  • Q:初期値付きのレジスタRegInitや既存の信号を受けるRegNextの場合も一緒で良いのか?

この問についての答えですが、以下のようになります。

  • A:ちょっと宣言の仕方が変わります。

早速見てみましょう。

RegInitを使う場合
val vecRegInit = RegInit(VecInit(Seq.fill(10)(0.U)))

見てお気づきになられたと思いますが、上記の例では全て0.Uレジスタが初期化されます。

このやり方は以下のChiselのwikiで発見しました。

github.com

RegNextを使う場合

RegNextは2回目で紹介したとおり、特定の信号をレジスタ受けする場合に使用しますが、初期値のあり/なしを選択することが可能です。これらのケースについてそれぞれの記述を見てみましょう。

val vecReg = Reg(Vec(10, UInt(8.W)))
// 初期値なしの場合
val vecRegNextWoInit = RegNext(vecReg)   // 元のハードウェア要素のVecを入れればOK
// 初期値ありの場合
val vecRegNextWithInit = RegNext(vecReg, VecInit(Seq.fill(10)(0.U(8.W))) // 別途特定の値の初期化したVecInitが必要

ここでは全てのレジスタを固定値0.Uで生成するためにSeq.fillを使っていますが、それぞれ別の値で初期化したい場合にはVecInitに渡すSeqをその値で埋めたものを渡せばOKです。

レジスタファイルの例で見るVecの使い方

例えばVerilog HDLでCPUで使うようなレジスタファイルを作成するケースを考えてみます。
その場合、メモリ配列を使って以下のような感じになると思います。 使う場合コードは以下のようにgenerate構文を使って処理するようなイメージになると思います。

- Verilog HDLでレジスタファイル

module reg_file
(
  input         clk,
  input [4:0]   rdaddr_0,
  output [31:0] rddata_0,
  input [4:0]   rdaddr_1,
  output [31:0] rddata_0,
  input [4:0]   wraddr,
  input         wren,
  input [31:0]  wrdata
  );

  reg [31:0] regs[0:31];

  always @(posedge clk) begin
      if (wren) begin
          regs[wraddr] <= wrdata;
      end
  end

  rddata_0 = regs[rdaddr_0];
  rddata_1 = regs[rdaddr_1];
endmodule

同じものをChiselで書いてみると以下のようになります。

class RegFile extends Module {
  val io = IO(new Bundle {
    val rdaddr_0 = Input(UInt(5.W))
    val rddata_0 = Output(UInt(32.W))
    val rdaddr_1 = Input(UInt(5.W))
    val rddata_1 = Output(UInt(32.W))
    val wren = Input(Bool())
    val wraddr = Input(UInt(5.W))
    val wrdata = Input(UInt(32.W))
  })

  val regFileVec = RegInit(VecInit(Seq.fill(10)(false.B)))

  when (io.wren) {
    regFileVec(io.wraddr) := io.wrdata
  }

  io.rddata_0 := regFileVec(io.rdaddr_0)
  io.rddata_1 := regFileVec(io.rdaddr_1)
}

ほぼ見た目一緒ですね(´・ω・`)

せっかくVecが使えるようになったのでリード系の端子もまとめてしまいましょう。

class RegFile extends Module {
  val io = IO(new Bundle {
    val rdaddr = Input(Vec(2, UInt(5.W)))
    val rddata = Output(Vec(2, UInt(32.W)))
    val wren = Input(Bool())
    val wraddr = Input(UInt(5.W))
    val wrdata = Input(UInt(32.W))
  })

  val regFileVec = RegInit(VecInit(Seq.fill(32)(false.B)))

  when (io.wren) {
    regFileVec(io.wraddr) := io.wrdata
  }

  for ((rddata, addr) <- io.rddata.zip(io.rdaddr)) {
    rddata := regFileVec(addr)
  }
}

もう少しScalaっぽくするためにforループ部分を高階関数を使って書き直すと以下のようになります。
ですがコレくらいのものだと、上の例のようにzipで作ったイテレータforループで回したほうがわかりやすいですね^^;

  io.rddata.zip(io.rdaddr).foreach {
    case (data, addr) => data := regFileVec(addr)
  }

因みに”Verilog HDLのメモリ配列に相当する要素”と書きましたが、これは文法上の扱いだけでChiselのVecはRTLに変換するとVecに指定したChiselのハードウェア要素にインデックスが付与された形でベタにRTL上に展開されます。
以下は上のRegFileの要素数を4に変更して生成したRTLです。

module RegFile( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [4:0]  io_rdaddr_0, // @[:@6.4]
  output [31:0] io_rddata_0, // @[:@6.4]
  input  [4:0]  io_rdaddr_1, // @[:@6.4]
  output [31:0] io_rddata_1, // @[:@6.4]
  input         io_wren, // @[:@6.4]
  input  [4:0]  io_wraddr, // @[:@6.4]
  input  [31:0] io_wrdata // @[:@6.4]
);
  reg  regFileVec_0; // @[cmd18.sc 12:27:@13.4]
  reg [31:0] _RAND_0;
  reg  regFileVec_1; // @[cmd18.sc 12:27:@13.4]
  reg [31:0] _RAND_1;
  reg  regFileVec_2; // @[cmd18.sc 12:27:@13.4]
  reg [31:0] _RAND_2;
  reg  regFileVec_3; // @[cmd18.sc 12:27:@13.4]
  reg [31:0] _RAND_3;
  wire [1:0] _T_63; // @[:@15.6]
  wire  _regFileVec_T_63; // @[cmd18.sc 15:27:@16.6]
  wire  _GEN_0; // @[cmd18.sc 15:27:@16.6]
  wire  _GEN_1; // @[cmd18.sc 15:27:@16.6]
  wire  _GEN_2; // @[cmd18.sc 15:27:@16.6]
  wire  _GEN_3; // @[cmd18.sc 15:27:@16.6]
  wire  _GEN_4; // @[cmd18.sc 14:18:@14.4]
  wire  _GEN_5; // @[cmd18.sc 14:18:@14.4]
  wire  _GEN_6; // @[cmd18.sc 14:18:@14.4]
  wire  _GEN_7; // @[cmd18.sc 14:18:@14.4]
  wire [1:0] _T_67; // @[:@18.4]
  wire  _GEN_9; // @[cmd18.sc 18:15:@19.4]
  wire  _GEN_10; // @[cmd18.sc 18:15:@19.4]
  wire  _GEN_11; // @[cmd18.sc 18:15:@19.4]
  wire [1:0] _T_71; // @[:@20.4]
  wire  _GEN_13; // @[cmd18.sc 19:15:@21.4]
  wire  _GEN_14; // @[cmd18.sc 19:15:@21.4]
  wire  _GEN_15; // @[cmd18.sc 19:15:@21.4]
  assign _T_63 = io_wraddr[1:0]; // @[:@15.6]
  assign _regFileVec_T_63 = io_wrdata[0]; // @[cmd18.sc 15:27:@16.6]
  assign _GEN_0 = 2'h0 == _T_63 ? _regFileVec_T_63 : regFileVec_0; // @[cmd18.sc 15:27:@16.6]
  assign _GEN_1 = 2'h1 == _T_63 ? _regFileVec_T_63 : regFileVec_1; // @[cmd18.sc 15:27:@16.6]
  assign _GEN_2 = 2'h2 == _T_63 ? _regFileVec_T_63 : regFileVec_2; // @[cmd18.sc 15:27:@16.6]
  assign _GEN_3 = 2'h3 == _T_63 ? _regFileVec_T_63 : regFileVec_3; // @[cmd18.sc 15:27:@16.6]
  assign _GEN_4 = io_wren ? _GEN_0 : regFileVec_0; // @[cmd18.sc 14:18:@14.4]
  assign _GEN_5 = io_wren ? _GEN_1 : regFileVec_1; // @[cmd18.sc 14:18:@14.4]
  assign _GEN_6 = io_wren ? _GEN_2 : regFileVec_2; // @[cmd18.sc 14:18:@14.4]
  assign _GEN_7 = io_wren ? _GEN_3 : regFileVec_3; // @[cmd18.sc 14:18:@14.4]
  assign _T_67 = io_rdaddr_0[1:0]; // @[:@18.4]
  assign _GEN_9 = 2'h1 == _T_67 ? regFileVec_1 : regFileVec_0; // @[cmd18.sc 18:15:@19.4]
  assign _GEN_10 = 2'h2 == _T_67 ? regFileVec_2 : _GEN_9; // @[cmd18.sc 18:15:@19.4]
  assign _GEN_11 = 2'h3 == _T_67 ? regFileVec_3 : _GEN_10; // @[cmd18.sc 18:15:@19.4]
  assign _T_71 = io_rdaddr_1[1:0]; // @[:@20.4]
  assign _GEN_13 = 2'h1 == _T_71 ? regFileVec_1 : regFileVec_0; // @[cmd18.sc 19:15:@21.4]
  assign _GEN_14 = 2'h2 == _T_71 ? regFileVec_2 : _GEN_13; // @[cmd18.sc 19:15:@21.4]
  assign _GEN_15 = 2'h3 == _T_71 ? regFileVec_3 : _GEN_14; // @[cmd18.sc 19:15:@21.4]
  assign io_rddata_0 = {{31'd0}, _GEN_11};
  assign io_rddata_1 = {{31'd0}, _GEN_15};

  always @(posedge clock) begin
    if (reset) begin
      regFileVec_0 <= 1'h0;
    end else begin
      if (io_wren) begin
        if (2'h0 == _T_63) begin
          regFileVec_0 <= _regFileVec_T_63;
        end
      end
    end
    if (reset) begin
      regFileVec_1 <= 1'h0;
    end else begin
      if (io_wren) begin
        if (2'h1 == _T_63) begin
          regFileVec_1 <= _regFileVec_T_63;
        end
      end
    end
    if (reset) begin
      regFileVec_2 <= 1'h0;
    end else begin
      if (io_wren) begin
        if (2'h2 == _T_63) begin
          regFileVec_2 <= _regFileVec_T_63;
        end
      end
    end
    if (reset) begin
      regFileVec_3 <= 1'h0;
    end else begin
      if (io_wren) begin
        if (2'h3 == _T_63) begin
          regFileVec_3 <= _regFileVec_T_63;
        end
      end
    end
  end
endmodule

Chiselの記述によってRTLに変換した際に凄まじいif文のネストを含むRTLが生成されることがありますので、変換したRTLはチェックすることをお勧めします(前にRISC-VのレジスタファイルをVecで生成したら32個のレジスタ分だけif文のネストが出来たりしました)。

上記の例で見た様にChiselのVec型は同じ型のIOポートを纏めるためにも使用できます。
これを使うことでマルチプレクサやアービターのような回路をすっきりさせることが出来るようになります。
ここの例ではまだ適用していませんが、次に紹介予定のBundleと組み合わせることで更に柔軟な表現が可能となり、Chiselで実装をするメリットが大きくなってくると個人的には思っています。
ということでChiselのaggregate型の一つVecの解説でした。
次回はもうひとつのaggregate型であるBundleについて紹介していく予定です。
第7回に続く。