一、热插拔检测原理:USB设备热插拔机制、GPIO中断检测、udev规则与uevent事件

大家好,我是老李。做Camera驱动这么多年,热插拔这块儿坑不少。今天咱们就聊聊,摄像头是怎么做到「插上就能用,拔掉就消失」的。

说白了,热插拔检测就三种路子:USB总线自带的机制、GPIO中断、还有用户空间的udev。我一个个讲,你跟着走一遍就明白了。

1.1 USB设备热插拔机制

USB摄像头是最常见的。它的热插拔,其实是USB协议本身就支持的能力。

你想想看,USB主机控制器(比如EHCI、XHCI)会定时扫描端口。当检测到D+或D-线上电平变化,就知道有设备插入或拔出了。这个过程叫「枚举」。

我刚开始做USB Camera驱动时,以为热插拔是驱动自己轮询的。后来才发现,USB core层早就帮你搞定了。驱动只需要注册好probe和disconnect回调函数。

来看一个典型的USB摄像头驱动probe函数:

static int uvc_probe(struct usb_interface *intf,
                     const struct usb_device_id *id)
{
    struct uvc_device *dev;
    int ret;

    // 分配设备结构体
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    // 初始化V4L2设备
    ret = uvc_register_video_device(dev);
    if (ret < 0) {
        kfree(dev);
        return ret;
    }

    // 注册到V4L2框架
    uvc_v4l2_register(dev);

    dev_info(&intf->dev, "UVC camera detected\n");
    return 0;
}

嗯,这里要注意:probe函数里千万别做耗时操作。我见过有人在这里做固件下载,结果USB枚举超时,设备直接挂了。

避坑指南:我曾经在probe里加了5秒的延时等待传感器稳定,结果USB core直接认为设备无响应,把设备踢掉了。后来改成用workqueue延时初始化,问题解决。

1.2 GPIO中断检测

有些摄像头不是USB接口的,比如MIPI CSI摄像头。它们没有总线枚举机制,得靠GPIO中断来检测。

我个人习惯用设备树来描述GPIO中断。比如这样:

camera_sensor: camera@10 {
    compatible = "ov5640";
    reg = <0x10>;
    
    // 电源检测GPIO,低电平表示摄像头在位
    power-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
    
    // 中断引脚,上升沿表示插入,下降沿表示拔出
    interrupt-parent = <&gpio2>;
    interrupts = <5 IRQ_TYPE_EDGE_BOTH>;
};

驱动里怎么处理?我一般这样写:

static irqreturn_t camera_detect_irq(int irq, void *data)
{
    struct camera_dev *camera = data;
    int value;

    // 读取GPIO电平,判断是插入还是拔出
    value = gpio_get_value(camera->detect_gpio);
    
    if (value == 0) {
        // 摄像头插入
        schedule_work(&camera->init_work);
    } else {
        // 摄像头拔出
        schedule_work(&camera->deinit_work);
    }

    return IRQ_HANDLED;
}

为什么用workqueue而不是直接在中断里处理?因为中断上下文不能调用可能导致休眠的函数,比如i2c读写、内存分配。我刚开始不懂,直接在中断里调了i2c_transfer,结果内核直接报错「scheduling while atomic」。

小技巧:GPIO中断检测时,建议加上防抖处理。我一般用hrtimer延迟50ms再读取电平,避免插拔瞬间的毛刺导致误判。

1.3 udev规则与uevent事件

驱动层检测到设备后,怎么通知用户空间?靠的就是uevent。

uevent是内核发送给用户空间的事件。当USB设备插入时,内核会发送一个uevent,包含设备信息、动作(add/remove)等。udev在用户空间监听这些事件,根据规则执行相应操作。

我举个例子。你想在插入摄像头时自动加载驱动模块,可以写个udev规则:

# /etc/udev/rules.d/99-camera.rules
ACTION=="add", SUBSYSTEM=="video4linux", KERNEL=="video[0-9]*", \
    RUN+="/usr/local/bin/camera_plugged.sh"

ACTION=="remove", SUBSYSTEM=="video4linux", KERNEL=="video[0-9]*", \
    RUN+="/usr/local/bin/camera_unplugged.sh"

这个规则的意思是:当video4linux子系统有设备添加时,执行camera_plugged.sh脚本。拔出时执行另一个脚本。

你可能会问,为什么不用systemd的udev?其实udev本身就是systemd的一部分。但有些嵌入式系统不用systemd,那就得用mdev或者直接监听netlink socket。

我个人习惯在应用层写一个守护进程,监听uevent:

#include <linux/netlink.h>
#include <sys/socket.h>

int monitor_uevent(void)
{
    int sock;
    struct sockaddr_nl addr;

    sock = socket(PF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
    if (sock < 0) {
        perror("socket");
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.nl_family = AF_NETLINK;
    addr.nl_pid = getpid();
    addr.nl_groups = 1;  // 监听所有uevent

    if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sock);
        return -1;
    }

    // 循环读取uevent
    char buf[4096];
    while (1) {
        recv(sock, buf, sizeof(buf), 0);
        // 解析uevent,判断是否是摄像头设备
        if (strstr(buf, "video4linux")) {
            if (strstr(buf, "ACTION=add"))
                handle_camera_add();
            else if (strstr(buf, "ACTION=remove"))
                handle_camera_remove();
        }
    }
}

这段代码虽然简单,但很实用。我做过的一个工业相机项目,就是用这种方式实现热插拔检测的。不用依赖udev,完全自己控制。

核心要点:
  • USB热插拔靠总线枚举,驱动只需实现probe/disconnect
  • GPIO中断适合非USB摄像头,注意防抖和中断上下文限制
  • uevent是内核到用户空间的桥梁,udev是用户空间的规则引擎
  • 实际项目中,建议驱动层和应用层都做热插拔检测,双重保险

好了,这一章就讲到这里。下一章咱们聊聊V4L2框架的核心数据结构,看看video_device、v4l2_device这些结构体到底怎么用。到时候我会拿一个实际项目中的代码来拆解,保证你听完就能上手。