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

FPGA项目实战:基于Verilog的简易CPU设计全流程

二牛学FPGA二牛学FPGA
技术分享
1小时前
0
0
3

Quick Start

  • 准备环境:安装 Vivado 2023.2(或更高版本),下载 Xilinx Artix-7 系列板卡(如 Nexys A7-100T)的板级支持包。
  • 获取工程模板:从课程资源库(或 GitHub 仓库)下载 simple_cpu 工程压缩包,解压至本地无中文路径的文件夹。
  • 打开工程:启动 Vivado → Open Project → 选择 simple_cpu.xpr
  • 运行综合:在 Flow Navigator 中点击 Run Synthesis,等待综合完成(约 2-5 分钟,取决于机器性能)。
  • 运行实现:综合成功后,点击 Run Implementation,完成布局布线。
  • 生成比特流:实现完成后,点击 Generate Bitstream
  • 连接板卡:将 Nexys A7 通过 USB 线连接至 PC,打开电源开关(SW16 拨至 ON)。
  • 下载程序:在 Vivado 中点击 Open Hardware ManagerAuto ConnectProgram Device,选择生成的 .bit 文件并烧录。
  • 观察现象:板卡上 7 段数码管应循环显示数字 0-9,LED 阵列按预设模式闪烁(验证 CPU 执行简单程序)。
  • 验收点:若数码管显示正确,且无时序违例(Setup/Hold Slack > 0),则 Quick Start 成功。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A100T (Nexys A7-100T)XC7A35T (Nexys A7-35T) 或 Spartan-7 系列,需调整引脚约束
EDA 版本Vivado 2023.2 (HLx Edition)Vivado 2022.2 或 2024.1(需注意 IP 核兼容性)
仿真器Vivado Simulator (xsim)ModelSim/QuestaSim(需编译库)
时钟/复位板载 100MHz 差分晶振(P/N 引脚)外部时钟源,需修改 MMCM 配置
接口依赖UART (USB-UART 桥接)、7 段数码管、LED 阵列若使用其他板卡,需重写引脚约束
约束文件simple_cpu.xdc(包含时钟周期、I/O 标准)无替代,必须根据板卡调整
RAM/ROM 初始内容program.hex(汇编程序机器码)可用 $readmemh 加载自定义程序
调试工具ILA (Integrated Logic Analyzer) IP 核逻辑分析仪(如 Saleae)或串口打印

目标与验收标准

本项目的目标是设计并实现一个基于 Verilog 的 8 位简易 CPU,支持基本算术/逻辑运算、条件跳转与内存访问。验收标准如下:

  • 功能点:CPU 能正确执行预编译的测试程序(循环显示 0-9、LED 跑马灯),通过 UART 输出调试信息。
  • 性能指标:主频 ≥ 50 MHz(典型值 100 MHz),无时序违例(Setup Slack > 0.1 ns)。
  • 资源占用:LUT ≤ 800,FF ≤ 600,BRAM ≤ 2 个(18Kb 模式),DSP 可选。
  • 关键波形:仿真中 pc(程序计数器)递增正确,alu_result 在算术指令后更新,mem_wr_en 在写指令时拉高。
  • 日志验证:Vivado 实现报告无严重警告(Critical Warning),时序报告显示 WNS(最差负时序裕量)≥ 0。

实施步骤

阶段一:工程结构与顶层模块

创建 Vivado 工程,添加以下源文件(路径示例:src/rtl/):

simple_cpu_top.v    // 顶层模块,例化 CPU 核心与外围(时钟、复位、UART、数码管)
cpu_core.v          // CPU 核心:取指、译码、执行、写回
alu.v               // 算术逻辑单元
register_file.v     // 寄存器堆(8×8-bit)
program_memory.v    // 指令存储器(ROM,加载 hex 文件)
data_memory.v       // 数据存储器(RAM,单端口)
clk_wiz_0.xci       // 时钟 IP(MMCM,100MHz 输入→50MHz 核心时钟)
simple_cpu.xdc      // 引脚约束

