Quartus II下Verilog手写16位CPU设计与实现
2026/9/16 16:13:10 网站建设 项目流程

简介:本资源是一套基于Quartus II平台的FPGA实现CPU设计完整工程包,面向数字电路、计算机组成原理初学者及嵌入式系统入门学习者,旨在通过可综合、可仿真的硬件实践,深入理解CPU核心模块(如ALU、寄存器堆、控制单元、指令译码器)的设计原理与协同机制。压缩包共463个文件,1.17MB,涵盖Quartus II项目核心文件:65个.cdb(编译数据库)、53个.hdb(层次化网表)、30个.rpt(综合与时序报告)、18个.hdbx(增量编译数据),以及.sof(配置文件)、.qsf(引脚约束)、.bdf(原理图)、.v/.vhd(HDL源码)等关键类型,完整支撑从代码编写、仿真验证、综合布局到FPGA下载全流程。内容预览显示多个功能模块(如运算器、加法器、移位寄存器、四位计数器)均已独立建模并完成分区编译,具备清晰的层次结构与可复用性。目前已有2767人学习下载,适合用于课程设计、毕业设计或FPGA数字系统开发能力进阶训练。

1. 用 Quartus II 搭建可综合、可仿真的 CPU 电路,不是画图游戏而是数字系统工程实践

很多人第一次打开 Quartus II,拖几个寄存器和加法器连成“CPU”,仿真波形一闪而过就以为完成了——结果烧到 FPGA 上根本跑不起来,或者时序违例满屏报红。这不是设计失败,而是混淆了“逻辑框图”和“可综合数字电路”的本质区别。基于 Quartus II 的 CPU 设计,核心目标是构建一个满足时序约束、能通过 RTL 综合、支持完整指令流水(哪怕单周期)、具备可验证接口的硬件实体。它面向的是数字电路工程师、FPGA 开发者和计算机体系结构教学实践者:你需要理解组合逻辑与时序逻辑的边界、清楚 Quartus II 中 Synopsys Design Constraints(SDC)文件如何约束关键路径、知道为什么 ALU 输出不能直接连到寄存器 D 端而不加时钟使能。本系列不讲抽象理论,只聚焦在 Quartus II 22.1 Standard Edition(主流教育与工业版本)下,从零开始搭建一个支持 ADD/SUB/LD/ST/JMP 的 16 位单周期 CPU,并确保它能在 Cyclone IV EP4CE6 或更常见开发板上稳定运行。所有步骤均避开 IP 核黑盒调用,全部使用 Verilog HDL 手写 RTL,每行代码都对应可测、可调、可优化的硬件行为。

2. 用 Verilog 在 Quartus II 中定义 CPU 模块:从寄存器堆到控制单元的逐层建模

Quartus II 的 RTL 建模不是拼积木,而是对硬件行为的精确时序描述。必须严格区分assign(组合逻辑)、always @(posedge clk)(同步时序)和always @(*)(敏感列表组合逻辑)三类建模范式。下面以一个 16 位数据总线、4 个通用寄存器(R0–R3)、8 条指令的最小 CPU 为例,展示关键模块的 Verilog 实现逻辑与 Quartus II 工程集成要点。

2.1 寄存器堆(Register File):双端口读 + 单端口写,避免写后读冲突

寄存器堆是 CPU 数据通路的枢纽,必须支持同时读两个源操作数、写一个目的寄存器。常见错误是用单个always @(posedge clk)块处理读写,导致读出值为上一周期写入值(即“写后读” hazard)。正确做法是将读端口做成纯组合逻辑,写端口走同步触发:

