【嵌入式Linux应用开发 | V4L2】摄像头V4L2应用程序开发(MIPI摄像头)
一、 程序编写流程
本文借鉴麦兜(chenchengwudi@sina.com)的笔记从用户空间使用V4L2子系统。
下文以泰山派RK3566开发板为例,系统为Buildroot,虚拟机使用Ubuntu18.04,摄像头使用OV5695摄像头。
USB免驱摄像头可查看 嵌入式Linux应用开发——摄像头V4L2应用程序开发(USB免驱摄像头)
- 嵌入式Linux学习交流群:
1005210698- 本文相关源码以及更多免费资料可加群获取
1.1 打开设备节点
open:打开设备节点 /dev/videoX
fd = open("/dev/video0", O_RDWR | O_NONBLOCK);
说明1:
- 对于
V4L2设备,也可以在open函数中指定O_NONBLOCK标志,即以非阻塞模式打开。在非阻塞模式下,如果在进行ioctI+VIDIOC_DQBUF操作时没有已经就绪的缓冲区,则应用程序不会阻塞,而是立即返回-1,并且将errno置为EAGAIN
说明2:
- 在泰山派RK3566中,OV5695摄像头对应的设备节点是
/dev/video0
1.2 查询设备能力
ioctl VIDIOC_QUERYCAP:Query Capbility,查询设备能力,比如
- 确认是否是“捕获设备”,因为有些节点是输出设备
- 确认是否支持
mmap操作,还是仅支持read/write操作
struct v4l2_capability cap;
memset(&cap, 0, sizeof(cap));
if(0 <= ioctl(fd, VIDIOC_QUERYCAP, &cap)){
printf("driver = %s\n", cap.driver);
printf("card = %s\n", cap.card);
printf("bus_info = %s\n",cap.bus_info);
printf("version = %u.%u.%u\n",(cap.version >>16) & 0xFF,
(cap.version >> 8) & 0xFF, cap.version & 0xFF);
printf("capability = 0x%x\n", cap.capabilities);
printf("device_caps = 0x%x\n",cap.device_caps);
}
打印信息
v4l2_capability结构体
struct v4l2_capability {
__u8 driver[16]; // 驱动模块的名称(例如:“bttv”)
__u8 card[32]; // 卡片的名称(例如:“运营商天盛Hauppauge”)
__u8 bus_info[32]; // 总线的名称(例如:pci_name(pci_dev))
__u32 version; // 内核版本
__u32 capabilities; // 物理设备的整体功能
__u32 device_caps; // 通过这个特定设备(节点)访问的功能
__u32 reserved[3]; // 为将来的扩展保留字段
};
说明1: capabilities 和 device_cap字段的构成
capabilities 和 device_cap 字段由一系列标志位构成,此处简要说明如下概念的含义:
single-planar和multi-planar用于描述图像类型的存储方式capture和output用于描述设备数据流向,capture的数据是从设备到内存(e.g.视频采集设备),output的数据是从内存到设备 (e.g.视频输出设备)memory-to-memory也是用于描述设备的数据流向,即数据从内存到设备再到内存,典型的就是图像codec的数据流向(e.g.待编码的图像来自内存,编码后的码流也输出到内存)

说明2:实验硬件设备能力分析
- 本次设备的
capabilities为 0x84201000,表示该设备支持V4L2_CAP_DEVICE_CAPS、V4L2_CAP_STREAMING、V4L2_CAP_EXT_PIX_FORMAT、V4L2_CAP_VIDEO_CAPTURE_MPLANE - 其中
V4L2_CAP_VIDEO_CAPTURE_MPLANE尤为重要,与 USB摄像头 不同,该摄像头支持的是 多平面格式图像
1.3 枚举设备格式
ioctl VIDIOC_ENUM_FMT:枚举设备支持的格式
struct v4l2_fmtdesc fmtdesc;
memset(&fmtdesc, 0, sizeof(fmtdesc));
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
while(1){
if (0 > ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
if(error != EINVAL){
perror("VIDIOC_ENUM_FMT error");
goto err;
}
else {
printf("enumerate end\n");
break;
}
}
printf("index = %d\n",fmtdesc.index);
printf("type = %d\n",fmtdesc.type);
printf("flags = 0x%x\n", fmtdesc.flags);
printf("description = %s\n", fmtdesc.description);
printf("pixelformat = %c.%c.%c.%c\n\n",fmtdesc.pixelformat & 0xFF,
(fmtdesc.pixelformat >> 8)& 0xFF,(fmtdesc.pixelformat >>16) & 0xFF,
(fmtdesc.pixelformat >>24) & 0xFF);
fmtdesc.index++;
}
打印信息
- 只截取了一部分,后续将图像格式设置为
NV12格式
v4l2_fmtdesc结构体
struct v4l2_fmtdesc {
__u32 index; /* 格式枚举序号 */
__u32 type; /* 缓冲区类型 */
__u32 flags;
__u8 description[32]; /* 描述 */
__u32 pixelformat; /* 像素格式 */
__u32 reserved[4];
};
说明1: type 缓冲区类型
- 从实现逻辑上来说,是驱动程序先指定
vb2_queue结构体的缓冲区类型,之后应用程序需要指定相应的缓冲区类型才能索引到正确的vb2_queue结构体

