4. 图像数据流解析:MIPI/DVP接口时序与像素数据格式

图像数据流解析,说白了就是搞懂摄像头送出来的数据长什么样、什么时候来、怎么接住它。我刚开始做这个的时候,觉得不就是几根线嘛,结果一上板,满屏雪花点,折腾了两天才发现是同步信号没对齐。嗯,这章咱们就把这块彻底讲透。

4.1 两种主流接口:MIPI 与 DVP

摄像头和FPGA之间通信,目前最常见的就是两种方式:DVP(并行接口)和MIPI(串行接口)。

  • DVP接口:说白了就是“一把线”同时传。数据线8位、10位或12位,加上行同步(HSYNC)、场同步(VSYNC)和像素时钟(PCLK)。优点是简单,逻辑分析仪一抓就能看明白。缺点是线多,频率高了容易串扰。
  • MIPI接口:差分对串行传输,通常1~4对数据lane加一对时钟lane。优点是高速、抗干扰,现在手机摄像头基本都是MIPI。缺点是需要专门的DPHY IP核,调试起来比DVP麻烦不少。

我个人习惯:如果项目对帧率要求不高(比如30fps以下),优先选DVP接口的摄像头,调试周期能缩短一半。高速场景(比如1080P@60fps以上)才上MIPI。

4.2 DVP接口时序解析

DVP的时序其实很直观。我画个图你就明白了:

DVP接口时序示意图 PCLK VSYNC 帧有效(高电平) HSYNC 行有效 行有效 行有效 DATA[7:0] 像素0 像素1 像素2 像素3 像素4 每个PCLK上升沿采样一个像素 PCLK VSYNC HSYNC DATA

你看这个图,VSYNC拉高表示一帧开始,HSYNC拉高表示一行开始。在HSYNC高电平期间,每个PCLK上升沿数据线上就有一个像素。就这么简单。

避坑指南:我曾经遇到过一款摄像头,它的VSYNC是低电平有效。当时没仔细看数据手册,直接按高有效写代码,结果图像全是乱的。所以拿到新摄像头,第一件事就是确认同步信号的极性。

4.3 MIPI接口时序要点

MIPI比DVP复杂不少。它有两种工作模式:高速模式(HS)和低功耗模式(LP)。数据传输走HS模式,控制走LP模式。

MIPI的一帧数据长这样:

  • 帧起始(FS):一个特殊的包,告诉接收端新帧开始了
  • 行数据包:每行一个包,包含行起始码、像素数据和ECC校验
  • 帧结束(FE):收尾包

每个数据包的结构是:

字段 长度 说明
SoT(起始传输) 固定序列 用于接收端同步
数据标识符(DI) 1字节 包含虚拟通道号和数据类型
字计数(WC) 2字节 有效数据字节数
有效数据 WC字节 像素数据
ECC 1字节 纠错码
EoT(结束传输) 固定序列 包结束

注意:MIPI的物理层对PCB布线要求很高。差分对要等长,阻抗要控制100欧姆。我见过一个项目,MIPI线长了5mm没等长,结果图像偶尔出现条纹,查了三天才找到原因。

4.4 像素数据格式:RAW RGB 与 YUV

摄像头送出来的像素数据,常见的有两种格式:RAW RGB和YUV。你想想看,这俩有啥区别?

4.4.1 RAW RGB格式

RAW RGB就是传感器直接输出的原始数据。每个像素只记录一种颜色分量(R、G或B),通过Bayer阵列排列。常见的格式有:

  • RAW8:每个像素8位,一个像素一个字节
  • RAW10:每个像素10位,4个像素打包成5个字节
  • RAW12:每个像素12位,2个像素打包成3个字节

Bayer阵列的排列方式有四种:BGGR、GBRG、GRBG、RGGB。这个顺序搞错了,图像颜色就全乱了。

我建议:调试阶段先用RAW8格式,数据量小,逻辑分析仪抓起来方便。等接口调通了,再切到RAW10或RAW12提高画质。

4.4.2 YUV格式

