资讯动态

PYNQ HDMI视频通路全解析:从VDMA到帧缓冲的实践指南

发布时间:2026/10/14 20:57:19 来源:尧图企业网站定制
简介面向PYNQ及Zynq嵌入式开发者的HDMI输入输出参考工程以实测跑通的完整链路演示如何配合Vivado与Python配置视频接口覆盖接口层、FPGA逻辑层与软件控制层适合正在设计视频采集、显示回环或图像处理应用的FPGA工程师及进阶学习者。压缩包共380个文件、3.14MB主体为v/vhd/vhdl硬件描述代码xdc约束文件定义引脚与时钟xci、bit与dcp保存Vivado工程配置及综合结果runme.bat、sh、tcl脚本承担自动化编译与运行配合Video.bd块设计与txt说明形成从逻辑到上板的完整闭环。目前已有2638人学习下载。借助这套已跑通的工程使用者既可对照接收发送两侧的时钟同步、色度空间转换与AXI4-Stream数据通路实现快速排查自身配置问题也能直接复用其中Verilog/VHDL代码与Python控制思路将视频接口逻辑迁移到同类Zynq项目中大幅缩短二次开发周期。1. PYNQ HDMI从一条黑屏的线到一整套视频通路一根HDMI线接上PYNQ板卡屏幕却一片漆黑——这是很多人第一次碰PYNQ HDMI时遇到的场景。板子上明明标注了HDMI in和HDMI out两个接口看起来就像一台微型电视可真要让输入变成Python里的一帧图像、再让处理结果输出到显示器中间隔着的是一整套视频通路而不是一根线。这个标题要讲的就是怎么把这条路打通HDMI in如何变成VDMA里的帧缓冲处理完的图像又如何从内存推到HDMI out。这套方案并不绑定某种特定板卡PYNQ能跑通用Zynq也行区别只在于验证速度。适合手上有Zynq或PYNQ开发板、想搞视频采集与显示的人也适合那些正在为视频通路选型拿不准的嵌入式视觉从业者。2. 从物理接口到DDR地址PYNQ HDMI的视频通路与选型2.1 输入和输出两条方向相反的TMDS链路HDMI的物理层走的是TMDS差分对但输入和输出完全不是一回事。输入端HDMI RX要把高速串行数据里的像素时钟恢复出来再解出RGB24、行场同步信号输出端HDMI TX要把像素数据和时钟编码成差分串行信号送出去。方向相反链路结构也不同所以别指望拿一个IP核复用它。输入端有一套时钟恢复逻辑输出端则要自己产生像素时钟并编码两边都要处理热插拔检测HPD和5V电源关系。很多人第一次看到PYNQ板上有两个HDMI口误以为用跳线把in接到out就能在屏幕上看到输入源实际拿到的只是一片黑或满屏雪花原因就在物理层这第一步就没过。我把这套路线拆开看大概是这样一条数据流HDMI物理接口、接收/发送IP核、AXI4-Stream流、VDMA、DDR内存最后才到CPU。PYNQ上面这套链路通常是预置的但你不知道它为什么能工作出了问题就抓瞎所以我还是建议从链路选型开始理解它。注意HDMI输入的时钟恢复不是说给个参考时钟就行IP核内部会从TMDS数据里提取时钟。如果输入源不稳定或者线材太差恢复出来的像素时钟会有抖动后面整条通路都跟着出问题。所以别把锅都甩给VDMA先看源端。2.2 AXI4-Stream与VDMA视频流怎么变成内存里的帧视频解出来后标准的做法是包成AXI4-Stream流每个时钟周期传输一个像素伴随行同步和帧同步信号。AXI4-Stream的好处是协议简单没有地址概念适合做流式处理。但CPU没法直接操作流所以必须把它搬进DDR内存这就轮到VDMA出场。VDMA有大名鼎鼎的三件事S2MM通道把AXI4-Stream写入内存MM2S通道把内存里的帧读出来变成AXI4-Stream还支持行步长、帧缓冲循环和中断通知。这里有个选型上的关键点视频像素数据在DDR里不是简单的连续堆叠。一行1920像素乘以3字节共5760字节但DDR访问和对齐要求可能让每行之间有填充。VDMA有一组寄存器专门设置行步长FRMDLY_STRIDE目的就是让每一行的起始地址可以自由指定处理这种行对齐问题。如果你自己写逻辑搬数据行步长、跨时钟域、帧同步三件事会让你的RTL代码膨胀到不可维护。VDMA把这些都做成寄存器配置这也是它在这个方案里不可替代的原因。从PYNQ视角看VDMA的寄存器会映射到PS端的某一段地址Python里用MMIO就能读写。这套方案的本质就是让FPGA负责像素级的搬运和编解码让CPU只管处理帧各干各的。2.3 为什么选VDMA而不是自己写DMA有人在论坛问过ZXU的FPGA里能不能自己写个状态机去搬视频帧答案是可以但别这么干。自己写DMA首先要把AXI4-Stream的时序吃透是每像素一个beat还是每行一个beattlast和tuser怎么拉光调试这个就够喝一壶的。其次视频帧是周期性的你要处理帧边界、行边界、缓冲切换的瞬间任何时序没算对图像就撕裂或者花屏。VDMA是经过实际验证的IP它的循环模式可以自动在多个帧缓冲之间轮转你不必在每一帧结束时介入。另外一点是VDMA的中断机制很成熟。SYNC帧完成、SOF、DMA错误都能触发中断可以在PS里监听。对PYNQ来说可以直接轮询寄存器读帧计数不必上中断这在调试阶段反而简单。选VDMA不是因为它名字好听而是因为它的寄存器语义和驱动支持已经很稳定你只需要配几个地址和尺寸就能把视频帧搬进内存省出的时间可以用来处理真正的图像算法。2.4 PYNQ和通用Zynq的落地差异标题里说“zynq也可使用”是实话但实际的落地路径差别不小。PYNQ这边官方镜像已经预装好Python环境、pynq库和一套可用的overlay打开Jupyter就能操作。通用Zynq那边你得自己在Vivado里搭Block Design做FSBL启动写设备树甚至要手写一个字符设备驱动暴露帧缓冲给用户态。本质上视频通路是一样的但验证速度差一个量级。我的经验是如果手头有PYNQ先用它的overlay把视频通路跑通确认HDMI in能出帧、out能出画面再开始按自己的需求裁剪。PYNQ里overlay其实就是一个比特流加一段Python封装它的IP互联逻辑完全可以导出来在通用Zynq上重新生成。唯一要注意的是PYNQ镜像里的overlay会默认初始化一部分外设换到纯Zynq时这些初始化要自己在FSBL里补上否则就会出现“同样的比特流跑不起来”的尴尬。3. HDMI in 出图接收通路配置与第一帧的诞生3.1 接收IP核的最小参数表HDMI输入侧的IP核不同版本叫法不同但核心寄存器思路是相近的。我一般会先把数据宽度、像素格式、同步极性这三组参数定下来这三样错了画面基本就是花屏和偏色。下面这份参数表是我在PYNQ和Zynq上常用的起点按1080p60输入来设的。参数项推荐值影响什么像素数据宽度24bitRGB888每像素3字节最通用的格式输出格式RGB888如果输出YUV422内存排布完全不同帧缓冲数量2少于2会造成撕裂行同步极性按输入源配置极性反了画面会偏移或抖动场同步极性按输入源配置极性反了会帧同步错乱像素时钟去抖开启处理信号质量不佳时的抖动先澄清一个容易误解的点HDMI RX解出来的数据不是直接接到VDMA就完事的中间一般要过一个“Video In to AXI4-Stream”的桥接把行场同步信号转成AXI4-Stream的tuser和tlast。如果你用的是某个现成的HDMI RX IP它会封装好这些细节但如果你是自己在Vivado里搭一定记得在RX IP后端加这个转换否则VDMA收到的流没有帧边界帧计数永远不动。3.2 用PYNQ配置VDMA并拿到第一帧拿到第一帧的关键动作就是把VDMA的S2MM通道配好然后轮询帧计数器。下面是简化版流程适用于PYNQ镜像里已经加载好overlay、且overlay里包含AXI VDMA的情况。import time import numpy as np from pynq import Overlay, MMIO, allocate # 载入overlayip_dict里能拿到VDMA的物理地址 ovl Overlay(video.bit) vdma_base ovl.ip_dict[axi_vdma_0][phys_addr] vdma MMIO(vdma_base, length0x1000) W, H 1920, 1080 stride W * 3 # RGB888每行字节数 # 分配物理连续内存VDMA只能搬物理地址 buf allocate(shape(H, W, 3), dtypenp.uint8, cacheable0) phy_addr buf.physical_address # S2MM寄存器偏移 S2MM_CR 0x30 S2MM_SRR 0x34 S2MM_ADDR 0x3C S2MM_STRD 0x44 S2MM_HSIZE 0x48 S2MM_VSIZE 0x4C S2MM_FRMC 0x5C # 复位S2MM通道0x0A是复位命令 vdma.write(S2MM_SRR, 0x0A) time.sleep(0.01) # 控制寄存器写0x1003循环模式 帧同步模式 启动DMA vdma.write(S2MM_CR, 0x1003) vdma.write(S2MM_ADDR, phy_addr) vdma.write(S2MM_STRD, stride) # 行步长 vdma.write(S2MM_HSIZE, stride) # 每行字节数 vdma.write(S2MM_VSIZE, H) # 行数 # 等待第一帧到来 last_cnt vdma.read(S2MM_FRMC) timeout 100 while vdma.read(S2MM_FRMC) last_cnt: time.sleep(0.05) timeout - 1 if timeout 0: raise RuntimeError(HDMI in 无帧数据先查RX链路) # 内存直接view成numpy数组注意这是BGR还是RGB取决于IP配置 frame buf.view(np.uint8).reshape(H, W, 3)这里要说清楚几个参数的含义。S2MM_CR里的bit0是DMA运行控制置1启动bit2是帧同步使能团队里常叫它“genlock”用来让VDMA跟着输入帧节奏走bit3是循环模式让它自动在不同帧缓冲之间轮转。0x1003从二进制角度就是这些位都置上再加上bit12的值具体bit位不用死记手册里有表格关键是理解循环模式不是可选项单缓冲模式下CPU还没读完上一帧下一帧就会覆盖画面直接撕裂。HSIZE是每行字节数VSIZE是行数这两个值如果和RX IP的输出尺寸不匹配VDMA搬出来的数据就是错位图像不是花屏就是斜切。提示allocate里的cacheable参数建议设为0。如果设成1CPU缓存和DMA的一致性会让你看到“图像更新慢”或者“偶尔出现旧帧”的怪现象这会让人误以为是视频源的问题实际上是缓存没刷。3.3 拿到帧后的三个检查颜色、对齐、帧率第一帧出来别急着欢呼先做三个检查。第一个是颜色通道HDMI链路传输的是RGB但OpenCV的默认内存排列是BGR。如果你拿到的帧里红色物体变成了蓝色不用改IP配置在Python里转换一下就行这是正常现象。第二个是图像是否对齐如果画面是斜的或者上下分块错位基本可以断定FRMDLY_STRIDE填错了。第三个是帧率用帧计数器差值除以时间算出来是不是接近60fps或30fps。如果帧率只有个位数优先怀疑DDR带宽和时钟频率而不是FPGA逻辑跑不动。还有个容易被忽略的点HDMI源要持续输出VDMA才有数据。如果你连的是相机或者某台电脑先把源端的输出分辨率固定下来不要让它自动协商。分辨率变化一次RX IP的时钟恢复就要重新锁定这个过程中VDMA可能拿到半帧或者花屏这不是你的配置问题是源端切换分辨率导致的正常现象。4. HDMI out 显示把帧缓冲搬到显示器上4.1 为什么输出必须双缓冲输入侧双缓冲是为了防止写入覆盖输出侧的双缓冲则是为了不让显示器读到“正在被修改”的帧。显示器是个无情的消费者它按照像素时钟一帧接一帧地读数据如果内存里只有一块缓冲CPU在写第100行的时候显示器正好读到第80行画面就会从第80行开始出现新旧内容劈开的撕裂线。VDMA的MM2S通道在循环模式下会自动在设定的帧缓冲之间切换指向当前正在输出的缓冲。应用端处理完一帧后不是去改当前屏幕正在读的那块而是写另一块然后让VDMA切换指针。这块切换逻辑如果用寄存器直接操作常见做法是通过帧计数器的变化判断显示器读到哪一帧了再决定要不要切。PYNQ的Video库封装了这部分但我更建议手动配一次因为封装之后出了问题很难定位。4.2 配置VDMA的MM2S通道并推一帧画面输出通道的配置方向和输入几乎对称只是寄存器前缀换成MM2S。我一般会在输入正常出帧之后再配输出避免两个通道同时调参时不知道是谁的问题。MM2S_CR 0x00 MM2S_SRR 0x04 MM2S_ADDR 0x0C MM2S_STRD 0x14 MM2S_HSIZE 0x18 MM2S_VSIZE 0x1C def hdmi_out_start(vdma, addr, stride, width_bytes, height): vdma.write(MM2S_SRR, 0x0A) # 复位MM2S通道 time.sleep(0.01) vdma.write(MM2S_CR, 0x1003) # 循环模式 启动 vdma.write(MM2S_ADDR, addr) # 帧缓冲物理地址 vdma.write(MM2S_STRD, stride) # 行步长一般等于width*3 vdma.write(MM2S_HSIZE, width_bytes) vdma.write(MM2S_VSIZE, height)配置好之后往那块物理内存里写任何数据显示器上都会实时显示。配合OpenCV一套常见的处理循环长这样import cv2 # frame_in 是S2MM通道写入的内存地址frame_out 是MM2S通道要读的内存 frame_in buf_in.view(np.uint8).reshape(H, W, 3) frame_out buf_out.view(np.uint8).reshape(H, W, 3) while True: # 轮询等待新帧 while vdma.read(S2MM_FRMC) last_cnt: time.sleep(0.001) last_cnt vdma.read(S2MM_FRMC) # 转灰度再转回RGB做一个简单处理示例 gray cv2.cvtColor(frame_in, cv2.COLOR_RGB2GRAY) frame_out[:, :, 0] gray frame_out[:, :, 1] gray frame_out[:, :, 2] gray这段代码里有个约定frame_in和frame_out是不同的物理内存缓冲。如果你把处理结果直接写回frame_in而VDMA还没读完这一帧轻则画面闪烁重则读到半个旧帧半个新帧。所以输出通道一定要配独立的缓冲别省这块内存。4.3 时序参数分辨率、像素时钟与H/V同步输出侧看起来只是把像素数据编码成TMDS但底层要保证像素时钟精确。1080p60的像素时钟是148.5MHz720p60是74.25MHz480p大约27MHz。这个时钟不是你随便一个PLL分频就能来的需要根据板载参考时钟算好倍频和分频系数。分辨率像素时钟行同步极性场同步极性典型带宽消耗1920x108060148.5MHz正极性正极性约373MB/s1280x7206074.25MHz正极性正极性约166MB/s640x4806027MHz负极性负极性约55MB/s带宽这块经常被忽略。以1080p60为例RGB888一帧是192010803字节约6.2MB每秒60帧就是373MB/s。这只是写入方向如果你同时跑S2MM和MM2SDDR带宽占用直接翻倍实际还要算上刷新和操作系统开销。Zynq-7020的内存控制器带宽并不是无限大的跑到4K分辨率或者同时开多路视频流就要做取舍了。我一般会在设计前先算好带宽预算别等画面卡了才想起来查DDR。还有一点容易翻车FPGA逻辑时钟和像素时钟是异步的。如果HDMI TX的像素时钟域和VDMA输出侧的AXI时钟域没有做同步偶尔会出现像素数据中间有毛刺表现为画面偶尔有几行颜色偏掉。这个问题不好抓因为它不是稳定复现。自查的方法是看IP核内部是否带了异步FIFO没带的话就要在Block Design里显式加一个CDC同步器。5. PYNQ HDMI避坑清单五个让画面黑屏或花屏的真问题5.1 插上HDMI源没有信号VDMA帧计数不涨现象输入源明明在输出HDMI线插在板卡上了但是轮询S2MM的帧计数器数值纹丝不动。很多人这时候开始怀疑VDMA配置反复改寄存器问题依然存在。原因大概率不是VDMA的事是HDMI RX链路没锁定。输入源的TMDS时钟没有被正确恢复或者HPD引脚没拉起来IP核根本没有进入工作状态。还有一个常见原因是输入的5V电源线被某些转接头断开了HDMI规范里源端要通过5V引脚给接收端供电检测这个引脚断了接收端认为没有设备插入整个链路保持休眠。解决先查RX IP核的状态寄存器看里面的pll_lock或者cdr_lock位有没有置1。没有锁定就换一根短一点的HDMI线或者把输入源的分辨率强制设到720p60再试。接着用万用表确认连接器的5V引脚有电压。这两步能解决八成“没信号”的问题。5.2 画面整体偏色红蓝通道互换现象图像能出来但颜色完全不对红色物体显示成蓝色肤色变成了紫色。取一帧图像存成文件用工具看像素值能明显看到R和B通道值互换了。原因这不是HDMI链路的bug是内存数据排列和后续处理的颜色空间不一致。HDMI传输标准里像素排列是RGB但OpenCV读取图像后默认当作BGR处理于是一次不转换就是通道互换。另一种可能是RX IP核的数据宽度配成30bit高位补0导致每个像素多出一个字节这时颜色看起来就像“洗过一样”发白且偏色。解决先查IP配置是不是24bit RGB888如果是那就在Python里做一次通道转换cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)。注意不要在OpenCV处理之后再转那会导致处理结果颜色又不对。这个坑的隐蔽性在于它不是完全花屏而是“看着像那么回事但颜色都错了”。5.3 图像斜切画面像被“劈了一刀”现象画面内容能辨认出来但整幅图像是斜的比如一条垂直线变成了斜线或者图像分成上下两半中间有明显错位。原因VDMA的FRMDLY_STRIDE和实际的DDR行布局不匹配。如果你直接把行步长设成width*3有时候会漏掉行尾的填充字节导致下一行从偏移位置开始读。这种情况在分辨率不是DDR对齐值的时候尤其明显。解决显式计算行步长把对齐因素考虑进去。做法是对每行字节数向上取整到64字节对齐然后把这个对齐后的值同时填进S2MM_STRD和MM2S_STRD。代码里就是加一行stride ((W * 3 63) // 64) * 64别小看这个64字节对齐很多图像斜切的玄学问题最后都落在这上面。5.4 输出画面闪烁、撕裂屏幕上出现横向断层现象显示器上画面频繁出现横线断层的位置上下移动看起来像画面被水平切了几刀。原因输出通道的帧缓冲数量配置成了1或者应用层在VDMA还在读旧帧的时候直接就改了旧帧的内容。单缓冲模式下HDMI TX每时每刻都在读同一块内存你处理完写回去的那一刻显示端正好读到同一行一次撕裂就发生了。解决把VDMA的帧缓冲数量配置到2或3处理逻辑上一帧写缓冲A下一帧写缓冲B写完后再切换VDMA的起始地址。切换动作最好跟着帧计数器的变化走不要CPU觉得自己写完了就立刻切。一条血泪经验撕裂问题在静态桌面场景下不明显一播视频或者跑实时算法就原形毕露调试时别用静态图将就。5.5 从PYNQ换到纯Zynq同样的overlay起不来现象在PYNQ上跑得好好的HDMI进出方案把同一个bitstream放到纯Zynq平台上上电后没有任何输出或者在Linux启动过程中报设备树节点错误。原因PYNQ的overlay不只是比特流它还依赖PYNQ镜像里预置的设备树覆盖和启动初始化逻辑。HDMI IP核的复位信号、时钟使能通常是PS端在启动过程中通过GPIO或AXI寄存器拉起来的。纯Zynq平台没有这层初始化FPGA加载了但相关IP还停在复位状态自然就没有输出。解决在纯Zynq上不要照搬PYNQ的overlay而是把Block Design重建出来把复位逻辑挂到PS的GPIO或者用一个AXI GPIO模块管理并在FSBL里加上对应的初始化代码。另外一个简单办法是省掉PS初始化把IP核的复位信号设计成上电自动释放这样至少能先出画面再考虑更细的控制。踩过一次这个坑之后我每次做纯Zynq方案都会先列一个“上电后哪些IP必须被初始化”的清单避免再次在看不见的地方翻车。6. 从PYNQ到纯Zynq移植裁剪与FPS验证PYNQ的价值是快速验证但产品落地往往要剥掉Python运行时直接把方案放到更精简的Zynq平台上。移植时我会按这个顺序做先在Vivado里把PYNQ那个overlay的BD源码拿出来删掉和视频无关的IP然后保留AXI VDMA、HDMI RX/TX IP、一个AXI GPIO用来做复位控制最后把VDMA的地址写进设备树启动后在应用层直接映射寄存器。性能验证有个很实用的技巧用帧计数器算FPS不用上调试器。具体做法是读一次S2MM的帧计数值睡一秒钟再读一次差值就是每秒帧数。这个方法比用示波器看时序快多了也足够判断系统有没有掉帧。要注意的是中间如果有分辨率切换帧计数器不会清零要用两次差值的绝对值做判断。# 用户态读取帧计数值 devmem 0x4300005C sleep 1 devmem 0x4300005C地址0x4300005C只是示例实际地址以你的Block Design地址映射为准。差值在60左右说明输入通路饱和差值远小于60就要查时钟和DDR带宽。我自己在这条路上栽过不少次跟头印象最深的一次是把复位逻辑挂在了一个会被Linux禁用的GPIO上导致每次系统启动到一半HDMI通路就复位了。从那以后我习惯把任何不被用户态直接操作的复位信号都独立命名并在FSBL里显式初始化一次。这个思路对于你未来调试PYNQ也好、纯Zynq也好应该都能省下不少时间。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