☰
基于picoRV32与PYNQ-Z2的RISC-V SoC搭建与验证实战
2026/10/24 12:15:16 网站建设 项目流程

简介:这份《硬件结构设计》综合实验报告面向计算机组成原理、嵌入式系统与FPGA开发方向的学习者,围绕开源RISC-V核picoRV32展开,讲解如何在PYNQ-Z2开发板上搭建完整SoC并完成软硬件协同编程。报告完整覆盖嵌入式系统设计流程、硬件方案与软件方案,涉及IP核包装、AXI总线互联、比特流生成、oDisk解压与RISC-V功能验证等关键环节,并附有实验心得与问题解决思路。资源包为1个docx文档,约3.01MB,共29页,含目录、实验说明、设计方案、运行结果分析及心得等章节,结构清晰,便于按模块查阅。目前已有1582人学习下载,适合作为课程实验参考、SoC入门实践与RISC-V软硬件结合学习的完整案例。

1. 从一份 29 页实验报告说起:picoRV32 怎么在 PYNQ-Z2 上跑通一个完整 SoC

很多人第一次接触 RISC-V 和 SoC 设计,卡住的地方不是写 Verilog,而是“我写的核到底怎么跟外设连起来、怎么把编译好的程序塞进去跑”。这份《硬件结构设计》综合实验报告给了一条很具体的路径:用开源的 picoRV32 作为处理器核,在 PYNQ-Z2 上搭一个完整的 SoC,再借助 RISC-V-On-PYNQ Overlay 把工具链和 Jupyter Notebook 串起来,让你在浏览器里写 C 或 RISC-V 汇编,编译完直接丢给 picoRV32 执行。它适合正在学计算机组成原理、想动手做嵌入式系统设计的学生,也适合想从 FPGA 验证转向 RISC-V 软硬协同的开发者。报告本身 29 页,覆盖了从 PYNQ 框架介绍、picoRV32 内存接口、PCPI 协处理器接口,到 IP 核包装、比特流生成、oDisk 解压、云平台验证的完整流程,是一份能照着复现的工程记录。

2. 硬件结构设计拆解:picoRV32 核、AXI 互联与 PYNQ Overlay 怎么拼

2.1 picoRV32 的两种形态与内存接口时序

picoRV32 实现的是 RISC-V RV32IMC 指令集,可以按需裁剪成 RV32E、RV32I、RV32IC、RV32IM 或 RV32IMC。它有两个变体:picorv32提供简单的本机内存接口,适合在简单环境里直接用;picorv32_axi提供 AXI-4 Lite Master 接口,方便跟已有 AXI 系统集成。报告里选的是后者,因为 PYNQ-Z2 的 PS 侧和 PL 侧之间走的就是 AXI。

本机内存接口的信号定义很精简,读传输和写传输都靠mem_valid和mem_ready握手:

// picoRV32 本机内存接口关键信号 output mem_valid; // 主机发起传输 output mem_instr; // 当前传输是指令取指 input mem_ready; // 从机确认传输 output [31:0] mem_addr; // 读写地址 output [31:0] mem_wdata; // 写数据 output [ 3:0] mem_wstrb; // 写字节选通,0 表示读 input [31:0] mem_rdata; // 读数据

读传输时mem_wstrb为 0,mem_wdata不用;写传输时从机把mem_ready拉高表示写入完成。这里不需要外部等待周期,mem_ready可以和mem_valid同周期变高,也可以直接绑成常数 1。我一般会在自己搭 SoC 时把mem_ready绑 1 先跑通,确认取指和读写没问题,再换成真实的 RAM 控制器。

2.2 PCPI 协处理器接口:给核加自定义指令的入口

PCPI(Pico 协处理器接口)是 picoRV32 留给外部核实现非分支指令的通道。当遇到不支持的指令且 PCPI 特性被激活时,pcpi_valid拉高,指令字出现在pcpi_insn上,rs1和rs2字段解码后分别输出到pcpi_rs1和pcpi_rs2。外部 PCPI 核可以解码、执行,完成后拉高pcpi_ready,可选地把结果写到pcpi_rd并拉高pcpi_wr。

// PCPI 接口信号 output pcpi_valid; output [31:0] pcpi_insn; output [31:0] pcpi_rs1; output [31:0] pcpi_rs2; input pcpi_wr; input [31:0] pcpi_rd; input pcpi_wait; input pcpi_ready;

