第五十一章:基于OV7725的PL以太网视频传输实验(初学者详解)
本章在讲什么?(先看这个)
上一章我们用 OV5640 摄像头做了以太网视频传输,这一章把摄像头换成 OV7725,其余部分几乎原封不动。整件事的流程是:
- FPGA 通过 SCCB 总线(兼容 IIC)配置 OV7725 的寄存器,让它输出 640×480 的 RGB565 图像;
- 图像数据进入封装模块,加上帧头后通过以太网顶层模块用 UDP 发出去;
- PC 上的上位机软件收到 UDP 数据后实时显示画面。
你可能会问:刚学完 OV5640 为什么又要学 OV7725?因为这两个传感器是 OmniVision(豪威)家最经典的两个型号,定位完全不同:
| 对比项 | OV7725 | OV5640 |
|---|---|---|
| 像素 | 约 30 万(VGA 640×480) | 约 500 万 |
| 帧率 | 最高 60fps(低分辨率高帧率) | 高分辨率下约 15fps |
| 特点 | 成本低、速度快、实时性好 | 画质好、功能全 |
| 典型用途 | 对实时性要求高、画质要求不高的场合 | 高清拍照、视频设备 |
所以 OV7725 实验的价值在于:用最简单的摄像头,把"采集 → 打包 → 以太网发送"这条链路跑通。而且本章的工程结构、模块划分和上一章完全一致,唯一需要重写的就是 I2C 配置模块(i2c_cfg)——这正好检验你是不是真的理解了上一章的内容,还是只是照抄了一遍。
核心概念拆解
1. OV7725 是怎么工作的
先看功能框图:

数据流向很好理解:
- 感光阵列(image array,640×480)在 XCLK 时钟驱动下采样,输出模拟信号;
- 模拟信号处理器在时序发生器控制下做算法处理,分成 G(绿)和 R/B(红/蓝)两路;
- 经过 AD 转换器变成数字信号,交给 DSP 做图像处理;
- 最终输出所配置格式的 10 位视频数据流。
模拟处理和 DSP 的行为都由内部寄存器控制,而配置寄存器的接口就是 SCCB 接口——这就是我们下一小节的主角。
2. SCCB 协议:和 IIC 像是双胞胎,但有两处关键差异
SCCB(Serial Camera Control Bus)是 OV 公司定义的串行摄像头控制总线,两线制:SIO_C(时钟)和 SIO_D(数据),分别对应 IIC 的 SCL 和 SDA。
写传输协议:

结构是:ID Address(W)→ Sub-address(寄存器地址)→ Write Data,三个 Phase。和 IIC 写协议几乎一样,但注意第 9 位:
- IIC 里第 9 位是应答位(ACK),主机要检查;
- SCCB 里第 9 位是 Don't Care(X)——从机可能应答也可能不应答,主机不判断,直接默认传输完成。
这是第一个差异。OV7725 的 7 位器件地址是 7'h21,所以写地址 ID Address(W) = 8'h42(器件地址左移 1 位,低位补 0),读地址 ID Address(R) = 8'h43(低位补 1)。
读传输协议:

这是第二个差异,也是最容易翻车的地方:
- IIC 读寄存器时,写完寄存器地址后会发一个 Repeated Start(重复起始信号) 再转读;
- SCCB 没有 Repeated Start 的概念。读操作分两段:先做一次"虚写"(写器件地址+寄存器地址,让地址指针指到位),然后发一个 Stop 停止总线,再重新 Start 发读地址读数据;
- 最后一位由主机发 NA(高电平),因为 SCCB 不支持连续读。
好消息是:本章只用 SCCB 的写操作来配置寄存器,写协议和 IIC 几乎一样,所以可以直接复用"EEPROM 读写实验"里的 i2c_dri 驱动模块,一行都不用改。
3. 本实验用到的关键寄存器
OV7725 有 0x00~0xAC 共 173 个寄存器(8 位地址、8 位数据——记住这一点,后面配置模块要用),没必要全配,关键的有这几个:
| 地址 | 寄存器 | 作用 |
|---|---|---|
| 0x12 | COM7 | Bit[7]:软件复位;Bit[6]:0=VGA/1=QVGA;Bit[3:2]:RGB 格式(01=RGB565);Bit[1:0]:输出格式(10=RGB) |
| 0x0C | COM3 | Bit[0]:彩条测试使能 |
| 0x0D | COM4 | Bit[7:6]:PLL 倍频(00 直通/01 4x/10 6x/11 8x) |
| 0x11 | CLKRC | 内部时钟分频,F = 输入时钟 × PLL倍频 / [(CLKRC[5:0]+1)×2] |
| 0x13 | COM8 | Bit[2]/Bit[1]/Bit[0]:AGC/AWB/AEC 自动功能使能 |
| 0x15 | COM10 | 各信号的极性翻转、PCLK 输出选项等 |
两个实用技巧:
- 调试神器——彩条模式:把 COM3 的 Bit[0] 置 1,摄像头不拍真实图像,输出固定的彩条。如果彩条能正常显示,说明时序和传输链路没问题,问题出在光学/曝光部分。
- 分辨率怎么配:OV7725 支持 VGA(640×480)、QVGA(320×240)、CIF(352×288)以及任意尺寸,由 0x12(COM7)、0x17~0x1A(HSTART/HSIZE/VSTRT/VSIZE)、0x32(HREF)等一组寄存器共同决定。
4. 上电时序与关键时间参数

从数据手册特性图里能提炼出几个必须遵守的时间约束:
- 输入时钟 XCLK 范围:10MHz ~ 48MHz;
- SCCB 时钟 SIO_C 最大 400KHz;
- 软件复位(寄存器 0x12 Bit[7])或硬件复位(cam_rst_n 引脚)后,要等待最大 1ms 才能配置其它寄存器;
- 每次配置完寄存器后,需要最大 300ms(约 10 帧图像输出时间)才能输出稳定的视频流。
这几个时间在后面写配置模块时全都要体现出来,漏了任何一个都可能"明明代码没问题,画面就是出不来"。
5. VGA 帧时序:算清楚一帧到底有多久

几个基本概念先对齐:
- VSYNC:场同步信号,上升沿是一帧的开始;
- HREF:行有效信号(本实验用 HREF 格式,不是 HSYNC),高电平期间输出有效像素;
- D[9:0]:数据线,RGB 格式下只有高 8 位 D[9:2] 有效;
- tp 与 tPCLK 的关系:这是本节最大的坑——RGB 格式下一个数据周期 tp = 两个像素时钟 tPCLK。因为 RGB565 一个像素 16 位,而 OV7725 每个像素周期只能传 8 位,所以要两个时钟才吐出一个完整像素。
一行数据的构成:tLine = 784tp = 640tp(有效)+ 144tp(消隐)。
一帧的构成:tFrame = (4 + 20 + 480 + 6) × tLine = 510 tLine,即 VSYNC 同步脉冲 4 行 + 等待 20 行 + 480 行有效数据 + 尾部等待 6 行。
拿这个公式算帧率,以 PCLK = 25MHz(周期 40ns)为例:
一帧时间 = 510 × 784tp × 2tPCLK = 799680 × 40ns ≈ 31.99ms
帧率 ≈ 1000ms / 31.99ms ≈ 31Hz
如果把 PCLK 提到最大的 48MHz,帧率约 60Hz——这就是 OV7725 "低分辨率高帧率"的由来。
RGB565 数据怎么采样:数据在 PCLK 下降沿改变,所以 FPGA 要在 PCLK 上升沿采集,这是最稳定的采样点。HREF 为高时,先输出 RGB565 的高 8 位(first byte),再输出低 8 位(second byte),两拍拼成一个 16 位像素。
6. 硬件设计的唯一差异:SGM_CTRL 引脚
硬件电路和 OV5640 实验基本相同,唯一要注意的是摄像头扩展口第 18 脚:
- OV5640 实验中这个脚输出
cam_sgm_ctrl,用来控制摄像头休眠; - OV7725 模块上这个脚是 SGM_CTRL(时钟选择):低电平用外部时钟(FPGA 要输出 XCLK),高电平用模块自带的晶振(FPGA 不用给时钟)。
本实验把它固定为 1'b1,直接用摄像头板载晶振,省掉一路时钟输出。同时 cam_rst_n 固定为 1'b1,不做硬件复位。
7. 系统架构:只换一颗"心脏"
整体架构和 OV5640 实验完全一致:

