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

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

自分で作ったBundleでレジスタ作ったらハマった話

スポンサーリンク

今回は最近進めていたRISC-Vの実装の際にBundleの使い方でハマったのでそれについての話を

Bundleのおさらい

基本的な使い方

ChiselのBundleは複数の基本型を構造化するために使用できるものであった。
簡単な例を見てみよう。

/**
 * IOポートを束ねるためにBundleを使う
 *  → たぶん一番最初に見る使い方
 */
class MyType extends Bundle {
  val a = Input(UInt(8.W))
  val b = Input(Bool())
}

class MyModule extends Module {
  // MyTypeのインスタンスでIOポートを作成
  val io = IO(new MyType)
}

上記のコードをRTLに変換すると、以下のようにMyTypeがモジュールのIOとして実体化する。

module cmd5HelperMyModule( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [7:0] io_a, // @[:@6.4]
  input        io_b // @[:@6.4]
);
  initial begin end
endmodule

この例でわかるようにBundleはChiselの他のデータ型を構造化出来るもので、オブジェクト指向言語のクラスに相当するものと言える。 ハードウェアの括りで言うと、System Verilogのクラスに当たる概念と思ってもらうと良いと思う。
これを使うことでChiselの”新しい型を定義して使う”ことが出来る。

BundleインスタンスRegを作る

先に見たとおりBundleを使って作った自分のクラスを使ってIOを宣言することが出来た。
IOポートが作れることから推測できると思うが、他のChiselのハードウェアを作る際にも自分で定義したBundleクラスを使うことが出来る。
また例にわざとらしく書いたけどBundleScalaのクラスなので内部にメソッドを定義できる。

/**
 * BundleもScalaのクラスなのでメソッドを定義できる
 */
class MyType extends Bundle {
  val a = Input(UInt(8.W))
  val b = Input(Bool())

  /**
   * 超わざとらしいし、そもそも":="で足りるけどメソッド化
   */
  def connect[T <: MyType](v: T): Unit = {
    a := v.a
    b := v.b
  }
}

class MyModule extends Module {
  // MyTypeのインスタンスでIOポートを作成
  val io = IO(new Bundle {
      val in = new MyType
      val sel = Input(Bool())
      val out = Flipped(new MyType)
  })

  val myTypeWire = Wire(new MyType) // MyTypeでWireを作成
  val myTypeReg = Reg(new MyType)   // MyTypeでRegを作成

  myTypeWire.connect(io.in)
  myTypeReg := myTypeWire
  io.out := myTypeReg
}

上記のコードからRTLを生成すると以下のようにレジスタで入力のMyTypeを受けたものがoutに出力される。

module cmd7HelperMyModule( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [7:0] io_in_a, // @[:@6.4]
  input        io_in_b, // @[:@6.4]
  output [7:0] io_out_a, // @[:@6.4]
  output       io_out_b // @[:@6.4]
);
  reg [7:0] myTypeReg_a; // @[cmd7.sc 14:22:@9.4]
  reg [31:0] _RAND_0;
  reg  myTypeReg_b; // @[cmd7.sc 14:22:@9.4]
  reg [31:0] _RAND_1;
  assign io_out_a = myTypeReg_a;
  assign io_out_b = myTypeReg_b;

  always @(posedge clock) begin
    myTypeReg_a <= io_in_a;
    myTypeReg_b <= io_in_b;
  end
endmodule

本題:自分で作ったBundleRegを作ったらハマった

ということで前提を紹介したところで本題。
先に自分が勘違いしていて「挙動が納得できーーーーん!!」ってなったコード簡略化したものを掲載する。
下記のコードで自分が勘違いしていたものを質問として書くと

  • MyModuleのmyTypeReg.validはio.inの変化に対して何サイクル遅れるか?

ということだ。
そんなわけで上記の問の答えがわかる方は、これ以上読んでもきっと情報無いですm(_ _)m

import chisel3._

/**
  * BundleもScalaのクラスなのでメソッドを定義できる
  */
class MyType extends Bundle {
  val a = UInt(8.W)
  val b = UInt(8.W)
  val valid = Bool()

  def decode(v: UInt): Unit = {
    a := v(15, 8)
    b := v(7, 0)
    valid := a === b
  }
}

/**
  * Bundleで作ったRegの出力信号を確認するだけのモジュール
  */
class MyModule extends Module {
  // MyTypeのインスタンスでIOポートを作成
  val io = IO(new Bundle {
    val in = Input(UInt(16.W))
    val out = Output(new MyType)
  })

  val myTypeReg = Reg(new MyType)   // MyTypeでRegを作成

  myTypeReg.decode(io.in)
  io.out := myTypeReg
}

テストコードを作って確認

そんなわけでテストコードを作って確認してみよう。
あえて自分が勘違いしていた挙動でテストを作成すると以下のようになる。
validの生成論理はa === bなのでio.in = 0xff01にすると不一致になってabと同じサイクルでfalse.Bに落ちる」ことをテストしている。

import chisel3.iotesters._

