设计 RISC-V PLIC 中断嵌套:threshold 一个寄存器当“优先级闸门“

设计一套 RISC-V 中断嵌套,核心就一件事:把 PLIC 的 threshold 当"优先级闸门"。claim 之后设成当前中断的优先级,硬件自动只放行更高优先级的中断,抢占式嵌套就落地了。全文按"目标 → 约束 → 选型 → 决策 → 落地 → 权衡"走一遍。


先定目标:高优先级必须能插队

场景很朴素:一个低优先级中断 L(priority=5)正在处理,突然来了个高优先级中断 H(priority=10)。

如果 H 只能等 L 跑完,那从触发到响应可能差出几十微秒------对硬实时系统,这几十微秒可能就是过载和丢数据的区别。

所以设计目标一句话:让 H 能打断 L,抢先执行,H 跑完再还给 L。 也就是"中断嵌套"。


硬件先给你三条紧箍咒

设计之前,先看清 RISC-V 的硬件底牌,三条约束决定了方案边界:

  1. 进 trap 就关总闸 :CPU 一进中断,硬件自动把 mstatus.MIE 清 0。这意味着默认"不嵌套",想嵌套得手动重开。
  2. 核只看到 meip 一位 :PLIC 是核外设备,几十个外设源汇成一根 meip 线。具体是哪个源、优先级多高,软件得去 PLIC 里 claim 才知道。
  3. 硬件不自动保存通用寄存器:对比 Cortex-M 的 NVIC 自动压栈,RISC-V 进出中断的寄存器保存全靠软件手写。

这三条叠加,决定了"嵌套"不是改个配置就行,而是要自己设计一套进出逻辑。


三个方案摆出来比

嵌套这事,至少有三种做法,先摆开看:

方案 思路 嵌套能力 复杂度 谁在用
A. threshold 硬件仲裁 claim 后设 threshold,让 PLIC 挡掉低优先级 任意深度 中 裸机硬实时
B. 软件优先级 ISR 里手动读优先级、手动判能不能抢占 能,但费劲 高 少见
C. 快进快出 干脆不嵌套,中断只投递事件,重活丢给任务 无 低 FreeRTOS 等 RTOS

方案 B 的坑在于:软件得自己在 ISR 里维护优先级比较逻辑,还要处理"谁在跑、谁在等",稍不留神就写出一堆竞态。

方案 C 其实不是"嵌套",而是绕开嵌套------用任务优先级去实现抢占,中断本身保持一进一出。

真正要"中断嵌套"这个能力,方案 A 是最干净的。下面细说为什么。


为什么是 threshold

方案 A 的精髓就一句:把"当前正在处理的中断优先级"固化到硬件里,让 PLIC 仲裁器替你判断"谁能插队"。

具体机制拆开看。PLIC 有五个关键寄存器:

寄存器 作用
priority(每源一个) 源优先级,0=禁用,越大越高
pending(每源一位) 等待状态,claim 时硬件清掉
enable(每 context 位图) 按 hart×特权级分组使能
threshold(每 context 一个) 优先级阈值,屏蔽 priority ≤ threshold 的源
claim/complete(每 context 一对) 读 claim 取中断号,写 complete 释放

关键在 threshold 的比较语义------严格大于 :源 priority > threshold 才会被仲裁选中。

那方案就出来了:进 ISR 后,把 threshold 设成当前中断的优先级。于是优先级 ≤ 当前的中断全被硬件挡住,只有更高的源能再次触发 meip。你根本不用在软件里写"优先级谁高谁低",PLIC 仲裁器全包了。

选 threshold 的三条理由:

  1. 硬件自动仲裁,软件零判断;
  2. 抢占是确定性的,由 PLIC 仲裁器保证,不靠手写逻辑;
  3. 改动最小,只动一个寄存器。

threshold 方案怎么落地

三层开关

一个外部中断要进来,得三关同时开。嵌套就是在这三层上做文章:

