Linux--V4L2应用程序开发(二)获取数据

一、采集数据流程

步骤:

  1. 请求缓冲区:通过VIDIOC_REQBUFS ioctl请求分配缓冲区。
  2. 查询缓冲区:通过VIDIOC_QUERYBUF ioctl获取缓冲区信息。
  3. 内存映射:使用mmap函数将设备缓冲区映射到用户空间。
  4. 队列缓冲区:通过VIDIOC_QBUF ioctl将缓冲区放入输入队列。
  5. 启动捕获:通过VIDIOC_STREAMON ioctl启动视频流。
  6. 等待帧数据:使用selectpoll函数等待设备缓冲区数据就绪。
  7. 取出缓冲区:通过VIDIOC_DQBUF ioctl从队列中取出已填充的数据缓冲区。
  8. 处理数据:处理从缓冲区中取出的数据。
  9. 再次队列缓冲区:处理完数据后,再次通过VIDIOC_QBUF ioctl将缓冲区放入输入队列,循环使用。

申请buffer用来放置摄像头数据

  • ioctl VIDIOC_REQBUFS:申请buffer,APP可以申请很多个buffer,但是驱动程序不一定能申请到。

首先是申请缓冲区vidioc_reqbufs(),应用层ioctl调用此函数,让其分配若干个buf,应用层后面将从这些buf读取视频数据。驱动先从传入的v4l2_requestbuffers结构体获得count(buf数量),每个buf的大小是前面my_uvc_format的sizeimage(每帧图像大小,详见补充内容),且长度页对齐。

                        

  • ioctl VIDIOC_QUERYBUF和mmap:查询buffer信息、映射

    • 如果申请到了N个buffer,这个ioctl就应该执行N次

    • 执行mmap后,APP就可以直接读写这些buffer

  • ioctl VIDIOC_QBUF:把buffer放入"空闲链表"

    • 如果申请到了N个buffer,这个ioctl就应该执行N次

获取数据

  • ioctl VIDIOC_STREAMON:启动摄像头

存储数据

  • 这里是一个循环:使用poll/select监测buffer,然后从"完成链表"中取出buffer,处理后再放入"空闲链表"

    • poll/select

    • ioctl VIDIOC_DQBUF:从"完成链表"中取出buffer

    • 处理:前面使用mmap映射了每个buffer的地址,处理时就可以直接使用地址来访问buffer

    • ioclt VIDIOC_QBUF:把buffer放入"空闲链表"

  • ioctl VIDIOC_STREAMOFF:停止摄像头

二、代码如下:


    struct v4l2_requestbuffers rb;
    memset(&rb, 0,sizeof(struct v4l2_requestbuffers));
    rb.count =32;
    rb.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
    rb.memory = V4L2_MEMORY_MMAP;  

    /*申请buffer*/
    if(0 == ioctl(fd, VIDIOC_REQBUFS,&rb))
    {
        buf_cnt = rb.count;       
        for(i = 0; i<rb.count;i++)
        {
            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;
            if(0==ioctl(fd, VIDIOC_QUERYBUF,&buf))/*查询申请到buf是否成功*/
            {
                bufs[i] = mmap(0, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED,fd, buf.m.offset);/*申请成功后,mmap这些buffer*/
                if(bufs[i]==MAP_FAILED)
                {
                    perror("Unable to map buffer");
                    return -1;
                }
            }
            else
            {
                printf("can not query buffer\n");
                return -1;
            }
           
        }
        printf("map %d buffers ok\n",buf_cnt) ; 
    }
    else
    {
        printf("can not request buffers\n ");
    }

    /*把所有buffer放入空闲链表中*/
    for(i =0; i<buf_cnt;i++)
    {
        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;
        if(0 != ioctl(fd, VIDIOC_QBUF,&buf))
        {
            perror("Uable to queue buffer");
            return -1;
        }
        
    }
    printf("queue buffers ok\n");


    /*启动摄像头*/
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if(0 != ioctl(fd,VIDIOC_STREAMON, &type))
    {
        perror("Uable to start capture");
        return -1;
    }
    printf("start capture ok\n");

    while(1)
    {
        /*poll*/
        memset(fds, 0, sizeof(fds));
        fds[0].fd = fd;
        fds[0].events = POLLIN;
        if(1 ==poll(fds,1,-1))
        {
        /*把buffer取出队列*/
        struct v4l2_buffer buf;
        memset(&buf ,0, sizeof(struct v4l2_buffer));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        if(0 != ioctl(fd, VIDIOC_DQBUF,&buf))
        {
            perror("Unable to dequeue buffer");
            return -1;
        }
        /*把buffer数据存为文件*/
        sprintf(filename, "video_raw_DATA_%04d.jpg",file_cnt++);
        int fd_file =open(filename, O_RDWR|O_CREAT,0666);
        if(fd_file < 0)
        {
            printf("can not creat file :%s \n", filename);
            return -1;
        }
        write(fd_file, bufs[buf.index], buf.bytesused);
        close(fd_file);
        /*把buffer放入队列*/
        if(0 != ioctl(fd, VIDIOC_QBUF,&buf))
        {
            perror("Uable to queue buffer");
            return -1;
        }       
        }

    }

    if(0 != ioctl(fd,VIDIOC_STREAMOFF, &type));
    {
        perror("Uable to stop capture");
        return -1;
    }
    printf("stop capture ok\n");
    close(fd);

