3 Star 26 Fork 6

lanzhoo / TangNano9k_Tutorial

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
TinyCore_02.md 12.07 KB
一键复制 编辑 原始数据 按行查看 历史

简单CPU原型设计(2):译码与执行:加法与跳转

上节演示了简单的取指令,本节以加法指令和跳转指令为例演示译码与执行过程及Core内部结构。

RISC-V指令集架构

RISC-V规范

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进行实现和讲解。

“chapter 2” RV32I指令集

“2.1 Programmers’ Model for Base Integer Subset”

“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)。

“2.2 Base Instruction Formats”

“Figure 2.2”描述了4种指令格式。从中我们可以看到:

  • bit[6:0]是opcode,即从中可以识别指令的类别;
  • bit[14:12]是funct3,从中可以同类别中不同的指令(R-Type格式还需要加上bit[31:25]一起区分);
  • rs1是源寄存器1索引(index),在bit[19:15];rd是目的寄存器索引,在bit[11:7];rs2是源寄存器2索引,在bit[24:20];每个索引用5个比特表示,选择32个通用寄存器中的一个;
  • imm是立即数,位数和位置在不同格式中有不同;

“2.3 Immediate Encoding Variants”

“Figure 2.3”和“Figure 2.4”描述立即数imm的具体格式,用于怎样从指令中提取立即数的值。

寄存器的值加立即数的指令:ADDI

“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去。

无条件跳转指令:JAL 和 J

“2.5 Control Transfer Instructions”描述跳转指令的格式和功能。其中“Unconditional Jumps”是无条件跳转。

指令的格式如下:

它的作用是:

  • 将pc+4送rd;
  • 将pc+符号位扩展的imm送pc;

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};

从Register File取操作数

对于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计算

ADI指令的两个操作数,rs1的值从regfile获得,立即数imm_I从指令获得。加法实现语句为:

    //ADI operation
    wire [31:0] adi_result = rf_rs1data + i_imm_I;

回写rd

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频率的最大值,是频率再往高走需要采用流水线的原因。

J指令实现

无条件跳转指令的实现实质就是改变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;

LED显示

从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]};

regfile模块的设计

按规范的要求,RV32I包含31个32位的通用寄存器,还有一个x0返回全0。

reg [31:0] rf_r [1:31];

在core回写数据时,regfile要将数据写入指定的寄存器。其中,相关端口的作用是:

  • i_rf_rdwen为1时,允许写入;
  • i_rf_rdidx选择31个寄存器中的1个;
  • i_rf_rd_data为要写入的数据;
  • 在i_rf_rd_wr_clk的上升沿写入;

代码中使用了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]亮,表示跳转指令。

Verilog
1
https://gitee.com/lanzhoo/TangNano9k_Tutorial.git
git@gitee.com:lanzhoo/TangNano9k_Tutorial.git
lanzhoo
TangNano9k_Tutorial
TangNano9k_Tutorial
master

搜索帮助