0%

Fetch

取值

取值特点

  • 非分支跳转指令
  • 分支跳转指令
  • 指令长度不对齐

快速取值

存储器延时越小越好。

但片外 DDR 或 Flash 可能需要几十个周期的延时,片上 SRAM 也可能需要几个周期的延时。

  • ITCM 指令紧耦合存储器

    配置一段较小容量的存储器(几十 KB 的 SRAM)用于存储指令,且在物理上离处理器核很近,专属于存储器,因此能取得很小的延时。

    • 优点
      • 能保证实时性;简单。
    • 缺点
      • 使用地址区间寻址,不能像 Cache 映射无限的存储器空间。
      • 为了保证足够小的访问延时,无法将容量做到很大,因此只能用于存放容量大小有限的关键程序指令。
  • I-Cache 指令缓存

    利用软件程序的时间局部性和空间局部性,将容量巨大的外部指令存储器空间动态映射到容量有限的指令缓存中,可将访问指令存储器的平均延时降低到最小。

    • 缺点
      • 无法保证实时性。缓存容量有限,因此访问缓存存在不确定性。一旦缓存不命中(Cache Miss),则需要从外部存储器存取数据,造成较大延时。
    • CPU cache

非对齐指令

指令长度可以不相等。

例:一条32位指令处于地址不对齐的位置,则需要分两个周期读出两个32位数据,再各取一部分拼接成32位指令。

  • 普通指令非对齐

    • 剩余缓存 Leftover Buffer

      剩余缓存保存上次取值后没用完的比特位,供下个周期使用。(待下个周期取出数据后,直接拼接成完整的指令。)

  • 分支跳转指令非对齐

    • 多体 (Bank) 化的 SRAM 进行指令存储

      奇偶交错方式:使用两块32位宽的 SRAM 交错地进行存储,两个连续的32位指令字将会被分别存储在两块 SRAM 中。则一个周期可以同时访问两块 SRAM 取出两个连续的32位指令字,拼接得到真正的32位指令。

