韦东山第3期嵌入式Linux项目-视频监控-2-从零写USB摄像头驱动(UVC驱动)

本文详细介绍了如何从零开始编写USB摄像头的UVC驱动,涵盖分析USB设备描述符、搭建驱动框架、实现数据传输、设置参数、urb处理、调试及属性设置等多个步骤。通过分析设备描述符,理解USB摄像头的硬件特性,并逐步构建驱动程序,最终实现视频数据的传输和控制功能。
摘要由CSDN通过智能技术生成

近期将之前学习嵌入式的笔记进行了整理,内容涉及到基础知识以及嵌入式开发过程中比较重要的模块开发知识,文章中有我在学习过程中的标注,非常详细,可以让入门学习事半功倍。
在这里插入图片描述

获取链接 提取码:qk7t

一、从零写USB摄像头驱动—分析描述符

USB设备插入电脑后,电脑上就会相应的显示其是某种设备(U盘,摄像头,信号采集卡等等),表明这些设备“身份”的就叫做设备描述符(就是一些格式化的数据)。操作系统通过底层的USB总线驱动程序就访问/读取这些描述符信息。

在这里插入图片描述
上图是USB设备通用的描述符j结构,对于USB摄像头,其中会有一些自己定义的描述符(主要指摄像头中用于控制和传输数据的接口描述符)。

注意:这些USB设备的描述符在USB设备接入的时候由USB总线驱动程序全部读取出来存放到了内存中,因此可以直接引用它们。

USB摄像头的描述符:
在这里插入图片描述

现在首先开始搭建UVC驱动程序的框架结构:

UVC驱动程序以USB设备驱动程序为基础:接上usb设备后,USB设备驱动程序会在usb_driver结构体–>id_table中查找是否能够支持该设备,如果可以,就调用其中的probe函数进行下一步操作:

id_table:表示该usb设备驱动程序所能支持的USB设备。
在这里插入图片描述
在uvc_driver.c文件中定义了id_table,首先是针对一些特定厂家的,最后有关于UVC通用类的设备的id_table:
在这里插入图片描述
在这里插入图片描述
这里的通用表示:如果接入的usb设备接口信息满足 USB_INTERFACE_INFO(USB_CLASS_VIDEO, 1, 0) 的条件,就可以支持。详细解读:
bInterfaceClass接口类为:USB_CLASS_VIDEO
bInterfaceSubClass接口子类为:1
bInterfaceProtocol接口协议为:0
在这里插入图片描述
查阅UVC规范:
在这里插入图片描述
发现这里是“1”,表示子类为视频控制接口 VideoControl Interface ,没有规定视频数据流接口(VideoStreaming Interface)的原因是VC Interface和VS Interface 是配对的,且每一个VS Interface 都从属于一个VC Interface,因此找到了VC Interface就可以找到对应的VS Interface。

也可以加入VS Interface:
在这里插入图片描述
写好后编译装载驱动程序:
在这里插入图片描述
发现probe函数被调用了两次,是因为USB摄像头中有两个接口(VC和VS)。

使用 lsusb 工具可以将USB设备的详细信息打印出来,因此分析该工具的源码可以帮助我们打印描述符信息:

lsusb.c:
main
    dumpdev
        dump_device
        dump_config
        	for (i = 0 ; i < config->bNumInterfaces ; i++)  //一个配置下可能有多个接口Interfaces,将多个接口的描述符都打印出来
        		dump_interface(dev, &config->interface[i]);
                	for (i = 0; i < interface->num_altsetting; i++)  //一个接口中又可能有多个设置altsetting,将多个设置描述符打印出来
                		dump_altsetting(dev, &interface->altsetting[i]);

可以结合图2-2和UVC 1.5 Class specification文档来一个一个分析USB摄像头的描述符信息。

1. 打印出了设备描述符和配置描述符:

这些描述符被usb总线驱动程序读取出来以后都保存在内存里,可以随时取出打印出来。
只有一个设备和一个配置描述符:
在这里插入图片描述

2. IAD:接口联合体描述符:

一个usb摄像头有可能有不同的接口(VideoControl 接口、VideoStreaming接口),IAD描述符中就讲解这些接口。
在这里插入图片描述
在这里插入图片描述

3. 接口描述符usb_host_interface desc :

