FPGA驱动OV5640视频采集与以太网传输:Verilog实现要点解析
简介这套FPGA视频采集与以太网传输工程资源面向FPGA学习者和嵌入式开发工程师聚焦Xilinx XC6SLX16芯片驱动OV5640摄像头完成视频采集并用Verilog HDL实现从图像缓存、色彩空间转换到以太网帧发送的完整链路可解决实时视频数据硬件传输难、工程搭建繁琐的问题。压缩包约11.85MB共952个文件除Verilog/VHDL源文件外还包含ISE工程文件、UCF/XDC约束文件、Tcl/Shell/Do脚本、PDF说明文档以及FIFO、DCM等常用IP核配置便于整体还原和二次开发。目前已有256人学习下载适合具备一定数字电路与Verilog基础的读者作为FPGA视频处理方向的项目参考。资源内部模块划分清晰涉及摄像头参数初始化、图像数据FIFO缓存、数据格式转换、以太网UDP封装等关键环节可帮助快速搭建同类视频采集传输系统并理解从RTL设计、仿真综合到下载调试的完整流程。整体来说这类视频FPGA应用工程对硬件调试和逻辑设计能力的提升很有帮助。1. FPGA XC6SLX16 驱动 OV5640 视频采集再走以太网真正难的不是协议FPGA XC6SLX16 驱动 OV5640 摄像头采集视频实现以太网视频传输Verilog HDL 实现这个标题看起来像课程设计但把它拆开是一条很典型的低成本视频采集链路。OV5640 输出 DVP 并行数据XC6SLX16 用不到两万逻辑单元把像素收下来再组 UDP 包从 RGMII 口发给 PC。实际跑过这个方案的人会知道调试时间大多不在 Verilog 语法而是集中在三处OV5640 初始化表是否完整、DVP 的 HREF/PCLK 和像素数据是否对齐、跨时钟域 FIFO 会不会写满或读空。这个设计适合工业相机原型、低分辨率视频传输盒也适合在 Zynq 或 ARM 方案之前做纯 FPGA 图像处理验证。先把这三件事理清楚后面调任何分辨率都不会乱。2. 系统架构与时序OV5640 数据流、带宽计算与 FIFO 缓存2.1 从 OV5640 的 DVP 引脚开始OV5640 同时支持 DVP 和 MIPI 两类输出XC6SLX16 上没有 MIPI CSI-2 硬核所以这里只讨论 DVP。DVP 是一组并行信号FPGA 需要根据 PCLK 在正确的沿采样数据再用 VSYNC 和 HREF 判断帧与行的有效区间。下表是 DVP 模式下最关键的引脚信号方向作用调试时重点看PCLK输入像素时钟由 OV5640 输出是否稳定翻转频率是否符合分辨率VSYNC输入帧同步信号下降沿是否周期出现判断出图节奏HREF输入行有效信号高电平期间 pdata 有效高电平宽度是否接近一行像素数pdata[7:0]输入像素数据与 HREF/ PCLK 是否对齐如果配置成 8 位 DVP输出 RGB565 时每个像素占两个 PCLK。常见做法是先收低字节再收高字节拼成一个 16 bit 像素后写入 FIFO。下面这段 Verilog 代码是采集侧的核心逻辑module ov5640_dvp_capture #( parameter DATA_BITS 8 )( input wire rst_n, input wire pclk, input wire vsync, input wire href, input wire [DATA_BITS-1:0] pdata, output reg [15:0] pixel_out, output reg pixel_valid, output reg frame_start ); reg vsync_d1, vsync_d2; reg [7:0] byte_buf; reg [1:0] byte_cnt; always (posedge pclk or negedge rst_n) begin if (!rst_n) begin vsync_d1 1b0; vsync_d2 1b0; byte_cnt 2d0; pixel_valid 1b0; byte_buf 8d0; frame_start 1b0; end else begin vsync_d1 vsync; vsync_d2 vsync_d1; // 下降沿表示一帧开始 if ({vsync_d1, vsync_d2} 2b10) frame_start 1b1; else frame_start 1b0; // 只在 href 有效时拼像素消隐区直接丢弃 if (href) begin if (byte_cnt 2d0) begin byte_buf pdata; byte_cnt 2d1; pixel_valid 1b0; end else begin pixel_out {byte_buf, pdata}; byte_cnt 2d0; pixel_valid 1b1; end end else begin byte_cnt 2d0; pixel_valid 1b0; end end end endmodule代码里的byte_cnt用来区分 RGB565 的第一个字节和第二个字节。vsync_d1和vsync_d2是为了做边沿检测同时也能滤掉一部分亚稳态。如果 OV5640 配置成 16 位 DVP 模式那么pdata就直接是 16 bit 像素不再需要byte_cnt拼接逻辑。2.2 带宽估算为什么 720p 必须上千兆口很多项目第一版就把 PHY 选成百兆结果 VGA 都传不动。RGB565 不做压缩时带宽等于分辨率乘帧率乘 2 字节。具体数据见下表视频格式RGB565 数据率网口建议QVGA 320x240304.6 MB/s约 37 Mbps百兆可行VGA 640x4803018.4 MB/s约 147 Mbps百兆不够720p 1280x7203055.3 MB/s约 442 Mbps千兆可行1080p 1920x108030124.4 MB/s约 995 Mbps千兆接近极限以太网帧还有前导码、帧间隙和 IP/UDP 头实际有效吞吐要再打 5% 到 8% 折扣。所以 1080p30 的 RGB565 直接发千兆口很容易丢包更稳的做法是把输出降到 720p30或者改成 MJPEG 压缩再传输。这个结论也决定了系统里 FIFO 深度的设计方向。2.3 用异步 FIFO 把 PCLK 域搬到以太网时钟域摄像头 PCLK 和以太网发送时钟不是同源时钟跨时钟域传输不能用普通寄存器组必须用双时钟 FIFO。XC6SLX16 内部只有约 72 KB BRAM放不下一整帧 720p所以不要把它当帧缓存用。不加外部 DDR 时常见做法是配置成行缓存加弹性 FIFO写端速率跟着 PCLK 走读端跟着以太网发送时钟走。只要读端平均消费速度不低于写端生产速度FIFO 就不会被读空。下面是一个典型的异步 FIFO 例化fifo_async #( .DATA_WIDTH (16), .DATA_DEPTH (1024), .WRITE_THRESH (768) ) u_video_fifo ( .wr_clk (pclk), .wr_en (pixel_valid), .wr_data (pixel_out), .wr_full (fifo_full), .rd_clk (eth_tx_clk), .rd_en (fifo_rd_en), .rd_data (fifo_dout), .rd_empty (fifo_empty) );WRITE_THRESH是写端 almost full 阈值建议留下 20% 余量。视频数据是突发的行有效期间写入速率接近峰值行消隐和帧消隐期间写入停止所以阈值设得太满遇到连续两行长突发就会丢像素。3. Verilog HDL 驱动 OV5640SCCB 配置、初始化表与上电时序3.1 SCCB 协议在 Verilog 里的落地SCCB 本质上是简化版 I2COV5640 的 7 位从机地址是 0x21写地址 0x42读地址 0x43。写一个寄存器需要完成START、发送设备地址、等待 ACK、发送 16 位寄存器地址、等待 ACK、发送 8 位数据、等待 ACK、STOP。SCL 高电平期间 SDA 不能变化SCL 低电平时才能切换数据。最容易犯的错误是刚拉低 SCL 就立刻切数据位这样边沿变化太快示波器上看起来没问题但摄像头经常丢 ACK。下面给一个状态机骨架parameter IDLE 4d0, START 4d1; parameter SEND_ADDR 4d2, ACK1 4d3; parameter SEND_REGH 4d4, ACK2 4d5; parameter SEND_REGL 4d6, ACK3 4d7; parameter SEND_DATA 4d8, ACK4 4d9, STOP 4d10; reg [3:0] state, next_state; reg [3:0] bit_cnt; reg [7:0] sda_buf; always (posedge clk) begin if (!rst_n) state IDLE; else state next_state; end always (*) begin next_state state; case (state) IDLE: if (sccb_start) next_state START; START: next_state SEND_ADDR; SEND_ADDR: if (bit_cnt 4d8) next_state ACK1; ACK1: next_state SEND_REGH; // SEND_REGH、SEND_REGL、SEND_DATA 同样用 bit_cnt 计数 ACK4: next_state STOP; STOP: next_state IDLE; endcase endSEND_ADDR、SEND_REGH这些状态里先把要发的字节放入sda_buf再用bit_cnt从最高位开始逐位输出。ACK 状态需要把 SDA 设成高阻输入在第九个 SCL 周期检测 SDA 低电平如果不做 ACK 判断初始化失败时很难定位。3.2 OV5640 初始化表分辨率、输出格式和关键寄存器OV5640 的初始化表很长但真正和输出尺寸强相关的是图像窗口寄存器。配置 1280x720 时常用地址如下寄存器地址作用720p 典型值0x3808输出宽度高 8 位0x050x3809输出宽度低 8 位0x000x380A输出高度高 8 位0x020x380B输出高度低 8 位0xD00x4300输出格式控制按 RGB565 配置在 Verilog 里初始化表通常放在 ROM 或分布式 RAM 中每一条记录是 24 bit高 8 位是寄存器地址中 8 位是寄存器地址低字节这里要注意 SCCB 写入的寄存器地址是 16 位所以实际记录格式是{8h38, 8h08, 8h05}也就是地址高字节、地址低字节、数据。下面这段只演示四条分辨率配置localparam integer INIT_SIZE 4; reg [23:0] init_rom [0:INIT_SIZE-1]; initial begin init_rom[0] 24h380805; // 0x3808 0x05宽度高字节 init_rom[1] 24h380900; // 0x3809 0x00宽度低字节 init_rom[2] 24h380A02; // 0x380A 0x02高度高字节 init_rom[3] 24h380BD0; // 0x380B 0xD0高度低字节 end always (posedge clk) begin if (init_index INIT_SIZE) begin sccb_reg_addr init_rom[init_index][23:8]; sccb_wr_data init_rom[init_index][7:0]; sccb_start 1b1; if (sccb_done) init_index init_index 1b1; end end这段代码只演示了分辨率部分。实际工程里还需要配置 PLL、AEC/AGC、镜像翻转、输出格式等几十个寄存器没有厂商例程时不要按手册一个地址一个地址猜值最好先用驱动板把一份能出图的配置导出来再固化到 ROM 里。3.3 上电时序与复位释放先断电再写寄存器OV5640 要求电源稳定后复位引脚保持低电平一段时间再释放释放后还要再等至少几毫秒才能开始 SCCB 操作。很多板子复位引脚由 FPGA 引脚控制可以用计数器产生延迟reg [15:0] delay_cnt; reg cam_reset_n; always (posedge clk) begin if (delay_cnt 16d50_000) begin cam_reset_n 1b0; delay_cnt delay_cnt 16d1; end else begin cam_reset_n 1b1; end end如果系统时钟是 50 MHz50_000 个周期正好是 1 ms这个时间高于 OV5640 复位低电平最小宽度要求。复位释放后要再等几毫秒再置位sccb_start否则摄像头自身 PLL 还没稳定寄存器写下去没有响应。4. 以太网视频传输UDP 帧组装、RGMII 与三速以太网 MAC4.1 为什么用 UDP 而不是 TCPXC6SLX16 资源有限塞不下完整 TCP 协议栈。视频传输本身允许偶发丢包UDP 无连接、包头短、发送状态机简单是 FPGA 视频传输最常用的选择。只要接收端 PC 用 Wireshark 抓包看到帧和数据长度正常就说明链路已经通了。UDP 的问题是会乱序和丢包所以 PC 端最好在数据里带帧序号接收软件按帧序号丢弃残缺帧。这个帧序号可以直接放在 UDP 载荷前 2 字节FPGA 侧用一个计数器递增。4.2 三速以太网 MAC 和 PHY 接口怎么选XC6SLX16 这类 Spartan-6 器件工程里直接用官方三速以太网 MAC IP 核比自己写 MAC 省掉 CRC、FIFO 和状态机。IP 核挂在 FPGA 内部一侧对外通常是 GMII 或 RGMII。区别如下接口模式时钟频率数据位宽采样方式MII 100M25 MHz4 bit单沿GMII 1000M125 MHz8 bit单沿RGMII 1000M125 MHz4 bitDDR 双沿RGMII 100M25 MHz4 bitDDR 双沿RGMII 只需要 12 根信号线比 GMII 省一半引脚是 XC6SLX16 这类小封装芯片比较合理的选型。外部 PHY 用 RTL8211、KSZ9031 之类都常见FPGA 侧把 RGMII 的 TX_CTL 和 TXD 在时钟上下沿对齐就行。4.3 用固定帧头省掉 ARPUDP 包怎么拼不上操作系统的 FPGA 不需要动态 ARP。PC 网卡地址和 IP 可以预先知道直接把目标 MAC、源 MAC、IP 头、UDP 头做成常量。下面这些常量在项目中通常放在一个单独的头文件里localparam [47:0] DEST_MAC 48h00_11_22_33_44_55; localparam [47:0] SRC_MAC 48hAA_BB_CC_DD_EE_FF; localparam [15:0] ETH_TYPE 16h0800; // IPv4 localparam [31:0] SRC_IP {8d192, 8d168, 8d1, 8d10}; localparam [31:0] DEST_IP {8d192, 8d168, 8d1, 8d100}; localparam [15:0] UDP_SPORT 16d5000; localparam [15:0] UDP_DPORT 16d5000;发送状态机的核心顺序是先发 6 字节目标 MAC再发 6 字节源 MAC再发 0x0800然后 IP 头、UDP 头、视频数据。以太网传输采用大端序也就是一个 48 bit MAC 值要先发最高 8 位。代码片段如下SEND_MAC_DST: begin gmii_tx_en 1b1; gmii_txd DEST_MAC[47 - byte_cnt*8 -: 8]; if (byte_cnt 12d5) begin tx_state SEND_MAC_SRC; byte_cnt 12d0; end else begin byte_cnt byte_cnt 12d1; end endbyte_cnt表示当前已发送的第几个字节47 - byte_cnt*8可以依次取出 MAC 地址的最高字节、次高字节直到最低字节。IP 头和 UDP 头也按同样的方式逐字节发出长度字段根据当前包的有效负载字节数预先算好填入 IP 头的total_length和 UDP 头的length。4.4 IP 头部校验和不把进位算错IP 头校验和必须正确UDP 校验和可以让它等于 0IPv4 协议允许接收端不校验 UDP。IP 头一共 20 字节按 16 bit 一组相加再把高 16 位进位折回低 16 位最后取反。Verilog 里用函数实现比较直观function [15:0] checksum16; input [159:0] hdr; integer i; reg [31:0] acc; begin acc 0; for (i 0; i 10; i i 1) acc acc hdr[i*16 : 16]; while (acc[31:16] ! 0) acc acc[15:0] {16h0, acc[31:16]}; checksum16 ~acc[15:0]; end endfunctionfor循环把 20 字节的 IP 头分成 10 个 16 bit 字累加。while循环是关键每加一次都可能产生进位必须把高 16 位再次加到低 16 位直到没有进位为止。Wireshark 里看到 IP checksum errors基本就是这里少加了进位。5. 上板验证ChipScope 抓采集Wireshark 看包时序约束调一次5.1 Camera 不上流的三个检查点上板后先不要急着看以太网先用 ChipScope 或 ILA 抓 PCLK 和 HREF。第一个检查 PCLK 是否在翻转如果 PCLK 一直不变说明 SCCB 初始化没起来或摄像头没有退出复位第二个看 VSYNC 下降沿间隔是否是预期的帧周期比如 30 fps 应该是 33 ms第三个看 HREF 高电平期间有没有pixel_valid脉冲。如果 PCLK 正常但没有 HREF优先检查寄存器 0x3808/0x380A 是否写成功。5.2 Wireshark/tshark 抓视频流PC 端抓包建议直接用 tshark过滤出目标 UDP 端口再检查帧长度是否稳定tshark -i eth0 -f udp port 5000 -w video.pcap tshark -r video.pcap -Y udp.port5000 -T fields -e ip.src -e frame.len | head -20第一行把 UDP 5000 端口的包存成 pcap。第二行只解析源 IP 和帧长度用来确认 FPGA 发出来的包长度是否和配置一致。如果一包都抓不到先用ethtool eth0看网卡是否协商上千兆再看 PHY 的 link 灯。5.3 set_input_delay 与 PCLK 约束OV5640 的 PCLK 是从摄像头端输出的时钟FPGA 需要约束数据相对 PCLK 的建立保持时间。Vivado 工程里通常这样写create_clock -name clk_pix -period 13.468 [get_ports ov5640_pclk] set_input_delay -clock clk_pix -max 6.0 [get_ports {ov5640_pdata[*]}] set_input_delay -clock clk_pix -min 2.0 [get_ports {ov5640_pdata[*]}]13.468 ns 对应约 74.25 MHz 的像素时钟。max和min由 OV5640 手册里的输出建立保持时间加 PCB 走线延迟估算不要照搬同一组值如果 timing report 里 setup 违例就把 max 调大hold 违例就调 min。ISE 工程里对应写法是 UCF 的OFFSET IN ...但参数思考方式一样。在 Wireshark 里看到大量短包、长度忽大忽小时先不要怀疑 CRC去确认帧头里 MAC 地址的字节顺序是否反过来。本文还有配套的精品资源点击获取