FPGA開発日記

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

自作CPUの割込みコントローラとISS一致検証タイミングの検討

自作CPUに対して割込みコントローラの実装をしているが、検証環境との調整でいくつか悩んでいる。

主に、CLINTなどの割込みコントローラの動作と、検証用シミュレータSpikeとのタイミングを合わせる方法だ。

  • 何が問題か?

    • CLINTが割り込みを挿入しても、次の命令フェッチのPCが切り替わるだけですぐに割込みが入るわけではない。
    • 一方でSpike割り込みを挿入した瞬間にレジスタが書き換えられ、割込みが挿入されるので、RTLとISSでタイミングがずれる
  • これを解決するためには、RTL側が割り込みが挿入され、それに基づきフロントエンドがフェッチを変えたことをROBの最後まで記憶しておき、コミット時に同じタイミングでSpikeに伝えることになる。

  • これにより、RTLにSIMULATION記述を追加し、割込みによりフローが変わったことを示し、コミット時にそれをSpikeに通知する信号が必要になる。

で、単純にRTLを実装すると、

  1. CLINTで割り込みを通知する
  2. MIPを更新して、MIE & MIPでCPUのフェッチ先を変える

で、この最中にパイプラインを走っていた命令がコミットすると、MSTATUSがすでに更新されているため、ISSとのMSTATUSの一致比較ミスとなってしまう。 これを許すかどうか、というのも一つの問題なのだが、そもそも、MIPが挿入されて割込みにモードが変わった場合、パイプラインをフラッシュすべきかどうか、という問題がある。

この場合、Rocketの動作はどのようになっているのだろう?


まず、CSR内でMIEとMIPが立ち上がっているとことをpending_interruptsで通知される。

  val pending_interrupts = high_interrupts | (read_mip & reg_mie)

ここからいろいろあって、Rocket側に割込み通知が送信されるものと思われる。

  val m_interrupts = Mux(nmie && (reg_mstatus.prv <= PRV.S || reg_mstatus.mie), ~(~pending_interrupts | read_mideleg), UInt(0))
  val s_interrupts = Mux(nmie && (reg_mstatus.v || reg_mstatus.prv < PRV.S || (reg_mstatus.prv === PRV.S && reg_mstatus.sie)), pending_interrupts & read_mideleg & ~read_hideleg, UInt(0))
  val vs_interrupts = Mux(nmie && (reg_mstatus.v && (reg_mstatus.prv < PRV.S || reg_mstatus.prv === PRV.S && reg_vsstatus.sie)), pending_interrupts & read_hideleg, UInt(0))
  val (anyInterrupt, whichInterrupt) = chooseInterrupt(Seq(vs_interrupts, s_interrupts, m_interrupts, nmi_interrupts, d_interrupts))
  val interruptMSB = BigInt(1) << (xLen-1)
  val interruptCause = UInt(interruptMSB) + (nmiFlag << (xLen-2)) + whichInterrupt
  io.interrupt := (anyInterrupt && !io.singleStep || reg_singleStepped) && !(reg_debug || io.status.cease)
  io.interrupt_cause := interruptCause

このあと、CSR側から割込みベクタを挿入して、割込みに切り替わるらしい。 その間は、どうもすべての命令のコミットを停止しているように見える?

  val tvec = Mux(trapToDebug, debugTVec, Mux(trapToNmi, nmiTVec, notDebugTVec))
  io.evec := tvec
  io.imem.req.valid := take_pc
  io.imem.req.bits.speculative := !take_pc_wb
  io.imem.req.bits.pc :=
    Mux(wb_xcpt || csr.io.eret, csr.io.evec, // exception or [m|s]ret
    Mux(replay_wb,              wb_reg_pc,   // replay
                                mem_npc))    // flush or branch misprediction