一、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>

二、 程序编写流程

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:capabilitiesdevice_cap字段的构成:
capabilitiesdevice_cap 字段由一系列标志位构成,此处简要说明如下概念的含义:

  1. single-planarmulti-planar 用于描述图像类型的存储方式
  2. captureoutput 用于描述设备数据流向,capture 的数据是从设备到内存(e.g.视频采集设备),output 的数据是从内存到设备 (e.g.视频输出设备)
  3. 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

  1. 格式标志flags由一系列标志位构成在这里插入图片描述
  2. 实验硬件设备支持格式的标志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:图像存储方式

  1. YUYV 格式的存储方式和采样方式如下图,详细参考2.7.12. V4L2_PIX_FMT_YUYV (‘YUYV’)
    在这里插入图片描述
  2. 以图像格式位 YUYV 为例,分辨率在1280* 720的情况下,驱动程序填充了 bytesperlinesizeimage 字段,其中:
    • bytesperline = 2560 (= 1280 * 2, YUYV格式存储每个像素需要 2B)
    • sizeimage = 2764800 (= 1280 * 720 * 2,YUYV 格式存储每个像素需要 2B)
  3. 以图像格式为 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:申请 bufferAPP 可以申请多个 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_QUERYBUFmmap:申请 buffer 信息、映射

  • 如果申请到了 Nbuffer,这个 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 放入“空间链表

  • 如果申请到了 Nbuffer,这个 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:缓冲区管理概述

  1. 对于一个内核态的 vb2_queue 结构体(也就是一个缓冲区队列),V4L2框架会维护2个缓冲区链表
    在这里插入图片描述
    • queued_list当前从用户空间排队的缓冲区列表。(对于图像采集设备,应用程序将缓冲区通过 VIDIOC_QBUF 操作加入 queued_list ,以便设备填充数据)
    • done_list准备退出用户空间队列的缓冲区列表。(对于图像采集设备,驱动程序会将填充好数据的缓冲区加入 done_list ,等待应用程序通过 VIDIOC_DQBUF 操作取出)
  2. 缓冲区在 queued_listdone_list 链表之间的流转状态如下:
    • 分配的缓冲区既不在 queued_list,也不在 done_list
    • 应用程序通过 VIDIOC_QBUF 操作,将缓冲区加入 queued_list,以便驱动程序使用
    • 驱动程序从 queued_list 获取缓冲区使用,在操作完成后(e.g.填充完图像后),将缓冲区再加入 done_list
    • 应用程序通过 VIDIOC_DQBUF 操作,将缓冲区同时从 done_listqueued_list 链表中取出使用
  3. 需要注意的是,VIDIOC_QBUF 操作和 VIDIOC_DQBUF 操作中传递 v4I2_buffer 结构体的目的,是让内核态 V4L2 框架可以通过用户态传递的 v4l2_buffer 结构体信息索引到相应的 vb2_buffer 结构体。因此用户态一般只需要定义一个 v4l2_buffer 结构体类型变量,在传递时设置相应的字段即可
  4. 缓冲区加入队列会将缓冲区的内存页锁定在物理内存中,这样这些页面就不会被交换到磁盘中,以便于硬件设备访问 (通常是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,处理再放入“空闲链表

  1. poll/select
memset(fds, 0, sizeof(struct fds));
fds[0].fd = fd;
fds[0].events = POLLIN;
poll(fds, 1, -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 这类压缩类型,则每帧图像有效数据的长度是不同的(但是都不会超过缓冲区长度)
  1. 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应用代码)

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