逐行说明

  • 第 1 行:顶层模块 simple_cpu_top 负责例化所有子模块,并处理板级接口(差分时钟输入、复位按钮、UART TX/RX、7 段数码管位选与段选)。
  • 第 2 行cpu_core 是 CPU 的数据通路与控制逻辑,包含程序计数器(PC)、指令寄存器(IR)、状态机(FSM)等。
  • 第 3 行:ALU 实现 8 种运算:加、减、与、或、异或、左移、右移、比较(等于/大于)。
  • 第 4 行:寄存器堆提供 8 个 8 位寄存器(R0-R7),支持同步写、异步读。
  • 第 5 行:程序存储器使用 BRAM 实现,通过 $readmemh 加载 program.hex,只读。
  • 第 6 行:数据存储器也是 BRAM,单端口,支持读写。
  • 第 7 行:时钟 IP 将 100MHz 差分输入转换为 50MHz 单端时钟,并产生锁相环锁定信号 locked 用于复位逻辑。
  • 第 8 行:约束文件定义所有 I/O 引脚位置与电平标准,以及时钟周期(20 ns 对应 50 MHz)。

阶段二:CPU 核心模块(cpu_core.v)

核心模块包含取指(IF)、译码(ID)、执行(EX)、写回(WB)四个阶段,采用多周期(非流水线)设计以降低复杂度。关键代码片段如下:

module cpu_core (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [7:0] pc,          // 程序计数器
    input  wire [7:0] instr,       // 指令输入(来自 ROM)
    output reg  [7:0] alu_result,  // ALU 结果
    output reg        mem_wr_en,   // 数据存储器写使能
    output reg  [7:0] mem_addr,    // 数据存储器地址
    output reg  [7:0] mem_wr_data, // 数据存储器写数据
    input  wire [7:0] mem_rd_data  // 数据存储器读数据
);

// 内部寄存器
reg [2:0] state;  // FSM 状态
reg [7:0] ir;     // 指令寄存器
reg [7:0] reg_a, reg_b; // 操作数暂存