YUV把亮度(Y)和色度(U、V)分开。人眼对亮度敏感、对色度不敏感,所以色度可以压缩。常见的YUV格式有:

格式 采样方式 每像素位数
YUV422 每2个像素共享U、V 16位
YUV420 每4个像素共享U、V 12位
YUV444 每个像素独立U、V 24位

YUV422在嵌入式系统中用得最多。数据排列通常是Y0 U0 Y1 V0 Y2 U1 Y3 V1... 说白了就是亮度连续,色度隔一个采一个。

4.5 编写数据捕获模块

好了,理论说完了,咱们写代码。下面是一个DVP接口的数据捕获模块,Verilog写的:

module dvp_capture (
    input  wire        pclk,      // 像素时钟
    input  wire        rst_n,     // 复位
    input  wire        vsync,     // 场同步
    input  wire        hsync,     // 行同步
    input  wire [7:0]  data_in,   // 像素数据
    output reg  [7:0]  pixel_out, // 输出像素
    output reg         pixel_vld, // 像素有效
    output reg         frame_vld  // 帧有效
);

    // 边沿检测
    reg vsync_d1, vsync_d2;
    reg hsync_d1;
    
    always @(posedge pclk or negedge rst_n) begin
        if (!rst_n) begin
            vsync_d1 <= 1'b0;
            vsync_d2 <= 1'b0;
            hsync_d1 <= 1'b0;
        end else begin
            vsync_d1 <= vsync;
            vsync_d2 <= vsync_d1;
            hsync_d1 <= hsync;
        end
    end
    
    // 帧开始/结束检测
    wire vsync_pos = vsync_d1 & !vsync_d2;  // 上升沿
    wire vsync_neg = !vsync_d1 & vsync_d2;  // 下降沿
    
    // 行有效信号
    wire hsync_pos = hsync_d1 & !hsync;     // 上升沿
    wire line_active = hsync;                // 行有效期间
    
    // 帧状态机
    reg [1:0] state;
    localparam IDLE   = 2'b00;
    localparam FRAME  = 2'b01;
    
    always @(posedge pclk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
            frame_vld <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    frame_vld <= 1'b0;
                    if (vsync_pos) state <= FRAME;
                end
                FRAME: begin
                    frame_vld <= 1'b1;
                    if (vsync_neg) state <= IDLE;
                end
            endcase
        end
    end
    
    // 像素捕获
    always @(posedge pclk or negedge rst_n) begin
        if (!rst_n) begin
            pixel_out <= 8'd0;
            pixel_vld <= 1'b0;
        end else begin
            if (frame_vld && line_active) begin
                pixel_out <= data_in;
                pixel_vld <= 1'b1;
            end else begin
                pixel_vld <= 1'b0;
            end
        end
    end

endmodule

这段代码的核心逻辑:

  1. 检测VSYNC上升沿,开始捕获一帧
  2. 在帧有效期间,检测HSYNC高电平,捕获一行
  3. 每个PCLK上升沿,把数据线上的值存下来
  4. 输出像素值和有效标志

调试技巧:我第一次调这个模块时,发现图像有错位。后来用ILA抓了VSYNC和HSYNC的波形,发现VSYNC下降沿和最后一行的HSYNC有重叠。解决办法是把帧结束判断改成「检测到VSYNC下降沿后,再等一个PCLK才结束」。这种小坑,不踩一次真不知道。

4.6 数据格式转换

捕获到原始数据后,往往需要做格式转换。比如RAW10转RAW8,或者Bayer转RGB。这里给个简单的RAW10转RAW8的例子:

// RAW10转RAW8:取高8位
assign pixel_out = raw10_data[9:2];

嗯,就是这么简单。但要注意,直接截断会损失精度。如果追求画质,可以用抖动算法。

对于Bayer转RGB,常用的方法是双线性插值。说白了就是拿周围像素的平均值来补全缺失的颜色分量。这个算法网上有很多现成的IP,我就不重复造轮子了。

总结一下:图像数据流解析,核心就三件事——搞懂接口时序、认准数据格式、写好捕获逻辑。把这三点吃透了,上板联调就成功了一半。

可可精品资料库