异常:CSR寄存器

为了能运行更多的C程序,并且在出现问题时记录下来,CSR寄存器是必不可缺少的。他也是切换特权模式的重要组件。虽然RISCV规范中已经将CSR从I指令集中移除,但是支持一下还是很重要的。

不多,支持最基础的机器模式相关寄存器即可。

CSR寄存器和通用寄存器堆类似,也是同步写异步读。但它不放在ID级,而是在EX级,因为我们的异常检测主要放在EX级进行。此外,CSR需要立即响应异常。另一方面,将CSR读写放在关键路径较长的EX级,也可以防止拉长其他级的关键路径。

CSR寄存器模块 CSR.sv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
`include "include/defines.svh"

// CSR(控制和状态寄存器)模块
// 实现 RV32I 的 Zicsr 扩展
// 支持机器级 CSR 和基本的特权模式切换
module CSR (
input logic clk,
input logic rst_n,

// CSR 指令接口
input logic csr_we, // CSR 写使能
input logic [11:0] csr_addr, // CSR 地址
input logic [31:0] csr_wdata, // 写入 CSR 的数据
input logic [ 2:0] csr_op, // CSR 操作类型(funct3)
output logic [31:0] csr_rdata, // 从 CSR 读取的数据
// 异常/陷阱接口
input logic exception_valid, // 异常发生
input logic [31:0] exception_pc, // 引发异常的指令的 PC
input logic [31:0] exception_cause, // 异常原因代码
input logic [31:0] exception_tval, // 异常陷阱值(例如非法指令编码)
// MRET 接口
input logic mret_valid, // 执行 MRET 指令
// 陷阱输出
output logic trap_to_mmode, // 信号重定向到陷阱处理程序
output logic [31:0] trap_target, // 陷阱处理程序地址(mtvec)
output logic [31:0] mret_target // 从陷阱返回的地址(mepc)
);

// 机器级 CSR
// mstatus - 机器状态寄存器
logic [31:0] mstatus;
// mtvec - 机器陷阱向量基地址
logic [31:0] mtvec;
// mepc - 机器异常程序计数器
logic [31:0] mepc;
// mcause - 机器原因寄存器
logic [31:0] mcause;
// mscratch - 机器暂存寄存器
logic [31:0] mscratch;
// mtval - 机器陷阱值寄存器
logic [31:0] mtval;
// mie - 机器中断使能
logic [31:0] mie;
// mip - 机器中断挂起
logic [31:0] mip;
// MISA
logic [31:0] misa;
// mcycle - 机器周期计数器(64 位,分为低位和高位)
logic [63:0] mcycle;
// MISA 硬编码为 RV32I,支持 Zicsr 和 Zmmul
// always_comb begin
// misa = 32'h4000_0110; // RV32I(基础 ISA)+ Zicsr + Zmmul
// end

// mstatus 位位置
localparam integer MIE_BIT = 3; // 机器中断使能
localparam integer MPIE_BIT = 7; // 机器先前中断使能
localparam integer MPP_LOW = 11; // 机器先前特权(低位)
localparam integer MPP_HIGH = 12; // 机器先前特权(高位)

// CSR 读取逻辑
always_comb begin
case (csr_addr)
`CSR_MSTATUS: csr_rdata = mstatus;
`CSR_MTVEC: csr_rdata = mtvec;
`CSR_MEPC: csr_rdata = mepc;
`CSR_MCAUSE: csr_rdata = mcause;
`CSR_MIE: csr_rdata = mie;
`CSR_MIP: csr_rdata = mip;
`CSR_MSCRATCH: csr_rdata = mscratch; // mscratch
`CSR_MTVAL: csr_rdata = mtval; // mtval
`CSR_MISA: csr_rdata = misa;
`CSR_MCYCLE, `CSR_CYCLE: csr_rdata = mcycle[31:0]; // mcycle 低 32 位
`CSR_MCYCLEH, `CSR_CYCLEH: csr_rdata = mcycle[63:32]; // mcycle 高 32 位
default: csr_rdata = 32'b0;
endcase
end

