第50章:基于OV5640的PL以太网视频传输实验
前几章我们打通了以太网协议栈,这一章让它干"正经活":把 OV5640 摄像头拍到的画面,通过网线实时传到电脑上显示。这是前面所有知识的综合应用——IIC 配置、FIFO 缓存、UDP 收发,一个都不落。
本章在讲什么?(先看这个)
实验任务:开发板采集 OV5640 图像(640×480 @RGB565)→ 通过 PL 千兆网口以 UDP 发送 → 电脑上位机软件实时显示视频。
新增知识块有三块:
- OV5640 摄像头:SCCB 配置接口(兼容 IIC)、DVP 输出时序、RGB565 格式;
- 图像封装:给每帧图像加帧头,抵御 UDP 丢包;
- 控制流设计:上位机发"1"/"0"命令控制视频开始/停止。
核心概念拆解
50.1 OV5640:500W 像素 CMOS 传感器
OV5640 是 OmniVision(豪威科技)的 1/4 英寸图像传感器,感光阵列 2592×1944(500W 像素),最高 90fps VGA。内部集成了 ISP(图像信号处理)、AEC(自动曝光)、AWB(自动白平衡)等功能,功能框图的核心链路是:
感光阵列 → 增益放大 → 10位AD → ISP处理 → 输出10位视频数据流
↑
时序发生器输出 VSYNC/HREF/PCLK
配置这些功能的接口是 SCCB(串行摄像头控制总线)——它兼容 IIC,所以我们可以直接复用之前 EEPROM 实验的 I2C 驱动模块。
SCCB 与 IIC 的两个差别要记住:
- 写传输:第 9 位是"不必关心位"(IIC 是应答位),基本无感;
- 读传输:没有重复开始(restart)的概念——先做一次"虚写"(写器件地址+寄存器地址),发停止信号后再发起读操作;且读最后一个字节时主机必须给 NA(高电平)。
OV5640 的器件地址是 7'h3c,寄存器地址是 16 位(两个字节),这是与 EEPROM(8 位地址)的关键区别,I2C 驱动的 BIT_CTRL 参数要设为 1。
50.2 三个"窗口":图像尺寸怎么裁剪
OV5640 的输出尺寸由三级窗口层层决定:

- ISP 输入窗口(0x3800~0x3807):从 2632×1951 的物理阵列里圈出进入 ISP 处理的区域;
- 预缩放窗口(0x3810~0x3813):在 ISP 窗口内偏移裁剪;
- 输出大小窗口(0x3808~0x380b):最终输出尺寸,本实验设为 640×480。
关键规则:输出窗口与预缩放窗口比例不一致时,图像会被缩放(变形);比例一致才是 1:1。
50.3 RGB565 输出与时序
把寄存器 0x4300 的 Bit[7:4] 设为 6、Bit[3:0] 设为 1,即输出 RGB565、RGB 顺序。RGB565 模式下 10 位数据线只有高 8 位(D[9:2])有效,一个像素分两次送出:

- 第一个字节 = RGB565 高 8 位,第二个字节 = 低 8 位;
- 数据在 PCLK 下降沿改变,所以 FPGA 在 PCLK 上升沿采样;
- HREF 高电平期间数据有效;
- VSYNC 高电平是一帧的同步脉冲,低电平期间输出有效数据(帧同步下降沿之后就是第一行数据)。
带宽算一笔账:640×480×16bit×60fps ≈ 281.25Mbps,远低于千兆网实际可用速率,所以不需要片外存储器缓存,FIFO 就够用——这也是整个实验架构能做得很简洁的原因。
50.4 彩条测试与补光灯
两个实用小功能:
- 彩条模式:寄存器 0x503d 写 0x80 打开、0x00 关闭。摄像头花屏时先开彩条,能快速判断是采集问题还是摄像头问题;
- LED 补光灯:0x3016=0x02、0x301c=0x02 使能,0x3019=0x02 开灯/0x00 关灯。
50.4 程序设计:五大模块协作
总体架构

