一、热插拔检测原理: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枚举超时,设备直接挂了。
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」。
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这些结构体到底怎么用。到时候我会拿一个实际项目中的代码来拆解,保证你听完就能上手。