今回は最近進めていた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
クラスを使うことが出来る。
また例にわざとらしく書いたけどBundle
はScalaのクラスなので内部にメソッドを定義できる。
/** * 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
本題:自分で作ったBundle
でReg
を作ったらハマった
ということで前提を紹介したところで本題。
先に自分が勘違いしていて「挙動が納得できーーーーん!!」ってなったコード簡略化したものを掲載する。
下記のコードで自分が勘違いしていたものを質問として書くと
- 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
にすると不一致になってa
とb
と同じサイクルで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サイクル遅れます。
原因は??
もうお察しの人が多いかと思うが今回のMyBundle
レジスタの作り方だと、インスタンスした時に全ての信号がReg
になってしまう。
このため、上記のRTLで見たような論理が生成されることになるということだ。
これはFIRRTLのMyBundleレジスタの宣言を見ても気づくことが出来るものだった。
reg myTypeReg : {a : UInt<8>, b : UInt<8>, valid : UInt<1>}, clock @[MyBundle.scala 30:22] <- 全部Regで宣言される
解決策
ではこれを意図通りにBundle
で束ねた状態で扱いつつ、valid
がa
/b
と同時に変化するようにするにはどうすればいいかを考えておく。
方法だがvalid
をMyBundle
のメソッドにすれば解決可能だ。
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
という信号名では実体化しないのでわかりにくかったりする。
でも変数宣言してデコード書くよりは行数的にも少ないので、簡単な論理ならメソッドにしておくのが良いのかな、、と思った。