如果外部核需要多个周期执行,要在指令成功解码后立刻拉高pcpi_wait并保持到pcpi_ready拉高,否则 16 个时钟周期内没确认就会触发非法指令异常。这个机制在报告里被用来解释怎么把乘法、除法、桶形移位器这些模块挂到核上——ENABLE_MUL激活时 MUL 指令 40 个周期执行完,ENABLE_DIV激活时除法也是 40 个周期,BARREL_SHIFTER激活后移位操作跟其他 ALU 操作一样走。

2.3 PYNQ Overlay 的加载与 IP 核包装

PYNQ 把 FPGA 设计封装成 Overlay,软件开发者用 Python 就能加载和控制,不用碰 Verilog。加载 base overlay 的代码就两行:

from pynq import Overlay overlay_design = Overlay("base.bit")

实例化 Overlay 时会默认下载 bitstream 并解析 hwh 文件。报告里的流程是:先在 Vivado 里把 picoRV32 和必要外设包装成 IP 核,再跟其他组件通过 AXI 互联连起来,生成新的 bitstream 和 hwh 文件,覆盖原文件后放到 PYNQ-Z2 的指定目录。oDisk 解压出来的文件要放进对应文件夹,然后在 Jupyter Notebook 里重新加载 Overlay,确认 RISC-V 核出现在地址映射里。

注意:覆盖原文件前先备份一份 base.bit 和 base.hwh,不然加载失败时连默认 Overlay 都回不去。

2.4 Zynq-7000 的 PS-PL 分工与 AXI 互联

Zynq-7000 在单芯片里集成了 ARM Cortex-A9 双核 PS 和 28nm PL。PS 侧包含 APU、存储器接口、I/O 外设和内部互联四大块。APU 里有 A9 核、NEON 引擎、MMU、32KB I-Cache/D-Cache、512KB L2 Cache、256KB OCM SRAM、DMA 8 通道、GIC 等。存储器接口分两类:左侧接 SRAM/NOR、NAND、Q-SPI Flash 这些非易失存储器,右侧接 DDR2/3、DDR3L、LPDDR2 这些易失存储器。I/O 外设包括 USB、GigE、SD/SDIO、GPIO、UART、CAN、I2C、SPI 各两路。

picoRV32 作为 PL 里的软核,通过 AXI 跟 PS 通信。PS 上跑 Linux 和 Jupyter web server,PL 里跑 RISC-V 核和自定义逻辑。这种分工的好处是:PS 负责文件系统、网络、编译工具链这些重活,PL 只做需要并行或定制指令的计算。报告里验证 RISC-V 可用性的方式,就是在 Jupyter Notebook 里写一段程序,交叉编译成 RISC-V 二进制,放到 picoRV32 能访问的内存区域,然后观察执行结果。

3. 从 Vivado 到 Jupyter:一份可复现的 SoC 搭建与验证流程

3.1 模块设置与 IP 核包装

在 Vivado 里新建工程,选 PYNQ-Z2 对应的器件型号。把 picoRV32 的 Verilog 源码加进工程,根据实验需求配置参数:如果只需要基本指令集,ENABLE_MUL、ENABLE_DIV、BARREL_SHIFTER都可以先关掉,减小面积;如果要跑带乘除法的 C 程序,就把这几个打开。COMPRESSED_ISA控制是否支持压缩指令,ENABLE_PCPI控制是否开放协处理器接口。

包装 IP 核时,Vivado 的 IP Packager 会自动识别 AXI 接口。picorv32_axi已经带了 AXI-4 Lite Master,包装时确认地址位宽和数据位宽跟互联匹配。我一般会把 picoRV32 的时钟单独接一个 Clocking Wizard,方便后面调频率——picoRV32 的 fmax 很高,在 Artix-7 上跑 100MHz 以上很轻松,但内存控制器可能跟不上,需要根据实际时序报告调整。

3.2 将生成的 IP 与其他组件相连

在 Block Design 里把 picoRV32 的 AXI Master 接到 Zynq PS 的 AXI Slave 端口,中间可以插 AXI Interconnect 或 SmartConnect。如果 picoRV32 需要访问 PL 侧的 BRAM,就再挂一个 AXI BRAM Controller。地址映射要在 Address Editor 里分配好,确保 picoRV32 发出的地址能落到正确的从机。

