基于V4L2的摄像头捕获
V4L2(Video for Linux2)是Linux内核提供给应用程序访问音、视频驱动的统一接口,支持从摄像头采集数据到编码推流或保存文件等多种应用场景。它具有发现设备和操作设备的能力,通过一系列的回调函数来实现功能,比如设置摄像头的频率、帧频、视频压缩格式和图像参数等等。它也可以用于其他多媒体的开发,如音频开发等。
V4L2提供了一套通用的事件机制用于向用户空间异步传递事件。事件基于文件句柄(v4l2_fh)进行订阅,驱动通过函数将事件排队。用户空间通过系统调用来监听事件就绪,并通过ioctl函数调用相关命令来接收和处理事件。V4L2支持多种设备,提供了以下接口:
1. 视频采集接口(video capture interface):应用设备可以是高频头或者摄像头,V4L2的最初设计就是应用于该功能。
2. 视频输出接口(video output interface):可以驱动计算机的外围视频图像设备,用于输出视频信号。
3. 直接传输视频接口(video overlay interface):可以把视频采集设备采集过来的信号直接输出到输出设备上,不用经过系统CPU。
4. 视频间隔消隐信号接口(VBI interface):它可以使应用可以访问传输消隐期的视频信号。
5. 收音机接口(radio interface):可用来处理从AM或FM高频头设备接收来的音频流。
在Linux系统中,V4L2驱动的Video设备节点文件通常为/dev/videoX(比如/dev/video0)。V4L2驱动对用户空间提供字符设备,主设备号为81,对于视频设备,其次设备号为0-63。对于Radio收音机设备,其次设备号为64-127。对于Teletext广播设备,其次设备号为192-223。对于VBI视频消影设备,其次设备号为224-255。V4L2驱动的Video设备在用户空间可以通过各种ioctl函数调用进行控制,并且可以使用mmap进行内存映射。一般的编程流程如下图所示。

在进行V4L2开发中,一般会通过ioctl函数调用以下的命令标志符:
VIDIOC_REQBUFS:分配内存
VIDIOC_QUERYBUF:把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP:查询设备驱动功能
VIDIOC_ENUM_FMT:获取当前驱动支持的视频格式
VIDIOC_S_FMT:设置当前驱动的频捕获格式
VIDIOC_G_FMT:读取当前驱动的频捕获格式
VIDIOC_TRY_FMT:验证当前驱动的显示格式
VIDIOC_CROPCAP:查询驱动的修剪能力
VIDIOC_S_CROP:设置视频信号的边框
VIDIOC_G_CROP:读取视频信号的边框
VIDIOC_QBUF:把数据放回缓存队列
VIDIOC_DQBUF:把数据从缓存中读取出来
VIDIOC_STREAMON:开始视频显示函数
VIDIOC_STREAMOFF:结束视频显示函数
VIDIOC_QUERYSTD:检查当前视频设备支持的标准,例如PAL或NTSC。
以上这些IO调用,有些是必须的,有些是可选择的。
接下来通过野火STM32MP157开发板,运用V4L2实现一个摄像头动态显示和实时捕获照片的例子。所使用的摄像头型号为OV5640,外形如下图所示。

该摄像头有500万像素,最大分辨率为2592X1944,支持连续自动对焦,8位并口数据,支持RGB565、CCIR656、YUYV、YCbcR422和JPEG格式输出。也可以选择其他摄像头,但最好能输出RGB格式数据,因为这可以大大降低系统的格式转换运算。
首先可以通过下面的代码来测试一下摄像头所支持的数据格式和分辨率,文件名为test.c。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/videodev2.h> int main(int argc, char *argv[])
{const char *dev = argc > 1 ? argv[1] : "/dev/video0";int fd = open(dev, O_RDWR | O_NONBLOCK);if (fd < 0) { perror("open"); return 1; }struct v4l2_fmtdesc fmtdesc;memset(&fmtdesc, 0, sizeof(fmtdesc));fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;for (fmtdesc.index = 0; ; fmtdesc.index++) {if (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != 0) break;printf("%c%c%c%c : ",fmtdesc.pixelformat & 0xff,(fmtdesc.pixelformat>>8) & 0xff,(fmtdesc.pixelformat>>16) & 0xff,(fmtdesc.pixelformat>>24) & 0xff);struct v4l2_frmsizeenum frmsize;memset(&frmsize, 0, sizeof(frmsize));frmsize.pixel_format = fmtdesc.pixelformat;for (frmsize.index = 0; ; frmsize.index++) {if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) != 0) break;if (frmsize.type != V4L2_FRMSIZE_TYPE_DISCRETE) continue;printf("%ux%u ", frmsize.discrete.width, frmsize.discrete.height);/* 帧率 */struct v4l2_frmivalenum frmival;memset(&frmival, 0, sizeof(frmival));frmival.pixel_format = fmtdesc.pixelformat;frmival.width = frmsize.discrete.width;frmival.height = frmsize.discrete.height;printf("[");for (frmival.index = 0; ; frmival.index++) {if (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival) != 0) break;if (frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE)printf("%.2f ", (double)frmival.discrete.denominator / frmival.discrete.numerator);}printf("fps] ");}printf("\n");}close(fd);return 0; }
把上述代码进行交叉编译,然后在开发板上执行,显示如下。