层 寄存器 角色
第 1 层 mie.MEIE 外部中断局部使能,一次配好不动
第 2 层 mstatus.MIE 全局总闸,进 trap 被清 0,嵌套要重开
第 3 层 PLIC threshold 优先级门控,嵌套的核心

闸门怎么升降

用 L(pri=5) 被 H(pri=10) 抢占的例子,看 threshold 怎么一升一降:

复制代码
L 触发,进 ISR:
  claim L        → 拿到 ID_L,清 pending[L]
  threshold ← 5  ★ 屏蔽 ≤5,只剩 >5 能进
  MIE ← 1        ★ 重开总闸

H(pri=10)够格,抢占 L:
  claim H        → 拿到 ID_H
  threshold ← 10 ★ 屏蔽 ≤10
  MIE ← 1

H 处理完,退回 L:
  complete H     → 释放 H
  MIE ← 0
  threshold ← 5  ★ 降回 L 那层
  mret → 回到 L 的 ISR

L 处理完,退回任务:
  complete L
  MIE ← 0
  threshold ← 0  ★ 降回初始
  mret → 回到任务

一句话:threshold 是可升降的闸门,进一层 ISR 抬高到该层优先级,退一层降回去。 硬件只在"更高优先级"到来时开门。

完整时序(25 步)

摊平看,每步标了寄存器状态(mie.MEIE 全程 =1):

复制代码
时间向下 ─────────────────────────────────────────────

1  [任务运行]  (threshold=0, MIE=1, pending=∅)
   │  L 触发,priority=5
   ▼
2  [PLIC 仲裁]  5 > 0 通过 → meip 置位,pending={L}
   │
3  [硬件进 trap]  mepc←任务PC, MIE←0
   │
4  [L_ISR]  软件压栈保存上下文
   │
5  [claim L]  读 claim → ID_L,清 pending[L]
   │
6  [设阈值]  threshold ← 5     ★ 屏蔽 ≤5
   │
7  [开总闸]  MIE ← 1           ★ 允许更高优先级嵌套
   │
8  [处理 L...]  ← 只有 priority>5 能再进来
   │  H 触发,priority=10
   ▼
9  [PLIC 仲裁]  10 > 5 通过 → meip 再置位,pending={H}
   │
10 [硬件进 trap(嵌套)]  mepc←L处理点, MIE←0
   │
11 [H_ISR]  软件递归压栈(保存 L 的现场)
   │
12 [claim H]  读 claim → ID_H,清 pending[H]
   │
13 [设阈值]  threshold ← 10    ★ 屏蔽 ≤10
   │
14 [开总闸]  MIE ← 1
   │
15 [处理 H...]
   │
16 [complete H]  写 complete → H 可再次触发
   │
17 [关总闸]  MIE ← 0
   │
18 [恢复阈值]  threshold ← 5   ★ 回到 L 那层
   │
19 [mret]  PC←L处理点, MIE←MPIE(=1)
   │
20 [继续处理 L...]
   │
21 [complete L]  写 complete → L 可再次触发
   │
22 [关总闸]  MIE ← 0
   │
23 [恢复阈值]  threshold ← 0   ★ 回到初始
   │
24 [mret]  PC←任务PC, MIE←MPIE(=1)
   │
25 [任务继续运行]  (threshold=0, MIE=1, pending=∅)

三个寄存器关键时刻的状态,一表看穿:

时刻 threshold MIE pending
任务 0 1 ∅
L claim 前 0 0 {L}
L 处理中 5 1 ∅
H claim 前 5 0 {H}
H 处理中 10 1 ∅
H 返回 L 5 1 ∅
L 返回任务 0 1 ∅

代码骨架

核心一段,顺序是硬约束:

