## 8.4 硬件循环机制分析

8.4.1 硬件循环介绍

硬件循环是RI5CY引入的一项特殊机制,目的是提高包含有循环操作的代码的执行效率,如下是一段包含有循环操作的C代码。</br>

for(i=0;i<100;i++){
   d[i]=a[i]+1;
}

如果没有使用硬件循环,其对应的汇编代码如下。</br>

  mv x4, 0
  mv x5, 100
Lstart: lw x2, 0(x10)
  addi x10, x10, 4
  addi x2, x2, 1
  sw x2, 0(x11)
  addi x11, x11, 4
  addi x4, x4, 1
  bne x4, x5, Lstart

寄存器x4存放的是已执行的循环次数,寄存器x5存放的是需要执行的总循环次数,在每一次循环操作最后,要将寄存器x4加1,然后与寄存器x5比较,如果不相等,那么转移到循环起始位置继续执行。这样的一个过程有两个地方影响效率:</br> (1)每次循环,要将x4加1。</br> (2)每次循环,要做一个分支转移,如果分支预测做的不好,那么会浪费至少一个周期。</br> RI5CY引入硬件循环以改进循环效率,引入硬件循环后的汇编代码如下。</br>

  lp.setupi 100, Lend
  lw x2, 0(x10)
  addi x10, x10, 4
  addi x2, x2, 1
  sw x2, 0(x11)
Lend: addi x11, x11, 4

上述代码中涉及到RI5CY定制的硬件循环相关指令,在后续章节中将有介绍,此处,读者只需要理解第一条指令lp.setupi设置了循环次数,设置了循环段的终止地址,随后就是循环段代码,与没有使用硬件循环的汇编代码相比,此处减少了判断是否循环次数达到的代码,同时减少了分支转移指令,效率因此提高。

8.4.2 硬件循环相关的CSR

为了实现硬件循环,在RI5CY中定义了一些CSR寄存器,用来保存硬件循环代码段的一些属性,上一节中使用的指令lp.setupi就是用来设置这些寄存器的。每一个硬件循环代码段对应三个CSR,分别保存硬件循环代码段的起始地址、终止地址、循环次数,RI5CY支持嵌套硬件循环,相应的CSR分为两组,对应loop0、loop1,如表8-7所示。</br></br> 表8-7 硬件循环相关的CSR</br>

地址 名称 访问属性 描述
0x7B0 lpstart[0] 可读可写 硬件loop0的起始地址
0x7B1 lpendt[0] 可读可写 硬件loop0的终止地址
0x7B2 lpcount[0] 可读可写 硬件loop0的循环次数
0x7B4 lpstart[1] 可读可写 硬件loop1的起始地址
0x7B5 lpend[1] 可读可写 硬件loop1的终止地址
0x7B6 lpcount[1] 可读可写 硬件loop1的循环次数

8.4.3 硬件循环相关的指令

除了8.4.1节中用到的指令lp.setupi外,还有很多指令用来设置硬件循环,这些指令可以分两类,一类是长指令,一类是短指令,其区别如下。</br>

  • 长指令:每条指令只能设置硬件循环属性中的一条,比如只能设置硬件循环起始地址,但是该类指令不需要紧跟着硬件循环代码段。
  • 短指令:每条指令可以设置硬件循环的全部属性,包括硬件循环的起始地址、终止地址、循环次数等,但是该类指令需要紧跟着就是硬件循环代码段。</br> 硬件循环相关指令如表8-8所示,其编码如图8-20所示。</br></br> 表8-8 硬件循环相关指令[6]</br> </br>

</br> 图8-20 硬件循环相关指令的编码[6]</br>

8.4.4 硬件循环实现过程

