设计 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 不成对。

相关推荐
右耳朵猫AI2 天前
Python周刊2026W36 | Python 3.15候选版、RISC-V获CPython支持、PEP 843、asyncio修复
python·sqlite·risc-v
hongmai6668886 天前
ESP32-WROVER-IE-N4R8:外接天线+8MB内存,专治信号焦虑
笔记·单片机·嵌入式硬件·microsoft·risc-v
花 满 楼7 天前
RISC-V APLIC 中断模块总结——第二篇
单片机·嵌入式硬件·mcu·risc-v
论迹复利8 天前
FreeRTOS 在 RISC-V 上是如何“点火“的 —— 从 main() 到第一个任务的完整链路
java·开发语言·risc-v
花 满 楼8 天前
RISC-V APLIC 中断模块总结——第一篇
mcu·risc-v·中断·aplic
国科安芯9 天前
小卫星综合电子系统中功能安全与抗辐射加固的协同设计研究
嵌入式硬件·安全·架构·risc-v·抗辐射·小卫星·综合电子系统
国科安芯14 天前
小卫星综合电子系统中RISC-V抗辐射MCU的功能安全与多接口集成架构分析
单片机·嵌入式硬件·安全·fpga开发·架构·risc-v
论迹复利14 天前
PendSV与RISC-V trap——上下文切换的两种设计哲学
内核·freertos·risc-v
hongmai66688816 天前
竖插专用:ESP8684-WROOM-07-H2X模组上手记录
笔记·单片机·嵌入式硬件·物联网·risc-v