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的时序其实很直观。我画个图你就明白了:
你看这个图,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
这段代码的核心逻辑:
- 检测VSYNC上升沿,开始捕获一帧
- 在帧有效期间,检测HSYNC高电平,捕获一行
- 每个PCLK上升沿,把数据线上的值存下来
- 输出像素值和有效标志
调试技巧:我第一次调这个模块时,发现图像有错位。后来用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,我就不重复造轮子了。
总结一下:图像数据流解析,核心就三件事——搞懂接口时序、认准数据格式、写好捕获逻辑。把这三点吃透了,上板联调就成功了一半。