【北京迅为】《iTOP-3588开发板系统编程手册》-第19章 V4L2摄像头应用编程

RK3588是一款低功耗、高性能的处理器,适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用,RK3588支持8K视频编解码,内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP,内置NPU,支持INT4/INT8/INT16/FP16混合运算能力,支持安卓12和、Debian11、Build root、Ubuntu20和22版本登系统。了解更多信息可点击迅为官网   

【粉丝群】824412014

【实验平台】:迅为RK3588开发板

【内容来源】《iTOP-3588开发板系统编程手册》

【全套资料及网盘获取方式】联系淘宝客服加入售后技术支持群内下载

【视频介绍】:【强者之芯】 新一代AIOT高端应用芯片 iTOP -3588人工智能工业AI主板


第19章 V4L2摄像头应用编程 

在本章内容开始之前需要注意的是,本章节用到的摄像头为USB接口的UVC摄像头,开发板配套的OV5695摄像头不支持本章节实验(之后会进行适配)。

19.1 V4L2介绍

V4L2 (Video4Linux2) 是 Linux 内核中的一个框架,提供了一套用于视频设备驱动程序开发的 API。它是一个开放的、通用的、模块化的视频设备驱动程序框架,允许 Linux 操作系统和应用程序与各种视频设备(如摄像头、视频采集卡等)进行交互。

V4L2 提供了一个通用的 API,使应用程序能够访问和控制视频设备,包括获取设备信息、设置设备参数、采集视频数据、控制设备状态等。V4L2 还提供了一个统一的视频数据格式,允许应用程序在处理视频数据时无需考虑设备的具体格式。

下面我们来详细介绍一下 V4L2 的主要特性:

(1)模块化的架构

V4L2 是一个模块化的架构,允许多个设备驱动程序同时存在并共享同一个 API。每个设备驱动程序都是一个独立的内核模块,可以在运行时加载和卸载。这种架构可以使开发人员更容易地开发新的视频设备驱动程序,并允许多个驱动程序同时使用相同的 API。

(2)统一的设备节点

V4L2 提供了一种统一的设备节点,使应用程序可以使用相同的方式访问不同类型的视频设备。这种节点通常是 /dev/videoX,其中 X 是一个数字,表示设备的编号。应用程序可以通过打开这个节点来访问设备,并使用 V4L2 API 进行数据采集和控制。Buildroot系统启动之后使用以下命令对videoX节点进行查看,如下图所示:

ls /dev/video

(3)统一的视频数据格式

V4L2 提供了一个统一的视频数据格式,称为 V4L2_PIX_FMT,允许应用程序在处理视频数据时无需考虑设备的具体格式。V4L2_PIX_FMT 包括了许多常见的视频格式,如 RGB、YUV 等。应用程序可以使用 V4L2 API 来查询设备支持的数据格式,并选择适当的格式进行数据采集和处理。

(4)支持多种视频设备

V4L2 支持许多不同类型的视频设备,包括摄像头、视频采集卡、TV 卡等。每个设备都有自己的驱动程序,提供了相应的 V4L2 API。这些驱动程序可以根据设备的不同特性,提供不同的采集模式、数据格式、控制参数等。

(5)支持流式 I/O

V4L2 支持流式 I/O,即通过内存映射的方式将视频数据从设备直接传输到应用程序中。这种方式可以减少数据复制的次数,提高数据传输的效率。

(6)支持控制参数

  V4L2 允许应用程序通过 API 来控制视频设备的参数,包括亮度、对比度、色彩饱和度、曝光时间等。应用程序可以使用 V4L2 API 来查询设备支持的参数,并设置适当的值。

(7)支持事件通知

 V4L2 支持事件通知,当视频设备状态发生变化时,如视频信号丢失、帧率变化等,V4L2 驱动程序可以向应用程序发送通知,以便应用程序做出相应的处理。

从上面的特征可以看出,V4L2 提供了一套通用、灵活、可扩展的视频设备驱动程序框架,使得 Linux 操作系统和应用程序可以方便地与各种视频设备进行交互,并且不需要关心设备的具体实现细节。从而让开发人员能够更加专注于应用程序的开发。

