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 Manager → Auto Connect → Program 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。 - 排查方法:仿真时打印
state与pc,观察是否按 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_core到clk_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_p和clk_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_a和reg_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 更节省资源且不影响时序(读延迟可通过流水线吸收)。



