news 2026/4/27 14:12:53

RISC-V CPU实战——Quartus Prime下PicoRV32软核的Verilog实现与仿真调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RISC-V CPU实战——Quartus Prime下PicoRV32软核的Verilog实现与仿真调试

1. 从零开始:为什么选择PicoRV32与Quartus Prime?

如果你和我一样,是个对RISC-V CPU设计充满好奇的FPGA爱好者,但又觉得那些复杂的SoC项目让人望而却步,那么PicoRV32绝对是你入门的最佳选择。我第一次接触它的时候,就被它的“小而美”打动了。PicoRV32是一个开源的、采用RISC-V指令集架构的32位微处理器软核,代码简洁到只有一个Verilog文件(picorv32.v),但五脏俱全,支持RV32IMC指令子集,意味着基础的整数、乘法和压缩指令它都能处理。这种极简设计带来的最大好处就是可读性极强,你不需要面对动辄几十个模块的庞大工程,可以快速理解一个CPU是如何取指、译码、执行和访存的。

那么,开发环境为什么选Quartus Prime呢?说实话,在开源EDA工具盛行的今天,选择Intel(以前是Altera)的Quartus似乎有点“传统”。但我的理由很实际:资料多、生态稳、对Windows用户友好。尤其是当你手头正好有一块Cyclone或MAX 10系列的开发板时,Quartus提供了从设计、仿真、综合到下载的一站式解决方案。虽然一开始我也纠结过,觉得在Windows下搞RISC-V工具链会不会很麻烦,但实测下来,只要路径配置对了,Quartus配合自带的ModelSim-Altera进行仿真调试,整个流程是可以跑通的。这比为了一个项目去折腾Linux双系统或者WSL要直接得多,毕竟我们的目标是快速验证想法,而不是成为系统配置专家。

当然,这条路并非一帆风顺。我最初的想法很简单:把PicoRV32的源码扔进Quartus工程,写个顶层模块连上内存和GPIO,然后用一个.hex文件把程序灌进去,最后在ModelSim里看到CPU乖乖执行并输出结果。但现实是,我卡在了“加载.hex文件”这个看似基础的环节上很久。仿真时波形图一片空白,编译虽然通过但CPU好像根本没动。这段经历让我明白,在FPGA上玩软核,硬件描述语言(Verilog)是骨架,仿真调试才是灵魂。下面,我就把踩过的坑和最终的解决方案,毫无保留地分享给你。

2. 环境搭建与工程创建:避开那些初见的“坑”

万事开头难,搭建一个能跑起来的环境就成功了一半。这里我会详细列出Windows下的步骤,很多细节是官方教程里不会提的。

2.1 获取源码与工具链:缺一不可

首先,去GitHub上克隆PicoRV32的仓库。直接在命令提示符里运行git clone https://github.com/cliffordwolf/picorv32.git就行。如果网络不畅,也可以直接下载ZIP包。解压后,核心文件就是那个picorv32.v,这就是CPU本体。

接下来是工具链。这是很多新手会忽略或者感到困惑的地方。我们写的C语言或者汇编程序,CPU是看不懂的,必须翻译成二进制的机器码。RISC-V GCC工具链就是这个“翻译官”。你可能会问:“我只是仿真,还没到写C程序那步,需要装吗?”我的经验是:越早装越好。因为即使你最开始用手写机器码,后面迟早要用到它。而且工具链的安装过程能帮你验证环境。我推荐使用 xPack 提供的 Windows 预编译版本,比如xpack-riscv-none-elf-gcc,安装简单,添加到系统PATH后,在终端输入riscv-none-elf-gcc --version能出现版本信息就成功了。