RI5CY中与硬件循环实现有关的模块,如图8-21所示,包括如下:</br>

  • 流水线取指阶段模块riscv_if_stage,其中例化了riscv_hwloop_controller、riscv_prefetch_buffer两个与硬件循环实现有关的模块
  • 流水线译码阶段模块riscv_id_stage,其中例化了riscv_decoder、riscv_hwloop_regs两个与硬件循环实现有关的模块
  • 控制与状态寄存器模块rscv_sc_registers </br> 图8-21 RI5CY中与硬件循环实现有关的模块</br></br> 图8-21中对部分模块只给出了与硬件循环实现有关的输入输出接口,并简单绘制了其接口连接关系。下面结合图8-21,以及硬件循环指令执行过程,分析硬件循环实现原理。</br></br> 硬件循环相关指令在进入流水线的译码阶段时,才被识别出来,具体是在riscv_decoder模块中,相关代码如下,其中instr_rdata_i是从取指阶段传递过来的指令,OPCODE_HWLOOP是一个宏定义,其值是7'h7b,参考图8-20可知,正是硬件循环指令的opcode。随后,依据指令第12-14bit的值,进一步判断是哪一条硬件循环指令,并给出输出信号hwloop_we_o,以及一些复用选择信号的值,其中hwloop_we_o共有3bit,第0bit表示是否是设置循环段起始地址,第1bit表示是否是设置循环段结束地址,第2bit表示是否是设置循环次数。</br>

    module riscv_decoder
    (
    ......
    
    // from IF/ID pipeline
    input  logic [31:0] instr_rdata_i,           // instruction read from instr memory/cache
    
    output logic [2:0]  hwloop_we_o,             // write enable for hwloop regs
    output logic        hwloop_target_mux_sel_o, // selects immediate for hwloop target
    output logic        hwloop_start_mux_sel_o,  // selects hwloop start address input
    output logic        hwloop_cnt_mux_sel_o,    // selects hwloop counter input
    ......
    );
    always_comb
    begin
      ......
    
      hwloop_we                   = 3'b0;
      hwloop_target_mux_sel_o     = 1'b0;
      hwloop_start_mux_sel_o      = 1'b0;
      hwloop_cnt_mux_sel_o        = 1'b0;
    
      .......
    
      unique case (instr_rdata_i[6:0])
        OPCODE_HWLOOP: begin
          hwloop_target_mux_sel_o = 1'b0;
    
          unique case (instr_rdata_i[14:12])
            3'b000: begin
              // 是lp.starti指令:设置循环段起始地址
              hwloop_we[0]           = 1'b1;
              hwloop_start_mux_sel_o = 1'b0;
            end
    
            3'b001: begin
              // 是lp.endi指令:设置循环段结束地址
              hwloop_we[1]         = 1'b1;
            end
    
            3'b010: begin
              // 是lp.count指令:设置循环次数,其值就在读出的寄存器rega中
              hwloop_we[2]         = 1'b1;
              hwloop_cnt_mux_sel_o = 1'b1;
              rega_used_o          = 1'b1;
            end
    
            3'b011: begin
              // 是lp.counti指令:设置循环次数,其值是指令中立即数的值
              hwloop_we[2]         = 1'b1;
              hwloop_cnt_mux_sel_o = 1'b0;
            end
    
            3'b100: begin
              // 是lp.setup指令:需要设置循环段起始地址、结束地址、循环次数,其中循环次数读出的寄存器rega的值
              hwloop_we              = 3'b111;
              hwloop_start_mux_sel_o = 1'b1;
              hwloop_cnt_mux_sel_o   = 1'b1;
              rega_used_o            = 1'b1;
            end
    
            3'b101: begin
              // 是lp.setupi指令:需要设置循环段起始地址、结束地址、循环次数,其中循环次数是指令中立即数的值
              hwloop_we               = 3'b111;
              hwloop_target_mux_sel_o = 1'b1;
              hwloop_start_mux_sel_o  = 1'b1;
              hwloop_cnt_mux_sel_o    = 1'b0;
            end
    
            default: begin
              illegal_insn_o = 1'b1;
            end
          endcase
        End
    ......
    assign hwloop_we_o       = (deassert_we_i) ? 3'b0          : hwloop_we;
    

    上述信号送入流水线译码阶段,进一步处理,代码如下,其中最终确定了要写的硬件循环相关寄存器的情况。</br>

    // 硬件循环有两组寄存器,指令的第7bit表示是要操作哪一组寄存器
    assign hwloop_regid_int = instr[7];   
    
    // hwloop target mux
    always_comb
    begin
      case (hwloop_target_mux_sel)
        1'b0: hwloop_target = pc_id_i + {imm_iz_type[30:0], 1'b0};
        1'b1: hwloop_target = pc_id_i + {imm_z_type[30:0], 1'b0};
      endcase
    end
    
    // 硬件循环起始地址的两种情况,一种是当前指令地址加一个立即数的值,另一种是下一条指令地址的值
    always_comb
    begin
      case (hwloop_start_mux_sel)
        1'b0: hwloop_start_int = hwloop_target;   // for PC + I imm
        1'b1: hwloop_start_int = pc_if_i;         // for next PC
      endcase
    end
    
    // 硬件循环次数的两种情况,一种是立即数,另一种是读出的寄存器的值
    always_comb
    begin : hwloop_cnt_mux
      case (hwloop_cnt_mux_sel)
        1'b0: hwloop_cnt_int = imm_iz_type;
        1'b1: hwloop_cnt_int = operand_a_fw_id;
      endcase;
    end
    
    // 最终确定要写的硬件循环相关寄存器的情况
    assign hwloop_start = hwloop_we_int[0] ? hwloop_start_int : csr_hwlp_data_i;
    assign hwloop_end   = hwloop_we_int[1] ? hwloop_target    : csr_hwlp_data_i;
    assign hwloop_cnt   = hwloop_we_int[2] ? hwloop_cnt_int   : csr_hwlp_data_i;
    assign hwloop_regid = (|hwloop_we_int) ? hwloop_regid_int : csr_hwlp_regid_i;
    assign hwloop_we    = (|hwloop_we_int) ? hwloop_we_int    : csr_hwlp_we_i;
    

    参考图8-21可知,上述代码确定的信号,会送入到riscv_hwloop_regs模块对应端口,做进一步处理。riscv_hwloop_regs模块实际实现了两组硬件循环相关寄存器,所以上述信号,会修改这些硬件循环相关寄存器的值,代码如下。</br> ~~~verilog module riscv_hwloop_regs

    (

    parameter N_REGS = 2, parameter N_REG_BITS = $clog2(N_REGS) ) ( input logic clk, input logic rst_n,

    // from ex stage input logic [31:0] hwlp_start_data_i, input logic [31:0] hwlp_end_data_i, input logic [31:0] hwlp_cnt_data_i, input logic [2:0] hwlp_we_i, input logic [N_REG_BITS-1:0] hwlp_regid_i, // selects the register set

    // from controller input logic valid_i,

    // from hwloop controller input logic [N_REGS-1:0] hwlp_dec_cnt_i,

    // to hwloop controller output logic [N_REGS-1:0] [31:0] hwlp_start_addr_o, output logic [N_REGS-1:0] [31:0] hwlp_end_addr_o, output logic [N_REGS-1:0] [31:0] hwlp_counter_o );

logic [N_REGS-1:0] [31:0] hwlp_start_q; logic [N_REGS-1:0] [31:0] hwlp_end_q; logic [N_REGS-1:0] [31:0] hwlp_counter_q, hwlp_counter_n;

int unsigned i;

assign hwlp_start_addr_o = hwlp_start_q; assign hwlp_end_addr_o = hwlp_end_q; assign hwlp_counter_o = hwlp_counter_q;

// 设置硬件循环的起始地址 always_ff @(posedge clk, negedge rst_n) begin : HWLOOP_REGS_START if (rst_n == 1'b0) begin hwlp_start_q <= '{default: 32'b0}; end else if (hwlp_we_i[0] == 1'b1) begin hwlp_start_q[hwlp_regid_i] <= hwlp_start_data_i; end end

// 设置硬件循环的结束地址 always_ff @(posedge clk, negedge rst_n) begin : HWLOOP_REGS_END if (rst_n == 1'b0) begin hwlp_end_q <= '{default: 32'b0}; end else if (hwlp_we_i[1] == 1'b1) begin hwlp_end_q[hwlp_regid_i] <= hwlp_end_data_i; end end

// 设置硬件循环的执行次数,并且在每次执行一遍后,将执行次数减一 genvar k; for (k = 0; k < N_REGS; k++) begin assign hwlp_counter_n[k] = hwlp_counter_q[k] - 1; end

always_ff @(posedge clk, negedge rst_n) begin : HWLOOP_REGS_COUNTER if (rst_n == 1'b0) begin hwlp_counter_q <= '{default: 32'b0}; end else begin for (i = 0; i < N_REGS; i++) begin if ((hwlp_we_i[2] == 1'b1) && (i == hwlp_regid_i)) begin hwlp_counter_q[i] <= hwlp_cnt_data_i; end else begin if (hwlp_dec_cnt_i[i] && valid_i) hwlp_counter_q[i] <= hwlp_counter_n[i]; end end end end

// do not decrement more than one counter at once assert property ( @(posedge clk) (valid_i) |-> ($countones(hwlp_dec_cnt_i) <= 1) );

Endmodule

至此,硬件循环相关寄存器设置完毕,参考图8-21,这些寄存器的值通过接口hwlp_start_o、hwlp_end_o、hwlp_cnt_o送入流水线取指阶段的模块if_stage。后者例化了riscv_hwloop_controller、riscv_prefetch_buffer两个与硬件循环实现有关的模块。</br></br>
参考图8-21,hwlp_start_o、hwlp_end_o、hwlp_cnt_o实际是直接送入riscv_hwloop_controller模块,同时送入riscv_hwloop_controller模块的,还有当前正在读取的指令的地址current_pc_i。riscv_hwloop_controller模块判断current_pc_i是否等于hwlp_end_o中的一个值,如果相等,并且对应的hwlp_cnt_o不等于1(<font  color=#660000>实际在代码中跳转的条件比较复杂,笔者也没有看懂为何当hwlp_cnt_o等于2的时候,跳转条件是~hwlp_dec_cnt_id_i[i]</font>),那么就跳转到循环代码的起始地址,主要代码如下:</br>
~~~verilog
module riscv_hwloop_controller
#(
  parameter N_REGS = 2
)
(
  // from id stage
  input  logic [31:0]              current_pc_i,

  // from hwloop_regs
  input  logic [N_REGS-1:0] [31:0] hwlp_start_addr_i,
  input  logic [N_REGS-1:0] [31:0] hwlp_end_addr_i,
  input  logic [N_REGS-1:0] [31:0] hwlp_counter_i,

  // to hwloop_regs
  output logic [N_REGS-1:0]        hwlp_dec_cnt_o,

  // from pipeline stages
  input  logic [N_REGS-1:0]        hwlp_dec_cnt_id_i,

  // to id stage
  output logic                     hwlp_jump_o,
  output logic [31:0]              hwlp_targ_addr_o
);


  logic [N_REGS-1:0] pc_is_end_addr;

  // end address detection
  integer j;


  // 下面的代码判断当前正在取的指令地址current_pc_i是否等于任一硬件循环段的结束地址
  // 如果当前正在取的指令地址current_pc_i等于任一硬件循环段的结束地址,并且对应的hwlp_cnt_o不等于1
  // 那么设置pc_is_end_addr为1
  genvar i;
  generate
    for (i = 0; i < N_REGS; i++) begin
      always @(*)
      begin
        pc_is_end_addr[i] = 1'b0;
        if (current_pc_i == hwlp_end_addr_i[i]) begin
          if (hwlp_counter_i[i][31:2] != 30'h0) begin
            pc_is_end_addr[i] = 1'b1;
          end else begin
            case (hwlp_counter_i[i][1:0])
              2'b11:        pc_is_end_addr[i] = 1'b1;
              2'b10:        pc_is_end_addr[i] = ~hwlp_dec_cnt_id_i[i]; // only when there is nothing in flight
              2'b01, 2'b00: pc_is_end_addr[i] = 1'b0;
            endcase
          end
        end
      end
    end
  endgenerate

  // 如果pc_is_end_addr是1,那么表示跳转到硬件循环段的起始地址继续执行
  always_comb
  begin
    hwlp_targ_addr_o = 'x;
    hwlp_dec_cnt_o   = '0;

    for (j = 0; j < N_REGS; j++) begin
      if (pc_is_end_addr[j]) begin
        hwlp_targ_addr_o  = hwlp_start_addr_i[j];
        hwlp_dec_cnt_o[j] = 1'b1;
        break;
      end
    end
  end

  // output signal for ID stage
  assign hwlp_jump_o = (|pc_is_end_addr);

Endmodule

上述代码中的输出信号hwlp_jump_o、hwlp_targ_addr_o送入图8-21中的riscv_prefetch_buffer模块(参考8.3.2节,如果采用的是配置二,即指令预取Buffer等于指令缓存line的大小,比如128位,那么hwlp_jump_o、hwlp_targ_addr_o实际将送入riscv_prefetch_L0_buffer模块),后者将清除其内部FIFO中的内容,然后从硬件循环段起始地址重新取指填充FIFO。</br>

参考文献

[1]PULP - An Open Parallel Ultra-Low-Power Processing-Platform, http://iis-projects.ee.ethz.ch/index.php/PULP,2017-8 </br> [2]Florian Zaruba, Updates on PULPino, The 5th RISC-V Workshop, 2016.</br> [3]Michael Gautschi,etc,Near-Threshold RISC-V Core With DSP Extensions for Scalable IoT Endpoint Devices, IEEE Transactions on Very Large Scale Integration Systems</br> [4]Andreas Traber, Michael Gautschi,PULPino: Datasheet,2016.11</br> [5]http://www.pulp-platform.org/</br> [6]Andreas Traber,Michael Gautschi,Pasquale Davide Schiavone. RI5CY: User Manual version1.3. </br> [7]Andreas Traber, etc. PULPino: A small single-core RISC-V SoC. The 4th RISC-V Workshop, 2016.</br>

results matching ""

    No results matching ""