FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

FPGA项目实战:基于Verilog的简易CPU设计与仿真验证

二牛学FPGA二牛学FPGA
技术分享
4小时前
0
0
2

本指南旨在引导读者完成一个基于Verilog的简易CPU(中央处理器)核心的设计、仿真与验证。该CPU采用经典的RISC(精简指令集)架构,包含取指、译码、执行、访存、写回五个基本流水线阶段,并支持一个精简的指令集。通过本项目,读者将掌握CPU核心模块的划分、关键数据通路的设计、流水线控制逻辑的实现以及使用仿真工具进行功能验证的完整流程。本设计以可综合、可验证为目标,最终可在FPGA平台上运行简单的测试程序。

Quick Start

  • 步骤1:环境准备。安装Vivado 2020.1或更高版本,并准备一个支持Verilog-2001的文本编辑器。
  • 步骤2:获取源码。从项目仓库下载所有RTL源文件(cpu_top.v, pc_reg.v, if_id.v, id.v, id_ex.v, ex.v, ex_mem.v, mem.v, mem_wb.v, regfile.v, ctrl.v)和测试程序汇编文件(test.asm)。
  • 步骤3:生成指令存储器初始化文件。使用配套的汇编器脚本(assembler.py)将test.asm汇编成机器码,并输出为inst_rom.coe(Vivado COE格式)或inst_mem.mem(纯文本格式)。
  • 步骤4:创建Vivado工程。新建一个RTL项目,目标器件选择“xc7a100tcsg324-1”(或您手头的FPGA型号),并将所有RTL源文件添加到工程中。
  • 步骤5:添加存储器初始化文件。在工程中创建一个Block Memory Generator IP核,配置为单端口ROM,宽度32位,深度256,并加载inst_rom.coe文件作为初始化内容。将其输出连接到CPU顶层的指令存储器接口。
  • 步骤6:编写仿真测试平台。创建tb_cpu_top.v文件,实例化CPU顶层模块,提供时钟和复位信号。将CPU的数据存储器接口(写使能、地址、写数据)连接到测试平台进行监控。
  • 步骤7:运行行为仿真。将tb_cpu_top.v设置为仿真顶层,运行仿真至少2000个时钟周期。在波形窗口中观察程序计数器(PC)、指令(inst)、寄存器文件写使能(reg_we)、写地址(reg_waddr)和写数据(reg_wdata)等关键信号。
  • 步骤8:验证结果。检查仿真结束时,寄存器R1(地址1)的值是否等于0x0000000C(十进制12),寄存器R2(地址2)的值是否等于0x00000048(十进制72)。这是测试程序计算1到8之和以及阶乘的预期结果。
  • 步骤9:综合与实现(可选)。对设计进行综合(Synthesis)和实现(Implementation),查看资源报告(LUT、FF、BRAM)和时序报告(WNS)。
  • 步骤10:上板验证(可选)。添加引脚约束文件(.xdc),生成比特流,下载到FPGA。通过ILA(集成逻辑分析仪)或外部逻辑分析仪抓取实际运行信号,与仿真结果对比。

前置条件与环境

项目推荐值/说明替代方案/备注
FPGA开发板/器件Xilinx Artix-7系列 (如xc7a100t)任何支持Block RAM和足够逻辑资源的FPGA,需相应调整约束。
EDA工具Vivado 2020.1Vivado 2018.3及以上,或Quartus Prime (需适配IP和约束语法)。
仿真工具Vivado Simulator (XSim)ModelSim/QuestaSim,需正确编译Xilinx仿真库。
设计语言Verilog-2001SystemVerilog (可增强可读性和验证能力)。
时钟与复位单时钟域,同步低有效复位。时钟频率目标50MHz。复位极性可调整,初始频率建议≤50MHz以保证时序收敛。
指令存储器Block Memory Generator IP (ROM)使用分布式RAM (LUTRAM)或预定义的Verilog数组 ($readmemh),后者仅用于仿真。
数据存储器设计外接,在测试平台中模拟可集成Block RAM IP作为数据RAM,构成最小系统。
关键约束文件cpu_top.xdc (定义时钟、复位引脚和时序)必须创建,否则实现阶段时钟无约束,时序报告无效。
验证依赖测试程序汇编文件 (test.asm)及汇编器需Python环境运行汇编脚本,或手动计算机器码。

目标与验收标准

完成本项目的标志是设计出一个能正确执行预定指令集、通过仿真验证并可在FPGA上运行的五级流水线简易CPU。

  • 功能正确性:CPU能顺序执行存储在ROM中的测试程序。该程序包含算术运算(ADD, SUB)、逻辑运算(AND, OR)、立即数加载(LI)、条件分支(BEQ)和跳转(JMP)指令,最终计算结果存入指定寄存器。
  • 仿真验收:在行为仿真中,运行测试程序后,寄存器文件中的R1和R2值必须分别等于0x0000000C和0x00000048。波形中应能清晰观察到五级流水线的推进、数据前递(如已实现)以及控制冒险引起的流水线停顿(或冲刷)。
  • 可综合性:设计必须能通过Vivado的综合(Synthesis)而无错误,关键模块无锁存器(Latch)推断。
  • 性能指标(上板后):在目标器件上实现后,时序报告中的最差负松弛(WNS)应大于0,即满足50MHz的时钟约束。资源占用应在合理范围内(例如,Artix-7 xc7a100t器件上,LUT使用 < 2000, FF使用 < 1000)。
  • 上板验证(可选但推荐):将CPU比特流下载至FPGA,通过ILA抓取到的PC值变化序列和寄存器写入事件,应与仿真波形关键节点一致。