说明2:格式标志 flags
-
格式标志flags由一系列标志位构成

-
实验硬件设备支持格式的标志flags字段值如下,
YUYV格式的flags字段值为0x0,即YUYV格式既不是压缩格式,也不是软件模拟格式
1.4 枚举指定格式分辨率
ioctl VIDIOC_ENUM_FRAMESIZES:枚举指定格式的分辨率
struct v4l2_frmsizeenum frmsize;
memset(&frmsize, 0, sizeof(frmsize));
frmsize.index = 0;
frmsize.pixel_format = V4L2_PIX_FMT_NV12;
while(1){
ret = ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize);
if(ret < 0){
printf("enumerate end\n");
break;
}
printf("type = %d\n", frmsize.type);
switch (frmsize.type) {
case V4L2_FRMSIZE_TYPE_DISCRETE:
printf("V4L2_FRMSIZE_TYPE_DISCRETE\n");
break;
case V4L2_FRMSIZE_TYPE_CONTINUOUS:
printf("V4L2_FRMSIZE_TYPE_CONTINUOUS\n");
break;
case V4L2_FRMSIZE_TYPE_STEPWISE:
printf("V4L2_FRMSIZE_TYPE_STEPWISE\n");
break;
}
printf("index = %d\n",frmsize.index);
if(frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE)
printf("width = %d, height = %d\n",
frmsize.discrete.width, frmsize.discrete.height);
if(frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE
|| frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS){
printf("min_width = %d, min_height = %d\n",
frmsize.stepwise.min_width, frmsize.stepwise.min_height);
printf("max_width = %d, max_height = %d\n",
frmsize.stepwise.max_width, frmsize.stepwise.max_height);
printf("step_width = %d, step_height = %d\n",
frmsize.stepwise.step_width, frmsize.stepwise.step_height);
}
frmsize.index++;
}
}
打印信息
v4l2_frmsizeenum结构体
struct v4l2_frmsizeenum {
__u32 index; // 分辨率遍历序列
__u32 pixel_format; // fourcc格式编码
__u32 type; // 设备支持的帧大小类型
union { /* Frame size */
struct v4l2_frmsize_discrete discrete;
struct v4l2_frmsize_stepwise stepwise;
};
__u32 reserved[2]; /* Reserved space for future use */
};
struct v4l2_frmsize_discrete {
__u32 width; // 帧宽[像素]
__u32 height; // 帧高[像素]
};
struct v4l2_frmsize_stepwise {
__u32 min_width; // 最小帧宽[像素]
__u32 max_width; // 最大帧宽[像素]
__u32 step_width; // 帧宽步长[像素]
__u32 min_height; // 最小帧高[像素]
__u32 max_height; // 最大帧高[像素]
__u32 step_height; // 帧高步长[像素]
};
说明1:关于分辨率
- 分辨率类型由
v4l2_frmsizetypes枚举值指定,具体枚举值如下
- 离散型分辨率:设备支持的分辨率由一系列离散值构成
- 阶梯型分辨率:设备支持的分辨率在指定的范围内以步长为单位递增
- 连续型分辨率:连续型分辨率可以理解为阶梯型分辨率的特例,即步长为1的阶梯型分辨率
1.5 设置图像格式
ioctl VIDIOC_S_FMT:在上面枚举出的格式里,选择一个来设置格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
fmt.fmt.pix_mp.width = 1280;
fmt.fmt.pix_mp.height = 720;
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
int ret = ioctl(vd->fd, VIDIOC_S_FMT, &fmt);
printf("type = %d\n", fmt.type);
printf("width = %d\n", fmt.fmt.pix_mp.width);
printf("height = %d\n",fmt.fmt.pix_mp.height);
printf("pixelformat = %c.%c.%c.%c\n",fmt.fmt.pix_mp.pixelformat & 0xFF,
(fmt.fmt.pix_mp.pixelformat >>8) & 0xFF,
(fmt.fmt.pix_mp.pixelformat >>16) & 0xFF,
(fmt.fmt.pix_mp.pixelformat >>24) & 0xFF);
printf("field = 0x%x\n", fmt.fmt.pix_mp.field);
printf("num_plans = %d\n", fmt.fmt.pix_mp.num_planes);
printf("colorspace = 0x%x\n", fmt.fmt.pix_mp.colorspace);
printf("plane: sizeimage = %d, bytesperline = %d\n",
fmt.fmt.pix_mp.plane_fmt[0].sizeimage,
fmt.fmt.pix_mp.plane_fmt[0].bytesperline);
}
打印信息
说明1:图像存储方式
YUYV格式的存储方式和采样方式如下图,详细参考V4L2_PIX_FMT_NV12 (‘NV12’)