可见OV5640摄像头提供的输出数据格式和分辨率可以有很多种选择,特别是在640X480的分辨率下可以实现60Hz的帧频率。
下面给出本例的代码,文件名为camera.c。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <errno.h> #include <signal.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <fcntl.h> #include <unistd.h> #include <linux/videodev2.h> #include <linux/fb.h> #define CAM_DEV "/dev/video0" //设备节点文件 #define CAM_WIDTH 640 //视频高度(像素) #define CAM_HEIGHT 480 //视频宽度(像素) #define CAM_FORMAT V4L2_PIX_FMT_RGB565 //视频格式 #define BUFFER_COUNT 4 //缓冲区数量 #define FB_DEV "/dev/fb0" //显示设备 static int cam_fd = -1; //定义摄像头描述符 static int fb_fd = -1; //定义LCD描述符 static uint8_t *fb_base = NULL; //定义映射显示空间指针 static size_t fb_size = 0; //定义Framebuffer显存大小变量 static void *cam_bufs[BUFFER_COUNT] = {NULL}; //定义缓冲区数组 static size_t cam_buf_len[BUFFER_COUNT] = {0}; //定义缓冲区长度数组 static struct fb_var_screeninfo vinfo; //定义动态可变参数结构体 static struct fb_fix_screeninfo finfo; //定义静态不可变参数结构体 static unsigned int fb_line_length = 0; //定义行字节数变量 static volatile sig_atomic_t quit = 0; //定义信号原子变量 //信号处理 static void sig_handler(int sig) {quit = 1; } //直接拷贝RGB565数据到Framebuffer static void rgb565_to_fb(const uint8_t *src) {uint8_t *fb = fb_base; //获取映射显示空间指针const int src_stride = CAM_WIDTH * 2; //获取每行像素在内存中占用的字节数const int fb_stride = fb_line_length; //获取每行像素在显存中所占的字节数//以下把一帧摄像头图像按行拷贝到Framebuffer的对应位置for(int y=0; y<CAM_HEIGHT; y++)memcpy(fb+y*fb_stride, src+y*src_stride, src_stride); } //V4L2初始化 static int v4l2_init(void) {cam_fd = open(CAM_DEV, O_RDWR); //打开摄像头设备节点if(cam_fd < 0){perror("open camera");return -1;}struct v4l2_format fmt; //定义v4l2的格式配置结构体fmtmemset(&fmt, 0, sizeof(fmt)); //填充0,清除结构体fmt内容fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //配置v4l2为视频捕获方式fmt.fmt.pix.width = CAM_WIDTH; //配置视频宽度fmt.fmt.pix.height = CAM_HEIGHT; //配置视频高度fmt.fmt.pix.pixelformat = CAM_FORMAT; //配置视频格式fmt.fmt.pix.field = V4L2_FIELD_ANY; //自动配置场扫描方式if(ioctl(cam_fd, VIDIOC_S_FMT, &fmt) < 0)//使用配置好的fmt进行视频设置 {perror("VIDIOC_S_FMT");return -1;}struct v4l2_requestbuffers req; //定义v4l2的申请缓冲区结构体regmemset(&req, 0, sizeof(req)); //填充0,清除结构体reg内容req.count = BUFFER_COUNT; //配置缓冲区数量req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //配置v4l2为视频捕获方式req.memory = V4L2_MEMORY_MMAP; //配置缓冲区内存类型为内存映射方式if(ioctl(cam_fd, VIDIOC_REQBUFS, &req) < 0)//使用配置好的reg进行缓冲区申请 {perror("VIDIOC_REQBUFS");return -1;}for (unsigned int i=0; i<req.count; i++) //根据缓冲区数量循环配置各索引项内容 {struct v4l2_buffer buf; //定义v4l2的缓冲区结构体bufmemset(&buf, 0, sizeof(buf)); //填充0,清除结构体buf内容buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //配置v4l2为视频捕获方式buf.memory = V4L2_MEMORY_MMAP; //配置缓冲区内存类型为内存映射方式buf.index = i; //配置索引if(ioctl(cam_fd, VIDIOC_QUERYBUF, &buf) < 0)//查询缓冲区地址和大小(转换成物理地址)并写入buf中 {perror("VIDIOC_QUERYBUF");return -1;}cam_buf_len[i] = buf.length; //获取缓冲区实际长度//将V4L2内核缓冲区映射到用户空间的虚拟地址cam_bufs[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, cam_fd, buf.m.offset);if(cam_bufs[i] == MAP_FAILED){perror("mmap camera buffer");return -1;}if(ioctl(cam_fd, VIDIOC_QBUF, &buf) < 0)//把配置好的buf数据放回到缓存队列 {perror("VIDIOC_QBUF");return -1;}} //循环配置到此结束int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //定义v4l2类型为视频捕获方式if(ioctl(cam_fd, VIDIOC_STREAMON, &type) < 0) //开启视频捕获 {perror("VIDIOC_STREAMON");return -1;}printf("Camera streaming started!\n");return 0; } //Framebuffer(LCD)初始化 static int fb_init(void) {fb_fd = open(FB_DEV, O_RDWR); //打开显示设备节点if(fb_fd < 0){ perror("open framebuffer");return -1;}if(ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0)//获取硬件决定且不可更改的固定参数,并写入finfo中 {perror("FBIOGET_FSCREENINFO");close(fb_fd);return -1;}if(ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0)//获取可以动态修改的可变参数,并写入vinfo中 {perror("FBIOGET_VSCREENINFO");close(fb_fd);return -1;}if(vinfo.bits_per_pixel != 16) //判断色深是否为16位 {fprintf(stderr, "Only 16bpp framebuffer supported\n");close(fb_fd);return -1;}fb_line_length = finfo.line_length; //获取硬件决定固定参数中的行长度(行字节数)fb_size = finfo.smem_len; //获取Framebuffer显存的总大小//若fb_size为0,则计算一个合理的映射大小:行字节数X虚拟高度,保证一定能够覆盖整个屏幕区域if(fb_size == 0)fb_size = fb_line_length * vinfo.yres_virtual;//将整个Framebuffer显存映射到用户进程的虚拟地址空间fb_base = mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);if(fb_base == MAP_FAILED){perror("mmap framebuffer");close(fb_fd);return -1;}return 0; } //退出时解除映射 static void cleanup(void) {if(cam_fd >= 0) //如果已经打开了摄像头设备节点 {int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //定义v4l2类型为视频捕获方式ioctl(cam_fd, VIDIOC_STREAMOFF, &type); //关闭视频捕获 }for(int i=0; i<BUFFER_COUNT; i++){if(cam_bufs[i] && (cam_bufs[i] != MAP_FAILED))munmap(cam_bufs[i], cam_buf_len[i]);//解除摄像头的内存映射 }if(cam_fd >= 0)close(cam_fd); //关闭摄像头设备节点if(fb_base && (fb_base != MAP_FAILED))munmap(fb_base, fb_size); //解除Framebuffer的内存映射if(fb_fd >= 0)close(fb_fd); //关闭显示设备节点 } //主函数 int main(void) {struct sigaction sa; //声明一个结构体,用于描述对某个信号的处理方式。memset(&sa, 0, sizeof(sa)); //把结构体全部清零sa.sa_handler = sig_handler; //设置信号处理函数的句柄 sigemptyset(&sa.sa_mask); //清空信号处理期间的阻塞信号集//将上述设置应用到SIGINT信号(即终端按下Ctrl+C时产生的信号)if(sigaction(SIGINT, &sa, NULL) < 0){perror("sigaction");return EXIT_FAILURE;}if (fb_init() < 0) //调用显示初始化 { cleanup();return EXIT_FAILURE;}if (v4l2_init() < 0) //调用v4l2初始化 { cleanup();return EXIT_FAILURE;}printf("RGB565 preview (Ctrl+C to exit)\n");while(!quit){struct v4l2_buffer buf; //定义v4l2的缓冲区结构体bufmemset(&buf, 0, sizeof(buf)); //填充0,清除结构体buf内容buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//配置v4l2为视频捕获方式buf.memory = V4L2_MEMORY_MMAP; //配置缓冲区内存类型为内存映射方式//把数据从缓存中读取出来if(ioctl(cam_fd, VIDIOC_DQBUF, &buf) < 0){if (errno == EINTR)continue;perror("DQBUF");break;}rgb565_to_fb(cam_bufs[buf.index]); //调用显示//把数据放回缓存队列if (ioctl(cam_fd, VIDIOC_QBUF, &buf) < 0){perror("QBUF");break;}}printf("Exiting...\n");cleanup(); //调用解除映射return 0; }
把上述程序交叉编译后在开发板上运行,就可以在LCD上看到摄像头的实时影像了。整个影像清晰流畅,通过top命令观察cup的占用率仅在34%左右, 执行效率较高。
从上述程序中可以看到,使用v4l2的形式基本上是固定的,一般都是先定义一个v4l2的结构体,然后对其进行参数填充,最后通过ioctl以命令(具体见前面的命令标志符)的方式进行设置。所引用的命令一般分为输入型、输出型和双向型三类,具体取决于所传入的结构体里哪些字段是输入、哪些是输出。简单来说,ioctl命令虽然分输入、输出和双向型,但在V4L2中它通常体现在同一个结构体里部分字段是输入、部分字段是输出,而不是整个结构体简单地归类为纯输入或纯输出。
下面总结一下v4l2摄像头捕获的使用步骤,如下。
1、打开设备。摄像头的设备文件一般是“/dev/video0”,打开之前需要确认一下具体名称。
2、查询摄像头支持的格式。这一步就是前面的test.c程序,用来获取摄像头的输出格式、分辨率及帧频等。如果对所使用的摄像头参数比较了解,可以跳过这步。本步聚在实际进行图像捕获时并不需要。
3、设置格式(命令标志为VIDIOC_S_FMT)。这一步需要使用结构体“v4l2_format”,通过它设置像素格式、分辨率、场模式等,驱动会返回实际支持的格式。若设置有问题,实际值可能会与设置值不同,必要时可以把设置好的值再打印出来检查。
4、请求缓冲区(命令标志为VIDIOC_REQBUFS)。这一步需要使用结构体“v4l2_requestbuffers”,通过它向驱动申请若干内存映射(V4L2_MEMORY_MMAP)缓冲区,数量一般 3~4 个。
5、查询缓冲区并映射到用户空间(命令标志为VIDIOC_QUERYBUF)。这一步需要使用结构体“v4l2_buffer”,通过它对每个缓冲区执行查询并获取物理偏移,然后通过mmap函数映射到进程地址空间。
6、将所有缓冲区入队(命令标志为VIDIOC_QBUF)。这一步仍然使用结构体“v4l2_buffer”,在开始视频流传输之前,通过它把全部缓冲区提交给驱动。
7、开启视频流(命令标志为VIDIOC_STREAMON)。这一步直接通过ioctl引用命令即可,不需要结构体,但需要传入一个值为V4L2_BUF_TYPE_VIDEO_CAPTURE的变量。
8、循环采集帧(VIDIOC_DQBUF → 处理 → VIDIOC_QBUF)。这一步循环采集,先出队,获取一帧数据(务必设置 type 和 memory)。再进行处理,直接使用buffers[buf.index]指向的帧数据。最后再入队,在处理完成后,把同一个缓冲区索引放回队列供驱动继续使用。这步仍然使用了结构体“v4l2_buffer”,还引用了出队(VIDIOC_DQBUF)和入队(VIDIOC_QBUF)两条命令。这里简要解释一下,出队就是在第4步中申请的缓冲区中拿出一个已经由驱动填充了视频数据的缓冲区来,处理就是读取这些数据,入队就是把已经读取过数据的缓冲区再放回去让驱动再填充视频数据。这里的队指的是队列,一般是由4个缓冲区组成,进行循环使用。
9、停止视频流(命令标志为VIDIOC_STREAMOFF)。这一步直接通过ioctl引用命令即可,不需要结构体,但需要传入一个值为V4L2_BUF_TYPE_VIDEO_CAPTURE的变量。
10、解除映射并关闭设备。这步需要先调用munmap函数把第5步映射的空间解除映射,然后关闭设备。
在上面的代码中,在main函数中的前10行代码是用来捕获Ctrl+C事件的,如果在实际工程中使用本例代码,这部分可以不要(包括前面定义的sig_handler函数)。
最后通过Qt来实现一个可以照相的实例。