linux摄像头设备的应用编程

142 阅读38分钟

大家好,我是 杰哥

linux摄像头设备的应用编程


下面是我的gitee仓库,欢迎大家关注↓ gitee源码仓库链接跳转


1.Linux摄像头设备应用编程概述

Linux系统一个很性感的地方就是:应用层的程序员无需详细了解驱动层面的具体实现,只需要调用驱动程序员写好的函数就可以从设备得到数据。对于获取设备数据的套路,基本是相同的:打开这个设备,然后ioctl发命令给设备驱动程序,告诉设备驱动程序我要从你的设备获取什么类型->什么格式的数据?然后驱动程序员就会根据应用程序猿发过来的指令,去读写,配置设备的各种寄存器从而达到:获取设备数据的目的,获取到数据后驱动程序员会把数据放到某块内存A。(CLC001)

1.1知识点补充:

驱动空间的内存与应用空间内存隔离

​ 内存A是驱动空间的内存,应用空间的程序无法通过指针直接访问驱动空间的内存,这是为什么?因为如果应用空间的程序可以通过指针直接访问驱动空间的内存,那有的程序员写了一个很烂的应用程序,这个应用程序里面非法访问了驱动空间的内存导致驱动程序段错误,驱动是操作系统的成员,驱动程序段错误奔溃退出,操作系统Linux就卡死。为了保护操作系统,Linux把驱动空间和应用空间的内存指针分开,应用层面的指针段错误不会导致操作系统奔溃。

mmap:m-map(m就是memory内存的英文单词缩写,map就是映射的英文单词)

​ 那回归刚才的操作,驱动程序员会把获取到的设备数据放到某块驱动空间的内存A,那应用程序员无法在应用层通过指针的方式访问驱动空间的内存A,那应用层怎么拿到设备的数据?驱动程序提供了mmap函数,map这个英文翻译成中文就是映射,映射的概念是什么?我的货物存储在1005号仓库,为了保护隐私,我不能让外人知道的东西放在了1005号仓库,那客人想要货物怎么办?我让客人写个清单放到1001号房间,有个店小二从1001号房间拿到清单,然后走到1005号仓库按清单取好货物,然后把货物搬到1001号房间,客人在1001号房间就可以把货物拿走,同时,客人根本不知道货物存放的真实地址在哪里。这里面的店小二就是那个映射(map),1005号仓库就是驱动空间的内存,1001号房间就是应用空间的内存,店小二就是驱动程序员实现的mmap函数,通过店小二,在保护驱动空间的内存安全的前提下,我在应用程序可以通过应用层的指针得到驱动空间的内存上面的数据,如果应用程序段错误,也就是错在1001号房间而已,店小二不把段错误转发到1005号仓库,1005号仓库就永远安全。总结:Linux系统存在驱动空间的内存与应用空间内存隔离,mmap函数在保护了驱动空间安全的前提下使得应用程序能够使用应用层的指针访问驱动空间的数据。

回到上面的(CLC001)处,驱动程序员已经把设备的数据放到驱动空间的内存A,然后应用程序员调用驱动的mmap函数把驱动空间的内存A头指针映射为应用空间的指针,在应用程序里面就可以*ptr读取驱动空间的内存A的数据了(带入店小二的例子),在摄像头设备的应用层程序编写里面:打开摄像头设备,ioctl设置摄像头获取图片的格式,然后映射好内存,就可以开始ioctl循环从映射好内存指针处读取驱动程序放进来的图片数据了 ,图片连着播放就是动图,动图不就是视频了。

001.png

2.Linux摄像头设备应用编程用到的直接驱动程序:V4L2

为什么说是Linux摄像头设备应用编程用到的直接驱动程序是v4l2,具体的不同型号的摄像头物理接口不同,有USB接口的摄像头,有MIPI接口摄像头,有非标协议接口的摄像头,这些不同协议的物理接口需要驱动程序配合,比如USB接口的摄像头你要写USB驱动程序,MIPI接口摄像头你要写MIPI驱动程序...这些USB驱动程序、MIPI驱动程序最终会把采集到的摄像头图片数据发送到v4l2驱动程序。所以说我们就向直接的驱动程序:V4L2驱动程序索取摄像头数据即可。v4l2驱动对应的文件在/dev/video0、/dev/video1、/dev/video2那里

002.png

我们只需要调用open、ioctl、mmap函数就可以从这些设备文件读取图像数据了。

3.废话不多说,开始编写应用程序(调用V4L2驱动程序的接口)

3.1编写cam001.c程序,打开设备,获取设备原始属性,这一步主要是看设备有没有捕获视频的能力

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <linux/videodev2.h>

int fd = -1;
struct v4l2_capability cap;

