资讯动态

PYNQ/Zynq HDMI输入输出实战:从视频链路搭建到VDMA驱动调试

发布时间:2026/10/14 20:51:34 来源:尧图企业网站定制
简介这份资源针对PYNQ平台兼容Zynq提供了一套已跑通的HDMI输入输出工程面向嵌入式与FPGA开发者用于快速实现视频采集、显示及图像处理。工程展示了基于AXI4-Stream的HDMI收发控制包含时钟同步、色度空间转换、视频时序生成等关键环节并支持通过Python API调用硬件资源。资源共380个文件、约3.14MB以.v/.vhd/.vhdl硬件描述、.xdc约束、.xci IP配置、.tcl脚本及工程日志为主同时包含Vivado工程文件、模块设计和比特流便于直接导入验证或二次开发。借助本工程读者可以学习完整的HDMI输入输出链路搭建方法、常见调试排错思路以及如何将PYNQ的软硬件协同能力用于实际多媒体项目。目前已有2638人学习适合需要快速上手Zynq视频接口设计的工程师和学生。1. 在 PYNQ 上跑通 HDMI in/out先要过这三道坎把 HDMI 输入输出同时放到 PYNQ 或 Zynq 上真正劝退人的不是 Python 接口而是黑匣子一样的视频链路插上线黑屏、分辨率识别不出来、画面撕裂。HDMI 输入不是拿逻辑分析仪抓 TMDS 差分线就完事它背后藏着 EDID、HPD、像素时钟恢复、VTC 时序、VDMA 缓冲和 AXI-Stream 视频协议。这套资源解决的就是把 HDMI in 和 HDMI out 两条通路在 PYNQ/Zynq 上完整打通并给出可复现的 Block Design 源码包与 Python 驱动。适合做视频采集、图像处理、异构计算实验以及被“HDMI 只有输出没有输入”这类套路板卡坑过的人。按文章顺序推一遍你能把链路和踩坑全部对齐。2. HDMI 视频通路选型RX/TX IP、时钟与 AXI-Stream 链路怎么搭2.1 RX/TX 两侧的 IP 分工PHY、桥接 IP 与 VTC 各自干什么HDMI 物理层拿到的是 TMDS 差分信号一组时钟通道带三根数据通道。RX 侧要做的事情不是“把线接上就行”而是先恢复像素时钟再把串行数据解成并行像素最后按 HDMI 规定的 Blanking 时序把 DE、HSYNC、VSYNC 还原出来。TX 侧则是反过程把并行像素和时序信号编码成 TMDS 差分对发出去。在这个资源里我建议直接把链路拆成三层。第一层是 PHY 层 IP负责 TMDS 时钟恢复、解串和编码常见的做法是使用 HDMI 1.4/2.0 的 RX PHY 或 TX PHY 类 IP第二层是视频桥接层负责把 PHY 出来的并行视频时序转成 AXI4-Stream 协议典型组件是 Video In to AXI4-Stream / AXI4-Stream to Video Out 这类桥第三层是控制与搬运层包括 Video Timing ControllerVTC负责生成或解析时序以及 VDMA 负责在 DDR 和视频端之间搬运帧数据。三者角色不重叠缺一个都不行。PHY 没有解串能力桥接 IP 没有时序恢复能力VTC 只算时序不搬数据VDMA 只搬数据不认识视频格式。你若想在非 PYNQ 的通用 Zynq 板上复刻这套工程原理也一样PHY 层会根据板卡的高速接口方案换成对应的差分 IP桥接层和 VTC 保持不变。层次典型 IP职责关注点PHY 层HDMI RX/TX PHYTMDS 解串/编码、时钟恢复、HPD 检测接线、电源域、复位桥接层Video In to AXI4-Stream / AXI4-Stream to Video Out把视频时序转成 AXI-Stream 包TUSER/TLAST 信号位时序/搬运层VTC、VDMA生成时序、DDR 与视频间的 DMA 搬运寄存器配置、缓冲地址这套组合也是当前工具链里最稳的方案。老版本里有人用自定义 RTL 直接解 DVI 时序不是不行而是你要自己管的东西太多行场消隐窗口、DE 信号与有效像素对齐、帧起始判断。一旦分辨率从 720p 换到 1080p所有参数都要重新算而用桥接 IP 加 VTC 的方案是参数化配置切换分辨率只需要改 VTC 和 VDMA 的行列值。2.2 时钟拓扑与复位顺序是通路的命门视频链路里最玄学的部分其实是时钟。RX 侧输入的 TMDS clock 由源设备决定比如 1080p60 是 148.5 MHz。这个时钟不是直接拿去当像素时钟用的它从 TMDS clock lane 进来后要先通过 IDELAY 和 MMCM/PLL 做相位对齐再由 PHY 恢复出 bit clock 和 pixel clock。TX 侧的像素时钟通常由板载晶振经 MMCM/PLL 产生同样要求与视频流时钟严格同频同相。经验是把 RX、TX 两侧的像素时钟放在同一个 MMCM/PLL 的 BUFG 网络里管理避免跨时钟域带来的花屏。某些工具链会自动插入 Clocking Wizard但默认生成的时钟频率未必匹配 HDMI 标准。1080p60 的像素时钟 148.5 MHz行总数 2200帧总数 1125。你可以在 Clocking Wizard 里指定输入频率和输出频率然后手工把行场参数填进 VTC不要只点一个“auto”。复位顺序则是另一个坑。常见的做法是MMCM/PLL 输出 locked 信号后先把 PHY 复位释放再等 PHY 层报告接口稳定然后依次释放桥接 IP 和 VTC 的复位。反过来操作会出现一种典型现象寄存器全配好了HDMI 源端也正常但内部一直检测不到 DE帧计数永远停在 0。2.3 Block Design 的关键连线与 AXI 地址分配Vivado 里搭 Block Design 时AXI 地址分配是最容易随手出错的地方。VDMA 的 master 端口要接到 PS 侧的高速端口比如 HP/GF 口而不是普通 GP 口否则 1080p 的帧带宽会把 AXI 总线拖死。VTC、PHY 控制寄存器、GPIO 这类低速控制走 AXI-Lite 从端口即可VDMA 的 S2MM/MM2S 通道各自有独立的控制寄存器也需要挂到 AXI-Lite 总线上。给一个参考分配外设基地址端口类型VTC0x43C0_0000AXI-Lite SlaveVDMA S2MM/MM2S0x43C1_0000AXI-Lite Slave AXI MasterTX PHY 控制0x43C2_0000AXI-Lite SlaveRX PHY 控制0x43C3_0000AXI-Lite SlaveEDID IIC0x43C4_0000AXI-Lite Slave地址分配好后必须保证 Address Editor 里每个 IP 的地址范围都能让软件访问到。PYNQ 加载 Overlay 时会读取 hwh 文件里的地址映射若某个 IP 没有导出地址段Overlay 能加载成功但你访问对应外设时就会拿到空的寄存器对象。排查这个问题的第一步不是看代码而是打开 hwh 文件查一下有没有addr_segment信息。3. 把 Overlay 跑起来Vivado 工程封包与 PYNQ 驱动接入3.1 从一个 Block Design 到可加载的 PYNQ OverlayPYNQ 加载的自定义硬件不只是一个 bitstream它还需要硬件描述文件。Vivado 里完成 Block Design 综合实现后要勾选导出硬件信息并确认包含 bitstream。导出后你会得到.bit、.hwh和.tcl三个文件。PYNQ 开发时.bit和.hwh同名且放在同一路径下Overlay类就能完成解析。3. PYNQ Overlay 构建把 Zynq 的 HDMI 输入输出变成 Python 可调用硬件PYNQ 的优势是Overlay模块把硬件设计封装成可直接操作的寄存器对象。你不需要在 Linux 里写一堆字符驱动或/dev/mem代码只需要提供正确的 bit 流和 hwh 文件。对这块 HDMI 工程来说PYNQ 侧任务很简单通过Overlay加载硬件配置 VDMA 的缓冲地址和帧参数然后等着 DMA 把帧搬到内存里即可。加载 Overlay 的流程听起来简单实际操作中有不少版本差异。新的 PYNQ 版本已经在用.hwh生成硬件描述而老版本可能还依赖.tcl来自动解析寄存器映射。建议以.hwh为准在 Vivado 工程目录下检查文件是否真的生成成功。cp hdmi_rx_tx.bit /home/xilinx/jupyter_notebooks/hdmi/ cp hdmi_rx_tx.hwh /home/xilinx/jupyter_notebooks/hdmi/ cd /home/xilinx/jupyter_notebooks/hdmi/ python3这里看不到输出是很正常的接下来在 Python 里加载from pynq import Overlay ol Overlay(hdmi_rx_tx.bit) ol.download() print(HDMI Overlay loaded)这段代码有两个容易卡住的地方。第一Overlay构造时会把 bit 文件写入 PL如果 bit 文件与当前 PL 配置不匹配会报设备树错误第二download()在有些 PYNQ 版本里不是必需的因为Overlay构造时就已经下载了重复下载有时反而会把已经初始化的外设复位。我的做法是构造后直接访问 IP不做额外download()除非明确需要恢复初始状态。硬件加载完成后下一步是配置寄存器。HDMI 工程里通常有四个控制块VTC 负责时序参数VDMA 负责搬运IIC 负责读 EDIDGPIO 负责 HPD 控制。每个控制块在 PYNQ 里都映射为ol.ip_dict中的一个键。你可以用ol.ip_dict查看所有已注册外设名称。for name, info in ol.ip_dict.items(): print(name, hex(info[phys_addr]))这条命令能快速确认你需要的 IP 是否真的在硬件里暴露出来了。如果发现找不到axi_vdma_0这类名称多半是 Block Design 里给它忘记分配地址或导出时被裁剪了返回去检查第 2.3 节那张地址表。3.2 最小驱动先往 TX 端推一帧彩条数据硬件跑通后的第一个目标是让 TX 端输出画面。最稳的验证手段不是接显示器而是先用 VDMA 往 TX 输出路径灌一帧纯色图像再让 VTC 把时序打出去。如果这条链路通了接上显示器基本就是一次点亮。我一般会先分配一块内存里面填满彩条或纯色块。因为 PYNQ 的allocate返回的缓冲区物理地址是连续的且已经做了页对齐适合直接给 VDMA 用。注意像素格式要与 TX 侧配置一致常见是 BGRA 四个字节表示一个像素。from pynq import Overlay, allocate import numpy as np ol Overlay(hdmi_rx_tx.bit) WIDTH, HEIGHT 1920, 1080 buf allocate(shape(HEIGHT, WIDTH, 4), dtypenp.uint8) # 填充一幅左右两半的测试图左半红右半蓝 buf[:, :WIDTH//2, 0] 255 # B 通道 buf[:, WIDTH//2:, 2] 255 # R 通道 buf.flush() # 把测试帧地址写到 TX 侧 VDMA 的 MM2S 通道 vdma ol.axi_vdma_0 mm2s vdma.mm2s mm2s.start(buf, frame_count1)buf.flush()这段非常关键。PYNQ 分配的内存与 CPU 缓存存在一致性管理问题。你往缓冲区里写入 numpy 数据后只有执行flush()把数据写回内存VDMA 从 DDR 里读出来的内容才是你填进去的图案。很多第一次跑视频链路的新手会漏掉这一步结果 TX 端输出的是一帧全黑或花屏然后开始怀疑 PHY 和时序配置。mm2s.start(buf, frame_count1)的作用是让 MM2S 通道把 DDR 中的指定缓冲连续读出经过 AXI4-Stream 转 Video 桥后送到 TX PHY。frame_count表示传输多少帧。设为 1 表示只输出一帧但实际 HDMI TX 端如果只有一帧数据信号会因为没有后续帧而变黑。常见做法是配置成循环模式或让应用程序持续更新缓冲。如果想做静态彩条测试可以把缓冲写到 DDR 后用 DMA 持续循环读取。3.3 从 RX 通路读回一帧图像TX 通路验证完再验证 RX 通路。RX 通路的数据路径是PHY - Video In to AXI4-Stream - VDMA S2MM - DDR。软件侧只需要配置 S2MM 通道的目的地址并等待帧完成中断或轮询寄存器状态。把上面的代码反向用一遍rx_buf allocate(shape(HEIGHT, WIDTH, 4), dtypenp.uint8) s2mm vdma.s2mm s2mm.start(rx_buf, frame_count1) # 轮询帧计数寄存器等待第一帧写入完成 while vdma.read(0x24) 0x1000 0: pass rx_buf.invalidate() print(captured pixel[0,0]:, rx_buf[0, 0])这里rx_buf.invalidate()同样是为了解决缓存一致性问题。VDMA 写入 DDR 的内容不会主动同步到 CPU 缓存invalidate()会让缓存失效强制 CPU 重新从内存读取否则你拿到的可能全是旧数据。盲区在于read(0x24)读的是 S2MM 通道的状态寄存器具体偏移会因 IP 版本不同而变更稳妥的做法是读 S2MM 的帧计数寄存器并确认其值在递增。只要帧计数在涨说明数据链路已经通了如果一直为零再回去查 PHY 锁定状态和 VTC 的 DE 信号。4. EDID、VDMA 与缓冲管理软件侧必须抠的细节4.1 EDID 配不对HDMI 源端根本不肯说话HDMI 输入链路的第一个拦路虎不是像素时钟而是 EDID 和 HPD。源端设备比如笔记本电脑通过 DDC 通道读取显示端的 EDID读到合法内容后才会开始输出 TMDS 信号。如果你用 HDMI 输入口接电脑FPGA 这边必须模拟一个显示器事务否则源端会认为没有设备连接直接不输出画面表现为黑屏。解决思路分两步。第一步把 HPD 引脚拉高FPGA 侧通常把 HPD 信号接入一个 GPIO在软件里配置输出高电平。注意 HPD 不是上拉就能解决的有些板卡在 PHY 输入端自带 HPD 上拉电阻有些则必须由 FPGA 输出控制接线方式要看硬件原理图。第二步是在 DDC 总线上挂一个 EEPROM 或由 FPGA 内嵌的 RAM 实现 EDID 存储。最省事的做法是用 AXI IIC IP 挂一个 256 字节的 ROM把标准 1080p EDID 内容写入首段。EDID 内容的制作并不复杂。你不需要完整理解 EDID 1.3 的全部字段只需要注意结构128 字节基础块前 8 字节是固定头00 FF FF FF FF FF FF 00最后 1 字节是整个块的校验和保证前 128 字节累加结果为 0。很多开源仓库里都有 C 语言或 Python 构造 EDID 的脚本改一下厂商 ID 和分辨率的 VIC 编号就能用。4.2 VDMA 参数行跨距与帧缓冲设置不能按直觉填VDMA 的寄存器配置看似简单但“为什么画面斜着撕裂”这类问题十有八九出在行跨距和缓冲地址设置上。PYNQ 的allocate给出一块连续的线性内存每个像素占用 4 字节时一帧 1080p 的字节数是1920 * 1080 * 4 8294400字节。VDMA 的 S2MM 通道要把视频流按行写入这个内存就必须告诉它每行多长。Xilinx AXI VDMA 有三组关键参数。S2MM 的 HSIZE 寄存器指定一行有效数据的字节数减 1VSIZE 指定总行数减 1FRMDLY_STRIDE 则是帧延迟与跨距组合字段。行跨距指内存里一行数据实际占用的字节数。若画面没有补齐或对齐行跨距可能大于有效行字节例如做 64 字节对齐时需要按align(1920*4, 64)向上取整。但如果你只是一帧接一帧线性存放缓冲最简单的做法是让跨距等于有效字节数即1920 * 4 7680。双缓冲是三倍缓冲中最常用的一种。S2MM 通道有两个起始地址寄存器分别对应缓冲 0 和缓冲 1。VDMA 在传输完一帧后切换到另一个地址软件在上一次传输完成的帧上做处理同时硬件正在往另一个缓冲写新帧。这样既避免帧撕裂也让 CPU 和 DMA 并行工作。配置双缓冲时帧计数要按 2 取余来决定当前写到哪个地址。4.3 缓存一致性与 DMA 地址是 PYNQ 视频开发的隐形门槛PYNQ 的allocate函数返回的是虚拟地址但把它交给 VDMA 前必须转成物理地址。buf.device_address返回的就是所分配连续内存的物理地址。很多误用是在 numpy 数组上直接把__array_interface__[data][0]拿给 DMA得到的是虚拟地址DMA 会读到非法内存。另一个坑是缓存一致性。DDR 是多个 master 共享的CPU 写入数据时可能只写了 Cache没立刻刷到 DDRDMA 写内存后CPU 读取时也可能读到 Cache 里的旧值。PYNQ 的缓冲区提供了flush()和invalidate()来管理这个问题。凡是 CPU 写、DMA 读的场景写入后要flush()凡是 DMA 写、CPU 读的场景读取前要invalidate()。在双缓冲工作流里这两步要精确穿插。拿 S2MM 双缓冲采集为例处理帧 N 之前要对帧 N 的缓冲做invalidate()处理完后下一轮如果复用该缓冲则不需要手动清理因为硬件会整帧覆盖。整个过程的核心是CPU 只碰当前被 DMA 释放的缓冲不要碰正在被硬件写入的缓冲。5. 避坑排查花屏、黑屏、无输出的常见问题与对策5.1 现象与根因速查表我整理了几个出现频率最高的问题按“现象→原因→对策”列成表格方便你在调试时直接对照。现象根因解决方式显示器完全无信号HDMI 源端也不输出HPD 未拉高或 EDID 非法用 GPIO 拉高 HPD用 IIC 读回 EDID 校验和有输出但画面强烈花屏/斜纹像素时钟与 VTC 行场参数不匹配核对 1080p60 时序参数与 MMCM 配置图像颜色整体偏绿或发紫色彩空间或像素格式不匹配检查 RX/TX 两侧是 RGB 还是 YCbCr字节序 BGR/RGB 是否一致画面左右撕裂、上下错位VDMA 行跨距配置错误确认 HSIZE 与 FRMDLY_STRIDE 的字节数黑屏断电重启后正常热插拔后失效HPD 电平变化没有被软件处理中断或轮询中重新读取 PHY 状态并恢复 EDID 响应运行一段时间后花屏时钟漂移或复位顺序不当检查 MMCM/PLL unlocked 信号并设计异步复位释放逻辑5.2 上板先测回环用状态 LED 定位问题域HDMI 链路的问题往往不是单点故障而是多个环节叠加。我的习惯是先做回环自测把链路切成 TX 与 RX 两段分别隔离。做法很有分支在 Block Design 里把 TX 输出的时序与数据同时接到 RX 侧 PHY 的输入端模拟外部线缆但不真正使用 HDMI 物理连接器。这种内部回环不需要焊接跳线只需要在 hwh 或 Tcl 里加一段虚拟连接。回环测试的参照图像用纯彩条。因为彩条颜色块边界非常锐利任何 RGB 字节序或宽度不对你马上就能从采集帧里看出来。如果内部回环通过再接真实 HDMI 线缆测试这时如果再出问题就可以把锅明确指向外部源设备、EDID 或物理层信号质量。我还会在 PL 里加一个简单的状态寄存器把 PHY locked、VTC DE 信号、VDMA 帧计数、HPD 状态汇总成几个 bit。加载 Overlay 后通过 Python 读一次状态寄存器基本够定位出是哪一层还在摸黑。这个状态寄存器在交付源码包时也会留好接口方便后续二次开发。5.3 三个典型踩坑HPD 误接、色彩转换错误、VDMA 寄存器写入顺序第一个坑是 HPD 引脚误接。现象是 HDMI 源端识别不到显示器明明 EDID 写在 ROM 里源端就是不输出。原因是 HPD 信号在 HDMI 规范里需要由显示端主动拉高而有些工程把 HPD 误接成下拉或浮空。解决方法是查硬件原理图确认 HPD 连接方向再通过 GPIO 控制该引脚输出高电平。第二个坑是色彩转换错误。现象是画面出来了但颜色明显不对红色变蓝色肤色发绿。原因是 PHY 输出给桥接 IP 的像素格式可能是 4:2:2而 VDMA 端按 4:4:4 存成了四字节 RGBA。这两个格式间的转换必须由视频处理子系统内的转换模块完成而不能靠软件硬改。解决方式很简单确认链路各段使用同一种 YCbCr/RGB 子采样格式必要时放弃“通用格式”直接用 4:4:4 RGB 走通全链路。第三个坑是 VDMA 寄存器写入顺序。现象是配置完 VDMA 并启动后第一帧正常后续所有帧都停在同一个画面。原因是在 DMA 尚未停止时修改了缓冲地址或帧参数。VDMA 不是任何时刻写入都有效的寄存器——如果你在传输过程中把 S2MM_START_ADDR 改了行为未定义。正确顺序是先把控制寄存器的复位位置位等待 VDMA 进入复位态再写地址和长度最后清除复位位并启动。这个顺序在 Xilinx 文档里叫“寄存器更新顺序”我通常封装成一个vdma_configure()函数每次修改参数都走一遍宁可慢一百个周期也不省这一步。6. 进阶用帧计数器和图像指纹做无显示器通路自检HDMI 调试时不可能每次都在手边备一台显示器。最有效的无显示器验证手段是组合使用帧计数器和图像指纹。帧计数器的原理很简单S2MM 通道在写完一帧时会将帧计数寄存器递增软件持续读取该寄存器只要数值随时间增长就证明 RX 链路的数据在持续搬运。即使画面全黑数据流通也说明 PHY 锁定正常、桥接 IP 工作正常。但我更推荐做一个图像指纹函数。它比帧计数更进一步能够在没有显示器的情况下判断图像内容是否正确。原理是抓取采集帧的几个 ROI计算 ROI 的平均颜色再和源端测试图的理论颜色做差值比对。比如我往 TX 端推一帧左红右蓝的测试图RX 采集到后检测左上角 ROI 的平均值是否接近纯红、右上角 ROI 是否接近纯蓝。import numpy as np def check_frame_pattern(frame): h, w frame.shape[:2] left_roi frame[h//2-50:h//250, w//4-50:w//450, :] right_roi frame[h//2-50:h//250, 3*w//4-50:3*w//450, :] left_mean left_roi.mean(axis(0, 1)) right_mean right_roi.mean(axis(0, 1)) # 左红右蓝按 BGRA 顺序表示 left_ok left_mean[2] 200 and left_mean[0] 50 right_ok right_mean[0] 200 and right_mean[2] 50 return left_ok and right_ok这个函数返回 True 说明链路没问题False 则说明要么色彩顺序错了要么图像没有正确搬运。我也会把 ROI 位置和判定阈值做成参数这样不同分辨率、不同测试图案都能复用。从那以后我每次拿到新的 HDMI 板卡都会先在 PL 里放一个彩条生成器、一个帧计数器和一个图像指纹函数跑一遍回环再连真实信号源。这套三板斧看起来土但确实帮我快速砍掉了至少一半的联调时间。HDMI 链路涉及太多看不见的时序问题能自动化验证的步骤都自动化掉剩下的变量就是真正的硬件问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