#1024程序员节 | 征文#
往期内容
I2C子系统专栏:
- 专栏地址:IIC子系统_憧憬一下的博客-CSDN博客
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有往期内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树_憧憬一下的博客-CSDN博客
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有往期内容观看顺序
1. 回顾字符设备驱动程序
1. 确定主设备号
字符设备驱动需要一个主设备号来标识。你可以选择一个静态的设备号,或者使用动态分配的设备号。
#define DEVICE_NAME "my_chrdev"
#define CLASS_NAME "my_class"
static int major; // 保存主设备号
可以通过 register_chrdev()
函数静态注册一个设备号,或者通过 alloc_chrdev_region()
动态分配设备号。
2. 创建 file_operations
结构体
file_operations
结构体定义了设备文件操作的函数指针,如打开、读写、控制等。你需要在这里填充相关的操作函数:
static int drv_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device opened\n");
return 0;
}
static int drv_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device closed\n");
return 0;
}
static ssize_t drv_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
printk(KERN_INFO "Reading from device\n");
return 0; // 返回读取的字节数
}
static ssize_t drv_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
printk(KERN_INFO "Writing to device\n");
return len; // 返回写入的字节数
}
static struct file_operations fops = {
.open = drv_open,
.release = drv_release,
.read = drv_read,
.write = drv_write,
};
3. 注册 file_operations
结构体
通过 register_chrdev()
将 file_operations
注册到内核中。如果主设备号是动态分配的,需要通过 major
保存分配的设备号。
static int __init my_driver_init(void) {
// 动态分配主设备号
major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) {
printk(KERN_ALERT "Failed to register char device\n");
return major;
}
printk(KERN_INFO "Registered char device with major number %d\n", major);
// 创建设备类
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_ALERT "Failed to create class\n");
return PTR_ERR(my_class);
}
// 创建设备节点
my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
class_destroy(my_class);
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(my_device);
}
return 0;
}
在这里,我们使用 class_create()
和 device_create()
来创建设备类和设备节点,这样用户空间可以通过 /dev/my_chrdev
访问设备。
4. 编写入口函数
入口函数是模块加载时执行的代码,一般用于初始化设备、分配资源等。
static int __init my_driver_init(void) {
printk(KERN_INFO "Initializing character device driver\n");
// 注册字符设备
major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) {
printk(KERN_ALERT "Failed to register a major number\n");
return major;
}
printk(KERN_INFO "Registered with major number %d\n", major);
// 创建类和设备节点
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_ALERT "Failed to register device class\n");
return PTR_ERR(my_class);
}
my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
class_destroy(my_class);
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_ALERT "Failed to create the device\n");
return PTR_ERR(my_device);
}
printk(KERN_INFO "Device class created\n");
return 0;
}
5. 编写出口函数
当卸载驱动时需要释放相关资源,这个过程在出口函数中完成。
static void __exit my_driver_exit(void) {
device_destroy(my_class, MKDEV(major, 0)); // 删除设备节点
class_unregister(my_class); // 注销设备类
class_destroy(my_class); // 销毁类
unregister_chrdev(major, DEVICE_NAME); // 注销字符设备
printk(KERN_INFO "Character device driver unloaded\n");
}
6. 辅助函数
使用 class_create
和 device_create
可以帮助自动创建 /dev
中的设备节点,从而方便用户访问。
class_create(THIS_MODULE, CLASS_NAME)
:创建一个类device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME)
:在/dev
中创建设备节点
2. Input子系统框架
从图中可以看到,整个 Linux 输入子系统架构可以分为三大层:Input Device 层、Input Core 层 和 Input Handler 层。
1. Input Device 层(输入设备层)
这个层主要是和硬件直接打交道的地方,如键盘、鼠标、触摸屏、GPIO
键等输入设备。输入设备的驱动程序从硬件中获取数据,并将这些原始数据转换为标准的输入事件,然后上报给核心层(Input Core)。核心组件:
- input_dev 结构体:这是设备驱动程序和输入子系统交互的核心结构体。它定义了设备驱动程序如何报告事件,比如按键按下、鼠标移动等。
简要流程: 硬件设备产生输入信号(如按下按键),设备驱动程序通过
input_dev
将这些输入信号传递给 Input Core。
2. Input Core 层(输入核心层)
这个层负责接收来自底层输入设备的事件,并将这些事件分发给上层的处理器(Handler)。它起到桥梁的作用,将底层设备的事件转换为上层可以理解的标准化事件。
核心组件:
- input.c:是 Input Core 层的主要模块,它提供了 API 和机制来管理输入设备和输入事件。输入核心会将设备上报的事件(如键值、坐标)转交给处理这些事件的
handler
。功能概述:
- 接收底层设备上报的输入事件。
- 将事件转发给合适的
handler
进行处理。
3. Input Handler 层(输入处理层)
这个层负责处理输入核心层转发的输入事件,并向用户空间提供访问接口。在 Linux 中,
evdev
是最常用的处理层,负责将输入事件暴露给用户空间。核心组件:
- evdev.c:该模块将输入设备的事件通过
/dev/input/eventX
设备节点暴露给用户空间的应用程序,应用程序可以通过open()
、read()
、ioctl()
等系统调用来访问这些事件。file_operations 结构体:
evdev_fops
定义了如何处理用户空间对输入设备的访问。这个结构体包括了多个操作函数,比如:
.read
:读取输入事件。.write
:写入数据。.poll
:轮询事件。.ioctl
:处理设备的控制命令。
4. 用户空间访问
用户空间的应用程序可以通过
/dev/input/eventX
设备节点直接访问输入设备。这些应用程序通过
open()
、ioctl()
、poll()
和read()
等接口访问设备数据,处理输入事件。举例:
tslib
:是用于处理触摸屏输入的库。libinput
:是一个更高级的输入处理库,用于统一处理键盘、鼠标、触摸屏等设备的输入事件。Input Device 层:负责将硬件设备的原始输入信号转换为标准事件。
Input Core 层:负责管理输入设备,并将标准事件传递给处理层。
Input Handler 层:将事件暴露给用户空间,用户通过访问
/dev/input/eventX
来读取输入事件。
3. Input子系统重要结构体
内核源码:
- \Linux-4.9.88\include\linux\input.h📎input.h
- \Linux-4.9.88\include\uapi\linux\input-event-codes.h📎input-event-codes.h
3.1 Input_dev
truct input_dev
是描述输入设备的核心结构体。它提供了输入设备的详细属性、支持的事件类型、处理事件的回调函数等。在开发输入设备驱动时,开发者需要初始化并注册该结构体,使其能够正确地处理输入事件并将其传递到上层。
\Linux-4.9.88\include\linux\input.h
struct input_dev {
const char *name; //输入设备的名称。例如,"USB Keyboard"
const char *phys; //输入设备的物理路径。例如,"usb-0000:00:1d.0-1/input0"。
const char *uniq; //输入设备的唯一标识符,通常用于区分具有相同类型的多个设备。
struct input_id id; //输入设备的硬件 ID,用于标识输入设备的制造商、产品 ID 和版本号。
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
//设备支持的事件类型。事件类型定义在 linux/input.h 中,如 EV_KEY 表示按键事件,EV_ABS 表示绝对坐标事件。
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];//设备支持的按键,如键盘按键或鼠标按键。
unsigned long relbit[BITS_TO_LONGS(REL_CNT)];//设备支持的相对运动事件,如鼠标的移动。
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];//设备支持的绝对坐标事件,如触摸屏的坐标数据。
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];//设备支持的杂项事件,如扫描码、遥控器信号等。
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];//设备支持的 LED 事件(如键盘指示灯)。
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];//设备支持的声音事件(如蜂鸣器)。
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];//设备支持的力反馈事件(如游戏手柄的振动)。
unsigned long swbit[BITS_TO_LONGS(SW_CNT)];//设备支持的开关事件。
unsigned int hint_events_per_packet;
// 每个数据包的建议事件数,优化设备的事件处理。
unsigned int keycodemax;
unsigned int keycodesize;
//输入设备支持的最大键码数和每个键码的大小。
void *keycode;//存储设备支持的键码映射表。
int (*setkeycode)(struct input_dev *dev,
const struct input_keymap_entry *ke,
unsigned int *old_keycode);//设置设备的键码映射。
int (*getkeycode)(struct input_dev *dev,
struct input_keymap_entry *ke);// 获取设备的键码映射。
struct ff_device *ff;//力反馈设备的相关信息。
unsigned int repeat_key;//保存最后一个重复的按键值。
struct timer_list timer;//用于处理按键重复事件的定时器
int rep[REP_CNT];//按键重复相关的参数,主要用于延迟与频率。
struct input_mt *mt;//多点触控相关的数据结构。
struct input_absinfo *absinfo;//保存绝对坐标相关的信息(如最小值、最大值、分辨率等)。
unsigned long key[BITS_TO_LONGS(KEY_CNT)];//保存按键的当前状态
unsigned long led[BITS_TO_LONGS(LED_CNT)];//保存 LED 的当前状态
unsigned long snd[BITS_TO_LONGS(SND_CNT)];//保存声音设备的当前状态
unsigned long sw[BITS_TO_LONGS(SW_CNT)];//保存开关的当前状态。
int (*open)(struct input_dev *dev);//打开设备时调用的函数,通常用于初始化设备。
void (*close)(struct input_dev *dev);// 关闭设备时调用的函数,通常用于释放资源。
int (*flush)(struct input_dev *dev, struct file *file);//刷新设备时调用的函数,通常用于清空缓冲区。
int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
//处理输入事件的回调函数
struct input_handle __rcu *grab;
spinlock_t event_lock;//用于保护输入事件处理的自旋锁。
struct mutex mutex;//用于保护设备其他部分的互斥锁。
unsigned int users;//记录打开该设备的用户数。
bool going_away;//标识设备是否正在被卸载。
struct device dev;//内核设备模型中的设备结构体。
struct list_head h_list; // 保存handle链表
struct list_head node;
//设备在输入子系统链表中的挂载点。
unsigned int num_vals;
unsigned int max_vals;
//当前设备的输入值数量及其最大值。
struct input_value *vals;//保存输入值的数组
bool devres_managed;//标识设备是否由资源管理器管理
};
3.2 input_handler
input_handler
作为输入事件的处理器,它的作用在于:
- 事件的接收与处理:它定义了如何接收和处理来自输入设备的事件,如键盘按键、鼠标移动等。
- 事件过滤:
filter
函数可以在事件传递前过滤掉不需要的事件,提高处理效率。 - 设备匹配与连接:通过
match
和connect
函数,它负责将处理程序与适当的设备连接起来。 - 事件传递:
event
和events
回调函数可以在设备产生事件时,将事件传递给上层逻辑,或通过驱动程序接口传递给用户空间应用。
工作流程简述
- 设备连接:当一个输入设备注册时,
input_handler
的match
函数会被调用,以确定该处理程序是否可以处理这个设备。如果匹配成功,connect
函数被调用来建立连接。 - 事件处理:一旦设备产生事件,
event
或events
回调会被调用来处理输入事件。根据设备和处理程序的实现,事件可以传递给用户空间或在内核中进一步处理。 - 设备断开:当设备断开时,
disconnect
函数会被调用,处理程序负责清理与设备连接相关的资源。
\Linux-4.9.88\include\linux\input.h
struct input_handler {
void *private;
//用于存储处理程序的私有数据。驱动程序可以使用它来保存与特定设备相关的上下文信息。
void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
//这是处理单个输入事件的回调函数。事件由 type、code 和 value 表示,分别代表事件类型、事件码和事件的值(如按键按下或松开、移动坐标等)。
void (*events)(struct input_handle *handle,
const struct input_value *vals, unsigned int count);
//处理多个输入事件的回调函数。vals 是一个包含多个事件的数组,count 是事件的数量。这允许处理一组输入事件,提高效率。
bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
//这是一个事件过滤函数。它用于判断某个事件是否应该被传递给上层。如果返回 true,则事件会被过滤掉,不会继续处理;如果返回 false,事件将继续传递。
bool (*match)(struct input_handler *handler, struct input_dev *dev);
//用于匹配处理程序和输入设备。返回 true 表示当前 handler 可以处理 dev 设备。
int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
//当有输入设备与 handler 匹配时调用的回调函数,用于连接设备。返回 0 表示成功连接,其他值表示失败。
void (*disconnect)(struct input_handle *handle);
//当输入设备断开时调用的回调函数,用于处理设备断开连接后的清理工作。
void (*start)(struct input_handle *handle);
//在 connect 成功之后调用的回调函数,通常用于初始化某些特定资源或启动设备处理程序。
bool legacy_minors;//指示处理程序是否使用了旧的次设备号 (minor number) 机制。这个标志主要用于向后兼容旧的输入设备模型。
int minor;//用于存储处理程序关联的次设备号。次设备号帮助区分同一类设备中的不同实例。
const char *name;//输入处理程序的名称,用于在内核日志或其他信息输出中标识处理程序。
const struct input_device_id *id_table;
//这是一个输入设备 ID 表,用于定义处理程序可以匹配的输入设备类型。它通常包含输入设备的厂商 ID、产品 ID 等信息,用于识别设备。
struct list_head h_list;//用于将 input_handler 添加到输入子系统的处理程序列表handle中,和相应的input_dev匹配保存进去
struct list_head node;//用于将 input_handler 添加到设备的处理程序链表中。
};
3.3 input_handle
输入子系统中的一个核心结构体,它用于连接输入设备 (input_dev
) 和输入事件处理程序 (input_handler
)。当设备和处理程序之间建立连接时,input_handle
负责维护两者之间的关联,确保事件能够从设备传递到合适的处理程序进行处理。
工作流程简述
- 设备与处理程序连接:当输入设备与输入处理程序匹配成功时,系统会创建并初始化一个
input_handle
,用来维持设备和处理程序的关系。 - 事件传递:一旦输入设备产生事件,
input_handle
会将这些事件从input_dev
传递给对应的input_handler
进行处理。 - 设备打开/关闭管理:
input_handle
中的open
标志位用于记录设备的状态,确保设备只在被打开时接收和处理事件,并且在关闭时停止事件处理。 - 设备断开:当输入设备断开连接时,系统会调用
input_handler
的disconnect
函数,并清理input_handle
中的资源。
\Linux-4.9.88\include\linux\input.h
struct input_handle {
void *private;
//这个指针用于保存输入处理程序的私有数据(上下文数据)。
//输入子系统在连接设备时,可能需要为每个连接的 input_handle 维护独立的数据。
int open; // 这个整数值用于表示设备是否已经打开。
const char *name;
//处理程序的名称,通常用于标识该输入处理程序,可以帮助调试和日志输出时识别当前使用的 input_handle。
struct input_dev *dev;//它表示 input_handle 正在处理的输入设备。
struct input_handler *handler;//它表示当前 input_handle 所使用的处理程序。
struct list_head d_node;
//这个链表节点用于将 input_handle 挂接到输入设备的处理程序链表中。这意味着一个输入设备可以有多个 input_handle 进行事件处理。
struct list_head h_node; //同里
};
3.4 input_event/value
是 Linux 输入子系统中用来描述输入设备事件的核心结构体,它表示从输入设备(如键盘、鼠标、触摸屏等)发送到内核的每个输入事件。
\Linux-4.9.88\include\linux\input.h
struct input_event {
struct timeval time; //用来记录事件发生的时间。它包括秒 (tv_sec) 和微秒 (tv_usec) 两个字段,表示该事件的时间戳。
__u16 type;
// 表示事件的类型。它决定了事件的类别,比如按键事件、相对移动事件、绝对位置事件等。
__u16 code;
//表示具体的事件代码。其含义依赖于 type 的值。
__s32 value;
//表示事件的值,其含义取决于 type 和 code 的组合。
};
struct input_value { //差不多,用这个比较多好像
__u16 type;
__u16 code;
__s32 value;
};
type:
\Linux-4.9.88\include\uapi\linux\input-event-codes.h
/*
* Event types
*/
#define EV_SYN 0x00
#define EV_KEY 0x01
#define EV_REL 0x02
#define EV_ABS 0x03
#define EV_MSC 0x04
#define EV_SW 0x05
#define EV_LED 0x11
#define EV_SND 0x12
#define EV_REP 0x14
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
这些宏定义描述了 Linux 输入子系统中不同的 事件类型 (Event Types)。这些类型是用于标识从输入设备(例如键盘、鼠标、触摸屏等)传递到内核的事件类别,每个类别都有不同的意义。struct input_event
结构体中的 type
字段使用这些宏来确定事件的类别。
EV_SYN
(0x00)
- 同步事件,用于标识一组输入事件的结束。例如,多个输入事件会通过
EV_SYN
通知系统完成一个事件序列。这是一个标志位,告诉系统已经处理完一个事件的所有数据。- 典型用途:报告鼠标或触摸屏输入后,系统应通过
EV_SYN
告诉用户空间事件已完成。
EV_KEY
(0x01)
- 按键事件,表示设备上的按键或按钮状态的变化。这是键盘、鼠标按钮、游戏控制器按钮等输入设备常用的事件类型。
- 典型用途:键盘按下某个按键(如
KEY_A
)或者鼠标点击时触发。
EV_REL
(0x02)
- 相对坐标事件,用于描述相对于当前坐标的位移量。常用于鼠标等设备,表示相对位置的改变。
- 典型用途:鼠标移动过程中生成的相对位移数据(
REL_X
,REL_Y
)。
EV_ABS
(0x03)
- 绝对坐标事件,用于描述设备的绝对坐标值。常用于触摸屏、操纵杆等设备,表示其当前位置。
- 典型用途:触摸屏上的触摸点的 X 和 Y 位置(
ABS_X
,ABS_Y
)。
EV_MSC
(0x04)
- 杂项事件,用于发送某些特定的输入设备数据,例如时间戳或者扫描码等。
- 典型用途:键盘扫描码事件(
MSC_SCAN
)。
EV_SW
(0x05)
- 开关事件,用于表示设备上的开关状态。例如,表示设备是否插入、盖子是否关闭等。
- 典型用途:检测设备状态(如笔记本的盖子是否关闭)。
EV_LED
(0x11)
- LED 事件,用于控制设备上的 LED 指示灯。例如,键盘上的大写锁定指示灯(Caps Lock)、数字锁定指示灯(Num Lock)等。
- 典型用途:设置或读取键盘上的 LED 状态。
EV_SND
(0x12)
- 声音事件,用于产生音频信号,例如蜂鸣器。
- 典型用途:系统蜂鸣器发出声音。
EV_REP
(0x14)
- 重复事件,用于控制按键重复行为。例如,按住某个键时,定义按键重复的时间间隔(重复速度和延迟)。
- 典型用途:按住某个键时产生的重复输入。
EV_FF
(0x15)
- 力反馈事件,用于与具有力反馈功能的设备(如游戏控制器)进行交互,产生物理反馈(震动等)。
- 典型用途:控制游戏控制器的震动反馈。
EV_PWR
(0x16)
- 电源事件,用于报告或控制设备的电源状态。
- 典型用途:电源按钮或设备的电源管理事件。
EV_FF_STATUS
(0x17)
- 力反馈状态事件,用于报告力反馈设备的状态。
- 典型用途:检测力反馈是否成功启动或停止。
EV_MAX
(0x1f)
- 事件类型的最大值,用于定义事件类型的范围。
EV_CNT
(EV_MAX + 1)
- 事件类型的数量(总计 32 种事件类型)。用于计算事件类型的总数。
这些事件类型用于在内核和用户空间之间传递输入设备的事件信息,驱动程序根据事件类型来处理输入设备的数据。例如:
- 键盘驱动程序会处理
EV_KEY
类型的事件,用于表示按键的按下与松开。- 鼠标驱动程序会处理
EV_REL
类型的事件,用于表示鼠标的移动。- 触摸屏驱动程序会处理
EV_ABS
类型的事件,用于表示触摸点的坐标。
通过这些事件类型,输入子系统可以统一处理不同输入设备的数据,并通过 /dev/input/eventX
接口传递给用户空间程序。
code:
以type为EV_KEY按键类型例子:
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
#define KEY_5 6
#define KEY_6 7
#define KEY_7 8
#define KEY_8 9
#define KEY_9 10
.......
以type为EV_ABS绝对位移类型例子:
/*
* Absolute axes
*/
#define ABS_X 0x00 //x方向
#define ABS_Y 0x01 /y方向
#define ABS_Z 0x02
#define ABS_RX 0x03
.......
value:
对于按键,它的 value 可以是 0(表示按键被按下)、1(表示按键被松开)、2(表示长按);
对于触摸屏,它的 value 就是坐标值、压力值。