嵌入式Linux平台下的UVC驱动和V4L2
0. 前言
在开发嵌入式系统的USB摄像头驱动的时候,有时候会出现插入了摄像头,却无法正常识别和工作的情况。这种时候,就需要修改内核代码,在内核中增加UVC驱动的支持。
在增加了驱动之后,就可以使用V4L2的接口来读取摄像头的数据了。
0.1 UVC设备简介
UVC全称为USB Video Class,即:USB视频类,是一种为USB视频捕获设备定义的协议标准。
UVC是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,已成为USB org标准之一。
支持 USB Video Class (UVC) standard 1.1可以让相机在所有的作业系统以及平台中使用(Windows, Linux, Mac etc.)。用户只需连接相机便可进行图像传输,而无需安装任何驱动程序 。
简单点说,就是只要USB摄像头是UVC摄像头,那这个摄像头的驱动就遵循一个通用的格式,可以实现免驱的操作。
在Linux系统中,UVC驱动的支持在Linux Kernel 2.4之后被增加到内核中。
但是为了让内核识别到这款摄像头,还要告诉内核这个USB的ID是UVC设备才行。
0.2 USB设备的VID和PID简介
每个USB设备都有VID(Vender ID,供应商识别码)和PID(Product ID,产品识别码),两者的长度均为2Byte。
PID和VID是主机识别USB设备时使用。
主机检测到USB设备后,首先会通过USB Class查询插入的是什么设备。
检测到插入设备的类型后,通过读取和检索VID和PID,主机就能知道当前连接的设备的类型,并能了解应该给这个USB设备加载什么样的驱动程序进行通信。
0.3 Linux下的V4L2简介
V4L2是Video for linux2的简称,为linux中关于视频设备的内核驱动。
在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写,摄像头在/dev/video*下,如果只有一个视频设备,通常为/dev/video0。当然,也有可能会是/dev/uvcvideo*,具体需要查看Kernel日志确定。
1. 内核中增加UVC摄像头驱动支持
为了让板子正常识别到这个摄像头,需要在内核中打开UVC摄像头的编译,并将这款摄像头的PID&VID添加到代码中。
1.1 摄像头的UID和PID的获取
- 将摄像头插入电脑,等待驱动程序自动安装完成。
- 打开设备管理器——照相机,找到新增的USB摄像头,右键属性。
- 在详细信息选项卡,属性选择硬件ID,就可以看到摄像头的VID和PID。
1.2 内核中打开UVC设备的支持
shell进入内核代码目录下,使用命令make menuconfig进入内核编译配置。
进入Device Drivers → Multimedia support → Media USB Adapters,找到并开启USB Video Class(UVC)如下图:
如果找不到这项,说明是相关的依赖项未打开。可以按/搜索,关键词USB_VIDEO_CLASS,能看到这项的依赖项:
将所有依赖项设置为Y(编译进内核)即可。
同样,Device Drivers → Multimedia support → V4L platform devices下,开启如下两项:
一般情况下来说这些项目都是默认开启的,检查一下即可。
1.3 内核中增加摄像头的PID和VID
打开内核代码driver/media/usb/uvc/uvc_driver.c,找到usb_device_id结构体uvc_ids。
仿照其他摄像头的代码,添加摄像头的PID和VID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | static struct usb_device_id uvc_ids[] = { //下面是新增的摄像头的id信息,VID是0x05A3,PID是0x9601 { .match_flags = USB_DEVICE_ID_MATCH_DEVICE | USB_DEVICE_ID_MATCH_INT_INFO, .idVendor = 0x05A3, //摄像头的VID .idProduct = 0x9601, .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = 1, .bInterfaceProtocol = 0, .driver_info = UVC_QUIRK_RESTRICT_FRAME_RATE }, //下面是原有的信息 /* LogiLink Wireless Webcam */ { .match_flags = USB_DEVICE_ID_MATCH_DEVICE | USB_DEVICE_ID_MATCH_INT_INFO, .idVendor = 0x0416, .idProduct = 0xa91a, .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = 1, .bInterfaceProtocol = 0, .driver_info = UVC_QUIRK_PROBE_MINMAX }, //... } |
完成后make整个内核,并将内核烧写入硬件平台中。
1.4 确认是否添加成功
启动平台后插入摄像头,如果出现如下uvc的打印信息,显示出了摄像头的PID&VID并挂在了input下,则说明添加成功。
1 2 3 4 5 6 7 8 9 | usb 1-1: new high-speed USB device number 2 using xhci-hcd hub 1-1:1.0: USB hub found hub 1-1:1.0: 4 ports detected usb 1-1.1: new high-speed USB device number 3 using xhci-hcd uvcvideo: Found UVC 1.00 device Stereo Vision 2 (05a3:9602) input: Stereo Vision 2 as /devices/platform/soc/12310000.xhci_1/usb1/1-1/1-1.1/1-1.1:1.0/input/input0 usb 1-1.2: new high-speed USB device number 4 using xhci-hcd uvcvideo: Found UVC 1.00 device Stereo Vision 1 (05a3:9601) input: Stereo Vision 1 as /devices/platform/soc/12310000.xhci_1/usb1/1-1/1-1.2/1-1.2:1.0/input/input1 |
这时候可以输入命令,查看/dev目录下的新增的设备节点:
1 2 3 4 5 | ~ # ls /dev/ #省略…… mtd2 tty23 video0 mtd2ro tty24 video1 #省略…… |
可以看到新增了video0、video1两个设备节点,这两个设备节点就是新增的摄像头设备。
2. 使用V4L2对摄像头进行读写
由于之前在4412平台上有写过UVC的摄像头代码,这里直接拿来用就可以。
详细C++代码如下:
另外这里还有个C格式的代码可供参考:
接下来以C++代码为例进行分析。
2.1 代码分析
2.1.1 main函数
先看main函数。下面的函数精简了一些无用的流程,只留下比较关键的地方。
关键代码已做注释。
可以看出,这里对摄像头的操作都是在Camera类里实现的,所以接下来要看的就是Camera类的相关代码来分析V4L2的相关操作。
另外,代码中的帧缓存image数组的定义是宽 * 高 * 2,这个定义的原因涉及到像素空间的编码格式,会在后面分析到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | int main(int argc, char **argv) { char *dev_name = NULL; FILE *outf = 0; unsigned int image_size; int camera_type = 1;//0:ITU, 1:UVC int width = 640; //图像宽高设置 int height = 480; dev_name = (char *)malloc(sizeof(char) * DEV_NAME_LENGTH); if (!dev_name) { printf("malloc mem error\\n"); return -1; } memset(dev_name, 0, sizeof(char) * DEV_NAME_LENGTH); //设备文件名 strcpy(dev_name, argv[1]); //摄像头的设备文件名作为argv[1]传入代码中 outf = fopen("out.yuv", "wb"); //打开输出文件 Camera *camera; //Camera类,用于对摄像头进行操作 unsigned char image[width * height * 2]; //定义一个数组作为帧缓存,数组大小是宽*高*2 //打开设备 camera = new Camera(dev_name, width, height, camera_type); if (!camera->OpenDevice()) { return -1; } //获取图像大小 image_size = camera->getImageSize(); //int frames=50; unsigned int writesize = 0; for (int i = 0; i < NUM_FRAM; i++) { if (!camera->GetBuffer(image)) //获取每一帧数据 { printf("fun:%s, line = %d\\n", __FUNCTION__, __LINE__); break; } printf("fun:%s, line = %d\\n", __FUNCTION__, __LINE__); writesize = fwrite(image, 1, image_size, outf); //写入文件 } camera->CloseDevice(); //关闭设备 fclose(outf); //关闭文件 return 0; } |
2.1.2 Camera类 – 构造函数和OpenDevice函数
构造函数的代码如下。这里并没有进行摄像头的打开操作,只是对一些参数进行了赋初值操作。
1 2 3 4 5 6 7 8 9 10 11 12 | Camera::Camera(char *DEV_NAME, int w, int h, int camera_type) { memcpy(dev_name,DEV_NAME,strlen(DEV_NAME)); io = IO_METHOD_MMAP;//IO_METHOD_READ;//IO_METHOD_MMAP; //摄像头读取方式为MMAP cap_image_size=0; width=w; height=h; if(1 == camera_type) { c_camera_type = 1; } } |
OpenDevice函数的逻辑很简单,依次执行open_device()函数、init_device()函数和start_capturing()函数。如果任何函数返回了错误,就直接跳出。
a. open_device()函数
open_device()函数的逻辑很简单。首先先用stat函数获取设备节点的文件信息,获取成功后判断文件是否为字符型设备,如果均为是,则使用Open函数进行打开,获取文件描述符。
b. init_device()函数
接下来看init_device函数。
这个函数关键是下面几行代码:
1 2 3 4 5 6 7 8 9 10 11 | struct v4l2_capability cap; struct v4l2_cropcap cropcap; struct v4l2_crop crop; v4l2_input input; xioctl(fd, VIDIOC_QUERYCAP, &cap); //获取设备支持的V4L2功能 ioctl(fd, VIDIOC_S_INPUT, &input); //设置设备输入 struct v4l2_format fmt; CLEAR (fmt); xioctl(fd, VIDIOC_G_FMT, &fmt); //获取设备信息 init_mmap(); //使用MMAP形式读取,在这里初始化 |
获取设备支持的V4L2功能主要是看结构体v4l2_capability的capabilities成员是否有下面两个标志位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) //该设备是否是V4L2视频采集设备? { fprintf(stderr, "%s is no video capture device\\n",dev_name); return false; } switch (io) { case IO_METHOD_READ: if (!(cap.capabilities & V4L2_CAP_READWRITE)) //如果读取模式为read模式,是否支持READWRITE接口? { fprintf(stderr, "%s does not support read i/o\\n",dev_name); return false; } break; case IO_METHOD_MMAP: case IO_METHOD_USERPTR: if (!(cap.capabilities & V4L2_CAP_STREAMING)) //如果以mmap或用户指针模式读取,是否支持STREAMING? { fprintf(stderr, "%s does not support streaming i/o\\n", dev_name); return false; } break; } |
获取设备信息主要看下面几个信息:
1 2 3 4 5 6 7 | xioctl(fd, VIDIOC_G_FMT, &fmt) printf(" fmt.fmt.pix.width = %d\\n", fmt.fmt.pix.width); //宽 printf(" fmt.fmt.pix.height = %d\\n", fmt.fmt.pix.height); //高 printf(" fmt.fmt.pix.sizeimage = %d\\n", fmt.fmt.pix.sizeimage); //图像大小 printf(" fmt.fmt.pix.bytesperline = %d\\n", fmt.fmt.pix.bytesperline); //每行字节数 printf(" fmt.fmt.pix.pixelformat = %d\\n", fmt.fmt.pix.pixelformat); //像素空间格式 printf("-#-#-#-#-#-#-#-#-#-#-#-#-#-\\n"); |
这里的像素空间格式是编码格式,例如YUV420/RGB888之类的。
在这里可以看到,摄像头的像素空间是YUYV,宽640,高480。
YUV是指亮度参量和色度参量分开表示的像素格式,YUYV编码格式就是将YUV按照4:2:2进行编码,即每个像素2字节,这也说明了为何上面的缓存大小为宽*高*2。
初始化MMAP,主要流程是先使用v4l2_requestbuffers结构体和ioctl的VIDIOC_REQBUFS申请缓冲区,然后再获取缓冲区的参数并使用mmap进行内存的映射。
关于mmap的介绍,可以查看我之前写的文章用MMAP读写Linux的寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | /* These definitions are defined in camera.h * struct buffer { void * start; size_t length;//buffer's length is different from cap_image_size }; struct buffer * buffers ; */ bool Camera::init_mmap(void) { struct v4l2_requestbuffers req; CLEAR (req); req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (-1 == xioctl(fd, VIDIOC_REQBUFS,&req)) //获取device memory的信息 { return false; } if (req.count < 2) //设备没有足够的缓存大小 { fprintf(stderr,"Insufficient buffer memory on %s\n", dev_name); return false; } buffers = (buffer *)calloc(req.count, sizeof(*buffers)); //给buffers结构体申请缓存空间 if (!buffers) { fprintf(stderr, "Out of memory\n"); return false; } for (n_buffers = 0; n_buffers < req.count;++n_buffers) { struct v4l2_buffer buf; CLEAR (buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers; if (-1 == xioctl(fd, VIDIOC_QUERYBUF, &buf)) //获取每个Buffer的device memory信息,供mmap函数使用 { errno_exit("VIDIOC_QUERYBUF"); } buffers[n_buffers].length = buf.length; //使用mmap函数将每个Buffer对应的device memory映射到用户空间 buffers[n_buffers].start = mmap( NULL /* start anywhere */, buf.length, PROT_READ | PROT_WRITE /* required */, MAP_SHARED /* recommended */, fd, buf.m.offset); if (MAP_FAILED == buffers[n_buffers].start) { return false; } } return true; } |
c. start_capturing()函数
我们是使用MMAP形式对摄像头数据进行读写的。在这种情况下,V4L2会在内存中构造一个Buffer队列。摄像头采集回来数据之后,会从队列中按顺序取出空白的Buffer并写入,而用户要处理数据时,只需要将V4L2写好的Buffer提取出来,处理完毕后再将Buffer作为空白Buffer入队即可。
这个函数主要做以下两件事情:
- 将init_mmap中申请到的内存使用VIDIOC_QBUF进行入队操作,告诉设备这些缓冲区可以进行数据的写入。
- 使用VIDIOC_STREAMON开始获取图像。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | bool Camera::start_capturing(void) { unsigned int i; enum v4l2_buf_type type; for (i = 0; i < n_buffers; ++i) { struct v4l2_buffer buf; CLEAR (buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) //将申请到的内存用VIDIOC_QBUF放入队列,告诉设备这个地方可以存放图像数据 { return false; } } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == xioctl(fd, VIDIOC_STREAMON, &type)) //VIDIOC_STREAMON开始获取图像信息 { return false; } return true; } |
2.1.3 Camera类 – GetBuffer函数
这个函数是具体获取摄像头采集到的图像数据的函数。
从代码中可以看出,这里的逻辑是将摄像头的文件描述符加入集合fds后,用select函数循环等待摄像头数据就绪,再通过read_frame函数获取一帧的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | bool Camera::GetBuffer(unsigned char *image) { fd_set fds; struct timeval tv; int r; FD_ZERO(&fds); //清空fd_set集合fds FD_SET(fd, &fds); //将摄像头的文件描述符加入集合fds中 /* Timeout. */ tv.tv_sec = 2; tv.tv_usec = 0; r = select(fd + 1, &fds, NULL, NULL, &tv); //select函数循环判断该集合中是否有文件可被读写。 //参数:maxfdp = fd + 1:int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错! // fds:要循环判断的文件描述符集合 // tv:超时时间,这里设置为2秒 if (-1 == r) //返回值小于0,select错误 { errno_exit("select"); } if (0 == r) //返回值为0,等待时间超时的时候仍然没有文件可以进行读写 { fprintf(stderr, "select timeout\n"); exit(EXIT_FAILURE); } read_frame(image); //这里才正式开始读取摄像头数据 } |
查看read_frame函数的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | int Camera::read_frame(unsigned char *image) { struct v4l2_buffer buf; CLEAR (buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) //将有帧数据的buf出队 { switch (errno) { case EAGAIN: //重试错误,直接return 0重试即可 return 0; case EIO: //可以忽略错误EIO,但是这里不忽略 default: errno_exit("VIDIOC_DQBUF"); } } assert(buf.index < n_buffers); //buffer的索引号应该小于总的buffer个数 memcpy(image, buffers[0].start, cap_image_size); //复制一帧数据到image中 if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) //将处理结束后的buf放回到队伍中 { errno_exit("VIDIOC_QBUF"); } return 1; } |
可以看出,这个函数主要做了下面三件事:
- 将有图像信息的buffer从队列中取出
- 处理buffer里的图像数据
- 将用完的buffer作为空白的Buffer放回队列中
2.1.4 Camera类 – CloseDevice函数
该函数和OpenDevice的过程相反,主要执行下面这些操作:
a. 停止摄像头采集数据
1 2 3 | type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &type)) errno_exit("VIDIOC_STREAMOFF"); |
b. 反映射内存(munmap)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void Camera::uninit_device(void) { unsigned int i; switch (io) { case IO_METHOD_MMAP: for (i = 0; i < n_buffers; ++i) if (-1 == munmap(buffers[i].start,buffers[i].length)) //munmap反映射内存 { errno_exit("munmap"); } break; } free(buffers); } |
c. 关闭文件
1 2 3 4 5 | void Camera::close_device(void) { if (-1 == close(fd)) errno_exit("close"); fd = -1; } |
至此,通过V4L2读取摄像头的数据的大概流程就梳理得差不多了。
2.2 测试
首先打开脚本build.sh,将编译工具改为自己硬件平台的交叉编译工具链的g++工具。
之后执行./build.sh,会生成可执行文件camera。
在板子上执行下面的命令:
1 | ./camera /dev/video0 640x480 #/dev/video0是设备节点的文件名 |
成功执行后,同目录下会生成out.yuv文件。
使用YUVPLAYER打开文件,设置好YUV类型、分辨率和帧率后,就能看到采集回来的图像。
到此,已经完成了从摄像头读取数据的操作。
当然如果有FFMPEG/OpenCV库,大可不必使用那么麻烦的方式去读取摄像头的数据。
例如,有了OpenCV库,就可以这样读取摄像头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <opencv2/opencv.hpp> using namespace cv; int main(){ VideoCapture cap; cap.open(0); //打开摄像头,序列号是/dev/videoi,i是0,1,2,cap.open(i) if(!cap.isOpened()) return -1; Mat frame; while(1) { cap>>frame;//等价于cap.read(frame); if(frame.empty()) break; imshow("video", frame); if(waitKey(20)>0)//按下任意键退出摄像头 因电脑环境而异,有的电脑可能会出现一闪而过的情况 break; } cap.release(); destroyAllWindows();//关闭所有窗口 return 0; } |
Leave A Comment