输出结果为:将每一帧数据采集为jpg格式保存在当前目录下。

三、代码知识点补充

1、mmap()

bufs[i] = mmap(0, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED,fd, buf.m.offset);

        mmap 函数用于将一个文件或设备(如视频设备)的内容映射到进程的地址空间。这样,可以通过指针直接访问文件或设备的内容,而不需要使用系统调用,如 readwrite,从而提高了访问效率。

参数解释

  • addr: 指定映射的起始地址。通常设为 0NULL,表示由内核决定映射区域的起始地址。
  • length: 要映射的文件部分的长度。这里是 buf.length,表示需要映射的缓冲区的大小。
  • prot: 映射区域的保护方式。可以是以下几个值的组合:
    • PROT_READ:页内容可以被读取。
    • PROT_WRITE:页内容可以被写入。
    • PROT_EXEC:页内容可以被执行。
  • flags: 映射对象的类型、映射选项和页是否可以共享等。常用值有:
    • MAP_SHARED:映射区内的写入数据会写回到原文件,同时对其他映射到该文件的进程可见。
    • MAP_PRIVATE:写入数据会产生一个写时拷贝(copy-on-write),对其他映射到该文件的进程不可见。
  • fd: 要映射到内存的文件描述符。这里是视频设备文件描述符 fd
  • offset: 文件映射的起始位置。通常是缓冲区的偏移量,这里是 buf.m.offset

返回值

mmap 成功时返回映射区的指针,失败时返回 MAP_FAILED,并设置 errno 以指示错误。

2、perror()

        perror 函数是 C 标准库中的一个函数,用于输出描述最近一次函数调用发生错误的错误信息。它会根据 errno 的值输出对应的错误消息。errno 是一个全局变量,用于记录最近一次系统调用或库函数调用错误的错误码。

        perror 函数根据 errno 的值,查找并输出对应的错误消息。错误消息通常来自系统定义的一系列错误描述字符串。errno 由最近一次出错的系统调用或标准库函数设置。

  • errno: errno 是由库函数和系统调用设置的全局变量,表示上一次操作的错误码。每个错误码对应一个错误消息。
  • 错误消息查找perror 根据当前 errno 的值,在系统预定义的错误消息列表中查找并输出相应的错误消息。

3、poll()函数

        外部阻塞式,内部监视多路 I/O,系统调用 poll()select()函数很相似,但函数接口有所不同。在 select()函数中,我们提供三个 fd_set 集 合,在每个集合中添加我们关心的文件描述符;而在 poll()函数中,则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。

poll 函数的内部轮询主要发生在有多个文件描述符时,它需要检查每一个文件描述符的状态。虽然这种检查会有一定的 CPU 开销,但相比纯轮询而言,开销要小得多,因为在大多数时间里,poll 函数处于阻塞状态,只有在事件发生时才会进行一次所有文件描述符状态的检查,已确定哪些文件描述符上有时间发生。

struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 要监视的事件 */
    short revents;  /* 发生的事件 */
};

4、memset()函数

       函数原型: void *memset(void *s, int c, size_t n);

memset 函数将内存块 s 的前 n 个字节设置为值 c。具体来说,它会把每个字节都设置为 c 的值(取 unsigned char 的值)。通常用于以下情况:

  • 初始化数组或结构体。
  • 重置缓冲区。
  • 为数据结构分配内存后进行清零。

5、帧图像大小

一帧图像的大小(即 sizeimage)取决于所使用的视频格式、分辨率以及每个像素所需的字节数。为了确定一帧图像的大小,需要知道以下参数:

  1. 分辨率:图像的宽度(width)和高度(height)。
  2. 像素格式:每个像素的字节数。例如,对于YUV格式,YUV420通常每个像素占1.5个字节,YUV422每个像素占2个字节,RGB24每个像素占3个字节等。

计算公式

图像大小的计算公式如下: sizeimage=width×height×bytes per pixel

示例1:YUV420格式

假设视频分辨率为 1024x768,并且使用 YUV420 格式,每个像素占 1.5 个字节。 sizeimage=1024×768×1.5=1179648 字节

示例2:RGB24格式

假设视频分辨率为 1024x768,并且使用 RGB24 格式,每个像素占 3 个字节。 sizeimage=1024×768×3=2359296 字节

四、遇到的问题

    /*启动摄像头*/
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if(0 != ioctl(fd,VIDIOC_STREAMON, &type))
    {
        perror("Uable to start capture");
        return -1;
    }
    printf("start capture ok\n");

在if语句后误加了;分号,导致if语句判断没有执行,而perror会一直执行

我是用Windows上的vscode通过ssh链接Ubuntu开发的,VScode没有报错且交叉编译也通过了,所以执行后一直报错Uable to start capture: Invalid argument

参考文章:https://blog.csdn.net/Parismoor/article/details/97374008

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值