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

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

Chiselのメモリにファイルのデータをロードする方法

スポンサーリンク

今回もChiselのTips的なやつでChiselのメモリにファイルのデータをロードする方法についてを紹介する。

ChiselのメモリMemに指定したファイルのデータを読み込む方法

内容的にはこのWikiの内容↓

github.com

じゃあそっち見るわ!!ってなるので、幾つか試してみた結果も載せるつもり。

今回のソースコード

そんなわけで動かすためのコードを適当に作ってみよう。
今回試したコードは以下のリポジトリに置いてあるので、全部見たい方はそちらもどーぞ

github.com

メモリ

超適当で簡単なメモリ。

// See LICENSE for license details.

import chisel3._
import chisel3.util.experimental.loadMemoryFromFile
import firrtl.annotations.MemoryLoadFileType

/**
  * シンプルなメモリ
  * @param loadFilePath 読み込むHEX文字列データが入ったファイルのパス
  * @param fileType ファイルタイプ
  */
class Memory(loadFilePath: String,
             fileType: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex) extends Module {
  val io = IO(new Bundle {
    val addr = Input(UInt(10.W))
    val wren = Input(Bool())
    val rden = Input(Bool())
    val wrData = Input(UInt(8.W))
    val rdData = Output(UInt(8.W))
  })

  val m = Mem(16, UInt(8.W))

  /**
    * これでファイル中のメモリデータがメモリにロードされる
    * loadFilePathが存在してる時にのみ、読むようにした方が良かったな、コレ。
    */
  if (loadFilePath != "") {
    println(s"load file : ${loadFilePath}")
    loadMemoryFromFile(m, loadFilePath, fileType)
  }

  when(io.wren) {
    m(io.addr) := io.wrData
  }

  io.rdData := m(io.addr)
}

テスト用モジュール

これもすごくシンプルなテスト回路。一応メモリにはライト処理もつけたので、ユニットテスト用のモジュールにはライト用のメソッドが存在してるけど、別にいらなかったことに気づいた。

import scala.io.Source
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
import firrtl.annotations.MemoryLoadFileType

/**
  * Memのテストユニット
  * @param c Memのインスタンス
  */
class MemUnitTester(c: Memory) extends PeekPokeTester(c) {
  /**
    * メモリ・ライト
    * @param addr メモリのワードアドレス
    * @param data 書き込むデータ
    */
  def write(addr: BigInt, data: BigInt): Unit = {
    poke(c.io.wren, true)
    poke(c.io.addr, addr)
    poke(c.io.wrData, data)
    step(1)
  }

  /**
    * メモリ・リード
    * @param addr メモリのワードアドレス
    * @return リードしたメモリのデータ
    */
  def read(addr: BigInt): BigInt = {
    poke(c.io.addr, addr)
    step(1)
    peek(c.io.rdData)
  }

  /**
    * 期待値比較付きメモリ・リード
    * @param addr メモリのワードアドレス
    * @param exp 期待値
    * @return リードの比較結果
    */
  def readCmp(addr: Int, exp: BigInt): Boolean = {
    read(addr)
    expect(c.io.rdData, exp)
  }
}

/**
  * Memのテストクラス
  */
class MemoryTester extends ChiselFlatSpec {

  behavior of "Mem"

  val defaultArgs = Array(
    "--generate-vcd-output", "off",
    "--backend-name", "treadle"
    //"--is-verbose"
  )

  val loadFileDir = "subprj/load-chisel-mem/src/test/resources/"

  it should "16進文字列のメモリデータが入ったファイルをロード出来る" in {
    val filePath = loadFileDir + "test00_basic.hex"
    Driver.execute(defaultArgs, () => new Memory(filePath)) {
      c => new MemUnitTester(c) {
        reset()

        //
        var addr = 0
        for (line <- Source.fromFile(filePath).getLines()) {
          readCmp(addr, BigInt(line, 16))
          addr += 1
        }

      }
    } should be (true)
  }
}

とりあえずテストを実行

テストも書いたので、早速実行してみる。
なおテスト時に読み込むファイルの中身は以下のように読み込みたいデータが16進文字列で1行毎に書かれているもの。

  • test00_basic.hex
03
76
82
37
35
31
7e
4f
45
e2
b5
53
7a
5b
02
17

実行すると以下のようにテスト側でファイルからリードした値とメモリのデータが一致しPASSする。
因みにだが、今現在この機能がサポートされているバックエンドは、手元で試せる範囲では

  • treadle
  • verilator

の2つになるようだ。バックエンドがfirrtlだとメモリにデータが設定されずに試験がFAILする。

[IJ]sbt:loadChiselMem> testOnly MemoryTester -- -z メモリデータ
[info] [0.002] Elaborating design...
load file : subprj/load-chisel-mem/src/test/resources/test00_basic.hex
[info] [0.159] Done elaborating.
Total FIRRTL Compile Time: 291.1 ms
Total FIRRTL Compile Time: 102.9 ms
file loaded in 0.200993415 seconds, 25 symbols, 13 statements
[info] [0.001] SEED 1555817678504
test Memory Success: 31 tests passed in 51 cycles in 0.024450 seconds 2085.86 Hz
[info] [0.010] RAN 46 CYCLES PASSED
[info] MemoryTester:
[info] Mem
[info] - should 16進文字列のメモリデータが入ったファイルをロード出来る
[info] ScalaTest
[info] Run completed in 1 second, 299 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 2 s, completed 2019/04/21 12:34:39

