尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

USB摄像头驱动开发实战:从设备枚举到V4L2视频流打通

USB摄像头驱动开发实战:从设备枚举到V4L2视频流打通 简介这份PDF面向Linux内核与嵌入式开发方向的工程师及高年级学生聚焦USB摄像头驱动开发这一典型场景帮助读者理解如何编写符合Video for Linux标准的驱动程序解决通用驱动难以充分利用USB带宽、帧速偏低、不易满足实时监控需求的问题。资源包共1个文件为178KB的PDF文档内容源自期刊论文结构紧凑便于快速通读。文中系统梳理了USB摄像头驱动的编写方法包括声明video_device结构与file_operation结构、借助usb_register()与usb_unregister()完成注册与销毁并重点讲解双URB轮流通信、双帧缓冲等提高采集速度的技巧同时给出驱动架构、注册销毁流程与实时监控场景下的优化思路。目前已有233人学习适合希望深入Linux设备驱动与V4L框架的读者参考。1. USB摄像头驱动开发从设备枚举到视频流打通插上USB摄像头lsusb能看到设备/dev/video0却迟迟不出现或者出现了但ffmpeg一抓流就报VIDIOC_STREAMON: Invalid argument——这是很多做嵌入式Linux和国产Linux镜像适配的工程师都会撞上的场景。USB摄像头驱动开发本质是围绕USB设备模型和V4L2子系统把一颗摄像头的枚举、配置、传输、格式协商四件事在Linux内核里跑通。它适合已经会写字符设备驱动、懂USB协议基础、需要在特定板子或定制内核上接入非标摄像头的从业者。这篇笔记按“先搞清USB和V4L2怎么咬合再动手写最小驱动最后处理传输和踩坑”的顺序展开代码基于内核USB gadget与V4L2的常见写法参数按UVC和自定义设备两类分别说明。2. USB摄像头驱动的两层结构USB设备模型与V4L2怎么对接USB摄像头驱动不是一个单一模块它横跨两个内核子系统。上层是V4L2负责向用户空间暴露/dev/videoX、处理ioctl、管理缓冲区下层是USB核心负责设备枚举、端点配置、URBUSB Request Block收发。理解这两层怎么咬合决定了你后面写代码时知道该在哪一层排错。2.1 USB枚举阶段设备描述符里藏着摄像头的“身份证”USB设备插入后主机控制器先发GET_DESCRIPTOR拿设备描述符再拿配置描述符、接口描述符、端点描述符。摄像头通常是一个USB设备带多个接口一个视频控制接口VideoControlVC和一个或多个视频流接口VideoStreamingVS。VC接口负责设置亮度、对比度、分辨率这些控制项VS接口负责实际传视频数据。标准UVCUSB Video Class摄像头会声明bInterfaceClass 0x0EVideo、bInterfaceSubClass 0x01VideoControl或0x02VideoStreaming。如果你拿到的是一颗非标摄像头接口类可能是厂商自定义的0xFF这时候内核自带的uvcvideo驱动不会认它你就得自己写驱动去匹配。枚举阶段最该盯的是端点描述符里的wMaxPacketSize和bmAttributes。视频流一般走等时传输IsochronousbmAttributes低两位为01或批量传输Bulk0x02。等时传输保证带宽但不保证送达批量传输保证送达但不保证实时。选哪种直接决定你后面URB怎么提交、丢包怎么处理。/* 在probe函数里打印接口和端点信息确认枚举结果 */ static int mycam_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct usb_host_interface *alt intf-cur_altsetting; int i; dev_info(intf-dev, 接口号%d 端点数量%d\n, alt-desc.bInterfaceNumber, alt-desc.bNumEndpoints); for (i 0; i alt-desc.bNumEndpoints; i) { struct usb_endpoint_descriptor *ep alt-endpoint[i].desc; dev_info(intf-dev, 端点0x%02x 类型%d 最大包%d 间隔%d\n, ep-bEndpointAddress, ep-bmAttributes USB_ENDPOINT_XFERTYPE_MASK, le16_to_cpu(ep-wMaxPacketSize), ep-bInterval); } return 0; }这段代码放在probe里作用是确认内核枚举到的接口和端点跟你预期一致。bEndpointAddress最高位为1表示输入端点IN摄像头的数据端点一定是IN。wMaxPacketSize对高速设备最大1024全速设备最大1023等时或64批量。bInterval对等时端点表示轮询间隔单位是帧全速或微帧高速。如果这里打印出来的端点类型是控制传输0说明你匹配错了接口视频流不在这个接口上。2.2 V4L2设备注册video_device和v4l2_device的挂接顺序USB层拿到设备后要向V4L2注册。顺序是先分配v4l2_device再分配video_device设置fops和ioctl_ops最后video_register_device。很多新手在这里翻车是因为把video_device的release回调忘了写卸载模块时内核直接报BUG: unable to handle kernel NULL pointer dereference。static const struct v4l2_file_operations mycam_fops { .owner THIS_MODULE, .open mycam_open, .release mycam_close, .read mycam_read, .poll mycam_poll, .mmap mycam_mmap, .unlocked_ioctl video_ioctl2, /* 交给V4L2核心处理 */ }; static const struct v4l2_ioctl_ops mycam_ioctl_ops { .vidioc_querycap mycam_querycap, .vidioc_enum_fmt_vid_cap mycam_enum_fmt, .vidioc_g_fmt_vid_cap mycam_g_fmt, .vidioc_s_fmt_vid_cap mycam_s_fmt, .vidioc_reqbufs mycam_reqbufs, .vidioc_querybuf mycam_querybuf, .vidioc_qbuf mycam_qbuf, .vidioc_dqbuf mycam_dqbuf, .vidioc_streamon mycam_streamon, .vidioc_streamoff mycam_streamoff, }; static int mycam_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct mycam_dev *dev; int ret; dev kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev-vdev video_device_alloc(); if (!dev-vdev) { ret -ENOMEM; goto err_free; } dev-vdev-fops mycam_fops; dev-vdev-ioctl_ops mycam_ioctl_ops; dev-vdev-release video_device_release; dev-vdev-v4l2_dev dev-v4l2_dev; dev-vdev-device_caps V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; ret v4l2_device_register(intf-dev, dev-v4l2_dev); if (ret) goto err_vdev; ret video_register_device(dev-vdev, VFL_TYPE_VIDEO, -1); if (ret) goto err_v4l2; usb_set_intfdata(intf, dev); return 0; err_v4l2: v4l2_device_unregister(dev-v4l2_dev); err_vdev: video_device_release(dev-vdev); err_free: kfree(dev); return ret; }video_device_alloc分配的是V4L2的设备结构release必须指向video_device_release或你自己实现的释放函数否则卸载时内存泄漏。device_caps里V4L2_CAP_VIDEO_CAPTURE表示这是采集设备V4L2_CAP_STREAMING表示支持流式IO。video_register_device的第三个参数传-1表示让内核自动分配/dev/videoX编号传具体数字则强制用那个编号。usb_set_intfdata把设备结构挂到接口上后面disconnect时用usb_get_intfdata取回来。这里有个容易忽略的点v4l2_device_register必须在video_register_device之前调用因为video_device的v4l2_dev字段要指向已经注册的v4l2_device。顺序反了/dev/video0能出现但v4l2-ctl --all会报failed to open。3. 从零写一个最小USB摄像头驱动代码骨架与关键参数上一章讲的是结构这一章落到可编译的骨架。我一般会先写一个只做枚举和注册、不传数据的“空壳驱动”确认/dev/videoX能出现、v4l2-ctl --info能读到能力集再往里填传输逻辑。这样排错时能分清是USB层没通还是V4L2层没通。3.1 模块入口与USB设备ID表#include linux/module.h #include linux/usb.h #include media/v4l2-device.h #include media/v4l2-ioctl.h #include media/videobuf2-vmalloc.h #define MYCAM_VID 0x1234 #define MYCAM_PID 0x5678 static const struct usb_device_id mycam_id_table[] { { USB_DEVICE(MYCAM_VID, MYCAM_PID) }, { } }; MODULE_DEVICE_TABLE(usb, mycam_id_table); static struct usb_driver mycam_usb_driver { .name mycam, .id_table mycam_id_table, .probe mycam_probe, .disconnect mycam_disconnect, }; module_usb_driver(mycam_usb_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(engineer); MODULE_DESCRIPTION(Minimal USB camera driver);USB_DEVICE(vid, pid)用厂商ID和产品ID匹配设备。如果你不知道摄像头的VID/PID插上后lsusb看或者cat /sys/kernel/debug/usb/devices找。module_usb_driver是宏展开后就是module_init和module_exit的封装省去手写。MODULE_LICENSE(GPL)必须写否则内核会标记为污染某些发行版直接拒绝加载。disconnect函数里要做的事停流、释放URB、注销V4L2设备、释放内存。顺序和probe相反。static void mycam_disconnect(struct usb_interface *intf) { struct mycam_dev *dev usb_get_intfdata(intf); usb_set_intfdata(intf, NULL); if (!dev) return; if (dev-streaming) mycam_stop_streaming(dev); video_unregister_device(dev-vdev); v4l2_device_unregister(dev-v4l2_dev); kfree(dev); }video_unregister_device会触发vdev-release所以不要在disconnect里再手动video_device_release否则双重释放。这是血泪经验我见过有人在disconnect里既调video_unregister_device又调video_device_release结果卸载模块时内核直接 panic。3.2 格式协商enum_fmt、g_fmt、s_fmt三个ioctl怎么写用户空间用v4l2-ctl --list-formats-ext时内核调的是vidioc_enum_fmt_vid_cap。这个函数要遍历驱动支持的像素格式逐个填struct v4l2_fmtdesc。static int mycam_enum_fmt(struct file *file, void *fh, struct v4l2_fmtdesc *f) { static const u32 formats[] { V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_YUYV, }; if (f-index ARRAY_SIZE(formats)) return -EINVAL; f-pixelformat formats[f-index]; f-flags 0; return 0; } static int mycam_g_fmt(struct file *file, void *fh, struct v4l2_format *f) { struct mycam_dev *dev video_drvdata(file); f-fmt.pix.width dev-width; f-fmt.pix.height dev-height; f-fmt.pix.pixelformat dev-pixfmt; f-fmt.pix.field V4L2_FIELD_NONE; f-fmt.pix.bytesperline dev-width * 2; /* YUYV每像素2字节 */ f-fmt.pix.sizeimage dev-width * dev-height * 2; f-fmt.pix.colorspace V4L2_COLORSPACE_SRGB; return 0; }enum_fmt里f-index从0开始用户空间循环调用直到返回-EINVAL。g_fmt返回当前格式s_fmt设置格式。bytesperline对YUYV是宽度乘2对MJPEG通常等于sizeimage因为MJPEG是压缩流一行不是一个固定像素数。sizeimage是缓冲区大小必须大于等于一帧最大可能字节数否则reqbufs时内核会报Invalid argument。s_fmt里要做参数校验宽度高度不能超过摄像头实际支持的范围像素格式必须在enum_fmt列表里。校验失败返回-EINVAL用户空间v4l2-ctl会打印VIDIOC_S_FMT: failed: Invalid argument。3.3 URB提交与缓冲区管理等时传输的完整流程视频流启动时vidioc_streamon被调用。这里要分配URB、提交URB、把数据从URB拷贝到V4L2缓冲区。等时传输的URB比较特殊一个URB可以带多个等时包iso packet每个包对应一个微帧。static int mycam_start_streaming(struct mycam_dev *dev) { int i, ret; for (i 0; i MYCAM_NUM_URBS; i) { struct urb *urb usb_alloc_urb(MYCAM_ISO_PACKETS, GFP_KERNEL); if (!urb) { ret -ENOMEM; goto err_free_urbs; } urb-dev dev-udev; urb-pipe usb_rcvisocpipe(dev-udev, dev-ep_in); urb-transfer_flags URB_ISO_ASAP; urb-interval 1; urb-complete mycam_urb_complete; urb-context dev; urb-number_of_packets MYCAM_ISO_PACKETS; for (int j 0; j MYCAM_ISO_PACKETS; j) { urb-iso_frame_desc[j].length dev-max_packet; } urb-transfer_buffer usb_alloc_coherent( dev-udev, MYCAM_ISO_PACKETS * dev-max_packet, GFP_KERNEL, urb-transfer_dma); if (!urb-transfer_buffer) { usb_free_urb(urb); ret -ENOMEM; goto err_free_urbs; } dev-urb[i] urb; ret usb_submit_urb(urb, GFP_KERNEL); if (ret) { dev_err(dev-intf-dev, 提交URB失败: %d\n, ret); goto err_free_urbs; } } return 0; err_free_urbs: mycam_free_urbs(dev); return ret; }usb_alloc_urb第二个参数是等时包数量传0表示普通URB。usb_rcvisocpipe构造等时输入管道参数是设备指针和端点地址。URB_ISO_ASAP让USB核心自动安排传输起始帧不指定具体帧号。interval对等时端点表示每多少个微帧传一次高速设备通常设1。usb_alloc_coherent分配DMA一致性内存因为等时传输要求物理连续。usb_submit_urb提交后异步执行完成时回调mycam_urb_complete。回调里要做的事检查urb-status遍历iso_frame_desc把有效数据拷贝到当前V4L2缓冲区然后重新提交URB。如果urb-status是-ESHUTDOWN说明设备已断开不要再提交。static void mycam_urb_complete(struct urb *urb) { struct mycam_dev *dev urb-context; int i; if (urb-status -ESHUTDOWN) return; for (i 0; i urb-number_of_packets; i) { if (urb-iso_frame_desc[i].status 0) { void *data urb-transfer_buffer urb-iso_frame_desc[i].offset; int len urb-iso_frame_desc[i].actual_length; mycam_copy_to_vb2(dev, data, len); } } usb_submit_urb(urb, GFP_ATOMIC); }mycam_copy_to_vb2把数据拷到videobuf2的缓冲区然后vb2_buffer_done标记缓冲区就绪用户空间dqbuf就能取到。这里要注意等时传输的actual_length可能小于length甚至为0这是正常的不要当错误处理。如果连续多个包status非0检查带宽是否够——等时传输预留带宽USB总线带宽不足时提交URB会返回-ENOSPC。4. 避坑与排查USB摄像头驱动开发中最容易翻车的5个点这一章按“现象 → 原因 → 解决”写都是我实际调试时记录下来的。现象/dev/video0出现但v4l2-ctl --stream-mmap一直阻塞不返回帧。原因URB提交了但回调没触发或者回调触发了但没调vb2_buffer_done。常见于usb_submit_urb返回0但URB实际没进入传输队列比如端点地址写错、管道类型不对。 解决在mycam_urb_complete入口加printk确认回调是否执行。如果没执行检查usb_rcvisocpipe的端点地址是否和描述符里的bEndpointAddress一致。如果执行了但用户空间没数据检查vb2_buffer_done是否在持有锁的情况下调用videobuf2要求不能在原子上下文里调。现象ffmpeg -f v4l2 -i /dev/video0 out.mp4报VIDIOC_STREAMON: Invalid argument。原因reqbufs时申请的缓冲区数量为0或者streamon时驱动检查到格式未设置。 解决在vidioc_reqbufs里确保count至少为2并且vb2_queue的io_modes包含VB2_MMAP | VB2_READ。在streamon里先检查dev-pixfmt是否有效无效返回-EINVAL。用户空间先v4l2-ctl --set-fmt-videowidth640,height480,pixelformatYUYV再抓流。现象抓到的图像上半部分正常下半部分花屏或绿屏。原因等时传输丢包或者bytesperline设置错误导致用户空间按错误步长解析。 解决等时传输丢包是物理层问题降低分辨率或改用批量传输。bytesperline对YUYV必须是宽度乘2如果设成宽度乘1用户空间解析时每行少读一半图像就错位。用v4l2-ctl --get-fmt-video确认驱动返回的bytesperline。现象卸载模块时内核报BUG: unable to handle kernel paging request。原因URB没释放就卸载了模块或者video_device的release回调里访问了已释放的内存。 解决disconnect里先usb_kill_urb停掉所有URB再usb_free_coherent释放DMA内存最后video_unregister_device。顺序不能乱。release回调里只做video_device_release不要访问dev的其他字段。现象dmesg里刷usb 1-1: cannot submit urb (err -28)。原因-28是-ENOSPCUSB总线带宽不足。等时传输要预留带宽多个等时设备同时工作时容易撞上。 解决降低wMaxPacketSize或增大bInterval减少每微帧的数据量。或者把摄像头插到独立的USB控制器上避免和高速存储设备共享带宽。用lsusb -t看USB拓扑确认摄像头没和其他等时设备挂在同一个根集线器下。5. 用v4l2-ctl和wireshark验证驱动从抓包到出图的闭环驱动写完只是第一步能稳定出图才算数。我一般用三层验证先看V4L2层能力集再看USB层传输最后看实际图像。5.1 v4l2-ctl的五个必用命令# 1. 查看设备能力集确认CAPTURE和STREAMING都置位 v4l2-ctl -d /dev/video0 --info # 2. 列出支持的像素格式和分辨率 v4l2-ctl -d /dev/video0 --list-formats-ext # 3. 设置格式 v4l2-ctl -d /dev/video0 --set-fmt-videowidth640,height480,pixelformatYUYV # 4. 抓10帧存成raw文件 v4l2-ctl -d /dev/video0 --stream-mmap --stream-count10 --stream-toframe.raw # 5. 查看当前格式确认驱动返回的bytesperline和sizeimage v4l2-ctl -d /dev/video0 --get-fmt-video--info输出里Device Caps必须包含Video Capture和Streaming。--list-formats-ext里每个格式会列出支持的分辨率和帧率如果只列格式不列分辨率说明驱动的enum_framesizes没实现。--stream-to抓下来的raw文件用ffmpeg -f rawvideo -pix_fmt yuyv422 -s 640x480 -i frame.raw out.png转成图片看。5.2 用usbmon抓USB传输确认URB真的在跑内核自带usbmon模块加载后会在/sys/kernel/debug/usb/usbmon/下生成抓包接口。# 加载usbmon modprobe usbmon # 找到摄像头所在的总线号比如bus 1 ls /sys/kernel/debug/usb/usbmon/ # 抓bus 1的包输出到文件 cat /sys/kernel/debug/usb/usbmon/1u usbmon.log # 抓10秒后停掉 sleep 10 kill %1 # 用wireshark打开usbmon.log分析 wireshark usbmon.log在wireshark里过滤usb.endpoint_address 0x81假设IN端点是0x81看等时包的usb.iso.data_length是否稳定。如果大量包长度为0说明带宽不够或端点配置错误。如果包长度正常但驱动没收到检查urb-complete回调是否被覆盖。5.3 一个容易忽略的验证用v4l2-compliance跑合规性测试v4l2-compliance是V4L2官方测试工具能查出很多隐藏问题。# 安装 apt install v4l-utils # 跑合规性测试 v4l2-compliance -d /dev/video0 -v输出里fail项必须为0。常见fail包括VIDIOC_QUERYCAP没填bus_info、VIDIOC_ENUM_FMT没处理index越界、VIDIOC_REQBUFS没正确返回缓冲区数量。这些在简单抓流时可能不暴露但用GStreamer或OpenCV时就会翻车。我自己的习惯是每次改完驱动先跑v4l2-compliance再跑v4l2-ctl --stream-mmap最后用OpenCV读一帧确认。三步都过才认为这次改动是安全的。这套流程帮我省掉了无数次“抓流正常但OpenCV报错”的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表