FPGA毕业设计选题:基于RISC-V的简单处理器设计与验证

FPGA小白
文章2026-07-09
22 0 0 0

Quick Start

  1. 准备环境:安装 Vivado 2024.1(或更新版本),确保包含 RISC-V 工具链(如 riscv64-unknown-elf-gcc)与仿真器(Vivado Simulator 或 ModelSim)。
  2. 获取模板工程:从 GitHub 克隆一个最小 RISC-V RV32I 核(如 picorv32 或 serv),解压至工作目录。
  3. 创建 Vivado 工程:打开 Vivado → Create Project → 选择目标器件(如 XC7A35T-1CSG324C)→ 添加所有 RTL 源文件(.v/.sv)与约束文件(.xdc)。
  4. 运行综合(Synthesis):点击 Run Synthesis,等待完成。检查无关键警告(如未连接端口或组合环路)。
  5. 运行实现(Implementation):点击 Run Implementation,完成后查看资源利用率(LUT/FF/BRAM)与时序裕量(WNS ≥ 0)。
  6. 编写测试程序:用 C 语言写一个简单的循环(如计算斐波那契数列),用 riscv64-unknown-elf-gcc 编译为二进制文件(-march=rv32i -mabi=ilp32)。
  7. 加载程序到仿真:将二进制文件通过 $readmemh 或 $readmemb 加载到指令存储器(BRAM)中,运行行为仿真。
  8. 观察波形:在仿真波形中检查 PC 递增、指令译码、寄存器写回等信号,确认 CPU 正确执行程序。
  9. 上板验证(可选):将程序固化到 BRAM,通过 UART 输出结果(如斐波那契数列第 N 项),用串口助手查看。

验收点:仿真波形中 PC 从 0x0000 开始递增,寄存器 x1 在正确时钟周期更新为预期值;上板后串口输出与软件仿真一致。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35T(如 Nexys A7)其他 7 系列或 Spartan-6;若资源紧张可选 Lattice iCE40
EDA 版本Vivado 2024.1(示例配置)Vivado 2023.x 或 ISE(仅限 Spartan-6)
仿真器Vivado Simulator(xsim)ModelSim/QuestaSim 或 Verilator(仅仿真)
时钟/复位板载 100MHz 差分时钟(或 50MHz 单端)外部晶振通过 MMCM 降频至 50MHz
接口依赖UART(115200 baud)用于输出GPIO 驱动 LED 显示状态
约束文件至少包含主时钟周期约束(create_clock)若仅仿真可省略
RISC-V 工具链riscv64-unknown-elf-gcc(版本 12.2.0 以上)LLVM/Clang 或预编译二进制(如 SiFive 工具链)
调试工具Vivado Logic Analyzer(ILA)OpenOCD + GDB(需 JTAG 接口)

注意:以上为典型配置。实际工程中请以所选板卡的数据手册为准,特别是时钟频率与 BRAM 容量(示例中 XC7A35T 有 50 个 BRAM,每个 36Kb)。

目标与验收标准

  • 功能点:实现 RV32I 基础指令集(至少包含算术、逻辑、分支、访存指令),支持 5 级流水线(取指、译码、执行、访存、写回)或单周期实现。
  • 性能指标:Fmax ≥ 50MHz(示例配置);CPI(每指令周期数)对于单周期设计为 1,流水线设计约 1.2(含分支预测失败惩罚)。
  • 资源占用:LUT ≤ 2000,FF ≤ 1500,BRAM ≤ 4(示例配置,视实现复杂度而定)。
  • 验收方式:
    • 仿真:运行至少 10 条不同指令的测试程序,波形显示 PC 正确递增,写回寄存器值符合预期。
    • 上板:通过 UART 输出“Hello World”或简单计算结果,与软件仿真一致。
    • 时序:Vivado 实现后报告 WNS ≥ 0(最差负时序裕量非负)。

实施步骤

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

创建以下目录结构:

riscv_core/
├── rtl/
│   ├── riscv_top.v          # 顶层模块,包含 CPU 核与存储器
│   ├── riscv_core.v         # 流水线控制逻辑
│   ├── riscv_decode.v       # 指令译码器
│   ├── riscv_execute.v      # 执行单元(ALU)
│   ├── riscv_mem.v          # 访存接口
│   ├── riscv_wb.v           # 写回逻辑
│   └── riscv_regfile.v      # 寄存器文件(32 x 32bit)
├── sim/
│   ├── tb_riscv_top.v       # 测试平台
│   └── test_program.hex     # 编译后的指令存储器初始化文件
├── constraints/
│   └── riscv_top.xdc        # 时钟与 I/O 约束
└── scripts/
    └── run.tcl              # 自动化运行脚本