// 状态机参数
localparam IF = 3'd0, ID = 3'd1, EX = 3'd2, WB = 3'd3;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        pc <= 8'd0;
        state <= IF;
        ir <= 8'd0;
        alu_result <= 8'd0;
        mem_wr_en <= 1'b0;
    end else begin
        case (state)
            IF: begin
                ir <= instr;          // 取指令
                state <= ID;
            end
            ID: begin
                // 译码:根据 ir[7:4] 判断操作类型
                // 从寄存器堆读取操作数(此处简化,直接使用 ir[3:0] 作为立即数或地址)
                reg_a <= register_file[ir[3:2]];  // 假设 ir[3:2] 为寄存器索引
                reg_b <= register_file[ir[1:0]];
                state <= EX;
            end
            EX: begin
                case (ir[7:4])
                    4'b0000: alu_result <= reg_a + reg_b;  // ADD
                    4'b0001: alu_result <= reg_a - reg_b;  // SUB
                    // 其他运算省略
                endcase
                // 若为内存写指令(如 STORE),设置 mem_wr_en
                if (ir[7:4] == 4'b1000) begin
                    mem_wr_en <= 1'b1;
                    mem_addr <= reg_b;
                    mem_wr_data <= reg_a;
                end else begin
                    mem_wr_en <= 1'b0;
                end
                state <= WB;
            end
            WB: begin
                // 写回:将 alu_result 写入目标寄存器(若为 ALU 指令)
                if (ir[7:4] != 4'b1000) begin  // 非 STORE 指令才写回
                    register_file[ir[3:2]] <= alu_result;
                end
                // 更新 PC(默认 +1,跳转指令由译码阶段修改)
                pc <= pc + 1;
                state <= IF;
            end
        endcase
    end
end

endmodule

逐行说明

  • 第 1-14 行:模块端口声明。pc 为程序计数器输出(便于调试),instr 来自 ROM,alu_result 等连接数据存储器。注意 mem_wr_en 为寄存器输出,避免组合逻辑毛刺。
  • 第 16-20 行:内部寄存器定义。state 为 3 位状态机,ir 存储当前指令,reg_a/reg_b 暂存操作数。所有寄存器在时钟上升沿更新,符合同步设计。
  • 第 22 行:状态机参数,定义四个阶段:取指、译码、执行、写回。注意状态编码使用独热码可优化 Fmax,但此处为简化使用二进制编码。
  • 第 24-59 行:主时序逻辑。复位时清零 PC、状态机、指令寄存器等。正常运行时,每个时钟周期完成一个阶段(共 4 周期/指令)。
  • 第 28-31 行:IF 阶段:将 ROM 输出 instr 锁存到 ir,下一周期进入 ID。注意 instr 必须在本周期有效(ROM 读延迟为 1 周期,需在顶层处理好)。
  • 第 32-36 行:ID 阶段:从寄存器堆读取操作数。此处使用组合逻辑读(register_file[addr] 为 wire 类型),实际实现中寄存器堆应为异步读、同步写。
  • 第 37-49 行:EX 阶段:根据指令高 4 位执行运算。示例仅给出 ADD/SUB,其他运算类似。对于 STORE 指令(4'b1000),设置内存写使能及地址/数据。
  • 第 50-58 行:WB 阶段:将 ALU 结果写回寄存器堆(非 STORE 指令)。PC 自动加 1(跳转指令需在 ID 阶段修改 PC,此处未实现)。状态机回到 IF 开始下一条指令。

常见坑与排查(阶段二)

  • 坑 1:寄存器堆写冲突。在 WB 阶段写回时,若同一周期内读操作数(ID 阶段)也访问同一地址,会发生读旧值而非新值。解决方案:使用写优先(write-first)的 BRAM 模式,或增加写后读旁路(bypass)。
  • 坑 2:状态机死锁。若复位信号 rst_n 毛刺或异步复位释放时序不满足,状态机可能进入非法状态。检查:复位同步器(至少两级触发器)与复位释放时间 > 100 ns。
  • 排查方法:仿真时打印 statepc,观察是否按 IF→ID→EX→WB 循环。若卡在某状态,检查对应阶段的组合逻辑是否有 latch(如 case 未覆盖全)。

阶段三:时序约束与 CDC

约束文件 simple_cpu.xdc 关键内容:

# 时钟约束
create_clock -period 20.000 [get_ports clk_p]

# 生成时钟(MMCM 输出)
create_generated_clock -name clk_core -source [get_pins clk_wiz_0/inst/mmcm_adv_inst/CLKIN1] \
    -divide_by 2 [get_pins clk_wiz_0/inst/mmcm_adv_inst/CLKOUT0]

# 输入延迟(假设外部器件最大延迟 2 ns)
set_input_delay -clock [get_clocks clk_core] -max 2.0 [get_ports {rst_n}]

# 输出延迟
set_output_delay -clock [get_clocks clk_core] -max 4.0 [get_ports {uart_tx}]

# 伪路径:跨时钟域信号(如 UART 接收)
set_false_path -from [get_clocks clk_core] -to [get_clocks clk_uart]

逐行说明

  • 第 1-2 行:定义主时钟 clk_p(差分输入正端),周期 20 ns(50 MHz)。注意差分时钟只需约束正端,负端自动处理。
  • 第 4-5 行:定义 MMCM 生成的 clk_core(50 MHz),源时钟为 MMCM 输入引脚,分频系数 2。此约束使 Vivado 能正确分析核心逻辑的时序。
  • 第 7-8 行:输入延迟约束,告知工具外部信号 rst_n 相对于时钟的延迟最大值。若未约束,工具会假设理想输入,导致过乐观结果。
  • 第 10-11 行:输出延迟约束,确保 UART TX 信号在外部接收器建立时间前稳定。
  • 第 13-14 行:伪路径声明,忽略 clk_coreclk_uart 的跨时钟域路径(UART 接收使用独立时钟域,通过双触发器同步)。

阶段四:验证与仿真

编写测试平台 tb_simple_cpu.v,例化顶层并加载程序。关键步骤:

module tb_simple_cpu;

reg clk_p, clk_n;
reg rst_n;
wire uart_tx;

// 生成差分时钟
always begin
    clk_p = 1'b0; #5; clk_p = 1'b1; #5;  // 100 MHz 周期 10 ns
end
assign clk_n = ~clk_p;

initial begin
    rst_n = 1'b0;
    #100 rst_n = 1'b1;  // 复位 100 ns
    #2000;
    $finish;
end

// 例化顶层
simple_cpu_top u_dut (
    .clk_p(clk_p),
    .clk_n(clk_n),
    .rst_n(rst_n),
    .uart_tx(uart_tx)
);

// 监控信号
initial begin
    $monitor("Time=%0t pc=%d instr=%h alu_result=%h", $time, u_dut.cpu_core.pc, u_dut.cpu_core.ir, u_dut.cpu_core.alu_result);
end

endmodule

逐行说明

  • 第 1-5 行:测试平台端口声明。注意差分时钟需要两个 reg 类型变量(clk_pclk_n),顶层模块期望差分输入。
  • 第 7-10 行:生成 100 MHz 差分时钟:每 5 ns 翻转一次,周期 10 ns(100 MHz)。clk_n 取反得到。
  • 第 12-16 行:复位逻辑:初始低电平,100 ns 后释放。仿真运行 2000 ns 后结束。
  • 第 18-24 行:例化顶层模块,连接所有端口。注意差分时钟的接法。
  • 第 26-28 行:监控关键信号:$monitor 在每次信号变化时打印时间、PC、指令和 ALU 结果,便于调试。

常见坑与排查(阶段四)

  • 坑 1:仿真中 PC 不递增。原因:状态机卡在 IF 阶段,检查 instr 是否有效(ROM 读延迟导致第一周期为 X)。解决:在顶层中,ROM 输出需在 IF 阶段之前已稳定,可增加一个周期的流水线延迟。
  • 坑 2:ALU 结果恒为 0。原因:译码阶段未正确读取寄存器堆(如地址位宽不匹配)。检查:打印 reg_areg_b 值,确认是否为预期操作数。

原理与设计说明

本设计采用多周期非流水线架构,而非单周期或流水线,原因如下:

  • 资源 vs Fmax:单周期 CPU 需要在一个时钟内完成取指、译码、执行、写回,组合逻辑路径极长,Fmax 通常低于 30 MHz(以 Artix-7 为例)。多周期将路径拆分为 4 个阶段,每阶段逻辑深度降低,Fmax 可达 100 MHz 以上。
  • 吞吐 vs 延迟:多周期 CPI(每指令周期数)为 4,而单周期 CPI=1。但多周期允许更高主频,实际吞吐量(指令/秒)可能更高。对于教学目的,4 CPI 足够展示 CPU 内部状态。
  • 易用性 vs 可移植性:非流水线设计无需处理数据冒险与控制冒险,适合初学者理解。若需移植到其他器件(如 Lattice、Intel),只需修改时钟 IP 与引脚约束,核心逻辑不变。

关键权衡:寄存器堆实现。本设计使用 BRAM 实现寄存器堆,而非分布式 RAM(LUT)。BRAM 提供双端口读写,但读延迟为 1 周期。若使用分布式 RAM,读延迟为 0(组合),但会消耗更多 LUT(8×8 寄存器约 64 LUT vs 1 个 BRAM)。对于 8 个寄存器,BRAM 更节省资源且不影响时序(读延迟可通过流水线吸收)。

验证与结果

标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/43463.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
1.11K21.55W4.12W3.67W
分享:
成电国芯FPGA赛事课即将上线
2026年Q2:国产FPGA生态崛起,对新手选型的影响
2026年Q2:国产FPGA生态崛起,对新手选型的影响上一篇
FPGA 学习路线指南:Verilog 与 VHDL 先学哪个更高效?——基于 2026 年 5 月行业生态的快速评估与实施手册下一篇
FPGA 学习路线指南:Verilog 与 VHDL 先学哪个更高效?——基于 2026 年 5 月行业生态的快速评估与实施手册
相关文章
总数:1.17K
FPGA仿真死锁自动化检测:基于SystemVerilog的监控模块设计与实施指南

FPGA仿真死锁自动化检测:基于SystemVerilog的监控模块设计与实施指南

QuickStart准备环境:安装支持SystemVerilog的仿真…
技术分享
11天前
0
0
25
0
FPGA图像处理:基于Verilog的直方图均衡化设计与实现指南

FPGA图像处理:基于Verilog的直方图均衡化设计与实现指南

QuickStart(快速上手)安装Vivado2020.1及以…
技术分享
14天前
0
0
25
0
FPGA中同步FIFO与异步FIFO的实现差异与资源对比指南

FPGA中同步FIFO与异步FIFO的实现差异与资源对比指南

QuickStart:快速上手本指南面向FPGA设计工程师,旨在帮助您…
技术分享
20天前
0
0
35
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容