拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Verilog的五级流水线Cache与中断联合设计实战指南

简介基于Verilog带cache和中断的五级流水通用处理器的设计与实现是一份面向计算机体系结构课程、数字电路设计入门及处理器设计课题研究的参考资料。文档系统介绍五级流水CPU的取指、译码、执行、访存、写回架构设计了16位指令集覆盖算数与逻辑指令、跳转指令、多重子程序调用指令和中断处理程序能够帮助读者建立处理器开发的整体认识。在具体实现方面文档详述了子程序调用、异常处理、数据高速缓存、结构相关处理和控制相关处理等模块的Verilog编写方法并通过旁路技术等策略解决数据相关、控制相关与结构相关问题展现出流水线设计中的关键优化手段。同时基于硬件模拟软件完成处理器仿真测试基本运算、存储访问、跳转及中断响应均可正常运行为课程设计或毕业设计提供完整参考。资源包仅含1个PDF文件大小961KB已有160人学习适合需要处理器设计和可综合代码样例的读者系统学习尤其适合作为计算机组成原理课程的课外拓展材料。1. 为什么是五级流水 Cache 中断把 cache 直接挂上五级流水线结构冒险立刻出现一条 load 指令在 MEM 段访存同周期 IF 段可能还要取指再加上中断请求指令生命周期里又多了“被中断”这条异常路径。很多课程作业只写到三级流水因为五级流水真正麻烦的限制路径不是 ALU 运算而是 Cache 缺失和中断现场保存。这篇博客适合用 Verilog 写过流水线但没打通命中率与中断联合调试的人也适合 IC 秋招前系统整理处理器取指与访存链路的人。2. 五级流水数据通路与冒险处理的Verilog落法2.1 流水级边界哪级寄存器决定性能五级流水把指令生命周期切成取指IF、译码ID、执行EX、访存MEM、写回WB。每一级之间必须有流水寄存器常见命名是 if_id、id_ex、ex_mem、mem_wb。除了数据信号还把这段时间内需要的控制信号一起锁存避免后面看到的是当前周期入口处的状态。比“哪些寄存器”更关键的其实是使能与清零。使能来自流水线停顿清零来自 flush 和 bubble。一个典型的 id_ex 寄存器如下必须支持三种动作正常工作、冻结、清除。always (posedge clk or negedge rst_n) begin if (!rst_n) begin id_ex_valid 1b0; id_ex_rd 5d0; end else if (stall_idex) begin id_ex_valid 1b0; // 插入气泡控制信号清掉 end else if (flush_idex) begin id_ex_valid 1b0; // 冲刷误取指令 end else begin id_ex_valid if_id_valid; // 正常传递 id_ex_rd if_id_rd; end end这里stall_idex和flush_idex必须互斥否则逻辑容易陷入到底该填 0 还是填原值的混乱。stall 针对 load-useflush 针对分支和中断。id_ex_valid是每个流水寄存器都该有的有效位很多调试问题最后都回到 valid 传播断了而不是数据位传错。2.2 数据冒险转发单元与Load-Use停顿数据冒险的经典判别标准是同一个寄存器写入还没提交新的读就来了。为避免全停顿几乎无条件先在 EX/MEM 和 MEM/WB 这两个源上加转发。下面的转发单元把更靠近当前执行的源优先。module fwd_unit( input wire [4:0] ex_rs1, ex_rs2, input wire [4:0] mem_rd, wb_rd, input wire mem_regwrite, wb_regwrite, output reg [1:0] fwd_a, fwd_b ); wire fa_mem mem_regwrite (mem_rd ! 5d0) (mem_rd ex_rs1); wire fa_wb wb_regwrite (wb_rd ! 5d0) (wb_rd ex_rs1) !fa_mem; wire fb_mem mem_regwrite (mem_rd ! 5d0) (mem_rd ex_rs2); wire fb_wb wb_regwrite (wb_rd ! 5d0) (wb_rd ex_rs2) !fb_mem; always (*) begin if (fa_mem) fwd_a 2b10; // 来自EX/MEM流水寄存器 else if (fa_wb) fwd_a 2b01; // 来自MEM/WB流水寄存器 else fwd_a 2b00; end always (*) begin if (fb_mem) fwd_b 2b10; else if (fb_wb) fwd_b 2b01; else fwd_b 2b00; end endmodule注意2b10从 EX/MEM 结果寄存器取2b01从 MEM/WB 取。因为 EX/MEM 阶段的结果比 MEM/WB 阶段更新所以当两者同时指向同一寄存器时EX/MEM 源需要被优先判断。转发只解决 ALU 结果和 load 结果已经进入流水线但还没写寄存器堆的情况解决不了 load 结果还没从数据 Cache 回来时下一拍就要用的情况。Load-Use 是唯一需要硬件停顿的情况一条 load 在 EX 阶段算出地址下一拍地址在 MEM 访存但下一条指令在 ID 阶段已经需要立即读数。转发来不及必须把 PC 和 IF/ID 冻结一拍。判定条件一般写为wire load_use_stall id_ex_memread (id_ex_rd ! 5d0) ((id_ex_rd if_id_rs1) || (id_ex_rd if_id_rs2)); assign stall_pc load_use_stall; assign stall_ifid load_use_stall; assign flush_idex load_use_stall;这里把 ID/EX 中 load 的写寄存器号与当前 IF/ID 译码出的所有源寄存器做比较。注意id_ex_memread必须来自 id_ex 流水寄存器而不是 EX 阶段的原始控制信号否则会晚一拍。控制信号与数据信号的同步是五级流水调试中最常犯的错误。2.3 控制冒险分支判断的所在级决定flush成本控制冒险的解法非常多从静态预测不跳、延迟槽到动态分支历史表。在通用处理器和可综合教学实现里最简单且靠谱的是“预测不跳转”。这意味着装入分支则默认顺序取下一 PC到 ID 级发现跳转再纠正。如果把分支判断放在 ID 级flush 只需要清掉 IF/ID 中这一条已经误取的指令重新取跳转目标代价是 1 拍。如果放在 EX 级代价变成 2 拍因为 ID 和 IF 都取了错指令。常见的通用处理器会把比较器搬到 ID 级代价是组合路径变长但收益很直接。冒险类型判断条件采用的解决方式流水线代价数据冒险ex/mem 或 mem/wb 的 rd 与 ex 的 rs 相等转发通常 0 拍数据冒险load 的目标与后续指令源相等load-use stall1 拍控制冒险分支目标地址与跳转条件在 ID 级得出flush IF/ID 并重定向 PC1 拍结构冒险指令/数据同时访问同一存储器指令/数据 Cache 分离无 stall到这里必须说明一个边界如果后续接了 Cache前面“访存每拍都完成”的假设失效。Cache 命中时流水线保持每周期推进miss 时会拉低 Cache ready 信号把前面所有阶段的 valid 都冻住。这是下一章的内容。3. 给五级流水挂Cache映射、写策略与访存状态机3.1 Cache地址切分与映射方式选型带 Cache 的处理器首先要解决两个问题Cache 多大、替换策略怎么样。经典地址组织把 32 位地址切成 Tag、Index、Offset 三段。如果 16KB Cache、32 字节 Cache 行、4 路组相联则 Offset 占 5 位Index 占 log2(1024/4)8 位Tag 占剩下的 19 位。使用组相联而不是直接映射是因为直接映射在访问序列里同一 Index 不同 Tag 时冲突太明显。映射方式标签比较次数硬件成本冲突行为直接映射1 次最低同 index 不同 tag 直接替换组相联组内 N 路并行比较中等要 N 路 tag SRAM冲突比直接映射小命中率更高全相联全 Cache 行比较最高对标准 SRAM 不友好基本无冲突但时序和成本难接受选择组相联时建议从 2 路或 4 路起步不要追求 8 路。FPGA 上 tag 比较使用组合比较器路数翻倍比较器面积和关键路径都会增长。替换策略用 LRU 还是伪 LRU对小路数用计数器实现的 PLRU 就够几乎不亏命中率而且电路简单。3.2 写直达与写回脏位带来的访存状态数据 Cache 写策略要考虑和主存的一致性。写直达好处是永远不会出现“内存里数据是旧的”但每条 store 都要等总线流水线性能损失明显。写回只在脏行被替换时写。在实际项目中写回几乎是必选因为它能过滤高频写地址只把替换出的整行推给主存。写回要求 Cache 行带 dirty 标志。从 IDLE 到 REFILL 到 WRITEBACK 的状态转移因此出现。一个最小但完整的四状态 Cache 状态机会很干净localparam IDLE 2d0; localparam REFILL 2d1; localparam WRITEBACK 2d2; localparam WAIT_WB 2d3; reg [1:0] dc_state; reg dirty_flag; always (posedge clk or negedge rst_n) begin if (!rst_n) begin dc_state IDLE; end else case (dc_state) IDLE: begin if (req !hit_miss) begin if (dirty_flag replace_en) dc_state WRITEBACK; else dc_state REFILL; end end WRITEBACK: if (mem_gnt) dc_state REFILL; REFILL: if (mem_gnt mem_beat_done) dc_state IDLE; endcase end参数说明hit_miss是 Tag 比较结果1 表示命中replace_en表示当前 set 中选中一路mem_gnt是主存返回的握手信号。写回状态不直接回 IDLE而是先等主存接受写数据和地址再去申请读缺失行避免数据总线被覆盖。FPGA 上 BRAM 读延迟固定为 1 拍因此命中判断要在 request 发出后一拍才能稳定设计配套状态时也要把这 1 拍算进去。3.3 Cache与流水线的握手机制流水线并不能像仿真玩具那样“一拍给地址下一拍直接返回数据”。一般用 ready/valid 握手valid1 表示流水线想访问ready1 表示 Cache 本周期能完成只有两者同时为高这次访问才提交到下一级。miss 时 ready 拉低流水线整体冻结。assign dmem_req ex_mem_valid (ex_mem_op OP_LD || ex_mem_op OP_ST); assign dmem_stall dmem_req !dcache_ready; assign stall_pc dmem_stall; // 冻结 PC assign stall_ifid dmem_stall; // 冻结取指这里dcache_ready用一拍组合输出才能保证在最后一个可用周期时WB 前的数据已经在总线上。注意当 stall 时ex_mem 寄存器的内容不能覆盖为 0否则 miss 返回后指令会丢失。所以对访存级使用“使能信号”而不是“清除信号”。很多实现卡在 cache miss 就是因为这里用了同一条 flush 线。3.4 指令Cache与数据Cache分离综合项目一般把指令和数据 Cache 做成两块独立存储否则流水线在 MEM 阶段访数据时IF 阶段无法取指每拍都要仲裁。这种分离叫哈佛结构对 FPGA 很友好在 FPGA 上例化两块 BRAM各自有独立读端口指令路径和数据路径互不阻塞。如果想让项目更接近真实 SoC可以在总线层把两个 Cache 的 miss 请求汇聚到一个主存控制器用优先级仲裁器让指令请求和数据请求分时访问同一块主存。仲裁器不需要很复杂指令 miss 识别通常优先于数据 miss因为流水线等待着取指而数据 miss 不会立刻影响 IF。这个优先级策略能在不改变 Cache 逻辑的情况下显著减少取指停顿时间。4. 中断与异常从请求到流水线现场恢复4.1 中断请求、异常向量与CSR的状态在通用处理器里中断和异常通常会统一到一个入口。指令造成的异常非法指令、load 地址不对齐和外部中断共用一套现场保存流程区别只在 cause 寄存器。RISC-V 习惯的配置是mtvec 放向量基址mepc 放异常指令的 PCmcause 放原因mstatus.MIE 控制全局中断使能。设计时不要试图在硬件里保存全部通用寄存器那样寄存器文件需要多个写口面积直接爆炸。常见的做法是硬件只保存 epc 和 cause然后跳到 trap 入口软件在入口用一条sw序列把被中断的寄存器和mepc搬到内存栈。恢复靠mret硬件从 epc 回到断点。always (posedge clk or negedge rst_n) begin if (!rst_n) trap_taken 1b0; else if (mem_trap_trigger) trap_taken 1b1; else if (wb_valid wb_is_mret) trap_taken 1b0; end always (posedge clk or negedge rst_n) begin if (mem_trap_trigger) begin csr_mepc mem_pc; // 被中断指令的 PC csr_mcause 32h8000_0007; // 外部中断最高位表示中断 end end参数说明mem_trap_trigger在 MEM 级才拉起目的是实现精确异常csr_mcause高位置 1 表示异步中断0 表示同步异常。如果跳转目标本身 Cache miss流水线会走正常的 refill 流程不影响 CSR 中的 epc 内容。4.2 精确异常与流水线冲刷点的确定中断信号什么时候采样直接决定整条流水线要清多少。最差的做法是在 IF 看到中断就跳但此时 MEM 里还可能有一条未完成的 store中断服务程序无法确定之前的指令是否执行完这就是不精确异常。对通用处理器来说不精确异常会让系统行为不可复现。所以把采样点放在 MEM 级MEM 级之前的所有指令都已进入执行阶段MEM 后的年轻指令都有机会被丢弃。把 epc 锁存为当前 MEM 级指令的 PC然后所有更年轻的 IF/ID/EX 瞬间变为 bubblePC 重定向到 mtvec。这个做法的代价是中断响应延迟比 IF 级多 2 拍但换来严格顺序语义。assign flush_ifid mem_trap_trigger || branch_flush; assign flush_idex mem_trap_trigger || branch_flush; assign flush_exmem mem_trap_trigger;flush 之前已经写入 WB 的指令不能回滚它们仍然正常写回寄存器因为它们不是“未提交”。在 mepc 保存的同时WB 路径上的寄存器写入照常进行。很多刚开始做中断的人把 flush 线一路清到 WB结果寄存器文件被白白破坏。4.3 现场恢复与mret时序mret本身是一条正常流水线指令但 CSR 的恢复要早于下一轮取指所以必须在 MEM 或 WB 阶段把trap_taken清除并且让下一拍 PC 使用csr_mepc。常见做法是mret 指令进入 WB 级时把trap_taken清除同时把下一次 PC select 置为 CSR 值从 ID 开始的后续指令要被清空。如果 mret 在 EX 级判断那么 IF/ID 里已经取了 mret 后一条指令需要 flush 掉代价又是一拍。有些实现用延迟槽避免这个代价但在 RISC-V 里建议直接允许这一拍损失。中断现场的恢复代码可以只用两个信号wire use_mepc wb_is_mret wb_valid; wire pc_sel trap_taken ? 2b10 : use_mepc ? 2b01 : 2b00;因为use_mepc在 WB 阶段PC 重定向也在 WB 级完成能保证 mret 后的下一条指令确实从 epc 取。这个机制特别容易出错所以要在测试中主动构造“中断 - 长服务程序 - mret”的回环。4.4 中断、Cache写回和总线握手竞争当流水线测到中断时MEM 级可能正处在一个 Cache refill 过程的中间。不能因为 trap 就放弃 refill数据还没回到寄存器堆中断会把它掩盖。稳妥的做法是让中断请求把 ready 拉低但不打断当前状态机等到 Cache 完成本次访问才在下一次输入信号里采样 mem_trap_trigger。换句话说中断只影响 PC 流不影响访存电路的状态。如果主存接口同时正在处理写回脏行而此时中断到来控制器要区分“因为写回引起的 busy”和“因为 refill 引起的 busy”。设计总线状态机时不要用单一 busy 信号作为 trap 判断条件而应该用“cache 保持 req”和“流水线采样点”联合判断。很多总线 hang 到头都查不到就是因为这里把握手信号和流水线有效位混用了。5. 边验证边改的三板斧冒烟、对拍、波形切片5.1 最小冒烟程序先在测试 ROM 里放一小段程序覆盖五级流水最基本的路径普通指令、数据 Cache 读写、转发和中断入口。addi a0, zero, 5 # a0 5 sw a0, 0(zero) # 写数据 Cache lw a1, 0(zero) # 读回 add a2, a1, a0 # 数据冒险触发转发 ebreak # 停住跑通的标准PC 按顺序执行前 4 条WB 阶段能依次看到 a05、a15、a210然后停在 ebreak。波形文件中需要打开流水线各 valid 信号确认 load-use 阻塞时 IF/ID 没有推进。5.2 用RISC-V工具链对拍寄存器用riscv64-unknown-elf-gcc把一段 C 程序编译成二进制再用仿真环境和软件模拟器各自运行同一镜像。把最终寄存器文件 dump 出来比对能立即暴露出访存顺序、Cache 一致性和中断现场的错误。make sim 21 | tee sim.log gtkwave dump.vcd比对时不要只比最后一个 PC还要比对每个触发点上的寄存器堆快照。中断服务程序里通常会有临时寄存器如果 mepc 恢复错了快照会在 mret 后第一拍就开始偏离。5.3 波形切片定位Cache与中断交互在 gtkwave 里不要直接把顶层全部信号 dump 出来否则 vcd 文件增长得飞快。只选择与 Cache 状态机和中段响应相关的信号导出dc_state、trap_taken、mem_gnt、dcache_ready。导出后重点看中断触发时dc_state是否停在 REFILL 或 WRITEBACK 上。如果波形里看到trap_taken拉高时dcache_ready还是 0说明硬件没有先完成当前访存中断处理器的现场保存会因为 load 数据未到而被覆盖。反过来如果dc_state已经回到 IDLE 而trap_taken还没来说明中断请求在总线上被丢了一拍。这两个界面的时序远比 ALU 里的运算结果更容易让板子宕机。做中断与 IO 交互时我一般会在总线接口加一个异步 FIFO 来吸收主存延迟把总线握手和 Cache 状态机彻底解耦这样波形切片时看到的不再是一个勉强能跑的线性流程。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门