- 泰山派底层驱动强制设置为了1个平面,将
NV12的 两个平面合并为单一连续内存块,形成逻辑上的单平面 - 以图像格式位
YUYV为例,分辨率在1280 * 720的情况下,驱动程序填充了1个平面的bytesperline和sizeimage字段,其中:bytesperline= 1280 (= 1280 * 1, 存储一个Y像素需要1B)sizeimage= 1382400 (= 1280 * 720 * 3/2,UV分量水平采样率和垂直采样率都是是Y分量的一半,存储一组UV分量需要2B,这里只有一个平面,则将Y和UV相加)
v4l2_format结构体
struct v4l2_format {
__u32 type; // 数据流类型
union {
struct v4l2_pix_format pix; // 图像格式的定义
struct v4l2_pix_format_mplane pix_mp; // 多平面图像格式的定义
struct v4l2_window win; // 覆盖图像的定义
struct v4l2_vbi_format vbi; // 原始VBI捕获或输出参数
struct v4l2_sliced_vbi_format sliced; // 切片VBI捕获或输出参数
struct v4l2_sdr_format sdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */
struct v4l2_meta_format meta; /* V4L2_BUF_TYPE_META_CAPTURE */
__u8 raw_data[200]; // 未来扩展和自定义格式的占位符
} fmt;
};
v4l2_pix_format_mplane结构体
struct v4l2_pix_format_mplane {
__u32 width; // 图像宽度(单位:像素)
__u32 height; // 图像高度(单位:像素)
__u32 pixelformat; // 像素格式(如 V4L2_PIX_FMT_NV12)
__u32 field; // 视频场类型(如顶场、底场、隔行扫描等)
__u32 colorspace; // 颜色空间(如 BT.601、BT.709 等)
struct v4l2_plane_pix_format plane_fmt[VIDEO_MAX_PLANES]; // 各平面的像素格式参数
__u8 num_planes; // 实际使用的平面数量(<= VIDEO_MAX_PLANES)
__u8 flags; // 标志位(如是否连续存储、预乘Alpha等)
union {
__u8 ycbcr_enc; // Y'CbCr 编码范围(如 BT.601 或 BT.709)
__u8 hsv_enc; // HSV 编码方式(若适用)
};
__u8 quantization; // 量化范围(全范围或有限范围)
__u8 xfer_func; // 传输函数(如 BT.709、sRGB 等)
__u8 reserved[7]; // 保留字段(对齐或扩展用)
} __attribute__ ((packed)); // 紧凑内存对齐(无填充字节)
** v4l2_plane_pix_format 结构体:**
struct v4l2_plane_pix_format {
__u32 sizeimage; // 当前平面的图像数据总大小(字节),包含所有行和可能的填充
__u32 bytesperline; // 每行的字节数
__u16 reserved[6]; // 保留字段
} __attribute__ ((packed)); // 紧凑内存对齐(无填充字节)
说明2:VIDIOC_TRY_FMT命令
VIDIOC_TRY_FMT命令和VIDIOC_S_FMT命令的唯一区别就是他不会修改驱动程序的状态,也就是说==VIDIOC_TRY_FMT命令只是尝试设置而不会真正设置图像格式==
1.6 申请缓冲区
ioctl VIDIOC_REQBUFS:申请 buffer,APP 可以申请多个 buffer,但是驱动程序不一定申请到
struct v4l2_requestbuffers rb;
memset(&rb, 0, sizeof(rb));
rb.count = 3;
rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
rb.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_REQBUFS, &rb);
printf("type = %d\n",rb.type);
printf("count = %d\n",rb.count);
printf("memory = %d\n", rb.memory);
打印信息
v4l2_requestbuffer结构体
struct v4l2_requestbuffers {
__u32 count; // 驱动程序会根据实际分配的缓冲区个数对其修改
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 reserved[2];
};
说明1:
- 如果执行
VIDIOC_REQBUFS命令时将count字段设置为0,表示释放已分配的所有缓冲区
1.7 映射缓冲区
ioctl VIDIOC_QUERYBUF 和 mmap:申请 buffer 信息、映射
- 如果申请到了
N个buffer,这个ioctl就应该执行N次 - 执行
mmap后,APP就可以直接读写这些buffer
struct v4l2_buffer buf;
struct v4l2_plane planes[VIDEO_MAX_PLANES];
for(int i = 0; i < rb.count; i++){
memset(&buf, 0, sizeof(buf));
memset(&planes, 0, sizeof(planes));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.m.planes = planes;
buf.length = 1;
ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
if(ret < 0){
printf("VIDIOC_QUERYBUF error\n");
goto err;
}
user_bufs[i].planes[0].len = planes[0].length;
user_bufs[i].planes[0].m.offset = planes[0].m.mem_offset;
user_bufs[i].planes[0].start = mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, planes[0].m.mem_offset);
if(user_bufs[i].planes[0].start == MAP_FAILED){
printf("Unable to map buffer\n");
goto err;
}
printf("index = %d\n", buf.index);
printf("type = %d\n", buf.type);
printf("memory = %d\n", buf.memory);
printf("length = %d\n", buf.length);
printf("sequence = %d\n", buf.sequence);
printf("plane: legnth = %d, bytesused = %d, mem_offset = 0x%x, start = %p\n",
planes[0].length, planes[0].bytesused,
planes[0].m.mem_offset, user_bufs[i].planes[0].start);
printf("\n");
}
打印信息
说明1:
-
multi-planar图像格式在VIDIOC_QUERYBUF控制命令中length字段的设置 -
对于
multi-planar图像格式,length字段设置的是v4l2_planes数组的成员个数 -
因为内核驱动是单平面,这里的
buf.length手动设置为1 -
内核会检查作为入参设置的
length字段,该值必须满足如下范围。可见此时传递的v4l2_planes数组成员个数可以超过图像格式实际的plane个数。后续在作为出参时,内核会对其进行修正 -
[buf.m.planes = 8]
-
在
VIDIOC_QUERYBUF控制命令返回时,内核会根据图像格式实际的plane个数修正作为出参的length字段
v4l2_buffer结构体
struct v4l2_buffer {
__u32 index; // 缓冲区序号(实际分配个数 - 1)
__u32 type; // 缓冲区类型(type == *_MPLANE 是多平面缓冲区)
// 缓冲区(有效载荷)中数据占用的字节数,多平面缓冲区未使用(设置为0)
__u32 bytesused;
__u32 flags; // 缓冲区信息标志
__u32 field; // 缓冲区中图像的字段顺序(enum v4l2_field)
struct timeval timestamp; // 缓冲区时间戳
struct v4l2_timecode timecode; // 缓冲区时间编码
__u32 sequence; // 缓冲区的序列计数
/* memory location */
__u32 memory; // 缓冲区内存类型(enum v4l2_memory)
union {
__u32 offset; // single-planar mmap类型缓冲区内存信息
unsigned long userptr; // single-planar userptr类型缓冲区内存信息
struct v4l2_plane *planes; // multi-planar 类型信息
__s32 fd; // single-planar dmabuf类型缓冲区内存信息
} m;
__u32 length;
__u32 reserved2;
__u32 reserved;
};
struct v4l2_plane {
__u32 bytesused; // 平面中数据占用的字节数(有效载荷)
__u32 length; // 这个平面的大小(不是有效载荷),以字节为单位
union {
__u32 mem_offset; // mmap 类型缓冲区内存信息
unsigned long userptr; // userptr 类型缓冲区内存信息
__s32 fd;
} m;
__u32 data_offset; // 数据在平面上的偏移量;通常为0,除非在数据前面有一个标题
__u32 reserved[11];
};
1.8 将缓冲区加入队列
ioctl VIDIOC_QBUF:把 buffer 放入“空间链表”
- 如果申请到了 N 个 buffer,这个 ioctl 就应该执行 N 次
for(int i = 0; i < rb.count; i++){
memset(&buf, 0, sizeof(buf));
memset(&planes, 0, sizeof(planes));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.m.planes = planes;
buf.length = 1;
ret = ioctl(fd, VIDIOC_QBUF, &buf);
if(ret < 0){
printf("VIDIOC_QBUF error\n");
goto err;
}
}
说明1:V4L2缓冲区管理概述
-
对于一个内核态的
vb2_queue结构体(也就是一个缓冲区队列),V4L2框架会维护2个缓冲区链表
queued_list:当前从用户空间排队的缓冲区列表。(对于图像采集设备,应用程序将缓冲区通过VIDIOC_QBUF操作加入queued_list,以便设备填充数据)done_list:准备退出用户空间队列的缓冲区列表。(对于图像采集设备,驱动程序会将填充好数据的缓冲区加入done_list,等待应用程序通过VIDIOC_DQBUF操作取出)
-
缓冲区在
queued_list和done_list链表之间的流转状态如下:- 分配的缓冲区既不在
queued_list,也不在done_list - 应用程序通过
VIDIOC_QBUF操作,将缓冲区加入queued_list,以便驱动程序使用 - 驱动程序从
queued_list获取缓冲区使用,在操作完成后(e.g.填充完图像后),将缓冲区再加入done_list - 应用程序通过
VIDIOC_DQBUF操作,将缓冲区同时从done_list和queued_list链表中取出使用
- 分配的缓冲区既不在
-
需要注意的是,
VIDIOC_QBUF操作和VIDIOC_DQBUF操作中传递v4I2_buffer结构体的目的,是让内核态V4L2框架可以通过用户态传递的v4l2_buffer结构体信息索引到相应的vb2_buffer结构体。因此用户态一般只需要定义一个v4l2_buffer结构体类型变量,在传递时设置相应的字段即可 -
缓冲区加入队列会将缓冲区的内存页锁定在物理内存中,这样这些页面就不会被交换到磁盘中,以便于硬件设备访问 (通常是DMA操作)
1.9 开启流传输
ioctl VIDIOC_STRTAMON:启动摄像头
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
1.10 采集图像
这里是个循环:使用 poll/select 监测 buffer,然后从“完成链表”中取出 buffer,处理再放入“空闲链表”
poll/select
memset(fds, 0, sizeof(struct fds));
fds[0].fd = fd;
fds[0].events = POLLIN;
poll(fds, 1, -1)
ioctl VIDIOC_DQBUF:从“完成链表”中取出buffer
for(int i = 0; i < rb.count; i++){
ret = poll(p, 1, -1);
memset(&buf, 0, sizeof(buf));
memset(&planes, 0, sizeof(planes));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.m.planes = planes;
buf.length = 1;
ret = ioctl(fd, VIDIOC_DQBUF, &buf);
说明1:
- 用户态在向
VIDIOC_DQBUF命令传递v4I2_buffer结构体地址时,不需要设置其中的memory字段,只需要设置type字段即可。 - 内核态在处理
VIDIOC_DQBUF命令时不会检查memory字段,并且会根据缓冲区队列的memory字段设置v4l2_buffer作为出参时的memory字
- 处理:前面使用
mmap映射了每个buffer的地址,处理时就可以直接使用地址来访问buffer
sprintf(file_name, "image%d.nv12", file_index++);
fp = fopen(file_name, "w+");
fwrite(user_bufs[buf.index].planes[0].start, planes[0].bytesused, 1, fp);
fclose(fp);
printf("index = %d\n", buf.index);
printf("type = %d\n", buf.type);
printf("memory = %d\n", buf.memory);
printf("length = %d\n", buf.length);
printf("sequence = %d\n", buf.sequence);
printf("plane: legnth = %d, bytesused = %d, mem_offset = 0x%x\n",
planes[0].length, planes[0].bytesused, planes[0].m.mem_offset);
printf(" data_offset = 0x%x, start = %p\n\n",
planes[0].data_offset,user_bufs[buf.index].planes[0].start);
打印信息:
说明1:
- 在将缓冲区中的图像写入文件时,应该使用
planes[0].bytesused字段,该字段标识缓冲区中有效数据的长度 - 对于
YUV类型图像格式,由于是非压缩类型,缓冲区中有效数据长度和缓冲区长度是一致的。如果将格式改为MJPEG这类压缩类型,则每帧图像有效数据的长度是不同的(但是都不会超过缓冲区长度)
说明2:
- 捕获到的图像需使用支持YUV的工具打开
- 可使用 YuvPlayer 软件查看,需设置对应的像素和分辨率
ioctl VIDIOC_QBUF:把buffer放入“空闲链表”
ioctl(fd, VIDIOC_QBUF, &buf);
说明2:
- 当映射了多个缓冲区时,将
buff放回空闲链表时,需指定buffer所对应的索引 - 此处是封装在一个函数里,可以不用设置
1.11 关闭流传输
ioctl VIDIOC_STREAMOFF:停止摄像头
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMOFF, &type);
二、程序源码
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)