// 根据操作类型计算新的 CSR 值
logic [31:0] csr_new_value;
always_comb begin
case (csr_op)
`FUNCT3_CSRRW, `FUNCT3_CSRRWI: csr_new_value = csr_wdata; // 写入
`FUNCT3_CSRRS, `FUNCT3_CSRRSI: csr_new_value = csr_rdata | csr_wdata; // 设置位
`FUNCT3_CSRRC, `FUNCT3_CSRRCI: csr_new_value = csr_rdata & (~csr_wdata); // 清除位
default: csr_new_value = csr_rdata;
endcase
end

// CSR 写入逻辑
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位值
mstatus <= 32'h0000_1800; // MPP = 11(机器模式)
mtvec <= 32'h0114_5140; // 默认陷阱处理程序地址
mepc <= 32'h0000_0000;
mcause <= 32'h0000_0000;
mscratch <= 32'h0000_0000;
mtval <= 32'h0000_0000;
mie <= 32'h0000_0000;
mip <= 32'h0000_0000;
misa <= 32'h4000_0100; // RV32I
end else if (exception_valid) begin
// 异常发生时:保存状态并更新 CSR
mepc <= exception_pc;
mcause <= exception_cause;
// 保存陷阱值(非法指令编码或地址)
mtval <= exception_tval;
// 更新 mstatus:保存 MIE 到 MPIE,将 MPP 设置为当前特权(目前始终为 M 模式)
mstatus[MPIE_BIT] <= mstatus[MIE_BIT]; // MPIE = MIE
mstatus[MIE_BIT] <= 1'b0; // 禁用中断
mstatus[MPP_HIGH:MPP_LOW] <= 2'b11; // MPP = 机器模式
end else if (mret_valid) begin
// 执行 MRET 时:恢复状态
mstatus[MIE_BIT] <= mstatus[MPIE_BIT]; // MIE = MPIE
mstatus[MPIE_BIT] <= 1'b1; // MPIE = 1
mstatus[MPP_HIGH:MPP_LOW] <=
2'b11; // MPP = 机器模式(因为我们仅支持 M 模式)
end else if (csr_we) begin
// 正常 CSR 写入
case (csr_addr)
`CSR_MSTATUS: mstatus <= csr_new_value & 32'h0000_1888; // 屏蔽可写位
`CSR_MTVEC: mtvec <= {csr_new_value[31:2], 2'b00}; // 对齐到 4 字节
`CSR_MEPC: mepc <= {csr_new_value[31:2], 2'b00}; // 对齐到 4 字节
`CSR_MCAUSE: mcause <= csr_new_value;
`CSR_MIE: mie <= csr_new_value;
`CSR_MSCRATCH: mscratch <= csr_new_value; // mscratch
`CSR_MTVAL: mtval <= csr_new_value; // mtval
`CSR_MISA: misa <= misa; // 只读
default: ;
endcase
end
end

// mcycle 计数器 - 无条件每个时钟周期递增
// 注意:这是一个简化实现,不支持 mcountinhibit。
// 计数器始终运行,除非通过 CSR 指令显式写入。
// mcycle 可通过 CSR 指令(MCYCLE/MCYCLEH)写入
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mcycle <= 64'h0;
end else if (csr_we && csr_addr == `CSR_MCYCLE) begin
mcycle[31:0] <= csr_new_value;
end else if (csr_we && csr_addr == `CSR_MCYCLEH) begin
mcycle[63:32] <= csr_new_value;
end else begin
mcycle <= mcycle + 64'h1;
end
end

// 陷阱和返回信号
assign trap_to_mmode = exception_valid;
assign trap_target = {mtvec[31:2], 2'b00}; // 使用直接模式(MODE=0)
assign mret_target = mepc;

endmodule

为什么在RISCV规范内,32位CPU的时钟周期计数器也要设计为64位?

为了避免计数器频繁溢出、简化软件、性能分析的使用RV32 的时钟/指令计数器也要以 64 位来实现。如果按照32位算,假设主频为100Mhz,只要42秒就会溢出——这也太折磨了。但32位的寄存器最大位宽不够,因此要分两个,一个MCYCLE一个MCYCLEH

在读取时,要读三遍:

1
2
3
csrr   t0, cycleh    # read high
csrr t1, cycle # read low
csrr t2, cycleh # read high again

因为这两个 CSR 的读取不是原子的:在读它们的间隙时,低 32 位可能发生溢出并把进位加到高 32 位上。我总不能为这个实现A扩展吧。

当然,对于64位的就简单多了,读取一遍就行了。

顶层模块修改

在顶层模块内加上相关的异常判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 异常处理:
// 异常分为两类:
// 1. ID级异常:非法指令 - 需要提前检测以防止其前面的指令提交
// 2. EX级异常:地址未对齐、ECALL - 在执行阶段检测
//
// - 指令地址未对齐:mcause = 0,mtval = 未对齐的地址
// - 非法指令:mcause = 2,mtval = 指令编码
// - 加载地址未对齐:mcause = 4,mtval = 未对齐的地址
// - 存储地址未对齐:mcause = 6,mtval = 未对齐的地址
// - ECALL:mcause = 11,mtval = 0

// 地址未对齐检测(EX级)
// 对于JALR:目标地址必须4字节对齐(对于没有C扩展的RV32I,位1必须为0)
// JALR目标 = (rs1 + imm) & ~1,所以我们检查alu_result的位1(掩码之前)
logic instr_misaligned_EX;
logic [31:0] jalr_target_EX;
assign jalr_target_EX = {alu_result_EX[31:1], 1'b0}; // JALR target after masking bit 0
assign instr_misaligned_EX = (jump_type_EX == `JUMP_JALR) && (alu_result_EX[1] != 1'b0);

// 加载/存储地址未对齐检测(EX级)
// LW/SW:必须4字节对齐(位[1:0] == 00)
// LH/LHU/SH:必须2字节对齐(位[0] == 0)
// LB/LBU/SB:无对齐要求
logic load_misaligned_EX;
logic store_misaligned_EX;
always_comb begin
load_misaligned_EX = 1'b0;
store_misaligned_EX = 1'b0;
case (sl_type_EX)
`MEM_LW: load_misaligned_EX = (alu_result_EX[1:0] != 2'b00);
`MEM_LH,
`MEM_LHU: load_misaligned_EX = (alu_result_EX[0] != 1'b0);
`MEM_SW: store_misaligned_EX = (alu_result_EX[1:0] != 2'b00);
`MEM_SH: store_misaligned_EX = (alu_result_EX[0] != 1'b0);
default: begin
load_misaligned_EX = 1'b0;
store_misaligned_EX = 1'b0;
end
endcase
end

// EX级异常 (不包括非法指令,非法指令在ID级处理)
logic exception_valid_EX;
logic take_branch_normal;
assign exception_valid_EX = (instr_misaligned_EX || load_misaligned_EX ||
store_misaligned_EX || is_ecall_EX) && valid_EX;

// 总异常信号:EX级异常 OR ID级非法指令异常
// 优先级:EX级异常 > ID级异常
// 注意:当EX级有有效的跳转/分支时,ID级的指令将被flush,
// 所以ID级的非法指令异常不应该生效
// 这防止了跳转到数据区域时,数据被误认为非法指令而触发异常
assign exception_valid = exception_valid_EX ||
(illegal_instr_exception_ID && !take_branch_normal);

// 异常PC和原因/值的选择
always_comb begin
if (instr_misaligned_EX && valid_EX) begin
// EX级地址未对齐异常优先
exception_pc = pc_EX;
exception_cause = 32'd0; // Instruction address misaligned
exception_tval = jalr_target_EX;
end else if (load_misaligned_EX && valid_EX) begin
exception_pc = pc_EX;
exception_cause = 32'd4; // Load address misaligned
exception_tval = alu_result_EX;
end else if (store_misaligned_EX && valid_EX) begin
exception_pc = pc_EX;
exception_cause = 32'd6; // Store address misaligned
exception_tval = alu_result_EX;
end else if (is_ecall_EX && valid_EX) begin
exception_pc = pc_EX;
exception_cause = 32'd11; // ECALL
exception_tval = 32'b0;
end else if (illegal_instr_exception_ID) begin
// ID级非法指令异常
exception_pc = illegal_instr_pc_ID;
exception_cause = 32'd2; // Illegal instruction
exception_tval = illegal_instr_encoding_ID;
end else begin
// 默认值 (不应该到达这里)
exception_pc = pc_EX;
exception_cause = 32'd0;
exception_tval = 32'b0;
end
end

然后跑一下riscv-test,一切顺利。