然后是Quartus Prime的安装。建议安装Lite Edition(免费版),它已经包含了ModelSim-Altera的仿真功能。安装时注意两点:一是安装路径不要有中文和空格;二是在安装组件选择时,务必勾选上ModelSim-Altera。安装完成后,打开Quartus,第一件事就是设置ModelSim的路径:点击Tools->Options->General->EDA Tool Options。在“ModelSim-Altera”一项里,指向你的安装路径,通常是C:\intelFPGA\XX.X\modelsim_ase\win32aloem(XX.X是你的Quartus版本号)。这一步没做对,后续仿真根本启动不了。

2.2 创建你的第一个Quartus工程

打开Quartus,File->New Project Wizard。项目名称和顶层模块名我习惯都叫top。在添加文件时,把picorv32.v加进去。然后,我们需要创建自己的顶层文件top.v

这里就是第一个关键点了:如何连接CPU和内存?PicoRV32通过一个简单的内存接口与外界通信。这个接口有几个关键信号:mem_valid(请求有效)、mem_addr(地址)、mem_wdata(写数据)、mem_wstrb(写字节使能,用于控制写哪个字节)、mem_rdata(读数据)。我们的任务就是设计一个内存模块(比如用FPGA内部的BRAM)来响应这些请求。

我最初写的顶层模块结构如下,它包含了一个BRAM和一个简单的GPIO外设:

module top ( input wire clk, input wire resetn, output reg [31:0] gpio_out ); // CPU接口信号 wire mem_valid; wire [31:0] mem_addr; wire [31:0] mem_wdata; wire [3:0] mem_wstrb; reg [31:0] mem_rdata; // 实例化PicoRV32 CPU picorv32 cpu ( .clk(clk), .resetn(resetn), .mem_valid(mem_valid), .mem_addr(mem_addr), .mem_wdata(mem_wdata), .mem_wstrb(mem_wstrb), .mem_rdata(mem_rdata) ); // 声明一个BRAM数组,并用synthesis属性指定初始化文件 reg [31:0] bram[0:1023] /* synthesis ram_init_file = "ramdata.hex" */; // BRAM写逻辑(时序逻辑) always @(posedge clk) begin if (mem_valid && (|mem_wstrb)) begin // 根据mem_wstrb按字节写入 if (mem_wstrb[0]) bram[mem_addr[11:2]][7:0] <= mem_wdata[7:0]; if (mem_wstrb[1]) bram[mem_addr[11:2]][15:8] <= mem_wdata[15:8]; if (mem_wstrb[2]) bram[mem_addr[11:2]][23:16] <= mem_wdata[23:16]; if (mem_wstrb[3]) bram[mem_addr[11:2]][31:24] <= mem_wdata[31:24]; end end // GPIO逻辑(地址映射到0x10000000) localparam GPIO_ADDR = 32'h1000_0000; always @(posedge clk or negedge resetn) begin if (!resetn) gpio_out <= 0; else if (mem_valid && (|mem_wstrb) && (mem_addr == GPIO_ADDR)) gpio_out <= mem_wdata; end // BRAM读逻辑(组合逻辑) always @(*) begin mem_rdata = 0; if (mem_valid && !(|mem_wstrb)) begin // 读请求 if (mem_addr == GPIO_ADDR) mem_rdata = gpio_out; else mem_rdata = bram[mem_addr[11:2]]; end end endmodule

这个代码编译肯定能通过,但如果你直接仿真,会发现CPU可能不工作。因为这里隐藏了两个大坑:第一,BRAM的初始化文件ramdata.hex在仿真时并没有被加载进去。那个/* synthesis ram_init_file */注释是给综合器(Quartus)看的,用于生成FPGA比特流时初始化RAM,但ModelSim仿真器不认识它。第二,PicoRV32的接口比这个简单例子要稍微复杂一点,它还有mem_readymem_instr等信号,需要正确处理。

3. 仿真调试实战:让CPU真正“跑”起来

编译通过只是第一步,看到仿真波形动起来才是胜利。这里我分享两种最实用的方法,特别是第二种,是我调试成功的关键。

3.1 方法一:使用Quartus自带的仿真流程(及常见问题)

