【嵌入式Linux应用开发 | V4L2】摄像头V4L2应用程序开发(USB免驱摄像头)
一、V4L2简介
Linux系统上的Video设备多种多样,如通过Camera Host控制器接口连接的摄像头,通过USB总线连接的摄像头等。为了兼容更多的硬件,Linux内核抽象了V4L2(Video for Linux Two)子系统。V4L2子系统是Linux内核中关于Video(视频)设备的API接口,是V4L(Video for Linux)子系统的升级版本。V4L2子系统向上为虚拟文件系统提供了统一的接口,应用程序可通过虚拟文件系统访问Video设备。V4L2子系统向下给Video设备提供接口,同时管理所有Video设备。Video设备又分为主设备和从设备,对于Camera来说,Camera Host控制器为主设备,负责图像数据的接收和传输,从设备为Camera Sensor,一般为I2C接口,可通过从设备控制Camera采集图像的行为,如图像的大小、图像的FPS等。
1.1 概述
V4L2 应用编程所需的宏定义和数据结构可通过如下头文件包含:
#include <linux/videodev2.h>
二、 程序编写流程
- 本文借鉴麦兜(chenchengwudi@sina.com)的笔记从用户空间使用V4L2子系统。
- 下文以泰山派RK3566开发板为例,系统为Buildroot,虚拟机使用Ubuntu18.04,摄像头使用USB免驱摄像头。
- MIPI摄像头应用编写可查看 嵌入式Linux应用开发——摄像头V4L2应用程序开发(MIPI摄像头)
- 嵌入式Linux学习交流群:
1005210698- 本文相关源码以及更多免费资料可加群获取
2.1 打开设备节点
open:打开设备节点 /dev/videoX
fd = open("/dev/video1", O_RDWR);
对于
V4L2设备,也可以在open函数中指定O_NONBLOCK标志,即以非阻塞模式打开。在非阻塞模式下,如果在进行ioctl VIDIOC_DQBUF操作时没有已经就绪的缓冲区,则应用程序不会阻塞,而是立即返回-1,并且将errno置为EAGAIN
2.2 查询设备能力
ioctl VIDIOC_QUERYCAP:Query Capbility,查询设备能力,比如
- 确认是否是“捕获设备”,因为有些节点是输出设备
- 确认是否支持
mmap操作,还是仅支持read/write操作
struct v4l2_capability cap;
memset(&cap, 0, sizeof(struct v4l2_capability));
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);
if(!(cap.capability & V4L2_CAP_VIDEO_CAPTURE)){
printf("%s is nor a video capture device\n", cap.card);
return -1;
}
}
打印信息
说明1: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]; // 为将来的扩展保留字段
};
说明2: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.3 枚举设备格式
ioctl VIDIOC_ENUM_FMT:枚举设备支持的格式
struct v4l2_fmtdesc fmtdesc;
memset(&fmtdesc, 0, sizeof(struct v4l2_fmtdesc));
fmtdesc.index = 0;
// 设置缓冲区类型为视频捕获
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while(1){
fmtdesc.index = i++;
ret = ioctl(fd, VIDEOC_ENUM_FMT, &fmtdesc);
if (ret < 0) {
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);
}
打印信息
说明1:v4l2_fmtdesc结构体
struct v4l2_fmtdesc {
__u32 index; /* 格式枚举序号 */
__u32 type; /* 缓冲区类型 */
__u32 flags;
__u8 description[32]; /* 描述 */
__u32 pixelformat; /* 像素格式 */
__u32 reserved[4];
};
说明2:type 缓冲区类型
从实现逻辑上来说,是驱动程序先指定
vb2_queue结构体的缓冲区类型,之后应用程序需要指定相应的缓冲区类型才能索引到正确的vb2_queue结构体

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

