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

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

Chiselで2次元メモリっぽいのを作る(2)

スポンサーリンク

令和になって最初の投稿。ほんとは昨日上げたかったんだけど。。 いずれにしても読んでくれてる方は引き続きどうぞよろしくですm(_ _)m

前回の記事の終わりに以下のようなこと書いた。

MemBaseにマスク付きのライトタスクが用意されているので、ひょっとするとそっちを使うと何か変化が起きるのかもしれないので別途試してみようと思う。

www.tech-diningyo.info

なので、今回は上記のネタ提起の回収編を書こうと思う。 あと実は前回の2次元メモリは不要な記述があったので、それについての訂正も。。。

Chiselで2次元のメモリの続き

前回に書いたとおり2次元メモリはSystem Verilogで使える以下のような奴のこと。

reg [7:0][2:0] mem[0:1023]

でこれを実現するにあたって、前回実装したものが以下のようなものだった。

注:前回のものから少し変更しました。変更点は以下の2点。

  1. Bundleで包んでいたVecを直接そのままMemに渡す形にした
    1. 前回の記事ではメモリ内のVecは変数colに入っていた
  2. 書き込み処理を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)
}

Memwriteメソッドを使用する形に書き換えてみる

ご存じの方もいると思うがChiselのMemスーパークラスMemBaseにメモリにアクセスするためにread/writeメソッドが用意されている。
書き込み用のメソッドwriteには2つの実装が用意されており、そのうちの一つがVecを受け取ることを想定している形になっているので、上記のMem2Dクラスをwriteメソッドを使用した形に変更して生成されるRTLに変化があるかを確認してみようと思う。

MemBasewriteの実装

まずは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を書き換える

今回の場合はTVec(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を作って&&とる実装も試してみたけど、結果は全く変わらんかった。
UIntWire(Vec[UInt])の変換を行う必要があるとこは多少面倒な感じもするけど、write使うほうが良さそうかな。いろいろすっきりするし。