FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://sites.google.com/site/fpgadevelopindex/

A short User Guide to Chisel勉強中(3)

f:id:msyksphinz:20170806234951p:plain

前回の続き。Chisel勉強中。このページから。

github.com

ステート記述 (State Elements)

Chiselで記述できる最も簡単なステート記述は、ポジティブエッジのステートマシンで、以下のように記述する。

val reg = RegNext(in)

この場合、regは1サイクル遅れてRegNext(in)を出力するフリップフロップとなる。クロックとリセットは明示的には記述されていないが、暗黙的に必要な場所から接続される。 この場合、初期値が指定されている場合は良いが、初期値が指定されていない場合はリセット信号が入るまでは値が変化しない。

例えば、立ち上がりエッジを検出する記述は、過去の値がfalseで、現在の値がTrueであることを検出すればよいため、以下のようになる。

def risingedge(x: Bool) = x && !RegNext(x)

次に、値をカウントアップしていき最大値に到達するとラップして0に戻るようなアップカウンタを記述する場合は、以下のようになる。

def counter(max: UInt) = {
  val x = Reg(init = 0.asUInt(max.getWidth))
  x := Mux(x === max, 0.U, x + 1.U)
  x
}

まず、リセットが入った時点でinitが参照されカウンタレジスタは0に設定される。:=により、xはインクリメントされ、xがmaxに到達すると0になる。 カウンタは非常に便利に使用することができる。例えば、カウンタが0になるとパルスを生成するようなパルスジェネレータは、以下のような記述で作成できる。

// nサイクル単位でパルスを生成する。
def pulse(n: UInt) = counter(n - 1.U) === 0.U

矩形波を発生するカウンタは、パルスに応じてTrueとFalseを切り替えるようにすればよい。

// 入力があると、内部の状態を反転させる。
def toggle(p: Bool) = {
  val x = Reg(init = false.B)
  x := Mux(p, !x, x)
  x
}
// 指定した周期で矩形波を生成する。
def squareWave(period: UInt) = toggle(pulse(period/2))

メモリ

ChiselはROMとRAMを生成することができる。

ROM

Vecデータ型を用いて、読み込み専用のメモリ(ROM)を生成することができる。

    Vec(inits: Seq[T])
    Vec(elt0: T, elts: T*)

initはROMの初期値を指定する。例えば、それぞれROMの初期値を1,2,4,8がループしているものとして、カウンタをアドレスジェネレータとして以下のように記述できる。

    val m = Vec(Array(1.U, 2.U, 4.U, 8.U))
    val r = m(counter(m.length.U))

次に、正弦関数のn値のルックアップテーブルを以下のようにして記述することができる。ampは、ROMに内蔵する値をスケーリングするために使用している値である。

    def sinTable(amp: Double, n: Int) = {
      val times = 
        (0 until n).map(i => (i*2*Pi)/(n.toDouble-1) - Pi)
      val inits = 
        times.map(t => round(amp * sin(t)).asSInt(32.W))
      Vec(inits)
    }
    def sinWave(amp: Double, n: Int) = 
      sinTable(amp, n)(counter(n.U))

Mem

Chiselはメモリは特殊なモジュールとして処理している。これは、メモリはハードウェアによっていくつかの種類が存在するからである。 例えば、FPGAのメモリはASICのメモリのそれとはまったく異なる。 メモリのVerlilog動作モデルにマップすることのできるメモリの抽象モデルか、外部のIPベンダから入手したメモリジェネレータと接続するためのメモリ抽象モデルを定義することができる。

ChiselはMemデータ型を用いてランダムアクセスメモリを実装することができるが、これは組み合わせ・非同期読み込み、順序・同期書き込みとなる。つまり、Memレジスタバンクのような動作をする。

もう一つ、ChiselはSyncReadMemという 順序・同期読み込み、順序・同期書き込み を行うメモリお定義できる。現代のほとんどのSRAM(ASIC用、FPGA用)は非同期読み込みをサポートしないため、SyncReadMemは現代のSRAMのような動作をすると考えることができる。

メモリのポートは、Uint型のインデックスを持っている。 1024エントリのレジスタファイルで、1ポート書き込み、同期読み込みのレジスタファイルは以下のようにして定義できる。

val width:Int = 32
val addr = Wire(UInt(width.W))
val dataIn = Wire(UInt(width.W))
val dataOut = Wire(UInt(width.W))
val enable = Wire(Bool())

// assign data...

// Create a synchronous-read, synchronous-write memory (like in FPGAs).
val mem = SyncReadMem(1024, UInt(width.W))
// Create one write port and one read port.
mem.write(addr, dataIn)
dataOut := mem.read(addr, enable)

