第50章:基于OV5640的PL以太网视频传输实验

前几章我们打通了以太网协议栈,这一章让它干"正经活":把 OV5640 摄像头拍到的画面,通过网线实时传到电脑上显示。这是前面所有知识的综合应用——IIC 配置、FIFO 缓存、UDP 收发,一个都不落。

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

实验任务:开发板采集 OV5640 图像(640×480 @RGB565)→ 通过 PL 千兆网口以 UDP 发送 → 电脑上位机软件实时显示视频。

新增知识块有三块:

  1. OV5640 摄像头:SCCB 配置接口(兼容 IIC)、DVP 输出时序、RGB565 格式;
  2. 图像封装:给每帧图像加帧头,抵御 UDP 丢包;
  3. 控制流设计:上位机发"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 的两个差别要记住:

  1. 写传输:第 9 位是"不必关心位"(IIC 是应答位),基本无感;
  2. 读传输没有重复开始(restart)的概念——先做一次"虚写"(写器件地址+寄存器地址),发停止信号后再发起读操作;且读最后一个字节时主机必须给 NA(高电平)。

OV5640 的器件地址是 7'h3c,寄存器地址是 16 位(两个字节),这是与 EEPROM(8 位地址)的关键区别,I2C 驱动的 BIT_CTRL 参数要设为 1。

50.2 三个"窗口":图像尺寸怎么裁剪

OV5640 的输出尺寸由三级窗口层层决定:

图50.1.4 图像窗口设置

  1. ISP 输入窗口(0x3800~0x3807):从 2632×1951 的物理阵列里圈出进入 ISP 处理的区域;
  2. 预缩放窗口(0x3810~0x3813):在 ISP 窗口内偏移裁剪;
  3. 输出大小窗口(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])有效,一个像素分两次送出:

图50.1.5 RGB565模式时序图

  • 第一个字节 = 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 程序设计:五大模块协作

总体架构

图50.4.1 系统架构图

  • 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 接口

验证步骤:

  1. 确认千兆连接:网络状态必须是 1000Mbps,否则带宽不够,视频无法传输(排查网卡、网线是否支持千兆);
  2. 电脑 IP 设 192.168.1.102,插入摄像头(镜头朝外,注意与 J4 扩展 IO 复用,别接其他设备);
  3. 打开上位机 ATK-XCAM.exe(打不开先装 vc_redist.x64.exe),按界面填入开发板 IP 和端口;
  4. 点"打开"→ 上位机发 "1" → 画面出现实时视频;点"关闭"即停。

图50.5.7 上位机实时显示画面

如果电脑性能差,显示可能卡顿甚至崩溃——UDP 不保证送达,上位机解析压力也大,这是正常现象。

初学者容易踩的坑

  1. 网速不是千兆:281Mbps 的图像流量在百兆网(实际 ~90Mbps)下必丢包,很多人卡在这第一步。
  2. SCCB 读协议写错:OV5640 读寄存器要先"虚写"再读,且没有 restart;直接套 IIC 读时序会读回垃圾数据。
  3. BIT_CTRL 参数忘设 1:OV5640 寄存器地址是 16 位,I2C 驱动还按 8 位地址模式跑,配置全落空。
  4. 帧头被修改:上位机按 f0_5a_a5_0f 解析帧头,FPGA 侧改成别的值,上位机就再也找不到帧起始了。
  5. 跨时钟域信号直连:cam_pclk 域的 neg_vsync 直接喂给 eth_tx_clk 域逻辑,亚稳态随机出现,表现为偶发丢帧——必须在目的时钟域重新采沿。
  6. 上电即配寄存器:OV5640 上电后必须等 20ms 才能配置,软件复位后还要再等 20ms,急着想配置会初始化失败。
  7. PWDN 引脚悬空:cam_pwdn 必须拉低(正常模式),拉高摄像头就休眠了,怎么配寄存器都没反应。

小结 & 下一步

本章是以太网系列的综合应用大考:

  • OV5640 的 SCCB 配置(IIC 兼容但读时序有差异)、三级窗口缩放体系、RGB565 双字节输出时序;
  • 帧头 + 分辨率的图像封装协议,用"低碰撞概率帧头 + 指针归位"优雅化解 UDP 丢包问题;
  • 双时钟域(cam_pclk / eth_tx_clk)架构下异步 FIFO 的完整应用:缓存、复位时机、凑包触发;
  • 完整的"命令—数据"交互设计:上位机发指令控制视频流开关。

玩转了 OV5640,下一章 OV7725 视频传输实验 是它的"简配版"——摄像头不同、架构相通,正好检验你是否真的学会了。