Zynq 实战 16|PYNQ 框架:Jupyter 里 Python 控 PL
Zynq 实战 16|PYNQ 框架:Jupyter 里 Python 控 PL
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 16 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 15|Vitis HLS:用 C 写硬件 IP 的正确姿势》
0. 这一篇要解决什么问题
第 06 篇做了 PWM IP,第 12 篇做了 AXI DMA,之前的驱动都是 C 或 PetaLinux 内核驱动。这一篇换个思路:用 PYNQ 框架,在 Jupyter Notebook 里用 Python 直接操控 PL。
PYNQ 的核心卖点:把 FPGA 配置(bitstream)和 Python API 打包成一个 Overlay,用 3 行代码加载 PL,再用 mmio.write(offset, value) 写寄存器。对于快速原型、教学演示、算法验证场景,比写 Linux 驱动快 10 倍。
本篇覆盖:
- PYNQ v3.0 SD 卡镜像烧录、WiFi 连接、Jupyter 访问
- Overlay 三件套(.bit + .hwh + .tcl)是什么、从哪来
- 用 PYNQ API 控制第 06 篇 PWM IP + 第 12 篇 AXI DMA
- 自己从 Vivado 工程生成 Overlay(不依赖官方预装的 overlay)
- PYNQ vs 原生 PetaLinux:什么时候该用哪个
本篇不覆盖 PYNQ 的 RFSoC 扩展和 Composable Pipeline——那是 UltraScale+ 的内容。
1. PYNQ v3.0 环境搭建
1.1 SD 卡镜像下载与烧录
PYNQ v3.0 是目前(2023 年底)Pynq-Z2 的最新稳定版,基于 Ubuntu 22.04 LTS,Python 3.10。
下载:
https://github.com/Xilinx/PYNQ/releases/tag/v3.0.0
→ pynq_z2_v3.0.0.img.zip(约 3.4 GB)
🚧 避坑:PYNQ 官网(pynq.io)的下载链接有时会 404 或者重定向到旧版本。直接去 GitHub Releases 页面下载最可靠。另外,v3.0.0 和 v2.x 的 API 有破坏性变更——
from pynq import Overlay的方式没变,但allocate()的行为和 DMA API 有调整。本篇全部基于 v3.0.0。
烧录(macOS/Linux):
# 解压
unzip pynq_z2_v3.0.0.img.zip
# 找到 SD 卡设备(macOS 示例)
diskutil list
# 假设 SD 卡是 /dev/disk4
# 卸载(不是 eject)
diskutil unmountDisk /dev/disk4
# 烧录(约 10 分钟)
sudo dd if=pynq_z2_v3.0.0.img of=/dev/rdisk4 bs=4m status=progress
# 烧录完毕后 eject
diskutil eject /dev/disk4
Windows 用 Balena Etcher,流程一样,选镜像文件和目标设备。
1.2 首次启动与网络
Pynq-Z2 的启动模式拨码开关(JP4):
- SD 卡启动:
SD(靠近 HDMI 那侧拨到 SD 位置)
用网线把 Pynq-Z2 的 ETH0 接到电脑或路由器,启动后约 60 秒:
# 固定 IP(网线直连电脑时)
http://192.168.2.99
# DHCP(接路由器时)
# 查 IP:路由器管理界面,或 nmap -sn 192.168.1.0/24 | grep pynq
默认密码:xilinx
WiFi 配置(可选):
# SSH 登录
ssh xilinx@192.168.2.99
# 配置 WiFi
nmcli dev wifi connect "你的WiFi名" password "密码"
ip addr show wlan0
# 之后可以拔掉网线用 WiFi
1.3 Jupyter Notebook 访问
浏览器打开 http://192.168.2.99,输入密码 xilinx,进入 JupyterLab 界面。
默认目录结构:
/home/xilinx/jupyter_notebooks/
├── base/ ← 官方示例(GPIO、LED、DMA)
├── common/ ← 共享工具
└── pynq/ ← 你的工程目录
2. Overlay 概念:三件套是什么
三件套各自的作用:
| 文件 | 内容 | 必须? | 来源 |
|---|---|---|---|
design.bit | FPGA 配置数据(二进制) | ✅ 必须 | Vivado Generate Bitstream |
design.hwh | IP 实例名/地址/参数的 XML 描述 | ✅ 必须(PYNQ v2.5+) | .xsa 解压出来 |
design.tcl | Block Design 重建脚本 | 可选 | Vivado File→Export→Block Design |
PYNQ 如何用 HWH:Overlay("design.bit") 加载时,PYNQ 解析同名的 design.hwh,自动发现里面所有 IP 实例(名字、类型、地址),然后为每个 IP 构造对应的 Python 对象:
- AXI GPIO →
pynq.lib.AxiGPIO - AXI DMA →
pynq.lib.dma.DMA - 未知 IP →
pynq.MMIO(直接寄存器读写)
3. 加载官方 Base Overlay
PYNQ 镜像里预装了 base overlay,包含 Pynq-Z2 所有外设的 PL 配置。先用它验证环境:
# Notebook Cell 1 — 加载 base overlay
from pynq import Overlay
from pynq.lib import AxiGPIO
import time
# 加载 PL 配置(约 1 秒)
ol = Overlay('/home/xilinx/pynq/overlays/base/base.bit')
print("Overlay 加载成功")
print(ol.ip_dict.keys()) # 查看所有 IP 实例名
输出(节选):
Overlay 加载成功
dict_keys(['axi_gpio_0', 'axi_gpio_1', 'axi_timer_0', 'axi_dma_0', ...])
# Notebook Cell 2 — 控制 4 个 LED(Pynq-Z2 板载)
# base overlay 里 LED 连接在 axi_gpio_0 的 channel 1
leds = ol.leds # base overlay 里有预定义的快捷属性
# 等价写法:leds = AxiGPIO(ol.ip_dict['axi_gpio_0']).channel1
for i in range(4):
leds[i].toggle()
time.sleep(0.1)
print(f"LED[{i}] 切换")
# 全亮
leds.write(0xf, 0xf)
time.sleep(0.5)
# 全灭
leds.write(0x0, 0xf)
print("LED 控制完成")
4. 用 MMIO 控制第 06 篇 PWM IP
如果你的 Vivado 工程里有第 06 篇的 PWM IP(基地址 0x43C00000),可以直接用 MMIO:
# Notebook Cell 3 — 用 MMIO 控制 PWM IP
# 不需要专属 Python class,直接读写寄存器
from pynq import MMIO
# PWM IP 寄存器映射(和第 06、11 篇一致)
PWM_BASE = 0x43C00000
PWM_RANGE = 0x10000 # 64KB
# 偏移定义
CTRL_OFF = 0x00 # [0]=enable, [1]=reset
PERIOD_OFF = 0x04 # PWM 周期(100MHz 时钟周期数)
HIGH_OFF = 0x08 # 高电平时长
STATUS_OFF = 0x0C # [0]=busy, [1]=irq_pending
# 创建 MMIO 对象
pwm = MMIO(PWM_BASE, PWM_RANGE)
def pwm_set_duty(duty_pct, freq_hz=1000):
"""设置 PWM 占空比和频率
Args:
duty_pct: 占空比 0-100
freq_hz: 频率(Hz),默认 1kHz
"""
PL_CLK_HZ = 100_000_000 # FCLK_CLK0 = 100 MHz
period = PL_CLK_HZ // freq_hz # = 100000 for 1kHz
high = int(period * duty_pct / 100)
pwm.write(CTRL_OFF, 0x00) # 先停止(防止毛刺)
pwm.write(PERIOD_OFF, period)
pwm.write(HIGH_OFF, high)
pwm.write(CTRL_OFF, 0x01) # 启动
print(f"PWM: {freq_hz}Hz, 占空比 {duty_pct}%, PERIOD={period}, HIGH={high}")
def pwm_read_status():
ctrl = pwm.read(CTRL_OFF)
period = pwm.read(PERIOD_OFF)
high = pwm.read(HIGH_OFF)
status = pwm.read(STATUS_OFF)
duty = high / period * 100 if period > 0 else 0
print(f"CTRL=0x{ctrl:08x} PERIOD={period} HIGH={high} STATUS=0x{status:08x}")
print(f"实际占空比: {duty:.1f}%")
return duty
# 测试:从 10% 增到 90%
print("=== PWM 占空比扫描 ===")
for d in [10, 25, 50, 75, 90]:
pwm_set_duty(d)
time.sleep(0.2)
pwm_read_status()
print()
# 停止
pwm.write(CTRL_OFF, 0x00)
print("PWM 已停止")
5. 完整 Notebook:LED 控制 + PWM 状态监控
# ============================================================
# zynq_pwm_led_demo.ipynb — Zynq 实战 16 演示 Notebook
# 依赖:PYNQ v3.0,自定义 overlay(含 PWM IP + AXI GPIO)
# ============================================================
# Cell 1: 导入 & 加载 overlay
# ─────────────────────────────────────────────────────────────
from pynq import Overlay, MMIO
from pynq.lib import AxiGPIO
import numpy as np
import matplotlib.pyplot as plt
import time, ipywidgets as widgets
from IPython.display import display
# 加载你自定义的 overlay(见第 7 节如何生成)
# 如果先用 base overlay 测试,改成 '/home/xilinx/pynq/overlays/base/base.bit'
ol = Overlay('/home/xilinx/pynq/overlays/pwm_demo/pwm_demo.bit')
print("✅ Overlay 已加载")
print(f"包含 IP: {list(ol.ip_dict.keys())}")
# Cell 2: 初始化外设
# ─────────────────────────────────────────────────────────────
# LED(AXI GPIO)
gpio = AxiGPIO(ol.ip_dict['axi_gpio_0'])
leds = gpio.channel1 # channel1 = output(LED)
btns = gpio.channel2 # channel2 = input(按钮)
# PWM IP(MMIO 直接访问)
PWM_BASE = 0x43C00000
pwm = MMIO(PWM_BASE, 0x10000)
print("✅ GPIO & PWM 初始化完成")
# Cell 3: 交互式 PWM 控制 Widget
# ─────────────────────────────────────────────────────────────
duty_slider = widgets.IntSlider(
value=50, min=0, max=100, step=1,
description='占空比%:', style={'description_width': 'initial'}
)
freq_dropdown = widgets.Dropdown(
options=[('100 Hz', 100), ('1 kHz', 1000), ('10 kHz', 10000)],
value=1000,
description='频率:'
)
start_btn = widgets.Button(description='启动 PWM', button_style='success')
stop_btn = widgets.Button(description='停止 PWM', button_style='danger')
status_out = widgets.Output()
def on_start(_):
d = duty_slider.value
f = freq_dropdown.value
PL_CLK = 100_000_000
period = PL_CLK // f
high = period * d // 100
pwm.write(0x00, 0) # 停止
pwm.write(0x04, period)
pwm.write(0x08, high)
pwm.write(0x00, 1) # 启动
with status_out:
status_out.clear_output(wait=True)
print(f"▶ 运行中: {f}Hz, {d}% 占空比")
print(f" PERIOD={period}, HIGH={high}")
# LED 用占空比编码(高 2 位 → 4 颗 LED)
led_val = int(d / 25) # 0,1,2,3,4 → 0~4 颗 LED 亮
leds.write(min(led_val, 0xF), 0xF)
def on_stop(_):
pwm.write(0x00, 0)
leds.write(0x0, 0xF)
with status_out:
status_out.clear_output(wait=True)
print("⏹ PWM 已停止")
start_btn.on_click(on_start)
stop_btn.on_click(on_stop)
display(widgets.VBox([
widgets.HBox([duty_slider, freq_dropdown]),
widgets.HBox([start_btn, stop_btn]),
status_out
]))
# Cell 4: 读取按钮状态
# ─────────────────────────────────────────────────────────────
print("按钮状态(按 Pynq-Z2 的 BTN0-3):")
btn_val = btns.read()
for i in range(4):
state = "按下" if (btn_val >> i) & 1 else "未按"
print(f" BTN{i}: {state}")
# Cell 5: 占空比扫描 + 实时绘图
# ─────────────────────────────────────────────────────────────
duties = list(range(0, 101, 5))
measured = []
for d in duties:
pwm.write(0x00, 0)
period = 100000
high = period * d // 100
pwm.write(0x04, period)
pwm.write(0x08, high)
pwm.write(0x00, 1)
time.sleep(0.05)
# 回读验证
rd_high = pwm.read(0x08)
actual = rd_high / period * 100
measured.append(actual)
pwm.write(0x00, 0)
plt.figure(figsize=(8, 4))
plt.plot(duties, measured, 'o-', color='#3b82f6', label='实测占空比')
plt.plot(duties, duties, '--', color='#94a3b8', label='理想值')
plt.xlabel('设定占空比 (%)')
plt.ylabel('实测占空比 (%)')
plt.title('PWM IP 占空比线性度(PYNQ MMIO 读回验证)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('/home/xilinx/pwm_linearity.png', dpi=150)
plt.show()
print("图表已保存")
6. 用 pynq.lib.dma 做 AXI DMA 数据搬运
接第 12 篇的 AXI DMA 工程(MM2S + S2MM 通道):
# Cell 6: AXI DMA 数据搬运演示
# ─────────────────────────────────────────────────────────────
# 依赖:overlay 里有 axi_dma_0,连接了 loopback(S2MM 接 MM2S)
# 或者接第 15 篇的 FIR IP
import pynq
from pynq import allocate
import numpy as np
# 获取 DMA 对象(PYNQ v3.0 API)
dma = ol.axi_dma_0
N = 1024 # 传输样本数
# allocate: 在 PS DDR 中分配 CMA(连续物理内存)buffer
# dtype=np.int16 对应 FIR IP 的 16-bit AXI Stream 数据
# PYNQ v3.0 里 allocate 返回 pynq.buffer.PynqBuffer,支持 numpy 操作
in_buf = allocate(shape=(N,), dtype=np.int16, cacheable=False)
out_buf = allocate(shape=(N,), dtype=np.int16, cacheable=False)
# 生成测试数据:100Hz 正弦波(Q2.13 定点,8192 = 1.0)
FS = 48000
t = np.arange(N) / FS
sig = 0.4 * np.sin(2 * np.pi * 100 * t) + 0.4 * np.sin(2 * np.pi * 20000 * t)
in_buf[:] = np.clip(sig * 8192, -8192, 8191).astype(np.int16)
print(f"输入 buffer: {N} 样本, 物理地址 = 0x{in_buf.physical_address:08x}")
print(f"输出 buffer: {N} 样本, 物理地址 = 0x{out_buf.physical_address:08x}")
# 启动 DMA 传输(S2MM 先 start,等待接收数据)
t_start = time.perf_counter()
# sendchannel: MM2S(PS DDR → PL)
# recvchannel: S2MM(PL → PS DDR)
dma.recvchannel.transfer(out_buf)
dma.sendchannel.transfer(in_buf)
# 等待两个通道都完成
dma.sendchannel.wait()
dma.recvchannel.wait()
t_end = time.perf_counter()
elapsed_ms = (t_end - t_start) * 1000
# 计算吞吐量
bytes_transferred = N * 2 * 2 # 双向,每样本 2 字节
throughput_mb = bytes_transferred / (t_end - t_start) / 1e6
print(f"\nDMA 传输完成")
print(f" 耗时: {elapsed_ms:.2f} ms")
print(f" 吞吐: {throughput_mb:.1f} MB/s(双向)")
print(f" 输出前 8 样本: {out_buf[:8]}")
# 如果接 FIR IP,验证滤波效果
input_rms = np.sqrt(np.mean(in_buf.astype(float) ** 2))
output_rms = np.sqrt(np.mean(out_buf.astype(float) ** 2))
print(f"\n输入 RMS: {input_rms:.1f} LSB")
print(f"输出 RMS: {output_rms:.1f} LSB(低通滤波后 20kHz 分量被衰减)")
# 释放 CMA buffer
in_buf.freebuffer()
out_buf.freebuffer()
print("Buffer 已释放")
实测数字(PYNQ v3.0, Pynq-Z2, N=1024 int16):
| 指标 | 数值 |
|---|---|
| 单次 DMA 传输(1024×2B MM2S+S2MM) | 1.3 ms |
| 等效吞吐(双向) | ~3.1 MB/s |
| AXI HP 理论带宽 | 1200 MB/s |
🚧 避坑:PYNQ v3.0 的
allocate()默认cacheable=False,即 buffer 是非缓存映射(等价于第 11 篇讲的pgprot_noncached)。不要设成cacheable=True——那样 Python 侧的写操作会停在 CPU cache 里,DMA 从物理地址读到的是旧数据。这是 PYNQ 初学者最常见的数据不一致问题。
7. 从自己的 Vivado 工程生成 PYNQ Overlay
官方预装的 base overlay 功能固定。要控制你自己设计的 IP,需要生成自己的 overlay。
7.1 Vivado 工程准备
确保你的 Block Design 包含:
processing_system7_0(配置好 FCLK_CLK0 = 100 MHz)- 你的自定义 IP(比如第 06 篇 PWM IP + 第 15 篇 FIR IP)
- AXI Interconnect / SmartConnect 连接好
- 地址已在 Address Editor 分配
7.2 生成 Bitstream
Flow Navigator → Generate Bitstream(Vivado 2023.2)
完成后(约 5-10 分钟),bitstream 在:
<vivado-project>/<project-name>.runs/impl_1/<top_name>.bit
7.3 导出 Hardware(获取 .hwh)
Vivado → File → Export → Export Hardware
Include bitstream: ✅ 勾选
Output file: pwm_demo.xsa
.xsa 是一个 zip 包,里面包含 .hwh 文件:
# 解压 .xsa 获取 .hwh
cp pwm_demo.xsa /tmp/pwm_demo.zip
cd /tmp && unzip pwm_demo.zip
ls *.hwh
# → pwm_demo.hwh(或类似名字,看 Block Design 顶层名)
🚧 避坑:
.hwh文件名必须和.bit文件名完全一致(除了后缀)。如果.bit叫pwm_demo.bit,那.hwh必须叫pwm_demo.hwh。PYNQ 在Overlay("pwm_demo.bit")时会自动查找同目录下的pwm_demo.hwh,文件名不匹配则报错FileNotFoundError: hwh file not found。
7.4 传到板子并加载
# 在开发机上
scp pwm_demo.bit xilinx@192.168.2.99:/home/xilinx/pynq/overlays/pwm_demo/
scp pwm_demo.hwh xilinx@192.168.2.99:/home/xilinx/pynq/overlays/pwm_demo/
# Jupyter Notebook
from pynq import Overlay
ol = Overlay('/home/xilinx/pynq/overlays/pwm_demo/pwm_demo.bit')
# 查看 PYNQ 自动识别的 IP
print(ol.ip_dict)
# 输出类似:
# {'pwm_ctrl_0': {'phys_addr': 0x43c00000, 'addr_range': 65536, ...},
# 'fir5_filter_0': {'phys_addr': 0x43c10000, ...}}
# 直接访问自定义 IP(未知 IP 类型 → 自动用 MMIO 包装)
pwm = ol.pwm_ctrl_0
print(type(pwm)) # <class 'pynq.mmio.MMIO'>
pwm.write(0x04, 100000) # 写 PERIOD 寄存器
7.5 为自定义 IP 写 Python Driver(可选)
PYNQ 允许你为自定义 IP 绑定 Python 类:
# pwm_driver.py — 放到 overlay 同目录下,或加入 PYTHONPATH
from pynq import DefaultIP
class PwmDriver(DefaultIP):
"""Pynq-Z2 自定义 PWM IP 的 Python 驱动"""
bindto = ['kaiyo:hls:pwm_ctrl:1.0'] # 必须和 IP component.xml 里的 VLNV 一致
PL_CLK_HZ = 100_000_000
CTRL_OFF = 0x00
PERIOD_OFF = 0x04
HIGH_OFF = 0x08
STATUS_OFF = 0x0C
def __init__(self, description):
super().__init__(description)
def start(self):
self.write(self.CTRL_OFF, 0x01)
def stop(self):
self.write(self.CTRL_OFF, 0x00)
def set_duty(self, duty_pct, freq_hz=1000):
"""设置占空比(0-100)和频率(Hz)"""
period = self.PL_CLK_HZ // freq_hz
high = period * duty_pct // 100
self.stop()
self.write(self.PERIOD_OFF, period)
self.write(self.HIGH_OFF, high)
self.start()
return period, high
@property
def duty_pct(self):
period = self.read(self.PERIOD_OFF)
high = self.read(self.HIGH_OFF)
return high / period * 100 if period > 0 else 0
@property
def is_running(self):
return bool(self.read(self.CTRL_OFF) & 0x01)
加载后,ol.pwm_ctrl_0 会自动变成 PwmDriver 实例:
from pwm_driver import PwmDriver # 或加入 overlay 目录的 __init__.py
ol = Overlay('pwm_demo.bit')
pwm = ol.pwm_ctrl_0 # 现在是 PwmDriver 实例
pwm.set_duty(75, freq_hz=1000)
print(f"当前占空比: {pwm.duty_pct:.1f}%")
print(f"运行中: {pwm.is_running}")
8. PYNQ vs 原生 PetaLinux:取舍判断
这是一个值得认真回答的问题,不是 “PYNQ 更简单所以都用 PYNQ”。
PYNQ 的真实代价
| 项目 | PYNQ v3.0 | 原生 PetaLinux 2023.2 |
|---|---|---|
| 镜像大小 | 3.4 GB(SD 卡) | ~500 MB |
| 启动时间 | ~75 秒(Ubuntu 完整启动) | ~20 秒(精简 rootfs) |
| DDR 占用(空闲) | ~420 MB(Ubuntu + Jupyter) | ~80 MB |
| Python 调用 MMIO 延迟 | ~15 μs/次(含 Python 解释开销) | ~1 μs(C 直接 mmap) |
| 中断处理延迟 | 不适合(Python GIL 问题) | 适合(内核驱动 ISR μs 级) |
用 PYNQ 的场景
- 算法快速原型:新 IP 验证,从 HLS 导出到 Python 控制只需 30 分钟
- 教学演示:Jupyter Notebook 结合图表,展示效果直观
- 数据科学 + FPGA:numpy/pandas 处理 DMA 数据,快速分析
- 竞赛/黑客松:要在几小时内出演示,PYNQ 是正确选择
用原生 PetaLinux 的场景
- 商业产品:发货的产品固件,不应该带 Jupyter 服务
- 实时性要求:中断延迟 <10 μs,调度抖动敏感
- 资源受限:DDR 只有 256 MB,装不下 PYNQ
- 安全合规:Ubuntu 的 attack surface 太大,产品需要最小化 rootfs
- 多核/AMP 场景:第 18 篇 OpenAMP,需要精确控制核心分配
🚧 避坑:有人把 PYNQ 直接用在量产产品里,然后遇到 Ubuntu 22.04 的安全更新破坏了 PYNQ 的 Python 包依赖,导致 Overlay 加载失败。PYNQ 的依赖链(Ubuntu + Python 包)更新频率远高于 Xilinx BSP,量产版本锁死依赖版本很麻烦。PYNQ 是开发工具,不是产品平台。
9. 本篇你应该带走的判断
- 能独立烧录 PYNQ v3.0 SD 卡,访问 Jupyter Notebook
- 理解 Overlay 三件套(.bit/.hwh/.tcl)各自的作用
- 知道
.hwh从 Vivado Export Hardware 的.xsa解压得到 - 能用
MMIO读写自定义 IP 寄存器,用AxiGPIO控制 GPIO - 知道
allocate(cacheable=False)和 DMA 一致性的关系 - 能判断自己的项目该用 PYNQ 还是原生 PetaLinux
10. 下一篇预告
下一篇 《Zynq 实战 17|lwIP + 千兆以太网:一个能用的 HTTP Server》,我们会:
- 在裸机(Vitis 无 OS)下用 lwIP 2.1.x 写一个 HTTP Server
- 返回板子状态 JSON(温度、PWM 占空比、运行时间)
- 测试 PS GEM0(千兆 PHY RTL8211E)的实际吞吐:940 Mbps RX / 880 Mbps TX
- 踩透 PHY reset 时序、MIO 配置、jumbo frame 的坑
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| PYNQ Docs | PYNQ v3.0.0 Documentation | Overlay/MMIO/DMA API 完整参考 |
| UG1144 | PetaLinux Tools Reference Guide 2023.2 | 对比 PYNQ 和 PetaLinux 工作流 |
| UG994 | Vivado Design Suite User Guide: Designing IP Subsystems | Block Design → Export Hardware → .xsa 流程 |
| PG021 | AXI GPIO Product Guide | AxiGPIO IP 寄存器映射,与 PYNQ AxiGPIO API 对照 |
| PG021 | AXI DMA Product Guide (PG021) | DMA MM2S/S2MM 通道控制,对应 pynq.lib.dma |
| GitHub | Xilinx/PYNQ releases | v3.0.0 镜像下载,API 变更说明 |
这是《Zynq FPGA 嵌入式系统设计实战》系列第 16 篇。 如果你在 Overlay 加载失败(hwh 找不到)或 DMA 数据不一致上踩了坑,欢迎留言描述症状——尤其说明 PYNQ 版本和 Vivado 版本,这两个版本不匹配是最常见的根因。