0%

取值

取值特点

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

快速取值

存储器延时越小越好。

但片外 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。

流水线

面积换性能

  • 流水线
    • 一个周期执行多条指令
    • 需要额外寄存器
    • 每级流水线内部有各自的组合逻辑数据通路,硬件资源无复用。
  • 状态机
    • 几个周期执行一条指令
    • 硬件资源可以复用

流水线深度

  • 流水线越深

    • 吞吐率高,性能高

    • 面积开销大

    • 反压

    • 分支预测失败/流水线冲刷(Pipeline Flush)

      取值阶段无法得知结果是否跳转,只能进行预测。

      若预测失败,需要将预取的错误指令流全部丢弃,重新取正确的指令流。

      流水线越深,则预取了更多的错误指令,性能的损失、功耗的浪费越严重。

  • 流水线越浅

    • 面积开销小,功耗低
    • 性能弱

反压

  • 取消握手
  • 乒乓缓存
  • 前向旁路缓存

流水线冲突

  • 资源冲突

    • 最常见的是运算单元的冲突

      例:除法运算需要多个周期,若前序除法指令完成运算之前,后序除法指令也需要除法器,则存在资源冲突

    • 解决方法:复制硬件资源、流水线停顿等

  • 数据冲突

    • WAR相关性(先读后写相关性)

      后序指令不能比和它有WAR相关性的前序指令先执行。

      否则后序指令先写回结果到通用寄存器组,导致前序指令读取错误的操作数。

    • WAW相关性(先写后写相关性)

      后序指令不能比和它有WAW相关性的前序指令先执行。

      否则后序指令先写回结果,导致前序指令再写回结果,导致结果被覆盖。

    • RAW相关性(先写后读相关性)

      后序指令不能比和它有RAW相关性的前序指令先执行。

      否则前序指令未写回结果,导致后序指令读取错误的操作数。

    • 解决方法

      • WAR、WAW

        寄存器重命名 (Register_renaming)

        1

      • RAW:真数据相关

        动态调度 (Tomasulo_algorithm)

        • 采用数据旁路传播,尽可能让前序指令的计算结果更快地旁路传播给后序指令的操作数。

        • 尽可能让后序指令在等待过程中不阻塞流水线,让其他无关的指令继续执行。

        • 在每个运算单元前配置乱序发射队列。

          发射队列仅追踪RAW相关性,并不存放操作上,因此可以做到很深。

          在发射队列中的指令一旦解除相关性,再从发射队列中发射出来读取物理寄存器组,然后再发送给运算单元开始计算。

蜂鸟 E200

使用标准 DFF 模块例化生成寄存器

1
2
3
4
5
// 例化
wire flg_r;
wire fkg_nxt = ~flg_r;
wire flg_ena = (ptr_r == ('E203_OITF_DEPTH-q') & ptr_ena)
sirv_gnrl_fddlr #(1) flg_dfflrs(flg_ena, flg_nxt, flg_r, clk, rst_n);
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// DFF
// DFF with Load-enable and Reset, Default reset value is 0
module sirv_gnrl_dfflr # (
parameter DW = 32
) (

input lden,
input [DW-1:0] dnxt,
output [DW-1:0] qout,

input clk,
input rst_n
);

reg [DW-1:0] qout_r;