Quartus允许你直接启动RTL仿真。步骤是:Assignments->Settings->EDA Tool Settings->Simulation。在这里选择Tool nameModelSim-AlteraFormat for output netlistVerilog HDL。然后点击Tools->Run Simulation Tool->RTL Simulation

理想情况下,ModelSim会自动启动并开始仿真。但新手90%会在这里遇到问题。最常见的就是弹出一个错误框,说“无法启动仿真”或者“找不到可执行文件”。这几乎都是因为前面提到的ModelSim路径没有正确设置。请务必回到Tools->Options里仔细检查。

如果ModelSim成功启动了,但波形窗口里什么都没有,或者只有时钟在跳变,CPU信号全是红色(不定态),那问题就出在程序没有加载进内存。为了解决仿真时加载HEX文件的问题,我们需要修改top.v,使用Verilog的系统任务$readmemh。但要注意,这个任务不能被综合,所以要用 `ifdef 宏定义把它包起来。

// 在top.v模块内部添加 `ifdef SIMULATION initial begin $display("[SIM] Loading ramdata.hex..."); $readmemh("ramdata.hex", bram); end `endif

同时,你需要创建一个ramdata.hex文件放在工程根目录。这里格式很重要:它是一个纯文本文件,每行一个32位的十六进制数,代表一条指令。比如:

93000000 00000013

然后,你需要在Quartus的仿真设置里,添加一个宏定义SIMULATION。路径是:Assignments->Settings->Simulation->More EDA Netlist Writer Settings...。在“设置”表格中,添加Name: SIMULATION, Value: 1。这样,在仿真时,ifdef SIMULATION块里的代码就会生效。

即使这样做了,我当初还是失败了。现象是仿真运行一下马上就结束了,看不到任何过程。这是因为缺少一个持续运行的测试平台(Testbench)。直接仿真顶层模块,没有给时钟和复位信号施加激励,仿真自然就结束了。这引出了更可靠的方法二。

3.2 方法二:编写独立的Testbench文件(推荐)

这是我最终调试成功的方法,也是工业界最常用的仿真方式。我们创建一个独立的top_tb.v文件,它不参与综合,只用于仿真。在这个文件里,我们生成时钟、产生复位序列、实例化我们的设计(DUT),并控制仿真过程。

`timescale 1ns/1ps // 时间单位/精度 module top_tb(); // 声明激励信号 reg clk = 0; reg resetn = 0; wire [31:0] gpio_out; // 实例化被测设计 (DUT) top uut ( .clk(clk), .resetn(resetn), .gpio_out(gpio_out) ); // 生成50MHz时钟:周期20ns,每10ns翻转一次 always #10 clk = ~clk; // 初始化与仿真控制 initial begin // 注意:此时top模块内部的 $readmemh 会负责加载HEX文件 // 我们只需要提供复位信号 resetn = 0; // 开始复位 #100; // 保持100ns resetn = 1; // 释放复位 // 让仿真运行一段时间,例如2000ns #2000; $display("Simulation finished. GPIO out = 0x%h", gpio_out); $stop; // 暂停仿真(在ModelSim中可继续) // $finish; // 结束仿真 end endmodule

接下来是关键操作:

  1. top_tb.v添加到工程。
  2. 设置它为仿真顶层:Assignments->Settings->Simulation-> 在 “Compile test bench” 点击Test Benches...->New。在“Test bench name”里输入top_tb,在“Top level module in test bench”里也输入top_tb,然后通过“File name”后面的...按钮添加你的top_tb.v文件。
  3. 再次运行Tools->Run Simulation Tool->RTL Simulation

这一次,ModelSim应该会运行一段时间,并在控制台打印出结束信息。但是,你可能仍然看不到GPIO输出预期的值。问题出在哪?很可能是因为CPU的接口连接不完整。回头检查我们的top.v,PicoRV32模块其实有mem_readymem_instr输入端口,我们之前悬空了。一个更健壮的实例化应该是这样的:

