首页 / 资讯中心 / 文章详情

picoRV32与PYNQ-Z2的RISC-V SoC全链路实践

picoRV32与PYNQ-Z2的RISC-V SoC全链路实践 ★ FEATURED ARTICLE
简介这份《硬件结构设计》综合实验报告面向计算机组成原理、嵌入式系统与FPGA开发方向的学生及自学者围绕在PYNQ-Z2开发板上用开源RISC-V核picoRV32搭建完整SoC展开重点在于说明硬件结构设计与软件编程如何衔接。报告共29页依次梳理实验要求与PYNQ框架、嵌入式系统设计流程、硬件与软件两套方案并给出模块设置与IP核包装、IP组件互联、比特流生成与部署、在云平台验证RISC-V指令执行等环节的结果与分析末尾附小组实验心得与问题复盘。资源包为1个docx文档约3.01MB按章节编号组织便于对照实验阶段查阅。已有1583人学习适合作为课程综合实验的参考模板也可用于了解RISC-V轻量核与SoC集成的完整流程、整理报告框架与排错思路。1. 从一份 29 页的硬件结构设计实验报告说起计算机组成原理这类课多数人的实验止步于在纸上画数据通路。这份 29 页的硬件结构设计综合实验报告把整套东西搬到了真板子上用开源 RISC-V 核 picoRV32 当 CPU在 PYNQ-Z2 上拼出一个完整的 SoC再在 Jupyter Notebook 里写 C 或 RISC-V 汇编交叉编译成二进制喂给这颗核去执行。写 Verilog 的人不少但能从 RTL 一路走到 IP 打包、AXI 连线、bitstream 生成、hwh 解析、工具链编译、板上验证把这条链路真正打通的人不多中间每一段都可能卡人半天。这份资源适合三类人想搞清楚软硬件边界到底在哪的在校学生想给 FPGA 塞一颗软核当控制器的工程人员以及手里有 PYNQ 板子但只会跑现成 overlay 的开发者。2. 架构分层PYNQ 三层栈和 Zynq-7000 的 PS/PL 边界2.1 PYNQ 三层架构里哪一层归你写PYNQ 把整个系统切成三层以 FPGA 设计为主的硬件层、以 Linux 内核加 Python 为主的软件层、以 Jupyter Notebook 为主的应用层。这个分法不只是学术归类它直接决定了你改一个功能时改动该落在哪个文件里。硬件层是 PL 里的 bitstream也就是你用 Vivado 生成的那份配置。软件层是板子上跑的 Linux 加 PYNQ 的 Python 库负责加载 bitstream、暴露寄存器读写接口。应用层就是浏览器里的 Jupyter Notebook你在里面写 Python 调硬件顺带写被调用的那段 RISC-V 程序。这套分层最实际的价值在于把 FPGA 的实现细节和上层应用解耦了。软件开发者拿到 PYNQ 的 library 之后用 Python 就能读写 FPGA 模块里的寄存器不需要懂 Verilog硬件工程师只管把设计打包成 overlay把接口和地址在 hwh 里描述清楚剩下的交给对面。这也是这份实验报告能成立的前提——如果每一步都要手写 Verilog一周时间根本做不完。还有个容易被忽略的细节PYNQ 基于 Ubuntu 发行版构建最大的好处是可以直接复用 Ubuntu 的根文件系统apt 里那一堆包不用移植就能装。代价是镜像大、冷启动慢但对开发效率来说是划算的。RISC-V 交叉编译工具链也能从这条路子拿到后面能在 Notebook 里直接编译 C 代码靠的就是这一层。2.2 Zynq-7000 的 PS 侧四大块APU、存储接口、IOP、互联picoRV32 是挂在 PL 里的软核它自己不产生任何“系统能力”——没有文件系统没有网络没有大容量内存。这些全在 PS 那边。所以先把 PS 拆明白才知道哪些事该交给谁。PS 侧分成四块。第一块 APU应用处理器单元包含单核或双核 Cortex-A97000S 系列只有单核外加 SWDT 系统看门狗、TTC 三重定时器/计数器、系统级控制寄存器、8 通道 DMA、GIC 通用中断控制器、Snoop 控制器、AWDT、Timer以及 512KB 的 L2 Cache 和 256KB 的片上 SRAMOCM。每个 A9 核内部还有 FPU 与 NEON 引擎、MMU、32KB 指令缓存和 32KB 数据缓存。这些跟 picoRV32 的关系是PS 跑 Linux当主机用Jupyter、文件系统、交叉编译产物、DDR 控制器都在这边picoRV32 只负责执行你丢进去的那段指令流。PS 的 DDR 控制器挂着板载内存但 picoRV32 的原生内存接口并不认识 DDR——要么给它单独接一块 BRAM要么走 AXI 让核去访问 DDR。这个选择直接决定了你后面能写多大的程序是整份设计里最早需要拍板的一件事。第二块是存储器接口分两组左边是 SRAM/NOR、ONFI 1.0 NAND、Q-SPI都属于接非易失存储的右边是 DDR 接口支持 DDR2、DDR3、DDR3L、LPDDR2。第三块是 IOP 输入输出外设从上到下依次是 2 路 USB、2 路千兆以太网、2 路 SD/SDIO、GPIO、2 路 UART、2 路 CAN、2 路 I2C、2 路 SPI。第四块是内部互联把这些东西串起来。电源域那一段值得单独提一句PS 和 PL 位于各自独立的电源域不用 PL 的时候可以关掉而且 PS 里的处理器永远先启动PL 的配置由 CPU 上运行的软件来管理。这句话解释了 PYNQ 为什么能做到“运行中动态加载 overlay”——Linux 早就跑起来了加载 bitstream 不过是往 PL 的配置端口写一段数据而已。2.3 Overlay 作为“硬件库”的加载机制Overlay 可以理解成 ZYNQ 的 PL 设计打成的一个包也叫硬件库。它和软件库一样可以按需动态加载进 FPGA。板子启动时默认会下载一个叫 base 的 overlay 当参考设计之后新的 overlay 可以上传或拷贝进去在系统运行中换掉。Python 侧的入口只有一行实例化Overlay。这一行背后做了两件事——下载 bitstream解析 hwh 文件早期版本是 Tcl。hwh 里记录着这个设计里有哪些 IP、每个 IP 的寄存器基地址、中断号、时钟连接关系这些元数据。没有它Python 就不知道ol.xxx这种属性访问该映射到哪个物理地址上。from pynq import Overlay # 实例化即下载 bitstream并自动解析同目录下的 .hwh 描述文件 overlay_design Overlay(base.bit) # 之后按 block design 里给 IP 起的名字直接取到可读写的句柄 # 比如设计里有个 IP 叫 riscv_soc就能这样访问 # soc overlay_design.riscv_socOverlay的入参是 bit 文件名不必写全路径只要该文件在板子的搜索路径下即可。hwh 是自动找同名文件的不用手动指定——这一点很多人第一次用会误以为要额外传参然后卡在那里反复检查代码其实问题往往出在文件没拷全。bit 和 hwh 必须成对出现在同一个目录、文件名主干还得一致这是后面排错的第一顺位。3. picoRV32 核解剖mem 接口时序、PCPI 协处理和三档资源取舍3.1 本机内存接口七个信号撑起一次读写picoRV32 有两种形态picorv32提供简单的原生内存接口容易在简单环境里用picorv32_axi提供 AXI-4 Lite 主接口方便接进已经用 AXI 的现有系统。此外还有个picorv32_axi_adapter专门把原生内存接口桥接到 AXI4 上。三个东西的关系后面再说先看最基础的原生接口。信号方向位宽含义mem_validoutput1发起一次访问mem_instroutput1本次访问是否为取指mem_readyinput1从设备应答表示本次传输完成mem_addroutput32字节地址mem_wdataoutput32写数据mem_wstrboutput4字节写使能全 0 表示读mem_rdatainput32读数据在 mem_ready 有效时被采样读传输的判定条件是mem_wstrb全为 0此时mem_wdata无效。从设备把mem_addr对应的数据放到mem_rdata上在mem_ready为高的那个周期被采样。这里有个很讨巧的设计不需要外部等待周期mem_ready可以在mem_valid拉高的同一周期就变高甚至可以绑成常量 1读操作完全可以异步实现。写传输反过来mem_wstrb非 0那几位就是字节使能数据在mem_wdata上从设备写完后拉高mem_ready确认。同样可以不插等待周期。一个最简单的 32 位 BRAM 从设备长这样// 单周期响应的 BRAM 从设备深度 4K 字16KB无等待周期 module simple_ram ( input wire clk, input wire mem_valid, input wire mem_instr, input wire [31:0] mem_addr, input wire [31:0] mem_wdata, input wire [ 3:0] mem_wstrb, output reg [31:0] mem_rdata, output wire mem_ready ); reg [31:0] ram [0:4095]; // 组合逻辑直接应答核发起的同一拍就完成省掉状态机 assign mem_ready mem_valid; // 写按字节使能逐字节落盘地址低 2 位由 wstrb 吸收 always (posedge clk) begin if (mem_valid mem_wstrb ! 4b0000) begin if (mem_wstrb[0]) ram[mem_addr[13:2]][ 7: 0] mem_wdata[ 7: 0]; if (mem_wstrb[1]) ram[mem_addr[13:2]][15: 8] mem_wdata[15: 8]; if (mem_wstrb[2]) ram[mem_addr[13:2]][23:16] mem_wdata[23:16]; if (mem_wstrb[3]) ram[mem_addr[13:2]][31:24] mem_wdata[31:24]; end end // 读异步出数mem_ready 有效的那一拍核自己采样 always (*) begin mem_rdata ram[mem_addr[13:2]]; end endmodule这段代码里三个点值得说清。第一mem_ready mem_valid是最省事的写法但它意味着每一条访存指令都在一个周期内完成一旦你把 BRAM 换成一个需要多周期的外设就必须改成状态机否则核会用错误的数据继续往下跑。第二mem_addr[13:2]这个取法跳过了低两位因为字节偏移已经由mem_wstrb表达了非对齐访问在原生接口上其实是支持的但 small 配置下的核不会主动发非对齐访问。第三读写共用同一个mem_valid靠mem_wstrb区分所以从设备里必须先把方向判断清楚再动作否则会出现“读的时候顺手写了一笔”的诡异现象。还有一点血泪经验mem_valid一旦拉高从设备必须有应答。如果因为地址解码没命中而让mem_ready一直不拉高核会原地卡死现象是 PC 停在第一条指令不动连复位都救不回来只能重新下载 bitstream。3.2 PCPI给核外挂一条自定义指令picoRV32 留了个叫 PCPIPico 协处理器接口的口子用来在外部核里实现非分支指令。信号如下信号方向位宽含义pcpi_validoutput1遇到不支持且开启了 PCPI 的指令pcpi_insnoutput32指令字本身pcpi_rs1output32解出的第一个源寄存器值pcpi_rs2output32解出的第二个源寄存器值pcpi_wrinput1是否写回结果pcpi_rdinput32写回的数据pcpi_waitinput1需要多周期时提前拉高pcpi_readyinput1指令执行完成工作流程是核碰到一条自己不支持的指令且 PCPI 特性开着就把pcpi_valid拉高指令字原样输出到pcpi_insn同时把 rs1、rs2 字段解出来放到pcpi_rs1、pcpi_rs2。外部核自己解码、执行执行完拉pcpi_ready想写回结果就连pcpi_wr一起拉高并给出pcpi_rdpicoRV32 会自己把 rd 字段解出来写进对应寄存器。module pcpi_add3 ( input wire clk, input wire resetn, input wire pcpi_valid, input wire [31:0] pcpi_insn, input wire [31:0] pcpi_rs1, input wire [31:0] pcpi_rs2, output reg pcpi_wr, output reg [31:0] pcpi_rd, output reg pcpi_wait, output reg pcpi_ready ); // custom-0 操作码 0x0Bfunct3/funct7 自己约定别和已有指令撞 wire is_custom (pcpi_insn[6:0] 7b0001011) (pcpi_insn[14:12] 3b000) (pcpi_insn[31:25] 7b0000001); always (posedge clk) begin if (!resetn) begin pcpi_wr 1b0; pcpi_ready 1b0; pcpi_wait 1b0; end else begin pcpi_wr 1b0; pcpi_ready 1b0; if (pcpi_valid is_custom) begin pcpi_rd pcpi_rs1 pcpi_rs2 32d3; // 示例运算 pcpi_wr 1b1; pcpi_ready 1b1; // 单周期完成 end end end endmodule参数说明一下is_custom里用 opcode0x0Bcustom-0配合 funct3、funct7 做二次区分是为了给自己留出多条自定义指令的空间不然全挤在同一个编码上后面没法扩展。pcpi_ready和pcpi_wr在同一个时钟周期内一起拉高适用于单周期就能算完的组合逻辑如果是除法、开方这类需要十几个周期的运算必须先把pcpi_wait拉起来并一直保持到pcpi_ready有效否则核会在 16 个时钟周期之后判定非法指令直接跳异常处理——这个 16 周期超时是硬性的没有商量余地。3.3 small / regular / large三档配置到底差在哪picoRV32 的参数化程度很高报告里给出的三档配置是理解它的捷径配置开启的东西适用场景small不开计数器指令、不开两周期 ALU、外部锁存 mem_rdata、忽略非对齐访存和非法指令面积敏感、只跑简单的裸机循环regular默认配置通用绝大多数实验用这档largePCPI、IRQ、MUL、DIV、BARREL_SHIFTER、COMPRESSED_ISA 全开要跑编译器产物、要接协处理器乘法器和除法器都是约 40 个周期出一条结果不能指望它当 DSP 用。开了 BARREL_SHIFTER 之后移位操作和普通 ALU 操作走同一个周期不开的话移位是软件循环模拟的性能差一个数量级。那 C 代码还能不能跑能。RV32I 基础指令集本身不含乘除法编译器遇到乘法会自动调 libgcc 里的__mulsi3软实现。所以不开 MUL/DIV 依然可以编译执行 C 程序只是慢而且你用的 libgcc 必须是 RV32I 版本的拿 RV32IM 的库会直接链接失败。这个坑我在第一次换工具链的时候踩过报错信息里全是未定义符号排查起来相当费劲。3.4 AXI 变体与 picorv32_axi_adapter 的取舍三个东西怎么选其实取决于你的内存架构只有一个软核加一块 BRAM用原生接口最省事一根线都不用多连软核要访问 DDR 或者 PS 侧的外设用picorv32_axi变体核直接以 AXI4 Lite 主设备身份发请求设计里想塞多个核共享同一块本机 RAM就在每个核外面套一个picorv32_axi_adapter内部走原生接口互通对外统一成 AXI4 出口。报告里提到的资源评估就是把这些变体分别在 Artix-7、Kintex-7、UltraScale 上跑布局布线、用二分搜索找满足时序的最短时钟周期——这种做法的意义在于picoRV32 的 fmax 足够高通常可以并进现有设计而不跨时钟域加进去基本不影响时序收敛。对这份实验来说走的是第一个方案软核加 BRAMPS 通过 AXI 从口灌数据。理由很朴素——程序就几百字节走 DDR 纯属浪费。4. 从 RTL 到 bitstream模块封装、IP 连线与 Overlay 产物落地4.1 模块设置与 IP 核打包这一步的目标是把你写的 RTL 变成一个 Vivado 认得的 IP 核能在 block design 里拖出来连线。动手之前先整理顶层端口时钟、复位、给 PS 用的 AXI4 Lite 从口、picoRV32 的 mem 接口、以及一两个调试用的输出。module riscv_soc #( parameter integer C_S_AXI_DATA_WIDTH 32, parameter integer C_S_AXI_ADDR_WIDTH 16 // 64KB 地址空间 )( // PS 侧 AXI4-Lite 从口PS 通过它配置寄存器、灌程序镜像 input wire s_axi_aclk, input wire s_axi_aresetn, input wire [C_S_AXI_ADDR_WIDTH-1:0] s_axi_awaddr, // ……其余 AXI 信号按标准模板补齐 // 调试口把核当前的 PC 拉出来Python 侧读一下就知道它是不是还活着 output wire [31:0] dbg_pc, output wire dbg_trap );打包 IP 的时候有个细节Vivado 会根据端口命名去猜接口类型名字不规范的话会被拆成一堆离散信号连线时很麻烦。所以时钟叫s_axi_aclk、复位叫s_axi_aresetn是有讲究的不是随便起的。另外dbg_pc这类调试输出强烈建议留着——板上跑不通的时候能在 Python 里读到 PC 值比对着 LED 猜快一百倍。4.2 在 block design 里用 AXI 把 IP 和 PS 连起来打开 block design拖进 Zynq7 Processing System、Processor System Reset、AXI Interconnect再把刚才打包的 IP 加进来。连线顺序是PS 的M_AXI_GP0接到 Interconnect 的主口Interconnect 的从口接到自建 IP 的S_AXI自建 IP 的时钟接 PS 的FCLK_CLK0复位接peripheral_aresetn。这里容易忽略的是时钟域。picoRV32 和 BRAM 都跑在FCLK_CLK0上如果 PS 那边配了别的时钟频率整个设计就会跨时钟域得加同步器。为了省事我一般把 PL 侧全部锁在同一个时钟上实验阶段 50MHz 足够。连线完之后一定要打开 Address Editor 分配地址。这个基地址后面 Python 侧会用到——soc.write(offset, value)里的 offset 是相对于 IP 基地址的偏移不是绝对物理地址。4.3 生成 bit 与 hwh并放到板子的指定目录Generate Bitstream 跑完在实现目录下会同时得到.bit和.hwh两个文件。报告里“生成文件并覆盖原文件”这一步说的就是拿新产物替换掉板子上旧的那份覆盖前建议先把旧的备份一份——我第一次做的时候直接把参考设计覆盖了想回退发现没留底只能重新生成一遍。把这两个文件传到板子上一般走 SCP 或者 Jupyter 的上传功能落到 Jupyter Notebook 能找到的目录下比如/home/xilinx/jupyter_notebooks/riscv/。传完确认三件事文件名主干一致、两个文件都在、目录权限可读。报告里提到的“oDisk 解压文件”和“放入指定文件夹”本质上就是这个搬运加解压的过程没什么玄学但文件少一个就一定跑不起来。4.4 在 Jupyter 里验证核是不是真的跑起来了程序编译用 RISC-V 交叉工具链# -march 要和核的配置对上开了 COMPRESSED_ISA 才能用 c 扩展 riscv32-unknown-elf-gcc -marchrv32imc -mabiilp32 \ -nostdlib -nostartfiles -T link.ld -O2 \ -o hello.elf hello.c # 链接产物转纯二进制ELF 头核不认识 riscv64-unknown-elf-objcopy -O binary hello.elf hello.bin链接脚本的关键是ORIGIN必须和 picoRV32 复位后第一条取指的地址对齐OUTPUT_ARCH(riscv) ENTRY(_start) MEMORY { RAM (rwx) : ORIGIN 0x00000000, LENGTH 16K } SECTIONS { .text : { *(.text.start) *(.text*) *(.rodata*) } RAM .bss : { *(.bss*) *(COMMON) } RAM }ORIGIN写错的后果是程序被灌到 BRAM 里但核从另一个地址取指跑出来全是非法指令。要改就改链接脚本别去改核的复位向量——改核会把所有工具链配置一起带偏。板上加载这一段用 PYNQ 的 Python 接口from pynq import Overlay import numpy as np # 加载我们自己生成的 overlayhwh 会被自动解析 ol Overlay(riscv_soc.bit) soc ol.riscv_soc with open(hello.bin, rb) as f: image f.read() # 镜像按 4 字节对齐补齐末尾填 0 padded image.ljust((len(image) 3) // 4 * 4, b\x00) words np.frombuffer(padded, dtypeu4) # 地址映射0x0000 起是控制寄存器0x1000 起映射到核眼里的 BRAM 起点 for i, w in enumerate(words): soc.write(0x1000 i * 4, int(w)) soc.write(0x0000, 0x1) # 控制寄存器 bit0释放核复位 print(hex(soc.read(0x0004))) # 读 PC 寄存器看核跑到哪了两个地址空间要分清楚核视角里 BRAM 从0x00000000开始所以链接脚本写 0PS 视角里整个 IP 占 64KB控制寄存器在0x0000BRAM 窗口从0x1000开始。同一个 BRAM 在两个视角里有两个地址写进0x1000的第 0 个字正好落在核取指的第 0 个位置。搞混这两个视角现象就是程序灌进去了但核纹丝不动。最后读一下dbg_pc如果这个值在不停变化说明核在正常执行如果停在某个固定值多半是访存卡住了回到 3.1 节检查mem_ready。5. 避坑与排查五条上板之后才发现的坑5.1 五个现象、原因和解决现象一Overlay(xxx.bit)报找不到描述文件或者加载后访问ol.xxx抛属性错误。原因通常只有两个要么 hwh 没跟着 bit 一起拷到板子上要么两个文件的文件名主干不一致——比如 bit 叫riscv_soc.bithwh 还叫design_1.hwh。解决方式是把它们改成同名同目录重新加载。顺带说一句Vivado 每次重新生成 bitstreamhwh 也会跟着重新生成版本必须配对使用拿旧的 hwh 配新的 bit 会出现寄存器地址错位这种极难排查的问题。现象二核完全不启动PC 停在 0 不动串口没有任何输出。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站