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

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

Chisel Bootcamp - 幕間(5) - OneHotなデータを扱うライブラリ(UIntToOHとOHToUInt)

スポンサーリンク

前回の記事ではChiselの標準ライブラリに含まれているビット単位の操作を行うブロックをを紹介した。

www.tech-diningyo.info

今日は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を見ていく。