29、项目实战2:网络摄像头(OV5640+以太网UDP传输+PC上位机显示)
这个项目,说实话,是我个人觉得整个课程里最有意思的一个。
为什么?因为它把图像采集、高速传输、PC端显示全串起来了。你做完这个,基本就能理解一个完整的视频传输链路是怎么跑的。
说白了,就是把OV5640摄像头拍到的画面,通过FPGA打包成UDP包,扔到以太网上,最后在PC的上位机软件里显示出来。
29.1 项目整体架构
先看大框架。整个系统分四块:
- 图像采集端:OV5640摄像头,输出DVP并行数据。
- FPGA处理核心:负责数据缓存、格式转换、UDP组包。
- 以太网传输:PHY芯片(RTL8211或YT8512),走UDP协议。
- PC上位机:用Python或C#写的接收程序,解析UDP包并显示图像。
我习惯把FPGA内部再分成几个模块:
cmos_capture:抓取OV5640的像素数据。frame_buffer:用DDR3或内部BRAM做帧缓存。udp_packet:把图像数据切成MTU大小的UDP包。eth_tx:MAC层发送,控制PHY芯片。
29.2 OV5640配置与数据采集
OV5640上电后,默认输出是RGB565格式,分辨率640x480。但我们需要调整一下。
我个人建议输出格式用JPEG压缩流,而不是原始RGB。为什么?因为640x480的RGB数据,一帧就有640*480*2 = 614400字节。如果每秒30帧,那就是18MB/s的数据量。UDP虽然快,但丢包风险也大。JPEG压缩后,一帧可能只有几十KB,传输压力小很多。
配置关键寄存器:
// 设置输出格式为JPEG
SCCB_Write(0x3006, 0x00); // 关闭ISP
SCCB_Write(0x3018, 0x04); // 设置DVP输出
SCCB_Write(0x3034, 0x1A); // 设置PLL
SCCB_Write(0x4300, 0x30); // 输出格式:JPEG
SCCB_Write(0x501F, 0x01); // 使能JPEG输出
嗯,这里要注意:OV5640的JPEG输出不是连续的,它会在每帧开始和结束时插入一些特殊标记。你需要在FPGA里检测这些标记,才能正确切分帧数据。
29.3 数据缓存与UDP组包
JPEG数据是变长的,一帧可能大也可能小。所以我们需要一个缓存区。
我项目中用过两种方案:
- 方案A:用DDR3做帧缓存。适合高分辨率,但控制复杂。
- 方案B:用内部BRAM做行缓存。适合低分辨率,简单可靠。
对于640x480的JPEG流,我推荐方案B。因为JPEG压缩后,一帧数据通常不超过100KB。Pango系列FPGA的BRAM足够用。
UDP组包时,要注意MTU限制。标准以太网MTU是1500字节,减去IP头和UDP头(20+8=28字节),实际有效载荷最多1472字节。
所以你要把一帧JPEG数据切成多个1472字节的包。每个包加一个自定义头部,包含帧序号和包序号。
// UDP包自定义头部结构
typedef struct {
uint16_t frame_id; // 帧序号
uint16_t packet_id; // 包序号
uint16_t data_len; // 本包数据长度
uint16_t total_packets; // 总包数
} udp_header_t;
PC上位机收到包后,根据帧序号和包序号重组数据。如果发现丢包,可以请求重传,或者直接丢弃这一帧。
29.4 以太网MAC层实现
紫光同创的FPGA里,我一般用GMII接口连接PHY芯片。时钟125MHz,数据位宽8bit。
MAC层需要实现:
- 前导码生成(7字节0x55 + 1字节0xD5)
- MAC地址填充
- IP/UDP校验和计算
- CRC32校验
校验和计算是个坑。IP头校验和可以用查表法快速计算,但UDP校验和是可选的。我建议把UDP校验和设为0,这样PHY芯片不会校验,能省不少逻辑资源。
发送流程大致如下:
// 伪代码:UDP包发送状态机
state_idle:
if (有数据要发送) goto state_preamble;
state_preamble:
发送8字节前导码;
goto state_mac;
state_mac:
发送14字节MAC头(目标MAC+源MAC+类型);
goto state_ip;
state_ip:
发送20字节IP头;
goto state_udp;
state_udp:
发送8字节UDP头;
goto state_data;
state_data:
发送1472字节图像数据;
goto state_crc;
state_crc:
发送4字节CRC32;
goto state_idle;
我曾经在调试时发现,CRC32计算错了,导致PC端网卡直接丢弃了所有包。后来用Wireshark抓包才发现,原来是我把CRC的初始值设错了。嗯,细节决定成败。
29.5 PC上位机显示
上位机我用Python写的,用socket接收UDP数据,用OpenCV显示图像。
核心代码就几行:
import socket
import cv2
import numpy as np
# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 8080))
frame_buffer = {}
current_frame_id = 0
while True:
data, addr = sock.recvfrom(2048)
header = data[:8] # 解析自定义头部
frame_id = int.from_bytes(header[0:2], 'big')
packet_id = int.from_bytes(header[2:4], 'big')
total_packets = int.from_bytes(header[6:8], 'big')
# 缓存数据包
if frame_id not in frame_buffer:
frame_buffer[frame_id] = [None] * total_packets
frame_buffer[frame_id][packet_id] = data[8:]
# 检查是否收齐一帧
if all(p is not None for p in frame_buffer[frame_id]):
jpeg_data = b''.join(frame_buffer[frame_id])
img = cv2.imdecode(np.frombuffer(jpeg_data, np.uint8), cv2.IMREAD_COLOR)
cv2.imshow('Camera', img)
cv2.waitKey(1)
del frame_buffer[frame_id]
你想想看,这个上位机其实很简单。但实际跑起来,你会发现帧率可能只有10-15fps。为什么?因为Python的socket接收速度有限,而且OpenCV的imdecode也需要时间。
如果想提高帧率,可以用C++写上位机,或者用硬件解码。不过对于学习来说,Python版本已经够用了。
29.6 调试与避坑指南
这个项目调试起来,说实话,有点麻烦。因为涉及硬件和软件两边,问题定位比较困难。
我总结几个常见坑:
- OV5640初始化失败:SCCB时序不对,或者上电顺序不对。建议用示波器抓SCCB波形,确认每个ACK都有回应。
- UDP包发不出去:检查PHY芯片的复位时序,以及MAC地址是否配置正确。可以用Wireshark在PC端抓包,看有没有数据到达。
- 图像花屏:JPEG数据被截断或顺序错乱。检查FPGA里的帧缓存逻辑,确保每个包的数据完整。
- 帧率太低:可能是UDP包间隔太大,或者上位机处理太慢。可以尝试增大UDP发送缓冲区,或者减少每帧的JPEG质量。
我记得第一次跑通这个项目时,看到PC屏幕上显示出摄像头画面,那种成就感真的难以形容。虽然只有15fps,但那是自己一行一行代码写出来的。
好了,这一章就到这里。下一章我们会做一个更复杂的项目:基于PCIe的高速数据采集卡。到时候见。