- i2c_cfg + i2c_dri:上电后延时 20ms,然后按 250 项配置表初始化摄像头(复用 EEPROM 实验的 I2C 驱动);
- start_transfer_ctrl:解析 UDP 收到的命令,收到 "1" 开始传输、"0" 停止;
- img_data_pkt:图像数据封装——加帧头、行场分辨率,用异步 FIFO 跨时钟域缓存,凑够一包就触发 UDP 发送;
- eth_top:第 49 章的 UDP 以太网顶层,原样复用。
i2c_cfg:250 个寄存器的"流水账"
配置模块的核心是一个巨大的 case 表,每项 24 位 = 16 位地址 + 8 位数据。控制逻辑三步走:
//1. 上电延时20ms(OV5640要求)
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
start_init_cnt <= 15'd0;
else if(start_init_cnt < 15'd20000)
start_init_cnt <= start_init_cnt + 15'd1;
end
//2. 每次i2c_done后触发下一个寄存器配置
else if(i2c_done && (init_reg_cnt < REG_NUM))
i2c_exec <= 1'b1;
//3. 全部配置完拉高init_done
关键的配置项挑几个说:
8'd0 : i2c_data <= {16'h300a,8'h00}; //先读OV5640 ID(验证通信)
8'd2 : i2c_data <= {16'h3008,8'h82}; //Bit[7]=1:软件复位
8'd3 : i2c_data <= {16'h3008,8'h02}; //正常工作模式
8'd54 : i2c_data <= {16'h4300,8'h61}; //格式控制: RGB565
8'd220: i2c_data <= {16'h3808,{4'd0,cmos_h_pixel[11:8]}}; //输出水平像素
8'd221: i2c_data <= {16'h3809,cmos_h_pixel[7:0]};
8'd245: i2c_data <= {16'h503d,8'h00}; //8'h80:彩条显示 8'h00:正常模式
注意软件复位后要再等 20ms 才能配其他寄存器。分辨率相关寄存器(0x3808~0x380f)不写死,而是由顶层参数传入——想改分辨率只改参数就行,这就是拓展训练"改成 800×480"的入口。
start_transfer_ctrl:一个字节的开关
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
transfer_flag <= 1'b0;
else if(udp_rec_pkt_done && udp_rec_byte_num == 1'b1) begin //只认单字节命令
if(udp_rec_data == START) //"1"
transfer_flag <= 1'b1;
else if(udp_rec_data == STOP) //"0"
transfer_flag <= 1'b0;
end
end
udp_rec_byte_num == 1'b1 这个条件很重要:只响应恰好 1 字节的包,避免图像数据里碰巧出现 "1" 被误当命令。
img_data_pkt:跨时钟域 + 帧头设计(本章精华)
这个模块横跨两个时钟域:cam_pclk(摄像头像素时钟,写侧)和 eth_tx_clk(以太网发送时钟,读侧),中间用 2048×8bit 异步 FIFO 缓存。
帧头设计:UDP 会丢包(电脑忙时来不及收),如果不加处理,丢包后的画面会错位。解决办法是每帧图像的第一行前加 8 字节:
帧头(4字节) + 水平分辨率(2字节) + 垂直分辨率(2字节)
= 32'hf0_5a_a5_0f + 16'd640 + 16'd480
上位机解析到帧头就把显示指针归位,即使中途丢包也能在下帧恢复。帧头值选 f0_5a_a5_0f 是因为它与真实像素数据撞车的概率极低,且不可随意修改(上位机按此解析)。
写入逻辑利用 VSYNC 下降沿识别"第一行":
if(head_flag) begin //帧头标志有效
if(head_cnt == 4'd0)
wr_fifo_data <= IMG_FRAME_HEAD[31:24]; //帧头4字节
...
else if(head_cnt == 4'd7)
wr_fifo_data <= CMOS_V_PIXEL[ 7: 0]; //垂直分辨率
end
else if(img_data_en_d0) begin
wr_fifo_en <= 1'b1;
wr_fifo_data <= img_data_d0; //正常图像数据
end
发送字节量的动态切换也值得注意:第一行发 640×2+8=1288 字节,其余行发 1280 字节。
跨时钟域采沿:neg_vsync 在 cam_pclk 域产生,不能直接给 eth_tx_clk 域用,所以 img_vsync 在发送时钟域单独再打三拍采沿——这是标准的跨时钟域处理手法。
FIFO 复位的小心思:
async_fifo_2048x8b async_fifo_2048x8b_inst (
.rst (pos_vsync | (~transfer_flag) ), //帧同步上升沿或停止命令都清空FIFO
.wr_clk(cam_pclk ),
.rd_clk(eth_tx_clk),
...
);
视频传输中途点"停止",FIFO 里残留的数据会被清空,避免下一帧开头混入旧数据。
发送触发:FIFO 缓存量 ≥ 待发字节数时拉高 udp_tx_start_en,发送完成后等下一批凑够再发,周而复始。
50.5 下载验证
摄像头管脚(以太网管脚沿用前面实验):
| 信号 | 管脚 | 说明 |
|---|---|---|
| cam_pclk | W14 | 像素时钟(约束了 CLOCK_DEDICATED_ROUTE FALSE) |
| cam_data[7:0] | R14/Y14 等 | 8 位数据(加 IOB TRUE 约束) |
| cam_scl/cam_sda | T10/T11 | SCCB 接口 |
验证步骤:
- 确认千兆连接:网络状态必须是 1000Mbps,否则带宽不够,视频无法传输(排查网卡、网线是否支持千兆);
- 电脑 IP 设 192.168.1.102,插入摄像头(镜头朝外,注意与 J4 扩展 IO 复用,别接其他设备);
- 打开上位机 ATK-XCAM.exe(打不开先装 vc_redist.x64.exe),按界面填入开发板 IP 和端口;
- 点"打开"→ 上位机发 "1" → 画面出现实时视频;点"关闭"即停。

如果电脑性能差,显示可能卡顿甚至崩溃——UDP 不保证送达,上位机解析压力也大,这是正常现象。
初学者容易踩的坑
- 网速不是千兆:281Mbps 的图像流量在百兆网(实际 ~90Mbps)下必丢包,很多人卡在这第一步。
- SCCB 读协议写错:OV5640 读寄存器要先"虚写"再读,且没有 restart;直接套 IIC 读时序会读回垃圾数据。
- BIT_CTRL 参数忘设 1:OV5640 寄存器地址是 16 位,I2C 驱动还按 8 位地址模式跑,配置全落空。
- 帧头被修改:上位机按
f0_5a_a5_0f解析帧头,FPGA 侧改成别的值,上位机就再也找不到帧起始了。 - 跨时钟域信号直连:cam_pclk 域的 neg_vsync 直接喂给 eth_tx_clk 域逻辑,亚稳态随机出现,表现为偶发丢帧——必须在目的时钟域重新采沿。
- 上电即配寄存器:OV5640 上电后必须等 20ms 才能配置,软件复位后还要再等 20ms,急着想配置会初始化失败。
- PWDN 引脚悬空:cam_pwdn 必须拉低(正常模式),拉高摄像头就休眠了,怎么配寄存器都没反应。
小结 & 下一步
本章是以太网系列的综合应用大考:
- OV5640 的 SCCB 配置(IIC 兼容但读时序有差异)、三级窗口缩放体系、RGB565 双字节输出时序;
- 帧头 + 分辨率的图像封装协议,用"低碰撞概率帧头 + 指针归位"优雅化解 UDP 丢包问题;
- 双时钟域(cam_pclk / eth_tx_clk)架构下异步 FIFO 的完整应用:缓存、复位时机、凑包触发;
- 完整的"命令—数据"交互设计:上位机发指令控制视频流开关。
玩转了 OV5640,下一章 OV7725 视频传输实验 是它的"简配版"——摄像头不同、架构相通,正好检验你是否真的学会了。