分支指令

  • 分支指令类型

    • 无条件跳转/分支指令

      • 无条件直接跳转/分支指令

        跳转目标地址从指令编码中的立即数可以直接计算得到。

      • 无条件间接跳转/分支指令

        跳转目标地址需要从寄存器索引的操作数中计算得到。

    • 带条件跳转/分支指令

      • 带条件直接跳转/分支指令
      • 带条件间接跳转/分支指令
  • 预测方向

    预测是否跳转

    • 静态预测

      仅依靠分支指令本身的信息进行预测

      • 总是预测分支指令不会跳转

        取值单元总是顺序取分支指令的下一条指令。若需要跳转,则冲刷流水线,重新进行取值。

      • 分支延迟槽 Delay Slot

        在每一条分支指令后紧跟的一条或若干条指令不受分支跳转的影响,不管分支是否跳转,后面几条指令都一定会被执行。

      • BTFN 预测 (Back Taken, Forward Not Taken)

        对向后跳转(目标地址小于当前分支地址)的预测为跳,对向前跳转的预测则为不跳。因为实际汇编程序中向后跳转的情形要多于向前跳转。

    • 动态预测

      依赖已经执行过的指令的历史信息和分支指令本身的信息进行预测

      • 一比特饱和计数器 1-bit Saturating Counter

        每次分支指令执行后,用此计数器记录上一次的方向。

        下一次分支指令永远采用上次记录的方向作为本次的预测。

      • 两比特饱和计数器 2-bit Saturating Counter

        状态机转换:

        预测精度高于 1-bit Saturating Counter 。

        “两比特饱和计数器”对于预测一条分支指令很有效,但是处理器执行的指令流中存在着众多的不同分支指令(位于不同的PC值位置)。假设只使用一个“两比特饱和计数器”在任何分支指令执行时均进行更新,那么必然会互相冲击,预测的结果会很不理想。

        最理想的情况是为每一条分支指令都分配专有的“两比特饱和计数器”为其进行预测,但是指令数目众多(32位架构理论上有4G的地址空间),不可能提供巨量的两比特饱和计数器(硬件资源开销无法接受)。所以只能够使用有限个“两比特饱和计数器”组织成一个表格,然后对于每条分支指令使用某种形式寻址方式索引表格中的某个表项的“两比特饱和计数器”。由于表格中的表项数目有限而指令数目众多,因此很多不同的分支指令都会不可避免地指向同样的表项,这种问题称为别名重合(Aliasing)。

      • 一级预测器

        将有限个 2-bit Saturating Counter 组织成一维的表格,称为预测器表格(Predictor Table),并直接用 PC 值的一部分进行索引。

        索引机制过于简单,如低10位相同但高10位不同的 PC 对应的指令,指向的表项相同。

      • 两级预测器(相关预测器)

        将有限个 2-bit Saturating Counter 组织成 PHT (Pattern History Table),并使用该分支跳转的历史(Branch History)作为PHT的索引。

        • 局部分支预测器

          使用分立的局部历史缓存(Local Branch Buffer)来保存不同分支指令的分支历史。

        • 全局分支预测器

          所有分支指令共享全局历史缓存(Global History Buffer)。

          不同指令会互相冲击;优势是节省资源,PHT 容量越大,优势越明显。

          • 代表算法
            • Gshare:将 PC 一部分与全局历史缓存异或作为索引。
            • Gselect:将 PC 一部分与全局历史缓存拼接作为索引。
  • 预测地址

    预测跳转目标地址

    • BTB 分支目标缓存

      使用容量有限的缓存保存最近执行过的分支指令的 PC 值以及其跳转目标地址。

      后续需要取值的 PC 值与 BTB 中存储的各个 PC 值进行比较,若匹配则跳转至目标地址。

      • 缺点
        • 不能将 BTB 容量做到太大,否则面积和时序无法接受。
        • 对于间接跳转/分支指令的预测效果不理想,因为寄存器中的操作数可能更新,导致跳转地址变化。
    • RAS 返回地址堆栈

      使用容量有限的硬件堆栈来存储函数调用的返回地址。

      间接跳转/分支指令可以用于函数的调用和返回,而函数的调用和返回往往成对出现,因此可在函数调用时将当前 PC 值加4(或2),即其顺序执行的下一条指令的 PC 值压入 RAS 堆栈中,等到函数返回时将 RAS 中的值弹出,就可以快速的为该函数返回的分支跳转指令预测目标地址。

      RAS 深度有限,若出现多次函数嵌套,可能带着堆栈溢出,影响准确率,需硬件特殊处理。

    • Indirect BTB 间接分支目标缓存

      与普通 BTB 相似,但采用高级的索引方法进行匹配(而非简单的 PC 值比较)。

      硬件开销大,只有高级处理器会使用。

  • Branch_predictor

RISC-V 对于取值的简化

  • 规整的指令编码格式

  • 指令长度指示码放于低位

    方便取值逻辑在顺序取值的时候以最快的速度译码出指令的长度。

    若不支持压缩指令子集(16位),opcode 最低两位固定为 11

  • 简单的分支跳转指令

    jal 可用于子程序调用,并将子程序返回地址存在结果寄存器。

    jalr 可用于子程序返回指令,使用 jal 的结果寄存器可从子程序返回。

    bxx 将两个操作数进行比较,满足条件则跳转。

    • 无条件直接跳转/分支指令

      jal:jal rd, imm20

      PC = PC + SEXT[imm[20:1] << 1]

      R[rd] = PC + 4

    • 无条件间接跳转/分支指令

      jalr:jalr rd, rs1, imm12

      PC = R[rs1] + SEXT[imm12]

      R[rd] = PC + 4

    • 带条件直接跳转/分支指令

      PC = PC + SEXT[imm[12:1] << 1]

      beq:两个整数操作数相等则跳转。

      bne:两个整数不相等则跳转。

      blt:第一个有符号数小于第二个有符号数则跳转。

      bltu:第一个无符号数小于第二个无符号数则跳转。

      bge:第一个有符号数大于等于第二个有符号数则跳转。

      bgeu:第一个无符号数大于等于第二个无符号数则跳转。

  • 没有分支延迟槽指令

    早期 RISC 处理器没有高级的硬件动态分支预测器,分支延迟槽能取得可观的效果,但是也导致硬件设计复杂。

    现代高性能处理器的分支预测算法精度已经非常高,分支延迟槽效果不明显。且对于低功耗小面积的处理器可以使用简单的电路实现,无分支延迟槽能减少功耗和提高时序。

  • 提供明确的静态分支预测依据

    RISC-V 架构明确规定,编译器生成的代码应该尽量优化,使得向后跳转的分支指令比向前跳转的分支指令有更大的概率进行跳转。

    对于使用静态预测的低端处理器,可以提高静态预测的准确率。

  • 提供明确的 RAS (Return Address Stack) 依据

    如果使用 jal 指令,且目标寄存器索引值 rd 等于 x1 或者 x5,则属于需要进行 RAS 压栈。
    如果使用 jalr 指令,则按照使用的寄存器值 (rs1 和 rd)的不同,明确规定了相应的 RAS 压栈或者出栈行为:

蜂鸟 E200 的取值实现

  • 假定绝大多数取值都发生在 ITCM,不使用 I-Cache 。蜂鸟 E200 应用于低功耗和小面积场景,ITCM 满足实时性的要求;且这种级别处理器的代码量不大,往往可以全部加载在 ITCM 中。

    ITCM 使用单周期访问的 SRAM,一个周期就可以取一条指令。

    某些特殊情况下,指令需要从外部存储器中读取,此时需要通过 BIU 使用系统存储接口访问外部存储器,延时高,故软件设计应注意。

  • 连续不断

    Mini-Decode 将取回的指令进行部分译码。若译码的信息显示为分支跳转指令,则 Simple-BPU 进行分支预测,使用译码得出的信息和分支预测的信息进行下一条指令 PC 的生成。

    一个周期内完成众多步骤,则主频受到制约。但 RISC-V 架构简单,译码、分支预测消耗延时不算太大,且蜂鸟 E200 重在低功耗和小面积,可适当放弃主频。

Mini-Decode

  • 译码输入

    1
    input  [`E203_INSTR_SIZE-1:0] instr,  // instruction fetched
  • 译码输出

    1
    2
    3
    4
    5
    output dec_rv32,  // 16bit or 32bit
    output dec_bjp, // Common instruction or branch/jump instruction
    output dec_jal, // jal instruction
    output dec_jalr, // jalr instruction
    output dec_bxx, // bxx instruction
  • 例化调用完整的Decode模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Instantiate a Decode module, but set unrelated input signal to 0, hang unrelated output signal to air.
    e203_exu_decode u_e203_exu_decode(

    .i_pc(`E203_PC_SIZE'b0), // Set unrelated input signal to 0.
    .i_prdt_taken(1'b0),
    .i_muldiv_b2b(1'b0),

    .dec_misalgn(), // Hang unrelated output signal to air.
    .dec_buserr(),
    .dec_ilegl(),

    ...
    );

    只需要维护一份 Decode 模块的源代码,而非 Full-Decode 和 Mini-Decode 两份。

Simple-BPU

蜂鸟 E200 面向低功耗应用,只采用最简单的静态预测。

  • 带条件直接跳转指令 BXX

    • 预测方向:向后跳则预测为需要跳,向前跳则预测为不需要跳。

      1
      2
      // The JAL and JALR is always jump, bxxx backward is predicted as taken  
      assign prdt_taken = (dec_jal | dec_jalr | (dec_bxx & dec_bjp_imm[`E203_XLEN-1]));
    • 预测目标地址:PC = PC + SEXT[imm[12:1] << 1]

      1
      2
      3
      4
      5
      6
      7
      // Operand 1: If bxx, use it's PC.
      assign prdt_pc_add_op1 = (dec_bxx | dec_jal) ? pc[`E203_PC_SIZE-1:0]
      : (dec_jalr & dec_jalr_rs1x0) ? `E203_PC_SIZE'b0
      : (dec_jalr & dec_jalr_rs1x1) ? rf2bpu_x1[`E203_PC_SIZE-1:0]
      : rf2bpu_rs1[`E203_PC_SIZE-1:0];
      // Operand 2: Use offset.
      assign prdt_pc_add_op2 = dec_bjp_imm[`E203_PC_SIZE-1:0];
  • 无条件直接跳转指令 jal

    • 预测方向:一定会跳转。

      1
      2
      // The JAL and JALR is always jump, bxxx backward is predicted as taken  
      assign prdt_taken = (dec_jal | dec_jalr | (dec_bxx & dec_bjp_imm[`E203_XLEN-1]));
    • 预测目标地址:offset + PC

      1
      2
      3
      4
      5
      6
      7
      // Operand 1: If jal, use it's PC.
      assign prdt_pc_add_op1 = (dec_bxx | dec_jal) ? pc[`E203_PC_SIZE-1:0]
      : (dec_jalr & dec_jalr_rs1x0) ? `E203_PC_SIZE'b0
      : (dec_jalr & dec_jalr_rs1x1) ? rf2bpu_x1[`E203_PC_SIZE-1:0]
      : rf2bpu_rs1[`E203_PC_SIZE-1:0];
      // Operand 2: Use offset.
      assign prdt_pc_add_op2 = dec_bjp_imm[`E203_PC_SIZE-1:0];
  • 无条件间接跳转指令 jalr

    • 预测方向:一定会跳转。

      1
      2
      // The JAL and JALR is always jump, bxxx backward is predicted as taken  
      assign prdt_taken = (dec_jal | dec_jalr | (dec_bxx & dec_bjp_imm[`E203_XLEN-1]));
    • 预测目标地址:offset + 操作数

      操作数来自其 rs1 索引的 Regfile ,根据 rs1 索引值判断。

      • rs1 == x0

        操作数是0。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        // The JALR rs1 index: x0.
        wire dec_jalr_rs1x0 = (dec_jalr_rs1idx == `E203_RFIDX_WIDTH'd0);

        // Operand 1: If jalr with rs1 == x0, use 0.
        assign prdt_pc_add_op1 = (dec_bxx | dec_jal) ? pc[`E203_PC_SIZE-1:0]
        : (dec_jalr & dec_jalr_rs1x0) ? `E203_PC_SIZE'b0
        : (dec_jalr & dec_jalr_rs1x1) ? rf2bpu_x1[`E203_PC_SIZE-1:0]
        : rf2bpu_rs1[`E203_PC_SIZE-1:0];
        // Operand 2: Use offset.
        assign prdt_pc_add_op2 = dec_bjp_imm[`E203_PC_SIZE-1:0];
      • rs == x1

        操作数来自 x1。

        x1 常用于 link 寄存器作为函数返回跳转指令,故对其特别加速,将 x1 从 处于 EXU 中的 Regfile 中直接硬连线出。

        为了防止 RAW 相关性,需要判断 OITF 为空,以及当前 EXU指令没有写回 x1。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        // The JALR rs1 index: x1.
        wire dec_jalr_rs1x1 = (dec_jalr_rs1idx == `E203_RFIDX_WIDTH'd1);

        // OITF not empty, means a long instruction is running, maybe write back to x1.
        // Instruction in IR register will write back to x1.
        wire jalr_rs1x1_dep = dec_i_valid & dec_jalr & dec_jalr_rs1x1 & ((~oitf_empty) | (jalr_rs1idx_cam_irrdidx));

        // x1 RAW dependency resists, bpu_wait will prevent IFU from generating next PC until RAW removed.
        assign bpu_wait = jalr_rs1x1_dep | jalr_rs1xn_dep | rs1xn_rdrf_set;

        // Operand 1: If jalr with rs1 == x1, use x1 data directly from Regfile.
        assign prdt_pc_add_op1 = (dec_bxx | dec_jal) ? pc[`E203_PC_SIZE-1:0]
        : (dec_jalr & dec_jalr_rs1x0) ? `E203_PC_SIZE'b0
        : (dec_jalr & dec_jalr_rs1x1) ? rf2bpu_x1[`E203_PC_SIZE-1:0]
        : rf2bpu_rs1[`E203_PC_SIZE-1:0];
        // Operand 2: Use offset.
        assign prdt_pc_add_op2 = dec_bjp_imm[`E203_PC_SIZE-1:0];
      • rs == xn

        操作数来自 xn。

        xn 需要从 Regfile 的 Read Port 1 读出,需要判定是否空闲且不存在资源冲突。

        为了防止 RAW 相关性,需要判断当前 EXU 中无任何指令。

        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
        // The JALR rs1 index: xn.
        wire dec_jalr_rs1xn = (~dec_jalr_rs1x0) & (~dec_jalr_rs1x1);

        // OITF not empty, means a long instruction is running, maybe write back to xn.
        // Instruction in IR register maybe writes back to xn.
        wire jalr_rs1xn_dep = dec_i_valid & dec_jalr & dec_jalr_rs1xn & ((~oitf_empty) | (~ir_empty));

        // Judge if Read Port 1 has conflict.
        wire rs1xn_rdrf_r;
        wire rs1xn_rdrf_set = (~rs1xn_rdrf_r) & dec_i_valid & dec_jalr & dec_jalr_rs1xn & ((~jalr_rs1xn_dep) | jalr_rs1xn_dep_ir_clr);
        wire rs1xn_rdrf_clr = rs1xn_rdrf_r;
        wire rs1xn_rdrf_ena = rs1xn_rdrf_set | rs1xn_rdrf_clr;
        wire rs1xn_rdrf_nxt = rs1xn_rdrf_set | (~rs1xn_rdrf_clr);
        sirv_gnrl_dfflr #(1) rs1xn_rdrf_dfflrs(rs1xn_rdrf_ena, rs1xn_rdrf_nxt, rs1xn_rdrf_r, clk, rst_n);
        // Read Port 1 enable signal.
        assign bpu2rf_rs1_ena = rs1xn_rdrf_set;

        // xn RAW dependency resists or using Read Port 1 period, bpu_wait will prevent IFU from generating next PC until RAW removed or data from Regfile read.
        assign bpu_wait = jalr_rs1x1_dep | jalr_rs1xn_dep | rs1xn_rdrf_set;

        // Operand 1: If jalr with rs1 == xn, use xn data from Regfile's Read Port 1.
        assign prdt_pc_add_op1 = (dec_bxx | dec_jal) ? pc[`E203_PC_SIZE-1:0]
        : (dec_jalr & dec_jalr_rs1x0) ? `E203_PC_SIZE'b0
        : (dec_jalr & dec_jalr_rs1x1) ? rf2bpu_x1[`E203_PC_SIZE-1:0]
        : rf2bpu_rs1[`E203_PC_SIZE-1:0];
        // Operand 2: Use offset.
        assign prdt_pc_add_op2 = dec_bjp_imm[`E203_PC_SIZE-1:0];

PC

  • reset

    reset 后第一次取值使用 CPU_top 顶层输入信号 pc_rtvee,可在 SoC 顶层集成时指定。

  • 顺序取值

    根据指令长度判断:16位 -> PC + 2,32位 -> PC + 4

  • 分支指令

    使用 Simple-BPU 预测的 PC

  • EXU 流水线冲刷

    使用 EXU 送来的 PC

访问 ITCM 和 BIU

e203 支持 RVC,每次固定取值 32 位。

非对齐指令取值采用了剩余缓存技术 Leftover BUffer

  • 访问 ITCM
    • ITCM 由 SRAM 组成,上次访问 SRAM 后其输出值会一直保持 (Hold-up),直到下次读写,由此省略了 SRAM 输出处的 64 比特寄存器开销。
    • SRAM 宽度 64 位,IFU 每次只取 32 位。如果上次访问了 SRAM ,下一次取值不会真的 “读” SRAM,而是利用 Hold-up 特性得到指令,避免 SRAM 重复打开的动态功耗。
  • 顺序取值
    • 一个 32 位的指令越过了 64 位的 SRAM 边界,则将 SRAM 当前输出的最高 16 位存入 16 比特的剩余缓存 Leftover Buffer 中。并发起新一轮的 SRAM 读操作,将新取得的 SRAM 低 16 位与剩余缓存中的 16 位拼接成 32 位指令。
  • 非顺序取值
    • 一个 32 位的指令越过了 64 位的 SRAM 边界,则需要连续发起两次 ITCM 读操作。第一次读取 SRAM 输出的最高 16 位存入 16 比特的剩余缓存 Leftover Buffer 中,并发起新一轮的 SRAM 读操作,将新取得的 SRAM 低 16 位与剩余缓存中的 16 位拼接成 32 位指令。
    • 会造成一个周期的性能损失。没有设计多体 (bank) 化的 ITCM。