class MyBundleTester extends ChiselFlatSpec {

  behavior of "MyModule"

  it should "Bundleを使って作ったやつの副作用" in {
    Driver.execute(Array(
      "--generate-vcd-output=on"
    ), () => new MyModule) {
      c => new PeekPokeTester(c) {
        poke(c.io.in, 0xff01)

        // レジスタなので1サイクル後に変化する(と思ってた)
        step(1)
        expect(c.io.out.a, 0xff)
        expect(c.io.out.b, 0x1)
        expect(c.io.out.valid, false)
      }
    } should be (true)
  }
}

さて早速実行してみよう。

[info] Done compiling.
[info] [0.002] Elaborating design...
[info] [0.131] Done elaborating.
Total FIRRTL Compile Time: 267.1 ms
Total FIRRTL Compile Time: 77.7 ms
file loaded in 0.135968616 seconds, 12 symbols, 6 statements
[info] [0.001] SEED 1557497431467
[info] [0.003] EXPECT AT 1   io_out_valid got 1 expected 0 FAIL
test MyModule Success: 2 tests passed in 6 cycles in 0.028182 seconds 212.90 Hz
[info] [0.007] RAN 1 CYCLES FAILED FIRST AT CYCLE 1
[info] MyBundleTester:
[info] MyModule
[info] - should Bundleを使って作ったやつの副作用 *** FAILED ***
[info]   false was not true (MyBundleTester.scala:11)
[info] ScalaTest
[info] Run completed in 1 second, 84 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0

はい、FAILしましたね。11行目はvalidの部分なのでそこの認識が違っていたことが再確認できました。

RTLはどうなってる?

ということでRTLも確認してみよう。

module MyModule( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [15:0] io_in, // @[:@6.4]
  output [7:0]  io_out_a, // @[:@6.4]
  output [7:0]  io_out_b, // @[:@6.4]
  output        io_out_valid // @[:@6.4]
);
  reg [7:0] myTypeReg_a; // @[MyBundle.scala 30:22:@8.4]
  reg [31:0] _RAND_0;
  reg [7:0] myTypeReg_b; // @[MyBundle.scala 30:22:@8.4]
  reg [31:0] _RAND_1;
  reg  myTypeReg_valid; // @[MyBundle.scala 30:22:@8.4]
  reg [31:0] _RAND_2;
  assign io_out_a = myTypeReg_a; // @[MyBundle.scala 33:10:@17.4]
  assign io_out_b = myTypeReg_b; // @[MyBundle.scala 33:10:@16.4]
  assign io_out_valid = myTypeReg_valid; // @[MyBundle.scala 33:10:@15.4]
  always @(posedge clock) begin
    myTypeReg_a <= io_in[15:8];
    myTypeReg_b <= io_in[7:0];
    myTypeReg_valid <= myTypeReg_a == myTypeReg_b;
  end
endmodule

RTLを見れば一目瞭然でしたね。myTypeReg_valid <= myTypeReg_a == myTypeReg_b;となっており、a/bの比較結果がvalidに入るので1サイクル遅れる。。。と。
最初にハマった時はRISC-Vのデコード論理をBundleで書いていて、各種命令をデコードした1bitの信号に対してそれらを束ねて作ったvalid信号が2サイクル遅れてきてドハマりしました。。。 因みに波形は以下のようa/bの変化に対して更に1サイクル遅れます。

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

原因は??

もうお察しの人が多いかと思うが今回のMyBundleレジスタの作り方だと、インスタンスした時に全ての信号がRegになってしまう。
このため、上記のRTLで見たような論理が生成されることになるということだ。
これはFIRRTLのMyBundleレジスタの宣言を見ても気づくことが出来るものだった。

reg myTypeReg : {a : UInt<8>, b : UInt<8>, valid : UInt<1>}, clock @[MyBundle.scala 30:22] <- 全部Regで宣言される

解決策

ではこれを意図通りにBundleで束ねた状態で扱いつつ、valida/bと同時に変化するようにするにはどうすればいいかを考えておく。
方法だがvalidMyBundleのメソッドにすれば解決可能だ。

class MyType extends Bundle {
  val a = UInt(8.W)
  val b = UInt(8.W)

  def decode(v: UInt): Unit = {
    a := v(15, 8)
    b := v(7, 0)
  }

  def valid: Bool = a === b
}

ただしこの方法だとMyModuleクラスの出力ポートにはvalidが存在しなくなる。
というか出力ポートだけでなく、validを参照してもそれはメソッドの呼び出しに置き換わり、ハードウェア的にはMyBundle.a === MyBundle.bに置き換わってしまう。
そのため階層をまたぐようなケースだと、複数の階層でMyBundle.a === MyBundle.bの論理が複製されるのでちょっと注意が必要。
あと波形とってデバッグする場合にも当然validという信号名では実体化しないのでわかりにくかったりする。

でも変数宣言してデコード書くよりは行数的にも少ないので、簡単な論理ならメソッドにしておくのが良いのかな、、と思った。