实施步骤

阶段一:工程结构与模块划分

采用经典的五级流水线结构:取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)。每个阶段对应一个主要模块,阶段间通过流水线寄存器(如if_id)传递数据和控件。

  • 顶层模块(cpu_top):实例化所有流水线模块、寄存器文件、控制器和存储器接口。连接全局时钟和复位。
  • 取指(pc_reg, if):PC寄存器在每个时钟沿更新。从指令ROM读取指令。
  • 译码(id):解析指令,从寄存器文件读取源操作数,产生ALU操作码、立即数等控制信号。
  • 执行(ex):包含ALU,完成算术逻辑运算、地址计算和分支判断。
  • 访存(mem):处理Load/Store指令(本简易CPU可先只实现Store,或作为数据通路预留)。
  • 写回(wb):将运算结果或访存结果写回寄存器文件。
  • 控制器(ctrl):检测数据冒险(RAW)和控制冒险(分支、跳转),产生流水线停顿(Stall)和冲刷(Flush)信号。

常见坑与排查:

  • 坑1:组合逻辑环路。译码阶段从寄存器文件读数据,而写回阶段在同一时钟沿写寄存器。如果写回地址与读地址相同,且组合逻辑路径形成环路,会导致仿真振荡或综合错误。排查:确保寄存器文件实现为同步写、异步读(或同步读但使用前一时钟周期的写数据)。标准做法是写操作在时钟上升沿生效,读端口直接输出对应地址寄存器的值。
  • 坑2:流水线寄存器输出未初始化。在复位释放后,流水线寄存器(如if_id_inst)输出为不定态X,可能导致后续模块行为异常。排查:在声明流水线寄存器时赋初值(如reg [31:0] if_id_inst = 32‘h0;),或在复位逻辑中为所有寄存器输出赋予明确的初始值(如NOP指令)。

阶段二:关键模块实现要点

1. 寄存器文件(regfile.v):这是数据冒险的根源,需精心设计。

module regfile (
    input clk,
    input rst_n,
    input we,           // 写使能,来自写回阶段
    input [4:0] waddr, // 写地址
    input [31:0] wdata,// 写数据
    input [4:0] raddr1,// 读地址1
    input [4:0] raddr2,// 读地址2,
    output reg [31:0] rdata1, // 读数据1
    output reg [31:0] rdata2  // 读数据2
);
    reg [31:0] rf [31:0]; // 32个32位寄存器
    // 异步读
    always @(*) begin
        rdata1 = (raddr1 == 5‘b0) ? 32‘h0 : rf[raddr1]; // 寄存器0恒为0
        rdata2 = (raddr2 == 5‘b0) ? 32‘h0 : rf[raddr2];
    end
    // 同步写
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // 可选择性初始化部分寄存器
        end else if (we &amp;&amp; waddr != 5‘b0) begin // 寄存器0不可写
            rf[waddr] &lt;= wdata;
        end
    end
endmodule

注意:异步读意味着读出的数据是当前时钟周期内寄存器数组的最新值。这引入了RAW冒险,必须通过数据前递或流水线停顿解决。寄存器0(rf[0])应硬件置零,这是RISC架构的常见约定。

2. 数据前递(Forwarding)单元:为了减少因RAW冒险导致的流水线停顿,需在EX阶段前加入前递逻辑。比较EX/MEM和MEM/WB阶段的目的寄存器地址与ID阶段的源寄存器地址,若匹配且写使能有效,则将后续阶段的结果前递给EX阶段的ALU输入。

// 在EX模块或独立的forwarding模块中
always @(*) begin
    // 默认值:使用来自ID阶段的寄存器读数据
    forward_a = 2‘b00;
    forward_b = 2‘b00;
    // 前递来自EX/MEM阶段的结果(优先级高)
    if (ex_mem_we &amp;&amp; (ex_mem_waddr != 0) &amp;&amp; (ex_mem_waddr == id_ex_raddr1)) begin
        forward_a = 2‘b01; // 选择ex_mem_alu_result
    end else if (mem_wb_we &amp;&amp; (mem_wb_waddr != 0) &amp;&amp; (mem_wb_waddr == id_ex_raddr1)) begin
        forward_a = 2‘b10; // 选择mem_wb_wdata
    end
    // 对raddr2进行类似判断...
end
// ALU操作数选择
assign alu_src1 = (forward_a == 2‘b01) ? ex_mem_alu_result :
                   (forward_a == 2‘b10) ? mem_wb_wdata : id_ex_rdata1;
assign alu_src2 = ... // 类似处理,还需考虑立即数选择

