FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Chisel + Diplomacyで自作SoCを設計する検討 (2. 外部メモリアクセスのためのインタフェースを作る)

Chiselを使ったDiplomacyのデザイン、久しぶりに再開した。Chisel + Diplomacyを使って簡単なSoCを作ってみているが、まずは外部からメモリにデータを書き込むパスが必要だろう。これについて検討する。

Rocket-ChipなどのSoC構成では、DTMというモジュールを使ってこれらの制御を行っている。DTMJTAGとも接続されており外部から接続することができる。今回はそこまでする気が無いので、外部からELFファイルを受け取ってロードするくらいの機能で良い。今回はDTMは2つのユニットで構成することにする。以下の基本的な構成はSiFiveの構成から引っ張ってきた。

  • C++側:DPI-Cなどを使って外部との通信を行う。
  • Verilog側:DPI-CをラップしてVerilogで記述されたモジュールに接続する。
f:id:msyksphinz:20201124230723p:plain:w200

まずC++側は簡単に、リクエストを発行して書き込みアドレスとデータを送信するための簡単なコードとする。

extern "C" int debug_tick(
    unsigned char *debug_req_valid,
    unsigned char debug_req_ready,
    int *debug_req_bits_addr,
    int *debug_req_bits_data)
{

  static int count = 0;
  static int addr = 0;
  static int data = 0xdead000;

  if (debug_req_ready && count < 100) {
    *debug_req_valid     = 1;
    *debug_req_bits_addr = addr;
    *debug_req_bits_data = data;

    addr += 4;
    data += 4;

    count++;
  } else {
    *debug_req_valid     = 0;
  }

  return 0;
}

そしてこれを受け取るのがVerilog側で、Verilog側のDTMは非常に簡易なプロトコルを通じてTileLinkを読み書きできるユニットLoaderにデータを転送する。

module sim_dtm(
  input               clock,
  input               reset,

  output logic        req_valid,
  output logic [31:0] req_bits_addr,
  output logic [31:0] req_bits_data,
  input logic         req_ready
);

....
    
always_ff @(posedge clock) begin
  req_valid_reg     <= __debug_req_valid;
  req_bits_addr_reg <= __debug_req_bits_addr;
  req_bits_data_reg <= __debug_req_bits_data;
end

assign req_valid     = req_valid_reg;
assign req_bits_addr = req_bits_addr_reg;
assign req_bits_data = req_bits_data_reg;

int debug_tick_val;

always_ff @(negedge clock) begin
  if (reset) begin
  end else begin
    debug_tick_val = debug_tick(
      __debug_req_valid,
      req_ready,
      __debug_req_bits_addr,
      __debug_req_bits_data
      );
  end
end

endmodule // sim_dtm

LoaderDTMはChiselを通じて接続されており、LoaderはLazyModuleを通じてインスタンスされている。このDTMとのインタフェースはTileLinkではないので、LazyModule内のLazyModuleImpに作成されたポートを使用して接続している。

class TestHarness()(implicit p: Parameters) extends Module {
  val io = IO(new Bundle {
    val success = Output(Bool())
  })

  val ldut = LazyModule(new core_complex(4, 5000))
  val dut = Module(ldut.module)

  val dtm = Module(new sim_dtm);
  dtm.io.clock := clock
  dtm.io.reset := reset
  dut.io.req   := dtm.io.req.valid
  dut.io.addr  := dtm.io.req.bits.addr
  dut.io.data  := dtm.io.req.bits.data
  dtm.io.req.ready := dut.io.ready

...

core_complex内に直接接続用のmoduleを用意しており、これを通じてloaderのI/Oポートと接続している。

class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
  val loader = LazyModule(new loader("loader"))
...
  lazy val module = new LazyModuleImp(this) {
    val io = IO(new Bundle {
      val req   = Input(Bool())
      val addr  = Input(UInt(32.W))
      val data  = Input(UInt(32.W))
      val ready = Output(Bool())
    })

    loader.module.io.req  := io.req
    loader.module.io.addr := io.addr
    loader.module.io.data := io.data
    io.ready := loader.module.io.ready

Loaderの実装は以下のようになっており、ステートマシンを使ってTileLinkにリクエストを送信している。

class loader(name: String)(implicit p: Parameters) extends LazyModule {
  val node = TLClientNode(Seq(TLClientPortParameters(Seq(TLClientParameters(name = name)))))

  lazy val module = new LazyModuleImp(this) {
    val io = IO(new Bundle {
      val req = Input(Bool())
      val addr = Input(UInt(32.W))
...
    switch (trans_state) {
      is (s_init) {
        io.ready := true.B
        
        when(io.req) {
          reg_a_valid := true.B
          reg_a_address := io.addr
          reg_a_data := io.data
          io.ready := false.B

          trans_state := s_trans
        }
      }
      is (s_trans) {
        when(out.a.fire) {
          reg_a_valid := false.B
          io.ready := true.B

          trans_state := s_init
        }
      }
    }
    out.a.valid := reg_a_valid
    out.a.bits.address := reg_a_address
    out.a.bits.data := reg_a_data
    out.a.bits.opcode := TLMessages.PutFullData
    out.a.bits.mask := 0xf.U
    out.a.bits.size := 0.U
    out.a.bits.param := 0.U

  }

シミュレーションしてみると、一応シーケンシャルにメモリリクエストを出力できているようだ。

f:id:msyksphinz:20201124230751p:plain