LoginSignup
2
0

More than 3 years have passed since last update.

Chisel入門書「Digital Design with Chisel」9章の勉強記録

Last updated at Posted at 2019-09-17

記事の概要

Chiselの入門書「Digital Design with Chisel」の9章の勉強記録です。
本文の概要を備忘録として整理し、また実際に行った演習を紹介します。

本のpdfデータとプログラム一式は無料で以下から入手できます。
https://raw.githubusercontent.com/wiki/schoeberl/chisel-book/chisel-book.pdf
https://github.com/schoeberl/chisel-book

9.1 Configure with Parameters

Chiselでコンポーネントや関数をパラメーターを指定して構成する方法を紹介しています。

9.1.1 Simple Parameters

ビット幅をパラメータとして定義する例

  • 加算器
class ParamAdder(n: Int) extends Module { 
  val io = IO(new Bundle{ 
    val a = Input(UInt(n.W)) 
    val b = Input(UInt(n.W)) 
    val c = Output(UInt(n.W)) 
  })

  io.c := io.a + io.b
}

以下のようにパラメータにビット幅を定義して使用します。

val add8 = Module(new ParamAdder(8)) 
val add16 = Module(new ParamAdder(16))

9.1.2 Functions with Type Parameter

型ををパラメータとして定義する例

  • マルチプレクサ
def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {

  val ret = WireInit(fPath) 
  when (sel) { 
    ret := tPath 
  } 
  ret
}

ここでは、型パラメータTに対して、"[T <: Data]"のように上限境界<:が使用されています。
これは型パラメータTには、Chisel型DataおよびDataのサブクラスしか使用できないことを意味しています。
また、tPathとfPathは型が一致していないといけません。

val resA = myMux(selA, 5.U, 10.U)

以下のように、tPathとfPathの型が異なるとエラーになります。

val resErr = myMux(selA, 5.U, 10.S)

パラメータにData型"Uint"ではなく、Dataのサブ型を使用する例を以下に示します。
Bundleにより2つのフィールドを持つタイプを定義します。

class ComplexIO extends Bundle {
  val d = UInt(10.W) 
  val b = Bool()
}

まず、ワイヤを作成してから、サブフィールドを設定することで、定数を定義します。
すると、この複合型をパラメータに使用して、マルチプレクサを作成できます。

val tVal = Wire(new ComplexIO) 
tVal.b := true.B 
tVal.d := 42.U 
val fVal = Wire(new ComplexIO) 
fVal.b := false.B 
fVal.d := 13.U

// The mulitplexer with a complex type 
val resB = myMux(selB, tVal, fVal)

Wireを初期化せずに使用したい場合

関数myMuxにおいては、WireInitを使用して、初期値を持つ型パラメータTのワイヤを作成しました。
そのため、初期値を持たないワイヤはtPathやfPathに使用できません。

もし、初期値を持たない型のみのワイヤを使用したい場合、fPath.cloneTypeを使用します。

def myMuxAlt[T <: Data](sel: Bool, tPath: T, fPath: T): T = {

  val ret = Wire(fPath.cloneType) 
  ret := fPath 
  when (sel) { 
    ret := tPath 
  } 
  ret
}

ここでcloneTypeを使用する理由が分かりませんでした。
cloneTypeはBundleで作成した型をテンプレートとして使用する時に、自動でクローンを作成ができない場合に必要になるという理解でいます。
Wireを初期化なしで使用することと何の関係があるのか分かりませんでした。初期値がないから自動でクローン作成できないので必要になるとか?

9.1.3 Modules with Type Parameters

以下のようにモジュールでもパラメーターを使用できます。

クラスNocRouterを定義する際、インターフェースのデータ型をハードコードせずにパラメータ化します。また、ポートの数nもパラメータ化しておきます。