19.2 V4L2视频采集步骤

V4L2视频采集的常用步骤如下所示:

步骤

步骤描述

1

打开视频设备

使用 open() 系统调用打开相应的视频设备文件,获取文件描述符以便后续的操作。

2

查询设备能力

使用 ioctl() 系统调用发送 VIDIOC_QUERYCAP 命令查询视频设备的基本信息,如设备名称、版本号、驱动程序信息等。

3

设置采集参数

使用 ioctl() 系统调用发送 VIDIOC_S_FMT 命令来设置采集参数,如视频格式、分辨率、帧率等。需要检查参数是否被设备支持。

4

请求帧缓冲

使用 ioctl() 系统调用发送 VIDIOC_REQBUFS 命令来请求帧缓冲,指定帧缓冲的数量和类型等参数。

5

映射帧缓冲

使用 ioctl() 系统调用发送 VIDIOC_QUERYBUF 命令来查询帧缓冲的信息,如帧缓冲的地址和大小等。然后使用 mmap() 系统调用将帧缓冲映射到用户空间。

6

启动视频采集

使用 ioctl() 系统调用发送 VIDIOC_STREAMON 命令来启动视频采集,视频设备开始采集视频帧并将其存储到帧缓冲中。

7

读取视频帧数据

使用 read() 系统调用读取视频帧数据,也可以使用 select() 系统调用等待视频帧的到来并读取视频帧数据。

8

停止视频采集和释放资源

使用 ioctl() 系统调用发送 VIDIOC_STREAMOFF 命令来停止视频采集。然后使用 munmap() 系统调用将帧缓冲从用户空间解除映射,最后使用 close() 系统调用关闭视频设备,释放资源。

对于打开和关闭设备想必大家已经非常熟悉了,本小节将对上述用到的ioctl参数和宏进行讲解。

19.2.1查询设备能力

在使用V4L2进行视频采集前,需要先通过查询设备能力来获取设备可以提供的视频格式、分辨率等信息。

(1)查询设备的基本信息

在程序中使用VIDIOC_QUERYCAP命令通过ioctl()函数查询设备的基本信息,例如设备名称、版本号以及已支持的标准等等,使用代码如下所示:

// 定义一个v4l2_capability结构体的变量cap
struct v4l2_capability cap; 
// 使用ioctl函数发送VIDIOC_QUERYCAP命令来获取视频设备的基本信息,并将结果保存到cap变量中 
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) 
{ 
perror("VIDIOC_QUERYCAP"); 
return -1; 
} 

在查询设备信息之后,cap结构体的各个字段将被填充,可以通过这些字段来获取设备的基本信息。

struct v4l2_capability结构体定义在linux/videodev2.h头文件中,用于获取设备的能力和驱动程序的一些信息,包括设备名称、驱动名称、是否支持视频捕获和视频输出等。结构体定义如下:

struct v4l2_capability //描述 video 设备功能和设备信息的结构体
 { 
	 __u8 driver[16]; //设备所属的 driver 名称
	 __u8 card[32]; //设备名称
	 __u8 bus_info[32];//设备所连接的总线信息,如 USB 控制器
	 __u32 version;//设备 driver 版本
	 __u32 capabilities; //设备支持的能力,如视频捕获、输出、调整、元数据等
	 __u32 device_caps; //设备特有的能力
	 __u32 reserved[3]; //保留字段 
 };

(2)查询设备支持的视频格式

查询设备的能力后,应用程序还应该查询设备支持的视频格式。这可以通过向VIDIOC_ENUM_FMT命令传递一个v4l2_fmtdesc结构体完成,驱动程序将返回支持的视频格式和Resolutions,使用VIDIOC_ENUM_FMT命令通过ioctl()函数来查询设备支持的视频格式程序示例如下所示:

struct v4l2_fmtdesc fmt; // 定义v4l2_fmtdesc结构体变量fmt 
memset(&fmt, 0, sizeof(fmt)); // 将fmt结构体的所有成员变量初始化为0 
fmt.index = 0; // 设置fmt结构体的index成员变量为0 
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置fmt结构体的type成员变量为V4L2_BUF_TYPE_VIDEO_CAPTURE,表示视频捕捉类型 
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0) // 使用while循环,每次调用ioctl函数执行VIDIOC_ENUM_FMT命令,返回值为0表示命令执行成功 
{ 
	fmt.index++; // 每次循环将fmt结构体的index成员变量加1 
	printf("Format: %s\n", fmt.description); // 在控制台输出fmt结构体中的description成员变量值,即所枚举到的格式名称 
}

该代码段列出了设备支持的所有视频格式,包括每种格式的名称、描述和FourCC编码。FourCC编码是四个字符的代码,用于唯一识别每种视频格式。

v4l2_fmtdesc结构体内容如下所示:

struct v4l2_fmtdesc {
  __u32            index;          // 格式编号,由应用程序提供
  enum v4l2_buf_type type;          // 缓冲类型,比如 V4L2_BUF_TYPE_VIDEO_CAPTURE
  __u32            flags;          // 支持的格式的标志
  __u8             description[32];// 格式的描述信息,以空字符结束
  __u32            pixelformat;    // 格式的四字符编码
  __u32            reserved[4];    // 保留字段,必须设置为0
};

(3)查询支持分辨率

在 V4L2 驱动程序中,摄像头通常支持多种不同的像素格式,每种像素格式都可以支持不同的帧大小。为了查询摄像头支持的所有帧大小,可以使用 v4l2_frmsizeenum 结构体和 VIDIOC_ENUM_FRAMESIZES 命令。程序示例如下所示:

struct v4l2_frmsizeenum frmsize;// 定义一个名为 frmsize 的 v4l2_frmsizeenum 结构体变量
memset(&frmsize, 0, sizeof(frmsize));// 将 frmsize 变量的内存清零,使其所有位都变为 0
frmsize.index = 0;// 设置 frmsize 变量的 index 成员变量为 0

// 设置 frmsize 变量的 pixel_format 成员变量为 V4L2_PIX_FMT_YUYV
frmsize.pixel_format = V4L2_PIX_FMT_YUYV;

// while 循环,当 VIDIOC_ENUM_FRAMESIZES 命令执行成功时继续循环
// VIDIOC_ENUM_FRAMESIZES 命令用于获取指定像素格式的所有帧大小
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) 
{
frmsize.index++;    // 增加 frmsize 变量的 index 成员变量值
printf("Width: %d, Height: %d\n", (int)frmsize.discrete.width,
(int)frmsize.discrete.height);    // 输出 frmsize 变量的 discrete 结构体的 width 和 height 成员变量
}

 上面的代码逐一查询当前设备支持的分辨率,并输出分辨率的宽度和高度。 v4l2_frmsizeenum结构体具体内容如下所示:

struct v4l2_frmsizeenum {
	 __u32           index;          /* 帧大小编号 */
	 __u32           pixel_format;   /* 像素格式 */
	 __u32           type;           /* 设备支持的帧大小类型 */

	 union {                        /* 帧大小 */
		 struct v4l2_frmsize_discrete    discrete;   /* 离散的帧大小 */
		 struct v4l2_frmsize_stepwise    stepwise;   /* 非离散的帧大小 */
	 };

	__u32   reserved[2];              /* 保留空间以备未来使用 */
};

(4)查询支持的帧率范围

在使用视频设备时,通常需要查询设备支持的帧率范围以便进行设置。在 V4L2 中,可以通过以下步骤查询视频设备支持的帧率范围:可以使用v4l2_frmivalenum 结构体和VIDIOC_ENUM_FRAMEINTERVALS命令通过ioctl()函数来查询当前设备支持的帧率范围等参数,例如:

// 定义一个 v4l2_frmivalenum 结构体用于查询设备支持的帧率范围
struct v4l2_frmivalenum frmival;
// 使用 0 值填充结构体内存,相当于初始化结构体
memset(&frmival, 0, sizeof(frmival));
// 设置查询的帧率范围的序号为 0
frmival.index = 0;
// 设置查询的像素格式为 V4L2_PIX_FMT_YUYV
frmival.pixel_format = V4L2_PIX_FMT_YUYV;
// 设置查询的帧率范围的分辨率为 640x480
frmival.width = 640;
frmival.height = 480;
// 通过循环遍历查询设备支持的所有帧率
while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival) == 0) 
{
    // 将帧率范围序号加 1,继续查询下一个帧率范围
    frmival.index++;
    // 打印支持的帧率的分子和分母
    printf("Interval: %d/%d\n", (int)frmival.discrete.numerator,
            (int)frmival.discrete.denominator);
}

上面的代码逐一查询当前设备支持的帧率范围,并输出帧率的分子和分母。

v4l2_frmsizeenum结构体具体内容如下所示:

struct v4l2_frmivalenum {
    __u32 index;                    // 查询的帧率范围序号
    __u32 pixel_format;             // 像素格式
    __u32 width, height;            // 分辨率
    __u32 type;                     // 帧率类型
    union {
        struct v4l2_frmival_discrete discrete;    // 离散帧率
        struct v4l2_frmival_stepwise stepwise;    // 范围帧率
    };
    __u32 reserved[2];              // 保留空间
};

至此,关于查询设备能力的API就讲解完成了,根据上述API可以很容易地获取到V4L2设备的详细信息,从格式、分辨率到帧率等信息,这对于开发多媒体应用程序非常有帮助。

19.2.2设置采集参数

在使用V4L2进行视频采集时,设置采集参数是非常重要的一步。采集参数会影响视频数据的质量和传输速度,合适的采集参数可以使得视频数据的质量更好,传输速度更快。

设置采集参数可以使用v4l2_format和VIDIOC_S_FM命令通过调用ioctl函数来实现。使用v4l2_format结构体来描述要设置的格式和参数,该结构体定义如下:

struct v4l2_format {
    enum v4l2_buf_type type;    // 缓冲类型,必须设置
    union {
        struct v4l2_pix_format  pix;    // 像素格式
        struct v4l2_window  win;    // 窗口格式
        struct v4l2_vbi_format  vbi;    // VBI格式
        struct v4l2_sliced_vbi_format sliced;  // 切片VBI格式
        __u8  raw_data[200];    // 原始格式
    } fmt;  // 格式类型,必须设置
};

在v4l2_format结构体中,必须设置type和fmt字段,其中type指定了要设置的缓冲类型,例如视频捕获或视频输出。 fmt字段是一个联合体,可以根据type的值选择其中的一种格式类型,例如像素格式、窗口格式、VBI格式、切片VBI格式或原始格式。而对于像素格式,可以使用v4l2_pix_format结构体来描述采集参数。该结构体定义如下:

struct v4l2_pix_format {
    __u32  width;          // 宽度
    __u32  height;         // 高度
    __u32  pixelformat;    // 像素格式
    enum v4l2_field field; // 图像扫描方式
    __u32  bytesperline;   // 一行所占字节数
    __u32  sizeimage;      // 图像数据大小
    enum v4l2_colorspace  colorspace;  // 颜色空间
    __u32  priv;           // 私有数据
};

例如,如果要设置像素格式为YUYV(YUV422)格式,图像的宽度和高度分别为640和480像素,则可以使用以下代码:

struct v4l2_format fmt; // 定义V4L2的格式结构体
memset(&fmt, 0, sizeof(fmt)); // 将结构体清零
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 指定格式类型为视频采集
fmt.fmt.pix.width = 640; // 视频采集分辨率的宽度
fmt.fmt.pix.height = 480; // 视频采集分辨率的高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 视频数据的像素格式,例如YUYV、MJPEG等
fmt.fmt.pix.field = V4L2_FIELD_ANY; // 视频数据的扫描方式,例如隔行扫描或逐行扫描等
if (ioctl(fd, VIDIOC_S_FMT < 0, &fmt)) 
{
    printf("ioctl error: VIDIOC_S_FMT\n");
    return -1;
}

19.2.3请求帧缓冲

