上节演示了简单的取指令,本节以加法指令和跳转指令为例演示译码与执行过程及Core内部结构。
RISC-V规范可以从https://riscv.org/technical/specifications/获取。
以下文字基于2017年的版本进行解读(在specs_and_datasheets仓库中上传了供参考)。
riscv-spec-v2.2.pdf是描述常规指令及架构的规范文件,riscv-privileged-v1.10.pdf是描述特权指令及架构的规范文件。
我们先打开riscv-spec-v2.2.pdf,看到它的标题是“Volume 1: User-Level ISA”,版本是2.2。
往下拉,在“Preface”页,看到一个表格,列出“RISC-V ISA modules”的版本。这些modules分为Base和Extension两类,Base包含RV32I、RV32E、RV64I、RV128I,Extension包括M、A、F、D等。
Base中的RV表示RISC-V,32、64、128表示指令的长度,I表示整数。
意思是,RISC-V规范中定义了许多适用于不同场合的指令,它们按module进行分类,一个CPU/Core必须实现其中一种且仅一种Base module规定的指令,然后可以扩展零到多个Extension module规定的指令。
RV32I是最基础的指令集,本系列帖子围绕RV32I进行实现和讲解。
“2.1 Programmers’ Model for Base Integer Subset”描述从程序员角度能够看到的模型,就是31个保存32位整数的通用寄存器x1-x31、1个值恒为0的寄存器x0、程序计数器pc。通常用x1存储函数调用时的返回地址。
“the program counter pc holds the address of the current instruction”,注意:PC的值指向当前指令的地址。有些指令集架构是指向下一指令的地址(比如avr)。
“Figure 2.2”描述了4种指令格式。从中我们可以看到:
“Figure 2.3”和“Figure 2.4”描述立即数imm的具体格式,用于怎样从指令中提取立即数的值。
“2.4 Integer Computational Instructions”描述整数运算指令的格式和功能。
我们这节只关注一条指令:ADDI。
ADDI用于将一个寄存器(rs1)的值加一个立即数(imm),然后写入另一个寄存器(rd,可以与rs1相同)。
其格式如图所示:
其中op-code的值为OP-IMM,funct3的值为ADDI。
OP-IMM对应的二进制数值在“Chapter 19 RV32/64G Instruction Set Listings”的“Table 19.1”中给出,可以看到OP-IMM对应的二进制数值为“0b0010011”。
ADDI指令对应的op-code和funct3的值还可以从“Table 19.2”中查到,funct3的值为“0b000”。
也就是说,给出一个32位的二进制数,如果它的bit[6:0]是0b0010011、bit[14:12]是0b000,那么对于一个RV32I的Core来说,它就是一条ADDI指令。它的bit[19:15]表示rs1,bit[11:7]表示rd,bit[31:20]表示立即数imm。
这个Core就应当将rs1的值取出来,加上imm,然后写回到rd去。
“2.5 Control Transfer Instructions”描述跳转指令的格式和功能。其中“Unconditional Jumps”是无条件跳转。
指令的格式如下:
它的作用是:
J伪指令:当rd为x0时,第1步功能就不用实现,就是简单的跳转,指令符号可以简写为J。
从“Table 19.1”和“Table 19.2”中都可以查到,opcode JAL的值是1101111。
也就是说,给出一个32位的二进制数,如果它的bit[6:0]是0b1101111,那么对于一个RV32I的Core来说,它就是一条JAL指令。它的bit[11:7]表示rd,bit[31:12]表示乱序的立即数imm。
这个Core就应当将当前PC值加上4送rd,然后从imm得到跳转的相对偏移量offset,加到PC上。
用i表示取出的指令,则用Verilog的语法表示拼接关系:
offset={{12{i[31]}},i[19:12],i[20],i[30:21],1'b0};
其中{12{i[31]}}实现符号位扩展,将i[31]复制为12位。
复制工程03_tinycore_step01到03_tinycore_step02,修改工程文件名称。
修改core.v。
仅实现ADI指令和J伪指令(JAL x0,)。
core模块从指令存储器获得一条32比特的二进制数据,怎么判断它是不是ADI或J指令呢?
从上面的文字可见,ADI指令的opcode是OP-IMM(0b0010011),func3为0b000,因此用如下代码来识别:
wire [6:0] opcode = instruction[6:0];
wire [2:0] funct3 = instruction[14:12];
wire opcode_OP_IMM = (opcode == 7'b0010011);
wire inst_ADI = opcode_OP_IMM & (funct3 == 3'b000);
表达式“(opcode == 7'b0010011)”判断opcode是否OP-IMM,真则表达式的值为1,否则为0。 更严谨的表达方式为(不用操心真值为1还是0):
wire opcode_OP_IMM = (opcode == 7'b0010011) ? 1'b1 : 1'b0;
上面的后两句可以合并为:
wire inst_ADI = (opcode == 7'b0010011) & (funct3 == 3'b000);
当然,全部四句可以合并为:
wire inst_ADI = (instruction[6:0] == 7'b0010011) & (instruction[14:12] == 3'b000);
同理,判断是否J指令:
wire [6:0] opcode = instruction[6:0];
wire [4:0] i_rd_idx = instruction[11:7];
wire opcode_OP_JAL = (opcode == 7'b1101111);
wire inst_JAL = opcode_OP_JAL;
wire inst_J = inst_JAL & (i_rd_idx == 5'b0);
同样地,可以简化为:
wire inst_J = (instruction[6:0] == 7'b1101111) & (instruction[11:7] == 5'b0);
然后就是获取指令的操作数。
ADDI指令是将rs1的值加上imm,然后送rd。
获取rs1_idx:
wire [4:0] i_rs1_idx = instruction[19:15];
获取imm,I格式的立即数是12位,位置在instruction[31:20],最高位是符号位,需要将符号位进行扩展(复制),形成32位的有符号整数:
wire [31:0] i_imm_I = {{21{instruction[31]}},instruction[30:20]};
获取rd_idx:
wire [4:0] i_rd_idx = instruction[11:7];
对于JAL指令,有两个参数,rd表示将下一条指令的地址保存到rd,imm表示跳转的相对偏移量。
rd获取代码同上。
J格式的立即数在instruction[31:12],但顺序是乱的,需要按格式重新编排,并将符号位进行扩展。格式见上文或规范,代码如下:
wire [31:0] i_imm_J = {{12{instruction[31]}},instruction[19:12],instruction[20],instruction[30:21],1'b0};
对于ADI指令,指令中给出rs1_idx,需要从Register File取出对应寄存器的内容。
Register File由单独的模块实现(下文介绍),这里是引用的代码:
//fetch rs1 value from Register File
wire[31:0] rf_rs1data;
wire[31:0] rf_rs2data;
wire rf_rdwen;
wire[31:0] rf_rd_data;
wire[31:0] x16_value;
//register file module
regfile m_regfile(
.reset_n(reset_n),
.i_rf_rs1idx(i_rs1_idx),
.i_rf_rs2idx(i_rs2_idx),
.rf_rs1data(rf_rs1data),
.rf_rs2data(rf_rs2data),
.i_rf_rdidx(i_rd_idx),
.i_rf_rdwen(rf_rdwen),
.i_rf_rd_data(rf_rd_data),
.i_rf_rd_wr_clk(~core_clk),
.x16_value(x16_value)
);
regfile模块根据输入的i_rs1_idx,立即给出对应的寄存器值rf_rs1data。
ADI指令的两个操作数,rs1的值从regfile获得,立即数imm_I从指令获得。加法实现语句为:
//ADI operation
wire [31:0] adi_result = rf_rs1data + i_imm_I;
ADI指令的运算结果要写回regfile,rd_idx从指令获得,设置regfile的i_rf_rdwen为高,然后regfile模块会在i_rf_rd_wr_clk的上升沿将i_rf_rd_data的输入值写入指定的寄存器:
//write back
assign rf_rd_data = adi_result;
assign rf_rdwen = inst_ADI;
//register file module
regfile m_regfile(
.reset_n(reset_n),
.i_rf_rs1idx(i_rs1_idx),
.i_rf_rs2idx(i_rs2_idx),
.rf_rs1data(rf_rs1data),
.rf_rs2data(rf_rs2data),
.i_rf_rdidx(i_rd_idx),
.i_rf_rdwen(rf_rdwen),
.i_rf_rd_data(rf_rd_data),
.i_rf_rd_wr_clk(~core_clk),
.x16_value(x16_value)
);
从上面的代码可以看到:
.i_rf_rd_wr_clk(~core_clk),
core_clk的下降沿触发回写。
因为core_clk的上升沿触发PC值的改变,从而取下一条指令,因此半个core_clk的时长内要完成取指令、解释指令和执行指令(除回写外)的操作,这个过程决定了core_clk频率的最大值,是频率再往高走需要采用流水线的原因。
无条件跳转指令的实现实质就是改变PC寄存器的值,使其指向目的跳转位置。
PC寄存器的实现不再是一个不断递增的计数器,先声明一个32位的D触发器,在core_clk的上升沿写入新值。
//PC
reg [31:0] program_counter;
wire [31:0] pc_next;
// always @(posedge core_clk or negedge reset_n) begin
always @(posedge core_clk) begin
if (~reset_n) begin
program_counter <= ~(32'b0);
end
else begin
program_counter <= pc_next;
end
end
注意always结构的触发事件不再包含“negedge reset_n”,在core_clk的上升沿判断reset_n,这种reset称为同步reset。
program_counter的初始值改为全1,当第1个时钟来临时,加1变为0。(加4然后右移两位还是0,见后述。)
在core_clk的上升沿,将pc_next赋给program_counter。
pc_next的取值为:
//Jump
//
assign pc_next = inst_JAL ? program_counter + i_imm_J : program_counter + 4;
当遇到跳转指令时,下一条指令的位置是当前PC值加上跳转偏移量i_imm_J,否则是当前PC值加4。
为什么是加4而不是加1?
因为一条32位的指令占4个字节,而PC值的单位是字节。
因为指令存储器一个地址单元存32位数据,所以送给指令存储器的地址信号为:
assign ibus_addr = program_counter>>2;
assign ibus_re = 1'b1;
从regfile引出x16的值。
我们可以用LED查看x16的低6位:
assign monitor_port = x16_value;
在top.v中:
//display
assign led = ~monitor_port[5:0];
我们还可以用monitor_port来做debug用,比如查看指令的类型和送出的地址信号:
assign monitor_port = {inst_J, inst_ADI, ibus_addr[3:0]};
查看rd回写信号:
assign monitor_port = {rf_rdwen, rf_rdidx};
查看操作数:
assign monitor_port = {rf_rdwen, i_imm_I[4:0]};
按规范的要求,RV32I包含31个32位的通用寄存器,还有一个x0返回全0。
reg [31:0] rf_r [1:31];
在core回写数据时,regfile要将数据写入指定的寄存器。其中,相关端口的作用是:
代码中使用了generate代码自动复制功能:
reg [31:0] rf_r [1:31];
wire rf_wen[1:31];
genvar i;
generate
for (i=1; i<32; i=i+1) begin:regfile
assign rf_wen[i] = i_rf_rdwen & (i_rf_rdidx == i) ;
always @(posedge i_rf_rd_wr_clk or negedge reset_n) begin
if (~reset_n) begin
rf_r[i] <= 0;
end
else if (rf_wen[i]) begin
rf_r[i] <= i_rf_rd_data;
end
end
end
endgenerate
其核心代码是:
@(posedge i_rf_rd_wr_clk)
if (i_rf_rdwen & (i_rf_rdidx == i))
rf_r[i] <= i_rf_rd_data;
通过for循环,生成rf_r[1]到rf_r[31]的31份always结构代码。
core给出rs1_idx和rs2_idx,regfile给出对应的寄存器的数据,index为0时直接输出0:
assign rf_rs1data = (i_rf_rs1idx > 0) ? rf_r[i_rf_rs1idx] : 32'b0;
assign rf_rs2data = (i_rf_rs2idx > 0) ? rf_r[i_rf_rs2idx] : 32'b0;
将x16的值引出供观察:
assign x16_value = rf_r[16];
regfile模块的端口声明:
module regfile (
input reset_n,
input [4:0] i_rf_rs1idx,
input [4:0] i_rf_rs2idx,
output [31:0] rf_rs1data,
output [31:0] rf_rs2data,
output [31:0] x16_value,
input [4:0] i_rf_rdidx,
input i_rf_rdwen,
input [31:0] i_rf_rd_data,
input i_rf_rd_wr_clk //update rf[i_rf_rdidx] with i_rf_rd_data on posedge of i_rf_rd_wr_clk if (i_rf_rdwen == 1)
);
程序代码在rom_data.mi中,就两条指令。须重新生成gowin_prom。
#File_format=Hex
#Address_depth=8
#Data_width=32
00180813
ffdff06f
00000000
00000000
00000000
00000000
00000000
00000000
或者直接修改gowin_prom.v:
defparam prom_inst_0.INIT_RAM_00 = 256'h000000000000000000000000000000000000000000000000ffdff06f00180813;
第1条指令:0x00180813,转为二进制是:0b0000,0000,0001,1000,0000,1000,0001,0011。
对应汇编指令为:ADDI x16, x16, #1
第2条指令:0xffdff06f,转为二进制是:0b1111,1111,1101,1111,1111,0000,0110,1111。
对应汇编指令为:JAL x0, -4
等效c语言程序为:
void main(void) {
int i;
while(1) {
i=i+1;
}
}
大概1秒执行一条指令,每两条指令(一条加、一条跳转)更新一次x16,因此可以观察到led约每2秒加1。
假如查看指令的类型和送出的地址信号:
assign monitor_port = {inst_J, inst_ADI, ibus_addr[3:0]};
可以看到地址为0(led[3:0])时led[4]亮,表示加法指令;地址为1时led[5]亮,表示跳转指令。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。