一、为什么hold信号不是1bit的Hold_Flag_Bus被定义为2:0并给出 4 个有序状态Hold_None3b000、Hold_Pc3b001、Hold_If3b010、Hold_Id3b011。各级流水寄存器不是判断1而是做阈值比较。二、为什么gen_pipe_dff的hold拉高输出为什么是 def_val而不是dinmodule gen_pipe_dff #(parameter DW32)(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)beginif(!rst|hold_en)begin qout_rdef_val;endelsebegin qout_rdin;end end assign qoutqout_r;endmodulehold_en 并不是传统意义上的保持当前值而是插入气泡——当流水线某级需要暂停时向后级传递一个预定义的默认值通常是 NOP 或无效标记而不是让上游的 din 继续往下传。三、直接把 output reg [DW-1:0]qout 还是 设置个reg中间变量再assign好 module gen_pipe_dff #(parameter DW32)(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)beginif(!rst||hold_en)qoutdef_val;elseqoutdin;end endmodule 写法二module gen_pipe_dff #(parameter DW32)(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)beginif(!rst|hold_en)begin qout_rdef_val;endelsebegin qout_rdin;end end assign qoutqout_r;endmodule结论写法二更好原则时序逻辑只产生内部 reg不直接驱动 output port。四、stall、flush、bubble的通俗理解stall把真人按下暂停键演员还在舞台上之后继续演。flush把这个演员请下舞台他后面不演了。bubble舞台上空出一个位置用一个“没人演的空拍”补上。五、为什么同样是hold拉高pc_o是stall逻辑而if_id.v中是冲刷逻辑部分代码endelseif(hold_flag_iHold_Pc)begin pc_opc_o;部分代码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 - 10];区别是什么reg[RegBus] regs[0:31];是 32 个 32 位寄存器编号从 0 到 31reg[RegBus] regs[31:0];也是 32 个 32 位寄存器还是编号 0 到 31只是声明时写成降序范围结论功能上等价只是reg[RegBus] regs[0:RegNum-1];往往更符合“这是一个寄存器数组”的阅读习惯。七、寄存器为什么写逻辑需要考虑到rst而读逻辑不需要考虑rst// 写寄存器always (posedge clk)beginif(rstRstDisable)begin// 优先ex模块写操作if((we_iWriteEnable)(waddr_i!ZeroReg))begin regs[waddr_i]wdata_i;endelseif((jtag_we_iWriteEnable)(jtag_addr_i!ZeroReg))begin regs[jtag_addr_i]jtag_data_i;end end end// 读寄存器1always (*)beginif(raddr1_iZeroReg)begin rdata1_oZeroWord;// 如果读地址等于写地址并且正在写操作则直接返回写数据endelseif(raddr1_iwaddr_iwe_iWriteEnable)begin rdata1_owdata_i;endelsebegin rdata1_oregs[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每次要用这三个值时都得先给出 CSR 地址等组合读结果出来再拿这个结果参与控制。这样就麻烦了。不如给mtvec/mepc/mstatus开辟直接的通路九、为什么regs.v中写逻辑用时序打一拍而读要用组合逻辑这样会造成不同步吗这是典型的“同步写、异步读”寄存器堆是处理器寄存器堆最常见的实现方式之一。如果读做成时序逻辑地址先打一拍数据再下一拍出来整个流水线会被迫变长所以读得用组合逻辑如果写用组合逻辑通常会出大问题核心原因是寄存器堆就不再是“寄存器堆”了而更像一坨会随输入即时变化的纯组合网络。会发生只要写使能、写地址、写数据一变存储内容就可能立刻跟着变没有明确的“在时钟沿提交状态”这个边界十、ex.v和id.v里边都有 指令切片是从id.v传到ex.v好还是在ex.v再分解一次好两种做法区别是什么其实差别很小因为assign opcode inst_i[6:0]本质上只是连线切片不是复杂逻辑LUT消耗的也没有区别。十一、写完id.v有感设计cpu,也没有那么复杂定好每个模块的端口规定好做什么事就可以。比如id.v,先想谁会和他发生联系上一级是if_id.v,下一级是id_ex.v,还要读写寄存器regs.v和csr_reg.v。然后再看干什麽事确定输入输出端口。定好端口之后就是顺着逻辑写。
tinyriscv学习记录
一、为什么hold信号不是1bit的Hold_Flag_Bus被定义为2:0并给出 4 个有序状态Hold_None3b000、Hold_Pc3b001、Hold_If3b010、Hold_Id3b011。各级流水寄存器不是判断1而是做阈值比较。二、为什么gen_pipe_dff的hold拉高输出为什么是 def_val而不是dinmodule gen_pipe_dff #(parameter DW32)(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)beginif(!rst|hold_en)begin qout_rdef_val;endelsebegin qout_rdin;end end assign qoutqout_r;endmodulehold_en 并不是传统意义上的保持当前值而是插入气泡——当流水线某级需要暂停时向后级传递一个预定义的默认值通常是 NOP 或无效标记而不是让上游的 din 继续往下传。三、直接把 output reg [DW-1:0]qout 还是 设置个reg中间变量再assign好 module gen_pipe_dff #(parameter DW32)(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)beginif(!rst||hold_en)qoutdef_val;elseqoutdin;end endmodule 写法二module gen_pipe_dff #(parameter DW32)(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)beginif(!rst|hold_en)begin qout_rdef_val;endelsebegin qout_rdin;end end assign qoutqout_r;endmodule结论写法二更好原则时序逻辑只产生内部 reg不直接驱动 output port。四、stall、flush、bubble的通俗理解stall把真人按下暂停键演员还在舞台上之后继续演。flush把这个演员请下舞台他后面不演了。bubble舞台上空出一个位置用一个“没人演的空拍”补上。五、为什么同样是hold拉高pc_o是stall逻辑而if_id.v中是冲刷逻辑部分代码endelseif(hold_flag_iHold_Pc)begin pc_opc_o;部分代码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 - 10];区别是什么reg[RegBus] regs[0:31];是 32 个 32 位寄存器编号从 0 到 31reg[RegBus] regs[31:0];也是 32 个 32 位寄存器还是编号 0 到 31只是声明时写成降序范围结论功能上等价只是reg[RegBus] regs[0:RegNum-1];往往更符合“这是一个寄存器数组”的阅读习惯。七、寄存器为什么写逻辑需要考虑到rst而读逻辑不需要考虑rst// 写寄存器always (posedge clk)beginif(rstRstDisable)begin// 优先ex模块写操作if((we_iWriteEnable)(waddr_i!ZeroReg))begin regs[waddr_i]wdata_i;endelseif((jtag_we_iWriteEnable)(jtag_addr_i!ZeroReg))begin regs[jtag_addr_i]jtag_data_i;end end end// 读寄存器1always (*)beginif(raddr1_iZeroReg)begin rdata1_oZeroWord;// 如果读地址等于写地址并且正在写操作则直接返回写数据endelseif(raddr1_iwaddr_iwe_iWriteEnable)begin rdata1_owdata_i;endelsebegin rdata1_oregs[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每次要用这三个值时都得先给出 CSR 地址等组合读结果出来再拿这个结果参与控制。这样就麻烦了。不如给mtvec/mepc/mstatus开辟直接的通路九、为什么regs.v中写逻辑用时序打一拍而读要用组合逻辑这样会造成不同步吗这是典型的“同步写、异步读”寄存器堆是处理器寄存器堆最常见的实现方式之一。如果读做成时序逻辑地址先打一拍数据再下一拍出来整个流水线会被迫变长所以读得用组合逻辑如果写用组合逻辑通常会出大问题核心原因是寄存器堆就不再是“寄存器堆”了而更像一坨会随输入即时变化的纯组合网络。会发生只要写使能、写地址、写数据一变存储内容就可能立刻跟着变没有明确的“在时钟沿提交状态”这个边界十、ex.v和id.v里边都有 指令切片是从id.v传到ex.v好还是在ex.v再分解一次好两种做法区别是什么其实差别很小因为assign opcode inst_i[6:0]本质上只是连线切片不是复杂逻辑LUT消耗的也没有区别。十一、写完id.v有感设计cpu,也没有那么复杂定好每个模块的端口规定好做什么事就可以。比如id.v,先想谁会和他发生联系上一级是if_id.v,下一级是id_ex.v,还要读写寄存器regs.v和csr_reg.v。然后再看干什麽事确定输入输出端口。定好端口之后就是顺着逻辑写。