在使用 V4L2 进行视频采集时,需要申请一个或多个帧缓冲,用于存储采集到的视频数据。在请求帧缓冲之前,需要先设置好视频采集的参数(例如分辨率、帧率等)。可以使用v4l2_v4l2_requestbuffers结构体和VIDIOC_REQBUFS命令通过ioctl()函数来请求帧缓存,例如:

struct v4l2_requestbuffers req;//创建 V4L2 请求结构体并清零
memset(&req, 0, sizeof(req));
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//设置请求的帧缓冲类型
req.count = BUFFER_COUNT;//设置请求的帧缓冲个数
req.memory = V4L2_MEMORY_MMAP;//设置请求的帧缓冲内存的映射方式
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) //向内核发送请求,申请帧缓冲
{
    perror("request buffers failed");
    exit(EXIT_FAILURE);
}

其中,fd 为打开的视频设备文件描述符,BUFFER_COUNT 为请求的帧缓冲个数,V4L2_MEMORY_MMAP 表示请求内存的映射方式为 mmap,还可以选择其他的内存映射方式。

如果请求成功,内核会在内存中分配一块连续的内存区域,用于存储采集到的视频数据。并将分配的帧缓冲的信息保存在 V4L2 的缓冲结构体中。我们需要遍历缓冲结构体,将每个帧缓冲都映射到用户空间,以便后续使用。

v4l2_requestbuffers结构体内容如下所示:

struct v4l2_requestbuffers {
    __u32       count;              /* 请求数量 */
    enum v4l2_buf_type type;        /* 缓冲区类型 */
    enum v4l2_memory memory;        /* 分配内存方式 */
    __u32       reserved[2];
};

19.2.4映射帧缓冲

在上一节请求分配一些帧缓冲来存储视频帧数据之后,这些帧缓冲并不能直接使用,还需要将它们映射到进程的虚拟地址空间中,才能对其进行访问和处理。

在 V4L2 中,使用 ioctl 系统调用的 VIDIOC_QUERYBUF 命令可以查询一个帧缓冲的信息,并将查询到的信息可以填充 v4l2_buffer 结构体中,包括该缓冲的物理地址、大小等信息,查询完成后,需要将该帧缓冲映射到进程的虚拟地址空间中。在 V4L2 中,可以使用 mmap 系统调用进行映射。需要注意的是,mmap 映射的是物理地址,因此需要将 VIDIOC_QUERYBUF 返回的帧缓冲的物理地址转换为虚拟地址。在 mmap 映射成功后,即可在进程中使用指针来访问和处理该帧缓冲的数据了。映射帧缓冲的代码示例如下所示:

struct v4l2_buffer buf;
for (int i = 0; i < req.count; ++i) 
{ // 分配并映射帧缓冲
    memset(&buf, 0, sizeof(buf)); // 将结构体清零
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 帧缓冲类型为视频采集
    buf.memory = V4L2_MEMORY_MMAP; // 内存映射方式获取帧缓冲
    buf.index = i; // 选择第i个帧缓冲
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) 
{ // 获取帧缓冲的信息
        perror("VIDIOC_QUERYBUF");
        exit(EXIT_FAILURE);
    }
    void *addr = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, // 映射帧缓冲到用户空间
                      MAP_SHARED, fd, buf.m.offset);
if (addr == MAP_FAILED) 
{ // 判断映射是否成功
        perror("mmap");
        exit(EXIT_FAILURE);
    }
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) 
{ // 将帧缓冲插入队列中等待采集
        perror("VIDIOC_QBUF");
        exit(EXIT_FAILURE);
    }
}

v4l2_requestbuffers 结构体是用于请求分配帧缓冲的结构体,它在 v4l2 中扮演着非常重要的角色。它的定义如下:

struct v4l2_requestbuffers {
    __u32                count;      // 请求的帧缓冲数量
    enum v4l2_buf_type   type;       // 帧缓冲类型
    enum v4l2_memory     memory;     // 帧缓冲的内存类型
    __u32                reserved[2];
};

19.2.5启动视频采集

在上一小节将预备好的帧缓冲放入队列后,使用 ioctl 系统调用的 VIDIOC_STREAMON 命令启动视频采集,代码示例如下所示:

if (ioctl(fd, VIDIOC_STREAMON, &type) == -1)// 启动视频流
{
	perror("VIDIOC_STREAMON"); // 如果VIDIOC_STREAMON操作失败,输出错误信息
	exit(EXIT_FAILURE); // 退出程序
}

视频采集启动后,设备会开始采集视频数据并将其存储到预备好的帧。

19.2.6停止视频采集

闭视频流可以通过 ioctl 调用 VIDIOC_STREAMOFF 来完成,该 ioctl 调用需要传递一个枚举类型参数,表示关闭的是视频流的哪个方向(输入流还是输出流),示例代码如下所示:

if (ioctl(fd, VIDIOC_STREAMOFF, &type) == -1) //关闭视频流
{
    perror("VIDIOC_STREAMOFF");
    exit(EXIT_FAILURE);
}

最后还要通过 munmap() 函数取消内存映射。整理好的V4L2使用流程如下所示:

19.3 V4L2摄像头应用编程实验

本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\69”目录下,如下图所示:

实验要求:

通过V4L2摄像头采集应用采集USB摄像头的摄像信息,并显示在LCD液晶显示器上。

实验步骤:

首先进入到ubuntu的终端界面输入以下命令来创建 demo69_v4l2.c文件,如下图所示:

vim  demo69_v4l2.c

然后向该文件中添加以下内容:

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

typedef struct camera_format {
 unsigned char description[32]; //字符串描述信息
 unsigned int pixelformat; //像素格式
} cam_fmt;

static cam_fmt cam_fmts[2]; 

int lcdfd = 0; //LCD设备文件描述符
int *lcdptr = NULL; //LCD映射到内存的指针
int lcd_w=800, lcd_h=1280 ; //LCD屏幕的分辨率
int video_width = 640, video_height= 360; //摄像头采集数据的分辨率

static int fb_dev_init(void) 
{
	//打开LCD设备文件
	lcdfd = open("/dev/fb0", O_RDWR);
	if (lcdfd < 0) 
	{
		perror("LCD open failed:");
	}
	/*获取LCD信息*/
	struct fb_var_screeninfo info;
	int lret = ioctl(lcdfd, FBIOGET_VSCREENINFO, &info);
	if (lret < 0) 
	{
		perror("get info failed:");
	}

	//获取LCD的分辨率
	lcd_w = info.xres;
	lcd_h = info.yres;
	//映射LCD到内存
	lcdptr = (int *)mmap(NULL, lcd_w*lcd_h*4,PROT_READ | PROT_WRITE, MAP_SHARED, lcdfd, 0);
	if (lcdptr == NULL) 
	{
		perror("lcd mmap failed:");
	}

	//清空LCD屏幕并填充白色背景
	memset(lcdptr, 0xFF, lcd_w*lcd_h*4);
	return 0;
}

//将YUYV格式的数据转换为RGB格式
void yuyv_to_rgb(unsigned char *yuyvdata, unsigned char * rgbdata, int w, int h)
{
	int r1, g1, b1;
	int r2, g2, b2;
	for (int i = 0; i < w*h/2; i++) 
	{
		char data[4];
		memcpy(data, yuyvdata + i*4, 4);
		unsigned char Y0 = data[0];
		unsigned char U0 = data[1];
		unsigned char Y1 = data[2];
		unsigned char V1 = data[3];
		r1 = Y0 + 1.4075*(V1 - 128);
		if (r1 > 255)
			r1 = 255;
		if (r1 < 0)
			r1 = 0;
		g1 = Y0 - 0.3455*(U0 - 128) - 0.7169*(V1 - 128);
		if (g1 > 255)
			g1 = 255;
		if (g1 < 0)
			g1 = 0;
		b1 = Y0 + 1.779*(U0 - 128);
		if (b1 > 255)
			b1 = 255;
		if (b1 < 0)
			b1 = 0;

		r2 = Y1 + 1.4075*(V1 - 128);
		if (r2 > 255)
			r2 = 255;
		if (r2 < 0)
			r2 = 0;
		g2 = Y1 - 0.3455*(U0 - 128) - 0.7169*(V1 - 128);
		if (g2 > 255)
			g2 = 255;
		if (g2 < 0)
			g2 = 0;
		b2 = Y1 + 1.779*(U0 - 128);
		if (b2 > 255)
			b2 = 255;
		if (b2 < 0)
			b2 = 0;

		rgbdata[i*6 + 0] = r1;
		rgbdata[i*6 + 1] = g1;
		rgbdata[i*6 + 2] = b1;
		rgbdata[i*6 + 3] = r2;
		rgbdata[i*6 + 4] = g2;
		rgbdata[i*6 + 5] = b2;
	}
}

