v4l2应用框架-摄像头v4l2编程

1.简介

V4L2支持三种方式来采集图像,内存映射(mmap),直接读取方式(read)和用户指针,

1.1 内存映射mmap

内存映射一般用于连续视频数据的采集,申请到是连续的内存,应用调用VIDIOC_REQBUFS ioctl分配设备buffers,这个ioctl也可以用来改变buffers的数据以及释放分配的内存,在应用空间能够访问这些物理地址之前,必须调用mmap函数把这些物理空间映射为用户虚拟地址空间。

1.2 直接读取read

直接读取的方式相对速度慢一些,常用于静态图片数据的采集;

1.3 用户指针userptr

用户指针方式应用层负责分配需要的内存空间,然后以指针的形式传递给V4L2驱动层,从而获取图像数据,V4L2驱动会把capture的内容保存到指针所指的空间,一般来说,应用层需要确保这个内存空间物理上是连续的(IPU处理单元的需求),应用层在不需要的时候要负责释放申请的内存

2.v4l2采集原理

v4l2应用框架-摄像头v4l2编程_第1张图片
从上图可知,当视频采集的时候,驱动程序从视频采集输入队列取出一个帧缓冲区填充图像buffer,这个帧缓冲区填充完毕之后会放入视频采集输出队列,交由应用程序处理,应用程序处理完这个帧缓冲区之后在将已经处理过的帧缓冲区交由驱动程序,从而循环往复采集图像数据

3.应用程序编写步骤

一共分为五个步骤,总结为:
(1)打开设备,进行初始化参数设置,通过V4L2接口设置视频图像的采集窗口、采集的点阵大小和格式;
(2)申请图像帧缓冲,并进行内存映射,将这些帧缓冲区从内核空间映射到用户空间,便于应用程序读取、处理图像数据;
(3)将帧缓冲进行入队操作,启动视频采集;
(4)驱动开始视频数据的采集,应用程序从视频采集输出队列取出帧缓冲区,处理完后,将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据;
(5)释放资源,停止采集工作。

step1: 打开设备

int fd = open("/dev/videoX", flag);

flag:
O_RDWR:阻塞方式打开,需要捕获到视频数据才返回给应用程序
O_RDWR | O_NONBLOCK:非阻塞方式打开,未捕获到视频数据驱动也会将缓存中的数据返回给应用程序

step2:查询设备功能

 struct v4l2_capability
 {
   __u8 driver[16];       // 驱动名字
    __u8 card[32];         // 设备名字
     __u8 bus_info[32];    // 设备在系统中的位置
   __u32 version;         // 驱动版本号
    __u32 capabilities;    // 设备支持的操作
    __u32 reserved[4];    // 保留字段
};
 
struct v4l2_capability cap;
 
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0)
{
 	printf("unable to query device.\n", name);
    return -1;
}
if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
    {
     printf("%s is not a video capture device\n", name);
       return -1;
}

可查询的设备功能,使用方式和前面的一样

其中capabilities 字段标记着v4l2设备的功能

V4L2_CAP_VIDEO_CAPTURE    设备支持捕获功能

V4L2_CAP_VIDEO_OUTPUT     设备支持输出功能

V4L2_CAP_VIDEO_OVERLAY    设备支持预览功能

V4L2_CAP_STREAMING        设备支持流读写

V4L2_CAP_READWRITE        设备支持read、write方式读写

step3 显示输入设备/支持的格式

1.显示所有的输入设备

struct v4l2_input {
	__u32	     index;		/*  Which input */
	__u8	     name[32];		/*  Label */
	__u32	     type;		/*  Type of input */
	__u32	     audioset;		/*  Associated audios (bitfield) */
	__u32        tuner;             /*  Associated tuner */
	v4l2_std_id  std;
	__u32	     status;
	__u32	     reserved[4];
};

struct v4l2_input input;
 
input.index = 0;
while (!ioctl(fd, VIDIOC_ENUMINPUT, &input))
{
    printf("input:%s\n", input.name);
    ++input.index;
}

2.显示支持的格式

struct v4l2_fmtdesc
 {
     __u32 index;               // 要查询的格式序号,应用程序设置
     enum v4l2_buf_type type;   // 帧类型,应用程序设置
     __u32 flags;               // 是否为压缩格式
    __u8 description[32];      // 格式名称
    __u32 pixelformat;         //所支持的像素格式
    __u32 reserved[4];         // 保留
 };

