设计riscv cpu记录之一

发布时间:2026/6/28 4:51:56
设计riscv cpu记录之一 一、hold和flush谁的优先级高结论flush的优先级更高原因很简单flush的含义是“这条/这几条指令无效了必须冲掉”hold的含义是“先别往前走暂时停住”如果某一拍同时出现flush_i1和hold_i1通常说明控制逻辑要求这一级原来的内容不能再保留那就应该优先清空而不是继续保持旧指令所以应该执行flush。二、为什么不在id.v中设置一个reg_wdata_o?因为LUI指令的写回寄存器的数据就是inst_i的高20位。现在的做法是op1_o{inst_i[31:12],12b0};op2_oZeroWord;而不是reg_wdata_o{inst_i[31:12],12h0};这样做的理由是id.v的职责是“译码 准备操作数”不是“最终执行并写回结果”。ex.v的职责是“根据操作数和指令类型生成执行结果”。如果LUI也走统一通路那么 EX 级只要做一次“op1 op2或直通某路结果”的规则就能处理很多指令。那能不能新增reg_wdata_o可以语法和架构上都不是不行。但这样做意味着你要回答一个更大的问题reg_wdata_o是不是只给LUI用如果AUIPC也能在 ID 级算出来要不要也给它用JAL的PC4要不要也在 ID 级直接给CSR 指令要不要也部分在 ID 级给一旦开了这个口子你的写回数据来源会开始分散id.v的职责就会慢慢变重。三、为什么jal和jalr的立即数不一样JAL是Jump And Link作用是跳转到目标地址同时把返回地址保存到寄存器rd。它的行为可以概括成两步rd PC 4PC PC imm_jimm_j{{12{inst_i[31]}}, inst_i[19:12], inst_i[20], inst_i[30:21], 1b0}JALR是Jump And Link Register作用也是跳转 保存返回地址但它的跳转目标不是PC offset而是rs1 imm。它的行为是rd PC 4PC (rs1 imm_i){{20{inst_i[31]}}, inst_i[31:20]}为什么二者的立即数不一样它们俩虽然名字相近但是不是“同一种指令换个名字”而是两种不同设计目标的跳转指令。它们服务于两种不同的跳转方式JAL是PC 相对远跳转所以用更大的J-type立即数JALR是寄存器基址间接跳转所以用带rs1的I-type立即数。四、为什么LUI指令只有opcode就可以区分不用func3或者func7区分比如B型指令下有6 条具体指令一句话总结opcode 7b0110111可以直接认为就是LUIopcode 7b1100011只能说明它是B 型分支类至于具体是哪一条还要看funct3为什么这样设计呢本质原因是32 位指令的编码空间有限ISA 设计必须在“指令数量、译码复杂度、立即数字段、寄存器字段”之间做平衡。所以 RISC-V 才会设计成opcode先区分大类funct3/funct7再区分具体指令而不是“每条指令一个独立 opcode”。为什么不能每条指令都独占一个 opcode因为opcode只有 7 位理论上最多2^7 128种编码如果每条具体指令都占一个opcode会很快不够用为什么有的 opcode 只有一条主指令有的却是一组因为需求不同。LUI、AUIPC、JAL这类指令语义很独特单独给一个opcode很自然。B-type、Load、Store这类指令结构非常像只是“比较条件不同”或“访存宽度不同”所以共享同一个opcode更划算。五、在id.v中加不加jump_flag_o?因为jal jalr指令肯定是要跳转的tinyriscv中的id.v并没有设置jump_flag_o端口。需要明确的是如果跳转最终还是要等ex.v决定那么id.v里提前拉高jump_flag_o的意义不大只有当它能真正提前影响 PC 或流水线控制时才值得加。还是这样模块分工最清楚id准备比较数和跳转地址计算所需操作数ex真正执行比较、求目标地址、决定是否跳转