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

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

Chisel Bootcamp - Module3.2(3) - N-tap版FIRフィルタ・ジェネレータの作成

スポンサーリンク

前回の記事ではChisel BootcampはModule3.2にの続きでScalaのFIRフィルタのリファレンスモデルをテストし、確認したモデルをChiselのテスト回路に組み込んでいった。

www.tech-diningyo.info

今回は作成したChiselモジュール用のテスト回路を使って4tap固定のFIRフィルタジェネレータをN-tap対応版に変更していく。

Module 3.2: ジェネレータ:コレクション

例題:パラメタライズ版FIRフィルタジェネレータ

早速tap数をパラメタライズ可能にしたFIRフィルタジェネレータを見てみよう。

class MyManyElementFir(consts: Seq[Int], bitWidth: Int) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(bitWidth.W))
    val out = Output(UInt(bitWidth.W))
  })

  // 入力データをレジスタに格納
  val regs = mutable.ArrayBuffer[UInt]()
  for(i <- 0 until consts.length) {
      if(i == 0) regs += io.in
      else       regs += RegNext(regs(i - 1), 0.U)
  }
  
  // 入力データ x フィルタ係数
  val muls = mutable.ArrayBuffer[UInt]()
  for(i <- 0 until consts.length) {
      muls += regs(i) * consts(i).U
  }

  // 出力生成
  val scan = mutable.ArrayBuffer[UInt]()
  for(i <- 0 until consts.length) {
      if(i == 0) scan += muls(i)
      else scan += muls(i) + scan(i - 1)
  }

  io.out := scan.last
}

変更点はリファレンスモデルとしてN-tap版のFIRフィルタと同様にクラスのパラメータとしてconsts: Seq[Int]を入れるようにして、内部の演算処理をfor文を使ったループで処理するようにした点にある。

また、bitWidth: Intを入れたことで、入力及び出力のビット幅も変更可能となった。

基本的には、for文を使ってMAC演算を順に実行しているだけになっている。

mutable.ArrayBuffer

ただひとつだけ、見慣れないのがいる。それがmutable.ArrayBufferだ。データを格納していることもあり、なんとなくは想像もつくのだが、一応Scala Starndar Libraryで確認しておこう。

class ArrayBuffer[A] extends AbstractBuffer[A] with Buffer[A] with GenericTraversableTemplate[A, ArrayBuffer] with BufferLike[A, ArrayBuffer[A]] with IndexedSeqOptimized[A, ArrayBuffer[A]] with Builder[A, ArrayBuffer[A]] with ResizableArray[A] with CustomParallelizable[A, ParArray[A]] with Serializable

An implementation of the Buffer class using an array to represent the assembled sequence internally. Append, update and random access take constant time (amortized time). Prepends and removes are linear in the buffer size. 

そうか、mutableなのか。。これまであんまり意識してなかったけどList/Seqはimuutableだった。まあいずれにしてもデータを格納してることに違いはない。

mutable.ArrayBufferでは+=を使ってappend処理が出来る他にもdeleteやinsertと処理が可能となっている。

FIRフィルタのリファレンスデザインでは、以下のように最初にtap数分の領域を確保した後、take::で新しいリストを作成していた。

var pseudoRegisters = List.fill(taps)(0)
pseudoRegisters = value :: pseudoRegisters.take(taps - 1)

これに対して、ArrayBufferを使った処理では、最初に空のArrayBufferを確保した後にfor文を使って要素を増やしていっている。

// 入力データをレジスタに格納
val regs = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
  if(i == 0) regs += io.in             // ArrayBufferの先頭にio.inを格納
  else       regs += RegNext(regs(i - 1), 0.U) // i == 1以降は直前の要素を接続
}

このArrayBufferを使って演算した結果を格納する領域を作り、FIRフィルタの計算を実現している。Chiselというかハードウェア的にはデータを格納する領域は最初のfor文(上記の抜粋したコード)で作成したRegNextのみとなる。

最終的な計算結果をArrayBufferlastを使うことでArrayBufferの末尾の要素を取り出すことによって、参照しそのデータをio.outにつないで出力してFIRフィルタモジュールの機能が完成となる。

FIRフィルタのRTL

試しに、最初に作った4-tap版FIRフィルタを作ってみよう。

new MyManyElementFir(Seq(1, 1, 1, 1), 8)

上記を実行して得られたRTLは以下の様になる(例によって、不要な部分はカット)

