前回の記事ではChiselの標準ライブラリに含まれているビット単位の操作を行うブロックをを紹介した。
今日はChiselからOneHotエンコードのユーティリティを勉強していく。
Module 3.2幕間: Chiselの標準ライブラリ
OneHotエンコード・ユーティリティ・ブロック
Chiselには以下のOneHotデータに関するライブラリが用意されている。
UIntToOH
:UInt
型のデータをOneHotなデータに変換OHToUInt
: OneHotなデータをUInt
型に変換
UIntToOH
早速例を見ていこう。まずはUIntToOH
から
Driver(() => new Module { // Example circuit using UIntToOH val io = IO(new Bundle { val in = Input(UInt(4.W)) val out = Output(UInt(16.W)) }) io.out := UIntToOH(io.in) }) { c => new PeekPokeTester(c) { poke(c.io.in, 0) println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}") poke(c.io.in, 1) println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}") poke(c.io.in, 8) println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}") poke(c.io.in, 15) println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}") } }
これまでChiselの標準ライブラリを紹介して来た中で紹介したものしか使われていないので、特に書くこともない。。
上記を実行すると以下の出力が得られる。
[info] [0.001] Elaborating design... [info] [0.640] Done elaborating. Total FIRRTL Compile Time: 183.1 ms Total FIRRTL Compile Time: 9.7 ms End of dependency graph Circuit state created [info] [0.002] SEED 1545485710840 [info] [0.004] in=0, out=0b1 [info] [0.005] in=1, out=0b10 [info] [0.005] in=8, out=0b100000000 [info] [0.006] in=15, out=0b1000000000000000 test cmd2Helperanonfun1anon2 Success: 0 tests passed in 5 cycles taking 0.019298 seconds [info] [0.008] RAN 0 CYCLES PASSED
テスト中のprintln
で出力されている部分を見るとわかると思うが、それぞれpoke
を使って入力したUInt
のデータがOneHotなデータに変換されているのがわかると思う。
生成されるRTL
なお、この回路をRTL化すると以下の様になる。
module cmd3HelperUIntToOHWrapper( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [3:0] io_in, // @[:@6.4] output [15:0] io_out // @[:@6.4] ); wire [15:0] _T_10; // @[OneHot.scala 45:35:@8.4] assign _T_10 = 16'h1 << io_in; // @[OneHot.scala 45:35:@8.4] assign io_out = _T_10; endmodule
、、、まあ、ものすごくシンプルで、そうなるよね。。。と言ったところか。これ使うんなら、ビット幅はパラメタライズして使うかな、多分。
Chiselのソースコード
これだけじゃなんだかなぁ。。。という気もするので、実際のChiselのソースコードを確認してみよう。
object UIntToOH { def apply(in: UInt): UInt = 1.U << in def apply(in: UInt, width: Int): UInt = width match { case 0 => 0.U(0.W) case 1 => 1.U(1.W) case _ => val shiftAmountWidth = log2Ceil(width) val shiftAmount = in.pad(shiftAmountWidth)(shiftAmountWidth - 1, 0) (1.U << shiftAmount)(width - 1, 0) } }
new
が不要だから、そうなんだろうなとは思っていたが、これもobject
で作れられているのね。
あと、実はインスタンス時に第二引数でビット幅が指定できるようになってた。所望のビット幅入れとくとそこで切り詰めてくれるみたい。
例えば上のテストコードのUIntToOH(io.in)
をUIntToOH(io.in, 3)
にして実行すると以下の様にテストの出力が変わる。
[info] [0.000] Elaborating design... [info] [0.068] Done elaborating. Total FIRRTL Compile Time: 8.7 ms Total FIRRTL Compile Time: 6.4 ms End of dependency graph Circuit state created [info] [0.000] SEED 1545488263600 [info] [0.001] in=0, out=0b1 [info] [0.002] in=1, out=0b10 [info] [0.002] in=8, out=0b1 [info] [0.003] in=15, out=0b0 test cmd22Helperanonfun1anon2 Success: 0 tests passed in 5 cycles taking 0.007413 seconds [info] [0.004] RAN 0 CYCLES PASSED
OHToUInt
続いて、OHToUInt
だ。これは機能的にはUIntToOH
の逆方向なので、サンプルコードで使われるテストも先ほどのテストコードをちょうどひっくり返したようなものになる。
Driver(() => new Module { // Example circuit using OHToUInt val io = IO(new Bundle { val in = Input(UInt(16.W)) val out = Output(UInt(4.W)) }) io.out := OHToUInt(io.in) }) { c => new PeekPokeTester(c) { poke(c.io.in, Integer.parseInt("0000 0000 0000 0001".replace(" ", ""), 2)) println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}") poke(c.io.in, Integer.parseInt("0000 0000 1000 0000".replace(" ", ""), 2)) println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}") poke(c.io.in, Integer.parseInt("1000 0000 0000 0001".replace(" ", ""), 2)) println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}") // Some invalid inputs: // None high poke(c.io.in, Integer.parseInt("0000 0000 0000 0000".replace(" ", ""), 2)) println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}") // Multiple high poke(c.io.in, Integer.parseInt("0001 0100 0010 0000".replace(" ", ""), 2)) println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}") } }
ちょっと異なっているのは、「入力の信号がOneHotじゃ無いものが含まれていた場合にどうなるか」という部分についてを確認するコードが含まれているところか。
これを実行すると以下の様になる。
[info] [0.000] Elaborating design... [info] [0.015] Done elaborating. Total FIRRTL Compile Time: 27.5 ms Total FIRRTL Compile Time: 20.0 ms End of dependency graph Circuit state created [info] [0.000] SEED 1545486378310 [info] [0.003] in=0b1, out=0 [info] [0.005] in=0b10000000, out=7 [info] [0.007] in=0b1000000000000001, out=15 [info] [0.009] in=0b0, out=0 [info] [0.011] in=0b1010000100000, out=15 test cmd4Helperanonfun1anon2 Success: 0 tests passed in 5 cycles taking 0.019214 seconds [info] [0.012] RAN 0 CYCLES PASSED
OneHotじゃ無いデータが来た時の挙動だけ抜粋しておこう。
- データがALL 0 の場合:出力は"0"になる
- 複数のビットが0x1の場合:出力はMSB側のビットが優先される
一応RTLを確認してみよう。こんな出力が得られた。2のべき乗単位で分解したデータを評価して、重み持たせてる感じ。
module cmd6HelperOHToUIntWrapper( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [15:0] io_in, // @[:@6.4] output [3:0] io_out // @[:@6.4] ); wire [7:0] _T_9; // @[OneHot.scala 26:18:@8.4] wire [7:0] _T_10; // @[OneHot.scala 27:18:@9.4] wire _T_12; // @[OneHot.scala 28:14:@10.4] wire [7:0] _T_13; // @[OneHot.scala 28:28:@11.4] wire [3:0] _T_14; // @[OneHot.scala 26:18:@12.4] wire [3:0] _T_15; // @[OneHot.scala 27:18:@13.4] wire _T_17; // @[OneHot.scala 28:14:@14.4] wire [3:0] _T_18; // @[OneHot.scala 28:28:@15.4] wire [1:0] _T_19; // @[OneHot.scala 26:18:@16.4] wire [1:0] _T_20; // @[OneHot.scala 27:18:@17.4] wire _T_22; // @[OneHot.scala 28:14:@18.4] wire [1:0] _T_23; // @[OneHot.scala 28:28:@19.4] wire _T_24; // @[CircuitMath.scala 30:8:@20.4] wire [1:0] _T_25; // @[Cat.scala 30:58:@21.4] wire [2:0] _T_26; // @[Cat.scala 30:58:@22.4] wire [3:0] _T_27; // @[Cat.scala 30:58:@23.4] assign _T_9 = io_in[15:8]; // @[OneHot.scala 26:18:@8.4] assign _T_10 = io_in[7:0]; // @[OneHot.scala 27:18:@9.4] assign _T_12 = _T_9 != 8'h0; // @[OneHot.scala 28:14:@10.4] assign _T_13 = _T_9 | _T_10; // @[OneHot.scala 28:28:@11.4] assign _T_14 = _T_13[7:4]; // @[OneHot.scala 26:18:@12.4] assign _T_15 = _T_13[3:0]; // @[OneHot.scala 27:18:@13.4] assign _T_17 = _T_14 != 4'h0; // @[OneHot.scala 28:14:@14.4] assign _T_18 = _T_14 | _T_15; // @[OneHot.scala 28:28:@15.4] assign _T_19 = _T_18[3:2]; // @[OneHot.scala 26:18:@16.4] assign _T_20 = _T_18[1:0]; // @[OneHot.scala 27:18:@17.4] assign _T_22 = _T_19 != 2'h0; // @[OneHot.scala 28:14:@18.4] assign _T_23 = _T_19 | _T_20; // @[OneHot.scala 28:28:@19.4] assign _T_24 = _T_23[1]; // @[CircuitMath.scala 30:8:@20.4] assign _T_25 = {_T_22,_T_24}; // @[Cat.scala 30:58:@21.4] assign _T_26 = {_T_17,_T_25}; // @[Cat.scala 30:58:@22.4] assign _T_27 = {_T_12,_T_26}; // @[Cat.scala 30:58:@23.4] assign io_out = _T_27; endmodule
Chiselのソースコード
こちらも実際のChiselのソースコードを確認してみよう。
なるほど、再帰呼出し使って計算するのか。ほんとにこのコードがそのままRTLに変換されてるってことか。
object OHToUInt { def apply(in: Seq[Bool]): UInt = apply(Cat(in.reverse), in.size) def apply(in: Vec[Bool]): UInt = apply(in.asUInt, in.size) def apply(in: Bits): UInt = apply(in, in.getWidth) def apply(in: Bits, width: Int): UInt = { if (width <= 2) { Log2(in, width) } else { val mid = 1 << (log2Ceil(width)-1) val hi = in(width-1, mid) val lo = in(mid-1, 0) Cat(hi.orR, apply(hi | lo, mid)) } } }
こっちはapply
が4つもある。バリエーションは以下の通り。
(in: Bits)
:これがサンプルコードで見た使い方。ビット幅も計算してよしなにやってくれる。(in: Bits, width: Int)
:UIntToOH
の場合と同様にビット幅で切り詰め。(in: Seq[Bool])
: 入力がBool
型で構成されたSeq
でもOK。Seq
の場合は値がひっくり返る模様。まあイメージ的にも合ってるかな。(in: Vec[Bool])
: 入力がBool
型で構成されたVec
でもOK
今日勉強した2つのライブラリは結構使えそうな気配。。レジスタのアドレスをビットマップにするとか、その逆も。
ということで今日はここまで。次はMux
を見ていく。