前回の記事ではChiselの標準ライブラリに含まれているOneHotエンコードに関するブロックを紹介した。
今日はChiselのライブラリに用意されている2つのマルチプレクサについて見ていく。
Module 3.2幕間: Chiselの標準ライブラリ
マルチプレクサ
Chiselには以下の2つのマルチプレクサが存在している。
PriorityMux
: 選択用信号の一番小さいインデックスが出力されるMux1H
: 選択信号がOneHotな信号となることが保証出来る場合に使う。OneHotが前提なのでその分生成される回路は効率のいいものになる。
PriorityMux
早速サンプルを見ていこう。
Driver(() => new Module { // Example circuit using PriorityMux val io = IO(new Bundle { val in_sels = Input(Vec(2, Bool())) val in_bits = Input(Vec(2, UInt(8.W))) val out = Output(UInt(8.W)) }) io.out := PriorityMux(io.in_sels, io.in_bits) }) { c => new PeekPokeTester(c) { poke(c.io.in_bits(0), 10) poke(c.io.in_bits(1), 20) // Select higher index only poke(c.io.in_sels(0), 0) poke(c.io.in_sels(1), 1) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") // Select both - arbitration needed poke(c.io.in_sels(0), 1) poke(c.io.in_sels(1), 1) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") // Select lower index only poke(c.io.in_sels(0), 1) poke(c.io.in_sels(1), 0) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") } }
使い方としては、以下の様に選択用のビットマップ信号とそれに対応するVec
を入れるのが基本的なものとなる。
PriorityMux(io.in_sels, io.in_bits)
今回のケースではVec[Bool]
が選択用の2bitの信号で、Vec[UInt]
が入力のデータとなっている。
このサンプルを実行すると以下の出力が得られる。
[info] [0.001] Elaborating design... [info] [0.671] Done elaborating. Total FIRRTL Compile Time: 190.7 ms Total FIRRTL Compile Time: 10.8 ms End of dependency graph Circuit state created [info] [0.001] SEED 1545573966896 [info] [0.003] in_sels=Vector(0, 1), out=20 [info] [0.004] in_sels=Vector(1, 1), out=10 [info] [0.005] in_sels=Vector(1, 0), out=10 test cmd2Helperanonfun1anon2 Success: 0 tests passed in 5 cycles taking 0.022209 seconds [info] [0.008] RAN 0 CYCLES PASSED
ここではVec[Bool]
を使っているので2番めのテストケースでは
- Vec[0] = 1'b1
- Vec[1] = 1'b1
となるがインデックスの小さいほうが優先されるため、出力がout=20
となっている。
生成されるRTL
一応RTLも確認。
module cmd3Helperanonfun1anon2( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [7:0] io_in, // @[:@6.4] output [7:0] io_out // @[:@6.4] ); wire [3:0] _T_13; // @[Bitwise.scala 103:21:@10.4] wire [7:0] _T_14; // @[Bitwise.scala 103:31:@11.4] wire [3:0] _T_15; // @[Bitwise.scala 103:46:@12.4] wire [7:0] _GEN_0; // @[Bitwise.scala 103:65:@13.4] wire [7:0] _T_16; // @[Bitwise.scala 103:65:@13.4] wire [7:0] _T_18; // @[Bitwise.scala 103:75:@15.4] wire [7:0] _T_19; // @[Bitwise.scala 103:39:@16.4] wire [5:0] _T_23; // @[Bitwise.scala 103:21:@20.4] wire [7:0] _GEN_1; // @[Bitwise.scala 103:31:@21.4] wire [7:0] _T_24; // @[Bitwise.scala 103:31:@21.4] wire [5:0] _T_25; // @[Bitwise.scala 103:46:@22.4] wire [7:0] _GEN_2; // @[Bitwise.scala 103:65:@23.4] wire [7:0] _T_26; // @[Bitwise.scala 103:65:@23.4] wire [7:0] _T_28; // @[Bitwise.scala 103:75:@25.4] wire [7:0] _T_29; // @[Bitwise.scala 103:39:@26.4] wire [6:0] _T_33; // @[Bitwise.scala 103:21:@30.4] wire [7:0] _GEN_3; // @[Bitwise.scala 103:31:@31.4] wire [7:0] _T_34; // @[Bitwise.scala 103:31:@31.4] wire [6:0] _T_35; // @[Bitwise.scala 103:46:@32.4] wire [7:0] _GEN_4; // @[Bitwise.scala 103:65:@33.4] wire [7:0] _T_36; // @[Bitwise.scala 103:65:@33.4] wire [7:0] _T_38; // @[Bitwise.scala 103:75:@35.4] wire [7:0] _T_39; // @[Bitwise.scala 103:39:@36.4] assign _T_13 = io_in[7:4]; // @[Bitwise.scala 103:21:@10.4] assign _T_14 = {{4'd0}, _T_13}; // @[Bitwise.scala 103:31:@11.4] assign _T_15 = io_in[3:0]; // @[Bitwise.scala 103:46:@12.4] assign _GEN_0 = {{4'd0}, _T_15}; // @[Bitwise.scala 103:65:@13.4] assign _T_16 = _GEN_0 << 4; // @[Bitwise.scala 103:65:@13.4] assign _T_18 = _T_16 & 8'hf0; // @[Bitwise.scala 103:75:@15.4] assign _T_19 = _T_14 | _T_18; // @[Bitwise.scala 103:39:@16.4] assign _T_23 = _T_19[7:2]; // @[Bitwise.scala 103:21:@20.4] assign _GEN_1 = {{2'd0}, _T_23}; // @[Bitwise.scala 103:31:@21.4] assign _T_24 = _GEN_1 & 8'h33; // @[Bitwise.scala 103:31:@21.4] assign _T_25 = _T_19[5:0]; // @[Bitwise.scala 103:46:@22.4] assign _GEN_2 = {{2'd0}, _T_25}; // @[Bitwise.scala 103:65:@23.4] assign _T_26 = _GEN_2 << 2; // @[Bitwise.scala 103:65:@23.4] assign _T_28 = _T_26 & 8'hcc; // @[Bitwise.scala 103:75:@25.4] assign _T_29 = _T_24 | _T_28; // @[Bitwise.scala 103:39:@26.4] assign _T_33 = _T_29[7:1]; // @[Bitwise.scala 103:21:@30.4] assign _GEN_3 = {{1'd0}, _T_33}; // @[Bitwise.scala 103:31:@31.4] assign _T_34 = _GEN_3 & 8'h55; // @[Bitwise.scala 103:31:@31.4] assign _T_35 = _T_29[6:0]; // @[Bitwise.scala 103:46:@32.4] assign _GEN_4 = {{1'd0}, _T_35}; // @[Bitwise.scala 103:65:@33.4] assign _T_36 = _GEN_4 << 1; // @[Bitwise.scala 103:65:@33.4] assign _T_38 = _T_36 & 8'haa; // @[Bitwise.scala 103:75:@35.4] assign _T_39 = _T_34 | _T_38; // @[Bitwise.scala 103:39:@36.4] assign io_out = _T_39; endmodule
前回のOHToUInt
と同じような感じで2のべき乗単位で分解して選択したデータの論理和を取っていく感じの模様。
Chiselのソースコード
ソースコードはどうなっているのかも勉強がてら確認。
object PriorityMux { def apply[T <: Data](in: Seq[(Bool, T)]): T = SeqUtils.priorityMux(in) def apply[T <: Data](sel: Seq[Bool], in: Seq[T]): T = apply(sel zip in) def apply[T <: Data](sel: Bits, in: Seq[T]): T = apply((0 until in.size).map(sel(_)), in) }
前回のUIntToOH
などと一緒で複数のapply
が定義されている。これらのapply
からするとサンプルで見たような(Vec[Bool], Vec[UInt])
の組み合わせ以外にも
(Vec[Bool, UInt])
(UInt, Vec[Uint])
のような組み合わせを入れることも可能なようだ。(T
になってるのでChiselのData
であればUInt
の部分に適用可能はず)
Mux1H
こちらもまずはサンプルをチェック。
Driver(() => new Module { // Example circuit using Mux1H val io = IO(new Bundle { val in_sels = Input(Vec(2, Bool())) val in_bits = Input(Vec(2, UInt(8.W))) val out = Output(UInt(8.W)) }) io.out := Mux1H(io.in_sels, io.in_bits) }) { c => new PeekPokeTester(c) { poke(c.io.in_bits(0), 10) poke(c.io.in_bits(1), 20) // Select index 1 poke(c.io.in_sels(0), 0) poke(c.io.in_sels(1), 1) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") // Select index 0 poke(c.io.in_sels(0), 1) poke(c.io.in_sels(1), 0) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") // Select none (invalid) poke(c.io.in_sels(0), 0) poke(c.io.in_sels(1), 0) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") // Select both (invalid) poke(c.io.in_sels(0), 1) poke(c.io.in_sels(1), 1) println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}") } }
こちらは冒頭に書いたとおり、選択用の信号はOneHotであることが前提となっている。そのため、エラーケースに相当する複数ビットが1'b1
になっているケースもテストが実行されている。
これを実行すると以下の出力が得られる。
[info] [0.000] Elaborating design... [info] [0.067] Done elaborating. Total FIRRTL Compile Time: 18.2 ms Total FIRRTL Compile Time: 10.5 ms End of dependency graph Circuit state created [info] [0.001] SEED 1545574832259 [info] [0.003] in_sels=Vector(0, 1), out=20 [info] [0.004] in_sels=Vector(1, 0), out=10 [info] [0.005] in_sels=Vector(0, 0), out=0 [info] [0.007] in_sels=Vector(1, 1), out=30 test cmd4Helperanonfun1anon2 Success: 0 tests passed in 5 cycles taking 0.009944 seconds [info] [0.008] RAN 0 CYCLES PASSED
結果として見ておくべきなのは後半2つのテストケースだと思う。
in_sels=Vector(0, 0), out=0
: 選択信号が"0"の場合は出力が0になる。in_sels=Vector(1, 1), out=30
: 複数ビットが立った場合は1'b1
となったビットを足し合わせたものになる??
生成されるRTL
先のエラーケース時の挙動が回路的にどうなっているのかは認識しておきたいのでRTLも見てみよう。
エラーケースではあるので本来は上位層でそのような信号が入力されないようにケアをするべきではあるんだけどね。
module cmd5Helperanonfun1anon2( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_in_sels_0, // @[:@6.4] input io_in_sels_1, // @[:@6.4] input [7:0] io_in_bits_0, // @[:@6.4] input [7:0] io_in_bits_1, // @[:@6.4] output [7:0] io_out // @[:@6.4] ); wire [7:0] _T_37; // @[Mux.scala 19:72:@8.4] wire [7:0] _T_39; // @[Mux.scala 19:72:@9.4] wire [7:0] _T_40; // @[Mux.scala 19:72:@10.4] assign _T_37 = io_in_sels_0 ? io_in_bits_0 : 8'h0; // @[Mux.scala 19:72:@8.4] assign _T_39 = io_in_sels_1 ? io_in_bits_1 : 8'h0; // @[Mux.scala 19:72:@9.4] assign _T_40 = _T_37 | _T_39; // @[Mux.scala 19:72:@10.4] assign io_out = _T_40; endmodule
なるほど、足すは足すでも”論理和”になるということか。
Chiselのソースコード
ソースコードはどうなっているのかも勉強がてら確認。
object Mux1H { def apply[T <: Data](sel: Seq[Bool], in: Seq[T]): T = apply(sel zip in) def apply[T <: Data](in: Iterable[(Bool, T)]): T = SeqUtils.oneHotMux(in) def apply[T <: Data](sel: UInt, in: Seq[T]): T = apply((0 until in.size).map(sel(_)), in) def apply(sel: UInt, in: UInt): Bool = (sel & in).orR }
こちらは更に一つapply
が増えて4つになっている。増えたのは(sel: UInt, in: UInt])
かな。選択信号とデータの論理積とって1bitにしたものが戻る、、でいいのか??うーむ、もう少し読めるようにならんと。。
とりあえず今回はここまで。
次回のChiselネタではCounter
をサクッと見て、幕間をお終いにする予定。