在这里插入图片描述
probe函数的参数就有接口,如果这个接口能够被struct usb_driver myuvc_driver 所支持(和id_table中的项匹配),那么它就会作为一个参数传入probe函数,因此就可以直接使用该参数。
在这里插入图片描述
另外,一个接口可能有多种设置,正在使用的设置就在cur_altsetting 中。
在lsusb源码中也有体现:对于每一个配置中的每一个接口,打印接口中的设置参数:

在这里插入图片描述

在这里插入图片描述

1)VC 控制接口描述符:只有一个设置
在这里插入图片描述
2)VS 视频流接口描述符:共有13个设置
在这里插入图片描述

灰色框表示的是UVC规范自己定义的描述符

这些自定义的描述符都存在哪里呢?
–>所有那些UVC规范中所定义的描述符都存在一个buffer中:

在这里插入图片描述

VideoControl Interface的自定义描述符:

extra buffer of interface 0:
extra desc 0: 0d 24 01 00 01 4d 00 80 c3 c9 01 01 01 
                    VC_HEADER
extra desc 1: 12 24 02                 01 01 02 00 00 00 00 00 00 00 00 03 0e 00 00 
                    VC_INPUT_TERMINAL  ID
extra desc 2: 09 24 03                 02 01 01          00             04         00 
                    VC_OUTPUT_TERMINAL ID wTerminalType  bAssocTerminal bSourceID
extra desc 3: 0b 24 05                 03 01         00 00           02           7f 14      00 
                    VC_PROCESSING_UNIT ID bSourceID  wMaxMultiplier  bControlSize bmControls
extra desc 4: 1a 24 06                 04 ad cc b1 c2 f6 ab b8 48 8e 37 32 d4 f3 a3 fe ec 08            01        03         01 3f 00
                    VC_EXTENSION_UNIT  ID GUID                                            bNumControls  bNrInPins baSourceID

IT(01)  ===>  PU(03)  ===>  EU(04)  ===>  OT(02)

这里还有一个问题,这些功能组件(IT、PU、EU、OT)是怎么联系起来的呢?

以PU为例:PU的描述符中有一个SourceID,表示PU组件的数据来源,查看读取到的描述符信息,发现我们摄像头中PU的数据来源是01,正好是IT的UnitID值。
在这里插入图片描述

第一个字节表示描述符的长度;
第二个字节表示:这里的类自定义接口的宏 CS_INTERFACE 在这里都一样:均为0x24;
在这里插入图片描述
第三个字节表示自定义描述符的子类型,对于VC接口中不同的实体(entity,CT、PU、IT、OT、ST)都有不同的值对应:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

VC_DESCRIPTOR_UNDEFINED 0x00
VC_HEADER 0x01
VC_INPUT_TERMINAL 0x02
VC_OUTPUT_TERMINAL 0x03
VC_SELECTOR_UNIT 0x04
VC_PROCESSING_UNIT 0x05
VC_EXTENSION_UNIT 0x06
VC_ENCODING_UNIT 0x07

VideoStreaming Interface的自定义描述符:

extra buffer of interface 1:
extra desc 0: 0e 24 01              01 df 00 81 00 02 02 01 01 01 00 
                    VS_INPUT_HEADER bNumFormats 
extra desc 1: 1b 24 04                     01           05                   59 55 59 32 00 00 10 00 80 00 00 aa 00 38 9b 71  10              01 00 00 00 00 
                    VS_FORMAT_UNCOMPRESSED bFormatIndex bNumFrameDescriptors GUID                                             bBitsPerPixel
extra desc 2: 1e 24 05                     01          00              80 02   e0 01   00 00 ca 08 00 00 ca 08 00 60 09 00 15 16 05 00 01 15 16 05 00 
                    VS_FRAME_UNCOMPRESSED  bFrameIndex bmCapabilities  wWidth  wHeight  
                                                                         640x480
extra desc 3: 1e 24 05 02 00 60 01 20 01 00 80 e6 02 00 80 e6 02 00 18 03 00 15 16 05 00 01 15 16 05 00 
                    VS_FRAME_UNCOMPRESSED
