Zynq + OV5640 图像采集与千兆以太网 UDP 传输工程详解 简介本资源是一套面向嵌入式FPGA开发者的Zynq平台图像采集与网络传输实战工程聚焦OV5640摄像头驱动、AXI总线数据通路设计及UDP协议栈实现适用于数字图像处理、实时视频传输等应用场景适合具备Verilog基础与ARM裸机/轻量级Linux开发经验的中高级工程师学习实践。压缩包共1507个文件含183个Verilog.v逻辑模块、120个C源码.c及153个头文件.h覆盖PL端图像采集控制、PS端UDP封包发送、Vivado 2018.3工程配置与SDK软件集成另有70个约束文件.xdc、57个综合结果.dcp及大量IP核配置.xci与报告.rpt结构完整便于理解软硬件协同设计流程。资源包大小为113MB目前已有351人下载学习。读者可直接复现1280×64060Hz图像采集与以太网UDP流式传输获取从摄像头初始化、帧缓存管理、DMA搬运到UDP socket封装的全链路代码与调试痕迹特别包含多处__synthesis_is_complete__标记体现工程已通过完整综合实现验证。 做嵌入式视觉的朋友多少都遇到过这样的场景摄像头数据采回来了存在 DDR 里但怎么把它实时送到电脑上看就成了一个不大不小的坎。这次分享的工程就是一块 Zynq 开发板配一块 OV5640 摄像头把图像采集、DDR 缓存、以太网 UDP 传输这条链路完整跑通并附上 PL 端逻辑与 PS 端应用程序的完整源代码。Zynq 上接摄像头的方案一般有两条路一条是 MIPI CSI-2 接后端 ISP另一条就是今天要讲的 DVP 并行接口方案。OV5640 在这种方案里工作在 RGB565 或 JPEG 输出模式数据交给 PL 端逻辑采样再通过 AXI 总线写入 DDR最后由 PS 端用 lwIP 协议栈拆包经以太网 UDP 发送到电脑端显示。这套工程适合两类人一是刚开始接触 Zynq、想用一块入门开发板把视频通路跑通的学生或工程师二是需要在项目里快速验证图像采集和 UDP 传输链路的开发者。文中我会把硬件接口、PL 逻辑、PS 软件、上位机调试、常见坑点全部展开讲清楚所有关键参数都给出计算过程和实测结论方便直接抄作业。1. 项目概述与整体架构1.1 核心需求解析从标题拆开看这个工程包含三个核心功能点OV5640 图像采集、Zynq 数据缓存与处理、以太网 UDP 传输。这三个点单独拎出来都不算难但串到一起就是一个非常典型的 SoC 视频数据通路设计。OV5640 负责把光信号变成并行数字信号PL 端负责把传感器时序转成 AXI Stream 或内存映射格式DDR3 负责帧缓存PS 端 Cortex-A9 处理器跑 lwIP 协议栈把 DDR 里的图像数据封装成 UDP 包通过以太网 PHY 芯片发到上位机。为什么说这是 Zynq 学习路上绕不开的标配项目因为它几乎覆盖了 Zynq 开发的全部关键知识点MIO/EMIO 引脚分配、时钟与复位设计、异步 FIFO 跨时钟域、AXI 总线协议、VDMA 搬运、中断、lwIP 协议栈移植、MDIO 配置、PHY 芯片寄存器读写。把这些搞明白后面再去做 MIPI、PCIe、万兆网等更复杂的工程思路都是相通的。1.2 方案选型与设计取舍先说摄像头接口。OV5640 本身支持 DVP 和 MIPI 两种输出DVP 是经典的 8 位或 10 位并行接口引脚多但控制简单时序也直观适合用逻辑分析仪和示波器直接调试。MIPI 接口需要差分对和额外的 D-PHY 层逻辑在 Zynq-7000 上还得借助第三方的 MIPI IP 核对于入门工程而言没必要引入这个复杂度。所以我在这套工程里选择了 DVP 接口数据宽度 8 位工作模式 RGB565后续也可以配置成 JPEG 输出压缩率更高我这里为了演示通用性还是以 RGB565 为主。再说传输层协议为什么选 UDP 而不是 TCP。图像数据是典型的流媒体对时延敏感对少量丢包不敏感。TCP 有确认重传机制一旦网络出现拥塞重传的数据包会导致接收端抖动画面上表现出来就是越来越严重的卡顿和延时累积。UDP 则无所谓这一帧丢了一个包下一帧马上补上来显示端只是可能闪一下。千兆以太网在实验室这种几十米网线、无拥塞的交换机环境下UDP 的误码率和丢包率非常低用 UDP 做图像传输是合理的工程取舍。最后说 PS 端软件方案。本项目采用裸机 lwIP 协议栈RAW API 方式而不是跑 Linux。原因很直接裸机工程简单可控启动快方便观察寄存器行为Linux 虽然驱动完善但引入了设备树、驱动编译、网络栈调度等一系列额外变量调试难度陡增。如果你之后要接 USB、SATA、文件系统等复杂外设再切换到 Linux 也不迟。1.3 系统数据流与模块划分整个系统的数据流是这样的OV5640 传感器在 SCCB兼容 I2C配置完成后输出 PCLK、VSYNC、HREF、D[7:0] 信号。PL 端逻辑采集这些信号按行、按帧组织成像素数据流写入一个异步 FIFO 做跨时钟域处理。FIFO 读端接到 VDMA 的 AXI Stream 接口由 VDMA 将数据搬运到 DDR3 的帧缓冲区内。DDR3 里我们分配了多个帧缓冲区使用帧同步机制避免读写冲突。PS 端应用程序周期性查询或通过中断感知“新帧可读”从 DDR 将帧数据拷贝到发送缓冲区按帧头 包序号 数据载荷的方式拆成若干个 UDP 包通过 lwIP 发送。各模块职责如下表模块所在端主要职责关键接口/协议OV5640 传感器外部图像采集、色彩处理、输出 RGB565DVP 8bit SCCBCMOS 采集逻辑PL生成像素使能、行列同步信号自定义时序模块异步 FIFOPL跨时钟域缓存像素数据标准 FIFO IPVDMAPL将 AXI Stream 转为 AXI4 写 DDRAXI4 / AXI StreamDDR3 控制器PS帧数据缓存AXI4Cortex-A9 lwIPPS协议栈、UDP 组包发送UDP/IPRTL8211E PHY外部物理层收发RGMII2. 硬件平台与接口设计细节2.1 平台选型与资源规划这套工程我用的开发板核心器件是 XC7Z020-2CLG484也就是常见的 Zynq-7020。芯片上 PS 端有两个 Cortex-A9 核主频 766MHz部分等级可到 866MHzPL 端有 85K 逻辑单元、220 个 DSP48E1、140 块 BRAM36Kb 规模足够容纳这套图像采集逻辑。DDR3 选用两片 512MB 组成 1GB位宽 32bit工作频率 533MHz即 DDR3-1066实测带宽足够支撑 1080p60 的 RGB565 流。以太网 PHY 用得最多的是 RTL8211E-VB千兆 RGMII 接口支持 MDIO 管理。资源规划方面PL 端逻辑其实用得非常少CMOS 采集逻辑加一个 8K 深度的异步 FIFO加上 VDMA 的 AXI 接口信号总共大概占用不到 3K LUTBRAM 占用一两块对 Zynq-7020 来说非常宽裕。真正吃资源的是数据通路上的 DDR 带宽和 PS 端 CPU 的组包开销这一点在设计时要有数。2.2 OV5640 的 DVP 接口与 SCCB 控制OV5640 是 OmniVision 的 500 万像素 CMOS 传感器最高支持 2592x1944带内置 ISP。DVP 模式下关键信号包括PCLK像素时钟分辨率配置不同时频率不同。720p 30fps 时大约 36MHzVGA 30fps 时约 24MHz。VSYNC帧同步高电平有效默认配置一帧一个脉冲。HREF行同步高电平表示一行有效数据。D[7:0]并行像素数据RGB565 模式下每两个 PCLK 输出一个像素先高字节后低字节。SCCB 总线本质上就是 I2C用两个 GPIO 分别模拟 SCL 和 SDA 也可以。OV5640 的 8 位设备写地址是 0x78读地址是 0x7A。注意 SCCB 和 I2C 的一个细节差异SCCB 读操作时写完寄存器地址后需要停止条件再发第二次起始信号即重复起始并不被 SCCB 标准完全支持所以习惯上写“stop → start”而不是“repeated start”。PS 端可以用软件模拟也可以用 Zynq 的 I2C 控制器。上电时序是个容易被忽略的细节。OV5640 要求 DVDD 2.8V、AVDD 2.8V、DOVDD 1.8V 上电后至少等待 5ms再给 MCLK外部主时钟常用 24MHz然后等至少 2ms 拉高 PWDN 引脚再等待 20ms 才能开始通过 SCCB 写寄存器。如果上来就写寄存器传感器常常没有响应表现就是 I2C 写操作 ACK 正常但寄存器内容没有生效图像数据根本不出来。2.3 以太网 PHY 与 RGMII 接口配置Zynq-7000 的 PS 端集成两个千兆以太网 MACGEM支持 RGMII 和 GMII 接口。本工程使用 GEM0连接 RTL8211E PHY。RGMII 接口下 TX 时钟 125MHz千兆或 25MHz百兆数据线 TXD[3:0]、TXD_CTL 在时钟上升沿和下降沿各发 4 位信号因此千兆模式对时钟和数据的相对延迟要求比较严格。RTL8211E 的 MDIO 地址通过引脚配置常见板卡默认是 0x01上电后可以用 MDIO 读寄存器 0x02 来验证 PHY 是否就绪读到的值和 PHY 型号对应。PS 端 GEM 驱动会通过 MDIO 自动协商速率和双工模式但前提是 PHY 的复位时序正确。很多开发板把 PHY 复位引脚接到 PS 端 MIO需要在启动时先拉低再拉高保持至少 10ms 的低电平。如果复位时序不对RGMII 接口会直接 ping 不通。2.4 硬件连接与飞线注意事项如果用的是现成开发板加摄像头扩展板连线一般都有排针定义直接对插即可。如果是自己用杜邦线把 OV5640 模块和 FPGA 引脚连起来就要注意几点杜邦线长度尽量控制在 15 厘米以内8 根数据线和 PCLK 最好等长避免时钟到了数据还没稳定PCLK 建议加一个 33 欧姆到 47 欧姆的串联电阻改善信号完整性VSYNC、HREF 这两根控制线也不能忽略线太长会引入毛刺可能导致帧同步信号误触发。另外OV5640 模块的供电需要干净最好用开发板上的 2.8V/1.8V LDO尽量避免从 FPGA 的 3.3V 引脚直接取电因为传感器内部 ISP 对电源纹波敏感电源不干净会出现颜色偏色或暗部噪点明显的情况。3. PL 端逻辑设计从像素流到 DDR 缓存3.1 CMOS 采集时序逻辑PL 端最核心的模块就是 CMOS 采集逻辑它的任务是把 OV5640 输出的 VSYNC、HREF、PCLK、D[7:0] 转换成带有效信号的像素数据流。这里有一个容易踩坑的动作就是 PCLK 的采样沿。OV5640 默认在 PCLK 上升沿输出数据因此在逻辑里应该在上升沿打拍采样。如果采样沿选反了通常看到的现象是图像“错位”或颜色乱。核心逻辑伪码大概长这样reg [7:0] d0, d1; reg [15:0] pixel_data; reg pixel_valid; always (posedge pclk) begin d0 din; d1 d0; if (href vsync) begin pixel_data {d1, d0}; // RGB565 高字节在前 pixel_valid 1b1; end else begin pixel_valid 1b0; end end这个模块还应该输出帧起始信号和帧结束信号用于通知 VDMA 开始/结束一帧的搬运也用于 PS 端统计帧计数。我习惯在 VSYNC 上升沿打一拍后生成frame_start_pulse这个脉冲同时接到 VDMA 的S2MM_FrameSyncIn当 VDMA 配置了内部帧同步时或作为普通信号交给 PS 端中断。3.2 跨时钟域 FIFO 与深度选择OV5640 像素时钟 PCLK 和 AXI 总线工作时钟默认 150MHz 或 200MHz不在同一时钟域这里必须用一个异步 FIFO 做缓冲。关键是 FIFO 的深度怎么选需要保证在突发时不会溢出。从数据上看PCLK 24MHz、RGB565 16bit 时写入速率是 48MB/sAXI 读端 150MHz * 64bit 1.2GB/s读远快于写所以 FIFO 的核心作用是跨时钟域和匹配突发长度不是大容量缓存。一个 1K 深度的 FIFO 就非常宽裕了。但如果后续要切换到 JPEG 输出模式数据率会随着场景内容变化建议深度增大到 4K留足余量。FIFO 读写端口的位宽需要匹配写端 16bit读端 64bit。VDMA 的 AXI Stream 接口位宽通常配置为 64bit这样写带宽不变读带宽是 4 倍能有效避免读端突然连续取数时 FIFO 下溢。3.3 VDMA 还是 AXI DMA很多初学者在这里纠结图像数据到底用 VDMA 还是 AXI DMA我的建议是视频帧数据一律用 VDMA。虽然两者都能把数据从 AXI Stream 搬到内存但 VDMA 针对视频做了三项关键优化一是内置帧存储同步逻辑支持多帧缓冲和帧 ID 控制二是支持行同步和帧同步外部触发三是中断产生的位置可以配置比如每帧完成中断。AXI DMA 更适合流式数据比如以太网 DMA、FIFO 转内存这种场景它没有帧的概念需要自己维护帧边界复杂度和错误率都会上升。VDMA 关键参数配置如下参数配置值说明Stream Data Width64 bit与 FIFO 读端匹配Memory Map Data Width64 bit与 AXI 总线位宽匹配Burst Size16提高总线效率Frame Buffers3多缓冲降低帧冲突概率Pixel FormatRGB565对应 16bit 每像素Enable Frame SyncTrue外部帧同步触发使用 3 帧缓冲时VDMA 在 DDR 中按循环方式写入三块区域。好处是当 PS 端在读取当前帧时VDMA 已经写入第二、三帧即使 PS 端读取稍慢也不会把正在读的数据覆盖掉。代价是内存占用略高但 DDR3 空间足够。3.4 帧率与带宽估算以 VGA640x480、30fps、RGB565 为例一帧数据量为 640 × 480 × 2 614,400 字节约 0.59MB30fps 就是约 17.6MB/s。这个数据量对千兆以太网是毫无压力的千兆 UDP 理论有效负载可达约 118MB/s考虑以太网/IP/UDP 头后利用率超过 15%。但如果是 1080p30一帧 1920 × 1080 × 2 4.15MB30fps 就是约 124MB/s在千兆 UDDP 下已经到极限利用率抓包会看到持续满负荷稍有冲突就会丢包。所以这套工程里推荐默认分辨率选 800x480 或 720p既能看到清晰画面又能稳定传输。如果非要上 1080p建议输出 JPEG 流把数据量降下来。带宽估算公式如下单帧数据量字节 宽 × 高 × 每像素字节数传输带宽要求 单帧数据量 × 帧率UDP 有效负载率 1472 / 1514 ≈ 0.972千兆线速可用带宽 1000Mbps × 0.972 ≈ 972Mbps 121.5MB/s这些计算要在心里有底否则改分辨率时容易出现“帧率降下来了但依然丢包”的诡异现象。4. PS 端软件设计lwIP 与 UDP 发送链路4.1 Vitis 工程搭建与 BSP 配置PL 端工程在 Vivado 里综合实现后导出硬件描述文件然后打开 Vitis旧版叫 SDK创建应用工程。BSP 里需要勾选 lwIP 库版本通常选 2.0.3 或更新。这里提醒一下Vitis 里生成 BSP 时lwIP 库的配置界面里有一个lwipopts.h相关选项默认 DHCP 是开启的如果开发板没有接 DHCP 服务器会导致等待 DHCP 超时很久才进入应用建议在 BSP 设置里把LWIP_DHCP关闭改成静态 IP调试方便很多。同时要确认 BSP 中 CPU 中断正确连接GEM 中断、定时器中断都要绑定。lwIP 在裸机上需要定时器提供 TCP/IP 定时服务虽然 UDP 对定时要求低但 ARP 超时等机制也需要一般是配置一个 PS 端定时器周期 250ms 调用tcp_tmr()或sys_check_timeouts()。4.2 lwIP 协议栈配置与初始化lwIP 裸机使用有两种 APIRAW API 和 Socket API。从代码直观性和可维护性来看工程里我使用 Socket API也就是netconnAPI 的上层封装因为它是标准 Berkeley Socket 风格和 PC 端代码几乎一致容易理解。RAW API 性能更好但回调式编程对裸机工程复杂度高调试不方便。初始化代码骨架struct netif g_netif; static void network_init(void) { ip4_addr_t ip, mask, gw; IP4_ADDR(ip, 192, 168, 1, 10); IP4_ADDR(mask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); lwip_init(); netif_add(g_netif, ip, mask, gw, NULL, ethernetif_init, netif_input); netif_set_default(g_netif); netif_set_up(g_netif); sys_check_timeouts(); }netif_input是在轮询模式下从网卡驱动读数据包的函数裸机上需要主循环里不断调用sys_check_timeouts()和ethernetif_poll()如果需要轮询。如果使用中断收包lwip_init后还要使能 GEM 中断并在中断回调里调用tcpip_input()或netif-input()。4.3 图像数据打包与 UDP 发送打包发送是 PS 端程序的核心。每个以太网帧最大 1518 字节去掉 Ethernet 头 14 字节、IP 头 20 字节、UDP 头 8 字节用户数据最多 1472 字节。为了兼容某些交换机或 PC 网卡对 MTU 的严格限制工程里默认每包发送 1400 字节留 72 字节余量。通信协议设计上为了让接收端能识别图像帧边界我在每一帧的最前面加一个 12 字节的帧头偏移字节含义0-10xAA 0x55帧起始魔数2-5帧序号用于丢帧统计6-7一帧总包数用于接收端判断完整性8-11当前包序号用于包重组发送一帧的流程是先发送 12 字节帧头 第一段图像数据的包然后循环发送后续数据包。发送函数基于netconn的 UDP 接口struct netconn *udp_conn; udp_conn netconn_new(NETCONN_UDP); netconn_bind(udp_conn, IP_ADDR_ANY, 5001); netconn_connect(udp_conn, remote_ip, 5001); err netconn_send(udp_conn, pbuf);注意netconn_send是异步非阻塞还是阻塞取决于 TCPIP 线程模式。裸机下 socket API 默认不开启 TCPIP 线程netconn_send也会正常把数据交给网卡驱动发送。这里如果不开 TCPIP 线程则必须在主循环里调用sys_check_timeouts()同时网卡驱动发送时不能开启中断发送完成的等待机制ethernetif_init里通常配置为中断每包完成后释放 PBUF否则 pbuf 释放不及时会导致内存耗尽。一个很实用的经验把发送缓冲区改大比如PBUF_POOL_SIZE从默认的 16 加大到 64因为图像数据一次发送大量 pbuf如果池太小会频繁分配失败表现为 UDP 丢包或者发送线程阻塞。4.4 应用程序运行流程与交互主程序流程非常直接初始化 OV5640 寄存器 → 初始化 VDMA → 初始化 lwIP 网络 → 等待网络协商完成 → 主循环检测帧计数变化 → 有新帧则拆包发送。代码里我加了一个串口命令行支持几个简单命令读帧计数、设定目标 IP、查看发送丢包数、重启摄像头。在调试阶段非常有用不用每次改代码重新编译串口一条命令直接改目标地址省掉了反复烧 flash 的麻烦。5. 上位机接收与系统测试方法5.1 接收端软件选型上位机接收端有几种选择按部署难度从低到高排列VLC 播放器针对 MJPEG 流、Python Socket OpenCV 脚本、QT 自研界面、LabVIEW UDP 模块。这套工程因为是 RGB565 裸流所以 VLC 不能直接用需要写一个简单的转换脚本。推荐的做法是先用 Python 写一个快速验证脚本打开 UDP 端口接收数据解析帧头拼接成完整帧后转成 RGB888用 OpenCV 显示。这样 10 分钟就能看到图像等验证链路通了再决定要不要用 QT 写正式上位机。Python 脚本最大的好处是修改方便对新手友好不用编译环境。5.2 用 iperf3 做 UDP 打流测试在验证图像之前先验证网络本身的吞吐量。PC 端运行 iperf3 服务端Zynq 端如果烧写了 Linux 系统可以直接跑 iperf3但裸机下不方便这时可以反过来测试PC 端跑 iperf3 客户端去向 Zynq 打流或者用 zynq 端通过串口打印统计接收到的包数量来估算。更常用的做法是PC 上跑 iperf3 服务端Zynq 端用我们自己的代码按高频率发送纯载荷数据不带帧头的 UDP 包PC 端 iperf3 的-u参数就能统计出吞吐量和抖动iperf3 -u -s iperf3 -u -c 192.168.1.10 -b 100M -t 30如果 100Mbps 打流测试通过说明 800x480 的 RGB565 视频流约 40Mbps在链路上是安全的不需要怀疑网线或交换机问题。iperf3 输出的 jitter 指标如果超过 1ms 左右就要重点检查发送端的调度或缓存。5.3 Wireshark 抓包验证当图像效果不对时抓包是最直接的定位手段。在 PC 端用 Wireshark 抓 UDP 包过滤条件udp.port 5001然后观察包大小是否稳定在 1412 字节1400 数据 12 帧头帧序号是否正确递增是否能在数据里找到 0xAA 0x55 帧头特征这里分享一个小技巧Wireshark 自带的“以太网帧校验和计算器”功能可以帮助确认链路层是否有错帧。在 Wireshark 分析界面里选中 Ethernet 层能看到 FCS 校验结果是 good 还是 bad。如果出现大量 bad checksum说明 PHY 层接收到的数据已经有误码通常是 RGMII 时序或连接线质量问题和软件无关。另外设置 Wireshark 的udp.length过滤可以快速识别异常包比如有些包长度明显偏小可能是丢帧或组包漏洞。抓包时记得关闭 Wireshark 的“校验和验证”自动忽略设置否则它会直接把 UDP 校验和错误的包标红反而掩盖了真实问题。5.4 端到端联调流程一个稳定的联调顺序是先小分辨率小帧率再逐步提升。我一般从 320x240、10fps 开始确认画面完整后再调到 640x480、30fps最后才是 800x480 或 720p。每调一档观察上位机显示、串口日志里的帧计数和丢包统计三项数据。如果画面花屏但帧计数正常问题大概率在打包组帧逻辑如果帧计数跳变且丢包统计上升说明网络带宽或调度出了问题。6. 高频问题排查手册6.1 摄像头不出图、花屏、偏色OV5640 不出图首要怀疑点不是寄存器配置而是上电时序和时钟。用示波器量 MCLK 引脚确认 24MHz 时钟稳定输出再量 PCLK看配置后是否有像素时钟。如果 PCLK 一直是低电平说明传感器没有进入工作状态检查 PWDN 引脚电平是否正确多数模块是低电平为使能以及 SCCB 写寄存器后是否真的成功。图像花屏的排查思路是按“数据链路”逐级定位先看 FIFO 写端有没有数据再看读端有没有数据再看 VDMA 搬到 DDR 里的内容对不对。有一种很隐蔽的情况是RGB565 字节序反了图像颜色不对但轮廓正常。OV5640 默认高字节在前如果你的上位机按小端解析就会看到 B 通道和 R 通道互换颜色偏紫或偏蓝。这个不是硬件问题调换一下拼接顺序即可。偏色问题也可能是 SCCB 初始化时序过快导致的。OV5640 内部 ISP 寄存器很多如果某些 RGB Gain 寄存器没写成功颜色就会偏。这时候回读寄存器值确认写入结果比反复改代码更有效。6.2 网口 ping 不通Zynq 网口 ping 不通这是仅次于摄像头不出的第二大问题。排查顺序如下用 MDIO 读 PHY 寄存器 0x01确认 PHY 链接状态位是否置位如果没置位检查 PHY 复位、配置引脚和网线。看 RGMII 时钟是否正常TX 时钟、RX 时钟是否分别为 125MHz/25MHz。检查 GEM 驱动里 PHY 地址是否与实际一致常见 0x00、0x01、0x07 都试一遍。在 PC 端抓 ARP 请求看 Zynq 是否收到并回复。如果只看到 ARP 请求没有回复说明 PS 端协议栈还没正常运行重点查 lwIP 初始化和netif回调。实际维护中我遇到过 PHY 芯片工作在千兆模式但 PC 网卡强制百兆而导致协商失败的情况解决方案是直接修改 PHY 寄存器强制双工模式或者更换交换机端口。RGMII 的 TX/RX 延时控制也是大坑Zynq PS 端 GEM 默认已经做了内部延时部分 PHY 需要额外配置RTL8211E_DLY_TX才能对齐时钟如果 ping 能通但吞吐量极低优先检查这里。6.3 UDP 丢包严重UDP 丢包在图像传输中常见原因有三类。第一类是发送端来不及发裸机主循环里如果边发边采集CPU 被图像组包消耗过多导致 GEM 发送队列满部分包被丢弃。对策把帧数据按 DMA 方式交给 GEM 发送或提高 VDMA 帧缓冲数让发送端有时间补足。第二类是接收端缓冲区太小Windows 下 UDP 默认接收缓冲区只有 8KB 到 64KB如果你的上位机没有显式设置SO_RCVBUF1920x1080 的帧数据很容易溢出。在 C# 或 Python 里设置到 4MB 以上丢包现象会缓解很多。第三类是防火墙拦截Windows 防火墙默认拦截 UDP 高端口记得在防火墙中放行对应端口。还有一种隐蔽的丢包ARP 缓存过期。发送端在网线拔出或交换机重建时ARP 表项失效期间发出的包全部被丢弃。裸机 lwIP 下可以通过周期性发送 ARP 请求或在应用层加心跳包规避实际影响不大但要知道原理。6.4 开发板启动与加载异常开发板启动不起来先检查启动模式引脚。Zynq-7000 的启动模式由 MIO[6:4] 决定QSPI、SD、JTAG 三种方式经常被人改乱。如果 SD 卡启动但 BOOT.bin 不在 SD 卡里PS 端不会进入 app。工程输出 BOOT.bin 时注意包含 FSBL、PL 比特流、SSBL裸机 app 或 U-Boot缺一不可。另一个常见问题是“不带 DDR 的 Zynq 使用 OCM 加载”。如果板子上没有 DDR 颗粒VDMA 往 DDR 地址写数据自然是无效的。这时可以把分辨率降到 QVGA320x240RGB565 一帧 153,600 字节把 VDMA 的目标地址改为 OCM 地址 0xFFFC0000同时在 C 代码里把帧读取地址也改成这个。OCM 总共 256KBQVGA 一帧能放下但帧缓冲只能保留 1 帧读取和采集要严格错峰否则会出现撕裂画面。这个方案适合裸机演示但不适合工程落地。7. 工程源码结构与后续扩展7.1 源码目录与关键文件说明整个工程按 Vivado 和 Vitis 分成两部分源码结构如下project/ ├── vivado/ │ ├── block_design.tcl # Vivado Block Design 重建脚本 │ ├── constraints/ │ │ └── ov5640_udp.xdc # 引脚约束与时序约束 │ └── rtl/ │ ├── cmos_capture.v # OV5640 采集逻辑 │ └── cmos_fifo_wrapper.v # FIFO 实例化与接口封装 └── vitis/ ├── hw_export/ # 导出的硬件描述文件 └── app_udp_image/ ├── src/ │ ├── main.c # 主程序初始化与主循环 │ ├── ov5640_init.c # OV5640 寄存器配置表 │ ├── network_task.c # lwIP 初始化与 UDP 发送 │ └── vdma_driver.c # VDMA 配置与帧同步 ├── bsp/ # Vitis BSPlwIP 库勾选 └── app_config.h # 分辨率、目标 IP 等参数ov5640_init.c里保存了完整的寄存器配置表按分辨率组织成结构体数组方便切换。network_task.c里实现了上面提到的 12 字节帧头协议和 UDP 发送流程发送缓冲区的大小、端口号都在app_config.h里统一管理改参数不需要翻代码逻辑。7.2 可以继续扩展的方向这套工程跑通之后往下的扩展方向非常清晰。一是把 OV5640 的输出格式从 RGB565 改成 JPEGJPEG 压缩后一帧 720p 大约只有 50KB 到 100KB30fps 只占几十兆带宽上位机直接可以用 VLC 解码省掉自己写显示逻辑。二是升级到 4K 或高帧率采集这时 DVP 接口带宽吃紧需要切到 MIPI 接口和 Zynq Ultrascale 平台的硬核 MIPI CSI但数据通路和软件架构是类似的。三是把传输层从 UDP 升级为支持 RTP/RTSP 的流媒体方案便于接入现有视频平台或者引入 SRT 低延迟传输协议应对公网场景。四是针对实时性要求高的场景可以把发送模块从 CPU 扔进 PL 端用 AXI DMA 直接搬数据到 GEM 发送配合轻量级 FIFO延迟能压到几百微秒级别。还有一个实操中我认为很有价值的方向给这套工程加上图像预处理比如在 PL 端做灰度转换、边缘检测、ROI 裁剪将预处理后的数据再送 UDP这样上位机无需再做处理带宽需求也能大幅降低。毕竟很多工业视觉项目要的不是把原始图发出来而是先把数据压缩到足够小再走网络。从几年前第一次在 Xilinx 官方文档里看到类似参考设计到自己在板子上一步步调通 UART 打印、SCCB I2C、VDMA、lwIP这中间踩的坑比想象中多得多。但这套流程走完你对 Zynq 的“PSPL 协同一体化开发”就会有非常具体的体感——哪些活该交给 FPGA哪些活该交给 ARM数据从像素到网络的每一步延迟发生在哪里心里都会清清楚楚。我用下来最深的一个体会是把链路切分成“采、存、发”三段每段都用可观测的中间量帧计数、DDR 内的字节数、Wireshark 里的包验证比闷头看代码快十倍。最后再分享一个小技巧在 OV5640 初始化和网络初始化完成后用串口打印一个“sys_ready”的标志再把主循环里发送帧的计数和上位机收包计数做对比只要这两个数字以相同速率增长这个系统就基本稳定了。本文还有配套的精品资源点击获取