void lcd_show_rgb(unsigned char *rgbdata, int w, int h)
{
	unsigned int *ptr = lcdptr;
	for (int i = 0; i < h; i++) 
	{
		for (int j = 0; j < w; j++) 
		{
			memcpy(ptr + j, rgbdata + j*3, 3);
		}
		ptr += lcd_w;//偏移一行
		rgbdata += w*3;//偏移一行
	}
}

int main(int argc,char *argv[])
{
    int ret,i;
	int fd;
    unsigned short *base;
    unsigned short *start;
    int min_w, min_h;
    int j;


    fb_dev_init();
     /* 步骤一,打开视频设备 */
    fd = open(argv[1], O_RDWR);
    if (fd < 0) 
	{
        printf("file open error\n");
        return -1;
    }
    /* 步骤二,查询设备能力 */
	//查询设备的基本信息
	struct v4l2_capability cap; // 定义一个v4l2_capability结构体的变量cap
	// 使用ioctl函数发送VIDIOC_QUERYCAP命令来获取视频设备的基本信息,并将结果保存到cap变量中 
	if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) 
	{ 
		perror("VIDIOC_QUERYCAP"); 
		return -1; 
	} 
	//查看支持的图像格式、分辨率、帧率
	struct v4l2_fmtdesc fmtdesc = {0};//定义支持的像素格式结构体
	struct v4l2_frmsizeenum frmsize = {0};//定义支持的分辨率结构体
	struct v4l2_frmivalenum frmival = {0};//定义支持的帧率结构体
	fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//设置视频采集类型为 V4L2_BUF_TYPE_VIDEO_CAPTURE
	fmtdesc.index = 0;
	while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))//获取支持的像素格式
	{
		strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
		cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
		fmtdesc.index++;
	}
	frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	for(i=0;i<fmtdesc.index;i++)//枚举每一种像素格式
	{
		printf("description:%s\npixelformat:0x%x\n", cam_fmts[i].description,cam_fmts[i].pixelformat );
		frmsize.index = 0;
		frmsize.pixel_format = cam_fmts[i].pixelformat;
		frmival.pixel_format = cam_fmts[i].pixelformat;
		// 2.枚举出摄像头所支持的所有视频采集分辨率
		while (0 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) 
		{
			printf("size<%d*%d> ",frmsize.discrete.width,frmsize.discrete.height);
			frmsize.index++;
			frmival.index = 0;
			frmival.width = frmsize.discrete.width;
			frmival.height = frmsize.discrete.height;
			// 3. 获取摄像头视频采集帧率
			while (0 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) 
			{
				printf("<%dfps>", frmival.discrete.denominator/frmival.discrete.numerator);
				frmival.index++;
			}
			printf("\n");
		}
		printf("\n");
	}
    /*步骤三,设置采集参数,视频帧宽度、高度、格式、视频帧率等信息*/
    struct v4l2_format fmt = {0};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//type 类型
    fmt.fmt.pix.width = video_width; //设置视频帧宽度
    fmt.fmt.pix.height = video_height;//设置视频帧高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //设置像素格式 
    if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) 
	{
        printf("ioctl error: VIDIOC_S_FMT\n");
        return -1;
    }

    struct v4l2_streamparm streamparm = {0};
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_G_PARM, &streamparm);
    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) 
	{
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 30;//30fps
        if (0 > ioctl(fd, VIDIOC_S_PARM, &streamparm)) 
		{
            printf("ioctl error: VIDIOC_S_PARM");
            return -1;
        }
    }
    /*步骤四,请求帧缓冲*/
	struct v4l2_requestbuffers reqbuffer;
	reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	reqbuffer.count = 4;//缓存数量
	reqbuffer.memory = V4L2_MEMORY_MMAP;//映射方式
	ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuffer);
	if (ret < 0) 
	{
		printf("Request Queue space failed \n");
		return -1;
	}
	/*步骤五,映射帧缓冲*/
	struct v4l2_buffer mapbuffer;
	unsigned char *mptr[4];
	unsigned int size[4];//存储大小,方便释放
	mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	for (i = 0; i < 4; i++) 
	{
		mapbuffer.index = i;
		ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer);
		if (ret < 0) 
		{
			printf("Kernel space queue failed\n");
			return -1;
		}
		mptr[i] = (unsigned char *)mmap(NULL, mapbuffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset);
		size[i] = mapbuffer.length;
		//使用完毕,入队
		ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer);
		if (ret < 0) 
		{
			printf("ioctl error: VIDIOC_QBUF \n");
			return -1;
		}
	}

	/*步骤六,开启视频采集*/
	int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	ret = ioctl(fd, VIDIOC_STREAMON, &type);
	if (ret < 0)
	{
		printf("ioctl error: VIDIOC_STREAMON \n");
		return -1;
	}
	//步骤七,读取数据、对数据进行处理
	unsigned char rgbdata[video_width*video_height*3];
	while (1) 
	{
		struct v4l2_buffer readbuffer;
		//出队列
		readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);
		if (ret < 0) 
		{
			printf("ioctl error:VIDIOC_DQBUF \n");
		}
		//显示在LCD上
		yuyv_to_rgb(mptr[readbuffer.index], rgbdata, video_width, video_height);
		lcd_show_rgb(rgbdata, video_width, video_height);
		//通知内核已经使用完毕,入队列
		ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
		if (ret < 0) 
		{
			printf("ioctl error:VIDIOC_QBUF \n");
		}
	}
	//步骤八,停止视频采集和释放资源
	ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
	if (ret < 0) 
	{
		printf("ioctl error:VIDIOC_STREAMOFF \n");
		return -1;
	}

	//释放映射空间
	for (i = 0; i < 4; i++) 
	{
		munmap(mptr[i], size[i]);
	}
	close(fd);
	return 0;
}

