在嵌入式音视频开发中,我们经常会遇到这些问题:
摄像头数据到底是怎么从 Sensor 进入 DDR 的?
V4L2 采集为什么不用 CPU 一帧一帧 memcpy?
DMA 在 Camera 驱动里到底搬运了什么?
零拷贝为什么可以降低延迟、提升性能?
NV12、NV16、YUYV、RAW10、RAW12 这些格式在驱动里是怎么体现的?
本文结合一段 Rockchip CIF 驱动源码,从 代码结构、驱动框架、格式表分析、DMA 搬运机制、零拷贝原理 几个角度,把 Camera 驱动中最核心的一条数据链路讲清楚。
说明:当前源码片段主要包含头文件依赖、宏定义、
fcc_xysubs()函数以及out_fmts[]输出格式表,没有完整包含probe、stream_on、irq、vb2 queue等后续实现。因此本文会明确区分:哪些是当前源码中能直接看到的内容,哪些是基于 Rockchip CIF + V4L2 框架的典型驱动流程分析。

1. 这段代码属于什么驱动?
源码开头已经说明:
/*
* Rockchip CIF Driver
*
* Copyright (C) 2018 Rockchip Electronics Co., Ltd.
*/这是一段 Rockchip CIF Driver 代码。
CIF 可以理解为 Camera Interface,也就是摄像头接口控制器。它位于 Sensor 和内存之间,主要负责接收 Camera 数据,并通过 DMA 写入 DDR。
一个典型 Rockchip Camera 采集链路可以简化成这样:
Camera Sensor
|
| MIPI CSI-2 / DVP
v
MIPI CSI2 Receiver / DVP Interface
|
v
Rockchip CIF
|
| DMA Write
v
DDR Buffer
|
v
V4L2 / Encoder / ISP / RGA / NPU / Display从这个链路可以看出,CIF 驱动并不是简单地“读摄像头数据”,而是连接了:
Sensor 数据输入
MIPI CSI2 / DVP 接口
CIF 硬件控制器
V4L2 视频采集框架
videobuf2 buffer 队列
DMA 写 DDR
IOMMU 地址映射
后级编码器或图像处理模块所以 CIF 驱动本质上是一条高速视频数据通路的入口。
2. 从 include 看驱动整体架构
源码一开始 include 了很多头文件,这些头文件其实已经暴露了驱动的大体设计方向。
部分关键 include 如下:
#include <linux/pm_runtime.h>
#include <linux/reset.h>
#include <linux/iommu.h>
#include <media/v4l2-common.h>
#include <media/v4l2-event.h>
#include <media/v4l2-fh.h>
#include <media/v4l2-fwnode.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-subdev.h>
#include <media/videobuf2-dma-contig.h>
#include <media/videobuf2-dma-sg.h>
#include <soc/rockchip/rockchip_iommu.h>
#include <linux/dma-fence.h>
#include <linux/sync_file.h>
#include <linux/kfifo.h>
#include <linux/gpio/consumer.h>这些 include 可以分成几类。
2.1 电源、复位、硬件资源管理
#include <linux/pm_runtime.h>
#include <linux/reset.h>
#include <linux/gpio/consumer.h>这说明驱动会涉及:
runtime PM 电源管理
reset 控制器复位
GPIO 控制Camera 驱动里经常需要控制 sensor reset、powerdown、mclk、电源域等资源。
虽然当前片段没有展示 probe() 和 power on/off 代码,但从 include 可以看出,这个驱动是按照 Linux 设备驱动模型来管理硬件资源的。
2.2 V4L2 框架相关
#include <media/v4l2-common.h>
#include <media/v4l2-event.h>
#include <media/v4l2-fh.h>
#include <media/v4l2-fwnode.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-subdev.h>这说明 CIF 驱动接入的是 Linux V4L2 媒体框架。
用户态访问摄像头时,通常不是直接操作 CIF 寄存器,而是访问:
/dev/video0
/dev/video1
...用户程序通过 ioctl 与驱动交互,例如:
VIDIOC_QUERYCAP 查询设备能力
VIDIOC_ENUM_FMT 枚举支持格式
VIDIOC_S_FMT 设置图像格式
VIDIOC_REQBUFS 申请 buffer
VIDIOC_QBUF buffer 入队
VIDIOC_STREAMON 启动采集
VIDIOC_DQBUF 取出采集完成的 buffer
VIDIOC_STREAMOFF 停止采集其中 v4l2-subdev 非常关键。
它说明 CIF 并不是孤立工作的,而是会和 Sensor、MIPI CSI2 等 subdev 组成一个 media pipeline。
典型 media graph 类似这样:
Sensor Subdev
|
v
MIPI CSI2 Subdev
|
v
CIF Video Node
|
v
/dev/videoX2.3 videobuf2 buffer 管理
#include <media/videobuf2-dma-contig.h>
#include <media/videobuf2-dma-sg.h>这是 V4L2 采集驱动里非常核心的部分。
videobuf2 简称 vb2,是 V4L2 的 buffer 队列管理框架。
它主要负责:
用户申请 buffer
驱动分配或导入 buffer
用户 QBUF 入队
驱动把 buffer 交给硬件 DMA
DMA 写入一帧图像
中断通知完成
驱动 vb2_buffer_done
用户 DQBUF 取出 buffer这里包含两个 DMA 后端:
videobuf2-dma-contig
videobuf2-dma-sg含义如下:
后端 | 含义 | 典型场景 |
|---|---|---|
dma-contig | 分配物理连续内存 | 硬件不支持 IOMMU,要求连续物理地址 |
dma-sg | scatter-gather 分散页 | 硬件支持 IOMMU,可映射成连续 IOVA |
这说明 CIF 采集不是通过 CPU memcpy,而是基于 DMA buffer 体系完成的。
2.4 IOMMU 相关
#include <linux/iommu.h>
#include <soc/rockchip/rockchip_iommu.h>IOMMU 的作用是为外设提供一个设备视角下的虚拟地址空间。
CPU 访问内存一般使用 CPU 虚拟地址;
而 CIF、VPU、RGA、Display 这类硬件模块访问内存,需要 DMA 地址或者 IOVA 地址。
IOMMU 可以把一组物理页映射成设备可见的连续地址。
这对零拷贝非常重要。
因为在零拷贝场景下,同一块 buffer 可能会被多个硬件模块共享:
CIF 写入 buffer
Encoder 读取同一块 buffer
Display 显示同一块 buffer
RGA 处理同一块 buffer如果没有统一的 DMA/IOMMU buffer 管理机制,这种跨硬件共享会非常困难。
2.5 dma-fence 与 sync_file
#include <linux/dma-fence.h>
#include <linux/sync_file.h>这两个头文件通常和跨设备同步有关。
为什么需要同步?
因为多个硬件共享同一块 buffer 时,必须保证读写顺序正确。
例如:
CIF 正在写第 N 帧
Encoder 不能提前读取第 N 帧
Encoder 正在读取第 N 帧
CIF 不能提前复用这块 buffer否则就会出现:
半帧数据
画面撕裂
颜色异常
偶发花屏
buffer 被提前覆盖dma-fence 可以表示一次 DMA 操作完成的同步点。sync_file 可以把 fence 以 fd 的形式传递到用户态或者其他模块。
在现代 Linux 图形和视频系统中,dma-buf + fence 是零拷贝和跨硬件同步的重要基础。
3. 基础宏定义分析
源码中定义了一些基础参数:
#define CIF_REQ_BUFS_MIN 1
#define CIF_MIN_WIDTH 64
#define CIF_MIN_HEIGHT 64
#define CIF_MAX_WIDTH 8192
#define CIF_MAX_HEIGHT 8192
#define OUTPUT_STEP_WISE 8这几个宏说明 CIF 输出尺寸有基本边界:
最小分辨率:64 x 64
最大分辨率:8192 x 8192OUTPUT_STEP_WISE 为 8,通常用于宽高或者 crop 参数的步进对齐。
很多图像硬件对宽、高、stride 都有对齐要求,例如:
8 字节对齐
16 字节对齐
64 字节对齐
256 字节对齐如果没有满足硬件对齐要求,可能会出现:
DMA 写错行
图像倾斜
画面花屏
颜色错乱
编码器无法直接读取4. Plane 定义分析
源码中有如下定义:
#define RKCIF_PLANE_Y 0
#define RKCIF_PLANE_CBCR 1
#define RKCIF_MAX_PLANE 3这说明驱动内部会按 plane 管理图像数据。
以 NV12 为例:
Y plane :亮度数据
UV plane :色度数据,UV 交织NV12 的内存布局可以理解为:
buffer_base
|
+-- Y plane
|
+-- UV plane虽然很多 V4L2 格式在 memory plane 上可能是一个平面,也就是 mplanes = 1,但从图像内容上看,它仍然可以拆成 Y 和 UV 两个 component plane。
因此代码里会同时看到:
cplanes:component planes,图像内容平面数量
mplanes:memory planes,V4L2 内存平面数量这两个概念要区分清楚。
5. Media Pad 定义分析
源码中还有:
#define STREAM_PAD_SINK 0
#define STREAM_PAD_SOURCE 1这对应 media controller 中的数据流方向。
sink pad :输入端
source pad :输出端对于 CIF 来说,它通常是从 MIPI CSI2 或 DVP 接收图像数据,所以:
CIF sink pad 接收来自上游的数据
CIF source pad 输出到 video node完整 media pipeline 可以理解为:
Sensor source pad
|
v
MIPI CSI2 sink pad
MIPI CSI2 source pad
|
v
CIF sink pad
CIF source pad
|
v
Video Node这个结构是 V4L2 Media Controller 框架管理复杂 Camera pipeline 的基础。
6. 高度 16 对齐:这是零拷贝优化的关键
源码中有一段非常重要的注释:
/*
* Round up height when allocate memory so that Rockchip encoder can
* use DMA buffer directly, though this may waste a bit of memory.
*/
#define MEMORY_ALIGN_ROUND_UP_HEIGHT 16这段注释的意思是:
分配内存时,把高度向上按 16 对齐,这样 Rockchip 编码器可以直接使用 DMA buffer,虽然会浪费一点内存。
这句话其实非常关键,它直接说明了驱动设计是在为 camera 到 encoder 的零拷贝链路 做准备。
为什么?
因为硬件编码器通常要求输入图像满足一定对齐条件,例如:
宽度对齐
高度对齐
stride 对齐
buffer size 对齐
plane offset 对齐如果 CIF 采集出来的 buffer 不满足编码器要求,后面就只能重新分配一块符合编码器要求的 buffer,然后做一次拷贝或者格式转换:
CIF DMA buffer
|
| memcpy / RGA 转换
v
Encoder input buffer这样就不是零拷贝了。
而这里提前把高度按 16 对齐,就是为了让 CIF 采集出来的 buffer 能被编码器直接使用:
CIF DMA 写入 buffer
|
| 同一块 DMA buffer
v
Encoder 直接读取这就是典型的工程优化:
采集端多分配一点内存,换取后级编码链路不再拷贝。
7. fcc_xysubs() 函数代码分析
源码中有一个很重要的小函数:
static int fcc_xysubs(u32 fcc, u32 *xsubs, u32 *ysubs)
{
switch (fcc) {
case V4L2_PIX_FMT_NV16:
case V4L2_PIX_FMT_NV61:
case V4L2_PIX_FMT_UYVY:
case V4L2_PIX_FMT_VYUY:
case V4L2_PIX_FMT_YUYV:
case V4L2_PIX_FMT_YVYU:
*xsubs = 2;
*ysubs = 1;
break;
case V4L2_PIX_FMT_NV21:
case V4L2_PIX_FMT_NV12:
*xsubs = 2;
*ysubs = 2;
break;
default:
return -EINVAL;
}
return0;
}这个函数的作用是:
根据 V4L2 fourcc 格式,计算 YUV 色度子采样比例。
7.1 什么是 xsubs 和 ysubs?
注释中写得很清楚:
/* Get xsubs and ysubs for fourcc formats
*
* @xsubs: horizontal color samples in a 4*4 matrix, for yuv
* @ysubs: vertical color samples in a 4*4 matrix, for yuv
*/可以简单理解为:
xsubs:水平方向色度采样比例
ysubs:垂直方向色度采样比例对于 YUV 格式,Y 是亮度,UV 是色度。
人眼对亮度更敏感,对色度不那么敏感,所以视频格式里常常会减少 UV 数据量,这就是色度子采样。
7.2 YUV422 格式分析
代码中这些格式被归为:
*xsubs = 2;
*ysubs = 1;对应格式包括:
V4L2_PIX_FMT_NV16
V4L2_PIX_FMT_NV61
V4L2_PIX_FMT_UYVY
V4L2_PIX_FMT_VYUY
V4L2_PIX_FMT_YUYV
V4L2_PIX_FMT_YVYU这类格式属于 YUV422。
含义是:
水平方向:2 个 Y 共用一组 UV
垂直方向:每一行都有 UV所以 YUV422 的数据量通常是:
width * height * 2 bytes例如 1920x1080 的 YUV422:
1920 * 1080 * 2 ≈ 4MB/frame如果是 60fps:
4MB * 60 ≈ 240MB/s这只是单路写 DDR 的数据量,还没计算后续处理和额外拷贝。
7.3 YUV420 格式分析
代码中 NV12 和 NV21 被归为:
*xsubs = 2;
*ysubs = 2;对应:
V4L2_PIX_FMT_NV21
V4L2_PIX_FMT_NV12这类格式属于 YUV420。
含义是:
水平方向:2 个 Y 共用一组 UV
垂直方向:2 行 Y 共用一行 UV所以 YUV420 的数据量通常是:
width * height * 1.5 bytes例如 1920x1080 的 NV12:
Y plane = 1920 * 1080
UV plane = 1920 * 1080 / 2
总大小 = 1920 * 1080 * 1.5 ≈ 3MB/frame所以 NV12 相比 YUV422 更省带宽,也更适合编码。
在实际工程中,Camera -> Encoder 链路经常优先选择 NV12。
7.4 default 返回 -EINVAL 的意义
函数末尾有:
default:
return -EINVAL;这表示如果传入的 fourcc 不属于函数支持的 YUV 格式,就返回非法参数。
这类函数通常会被用于:
计算 bytesperline
计算 sizeimage
计算 UV plane 偏移
判断格式是否合法如果格式不支持,驱动必须及时返回错误,否则后面 DMA 地址、stride、buffer size 都可能计算错误,最终导致花屏或者内存越界。
8. out_fmts[] 输出格式表分析
源码中最核心的数据结构是:
static const struct cif_output_fmt out_fmts[] = {
...
};这个表非常重要。
它的作用是把用户层看到的 V4L2 像素格式,转换成 CIF 硬件能够理解的格式配置。
也就是说:
V4L2_PIX_FMT_NV12
|
v
CIF 硬件寄存器格式值
|
v
CSI 写 DDR 类型
|
v
DMA 输出内存布局一个表项大致长这样:
{
.fourcc = V4L2_PIX_FMT_NV12,
.fmt_val = YUV_OUTPUT_420 | UV_STORAGE_ORDER_UVUV,
.cplanes = 2,
.mplanes = 1,
.bpp = { 8, 16 },
.csi_fmt_val = CSI_WRDDR_TYPE_YUV420SP,
.fmt_type = CIF_FMT_TYPE_YUV,
}下面逐个字段分析。
8.1 fourcc:用户层看到的格式
.fourcc = V4L2_PIX_FMT_NV12fourcc 是 V4L2 中用于描述像素格式的四字符编码。
用户态通过 v4l2-ctl 可以看到类似格式:
v4l2-ctl --list-formats-ext -d /dev/video0可能输出:
[0]: 'NV12' (Y/CbCr 4:2:0)
[1]: 'NV16' (Y/CbCr 4:2:2)
[2]: 'YUYV' (YUYV 4:2:2)
[3]: 'RG10' (10-bit Bayer RGRG/GBGB)驱动内部就是通过 fourcc 判断用户选择了哪种输出格式。
8.2 cplanes 和 mplanes:图像平面与内存平面
例如 NV12 表项:
.cplanes = 2,
.mplanes = 1,这说明:
cplanes = 2:图像内容上有 Y 和 UV 两个平面
mplanes = 1:V4L2 内存上用一个连续 buffer 表示NV12 内存布局可以画成:
+-----------------------------+
| Y plane |
| width * height |
+-----------------------------+
| UV plane |
| width * height / 2 |
+-----------------------------+虽然图像内容上分为 Y 和 UV,但是内存上通常是一整块连续 buffer。
这也是很多初学者容易混淆的地方:
component plane 不等于 memory plane8.3 fmt_val:CIF 硬件输出格式配置
以 NV12 为例:
.fmt_val = YUV_OUTPUT_420 | UV_STORAGE_ORDER_UVUV这说明硬件输出格式是:
YUV420
UV 顺序为 UVUV对应 NV12。
而 NV21 是:
.fmt_val = YUV_OUTPUT_420 | UV_STORAGE_ORDER_VUVU这说明:
YUV420
UV 顺序为 VUVU所以 NV12 和 NV21 的核心区别不是 Y plane,而是 UV 顺序:
NV12:UVUVUV...
NV21:VUVUVU...如果 UV 顺序配置错,最典型的现象就是:
亮度正常
颜色异常
画面偏绿
画面偏紫8.4 bpp:每个 plane 的位宽
NV12 表项中:
.bpp = { 8, 16 },这个字段要结合格式理解。
对于 NV12:
Y plane:每个 Y 是 8bit
UV plane:UV 交织,每组 UV 可以理解为 16bit所以这里用 {8, 16} 描述 Y 和 UV plane 的 bit per pixel 关系。
驱动后面通常会基于 bpp 计算:
bytesperline
sizeimage
plane offset
DMA 写入地址例如:
Y stride = width * 8 / 8
UV stride = width * 16 / 8 / xsubs具体计算方式要看驱动后续实现,但 bpp 字段一定会参与 buffer size 和地址偏移计算。
8.5 csi_fmt_val:CSI 写 DDR 类型
例如 NV12:
.csi_fmt_val = CSI_WRDDR_TYPE_YUV420SPNV16:
.csi_fmt_val = CSI_WRDDR_TYPE_YUV422RAW10:
.csi_fmt_val = CSI_WRDDR_TYPE_RAW10这个字段通常用于配置 CIF/CSI 写 DDR 时的数据类型。
可以理解为告诉硬件:
当前写 DDR 的数据类型是什么?
是 YUV420 semi-planar?
是 YUV422?
是 RAW8?
是 RAW10?
是 RGB888?硬件根据这个值决定如何组织 DMA 写入数据。
8.6 fmt_type:格式大类
表中可以看到:
.fmt_type = CIF_FMT_TYPE_YUV或者:
.fmt_type = CIF_FMT_TYPE_RAW这表示格式大类。
YUV 格式一般用于视频采集、编码、显示预览。
RAW 格式一般来自 Bayer Sensor,后续通常要经过 ISP 做:
黑电平校正
坏点校正
去马赛克
白平衡
降噪
颜色校正
Gamma如果直接采集 RAW,则用户层拿到的是原始 Bayer 数据,而不是正常 RGB/YUV 图像。
9. 典型格式表项代码走读
下面挑几个典型格式表项进行分析。
9.1 NV16:YUV422 semi-planar
{
.fourcc = V4L2_PIX_FMT_NV16,
.cplanes = 2,
.mplanes = 1,
.fmt_val = YUV_OUTPUT_422 | UV_STORAGE_ORDER_UVUV,
.bpp = { 8, 16 },
.csi_fmt_val = CSI_WRDDR_TYPE_YUV422,
.fmt_type = CIF_FMT_TYPE_YUV,
}NV16 是 YUV422 semi-planar 格式。
内存布局:
+-----------------------------+
| Y plane |
+-----------------------------+
| UVUVUVUV plane |
+-----------------------------+特点:
色度垂直方向不降采样
画质比 NV12 更好
数据量比 NV12 更大数据量:
width * height * 2 bytes适合对色彩要求较高的采集场景。
9.2 NV12:YUV420 semi-planar
{
.fourcc = V4L2_PIX_FMT_NV12,
.fmt_val = YUV_OUTPUT_420 | UV_STORAGE_ORDER_UVUV,
.cplanes = 2,
.mplanes = 1,
.bpp = { 8, 16 },
.csi_fmt_val = CSI_WRDDR_TYPE_YUV420SP,
.fmt_type = CIF_FMT_TYPE_YUV,
}NV12 是嵌入式视频里最常见的格式之一。
内存布局:
+-----------------------------+
| Y plane |
+-----------------------------+
| UVUVUVUV plane |
+-----------------------------+数据量:
width * height * 1.5 bytes它常用于:
Camera 采集
硬件编码 H.264/H.265
视频预览
NPU 前处理
RGA 图像处理如果目标是 Camera -> Encoder,NV12 通常是优先选择。
9.3 NV21:和 NV12 只差 UV 顺序
{
.fourcc = V4L2_PIX_FMT_NV21,
.fmt_val = YUV_OUTPUT_420 | UV_STORAGE_ORDER_VUVU,
.cplanes = 2,
.mplanes = 1,
.bpp = { 8, 16 },
.csi_fmt_val = CSI_WRDDR_TYPE_YUV420SP,
.fmt_type = CIF_FMT_TYPE_YUV,
}NV21 和 NV12 非常像。
区别是:
NV12:UVUVUV
NV21:VUVUVU如果后级模块期望 NV12,但驱动输出 NV21,就会出现颜色异常。
调试时可以重点观察:
画面亮度是否正常
颜色是否偏紫
颜色是否偏绿
肤色是否异常这类问题多数和 UV 顺序有关。
9.4 YUYV / UYVY:Packed YUV422
源码中也支持:
V4L2_PIX_FMT_YUYV
V4L2_PIX_FMT_YVYU
V4L2_PIX_FMT_UYVY
V4L2_PIX_FMT_VYUY这些属于 packed YUV422。
例如 YUYV 的内存排列可以理解为:
Y0 U0 Y1 V0 Y2 U1 Y3 V1 ...UYVY 则是:
U0 Y0 V0 Y1 U1 Y2 V1 Y3 ...这类格式常见于:
USB Camera
DVP Sensor
调试采集
部分显示输入但是如果后面要接编码器,而编码器只支持 NV12,那么中间可能需要格式转换。
一旦发生格式转换,就可能引入额外拷贝或 RGA 处理,零拷贝链路就会变复杂。
9.5 RGB24 / BGR24 / RGB565
源码中支持:
V4L2_PIX_FMT_RGB24
V4L2_PIX_FMT_BGR24
V4L2_PIX_FMT_RGB565
V4L2_PIX_FMT_BGR666RGB 格式更直观,但数据量通常更大。
例如 RGB24:
每个像素 3 bytes
1920 * 1080 * 3 ≈ 6MB/frame60fps 下:
6MB * 60 ≈ 360MB/s相比 NV12:
1920 * 1080 * 1.5 ≈ 3MB/frameRGB24 的带宽几乎是 NV12 的 2 倍。
所以如果后面要编码 H.264/H.265,一般不建议优先使用 RGB 格式。更好的选择通常是让 CIF 直接输出 NV12 或 NV16。
9.6 RAW8 / RAW10 / RAW12:Bayer 原始数据
源码中支持多种 Bayer RAW 格式:
V4L2_PIX_FMT_SRGGB8
V4L2_PIX_FMT_SGRBG8
V4L2_PIX_FMT_SGBRG8
V4L2_PIX_FMT_SBGGR8
V4L2_PIX_FMT_SRGGB10
V4L2_PIX_FMT_SGRBG10
V4L2_PIX_FMT_SGBRG10
V4L2_PIX_FMT_SBGGR10
V4L2_PIX_FMT_SRGGB12
...例如 RAW10 表项:
{
.fourcc = V4L2_PIX_FMT_SRGGB10,
.cplanes = 1,
.mplanes = 1,
.bpp = { 16 },
.raw_bpp = 10,
.csi_fmt_val = CSI_WRDDR_TYPE_RAW10,
.fmt_type = CIF_FMT_TYPE_RAW,
}这里有一个很重要的细节:
raw_bpp = 10
bpp = 16这说明原始 sensor 数据是 10bit,但写入 DDR 时可能按照 16bit 容器来存储。
也就是说,RAW10 不一定在内存中紧凑地按 10bit 排列,它可能被 unpack 成 16bit。
这样做的好处是:
地址计算简单
DMA 写入对齐更友好
CPU 或 ISP 读取更方便代价是:
占用内存更多
DDR 带宽更大这也是调试 RAW10/RAW12 时非常容易踩坑的地方。
如果你以为 RAW10 一定是:
width * height * 10 / 8但实际驱动按照 16bit 存储,那么你计算出来的 sizeimage 就会偏小,最终导致取帧异常或者图像错乱。
10. 根据代码推导完整采集流程
当前源码片段没有展示完整的 stream_on、irq handler 和 vb2_ops,但结合 V4L2 + Rockchip CIF 的典型结构,完整采集流程一般如下:
1. 用户打开 /dev/videoX
|
2. VIDIOC_QUERYCAP 查询能力
|
3. VIDIOC_ENUM_FMT 枚举 out_fmts[] 支持的格式
|
4. VIDIOC_S_FMT 设置分辨率和像素格式
|
5. 驱动根据 fourcc 查找 out_fmts[] 表项
|
6. 根据 bpp、xsubs、ysubs 计算 stride 和 sizeimage
|
7. VIDIOC_REQBUFS 申请 DMA buffer
|
8. VIDIOC_QBUF 把 buffer 放入 vb2 队列
|
9. VIDIOC_STREAMON 启动 Sensor / MIPI / CIF
|
10. CIF 配置 DMA 地址和格式寄存器
|
11. Sensor 输出图像数据
|
12. CIF 接收数据并 DMA 写入 DDR
|
13. 一帧完成后触发中断
|
14. 驱动调用 vb2_buffer_done
|
15. 用户 VIDIOC_DQBUF 取出一帧从软件角度看:
V4L2 ioctl
|
v
vb2 buffer queue
|
v
CIF register config
|
v
DMA write
|
v
IRQ frame done
|
v
vb2_buffer_done从硬件角度看:
Sensor
|
v
MIPI CSI2 / DVP
|
v
CIF
|
v
DMA
|
v
DDR11. DMA 搬运到底做了什么?
DMA 全称是 Direct Memory Access,直接内存访问。
在 Camera 场景中,DMA 的本质是:
CIF 硬件作为 bus master,
直接把接收到的图像数据写入 DDR buffer,
CPU 不参与每个像素的搬运。CPU 主要负责:
配置 CIF 寄存器
设置 DMA 目标地址
设置输出格式
设置 stride
设置 crop/width/height
启动 stream
处理中断
维护 buffer 队列真正的视频像素搬运由硬件完成。
12. DMA 写 NV12 的过程
以 NV12 为例,一帧图像有两个逻辑平面:
Y plane
UV plane如果分辨率是 1920x1080:
Y 大小 = 1920 * 1080
UV 大小 = 1920 * 1080 / 2
总大小 = 1920 * 1080 * 1.5内存布局:
buffer_base
|
+-- Y plane
| size = y_stride * aligned_height
|
+-- UV plane
size = uv_stride * aligned_height / 2驱动通常需要给硬件配置:
Y plane DMA 地址
UV plane DMA 地址
Y stride
UV stride
图像宽度
图像高度
输出格式对于 single-planar NV12,虽然 V4L2 层面可能只有一个 memory plane,但驱动内部仍然会计算:
Y address = buffer_base
UV address = buffer_base + y_stride * aligned_height这就是为什么前面 cplanes = 2、mplanes = 1 要分开理解。
13. DMA 为什么比 CPU memcpy 高效?
假设采集 4K60 NV12:
3840 * 2160 * 1.5 * 60 ≈ 746MB/s这只是 CIF DMA 写 DDR 的原始数据量。
如果中间再做一次 CPU memcpy,相当于额外发生:
读 746MB/s
写 746MB/s也就是接近:
1.5GB/s的额外 DDR 带宽开销。
如果链路是:
Camera -> RGA -> Encoder -> Display中间每多一次拷贝,带宽都会被进一步放大。
所以嵌入式视频系统中,真正的优化往往不是“写一个更快的 memcpy”,而是:
让 memcpy 根本不要发生14. DMA 搬运最容易出问题的地方
14.1 DMA 地址错误
硬件 DMA 不能直接使用普通用户态虚拟地址。
它需要的是:
物理地址
或 IOMMU 映射后的 IOVA 地址如果地址配置错,轻则图像异常,重则总线错误。
14.2 stride 错误
图像宽度不一定等于内存 stride。
例如:
width = 1920
stride = 2048这是很常见的对齐情况。
如果 stride 配错,常见现象是:
画面倾斜
错行
花屏
图像撕裂14.3 UV offset 错误
NV12、NV21、NV16、NV61 都涉及 UV plane。
如果 UV 起始地址计算错,会出现:
亮度正常
颜色异常
画面偏绿
画面偏紫
色块错位14.4 RAW10/RAW12 sizeimage 错误
RAW10/RAW12 要特别注意:
raw_bpp 不一定等于内存 bpp如果驱动把 RAW10 unpack 到 16bit,那么 sizeimage 应该按 16bit 计算,而不是按 10bit 计算。
否则用户态 mmap 后读取数据时,很容易出现:
一帧大小不对
图像错位
行数据错乱
后半帧异常14.5 Cache 同步问题
如果 CPU 和 DMA 同时访问同一块 buffer,就要考虑 cache coherency。
典型问题:
CPU 看到旧数据
硬件读到旧数据
偶发花屏
偶发撕裂在 vb2 / dma-buf 体系里,很多 cache 同步动作会由框架处理,但驱动必须正确遵守 buffer 生命周期。
15. 零拷贝是什么?
零拷贝不是“不发生 DMA”。
在 Camera 场景里,零拷贝通常指:
摄像头采集出来的 DMA buffer,
不再被 CPU memcpy 到另一块 buffer,
而是直接交给后级硬件模块使用。也就是说:
有 DMA
但没有 CPU memcpy普通链路可能是:
CIF DMA -> Kernel Buffer
Kernel Buffer -> User Buffer
User Buffer -> Encoder Buffer
Encoder 读取 Encoder Buffer零拷贝链路则是:
CIF DMA -> DMA-BUF
Encoder 直接读取同一个 DMA-BUF关键区别是:
同一块物理内存或同一组物理页,
被多个硬件模块共享。16. Camera 到 Encoder 的零拷贝链路
典型零拷贝编码流程:
1. CIF 驱动分配 DMA buffer
|
2. CIF DMA 把图像写入 buffer
|
3. 用户态 DQBUF 拿到 buffer
|
4. 用户态获取 dmabuf fd
|
5. 把 dmabuf fd 传给编码器
|
6. 编码器 import 这块 dmabuf
|
7. 编码器通过 IOMMU 映射同一块 buffer
|
8. 编码器直接读取该 buffer 编码数据没有被 CPU 重新复制。
这就是零拷贝的核心价值。
17. 零拷贝成立的几个条件
17.1 格式兼容
CIF 输出格式必须是后级模块能接受的格式。
例如编码器通常更喜欢:
NV12
NV16如果 CIF 输出 RGB24,而编码器只接受 NV12,中间就需要格式转换。
格式转换可能由 CPU 做,也可能由 RGA 做。
但只要产生新的输出 buffer,就不再是严格意义上的 Camera -> Encoder 零拷贝。
17.2 对齐兼容
源码里的这句非常关键:
#define MEMORY_ALIGN_ROUND_UP_HEIGHT 16它说明采集端分配 buffer 时,提前考虑了 Rockchip encoder 的输入对齐要求。
如果不提前对齐,就可能变成:
CIF buffer 不满足 encoder 要求
|
重新分配 encoder buffer
|
copy / RGA 转换
|
encoder 使用新 buffer这样就破坏了零拷贝。
17.3 地址空间兼容
CIF 和 Encoder 是不同硬件模块。
它们要共享同一块 buffer,需要依赖:
dma-buf
sg_table
IOMMU mapping
DMA address / IOVA这也是代码中同时出现:
#include <media/videobuf2-dma-contig.h>
#include <media/videobuf2-dma-sg.h>
#include <linux/iommu.h>
#include <soc/rockchip/rockchip_iommu.h>的原因。
17.4 同步正确
零拷贝最大的问题之一是同步。
必须保证:
CIF 写完后,Encoder 才能读
Encoder 读完后,CIF 才能复用这类同步可以通过:
中断
vb2 buffer 状态
dma-fence
sync_file
显式 fence fd来实现。
源码中出现:
#include <linux/dma-fence.h>
#include <linux/sync_file.h>说明该驱动或相关链路具备和 fence 同步机制结合的可能。
18. 从这段代码看驱动设计思路
虽然源码片段不完整,但已经能看出几个明确设计方向。
第一,驱动以 V4L2 为上层接口
通过 V4L2 ioctl 向用户态暴露能力,用户不用直接操作硬件寄存器。
第二,使用 vb2 管理 DMA buffer
这说明数据通路是面向高性能视频采集设计的,不是普通 CPU 拷贝模型。
第三,格式表是驱动和硬件之间的桥
out_fmts[] 把 V4L2 fourcc 转换成 CIF 硬件寄存器需要的格式值。
第四,IOMMU 和 dma-buf 是跨模块共享基础
Camera、RGA、Encoder、Display 这类硬件要共享 buffer,离不开 DMA 地址映射和 buffer 生命周期管理。
第五,高度 16 对齐直接服务于零拷贝
这不是随便写的宏,而是为了让 encoder 可以直接使用 camera 采集出来的 DMA buffer。
19. 调试 Rockchip CIF/V4L2 时重点看什么?
19.1 看格式是否一致
需要确认:
Sensor 输出格式
MIPI CSI2 接收格式
CIF 输出格式
V4L2 fourcc
用户态设置格式
后级 encoder 输入格式重点关注:
NV12 / NV21
NV16 / NV61
YUYV / UYVY
RAW8 / RAW10 / RAW1219.2 看 stride 和 sizeimage
很多花屏不是 sensor 的问题,而是:
bytesperline 算错
sizeimage 算错
UV plane offset 算错
height alignment 没处理尤其是 NV12 和 RAW10/RAW12。
19.3 看 DMA 地址
需要确认:
DMA 地址是否正确
IOMMU 映射是否成功
buffer 是否物理连续
sg table 是否正确19.4 看中断是否正常
一帧完成后,CIF 通常会产生 frame done interrupt。
如果中断不正常,用户态可能一直卡在:
VIDIOC_DQBUF19.5 看是否真的零拷贝
判断零拷贝不要只看有没有 DMA,而要看有没有额外 memcpy。
重点排查:
用户态是否 memcpy 了一份图像
encoder 是否 import 同一个 dmabuf fd
中间是否经过 RGA 转换
格式是否被迫转换
buffer 对齐是否满足后级要求20. 总结
这段 Rockchip CIF 驱动代码虽然只是片段,但已经能看出 Camera 驱动的核心主线:
通过 V4L2 暴露采集接口
通过 out_fmts[] 管理输出格式
通过 vb2 管理 DMA buffer
通过 CIF 硬件 DMA 写入 DDR
通过 IOMMU / dma-buf / fence 支撑跨模块零拷贝从工程角度看,Camera 驱动的关键不是“能不能出图”,而是:
格式是否正确
stride 是否正确
buffer 是否对齐
DMA 地址是否正确
同步是否可靠
后级能否零拷贝使用真正高性能的视频链路应该是:
Sensor -> CIF -> DMA-BUF -> Encoder / RGA / Display而不是:
Sensor -> CIF -> buffer -> memcpy -> buffer -> encoder对于嵌入式音视频系统来说,优化的核心往往不是写一个更快的拷贝函数,而是从架构上减少拷贝,让数据在硬件模块之间通过 DMA buffer 直接流动。
这就是 DMA 和零拷贝的真正价值。

被折叠的 条评论
为什么被折叠?



