自作CPUに対して割込みコントローラの実装をしているが、検証環境との調整でいくつか悩んでいる。
主に、CLINTなどの割込みコントローラの動作と、検証用シミュレータSpikeとのタイミングを合わせる方法だ。
何が問題か?
これを解決するためには、RTL側が割り込みが挿入され、それに基づきフロントエンドがフェッチを変えたことをROBの最後まで記憶しておき、コミット時に同じタイミングでSpikeに伝えることになる。
これにより、RTLに
SIMULATION
記述を追加し、割込みによりフローが変わったことを示し、コミット時にそれをSpikeに通知する信号が必要になる。
で、単純にRTLを実装すると、
- CLINTで割り込みを通知する
- 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