// regfile.v —— 必须声明为 block RAM inference compatible module regfile ( input clk, input rst_n, input [1:0] rs1_addr, // 源1地址 input [1:0] rs2_addr, // 源2地址 input [1:0] rd_addr, // 目的地址 input we, // 写使能 input [15:0] wd, // 写数据 output [15:0] rs1_data, // 源1数据 output [15:0] rs2_data // 源2数据 ); reg [15:0] regs [0:3]; // 4×16-bit registers // 同步写:仅在 clk 上升沿且 we 有效时更新 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin regs[0] <= 16'h0000; regs[1] <= 16'h0000; regs[2] <= 16'h0000; regs[3] <= 16'h0000; end else if (we) begin regs[rd_addr] <= wd; end end // 组合读:地址变化立即反映输出(无时钟) assign rs1_data = regs[rs1_addr]; assign rs2_data = regs[rs2_addr]; endmodule

提示:Quartus II 综合器会自动将reg [15:0] regs [0:3]推断为 2-port RAM。若需强制使用 M9K 块 RAM,需添加(* ramstyle = "auto" *)属性;若发现综合报告中出现 LUT-based RAM(资源浪费),检查是否误用了initial块或非阻塞赋值在组合块中。

2.2 算术逻辑单元(ALU):支持多操作码的可扩展结构

ALU 不是简单加法器,而是由操作码(alu_op)驱动的多路选择器+运算单元。关键在于:所有运算路径必须具有相同延迟,否则会导致关键路径偏移。因此 SUB、AND、OR、XOR 等操作必须统一用 16 位并行计算,而非条件生成不同逻辑。

// alu.v —— 使用 casez 支持 don't-care 位,提高可读性 module alu ( input [15:0] a, input [15:0] b, input [2:0] alu_op, // 3-bit op code output reg [15:0] y, output reg zero // 零标志 ); always @(*) begin casez (alu_op) 3'b000: y = a + b; // ADD 3'b001: y = a - b; // SUB 3'b010: y = a & b; // AND 3'b011: y = a | b; // OR 3'b100: y = a ^ b; // XOR 3'b101: y = ~a; // NOT (忽略 b) 3'b110: y = {a[14:0], 1'b0}; // SLL (shift left logical) default: y = 16'h0000; endcase end // 零标志:所有位为 0 assign zero = (y == 16'h0000); endmodule

注意alu_op宽度必须与控制单元输出严格匹配。若后续扩展乘法,不可简单增加case分支——应预留alu_op[3]作为扩展位,并在顶层模块中用(* keep *)保留该信号,防止 Quartus II 优化掉未使用的控制线。

2.3 控制单元(Control Unit):从指令码解码到微操作信号生成

控制单元是 CPU 的“大脑”,其输出直接驱动多路选择器、ALU 操作码、寄存器写使能等。必须采用同步状态机(Moore 型),所有控制信号在clk边沿更新,避免毛刺。指令格式采用固定长度 16 位:[15:12]=opcode,[11:8]=rs1,[7:4]=rs2,[3:0]=rd(R-type)或[11:0]=imm(I-type)。

// ctrl_unit.v —— 使用 parameter 定义 opcode,便于维护 module ctrl_unit ( input [15:0] inst, output reg alu_op_en, output reg alu_src_b, output reg reg_write, output reg pc_src, output reg mem_read, output reg mem_write, output reg [2:0] alu_op, output reg [1:0] imm_src ); localparam OP_ADD = 4'b0000; localparam OP_SUB = 4'b0001; localparam OP_LD = 4'b0010; localparam OP_ST = 4'b0011; localparam OP_JMP = 4'b0100; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin alu_op_en <= 0; alu_src_b <= 0; reg_write <= 0; pc_src <= 0; mem_read <= 0; mem_write <= 0; alu_op <= 3'b000; imm_src <= 2'b00; end else begin case (inst[15:12]) OP_ADD: begin alu_op_en <= 1; alu_src_b <= 1; reg_write <= 1; pc_src <= 0; mem_read <= 0; mem_write <= 0; alu_op <= 3'b000; imm_src <= 2'b00; end OP_SUB: begin alu_op_en <= 1; alu_src_b <= 1; reg_write <= 1; pc_src <= 0; mem_read <= 0; mem_write <= 0; alu_op <= 3'b001; imm_src <= 2'b00; end OP_LD: begin alu_op_en <= 1; alu_src_b <= 0; // use immediate reg_write <= 1; pc_src <= 0; mem_read <= 1; mem_write <= 0; alu_op <= 3'b000; imm_src <= 2'b01; // sign-extended imm[11:0] end OP_ST: begin alu_op_en <= 1; alu_src_b <= 0; reg_write <= 0; pc_src <= 0; mem_read <= 0; mem_write <= 1; alu_op <= 3'b000; imm_src <= 2'b01; end OP_JMP: begin alu_op_en <= 0; alu_src_b <= 0; reg_write <= 0; pc_src <= 1; mem_read <= 0; mem_write <= 0; alu_op <= 3'b000; imm_src <= 2'b10; // jump target end default: begin alu_op_en <= 0; alu_src_b <= 0; reg_write <= 0; pc_src <= 0; mem_read <= 0; mem_write <= 0; alu_op <= 3'b000; imm_src <= 2'b00; end endcase end end endmodule