extra desc 4: 1e 24 05 03 00 40 01 f0 00 00 80 32 02 00 80 32 02 00 58 02 00 15 16 05 00 01 15 16 05 00 
extra desc 5: 1e 24 05 04 00 b0 00 90 00 00 a0 b9 00 00 a0 b9 00 00 c6 00 00 15 16 05 00 01 15 16 05 00 
extra desc 6: 1e 24 05 05 00 a0 00 78 00 00 a0 8c 00 00 a0 8c 00 00 96 00 00 15 16 05 00 01 15 16 05 00 

extra desc 7: 1a 24 03 00 05 80 02 e0 01 60 01 20 01 40 01 f0 00 b0 00 90 00 a0 00 78 00 00 
                    VS_STILL_IMAGE_FRAME
extra desc 8: 06 24 0d 01 01 04 

在这里插入图片描述

注意:这里需要注意的是,一个摄像头可能支持多种格式FORMAT,每一种格式下又可能出现多种分辨率FRAME。

VS_INPUT_HEADER 0x01
VS_STILL_IMAGE_FRAME 0x03
VS_FORMAT_UNCOMPRESSED 0x04
VS_FRAME_UNCOMPRESSED 0x05
VS_COLORFORMAT 0x0D

4. Interrupt_Endpoint 端点描述符:

在这里插入图片描述

总结:从描述符的分析中我们可以知道:
1)该USB摄像头的厂家ID、支持的协议、电源参数等硬件信息
2)支持哪些格式,哪些分辨率等等信息,并且是否支持调整亮度等等功能。

二、从零写USB摄像头驱动—实现数据传输_框架

※ 补充内容:
为了避免每次使用dmesg命令去查看内核的打印信息,用以下方法解决:

A.设置ubuntu让它从串口0输出printk信息
a. 设置vmware添加serial port, 使用文件作为串口
b. 启动ubuntu,修改/etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT=""
GRUB_CMDLINE_LINUX=“console=tty0 console=ttyS0,115200n8”

sudo update-grub
sudo reboot

c. ubuntu禁止root用户登录
先修改root密码: sudo passwd root
然后执行"su root"就可以用root登录了

d. echo “8 4 1 7” > /proc/sys/kernel/printk

再次重启后,只要执行这2个命令就可以:
su root
echo “8 4 1 7” > /proc/sys/kernel/printk

按照以上设置后就可以在文件中实时看到内核的打印信息了。

1. 总体框架

在这里插入图片描述

1.构造一个usb_driver
2.设置
   probe:
        2.1. 分配video_device:video_device_alloc
        2.2. 设置
           .fops
           .ioctl_ops (里面需要设置11项)
           如果要用内核提供的缓冲区操作函数,还需要构造一个videobuf_queue_ops
        2.3. 注册: video_register_device      
  id_table: 表示支持哪些USB设备      
3.注册: usb_register

具体代码仿照之前写的 myvivi.c 来进行:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、从零写USB摄像头驱动—实现数据传输_简单函数

编写的顺序与上一节框架中讲解的函数调用顺序一致。

在这里插入图片描述
在这里插入图片描述

A3 列举支持哪种格式

/* A3 列举支持哪种格式
 * 参考: uvc_fmts 数组
 */
static int myuvc_vidioc_enum_fmt_vid_cap(struct file *file, void  *priv,
					struct v4l2_fmtdesc *f)
{
    /* 人工查看描述符可知我们用的摄像头只支持1种格式 */
	if (f->index >= 1)
		return -EINVAL;

    /* 支持什么格式呢?
     * 查看VideoStreaming Interface的描述符,
     * 得到GUID为"59 55 59 32 00 00 10 00 80 00 00 aa 00 38 9b 71"
     */
	strcpy(f->description, "4:2:2, packed, YUYV");
	f->pixelformat = V4L2_PIX_FMT_YUYV;    
    
	return 0;
}

/* A4 返回当前所使用的格式 */
static int myuvc_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
					struct v4l2_format *f)
{
    memcpy(f, &myuvc_format, sizeof(myuvc_format));
	return (0);
}

A5 测试驱动程序是否支持某种格式, 并强制设置该格式

/* A5 测试驱动程序是否支持某种格式, 强制设置该格式 
 * 参考: uvc_v4l2_try_format
 *       myvivi_vidioc_try_fmt_vid_cap
 */
static int myuvc_vidioc_try_fmt_vid_cap(struct file *file, void *priv,
			struct v4l2_format *f)
{
    if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
    {
        return -EINVAL;
    }