int main(int argc, char **argv)
{
	if(2 != argc)
	{
		printf("param error, must like this : @exec_file @devname\n");
		goto exit;
	}
		
	// 01以阻塞形式打开video设备
	if(-1 == (fd = open(argv[1], O_RDWR)))
	{
		printf("open %s failed\n", argv[1]);
		goto exit;
	}

	// 02通过ioctl向已经打开的设备发送命令VIDIOC_QUERYCAP,要求驱动程序返回设备信息到v4l2_capability结构体里面
    memset(&cap, 0, sizeof(cap));
	if(-1 == ioctl(fd, VIDIOC_QUERYCAP, &cap))
	{
		printf("get video dev info failed\n");
		goto exit;
	}
    
    // 03通过设备返回到v4l2_capability结构体里面的capabilities成员来判断这个驱动文件下面挂载的设备有没有捕获图片的能力
    // 没有捕获图片的能力的文件我们就无法从他那里获取摄像头图片,就要退出程序
    if(0 == (V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
	{
		printf("is no V4L2_CAP_VIDEO_CAPTURE dev\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("version : %x\n",		cap.version);
	printf("card name : %s\n",		cap.card);
	printf("device_caps : %x\n",	cap.device_caps);
	printf("driver name : %s\n",	cap.driver);
    printf("capabilities : %x\n",	cap.capabilities);
	printf("bus_info name : %s\n",	cap.bus_info);
    printf("==============================================================\n");
	
	if(-1 != fd)
		close(fd);
	return 0;
	
exit:
	if(-1 != fd)
		close(fd);
	return 1;
}

V4L2_CAP_VIDEO_CAPTURE 是 V4L2 的一个枚举值,用于表示视频捕获设备的能力。每个 V4L2 设备都可以具有不同的能力,通过检查 V4L2_CAP_VIDEO_CAPTURE 的数值,可以确定设备是否支持视频捕获功能。在 V4L2 规范中,V4L2_CAP_VIDEO_CAPTURE 的数值为 0x00000001,它表示设备支持视频捕获功能。你可以使用以下代码来检查 V4L2_CAP_VIDEO_CAPTURE 的数值:

#include <linux/videodev2.h>
#include <stdio.h>

int main() 
{
    int cap = V4L2_CAP_VIDEO_CAPTURE;
    if (cap & V4L2_CAP_VIDEO_CAPTURE) 
    {
        printf("设备支持视频捕获功能\n");
    } 
    else 
    {
        printf("设备不支持视频捕获功能\n");
    }
    
    return 0;
}

编译运行cam001.c程序,对/dev/video0、/dev/video1、/dev/video2逐个文件查看设备是否具有视频捕获功能,通过实验我们可以看到/dev/video0设备不具有视频捕获功能,放弃对/dev/video0设备编程,/dev/video1、/dev/video2具有视频捕获功能选一个来进行编程。

004.png

3.2编写cam002.c程序,在cam001.c的基础上:查询像素格式、查询分辨率、查询帧率、设置图片格式,这一步主要是看看摄像头支持什么格式?并且按照摄像支持的格式范围(不要超过参数的限定值,否则摄像头不工作)来设置图片格式

在cam001.c程序里面,我们通过ioctl向v4l2驱动程序发送VIDIOC_QUERYCAP命令,判断了videox设备是否有捕捉图片的能力,从v4l2_capability结构体的device_caps成员获取了设备视频输入类型,接下来的cam001.c程序会通过ioctl向v4l2驱动程序发送4个命令分别是:VIDIOC_ENUM_FMT、VIDIOC_ENUM_FRAMESIZES、VIDIOC_ENUM_FRAMEINTERVALS、VIDIOC_S_FMT。按照前面的顺序发送即可

3.2.1补充知识点

V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE和V4L2_BUF_TYPE_VIDEO_CAPTURE都是 V4L2(Video for Linux Two) 视频设备的捕获类型。 V4L2_BUF_TYPE_VIDEO_CAPTURE表示单平面视频捕获类型。这种类型的视频捕获使用一个平面(plane)来存储像素数据,每个像素点占用一个字节或多个字节的连续内存空间。 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE表示多平面视频捕获类型。这种类型的视频捕获使用多个平面来存储像素数据,每个平面可能使用不同的颜色分量进行存储,例如 YUV 格式的视频捕获会使用三个平面分别存储亮度(Y)、色度蓝(U)和色度红(V)的数据。 在使用 V4L2 接口进行视频捕获时,你需要根据具体的需求选择合适的捕获类型。如果你的应用程序只需要获取一般的 RGB 或 YUV 格式的视频数据,那么使用V4L2_BUF_TYPE_VIDEO_CAPTURE就可以了。但如果你需要处理特殊的视频格式,比如带有额外的平面或颜色分量的视频,那么你可能需要使用V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE来处理更复杂的视频数据。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <linux/videodev2.h>

int fd = -1;
struct v4l2_capability cap;
enum v4l2_buf_type capture_type;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_frmsizeenum frmsize;
struct v4l2_frmivalenum fival;
struct v4l2_format fmt;

int main(int argc, char **argv)
{
	if(2 != argc)
	{
		printf("param error, must like this : @exec_file @devname\n");
		goto exit;
	}
		
	// 01以阻塞形式打开video设备
	if(-1 == (fd = open(argv[1], O_RDWR)))
	{
		printf("open %s failed\n", argv[1]);
		goto exit;
	}

	// 02通过ioctl向已经打开的设备发送命令VIDIOC_QUERYCAP,要求驱动程序返回设备信息到v4l2_capability结构体里面
    memset(&cap, 0, sizeof(cap));
	if(-1 == ioctl(fd, VIDIOC_QUERYCAP, &cap))
	{
		printf("get video dev info failed\n");
		goto exit;
	}
    
    // 03通过设备返回到v4l2_capability结构体里面的capabilities成员来判断这个驱动文件下面挂载的设备有没有捕获图片的能力
    // 没有捕获图片的能力的文件我们就无法从他那里获取摄像头图片,就要退出程序
    if(0 == (V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
	{
		printf("is no V4L2_CAP_VIDEO_CAPTURE dev\n");
		goto exit;
	}
    
    // 04根据cap.device_caps成员识别设备支持的视频输入类型,见上面的3.2.1补充知识点
    if (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE) 
	{
		capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
	} 
	else if (cap.device_caps & V4L2_BUF_TYPE_VIDEO_CAPTURE) 
	{
		capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	} 
	else 
	{
		printf("is no V4L2_CAP_VIDEO\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("version : %x\n",		cap.version);
	printf("card name : %s\n",		cap.card);
	printf("device_caps : %x\n",	cap.device_caps);
	printf("driver name : %s\n",	cap.driver);
    printf("capabilities : %x\n",	cap.capabilities);
	printf("bus_info name : %s\n",	cap.bus_info);
    printf("==============================================================\n");
    
    // 05通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FMT,要求驱动程序返回像素格式到v4l2_fmtdesc结构体里面
	memset(&fmtdesc, 0, sizeof(fmtdesc));
	fmtdesc.type = capture_type; //支持的设备视频输入类型
	fmtdesc.index = 0;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
	{
		printf("get no VIDIOC_ENUM_FMT\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("description : %s\n",	fmtdesc.description);
	printf("type : %x\n",			fmtdesc.type);
	printf("index : %x\n",			fmtdesc.index);
	printf("flags : %x\n",			fmtdesc.flags);
	printf("mbus_code : %x\n",		fmtdesc.mbus_code);
	printf("pixelformat : %x\n",	fmtdesc.pixelformat);
	printf("==============================================================\n");
    
    // 06通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMESIZES,要求驱动程序返回分辨率格式到v4l2_frmsizeenum结构体里面
    memset(&frmsize, 0, sizeof(frmsize));
	frmsize.pixel_format = fmtdesc.pixelformat;
	frmsize.index = 0;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
	{
		printf("get no VIDIOC_ENUM_FRAMESIZES\n");
		goto exit;
	}
    
    // 07判断设备是否为离散设备。一般是离散设备就设置默认帧率,连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
	if(V4L2_FRMSIZE_TYPE_DISCRETE != frmsize.type)
	{
		printf("get no V4L2_FRMSIZE_TYPE_DISCRETE\n");
		goto exit;
	}
    
    printf("==============================================================\n");
    printf("width : %x\n", 			frmsize.discrete.width);
	printf("height : %x\n", 		frmsize.discrete.height);
	printf("type : %x\n",			frmsize.type);
	printf("index : %x\n",			frmsize.index);
	printf("pixel_format : %x\n",	frmsize.pixel_format);
    printf("==============================================================\n");
    
    // 08通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMEINTERVALS,要求驱动程序返回帧率格式到v4l2_frmivalenum结构体里面
    memset(&fival, 0, sizeof(fival));
	fival.pixel_format = frmsize.pixel_format;
	fival.width = frmsize.discrete.width;
	fival.height = frmsize.discrete.height;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fival))
	{
		printf("get no VIDIOC_ENUM_FRAMEINTERVALS\n");
		goto exit;
	}		
	
    // 09判断设备是否为离散设备。一般是离散设备就设置默认帧率,连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
	if (V4L2_FRMIVAL_TYPE_DISCRETE != fival.type) 
	{
		printf("get no V4L2_FRMIVAL_TYPE_DISCRETE\n");
		goto exit;
	}
	
	int frameRate = fival.discrete.denominator / fival.discrete.numerator; // 计算帧率
	printf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-frameRate : %d\n", frameRate);
    
    // 10:05~09步已经查询到了像素格式、分辨率、帧率,我们已经知道了这个摄像头设备支持的格式,这一步就把之前获取到的图像格式设置进驱动程序,以后驱动程序就会按照我们设置好的图像格式向我们应用程序通过图片
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = capture_type;
    fmt.fmt.pix.width = fival.width;
    fmt.fmt.pix.height = fival.height;
    fmt.fmt.pix.pixelformat = fmtdesc.pixelformat;
	if(-1 == ioctl(fd, VIDIOC_S_FMT, &fmt))
	{
        printf("get no VIDIOC_S_FMT\n");
		goto exit;
    }
    
    printf("set cam get picture fmt success\n");
    
	if(-1 != fd)
		close(fd);
	return 0;
	
exit:
	if(-1 != fd)
		close(fd);
	return 1;
}

编译运行程序,对/dev/video1结点进行查询设置,可以看到实验现象如下,成功设置且打印出来像素格式、分辨率、帧率、图片格式

005.png

3.3编写cam003.c程序,在cam002.c的基础上:申请驱动内存buf,存放摄像头采集到的图片数据,上一步设置好了按照什么格式采集,采集完的图片数据总得有地方放吧,这一步就是解决采集到图片放哪的问题,申请驱动内存存放图片数据,然后把摄像头捕获到图片作为照片保留起来

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/videodev2.h>
#include <string.h>
#include <signal.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdint.h>

int fd = -1;
struct v4l2_capability cap;
enum v4l2_buf_type capture_type;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_frmsizeenum frmsize;
struct v4l2_frmivalenum fival;
struct v4l2_format fmt;
struct v4l2_requestbuffers req_buffers;

void saveJPEGFrame(uint8_t* frameData, size_t frameSize, int frameIndex) {
    char filename[20];
    snprintf(filename, sizeof(filename), "./frame_%d.jpg", frameIndex);

    // 创建输出文件
    FILE* outFile = fopen(filename, "wb");
    if (outFile == NULL) {
        perror("无法创建输出文件");
        return;
    }

    // 将 JPEG 数据写入文件
    fwrite(frameData, 1, frameSize, outFile);

    // 关闭文件
    fclose(outFile);
}

void saveMJPEG(struct v4l2_buffer *buf, uint8_t* buffer, int frameIndex) {
    uint8_t* frameData = buffer + buf->m.offset;
    size_t remainingData = buf->bytesused;

    while (remainingData > 0) {
        // 找到 JPEG 图像的起始位置
        uint8_t* startMarker = frameData;
        size_t frameSize = 0;

        // 在数据中查找 JPEG 图像的结束标记
        while (frameSize < remainingData) {
            if (startMarker[0] == 0xFF && startMarker[1] == 0xD9) {
                // 找到了 JPEG 图像的结束标记
                frameSize += 2; // 包括结束标记本身
                break;
            }
            startMarker++;
            frameSize++;
        }

        // 保存 JPEG 图像
        saveJPEGFrame(frameData, frameSize, frameIndex);

        // 更新下一个 JPEG 图像的起始位置和剩余数据大小
        frameData += frameSize;
        remainingData -= frameSize;
    }
}

int main(int argc, char **argv)
{
	if(2 != argc)
	{
		printf("param error, must like this : @exec_file @devname\n");
		goto exit;
	}
		
	// 01以阻塞形式打开video设备
	if(-1 == (fd = open(argv[1], O_RDWR)))
	{
		printf("open %s failed\n", argv[1]);
		goto exit;
	}

	// 02通过ioctl向已经打开的设备发送命令VIDIOC_QUERYCAP,要求驱动程序返回设备信息到v4l2_capability结构体里面
    memset(&cap, 0, sizeof(cap));
	if(-1 == ioctl(fd, VIDIOC_QUERYCAP, &cap))
	{
		printf("get video dev info failed\n");
		goto exit;
	}
    
    // 03通过设备返回到v4l2_capability结构体里面的capabilities成员来判断这个驱动文件下面挂载的设备有没有捕获图片的能力
    // 没有捕获图片的能力的文件我们就无法从他那里获取摄像头图片,就要退出程序
    if(0 == (V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
	{
		printf("is no V4L2_CAP_VIDEO_CAPTURE dev\n");
		goto exit;
	}
    
    // 04根据cap.device_caps成员识别设备支持的视频输入类型,见上面的3.2.1补充知识点
    if (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE) 
	{
		capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
	} 
	else if (cap.device_caps & V4L2_BUF_TYPE_VIDEO_CAPTURE) 
	{
		capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	} 
	else 
	{
		printf("is no V4L2_CAP_VIDEO\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("version : %x\n",		cap.version);
	printf("card name : %s\n",		cap.card);
	printf("device_caps : %x\n",	cap.device_caps);
	printf("driver name : %s\n",	cap.driver);
    printf("capabilities : %x\n",	cap.capabilities);
	printf("bus_info name : %s\n",	cap.bus_info);
    printf("==============================================================\n");
    
    // 05通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FMT,要求驱动程序返回像素格式到v4l2_fmtdesc结构体里面
	memset(&fmtdesc, 0, sizeof(fmtdesc));
	fmtdesc.type = capture_type; //支持的设备视频输入类型
	fmtdesc.index = 0;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
	{
		printf("get no VIDIOC_ENUM_FMT\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("description : %s\n",	fmtdesc.description);
	printf("type : %x\n",			fmtdesc.type);
	printf("index : %x\n",			fmtdesc.index);
	printf("flags : %x\n",			fmtdesc.flags);
	printf("mbus_code : %x\n",		fmtdesc.mbus_code);
	printf("pixelformat : %x\n",	fmtdesc.pixelformat);
	printf("==============================================================\n");
    
    // 06通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMESIZES,要求驱动程序返回分辨率格式到v4l2_frmsizeenum结构体里面
    memset(&frmsize, 0, sizeof(frmsize));
	frmsize.pixel_format = fmtdesc.pixelformat;
	frmsize.index = 0;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
	{
		printf("get no VIDIOC_ENUM_FRAMESIZES\n");
		goto exit;
	}
    
    // 07判断设备是否为离散设备。一般是离散设备就设置默认帧率,连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
	if(V4L2_FRMSIZE_TYPE_DISCRETE != frmsize.type)
	{
		printf("get no V4L2_FRMSIZE_TYPE_DISCRETE\n");
		goto exit;
	}
    
    printf("==============================================================\n");
    printf("width : %x\n", 			frmsize.discrete.width);
	printf("height : %x\n", 		frmsize.discrete.height);
	printf("type : %x\n",			frmsize.type);
	printf("index : %x\n",			frmsize.index);
	printf("pixel_format : %x\n",	frmsize.pixel_format);
    printf("==============================================================\n");
    
    // 08通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMEINTERVALS,要求驱动程序返回帧率格式到v4l2_frmivalenum结构体里面
    memset(&fival, 0, sizeof(fival));
	fival.pixel_format = frmsize.pixel_format;
	fival.width = frmsize.discrete.width;
	fival.height = frmsize.discrete.height;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fival))
	{
		printf("get no VIDIOC_ENUM_FRAMEINTERVALS\n");
		goto exit;
	}		
	
    // 09判断设备是否为离散设备。一般是离散设备就设置默认帧率,连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
	if (V4L2_FRMIVAL_TYPE_DISCRETE != fival.type) 
	{
		printf("get no V4L2_FRMIVAL_TYPE_DISCRETE\n");
		goto exit;
	}
	
	int frameRate = fival.discrete.denominator / fival.discrete.numerator; // 计算帧率
	printf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-frameRate : %d\n", frameRate);
    
    // 10::05~09步已经查询到了像素格式、分辨率、帧率,我们已经知道了这个摄像头设备支持的格式,这一步就把之前获取到的图像格式设置进驱动程序,以后驱动程序就会按照我们设置好的图像格式向我们应用程序通过图片
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = capture_type;
    fmt.fmt.pix.width = fival.width;
    fmt.fmt.pix.height = fival.height;
    fmt.fmt.pix.pixelformat = fmtdesc.pixelformat;
	if(-1 == ioctl(fd, VIDIOC_S_FMT, &fmt))
	{
        printf("get no VIDIOC_S_FMT\n");
		goto exit;
    }
    
    printf("set cam get picture fmt success\n");
    
    // 11通过ioctl向已经打开的设备发送命令VIDIOC_REQBUFS,申请创建n个buffer内存存放图片数据
    memset(&req_buffers, 0, sizeof(req_buffers));
    req_buffers.type = capture_type; // 摄像头捕获类型
    req_buffers.memory = V4L2_MEMORY_MMAP;
    req_buffers.count = 4; // 申请buffer数量为4
    if(-1 == ioctl(fd, VIDIOC_REQBUFS, &req_buffers)) 
	{
        printf("get no VIDIOC_REQBUFS\n");
		goto exit;
    }
    
    // 定义结构体,含有指针以及指针内容的长度,保存驱动程序映射上来的驱动空间指针
    struct MyVideoBuffer
    {
        unsigned char *data;
        int len;
    };
    struct MyVideoBuffer mVideoBuffer[4];
    
    // 12申请4个buffer,需要循环4次对每个buffer进行初始化参数设置和mmap映射到应用层指针
    for (int i = 0; i < req_buffers.count; i++) 
	{
       	// 定义v4l2_buffer结构,每个buffer需要初始化为什么格式?在buffer结构的成员指定好
        struct v4l2_buffer buffer;
		memset(&buffer, 0, sizeof(buffer));
        buffer.index = i; // 初始化第0~3个buffer中的第i个
        buffer.memory = V4L2_MEMORY_MMAP;
        buffer.type = capture_type; //支持的设备视频输入类型
 
        // 通过ioctl向已经打开的设备发送命令VIDIOC_QUERYBUF,将上面v4l2_buffer结构制定好的参数作为初始值设置到第i个buffer
        if (-1 == ioctl(fd, VIDIOC_QUERYBUF, &buffer)) 
		{
            printf("get %d VIDIOC_QUERYBUF error\n", i);
			goto exit;
        }
		
        // 映射buffer的驱动空间地址到应用层的指针并且保存指针。
        mVideoBuffer[i].data= (unsigned char *)mmap(NULL, buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buffer.m.offset);
        mVideoBuffer[i].len = buffer.length;

        // 通过ioctl向已经打开的设备发送命令VIDIOC_QBUF: 将buffer送到驱动程序的队列排队,准备接收数据。
        if (-1 == ioctl(fd, VIDIOC_QBUF, &buffer)) 
		{
            printf("set %d VIDIOC_QUERYBUF to line error\n", i);
			goto exit;
        }
    }
    
    // 13前面主要完成两件事情,一是设置了捕获图片的格式,二是申请映射好了存放图片数据的内存;那到了这里就可以打开摄像头的开关,摄像头就会像脱缰的野狗开始不断捕捉图片,通过ioctl向已经打开的设备发送命令VIDIOC_STREAMON,摄像头设备就开始拍摄每一张图片了
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (-1 == ioctl(fd, VIDIOC_STREAMON, &type)) 
	{
		printf("set VIDIOC_STREAMON error\n");
		goto exit;
	}
    
    // 下面就是while(1)开始不断读取数据了
    // 因为第01步里面我们是以阻塞的方式打开设备,阻塞方式就是驱动程序采集到图片完成后才会把这张采集到的图片刷新到我们第12步里面申请好的4个buffer里面的某一个,“通过ioctl向已经打开的设备发送命令VIDIOC_QBUF: 将buffer送到驱动程序的队列排队,准备接收数据”在驱动程序采集到图片完成后才会把图片刷新到我们通过命令VIDIOC_QBUF排队到队列里面的buffer,如果你以非阻塞的形式打开设备,就是无论驱动程序有没有采集到图片,哪怕没有图片,也会去刷新我们排队到队列里面的buffer。具体使用阻塞还是非阻塞,看场景,这里为了学习方便用阻塞
    // 既然是阻塞,也就是只有在驱动程序采集到图片完成后才会把图片刷新到buffer,那再没有完成采集的时候那段时间里如果还在while1的循环里面等待,不是太浪费CPU了吗?应该没有完成采集的这段时间把CPU让出来,一完成采集我马上把buffer里面的数据读走。那我们必须借助select函数来实现这种功能
    //通过select设置超时监听
	fd_set fds;
	struct timeval tv;
	FD_ZERO(&fds);
	FD_SET(fd, &fds);
    int frameId = 0;
    while(1)
    {
        FD_ZERO(&fds);
		FD_SET(fd, &fds);
		tv.tv_sec = 0;
		tv.tv_usec = 50000;
		
        // 通过select监听fd这个设备,因为是阻塞的,如果设备采集完成select会触发类似“中断”的东西,返回大于0
        // 如果select监听fd这个设备返回-1失败说明设备还没有采集完数据,就把CPU让出去
		if(0 >= select(fd + 1, &fds, NULL, NULL, &tv)) 
			continue;
        
        // 走到这里说明设备采集完成,没有采集完成的在上面一步就continue;出去了
        // 通过ioctl向已经打开的设备发送命令VIDIOC_DQBUF,从队列获取一个buffer也就是一帧图像的数据
		struct v4l2_buffer t_buffer;
		memset(&t_buffer, 0, sizeof(t_buffer));
		t_buffer.type = capture_type; //支持的设备视频输入类型
		t_buffer.memory = V4L2_MEMORY_MMAP;
		if(-1 == ioctl(fd, VIDIOC_DQBUF, &t_buffer)) 
			continue;
		
        // 判断获取的状态有没有出错
		if (t_buffer.flags & V4L2_BUF_FLAG_ERROR) 
		{
			printf("v4l2 buf error! buf flag 0x%x, index=%d", t_buffer.flags, t_buffer.index);
			continue;
		}
		
		// 保存 MJPEG 图像,从之前映射好的应用层指针那里读取一帧图像的数据,并转化为JPG格式的图片保存在文件里
		saveMJPEG(&t_buffer, mVideoBuffer[t_buffer.index].data, frameId);
		
		// 通过ioctl向已经打开的设备发送命令VIDIOC_QBUF,将用完的buffer送回队列重新等待填充数据
		if (-1 == ioctl(fd, VIDIOC_QBUF, &t_buffer)) 
			continue;
        
        frameId++; // 更新一次图片的编号,保存为每张图片的id
        printf("KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK-1 len::%d index::%d\n", t_buffer.length,t_buffer.index);
    }
    
    // 14通过ioctl向已经打开的设备发送命令VIDIOC_STREAMOFF,关闭图像采集
    if(-1 == ioctl(fd, VIDIOC_STREAMOFF, &type)) 
	{
        printf("set VIDIOC_STREAMOFF error\n");
		goto exit;
    }
	
    // 15释放buffer映射
    for(int i=0; i<4; i++) 
        munmap(mVideoBuffer[i].data, mVideoBuffer[i].len);
    
    // 16通过ioctl向已经打开的设备发送命令VIDIOC_REQBUFS: 清除buffer
    struct v4l2_requestbuffers req_buffers_clear;
	memset(&req_buffers_clear, 0, sizeof(req_buffers_clear));
    req_buffers_clear.type = capture_type; //支持的设备视频输入类型
    req_buffers_clear.memory = V4L2_MEMORY_MMAP;
    req_buffers_clear.count = 0;
    if(-1 == ioctl(fd, VIDIOC_REQBUFS, &req_buffers_clear)) 
	{
        printf("set VIDIOC_REQBUFS error\n");
		goto exit;
    }
    
    // 17关闭设备
	if(-1 != fd)
		close(fd);
	return 0;
	
exit:
	if(-1 != fd)
		close(fd);
	return -1;
}

编译运行我们上面的代码。执行起来,然后可以看到摄像头捕捉到的照片已经作为文件保存在本路径下面了。。。在不借助中间件的库的情况下,我们通过编写应用程序直接从驱动获取照片。对于初学的你,有没有觉得自己又进步了?

006.png

007.png

4.总结

怎么编写应用程序从驱动获取摄像头照片,可分为几个步骤:

1.打开设备,ioctl发命令给驱动程序获取、设置图像格式参数

2.分配内存、映射内存;存放采集到的图片数据

3.循环获取、保存获取到的图片数据到文件

4.解除内存映射

5.释放内存,关闭设备

最终版本代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/videodev2.h>
#include <string.h>
#include <signal.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdint.h>

int fd = -1;
struct v4l2_capability cap;
enum v4l2_buf_type capture_type;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_frmsizeenum frmsize;
struct v4l2_frmivalenum fival;
struct v4l2_format fmt;
struct v4l2_requestbuffers req_buffers;

void saveJPEGFrame(uint8_t* frameData, size_t frameSize, int frameIndex) {
    char filename[20];
    snprintf(filename, sizeof(filename), "./frame_%d.jpg", frameIndex);

    // 创建输出文件
    FILE* outFile = fopen(filename, "wb");
    if (outFile == NULL) {
        perror("无法创建输出文件");
        return;
    }

    // 将 JPEG 数据写入文件
    fwrite(frameData, 1, frameSize, outFile);

    // 关闭文件
    fclose(outFile);
}

void saveMJPEG(struct v4l2_buffer *buf, uint8_t* buffer, int frameIndex) {
    uint8_t* frameData = buffer + buf->m.offset;
    size_t remainingData = buf->bytesused;

    while (remainingData > 0) {
        // 找到 JPEG 图像的起始位置
        uint8_t* startMarker = frameData;
        size_t frameSize = 0;

        // 在数据中查找 JPEG 图像的结束标记
        while (frameSize < remainingData) {
            if (startMarker[0] == 0xFF && startMarker[1] == 0xD9) {
                // 找到了 JPEG 图像的结束标记
                frameSize += 2; // 包括结束标记本身
                break;
            }
            startMarker++;
            frameSize++;
        }

        // 保存 JPEG 图像
        saveJPEGFrame(frameData, frameSize, frameIndex);

        // 更新下一个 JPEG 图像的起始位置和剩余数据大小
        frameData += frameSize;
        remainingData -= frameSize;
    }
}

volatile int is_exec = 1;

void handler(int signum) 
{
	is_exec = 0;
}

int main(int argc, char **argv)
{
	if(2 != argc)
	{
		printf("param error, must like this : @exec_file @devname\n");
		goto exit;
	}
	
	// 注册信号响应函数,在收到CTL+C信号的时候退出主循环,释放内存关闭设备
	signal(SIGINT, handler);
		
	// 01以阻塞形式打开video设备
	if(-1 == (fd = open(argv[1], O_RDWR)))
	{
		printf("open %s failed\n", argv[1]);
		goto exit;
	}

	// 02通过ioctl向已经打开的设备发送命令VIDIOC_QUERYCAP,要求驱动程序返回设备信息到v4l2_capability结构体里面
    memset(&cap, 0, sizeof(cap));
	if(-1 == ioctl(fd, VIDIOC_QUERYCAP, &cap))
	{
		printf("get video dev info failed\n");
		goto exit;
	}
    
    // 03通过设备返回到v4l2_capability结构体里面的capabilities成员来判断这个驱动文件下面挂载的设备有没有捕获图片的能力
    // 没有捕获图片的能力的文件我们就无法从他那里获取摄像头图片,就要退出程序
    if(0 == (V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
	{
		printf("is no V4L2_CAP_VIDEO_CAPTURE dev\n");
		goto exit;
	}
    
    // 04根据cap.device_caps成员识别设备支持的视频输入类型,见上面的3.2.1补充知识点
    if (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE) 
	{
		capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
	} 
	else if (cap.device_caps & V4L2_BUF_TYPE_VIDEO_CAPTURE) 
	{
		capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	} 
	else 
	{
		printf("is no V4L2_CAP_VIDEO\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("version : %x\n",		cap.version);
	printf("card name : %s\n",		cap.card);
	printf("device_caps : %x\n",	cap.device_caps);
	printf("driver name : %s\n",	cap.driver);
    printf("capabilities : %x\n",	cap.capabilities);
	printf("bus_info name : %s\n",	cap.bus_info);
    printf("==============================================================\n");
    
    // 05通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FMT,要求驱动程序返回像素格式到v4l2_fmtdesc结构体里面
	memset(&fmtdesc, 0, sizeof(fmtdesc));
	fmtdesc.type = capture_type; //支持的设备视频输入类型
	fmtdesc.index = 0;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
	{
		printf("get no VIDIOC_ENUM_FMT\n");
		goto exit;
	}
	
    printf("==============================================================\n");
    printf("description : %s\n",	fmtdesc.description);
	printf("type : %x\n",			fmtdesc.type);
	printf("index : %x\n",			fmtdesc.index);
	printf("flags : %x\n",			fmtdesc.flags);
	printf("mbus_code : %x\n",		fmtdesc.mbus_code);
	printf("pixelformat : %x\n",	fmtdesc.pixelformat);
	printf("==============================================================\n");
    
    // 06通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMESIZES,要求驱动程序返回分辨率格式到v4l2_frmsizeenum结构体里面
    memset(&frmsize, 0, sizeof(frmsize));
	frmsize.pixel_format = fmtdesc.pixelformat;
	frmsize.index = 0;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
	{
		printf("get no VIDIOC_ENUM_FRAMESIZES\n");
		goto exit;
	}
    
    // 07判断设备是否为离散设备。一般是离散设备就设置默认帧率,连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
	if(V4L2_FRMSIZE_TYPE_DISCRETE != frmsize.type)
	{
		printf("get no V4L2_FRMSIZE_TYPE_DISCRETE\n");
		goto exit;
	}
    
    printf("==============================================================\n");
    printf("width : %x\n", 			frmsize.discrete.width);
	printf("height : %x\n", 		frmsize.discrete.height);
	printf("type : %x\n",			frmsize.type);
	printf("index : %x\n",			frmsize.index);
	printf("pixel_format : %x\n",	frmsize.pixel_format);
    printf("==============================================================\n");
    
    // 08通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMEINTERVALS,要求驱动程序返回帧率格式到v4l2_frmivalenum结构体里面
    memset(&fival, 0, sizeof(fival));
	fival.pixel_format = frmsize.pixel_format;
	fival.width = frmsize.discrete.width;
	fival.height = frmsize.discrete.height;
	if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fival))
	{
		printf("get no VIDIOC_ENUM_FRAMEINTERVALS\n");
		goto exit;
	}		
	
    // 09判断设备是否为离散设备。一般是离散设备就设置默认帧率,连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
	if (V4L2_FRMIVAL_TYPE_DISCRETE != fival.type) 
	{
		printf("get no V4L2_FRMIVAL_TYPE_DISCRETE\n");
		goto exit;
	}
	
	int frameRate = fival.discrete.denominator / fival.discrete.numerator; // 计算帧率
	printf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-frameRate : %d\n", frameRate);
    
    // 10::05~09步已经查询到了像素格式、分辨率、帧率,我们已经知道了这个摄像头设备支持的格式,这一步就把之前获取到的图像格式设置进驱动程序,以后驱动程序就会按照我们设置好的图像格式向我们应用程序通过图片
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = capture_type;
    fmt.fmt.pix.width = fival.width;
    fmt.fmt.pix.height = fival.height;
    fmt.fmt.pix.pixelformat = fmtdesc.pixelformat;
	if(-1 == ioctl(fd, VIDIOC_S_FMT, &fmt))
	{
        printf("get no VIDIOC_S_FMT\n");
		goto exit;
    }
    
    printf("set cam get picture fmt success\n");
    
    // 11通过ioctl向已经打开的设备发送命令VIDIOC_REQBUFS,申请创建n个buffer内存存放图片数据
    memset(&req_buffers, 0, sizeof(req_buffers));
    req_buffers.type = capture_type; // 摄像头捕获类型
    req_buffers.memory = V4L2_MEMORY_MMAP;
    req_buffers.count = 4; // 申请buffer数量为4
    if(-1 == ioctl(fd, VIDIOC_REQBUFS, &req_buffers)) 
	{
        printf("get no VIDIOC_REQBUFS\n");
		goto exit;
    }
    
    // 定义结构体,含有指针以及指针内容的长度,保存驱动程序映射上来的驱动空间指针
    struct MyVideoBuffer
    {
        unsigned char *data;
        int len;
    };
    struct MyVideoBuffer mVideoBuffer[4];
    
    // 12申请4个buffer,需要循环4次对每个buffer进行初始化参数设置和mmap映射到应用层指针
    for (int i = 0; i < req_buffers.count; i++) 
	{
       	// 定义v4l2_buffer结构,每个buffer需要初始化为什么格式?在buffer结构的成员指定好
        struct v4l2_buffer buffer;
		memset(&buffer, 0, sizeof(buffer));
        buffer.index = i; // 初始化第0~3个buffer中的第i个
        buffer.memory = V4L2_MEMORY_MMAP;
        buffer.type = capture_type; //支持的设备视频输入类型
 
        // 通过ioctl向已经打开的设备发送命令VIDIOC_QUERYBUF,将上面v4l2_buffer结构制定好的参数作为初始值设置到第i个buffer
        if (-1 == ioctl(fd, VIDIOC_QUERYBUF, &buffer)) 
		{
            printf("get %d VIDIOC_QUERYBUF error\n", i);
			goto exit;
        }
		
        // 映射buffer的驱动空间地址到应用层的指针并且保存指针。
        mVideoBuffer[i].data= (unsigned char *)mmap(NULL, buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buffer.m.offset);
        mVideoBuffer[i].len = buffer.length;

        // 通过ioctl向已经打开的设备发送命令VIDIOC_QBUF: 将buffer送到驱动程序的队列排队,准备接收数据。
        if (-1 == ioctl(fd, VIDIOC_QBUF, &buffer)) 
		{
            printf("set %d VIDIOC_QUERYBUF to line error\n", i);
			goto exit;
        }
    }
    
    // 13前面主要完成两件事情,一是设置了捕获图片的格式,二是申请映射好了存放图片数据的内存;那到了这里就可以打开摄像头的开关,摄像头就会像脱缰的野狗开始不断捕捉图片,通过ioctl向已经打开的设备发送命令VIDIOC_STREAMON,摄像头设备就开始拍摄每一张图片了
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (-1 == ioctl(fd, VIDIOC_STREAMON, &type)) 
	{
		printf("set VIDIOC_STREAMON error\n");
		goto exit;
	}
    
    // 下面就是while(1)开始不断读取数据了
    // 因为第01步里面我们是以阻塞的方式打开设备,阻塞方式就是驱动程序采集到图片完成后才会把这张采集到的图片刷新到我们第12步里面申请好的4个buffer里面的某一个,“通过ioctl向已经打开的设备发送命令VIDIOC_QBUF: 将buffer送到驱动程序的队列排队,准备接收数据”在驱动程序采集到图片完成后才会把图片刷新到我们通过命令VIDIOC_QBUF排队到队列里面的buffer,如果你以非阻塞的形式打开设备,就是无论驱动程序有没有采集到图片,哪怕没有图片,也会去刷新我们排队到队列里面的buffer。具体使用阻塞还是非阻塞,看场景,这里为了学习方便用阻塞
    // 既然是阻塞,也就是只有在驱动程序采集到图片完成后才会把图片刷新到buffer,那再没有完成采集的时候那段时间里如果还在while1的循环里面等待,不是太浪费CPU了吗?应该没有完成采集的这段时间把CPU让出来,一完成采集我马上把buffer里面的数据读走。那我们必须借助select函数来实现这种功能
    //通过select设置超时监听
	fd_set fds;
	struct timeval tv;
	FD_ZERO(&fds);
	FD_SET(fd, &fds);
    int frameId = 0;
    while(is_exec)
    {
        FD_ZERO(&fds);
		FD_SET(fd, &fds);
		tv.tv_sec = 0;
		tv.tv_usec = 50000;
		
        // 通过select监听fd这个设备,因为是阻塞的,如果设备采集完成select会触发类似“中断”的东西,返回大于0
        // 如果select监听fd这个设备返回-1失败说明设备还没有采集完数据,就把CPU让出去
		if(0 >= select(fd + 1, &fds, NULL, NULL, &tv)) 
			continue;
        
        // 走到这里说明设备采集完成,没有采集完成的在上面一步就continue;出去了
        // 通过ioctl向已经打开的设备发送命令VIDIOC_DQBUF,从队列获取一个buffer也就是一帧图像的数据
		struct v4l2_buffer t_buffer;
		memset(&t_buffer, 0, sizeof(t_buffer));
		t_buffer.type = capture_type; //支持的设备视频输入类型
		t_buffer.memory = V4L2_MEMORY_MMAP;
		if(-1 == ioctl(fd, VIDIOC_DQBUF, &t_buffer)) 
			continue;
		
        // 判断获取的状态有没有出错
		if (t_buffer.flags & V4L2_BUF_FLAG_ERROR) 
		{
			printf("v4l2 buf error! buf flag 0x%x, index=%d", t_buffer.flags, t_buffer.index);
			continue;
		}
		
		// 保存 MJPEG 图像,从之前映射好的应用层指针那里读取一帧图像的数据,并转化为JPG格式的图片保存在文件里
		saveMJPEG(&t_buffer, mVideoBuffer[t_buffer.index].data, frameId);
		
		// 通过ioctl向已经打开的设备发送命令VIDIOC_QBUF,将用完的buffer送回队列重新等待填充数据
		if (-1 == ioctl(fd, VIDIOC_QBUF, &t_buffer)) 
			continue;
        
        frameId++; // 更新一次图片的编号,保存为每张图片的id
        printf("KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK-1 len::%d index::%d\n", t_buffer.length,t_buffer.index);
    }
    
    // 14通过ioctl向已经打开的设备发送命令VIDIOC_STREAMOFF,关闭图像采集
    if(-1 == ioctl(fd, VIDIOC_STREAMOFF, &type)) 
	{
        printf("set VIDIOC_STREAMOFF error\n");
		goto exit;
    }
	
    // 15释放buffer映射
    for(int i=0; i<4; i++) 
        munmap(mVideoBuffer[i].data, mVideoBuffer[i].len);
    
    // 16通过ioctl向已经打开的设备发送命令VIDIOC_REQBUFS: 清除buffer
    struct v4l2_requestbuffers req_buffers_clear;
	memset(&req_buffers_clear, 0, sizeof(req_buffers_clear));
    req_buffers_clear.type = capture_type; //支持的设备视频输入类型
    req_buffers_clear.memory = V4L2_MEMORY_MMAP;
    req_buffers_clear.count = 0;
    if(-1 == ioctl(fd, VIDIOC_REQBUFS, &req_buffers_clear)) 
	{
        printf("set VIDIOC_REQBUFS error\n");
		goto exit;
    }
    
    // 17关闭设备
	if(-1 != fd)
		close(fd);
	return 0;
	
exit:
	if(-1 != fd)
		close(fd);
	return -1;
}

5.写在最后

对v4l2驱动程序函数的调用步骤是一个非常固定的套路,在第4点总结中已经给出。每句代码也做详细的注释。可以仔细阅读

附加一个cpp版本的demo

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/videodev2.h>
#include <string.h>
#include <signal.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdint.h>
#include <string>
#include <cstdio>
#include <vector>
#include <iostream>

// 定义结构体,含有指针以及指针内容的长度,保存驱动程序映射上来的驱动空间指针
struct MyVideoBuffer
{
	uint8_t *data;
	int len;
};

class GetFrame
{
public:
	GetFrame(std::string &t_devname, int t_frameBufNums);
	GetFrame(std::string &&t_devname, int t_frameBufNums);
	~GetFrame();

public:
	int InitDev(void);
	int GetFrameBuffer(void);
	int StreamOn(void);
	int StreamOff(void);
	int UnInitDev(void);
	int GetFd(void);
	void CloseFd(void);
	void SavePictureToFile(struct v4l2_buffer &buf, int t_frameId);
	enum v4l2_buf_type GetCaptureType(void);
	
private:
	void saveJPEGFrame(uint8_t* frameData, size_t frameSize, int frameIndex);
	void saveMJPEG(struct v4l2_buffer &buf, uint8_t* buffer, int frameIndex);
	void SetFrameBufNums(int t_frameBufNums);

private:
	std::string devname;
	int fd{-1};
	int frameRate{0};
	int frameBufNums{5};
	enum v4l2_buf_type capture_type;
	std::vector<MyVideoBuffer> mVideoBuffer;
};

enum v4l2_buf_type GetFrame::GetCaptureType(void)
{
	return capture_type;
}

GetFrame::GetFrame(std::string &t_devname, int t_frameBufNums)
{
	devname.clear();
	devname.swap(t_devname);

	mVideoBuffer.clear();
	SetFrameBufNums(t_frameBufNums);
}

GetFrame::GetFrame(std::string &&t_devname, int t_frameBufNums) : devname(std::move(t_devname))
{
	mVideoBuffer.clear();
	SetFrameBufNums(t_frameBufNums);
}

GetFrame::~GetFrame()
{
	devname.clear();
	StreamOff();
  UnInitDev();

	if(-1 != fd)
		close(fd);
}

int GetFrame::StreamOn(void)
{
	// 13前面主要完成两件事情,一是设置了捕获图片的格式,二是申请映射好了存放图片数据的内存;
	// 那到了这里就可以打开摄像头的开关,摄像头就会像脱缰的野狗开始不断捕捉图片,通过ioctl向已经打开的设备发送命令VIDIOC_STREAMON,摄像头设备就开始拍摄每一张图片了
  int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	return ioctl(fd, VIDIOC_STREAMON, &type);
}

int GetFrame::StreamOff(void)
{
  // 14通过ioctl向已经打开的设备发送命令VIDIOC_STREAMOFF,关闭图像采集
	int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  return ioctl(fd, VIDIOC_STREAMOFF, &type);
}

int GetFrame::InitDev(void)
{
	do
	{
		if(devname.empty())
		{
			std::cout << "has no devname" << std::endl;
			break;
		}

		if(-1 == (fd = open(devname.c_str(), O_RDWR)))
		{
			std::cout << "open " << devname <<" failed" << std::endl;
			break;
		}

		// 02通过ioctl向已经打开的设备发送命令VIDIOC_QUERYCAP
		// 要求驱动程序返回设备信息到v4l2_capability结构体里面
    struct v4l2_capability cap;
		memset(&cap, 0, sizeof(cap));
		if(-1 == ioctl(fd, VIDIOC_QUERYCAP, &cap))
		{
			std::cout << "get video dev info failed" << std::endl;
			break;
		}

		// 03通过设备返回到v4l2_capability结构体里面的capabilities成员
		// 来判断这个驱动文件下面挂载的设备有没有捕获图片的能力
    // 没有捕获图片的能力的文件我们就无法从他那里获取摄像头图片,就要退出程序
		if(0 == (V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
		{
			std::cout << "is no cap dev" << std::endl;
			break;
		}
    
    // 04根据cap.device_caps成员识别设备支持的视频输入类型
    if (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE) 
		{
			capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
		} 
		else if (cap.device_caps & V4L2_BUF_TYPE_VIDEO_CAPTURE) 
		{
			capture_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		} 
		else 
		{
			std::cout << "unknow cap_type dev" << std::endl;
			break;
		}

		// 05通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FMT
		// 要求驱动程序返回像素格式到v4l2_fmtdesc结构体里面
		struct v4l2_fmtdesc fmtdesc;
		memset(&fmtdesc, 0, sizeof(fmtdesc));
		fmtdesc.type = capture_type; //支持的设备视频输入类型
		fmtdesc.index = 0;
		if(-1 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
		{
			std::cout << "get enum fmt failed" << std::endl;
			break;
		}

		// 06通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMESIZES
		// 要求驱动程序返回分辨率格式到v4l2_frmsizeenum结构体里面
		struct v4l2_frmsizeenum frmsize;
    memset(&frmsize, 0, sizeof(frmsize));
		frmsize.pixel_format = fmtdesc.pixelformat;
		frmsize.index = 0;
		if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
		{
			std::cout << "get enum frame size failed" << std::endl;
			break;
		}
    
    // 07判断设备是否为离散设备。一般是离散设备就设置默认帧率
		// 连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
		if(V4L2_FRMSIZE_TYPE_DISCRETE != frmsize.type)
		{
			std::cout << "frame type is no discrete" << std::endl;
			break;
		}

		// 08通过ioctl向已经打开的设备发送命令VIDIOC_ENUM_FRAMEINTERVALS
		// 要求驱动程序返回帧率格式到v4l2_frmivalenum结构体里面
		struct v4l2_frmivalenum fival;
    memset(&fival, 0, sizeof(fival));
		fival.pixel_format = frmsize.pixel_format;
		fival.width = frmsize.discrete.width;
		fival.height = frmsize.discrete.height;
		if(-1 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fival))
		{
			std::cout << "get enum frame intervals size failed" << std::endl;
			break;
		}		
	
    // 09判断设备是否为离散设备。一般是离散设备就设置默认帧率
		// 连续设备的的话考虑最大可接受的帧率,这里按照离散设备做处理
		if (V4L2_FRMIVAL_TYPE_DISCRETE != fival.type) 
		{
			std::cout << "frame ival type is no discrete" << std::endl;
			break;
		}
	
		frameRate = fival.discrete.denominator / fival.discrete.numerator; // 计算帧率

    // 10::05~09步已经查询到了像素格式、分辨率、帧率,我们已经知道了这个摄像头设备支持的格式
		// 这一步就把之前获取到的图像格式设置进驱动程序,以后驱动程序就会按照我们设置好的图像格式向我们应用程序通过图片
		struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = capture_type;
    fmt.fmt.pix.width = fival.width;
    fmt.fmt.pix.height = fival.height;
    fmt.fmt.pix.pixelformat = fmtdesc.pixelformat;
		if(-1 == ioctl(fd, VIDIOC_S_FMT, &fmt))
		{
			std::cout << "set s fmt failed" << std::endl;
			break;
		}

		return 0;
	} while(0);

	return -1;
}

int GetFrame::GetFrameBuffer(void)
{
	do
	{
    // 11通过ioctl向已经打开的设备发送命令VIDIOC_REQBUFS,申请创建n个buffer内存存放图片数据
		struct v4l2_requestbuffers req_buffers;
    memset(&req_buffers, 0, sizeof(req_buffers));
    req_buffers.type = capture_type; // 摄像头捕获类型
    req_buffers.memory = V4L2_MEMORY_MMAP;
    req_buffers.count = frameBufNums; // 申请buffer数量
    if(-1 == ioctl(fd, VIDIOC_REQBUFS, &req_buffers)) 
		{
      std::cout << "set s fmt failed" << std::endl;
			break;
    }
    
		// ... 
    if(mVideoBuffer.empty())
		{
			for (int i = 0; i < frameBufNums; i++)
			{
				MyVideoBuffer t_oMyVideoBuffer;
				t_oMyVideoBuffer.data = nullptr;
				t_oMyVideoBuffer.len = 0;
				mVideoBuffer.emplace_back(t_oMyVideoBuffer);
			}
		}
    
    // 12申请4个buffer,需要循环4次对每个buffer进行初始化参数设置和mmap映射到应用层指针
    for (int i = 0; i < req_buffers.count; i++) 
		{
			// 定义v4l2_buffer结构,每个buffer需要初始化为什么格式?在buffer结构的成员指定好
			struct v4l2_buffer buffer;
			memset(&buffer, 0, sizeof(buffer));
			buffer.index = i; // 初始化第0~3个buffer中的第i个
			buffer.memory = V4L2_MEMORY_MMAP;
			buffer.type = capture_type; //支持的设备视频输入类型
 
      // 通过ioctl向已经打开的设备发送命令VIDIOC_QUERYBUF,将上面v4l2_buffer结构制定好的参数作为初始值设置到第i个buffer
      if (-1 == ioctl(fd, VIDIOC_QUERYBUF, &buffer)) 
			{
				std::cout << "set " << i << " error" << std::endl;
				break;
      }
		
			// 映射buffer的驱动空间地址到应用层的指针并且保存指针。
			mVideoBuffer[i].data= (unsigned char *)mmap(NULL, buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buffer.m.offset);
			mVideoBuffer[i].len = buffer.length;

			// 通过ioctl向已经打开的设备发送命令VIDIOC_QBUF: 将buffer送到驱动程序的队列排队,准备接收数据。
			if (-1 == ioctl(fd, VIDIOC_QBUF, &buffer)) 
			{
				std::cout << "set " << i << " to line error" << std::endl;
				break;
			}
    }

		return 0;
	} while(0);

	return -1;
}

void GetFrame::CloseFd(void)
{
	if(-1 != fd)
		close(fd);
}

void GetFrame::saveJPEGFrame(uint8_t* frameData, size_t frameSize, int frameIndex) 
{
	std::string filename;
	filename.clear();
	filename.append("./frame_");
	filename.append(std::to_string(frameIndex));
	filename.append(".jpg");

  // 创建输出文件
	FILE* outFile = fopen(filename.c_str(), "wb");
	if(outFile == NULL) 
	{
		perror("无法创建输出文件");
		return;
	}

	// 将 JPEG 数据写入文件
	fwrite(frameData, 1, frameSize, outFile);

	// 关闭文件
	fclose(outFile);
}

void GetFrame::saveMJPEG(struct v4l2_buffer &buf, uint8_t* buffer, int frameIndex) 
{
	uint8_t* frameData = buffer + buf.m.offset;
	size_t remainingData = buf.bytesused;

	while (remainingData > 0) 
	{
		// 找到 JPEG 图像的起始位置
		uint8_t* startMarker = frameData;
		size_t frameSize = 0;

		// 在数据中查找 JPEG 图像的结束标记
		while (frameSize < remainingData) 
		{
			if (startMarker[0] == 0xFF && startMarker[1] == 0xD9) 
			{
				// 找到了 JPEG 图像的结束标记
				frameSize += 2; // 包括结束标记本身
				break;
			}
			startMarker++;
			frameSize++;
		}

		// 保存 JPEG 图像
		saveJPEGFrame(frameData, frameSize, frameIndex);

		// 更新下一个 JPEG 图像的起始位置和剩余数据大小
		frameData += frameSize;
		remainingData -= frameSize;
	}
}

void GetFrame::SetFrameBufNums(int t_frameBufNums)
{
	if(0 > t_frameBufNums)
		return ;
	frameBufNums = t_frameBufNums;
}

int GetFrame::UnInitDev(void)
{
	// 15释放buffer映射
  for(int i=0; i<frameBufNums; i++) 
    munmap(mVideoBuffer[i].data, mVideoBuffer[i].len);
    
  // 16通过ioctl向已经打开的设备发送命令VIDIOC_REQBUFS: 清除buffer
  struct v4l2_requestbuffers req_buffers_clear;
	memset(&req_buffers_clear, 0, sizeof(req_buffers_clear));
  req_buffers_clear.type = capture_type; //支持的设备视频输入类型
  req_buffers_clear.memory = V4L2_MEMORY_MMAP;
  req_buffers_clear.count = 0;
  return ioctl(fd, VIDIOC_REQBUFS, &req_buffers_clear);
}

int GetFrame::GetFd(void)
{
	return fd;
}

void GetFrame::SavePictureToFile(struct v4l2_buffer &buf, int t_frameId)
{
	saveMJPEG(buf, mVideoBuffer[buf.index].data, t_frameId);
}

volatile int is_exec = 1;

void handler(int signum) 
{
	is_exec = 0;
}

int main(void)
{
	// 注册信号响应函数,在收到CTL+C信号的时候退出主循环,释放内存关闭设备
	signal(SIGINT, handler);

	GetFrame m_GetFrame("/dev/video1", 5);
	m_GetFrame.InitDev();
	m_GetFrame.GetFrameBuffer();
	m_GetFrame.StreamOn();
	int fd = m_GetFrame.GetFd();
	
	fd_set fds;
	struct timeval tv;
	FD_ZERO(&fds);
	FD_SET(fd, &fds);
  int frameId = 0;
  while(is_exec)
  {
    FD_ZERO(&fds);
		FD_SET(fd, &fds);
		tv.tv_sec = 0;
		tv.tv_usec = 50000;
		
		if(0 >= select(fd + 1, &fds, NULL, NULL, &tv)) 
			continue;
    
		struct v4l2_buffer t_buffer;
		memset(&t_buffer, 0, sizeof(t_buffer));
		t_buffer.type = m_GetFrame.GetCaptureType(); //支持的设备视频输入类型
		t_buffer.memory = V4L2_MEMORY_MMAP;
		if(-1 == ioctl(fd, VIDIOC_DQBUF, &t_buffer)) 
			continue;
		
    // 判断获取的状态有没有出错
		if (t_buffer.flags & V4L2_BUF_FLAG_ERROR) 
			continue;
		
		// 保存 MJPEG 图像,从之前映射好的应用层指针那里读取一帧图像的数据,并转化为JPG格式的图片保存在文件里
		// 或者做处理,发送给http进程
		m_GetFrame.SavePictureToFile(t_buffer, frameId);

		// 通过ioctl向已经打开的设备发送命令VIDIOC_QBUF,将用完的buffer送回队列重新等待填充数据
		if (-1 == ioctl(fd, VIDIOC_QBUF, &t_buffer)) 
			continue;
    
  	frameId++; // 更新一次图片的编号,保存为每张图片的id
    printf("KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK-1 len::%d index::%d\n", t_buffer.length,t_buffer.index);
  }
  
	return 0;
}