算术/逻辑运算指令 (R-Type)
这类指令操作两个寄存器,结果写入第三个寄存器。所有R-Type指令的Opcode都是0110011。
注:在此解释一下Opcode,Opcode是操作码指定操作类型。
| 指令 | 功能描述 | Funct3 | Funct7 |
| ADD | 加法:rd = rs1 + rs2 | 000 | 0000000 |
| SUB | 减法:rd = rs1 - rs2 | 000 | 0100000 |
| AND | 按位与:rd = rs1 & rs2 | 111 | 0000000 |
| OR | 按位或:rd = rs1 | rs2 | 110 | 0000000 |
| XOR | 按位异或:rd = rs1 ^ rs2 | 100 | 0000000 |
| SLT | 有符号数比较并置位:若rs1 < rs2,则rd=1,否则rd=0 | 010 | 0000000 |
| SLTU | 无符号数比较并置位:若rs1 < rs2,则rd=1,否则rd=0 | 011 | 0000000 |
| SLL | 逻辑左移:rd = rs1 << rs | 001 | 0000000 |
| SRL | 逻辑右移:rd = rs1 >> rs2 (高位补0) | 101 | 0000000 |
| SRA | 算术右移:rd = rs1 >> rs2 (高位补符号位) | 101 | 0100000 |
想必大家看完此表之后,会对R型指令有一个大体的了解,但对于Funct3与Funct7可能存在
困惑,接下来我会对于这两个进行一个解释:
1. Funct3(3位功能码)
长度:3个比特位(取值范围 0~7)。
位置:位于指令的第14~12位。
核心职责:区分指令的“变种”或“条件类型”。
典型应用场景:
分支指令(BEQ、BNE、BLT等):它们共享同一个Opcode。Funct3的3位编码用来告诉CPU使用哪种比较条件。
000 = BEQ(相等则跳转)
001 = BNE(不相等则跳转)
100 = BLT(小于则跳转)
加载/存储指令(LB、LH、LW等):Funct3用来指定数据的位宽(单位:字节)和是否带符号扩展。
000 = LB(加载字节,8位)
001 = LH(加载半字,16位)
010 = LW(加载字,32位)
2. Funct7(7位功能码)
长度:7个比特位(取值范围 0~127)。
位置:位于指令的第31~25位(即最高位段)。
核心职责:区分同类别下的“不同运算操作”,特别是扩展算术运算。
典型应用场景(最经典的案例):
在RISC-V中,ADD(加法) 和 SUB(减法) 拥有完全相同的Opcode、完全相同的Funct3(000),并且都需要两个源寄存器和目标寄存器。那么CPU怎么区分它们?
全靠 Funct7 来定乾坤:
Funct7 = 0000000 + Funct3 = 000 → 代表 ADD(加法)
Funct7 = 0100000 + Funct3 = 000 → 代表 SUB(减法)
这7个比特位允许CPU支持更复杂的操作,比如乘除扩展(M拓展)或位操作(B拓展)。
3. 为什么要把指令拆得这么碎?(设计哲学)
为什么不把Opcode直接做长一点(比如20位)来包含所有指令?
节省指令位宽,高效利用寄存器:Opcode越长,留给寄存器编号(rd/rs1/rs2)的位宽就越短(只有32位固定长度)。RISC-V用极短的7位Opcode(仅128种基础大类),把细分的压力转交给Funct3和Funct7,从而腾出更多的5位空间(共32个)来命名通用寄存器,极大提高了寄存器的利用效率。
简化译码器硬件:这种固定字段的分层译码方式,让CPU的译码电路不需要复杂的查表,直接通过硬连线(Hardwired Control)就能快速识别,有利于提高主频和降低功耗。
4.举例
①模块的外壳(输入输出端口)
Verilog用 module 和 endmodule 包裹。这就像芯片的引脚。
module alu (
// 输入信号
input wire [31:0] rs1, // 源操作数1 (对应你的rs1)
input wire [31:0] rs2, // 源操作数2 (对应你的rs2)
input wire [2:0] funct3, // 对应你的3位Funct3
input wire [6:0] funct7, // 对应你的7位Funct7
// 输出信号
output reg [31:0] rd // 结果 (对应你的rd)
);
②核心逻辑(case分支)
电路里区分指令,靠的是 case 语句(类似C语言的 switch)。
// 内部逻辑:只要输入信号(rs1/rs2/funct3/funct7)一变化,立即重新计算
always @(*) begin
case ({funct7, funct3}) // 拼接字段,作为选择条件
// ---------- 算术运算 ----------
10'b0000000_000: rd = rs1 + rs2; // ADD (funct7=0000000, funct3=000)
10'b0100000_000: rd = rs1 - rs2; // SUB (funct7=0100000, funct3=000)
// ---------- 位运算 ----------
10'b0000000_111: rd = rs1 & rs2; // AND (funct7=0000000, funct3=111)
10'b0000000_110: rd = rs1 | rs2; // OR (funct7=0000000, funct3=110)
10'b0000000_100: rd = rs1 ^ rs2; // XOR (funct7=0000000, funct3=100)
// ---------- 比较置位 ----------
10'b0000000_010: rd = ($signed(rs1) < $signed(rs2)) ? 32'd1 : 32'd0; // SLT (有符号)
10'b0000000_011: rd = (rs1 < rs2) ? 32'd1 : 32'd0; // SLTU (无符号)
// ---------- 移位运算 ----------
10'b0000000_001: rd = rs1 << rs2[4:0]; // SLL (逻辑左移)
10'b0000000_101: rd = rs1 >> rs2[4:0]; // SRL (逻辑右移,高位补0)
10'b0100000_101: rd = rs1 >>> rs2[4:0];// SRA (算术右移,高位补符号位, 注意Verilog用>>>)
default: rd = 32'b0; // 默认输出0
endcase
end
③时序逻辑(带时钟的写法)
上面的代码是组合逻辑(一通电立刻出结果,有延迟但没节拍)。在实际CPU流水线中,计算完
结果通常要在下一个时钟上升沿锁存。代码长这样:
// 带时钟和复位信号的写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd <= 32'b0; // 复位时结果清零
end else begin
case ({funct7, funct3})
10'b0000000_000: rd <= rs1 + rs2; // 注意这里用的是 <= (非阻塞赋值)
// ... (其余case和上面一样)
endcase
end
end
④整体的一个大概框架
module riscv_alu_top (
input wire clk, // 时钟
input wire rst_n, // 复位(低有效)
input wire [31:0] inst, // 输入的32位机器码
input wire [31:0] reg1, reg2,
output reg [31:0] result
);
wire [6:0] opcode = inst[6:0]; // 提取Opcode
wire [2:0] f3 = inst[14:12]; // 提取Funct3
wire [6:0] f7 = inst[31:25]; // 提取Funct7
// 调用上面的ALU模块 (实例化)
alu u_alu (
.rs1(reg1),
.rs2(reg2),
.funct3(f3),
.funct7(f7),
.rd(result)
);
endmodule
最后,关于RISC-V的其余内容主包正在努力更新中,宝子们可以关注等待后续