4. PLIC核心寄存器详解

好,咱们今天来聊聊PLIC最核心的四个寄存器。说实话,搞RISC-V中断设计,这几个寄存器你绕不开。我刚开始接触PLIC时,就是被这些寄存器搞得晕头转向,后来踩了几个坑才真正搞明白。

PLIC的核心寄存器,说白了就四类:优先级寄存器、中断挂起寄存器、中断使能寄存器、阈值寄存器。它们共同构成了整个中断系统的骨架。你想想看,没有优先级,中断来了谁先谁后?没有挂起位,你都不知道哪个中断在等着处理。没有使能位,中断来了你不想理它怎么办?没有阈值,低优先级的中断就别来烦CPU了。

4.1 优先级寄存器(Priority Registers)

优先级寄存器,每个中断源都有一个。在标准的SiFive PLIC实现中,每个中断源占用一个32位寄存器,但实际只用了低3位。也就是说,优先级范围是0到7。

关键点:优先级0表示该中断永远不会被触发。优先级7最高,1最低。这个设计其实挺巧妙的——你想禁用某个中断源,直接把优先级设成0就行,不用去动使能寄存器。

我在项目中遇到过一个问题:有同事把两个外设的中断优先级都设成了7,结果两个中断同时到来时,PLIC不知道该先处理哪个。嗯,这里要注意,同等优先级的中断,PLIC会按照中断源编号的固定顺序来处理,编号小的优先。

寄存器布局很简单:

优先级寄存器(每个中断源一个)
地址偏移:0x000000 + 4 * interrupt_id
位域:
  [31:4]  - 保留,读返回0
  [3:0]   - 优先级值(0-7)

我的建议:设计时最好把优先级做成可配置的,别写死。我曾经在一个项目中把所有中断优先级都写死在RTL里,结果后期调试时想调整优先级,只能改代码重新综合,那叫一个痛苦。

4.2 中断挂起寄存器(Interrupt Pending Registers)

中断挂起寄存器,说白了就是记录「哪个中断正在等着被处理」。每个中断源对应一个bit,1表示有中断在等待,0表示没有。

这个寄存器是只读的,你写它也没用。中断源来了,硬件自动把对应位置1。你读完中断号(Claim),硬件自动清零。

挂起寄存器是分组的,每32个中断源一组。比如你有53个中断源,那就需要两个挂起寄存器:

挂起寄存器0(中断源0-31)
地址偏移:0x001000
位域:
  [31:0] - 每个bit对应一个中断源的挂起状态

挂起寄存器1(中断源32-52)
地址偏移:0x001004
位域:
  [20:0] - 中断源32-52的挂起状态
  [31:21] - 保留

我个人习惯在调试时经常读这个寄存器。它能告诉你当前系统里有多少个中断在排队,哪个中断被触发了但还没处理。有一次我发现一个外设的中断挂起位一直为1,查了半天才发现是中断服务程序里忘了做Claim操作。

注意:挂起寄存器反映的是中断源的电平或边沿状态。如果是电平触发的中断,即使你读走了中断号,只要外部电平没变,挂起位还是会立即重新置1。这个坑我踩过,当时以为中断处理完了,结果一退出又进来了,死循环。

4.3 中断使能寄存器(Interrupt Enable Registers)

中断使能寄存器,控制每个中断源是否被允许传递到目标Hart。每个中断源对应一个bit,1表示使能,0表示禁用。

这里有个有意思的设计:使能寄存器是per-hart的。什么意思?就是每个CPU核心都有自己的使能寄存器组。你可以在Hart 0上使能UART中断,在Hart 1上禁用,互不影响。

使能寄存器的地址布局和挂起寄存器类似,也是32个一组:

Hart 0的使能寄存器0(中断源0-31)
地址偏移:0x002000

Hart 0的使能寄存器1(中断源32-63)
地址偏移:0x002004

Hart 1的使能寄存器0(中断源0-31)
地址偏移:0x002080

Hart 1的使能寄存器1(中断源32-63)
地址偏移:0x002084

你发现规律了吗?每个Hart的使能寄存器组之间偏移0x80字节,也就是32个寄存器。这个设计很规整,我喜欢。

避坑指南:我曾经在初始化时忘了设置使能寄存器,结果外设中断来了,PLIC就是不往CPU送。查了两天才发现是使能位没写1。所以,初始化流程一定要记得:配优先级→设阈值→开使能,三步走。

4.4 阈值寄存器(Threshold Registers)

阈值寄存器,这是PLIC里一个很巧妙的设计。它也是个per-hart的寄存器,每个Hart都有自己的阈值。

阈值的作用是什么?说白了就是设置一个「门槛」。只有优先级高于这个门槛的中断,才会被转发到对应的Hart。优先级小于等于阈值的中断,会被PLIC直接忽略。

举个例子:你把阈值设成4,那么优先级1-4的中断都不会被转发,只有优先级5-7的中断才能到达CPU。

阈值寄存器(每个Hart一个)
Hart 0的阈值寄存器
地址偏移:0x002000 + 0x200000

Hart 1的阈值寄存器
地址偏移:0x002080 + 0x200000

位域:
  [31:4] - 保留
  [3:0]  - 阈值(0-7)

阈值设为0,表示所有使能的中断都能通过。阈值设为7,表示所有中断都被屏蔽——因为最高优先级就是7,没有比7更高的了。

实用技巧:我在做低功耗模式时,经常用阈值寄存器来快速屏蔽所有中断。把阈值设成7,比一个一个去清使能位快多了。退出低功耗时,再把阈值恢复成0,所有中断又回来了。这个操作是原子性的,不会丢中断。

4.5 四个寄存器的协同工作

这四个寄存器不是孤立的,它们共同决定了中断的整个处理流程。我画个简单的流程给你看:

  1. 中断源触发 → 挂起寄存器对应位置1
  2. PLIC检查使能 → 如果使能位为0,忽略该中断
  3. PLIC检查优先级 → 如果优先级为0,忽略该中断
  4. PLIC比较阈值 → 如果优先级 ≤ 阈值,不转发
  5. PLIC仲裁 → 在所有通过检查的中断中,选优先级最高的
  6. 转发到目标Hart → 设置外部中断信号

你看,每一步都对应一个寄存器。缺了哪个都不行。

寄存器 访问类型 per-hart? 主要作用
优先级寄存器 读写 否(全局) 设置每个中断源的优先级
挂起寄存器 只读 否(全局) 查看哪些中断在等待
使能寄存器 读写 控制每个中断源是否允许转发
阈值寄存器 读写 设置中断转发的优先级门槛

嗯,说到这儿,我觉得有必要提醒你一点:这些寄存器的复位值。大部分PLIC实现中,优先级寄存器和阈值寄存器的复位值都是0。这意味着什么?所有中断源默认优先级为0(被禁用),阈值也为0(所有中断都能通过)。所以上电后,你得先把需要的中断源优先级设成非0值,否则中断根本不会来。

好了,这四个核心寄存器就讲到这里。下一节我们会聊聊中断的Claim和Complete流程,那才是真正处理中断的关键步骤。