tinyriscv学习记录

一、为什么hold信号不是1bit的?

Hold_Flag_Bus 被定义为 2:0,并给出 4 个有序状态:Hold_None=3'b000Hold_Pc=3'b001Hold_If=3'b010Hold_Id=3'b011。各级流水寄存器不是判断 ==1,而是做阈值比较。

二、为什么gen_pipe_dff的hold拉高,输出为什么是 def_val?而不是din?

c 复制代码
module gen_pipe_dff #(
    parameter DW = 32)(

    input wire clk,
    input wire rst,
    input wire hold_en,

    input wire[DW-1:0] def_val,
    input wire[DW-1:0] din,
    output wire[DW-1:0] qout

    );

    reg[DW-1:0] qout_r;

    always @ (posedge clk) begin
        if (!rst | hold_en) begin
            qout_r <= def_val;
        end else begin
            qout_r <= din;
        end
    end

    assign qout = qout_r;

endmodule

hold_en 并不是传统意义上的"保持当前值",而是"插入气泡"------当流水线某级需要暂停时,向后级传递一个预定义的默认值(通常是 NOP 或无效标记),而不是让上游的 din 继续往下传。

三、直接把 output reg DW-1:0qout 还是 设置个reg中间变量再assign好 ?

c 复制代码
module gen_pipe_dff #(
    parameter DW =32
)(
   input  wire clk             ,
   input  wire rst             ,
   input  wire hold_en         ,
   input  wire [DW-1:0]din             ,
   output reg  [DW-1:0]qout            ,
   input  wire [DW-1:0]def_val         
    

    );
    always@(posedge clk)begin
        if(!rst || hold_en)
            qout <= def_val;
        else
            qout <= din;
    end
    
endmodule


写法二:module gen_pipe_dff #(
    parameter DW = 32)(

    input wire clk,
    input wire rst,
    input wire hold_en,

    input wire[DW-1:0] def_val,
    input wire[DW-1:0] din,
    output wire[DW-1:0] qout

    );

    reg[DW-1:0] qout_r;

    always @ (posedge clk) begin
        if (!rst | hold_en) begin
            qout_r <= def_val;
        end else begin
            qout_r <= din;
        end
    end

    assign qout = qout_r;

endmodule

结论:写法二更好,原则:时序逻辑只产生内部 reg,不直接驱动 output port。

四、stall、flush、bubble的通俗理解

stall:把真人按下暂停键,演员还在舞台上,之后继续演。

flush:把这个演员请下舞台,他后面不演了。

bubble:舞台上空出一个位置,用一个"没人演的空拍"补上。

五、为什么同样是hold拉高,pc_o是stall逻辑,而if_id.v中是冲刷逻辑?

部分代码:

c 复制代码
           end  else if(hold_flag_i >= `Hold_Pc  ) begin
                    pc_o <= pc_o;

部分代码:

c 复制代码
gen_pipe_dff #(32) inst_ff(clk,rst,hold_en,inst_i,inst,`INST_NOP);

原因:

  • pc_reg.v 里的 pc_o 是"下一次要去哪里取指"的状态。
  • if_id.v 里的内容是"已经取到、正准备往后执行"的那条指令。

所以 hold 拉高时,设计目标也不同:

  • PC:目标是 别再往前取新东西 ,所以最自然就是 pc_o <= pc_o,把取指位置停住。这表示"先别动,之后还从这里继续取"。这个状态本身是合法的、将来还要用,所以保留。
  • IF/ID:目标是 别让当前这条已取到的指令继续往后推进 。因为触发 Hold_If/Hold_Id 的典型原因是跳转、异常、中断、执行级多周期暂停等,此时 IF/ID 中那条指令很可能已经是错路径指令,或者至少这拍不该再作为有效指令进入后级。于是最安全的做法就是把它改成 NOP/0/INT_NONE,直接作废。

六、reg`RegBus] regs[0:`RegNum - 1;和reg`RegBus] regs[`RegNum - 1:0;区别是什么

  • reg[RegBus] regs[0:31]; 是 32 个 32 位寄存器,编号从 0 到 31

  • reg[RegBus] regs[31:0]; 也是 32 个 32 位寄存器,还是编号 0 到 31,只是声明时写成降序范围

结论:功能上等价,只是reg[RegBus] regs[0:RegNum-1]; 往往更符合"这是一个寄存器数组"的阅读习惯。

七、寄存器为什么写逻辑需要考虑到rst,而读逻辑不需要考虑rst?

c 复制代码
 
    // 写寄存器
    always @ (posedge clk) begin
        if (rst == `RstDisable) begin
            // 优先ex模块写操作
            if ((we_i == `WriteEnable) && (waddr_i != `ZeroReg)) begin
                regs[waddr_i] <= wdata_i;
            end else if ((jtag_we_i == `WriteEnable) && (jtag_addr_i != `ZeroReg)) begin
                regs[jtag_addr_i] <= jtag_data_i;
            end
        end
    end

    // 读寄存器1
    always @ (*) begin
        if (raddr1_i == `ZeroReg) begin
            rdata1_o = `ZeroWord;
        // 如果读地址等于写地址,并且正在写操作,则直接返回写数据
        end else if (raddr1_i == waddr_i && we_i == `WriteEnable) begin
            rdata1_o = wdata_i;
        end else begin
            rdata1_o = regs[raddr1_i];
        end
    end

八、csr_reg.v中clint_rdata_o不就是读出csr某个寄存器的值吗?为什么还要有 clint_csr_mtvec、 clint_csr_mepc 、clint_csr_mstatus 这三个端口?

clint 在处理中断/异常时, mtvec/mepc/mstatus直接参与控制决策,如果只保留 clint_rdata_o 这种"按地址读"的通用接口,那么 clint 每次要用这三个值时,都得先:

  1. 给出 CSR 地址
  2. 等组合读结果出来
  3. 再拿这个结果参与控制。
    这样就麻烦了。不如给mtvec/mepc/mstatus开辟直接的通路

九、为什么regs.v中写逻辑用时序打一拍,而读要用组合逻辑?这样会造成不同步吗?

这是典型的"同步写、异步读"寄存器堆,是处理器寄存器堆最常见的实现方式之一。

如果读做成时序逻辑:

  • 地址先打一拍
  • 数据再下一拍出来
  • 整个流水线会被迫变长
    所以,读得用组合逻辑

如果写用组合逻辑,通常会出大问题,核心原因是:寄存器堆就不再是"寄存器堆"了,而更像一坨会随输入即时变化的纯组合网络

会发生:

  • 只要写使能、写地址、写数据一变,存储内容就可能立刻跟着变
  • 没有明确的"在时钟沿提交状态"这个边界

十、ex.v和id.v里边都有 指令切片,是从id.v传到ex.v好还是在ex.v再分解一次好?两种做法区别是什么?

其实差别很小,因为assign opcode = inst_i6:0,本质上只是连线切片,不是复杂逻辑,LUT消耗的也没有区别。

十一、写完id.v有感

设计cpu,也没有那么复杂,定好每个模块的端口,规定好做什么事就可以。比如id.v,先想谁会和他发生联系,上一级是if_id.v,下一级是id_ex.v,还要读写寄存器regs.v和csr_reg.v。然后再看干什麽事确定输入输出端口。定好端口之后就是顺着逻辑写。