- 实验硬件设备支持格式的标志flags字段值如下,
MJPEG格式的flags字段值为0x1,即MJPEG格式是一种压缩格式YUYV格式的flags字段值为0x0,即YUYV格式既不是压缩格式,也不是软件模拟格式
2.4 枚举指定格式分辨率
ioctl VIDIOC_ENUM_FRAMESIZES:枚举指定格式的分辨率
struct v4l2_frmsizeenum fsenum;
frame_index = 0;
while(1){
memset(&fsenum, 0, sizeof(struct v4l2_frmsizeenum));
fsenum.pixel_format = fmtdesc.pixelformat;
fsenum.index = frame_index++;
if(0 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum)){
printf("index = %d\n", fsenum.index);
switch(fsenum.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;
default:
printf("invalid type\n");
break;
}
printf("分辨率:%d x %d\n\n",fsenum.discrete.width, fsenum.discrete.height);
}
else
break;
}
打印信息
说明1v4l2_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; // 帧高步长[像素]
};
说明2:关于分辨率
分辨率类型由 v4l2_frmsizetypes 枚举值指定,具体枚举值如下
- 离散型分辨率:设备支持的分辨率由一系列离散值构成
- 阶梯型分辨率:设备支持的分辨率在指定的范围内以步长为单位递增
- 连续型分辨率:连续型分辨率可以理解为阶梯型分辨率的特例,即步长为1的阶梯型分辨率
2.5 设置图像格式
ioctl VIDIOC_S_FMT:在上面枚举出的格式里,选择一个来设置格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(struct v4l2_format));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1024; // 设置视频捕获的宽度为 1024 像素
fmt.fmt.pix.height = 768; // 设置视频捕获的高度为 768 像素
// 设置像素格式为 MJPEG(Motion JPEG)
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
// 设置视频字段类型为任意,驱动程序将选择最适合的字段类型(如逐行扫描或隔行扫描)
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.width);
printf("height = %d\n",fmt.fmt.pix.height);
printf("pixelformat = %c.%c.%c.%c\n",fmt.fmt.pix.pixelformat & 0xFF,
(fmt.fmt.pix.pixelformat >>8) & 0xFF,
(fmt.fmt.pix.pixelformat >>16) & 0xFF,
(fmt.fmt.pix.pixelformat >>24) & 0xFF);
printf("field = 0x%x\n", fmt.fmt.pix.field);
printf("bytesperline = %d\n", fmt.fmt.pix.bytesperline);
printf("sizeimage = %d\n", fmt.fmt.pix.sizeimage);
printf("colorspace = %d\n", fmt.fmt.pix.colorspace);
printf("priv = 0x%x\n",fmt.fmt.pix.priv);
if(fmt.fmt.pix.priv==V4L2_PIX_FMT_PRIV_MAGIC){
printf("flags = 0x%x\n",fmt.fmt.pix.flags);
printf("ycbcr_enc = %d\n", fmt.fmt.pix.ycbcr_enc);
printf("quantization = %d\n",fmt.fmt.pix.quantization);
printf("xfer_func = %d\n", fmt.fmt.pix.xfer_func);
}
打印信息
说明1: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;
};
说明2:v4l2_pix_format结构体
struct v4l2_pix_format {
// 图像分辨率
__u32 width;
__u32 height;
__u32 pixelformat; // fourcc图像格式编码
__u32 field; // 图像的场格式
__u32 bytesperline; // 图像的跨距
// 缓冲区内存大小,用于存储一帧完整的图像,由驱动程序设置
// 对于压缩格式,则是存储码流的缓冲区内存大小
__u32 sizeimage;
__u32 colorspace; // 图像颜色空间
__u32 priv; // 扩展字段使用
__u32 flags; // 格式标志位
union {
/* enum v4l2_ycbcr_encoding */
__u32 ycbcr_enc;
/* enum v4l2_hsv_encoding */
__u32 hsv_enc;
};
// 量化范围
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
说明3:图像存储方式
YUYV格式的存储方式和采样方式如下图,详细参考2.7.12. V4L2_PIX_FMT_YUYV (‘YUYV’)
- 以图像格式位
YUYV为例,分辨率在1280* 720的情况下,驱动程序填充了bytesperline和sizeimage字段,其中:bytesperline= 2560 (= 1280 * 2,YUYV格式存储每个像素需要2B)sizeimage= 2764800 (= 1280 * 720 * 2,YUYV格式存储每个像素需要2B)
- 以图像格式为
MJPEG,由于码流没有跨距的概念,所以驱动程序将bytesperline字段设置为0;而sizeimage= 1280 * 720 * 2 的方式计算,这是因为MJPEG压缩格式的图像大小肯定不会大于YUYV格式的图像
说明4:VIDIOC_TRY_FMT命令
VIDIOC_TRY_FMT命令和VIDIOC_S_FMT命令的唯一区别就是他不会修改驱动程序的状态,也就是说VIDIOC_TRY_FMT命令只是尝试设置而不会真正设置图像格式
2.6 申请缓冲区
ioctl VIDIOC_REQBUFS:申请 buffer,APP 可以申请多个 buffer,但是驱动程序不一定申请到
struct v4l2_requestbuffers rb;
memset(&rb, 0, sizeof(struct v4l2_requestbuffers));
rb.count = BUF_COUNT;
rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
#if define(MMAP_BUF)
rb.memory = V4L2_MEMORY_MMAP;
#elif defined(USERPTR_BUF)
rb.memory = V4L2_MEMORY_USERPTR;
#elif defined(DMABUF_BUF)
rb.memory = V4L2_MEMORY_DMABUF;
#endif
ioctl(fd, VIDIOC_REQBUFS, &rb);
printf("type = %d\n",rb.type);
printf("count = %d\n",rb.count);
printf("memory = %d\n", rb.memory);
打印信息
说明1:v4l2_requestbuffer结构体
struct v4l2_requestbuffers {
__u32 count; // 驱动程序会根据实际分配的缓冲区个数对其修改
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 reserved[2];
};
如果执行
VIDIOC_REQBUFS命令时将count字段设置为0,表示释放已分配的所有缓冲区
2.7 映射缓冲区
ioctl VIDIOC_QUERYBUF 和 mmap:申请 buffer 信息、映射
- 如果申请到了
N个buffer,这个ioctl就应该执行N次 - 执行
mmap后,APP就可以直接读写这些buffer
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(struct v4l2_buffer));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
bufs[i] = mmap(0, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
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("bytesused = %d\n", buf.bytesused);
printf("offset = 0x%x\n", buf.m.offset);
printf("sequence = %d\n", buf.sequence);
打印信息
说明1: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];
};
2.8 将缓冲区加入队列
ioctl VIDIOC_QBUF:把 buffer 放入“空间链表”
- 如果申请到了
N个buffer,这个ioctl就应该执行N次
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(struct v4l2_buffer));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_QBUF, &buf)
说明1:缓冲区管理概述
- 对于一个内核态的
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操作)
4.9 开启流传输
ioctl VIDIOC_STRTAMON:启动摄像头
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
4.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
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(struct v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf)
用户态在向
VIDIOC_DQBUF命令传递v4I2_buffer结构体地址时,不需要设置其中的memory字段,只需要设置type字段即可。内核态在处理VIDIOC_DQBUF命令时不会检查memory字段,并且会根据缓冲区队列的memory字段设置v4l2_buffer作为出参时的memory字
- 处理:前面使用
mmap映射了每个buffer的地址,处理时就可以直接使用地址来访问buffer
sprintf(filename, "video_raw_data_%04d.jpg", file_cnt++);
int fd_file = open(filename, O_RDWR | O_CREAT, 0666);
printf("capture to %s\n", filename);
write(fd_file, bufs[buf.index], buf.bytesused);
close(fd_file);
- 在将缓冲区中的图像写入文件时,应该使用
bytesused字段,该字段标识缓冲区中有效数据的长度 - 对于
YUV类型图像格式,由于是非压缩类型,缓冲区中有效数据长度和缓冲区长度是一致的。如果将格式改为MJPEG这类压缩类型,则每帧图像有效数据的长度是不同的(但是都不会超过缓冲区长度)
ioctl VIDIOC_QBUF:把buffer放入“空闲链表”
ioctl(fd, VIDIOC_QBUF, &buf);
2.11 关闭流传输
ioctl VIDIOC_STREAMOFF:停止摄像头
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMOFF, &type);
三、程序源码
程序源码(是封装好的V4L2应用代码)
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)