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

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

Chisel Bootcamp - Module2.5 (2) - FIRフィルタ・ジェネレータ

スポンサーリンク

前回の記事ではChisel BootcampのModule2.5の最初の練習問題であるChiselを使ったFIRフィルタの設計を行った。

www.tech-diningyo.info

今回もMoudle2.5の続きで、前回のFIRフィルタを生成するジェネレータを作っていく。

なお前回の終わりにも書いたし、こんなgithubのIssueも見つけたので間に挟まっているIPXactのセクションはスキップすることにする。

https://github.com/freechipsproject/chisel-bootcamp/issues/16

Module 2.5: FIRフィルタ:全てを一つに

FIRフィルタのジェネレータ

ということでFIRフィルタのジェネレータだ。練習問題なのかと思ったら、実はそうではなかった。。

とりあえず仕様部分については部分は丸々引用させてもらうことにする。

このモジュールでは、Module3.2のジェネレータとコレクションで使用する例題を少しだけ修正したものを使用することにする。もしModule3.2を始めていないとしても心配はいらない。あなたはこれからMyManyDynamicElementVecFirがどのようにして動作するかの詳細についてを学ぶが、基本的な考え方は”これはFIRフィルタのジェネレータである”ということだ。

このジェネレータはパラメータとして、以下のパラメータを持つ

  • length:このパラメータはフィルタのタップ数を指定するもので、タップ数はMoudleの入力となる。

ジェネレータは以下の3つの入力を持つ

  • in:フィルタへの入力データ
  • valid:入力が有効であることを示すbool変数
  • consts:全てのタップのベクタ

また出力は以下になる

  • out:フィルタ出力

f:id:diningyo-kpuku-jougeki:20181103215907j:plain

上記をChiselで実装したコードが以下になる。

class MyManyDynamicElementVecFir(length: Int) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(8.W))
    val valid = Input(Bool())
    val out = Output(UInt(8.W))
    val consts = Input(Vec(length, UInt(8.W)))
  })
  
  // Such concision! You'll learn what all this means later.
  val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))
  taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }

  io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _)
}

うん、なんかコード自体は短くなったけど、途端に難しくなったな。

FIRフィルタ・ジェネレータの中身

タップ数を与えれられるのとvalidが入った事以外は前回のFIRフィルタと同様なはずなので、どこで何をしているかを考えてみよう。

レジスタの定義

val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))

Seq.fillの要素にRegInitを入れてるので、tapsにはio.inlength-1個のRegInitが格納されたSeqが入ってくる。

validを使ったレジスタの更新

taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }

ざっと流れは以下の通り

  1. taps.zip(taps.tail)tap.tailio.inを除いたSeqを作成して、それとtapszipでペアにする
  2. .foreach:1.のzipで作ったペアをforeachで一つずつ取り出す
  3. case(a, b)zipで取り出したペアをabに格納
    1. 例えば先頭のペアを取り出すと、aにはio.inbにはRegInitのひとつ目(taps[1]の要素)が入っている
  4. when (io.valid) { b := a}io.validTrueの時にのみb := aが実行
    1. 3-1.の通りtaps[0]にはio.intaps[1]以降はRegInitとなっており、io.validTrueの場合にのみb := aが実行されるので、処理的にはvalid信号が有効なときに入力データが後続のレジスタに格納されることになる。

畳み込み計算

最後の行で一気に計算を行い、出力にデータを代入している

io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _)

reduce以外はvalidを使ったレジスタの更新処理とほぼ一緒。ただ見ての通りforeachではなくmapを使っている。試しにforeachにしてみたらエラーになった。戻り値がどうなっているかを調べれば分かりそうだったので、確認したみた。

Trait Traversable | Scala Documentation

によると、foreachの定義は以下の様にUnit型を返すようになっているので後続に処理をつなげることが出来ない。

def foreach[U](f: Elem => U)

Seq.mapの場合は処理した後に新しいSeqを返すので、Seqで使用可能なメソッドを繋げることが出来るという風に解釈すれば良さそうだった。 因みにmapの処理内容は以下のScala Standard Libraryでは以下の様に定義されている。

def map[B](f: (A) ⇒ B): Seq[B] 

reduceはFIRフィルタの計算なので何をやっているかは推測できるのだが、きちんとは知らないので調べてみる。こちらも同様にScala Standard Libraryから

def reduce[A1 >: A](op: (A1, A1) ⇒ A1): A1 

説明は以下。

Reduces the elements of this traversable or iterator using the specified associative binary operator. The order in which operations are performed on elements is unspecified and may be nondeterministic.

  • A1 : A type parameter for the binary operator, a supertype of A.
  • op : A binary operator that must be associative.
  • returns : The result of applying reduce operator op between all the elements if the traversable or iterator is nonempty.

opに指定した演算子Seqの要素同士の演算を行い、Seqの中身の要素を返却ということになる。今回のFIRフィルタにおいてはmapで入力データと重みを乗算した結果をSeqとして受け取り、それに対して加算を行い、FIRフィルタの主力にしている。

たった3行なのに、なんて色々詰まってるんだ。。。

変換したVerilogコード

因みにメインの処理が僅か3行のChiselコードをVerilogに変換すると以下のようなモジュールとなる。 なお、見づらいので初期値をランダム化するifdefブロックについては削除した。別途コメントも入れてある。

