一、RISC-V中断级联的核心目标(扩展中断号)
RISC-V标准PLIC(平台级中断控制器)的中断号数量有限(比如基础版支持128个中断号),当外设中断源超过该数量时,需要通过中断级联(Cascade)扩展:
- 主PLIC:管理高优先级中断 + 下级从PLIC的中断请求(从PLIC的请求作为主PLIC的一个「聚合中断号」);
- 从PLIC:管理细分的低优先级中断源,多个从PLIC可级联到主PLIC;
- 核心逻辑:从PLIC将自身的中断请求聚合为1个中断号上报给主PLIC,主PLIC收到后,CPU通过读取从PLIC的寄存器确定具体的中断源。
二、Verilog实现(中断号扩展型级联)
以下实现1个主PLIC + 2个从PLIC的级联结构,扩展中断号数量:
- 主PLIC:原生支持16个中断号(1~15),其中:
- 1~8:直接管理高优先级外设(如DMA、UART);
- 9:级联「从PLIC1」的聚合中断;
- 10:级联「从PLIC2」的聚合中断;
- 从PLIC1:管理16个低优先级中断(GPIO0~GPIO15),聚合为1个中断号(主PLIC的9号);
- 从PLIC2:管理16个低优先级中断(I2C0~I2C15),聚合为1个中断号(主PLIC的10号);
- 最终总中断数:8 + 16 + 16 = 40个(通过级联从16个扩展到40个)。
verilog
`timescale 1ns/1ps
// -------------------------- 从PLIC模块(扩展中断号) --------------------------
module riscv_plic_slave (
input clk,
input rst_n,
// 外设中断源(16个)
input [15:0] slave_int_sources, // 16个中断源(bit0~bit15对应中断号1~16)
// 主PLIC接口
output cascade_int_req, // 向主PLIC发起的聚合中断请求
output [3:0] cascade_int_id, // 从PLIC内最高优先级中断号
// CPU配置/响应接口
input cpu_claim, // CPU读取中断(claim)
input cpu_complete, // CPU完成中断(complete)
input [3:0] cpu_hart_id // CPU核ID
);
// 中断优先级配置(简化:bit越高优先级越高)
localparam [3:0] PRIO_NONE = 4'd0;
reg [3:0] slave_int_prio; // 从PLIC内最高优先级中断号
reg slave_int_req; // 从PLIC中断请求
// 步骤1:仲裁从PLIC内最高优先级中断号
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
slave_int_prio <= PRIO_NONE;
slave_int_req <= 1'b0;
end else begin
// 从bit15到bit0遍历,找到第一个置位的中断源(高bit优先级高)
slave_int_prio <= PRIO_NONE;
slave_int_req <= 1'b0;
for (int i=15; i>=0; i=i-1) begin
if (slave_int_sources[i]) begin
slave_int_prio <= i + 1; // 中断号1~16对应bit0~bit15
slave_int_req <= 1'b1;
break;
end
end
end
end
// 步骤2:Pending寄存器(防止中断丢失)
reg [15:0] pending; // 每个中断源对应1位
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pending <= 16'h0;
end else begin
// 新中断触发置位pending,CPU处理后清除
pending <= (pending | slave_int_sources);
end
end
// CPU claim后清除对应的pending位
always @(posedge clk) begin
if (cpu_claim && (slave_int_prio != PRIO_NONE)) begin
pending[slave_int_prio - 1] <= 1'b0;
end
end
// 步骤3:仲裁时用pending而不是原始信号
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
slave_int_prio <= PRIO_NONE;
slave_int_req <= 1'b0;
end else begin
slave_int_prio <= PRIO_NONE;
slave_int_req <= 1'b0;
for (int i=15; i>=0; i=i-1) begin
if (pending[i]) begin // 用pending仲裁
slave_int_prio <= i + 1;
slave_int_req <= 1'b1;
break;
end
end
end
end
// 步骤4:CPU响应后清除中断请求
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
slave_int_req <= 1'b0;
end else if (cpu_complete && (slave_int_prio != PRIO_NONE)) begin
slave_int_req <= 1'b0;
end
end
// 输出到主PLIC的聚合中断
assign cascade_int_req = slave_int_req;
assign cascade_int_id = slave_int_prio;
endmodule
// -------------------------- 主PLIC模块(级联从PLIC) --------------------------
module riscv_plic_master (
input clk,
input rst_n,
// 直接外设中断源(8个:中断号1~8)
input [7:0] master_int_sources,
// 从PLIC1级联接口
input slave1_cascade_req,
input [3:0] slave1_cascade_id,
// 从PLIC2级联接口
input slave2_cascade_req,
input [3:0] slave2_cascade_id,
// CPU接口(RISC-V PLIC标准)
output cpu_interrupt_req, // 向CPU发起中断
input cpu_claim, // CPU读取中断ID
output [5:0] cpu_claim_id, // 给CPU的中断ID(扩展到40个)
input cpu_complete, // CPU完成中断
input [3:0] cpu_hart_id // CPU核ID
);
// 中断号定义(核心:级联扩展)
localparam [5:0] INT_DMA = 6'd1; // 主PLIC直接中断1
localparam [5:0] INT_UART = 6'd2; // 主PLIC直接中断2
localparam [5:0] INT_SLAVE1 = 6'd9; // 级联从PLIC1的聚合中断号
localparam [5:0] INT_SLAVE2 = 6'd10; // 级联从PLIC2的聚合中断号
localparam [5:0] INT_NONE = 6'd0;
reg [5:0] current_int_id; // 当前最高优先级中断ID
reg master_int_req; // 主PLIC向CPU的中断请求
// 步骤1:仲裁所有中断(直接中断 > 级联中断)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_int_id <= INT_NONE;
master_int_req <= 1'b0;
end else begin
current_int_id <= INT_NONE;
master_int_req <= 1'b0;
// 第一步:仲裁主PLIC直接中断(1~8,bit7~bit0对应中断号8~1)
for (int i=7; i>=0; i=i-1) begin
if (master_int_sources[i]) begin
current_int_id <= i + 1;
master_int_req <= 1'b1;
break;
end
end
// 第二步:若无直接中断,仲裁级联中断(从PLIC1 > 从PLIC2)
if (current_int_id == INT_NONE) begin
if (slave1_cascade_req) begin
// 级联中断ID:主PLIC中断号(9) + 从PLIC内偏移(1~16 → 9~24)
current_int_id <= INT_SLAVE1 + slave1_cascade_id - 1;
master_int_req <= 1'b1;
end else if (slave2_cascade_req) begin
// 级联中断ID:主PLIC中断号(10) + 从PLIC内偏移(1~16 → 25~40)
current_int_id <= INT_SLAVE2 + slave2_cascade_id - 1;
master_int_req <= 1'b1;
end
end
end
end
// 步骤2:CPU Claim/Complete接口(RISC-V标准)
assign cpu_interrupt_req = master_int_req;
assign cpu_claim_id = (cpu_claim) ? current_int_id : INT_NONE;
// 调试:打印中断号扩展信息
always @(posedge clk) begin
if (rst_n && master_int_req && cpu_claim) begin
if (current_int_id <= 8) begin
$display("[%0t] 主PLIC直接中断:ID=%0d", $time, current_int_id);
end else if (current_int_id <=24) begin
$display("[%0t] 级联从PLIC1中断:总ID=%0d (从PLIC1内ID=%0d)",
$time, current_int_id, current_int_id - INT_SLAVE1 + 1);
end else begin
$display("[%0t] 级联从PLIC2中断:总ID=%0d (从PLIC2内ID=%0d)",
$time, current_int_id, current_int_id - INT_SLAVE2 + 1);
end
end
end
endmodule
// -------------------------- 顶层模块(主+从PLIC级联) --------------------------
module riscv_interrupt_cascade_extend (
input clk,
input rst_n,
// 直接外设中断(8个:DMA/UART等)
input [7:0] master_ints,
// 从PLIC1中断源(16个:GPIO0~GPIO15)
input [15:0] slave1_ints,
// 从PLIC2中断源(16个:I2C0~I2C15)
input [15:0] slave2_ints,
// CPU接口
output cpu_int_req,
input cpu_claim,
output [5:0] cpu_claim_id,
input cpu_complete,
input [3:0] cpu_hart_id
);
// 从PLIC1实例化
wire slave1_cascade_req;
wire [3:0] slave1_cascade_id;
riscv_plic_slave u_slave1 (
.clk(clk),
.rst_n(rst_n),
.slave_int_sources(slave1_ints),
.cascade_int_req(slave1_cascade_req),
.cascade_int_id(slave1_cascade_id),
.cpu_claim(cpu_claim && (cpu_claim_id >=9 && cpu_claim_id <=24)),
.cpu_complete(cpu_complete && (cpu_claim_id >=9 && cpu_claim_id <=24)),
.cpu_hart_id(cpu_hart_id)
);
// 从PLIC2实例化
wire slave2_cascade_req;
wire [3:0] slave2_cascade_id;
riscv_plic_slave u_slave2 (
.clk(clk),
.rst_n(rst_n),
.slave_int_sources(slave2_ints),
.cascade_int_req(slave2_cascade_req),
.cascade_int_id(slave2_cascade_id),
.cpu_claim(cpu_claim && (cpu_claim_id >=25 && cpu_claim_id <=40)),
.cpu_complete(cpu_complete && (cpu_claim_id >=25 && cpu_claim_id <=40)),
.cpu_hart_id(cpu_hart_id)
);
// 主PLIC实例化
riscv_plic_master u_master (
.clk(clk),
.rst_n(rst_n),
.master_int_sources(master_ints),
.slave1_cascade_req(slave1_cascade_req),
.slave1_cascade_id(slave1_cascade_id),
.slave2_cascade_req(slave2_cascade_req),
.slave2_cascade_id(slave2_cascade_id),
.cpu_interrupt_req(cpu_int_req),
.cpu_claim(cpu_claim),
.cpu_claim_id(cpu_claim_id),
.cpu_complete(cpu_complete),
.cpu_hart_id(cpu_hart_id)
);
endmodule
// -------------------------- 测试用例 --------------------------
module tb_riscv_interrupt_cascade_extend;
reg clk;
reg rst_n;
wire cpu_int_req;
reg cpu_claim;
wire [5:0] cpu_claim_id;
reg cpu_complete;
reg [3:0] cpu_hart_id;
reg [7:0] master_ints;
reg [15:0] slave1_ints;
reg [15:0] slave2_ints;
// 实例化顶层模块
riscv_interrupt_cascade_extend u_top (
.clk(clk),
.rst_n(rst_n),
.master_ints(master_ints),
.slave1_ints(slave1_ints),
.slave2_ints(slave2_ints),
.cpu_int_req(cpu_int_req),
.cpu_claim(cpu_claim),
.cpu_claim_id(cpu_claim_id),
.cpu_complete(cpu_complete),
.cpu_hart_id(cpu_hart_id)
);
// 时钟生成
initial begin
clk = 1'b0;
forever #5 clk = ~clk;
end
// 测试流程
initial begin
// 初始化
rst_n = 1'b0;
cpu_claim = 1'b0;
cpu_complete = 1'b0;
cpu_hart_id = 4'd0;
master_ints = 8'b0000_0000;
slave1_ints = 16'h0000;
slave2_ints = 16'h0000;
#100;
rst_n = 1'b1;
#100;
// 步骤1:测试主PLIC直接中断(ID=2:UART)
$display("\n=== 步骤1:主PLIC直接中断(ID=2) ===");
master_ints = 8'b0000_0010; // ID=2置位
#20;
cpu_claim = 1'b1; // CPU读取中断ID
#10;
cpu_claim = 1'b0;
#20;
cpu_complete = 1'b1; // CPU完成中断
#10;
cpu_complete = 1'b0;
master_ints = 8'b0000_0000;
#100;
// 步骤2:测试级联从PLIC1中断(GPIO7 → 总ID=9+7=16)
$display("\n=== 步骤2:级联从PLIC1中断(GPIO7 → 总ID=16) ===");
slave1_ints = 16'h0080; // GPIO7(从PLIC1内ID=8)
#20;
cpu_claim = 1'b1;
#10;
cpu_claim = 1'b0;
#20;
cpu_complete = 1'b1;
#10;
cpu_complete = 1'b0;
slave1_ints = 16'h0000;
#100;
// 步骤3:测试级联从PLIC2中断(I2C15 → 总ID=10+15=25)
$display("\n=== 步骤3:级联从PLIC2中断(I2C15 → 总ID=25) ===");
slave2_ints = 16'h8000; // I2C15(从PLIC2内ID=16)
#20;
cpu_claim = 1'b1;
#10;
cpu_claim = 1'b0;
#20;
cpu_complete = 1'b1;
#10;
cpu_complete = 1'b0;
slave2_ints = 16'h0000;
#200;
$display("\n=== 测试结束 ===");
$finish;
end
endmodule
三、核心逻辑解释(重点:中断号扩展)
1. 中断号映射规则(级联扩展的核心)
| 中断类型 | 从PLIC内ID | 主PLIC聚合ID | 最终总中断ID | 对应外设 |
|---|---|---|---|---|
| 主PLIC直接中断 | 1~8 | - | 1~8 | DMA、UART等 |
| 从PLIC1级联 | 1~16 | 9 | 9~24 | GPIO0~GPIO15 |
| 从PLIC2级联 | 1~16 | 10 | 25~40 | I2C0~I2C15 |
- 核心:从PLIC的每个中断源,通过「主PLIC聚合ID + 从PLIC内偏移」映射为全局唯一的中断号,实现从16个到40个的扩展。
2. 级联关键逻辑
- 从PLIC:将自身16个中断源仲裁出最高优先级的中断号,然后向主PLIC发起1个「聚合中断请求」(仅1个信号);
- 主PLIC:把从PLIC的聚合中断请求当作普通中断源(占用1个主中断号),CPU读取主PLIC的中断ID后,通过ID范围判断属于哪个从PLIC,再读取对应从PLIC的寄存器得到具体中断源;
- CPU交互 :通过RISC-V标准的
claim/complete接口,CPU先读取主PLIC的全局中断ID,再根据ID归属,读取从PLIC的细分中断信息。
3. 仿真输出示例(直观看到中断号扩展)
=== 步骤1:主PLIC直接中断(ID=2) ===
[220] 主PLIC直接中断:ID=2
=== 步骤2:级联从PLIC1中断(GPIO7 → 总ID=16) ===
[420] 级联从PLIC1中断:总ID=16 (从PLIC1内ID=8)
=== 步骤3:级联从PLIC2中断(I2C15 → 总ID=25) ===
[620] 级联从PLIC2中断:总ID=35 (从PLIC2内ID=16)
=== 测试结束 ===
四、RISC-V标准对齐说明
-
PLIC级联规范 :RISC-V PLIC支持将多个从PLIC的
interrupt_req信号作为主PLIC的中断源,主PLIC仅需为每个从PLIC分配1个中断号,即可扩展成百上千个中断源; -
软件层适配 :CPU中断服务程序(ISR)逻辑:
cvoid plic_isr() { // 1. 读取主PLIC的全局中断ID uint32_t global_id = plic_claim(); // 2. 判断中断归属 if (global_id <=8) { // 处理主PLIC直接中断(如DMA/UART) } else if (global_id <=24) { // 计算从PLIC1内的本地ID uint32_t local_id = global_id - 9 + 1; // 处理从PLIC1的GPIO中断 } else if (global_id <=40) { // 计算从PLIC2内的本地ID uint32_t local_id = global_id -10 +1; // 处理从PLIC2的I2C中断 } // 3. 完成中断 plic_complete(global_id); }
五、PLIC寄存器接口(标准RISC-V定义)
RISC-V PLIC通过MMIO(内存映射IO)提供以下关键寄存器:
| 寄存器偏移 | 名称 | 说明 |
|---|---|---|
| 0x000000 | priority_base + interrupt_id * 4 | 设置每个中断源的优先级(0~7,0表示禁用) |
| 0x002000 | pending_base + (interrupt_id / 32) * 4 | 中断挂起寄存器 |
| 0x002080 | enable_base + hart_id * 0x100 + (interrupt_id / 32) * 4 | 中断使能寄存器 |
| 0x003FFFF | claim_complete | CPU Claim/Complete寄存器(读写同一地址) |
1. Priority(优先级)寄存器
- 每个中断号对应一个优先级寄存器
- 优先级范围:0~7(可配置),0表示该中断被禁用
- 优先级数值越大,优先级越高
2. Enable(使能)寄存器
- 每个Hart(CPU核)有独立的使能寄存器
- 需显式使能每个中断源,否则不会触发CPU中断
3. Claim/Complete 机制
c
// CPU侧标准操作流程
uint32_t plic_claim(void) {
return *(volatile uint32_t *)PLIC_CLAIM_COMPLETE_ADDR;
}
void plic_complete(uint32_t interrupt_id) {
*(volatile uint32_t *)PLIC_CLAIM_COMPLETE_ADDR = interrupt_id;
}
4. CPU_Claim / CPU_Complete 信号详解(Verilog版)
这两个信号是 CPU与PLIC交互的标准接口,用于中断的"认领"和"完成"。
4.1 信号定义
| 信号 | 方向 | 说明 |
|---|---|---|
cpu_claim |
CPU → PLIC | CPU读取中断ID(认领中断) |
cpu_claim_id |
PLIC → CPU | PLIC返回的中断号 |
cpu_complete |
CPU → PLIC | CPU处理完成(清除中断) |
cpu_interrupt_req |
PLIC → CPU | 有中断待处理(中断通知) |
4.2 交互时序
CPU PLIC CPU PLIC 1. 外设触发中断 2. CPU读取中断ID 3. CPU执行ISR 4. CPU完成中断 interrupt_req (拉高) cpu_claim = 1 claim_id = 5 (返回中断号) cpu_claim = 0 [执行中断处理程序...] cpu_complete = 1 interrupt_req (拉低) cpu_complete = 0
4.3 Verilog 代码示例
PLIC侧(中断控制器):
verilog
module plic_example (
input clk,
input rst_n,
input [7:0] int_sources, // 中断源
input cpu_claim, // CPU来读取中断
output [7:0] claim_id, // 返回给CPU的中断号
input cpu_complete, // CPU来完成中断
output interrupt_req // 中断请求信号
);
reg [7:0] current_id; // 当前中断ID
reg int_req; // 中断请求寄存器
// 仲裁:找出最高优先级的中断
always @(*) begin
current_id = 0;
int_req = 1'b0;
for (int i = 7; i >= 0; i--) begin
if (int_sources[i]) begin
current_id = i + 1; // 中断号从1开始
int_req = 1'b1;
break;
end
end
end
// 输出中断请求
assign interrupt_req = int_req;
// CPU claim:返回中断ID(读时有效)
assign claim_id = (cpu_claim) ? current_id : 0;
// CPU complete:清除中断
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
int_req <= 1'b0;
end else if (cpu_complete) begin
int_req <= 1'b0; // 清除中断请求
end
end
endmodule
4.4 关键理解点
| 概念 | 说明 |
|---|---|
| Claim(认领) | 告诉PLIC"我要处理这个中断了",读取中断ID,同时"锁定"这个中断 |
| Complete(完成) | 告诉PLIC"我已经处理完了",清除中断请求,允许新的中断触发 |
| 必须配对 | 每个中断必须先claim后complete,缺一不可 |
| 只读claim_id | claim_id只在cpu_claim=1时有效,其他时间为0 |
4.5 常见错误
| 错误 | 后果 |
|---|---|
| 只claim不complete | 中断无法再次触发(pending位无法清除) |
| 多次claim | 行为未定义,可能丢失中断 |
| claim后未处理就complete | 中断被提前清除,可能丢失 |
4.6 类比理解
| 操作 | 生活中类似 | 作用 |
|---|---|---|
interrupt_req |
电话铃响 | "有中断来了!" |
cpu_claim |
接电话 | "这个中断我来处理" |
cpu_complete |
挂电话 | "处理完了,可以再打来" |
六、中断处理完整流程
1. 流程图(使用Mermaid)
主PLIC直接中断
从PLIC级联中断
外设触发中断
PLIC仲裁
CPU响应
读取Claim寄存器
判断中断归属
执行主PLIC中断ISR
读取从PLIC获取细分中断号
执行从PLIC中断ISR
写入Complete清除中断
2. 时序图(使用Mermaid)
CPU 主PLIC 从PLIC 外设(GPIO/UART) CPU 主PLIC 从PLIC 外设(GPIO/UART) 步骤1: 外设触发中断 步骤2: CPU响应中断 步骤3: 判断中断归属 alt [中断ID ∈ [9, 24] (从PLIC1)] [中断ID ∈ [25, 40] (从PLIC2)] 步骤4: 执行ISR 步骤5: 清除中断 中断请求(assert) 聚合中断请求(cascade_int_req) cpu_interrupt_req cpu_claim(读取中断ID) cpu_claim_id=16 (从PLIC1: GPIO7) 读取细分中断号 返回本地ID=8 (GPIO7) 读取细分中断号 返回本地ID=16 (I2C15) 执行对应外设ISR cpu_complete cpu_complete 清除slave_int_req
七、多Hart(多核)支持
RISC-V PLIC原生支持多核,每个Hart有独立的:
- 使能寄存器(enable)
- 优先级阈值(threshold)
- Claim/Complete 寄存器
verilog
// 多核支持的关键:每个Hart独立的中断接口
module riscv_plic_multihart (
input clk,
input rst_n,
input [7:0] int_sources, // 中断源
// Hart 0 接口
output hart0_interrupt,
input hart0_claim,
output [7:0] hart0_claim_id,
input hart0_complete,
// Hart 1 接口
output hart1_interrupt,
input hart1_claim,
output [7:0] hart1_claim_id,
input hart1_complete
);
// 每个Hart独立仲裁,可以配置不同优先级阈值
endmodule
八、实际配置示例
c
// 1. 配置UART中断(主PLIC直接中断,ID=2)
#define PLIC_PRIORITY_UART 2
#define PLIC_ENABLE_UART (1 << 2)
// 设置优先级
*(volatile uint32_t *)0x10000008 = PLIC_PRIORITY_UART; // interrupt_id=2, offset=2*4
// 使能UART中断(Hart 0)
*(volatile uint32_t *)0x10002008 = PLIC_ENABLE_UART; // enable_base + 0x80 + 0
// 设置阈值(只响应优先级>1的中断)
*(volatile uint32_t *)0x10020000 = 1;
// 2. 配置GPIO中断(从PLIC1级联,假设映射到主PLIC ID=9)
#define PLIC_PRIORITY_GPIO 3
#define PLIC_ENABLE_GPIO (1 << 9)
// 从PLIC1设置GPIO优先级
*(volatile uint32_t *)0x20000004 = PLIC_PRIORITY_GPIO; // GPIO1, offset=1*4
九、硬件设计注意事项
-
时序关键路径
- 从外设中断信号到CPU中断请求的路径需严格约束
- 建议在PLIC入口添加寄存器打拍(pipeline)
-
中断优先级配置原则
- 时延敏感的外设(UART、DMA)配置高优先级
- 批量数据传输(I2C、SPI)配置低优先级
-
级联数量限制
- 主PLIC的剩余中断号决定最大级联数量
- 建议保留至少2个直接中断号给紧急外设
-
面积与性能权衡
- 从PLIC数量越多,面积越大,但中断细分能力越强
- 可根据实际外设数量规划从PLIC规模
十、中断丢失问题详解
1. 问题描述
如果代码中没有pending寄存器,直接用原始中断信号进行仲裁,会导致低优先级中断丢失:
verilog
// ❌ 错误做法:直接用原始信号仲裁
for (int i=15; i>=0; i=i-1) begin
if (slave_int_sources[i]) begin // 原始信号
slave_int_prio <= i + 1;
slave_int_req <= 1'b1;
break;
end
end
问题场景:
- 时刻T0: bit5=1, bit3=1 同时触发
- 仲裁选中bit5(高优先级),CPU开始处理
- 时刻T1: 外设撤销了bit5的中断请求(中断信号变为0)
- 时刻T2: CPU处理完bit5,尝试处理bit3,但此时bit3可能也已过期
- 结果:低优先级中断丢失
2. 解决方案:使用Pending寄存器
关键设计:
- 用
pending寄存器保存已触发的中断 - 仲裁时读取
pending而不是原始信号 - CPU claim后清除对应的pending位
- 外设中断信号只负责置位pending,不直接参与仲裁
verilog
// ✅ 正确做法
reg [15:0] pending;
// 新中断触发时置位pending
always @(posedge clk) begin
pending <= pending | slave_int_sources;
end
// CPU claim后清除pending
always @(posedge clk) begin
if (cpu_claim) begin
pending[slave_int_prio - 1] <= 1'b0;
end
end
// 仲裁用pending
for (int i=15; i>=0; i=i-1) begin
if (pending[i]) begin
// ...
end
end
3. 中断处理完整时序
CPU 仲裁器 Pending寄存器 外设 CPU 仲裁器 Pending寄存器 外设 T0: bit5=1, bit3=1 同时触发 T1: CPU claim T2: CPU响应bit3 中断请求 pending = 0b00101000 读取pending bit5=1(高优先级) 响应bit5中断 claim(读取ID=6) pending = 0b00001000 (bit3保留) 再次读取 bit3=1 响应bit3中断 claim(读取ID=4) pending = 0b00000000
4. 重要结论
| 机制 | 作用 |
|---|---|
| Pending寄存器 | 保存已触发但未处理的中断,防止丢失 |
| Claim机制 | 读取中断ID的同时清除对应pending位 |
| Complete机制 | 清除中断请求信号(slave_int_req) |
| 两者配合 | 确保中断处理完整,不丢失任何中断 |
十一、常见问题与调试
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 中断不触发 | 未使能中断/优先级为0 | 检查enable寄存器配置 |
| 中断频繁触发 | 未调用complete清除 | ISR结束后必须写complete |
| 中断号错误 | 级联ID映射错误 | 核对主PLIC+从PLIC偏移计算 |
| 多核中断混乱 | 未正确配置hart选择器 | 确保每个hart独立配置 |
| 低优先级中断丢失 | 未使用pending寄存器 | 改用pending机制保存中断 |
总结
- RISC-V中断级联的核心目标是扩展中断号数量:主PLIC为每个从PLIC分配1个聚合中断号,从PLIC管理细分中断源,实现中断源数量的倍数级扩展;
- 本实现中,主PLIC原生16个中断号,级联2个从PLIC后扩展到40个,可无限级联(只要主PLIC有剩余中断号);
- 核心逻辑是「从PLIC聚合中断请求 → 主PLIC映射全局中断ID → CPU根据ID归属处理细分中断」;
- 完全对齐RISC-V PLIC标准,可直接适配RISC-V CPU的中断控制器架构;
- 实际使用需注意多核支持、寄存器配置、优先级阈值等关键配置项。