class NocRouter[T <: Data](dt: T, n: Int) extends Module { 
  val io =IO(new Bundle { 
    val inPort = Input(Vec(n, dt)) 
    val address = Input(Vec(n, UInt(8.W))) 
    val outPort = Output(Vec(n, dt)) 
  })

// Route the payload according to the address 
// ...

NocRouterを使用するには、まず型パラメータTをBundleにより定義します。

class Payload extends Bundle { 
  val data = UInt(16.W) 
  val flag = Bool() 
}

これを用いてインスタンスを作成できます。

val router = Module(new NocRouter(new Payload, 2))

9.1.4 Parametrize Bundles

NocRouterは入力に2つのフィールドベクトルを使用しました。この2つはアドレス用とデータ用でしたが、これらをパラメータ化されたBundleでクラスPortにまとめます。

class Port[T <: Data](private val dt: T) extends Bundle { 
  val address = UInt(8.W) 
  val data = dt.cloneType 
}

(理解があいまいなので、以下の説明には誤りがあるかもしれません。)
Bundleをテンプレートとして使うと、同じ型を持つ新しいインスタンスを作成します。その際に、多くの場合は自動でクローンを作成してくれます。
ですが、Bundleがパラメータを持つ場合(上記ではdtの場合)、Chiselはそれをクローンできないので、手動でcloneTypeを実装しないといけません。

また、Bundleの型をクローンする際、例えばvecとして使用する場合、このパラメータがpublicフィールドになると邪魔なのでdtをprivateにしています。
(何が邪魔になるのか私は理解できていません。)

また、クラスNocRouter2を以下のように定義します。

class NocRouter2[T <: Data](dt: T, n: Int) extends Module { 
  val io =IO(new Bundle { 
    val inPort = Input(Vec(n, dt)) 
    val outPort = Output(Vec(n, dt)) 
  })

// Route the payload according to the address 
// ...

これらを用いて9.1.3と同様のインスタンスを作成できます。

val router = Module(new NocRouter2(new Port(new Payload), 2))

9.2 Generate Combinational Logic

ファイル「data.txt」の読み取り

Scala標準ライブラリSourceクラスを用いて、ファイル「data.txt」をハードウェア生成時に論理テーブルへ読み取ることができます。

import chisel3._ 
import scala.io.Source

class FileReader extends Module { 
  val io = IO(new Bundle { 
    val address = Input(UInt(8.W)) 
    val data = Output(UInt(8.W)) 
  })

  val array = new Array[Int](256) 
  var idx = 0

  // read the data into a Scala array 
  val source = Source.fromFile("data.txt") 
  for (line <- source.getLines()) { 
    array(idx) = line.toInt 
    idx += 1 
  }

  // convert the Scala integer array into the Chisel type Vec 
  val table = VecInit(array.map(_.U(8.W)))

  // use the table 
  io.data := table(io.address)
}

"val table = VecInit(array.map(_.U(8.W)))"について補足します。

Scala配列は、マッピング関数mapをサポートするSeqに暗黙裡に変換されます。
_.U(8.W)は、Scalaの配列の各Int値を表わす_を8ビット幅のUInt値にします。
VecInitによりChisel型のSeqからVecが作成されます。

BCD変換

2進数を2進化10進数(BCD)表現へ変換します。
例えば13は、2進数では1101で、BCDでは1_3=b0001_0011となります。

Chiselではハードウェア生成時にバイナリからBCDへの変換テーブルも生成できます。

import chisel3._

class BcdTable extends Module { 
  val io = IO(new Bundle { 
    val address = Input(UInt(8.W)) 
    val data = Output(UInt(8.W)) 
  })

  val array = new Array[Int](256)

  // Convert binary to BCD 
  for (i <- 0 to 99) { 
    array(i) = ((i/10)<<4) + i%10 
  }

  val table = VecInit(array.map(_.U(8.W))) 
  io.data := table(io.address)
}

9.3 Use Inheritance

Chiselはオブジェクト指向言語なので継承が使用できます。

まず抽象クラスTickerを考えます。
抽象クラスの作り方は、classキーワードの前にabstractをつけます。

abstract class Ticker(n: Int) extends Module { 
  val io = IO(new Bundle{ 
    val tick = Output(Bool()) 
  }) 
}

次にTickerを拡張し、TickerのサブクラスUpTickerを作成します。
UpTickerはカウントアップするカウンターを持ちます。

class UpTicker(n: Int) extends Ticker(n) {

