目录
一、V4L2简介
二、V4L2操作流程
1.打开摄像头
2.查询设备的属性/能力/功能
3.获取摄像头支持的格式
4.设置摄像头的采集通道
5.设置/获取摄像头的采集格式和参数
6.申请帧缓冲、内存映射、入队
(1)申请帧缓冲
(2)内存映射
(3)入队
7.开启视频采集
8.读取数据、对数据进行处理
9.结束视频采集
三、应用编程
V4L2(Video for linux two)是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范。使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X 标准一个数字编号:/dev/videox),每一个 videoX 设备文件就代表一个视频类设备。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备类设备。
V4L2摄像头驱动框架的访问是通过系统IO的接口 ------ ioctl函数,ioctl专用于硬件控制的系统IO的接口。
#include
//包含头文件
int ioctl(int fd, unsigned long request, ...);
fd: 文件描述符
request: 此参数与具体要操作的对象有关, 表示向文件描述符请求相应的操作
...: 可变参函数, 第三个参数需要根据 request 参数来决定,配合 request 来使用
返回值: 成功返回 0,失败返回-1
ioctl()是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,可以通过 ioctl 获取外设相关信息。通过 ioctl()来完成,搭配不同的 V4L2 指令(request
参数)请求不同的操作,这些指令定义在头文件 linux/videodev2.h 中,常用的如下图。
视频类设备对应的设备节点为/dev/videoX, X 为数字编号,通常从 0 开始,调用 open 打开,得到文件描述符 fd。
int fd = -1;
fd = open("/dev/video0", O_RDWR);
if (0 > fd)
{
perror( "open error");
exit(-1);
}
打开设备后,需要查询设备的属性,确定该设备是否是一个视频采集类设备。通过 ioctl()将获取到一个 struct v4l2_capability 类型数据, struct v4l2_capability 数据结构描述了设备的一些属性,结构体定义如下:
struct v4l2_capability {
__u8 driver[16]; /* 驱动的名字 */
__u8 card[32]; /* 设备的名字 */
__u8 bus_info[32]; /* 总线的名字 */
__u32 version; /* 版本信息 */
__u32 capabilities; /* 设备拥有的能力 */
__u32 device_caps;
__u32 reserved[3]; /* 保留字段 */
};
这里关注capabilities 字段,该字段描述了设备拥有的能力,该字段的值如下
所以可以通过判断 capabilities字段是否包含 V4L2_CAP_VIDEO_CAPTURE、 来确定它是否是一个摄像头设备。
struct v4l2_capability cap = {};
ioctl(fd,VIDIOC_QUERYCAP,&cap);
if(cap.capabilities&V4L2_CAP_VIDEO_CAPTURE)
{
//是一个摄像头
}
获取支持的像素格式使用 VIDIOC_ENUM_FMT 指令
struct v4l2_fmtdesc {
__u32 index; /*格式编号*/
__u32 type; /*摄像头的格式 V4L2_BUF_TYPE_VIDEO_CAPTURE*/
__u32 flags;
__u8 description[32]; /*描述信息:描述 pixelformat 像素格式。*/
__u32 pixelformat; /*类型格式 --- 4字节:像素格式编号*/
__u32 reserved[4];
};
pixelfoemat像素格式:
//定义格式的宏
#define v4l2_fourcc(a, b, c, d)\
((__u32)(a) | ((__u32)(b) << 8) | ((__u32)(c) << 16) | ((__u32)(d) << 24))
#define V4L2_PIX_FMT_YUYV v4l2_fourcc('Y', 'U', 'Y', 'V') /* 16 YUV 4:2:2 */
#define V4L2_PIX_FMT_YVYU v4l2_fourcc('Y', 'V', 'Y', 'U') /* 16 YVU 4:2:2 */
#define V4L2_PIX_FMT_MJPEG v4l2_fourcc('M', 'J', 'P', 'G') /* Motion-JPEG */
#define V4L2_PIX_FMT_JPEG v4l2_fourcc('J', 'P', 'E', 'G') /* JFIF JPEG */
v4l2_fourcc是 宏定义,通过这个宏以及对应的参数合成的一个u32 类型数据。
type类型:
获取设备的哪种功能对应的像素格式, 因为有些设备它可能即支持视频采集功能、又支持视频输出等其它的功能;
enum v4l2_buf_type {
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, //视频采集
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2, //视频输出
V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,
V4L2_BUF_TYPE_VBI_CAPTURE = 4,
V4L2_BUF_TYPE_VBI_OUTPUT = 5,
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10,
V4L2_BUF_TYPE_SDR_CAPTURE = 11,
V4L2_BUF_TYPE_SDR_OUTPUT = 12,
V4L2_BUF_TYPE_META_CAPTURE = 13,
/* Deprecated, do not use */
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
type 字 段 需 要 在 调 用 ioctl() 之 前 设 置 它 的 值 , 对 于 摄 像 头 , 需 要 将 type 字 段 设 置 为V4L2_BUF_TYPE_VIDEO_CAPTURE,指定将要获取的是视频采集的像素格式。
struct v4l2_fmtdesc fmt = {};
fmt.index = 0;//第一种格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//获取摄像头的格式
ioctl(fd,VIDIOC_ENUM_FMT,&fmt);
int index = 0;//使用通道0
ioctl(fd,VIDIOC_S_INPUT,&index);
使用 VIDIOC_G_FMT 指令查看设备当期的格式和使用 VIDIOC_S_FMT 指令设置设备的格式;
int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *fmt);
ioctl()会将获取到的数据写入到 fmt 指针所指向的对象中或者会使用 fmt 所指对象的数据去设置设备的格式, struct v4l2_format 结构体描述了格式相关的信息。
struct v4l2_format {
__u32 type;//V4L2_BUF_TYPE_VIDEO_CAPTURE
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 */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
struct v4l2_pix_format {
__u32 width;//像素宽度
__u32 height;//像素高度
__u32 pixelformat;//采集格式 V4L2_PIX_FMT_YVYU
__u32 field; /* V4L2_FIELD_NONE */
__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_*) */
__u32 ycbcr_enc; /* enum v4l2_ycbcr_encoding */
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
type 字段依然与前面介绍的结构体中的 type 字段意义相同,不管是获取格式、还是设置格式都需要在调用 ioctl()函数之前设置它的值。接下来是一个 union 共用体,当 type 被设置为V4L2_BUF_TYPE_VIDEO_CAPTURE 时, pix 变量生效,它是一个 struct v4l2_pix_format 类型变量,记录了视频帧格式相关的信息。
获取当前的格式、并设置格式:
struct v4l2_format format = {};
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
format.fmt.pix.width = 640;
format.fmt.pix.height = 480;
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
format.fmt.pix.field= V4L2_FIELD_NONE ;
ioctl(fd,VIDIOC_S_FMT,&format); //S:set
读取摄像头数据的方式有两种:一种是 read 方式,直接通过 read()系统调用读取摄像头采集到的数据;另一种是 streaming 方式。使用 VIDIOC_QUERYCAP 指令查询设备的属性、得到一个 struct v4l2_capability 类型数据, 其中 capabilities 字段记录了设备拥有的能力,当该字段包含
V4L2_CAP_READWRITE 时,表示设备支持 read I/O 方式读取数据;当该字段包含V4L2_CAP_STREAMING时,表示设备支持 streaming I/O 方式;使用streaming I/O 方式,需要向设备申请帧缓冲,并将帧缓冲映射到应用程序进程地址空间中。
使用 VIDIOC_REQBUFS 指令可申请帧缓冲:
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);
调用 ioctl()需要传入一个 struct v4l2_requestbuffers *指针, struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息, ioctl()会根据 reqbuf 所指对象填充的信息进行申请。
struct v4l2_requestbuffers {
__u32 count;//缓冲区块数 ----- 4
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 reserved[2];
};
type 字段与前面所提及到的 type 字段意义相同,count 字段用于指定申请帧缓冲的数量。
memory 字段可取值如下:
enum v4l2_memory {
V4L2_MEMORY_MMAP = 1,
V4L2_MEMORY_USERPTR = 2,
V4L2_MEMORY_OVERLAY = 3,
V4L2_MEMORY_DMABUF = 4,
};
通常将 memory 设置为 V4L2_MEMORY_MMAP 。
申请缓存:
struct v4l2_requestbuffers req = {};
req.count = 4; //申请4个帧缓存
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd,VIDIOC_REQBUFS,&req);
streaming I/O 方式会在内核空间中维护一个帧缓冲队列, 驱动程序会将从摄像头读取的一帧数据写入到队列中的一个帧缓冲,接着将下一帧数据写入到队列中的下一个帧缓冲;当应用程序需要读取一帧数据时,需要从队列中取出一个装满一帧数据的帧缓冲,这个取出过程就叫做出队;当应用程序处理完这一帧数据后,需要再把这个帧缓冲加入到内核的帧缓冲队列中,这个过程叫做入队。
使用 VIDIOC_REQBUFS 指令申请帧缓冲, 该缓冲区是由内核所维护的,应用程序不能直接读取该缓冲区的数据,需要将其映射到用户空间中,应用程序读取映射区的数据实际上是读取内核维护的帧缓冲中的数据。
在映射之前,需要查询帧缓冲的信息:帧缓冲的长度、偏移量。使用VIDIOC_QUERYBUF指令查询:
ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *buf);
调用 ioctl()需要传入一个 struct v4l2_buffer *指针, struct v4l2_buffer 结构体描述了帧缓冲的信息, ioctl()会将获取到的数据写入到 buf 指针所指的对象中。
struct v4l2_buffer {
__u32 index;//编号
__u32 type;//V4L2_BUF_TYPE_VIDEO_CAPTURE
__u32 bytesused;//使用的字节数
__u32 flags;
__u32 field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence;
/* memory location */
__u32 memory;//V4L2_MEMORY_MMAP
union {
__u32 offset;//偏移
unsigned long userptr;
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length;//长度
__u32 reserved2;
__u32 reserved;
};
index 字段表示一个编号, 申请的多个帧缓冲、 每一个帧缓冲都有一个编号,从 0 开始。一次 ioctl()调用只能获取指定编号对应的帧缓冲的信息,所以要获取多个帧缓冲的信息,需要重复调用多次,每调用一次ioctl()、 index 加 1,指向下一个帧缓冲。
type 字段和memory 字段与前面介绍的一样。length 字段表示帧缓冲的长度,共同体中的 offset 表示帧缓冲的偏移量。因为应用程序通过 VIDIOC_REQBUFS 指令申请帧缓冲时,内核会向操作系统申请一块内存空间作为帧缓冲区,内存空间的大小就等于申请的帧缓冲数量 * 每一个帧缓冲的大小,每一个帧缓冲对应到这一块内存空间的某一段,所以它们都有一个地址偏移量。
申请帧缓冲后、调用 mmap()将帧缓冲映射到用户地址空间
struct v4l2_buffer buf = {};
buf.index = xxx;//0~3
but.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd,VIDIOC_QUERYBUF,&buf);
//映射到用户空间
mmap(NULL,buf.length,.......,fd,buf.m.offset);
使用 VIDIOC_QBUF 指令将帧缓冲放入到内核的帧缓冲队列中,调用 ioctl()之前,需要设置 struct v4l2_buffer 类型对象的 memory、 type 字段
ioctl(fd,VIDIOC_QBUF,&buf);
使用 VIDIOC_DQBUF 指令开启视频采集,
ioctl(int fd, VIDIOC_STREAMON, int *type); //开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type); //停止视频采集
enum v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_STREAMON,&buf_type);
开启视频采集之后,便可以去读取数据 ,直接读取每一个帧缓冲的在用户空间的映射区即可读取到摄像头采集的每一帧图像数据。在读取数据之前,需要将帧缓冲从内核的帧缓冲队列中取出,这个操作叫做帧缓冲出队。帧缓冲出队,便可读取数据,对数据进行处理:将摄像头采集的图像显示到 LCD屏上;数据处理完成之后,再将帧缓冲入队,往复操作。
使用 VIDIOC_DQBUF 指令执行出队操作
ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);
while(1){
//从采集队列中取出一帧
ioctl(fd,VIDIOC_DQBUF,&buf);
//将该帧的数据拷贝走
memcpy(....);
//将取出的一帧放回队列
ioctl(fd,VIDIOC_QBUF,&buf);
//显示,存储......
}
结束视频采集,使用 VIDIOC_STREAMOFF 指令
enum v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_STREAMOFF,&buf_type);
//解除映射
//关闭设备文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct{
char *start;
size_t length;
}buffer_t;
buffer_t buffer[4];
buffer_t current;//保存当前取出的一帧
int lcd_fd;
int *memp;
unsigned int sign3 = 0;
int yuyv2rgb(int y, int u, int v)
{
unsigned int pixel24 = 0;
unsigned char *pixel = (unsigned char *)&pixel24;
int r, g, b;
static int ruv, guv, buv;
if(sign3)
{
sign3 = 0;
ruv = 1159*(v-128);
guv = 380*(u-128) + 813*(v-128);
buv = 2018*(u-128);
}
r = (1164*(y-16) + ruv) / 1000;
g = (1164*(y-16) - guv) / 1000;
b = (1164*(y-16) + buv) / 1000;
if(r > 255) r = 255;
if(g > 255) g = 255;
if(b > 255) b = 255;
if(r < 0) r = 0;
if(g < 0) g = 0;
if(b < 0) b = 0;
pixel[0] = r;
pixel[1] = g;
pixel[2] = b;
return pixel24;
}
int yuyv2rgb0(unsigned char *yuv, unsigned char *rgb, unsigned int width, unsigned int height)
{
unsigned int in, out;
int y0, u, y1, v;
unsigned int pixel24;
unsigned char *pixel = (unsigned char *)&pixel24;
unsigned int size = width*height*2;
for(in = 0, out = 0; in < size; in += 4, out += 6)
{
y0 = yuv[in+0];
u = yuv[in+1];
y1 = yuv[in+2];
v = yuv[in+3];
sign3 = 1;
pixel24 = yuyv2rgb(y0, u, v);
rgb[out+0] = pixel[0];
rgb[out+1] = pixel[1];
rgb[out+2] = pixel[2];
pixel24 = yuyv2rgb(y1, u, v);
rgb[out+3] = pixel[0];
rgb[out+4] = pixel[1];
rgb[out+5] = pixel[2];
}
return 0;
}
//LCD初始化
void lcd_init()
{
lcd_fd = open("/dev/fb0",O_RDWR);
if(lcd_fd==-1){
perror("open");;
exit(-1);
}
//映射
memp = mmap(NULL,800*480*4,PROT_READ|PROT_WRITE,MAP_SHARED,lcd_fd,0);
if(memp==MAP_FAILED){
perror("mmap");;
exit(-1);
}
}
void lcd_uninit()
{
munmap(memp,800*480*4);
close(lcd_fd);
}
int main()
{
lcd_init();
//1.打开摄像头
int fd = open("/dev/video7",O_RDWR);
if(fd==-1){
perror("open");
exit(-1);
}
//2.获取功能参数
struct v4l2_capability cap = {};
int res = ioctl(fd,VIDIOC_QUERYCAP,&cap);
if(res==-1){
perror("ioctl cap");
exit(-1);
}
if(cap.capabilities&V4L2_CAP_VIDEO_CAPTURE){
//设备是一个摄像头
printf("capture device!\n");
}
else{
printf("not a capture device!\n");
exit(-1);
}
//3.获取摄像头支持的格式
struct v4l2_fmtdesc fmt = {};
fmt.index = 0;//第一种格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//获取摄像头的格式
while((res=ioctl(fd,VIDIOC_ENUM_FMT,&fmt))==0){
printf("pixformat=%c%c%c%c,description=%s\n",fmt.pixelformat&0xff,(fmt.pixelformat>>8)&0xff,
(fmt.pixelformat>>16)&0xff,(fmt.pixelformat>>24)&0xff,fmt.description);
fmt.index++;
}
//4.设置采集通道
int index = 0;//使用通道0
res = ioctl(fd,VIDIOC_S_INPUT,&index);
if(res==-1){
perror("ioctl s_input");
exit(-1);
}
//5.设置摄像头的采集格式
struct v4l2_format format = {};
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
format.fmt.pix.width = 640;
format.fmt.pix.height = 480;
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//YUYV格式
format.fmt.pix.field= V4L2_FIELD_NONE;
res = ioctl(fd,VIDIOC_S_FMT,&format);
if(res==-1){
perror("ioctl s_fmt");
exit(-1);
}
//6.申请缓存空间
struct v4l2_requestbuffers req = {};
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
res = ioctl(fd,VIDIOC_REQBUFS,&req);
if(res==-1){
perror("ioctl reqbufs");
exit(-1);
}
//7.分配,映射,入队
size_t i,max_len = 0;
for(i=0;i<4;i++){
struct v4l2_buffer buf = {};
buf.index = i;//0~3
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
res = ioctl(fd,VIDIOC_QUERYBUF,&buf);
if(res==-1){
perror("ioctl querybuf");
exit(-1);
}
//记录最大长度
if(buf.length>max_len)
max_len = buf.length;
//映射
buffer[i].length = buf.length;
buffer[i].start = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
if(buffer[i].start==MAP_FAILED){
perror("mmap");
exit(-1);
}
//入队
res = ioctl(fd,VIDIOC_QBUF,&buf);
if(res==-1){
perror("ioctl qbuf");
exit(-1);
}
}
//申请临时缓冲区
current.start = malloc(max_len);
if(current.start==NULL){
perror("malloc");
exit(-1);
}
//8.启动摄像头
enum v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
res = ioctl(fd,VIDIOC_STREAMON,&buf_type);
if(res==-1){
perror("ioctl streamon");
exit(-1);
}
//延时
sleep(1);
//RGB缓冲区
char rgb[640*480*3];
//9.采集数据
while(1){
struct v4l2_buffer buf = {};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
//出队
res = ioctl(fd,VIDIOC_DQBUF,&buf);
if(res==-1){
perror("ioctl dqbuf");
}
//拷贝数据
memcpy(current.start,buffer[buf.index].start,buf.bytesused);
current.length = buf.bytesused;
//入队
res = ioctl(fd,VIDIOC_QBUF,&buf);
if(res==-1){
perror("ioctl qbuf");
}
//显示 保存 传输.....
//YUYV转RGB
yuyv2rgb0(current.start,rgb,640,480);
//显示到LCD
int x,y;
for(y=0;y<480;y++){
for(x=0;x<640;x++){
*(memp+y*800+x) = rgb[3*(y*640+x)]<<16 | rgb[3*(y*640+x)+1]<<8 | rgb[3*(y*640+x)+2];
}
}
}
//10.关闭摄像头采集
buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
res = ioctl(fd,VIDIOC_STREAMOFF,&buf_type);
if(res==-1){
perror("ioctl streamoff");
exit(-1);
}
//解除映射
for(i=0;i<4;i++){
munmap(buffer[i].start,buffer[i].length);
}
free(current.start);
close(fd);
lcd_uninit();
return 0;
}