各模块一句话回顾(详细设计都在前几章讲过):
| 模块 | 职责 | 来源章节 |
|---|---|---|
| clk_wiz_0 | MMCM 输出 50MHz(IIC 驱动时钟)+ 200MHz(IDELAYCTRL 参考) | 时钟章节 |
| i2c_dri | IIC/SCCB 底层驱动,SCL 250KHz | EEPROM 读写实验 |
| i2c_cfg | OV7725 寄存器配置表——本章唯一的新模块 | 本章 |
| start_transfer_ctrl | 解析上位机发来的 ASCII "1"/"0",控制传输开始/停止 | OV5640 视频传输实验 |
| img_data_pkt | 图像封装:加帧头 + 行场分辨率,内部 FIFO 跨时钟域 | OV5640 视频传输实验 |
| eth_top | 以太网收发(ARP/ICMP/UDP),RGMII 接口 | 第 47~49 章 |
总体模块框图:

8. I2C 配置模块:本章唯一要写的新代码
模块接口很简单:输入 clk(IIC 驱动时钟)、rst_n、i2c_done,输出 i2c_exec(触发执行)、i2c_data(高 8 位地址 + 低 8 位数据)、init_done(初始化完成)。
工作节奏是一个"发指令 → 等 i2c_done → 发下一条"的循环,核心代码看三段:
(1)上电延时 + 软复位后延时,共用一个计数器:
//cam_scl 配置成250khz,输入的clk 为1Mhz,周期为1us,1023*1us = 1.023ms
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
start_init_cnt <= 10'b0;
else if((init_reg_cnt == 7'd1) && i2c_done)
start_init_cnt <= 10'b0; //软复位完成,计数器清零重新计
else if(start_init_cnt < 10'd1023)
start_init_cnt <= start_init_cnt + 1'b1;
end
计数到 1023(约 1.023ms)才发下一条命令。这个计数器干了两件事:上电后先等它计满再开始配置(等电源稳定),以及软复位命令之后清零重计,满足"复位后等 1ms"的数据手册要求。
(2)触发执行信号:
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
i2c_exec <= 1'b0;
else if(start_init_cnt == 10'd1022)
i2c_exec <= 1'b1; //计数将满,触发第一次配置(软复位)
//只有刚上电和配置第一个寄存器增加延时
else if(i2c_done && (init_reg_cnt != 7'd1) && (init_reg_cnt < REG_NUM))
i2c_exec <= 1'b1; //其余寄存器:上一步完成立刻触发下一步
else
i2c_exec <= 1'b0;
end
注意条件里的 init_reg_cnt != 7'd1:第 1 个寄存器(软复位)配完后要多等 1ms,其余寄存器配完直接连发。
(3)寄存器配置表:
case(init_reg_cnt)
7'd0 : i2c_data <= {8'h12, 8'h80}; //COM7 BIT[7]:复位所有的寄存器
...
7'd12 : i2c_data <= {8'h0d, 8'h41}; //COM4 PLL 4倍频
7'd13 : i2c_data <= {8'h11, 8'h00}; //CLKRC 内部时钟配置
//Freq=multiplier/[(CLKRC[5:0]+1)*2]
7'd14 : i2c_data <= {8'h12, 8'h06}; //COM7 输出VGA RGB565 格式
...
default:i2c_data <= {8'h1C, 8'h7F}; //MIDH 制造商ID(只读,占位防重复改写)
endcase
三个值得咀嚼的细节:
- 时钟怎么算的:输入时钟 12MHz,COM4 配 4 倍频,CLKRC 配 0x00,代入公式
12MHz × 4 / [(0+1)×2] = 24MHz——所以 PCLK 是 24MHz,帧率大约 31Hz(代入第 5 小节的公式可以自己验一遍); - 第 0 项必须是软复位
{8'h12, 8'h80},让所有寄存器回到默认值再配置; - default 分支写成只读寄存器 MIDH:这是一种防御性写法——如果
init_reg_cnt意外跑飞,也不会重复改写已配置的寄存器。
另外,parameter REG_NUM = 7'd70 定义了配置总数,增删寄存器时必须同步修改它,否则 init_done 永远不会拉高。
9. 顶层模块:参数集中管理
顶层 ov7725_udp_pc 就是把各模块例化连起来,值得看的是参数区:
parameter BOARD_MAC = 48'h00_11_22_33_44_55; //开发板MAC
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10}; //开发板IP 192.168.1.10
parameter DES_MAC_DEFAULT = 48'hff_ff_ff_ff_ff_ff; //目的MAC(广播)
parameter DES_IP_DEFAULT = {8'd192,8'd168,8'd1,8'd102};//PC IP 192.168.1.102
parameter SLAVE_ADDR = 7'h21 ; //OV7725 器件地址
parameter BIT_CTRL = 1'b0 ; //寄存器地址8位(OV5640是16位,为1)
parameter CLK_FREQ = 26'd50_000_000 ; //i2c_dri 驱动时钟
parameter I2C_FREQ = 18'd250_000 ; //SCL 250KHz < 400KHz
四个 MAC/IP 参数集中定义、向下传递,换网络环境时只改这里即可。BIT_CTRL=0 是 OV7725 与 OV5640 在驱动层的关键区别:OV7725 寄存器地址是 8 位,OV5640 是 16 位。
i2c_dri 例化时 i2c_rh_wl 固定接 1'b0(只写不读),i2c_addr 取 i2c_data[15:8],i2c_data_w 取 i2c_data[7:0]。
初学者容易踩的坑
-
把 SCCB 读操作当成 IIC 来写:SCCB 没有 Repeated Start,读寄存器必须"虚写 → Stop → Start → 读",最后还要发 NA。本章只写不读所以没暴露,但以后想读摄像头 ID 时千万别套用 IIC 的写法。
-
软复位后不等 1ms 就配寄存器:数据手册明确要求复位后等待最大 1ms。很多"配置了但画面出不来"的玄学问题都是这个时间没留够。本章用
start_init_cnt计数 1023 个 1μs ≈ 1.023ms 来保证。 -
混淆 tp 和 tPCLK:RGB 格式下一个数据周期 tp 等于两个像素时钟。算行时间、帧时间时用错单位,帧率会差一倍,还会误判 PCLK 是否超频。
-
增删了配置表却忘了改 REG_NUM:
init_done的拉高条件是init_reg_cnt == REG_NUM && i2c_done,数量对不上初始化就永远"完不成",后面的逻辑可能一直等不到。 -
SGM_CTRL 电平接错导致没时钟:OV7725 模块的 SGM_CTRL 为低时用外部时钟(FPGA 必须输出 XCLK),为高时用板载晶振。如果照抄 OV5640 实验把引脚当休眠控制用,或者忘了拉高,摄像头可能根本不工作。
-
网口速率不是千兆:上位机收不到图像时先查 Windows 网络状态里的连接速度——必须是 1000Mbps。主机网卡、网线、交换机任何一环不满足千兆都不行,这与前面以太网章节的要求一致。
-
上位机卡顿误以为是 FPGA 问题:上位机显示性能受电脑影响很大,配置低的电脑可能出现卡顿甚至崩溃,这不代表开发板侧的代码有问题。
小结 & 下一步
本章用最小代价完成了一次"换传感器"的实战:SCCB 协议兼容 IIC 写操作,所以 i2c_dri 驱动原封不动;OV7725 寄存器地址 8 位(BIT_CTRL=0)、器件地址 7'h21,只需重写配置表 i2c_cfg;再加上"软复位后等 1ms"的上电时序处理,整条"摄像头 → UDP → 上位机"链路就跑通了。核心收获是三点:SCCB 与 IIC 的两处差异(Don't Care 位、无 Repeated Start)、帧率与 tp/tPCLK 的计算方法、寄存器配置模块的状态节奏(发 → 等 done → 发下一条)。
至此,摄像头的静态图像/视频传输两条路都走通了。下一章我们换一个完全不同的方向——学习高速 AD/DA,做一个频谱分析仪,看看 FPGA 是怎么玩转高频模拟信号的。