第五十一章:基于OV7725的PL以太网视频传输实验(初学者详解)

本章在讲什么?(先看这个)

上一章我们用 OV5640 摄像头做了以太网视频传输,这一章把摄像头换成 OV7725,其余部分几乎原封不动。整件事的流程是:

  1. FPGA 通过 SCCB 总线(兼容 IIC)配置 OV7725 的寄存器,让它输出 640×480 的 RGB565 图像;
  2. 图像数据进入封装模块,加上帧头后通过以太网顶层模块用 UDP 发出去;
  3. PC 上的上位机软件收到 UDP 数据后实时显示画面。

你可能会问:刚学完 OV5640 为什么又要学 OV7725?因为这两个传感器是 OmniVision(豪威)家最经典的两个型号,定位完全不同:

对比项 OV7725 OV5640
像素 约 30 万(VGA 640×480) 约 500 万
帧率 最高 60fps(低分辨率高帧率) 高分辨率下约 15fps
特点 成本低、速度快、实时性好 画质好、功能全
典型用途 对实时性要求高、画质要求不高的场合 高清拍照、视频设备

所以 OV7725 实验的价值在于:用最简单的摄像头,把"采集 → 打包 → 以太网发送"这条链路跑通。而且本章的工程结构、模块划分和上一章完全一致,唯一需要重写的就是 I2C 配置模块(i2c_cfg)——这正好检验你是不是真的理解了上一章的内容,还是只是照抄了一遍。

核心概念拆解

1. OV7725 是怎么工作的

先看功能框图:

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。

写传输协议:

SCCB 写传输协议

结构是: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)。

读传输协议:

SCCB 读传输协议

这是第二个差异,也是最容易翻车的地方:

  • 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. 上电时序与关键时间参数

OV7725 的特性

从数据手册特性图里能提炼出几个必须遵守的时间约束:

  • 输入时钟 XCLK 范围:10MHz ~ 48MHz
  • SCCB 时钟 SIO_C 最大 400KHz
  • 软件复位(寄存器 0x12 Bit[7])或硬件复位(cam_rst_n 引脚)后,要等待最大 1ms 才能配置其它寄存器;
  • 每次配置完寄存器后,需要最大 300ms(约 10 帧图像输出时间)才能输出稳定的视频流。

这几个时间在后面写配置模块时全都要体现出来,漏了任何一个都可能"明明代码没问题,画面就是出不来"。

5. VGA 帧时序:算清楚一帧到底有多久

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_ni2c_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_addri2c_data[15:8]i2c_data_wi2c_data[7:0]

初学者容易踩的坑

  1. 把 SCCB 读操作当成 IIC 来写:SCCB 没有 Repeated Start,读寄存器必须"虚写 → Stop → Start → 读",最后还要发 NA。本章只写不读所以没暴露,但以后想读摄像头 ID 时千万别套用 IIC 的写法。

  2. 软复位后不等 1ms 就配寄存器:数据手册明确要求复位后等待最大 1ms。很多"配置了但画面出不来"的玄学问题都是这个时间没留够。本章用 start_init_cnt 计数 1023 个 1μs ≈ 1.023ms 来保证。

  3. 混淆 tp 和 tPCLK:RGB 格式下一个数据周期 tp 等于两个像素时钟。算行时间、帧时间时用错单位,帧率会差一倍,还会误判 PCLK 是否超频。

  4. 增删了配置表却忘了改 REG_NUMinit_done 的拉高条件是 init_reg_cnt == REG_NUM && i2c_done,数量对不上初始化就永远"完不成",后面的逻辑可能一直等不到。

  5. SGM_CTRL 电平接错导致没时钟:OV7725 模块的 SGM_CTRL 为低时用外部时钟(FPGA 必须输出 XCLK),为高时用板载晶振。如果照抄 OV5640 实验把引脚当休眠控制用,或者忘了拉高,摄像头可能根本不工作。

  6. 网口速率不是千兆:上位机收不到图像时先查 Windows 网络状态里的连接速度——必须是 1000Mbps。主机网卡、网线、交换机任何一环不满足千兆都不行,这与前面以太网章节的要求一致。

  7. 上位机卡顿误以为是 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 是怎么玩转高频模拟信号的。