# 在 Vivado Tcl Console 里分配地址段的常见做法 assign_bd_address -offset 0x40000000 -range 64K [get_bd_addr_segs {picorv32_0/axi_0/Reg}] assign_bd_address -offset 0x00000000 -range 256K [get_bd_addr_segs {axi_bram_ctrl_0/S_AXI/Mem0}]

上面第一行把 picoRV32 的 AXI 寄存器空间映射到 0x40000000,第二行把 BRAM 映射到 0x00000000。实际地址要根据 PS 侧 DDR 和 OCM 的占用情况调整,避免冲突。连好后跑一次 Validate Design,确认没有未连接的接口和地址重叠。

3.3 生成比特流并覆盖原文件

Block Design 验证通过后,生成 HDL Wrapper,跑综合和实现。时序收敛后生成 bitstream,同时导出 hwh 文件。这两个文件就是 PYNQ 需要的 Overlay 文件。把生成的.bit和.hwh重命名为跟原 base 文件不同的名字,比如riscv_soc.bit和riscv_soc.hwh,然后通过 oDisk 或 SCP 传到 PYNQ-Z2 的/home/xilinx/jupyter_notebooks/目录下。

注意:hwh 文件必须和 bit 文件同名且同目录,否则 Overlay 加载时会解析不到 IP 信息,报 “No hwh file found” 或地址映射为空。

3.4 oDisk 解压与文件放置

报告里提到的 oDisk 是一个把文件打包成磁盘镜像再解压到开发板的工具。实际操作中,把包含 bit、hwh、RISC-V 二进制和测试脚本的压缩包通过 oDisk 解压到指定文件夹。解压后检查文件权限,确保xilinx用户可读。如果用的是 Jupyter Notebook 上传,直接拖拽文件到对应目录也行,但大文件建议走 oDisk 或 SCP,避免浏览器超时。

3.5 在 PYNQ-Z2 云平台上验证 RISC-V 是否可用

打开 Jupyter Notebook,新建一个 Python 3 notebook,先加载 Overlay:

from pynq import Overlay import subprocess # 加载自定义的 RISC-V SoC Overlay overlay = Overlay("/home/xilinx/jupyter_notebooks/riscv_soc.bit") print(overlay.ip_dict.keys()) # 确认 picoRV32 出现在 IP 列表里

ip_dict里应该能看到 picoRV32 对应的条目。然后写一段 RISC-V 汇编或 C 程序,用交叉编译工具链编译:

# 在 PYNQ-Z2 的 Linux 终端或 Notebook 里执行 riscv32-unknown-elf-gcc -march=rv32imc -mabi=ilp32 -nostdlib -o test.elf test.S riscv32-unknown-elf-objcopy -O binary test.elf test.bin

把test.bin放到 picoRV32 能访问的内存区域,通过 AXI 写入 BRAM 或 DDR 的指定地址,然后触发 picoRV32 开始执行。验证成功的标志是:picoRV32 跑完程序后,某个内存地址或 GPIO 输出出现预期值。报告里写的是“运行成功,实验成功”,实际调试时建议在程序末尾写一个 magic number 到固定地址,再用 Python 读回来比对,比看 LED 闪烁更可靠。

4. 避坑与排查:picoRV32 SoC 搭建中最容易翻车的 5 个点

4.1 Overlay 加载失败,报 hwh 文件缺失或解析错误

现象:Overlay("riscv_soc.bit")抛出异常,提示找不到 hwh 文件或解析失败。
原因:bit 和 hwh 文件名不一致,或者 hwh 文件没有跟 bit 放在同一目录;也有可能是 Vivado 导出时 hwh 版本跟 PYNQ 版本不匹配。
解决:确认两个文件同名同目录;如果版本不匹配,用 PYNQ 对应版本的 Vivado 重新导出,或者手动检查 hwh 里的 IP 列表是否完整。

4.2 picoRV32 取指失败,mem_ready 一直不拉高

现象:picoRV32 的mem_valid有波形,但mem_ready始终为低,核停住不执行。
原因:内存控制器没有正确响应,或者地址映射错了,picoRV32 访问的地址没有落到任何从机。
解决:先在 Vivado 里用 ILA 抓mem_valid、mem_addr、mem_ready,确认地址是否在已分配的范围内;如果地址正确但mem_ready不来,检查 BRAM Controller 的时钟和复位是否接好。临时可以把mem_ready绑 1 验证核本身能不能跑。

