前回の記事ではChisel BootcampのModule2.5の最初の練習問題であるChiselを使ったFIRフィルタの設計を行った。
今回も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:フィルタ出力
上記を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.in
とlength-1
個のRegInit
が格納されたSeq
が入ってくる。
valid
を使ったレジスタの更新
taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }
ざっと流れは以下の通り
taps.zip(taps.tail)
:tap.tail
でio.in
を除いたSeq
を作成して、それとtaps
をzip
でペアにする.foreach
:1.のzip
で作ったペアをforeach
で一つずつ取り出すcase(a, b)
:zip
で取り出したペアをa
、b
に格納- 例えば先頭のペアを取り出すと、
a
にはio.in
、b
にはRegInit
のひとつ目(taps[1]
の要素)が入っている
- 例えば先頭のペアを取り出すと、
when (io.valid) { b := a}
:io.valid
がTrue
の時にのみb := a
が実行- 3-1.の通り
taps[0]
にはio.in
、taps[1]
以降はRegInit
となっており、io.valid
がTrue
の場合にのみb := a
が実行されるので、処理的にはvalid信号が有効なときに入力データが後続のレジスタに格納されることになる。
- 3-1.の通り
畳み込み計算
最後の行で一気に計算を行い、出力にデータを代入している
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ブートキャンプを一通りこなした後にもう一度確認できればいいかな。