资讯动态

Vivado单周期CPU设计实战:从Verilog RTL到上板验证的完整指南

发布时间:2026/10/17 3:18:22 来源:尧图企业网站定制
简介这份资源面向计算机组成原理学习者与FPGA入门开发者提供一套基于Verilog实现的单周期CPU完整工程可在Vivado中完成编译、仿真、综合与下载全流程。设计覆盖取指、译码、执行、写回四个阶段包含指令寄存器、指令解码器、ALU、通用寄存器组、控制单元、内存接口与程序计数器等核心模块适合课程设计、实验报告与处理器原理验证等场景。压缩包共107个文件约1.31MB以Verilog源码与仿真文件为主辅以Vivado工程配置、综合实现报告、JTAG会话日志及批处理脚本便于直接打开工程复现实验并排查时序与逻辑问题。目前已有3633人学习下载可帮助读者理解单周期数据通路与控制信号生成方式掌握从代码到FPGA配置的完整设计链路。1. 单周期 CPU 设计在 Vivado 里到底怎么跑通从指令集到上板的第一道坎很多人第一次在 Vivado 里做单周期 CPU卡住的地方不是写不出 ALU而是仿真波形全红、上板后数码管乱跳回头查半天发现是复位没接对或者时钟约束漏了。这份资源就是一套完整的单周期 CPU 设计工程用 Verilog 描述跑在 Vivado 上覆盖取指、译码、执行、访存、写回五个阶段的组合逻辑加时序逻辑实现。它适合正在学计算机组成原理、想从理论落到 RTL 的本科生也适合需要快速搭一个可综合 CPU 骨架做验证的从业者。你拿到手能直接打开工程跑仿真、看综合报告、生成比特流不用从零搭 testbench。核心价值在于它把指令周期里那些容易含糊的细节——比如 PC 更新时机、寄存器堆写回冲突、立即数扩展方式——都固化成了可读的代码结构而不是散落在 PPT 里的框图。2. 单周期数据通路搭建从 PC 到写回的五段拆解2.1 为什么单周期结构适合作为第一个 CPU 工程单周期 CPU 的核心特征是每条指令在一个时钟周期内完成CPI 恒等于 1。这意味着你不需要处理流水线冒险、不需要前递单元、不需要分支预测控制逻辑就是一张真值表。对于刚接触 Verilog 和 Vivado 的人来说这是最干净的起点——所有信号在一个周期内稳定仿真波形一眼能看懂因果关系。常见做法是把数据通路拆成五个组合逻辑块取指单元根据 PC 从指令存储器读出 32 位指令译码单元拆出 opcode、rs1、rs2、rd 和立即数字段执行单元根据 ALUOp 做加减与或访存单元处理 load/store 的数据读写写回单元把结果送回寄存器堆。每个块之间用 wire 连接时钟只驱动 PC 和寄存器堆这两个时序元件。我一般会建议先画一张信号流向图再写代码因为单周期里最容易翻车的地方就是控制信号和 datapath 的对应关系。比如RegWrite什么时候拉高、MemToReg选哪一路回写这些如果不在纸上标清楚代码写到一半就会乱。2.2 取指与 PC 更新逻辑的实现细节取指阶段的核心是一个 32 位 PC 寄存器加一个指令存储器。PC 的更新逻辑决定了程序能不能按顺序跑也决定了分支跳转能不能生效。// pc_reg.v - 程序计数器与下一条指令地址选择 module pc_reg( input wire clk, input wire rst_n, input wire pc_sel, // 0: PC4, 1: 跳转目标 input wire [31:0] jump_addr, // 分支/跳转目标地址 output reg [31:0] pc, output wire [31:0] pc_next ); assign pc_next pc_sel ? jump_addr : (pc 32d4); always (posedge clk or negedge rst_n) begin if (!rst_n) pc 32h0000_0000; // 复位后从地址 0 开始取指 else pc pc_next; end endmodule这段代码里pc_sel是分支决策信号由译码阶段根据指令类型和 ALU 零标志位产生。jump_addr对于分支指令是pc 立即数偏移对于跳转指令是rs1 立即数。注意复位用的是低电平有效异步复位这是 Vivado 里比较稳妥的写法综合器能正确推断出 FDRE 带复位端。参数上唯一需要留意的是指令存储器深度。如果你用分布式 RAM 实现深度设 256 够跑大部分教学测试程序如果后面要扩到 1024 条指令得改成块 RAM 并加一级输出寄存器否则时序会紧张。2.3 译码与控制单元的真值表落地译码单元要做两件事一是从 32 位指令里切出各字段二是根据 opcode 和 funct3/funct7 生成所有控制信号。单周期里控制信号是纯组合逻辑用 case 语句写最直观。// ctrl_unit.v - 主控制信号生成节选 always (*) begin case (opcode) 7b0110011: begin // R-type reg_write 1b1; mem_to_reg 1b0; mem_read 1b0; mem_write 1b0; alu_src 1b0; branch 1b0; alu_op 2b10; end 7b0000011: begin // lw reg_write 1b1; mem_to_reg 1b1; mem_read 1b1; mem_write 1b0; alu_src 1b1; branch 1b0; alu_op 2b00; end 7b0100011: begin // sw reg_write 1b0; mem_to_reg 1b0; mem_read 1b0; mem_write 1b1; alu_src 1b1; branch 1b0; alu_op 2b00; end 7b1100011: begin // beq reg_write 1b0; mem_to_reg 1b0; mem_read 1b0; mem_write 1b0; alu_src 1b0; branch 1b1; alu_op 2b01; end default: begin reg_write 1b0; mem_to_reg 1b0; mem_read 1b0; mem_write 1b0; alu_src 1b0; branch 1b0; alu_op 2b00; end endcase endalu_op的编码含义是00 做加法用于地址计算01 做减法用于比较10 根据 funct3/funct7 做具体运算。alu_src选 ALU 的第二个操作数来自寄存器还是立即数。mem_to_reg决定写回数据来自 ALU 结果还是存储器读出值。这里有个容易忽略的点default分支必须把所有控制信号赋默认值否则综合出锁存器波形上会出现莫名其妙的保持状态。这是血泪经验新手十有八九会踩。2.4 寄存器堆与存储器接口的写法寄存器堆是两个读端口一个写端口的双端口 RAM写操作在时钟上升沿生效读操作组合逻辑输出。存储器接口在单周期里通常用行为级描述模拟综合时 Vivado 会自动推断。// regfile.v - 32x32 寄存器堆x0 恒为 0 module regfile( input wire clk, input wire rst_n, input wire we, input wire [4:0] rs1, rs2, rd, input wire [31:0] wdata, output wire [31:0] rdata1, rdata2 ); reg [31:0] regs [0:31]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) for (i 0; i 32; i i 1) regs[i] 32d0; else if (we rd ! 5d0) regs[rd] wdata; end assign rdata1 (rs1 5d0) ? 32d0 : regs[rs1]; assign rdata2 (rs2 5d0) ? 32d0 : regs[rs2]; endmodulerd ! 5d0这个判断保证了 x0 寄存器不会被写入读端口也强制返回 0。这是 RISC 类指令集的约定如果漏掉后面跑测试程序时 x0 被意外改写整个程序行为就全错了。存储器接口我一般会单独写一个data_mem.v用reg [31:0] mem [0:255]声明读用组合逻辑写用同步逻辑。注意 Vivado 综合时如果读地址直接连到存储器输出可能会推断成分布式 RAM时序报告里看 LUTRAM 占用就能确认。3. Vivado 工程配置与仿真验证从新建工程到波形确认3.1 新建工程与源文件组织打开 Vivado选 Create Project器件根据你手头的板子选。如果只是跑仿真选一个 Artix-7 的 xc7a35t 就够。添加源文件时把pc_reg.v、ctrl_unit.v、regfile.v、alu.v、data_mem.v、cpu_top.v全部加进去顶层设cpu_top。约束文件里至少要有两行时钟周期和复位引脚。比如板载 50MHz 时钟周期 20nscreate_clock -period 20.000 -name sys_clk [get_ports clk] set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk]综合之前先跑一次 Behavioral Simulation确认波形里 PC 在复位后从 0 开始递增指令存储器输出有变化。如果 PC 不动先查复位信号极性再查时钟有没有接对。3.2 编写 testbench 与观察关键波形testbench 不需要太复杂给时钟和复位跑几百个周期把 PC、指令、ALU 结果、写回地址都加到波形窗口。// tb_cpu.v - 单周期 CPU 仿真激励 module tb_cpu; reg clk, rst_n; wire [31:0] pc, instr, alu_result; cpu_top u_cpu( .clk(clk), .rst_n(rst_n), .pc_out(pc), .instr_out(instr), .alu_out(alu_result) ); initial begin clk 0; rst_n 0; #100 rst_n 1; #2000 $finish; end always #10 clk ~clk; // 20ns 周期 endmodule仿真跑起来后重点看几个时刻复位释放后第一个上升沿 PC 是否从 0 变 4第一条指令的 opcode 是否被正确译码写使能信号是否在正确的周期拉高。如果波形里出现红色 X多半是某个寄存器没复位或者多驱动冲突。我一般会在 testbench 里预加载几条指令到指令存储器比如先来一条 addi 给 x1 赋值再来一条 add 做加法最后 sw 存到数据存储器。这样跑完看寄存器堆和 data_mem 的内容就能判断通路是否完整。3.3 综合报告里要盯的三个数综合完成后打开 Report Utilization重点看三列LUT、FF、BRAM。单周期 CPU 不带流水线LUT 用量大概在几百到一千出头FF 在两百以内BRAM 取决于指令存储器深度。如果 LUT 数量异常大检查是不是某个 case 语句写成了优先级链而不是并行结构。再看 Report Timing Summary单周期设计的关键路径通常是从 PC 经过指令存储器、译码、ALU、数据存储器到写回。如果 WNS 是负数说明组合逻辑太长得考虑把存储器读改成同步输出或者插入流水寄存器。不过教学用途下 50MHz 一般都能过不用太焦虑。最后看 Report DRC如果有 unconnected port 或者 multi-driven net 的警告必须回去改代码。多驱动在 Verilog 里综合不会报错但上板行为不可预测属于典型的玄学问题源头。4. 避坑与常见问题排查单周期 CPU 调试中的五个真实翻车点4.1 现象仿真波形全红PC 始终为 X原因复位信号没有正确初始化或者always块里用了阻塞赋值导致竞争。单周期里 PC 寄存器必须用非阻塞赋值且复位分支要覆盖所有时序元件。解决检查pc_reg.v和regfile.v的复位逻辑确保每个reg在if (!rst_n)分支里都有明确赋值。testbench 里复位初始值设为 0延时足够长再拉高。4.2 现象综合后 LUT 数量暴涨时序不收敛原因控制单元的 case 语句没有写default分支综合器推断出锁存器导致组合逻辑环路。或者 ALU 里用了casez但没处理全条件。解决所有组合逻辑always (*)块开头给所有输出赋默认值case 必须带 default。ALU 的运算选择用完整 case 覆盖所有编码。4.3 现象上板后数码管显示乱码但仿真正常原因引脚约束文件里管脚分配和实际板子不对应或者时钟约束周期设得太小导致时序违例。仿真没有时序信息所以看不出问题。解决对照板子原理图逐个核对管脚号时钟周期先设 20ns 跑通再往下压。用 Vivado 的 I/O Planning 视图检查每个端口的方向和电平标准。4.4 现象load 指令读出的数据总是比预期晚一拍原因数据存储器用了同步读但单周期假设组合读。同步读会在时钟沿后才输出数据导致写回阶段拿到的是上一个地址的值。解决单周期里数据存储器读必须用组合逻辑assign rdata mem[addr]写用同步。如果坚持用同步读就得把整个 CPU 改成两级流水那就不是单周期了。4.5 现象分支指令跳转地址算错程序跑飞原因立即数扩展时符号位处理错误或者分支目标地址用了pc 4 offset但代码里只加了pc offset。解决分支偏移是相对于下一条指令的地址即pc 4 (imm 1)如果指令按半字对齐。检查立即数生成模块里{{20{inst[31]}}, inst[7], inst[30:25], inst[11:8]}的位拼接顺序少一位就全错。5. 进阶验证用汇编测试程序压一遍数据通路仿真波形看几条指令不够真正能暴露问题的是跑一段有分支、有访存、有数据相关的汇编程序。我一般会手写一小段机器码预加载到指令存储器里覆盖 add、sub、and、or、lw、sw、beq 这几类指令然后观察寄存器堆最终值和数据存储器内容是否和手算一致。具体做法是在 testbench 里用initial块给u_cpu.u_imem.mem[0]到mem[N]逐条赋值。比如先addi x1, x0, 10再addi x2, x0, 20然后add x3, x1, x2预期 x3 等于 30。接着sw x3, 0(x0)存到数据存储器地址 0再lw x4, 0(x0)读回来预期 x4 也等于 30。最后来一条beq x3, x4, 8如果相等就跳过下一条指令。跑完仿真后在波形里看寄存器堆的regs[3]和regs[4]是否都是 30data_mem.mem[0]是否也是 30。如果有一个不对就顺着那条指令的路径逐级查译码输出的控制信号对不对、ALU 操作数选对了没有、写回地址是不是 rd。还有一个技巧是在 Vivado 里用 Tcl 脚本批量跑仿真把不同测试程序的预期结果写成文件仿真结束后自动比对。这样改一版 RTL 就能快速回归不用每次手动看波形。我后来养成的习惯是每次动控制单元或者 ALU 的代码先把这组汇编测试跑一遍确认所有指令行为不变再去看综合报告。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