常见坑与排查:

  • 坑3:前递条件不完整。未检查写使能(we)或忽略了寄存器0,导致将无效数据前递,计算结果错误。排查:在仿真波形中,当发生RAW冒险时,检查ALU的输入数据是否正确地变成了前递的数据,而非旧的寄存器值。同时检查前递逻辑的条件判断语句是否覆盖了所有流水线阶段。
  • 坑4:Load-Use冒险未处理。当一条Load指令后面紧跟着使用该加载结果的指令时,即使有前递,数据在MEM阶段结束时才有效,EX阶段仍需等待。这是必须通过流水线停顿(插入一个气泡)解决的冒险。排查:在控制器(ctrl.v)中检测Load-Use情况,并产生一个周期的Stall信号,停顿IF和ID阶段,同时冲刷EX阶段的指令(变为NOP)。仿真中观察在Load指令后,流水线是否正确插入了一个气泡。

阶段三:验证与仿真

编写全面的测试平台是验证的关键。测试平台应能自动检查最终结果。

module tb_cpu_top();
    reg clk;
    reg rst_n;
    wire [31:0] debug_wb_pc;      // 写回阶段的PC,用于追踪
    wire        debug_wb_we;      // 写回使能
    wire [4:0]  debug_wb_waddr;   // 写回地址
    wire [31:0] debug_wb_wdata;   // 写回数据
    // 实例化CPU
    cpu_top u_cpu_top(...); // 连接所有端口
    // 时钟生成
    initial begin
        clk = 0;
        forever #10 clk = ~clk; // 50MHz时钟周期
    end
    // 复位与测试
    initial begin
        rst_n = 0;
        #100 rst_n = 1;         // 释放复位
        #2000;                  // 运行足够时钟周期
        // 自动检查结果
        if (u_cpu_top.u_regfile.rf[1] == 32‘h0000000C &amp;&amp;
            u_cpu_top.u_regfile.rf[2] == 32‘h00000048) begin
            $display("PASS: Reg[1]=0x%h, Reg[2]=0x%h",
                     u_cpu_top.u_regfile.rf[1], u_cpu_top.u_regfile.rf[2]);
        end else begin
            $display("FAIL: Reg[1]=0x%h, Reg[2]=0x%h",
                     u_cpu_top.u_regfile.rf[1], u_cpu_top.u_regfile.rf[2]);
            $finish;
        end
        $finish;
    end
endmodule

注意:通过层次化引用(如u_cpu_top.u_regfile.rf[1])直接检查寄存器内存数组,这是一种高效的结果验证方式。在波形窗口中,应重点观察:PC的递增与跳转、指令在流水线中的流动、数据前递事件、Stall/Flush信号的有效周期以及最终的寄存器写入。

原理与设计说明

本设计在简单性与效率之间做了如下权衡:

  • 五级流水线 vs 单周期:采用流水线旨在提高指令吞吐率(IPC接近1),但引入了冒险处理的复杂性。相比更深的流水线,五级在控制逻辑复杂度和性能提升之间取得了较好平衡,适合教学和入门。
  • 数据前递 vs 纯停顿:实现完整的前递逻辑(包括EX和MEM到EX的前递)可以消除大部分RAW冒险导致的停顿,显著提升性能,代价是增加了多路选择器和比较逻辑的资源开销。对于简易CPU,这是值得的。
  • 同步复位 vs 异步复位:本设计采用业界FPGA设计推荐的同步复位(或异步复位、同步释放)。这确保了复位信号像其他数据信号一样被时钟域安全地捕获,避免了复位撤除时可能发生的亚稳态问题,并有利于静态时序分析。
  • 分布式控制 vs 集中式控制:控制信号(ALU操作码、寄存器写使能等)在译码阶段产生,并随指令沿流水线传递。这是一种分布式控制,逻辑清晰,易于扩展新指令。集中式控制器(硬连线)通常性能更高,但修改更困难。
标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/34158.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
42816.68W3.90W3.67W
分享:
成电国芯FPGA赛事课即将上线
2026年FPGA入门:零基础如何用4个月掌握数字电路与Verilog核心
2026年FPGA入门:零基础如何用4个月掌握数字电路与Verilog核心上一篇
基于Verilog的5级流水线简易RISC CPU设计实现指南下一篇
基于Verilog的5级流水线简易RISC CPU设计实现指南
相关文章
总数:445
FPGA实现DDR5控制器:高速接口设计与信号完整性考量

FPGA实现DDR5控制器:高速接口设计与信号完整性考量

本文档旨在为FPGA工程师提供一套完整的、可实施的DDR5控制器实现方案…
技术分享
6天前
0
0
32
0
AI大模型训练芯片Chiplet互连设计与验证指南(2026)

AI大模型训练芯片Chiplet互连设计与验证指南(2026)

随着AI大模型参数规模突破万亿量级,单颗芯片面临的算力与内存墙瓶颈日益严…
技术分享
1天前
0
0
11
0
FPGA时序与并行计算快速上手指南:理科思维迁移实践

FPGA时序与并行计算快速上手指南:理科思维迁移实践

对于具备数学、物理等理科背景的学习者而言,转向FPGA开发并非从零开始。…
技术分享
2小时前
0
0
1
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容