2026年了,FPGA工程师做RISC-V软核,中断嵌套和优先级抢占怎么用Verilog实现才不丢中断?

开放6 回答 66 浏览

最近在安路FPGA上做RISC-V软核,发现中断嵌套处理特别容易出问题。现在用的是一段式状态机处理中断,但高优先级中断来了之后,低优先级的中断现场保存和恢复总是出错,偶尔还会丢中断。想请教下大家,中断嵌套的优先级抢占逻辑用Verilog怎么写才规范?现场保存是全部压栈还是只保存用到的寄存器?有没有什么成熟的架构可以参考?

分享:

6 个回答

  • 逻辑电路爱好者

    先别急着改 Verilog,你现在的核心问题不是「一段式状态机」这个写法本身,而是你把「中断现场保存」和「中断服务程序执行」耦合在了同一个状态机里。RISC-V 软核的中断处理,规范做法是硬件只做两件事:检测到中断请求且满足优先级条件时,把当前 PC 和必要的 CSR(比如 mstatus、mepc)压入专用栈或内存栈,然后跳转到中断向量;剩下的保存/恢复通用寄存器、处理嵌套,应该交给软件(比如 trap handler)或一个独立的硬件状态机去管。你现在的丢中断,大概率是因为在低优先级中断的现场保存还没完成时,高优先级中断来了,而你的状态机正处于「保存现场」这个中间态,它没法同时响应新中断——这就是典型的临界区问题。解决思路有两条路:一是把现场保存做成「可抢占」的,即保存现场的过程本身拆成多个小步骤,每步之间检查是否有更高优先级中断,有就先响应高优先级,但这样硬件复杂度上去了,而且你还要处理「保存了一半的现场」怎么恢复的问题;二是更实用的做法,硬件只保存 PC 和关键 CSR,通用寄存器全部由软件在 trap handler 里压栈,这样硬件状态机在保存完 PC 和 CSR 后立刻进入可接受新中断的状态,软件在压栈通用寄存器时如果又被高优先级打断,那没关系,因为软件压栈用的栈指针(sp)和返回地址在硬件里已经保存好了,新中断会重新走一遍硬件保存流程,最后软件层层返回时自然会把现场恢复对。至于现场保存是全部压栈还是只保存用到的,这个取决于你的编译器 ABI——如果软核跑的是 GCC,一般约定 trap handler 里保存所有被调用者保存寄存器(callee-saved),因为硬件不知道软件会用到哪些,全保存最稳,但如果你能保证 trap handler 里只用某几个寄存器,那可以只保存那几个,但这样一旦未来改代码就容易踩坑。成熟架构你可以参考 SiFive 的 Freedom E 系列或者 picorv32 的中断处理,但更推荐看 RISC-V 官方特权架构文档里对 mret 和 mepc 的行为描述,把硬件和软件的职责边界划清楚。你现在的状态机设计,本质上是在试图让硬件做太多事,反而把简单问题搞复杂了。你用的安路 FPGA 具体型号和软核的 CSR 实现程度是什么?如果 mstatus 里的 MIE 位和 MPIE 位没有正确实现,嵌套逻辑会非常难做。

  • Verilog入门者

    个人感觉你现在的做法有点「用状态机硬扛」的意思。中断嵌套的关键不是状态机怎么写,而是你要明确硬件只负责「最小必要操作」:保存 PC 和 mepc、关中断、跳转。剩下的寄存器保存全部放软件,软件里用一条指令就能开启中断(比如 csrsi mstatus, 0x8),这样高优先级中断来了,软件正在压栈时被打断也没关系,因为 sp 和返回路径都还在。你丢中断的原因很可能是硬件状态机在保存现场期间把全局中断屏蔽了,而这段屏蔽时间太长。试试把通用寄存器的保存移出硬件,只留 PC 和 CSR 的保存,中断响应时间会短很多,嵌套也就自然能工作了。至于全部压栈还是部分压栈,看你的 trap handler 怎么写,一般全保存更省心。

  • 码电路的阿明

    中断嵌套做得不顺,先别急着把问题归到「一段式状态机」头上,真正的坑往往在「现场保存期间全局中断被屏蔽了多久」这个点上。你现在的丢中断,大概率是低优先级中断进入后,硬件状态机在保存 PC、mstatus、mepc 和通用寄存器的那几个周期里,把全局中断使能拉低了,而高优先级中断恰恰在这个窗口到达,硬件一看中断被屏蔽,直接不采信,等窗口结束再去查,请求已经撤了——这就是最常见的丢法。

    我建议你先把「硬件保存」的范围缩到最小:只保存 PC 和必要的 CSR(mepc、mstatus),然后立刻跳转到 trap handler,通用寄存器的保存全部放到软件里做。这样硬件状态机从检测到中断到跳转,可能只要两三个周期,屏蔽窗口极短,高优先级中断基本不会撞上。软件压栈时,你可以在保存完 sp 和返回地址后,用一条 csrsi mstatus, 0x8 把全局中断打开,这样即使压栈压到一半被高优先级打断也没关系,因为 sp 已经指向正确的栈帧,低优先级的现场不会丢,返回时按嵌套顺序弹栈即可。

    至于全部压栈还是只保存用到的寄存器,个人建议软核阶段先无脑全保存。RISC-V 的 ABI 里 caller-saved 和 callee-saved 分得很细,但你自己的 trap handler 如果只写个简单的中断服务,很难保证每个路径都只碰特定寄存器,一旦漏保存一个,查 bug 的代价远超多压几个寄存器的几个周期。等你的软核跑稳定了,再考虑用编译器的 interrupt 属性或手动写汇编做部分压栈优化。

    另外,优先级抢占逻辑本身,建议把「仲裁」和「执行」拆开:仲裁模块只比较当前中断号和请求中断号的优先级,输出一个 grant;执行状态机拿到 grant 后,先完成当前现场保存,再跳到新中断向量。不要在一个状态机里既做仲裁又做保存,那样状态爆炸,边界条件特别容易漏。安路的话,看下你用的型号有没有内置的 PLIC 或 CLINT 参考,或者直接参考 SiFive 的 Freedom E310 那种简单 PLIC 写法,把 pending、enable、priority 三组寄存器做出来,仲裁逻辑就是比较 priority 数组里最高位,很直接。

    你现在的 trap handler 是用汇编写的还是 C 写的?如果 C 的话,编译器默认会保存一堆寄存器,现场保存的窗口可能比你想象的长很多。

  • 焊板子的小明

    丢中断十有八九是「保存现场期间关中断关太久」。你把硬件状态机里保存通用寄存器的部分砍掉,只留 PC 和 mepc/mstatus 的保存,跳转后立刻软件压栈,压栈开始时就把全局中断打开。这样高优先级来了,最多打断你正在压栈的软件代码,sp 已经指向正确位置,弹栈顺序不会乱。优先级仲裁单独做个模块,比较 pending 和 priority 寄存器,grant 出来再决定跳哪个向量。全部压栈吧,软核阶段别省那几个周期。你现在用的是安路哪款芯片?有些型号的 LUT 资源紧张,状态机写复杂了时序反而更差。

  • 芯片设计入门

    既然你提到安路,顺带说一句,他们家的 IDE 对软核调试的支持一般,你如果总是靠仿真看波形找丢中断,效率很低。建议在软核里加一个简单的计数器,每次进入中断向量就加一,返回时减一,用 JTAG 或 UART 把当前嵌套深度打出来,能快速定位是「没进中断」还是「进错了中断」。另外,你现在的优先级是固定编码还是软件可配?如果固定编码,仲裁逻辑用 casez 比较最高位就行;如果软件可配,就得做寄存器堆,优先级比较用循环找最大值,面积会大一点,但灵活很多。个人建议第一阶段先固定优先级,跑通嵌套再说。

  • 代码焊工

    这个问题让我想起当年在 Xilinx 上折腾 MicroBlaze 的类似经历。其实 RISC-V 软核的中断架构,你可以参考 Berkeley 的 Rocket 或 SiFive 的 E31,它们的 PLIC 和 CLINT 是分开的:CLINT 管 timer 和 software interrupt,PLIC 管外部中断。你的场景大概率只需要一个简化版 PLIC:每个中断源一个 pending 位,一个 enable 位,一个 priority 寄存器(比如 3 bit),仲裁逻辑就是找 pending 且 enable 且 priority 最大的那个,grant 后硬件把 mepc 改成对应中断向量地址,同时清掉 pending(或者软件清)。嵌套的关键在于:低优先级中断运行时,mstatus.MIE 是开的,所以高优先级来了可以再次触发仲裁,但你的硬件状态机必须保证「当前正在保存现场」的状态不会被新 grant 打断——解决办法是仲裁模块持续输出 grant,而执行模块只在「空闲」状态接受新 grant,进入「保存现场」后锁存当前 grant,完成保存跳转后才回到空闲。这样高优先级来了,grant 会更新,但执行模块还在忙,等低优先级现场保存完跳转后,执行模块空闲,再接受高优先级的 grant,跳过去。丢中断的另一个常见原因:你跳转后没有清 pending,导致同一个中断反复触发,看起来像丢,其实是重复进。你现在的 pending 是硬件自动清还是软件清?

登录后可在本页底部提交回答

提问者

FPGA小学生查看主页
浏览「其他」

描述场景与已尝试方案,更容易获得有效解答

相关问题

同分类

提问建议

  • 标题写清核心疑问,避免「求助」「请问」等空泛用语
  • 正文补充环境、版本、报错信息或截图
  • 先搜索本站是否已有相近问题,减少重复提问
  • 若与课程相关,请标明课时或章节便于讲师定位