    if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV)
        return -EINVAL;
    
    /* 调整format的width, height, 
     * 计算bytesperline, sizeimage
     */

    /* 人工查看描述符, 确定支持哪几种分辨率 */
    f->fmt.pix.width  = frames[frame_idx].width;
    f->fmt.pix.height = frames[frame_idx].height;
    
	f->fmt.pix.bytesperline =
		(f->fmt.pix.width * bBitsPerPixel) >> 3;
	f->fmt.pix.sizeimage =
		f->fmt.pix.height * f->fmt.pix.bytesperline;
    
    return 0;
}

设置参数:

/* A6 参考 myvivi_vidioc_s_fmt_vid_cap */
static int myuvc_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
					struct v4l2_format *f)
{
	int ret = myuvc_vidioc_try_fmt_vid_cap(file, NULL, f);
	if (ret < 0)
		return ret;

    memcpy(&myuvc_format, f, sizeof(myuvc_format));
    
    return 0;
}

A7 APP调用该ioctl让驱动程序分配若干个缓存, APP将从这些缓存中读到视频数据

/* A7 APP调用该ioctl让驱动程序分配若干个缓存, APP将从这些缓存中读到视频数据 
 * 参考: uvc_alloc_buffers
 */
static int myuvc_vidioc_reqbufs(struct file *file, void *priv,
			  struct v4l2_requestbuffers *p)
{
    int nbuffers = p->count;
    int bufsize  = PAGE_ALIGN(myuvc_format.fmt.pix.sizeimage);
    unsigned int i;
    void *mem = NULL;
    int ret;

    if ((ret = myuvc_free_buffers()) < 0)
        goto done;

    /* Bail out if no buffers should be allocated. */
    if (nbuffers == 0)
        goto done;

    /* Decrement the number of buffers until allocation succeeds. */
    for (; nbuffers > 0; --nbuffers) {
        mem = vmalloc_32(nbuffers * bufsize);
        if (mem != NULL)
            break;
    }

    if (mem == NULL) {
        ret = -ENOMEM;
        goto done;
    }

    /* 这些缓存是一次性作为一个整体来分配的 */
    memset(&myuvc_queue, 0, sizeof(myuvc_queue));

	INIT_LIST_HEAD(&myuvc_queue.mainqueue);
	INIT_LIST_HEAD(&myuvc_queue.irqqueue);

    for (i = 0; i < nbuffers; ++i) {
        myuvc_queue.buffer[i].buf.index = i;
        myuvc_queue.buffer[i].buf.m.offset = i * bufsize;   //其中每一个buffer的偏移地址
        myuvc_queue.buffer[i].buf.length = myuvc_format.fmt.pix.sizeimage;
        myuvc_queue.buffer[i].buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        myuvc_queue.buffer[i].buf.sequence = 0;
        myuvc_queue.buffer[i].buf.field = V4L2_FIELD_NONE;
        myuvc_queue.buffer[i].buf.memory = V4L2_MEMORY_MMAP;
        myuvc_queue.buffer[i].buf.flags = 0;
        myuvc_queue.buffer[i].state     = VIDEOBUF_IDLE;
        init_waitqueue_head(&myuvc_queue.buffer[i].wait);
    }

    myuvc_queue.mem = mem;  // 一整块buffer的起始地址
    myuvc_queue.count = nbuffers;
    myuvc_queue.buf_size = bufsize;
    ret = nbuffers;

done:
    return ret;
}

A8 查询缓存状态, 比如地址信息(APP可以用mmap进行映射)

/* A8 查询缓存状态, 比如地址信息(APP可以用mmap进行映射) 
 * 参考 uvc_query_buffer
 */
static int myuvc_vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{
    int ret = 0;
    
	if (v4l2_buf->index >= myuvc_queue.count) {
		ret = -EINVAL;
		goto done;
	}

    memcpy(v4l2_buf, &myuvc_queue.buffer[v4l2_buf->index].buf, sizeof(*v4l2_buf));

    /* 更新flags */
	if (myuvc_queue.buffer[v4l2_buf->index].vma_use_count)
		v4l2_buf->flags |= V4L2_BUF_FLAG_MAPPED;


	switch (myuv
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值