// 使用 always 块编写寄存器逻辑
always @(posedge clk or negedge rst_n)
begin : DFFLR_PROC
if (rst_n == 1'b0)
qout_r <= {DW{1'b0}};
else if (lden == 1'b1)
qout_r <= #1 dnxt;
end

assign qout = qout_r;

// 使用 assertion 捕捉 lden 信号的不定态
`ifndef FPGA_SOURCE//{
`ifndef DISABLE_SV_ASSERTION//{
//synopsys translate_off
sirv_gnrl_xchecker # (
.DW(1)
) sirv_gnrl_xchecker(
.i_dat(lden),
.clk (clk)
);
//synopsys translate_on
`endif//}
`endif//}

endmodule
  • 优点

    • 便于全局替换寄存器类型。

      例:带有 reset 的寄存器面积和时序会差一些,因此在数据通路上可以使用不带 reset 的寄存器,而只在控制通路上使用。

    • 便于在寄存器中全局插入延迟。

    • 明确的 load-enable 使能信号,方便综合工具自动插入寄存器级别的门控时钟,以降低动态功耗。

    • 便于规避 if-else 不能传播不定态的问题。

使用 assign 语法代替 if-else 和 case 语法

  • if-else 和 case 不能传播不定态

    • if-else 和case

      1
      2
      3
      4
      if(a)
      out = in1;
      else
      out = in2;

      若a为不定态,会等效于0,输出等于in2,没有将不定态传播出去。

      可能会在仿真阶段(综合结果应该是一样的)掩盖某些致命的错误,造成芯片功能错误。

    • assign

      1
      assign out = a ? in1:in2;

      out = x,能够将不定态传播出去。

  • if-else 和 case 会产生优先级的选择电路(级联)而非并行选择电路,不利于时序和面积。

    • if-else 和case

      1
      2
      3
      4
      5
      6
      7
      8
      if(sel1)
      out = in1;
      else if (sel2)
      out = in2;
      else if (sel3)
      out = in3;
      else
      out = 4'b0;

      会被综合成优先级选择电路,面积和时序均不够优化。

    case?

    • assign

      优先级选择:规避不定态 X 传播的问题。

      1
      2
      3
      4
      assign out = sel1 ? in1[3:0] :
      sel2 ? in2[3:0] :
      sel3 ? in3[3:0] :
      4'b0;

      并行选择:

      1
      2
      3
      assign out = ({4{sel1}} & in1[3:0])
      |({4{sel2}} & in2[3:0])
      |({4{sel3}} & in3[3:0]);

数据通路和控制通路的寄存器

带有 reset 的寄存器面积和时序会差一些,因此在数据通路上可以使用不带 reset 的寄存器,而只在控制通路上使用。

Clock 和 Reset 信号

Clock 和 Reset 信号只能接入 DFF 作为其时钟和复位信号,不能用于其他逻辑功能。

si [N]

1
{ "si", "Step in", cmd_si}
1
2
3
4
5
6
7
8
9
static int cmd_si(char *args){
if(args != NULL){
cpu_exec(atoi(args));
}else{
cpu_exec(1);
}

return 0;
}

info

1
{ "info", "Get information of regs or watchpoints", cmd_info}
1
2
3
4
5
6
7
8
9
static int cmd_info(char *args){
if(*args == 'r'){
isa_reg_display();
}else if(*args == 'w'){

}

return 0;
}

x [N] [EXPR]

1
{ "x", "Scan the memory", cmd_x}

规定 EXPR 只能是十六进制数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int cmd_x(char *args){
if(args != NULL){
// get the string of instruction number and expression
char *inst_num_in = strtok(args, " ");
char *expr_in = args + strlen(args) + 1;
// get the start address of scanning
paddr_t addr_start_scan;
sscanf(expr_in,"%x",&addr_start_scan);
// output
printf("inst_num_in: %d, addr_start: 0x%08x\n", atoi(inst_num_in), addr_start_scan);
for(u_char i = 0; i <= (atoi(inst_num_in)-1); i++){
printf("%08lx\n", paddr_read(addr_start_scan + i*4, 4));
}
}

return 0;
}

EXPR 正则表达式

1

RVK

  • K

    • Zkn (NIST)

      • Zkg

      • Zkb (Bit manipulation)

      • Zkne (AES Encryption)

      • Zknd (AES Decryption)

      • Zknh (SHA2)

    • Zkr (Entropy Source)

  • Zks (SM)

    • Zkg
    • Zkb
    • Zksed
    • Zksh

parse_args(int argc, char *argv[])

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 解析命令行选项参数
int getopt_long_only(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
// {const char *name; int has_arg; int *flag; int val}
// 匹配时:flag = NULL,返回 val ;flag != NULL ,返回 0
static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{0 , 0 , NULL, 0 },
};
int o;
// 依次检测是否有参数:-b -h -l xxx -d xxx -p xxx --batch --log xxx ...
// 若有则返回参数字符,若检测到 opstring 未定义的参数返回 ? ,检测完毕返回-1
while ( (o = getopt_long(argc, argv, "-bhl:d:p:", table, NULL)) != -1) {
switch (o) {
// batch mode
case 'b': sdb_set_batch_mode(); break;
// difftest
case 'p': sscanf(optarg, "%d", &difftest_port); break;
// log
case 'l': log_file = optarg; break;
// difftest
case 'd': diff_so_file = optarg; break;
// 存疑
case 1: img_file = optarg; return 0;
// 返回 ? :未定义参数
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\n");
exit(0);
}
}
return 0;
}

init_rand()

1
2
3
4
// 依据时间 time(0) 设置随机数种子 seed 以供 rand() 使用
void init_rand() {
srand(MUXDEF(CONFIG_TARGET_AM, 0, time(0)));
}

init_log(const char *log_file)

1
2
3
4
5
6
7
8
9
void init_log(const char *log_file) {
log_fp = stdout;
if (log_file != NULL) {
FILE *fp = fopen(log_file, "w");
Assert(fp, "Can not open '%s'", log_file);
log_fp = fp;
}
Log("Log is written to %s", log_file ? log_file : "stdout");
}

init_mem()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用随机数初始化内存
void init_mem() {
#if defined(CONFIG_PMEM_MALLOC)
pmem = malloc(CONFIGMSIZE);
assert(pmem);
#endif
#ifdef CONFIG_MEM_RANDOM
uint32_t *p = (uint32_t *)pmem;
int i;
for (i = 0; i < (int) (CONFIG_MSIZE / sizeof(p[0])); i ++) {
p[i] = rand();
}
#endif
Log("physical memory area [" FMT_PADDR ", " FMT_PADDR "]", PMEM_LEFT, PMEM_RIGHT);
}

IFDEF(CONFIG_DEVICE, init_device())

1

init_isa()

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
void init_isa() {
/* Load built-in image. */
memcpy(guest_to_host(RESET_VECTOR), img, sizeof(img));

/* Initialize this virtual computer system. */
restart();
}
// physical memory 128MB
static uint8_t pmem[CONFIG_MSIZE] PG_ALIGN = {};
uint8_t* guest_to_host(paddr_t paddr) { return pmem + paddr - CONFIG_MBASE; }
static const uint32_t img [] = {
0x00000297, // auipc t0,0
0x0002b823, // sd zero,16(t0)
0x0102b503, // ld a0,16(t0)
0x00100073, // ebreak (used as nemu_trap)
0xdeadbeef, // some data
};

static void restart() {
/* Set the initial program counter. */
cpu.pc = RESET_VECTOR;

/* The zero register is always 0. */
cpu.gpr[0] = 0;
}

test

1
2
3
4
5
6
7
8
9
10
11
12
13
#define __IGNORE(...)			// abandon the arguments
#define __KEEP(...) __VA_ARGS__ // transmit the arguments
#define IFNDEF(macro, ...) MUXNDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// transmit or abandon the arguments depending on whether the macro is defined
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define MUXNDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, Y, X)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
// if macro is defined, transmit "__P_DEF_0" or "__P_DEF_1" as "X, "
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define CHOOSE2nd(a, b, ...) b
// then it's CHOOSE2nd(X, a, b) => a => __IGNORE
// otherwise it's CHOOSE2nd(contain_comma a, b) => b => __KEEP

1
2
3
make -j8
make -B
make -n
1
2
3
make -nB \
| grep -ve '^\(\g\|echo\|mkdir\)' \ # 删去不要的东西
| vim - # 输出用 vim 打开
1
set nowrap
1
2
3
4
5
6
7
8
tree .
find . -name "*.c" -o -name "*.h"
vi .
vi **/gen-expr.c
fzf
vi $(fzf)
echo 'q' | /home/chms/ysyx-workbench/nemu/build/riscv64-nemu-interpreter
/home/chms/ysyx-workbench/nemu/build/riscv64-nemu-interpreter < in.txt
1
static inline
1
2
tjump
tabnew
1
2
3
gdb
layout asm
layout src
1
2
3
4
5
6
7
宏展开
gcc -E
makefile ->
# @$(CC) $(CFLAGS) -E $@ $< | \
# grep -ve '^#' | \
# clang-format - > $(basename $@).i
# rm ./*.d
1
2
3
4
5
6
gcc --verbose
gcc -v
gcc -fsanitize=address // 内存访问越界
strace bash
ssh -v
make -n

1
2
(0,x,x)>(1,0,x)>(2,0,0)>(3,0,1)>(4,1,1)>(2,1,1)>(3,1,2)>(4,3,2)>(2,3,2)>(3,3,3)>(4,6,3)>(2,6,3)...
(2,4851,98)>(3,4851,99)>(4,4950,99)>(2,4950,99)>(3,4950,100)>(4,5050,100)>(5,5050,100)>(5,5050,100)...