瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【公众号】迅为电子
【粉丝群】824412014(加群获取驱动文档+例程)
【视频观看】嵌入式学习之Linux驱动(第十三篇 输入子系统_全新升级)_基于RK3568
【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板
第146章 通用事件处理层connect函数分析
在之前的实验中,我们已经编写了设备驱动层代码和用户层代码,并且分析了输入子系统的上报数据格式。对于核心层代码,我们通常不需要自己编写,而事件处理层代码也通常不需要我们自己编写,因为Linux提供了一个通用的事件处理层代码,即evdev.c。从本章节开始,我们将着重分析evdev.c,这是Linux内核中的一个模块,用于处理输入设备的事件。evdev.c提供了一组通用的函数和数据结构,使得我们可以轻松地处理输入设备上报的事件数据。
evdev_handler结构体定义了处理输入设备事件的函数指针,在Linux内核中,evdev_handler结构体定义在include/linux/input.h头文件中的evdev.h中,如下所示:
static struct input_handler evdev_handler = {
.event = evdev_event, // 输入事件处理函数
.events = evdev_events, // 批量输入事件处理函数
.connect = evdev_connect, // 当input_dev和input_handler匹配成功之后执行的连接处理函数
.disconnect = evdev_disconnect, // 断开连接处理函数
.legacy_minors = true, // 设置为true,表示支持传统次设备号,如果设置为false,则使用动态的次设备号分配方式
.minor = EVDEV_MINOR_BASE, // 输入设备的基础次设备号
.name = "evdev", // 输入处理器的名称
.id_table = evdev_ids, // 输入设备ID表
};
在evdev.c文件中,定义了一个名为 evdev_init 的静态函数,其返回类型为 int,如下所示。该函数使用 __init 属性进行标记,这通常表示它是在内核初始化期间调用的函数。函数的主要作用是注册 evdev_handler 作为输入事件处理器。具体来说,它调用了 input_register_handler 函数,并将 evdev_handler 作为参数传递给它。input_register_handler 函数是一个用于注册输入事件处理器的函数,它负责将输入事件处理器添加到系统的输入事件处理链中,以便能够处理来自输入设备的事件。整个函数的返回值会被返回给调用者。这通常是一个表示成功或失败的整数值。
static int __init evdev_init(void)
{
return input_register_handler(&evdev_handler);
}
input_register_handler 函数如下所示,作用是注册输入事件处理器。它接收一个指向input_handler结构体的指针作为参数。函数的主要流程如下所示
1 获取互斥锁input_mutex,以防止多个线程同时访问关键部分。
2 初始化输入处理器的链表头h_list。
3 将输入处理器添加到输入处理器链表的末尾,使用list_add_tail函数。
4 遍历输入设备链表input_dev_list,对于每个输入设备,将输入处理器附加到它上面,使用input_attach_handler函数。
5 唤醒正在等待读取procfs的读者,使用input_wakeup_procfs_readers 函数。
6 释放互斥锁input_mutex,允许其他线程访问关键部分。
7 返回成功的状态码0。
int input_register_handler(struct input_handler *handler)
{
struct input_dev *dev; // 输入设备指针
int error; // 错误码
error = mutex_lock_interruptible(&input_mutex); // 获取互斥锁,以阻塞方式
if (error)
return error; // 如果获取锁失败,则返回错误码
INIT_LIST_HEAD(&handler->h_list); // 初始化输入处理器的链表头
list_add_tail(&handler->node, &input_handler_list); // 将输入处理器添加到输入处理器链表的末尾
list_for_each_entry(dev, &input_dev_list, node)
input_attach_handler(dev, handler); // 遍历输入设备链表,并将输入处理器附加到每个输入设备上
input_wakeup_procfs_readers(); // 唤醒正在等待读取 procfs 的读者
mutex_unlock(&input_mutex); // 释放互斥锁
return 0; // 返回成功的状态码
}
在上面的函数中输入处理器附加到每个输入设备上,也就是取出input_dev和新注册的handler进行匹配,匹配成功之后执行evdev_connect函数,如下所示:
在evdev_connect函数中,有一个新的结构体evdev,我们先来学习下这个结构体。
146.1 evdev和evdev_client结构体
evdev结构体定义了一个evdev设备的相关信息和状态,用于管理和控制输入事件的处理。通过这些成员,可以跟踪evdev设备的打开状态,当前占用设备的客户端,设备的互斥访问以及设备关联的其他信息。
struct evdev {
int open; // 记录 evdev 设备打开的状态
struct input_handle handle; // 输入事件处理器的句柄
wait_queue_head_t wait; // 等待队列头,用于休眠和唤醒等待事件的进程
struct evdev_client __rcu *grab; // 指向当前占用 evdev 设备的客户端
struct list_head client_list; // 与 evdev 设备关联的客户端链表
spinlock_t client_lock; // 用于保护客户端链表的自旋锁
struct mutex mutex; // 用于保护对 evdev 设备的互斥访问
struct device dev; // 与 evdev 设备关联的设备结构
struct cdev cdev; // evdev 设备的字符设备结构
bool exist; // 表示 evdev 设备是否存在
};
接下来我们解释下每个成员的含义:
open:记录evdev设备的打开状态,可能的取值为0(关闭)或1(打开)。
handle:用于处理输入事件的句柄,包含了与事件处理器相关的信息,如打开的输入设备和事件处理函数等。
wait:等待队列头,用于休眠和唤醒等待事件的进程。当没有输入事件时,进程可以通过等待在该队列上来阻塞。
grab: 指向当前占用evdev设备的客户端。当某个客户端占用了evdev设备时,其他客户端无法访问该设备。
client_list: 与evdev设备关联的客户端链表,用于管理连接到该设备的客户端。
client_lock:用于保护客户端链表的自旋锁,确保在多线程环境下对客户端链表的操作线程是安全的。
mutex:用于保护对evdev设备的互斥访问,确保在多线程环境下对设备的操作是互斥的。
dev:与evdev设备关联的设备结构体,用于表示设备的特定信息,如设备名称,设备号等。
cdev:evdev设备的字符设备结构,用于注册和管理字符设备。
exist:表示evdev设备是否存在的标志。如果设备存在,则为true;否则为false。
接下来我们来学习下evdev_client结构体。这个结构体定义了一个evdev客户端的相关信息和状态,用于管理与evdev设备相关联的客户端,在应用程序中每打开一遍event设备节点,就会用一个evdev_client结构体来表示,系统可以为每个evdev设备维护多个客户端,并管理每个客户端的状态和属性。在evdev.c中,也会对这个结构体进行操作,并根据客户端的状态和属性,将接收到的事件写入缓冲区或通知客户端。
struct evdev_client {
unsigned int head; // 缓冲区的头指针,指向下一个可写入的位置
unsigned int tail; // 缓冲区的尾指针,指向下一个可读取的位置
unsigned int packet_head; // [未来] 下一个数据包的第一个元素的位置
spinlock_t buffer_lock; // 用于保护对缓冲区、头指针和尾指针的访问的自旋锁
struct fasync_struct *fasync; // 用于异步通知的结构体指针
struct evdev *evdev; // 与客户端关联的 evdev 设备指针
struct list_head node; // 与 evdev 设备关联的客户端链表节点
enum input_clock_type clk_type; // 输入时钟类型
bool revoked; // 标志,指示客户端是否被撤销
unsigned long *evmasks[EV_CNT]; // 用于事件掩码的数组
unsigned int bufsize; // 缓冲区的大小
struct input_event buffer[]; // 输入事件缓冲区,可变长度数组
};
接下来我们解释下每个成员的含义:
head: 缓冲区的头指针,指向下一个可写入的位置。
tail:缓冲区的尾指针,指向下一个可读取的位置。
packet_head:下一个数据包的第一个元素的位置。
buffer_lock:用于保护对缓冲区,头指针和尾指针的访问的自旋锁,在多线程环境下确保对缓冲区的操作线程是安全的。
fasync:用于异步通知的结构体指针,在需要异步通知时,将其设置为相应的值。
evdev:与客户端关联的evdev设备指针,表示客户端所属的evdev设备。
node:与evdev设备关联的客户端链表节点,用于管理与设备关联的客户端。
clk_type:输入时钟类型,表示客户端使用的输入时钟类型。
revoked:标志,指示客户端是否被撤销。
evmasks:用于事件掩码的数组,存储了不同类型事件的掩码,数组的大小由EV_CNT定义。
bufsize:缓冲区的大小,表示缓冲区可以容纳的输入事件数量。
buffer[]:输入事件缓冲区,是一个可变长度数组,存储了输入事件数据。
146.2 evdev_connect函数
我们继续来学习下evdev_connect函数。这个函数用于在输入子系统中连接一个evdev设备。
static int evdev_connect(struct input_handler *handler, struct input_dev *dev,
const struct input_device_id *id)
{
struct evdev *evdev; // evdev 结构体指针,表示与设备关联的 evdev 实例
int minor; // 设备的次设备号
int dev_no; // 设备号
int error; // 错误码
minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);
if (minor < 0) {
error = minor;
pr_err("failed to reserve new minor: %d\n", error);
return error;
}
evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 分配 evdev 结构体的内存空间
if (!evdev) {
error = -ENOMEM;
goto err_free_minor;
}
INIT_LIST_HEAD(&evdev->client_list); // 初始化 evdev 的客户端链表头
spin_lock_init(&evdev->client_lock); // 初始化 evdev 的客户端自旋锁
mutex_init(&evdev->mutex); // 初始化 evdev 的互斥锁
init_waitqueue_head(&evdev->wait); // 初始化 evdev 的等待队列头
evdev->exist = true;
dev_no = minor;
/* Normalize device number if it falls into legacy range */
if (dev_no < EVDEV_MINOR_BASE + EVDEV_MINORS)
dev_no -= EVDEV_MINOR_BASE;
dev_set_name(&evdev->dev, "event%d", dev_no); // 设置 evdev 的设备名称
evdev->handle.dev = input_get_device(dev);
evdev->handle.name = dev_name(&evdev->dev);
evdev->handle.handler = handler;
evdev->handle.private = evdev;
evdev->dev.devt = MKDEV(INPUT_MAJOR, minor); // 设置 evdev 的设备号
evdev->dev.class = &input_class; // 设置 evdev 的设备类
evdev->dev.parent = &dev->dev; // 设置 evdev 的父设备
evdev->dev.release = evdev_free; // 设置 evdev 的释放函数
device_initialize(&evdev->dev); // 初始化 evdev 的设备结构体
error = input_register_handle(&evdev->handle); // 注册输入事件处理器
if (error)
goto err_free_evdev;
cdev_init(&evdev->cdev, &evdev_fops); // 初始化 evdev 的字符设备结构体
error = cdev_device_add(&evdev->cdev, &evdev->dev); // 将字符设备添加到系统
if (error)
goto err_cleanup_evdev;
return 0;
err_cleanup_evdev:
evdev_cleanup(evdev); // 清理 evdev
input_unregister_handle(&evdev->handle); // 反注册输入事件处理器
err_free_evdev:
put_device(&evdev->dev); // 释放 evdev 设备
err_free_minor:
input_free_minor(minor); // 释放次设备号
return error;
}
我们来对上面的代码解释一下。
1 通过input_get_new_minor函数获取一个新的次设备号minor,如果获取次设备号失败,则返回错误码。
2 使用kzalloc函数为evdev分配内存空间,evdev是一个指向struct evdev结构体的指针。
如果内存分配失败,则返回错误码并释放次设备号。
3 使用INIT_LIST_HEAD,spin_locspin_lock_init、mutex_init 和 init_waitqueue_headk_init、mutex_init 和 init_waitqueue_head初始化evdev的客户端链表头,客户端自旋锁,互斥锁和等待队列头。设置evdev->exist为true,表示evdev存在。根据次设备号计算设备号dev_no,并根据情况将其归一化为传统范围内的设备号。
4 使用dev_set_name为evdev->dev设置设备名称。
设置evdev->handle结构体的成员变量,包括dev(输入的设备指针),name(设备名称),handler(输入事件处理器)和private(指向evdev的指针)。
5 使用MKDEV宏设置evdev->dev的设备号。使用evdev->dev的设备类为input_class。设置evdev->dev的父设备为dev->dev。设置evdev->dev的释放函数为evdev_free。
6 使用device_initialize函数初始化evdev->dev的设备结构体。使用input_register_handle函数注册输入事件处理器。如果注册失败,则跳转到err_free_evdev进行清理。
7 使用cdev_init函数初始化evdev->cdev的字符设备结构体。使用cdev_device_add函数将字符设备添加到系统。如果添加失败,则跳转到err_cleanup_evdev进行清理。如果一切顺利,则返回0表示连接成功。在错误处理的路径中,调用evdev_cleanup_evdev进行清理evdev。
使用input_unregister_handle函数反注册输入事件处理器。调用put_device释放evdev的设备。调用input_free_minor释放次设备号。返回错误码表示连接失败。
综上所述,connect函数的主要任务是将输入设备与事件处理器关联起来,以便在事件发生时调用相应的处理函数。它通过注册输入处理器和设置回调函数来实现这一关联,并确保正确的事件处理器被调用。这种关联机制允许开发者根据需要自定义处理函数,以便根据输入设备上报的事件进行相应的处理。