一、采集数据流程
申请buffer用来放置摄像头数据
-
ioctl VIDIOC_REQBUFS:申请buffer,APP可以申请很多个buffer,但是驱动程序不一定能申请到
-
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
函数用于将一个文件或设备(如视频设备)的内容映射到进程的地址空间。这样,可以通过指针直接访问文件或设备的内容,而不需要使用系统调用,如 read
或 write
,从而提高了访问效率。
参数解释
- addr: 指定映射的起始地址。通常设为
0
或NULL
,表示由内核决定映射区域的起始地址。 - 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
的值)。通常用于以下情况:
- 初始化数组或结构体。
- 重置缓冲区。
- 为数据结构分配内存后进行清零。
四、遇到的问题
/*启动摄像头*/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