module cmd9HelperMyManyElementFir( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [7:0] io_in, // @[:@6.4]
  output [7:0] io_out // @[:@6.4]
);
  reg [7:0] regs_1; // @[cmd9.sc 10:33:@8.4]
  reg [31:0] _RAND_0;
  reg [7:0] regs_2; // @[cmd9.sc 10:33:@10.4]
  reg [31:0] _RAND_1;
  reg [7:0] regs_3; // @[cmd9.sc 10:33:@12.4]
  reg [31:0] _RAND_2;
  wire [8:0] muls_0; // @[cmd9.sc 15:23:@14.4]
  wire [8:0] muls_1; // @[cmd9.sc 15:23:@15.4]
  wire [8:0] muls_2; // @[cmd9.sc 15:23:@16.4]
  wire [8:0] muls_3; // @[cmd9.sc 15:23:@17.4]
  wire [9:0] _T_19; // @[cmd9.sc 21:28:@18.4]
  wire [8:0] scan_1; // @[cmd9.sc 21:28:@19.4]
  wire [9:0] _T_20; // @[cmd9.sc 21:28:@20.4]
  wire [8:0] scan_2; // @[cmd9.sc 21:28:@21.4]
  wire [9:0] _T_21; // @[cmd9.sc 21:28:@22.4]
  wire [8:0] scan_3; // @[cmd9.sc 21:28:@23.4]
  // 入力データ or レジスタ * フィルタ係数
  assign muls_0 = io_in * 8'h1; // @[cmd9.sc 15:23:@14.4]
  assign muls_1 = regs_1 * 8'h1; // @[cmd9.sc 15:23:@15.4]
  assign muls_2 = regs_2 * 8'h1; // @[cmd9.sc 15:23:@16.4]
  assign muls_3 = regs_3 * 8'h1; // @[cmd9.sc 15:23:@17.4]
    
  // 足し合わせる
  assign _T_19 = muls_1 + muls_0; // @[cmd9.sc 21:28:@18.4]
  assign scan_1 = _T_19[8:0]; // @[cmd9.sc 21:28:@19.4]
  assign _T_20 = muls_2 + scan_1; // @[cmd9.sc 21:28:@20.4]
  assign scan_2 = _T_20[8:0]; // @[cmd9.sc 21:28:@21.4]
  assign _T_21 = muls_3 + scan_2; // @[cmd9.sc 21:28:@22.4]
  assign scan_3 = _T_21[8:0]; // @[cmd9.sc 21:28:@23.4]
    
  // 出力
  assign io_out = scan_3[7:0];
    
  // レジスタによるデータの保存
  always @(posedge clock) begin
    if (reset) begin
      regs_1 <= 8'h0;
    end else begin
      regs_1 <= io_in;
    end
    if (reset) begin
      regs_2 <= 8'h0;
    end else begin
      regs_2 <= regs_1;
    end
    if (reset) begin
      regs_3 <= 8'h0;
    end else begin
      regs_3 <= regs_2;
    end
  end
endmodule

うん、なんとなくそれっぽい感じの回路になっている。

N-tap版FIRフィルタで作った4-tap FIRフィルタのテスト

では、既に作成済みのリファレンスモデル+Chiselテスト回路を使って上記のN-tap版FIRフィルタのテストを実施してみる。

val goldenModel = new ScalaFirFilter(Seq(1, 1, 1, 1))

Driver(() => new MyManyElementFir(Seq(1, 1, 1, 1), 8)) {
  c => new PeekPokeTester(c) {
    for(i <- 0 until 100) {
      val input = scala.util.Random.nextInt(8)

      val goldenModelResult = goldenModel.poke(input)

      poke(c.io.in, input)

      expect(c.io.out, goldenModelResult, s"i $i, input $input, gm $goldenModelResult, ${peek(c.io.out)}")

      step(1)
    }
  }
}

変更点はただ一点のみで、FIRフィルタのジェネレータをMyManyElementFirにしたことだ。

テスt実行結果

実行結果は以下の様に100サイクルのテストをパスすることが確認できた。

[info] [0.000] Elaborating design...
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 21.6 ms
Total FIRRTL Compile Time: 19.6 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1543132910330
test cmd9HelperMyManyElementFir Success: 100 tests passed in 105 cycles taking 0.044949 seconds
[info] [0.036] RAN 100 CYCLES PASSED

これでN-tap版FIRフィルタジェネレータが完成した。次回は4-tap固定FIRフィルタ・ジェネレータ→N-tapFIRフィルタ・ジェネレータの変換作業の仕上げとして、N-tap対応版FIRフィルタ・ジェネレータ用にChiselのテスト回路を変更していく。