FPGA開発日記

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

Chisel + Diplomacyで作る自作CPU:デバッグモニタ

自作CPUにおいてデバッグモニタというのは常に重要な機能で、私は特に波形を見るのがとても嫌いなのでデバッグ情報はなるべくテキスト形式で出力している。ちなみ今回のデバッグ機能というのはいわゆるJTAGのようなデバッグ機能ではなく、CPUをデバッグするための実装者向けデバッグ機能だ。

昔私の作った自作CPUでは、コアのパイプラインの流れが監視できるようにデバッグモニタを用意しており、デバッグビルド時にこのポートを生やして情報を出力していた。

    /* Debug-Port */
    io.dbg_monitor.hart_id := hart_id.U

    io.dbg_monitor.inst_fetch_req    := io.inst_bus.req
    io.dbg_monitor.inst_fetch_addr   := io.inst_bus.addr
    io.dbg_monitor.inst_fetch_ack    := io.inst_bus.ack
    io.dbg_monitor.inst_fetch_rddata := io.inst_bus.rddata

...

このデバッグモニタポートは、Chiselのコンフィグレーションクラスを通じて有効無効を決定する。もし無効ならばこのモニターはインスタンス化されない。

class CpuDebugMonitor [Conf <: RVConfig](conf: Conf) extends Bundle {
  override def cloneType: this.type =
    new CpuDebugMonitor(conf).asInstanceOf[this.type]

  val hart_id = Output(UInt())

  val inst_fetch_req    = if (conf.debug == true) Output(Bool())                 else Output(UInt(0.W))
  val inst_fetch_addr   = if (conf.debug == true) Output(UInt(conf.bus_width.W)) else Output(UInt(0.W))
  val inst_fetch_ack    = if (conf.debug == true) Output(Bool())                 else Output(UInt(0.W))
  val inst_fetch_rddata = if (conf.debug == true) Output(SInt(32.W))             else Output(SInt(0.W))

  val pc_update_cause = if(conf.debug == true) Output(UInt(3.W)) else Output(UInt(0.W))

...

このポートをどのようにSoC側から監視しようかと思ったのだが、結局はこのポートをVerilogのモジュールで引き取って良い具合なフォーマットに置き換えるのが良いのではないかと思った。

module sim_monitor
  (
   input logic        clock,
   input logic        reset,

   input logic        dbg_hart_id,

   input logic        dbg_inst_fetch_req,
   input logic [31:0] dbg_inst_fetch_addr,
...
  );
    
always_ff @ (posedge clock, posedge reset) begin
  if (reset) begin
  end else begin
    $write("HART%01d : ", dbg_hart_id);
    if (dbg_inst_fetch_req & dbg_inst_fetch_ack) begin

またしてもVerilogのモジュールとChiselのモジュールをどのようにつなげるのかという問題に立ち戻ってくるが、これについてはBlackBoxの機能を用いている。

class CoreTop [Conf <: RVConfig] (conf: Conf, hartid: Int, name: String)(implicit p: Parameters) extends LazyModule {
  val inst_node = TLClientNode(Seq(TLClientPortParameters(Seq(TLClientParameters(name = name + "_inst")))))
  val data_node = TLClientNode(Seq(TLClientPortParameters(Seq(TLClientParameters(name = name + "_name")))))

  lazy val module = new LazyModuleImp (this) {
    val io = IO(new Bundle {
      val run = Input(Bool())
      val done = Output(Bool())
      val error = Output(Bool())

      // dbg_monitorポートを用意しておく
      val dbg_monitor = Output(new CpuDebugMonitor(conf))
    })
...
    // dbg_monitorポートを接続する
    io.dbg_monitor := cpu.io.dbg_monitor
  }
}

このポートはTestHarness上で接続してしまおう。

  val sim_mon0 = Module(new sim_monitor(rv_conf))
  sim_mon0.io.clock := clock
  sim_mon0.io.reset := reset
  sim_mon0.io.dbg := dut.io.cpu0_dbg
  val sim_mon1 = Module(new sim_monitor(rv_conf))
  sim_mon1.io.clock := clock
  sim_mon1.io.reset := reset
  sim_mon1.io.dbg := dut.io.cpu1_dbg

そしてsim_monitorモジュールをVerilog側で宣言しておけば無事に接続できる、という訳だ。シミュレーションを走らせてみると、一応上手く監視できている様子。ただしRTLが間違っているので出力された情報は全く意味がないが...

               99781 HART1 :
               99781 HART0 : 11800044
               99791 HART1 : 11800044
               99791 HART0 :
               99801 HART1 :
               99801 HART0 : 1180004c
               99811 HART1 : 1180004c
               99811 HART0 :         R30<=0000000011801040
               99821 HART1 :         R30<=0000000011801040
               99821 HART0 :
               99831 HART1 :
f:id:msyksphinz:20201129183442p:plain