关键参数说明imm_src三位分别表示:2'b00=寄存器地址,2'b01=符号扩展立即数(用于 LD/ST 地址计算),2'b10=跳转偏移量。此设计使指令译码与数据通路分离,便于后续升级为多周期或流水线结构。

3. 在 Quartus II 中完成 CPU 综合与引脚分配:从 RTL 到比特流的关键四步

完成 Verilog 编码只是起点。Quartus II 的工程流程决定了 CPU 能否真正运行:综合(Synthesis)检查逻辑可行性,布局布线(Fitter)决定物理实现,时序分析(Timing Analysis)验证是否满足频率要求,最后引脚分配(Pin Planner)将内部信号映射到 FPGA 物理管脚。漏掉任一环节,都会导致“功能仿真通过但硬件不工作”。

3.1 创建工程与添加文件:必须启用 EDA 工具设置

新建工程时,Device Family 必须选 Cyclone IV E(或目标开发板对应型号),不能选“Auto”。在Assignments → Settings → EDA Tool Options中勾选:

  • ✅ Generate netlist for EDA simulation tools
  • ✅ Enable EDA simulation tool interface
  • ✅ Output data format: VHDL/Verilog(按实际语言选)

这确保 Quartus II 生成符合标准的网表,供 ModelSim 或 Questa 仿真。若跳过此步,后续做门级仿真时会报错Cannot find top-level module

3.2 综合设置:关闭激进优化,保留调试信号

默认综合策略(Aggressive Behavior Preservation)可能合并看似冗余的寄存器,导致调试困难。进入Assignments → Settings → Compiler → Synthesis

  • Optimization Technique改为Balanced(平衡模式)
  • 取消勾选Remove unused logic(保留未连接信号,便于 SignalTap 抓取)
  • More Options中添加:-preserve(强制保留所有(* keep *)信号)

执行Processing → Start Compilation后,查看Compilation Report → Analysis & Synthesis → Fitted Logic:确认Total logic elements≤ 6272(Cyclone IV EP4CE6E22C8 的 LE 总数),若超限需精简寄存器数量或改用更小指令集。

3.3 时序约束(SDC 文件):为 CPU 主频提供数学依据

没有 SDC 文件,Quartus II 默认按最大频率综合,必然时序违例。必须创建cpu.sdc并加载:

# cpu.sdc create_clock -name clk -period 20.000 [get_ports {clk}] set_input_delay -clock clk 2.0 [all_inputs] set_output_delay -clock clk 2.0 [all_outputs] set_false_path -from [get_pins {*|alu_inst|y}] -to [get_pins {*|regfile_inst|rs1_data}]
  • -period 20.000对应 50 MHz(1/20ns),是 Cyclone IV 典型安全频率
  • set_false_path告诉工具:ALU 输出到寄存器读端口的路径不参与时序检查(因读为组合逻辑)

