令和になって最初の投稿。ほんとは昨日上げたかったんだけど。。 いずれにしても読んでくれてる方は引き続きどうぞよろしくですm(_ _)m
前回の記事の終わりに以下のようなこと書いた。
MemBase
にマスク付きのライトタスクが用意されているので、ひょっとするとそっちを使うと何か変化が起きるのかもしれないので別途試してみようと思う。
なので、今回は上記のネタ提起の回収編を書こうと思う。 あと実は前回の2次元メモリは不要な記述があったので、それについての訂正も。。。
Chiselで2次元のメモリの続き
前回に書いたとおり2次元メモリはSystem Verilogで使える以下のような奴のこと。
reg [7:0][2:0] mem[0:1023]
でこれを実現するにあたって、前回実装したものが以下のようなものだった。
注:前回のものから少し変更しました。変更点は以下の2点。
-
Bundle
で包んでいたVec
を直接そのままMem
に渡す形にした- 前回の記事ではメモリ内の
Vec
は変数col
に入っていた
- 前回の記事ではメモリ内の
- 書き込み処理を
for
式で書くようにした
なお、この記事に合わせて前回の記事にも訂正を入れてます。
class Mem2D extends Module { val io = IO(new Bundle { val wren = Input(Bool()) val rden = Input(Bool()) val addr = Input(UInt(14.W)) val wrdata = Input(UInt(32.W)) val rddata = Output(UInt(32.W)) }) val m = Mem(16, Vec(4, UInt(8.W))) // when(io.wren) { for (i <- 0 until m(0).length) { m(io.addr)(i) := io.wrdata(((i + 1) * 8) - 1, i * 8) } } io.rddata := Cat(m(io.addr).reverse) }
Mem
のwrite
メソッドを使用する形に書き換えてみる
ご存じの方もいると思うがChiselのMem
のスーパークラスのMemBase
にメモリにアクセスするためにread
/write
メソッドが用意されている。
書き込み用のメソッドwrite
には2つの実装が用意されており、そのうちの一つがVec
を受け取ることを想定している形になっているので、上記のMem2D
クラスをwrite
メソッドを使用した形に変更して生成されるRTLに変化があるかを確認してみようと思う。
MemBase
のwrite
の実装
まずはwrite
クラスの実装を確認してみよう。
def write(idx: UInt, data: T, mask: Seq[Bool]) (implicit evidence: T <:< Vec[_], compileOptions: CompileOptions): Unit = { implicit val sourceInfo = UnlocatableSourceInfo val accessor = makePort(sourceInfo, idx, MemPortDirection.WRITE).asInstanceOf[Vec[Data]] val dataVec = data.asInstanceOf[Vec[Data]] if (accessor.length != dataVec.length) { Builder.error(s"Mem write data must contain ${accessor.length} elements (found ${dataVec.length})") } if (accessor.length != mask.length) { Builder.error(s"Mem write mask must contain ${accessor.length} elements (found ${mask.length})") } for (((cond, port), datum) <- mask zip accessor zip dataVec) when (cond) { port := datum } }
引数を見てもらえればわかる通り、型パラメータT
を受け取る形になっている。このT
は前回に確認したとおりインスタンス時に渡す第二引数の型でありChiselのData
から派生したものになる。
Mem2D
を書き換える
今回の場合はT
はVec(4, UInt(8.W))
になるのでwrite
メソッドを使用する場合は渡すデータもこの形に変更する必要がある。
このことを踏まえてMem2D
クラスを書き換えてみよう。
class Mem2D(useWriteTask: Boolean = false) extends Module { val io = IO(new Bundle { val wren = Input(Bool()) val rden = Input(Bool()) val addr = Input(UInt(14.W)) val wrdata = Input(UInt(32.W)) val rddata = Output(UInt(32.W)) }) val m = Mem(16, Vec(4, UInt(8.W))) if (useWriteTask) { val wrdata = Wire(Vec(4, UInt(8.W))) for (i <- 0 until m(0).length) { wrdata(i) := io.wrdata(((i + 1) * 8) - 1, i * 8) } m.write(io.addr, wrdata, Seq.fill(4)(true.B)) } else { when(io.wren) { for (i <- 0 until m(0).length) { m(io.addr)(i) := io.wrdata(((i + 1) * 8) - 1, i * 8) } } } io.rddata := Cat(m.read(io.addr).reverse) }
ライト部分を変更して生成されるRTLに差が出るかを確認したかったのでクラスパラメータとしてuseWriteTask
を用意してこれで切り替える形にした。
メモリへの書き込み部分については32bitのio.wrdata
を8bit x 4のWire(Vec)
に変換したwrdata
を使って書き込む様にしてある。
新しいMem2D
クラスのテスト
これに対して前回の記事で実施したテストを実行してみる。
テストクラスはかなり適当に以下の感じ。
/** * Mem2Dの単体テストクラス * @param c Mem2D */ class Mem2DUnitTester(c: Mem2D) extends PeekPokeTester(c) { /** * メモリライト * @param addr メモリアドレス * @param data 書き込むデータ */ def write(addr: Int, data: BigInt): Unit = { poke(c.io.wren, true) poke(c.io.addr, addr) poke(c.io.wrdata, data) step(1) poke(c.io.wren, false) } /** * メモリリード * @param addr メモリアドレス * @return 指定したアドレスのメモリの値 */ def read(addr: Int): BigInt = { poke(c.io.rden, true) poke(c.io.addr, addr) step(1) poke(c.io.rden, false) peek(c.io.rddata) } import scala.math.{floor, random} /** * テストシナリオ * - アドレス0-15に適当に値書いて読むだけ */ for (i <- 0 until 16) { val data = i + intToUnsignedBigInt(floor(random * 0xffffffffL).toInt) //0x70a0a001 write(i, data) step(1) println(s"read data = 0x${read(i).toInt.toHexString}") expect(c.io.rddata, data) } } /** * Mem2Dのテスト */ class Mem2DTester extends ChiselFlatSpec { behavior of "Mem2D" it should "32bit単位でメモリにアクセス出来る" in { Driver.execute(Array[String]("--generate-vcd-output=on"), () => new Mem2D) { c => new Mem2DUnitTester(c) } should be (true) } it should "Memのwriteを使っても32bit単位でメモリにアクセス出来る" in { Driver.execute(Array[String](), () => new Mem2D(true)) { c => new Mem2DUnitTester(c) } should be (true) } }
テスト結果
以下の様にwrite
を使う場合/使わない場合で同じ様にPASSすることが確認できた。
[info] Mem2DTester: [info] Mem2D [info] - should 32bit単位でメモリにアクセス出来る [info] - should Memのwriteを使っても32bit単位でメモリにアクセス出来る [info] ScalaTest [info] Run completed in 1 second, 647 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed.
RTLを比較してみる
ここまでで動作自体はwrite
を使用しない版と同等になっていることを確認できたので、RTLを生成して確認してみる。
ということでエラボレート用のメインオブジェクトを準備
/** * RTL生成用メインオブジェクト */ object ElaborateMem2D extends App { // write不使用 Driver.execute(Array[String]("-tn=Mem2D", "-td=rtl"), () => new Mem2D()) // write使用 Driver.execute(Array[String]("-tn=Mem2DWithWrite", "-td=rtl"), () => new Mem2D(true)) }
これを実行すると"rtl"というディレクトリの下に"Mem2D.*"と"Mem2DWithWrite.*"というファイルが幾つか生成される。write
を使用しない版のRTLコード全部については前回の記事をご覧いただくとして、ここではwrite
使用版で生成したRTLと不使用版のメモリ書き込み部分のみを記載する。
write
不使用版のメモリ書き込み部分の論理
always @(posedge clock) begin if(m_0__T_39_en & m_0__T_39_mask) begin m_0[m_0__T_39_addr] <= m_0__T_39_data; // @[Mem2D.scala 15:14:@8.4] end if(m_0__T_53_en & m_0__T_53_mask) begin m_0[m_0__T_53_addr] <= m_0__T_53_data; // @[Mem2D.scala 15:14:@8.4] end if(m_0__T_67_en & m_0__T_67_mask) begin m_0[m_0__T_67_addr] <= m_0__T_67_data; // @[Mem2D.scala 15:14:@8.4] end if(m_0__T_81_en & m_0__T_81_mask) begin m_0[m_0__T_81_addr] <= m_0__T_81_data; // @[Mem2D.scala 15:14:@8.4] end if(m_1__T_39_en & m_1__T_39_mask) begin m_1[m_1__T_39_addr] <= m_1__T_39_data; // @[Mem2D.scala 15:14:@8.4] end if(m_1__T_53_en & m_1__T_53_mask) begin m_1[m_1__T_53_addr] <= m_1__T_53_data; // @[Mem2D.scala 15:14:@8.4] end if(m_1__T_67_en & m_1__T_67_mask) begin m_1[m_1__T_67_addr] <= m_1__T_67_data; // @[Mem2D.scala 15:14:@8.4] end if(m_1__T_81_en & m_1__T_81_mask) begin m_1[m_1__T_81_addr] <= m_1__T_81_data; // @[Mem2D.scala 15:14:@8.4] end if(m_2__T_39_en & m_2__T_39_mask) begin m_2[m_2__T_39_addr] <= m_2__T_39_data; // @[Mem2D.scala 15:14:@8.4] end if(m_2__T_53_en & m_2__T_53_mask) begin m_2[m_2__T_53_addr] <= m_2__T_53_data; // @[Mem2D.scala 15:14:@8.4] end if(m_2__T_67_en & m_2__T_67_mask) begin m_2[m_2__T_67_addr] <= m_2__T_67_data; // @[Mem2D.scala 15:14:@8.4] end if(m_2__T_81_en & m_2__T_81_mask) begin m_2[m_2__T_81_addr] <= m_2__T_81_data; // @[Mem2D.scala 15:14:@8.4] end if(m_3__T_39_en & m_3__T_39_mask) begin m_3[m_3__T_39_addr] <= m_3__T_39_data; // @[Mem2D.scala 15:14:@8.4] end if(m_3__T_53_en & m_3__T_53_mask) begin m_3[m_3__T_53_addr] <= m_3__T_53_data; // @[Mem2D.scala 15:14:@8.4] end if(m_3__T_67_en & m_3__T_67_mask) begin m_3[m_3__T_67_addr] <= m_3__T_67_data; // @[Mem2D.scala 15:14:@8.4] end if(m_3__T_81_en & m_3__T_81_mask) begin m_3[m_3__T_81_addr] <= m_3__T_81_data; // @[Mem2D.scala 15:14:@8.4] end end
write
使用版のRTL
module Mem2D( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_wren, // @[:@6.4] input io_rden, // @[:@6.4] input [13:0] io_addr, // @[:@6.4] input [31:0] io_wrdata, // @[:@6.4] output [31:0] io_rddata // @[:@6.4] ); reg [7:0] m_0 [0:15]; // @[Mem2D.scala 15:14:@8.4] reg [31:0] _RAND_0; wire [7:0] m_0__T_70_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_0__T_70_addr; // @[Mem2D.scala 15:14:@8.4] wire [7:0] m_0__T_57_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_0__T_57_addr; // @[Mem2D.scala 15:14:@8.4] wire m_0__T_57_mask; // @[Mem2D.scala 15:14:@8.4] wire m_0__T_57_en; // @[Mem2D.scala 15:14:@8.4] reg [7:0] m_1 [0:15]; // @[Mem2D.scala 15:14:@8.4] reg [31:0] _RAND_1; wire [7:0] m_1__T_70_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_1__T_70_addr; // @[Mem2D.scala 15:14:@8.4] wire [7:0] m_1__T_57_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_1__T_57_addr; // @[Mem2D.scala 15:14:@8.4] wire m_1__T_57_mask; // @[Mem2D.scala 15:14:@8.4] wire m_1__T_57_en; // @[Mem2D.scala 15:14:@8.4] reg [7:0] m_2 [0:15]; // @[Mem2D.scala 15:14:@8.4] reg [31:0] _RAND_2; wire [7:0] m_2__T_70_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_2__T_70_addr; // @[Mem2D.scala 15:14:@8.4] wire [7:0] m_2__T_57_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_2__T_57_addr; // @[Mem2D.scala 15:14:@8.4] wire m_2__T_57_mask; // @[Mem2D.scala 15:14:@8.4] wire m_2__T_57_en; // @[Mem2D.scala 15:14:@8.4] reg [7:0] m_3 [0:15]; // @[Mem2D.scala 15:14:@8.4] reg [31:0] _RAND_3; wire [7:0] m_3__T_70_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_3__T_70_addr; // @[Mem2D.scala 15:14:@8.4] wire [7:0] m_3__T_57_data; // @[Mem2D.scala 15:14:@8.4] wire [3:0] m_3__T_57_addr; // @[Mem2D.scala 15:14:@8.4] wire m_3__T_57_mask; // @[Mem2D.scala 15:14:@8.4] wire m_3__T_57_en; // @[Mem2D.scala 15:14:@8.4] wire [15:0] _T_82; // @[Cat.scala 30:58:@35.4] wire [15:0] _T_83; // @[Cat.scala 30:58:@36.4] assign m_0__T_70_addr = io_addr[3:0]; assign m_0__T_70_data = m_0[m_0__T_70_addr]; // @[Mem2D.scala 15:14:@8.4] assign m_0__T_57_data = io_wrdata[7:0]; assign m_0__T_57_addr = io_addr[3:0]; assign m_0__T_57_mask = 1'h1; assign m_0__T_57_en = 1'h1; assign m_1__T_70_addr = io_addr[3:0]; assign m_1__T_70_data = m_1[m_1__T_70_addr]; // @[Mem2D.scala 15:14:@8.4] assign m_1__T_57_data = io_wrdata[15:8]; assign m_1__T_57_addr = io_addr[3:0]; assign m_1__T_57_mask = 1'h1; assign m_1__T_57_en = 1'h1; assign m_2__T_70_addr = io_addr[3:0]; assign m_2__T_70_data = m_2[m_2__T_70_addr]; // @[Mem2D.scala 15:14:@8.4] assign m_2__T_57_data = io_wrdata[23:16]; assign m_2__T_57_addr = io_addr[3:0]; assign m_2__T_57_mask = 1'h1; assign m_2__T_57_en = 1'h1; assign m_3__T_70_addr = io_addr[3:0]; assign m_3__T_70_data = m_3[m_3__T_70_addr]; // @[Mem2D.scala 15:14:@8.4] assign m_3__T_57_data = io_wrdata[31:24]; assign m_3__T_57_addr = io_addr[3:0]; assign m_3__T_57_mask = 1'h1; assign m_3__T_57_en = 1'h1; assign _T_82 = {m_1__T_70_data,m_0__T_70_data}; // @[Cat.scala 30:58:@35.4] assign _T_83 = {m_3__T_70_data,m_2__T_70_data}; // @[Cat.scala 30:58:@36.4] assign io_rddata = {_T_83,_T_82}; // @[Mem2D.scala 31:13:@38.4] /* 初期値のランダム化部分は省略 */ always @(posedge clock) begin if(m_0__T_57_en & m_0__T_57_mask) begin m_0[m_0__T_57_addr] <= m_0__T_57_data; // @[Mem2D.scala 15:14:@8.4] end if(m_1__T_57_en & m_1__T_57_mask) begin m_1[m_1__T_57_addr] <= m_1__T_57_data; // @[Mem2D.scala 15:14:@8.4] end if(m_2__T_57_en & m_2__T_57_mask) begin m_2[m_2__T_57_addr] <= m_2__T_57_data; // @[Mem2D.scala 15:14:@8.4] end if(m_3__T_57_en & m_3__T_57_mask) begin m_3[m_3__T_57_addr] <= m_3__T_57_data; // @[Mem2D.scala 15:14:@8.4] end end endmodule
一目瞭然でしたね。write
を使用すると不要な論理が生成されないので、こちらを使ったほうが見やすくて良いという結果になりました。
因みにwrite
使わない版io.wren
の部分に各バイトレーン用のmask
を作って&&
とる実装も試してみたけど、結果は全く変わらんかった。
UInt
→Wire(Vec[UInt])
の変換を行う必要があるとこは多少面倒な感じもするけど、write
使うほうが良さそうかな。いろいろすっきりするし。