c 复制代码
void machine_external_irq_handler(void)
{
    uint32_t id = PLIC_claim();               // 1. 拿 ID,同时清 pending
    if (id == 0) return;                      // 2. 伪中断,直接返回

    uint32_t pri = PLIC_priority(id);         // 3. 读这个源的优先级
    uint32_t saved = PLIC_threshold();        // 4. 记下旧 threshold
    PLIC_set_threshold(pri);                  // 5. 抬闸门到当前优先级

    save_mepc_mstatus();                      // 6. 软保存,因为要重入
    enable_global_irq();                      // 7. 重开 MIE,允许嵌套

    handle_irq(id);                           // 8. 干活,期间高优先级可抢占

    disable_global_irq();                     // 9. 关门
    PLIC_complete(id);                        // 10. 释放这个源
    PLIC_set_threshold(saved);                // 11. 降回旧阈值

    restore_mepc_mstatus();                   // 12. 软恢复
    mret();                                   // 13. 返回
}

三条顺序写错必翻车:先 claim 才能设 threshold (得先知道优先级);complete 放处理完 (否则同源立刻再触发);threshold 压栈保存(嵌套层次变了得能退回来)。


什么时候别用这套

threshold 真嵌套看着美,但 RTOS 里很少真这么干。因为嵌套会破坏可预测性------栈深度、临界区、上下文切换全都变复杂。

FreeRTOS 的 RISC-V 移植默认走"快进快出":中断里只 claim + 投递事件,重活交给任务,用任务优先级实现抢占,而不是中断嵌套。

所以边界很清楚:裸机 + 多优先级外设 + 硬实时,上 threshold 真嵌套;跑 RTOS,老实快进快出。


几个翻车点

  1. 伪中断:多 hart 组播时只有一个 claim 成功,其余读到 0。claim 返回 0 必须直接 return。
  2. claim/complete 要成对:claim 了不 complete,这个源永远死掉。
  3. 电平 vs 边沿:电平源得在设备侧撤除,否则 claim 完立刻又 pending;边沿源由 gateway 锁存,不丢。
  4. threshold 是严格大于:设 threshold=5,priority=5 的源会被挡,不是 ≥。
  5. complete 时机:处理完再 complete,否则同源递归。

最后

  • RISC-V 默认不嵌套,因为硬件进 trap 就清 mstatus.MIE。
  • 要嵌套,摆平三层开关,其中 PLIC threshold 是核心。
  • threshold 用法一句话:claim 后设成当前优先级,硬件就只放行更高优先级的中断。
  • 真嵌套适合裸机硬实时;RTOS 用快进快出,拿任务优先级代替中断嵌套。
  • 最容易翻车的是伪中断(claim=0)和 claim/complete 不成对。

相关推荐
ChipCamp2 小时前
Spike-pk在RISC-V RTL开发验证中的应用:黄金参考基准
risc-v·spike·difftest·co-simulation·spike-pk·riscv-pk
Hi2024021711 小时前
Vortex CUDA 生态适配:让 CUDA C、CUTLASS 与 Triton 在 RISC-V GPGPU 上运行
人工智能·risc-v·gpgpu
国科安芯21 小时前
用 AS32X601 做一块关节电机驱动板:三相桥、电流采样、封波保护、FOC 代码一次讲透
risc-v·foc·bldc·关节电机·机器人关节·as32x601·三相逆变桥
硅基手札1 天前
【risc-v专栏 07】启动与 bare-metal 流程
risc-v
硅基手札2 天前
【risc-v专栏 08】扩展生态与 -march/-mabi 匹配
risc-v
硅基手札2 天前
【risc-v专栏 06b】内存架构深挖:PMA / ePMP / Cache / CMO / MMU 细节 / RVWMO 形式化
架构·risc-v
硅基手札2 天前
【risc-v专栏03】CSR 速查与特权架构 M/S/U
单片机·架构·risc-v
IvorySQL2 天前
去 IOE 的最后一公里:IvorySQL 5.4 × RISC-V 实测
数据库·人工智能·ai·postgresql·risc-v
RISCV_Explorer2 天前
技术指南:从总线协议到软件可见行为——RISC-V 多核缓存一致性的 Linux 实践
linux·缓存·risc-v
RISCV_Explorer9 天前
RISC-V内存属性配置深度专题——Svpbmt页表位、PMA优先级与设备访问策略
risc-v