资讯动态

ZYNQ+FPGA视频采集全链路:OV5640到UDP上传的实战解析

发布时间:2026/9/12 7:10:32 来源:尧图企业网站定制
简介面向ZYNQ 7010嵌入式开发者提供基于FPGA驱动OV5640摄像头采集视频并通过UDP协议上传的完整工程方案覆盖SoC软硬件协同设计、接口配置、驱动逻辑与网络通信等环节。压缩包共562个文件约37.88MB以VHDL/Verilog源码、XDC约束、XCI/TCL配置脚本、BIT比特流、RPT报告及各类文档为主结构清晰便于按模块查阅。目前已有178人学习浏览适合学习ZYNQ/FPGA视频采集与UDP传输的开发者参考。借助本工程可掌握FPGA端图像数据接收预处理、ARM端驱动调用及UDP打包发送流程并可依据RTL与约束文件快速重建Vivado工程理解从寄存器配置到网络输出的完整调试路径。1. 摄像头数据从传感器到网线FPGA 才是决定上限的那一段视频上传项目真正把人卡住的通常不是网络而是 FPGA 把像素流摆进内存之前的流水线。OV5640 输出原始像素经过 ZYNQ 7010 的 PL 侧同步、缓冲、DMA 搬运再由 ARM 切成 UDP 包发出去任何一个环节的节拍没对上丢帧就不可避免。这篇从接口选型、寄存器初始化、采集状态机、VDMA 配置到 socket 发送把整条链路的参数和坑位捋一遍。适合正在选型视频采集方案的工程师也适合手上有 ZYNQ 平台、照着 Xilinx 的 example design 拼过之后却不知道下一步怎么调优的人。2. ZYNQ 7010 的 PL-PS 分工与 OV5640 的 DVP 接口初始化先明确一个结论ZYNQ 7010 上做摄像头采集建议直接用 OV5640 的 DVP 并行接口而不是一上来就上 MIPI CSI-2。原因很实际和芯片资源与调试成本直接相关。2.1 为什么选 DVP 而不是 MIPI CSI-2OV5640 本身同时支持 MIPI 和 DVP 两种输出但 MIPI 在 PL 侧要处理差分对、通道对对齐、Lane 分配至少消耗两个 PLLXilinx 的 MIPI CSI-2 IP 还涉及授权。而 DVP 是 8 位并行加 PCLK、VSYNC、HREF 三个同步信号逻辑上就是一个状态机加行缓冲在 7010 这种中小规模芯片上实现起来非常干净。带宽上 DVP 也没有想象中那么弱。以 720p30 的 YUV422 为例一像素 2 字节有效像素时钟约 55MHz加上行消隐和帧消隐典型 PCLK 在 65MHz 左右8 位总线上数据率约 520MbpsFPGA 内部资源完全吃得下。真正紧张的是 1080p30YUV422 下需要 16 位 DVP 模式数据率接近 1Gbps这已经逼近千兆以太网的理论上限后面会单独讨论这个问题。所以项目初期先用 DVP 把链路跑通再考虑要不要为高分辨率上 MIPI是一个投入产出比更高的路径。2.2 OV5640 寄存器初始化序列OV5640 上电后不会自动输出目标格式的视频流必须通过 SCCB兼容 I2C接口配置寄存器。初始化有几个关键点软复位、PLL 分频、输出尺寸、YUV422 格式、翻转方向。下面这是 720p30 YUV422 下最常用的一组寄存器配置不同批次传感器略有差异最终以 datasheet 为准。寄存器地址写入值功能说明0x30080x02软复位复位后 sensor 重新加载内部 PLL0x31030x11内部时钟分频设置0x30350x21PLL 分频系数0x30360x69PLL 倍频系数决定输出 PCLK0x38200x40垂直翻转配置0x38210x00水平镜像配置0x3808 / 0x38090x05 / 0x00水平输出尺寸 1280 高字节/低字节0x380A / 0x380B0x02 / 0xD0垂直输出尺寸 720 高字节/低字节初始化时序上有一个常见的坑软复位之后不能立刻写后面的寄存器OV5640 内部上电和时钟稳定需要时间。我一般会在写完 0x3008 后加至少 10ms 延时而不是直接连续写。这里的代码实现通常放到 FPGA 内部集成的一个 I2C 控制器里用状态机逐条发送。task i2c_write_reg; input [7:0] dev_addr; // OV5640 写地址直接传 0x3C input [7:0] reg_addr; input [7:0] reg_val; begin i2c_start(); i2c_write_byte(dev_addr); // 发送包含 W bit 的设备地址 i2c_write_byte(reg_addr); // 寄存器地址 i2c_write_byte(reg_val); // 寄存器值 i2c_stop(); #10000; // 两次写之间留至少 10us end endtask参数说明dev_addr是 8 位写地址OV5640 手册里写 0x3C这个值已经包含了最低位的写标志很多第一次接触的人会把 0x3C 再左移一位变成 0x78 发出去结果总线 ACK 都收不到。i2c_write_byte里的#10000是仿真延时实际工程中由状态机计数来控制目的就是防止连续写太快导致 sensor 内部寄存器写入失败。如果初始化完成后 VSYNC 一直没有信号优先抓 I2C 的 ACK 波形确认每个字节都有应答。2.3 设备树里给 PL 端和 DMA 做的准备OV5640 的像素数据从 FPGA 采集之后最终要经 VDMA 写入 DDR所以设备树里要先定义好 VDMA 节点。下面这是一个在 Vivado block design 中分配了基地址 0x43000000 的 VDMA 的典型设备树片段amba_pl: amba_pl { compatible simple-bus; #address-cells 1; #size-cells 1; ranges; vdma: vdma43000000 { compatible xlnx,axi-vdma-1.00.a; reg 0x43000000 0x1000; dma-channel43000030 { compatible xlnx,axi-vdma-s2mm-channel; interrupts 0 30 4; }; }; };逻辑说明reg里的 0x1000 是寄存器空间大小VDMA 的 S2MM 通道寄存器组在偏移 0x30 以内都能被这个范围覆盖。interrupts三个字段分别是 SPI 类型标记、中断号 30、触发方式高电平。这个中断号不是随便写的必须和 Vivado 里连接 ZYNQ 的 PL 中断输入对应上否则 Linux 驱动 probe 的时候会报 irq 申请失败。设备树编入 dtb 之后Xilinx 的 VDMA 驱动会注册一个 DMA 设备同时在内核中预留连续物理内存作为帧缓冲。预留大小建议按三帧计算比如 720p30 YUV422 一帧约 1.8MB三帧就是 5.4MB再留一点余量。CMA 太小会导致驱动 probe 成功但 buffer 分配失败dmesg里能看到明确的申请内存失败日志。3. Verilog 实现 OV5640 像素采集与 VDMA 乒乓缓冲采集模块是整个链路里最容易写错的地方问题大多出在对 VSYNC / HREF 时序的理解上。OV5640 在 DVP 模式下的行为是VSYNC 拉高表示新的一帧开始HREF 拉高期间每个 PCLK 上升沿输出一个有效像素行结束 HREF 拉低。帧率由 PCLK 和寄存器里的时序参数共同决定FPGA 这边不需要也不能去调整它只能被动跟着采样。3.1 行同步采集状态机状态机的核心是识别帧边界和行边界。我的做法是先把 VSYNC 做两级寄存器打拍消除跨时钟域的亚稳态再用边沿检测信号来驱动状态跳转。reg vsync_d1, vsync_d2; wire vsync_rise vsync_d1 ~vsync_d2; wire vsync_fall ~vsync_d1 vsync_d2; always (posedge pclk) begin case (state) IDLE: if (vsync_rise) state WAIT_FRAME; WAIT_FRAME: if (vsync_fall) state READ_LINE; READ_LINE: if (vsync_rise) state WAIT_FRAME; default: state IDLE; endcase end逻辑说明WAIT_FRAME状态的用途是过滤掉 VSYNC 上升沿之后的无效像素很多 OV5640 配置下帧起始位置有一段 blanking如果直接进采集会把噪声数据当成有效像素写进 FIFO。READ_LINE状态下用href作为行数据的门控信号同时data_in按字节写入行缓冲。为什么不在 IDLE 直接等href因为帧与帧之间如果传感器没有稳定输出HREF 上可能有不规则毛刺先等一个完整的 VSYNC 下降沿逻辑上更干净。这个状态机还隐含了一个设计取舍不做像素的跨时钟域处理。PCLK 直接作为采样时钟采集到的数据和 OV5640 是同步的但进入 AXI 总线域时必须经过异步 FIFO。实际工程里我会在 FIFO 的写侧用 PCLK读侧用 AXI 时钟 150MHz两侧频率不同也没有问题。3.2 FIFO 深度与带宽计算FIFO 深度选多大取决于行长度和 AXI 读侧可能等待的时间。如果 VDMA 的 AXI 写通道忙FIFO 必须能暂存至少一行的数据否则行首数据还没搬走下一行的数据已经把 FIFO 占满就会溢出丢行。分辨率数据格式典型 PCLK有效数据率建议 FIFO 深度720p30YUV422 8bit65MHz约 55MB/s4096 x 64bit1080p30YUV422 16bit130MHz约 124MB/s8192 x 64bit480p60YUV422 8bit27MHz约 24MB/s2048 x 64bit这里按 720p30 计算一行 1280 像素YUV422 一像素 2 字节行长度 2560 字节。4096 深度的 64 位 FIFO 可以存 32KB足够放下 12 行数据。FIFO 写侧几乎不会成为瓶颈瓶颈在 AXI 总线拿不到足够的突发带宽。ZYNQ 7010 的 PL 到 DDR 的带宽远高于 55MB/s只要 VDMA 的地址不冲突写入延迟不会让 FIFO 溢满。一个容易忽略的点FIFO 的读侧数据宽度要和 VDMA 的 AXI4-Stream 总线宽度匹配。VDMA 数据接口用 64 位时FIFO 读侧也要配成 64 位否则带宽利用率降低一半720p30 也许能跑1080p 就一定会丢帧。3.3 VDMA 乒乓缓冲配置VDMA 的 S2MM 通道把 AXI4-Stream 数据连续写入 DDR乒乓缓冲就是让 VDMA 在多个帧缓冲地址之间循环。这样 FPGA 往帧 0 写的时候ARM 可以从帧 1 读已完成的帧数据两者互不干扰。配置 VDMA 的步骤固定且顺序敏感把 VDMA 的 S2MM 通道软复位等待复位位自清零。依次写入帧缓冲地址 0、1、2地址需要 32 字节对齐。设置水平尺寸字节数和垂直尺寸行数对应一帧 2560 字节宽、720 行高。设置帧缓冲数量为 3使能循环模式。最后把 DMA 通道使能位拉高开始传输。配置项数值示例说明水平尺寸2560一行字节数必须和 FIFO 输出宽度对齐垂直尺寸720有效行数不含帧 blanking帧缓冲数量33 缓冲可避免 CPU 读帧时 VDMA 覆盖数据帧地址对齐32 字节AXI burst 传输要求不对齐会降低效率这个流程可以直接写在 C 代码里也可以让 Linux 驱动来做。我的习惯是交给内核的 xilinx-vdma 驱动管理用户态只通过 ioctl 启动传输不直接碰寄存器。内核驱动会把帧完成中断映射到 wait queue用户态阻塞等待即可。三缓冲的意义在于即使 ARM 在某一帧被调度延迟了VDMA 还有两个帧缓冲可以继续写不会因为 CPU 慢而丢帧。4. ARM 侧 UDP 上传的实现与性能边界数据进入 DDR 之后剩下的工作由 ARM 完成。ZYNQ 7010 的双核 A9 处理 720p30 的 UDP 封包没有问题关键在怎么把数据从内核高效拿到用户态再以正确的节奏发出去。4.1 把 VDMA 缓冲映射到用户空间最常见的错误用法是每一帧都通过read()从内核拷贝数据。一帧 1.8MB30fps 就是 55MB/s 的拷贝开销加上 socket 发送时的又一次内核拷贝双核 A9 会直接被内存拷贝拖垮。正确做法是用连续物理内存驱动u-dma-buf 或 dma-proxy把 DMA 缓冲直接映射到用户态让 VDMA 写和 CPU 读共用同一块物理内存。int fd open(/dev/udmabuf0, O_RDWR); struct udmabuf_info { unsigned long phys_addr; unsigned long size; } info; ioctl(fd, UDMABUF_INFO, info); unsigned char *frame mmap(NULL, info.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);逻辑说明mmap之后用户态指针frame指向的就是 VDMA 正在写入的物理缓冲。内核驱动的代码路径里VDMA 的帧缓冲地址被固定配置为info.phys_addr。ARM 端的 cache 处理由 DMA API 保证一致性用户态不需要手动flush。参数方面MAP_SHARED必须加否则 VDMA 写进来的数据不会回写用户态映射区info.size要大于等于三帧的缓冲容量。4.2 UDP socket 参数与逐帧分包发送socket 用SOCK_DGRAM不用 TCP原因很直接视频流容忍少量丢包但不能容忍 TCP 重传带来的延迟抖动。UDP 一发出去就完事帧与帧之间的间隔更稳定。下面是发送一帧数据的关键代码int s socket(AF_INET, SOCK_DGRAM, 0); int sndbuf 512 * 1024; setsockopt(s, SOL_SOCKET, SO_SNDBUF, sndbuf, sizeof(sndbuf)); struct sockaddr_in dst; dst.sin_family AF_INET; dst.sin_port htons(5000); inet_pton(AF_INET, 192.168.1.100, dst.sin_addr); while (1) { wait_frame_ready(); // 阻塞等待 VDMA 帧完成中断 int offset 0; int frame_size 1280 * 720 * 2; while (offset frame_size) { int chunk (frame_size - offset) 1472 ? 1472 : (frame_size - offset); sendto(s, frame offset, chunk, 0, (struct sockaddr *)dst, sizeof(dst)); offset chunk; } }逻辑说明SO_SNDBUF设置成 512KB 是必要的默认情况下 Linux 的 UDP 发送缓冲区只有几十 KB720p30 一帧分包后大约 1250 个包发送稍微慢一点就会触发缓冲区满sendto直接返回EAGAIN。1472这个数字是 1500 字节 MTU 减去 20 字节 IPv4 头和 8 字节 UDP 头之后的最大载荷如果不按这个切分包会在 IP 层被分片分片包在网络上更容易丢。发送节奏上每个包之间不需要 sleep因为sendto本身在内核协议栈里会有排队行为。整个 while 循环跑满一帧数据后再等下一帧中断就能保证发送速率跟随 sensor 的帧率不会无限加速。4.3 帧率与带宽边界UDP 虽然开销小但带宽上限是硬约束。下面按 1Gbps 以太网计算不同分辨率下的实际占用分辨率帧格式单帧大小30fps 带宽对 1Gbps 占用率720p30YUV4221.76MB约 423Mbps42%1080p30YUV4223.96MB约 950Mbps95%720p30YUV4201.32MB约 317Mbps32%数据很明确720p30 YUV422 在千兆网上还有接近 60% 的余量适合直接传1080p30 YUV422 按理论值算已经到 950Mbps加上交换机转发和 PHY 开销实际帧率一定会被压到 25fps 以下而且网络上一旦有广播风暴立即丢包。真要在 1Gbps 上传 1080p通常的做法是 FPGA 里先做 YUV422 到 YUV420 的转换或者直接上 H.264 编码把带宽压到几十 Mbps。这个转换假如需要临时验证也可以在采集状态机里跳过 UV 分量但会丢颜色信息只适合灰度场景。5. 验证与排错Wireshark 抓包、iperf3 打流与启动镜像UDP 视频链路的问题集中在两处采集侧节奏不对和网络侧带宽不够。这两类问题要用不同工具分别验证先确认网络本身再回头查 FPGA。5.1 用 Wireshark 判断 UDP 包的到达节奏接收端用 Wireshark 抓包过滤条件填udp.port 5000然后给列表增加一列tcp.time_delta。虽然名字里带 tcp但对 UDP 包同样生效显示的是相邻两个包的到达时间差。正常情况下一帧内的 UDP 包到达间隔是微秒级帧与帧之间会有一个明显空隙。如果抓包显示几百毫秒内没有包说明 FPGA 侧 VDMA 中断没有按时产生去查 VDMA 的中断回读寄存器确认帧完成位是否置位。如果每个包的 delta 都很大且不稳定瓶颈在发送端 CPU检查是不是用户态程序没有绑定到单独核或者sendto之前有内存拷贝。5.2 iperf3 打流测网络上限在主机上先起接收端再让 ZYNQ 作为发送端打流接收端执行iperf3 -s -p 5001ZYNQ 上执行iperf3 -c 192.168.1.100 -u -b 600M -l 1472。这里-b 600M模拟视频流的带宽占用-l 1472使用和实际视频包相同的 UDP 载荷大小避免测出来的是小包性能。如果 600M 打流时丢包率几乎为零说明网络链路没有瓶颈问题回到 FPGA 侧的 FIFO 溢出或者 VDMA 缓冲配置。如果丢包率很高先看交换机端口是否开启了流控再用ethtool eth0确认网卡实际协商速率是 1000Mb/s 而不是 100Mb/s。这个测试最好在开发板和接收主机直连的情况下做避免中间经过无管理交换机引入额外丢包。5.3 启动镜像与 FSBL 的坑PL 侧的 bitstream 和 ARM 侧的 u-boot、内核要打包成 boot.bin 和 image.ub 才能从 SD 卡启动。用 bootgen 生成 boot.bin 时如果 partition 列表里没有包含 FSBL工具会直接报a valid fsbl file is required for flash operation。原因是 ZYNQ 上电后由 BootROM 引导 FSBLFSBL 负责初始化 DDR 和配置 PL缺了这一层 bitstream 无法加载。正确的打包顺序是先编译 fsbl.elf注意选择与 7010 器件匹配的工程再和 bitstream、u-boot.elf 一起通过 bootgen 的 bif 文件生成 boot.binSD 卡第一个分区放 BOOT.BIN第二个分区放 image.ub 和设备树。FSBL 编译时如果打开过ENABLE_MPU之类的选项可能导致 DDR 初始化失败表现为串口完全没有输出此时要回看 FSBL 源码里的 DDR 参数是否和实际硬件颗粒匹配。启动路径确认没问题后还有一个排查顺序容易被跳过用ethtool -S eth0查丢包统计。rx_dropped和tx_dropped计数如果持续增长说明丢包发生在网卡驱动这一层而不是应用层或网络链路优先增大网卡 ring buffer 参数。这个命令执行一下只需要几秒却能帮你把问题范围缩小一半。本文还有配套的精品资源点击获取

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

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

免费获取报价