今回もChiselのTips的なやつでChiselのメモリにファイルのデータをロードする方法についてを紹介する。
ChiselのメモリMem
に指定したファイルのデータを読み込む方法
内容的にはこのWikiの内容↓
じゃあそっち見るわ!!ってなるので、幾つか試してみた結果も載せるつもり。
今回のソースコード
そんなわけで動かすためのコードを適当に作ってみよう。
今回試したコードは以下のリポジトリに置いてあるので、全部見たい方はそちらもどーぞ
メモリ
超適当で簡単なメモリ。
// 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
にはHex
とBinary
の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.scalaのMemoryInitilaizer
)
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にすると、コンパイルの時間が微妙にネックだったりもするので。もう少し頑張ればバイナリファイルをそのまま扱うなんてことも可能な感じなので、そっちもちょっと考えてみたい。