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
- PLIC检查使能 → 如果使能位为0,忽略该中断
- PLIC检查优先级 → 如果优先级为0,忽略该中断
- PLIC比较阈值 → 如果优先级 ≤ 阈值,不转发
- PLIC仲裁 → 在所有通过检查的中断中,选优先级最高的
- 转发到目标Hart → 设置外部中断信号
你看,每一步都对应一个寄存器。缺了哪个都不行。
| 寄存器 | 访问类型 | per-hart? | 主要作用 |
|---|---|---|---|
| 优先级寄存器 | 读写 | 否(全局) | 设置每个中断源的优先级 |
| 挂起寄存器 | 只读 | 否(全局) | 查看哪些中断在等待 |
| 使能寄存器 | 读写 | 是 | 控制每个中断源是否允许转发 |
| 阈值寄存器 | 读写 | 是 | 设置中断转发的优先级门槛 |
嗯,说到这儿,我觉得有必要提醒你一点:这些寄存器的复位值。大部分PLIC实现中,优先级寄存器和阈值寄存器的复位值都是0。这意味着什么?所有中断源默认优先级为0(被禁用),阈值也为0(所有中断都能通过)。所以上电后,你得先把需要的中断源优先级设成非0值,否则中断根本不会来。
好了,这四个核心寄存器就讲到这里。下一节我们会聊聊中断的Claim和Complete流程,那才是真正处理中断的关键步骤。