五级流水线CPU的Verilog实现:数据通路、冒险处理与仿真验证
发布时间:2026/9/14 12:23:56来源:尧图网络
简介一套面向计算机体系结构学习者的流水线CPU设计资料以Verilog HDL实现取指、译码、执行、访存、写回五个阶段并处理数据冒险、控制冒险与资源冲突等典型问题涉及转发路径与分支预测机制。压缩包约27.49MB内容以源代码、设计报告和原理图为主Verilog代码覆盖处理器各功能模块与控制信号生成可直接在Quartus工程中完成综合、仿真与下载验证设计报告详细阐述设计目标、系统架构、各阶段数据通路、性能评估以及调试中遇到的问题与解决方案原理图直观展示了模块间的连接关系。已有3936人学习下载。通过该资源可系统学习流水线CPU的模块划分、并行处理和硬件描述方法适合作为本科计算机组成原理课程设计、FPGA入门实践或自主体系结构学习的重要参考帮助完成从需求分析到RTL设计再到板级验证的完整流程。1. 为什么每份流水线CPU的Verilog代码都值得自己调一遍流水线CPU是所有计算机体系结构课设里性价比最高的一道题代码量比单周期CPU多一半但吞吐率提升、冒险处理和时序分析这几个核心概念正好在这段代码上全部落地。这个工程把五级流水线的完整数据通路用 Verilog HDL 写通了配套提供源代码、设计报告和 Quartus 原理图工程。源码按取指、译码、执行、访存、写回五个阶段分模块组织报告里把数据冒险的转发路径、控制冒险的分支清空逻辑写得很清楚。对正在做课设的本科生来说这是一份可以直接对照修改的参考实现对写过单周期处理器、想转流水线的 FPGA 开发者来说它的价值在于展示了一套完整的控制信号生成与冒险处理框架而不是零散的技术片段。2. 数据通路怎么搭五级流水线的阶段划分与流水线寄存器先把指令的生命周期拆开。一条指令在单周期 CPU 里一拍完成在五级流水线里被切成五段取指、译码、执行、访存、写回。每一段只做一件事段与段之间靠流水线寄存器传递数据。这套设计的核心不是那几条指令怎么执行而是四个流水线寄存器如何把五个独立的阶段串成一条可并行运作的链路。2.1 为什么恰好是五级而不是三级或七级五级是 RISC 处理器经过验证的经验折中。级数更少一个时钟周期内必须完成的数据通路太长关键路径拖慢整体频率级数更多最高频率能提上去但冒险处理的复杂度会指数级上升。对课程设计和入门级 FPGA 实现来说五级流水线是代码清晰度和性能提升之间比较好的平衡点。这个设计里每级的功能边界很清晰。取指级只看 PC组合逻辑读指令存储器译码级并行读两个寄存器操作数同时生成全部控制信号执行级只做 ALU 运算和分支判断访存级只碰数据存储器写回级把结果写回寄存器文件。边界清晰的直接收益是调试体验信号可以沿着数据通路逐级追哪一级出了问题立刻能定位。如果只用三级流水线访存会被合并进执行阶段load 指令的关键路径变长意味着同一块 FPGA 上能跑到的主频更低。七级流水线则要求分支预测在更早的阶段完成需要额外的目标地址预测逻辑这个复杂度对课设来说往往得不偿失。五级是在控制逻辑复杂度和时序约束之间最成熟的折中方案。2.2 流水线寄存器里到底存了什么四个流水线寄存器是数据通路的骨架。IF/ID 锁存取回来的指令、PC4 和预译码信息ID/EX 锁存两个读出的操作数、立即数扩展结果以及译码阶段生成的全部控制信号EX/MEM 锁存 ALU 结果、写回数据、目标寄存器号和访存控制信号MEM/WB 锁存访存结果或 ALU 结果以及回写地址。流水线寄存器主要锁存内容位宽估算IF/ID指令字、PC4、预译码标志约 70 bitID/EX两个操作数、立即数、六位控制信号、寄存器地址约 150 bitEX/MEMALU 结果、写回数据、访存控制信号、寄存器地址约 110 bitMEM/WB访存结果或 ALU 结果、写回控制信号、寄存器地址约 80 bit写 Verilog 的时候建议把位宽相近的信号合并成向量声明但地址和控制信号要分开否则综合后信号名会变得难以追踪。RegWrite、MemtoReg 这类控制信号需要逐级传递。写回使能在译码阶段生成但直到写回级才被使用中间经过两个流水线寄存器必须一路带过去不能丢。这是初学者最容易犯的错要么漏接要么在流水线寄存器里少声明了一位。2.3 寄存器文件的双读单写结构寄存器文件采用双口读、单口写。两个读口对应译码级两个源操作数的并行读取单写口对应写回级的串行更新。读操作是组合逻辑写操作是时序逻辑也就是说读请求在同一个周期内立即返回数据写请求在时钟上升沿生效。读口数量由指令格式决定。R 型指令需要同时读 rs 和 rt 两个寄存器I 型指令需要读 rs 并取立即数。双读口让译码级在一个周期内拿到两个操作数不必先读一个再读另一个。写口只有一个因为顺序流水线里每个周期最多只有一条指令到达写回级。寄存器文件的第一号寄存器 r0 需要硬连线为 0写使能有效且目标地址为 r0 时必须忽略写请求这是 MIPS 风格处理器约定俗成的设计。3. Verilog 模块划分与关键代码从 IF/ID 寄存器到 ALU 生成这一章直接进源码。工程的顶层把九个模块实例化在一起PC 寄存器、指令存储器、IF/ID 流水线寄存器、寄存器文件、ID/EX 流水线寄存器、ALU、EX/MEM 流水线寄存器、数据存储器、MEM/WB 流水线寄存器外加一个控制信号译码器。模块接口的信号命名统一带阶段前缀比如id_ex_rs、ex_mem_rd这样综合后追波形时一眼能看出信号属于哪一级。3.1 IF/ID 寄存器的停顿与清空实现IF/ID 是流水线的第二级入口它需要处理三个特殊事件复位清零、停顿保持、分支清空。这三个事件互斥优先级从高到低是复位、清空、停顿。清空时注入一条空指令等价于在流水线里插入气泡。module if_id ( input wire clk, input wire rst_n, input wire stall, input wire flush, input wire [31:0] instr_in, input wire [31:0] pc_plus4_in, output reg [31:0] instr, output reg [31:0] pc_plus4 ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin instr 32b0; pc_plus4 32b0; end else if (flush) begin instr 32h00000000; pc_plus4 32b0; end else if (!stall) begin instr instr_in; pc_plus4 pc_plus4_in; end end endmodule代码的逻辑顺序很关键。先判断复位再判断分支清空最后才是正常的停顿锁存逻辑。分支清空时指令寄存器被写成全零这正好对应空指令的编码格式译码级看到全零 opcode 时所有控制信号输出无效值等价于插入了气泡。停顿保持时寄存器内容不变化因为!stall条件不成立时 always 块不触发赋值从而保持上一个周期的值。pc_plus4在分支清空时也置零是为了避免后续取指使用陈旧地址。3.2 寄存器文件与 ALU 的实现要点寄存器文件是译码级的心脏。组合读口的实现要注意地址 0 的处理任何对 r0 的读请求都必须返回 0因为 MIPS 体系结构约定 r0 恒为 0。写口只在时钟上升沿写入并且要显式屏蔽对 r0 的写操作。module regfile ( input wire clk, input wire we, input wire [4:0] raddr1, input wire [4:0] raddr2, input wire [4:0] waddr, input wire [31:0] wdata, output reg [31:0] rdata1, output reg [31:0] rdata2 ); reg [31:0] mem [0:31]; always (*) begin rdata1 (raddr1 5b0) ? 32b0 : mem[raddr1]; rdata2 (raddr2 5b0) ? 32b0 : mem[raddr2]; end always (posedge clk) begin if (we waddr ! 5b0) mem[waddr] wdata; end endmodule读取用阻塞赋值是因为它是纯组合逻辑写入用非阻塞赋值保证是时序逻辑。这个写法在综合后生成的是带两个异步读口的 RAM 结构Xilinx 和 Altera 的器件都能直接映射到片上 Block RAM 或分布式 RAM。ALU 的控制信号是 ID 阶段根据指令 opcode 和 funct 字段译码得到的四位数。zero标志输出给 EX/MEM 流水线寄存器里的分支判断逻辑使用。module alu ( input wire [31:0] alu_a, input wire [31:0] alu_b, input wire [3:0] alu_ctrl, output reg [31:0] alu_result, output wire zero ); always (*) begin case (alu_ctrl) 4b0000: alu_result alu_a alu_b; 4b0001: alu_result alu_a - alu_b; 4b0010: alu_result alu_a alu_b; 4b0011: alu_result alu_a | alu_b; 4b0100: alu_result alu_a ^ alu_b; 4b0101: alu_result alu_a alu_b[4:0]; default: alu_result 32b0; endcase end assign zero (alu_result 32b0); endmodule这里有个容易忽略的细节移位操作alu_a alu_b[4:0]的移位量只取低 5 位这是 MIPS sll 指令的语义要求。zero信号在分支指令执行时直接决定是否跳转它在 EX 阶段末尾产生传到 EX/MEM 寄存器后由 MEM 阶段的控制逻辑判断是否 flush 前面两级。3.3 控制信号译码一条指令如何变成六组控制位控制信号用 case 语句按 opcode 译码R 型指令还要进一步看 funct 字段。生成信号包括 RegWrite、MemWrite、ALUSrc、MemtoReg、Branch 和 ALUOp。最直接的做法是不用宏定义直接用参数常量命名比如parameter OP_RTYPE 6b000000。always (*) begin case (opcode) OP_RTYPE: begin reg_write 1b1; alu_src 1b0; mem_write 1b0; mem_to_reg 1b0; branch 1b0; case (funct) 6b100000: alu_ctrl 4b0000; // add 6b100010: alu_ctrl 4b0001; // sub 6b100100: alu_ctrl 4b0010; // and 6b100101: alu_ctrl 4b0011; // or default: alu_ctrl 4b0000; endcase end OP_LW: begin reg_write 1b1; alu_src 1b1; mem_to_reg 1b1; end OP_SW: begin reg_write 1b0; alu_src 1b1; mem_write 1b1; end OP_BEQ: begin reg_write 1b0; branch 1b1; end default: begin reg_write 1b0; end endcase end这套译码逻辑把指令按操作类型粗分R 型再细分 ALU 控制。ALUSrc决定 ALU 的第二个输入选寄存器文件的数据还是立即数扩展值访存指令必须用立即数计算地址所以 ALUSrc 置位。寄存器写回的来源由MemtoReg选择load 指令从数据存储器返回值写寄存器算术运算直接用 ALU 结果写寄存器。4. 冒险处理转发路径、load-use 停顿与分支清空流水线能不能真正跑起来全看冒险处理对不对。三台数据冒险在顺序流水线里实际只有一种必须解决写后读 RAW。WAR 和 WAW 冒险在顺序提交的流水线里根本不会出现因为写回只发生在最后一级所有指令严格按序完成。把精力集中在 RAW 上控制逻辑不会失控。4.1 转发单元从流水线寄存器里偷数据RAW 冒险的典型场景是一条add指令的结果还在 EX/MEM 寄存器里下一条sub就要读这个寄存器做源操作数。正常流程下add要走到 WB 阶段写回寄存器文件sub在 EX 阶段向寄存器文件发读请求数据晚了一拍。解法是加一条旁路让sub在 EX 阶段的输入端直接从 EX/MEM 寄存器取 ALU 结果。// 转发控制判断 EX/MEM 和 MEM/WB 中是否有与当前源操作数匹配的目标寄存器 wire fwd_a_from_ex ex_mem_regwrite ex_mem_rd ! 5b0 ex_mem_rd id_ex_rs; wire fwd_a_from_mem mem_wb_regwrite mem_wb_rd ! 5b0 mem_wb_rd id_ex_rs !fwd_a_from_ex; wire fwd_b_from_ex ex_mem_regwrite ex_mem_rd ! 5b0 ex_mem_rd id_ex_rt; wire fwd_b_from_mem mem_wb_regwrite mem_wb_rd ! 5b0 mem_wb_rd id_ex_rt !fwd_b_from_ex; always (*) begin if (fwd_a_from_ex) alu_a_mux 2b10; // 来自 EX/MEMALU 运算结果 else if (fwd_a_from_mem) alu_a_mux 2b01; // 来自 MEM/WB访存读数或 ALU 结果 else alu_a_mux 2b00; // 来自寄存器文件 end转发优先级是 EX/MEM 优先于 MEM/WB因为更近的数据肯定更新这个条件必须用!fwd_a_from_ex显式排除否则当两条转发路径都命中时多路选择器会选出旧数据。ex_mem_rd ! 5b0的判断同样必要r0 是硬连线零不允许被写也就不存在转发意义。ALU 两个输入端的 MUX 分别由两组独立的选择信号控制这样 R 型指令的两个源操作数可以分别转发自不同的流水线级。常见做法是把 MUX 选择信号也作为调试信号引出到顶层仿真时可以直接看 ALU 输入来自哪里快速判断转发逻辑有没有起作用。4.2 load-use 停顿什么时候转发解决不了转发不是万能的。lw $3, 0($2)之后紧跟add $4, $3, $1lw的目标数据要到 MEM 阶段结束才从数据存储器读出来而add的源操作数在 EX 阶段开头就必须准备好中间隔了整整一个阶段。转发电路再快也追不上这个时序差距只能让流水线停一拍。assign stall id_ex_memread id_ex_rt ! 5b0 (id_ex_rt if_id_rs || id_ex_rt if_id_rt);停顿检测器只检查一种情况EX 阶段是 load 指令ID 阶段的下一条指令要读它的目标寄存器。条件成立时拉高全局 stall 信号IF/ID 寄存器保持、PC 保持、ID/EX 寄存器清洗成空指令。这样 load 的访存结果在下一个时钟沿写入 MEM/WB 寄存器再下一个周期已经到达 WB 级地址匹配后转发电路就能把它送到 EX 阶段的 ALU 输入。冒险类型处理方式性能代价ALU-ALU 数据冒险转发路径无load-use 数据冒险停顿一个周期 转发1 个周期分支控制冒险清空前两级2 个周期load-use 停顿代码里的id_ex_rt指的是 EX 阶段正在执行的 load 目标寄存器。MIPS 指令格式里 load 的目标地址位在 rt 字段与算术类指令的目标寄存器位在 rd 字段不同这个差异是最容易被忽略的地方。如果写成id_ex_rd综合不会报错但停顿条件永远匹配不上仿真时数据错乱。4.3 控制冒险分支指令的两拍代价分支指令在 EX 阶段才算出跳转目标地址此时它后面已经取了两条指令。这种控制流不确定导致的规定动作是假设分支不跳转等 EX 阶段算出结果后如果真的要跳立刻清空 IF/ID 和 ID/EX 两级流水线并把 PC 更新为跳转目标。assign flush ex_mem_branch ex_mem_zero;ex_mem_branch是分支指令标志ex_mem_zero来自 ALU 的零输出。两个条件同时满足说明分支确定跳转。flush 信号同时送进 IF/ID 和 ID/EX把这两级的指令替换为空操作等于丢弃了两条无效指令。ex_mem_branch信号在 EX/MEM 寄存器里仍然保留一路传递保证它到达 MEM 阶段时仍然有效。这是控制信号逐级传递的又一个例子任何时候都不能把某级产生的标志只留在本级。在 MEM 阶段做分支清空有个额外好处跳转目标地址已经在 EX/MEM 寄存器里锁存不需要额外增加路径。代价是每次分支跳转损失两个周期分支指令在典型程序里约占两成整体性能损失可以接受。如果想进一步优化可以在 ID 阶段提前比较分支条件但需要额外的前递逻辑对课设来说不是必须的。5. Quartus 工程下的仿真验证testbench 设计、波形观察与指令序列构造拿到工程先看目录结构。一般分为 rtl、sim、prj 三个目录rtl 下放全部设计源码sim 里放 testbenchprj 是 Quartus 工程文件。首次打开工程后先跑一次全编译确认综合无误再切换到仿真模式准备跑波形验证。5.1 testbench 结构与观测点testbench 不需要太复杂核心是把时钟和复位驱动起来再从顶层引出几个调试信号看内部状态。顶层设计里建议专门留出用于仿真观测的端口把 PC、当前指令字、流水线寄存器状态引到顶层。module tb_cpu(); reg clk; reg rst_n; wire [31:0] debug_pc; wire [31:0] debug_instr; wire [31:0] debug_alu_result; cpu_top u_cpu_top ( .clk (clk), .rst_n (rst_n), .debug_pc (debug_pc), .debug_instr(debug_instr), .debug_alu_result (debug_alu_result) ); initial begin clk 1b0; rst_n 1b0; #20 rst_n 1b1; #2000 $finish; end always #10 clk ~clk; endmodule时钟周期设为 20ns复位保持 20ns 后释放仿真跑 2 微秒足够看几十条指令执行。debug_前缀的信号是专门留给验证用的观测点综合时如果不去约束Quartus 默认不会把这些信号优化掉。观测波形的重点不是看最终结果而是看 PC 序列是否存在空拍和跳变这直接反映停顿和清空逻辑是否生效。5.2 三组指令序列验证三类冒险验证转发逻辑最直接的方法是构造一条紧邻的依赖链。先让addi $1, $0, 100写寄存器紧接着安排addi $2, $1, 5读同一个寄存器。第二条指令的源操作数从 EX/MEM 转发过来仿真波形里 ALU 输入的 MUX 选择信号会短暂出现10出现即验证通过。addi $1, $0, 100 # 20010064 addi $2, $1, 5 # 20220005 sw $1, 0($2) # AC410000 lw $3, 0($2) # 8C430000 add $4, $3, $1 # 00612020 beq $0, $0, 4 # 10040000load-use 测试看第三和第四条指令。lw后面紧跟add停顿检测器的 stall 信号会在一个周期内拉高同时 PC 停止递增IF/ID 寄存器内容保持。波形上能看到 PC 连续两拍停在同一个值这是流水线停顿最直观的表现。分支测试看最后一条beq无条件分支必然跳转flush 信号拉高时 IF/ID 和 ID/EX 同时被清空波形里 PC 会出现一个直接跳跃两拍之后地址的动作跳跃跨度正好对应分支目标地址的拼接逻辑。把这组指令完整跑一遍观察 PC 的三种形态正常递增、同址保持停顿、跳跃分支跳转流水线核心逻辑就全部验证完了。如果发现固定某个阶段的数据错误优先查对应流水线寄存器的使能信号是否接对再把波形放大到出错的时钟沿前后各一个周期核对数据是来自寄存器文件、转发通路还是流水线寄存器自身。本文还有配套的精品资源点击获取
网站建设高端定制企业官网