在使用VIDIOC_ENUM_FMT查询当前camera支持的所有格式时,注意需要设置v4l2_fmtdescindex变量,enum v4l2_buf_type type也要设置为V4L2_BUF_TYPE_VIDEO_CAPTURE,其他变量驱动会填充

struct v4l2_fmtdesc fmtdesc;
 
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmtdesc.index = 0;
 
while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
{
    printf("fmt:%s\n", fmtdesc.description);
 
    fmtdesc.index++;
}

step4 设置输入设备/支持的格式

1.设置输入设备

struct v4l2_input input;
 
input.index = 0;
while (!ioctl(fd, VIDIOC_ENUMINPUT, &input))
{
    printf("input:%s\n", input.name);
    ++input.index;
}
 
input.index = index; // 0,1,2....指定输入设备
 
if (ioctl(fd, VIDIOC_S_INPUT, &input) < 0)
{
    printf("%s:VIDIOC_S_INPUT failed\n", __func__);
    return -1;
}

2.设置图像格式
设置图像格式使用struct v4l2_format结构体,该结构体用来设置每一帧图像的具体格式,包括帧的类型和图像的长,宽等

1 struct v4l2_format
02 {
03     enum v4l2_buf_type type;          // 帧类型,应用程序设置
04     union fmt
05     {
06         struct v4l2_pix_format pix;   // 视频设备使用
07         structv 4l2_window win;
08         struct v4l2_vbi_format vbi;
09         struct v4l2_sliced_vbi_format sliced;
10         __u8 raw_data[200];
11     };
12 };
struct v4l2_format v4l2_fmt;
 
memset(&v4l2_fmt, 0, sizeof(struct v4l2_format));
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
v4l2_fmt.fmt.pix.width = width; //图像的宽度
v4l2_fmt.fmt.pix.height = height; //图像的高度
v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //像素格式
v4l2_fmt.fmt.pix.field = V4L2_FIELD_ANY;
 
if (ioctl(fd, VIDIOC_S_FMT, &v4l2_fmt) < 0)
{
    printf("ERR(%s):VIDIOC_S_FMT failed\n", __func__);
    return -1;
}

step5 申请帧缓冲区

 struct v4l2_requestbuffers
 {
    __u32 count;                    // 缓冲区内缓冲帧的数目
    enum v4l2_buf_type type;        // 缓冲帧数据格式
     enum v4l2_memorymemory;         // 区别是内存映射还是用户指针方式
     __u32 reserved[2];
};

struct v4l2_requestbuffers req;
 
req.count = nr_bufs; //缓存数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;//内存映射方式
 
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
{
    printf("ERR(%s):VIDIOC_REQBUFS failed\n", __func__);
    return -1;
}

step6 将申请的缓冲帧从内核空间映射到用户空间

read方式读取图像数据时会将图像数据从内核空间拷贝到用户空间,效率较低,内存映射相当于直接从内核空间读取内存,不需要将数据从内核空间拷贝到用户空间。

01 struct v4l2_buffer
02 {
03     __u32 index;                    //buffer 序号
04     enum v4l2_buf_type type;        //buffer 类型
05     __u32 byteused;                 //buffer 中已使用的字节数
06     __u32 flags;                    // 区分是MMAP 还是USERPTR
07     enum v4l2_field field;
08     struct timeval timestamp;       // 获取第一个字节时的系统时间
09     struct v4l2_timecode timecode;
10     __u32 sequence;                 // 队列中的序号
11     enum v4l2_memory memory;        //IO 方式,被应用程序设置
12     union m
13     {
14         __u32 offset;               // 缓冲帧地址,只对MMAP 有效
15         unsigned long userptr;
16     };
17     __u32 length;                   // 缓冲帧长度
18     __u32 input;
19     __u32 reserved;
20 };

代码:

struct v4l2_buffer v4l2_buffer;
void* addr[4];
 
for(i = 0; i < 4; i++)
{
    memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
    v4l2_buffer.index = i; //想要查询的缓存
    v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    v4l2_buffer.memory = V4L2_MEMORY_MMAP;
 
    /* 查询缓存信息 */
    ret = ioctl(fd, VIDIOC_QUERYBUF, &v4l2_buffer);
    if(ret < 0)
    {
        printf("Unable to query buffer.\n");
        return -1;
    }
 
    /* 内存映射 */
    addr[i] = mmap(NULL /* start anywhere */ ,
            v4l2_buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED,
            fd, v4l2_buffer.m.offset);
}

step7 将申请的缓冲帧放入队列

