V4L2框架简介
市面上有非常多型号的摄像头,各个摄像头的结构,性能各不相同,如果每个摄像头的使用程序各不相同,就对Linux的应用层开发造成了很大限制,更换一个摄像头就要重新写一遍程序,而V4L2框架就很好了解决了这个问题,V4L2(Video for Linux 2)是Linux内核中用于管理视频设备的标准化驱动框架,为上层应用程序提供统一的编程接口(如ioctl系统调用),支持各类视频输入,使得应用层开发中不必纠结摄像头的底层驱动方法,通过V4L2的API即可使用各种不同型号、品牌的摄像头。相应的,各个摄像头厂家只需要统一去适配V4L2框架即可。
驱动流程
VIDEO设备文件
在Linux系统中有一句话叫一切皆文件,摄像头也不例外,插入摄像头之后,系统会注册一个video设备文件,路径在/dev下,如果是USB免驱摄像头,遵循的是UVC协议,Linux系统默认支持UVC协议,会直接注册一个video文件;如果使用的是ov系列摄像头,通过MIPI-CSI传输的,就需要修改设备树进行适配,否则无法注册为video文件。本文以PC下的虚拟机为例,连接USB免驱摄像头。
查询video设备和支持的格式
使用命令行查询
使用命令可以查看系统内的video设备
v4l2-ctl --list-devices
#显示所有摄像头节点
可以看到这里有三个设备,但是我实际只插了一个摄像头,其中能被V4L2调用的只有video0,调用video1则显示没有支持的格式
查看对应video设备支持的格式、分辨率、最大帧率 、
v4l2-ctl --list-formats-ext -d /dev/video0
我的摄像头支持MJPG和YUYV两种格式,以及对应的分辨率和帧率信息。
使用程序查询
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main(int argc, char const *argv[])
{
int video_fd,tmp;
video_fd = open("/dev/video0",O_RDWR);
int i = 0;
struct v4l2_fmtdesc fmt_get;
fmt_get.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (1)
{
fmt_get.index = i++;
tmp = ioctl(video_fd,VIDIOC_ENUM_FMT,&fmt_get);
if(tmp < 0)
break;
printf("index=%d\n", fmt_get.index);
printf("description=%s\n", fmt_get.description);
unsigned char *p = (unsigned char *)&fmt_get.pixelformat;
printf("pixelformat=%c%c%c%c\n", p[0],p[1],p[2],p[3]);
}
close(video_fd);
return 0;
}
ioctl的第二个参数可以为不同的命令,对应第三个参数也有不同的数据结构,我们可以通过点击第二个参数然后跳转过去,就可以看到对应的第三个参数的数据结构。
我们这里使用的是VIDIOC_ENUM_FMT,对应的是struct v4l2_fmtdesc结构体
定义如下
struct v4l2_fmtdesc {
__u32 index; /* Format number */
__u32 type; /* enum v4l2_buf_type */
__u32 flags;
__u8 description[32]; /* Description string */
__u32 pixelformat; /* Format fourcc */
__u32 mbus_code; /* Media bus code */
__u32 reserved[3];
};
其中在查询之前需要设置index和type,index是要查询的格式的编号,由0开始,type是格式类型,我们这里是查询摄像头所以是图像采集,设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,有部分摄像头是多图层采集,就需要设置为V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE。
description描述了格式信息,pixelformat描述了对应格式的四字符代码,比如MJPEG就用MJPG四个字符表示,XRGB8888格式就用BX24四个字符表示
总结查询的流程大致为打开video设备文件,设置struct v4l2_fmtdesc的格式为捕获和设置查询的格式编号,因为同一个摄像头可能支持不同格式,然后调用ioctl查询,如果返回值小于0则查询错误,即没有编号对应的格式,对应到本程序中就是已经查询到了所有支持的格式
设置摄像头参数
设置摄像头参数使用的ioctl命令是VIDIOC_S_FMT,跳转之后查看对应的结构体为struct v4l2_format
struct v4l2_format {
__u32 type;
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */
struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
struct v4l2_sdr_format sdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */
struct v4l2_meta_format meta; /* V4L2_BUF_TYPE_META_CAPTURE */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
成员包括一个type,我的摄像头是单平面采集,所以设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,下边是一个共用体,单平面采集应该设置struct v4l2_pix_format pix
struct v4l2_pix_format {
__u32 width;
__u32 height;
__u32 pixelformat;
__u32 field; /* enum v4l2_field */
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
__u32 colorspace; /* enum v4l2_colorspace */
__u32 priv; /* private data, depends on pixelformat */
__u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */
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 */
};
这里成员繁多,需要设置宽,高,像素格式和图像占用的内存大小,也可以不设置大小,后续申请缓冲区时由驱动自行分配。程序如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#define WIDTH 1280
#define HEIGHT 720
int main(int argc, char const *argv[])
{
int video_fd,tmp;
video_fd = open("/dev/video0",O_RDWR);
struct v4l2_format fmt_set;
fmt_set.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt_set.fmt.pix.width = WIDTH;
fmt_set.fmt.pix.height = HEIGHT;
fmt_set.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt_set.fmt.pix.sizeimage = WIDTH * HEIGHT;
tmp = ioctl(video_fd,VIDIOC_S_FMT,&fmt_set);
if(tmp < 0)
perror("ioctl");
else
printf("设置成功\n");
close(video_fd);
return 0;
}
申请内核缓冲区队列
申请内核缓冲区队列用到的ioctl命令是VIDIOC_REQ_BUFS,跳转之后查询到对应的结构体为struct v4l2_requestbuffers
struct v4l2_requestbuffers {
__u32 count;
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 capabilities;
__u32 reserved[1];
};
count代表申请的内核缓冲区个数,一般为4-8,或者更高,调用ioctl之后会被设置为实际申请到的缓冲区个数。type表示缓冲区类型,这里设置为视频采集,memory代表内存分配模式,一般选择为V4L2_MEMORY_MMAP内存映射模式和V4L2_MEMORY_DMABUF使用硬件DMA加速,这里选择内存映射模式。capalilities是返回的缓存区特性,比如支持内存映射或者支持DMA传输等,reserved只能为0。
struct v4l2_requestbuffers req_bufs;
memset(&req_bufs,0,sizeof(req_bufs));
req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req_bufs.count = BUF_CNT; //缓冲区个数
req_bufs.memory = V4L2_MEMORY_MMAP;
tmp = ioctl(video_fd,VIDIOC_REQBUFS,&req_bufs);
if(tmp < 0)
perror("req_bufs");
else
printf("实际申请到的缓冲区个数:%d\n",req_bufs.count);
内存映射
将内核申请到的缓冲区映射到用户空间,这样摄像头采集到的数据在写入内核的缓冲区中时就会同步到用户空间,用户空间就可以像访问数组一样访问采集到的图像数据。
在映射之前需要先查询内核缓冲区,查询到之后再进行映射,申请了多个缓冲区就需要多次查询多次映射。查询内核空间需要用到的ioctl命令为VIDIOC_QUERYBUF,跳转之后查询到对应的结构体参数为struct v4l2_buffer
struct v4l2_buffer {
__u32 index;
__u32 type;
__u32 bytesused;
__u32 flags;
__u32 field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence;
/* memory location */
__u32 memory;
union {
__u32 offset;
unsigned long userptr;
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length;
__u32 reserved2;
union {
__s32 request_fd;
__u32 reserved;
};
};
需要设置的成员包括type,index,memory。type这里仍然设置为单平面采集,memory设置为映射,index为序号,从0开始。比如之前申请到了8个缓冲区,就需要查询并映射8次,用一个for循环来控制,index赋值为i,每次调用ioctl使用VIDIOC_QUERYBUF。在调用之后就要进行mmap内存映射,返回应用层的数据存放地址。内存映射需要用到长度,偏移量参数,这些参数在调用ioctl之后会被内核赋值。长度使用length成员,偏移量为成员m中的offset。注意,内存映射传入的文件描述符是video的文件描述符,不是成员m中的fd。
查询内核缓冲区也相当于一次出队,在完成内存映射之后需要将其入队,使用的ioctl命令是VIDIOC_QBUF,对应的结构体参数和查询一样,都为struct v4l2_buffer,此时不需要修改任何参数,直接调用ioctl即可完成入队。
struct v4l2_buffer query_buf;
memset(&query_buf,0,sizeof(query_buf));
query_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
query_buf.memory = V4L2_MEMORY_MMAP;
for (int i = 0; i < req_bufs.count; i++)
{
query_buf.index = i;
tmp = ioctl(video_fd,VIDIOC_QUERYBUF,&query_buf);
if(tmp < 0)
perror("querybuf");
//存储申请到的长度信息,用于释放资源时的munmap
video_map_len[i] = query_buf.length;
video_mpaddr[i] = mmap(NULL,query_buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,video_fd,query_buf.m.offset);
//查询后入队
tmp = ioctl(video_fd,VIDIOC_QBUF,&query_buf);
}
开启数据流
在完成像素大小和图像格式的设置以及分配好缓冲区之后就可以开启数据流了,开启数据流之后,硬件驱动层就会开始向缓冲区内写入数据,每个缓冲区存都会存放完整的一帧图像数据。前提是缓冲区已经通过QBUF入队,如果在上一步内存映射中最后没有进行入队,那么硬件就无法向缓冲区写入数据。
开启数据流用到的ioctl命令为VIDIOC_STREAMON,对应的第三个参数为int类型,需要传入的是type参数。
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tmp = ioctl(video_fd,VIDIOC_STREAMON,&type);
获取一帧图像数据
应用层想要获取采集到的每一帧图像数据,就需要调用ioctl的DQBUF命令进行出队,此时内核会选择一个已经填充了图像数据的缓冲区进行出队,之后需要调用QBUF命令将该缓冲区进行入队,使硬件驱动层可以继续向其中填充数据。
通过跳转到DQBUF命令的定义,可以看到对应的结构体定义和QUERY、QBUF相同,均为struct v4l2_buffer
在出队之前只需要设置memory和type即可,调用ioctl的DQBUF之后,内核会返回出队的缓冲区编号index,和这一帧图像占用的内存大小byteused。申请到的缓冲区已经映射到了应用层,其首地址存储在了指针数组video_mpaddr中,通过编号index作为索引即可获取到图像数据。在申请内核空间时我们申请的内存大小是1920*1080,但一帧图像并不一定会将缓冲区全部填满。以MJPG举例,其每一帧图像均为JPEG格式,JPEG是一种压缩格式,例如大小为1920*1080,但是JPEG的实际内存大小并没有1920*1080字节那么大,所以返回的byteused才是图像占用的内存大小。
入队只需要调用QBUF并传入出队时使用的结构体即可,不需要修改参数。
V4L2维护了两个队列,一个是IN队列,一个OUT队列。IN队列存放通过QBUF入队的缓冲区,硬件可以向其中写入数据。OUT队列存放的是已经被填充数据的缓冲区,等待通过DQBUF来取出数据,即出队。在开启数据流之后,通过不断的出队入队来循环采集每一帧图像,就形成了视频流。
struct v4l2_buffer read_buf;
memset(&read_buf,0,sizeof(read_buf));
read_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
read_buf.memory = V4L2_MEMORY_MMAP;
ioctl(video_fd,VIDIOC_DQBUF,&read_buf);
FILE *file;
file = fopen("my.jpeg","wb+");
//内核会返回使用的缓冲区编号和图像大小
fwrite(video_mpaddr[read_buf.index],1,read_buf.bytesused,file);
fclose(file);
printf("实际的图像大小:%d,写入的图像大小:%ld\n",read_buf.bytesused,cnt);
ioctl(video_fd,VIDIOC_QBUF,&read_buf);
关闭数据流
关闭数据流之后,硬件就不会再向缓冲区内写入数据,同时清空缓冲区队列,此时如果调用ioctl的DQBUF会返回错误码。
关闭数据流使用的ioctl命令是VIDIOC_STREAMOFF,对应的第三个参数为int型,和开启数据流相同,同样传入type参数。
释放资源
释放资源包括解除内存映射并关闭文件描述符。解除内存映射需要逐个解除每个缓冲区的映射。
//解除内存映射
for (int i = 0; i < req_bufs.count; i++)
{
tmp = munmap(video_mpaddr[i],video_map_len[i]);
if(tmp < 0)
perror("munmap");
}
//释放文件描述符
close(video_fd);
使用实例
以下是完整的V4L2使用流程,拍摄一张图片并保存为jpeg格式文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#define WIDTH 1280
#define HEIGHT 720
#define BUF_CNT 8
unsigned char * video_mpaddr[BUF_CNT];
u_int32_t video_map_len[BUF_CNT];
int main(int argc, char const *argv[])
{
int video_fd,tmp;
//打开video设备文件
video_fd = open("/dev/video0",O_RDWR);
if(video_fd < 0)
perror("open");
struct v4l2_format fmt_set;
memset(&fmt_set,0,sizeof(fmt_set));
//设置采集方式,宽,高,像素格式,缓冲区大小
fmt_set.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt_set.fmt.pix.width = WIDTH;
fmt_set.fmt.pix.height = HEIGHT;
fmt_set.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt_set.fmt.pix.sizeimage = WIDTH * HEIGHT;
tmp = ioctl(video_fd,VIDIOC_S_FMT,&fmt_set);
if(tmp < 0)
perror("set_fmt");
else
printf("设置成功\n");
//申请内核缓冲区
struct v4l2_requestbuffers req_bufs;
memset(&req_bufs,0,sizeof(req_bufs));
req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req_bufs.count = BUF_CNT; //缓冲区个数
req_bufs.memory = V4L2_MEMORY_MMAP;
tmp = ioctl(video_fd,VIDIOC_REQBUFS,&req_bufs);
if(tmp < 0)
perror("req_bufs");
else
printf("实际申请到的缓冲区个数:%d\n",req_bufs.count);
//查询内核缓冲区并逐个映射,入队
struct v4l2_buffer query_buf;
memset(&query_buf,0,sizeof(query_buf));
query_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
query_buf.memory = V4L2_MEMORY_MMAP;
for (int i = 0; i < req_bufs.count; i++)
{
query_buf.index = i;
tmp = ioctl(video_fd,VIDIOC_QUERYBUF,&query_buf);
if(tmp < 0)
perror("querybuf");
//存储申请到的长度信息,用于释放资源时的munmap
video_map_len[i] = query_buf.length;
video_mpaddr[i] = mmap(NULL,query_buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,video_fd,query_buf.m.offset);
//查询后入队
tmp = ioctl(video_fd,VIDIOC_QBUF,&query_buf);
if(tmp < 0)
perror("qbuf");
}
//开启数据流
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tmp = ioctl(video_fd,VIDIOC_STREAMON,&type);
if(tmp < 0)
perror("streamon");
//读取一帧数据,出队
struct v4l2_buffer read_buf;
memset(&read_buf,0,sizeof(read_buf));
read_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
read_buf.memory = V4L2_MEMORY_MMAP;
tmp = ioctl(video_fd,VIDIOC_DQBUF,&read_buf);
if(tmp < 0)
perror("dqbuf");
FILE *file;
file = fopen("my.jpeg","wb");
//内核会返回使用的缓冲区编号和图像大小
ssize_t cnt = fwrite(video_mpaddr[read_buf.index],1,read_buf.bytesused,file);
fclose(file);
printf("实际的图像大小:%d,写入的图像大小:%ld\n",read_buf.bytesused,cnt);
printf("%d %d %d %d\n",video_mpaddr[read_buf.index][0],video_mpaddr[read_buf.index][1],video_mpaddr[read_buf.index][read_buf.bytesused - 2],video_mpaddr[read_buf.index][read_buf.bytesused - 1]);
//入队
tmp = ioctl(video_fd,VIDIOC_QBUF,&read_buf);
if(tmp < 0)
perror("qbuf");
//关闭数据流
tmp = ioctl(video_fd,VIDIOC_STREAMOFF,&type);
if(tmp < 0)
perror("streamoff");
//解除内存映射
for (int i = 0; i < req_bufs.count; i++)
{
tmp = munmap(video_mpaddr[i],video_map_len[i]);
if(tmp < 0)
perror("munmap");
}
//释放文件描述符
close(video_fd);
return 0;
}
拍摄的图片如下,实际使用中由于摄像头的不同,可能有一个预热的过程,采集的前几帧图像可能有一些异常,可以尝试丢弃前几帧图像。