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

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

Chisel Bootcamp - Module3.2(1) - Scalaのコレクション型(SeqとList)

スポンサーリンク

前回の記事ではChisel BootcampではやっとModule3.1が終わりになった。

www.tech-diningyo.info

今回からはModule3.2に入り、Scalaのコレクション型を使ってChiselのジェネレータを書く方法を学んでいく。

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

モチベーション

まずはお決まりとなったモチベーションから。

ジェネレータには数多くのオブジェクトの変数を処理しなければならない場合がよく起きる。それはIOだったりモジュールだったり、テストベクタだったりと様々だ。コレクション型はこのような状況を処理するために重要な機能となる。このモジュールではScalaのコレクションとこれをどのようにしてChiselのジェネレータと一緒に使っていくかを紹介する。

ジェネレータとコレクション型

このモジュールでは以前のチャプターで出てきたFIRフィルタを使って学習を進めていく。

ということで、サンプルとなるFIRフィルタのコードを再度見てみよう。

class My4ElementFir(b0: Int, b1: Int, b2: Int, b3: Int) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(8.W))
    val out = Output(UInt(8.W))
  })

  val x_n1 = RegNext(io.in, 0.U)
  val x_n2 = RegNext(x_n1, 0.U)
  val x_n3 = RegNext(x_n2, 0.U)
  io.out := io.in * b0.U(8.W) + x_n1 * b1.U(8.W) +
    x_n2 * b2.U(8.W) + x_n3 * b3.U(8.W)
}

この記述はフィルタの重みをインスタンス時に変更出来るという点ではジェネレータとなるのだが、ジェネレータとしてはそれ以外の機能は持っていない。

このFIRフィルタ・ジェネレータをN tap対応のジェネレータに変更するにはどうすればいいのか?ということを考えていく。

通常は以下のようなステップを踏むとうまく行くだろう。

  1. ScalaでN tap対応を行ったFIRフィルタを実装し、これをリファレンスモデルとする
  2. リファレンスデザインを使ってテストを再度構築し、テストが所望の機能を確認出来ることを確認する
  3. Chiselで書いたMy4ElementFirをN tap対応版のジェネレータに変更する
  4. 2.で作成したテストを使って3.のN tap対応版FIRフィルタの機能を確認する

例題:FIRフィルタのリファレンスモデル

以下がBootcampに載っているFIRフィルタのリファレンスモデルのコードとなる。

/**
  * A naive implementation of an FIR filter with an arbitrary number of taps.
  */
class ScalaFirFilter(taps: Seq[Int]) {
  var pseudoRegisters = List.fill(taps.length)(0)

  def poke(value: Int): Int = {
    pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)
    var accumulator = 0
    for(i <- taps.indices) {
      accumulator += taps(i) * pseudoRegisters(i)
    }
    accumulator
  }
}

このリファレンスモデルのポイントについてを見ていこう。

Seq

コード中にあるとおりクラスのパラメータtapsSeq[Int]で定義されている。これにより、クラスのインスタンス時にtap数とフィルタのパラメータを任意に決めることが出来るようになる。

Registers

以下のコードはリファレンスモデルのFIRフィルタ中のレジスタに相当するコードとなる。

var pseudoRegisters = List.fill(taps.length)(0)

クラスのパラメータであるtapsSeq型なのでlengthを参照することでtap数がわかる。これをListに与えることでデータを格納する場所を用意し0で初期化している。

なので、上記のコードだけを実行すると以下のようにtaps数に与えた要素数を持つ領域が確保される。

val taps = 10
var pseudoRegisters = List.fill(taps)(0)
  • 実行結果
taps: Int = 10
pseudoRegisters: List[Int] = List(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
poke

Chiselのテストに合わせてpokeメソッドが用意されている。この関数を一回呼び出すと、リファレンスモデルで1cycleに相当する処理が行われる。

そのため、実際のFIRフィルタの計算処理はこの関数で実行されることになるので少し細かく見ていこう

レジスタのアップデート

以下のコードは入力された値を使ってレジスタをアップデートするコードとなる。

    pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)

Scalaもあんまりわかってないので、これを見た瞬間に::ってちょいちょい出てくるけど何だっけ??となってしまった。。

ということでこのコードだけ切り出して、少し遊んで見る。

val value = 100
var pseudoRegisters = List.fill(taps)(0)
pseudoRegisters = value :: pseudoRegisters.take(taps - 1)
  • 実行結果
value: Int = 100
pseudoRegisters: List[Int] = List(100, 0, 0, 0, 0, 0, 0, 0, 0, 0)

valueに入れた値が先頭に入った状態になった。

では次にtakeの引数を変更してみる。

val value = 100
var pseudoRegisters = List.fill(taps)(0)
pseudoRegisters = value :: pseudoRegisters.take(taps - 5)
  • 実行結果
value: Int = 100
pseudoRegisters: List[Int] = List(100, 0, 0, 0, 0, 0)

takeの引数を変更することで、処理後のリストのサイズが縮んだのがわかると思う。

そのため、元のコードでは以下の処理を行うことで、レジスタ用のリストの先頭に入力されたデータを入れるという処理を実現している。

  1. takepseudoRegistersの要素数taps.size - 1に変更
  2. value ::で1.のリストとvalueを結合して新しいリストを作成
List.take

一応、List.takeの処理をScala Standard Libraryを見て確認しておこう。

def take(n: Int): List[A]

Selects first n elements.

n:the number of elements to take from this list.

returns:a list consisting only of the first n elements of this list, or else the whole list, if it has less than n elements. If n is negative, returns an empty list.

書いてあるとおりで、takeを呼ぶと元のListの先頭からn個の要素を選んだリストが返却されるようだ。

::??

もうひとつついでに調べておこう。調べるのは::ってなんて呼べばいいの??という話。以下の記事が色々Scalaの記号についてをまとめてくださっているので非常にためになった。

qiita.com

あと、呼び方だが以下のstack overlflowの記事によるとcons operatorということになるみたい。

stackoverflow.com

Scala Standard Libraryのページは以下のページで良さそう。

Scalaの"::"のページ

先のQiitaの記事にも書いてあるけど、Scalaの記号は検索しにくいことこの上ないな。。。。

出力の計算

以下のループ処理でFIRフィルタの計算を実行して、最後にaccumulatorpoke関数のブロック式の値とすることで出力している。

var accumulator = 0
for(i <- taps.indices) {
  accumulator += taps(i) * pseudoRegisters(i)
}

これは特に難しいことはなく、indicesを呼ぶことでListのインデックスを取得してtapsに入っているフィルタの係数とpseudoRegistersの値を計算して積算していくだけとなっている。

List.indices

一応備忘の意味も込めてindicesの処理も確認しておく。

def indices: Range

Produces the range of all indices of this sequence.

returns
    a Range value from 0 to one less than the length of this sequence.

リファレンスモデルの中身を見るだけでそこそこの分量になってしまったので、ここで一回区切ることにしよう。