设计一套 RISC-V 中断嵌套,核心就一件事:把 PLIC 的 threshold 当"优先级闸门"。claim 之后设成当前中断的优先级,硬件自动只放行更高优先级的中断,抢占式嵌套就落地了。全文按"目标 → 约束 → 选型 → 决策 → 落地 → 权衡"走一遍。
先定目标:高优先级必须能插队
场景很朴素:一个低优先级中断 L(priority=5)正在处理,突然来了个高优先级中断 H(priority=10)。
如果 H 只能等 L 跑完,那从触发到响应可能差出几十微秒------对硬实时系统,这几十微秒可能就是过载和丢数据的区别。
所以设计目标一句话:让 H 能打断 L,抢先执行,H 跑完再还给 L。 也就是"中断嵌套"。
硬件先给你三条紧箍咒
设计之前,先看清 RISC-V 的硬件底牌,三条约束决定了方案边界:
- 进 trap 就关总闸 :CPU 一进中断,硬件自动把
mstatus.MIE清 0。这意味着默认"不嵌套",想嵌套得手动重开。 - 核只看到 meip 一位 :PLIC 是核外设备,几十个外设源汇成一根
meip线。具体是哪个源、优先级多高,软件得去 PLIC 里 claim 才知道。 - 硬件不自动保存通用寄存器:对比 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 的三条理由:
- 硬件自动仲裁,软件零判断;
- 抢占是确定性的,由 PLIC 仲裁器保证,不靠手写逻辑;
- 改动最小,只动一个寄存器。
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,老实快进快出。
几个翻车点
- 伪中断:多 hart 组播时只有一个 claim 成功,其余读到 0。claim 返回 0 必须直接 return。
- claim/complete 要成对:claim 了不 complete,这个源永远死掉。
- 电平 vs 边沿:电平源得在设备侧撤除,否则 claim 完立刻又 pending;边沿源由 gateway 锁存,不丢。
- threshold 是严格大于:设 threshold=5,priority=5 的源会被挡,不是 ≥。
- complete 时机:处理完再 complete,否则同源递归。
最后
- RISC-V 默认不嵌套,因为硬件进 trap 就清
mstatus.MIE。 - 要嵌套,摆平三层开关,其中 PLIC threshold 是核心。
- threshold 用法一句话:claim 后设成当前优先级,硬件就只放行更高优先级的中断。
- 真嵌套适合裸机硬实时;RTOS 用快进快出,拿任务优先级代替中断嵌套。
- 最容易翻车的是伪中断(claim=0)和 claim/complete 不成对。