module cmd15HelperMyManyDynamicElementVecFir( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [7:0] io_in, // @[:@6.4]
  input        io_valid, // @[:@6.4]
  output [7:0] io_out, // @[:@6.4]
  input  [7:0] io_consts_0, // @[:@6.4]
  input  [7:0] io_consts_1, // @[:@6.4]
  input  [7:0] io_consts_2, // @[:@6.4]
  input  [7:0] io_consts_3 // @[:@6.4]
);
  reg [7:0] taps_1; // @[cmd15.sc 10:66:@8.4]
  reg [31:0] _RAND_0;
  reg [7:0] taps_2; // @[cmd15.sc 10:66:@9.4]
  reg [31:0] _RAND_1;
  reg [7:0] taps_3; // @[cmd15.sc 10:66:@10.4]
  reg [31:0] _RAND_2;
  wire [7:0] _GEN_0; // @[cmd15.sc 11:64:@11.4]
  wire [7:0] _GEN_1; // @[cmd15.sc 11:64:@14.4]
  wire [7:0] _GEN_2; // @[cmd15.sc 11:64:@17.4]
  wire [15:0] _T_35; // @[cmd15.sc 13:56:@20.4]
  wire [15:0] _T_36; // @[cmd15.sc 13:56:@21.4]
  wire [15:0] _T_37; // @[cmd15.sc 13:56:@22.4]
  wire [15:0] _T_38; // @[cmd15.sc 13:56:@23.4]
  wire [16:0] _T_39; // @[cmd15.sc 13:71:@24.4]
  wire [15:0] _T_40; // @[cmd15.sc 13:71:@25.4]
  wire [16:0] _T_41; // @[cmd15.sc 13:71:@26.4]
  wire [15:0] _T_42; // @[cmd15.sc 13:71:@27.4]
  wire [16:0] _T_43; // @[cmd15.sc 13:71:@28.4]
  wire [15:0] _T_44; // @[cmd15.sc 13:71:@29.4]
  // io_validを使ったデータ選択かと思いきや、実は不要な信号
  assign _GEN_0 = io_valid ? io_in : taps_1; // @[cmd15.sc 11:64:@11.4]
  assign _GEN_1 = io_valid ? taps_1 : taps_2; // @[cmd15.sc 11:64:@14.4]
  assign _GEN_2 = io_valid ? taps_2 : taps_3; // @[cmd15.sc 11:64:@17.4]
  // 入力データ×フィルタ係数
  assign _T_35 = io_in * io_consts_0; // @[cmd15.sc 13:56:@20.4]
  assign _T_36 = taps_1 * io_consts_1; // @[cmd15.sc 13:56:@21.4]
  assign _T_37 = taps_2 * io_consts_2; // @[cmd15.sc 13:56:@22.4]
  assign _T_38 = taps_3 * io_consts_3; // @[cmd15.sc 13:56:@23.4]
  // 乗算後の値を加算
  assign _T_39 = _T_35 + _T_36; // @[cmd15.sc 13:71:@24.4]
  assign _T_40 = _T_39[15:0]; // @[cmd15.sc 13:71:@25.4]
  assign _T_41 = _T_40 + _T_37; // @[cmd15.sc 13:71:@26.4]
  assign _T_42 = _T_41[15:0]; // @[cmd15.sc 13:71:@27.4]
  assign _T_43 = _T_42 + _T_38; // @[cmd15.sc 13:71:@28.4]
  assign _T_44 = _T_43[15:0]; // @[cmd15.sc 13:71:@29.4]
  // _T_44が最終的なフィルタの出力データ
  assign io_out = _T_44[7:0];
    
  // レジスタの処理、valid有効時にデータを取り込む
  always @(posedge clock) begin
    if (reset) begin
      taps_1 <= 8'h0;
    end else begin
      if (io_valid) begin
        taps_1 <= io_in;
      end
    end
    if (reset) begin
      taps_2 <= 8'h0;
    end else begin
      if (io_valid) begin
        taps_2 <= taps_1;
      end
    end
    if (reset) begin
      taps_3 <= 8'h0;
    end else begin
      if (io_valid) begin
        taps_3 <= taps_2;
      end
    end
  end
endmodule

うーむ、やっぱりコードの密度が違いすぎる。。。早くもう少しまともに書けるようにならねば。。

FIRフィルタ・ジェネレータのテスト

最後に、上記のFIRフィルタ・ジェネレータをテストしておこう。前回に出てきたsanity checkのコードをベースに少しだけ修正して実行する。

// Simple sanity check: a element with all zero coefficients should always produce zero
Driver(() => new MyManyDynamicElementVecFir(4)) {
  c => new PeekPokeTester(c) {
    poke(c.io.consts(0), 0)
    poke(c.io.consts(1), 0)
    poke(c.io.consts(2), 0)
    poke(c.io.consts(3), 0)
    poke(c.io.in, 0)
    poke(c.io.valid, 1)
    step(1)
    poke(c.io.in, 4)
    expect(c.io.out, 0)
    step(1)
    poke(c.io.in, 5)
    expect(c.io.out, 0)
    step(1)
    poke(c.io.in, 2)
    expect(c.io.out, 0)
  }
}

変更した部分は、追加された仕様に関するもので、以下の2点のみだ。

  • 最初にio.validを有効にする
  • 同様にio.constsに値を設定する

このテストコードを実行すると、以下の様にパスすることが確認できた。

[info] [0.000] Elaborating design...
[info] [0.006] Done elaborating.
Total FIRRTL Compile Time: 13.8 ms
Total FIRRTL Compile Time: 12.9 ms
End of dependency graph
Circuit state created
[info] [0.001] SEED 1541304131806
test cmd15HelperMyManyDynamicElementVecFir Success: 3 tests passed in 8 cycles taking 0.007521 seconds
[info] [0.007] RAN 3 CYCLES PASSED

これでModule2.5についてはひとまず完了として、Moduleに入っていくことにする。Module2.5のDSPブロックについてはこのChiselブートキャンプを一通りこなした後にもう一度確認できればいいかな。