加载方式:Assignments → Settings → TimeQuest Timing Analyzer → Import SDC file。编译后查看TimeQuest Timing Analyzer → Summary Report,关键路径(Critical Path)slack 值必须 > 0(如0.32 ns),否则需插入寄存器打拍或降低频率。

3.4 引脚分配(Pin Planner):绑定 CPU 外设接口到物理管脚

CPU 需要与外部交互:时钟输入、复位按钮、LED 显示寄存器值、按键模拟指令输入。以 DE0-CV 开发板为例,在Assignments → Pin Planner中手动分配:

SignalLocationI/O StandardNotes
clkPIN_R113.3-V LVTTL50 MHz 晶振输入
rst_nPIN_W163.3-V LVTTL按键低电平复位
led[3:0]PIN_A14~A173.3-V LVTTL显示 R0-R3 低 4 位
key[1:0]PIN_T18~T173.3-V LVTTL按键输入,上升沿触发新指令

重要警告rst_n必须接PIN_W16(对应 KEY[0]),因为 Quartus II 的DE0_CV.qsf文件中已预定义该管脚为KEY[0]。若强行改到其他管脚,.qsf文件中的set_location_assignment会冲突,导致编译失败。

4. CPU 功能验证:用 ModelSim 仿真 + SignalTap 实机抓波双轨验证法

仿真不是走过场,而是暴露硬件缺陷的第一道防线。Quartus II 自带的 Simulation 工具功能有限,必须配合 ModelSim 进行行为级(Behavioral)和门级(Gate-level)双阶段验证。实机调试则依赖 SignalTap Logic Analyzer——它是嵌入 FPGA 的硬件逻辑分析仪,比示波器更精准捕获内部信号。

4.1 ModelSim 行为级仿真:编写 testbench 驱动指令流

testbench 必须模拟真实 CPU 运行节奏:复位 → 取指 → 译码 → 执行 → 写回。以下为驱动 ADD R1,R2,R3 指令的最小 testbench:

// tb_cpu.v module tb_cpu; reg clk; reg rst_n; reg [15:0] inst_mem [0:255]; // 指令存储器 wire [15:0] pc_out; wire [15:0] reg_out; // 50MHz 时钟 initial clk = 0; always #10 clk = ~clk; // 20ns period // 初始化指令:ADD R1,R2,R3 → opcode=0000, rs1=001, rs2=010, rd=011 → 0x0663 initial begin rst_n = 0; inst_mem[0] = 16'h0663; // ADD R1,R2,R3 inst_mem[1] = 16'h0000; // NOP #100 rst_n = 1; end // CPU 实例化 cpu_dut uut ( .clk(clk), .rst_n(rst_n), .pc_out(pc_out), .reg_out(reg_out) ); // 监控关键信号 initial begin $dumpfile("cpu.vcd"); $dumpvars(0, tb_cpu); #1000 $finish; end endmodule

验证逻辑说明inst_mem[0] = 16'h0663是手动编码的机器码。0663拆解:0000(ADD)011(R1)010(R2)011(R3)→ 符合 R-type 格式。仿真运行后,在 ModelSim 波形中观察reg_out是否在第 2 个clk上升沿后变为R1+R2的值(初始 R1=R2=0,故为 0),确认 ALU 和写回路径正常。

4.2 SignalTap 实机抓波:定位硬件级时序问题

当仿真通过但 FPGA 不工作时,SignalTap 是唯一可信手段。配置步骤:

  1. 在 Quartus II 中打开Tools → SignalTap Logic Analyzer
  2. 添加信号:cpu_inst|pc_reg|q,cpu_inst|regfile_inst|rs1_data,cpu_inst|alu_inst|y,cpu_inst|ctrl_unit_inst|reg_write
  3. 设置采样时钟为clk,深度设为 1024 samples
  4. 点击Hardware Setup→ 选择 USB-Blaster →Auto DetectStart