ファイルの形式について

ファイルのフォーマットについて少しだけ追加調査をしておく。 確認したのは以下の2点

  • 空行やコメントは入れれるかどうか
  • メモリのサイズを超えた場合の処理はどうなるか
ファイルのフォーマットについて

結論から書くと、

  • 空行やコメントは許容されない

だった。 これは上記で実施したテストファイルに空行やコメント(Scalaなので//で試した)を入れてみると簡単に確認できる。
確認した時の結果を記載しておく。

  • ファイルに空行が含まれる場合
[info] - should ファイルデータは16進文字列でのみ構成される *** FAILED ***
[info]   treadle.executable.TreadleException: loading memory m[4] <= : error: Zero length BigInteger
[info]   at treadle.executable.TreadleException$.apply(TreadleException.scala:10)
  • ファイルにコメント的なものを入れた場合
[info]   treadle.executable.TreadleException: loading memory m[4] <= // コメント: error: For input string: "// コメント"
[info]   at treadle.executable.TreadleException$.apply(TreadleException.scala:10)
[info]   at treadle.executable.MemoryInitializer$$anonfun$treadle$executable$MemoryInitiali

内部では入力されたファイルから1行読み取り、それをBigIntで変換しており(後述)、その変換の前に値のチェックが行われていないため、BigIntに変換できないデータは扱えない。

Binaryでの読み込み

MemoryLoadFileTypeにはHexBinaryの2つのタイプが用意されている。
先の例はMemoryLoadFileType.Hexを使用した場合だがBinaryも試してみよう。
ということで、先ほどのテストクラスにBinary版の試験を追加する。

  it should "ファイルタイプ == Binaryもあるけど動きが違う気がする" in {
    val filePath = loadFileDir + "test03.bin"

    Driver.execute(defaultArgs, () => new Memory(filePath, MemoryLoadFileType.Binary)) {
      c =>
        new MemUnitTester(c) {
          reset()

          //
          Source.fromFile(filePath).getLines().zipWithIndex.foreach {
            case (line, lineNo) => {
              readCmp(lineNo - 1, BigInt(line, 16))
            }
          }

        }
    } should be(false)
  }

テスト名で既に出落ちの感があるが、とりあえず読み込むファイルを変更して、ファイルタイプをBinaryに変更してテストを実行。 なおここで読み込んでいるtest03.binは最初のテスト時に記載した16進文字列データが含まれるものと全く同じになっている。
なっているのだが、、何故かこのテストはPASSしてしまう。

[info] MemoryTester:
[info] Mem
[info] - should ファイルタイプ == Binaryもあるけど動きが違う気がする
[info] ScalaTest
[info] Run completed in 1 second, 369 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1

今使ってるChisel-3.1.7だとここでannotateの第3引数がデフォルトになっており、外からBinaryを与えても強制的にHex扱いされる。
少し前のだとBinaryを指定するとエラーになってた気がするのでとりあえずHexのみサポートする形にしてるのかしら。

  def apply[T <: Data](
    memory: MemBase[T],
    fileName: String,
    hexOrBinary: MemoryLoadFileType.FileType = MemoryLoadFileType.Hex
  ): Unit = {
    annotate(ChiselLoadMemoryAnnotation(memory, fileName))
  }

因みにもう少し踏み込むと以下のような実装になってる。
なおこのコードはバックエンドが"treadle"の場合の実装(executable/Memory.scalaMemoryInitilaizer)
MemoryLoadFileType.Binaryって"バイナリファイル読める"じゃなくて2進数文字列のファイルを読み込めるってことなのか。 そうかVerilog HDLのreadmembとかの扱いってことか。(ここまで見てやっと気づいた。。)
個人的にはバイナリファイルをそのまま読み込めるのも素敵だと思うのだが。

  val memoryMetadata: Seq[MemoryMetadata] = memoryLoadAnnotations.flatMap { anno =>
    anno.target match {
      case ComponentName(memoryName, ModuleName(moduleName, _)) =>
        val radix = anno.hexOrBinary match {
          // MemoryLoadFileTypeに応じて基数選択
          case MemoryLoadFileType.Hex => 16
          case MemoryLoadFileType.Binary => 2
        }
        engine.symbolTable.moduleMemoryToMemorySymbol(s"$moduleName.$memoryName").toSeq.map { memorySymbol =>
          MemoryMetadata(memorySymbol, anno.getFileName, radix)
        }
      case _ => Seq()
    }
  }

  private def doInitialize(memorySymbol: Symbol, fileName: String, radix: Int): Unit = {
    io.Source.fromFile(fileName).getLines().zipWithIndex.foreach {
      case (line, lineNumber) if lineNumber < memorySymbol.slots =>
        try {
          // 基数選択結果をradixに入れて`BigInt`で変換
          val value = BigInt(line.trim, radix)
          engine.dataStore.update(memorySymbol, lineNumber, value)
        }
        catch {
          case t: TreadleException => throw t
          case t: Throwable =>
            throw TreadleException(s"loading memory ${memorySymbol.name}[$lineNumber] <= $line: error: ${t.getMessage}")
        }
      case _ =>
    }
  }

バックエンドをtreadleにした状態でも簡単にメモリの初期値設定が出来るっていうのは自分的には結構使い途がある。やっぱりverilatorにすると、コンパイルの時間が微妙にネックだったりもするので。もう少し頑張ればバイナリファイルをそのまま扱うなんてことも可能な感じなので、そっちもちょっと考えてみたい。