很多人把它当成教学框架,其实它是 AMD 官方的运行时。真正值钱的只有一个机制——overlay。
01为什么把它排在第一期
因为它是整条链路上投入产出比最高的一段。
绝大多数人第一次碰 ZYNQ,卡点完全一样:PL 里辛辛苦苦写的逻辑跑起来了,灯也闪了,但 PS 那边不知道该怎么把数据喂进去。你面前只有两条路——要么写裸机 C,翻 xparameters.h 找基地址、自己配中断;要么跑 Linux,写一个字符设备驱动,编译、insmod、创建设备节点。
两条路都要几个小时起步,而且第一次基本都会失败。失败的地方还特别隐蔽:地址对了但 cache 没刷、中断注册了但没使能、DMA 跑完了但你读的是旧 buffer。
PYNQ 把这一整段抹平了。一个 .bit 加一个 .hwh,Overlay() 一下,PL 里的 IP 就变成了 Python 对象,寄存器可以直接赋值,DMA 可以 transfer() 然后 wait()。
四条路的成本对比,很能说明问题:
| 方式 | 首次打通 | 后续改 IP | 适合谁 |
|---|---|---|---|
| 裸机 C | 半天到两天 | 重新生成 BSP、重编译 | 产品固件 |
| 字符设备驱动 | 两到三天 | 改驱动、重新编译内核模块 | 要进内核的正式方案 |
| UIO | 半天 | 改设备树、重载 | 轻量用户态驱动 |
| PYNQ | 十分钟 | 换个 .bit 就行 | 算法验证、快速迭代 |
注意最后一行不是"更快",是"改 IP 不用改软件"。这个性质才是它真正的价值。
02它到底是什么
REPOSITORY
https://github.com/Xilinx/PYNQ
拆开看是两层东西,很多人只知道第一层:
• 一个跑在 PS 上的 Linux 镜像,自带 Jupyter Notebook,浏览器打开就能写代码;
• 一个叫 pynq 的 Python 包,负责加载比特流、解析硬件信息、管理内存、封装中断。
关键在第二个文件:.hwh。它是 Vivado 导出比特流时一起生成的 XML,里面老老实实记着每个 IP 的基地址、寄存器偏移、位宽、中断号、甚至 IP 之间的连接关系。
PYNQ 在 Overlay() 的时候读这个文件,然后动态生成 Python 属性和方法。所以 ol.accel_0.register_map.CTRL = 1 这一行,背后是一次真实的 AXI-Lite 写事务——只不过地址是运行时从 XML 里查出来的。
这就是为什么你可以完全不写 C。硬件的"接口描述"被固化在比特流旁边,软件侧照着描述自动生成。
TIP
.hwh 是 Vivado 2019.2 之后的名字,更早的版本叫 .tcl。网上老教程里让你 Overlay("x.bit", "x.tcl") 的写法已经过时了,新版本只需要传 .bit,它自己去旁边找同名 .hwh。
03值得读的三处
▍pynq/overlay.py —— 整条链路的入口
看 Overlay.__init__ 和 _download()。它会先解析 hwh 建出 ip_dict(所有 IP 的名字、物理地址、地址区间、中断号),再按 IP 的 VLNV 类型去匹配驱动类。
匹配逻辑在 DefaultIP 和 _get_driver_class() 里。走通了这段你就明白:为什么有些 IP 能被识别成 DMA,有些只能当普通寄存器用——因为 PYNQ 内置了一张 VLNV 到驱动类的映射表,表里没有的,就退化成 DefaultIP。
这也解释了最常见的困惑:"为什么我的 IP 在 ip_dict 里,但没有 register_map?"——因为 register_map 依赖 hwh 里的寄存器描述,而只有走 AXI-Lite 且被 Vivado 正确标注的 IP 才有。
▍pynq/buffer.py —— 连续物理内存怎么分配
这是 90% 的人踩坑的地方。DMA 要的是物理连续内存,而 Linux 用户态拿到的是虚拟地址,页与页之间可能隔着十万八千里。
allocate() 走的是 xlnk 的替代方案:从内核的 CMA(连续内存分配器)池里拿物理连续页,再把物理地址映射到用户态。所以你在 PYNQ 里 allocate(shape=(4096,), dtype=np.uint32) 拿到的 buffer,buffer.physical_address 才是能写进 DMA 寄存器的那一个。
还有一个隐藏点:cache 一致性。ARM 的 L1/L2 是开启的,你往 buffer 写了数据,DMA 读到的可能是 cache 里的旧值。PYNQ 的 buffer 对象在 transfer() 前后会自动做 flush / invalidate,但这个"自动"只对通过它自己分配的 buffer 有效。你自己 numpy.zeros() 出来的数组传进去,它不会帮你刷——因为它压根不知道物理地址。
▍pynq/lib/ —— 一个"好用的 IP 封装"长什么样
里面是官方写好的 DMA、视频管线、RF 数据转换器、音频 codec 封装。想知道"如果我做的 IP 要给 20 个人用,接口该怎么设计",看这个目录比看任何规范都快。
04上手:十分钟跑通第一个 overlay
from pynq import Overlay, allocate
import numpy as np
ol = Overlay("accel.bit")
print(ol.ip_dict) # 所有 IP 的名字/地址/中断
ol.accel_0.register_map.CTRL = 1 # 直接写寄存器
buf = allocate(shape=(4096,), dtype=np.uint32)
buf[:] = np.arange(4096)
ol.axi_dma_0.sendchannel.transfer(buf)
ol.axi_dma_0.sendchannel.wait()
四行核心代码对应四件事:加载、查表、配置、搬数据。
如果你手上还没有板子,PYNQ 官网提供了预编译镜像,Zynq-7000 系列(PYNQ-Z1/Z2、Zybo Z7)和 Zynq UltraScale+(KV260、ZCU104)都有。烧到 SD 卡,插上网线,浏览器输板子的 IP 就能进 Jupyter。
TIP
第一次用建议直接跑镜像里自带的 base overlay 的示例 notebook。它会带你走一遍 ip_dict 打印、寄存器读写、DMA 传输,比看文档快得多,而且能验证整条链路是通的。
05三个坑
一、.bit 和 .hwh 必须同名、同目录。少一个,Overlay 不会报错,但 ip_dict 是空的,然后你会以为是比特流没下进去,反复重启板子。
二、PYNQ 版本和 Vivado 版本是绑死的。PYNQ 2.7 对应 Vivado 2020.2 左右,3.0 之后要求 Vivado 2022.1 以上。你用一个 2023.1 导出的 hwh 去喂 2.7 的镜像,多半解析失败或者 IP 识别不出来。镜像文档里写了哪个版本,就用那个版本导出。
三、Xlnk 已经废弃。2020 年以前所有教程都在用 from pynq import Xlnk; xlnk = Xlnk(),现在请换成 pynq.allocate。搜到老代码先看它引的是哪个。
06它不适合谁
不适合做量产产品。
PYNQ 的镜像是几个 GB 的通用 Linux,启动要几十秒,内存占用不小,而且它把"通用性"放在了"确定性"前面——中断延迟、DMA 吞吐都不是硬实时级别。你要做的是工业设备、车载、医疗,最终还是要回到裸机或者精简的 Linux + 自写驱动。
但如果你在做算法验证、教学、原型演示、论文里的对比实验,PYNQ 能让你把"硬件对不对"和"软件怎么写"这两件事解耦开——先专心把 PL 调对,Python 那边随时能改。
07写在最后
PYNQ 不教你硬件设计,它只负责把"硬件已经做好了"之后的最后一百米铺平。
而对做算法验证的人来说,这一百米往往是最贵的。
…………… END ……………
63