Chiselはこれ以外にも、マスク月のメモリなども生成させることができる。 シングルポートのSRAMは読み込みと書き込みの状態が排他的である必要があり、以下のようにして記述することができる。下記のような場合、同時に読み込みと書き込みが発生すると、読み込みのリクエストは無視され、書き込みが実行される。読み込みデータは無視される。

val mem = SyncReadMem(2048, UInt(32.W))
when (write) { mem.write(addr, dataIn) }
.otherwise { dataOut := mem.read(addr, read) }

MemSyncReadMemも、マスク月の書き込みをサポートしている。マスクビットがセットされた場合の書き込みは、以下のように定義できる。

val ram = Mem(256, UInt(32.W))
when (wen) { ram.write(waddr, wdata, wmask) }

インタフェースとバルク接続

モジュールをより便利に使えるようにするためには、インタフェースクラスを定義して、モジュールのI/Oをインタフェースクラスを通じて接続できるようになれば便利だ。 さらに、インタフェースは再利用することができ、モジュールのインタフェースクラスを統一しておけばモジュール間の接続も便利になる。

次に、インタフェースを定義することにより、送信元のモジュールと受信先のモジュールの接続の記述量を格段に減らすことができる。 最終的に、ユーザはインタフェースを大きく変更しても、インタフェースの要素が増減した場合の変更量を減らすことができるようになる。

ポート: サブクラスとネスト

ユーザはBundleのサブクラスとしてインタフェースを定義することができる。 例えば、以下のようにしてシンプルなハンドシェークのポートを定義する事ができる。

class SimpleLink extends Bundle {
  val data = Output(UInt(16.W))
  val valid = Output(Bool())
}

さらに、SimpleLinkに対して以下のようにパリティを追加することができる。

class PLink extends SimpleLink {
  val parity = Output(UInt(5.W))
}

一般的に、ユーザは継承を使ってインタフェースを構成する。

次に、フィルタインタf-エスを、2つのPLinkを1つの新しいFilterIOインタフェースとして定義する。

class FilterIO extends Bundle {
  val x = Flipped(new PLink)
  val y = new PLink
}

ここでflipはBundleの入出力の方向を反転させる処理である。次に、モジュールを拡張することでフィルタクラスを定義する。

class Filter extends Module {
  val io = IO(new FilterIO)
  ...
}

Bundleのベクタ

単一の要素だけでなく、階層的なインタフェースとして要素のベクタを定義することができる。 例えば、入力ベクタを受け取り、出力ベクタを渡すクロスバースイッチは、以下のようにして定義できる。

class CrossbarIo(n: Int) extends Bundle {
  val in = Vec(n, Flipped(new PLink))
  val sel = Input(UInt(sizeof(n).W)
  val out = Vec(n, new PLink)
}

バルク接続

2つのフィルタを1つのフィルタブロックに集約することができる。

class Block extends Module {
  val io = IO(new FilterIO)
  val f1 = Module(new Filter)
  val f2 = Module(new Filter)
  f1.io.x <> io.x
  f1.io.y <> f2.io.x
  f2.io.y <> io.y
}

<>のバルク接続インタフェースは、モジュール間が同じレイヤに存在すれば右辺と左辺で逆の方向を持ったインタフェースであり、親モジュールと子モジュールの関係であれば、同じ方向のインタフェースである。

バルク接続は、最下層のポートはお互いに同じ名前である必要がある。もし名前が異なっていれば、Chiselは接続を行わない。

マルチプレクサと入力選択

Chiselはいくつかのビルトイン入力選択モジュールを持っている。

Mux

2入力セレクタ。以下のようにしてネストして複数入力にすることも可能。

Mux(c1, a, Mux(c2, b, Mux(..., default)))

MuxCase

n入力のマルチプレクサ。わざわざ上記のMuxを連続させる必要はない。これを使えばよい。c1,c2などで記述された条件が成立した場合に選択。

MuxCase(default, Array(c1 -> a, c2 -> b, ...))

MuxLoopkup

n個をインデックス付きで選択するマルチプレクサ。

MuxLookup(idx, default, 
          Array(0.U -> a, 1.U -> b, ...))

これは、上記のMuxCaseを使って以下のように記述するのと同様である。

MuxCase(default, 
        Array((idx === 0.U) -> a,
              (idx === 1.U) -> b, ...))

Mux1H

ワンホットのセレクタ。下記の例では、io.selectorの各ビットに1が立ったらその値を返すという仕組み。

  val hotValue = chisel3.util.oneHotMux(Seq(
    io.selector(0) -> 2.U,
    io.selector(1) -> 4.U,
    io.selector(2) -> 8.U,
    io.selector(4) -> 11.U,
  ))

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com