逐行说明

  • 第 1 行:项目根目录名称,建议与设计一致。
  • 第 2-8 行:RTL 源文件,每个模块对应一个独立文件,便于综合与调试。
  • 第 9-11 行:仿真文件,包括测试平台与十六进制程序文件。
  • 第 12-13 行:约束与脚本,用于时序约束与自动化运行。

顶层模块示例(riscv_top.v):

module riscv_top (
    input  wire        clk,
    input  wire        rst_n,
    output wire [31:0] uart_tx_data
);

    // 内部信号
    wire [31:0] pc, instr;
    wire [31:0] alu_result, mem_data;
    wire        mem_write;

    // 实例化 CPU 核
    riscv_core u_core (
        .clk        (clk),
        .rst_n      (rst_n),
        .pc         (pc),
        .instr      (instr),
        .alu_result (alu_result),
        .mem_data   (mem_data),
        .mem_write  (mem_write)
    );

    // 指令存储器(BRAM)
    dist_mem_gen_0 u_imem (
        .clka  (clk),
        .wea   (1'b0),
        .addra (pc[15:2]),  // 字对齐
        .dina  (32'b0),
        .douta (instr)
    );

    // UART 输出(简化)
    assign uart_tx_data = alu_result;

endmodule

逐行说明

  • 第 1 行:模块声明,端口包括时钟、复位和 UART 输出。
  • 第 2-4 行:输入输出端口定义,clk 和 rst_n 为全局信号。
  • 第 6-10 行:内部连线,连接 CPU 核与存储器。
  • 第 12-19 行:实例化 CPU 核,传递端口。
  • 第 21-27 行:指令存储器使用 Xilinx 分布式存储器 IP(或 BRAM),地址按字对齐(pc[15:2])。
  • 第 29-30 行:简化 UART 输出,直接输出 ALU 结果。

阶段二:关键模块实现(以译码器为例)

译码器模块(riscv_decode.v):

module riscv_decode (
    input  wire [31:0] instr,
    output reg  [6:0]  opcode,
    output reg  [2:0]  funct3,
    output reg  [6:0]  funct7,
    output reg  [4:0]  rs1_addr,
    output reg  [4:0]  rs2_addr,
    output reg  [4:0]  rd_addr,
    output reg         reg_write
);

    always @(*) begin
        opcode   = instr[6:0];
        funct3   = instr[14:12];
        funct7   = instr[31:25];
        rs1_addr = instr[19:15];
        rs2_addr = instr[24:20];
        rd_addr  = instr[11:7];
        reg_write = (opcode == 7'b0110011) ||  // R-type
                    (opcode == 7'b0010011) ||  // I-type
                    (opcode == 7'b0000011) ||  // Load
                    (opcode == 7'b1100111);    // JALR
    end

endmodule

逐行说明

  • 第 1 行:模块声明,输入为 32 位指令。
  • 第 2-9 行:输出端口,包括 opcode、funct3 等字段,以及寄存器写使能。
  • 第 11-18 行:组合逻辑块,从指令中提取各字段。注意:RV32I 指令格式固定,字段位置由 RISC-V 规范定义。
  • 第 19-23 行:根据 opcode 判断是否写回寄存器。这里只列出了部分指令类型,实际需覆盖所有 RV32I 指令。

阶段三:时序与约束

约束文件(riscv_top.xdc)示例:

create_clock -period 20.000 -name sys_clk [get_ports clk]
set_input_delay -clock sys_clk 2.0 [get_ports rst_n]
set_output_delay -clock sys_clk 2.0 [get_ports uart_tx_data]

逐行说明

  • 第 1 行:创建 50MHz 时钟(周期 20ns),绑定到 clk 端口。
  • 第 2 行:设置输入延迟,确保复位信号在时钟沿前稳定。
  • 第 3 行:设置输出延迟,保证 UART 数据在时钟沿后有效。

常见坑与排查:

  • 时序违例:若 WNS 为负,检查关键路径(通常在 ALU 或寄存器文件)。可插入流水线寄存器或减少组合逻辑级数。
  • BRAM 初始化失败:确保 .hex 文件格式正确(每行一个 32 位十六进制数),且路径在 Vivado 中正确设置。

阶段四:验证与上板

测试平台(tb_riscv_top.v):

module tb_riscv_top;
    reg  clk, rst_n;
    wire [31:0] uart_tx_data;

    riscv_top uut (.*);

    initial begin
        clk = 0;
        forever #10 clk = ~clk;  // 50MHz
    end

    initial begin
        rst_n = 0;
        #100 rst_n = 1;
        #2000 $finish;
    end

    initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb_riscv_top);
    end
endmodule

逐行说明

  • 第 1 行:测试平台模块,无端口。
  • 第 2-3 行:声明激励与观测信号。
  • 第 5 行:实例化顶层模块,使用 .* 连接所有端口。
  • 第 7-9 行:生成 50MHz 时钟,周期 20ns。
  • 第 11-14 行:复位逻辑,先拉低 100ns 后释放,仿真运行 2000ns 后结束。
  • 第 16-19 行:生成 VCD 波形文件,用于调试。

上板步骤:

  • 生成 Bitstream → Open Target → Program Device。
  • 连接 UART 到 PC,使用串口助手(如 Putty)以 115200 波特率查看输出。
  • 若输出异常,检查 UART 模块的波特率生成器是否与约束一致。

原理与设计说明

为什么选择 RV32I 作为起点?

  • 指令集简洁:RV32I 仅 47 条指令,编码规整,适合教学与初学。
  • 生态成熟:有大量开源实现(如 picorv32、serv)与工具链支持。
  • 扩展性:后续可添加乘除法(M 扩展)、原子操作(A 扩展)或浮点(F 扩展)。

关键 trade-off:单周期 vs 流水线

指标单周期5 级流水线
CPI1≈1.2(含分支惩罚)
Fmax较低(约 30-50MHz)较高(约 80-150MHz)
资源较少(LUT < 1000)较多(LUT 约 1500-2500)
设计复杂度低中(需处理数据/控制冒险)
适合场景毕业设计基础版进阶版或性能要求高

为什么流水线设计需要处理冒险?

流水线中,后一条指令可能依赖前一条指令的结果(数据冒险),或分支指令改变 PC(控制冒险)。常用方案:插入气泡(暂停流水线)或转发(forwarding)。转发通过将 ALU 结果直接送到执行阶段输入,减少暂停周期。例如:

// 转发逻辑示例
if (ex_mem_reg_write && (ex_mem_rd_addr == id_ex_rs1_addr)) begin
    alu_src1 = ex_mem_alu_result;
end

逐行说明

  • 第 1 行:检查执行/访存阶段的寄存器写使能是否有效,且目标寄存器地址与译码阶段的源寄存器地址匹配。
  • 第 2 行:若匹配,则将 ALU 结果直接转发到执行阶段的输入,避免从寄存器文件读取旧值。

验证与结果

仿真结果示例(基于单周期实现,测试程序为斐波那契数列前 10 项):

指令PC寄存器 x1 值预期值
addi x1, x0, 00x00000x000000000x00000000
addi x1, x1, 10x00040x000000010x00000001
addi x2, x0, 10x00080x000000010x00000001
add x3, x1, x20x000C0x000000020x00000002
…………

资源与性能(示例配置,Vivado 2024.1 综合结果):

指标单周期5 级流水线
LUT8501800
FF4201100
BRAM22
Fmax62 MHz105 MHz
CPI1.01.15

测量条件:时钟约束 50MHz,目标器件 XC7A35T-1CSG324C,测试程序为 100 条指令的简单循环。以上数值为示例,实际结果以综合实现报告为准。

故障排查(Troubleshooting)

现象:上板后 UART 无输出 → 原因
    现象:仿真波形中 PC 不递增 → 原因:复位未释放或时钟未翻转 → 检查:rst_n 是否在仿真开始后拉高,clk 是否生成 → 修复:确认测试平台中复位逻辑与时钟生成。现象:指令存储器输出全为 X → 原因:.hex 文件未正确加载或路径错误 → 检查:Vivado 中 memory initialization file 路径是否绝对 → 修复:使用相对路径,并确认文件格式为每行一个 32 位十六进制数。现象:ALU 结果始终为 0 → 原因:译码器未正确解析 opcode 或 ALU 控制信号错误 → 检查:仿真波形中 opcode 与 funct3 字段是否与指令对应 → 修复:对照 RISC-V 规范检查译码逻辑。现象:上板后 UART 无输出 → 原因
分类
技术分享
标签
fpgaRISC-V处理器设计
浏览 22收藏 0赞 0评论 0
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

FPGA小白查看主页

同分类阅读

文章

延伸阅读与实操

  • 文章 + 课程联动深度文章常对应体系课章节,可一键选课。
  • 学习产出可参考笔记与作业案例在学习产出广场持续更新。

探索全站