简介:面向FPGA视频传输应用开发者,本工程基于OV5640摄像头采集视频,在FPGA内部完成像素缓存与逻辑处理后,通过千兆以太网发送,实现接近实时的视频传输链路,是从图像采集到网络输出的完整参考设计。资源共包含276个文件,压缩包仅约3.98兆字节,文件类型涉及硬件描述语言源码、仿真测试脚本、约束与工程配置、时序与面积报告以及可直接烧写的比特流文件,便于用户从代码到上板的完整学习。当前已有746人学习下载。通过研读该工程,可以掌握摄像头接口时序、跨时钟域FIFO设计、以太网帧封装与发送控制,以及处理高带宽视频流时的缓冲与流量控制方法;同时可利用附带脚本复现仿真与综合流程,尤其针对OV5640的SCCB配置、帧同步信号处理等细节,能帮助规避常见工程陷阱,适合FPGA视频处理和网络通信方向的开发者深入实践。 做FPGA的,十有八九都绕不开两座山:图像处理、以太网通信。这个“fpga实例-以太网传送视频程序”项目,就是把这两座山叠起来做的一套完整链路——FPGA端把视频源数据按UDP包拆分,通过千兆以太网送到电脑,电脑端用上位机接收、解析并显示,最终实现“摄像头在板端,画面在屏幕上”的实时视频预览。它解决的是高速图像数据的远距离、低成本回传问题,适合正在啃FPGA图像处理和网络协议栈、想找一个能真正跑通的综合项目的工程师参考。
我会按项目实战的顺序拆解整条链路:先梳理整体架构和方案选型,再把带宽计算、包格式设计、PHY寄存器配置一个个讲透,最后给出关键代码框架和实测排查记录。全程按我实际调板子的经验来讲,数据和代码都是可直接落地的,不是那种只讲概念的入门科普。
1. 项目架构:从像素到以太网帧的完整链路
1.1 数据流设计与模块划分
整个项目的本质是一台“视频发送机”:厂编采集视频像素,暂存到缓存里,接着按以太网帧格式封装并发送,PC端再负责收包、解包、拼帧。具体数据流可以描述为:视频源(摄像头或测试图生成)进入采集模块,送入DDR3或片上FIFO做缓冲,随后由打包模块按设定的包长切块并加上自定义包头,送到以太网MAC层封装成UDP/IP报文,再经RGMII接口交给PHY芯片转成差分信号从网线发送出去。
在FPGA内部,模块划分我习惯做成这样:
- 视频采集模块:驱动OV5640这类摄像头,或者直接生成彩条、棋盘格等测试图像,方便在没有相机时先调通链路。
- 帧缓存与读写仲裁:用DDR3 MIG IP实现大容量缓冲,或对低分辨率直接用Block RAM双口FIFO。缓存的作用是消除以太网发送的突发和视频帧率之间的速率不匹配。
- 打包控制模块:按行或按块把图像数据切成大小固定的数据段,每段加帧号、块号、长度字段,组包后交给协议栈。
- 以太网MAC+UDP/IP封装:如果用Xilinx,可以直接用Ethernet IP核(自带MAC和RGMII),也可以自己写精简的MAC收发逻辑,负责生成前导码、以太网头、IP头、UDP头以及CRC校验。
- PHY驱动与MDIO管理:通过MDIO总线配置PHY芯片的寄存器,读取link状态、协商速率,控制复位和休眠。
这套模块划分的好处是每一级都有明确的接口,调试时可以逐级自环:先发固定数据包验证PHY和MAC,再发图像数据验证DDR读写,最后跑全流程。如果你用的是国产板卡,比如黑金、高云之类带PHY的开发板,思路完全一样,只需要换对应厂商的IP或自己补MAC层。
1.2 为什么选UDP而不是TCP
这是刚开始做视频传输的人最容易纠结的问题。我的答案是:视频实时传输优先选UDP,除非你有很强的理由必须用TCP。原因有三点。
第一,TCP为了可靠性付出了巨大代价:握手建立连接需要时间,重传机制会引入延迟和乱序,窗口管理和拥塞控制逻辑复杂,用纯RTL实现全硬件TCP协议栈工作量非常大,调试周期长,而且传统TCP在丢包时的行为对实时视频并不友好——你宁愿丢一帧,也不愿等重传后的旧数据。
第二,UDP协议栈在FPGA里只有几十行状态机的量级:固定好源IP、目的IP、源端口、目的端口,填充长度字段和校验和就可以发出。UDP允许组播,同一路视频流可以被多台电脑同时接收,这在调试和演示时太方便了。
第三,可靠性的补偿可以由应用层来做:发送端给每个包编序号,接收端根据序号判断丢包并做容错显示;偶尔丢一两个包对画面影响不大,最多在解码帧的对应位置出现花屏,下一帧就恢复了。这套策略的实际效果远比TCP重传好。
2. 关键指标与协议细节:先把带宽和包格式算明白
2.1 带宽预算是怎么算的
项目动手之前第一件事不是写代码,而是拿计算器算带宽。以最常见的1080p@30fps、RGB565格式为例:一帧像素数据量是1920×1080×2字节≈4.15MB,每秒30帧就是约124.4MB/s。注意这里是没有加任何协议头的纯像素数据。
千兆以太网的物理线速是125MB/s,但这不是有效负载速率。每个以太网帧都有前导码8字节、帧间隔12字节、以太网头14字节、CRC 4字节,加上IP头20字节和UDP头8字节,实际有效载荷利用率大概在94%左右,也就是说千兆网能承载的视频裸流上限大约在117MB/s附近。1080p30 RGB565需要124.4MB/s,已经超了,所以裸传必然丢包。
实际项目里我通常这样取舍:如果坚持1080p30,就得换成MJPEG压缩或H.264硬编码,把码率压到二三十兆;如果不想动压缩,就退到720p30 RGB565,一帧约1.84MB,每秒55.3MB,千兆网还有明显余量,跑起来非常稳。这就是带宽预算的意义——它直接决定了你的分辨率和像素格式选型。
2.2 应用层包格式设计
UDP的单个数据报文长度受MTU限制。标准以太网MTU是1500字节,扣掉IP头20字节和UDP头8字节,UDP载荷最大是1472字节。如果超过这个值,IP层会做分片,而FPGA实现对分片包的发送接收、重组非常麻烦,所以我强烈建议应用层包长设一个更保守的值,比如1400字节。
分包时要让上位机能重建完整图像,我设计的自定义包头结构如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧序号 | 2字节 | 当前图像帧的编号,从0递增,用于上位机判断丢帧 |
| 块序号 | 2字节 | 当前帧内的分包序号,从0递增,用于按序拼接 |
| 数据长度 | 2字节 | 本包携带的像素数据字节数 |
| 保留字段 | 2字节 | 备用,可存分辨率、格式等标志 |
一帧720p30 RGB565的数据量是1843200字节,按1400字节一包计算,需要1317个UDP包。上位机收到第一个包的块号为0,就可以开始申请缓冲,根据块号直接写入对应偏移,直到收满一帧再显示。这个设计在带宽上留了余量,在工程上又足够简单,是业内很常见的做法。
2.3 为什么说MTU和IP分片是坑
新手最容易踩的坑就是包长一不小心超过1472字节,导致IP分片。FPGA端如果自己写IP层封装,分片逻辑意味着要维护多个分片的ID、偏移和标志位,状态机和存储都变得复杂;而像W5500这类以太网模块虽然内部有自己的协议栈,但分片处理能力依然有限。实际工作中,很多厂商的GigE Vision相机默认使用的包长是1500以内,为的就是规避分片。
所以我的原则是:包长宁小勿大。1400字节已经能满足绝大多数视频流,数据量上多出的几十字节开销完全可以忽略。结构简单换来的是稳定性,这在FPGA项目里永远是最值钱的事。
3. PHY寄存器配置与RGMII时序真相
3.1 MDIO读写怎么才能稳
PHY芯片是FPGA和网线的桥,FPGA通过MDIO接口(一根时钟MDC、一根数据MDIO)读取和配置它的寄存器。最常见的PHY有Realtek RTL8211系列、Marvell 88E1512、国产裕太微等,寄存器基本兼容IEEE 802.3定义,只是扩展寄存器有厂商差异。
初始化时最重要的两个寄存器是:
- 寄存器0(BMCR,基本控制寄存器):bit15是软件复位,bit12是自协商使能,bit13、8、6、5组合控制速度和双工模式。
- 寄存器1(BMSR,基本状态寄存器):bit5是自协商完成标志,bit2是link状态,bit6、7表示10M/100M能力。
典型的上电配置流程是:先对寄存器0写0x8000触发软复位,等待复位完成;确认bit12为1,使能自协商;轮询寄存器1的bit5和bit2,等待自协商完成且link起来;之后再读取寄存器0确认协商后的速度是千兆还是百兆。RTL8211的MDIO地址通常是0x00,但也有0x01的版本,上板前先看原理图确认地址引脚。
如果PHY配置正确,还能通过寄存器值判断物理链路问题。比如以太网线只接了4根线、或者对端是百兆交换机,协商结果会降到100M,带宽瞬间少了十倍,画面必然卡顿。
3.2 RGMII接收端时序为什么要调delay
RGMII接口在千兆模式下时钟频率是125MHz,但数据是在时钟上下沿都采样的:上升沿送低4位,下降沿送高4位,等效数据率250Mbps×4位=1000Mbps。问题在于,PHY和FPGA之间PCB走线会导致时钟和数据存在相位偏移,如果直接按沿采样,可能采到毛刺或不稳定的跳变沿。
解决方案有两个:一是在PCB设计时保证RGMII的TX时钟和TX控制、TX数据严格等长;二是在FPGA内部用IDELAY原语对接收时钟或数据做相位调整。Xilinx的Ethernet IP核内部已经集成了这些延迟校准,但如果你是自己写RGMII收发的,必须在时序约束里明确生成时钟和数据的相对关系,否则综合后的结果跑着跑着就随机出错。
实际调试时,我习惯在PHY寄存器里找有没有RGMII TX/RX delay的配置位,比如RTL8211F的0xa43之类的扩展寄存器,先通过PHY侧调整delay,不够再用FPGA的IDELAY微调。这是最快能稳定跑满千兆的办法。
3.3 PHY寄存器分析怎么用
说一个很典型的现场:板卡上电后UDP发送端一直发数据,但PC端Wireshark什么都抓不到。这时候不要盯着代码发呆,先读PHY寄存器。
如果读到寄存器1的bit2为0,说明物理link根本没起来,问题在网线、对端设备或者PHY芯片供电;如果link是好的但协商在100M,说明网线或对端不支持千兆,带宽预算会被打破。如果MDIO总线上读写一直超时或返回全1,优先检查MDIO地址是否正确、MDC时钟频率是否过高(最好低于2.5MHz)、MDIO引脚是否加上拉电阻。
寄存器分析是网络调试的基本功。与其乱猜,不如把PHY所有状态位的含义整理成一张表,逐个对照排查,效率会高很多。
4. 代码层面的关键实现
4.1 视频发送状态机的骨架
下面是一个简化版的UDP发送状态机,核心是把“从FIFO读数据→组UDP包→交给MAC发送”串起来。这里只列关键状态,实际工程里还要加超时保护、FIFO空满判断和数据计数器。
localparam IDLE = 4'd0; localparam SEND_HEAD = 4'd1; // 发送以太网头 + IP头 + UDP头 localparam SEND_DATA = 4'd2; // 发送1400字节有效载荷 localparam SEND_CRC = 4'd3; // 交给MAC层补FCS localparam WAIT_END = 4'd4; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else case (state) IDLE: begin if (fifo_empty == 1'b0 && eth_tx_ready) state <= SEND_HEAD; end SEND_HEAD: begin if (head_done) state <= SEND_DATA; end SEND_DATA: begin if (data_cnt == PKG_SIZE) state <= SEND_CRC; end SEND_CRC: begin if (crc_done) state <= WAIT_END; end WAIT_END: state <= IDLE; default: state <= IDLE; endcase end有个细节要提醒:发送UDP头中的长度字段,需要实时计算为“IP总长度=20+8+data_len”,如果组帧是逐字节流式发送,就要提前用一个计数器把当前包的data_len算出来,在发送头的时候填进去。很多人忘了这一步,导致Wireshark里能看到包但校验长度不对,接收端直接丢包。
4.2 RGMII输出数据的位拼装
如果不用Xilinx IP核,自己拼RGMII数据时,8位并行数据要拆分到上下沿。核心代码如下:
// 8bit数据转RGMII 4bit DDR输出 always @(posedge eth_tx_clk) begin rgmii_txd[3:0] <= tx_data[3:0]; // 上升沿发低4位 end always @(negedge eth_tx_clk) begin rgmii_txd[3:0] <= tx_data[7:4]; // 下降沿发高4位 end assign rgmii_tx_ctl = (posedge_tx_en) ? 1'b1 : tdata_en; // 控制信号同理由上下沿分别赋值这段代码看起来简单,但要注意:eth_tx_clk必须是由时钟管理器生成的125MHz时钟,并且和tx_data保持确定的相位关系。官方IP的做法是把时钟和数据一起约束,自己写RTL的话必须用ODDR原语,而不是像我上面那样单纯用两个always在不同沿赋值——综合工具不一定能正确映射。所以实际开放中我通常建议直接调用原语ODDR,既可靠又省事。工程上不要为了“省一个原语”去赌综合器的行为。
4.3 上位机怎么收和显示
PC端接收建议先用Python快速验证,再移植到C#或C++做最终版本。Python的socket接收很简单:
import socket import struct import cv2 import numpy as np sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("192.168.1.100", 5000)) # 绑到PC网卡IP sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024) frame = bytearray(1920 * 1080 * 2) while True: data, addr = sock.recvfrom(1500) frame_id, block_id, length = struct.unpack(">HHH", data[:6]) offset = block_id * 1400 frame[offset:offset+length] = data[6:6+length] # 当 block_id == 某帧最大块号时,一帧凑齐,转成图像显示这段程序里最容易忽视的是接收缓冲区大小。Linux下默认socket缓冲区可能只有几百KB,千兆网速率下瞬间就会丢包,所以要用setsockopt调大。另外绑定的IP必须和FPGA发送的目的IP在同一个网段,最简单的方式是PC网卡设静态IP,比如192.168.1.100,FPGA端发往192.168.1.100,两端无需DHCP。
5. 实测记录与避坑指南
5.1 实测数据结果
我在Artix-7板卡上跑720p30 RGB565实测,上位机显示稳定在28~30fps,网络吞吐约55MB/s,帧延迟在80~120ms之间,画面基本无撕裂。换到1080p30未压缩RGB565时,网络吞吐逼近118MB/s,开始出现偶发丢包,表现为画面横向撕裂或条状花屏,重传是不可能的,只能靠下一帧覆盖。
这个结果完全印证了带宽预算的结论:1080p裸流跑千兆已到极限,没有余量。如果要稳,要么压缩,要么降分辨率。很多工业相机选择在相机内部做MJPEG压缩,再用GigE Vision传出来,原因就在这里。
5.2 常见问题速查表
| 故障现象 | 可能原因 | 解决办法 |
|---|---|---|
| PC抓不到任何UDP包 | PHY link未建立、MDIO配置错误 | 先读PHY寄存器1确认link;检查网线和对端设备;确认MDC时钟频率 |
| 能抓到包但封包长度字段不对 | UDP长度字段计算错误 | 用Wireshark对比实际载荷和Length字段;在发送状态机里补算长度 |
| 画面撕裂、花屏 | 带宽超限、socket接收缓冲区过小、分包顺序错乱 | 降低分辨率或换压缩格式;调大接收缓冲区;检查帧号和块号解析 |
| 图像颜色错乱 | 像素格式字节序不一致 | 核对RGB565发送和接收端的高低字节顺序,必要时上位机做字节交换 |
| 高速跑几分钟后死机 | PHY复位时序违规、时钟约束缺失 | 检查上电复位时序;加约束后重新综合布局布线 |
| 网速只有100Mbps | 网线只接入4芯、对端设备不支持千兆 | 换完整8芯网线;确认PHY协商结果;按需强制千兆模式 |
5.3 几个我踩过的坑
第一个坑是MDC时钟频率。最开始我看PHY的datasheet说MDC最高支持25MHz,就直接在系统里分频出12.5MHz用,结果MDIO读写不稳定,时好时坏。问题是MDIO时序需要足够的建立保持时间,尤其在上拉电阻阻值偏大的情况下,时钟边沿和数据翻转会错位。后来我把MDC降到2.5MHz以下,读写才完全稳定。MDIO只是配置通道,不追求高速,慢一点反而更稳。
第二个坑是上电后立刻读PHY寄存器返回全F。有些PHY需要几十毫秒完成内部校准,如果FPGA复位结束马上发MDIO读命令,PHY还没准备好,返回数据自然是无效的。正确做法是在初始化状态机里加一个至少100ms的延时,等PHY稳定后再操作。
第三个坑是RGMII时钟约束缺失导致时序混乱。只要在Vivado里看到“RGMII接口时钟和数据路径未约束”之类的Critical Warning,就要停下来,要么用官方IP,要么自己加约束,不要指望跑仿真能发现——仿真是理想波形,上板才见真章。
第四个坑是上位机接收缓冲区不足时表现出的现象很像FPGA丢包:画面随机撕裂,但无线速统计时又发现PC实际收到的包远少于发出的包。这个排查花了我一晚上,后来用Wireshark抓包统计才发现丢包发生在操作系统内核缓冲区,和FPGA一点关系都没有。先确认接收端,再查发送端,排查顺序很重要。
6. 从项目延伸到工程级的改进思路
这个项目做通之后,很多人会问下一步怎么走。如果让我建议优先级,第一个加的模块是图像压缩。用FPGA实现MJPEG编码并不算困难,JPEG的核心DCT和霍夫曼编码都有成熟的IP或开源代码可参考,压缩后1080p码率能控制在20Mbps以内,千兆网余量巨大。
第二个值得加的是丢包重传或前向纠错。虽然在UDP上做重传会引入延迟,但如果只在关键帧或关键行上做选择重传,对画面质量提升非常明显。FEC则可以通过冗余包恢复少数丢失的包,带宽多花5%~10%,换来几乎无损的传输,适合数据链要求高的场合。
第三个方向是组播。FPGA发送端用UDP组播地址,多台PC同时接收同一路视频,对产线测试或教学演示都非常实用。RTL8211这类PHY对组播不需要额外配置,只需在IP层把目的IP设为组播地址,并确保交换机开启IGMP Snooping,否则会按广播泛洪处理。
最后分享一个我个人的工程习惯:每个模块都预留一个环回测试点。视频数据在进入打包模块之前,可以一键切到固定递增数;以太网发送端能向PC发固定模式的测试包;上位机收包软件里也加一个帧率、吞吐量统计窗口。这套“自测”机制在项目联调时能节省大量时间,因为你永远要先定位是发送端的问题还是接收端的问题,而不是对着黑盒子瞎猜。
本文还有配套的精品资源,点击获取