PYNQ 4.0 发布之后,我一直想找一个"既小巧、又能玩出花"的 Zynq 平台。手里正好有一套 snickerdoodle black 加 piSmasher 的组合——前者是一块约等于火柴盒大小的 Zynq 板卡,后者是一个能引出树莓派风格 40 引脚的转接板。把这两者拼在一起跑 PYNQ 4.0,听起来很折腾,但实际跑通之后的价值非常大:你既有了 PYNQ 的 Python 化 FPGA 开发体验,又能直接用上大量为树莓派设计的 HAT 外设、摄像头模组和显示模块,整体尺寸却比树莓派小一圈。
这篇文章就记录我从烧写镜像、确认板级支持,到把 piSmasher 引脚映射进 PYNQ overlay,再到实际点灯、读取 I2C 设备的完整过程。后面还包括我踩过的三个比较典型的坑,以及一份最小验证清单。如果你手里有 snickerdoodle black,或者你正打算把 PYNQ 跑在一块"非官方支持"的板卡上,这篇内容应该能帮你少走不少弯路。
1. 为什么是 PYNQ 4.0、snickerdoodle black 和 piSmasher 这个组合
先说清楚这套组合到底是什么,以及它解决什么问题,不然光看标题会以为只是又一篇刷镜像教程。
PYNQ 4.0 是面向 Zynq / Zynq UltraScale+ 系列器件的开源项目,核心思路是用 Python 直接驱动 FPGA 上的外设。它把 bitstream、设备树覆盖层、驱动库封装成 Jupyter 环境里的 Python 对象,让你不必先精通 Verilog 和驱动开发,也能完成不少硬件加速实验。比如控制一组 GPIO、读写 I2C 设备、搬运 DMA 数据流,在 PYNQ 里都是几行 Python 的事。
snickerdoodle black 是一块很小的开发板,核心是双核 Cortex-A9 加可编程逻辑的 Zynq-7000 系列芯片。板载资源包括 DDR3、eMMC、无线网卡、几个扩展接口,整体功耗不高,很适合做嵌入式视觉、桌面机器人、音频处理这类原型验证。它的尺寸注定它不可能像完整教学开发板那样把所有接口都摆在板子上,所以就需要扩展板来补足。
piSmasher 的作用就是把这个板卡"变成"一块树莓派形状的东西。它按照树莓派 40 引脚的定义把 snickerdoodle 的引脚引出来,同时保留了 CSI/DSI 摄像头和显示器的排线接口。这样你就可以把树莓派的 HAT、传感器模块、甚至摄像头软排线直接插上去,而不需要自己拿着万用表一个点一个点对引脚。
这套组合的独特点在于:它把 PYNQ 的软件生态、 snickerdoodle black 的 Zynq 算力、树莓派的外设生态三者接在了一起。换句话说,你可以在一个比钱包大不了多少的硬件上,同时拥有 Linux 应用开发、FPGA 可重构逻辑、以及成熟的树莓派配件生态。实际场景里比较典型的有:
- 接一个树莓派摄像头模块,在 FPGA 里做实时图像预处理,再把结果送进 Python 里的深度学习模型。
- 接一组 I2C 传感器,用 PYNQ overlay 里的硬件 I2C 控制器做主站,定时采样并绘图。
- 接一块树莓派 GPIO 扩展板,几十路 IO 都可以由 Jupyter 里的 Python 脚本直接控制。
要说适合谁,我觉得最适合的是这类人:已经会一点 Python,有 Linux 基础,又想快速验证 FPGA 加速效果,却不想一上来就啃全套 RTL 设计。PYNQ 4.0 在这个组合上的意义,是把你从"我要先会 FPGA"这个门槛里暂时解放出来,让硬件验证的节奏变得非常快。
当然,这个组合并不是 PYNQ 官方的一等公民。snickerdoodle black 严格来说属于社区支持的板卡,PYNQ 4.0 仓库里没有现成的官方镜像,所以你需要自己找社区编译好的镜像,或者从构建系统自己生成。这正是后面章节首先要注意的地方。
2. 从一张空 SD 卡到 Jupyter 控制台:镜像烧录与首次启动
这部分是纯基础设施工作。不要觉得简单,80% 的移植问题都出在第一步的镜像版本不对、分区不对、或者连接方式不对。我按照自己的操作顺序写一遍。
2.1 需要的硬件和接线
准备的东西不多:
- snickerdoodle black 板卡一块
- piSmasher 转接板一块
- 一张容量 8GB 以上的 MicroSD 卡
- 一条 USB-A 转 Micro-USB 数据线,用于供电和串口调试
- 一条 USB 转 TTL 串口线,如果板子板载的调试口不工作的话备用
- 网线或者 USB 无线网卡,用于接入局域网
先把 piSmasher 叠到 snickerdoodle black 上。注意方向,piSmasher 的 40 脚排母应该和 snickerdoodle 顶层的排针完全对齐,不能反插,一旦插反很容易直接烧掉 GPIO 端口。插好后,从侧面对一下位置,如果是标准排列,两边会非常整齐。
2.2 下载并写入镜像
我用的镜像是社区维护的 PYNQ 4.0 snickerdoodle 镜像。这类镜像通常以.img或.img.gz形式发布,对应 Zynq-7000 系列的 PYNQ Linux 4.0 分支。下载之后建议先比对一下校验值,我之前因为用了损坏的镜像,启动日志总是走到一半就卡死,浪费了很长时间。
写入 SD 卡我推荐直接使用dd,因为流程最透明:
sudo dd if=pynq_4.0_snickerdoodle.img of=/dev/sdX bs=4M conv=fsync status=progress注意/dev/sdX要替换成你 SD 卡的实际设备节点,千万别写错。如果担心手抖,可以用 balenaEtcher 这类带图形界面的工具,效果一样。写入完成之后,把 SD 卡插进 snickerdoodle black 的卡槽。
检查一下分区结构,正常的 PYNQ 镜像应该有两个分区:
| 分区 | 类型 | 内容 |
|---|---|---|
| 第一个 | FAT32 | 存放设备树、内核镜像、U-Boot 启动脚本 |
| 第二个 | ext4 | rootfs,也就是 Linux 根文件系统 |
如果镜像只有一个分区,多半是精简版或者仅启动引导分区,最好换一个完整镜像。
2.3 首次启动与登录方式
给板卡供电之前,先接好串口调试线。snickerdoodle black 的调试口通常映射成 Linux 下的/dev/ttyUSBx,用screen或者minicom打开:
screen /dev/ttyUSB0 115200然后上电。观察板卡 LED 状态:应该先快速闪烁几次,然后进入稳定状态。串口端会输出 U-Boot 启动日志,接着开始加载 Linux 内核。第一次启动会执行分区扩容逻辑,耗时比较长,可能要两三分钟,不要急着断电。
登录方法:
登录用户: root 默认密码: xilinx登录后第一件事是配置网络。如果用网线,一般 DHCP 会自动获取地址:
ip addr记下eth0或者wlan0的 IP。如果要用无线,需要先编辑/etc/wpa_supplicant/wpa_supplicant.conf,把 SSID 和密码写进去,再重启网络服务。
2.4 打开 Jupyter
在浏览器打开http://<板卡IP>:9090,看到 JupyterLab 登录页就算成功。PYNQ 4.0 默认用 JupyterLab 界面,比早期版本更接近现代开发环境。
进入之后先新建一个 Python 3 notebook,运行下面的检查命令:
import pynq pynq.__version__如果显示4.0.0或者类似的 4.x 版本号,说明基础环境已经就绪。比起马上跑外设,我更建议先运行一项简单的验证:
from pynq import Overlay Overlay('/home/xilinx/pynq/overlays/base/base.bit')如果系统里本来就带 base 覆盖层,这条命令会在 PL 侧下载一个最小的可编程逻辑配置。听到电流声或者看到串口日志里多出 FPGA 配置的相关信息,说明 PYNQ 到 FPGA 的下载链路已经通了。到此,你可以认为"PYNQ 4.0 已经在这套板卡组合上跑起来了",接下来才是真正有意思的部分:让 piSmasher 的引脚为你所用。
3. 内核和板级支持:决定你能不能顺利加载 overlay 的关键
很多人刷完镜像、打开 Jupyter 就以为万事大吉,结果一执行Overlay.download()就报Bitstream download failed。这通常是内核配置、设备树、或者覆盖层机制不匹配造成的。所以要专门说一说板级支持文件(BSP)这部分。
3.1 PYNQ overlay 的加载机制
先理解 PYNQ 4.0 是怎么把 bitstream 加载到 FPGA 里的。
在 Zynq 体系里,PL 配置由 PS 端的设备树和驱动共同完成。PYNQ 生成的硬件描述文件.hwh会被解析成一个设备树覆盖层(DTO),然后通过内核的 FPGA manager 框架加载 bitstream。你的内核需要打开以下配置项才有能力走通这条路:
CONFIG_FPGA:FPGA 框架主开关CONFIG_FPGA_ZYNQ:Zynq 系列的 FPGA manager 驱动CONFIG_FPGA_BRIDGE:负责 PL 与 PS 之间的桥接管理
使用社区镜像的好处是这些配置通常已经打开。如果你是自己编译内核,这几项一定要检查。用下面命令查看:
zcat /proc/config.gz | grep FPGA3.2 设备树里必须有的 fpga-region
如果你在启动时看到以下任意一种现象:
Overlay.download()卡死- 内核日志报
fpga_region not found - Jupyter 里报
DTS node not found
那很可能设备树里缺少fpga-region节点。标准 PYNQ 设备树里,你会看到类似这样的结构:
fpga0: fpga-region@0 { compatible = "fpga-region"; fpga-bridges = <&fpga0_bridge0>; ranges; };这个节点告诉内核:在这一片内存空间里,允许通过 FPGA manager 加载覆盖层。snickerdoodle black 的社区镜像基本都已经加上了这个节点,但是如果你替换了设备树,非常容易丢掉它。
3.3 覆盖层文件名的硬规则
PYNQ 4.0 里,Overlay(file_name)的查找逻辑是靠后缀匹配的。它要求 bitstream 文件和.hwh硬件描述文件同名。也就是说必须有my_overlay.bit和my_overlay.hwh放在同一目录下,两个文件主文件名必须完全一致。
这不是文档里特别显眼的内容,但实际使用中很容易踩。我的目录里曾经同时存在base.bit和Base.hwh,Python 直接说找不到匹配的文件。统一改成小写主名后立刻正常。PYNQ 4.0 还会生成对应的设备树覆盖层文件,一般不需要你手动维护,但前提是文件命名规则正确。
3.4 快速验证清单
在开始设计自定义覆盖层之前,先跑一遍这个列表,确认你的板级支持是健全的:
- 内核支持 FPGA manager:
dmesg | grep fpga - 设备树里能看到
fpga-region - 能使用
fpgautil或 PYNQ 直接加载官方示例 overlay - Jupyter 中
pynq.get_affinity()返回正常的hw或sw
如果这些都通过,说明 BSP 没问题,问题大概率出在你自己的硬件连接和覆盖层设计上。
4. 把 piSmasher 引脚变成 PYNQ 能认识的资源:我在引脚映射上踩的坑
这一章是整个移植过程的核心难点,也是我觉得最有价值的部分。piSmasher 是树莓派形状因子的转接板,但它的引脚号、电气定义、以及和 snickerdoodle black 内部逻辑引脚的对应关系,不一定和你想象中一致。
4.1 树莓派 40 引脚和 snickerdoodle 引脚的思维方式完全不同
树莓派的 40 引脚对应的是博通 SoC 的 GPIO 编号,比如物理引脚 11 对应 BCM GPIO 17。但 piSmasher 接到 snickerdoodle 上之后,这些引脚并不直接连接到 PS 端的固定 GPIO 控制器,而是通过 snickerdoodle 的扩展接口接在 PL 侧的可编程逻辑引脚上。
这就带来一个关键差异:树莓派上控制 BCM GPIO 17 的直接寄存器读写,和 PYNQ 里控制 PL 引脚的方式,根本不在一个层面。PYNQ 里你通常是通过 overlay 的 AXI GPIO 或 EMIO 来操作它们。换句话说,piSmasher 引出的 40 个引脚在你的 FPGA 逻辑里叫什么名字,完全取决于你加载的 bitstream,而不是硬件本身。
所以当你听到"我的 HAT 插上去没反应",第一反应不该是换驱动,而是先确认:当前加载的 overlay 有没有把对应物理引脚配置成你期望的功能。
4.2 我如何确认物理引脚映射关系
由于 piSmasher 只是把树莓派型接口引出到可编程逻辑引脚,不同版本的产品、甚至不同版本的 PCB 布局,都可能影响 GPIO 实际连接顺序。最可靠的方式是查看原理图和约束文件,其次是直接用万用表通断测试来确认。
我没有直接照搬网上现成的映射表,因为网上的资料有不少是以"早期原型板"为基准写的,和手里的版本有出入。我的做法是:
- 把 piSmasher 载板单独放好,什么都不插。
- 用万用表蜂鸣档,从树莓派物理引脚 1 开始,顺着 piSmasher 的信号走线,测量它在 snickerdoodle 对应的排针上落在哪个位置。
- 记录每一条走线的映射关系,做成一页笔记。
- 在高电平电压下再验证一遍,防止把相邻引脚误判成通路。
下表是我根据自己这块板子整理的一小段示例,不是标准答案,只是用来展示你应该记录的数据长什么样:
| piSmasher 物理引脚 | 树莓派 BCM 编号 | snickerdoodle 扩展引脚 | 信号方向 | 备注 |
|---|---|---|---|---|
| 3 | GPIO2 / I2C1 SDA | 某组差分对的 P 端 | 双向 | 接了上拉电阻 |
| 5 | GPIO3 / I2C1 SCL | 某组差分对的 N 端 | 双向 | 接了上拉电阻 |
| 7 | GPIO4 | 某普通 IO_L 引脚 | 输入输出 | 需要自己加约束 |
| 11 | GPIO17 | 某时钟专用引脚 | 输入输出 | 尽量不用作普通 GPIO |
你会发现,并不是所有"看起来应该顺序对应"的引脚都真的顺序对应。这就是我强调自己测量的原因。
4.3 把映射关系写进 PYNQ overlay
得到映射表之后,下一步是写进 Vivado 工程的约束文件(XDC)。在 PYNQ 4.0 的开发流程里,你要把所有 piSmasher 需要使用的引脚,在新建的 block design 中通过 AXI GPIO IP 引出,然后在 XDC 里把 IP 的输出端口约束到真实的 PL 引脚。
举一个最小例子。假设我需要在 40 引脚的第 11 脚(BCM GPIO17 位置)输出一个 LED 控制信号,在 Vivado 里我会:
- 创建一个 block design,添加
ZYNQ7 Processing System。 - 添加
AXI GPIOIP,比如命名为axi_gpio_0,设置成单通道输出,宽度设为 32。 - 把
axi_gpio_0的GPIO0_TRI_I或GPIO0_TRI_O端口引出到顶层。 - 因为外设是输出驱动 LED,顶层信号取名
gpio_led_tri_o[0]。 - 在 XDC 里写:
set_property PACKAGE_PIN <某个引脚编号> [get_ports {gpio_led_tri_o[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {gpio_led_tri_o[0]}]然后综合、实现、生成 bitstream。PYNQ 里的.hwh会记录这个 AXI GPIO 位于哪个地址,Python 里就能用ol.axi_gpio_0.channel1[0]来访问。
4.4 PS 引脚和 PL 引脚不要混淆
这里必须展开一个容易错的地方:snickerdoodle black 上有一部分 MIO 引脚由 PS 直接管理,比如 eMMC、SDIO、UART、USB 等,这类引脚不属于可编程逻辑,也不由 overlay 控制。piSmasher 引出的接口,绝大多数都在 PL 侧,所以你需要的是在 FPGA 配置完成后才能使用它们。
如果你试图通过 PYNQ 的ps.remote模块去访问某个 PL 引脚,你会得到一个"地址越界"或"寄存器不存在"的报错。正确的做法永远是:在 block design 里把这些引脚接到 AXI 外设上,然后用覆盖层对象访问。
假设你在 Python 里误把第 11 脚当成 PS GPIO 来写:
# 错误的做法:piSmasher 引脚不是 PS MIO ol = Overlay('base.bit') ol.axi_gpio_0.channel1[17].write(1) # 这取决于你的设计,不代表物理引脚 11上面这段代码的问题在于,它操作的不是物理第 11 脚,而是axi_gpio_0数据寄存器里的第 17 位。这个位是否对应物理引脚,取决于你在 block design 里怎么布线。所以,引脚映射没有捷径,只有拿原理图加万用表一步一步确认。
5. 第一个能跑的 base overlay:Vivado 工程、AXI GPIO、AXI IIC 全流程
我建议你不要一上来就去跑别人写好的复杂覆盖层,而是先自己搭一个最小可用的 overlay,把整条链路走通。这样后面不管是接摄像头还是加 DMA,都有基础。
5.1 Vivado 里的 block design
PYNQ 4.0 官方使用的是 Vivado 2023.1 或 2023.2 版本。我这次用的是 2023.2,版本过旧或过新都可能出现 IP 版本不兼容的问题。
新建工程的时候,器件选择要和 snickerdoodle black 的芯片一致,如 XC7Z020。在这个工程里,我搭建的最小系统只包含三个块:
ZYNQ7 Processing System:配置好 PS 侧 DDR、UART、SD、Ethernet。AXI GPIO:双通道,通道 1 设为输出,通道 2 设为输入,每通道 32 位。这里用来控制 piSmasher 引出的 LED、读取按键。AXI IIC:总线接口直接引出到 PL 引脚,用来读取 piSmasher 上 HAT 的 ID EEPROM。
创建 block design 之后,连接 AXI 互联,自动连线,然后:
make_bd_pins -all make_bd_intf_pins -all最后生成 HDL wrapper,开始综合、实现、生成 bitstream。这个过程对 Zynq-7020 来说通常只需要 5 到 15 分钟。
5.2 生成 PYNQ 需要的文件
PYNQ 不需要普通的.bit和.hwh之外的高级产品文件,只需要两个东西:
.bit:从 implements 目录复制.hwh:这是由 Vivado 生成的硬件手册文件,位于工程.srcs目录下的bd相关路径
把两个文件复制到板子的/home/xilinx/pynq/overlays/base/目录,注意文件名一致。如果你还没有目录,可以自行创建。
5.3 Python 侧的点灯实验
在 Jupyter Notebook 里执行:
from pynq import Overlay ol = Overlay('base.bit') led = ol.axi_gpio_0.channel1 btn = ol.axi_gpio_0.channel2 led.write(0x00000001) import time for i in range(8): led.write(1 << i) time.sleep(0.2)如果你的按钮接在第 3 位,再写一个回环测试:
if btn.read() & (1 << 3): print("button pressed")这样你就验证了从 Python 到 PL 引脚、再从 PL 引脚回到 Python 的完整通路。
5.4 AXI IIC 读取 HAT 的 ID EEPROM
树莓派 HAT 规范里,I2C 地址0x50通常挂着一个 ID EEPROM,里面记录了该 HAT 的厂商和设备信息。piSmasher 如果按规范留出了这两个引脚,那么你在自己的覆盖层里用 AXI IIC 对0x50发起读操作,理论上应该能读回固定长度的内容。
我的读取代码大致是这样:
iic = ol.axi_iic_0 data = iic.read(0x50, 0, 4) print(data)如果返回的不是乱码而是有意义的 ASCII,说明 I2C 通路和引脚映射都正确。如果一直返回0xFF,通常不是地址错了,而是总线没有正常初始化,或者物理上 SDA/SCL 引脚接反了,需要回去复查映射。
5.5 摄像头模组的 MIPI 通路
接树莓派摄像头模组是 piSmasher 最具吸引力的场景之一。但这里有个大前提:你的 snickerdoodle 必须支持 MIPI CSI 输入,同时你的覆盖层里要有一个 MIPI D-PHY 接收器 IP。这不是随便一个 GPIO 覆盖层能承担的工作,需要专门的视频通路设计。
如果你只是想先验证覆盖层能不能走通视频数据,可以先用一个简单的彩色条图案驱动显示器接口,而不是一上来就把摄像头模组插入到没有视频接收 IP 的覆盖层里。至少我自己在调试时,是在确认 GPIO/I2C 之后,才开始引入视频相关 IP 链路的。PYNQ 4.0 的pynq.lib.video模块提供了比较友好的接口,但底层依然依赖正确的硬件通路。
6. 移植过程中踩过的三个坑,以及我留下的最小验证清单
这部分专门记录我没在手册里看到、但实际折腾了很久的问题,希望对后来者有用。
6.1 坑一:没有先确认供电能力就接了满配 HAT
piSmasher 是转接板,供电链路最终来自 snickerdoodle black。如果你在上面的 40 引脚接口接了多个大电流传感器、显示屏、电机驱动板,板卡总电流会迅速超标。我第一次调试时接了七寸触摸屏和一个旧款 HAT,结果启动后几分钟内屏幕开始闪,无线模块反复掉线。用万用表一测,5V 输入端的电流已经接近板卡稳压模块的上限。
建议:分阶段上电。第一个 HAT 验证通过之后,再逐个叠加,每次叠完都观察电流和温升。需要大电流时,最好从外部供电模块直接给 HAT 供 5V,而不要把全部电流都压给 snickerdoodle black 的接口。
6.2 坑二:在覆盖层里动了 PYNQ 默认的base.bit却忘记备份
我为了测试自定义 GPIO 映射,直接把/home/xilinx/pynq/overlays/base/里的原始base.bit覆盖了。后来想验证 PYNQ 自带的一些示例,发现全部报错,因为它们的 Python 脚本还是按照默认 pin 设计来访问硬件。
这个坑的教训很简单:永远复制一份原始base.bit和base.hwh到别的目录,或者直接用cp备份到/home/xilinx/pynq/overlays/base_original/,之后怎么折腾都不心疼。
6.3 坑三:把 HAT 的 I2C 地址和 AXI IIC 的地址空间搞混
PYNQ 里的axi_iic_0是一个挂在 AXI 总线上的外设 IP,它有自己的内存映射基地址,比如0x41600000。而 HAT 的 EEPROM 挂在 I2C 总线上的地址,是0x50。这两个"地址"完全不是一回事。
我一开始在 Python 里写了iic.read(0x41600000),自然什么也读不到。正确做法是先由 PYNQ 的 overlay 对象解析出 AXI 外设,再用它去发起 I2C 总线上的传输。换句话说:
# 错误:把 AXI 地址当 I2C 地址 data = iic.read(0x41600000, 0) # 正确:先拿外设对象,再指定 I2C 从机地址 iic = ol.axi_iic_0 data = iic.read(0x50, 0, 8)这种混淆在早期很常见,因为你脑子里同时存在两条总线概念,稍不注意就越界了。
6.4 最小验证清单
我现在每次拿到一块新的 snickerdoodle black、piSmasher 组合,都会按这个清单过一遍,确认整套环境是健康的:
| 检查项 | 操作方式 | 通过标准 |
|---|---|---|
| 内核 FPGA manager | dmesg | grep fpga | 能看到驱动注册 |
| 设备树 fpga-region | /proc/device-tree/fpga0存在 | 能列出节点 |
| 默认覆盖层加载 | Overlay('base.bit') | 无报错 |
| PL GPIO 输出 | 点灯脚本 | LED 按预期亮灭 |
| PL GPIO 输入 | 按键读取脚本 | 读到状态变化 |
| I2C EEPROM 读取 | iic.read(0x50, 0, 4) | 返回非全 0xFF 数据 |
| 视频通路(可选) | VideoCapture类实例化 | 能获取帧尺寸 |
以上每一项如果失败,都可以直接定位是 BSP 问题、硬件接线问题、还是 overlay 设计问题,不用再靠猜。
7. 关于后续扩展和我的个人体会
这套组合跑通基础 overlay 之后,真正的乐趣才开始。我目前在准备两个扩展方向:一个是把图像传感器接上,用自定义覆盖层做实时边缘提取,再在 Python 里配合模型做识别;另一个是用多路 AXI GPIO 模拟一个简单的逻辑分析仪,把 piSmasher 引出的传感器信号全部抓下来,在 Jupyter 里直接画时序图。
这里再说一点,PYNQ 4.0 相比旧版在覆盖层管理上更稳定,JupyterLab 环境也顺手很多,但底层依然是 Linux 加设备树覆盖层那一套。遇到问题多方案并行,不如先把引脚映射确认得足够扎实,后面会省很多时间。
如果你手头的板子和我的不完全一样,尤其是引脚版本不同,一定不要直接复制我表里的映射关系。拿起万用表,花十分钟从物理引脚 1 开始逐项测一遍,这十分钟换来的是后面几十个小时不折腾。跑通之后的满足感会让你觉得,这套小体积组合确实值得。