4.3 交叉编译出来的二进制跑飞或触发非法指令异常

现象:程序加载后 picoRV32 触发异常,或者跑飞不返回。
原因:编译时-march和-mabi跟 picoRV32 实际配置的指令集不匹配,比如核只配了 RV32I 但编译用了 RV32IMC 的乘除法指令。
解决:确认 picoRV32 的ENABLE_MUL、ENABLE_DIV、COMPRESSED_ISA参数,编译时-march跟核配置一致。如果核没开乘除法,C 代码里避免用*、/,或者链接时用软件模拟库。

4.4 AXI 互联地址冲突导致 PS 侧访问异常

现象:加载 Overlay 后 PS 侧 Linux 卡死或报 AXI 总线错误。
原因:picoRV32 的 AXI 地址段跟 PS 侧已有外设地址重叠,或者 Address Editor 里没分配地址。
解决:在 Vivado 的 Address Editor 里检查所有 Master 的地址段,确保没有重叠;picoRV32 访问 DDR 时,地址要落在 PS 的 DDR 范围内,且不能跟 Linux 内核占用的区域冲突。

4.5 Jupyter Notebook 里编译工具链找不到

现象:在 Notebook 里执行riscv32-unknown-elf-gcc报 command not found。
原因:RISC-V 工具链没有装到 PYNQ-Z2 的 Linux 里,或者 PATH 没配。
解决:确认工具链已经解压到/home/xilinx/下,并在 Notebook 里用绝对路径调用,或者把工具链的bin目录加到PATH环境变量。如果用的是 RISC-V-On-PYNQ 提供的镜像,工具链通常已经预装,检查/opt/riscv/bin是否存在。

5. 二次开发怎么改:从跑通 Demo 到加自定义指令和外设

跑通报告里的流程只是起点。真正做二次开发时,我一般会从三个方向改:加自定义指令、挂新外设、换更大的内存。

加自定义指令走 PCPI 接口最直接。比如想加一条求绝对值的指令,在 picoRV32 顶层把ENABLE_PCPI打开,然后写一个 PCPI 协处理器模块,解码pcpi_insn里的自定义 opcode,从pcpi_rs1取操作数,算完把结果写到pcpi_rd并拉高pcpi_wr和pcpi_ready。如果计算需要多个周期,记得在解码成功后立刻拉高pcpi_wait,否则 16 个周期超时会触发非法指令异常。编译时用内联汇编调用这条指令,验证结果是否正确。

// PCPI 协处理器模块骨架 module pcpi_abs ( input pcpi_valid, input [31:0] pcpi_insn, input [31:0] pcpi_rs1, output pcpi_wr, output [31:0] pcpi_rd, output pcpi_wait, output pcpi_ready ); // 解码自定义指令,判断 opcode 和 funct3 // 计算绝对值,单周期完成 assign pcpi_rd = pcpi_rs1[31] ? -pcpi_rs1 : pcpi_rs1; assign pcpi_wr = pcpi_valid; assign pcpi_wait = 1'b0; assign pcpi_ready = pcpi_valid; endmodule

挂新外设的话,在 Block Design 里加 AXI GPIO 或 AXI Timer,分配到 picoRV32 能访问的地址段,然后在 C 程序里用指针读写对应地址。比如加一个 AXI GPIO 控制 LED,地址 0x41200000,写*(volatile uint32_t *)0x41200000 = 0x1;就能点亮。换更大内存就是把 BRAM 换成 DDR,通过 AXI Interconnect 接到 PS 的 DDR 端口,注意地址映射和缓存一致性——picoRV32 访问 DDR 时如果 PS 侧有缓存,需要处理 coherency,简单做法是让 picoRV32 访问非缓存区域。

验证方法上,我习惯在每次改完硬件后先跑一个最小测试:picoRV32 从 BRAM 取指,执行几条算术指令,把结果写到固定地址,Python 读回来比对。这个测试通过后再加外设和复杂程序。从那以后我每次改 Block Design 都强制走一遍这个最小测试,确认核本身没被改坏,再往下调。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询