Zynq Ov5640图像采集与以太网UDP传输工程全解析

Zynq Ov5640图像采集与以太网UDP传输工程全解析 简介本资源是面向嵌入式FPGA开发者的Zynq平台图像采集与网络传输实战工程聚焦OV5640摄像头在XC7Z020CLG484-1芯片上的1280×64060Hz实时采集与以太网UDP高速传输适用于智能视觉终端、远程监控及边缘图像处理等场景适合具备Verilog基础与ARM裸机/轻量系统开发经验的中级以上工程师学习实践。压缩包共1507个文件含183个Verilog逻辑模块负责MIPI/DCMI接口时序、图像缓存与DMA控制、120个C源文件运行于PS端ARM实现UDP socket封装、帧打包与网络调度及70个XDC约束文件辅以Vivado 2018.3工程配置、HDF硬件描述与bit流文件整体大小为113MB。已有351人下载学习资源结构完整包含多级目录组织的软硬协同工程框架、__synthesis_is_complete__标记的可验证综合结果、以及支持分辨率与帧率自定义的参数化设计模块便于读者快速复现、调试并拓展至其他图像传输协议。 我最早接触这个工程的时候手里只有一块Zynq开发板、一个Ov5640摄像头模组和一根网线。当时最迫切的需求是把摄像头采集到的画面通过以太网实时发到PC上用来做后续图像算法调试。当你搜到Zynq Ov5640 图像采集与以太网UDP传输工程这类项目时说明你已经准备好进入SoC图像处理这条链路了。这篇文章我会完整拆解这个工程背后的架构、代码逻辑、调试方法和坑点帮你搞清楚每一级数据是怎么从sensor跑到网口的而不是只复制一份代码然后发现跑不通。1. 从标题到工程先把图像采集这条链路画清楚1.1 数据在Zynq里到底怎么走很多人拿到这个标题第一反应是用Zynq把Ov5640的数据读出来然后调用以太网发出去。但Zynq不是单片机它分PSProcessing SystemARM硬核部分和PLProgrammable LogicFPGA部分所以设计的第一步是把数据路径想清楚。最典型的数据流是这样的Ov5640 摄像头 | | DVP并行接口PCLK / VSYNC / HREF / D[7:0] | v PL 端 Sensor 采集 IP把并行时序转成 AXI4-Stream | | AXI4-Stream v VDMAVideo Direct Memory AccessS2MM 通道 | v DDR3 内存中的帧缓存区Frame Buffer | | 中断通知 PS CPU v PS 端 ARM Cortex-A9 读取帧数据通过 LWIP 协议栈封装 UDP | v GEM 以太网控制器 - 外部 PHY - 网口 - PC这中间有三个关键角色PL负责采集和搬运DDR负责暂存PS负责协议栈和以太网发送。如果你能画出这条链路后面所有代码都是围绕它展开的。有些工程设计会把VDMA读通道MM2S再拉出来直接从DDR读数据回到PL再通过自定义MAC发送但这属于进阶玩法。大多数工程用的是标准方案PS的ARM核跑LWIPCPU从DDR里读图像帧然后调用UDP发送。这种方案的好处是协议栈成熟、调试方便坏处是CPU占用高后面会细说。1.2 为什么偏偏是UDP而不是TCP这是不少初学者会问的问题。图像数据量很大一帧720p的RGB565画面大约1.8MB如果按30fps跑每秒要传输55MB左右也就是440Mbps。TCP为了保证可靠传输需要三次握手、确认应答、丢包重传这些机制在大量数据连续传输时会产生明显延迟和带宽损耗。UDP就没这些包袱。它只负责把数据包发出去不关心收没收到因此延迟低、吞吐高。对于实时图像显示来说偶尔丢几帧画面完全能接受视觉上最多闪一下而如果用TCP一丢包就要重传后面的数据排队等延迟会越积越大实时性反而更差。所以图像采集UDP是一对黄金搭档。工程里如果看到TCP多半是用于控制指令或参数下发而不是传输视频流。1.3 这个工程解决的核心问题这个工程本质上解决了三件事第一如何用PL把并行摄像头数据接入Zynq的AXI体系第二如何用VDMA把密集的视频流搬进DDR避免CPU逐像素拷贝第三如何用LWIP把图像帧切成UDP包发出去。这三件事对应的恰好是采集、缓存、传输三个模块。适合学这个工程的人有两种一种是从FPGA转过来想做SoC图像处理的另一种是做嵌入式Linux但想接触裸机PL端开发的人。即使你之前没写过AXI协议只要按这条链路往下拆工程结构并不复杂。2. 硬件搭建与引脚分配Ov5640和PHY怎么接到Zynq才不踩坑2.1 Ov5640的DVP接口引脚Ov5640有DVP和MIPI两种输出接口。这个工程标题里写的Ov5640绝大多数是指DVP并行接口版本也就是我们经常买的那个带24Pin排针的摄像头模组。它输出的信号包括信号名方向说明SCL / SDA双向SCCB类似I2C控制总线用于配置寄存器PCLK输出像素时钟数据在PCLK边沿采样VSYNC输出帧同步信号一帧开始/结束的标志HREF输出行同步信号一行有效数据标志D[7:0]输出8位像素数据总线RESET输入复位低有效PWDN输入掉电控制高有效引脚分配时要特别注意电平匹配。Ov5640的I/O电压通常可以配置为1.8V或2.8VZynq的MIO和PL侧HP Bank通常支持1.8V/2.5V/3.3V。我自己习惯把OV5640的VIO接1.8V然后把Zynq的PL Bank电压设在1.8V这样信号线直连最保险。如果板卡上摄像头VIO是2.8V就需要确认Zynq Bank是否支持2.8V或者使用电平转换。2.2 Zynq PS端以太网PHY的连接方式Zynq-7000系列的PS端自带GEMGigabit Ethernet MAC控制器需要通过MIO引脚连到外部PHY。这个PHY芯片可以是RTL8211、KSZ9031、Marvell 88E1512等常见型号。连线包括RGMII数据/控制线TXD[3:0]、RXD[3:0]、TX_CTL、RX_CTL、GTX_CLK、RX_CLKMDIO管理接口MDC、MDIO用于配置和读取PHY状态PHY复位引脚通常用PS的MIO或者GPIO控制复位时序不对会导致网口ping不通市面上大多数Zynq开发板会把GEM0接到某个PHY上具体MIO编号要和Vivado工程里的引脚约束保持一致。如果你拿到一个陌生板卡第一步应该是查原理图确认PHY的MDIO地址是多少、用的是GEM0还是GEM1、PHY复位是高有效还是低有效。这些信息错一个后面网络调试就是噩梦。2.3 我踩过的引脚约束坑第一次搭这个工程时我在Vivado里配好了PS端MIO却在XDC文件里随便写了PL侧的OV5640引脚。结果编译能通过上板后摄像头死活没有数据。后来查了半天才发现OV5640的PCLK引脚和别的信号接反了——那位开发板的丝印上根本没有标PCLK只标了数字我看错了一个。还有一次是PHY的RGMII时钟问题。RGMII需要由MAC提供125MHz的GTX_CLK有的PHY也支持从外部晶振提供参考时钟。如果Vivado里GEM配置选择的时钟源和板卡实际不一致PHY的链路状态可能一直起不来。排查这个问题的快捷方法是看PS端的驱动是否读到PHY ID如果读出来全是0xFF大概率是MDIO地址或PHY供电问题。3. 代码层的关键路径Sensor配置、VDMA搬运与LWIP发送3.1 Ov5640寄存器配置不是简单调I2COv5640的控制接口叫SCCB时序上和I2C非常接近Zynq侧可以直接用PS的I2C控制器或者PL逻辑模拟。工程里一般放一大段初始化寄存器表从0x3000到0x5Axx。很多人直接复制这段表结果画面比例不对、颜色不对、甚至黑屏。关键点是寄存器写入顺序和延时是有讲究的。Ov5640上电后要先复位然后等待至少10ms再开始写寄存器其中不少寄存器要等PLL稳定才能继续写。分辨率、输出格式、时钟频率都集中在几个核心寄存器里比如0x3808/0x3809定义了输出宽度0x380A/0x380B定义了输出高度0x380C/0x380D代表水平总尺寸0x380E/0x380F代表垂直总尺寸。另外一个容易忽略的是输出格式。Ov5640可以输出YUV422、RGB565、JPEG等格式。工程里如果做UDP裸传最常用的是RGB565或YUV422如果做MJPEG压缩则设置成JPEG输出数据量会小很多。我建议在初始化的最后读一下0x300A和0x300B芯片ID确认I2C通信正常再继续后面的配置。3.2 VDMA的S2MM通道配置要点VDMA是Xilinx提供的一个IP核作用是把AXI-Stream流式数据写成DDR内存。它有三种模式常见工程里用的是S2MMStream to Memory Map也就是把摄像头的视频流存储到内存。配置VDMA时最重要的几个寄存器组# 以官方SDK驱动为例伪代码示意 Xil_Out32(VDMA_BASE 0x30, 0x00000003); // S2MM_VDMACR, 更新寄存器且循环模式 Xil_Out32(VDMA_BASE 0x34, (u32)frame_addr[0]); // 第0帧地址 Xil_Out32(VDMA_BASE 0x38, (u32)frame_addr[1]); // 第1帧地址 Xil_Out32(VDMA_BASE 0x40, hsize * bpp); // 行跨距 Xil_Out32(VDMA_BASE 0x44, vsize); // 垂直方向像素数 Xil_Out32(VDMA_BASE 0x48, hsize * bpp); // 水平方向字节数使用双帧缓冲Double Buffer会更流畅。VDMA在写第0帧时CPU可以发第1帧写第1帧时CPU发第0帧。如果只用单缓冲DMA写入和CPU读取会互相踩踏画面会出现撕裂或条纹。最重要的一点是帧地址对齐。DDR地址必须32字节对齐否则VDMA会报地址对齐错误。我遇到过一次帧地址写成了未对齐地址VDMA中断不触发整条链路像死机一样排查了很久才意识到是基地址偏移问题。3.3 LWIP裸机发送UDP的代码结构PS端软件工程一般基于Xilinx SDK创建使用lwIP库并且不需要跑操作系统。初始化流程大致是初始化GEM PHY读取PHY ID确认链路速率。调用lwip_init()初始化协议栈。分配静态IP地址比如192.168.1.10。创建UDP PCB绑定本地端口。在VDMA中断回调里获取一帧图像地址然后打包发送。发送代码的核心是udp_sendto。图像帧通常远大于单个UDP包所以不能一次性拷进去必须按MTU切分。下面是一个简化的发送片段void udp_send_image(u32 frame_addr, u32 frame_len) { u32 offset 0; struct pbuf *p; while (offset frame_len) { u32 chunk frame_len - offset; if (chunk 1472) { chunk 1472; // 1500 MTU - 20 IP头 - 8 UDP头 } p pbuf_alloc(PBUF_TRANSPORT, chunk, PBUF_RAM); if (p ! NULL) { memcpy(p-payload, (void *)(frame_addr offset), chunk); udp_sendto(udp_pcb, p, remote_ip, REMOTE_PORT); pbuf_free(p); } offset chunk; } }这个写法正确但性能不高。每次发送都要memcpy和pbuf_alloc/free在720p30fps数据量下会占用大量CPU时间。后续优化可以改用PBUF_REF或者直接用零拷贝接口但对入门工程来说先跑通最重要。4. 网口带宽算清楚再做传输从MTU到实际吞吐4.1 一帧图像到底有多大做图像传输前必须算清楚带宽否则你会在为什么帧率只有5fps这个问题上折腾半天。不同分辨率和像素格式的单帧大小如下分辨率像素格式位深单帧大小30fps数据量100M以太网能否承受320x240RGB56516bit150KB4.5MB/s 36Mbps勉强可行640x480RGB56516bit600KB18MB/s 144Mbps不行1280x720RGB56516bit1.8MB55MB/s 440Mbps不行1280x720MJPEG-通常100-300KB3-9MB/s可行这里要强调100M以太网理论速率100Mbps实际有效吞吐大约90-95Mbps折合约11MB/s。如果你想传RGB565的720p画面单靠100M口基本不可能。所以很多工程要么用千兆网口要么把分辨率降到QVGA要么在PL里做MJPEG压缩后再传。这是一个物理带宽约束不是代码能绕过去的。4.2 MTU和UDP载荷的取舍标准以太网帧MTU是1500字节减去IP头20字节再减去UDP头8字节UDP最大载荷就是1472字节。如果你一个包发送超过1472字节IP层会做分片接收端要重组。分片在局域网内一般也能工作但会带来两个问题一是多了头部开销二是如果中间某个分片丢了整个UDP包直接无效。所以工程里最好手动按照1472字节切片。发送端每次只发1472字节接收端根据包序号重新组帧。上位机端可以使用Socket接收然后在接收缓冲区里按序号拼接。这里还有一个小优化如果PHY支持Jumbo Frame比如9000字节MTU则可以把每个UDP包做大减少包数量降低CPU开销。但是用Jumbo Frame要求PC网卡也开启同样的配置否则抓包会看到大量丢包或超长帧异常。4.3 Wireshark抓包能看到什么调试UDP传输时Wireshark是最好的工具。抓包时注意过滤规则udp.port 5000可以看到发送端每个包的间隔、包长、序列连续性。如果发现包序号跳变说明发送端丢帧或者接收端丢包。如果包长超过1472说明发送代码没有正确分片或驱动层出现了GROGeneric Receive Offload合并。我曾经碰到一种情况PC端用Wireshark抓包能看到完整帧但自己的上位机程序却只能收到开头几包。后来发现是PC防火墙拦截了高频UDP流量或者上位机接收缓冲区太小。这已经是协议栈之外的问题了但调试时要想到。4.4 实测吞吐数据参考我用ZC702类似平台和RTL8211千兆PHY做过一组测试发送方式分辨率帧率平均带宽实测结果LWIP单包memcpy640x480 RGB56512fps约7.2MB/s画面流畅CPU接近70%LWIP单包memcpy1280x720 RGB5656fps约11MB/s明显掉帧优化后零拷贝640x480 RGB56525fps约15MB/sCPU约40%MJPEG压缩1280x72030fps5~8MB/s流畅从表里能看出光是能发出去和发得流畅之间还有很大的差距。大多数入门工程源码跑在Vivado默认配置下FPS上不去非常正常。5. 实测中的典型故障花屏、丢帧、ping不通的完整排查链5.1 花屏先查VDMA还是查摄像头花屏是图像采集工程里最热闹的问题原因可能是摄像头初始化失败、行场时序错位、DDR带宽不够、VDMA突发长度没配好。我的排查顺序是先区分有无图像和图像错不乱。如果屏幕或PC端收到的是雪花噪点基本是Ov5640输出数据本身就没有稳定同步优先查VSYNC/HREF时序和寄存器分辨率配置。如果能看到画面轮廓但颜色条纹乱跳大概率是RGB565的Byte Swap问题。OV5640输出顺序和VDMA期望的字节序不一致在采集IP里加一个字节交换模块即可。如果画面错位、偏移比如显示的是上下一半则要检查VDMA的行跨距和水平尺寸是否一致。我遇到过最隐蔽的一例摄像头输出640x480但PCIe上位机按1280x720解析结果画面拉丝。那是PC端问题不是Zynq问题。所以出现花屏时先固定一端用已知格式去验证另一端。5.2 帧率上不去的真实原因帧率低通常不是以太网带宽不够而是CPU和内存拷贝拖了后腿。LWIP裸机是个单线程循环它必须在接收VDMA中断后把DDR里的数据挨个memcpy到pbuf里。这个操作耗时非常长。另一个隐性瓶颈是DDR带宽。当VDMA写入和CPU读取同时进行时DDR仲裁会带来额外延迟。如果PL端还有MJPEG压缩模块那么这个瓶颈会更明显。因此要提升帧率第一件事就是减少拷贝次数直接让LWIP的pbuf指向VDMA缓存地址虽然要小心DMA和网卡同时访问同一块帧缓存。5.3 网口ping不通的定位步骤以太网连不通的问题我建议按下面的顺序来排查项现象解决办法PHY复位PHY寄存器读不出ID检查复位时序复位后延时至少10ms再访问MDIO地址地址错误导致读不到PHY查原理图PHY的AD[0]引脚上下拉网线/电脑IPping不通且ARP超时换网线设置同一网段关闭Win10防火墙GEM时钟RGMII RX_CLK无时钟检查MIO配置和PHY参考时钟频率软件初始化顺序lwIP链接不上确认在PHY link up后再初始化协议栈如果你用串口打印能看到Link up但PC还是ping不通那多半是ARP没有正确响应。可以在PC端arp -d清空缓存然后重新ping。5.4 用串口日志构建定位锚点调试这个工程时串口打印一定要到位。我在代码里至少保留三处日志OV5640芯片ID读取值判断I2C是否正常VDMA中断计数每收到一帧打印一次每次UDP发送完成后的包计数。有了这三个计数就能快速把问题定位到采集段、存储段还是网络段。否则一旦画面没有你都不知道是哪一段哑火了。6. 固件固化与后续升级方向QSPI、OCM与高吞吐改造6.1 从JTAG调试到BOOT.bin固化调试阶段可以直接从SDK下载程序但工程要交付或做上电自启动时需要生成BOOT.bin并烧写到QSPI Flash或SD卡里。Vivado里生成BOOT.bin的流程是先导出硬件创建FSBL然后制作启动镜像包含FSBL、bitstream、应用软件elf。SDK提供create_boot_image命令也可以用Xilinx的工具链制作。QSPI烧写完成后注意板卡的启动模式跳线。Zynq的启动模式由MIO[4:0]决定QSPI启动要保证这些引脚电平正确。否则板子上电后还是从JTAG或SD卡启动你会以为固化失败了。6.2 不带DDR的Zynq怎么用OCM加载有些Zynq封装里不接DDR这种情况下图像数据没法放到DDR3里但可以利用Zynq自带的OCMOn-Chip Memory。OCM大小通常为256KB只够存几帧QVGA图像。我做过一个极简工程用OCM存储VDMA写入的320x240灰度图像帧率大概10fps以太网裸传。这个方式局限性很大但好处是系统更简单不需要DDR的配置和训练。如果你用的是不带DDR的Zynq板卡而又想快速跑通UDP传输OCM是一条可行路线。它的主要难度在于VDMA地址不能高于OCM的地址范围并且OCM带宽和DDR比有明显差距。不要尝试用OCM传输高清图像一定会失败。6.3 高吞吐改造方向如果要把工程从能跑升级到好用我建议优先做三个改造第一让LWIP回调用PBUF_REF指向VDMA缓存省去memcpy。这要求你管理好帧缓存的生命周期避免DMA还在写时网卡就已经发送了。第二在PL端把RGB565先转成MJPEG或者把图像缩放到更低分辨率再送给VDMA。压缩后数据量大幅下降100M口也完全够用。第三如果传输带宽仍然不够考虑把GEM从100M改成千兆模式同时启用Jumbo Frame。这时候瓶颈通常从MAC转移到CPU需要优化驱动或用DMA描述符双缓冲。6.4 扩展思路从单向图传到双向控制这个工程的扩展空间其实很大。单向图像传输可以在PC端做显示和保存但真正做双目视觉或调参时PC还需要向Zynq发送控制命令比如修改曝光、切换分辨率、设定帧率。这时可以用同一个UDP端口承载双向数据或者开两个端口一个图传、一个控制。我在自己的项目里还加了一个简单的带宽统计功能每隔一秒统计发送字节数并预估当前帧率。这个功能对判断系统是否饱和非常有用。如果你也想加只需要在发送循环里累计字节计数用定时器每隔一秒打印一次即可。最后再分享一个经验做图像采集和传输不要一上来就追求高帧率先把一帧图像完整地从摄像头走到网口哪怕只有1fps也算跑通了关键路径。之后再逐步提高分辨率或帧率每次只改一个变量你才能清楚地知道瓶颈到底在哪。这个思路比换一堆代码和参数要有效得多。本文还有配套的精品资源点击获取