picorv32 cpu ( .clk (clk), .resetn (resetn), .mem_valid (mem_valid), .mem_instr (), // 可以暂时不连接或接0,用于区分指令/数据访问 .mem_ready (1‘b1), // 关键!告诉CPU内存永远就绪,零等待 .mem_addr (mem_addr), .mem_wdata (mem_wdata), .mem_wstrb (mem_wstrb), .mem_rdata (mem_rdata) );

mem_ready1‘b1是一种简化,假设我们的BRAM访问是单周期的,总是能准备好数据。这样修改后,再仿真,你可能会在波形里看到mem_valid信号偶尔拉高,CPU开始访问内存了!

4. 进阶调试:从指令注入到完整程序加载

当你看到CPU有活动迹象后,就可以开始真正的调试了。我建议分两步走:先确保CPU能执行最简单的指令,再过渡到从文件加载复杂程序。

4.1 第一步:指令注入,验证CPU内核

先不搞复杂的BRAM和HEX文件,我们用一个最直接的方法验证CPU核心是否工作:在Testbench里直接“喂”指令给CPU。这需要修改top.v,去掉真实的BRAM,用一个状态机根据CPU的取指地址,返回预设的指令码。

// 在top.v中,替换掉BRAM读逻辑 reg [1:0] instr_counter; always @(posedge clk or negedge resetn) begin if (!resetn) instr_counter <= 0; else if (mem_valid && !(|mem_wstrb)) // 如果是读请求(取指) instr_counter <= instr_counter + 1; end always @(*) begin case(instr_counter) 2‘d0: mem_rdata = 32’h10000537; // lui x10, 0x10000 (设置GPIO地址) 2‘d1: mem_rdata = 32’hCAFE05B7; // lui x11, 0xCAFE0 (设置数据) 2‘d2: mem_rdata = 32’h00B52023; // sw x11, 0(x10) (存储到GPIO) 2‘d3: mem_rdata = 32’h0000006F; // jal x0, 0 (死循环) default: mem_rdata = 32‘h00000013; // nop endcase end

同时,确保GPIO逻辑是有效的。然后,在top_tb.v中,我们可以添加一些调试打印,甚至将波形保存为VCD文件,便于用GTKWave等工具查看。

// 在top_tb.v的initial块中添加 initial begin $dumpfile("top_tb.vcd"); // 生成VCD波形文件 $dumpvars(0, top_tb); // 转储所有变量 // ... 原有的复位和延时代码 end

运行仿真后,打开生成的top_tb.vcd文件,你应该能清晰地看到程序计数器(PC)的变化、寄存器x10和x11被赋值,最终在GPIO总线上出现0xCAFE0000这个值。这一刻的成就感是无与伦比的——你亲手让一个RISC-V CPU执行了你安排的指令!

4.2 第二步:回归HEX文件,构建最小系统

验证了CPU核心没问题后,我们就可以回归最初的目标:从.hex文件加载程序。这次我们有了成功经验,就知道问题出在仿真与综合的代码隔离以及文件路径上。

首先,确保top.v是“二合一”的版本:既包含用于综合的synthesis ram_init_file属性,也包含用于仿真的ifdef SIMULATION块里的$readmemh

其次,确保ramdata.hex文件中的指令是正确的。你可以先用汇编器写一段简单的程序。例如,创建一个test.s文件:

lui x10, 0x10000 # 加载GPIO地址高位 lui x11, 0xCAFE0 # 加载数据值 sw x11, 0(x10) # 存储到GPIO地址 loop: jal x0, loop # 无限循环

然后使用之前安装的RISC-V工具链编译它:

riscv-none-elf-as -march=rv32i -o test.o test.s riscv-none-elf-objcopy -O verilog test.o test.hex

得到的test.hex文件可能格式需要调整一下,保留每行8个十六进制字符(32位)的格式,将其内容复制到ramdata.hex中。

最后,在ModelSim中仿真时,注意工作目录。ModelSim的工作目录默认可能是它自己的安装路径,而不是你的项目路径。这会导致$readmemh找不到ramdata.hex文件。解决方法有两种:一是在$readmemh中使用绝对路径(不推荐,不利于移植);二是在启动ModelSim后,在Transcript窗口使用cd命令切换到你的项目目录,例如cd C:/your_project_path

当你完成所有这些步骤,再次运行仿真,并在波形中看到CPU从地址0开始,依次读取你HEX文件中的指令并执行,最终GPIO输出预设的值时,恭喜你,你已经成功在Quartus和ModelSim环境下搭建并调试了一个完整的、可运行程序的RISC-V软核系统!这个过程虽然曲折,但每一步遇到的问题和解决方案,都让你对CPU软核、Verilog设计、以及仿真调试的理解加深了一层。这远比直接下载一个别人跑通的工程要有价值得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/27 11:24:14

番茄小说下载器:构建个人数字阅读库的全流程指南

番茄小说下载器&#xff1a;构建个人数字阅读库的全流程指南 【免费下载链接】Tomato-Novel-Downloader 番茄小说下载器不精简版 项目地址: https://gitcode.com/gh_mirrors/to/Tomato-Novel-Downloader 在数字阅读日益普及的今天&#xff0c;如何高效获取、管理和利用网…

作者头像 李华
网站建设 2026/4/18 21:23:24

Hunyuan-MT-7B效果展示:Flores-200测试集关键语种翻译截图集

Hunyuan-MT-7B效果展示&#xff1a;Flores-200测试集关键语种翻译截图集 1. 模型能力概览 Hunyuan-MT-7B是腾讯混元团队在2025年9月开源的多语言翻译模型&#xff0c;拥有70亿参数&#xff0c;专门针对多语言翻译场景优化。这个模型最吸引人的特点是&#xff1a;只需要16GB显…

作者头像 李华
网站建设 2026/4/18 21:20:44

Qt+MAI-UI-8B:跨平台桌面应用开发实战

QtMAI-UI-8B&#xff1a;跨平台桌面应用开发实战 1. 引言 想象一下&#xff0c;你正在开发一个桌面应用&#xff0c;用户可以直接用自然语言告诉应用该做什么&#xff1a;"帮我把这份文档转换成PDF&#xff0c;然后发邮件给客户"&#xff0c;而不是在菜单里一层层找…

作者头像 李华
网站建设 2026/4/18 22:14:57

华中科技大学-计算机组成原理实验-单总线CPU设计与实现

1. 从零开始&#xff1a;为什么单总线CPU是理解计算机心脏的最佳起点 我记得自己第一次接触计算机组成原理实验时&#xff0c;面对一堆密密麻麻的芯片和导线&#xff0c;头都大了。直到后来做了单总线CPU的设计实验&#xff0c;才真正有种“开窍”的感觉。华中科技大学的这个实…

作者头像 李华
网站建设 2026/4/18 21:20:47

ESXi主机升级失败排查与解决指南(一)

1. 从一次真实的升级失败说起&#xff1a;你的ESXi升级卡住了吗&#xff1f; 前几天&#xff0c;我正准备把实验室里一台老伙计——一台运行着ESXi 6.5的戴尔R740服务器——升级到更新的版本。这听起来是个常规操作&#xff0c;对吧&#xff1f;备份好虚拟机&#xff0c;下载好…

作者头像 李华
网站建设 2026/4/18 21:20:46

Hunyuan-MT 7B在软件测试中的应用:多语言测试用例生成

Hunyuan-MT 7B在软件测试中的应用&#xff1a;多语言测试用例生成 1. 引言 在全球化软件开发的今天&#xff0c;多语言支持已经成为产品成功的关键因素。然而&#xff0c;传统的软件测试面临着巨大挑战&#xff1a;如何高效生成覆盖多种语言的测试用例&#xff1f;手动编写多…

作者头像 李华