使用VIDIOC_QBUF命令,将申请的缓冲帧依次放入缓冲帧输入队列,等待图像采集设备依次填满buffer

struct v4l2_buffer v4l2_buffer;
 
for(i = 0; i < 4; i++)
{
    memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
    /* 想要放入队列的缓存 */
    v4l2_buffer.index = i; 
    v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    v4l2_buffer.memory = V4L2_MEMORY_MMAP;	
 
    ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buffer);
    if(ret < 0)
    {
        printf("Unable to queue buffer\n");
        return -1;
    }
}

step8 启动视频捕获视频

启动视频捕获设备捕获视频数据,使用VIDIOC_STREAMON命令

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
{
    printf("%s:VIDIOC_STREAMON failed\n", __func__);
    return -1;
}

step9 读取视频数据

读取数据的应用接口一般使用select或者poll,读取数据是一个视频数据不断入队列出队列的过程,一般为一个死循环,一般过程为:

while(1)
{
	读数据
	数据buffer出队列
	拷贝视频数据
	buffer入队列
}

1.判断数据是否可读

while (1) {

		struct pollfd fds;

		fds.fd = fd;
		fds.events = POLLIN;

		if (-1 == (rval = poll(&fds, 1, -1))) {
			printf("%s poll err: %m \n", __func__);
			return -errno;
		}


		if (rval > 0) {
			rval = get_video_data(fd,4,);//入队列
			if (rval < 0)
				return rval;

			count--;
		}

		if (count < 0)
			break;

	}

2.数据出队列:

struct v4l2_buffer buffer;
 
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
 
if (ioctl(fd, VIDIOC_DQBUF, &buffer) < 0)
{
    printf("%s:VIDIOC_DQBUF failed, dropped frame\n", __func__);
    return -1;
}

3.数据入队列
在数据读取完成后,将buf重新放入队列

struct v4l2_buffer v4l2_buf;
 
v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4l2_buf.memory = V4L2_MEMORY_MMAP;
v4l2_buf.index = i; //指定buf,出队列时的缓存下标
 
if (ioctl(fd, VIDIOC_QBUF, &v4l2_buf) < 0)
{
    printf("ERR(%s):VIDIOC_QBUF failed\n", __func__);
    return -1;
}

三个步骤整合为

while (1) {

		struct pollfd fds;

		fds.fd = dfd;
		fds.events = POLLIN;

		if (-1 == (rval = poll(&fds, 1, -1))) {
			printf("%s poll err: %m \n", __func__);
			return -errno;
		}


		if (rval > 0) {
			rval = get_video_data(fd,4,addr,buffer);
			if (rval < 0)
				return rval;

		}
	}
int get_video_data(int fd,int buf_count,unsigned char *addr,struct buffer *kbuf)
{	
	struct v4l2_buffer buf = {};


	buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	buf.memory = V4L2_MEMORY_MMAP;
 
   //buf出队列
	if (-1 == ioctl(fd, VIDIOC_DQBUF, &buf)) {
		printf("%s,VIDIOC_DQBUF err %n", __func__);
		return -errno;
	}

	if (buf.index > buf_count) {
		printf("%s,buffer index invalid, \n", __func__);
		return -1;
	}

	//拷贝buffer
	memcpy(addr, buf[vbuf.index].start, kbuf[buf.index].length);

 buf入队列
	if (-1 == ioctl(fd, VIDIOC_QBUF, &buf)) {
		printf("%s-->VIDIOC_QBUF err %m", __func__);
		return -errno;
	}


	return 0;
}
}

step10 停止设备

使用VIDIOC_STREAMOFF命令,关闭捕获图像数据。同注意取消内存映射和关闭句柄,以及释放内存,防止不必要的内存泄漏

//停止设备
int v4l2_stop(int fd){
	enum v4l2_buf_type type;

	type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

	if (-1 == ioctl(fd, VIDIOC_STREAMOFF, &type)) {
		printf("%s-->VIDIOC_STREAMOFF err %m", __func__);
		return -errno;

}

step11 取消内存映射

void v4l2_close (int fd)
{
	if (fd < 0)
		return;


	/* 释放缓冲区 */
	for(i = 0; i < buf_num; ++i)
   		 munmap(buf[i].start, buf[i]->length);

	close(fd);

	fd = -1;

	return ;
}

主要应用编写流程就是这样子,没有具体深入分析。
可以看这位大神的博客:从应用调用vivi驱动分析v4l2 – 应用代码编写

你可能感兴趣的:(Camera开发,音视频)