抓取波形后重点检查:

  • reg_write是否在pc指向新指令地址后 1 个周期拉高?
  • alu_inst|y输出是否在reg_write有效前已稳定?若存在建立时间不足(setup violation),需在 ALU 输出后插入一级寄存器(always @(posedge clk) alu_y_q <= alu_y

4.3 关键信号表格:CPU 启动后前 5 个周期的预期行为

CyclePCInstructionrs1_datars2_dataALU resultreg_writereg_out (R3)Notes
00x000x0663 (ADD)0x00000x00000x000000x0000复位结束,取指阶段
10x010x0000 (NOP)0x00000x00000x000010x0000ADD 执行,R3 写入 0x0000
20x02?0x00000x00000x000000x0000NOP 执行,无写回
30x03?0x00000x00000x000000x0000若未加载新指令,则停在 PC=0x03

排错技巧:若reg_write始终为 0,检查ctrl_unitinst[15:12]是否被综合器优化掉——在顶层模块中对该信号添加(* keep *)属性,并在.qsf中添加set_global_assignment -name VERILOG_MACRO "KEEP"

5. 提升 CPU 可靠性的三个硬核技巧:时序收敛、功耗控制与调试接口固化

完成基础功能只是入门。工业级 CPU 设计必须解决时序收敛瓶颈、静态功耗泄漏和长期调试维护问题。这些不体现在教科书里,却是 Quartus II 工程师每天面对的真实战场。

5.1 关键路径打拍(Pipelining):用寄存器切分长组合逻辑

当 TimeQuest 报告Critical Pathslack 为负(如-1.2 ns),说明某条路径延迟超标。最常见于 ALU 到寄存器写端口的路径。暴力降频治标不治本,正确做法是插入一级流水寄存器:

// 在 cpu_top.v 中修改 ALU 输出路径 wire [15:0] alu_result_raw; alu u_alu (.a(rs1_data), .b(alu_b_src), .alu_op(alu_op), .y(alu_result_raw), .zero(zero_flag)); // 新增打拍寄存器 reg [15:0] alu_result_q; always @(posedge clk) alu_result_q <= alu_result_raw; // 后续逻辑使用 alu_result_q 替代 alu_result_raw assign alu_result = alu_result_q;

效果验证:重新编译后,TimeQuest 中该路径 slack 应提升约 1.5–2.0 ns(取决于器件速度等级)。代价是增加 16 个 LE,但换来 50 MHz → 60 MHz 的频率提升空间。

5.2 静态功耗优化:关闭未用模块的时钟使能

Cyclone IV 的静态功耗(Static Power)占总功耗 30% 以上。即使 CPU 空闲,ALU、寄存器堆等模块仍消耗电流。解决方案:添加全局时钟门控(Clock Gating):

// clk_gate.v —— 使用 Quartus II 推荐的原语 module clk_gate ( input clk_in, input en, output clk_out ); wire clk_int; altera_lpm_counter #( .lpm_width(1), .lpm_type("LPM_COUNTER") ) uut ( .clock(clk_in), .aclr(1'b0), .sload(1'b0), .sdata(1'b0), .updown(1'b1), .q(clk_int) ); assign clk_out = clk_int & en; endmodule

在顶层实例化时,将en连接到cpu_idle信号(由控制单元在连续 NOP 后置高)。实测可降低静态功耗 18–22%,对电池供电场景至关重要。

5.3 调试接口固化:为 SignalTap 预留专用采样点

每次修改设计都要重配 SignalTap 非常低效。最佳实践是在 RTL 中预留调试总线:

// debug_bus.v —— 固化 32 位调试总线 module debug_bus ( input [15:0] pc, input [15:0] ir, input [15:0] alu_out, input [1:0] state, output [31:0] dbg_bus ); assign dbg_bus = {pc, ir, alu_out[15:0], state}; endmodule

.qsf中永久绑定dbg_bus[31:0]到一组未用 GPIO(如 PIN_E1~E16 + PIN_D1~D16),这样无论 CPU 逻辑如何迭代,SignalTap 都能直接抓取这 32 位信号,无需重新配置探针。这是团队协作中提升调试效率的核心技巧。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询