上述代码中已经添加了相应的注释,本小节使用的测试屏幕为800*1280的MIPI屏幕所以第21行的LCD屏幕分辨率定义的为800*1280,如果使用的是屏幕设置成相应的分辨率即可,第22行的摄像头采集数据的分辨率也可以根据摄像头的格式来进行修改。最后由于我们采集到的数据为yuyv格式,需要转换为rgb类型的数据才可正常显示,转换函数为第56行的yuyv_to_rgb函数,具体的转换原理大家可以自行查找。

保存退出之后,使用以下命令设置交叉编译器环境,并对demo69_v4l2.c进行交叉编译,编译完成如下图所示:

export PATH=/usr/local/arm64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH

aarch64-none-linux-gnu-gcc -o demo69_v4l2 demo69_v4l2.c

然后将交叉编译生成的demo71_v4l2文件拷贝到/home/nfs共享目录下,如下图所示: 

Buildroot系统启动之后,由于QT桌面会对显示信息造成干扰,所以需要使用以下命令将QT程序关闭:

 killall weston

然后使用以下命令进行nfs共享目录的挂载(其中192.168.1.7为作者ubuntu的ip地址,需要根据自身ubuntu的ip来设置),如下图所示:

mount -t nfs -o nfsvers=3,nolock 192.168.1.7:/home/nfs /mnt

nfs共享目录挂载到了开发板的/mnt目录下,进入到/mnt目录下,如下图所示: 

可以看到/mnt目录下demo69_v4l2文件已经存在了,然后使用以下命令运行该程序如下图所示:

./demo69_v4l2  /dev/video21

首先会打印USB摄像头的支持的图像格式、分辨率和帧率,最后会将摄像头采集到的图像显示到LCD液晶显示屏上。

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值