  val N = (n-1).U

  val cntReg = RegInit(0.U(8.W))

  cntReg := cntReg + 1.U 
  when(cntReg === N) { 
    cntReg := 0.U 
  }

  io.tick := cntReg === N
}

Tickerのサブクラスが複数あった場合、その全てを単一のテストベンチで試験できます。

import chisel3.iotesters.PeekPokeTester 
import org.scalatest._

class TickerTester[T <: Ticker](dut: T, n: Int) extends 
    PeekPokeTester(dut: T) {

  // -1 is the notion that we have not yet seen the first tick 
  var count = -1 
  for (i <- 0 to n * 3) { 
    if (count > 0) { 
      expect(dut.io.tick, 0) 
    } 
    if (count == 0) { 
      expect(dut.io.tick, 1) 
    } 
    val t = peek(dut.io.tick) 
    // On a tick we reset the tester counter to N-1, 
    // otherwise we decrement the tester counter 
    if (t == 1) { 
      count = n-1 
    } else { 
      count -= 1 
    }

    step(1)
  }
}

このテストベンチには3つのパラメータがあります。

  • TickerまたはTickerを継承するクラスのみを使用可能とする型パラメータ[T <:Ticker]
  • 型パラメータTもしくはそのサブタイプであるテスト対象の設計dut
  • 各tickに要すると思われるクロックサイクル数n

ここで更に2つのTickerのサブクラスを定義します。

サブクラスDownTickerはカウントダウンするカウンターを持ち、サブクラスNerdTickerは6.2.3でも紹介した-1までカウントし、最上位ビットを見てカウントリセットするカウンターを持ちます。

class DownTicker(n: Int) extends Ticker(n) {

  val N = (n-1).U

  val cntReg = RegInit(N)

  cntReg := cntReg - 1.U 
  when(cntReg === 0.U) { 
    cntReg := N 
  }

  io.tick := cntReg === N
}
class NerdTicker(n: Int) extends Ticker(n) {

  val N = n

  val MAX = (N - 2).S(8.W) 

  val cntReg = RegInit(MAX) 
  io.tick := false.B

  cntReg := cntReg - 1.S 
  when(cntReg(7)) { 
    cntReg := MAX 
    io.tick := true.B 
  }
}

この3つのバージョンの試験を先の1つのテストベンチで実行できます。

class TickerSpec extends FlatSpec with Matchers {

  "UpTicker 5" should "pass" in { 
    chisel3.iotesters.Driver(() => new UpTicker(5)) { c => 
      new TickerTester(c, 5) 
    } should be (true) }

  "DownTicker 7" should "pass" in { 
    chisel3.iotesters.Driver(() => new DownTicker(7)) { c => 
      new TickerTester(c, 7) 
    } should be (true) 
  }

  "NerdTicker 11" should "pass" in { 
    chisel3.iotesters.Driver(() => new NerdTicker(11)) { c => 
      new TickerTester(c, 11) 
    } should be (true) 
  }
}

以下のコマンドでこれらの試験は実行されます

sbt "testOnly TickerSpec"

参考

関連記事

Chisel入門書「Digital Design with Chisel」1章の勉強記録
Chisel入門書「Digital Design with Chisel」2章の勉強記録
Chisel入門書「Digital Design with Chisel」3章の勉強記録
Chisel入門書「Digital Design with Chisel」4章の勉強記録
Chisel入門書「Digital Design with Chisel」5章の勉強記録
Chisel入門書「Digital Design with Chisel」6章の勉強記録
Chisel入門書「Digital Design with Chisel」7章の勉強記録
Chisel入門書「Digital Design with Chisel」8章の勉強記録

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0