嵌入式Linux输入子系统驱动开发
嵌入式Linux输入子系统驱动开发
1. 输入子系统概述
Linux输入子系统是内核中专门处理输入设备的框架,它为各种输入设备(如键盘、鼠标、触摸屏、游戏手柄等)提供统一的接口和抽象层。输入子系统的主要目标是简化输入设备驱动的开发,提高代码的可重用性,并为用户空间应用程序提供一致的访问接口。
1.1 设计目标
- 抽象化: 将不同类型的输入设备抽象为统一的接口
- 模块化: 允许独立开发和加载不同的输入设备驱动
- 标准化: 提供标准的事件报告机制
- 可扩展性: 支持新类型输入设备的轻松集成
1.2 子系统组成
Linux输入子系统主要由三部分组成:
- 驱动层(Driver Layer): 负责与具体硬件交互,将硬件事件转换为标准输入事件
- 核心层(Core Layer): 提供统一的API和事件处理机制
- 事件处理层(Event Handler Layer): 将输入事件传递给用户空间应用程序
2. 输入子系统核心架构
2.1 核心数据结构
输入子系统的核心是input_dev
结构体,它定义了输入设备的基本属性和操作方法:
struct input_dev {const char *name;const char *phys;const char *uniq;struct input_id id;unsigned long evbit[BITS_TO_LONGS(EV_CNT)];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)];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 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 input_absinfo *absinfo;unsigned int rep[REP_CNT];struct timer_list timer;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 *grab;spinlock_t event_lock;struct mutex mutex;unsigned int users;bool going_away;bool sync;struct device dev;struct list_head h_list;struct list_head node;
};
2.2 事件类型
输入子系统定义了多种事件类型,每种类型对应不同的输入行为:
- EV_SYN: 同步事件,用于分隔事件包
- EV_KEY: 按键事件,包括键盘、按钮等
- EV_REL: 相对坐标事件,如鼠标移动
- EV_ABS: 绝对坐标事件,如触摸屏
- EV_MSC: 杂项事件,用于传输设备特定信息
- EV_SW: 开关事件,用于检测设备状态变化
- EV_LED: LED事件,用于控制设备LED
- EV_SND: 声音事件,用于控制设备声音
- EV_REP: 重复事件,用于按键重复
- EV_FF: 力反馈事件,用于振动反馈
- EV_PWR: 电源事件,用于电源管理
2.3 事件编码
每种事件类型都有相应的编码,用于标识具体的输入源。例如:
- KEY_0 到 KEY_9: 数字键
- KEY_A 到 KEY_Z: 字母键
- KEY_ENTER: 回车键
- KEY_SPACE: 空格键
- BTN_LEFT: 鼠标左键
- BTN_RIGHT: 鼠标右键
- ABS_X, ABS_Y: 绝对X、Y坐标
3. 事件类型与编码
3.1 事件类型详解
EV_KEY
按键事件是最常见的输入事件类型,用于表示按键的按下和释放。每个按键事件包含三个主要信息:
- type: 事件类型,固定为EV_KEY
- code: 按键编码,标识具体哪个按键
- value: 按键状态,0表示释放,1表示按下,2表示自动重复
EV_SYN
同步事件用于分隔事件包,确保事件的原子性。当多个相关事件需要同时处理时,它们会被打包在一个事件包中,以EV_SYN事件结束。常见的同步事件包括:
- SYN_REPORT: 表示一个完整的事件包结束
- SYN_CONFIG: 表示设备配置发生变化
- SYN_MT_REPORT: 多点触控事件报告
3.2 事件编码规范
Linux内核定义了标准的事件编码,确保不同设备之间的兼容性。编码遵循以下原则:
- 可读性: 编码名称应直观反映其功能
- 一致性: 相同类别的设备使用相似的编码方案
- 扩展性: 预留足够的编码空间以支持新设备
4. 输入设备注册流程
4.1 设备注册步骤
输入设备的注册流程遵循以下步骤:
-
分配输入设备结构体
使用input_allocate_device()
函数分配input_dev
结构体 -
设置设备属性
- 设置设备名称
name
- 设置物理路径
phys
- 设置唯一标识
uniq
- 设置设备名称
-
设置支持的事件类型
使用__set_bit()
函数设置设备支持的事件类型位图 -
设置支持的按键编码
对于按键设备,需要设置支持的按键编码 -
注册输入设备
使用input_register_device()
函数将设备注册到输入子系统 -
设备注销
在模块卸载时,使用input_unregister_device()
注销设备
4.2 代码示例
static int __init keyinput_init(void)
{int ret = 0;// 初始化按键ret = key_init(&keyinputdev);if (ret < 0) {goto fail_key_init;}// 分配输入设备keyinputdev.inputdev = input_allocate_device();if (keyinputdev.inputdev == NULL) {ret = -EINVAL;goto fail_input_allocate;}// 设置设备名称keyinputdev.inputdev->name = KEYINPUT_NAME;// 设置支持的事件类型__set_bit(EV_KEY, keyinputdev.inputdev->evbit);__set_bit(EV_REP, keyinputdev.inputdev->evbit);// 设置支持的按键__set_bit(KEY_0, keyinputdev.inputdev->keybit);// 注册输入设备ret = input_register_device(keyinputdev.inputdev);if (ret) {ret = -EINVAL;goto fail_input_reg;}return 0;fail_input_reg:input_free_device(keyinputdev.inputdev);
fail_input_allocate:
fail_key_init:return ret;
}
5. 中断处理机制
5.1 中断请求
在嵌入式系统中,按键通常通过GPIO引脚连接到处理器。当按键状态发生变化时,会触发GPIO中断。驱动程序需要注册中断处理函数来响应这些中断。
5.1.1 中断注册
ret = request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &keyinputdev);
参数说明:
- irqnum: 中断号
- handler: 中断处理函数
- flags: 中断触发方式
IRQF_TRIGGER_FALLING
: 下降沿触发IRQF_TRIGGER_RISING
: 上升沿触发IRQF_TRIGGER_EDGE
: 边沿触发IRQF_TRIGGER_LEVEL
: 电平触发
- name: 中断名称
- dev_id: 设备标识,用于共享中断
5.2 中断处理函数
中断处理函数应该尽可能简洁,避免长时间占用中断上下文。通常的做法是将耗时的操作推迟到下半部执行。
static irqreturn_t key0_handler(int irq, void *filp)
{struct keyinput_dev *dev = filp;dev->timer.data = (volatile long)filp;// 启动定时器进行防抖mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}
5.3 中断下半部
Linux内核提供了多种机制来处理中断下半部:
- 软中断(Softirq)
- tasklet
- 工作队列(Workqueue)
- 定时器(Timer)
在本例中,使用定时器作为中断下半部,既实现了防抖功能,又完成了按键状态的检测。
6. 定时器防抖技术
6.1 按键抖动问题
机械按键在按下和释放时会产生电气抖动,导致短时间内多次触发中断。如果不进行处理,会导致按键事件被错误地报告多次。
6.2 防抖解决方案
常见的防抖方法包括:
- 硬件防抖: 使用RC电路或施密特触发器
- 软件防抖: 使用延时检测
本驱动采用软件防抖方案,通过定时器实现:
static void timer_func(unsigned long arg)
{struct keyinput_dev *dev = (struct keyinput_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){input_event(dev->inputdev, EV_KEY, KEY_0, 1);input_sync(dev->inputdev);}else{input_event(dev->inputdev, EV_KEY, KEY_0, 0);input_sync(dev->inputdev);}
}
6.3 防抖参数选择
防抖时间的选择需要平衡响应速度和可靠性:
- 过短: 无法有效消除抖动
- 过长: 影响用户体验
通常,机械按键的抖动时间为5-20ms,因此选择20ms作为防抖时间是合理的。
7. 设备树(DTS)配置
7.1 设备树概述
设备树(Device Tree)是一种描述硬件配置的数据结构,它将硬件信息从内核代码中分离出来,提高了代码的可移植性和可维护性。
7.2 按键节点配置
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
7.2.1 属性说明
- compatible: 兼容性字符串,用于匹配驱动程序
- pinctrl-names: 引脚控制状态名称
- pinctrl-0: 默认状态下的引脚配置
- key-gpios: GPIO引脚配置
&gpio1
: GPIO控制器18
: GPIO编号GPIO_ACTIVE_HIGH
: 高电平有效
- interrupt-parent: 中断父节点
- interrupts: 中断配置
18
: 中断号IRQ_TYPE_EDGE_BOTH
: 边沿触发(上升沿和下降沿)
7.3 引脚控制配置
pinctrl_key: keygrp {fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080>;
};
7.3.1 引脚配置参数
- MX6UL_PAD_UART1_CTS_B__GPIO1_IO18: 引脚复用配置
- 0xF080: 电气特性配置
- 驱动强度
- 上拉/下拉电阻
- 压摆率等
8. 驱动代码详细分析
8.1 头文件包含
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/input.h>
8.1.1 头文件功能
- linux/module.h: 模块相关定义
- linux/kernel.h: 内核常用宏和函数
- linux/init.h: 初始化相关宏
- linux/fs.h: 文件系统相关定义
- asm/io.h: I/O操作函数
- asm/uaccess.h: 用户空间访问函数
- linux/cdev.h: 字符设备相关定义
- linux/device.h: 设备模型相关定义
- linux/of.h: 设备树相关定义
- linux/of_address.h: 设备树地址相关定义
- linux/slab.h: 内存分配函数
- linux/gpio.h: GPIO操作函数
- linux/of_gpio.h: 设备树GPIO操作函数
- linux/string.h: 字符串操作函数
- linux/interrupt.h: 中断相关定义
- linux/irq.h: IRQ相关定义
- linux/input.h: 输入子系统相关定义
8.2 宏定义
#define KEYINPUT_CNT 1
#define KEYINPUT_NAME "keyinputdev"
#define KEY0VALUE 0x10
#define KEYINVA 0xff
#define KEY_NUM 1
8.2.1 宏说明
- KEYINPUT_CNT: 设备计数
- KEYINPUT_NAME: 设备名称
- KEY0VALUE: 按键值
- KEYINVA: 无效按键值
- KEY_NUM: 按键数量
8.3 数据结构定义
8.3.1 key_desc结构体
struct key_desc
{char name[10];int gpio;int irqnum;unsigned char value;irqreturn_t (*handler)(int, void *);
};
该结构体描述单个按键的属性:
- name: 按键名称
- gpio: GPIO编号
- irqnum: 中断号
- value: 按键值
- handler: 中断处理函数
8.3.2 keyinput_dev结构体
struct keyinput_dev
{struct device_node *key_nd;struct key_desc key[KEY_NUM];struct timer_list timer;struct input_dev *inputdev;
};
该结构体描述整个输入设备:
- key_nd: 设备树节点
- key: 按键数组
- timer: 定时器
- inputdev: 输入设备结构体
8.4 函数实现
8.4.1 中断处理函数
static irqreturn_t key0_handler(int irq, void *filp)
{struct keyinput_dev *dev = filp;dev->timer.data = (volatile long)filp;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}
功能说明:
- 获取设备结构体指针
- 设置定时器数据
- 启动定时器(20ms后执行)
- 返回中断处理完成
8.4.2 定时器回调函数
static void timer_func(unsigned long arg)
{struct keyinput_dev *dev = (struct keyinput_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){input_event(dev->inputdev, EV_KEY, KEY_0, 1);input_sync(dev->inputdev);}else{input_event(dev->inputdev, EV_KEY, KEY_0, 0);input_sync(dev->inputdev);}
}
功能说明:
- 获取设备结构体指针
- 读取GPIO引脚状态
- 根据状态报告按键事件
- 同步事件
8.4.3 定时器初始化函数
void timer1_init(struct keyinput_dev *dev)
{init_timer(&dev->timer);dev->timer.function = timer_func;
}
功能说明:
- 初始化定时器
- 设置定时器回调函数
8.4.4 按键初始化函数
int key_init(struct keyinput_dev *dev)
{u8 ret = 0, i = 0;dev->key_nd = of_find_node_by_path("/key");if (dev->key_nd == NULL){ret = -EFAULT;goto fail_find_nd;}dev->key[i].handler = key0_handler;dev->key[i].value = KEY0VALUE;for (i = 0; i < KEY_NUM; i++){dev->key[i].gpio = of_get_named_gpio(dev->key_nd, "key-gpios", i);if (dev->key[i].gpio < 0){ret = -EFAULT;goto fail_get_gpio;}memset(dev->key[i].name, 0, sizeof(dev->key[i].name));sprintf(dev->key[i].name, "KEY%d", i);ret = gpio_request(dev->key[i].gpio, dev->key[i].name);if (ret){ret = -EFAULT;goto fail_gpio_req;}ret = gpio_direction_input(dev->key[i].gpio);if (ret){ret = -EFAULT;goto fail_gpio_dir;}dev->key[i].irqnum = gpio_to_irq(dev->key[i].gpio);ret = request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &keyinputdev);if (ret){ret = -EFAULT;goto fail_req_irq;}}timer1_init(&keyinputdev);return 0;
fail_req_irq:printk("Kernel: fail_req_irq\r\n");
fail_gpio_dir:for (i = 0; i < KEY_NUM; i++){gpio_free(dev->key[i].gpio);}printk("Kernel: fail_gpio_dir\r\n");
fail_gpio_req:printk("Kernel: fail_gpio_req\r\n");
fail_get_gpio:printk("Kernel: fail_get_gpio\r\n");
fail_find_nd:printk("Kernel: fail_find_nd\r\n");return ret;
}
功能说明:
- 查找设备树节点
- 配置按键属性
- 获取GPIO编号
- 请求GPIO
- 设置GPIO方向为输入
- 获取中断号
- 请求中断
- 初始化定时器
错误处理采用goto模式,确保资源正确释放。
8.4.5 模块初始化函数
static int __init keyinput_init(void)
{int ret = 0;ret = key_init(&keyinputdev);if (ret < 0){goto fail_key_init;}keyinputdev.inputdev = input_allocate_device();if (keyinputdev.inputdev == NULL){ret = -EINVAL;goto fail_input_allocate;}keyinputdev.inputdev->name = KEYINPUT_NAME;__set_bit(EV_KEY, keyinputdev.inputdev->evbit);__set_bit(EV_REP, keyinputdev.inputdev->evbit);__set_bit(KEY_0, keyinputdev.inputdev->keybit);ret = input_register_device(keyinputdev.inputdev);if (ret){ret = -EINVAL;goto fail_input_reg;}return 0;
fail_input_reg:input_free_device(keyinputdev.inputdev);printk("Driver: fail_input_reg\r\n");
fail_input_allocate:printk("Driver: fail_input_allocate\r\n");
fail_key_init:printk("Driver: fail_key_init\r\n");return ret;
}
功能说明:
- 初始化按键
- 分配输入设备
- 设置设备属性
- 注册输入设备
8.4.6 模块退出函数
static void __exit keyinput_exit(void)
{u8 i = 0;input_unregister_device(keyinputdev.inputdev);input_free_device(keyinputdev.inputdev);for (i = 0; i < KEY_NUM; i++){free_irq(keyinputdev.key[i].irqnum, &keyinputdev);gpio_free(keyinputdev.key[i].gpio);}del_timer(&keyinputdev.timer);
}
功能说明:
- 注销输入设备
- 释放输入设备内存
- 释放中断
- 释放GPIO
- 删除定时器
8.5 模块声明
module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");
9. 用户空间应用程序
9.1 应用程序功能
用户空间应用程序负责打开输入设备文件,读取按键事件,并显示按键状态。
9.2 代码分析
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <asm/ioctl.h>#define KEY0VALUE 0x10
#define KEYINVA 0xffint main(int argc, char *argv[])
{int cnt = 0;if (argc != 2){fprintf(stderr, "Usage: %s <led_device> <0|1>\n", argv[0]);return -1;}char *fileanme;unsigned char databuf[1];fileanme = argv[1];int fd = 0;fd = open(fileanme, O_RDWR);if (fd < 0){perror("open led device error");return -1;}while (1){unsigned char ch = 0;int ret = read(fd, &ch, sizeof(ch));// printf("User: ret is: %d", ret);if (ret < 0){// printf("User: read error");}else{if (ch == KEY0VALUE){printf("User: key is pressing, ret is: %d\r\n", ret);}}// sleep(100);}printf("APP runing finished! \r\n");close(fd);return 0;
}
9.3 功能说明
- 参数检查: 检查命令行参数数量
- 设备打开: 打开输入设备文件
- 事件读取: 循环读取按键事件
- 事件处理: 判断按键状态并输出信息
- 资源释放: 关闭设备文件
9.4 改进建议
当前应用程序存在以下问题:
- 错误的设备打开模式: 输入设备通常以只读模式打开
- 错误的读取方式: 输入事件应该使用
struct input_event
结构体读取 - 缺少事件解析: 没有正确解析输入事件
改进版本:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>int main(int argc, char *argv[])
{if (argc != 2) {fprintf(stderr, "Usage: %s <input_device>\n", argv[0]);return -1;}int fd = open(argv[1], O_RDONLY);if (fd < 0) {perror("open input device");return -1;}struct input_event ev;while (1) {ssize_t n = read(fd, &ev, sizeof(ev));if (n == (ssize_t)-1) {perror("read");break;} else if (n != sizeof(ev)) {fprintf(stderr, "short read\n");break;}if (ev.type == EV_KEY && ev.code == KEY_0) {if (ev.value == 1) {printf("Key 0 pressed\n");} else if (ev.value == 0) {printf("Key 0 released\n");}}}close(fd);return 0;
}
10. 编译与部署
10.1 Makefile分析
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)obj-m := keyinput.o
build : kernel_moduleskernel_modules:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modulesclean:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
10.1.1 变量说明
- KERNERDIR: 内核源码目录
- CURRENTDIR: 当前目录
- obj-m: 要编译的模块对象
10.1.2 目标说明
- build: 编译目标
- kernel_modules: 实际编译命令
- clean: 清理目标
10.2 编译步骤
-
设置环境变量
export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf-
-
编译模块
make -C /path/to/kernel M=$(pwd) modules
-
清理
make -C /path/to/kernel M=$(pwd) clean
10.3 部署步骤
-
复制模块到目标板
scp keyinput.ko root@target:/lib/modules/$(uname -r)/
-
加载模块
insmod keyinput.ko
-
验证模块加载
lsmod | grep keyinput
-
检查设备节点
ls /dev/input/
14. 扩展应用
14.1 多按键支持
修改驱动以支持多个按键:
#define KEY_NUM 4struct key_desc key[KEY_NUM] = {{.name = "KEY0", .gpio = -1, .irqnum = -1, .value = KEY_0},{.name = "KEY1", .gpio = -1, .irqnum = -1, .value = KEY_1},{.name = "KEY2", .gpio = -1, .irqnum = -1, .value = KEY_2},{.name = "KEY3", .gpio = -1, .irqnum = -1, .value = KEY_3},
};
14.2 自定义事件处理
添加自定义事件处理逻辑:
static void custom_event_handler(struct keyinput_dev *dev, int key_value)
{switch(key_value) {case KEY_0:// 处理KEY0事件break;case KEY_1:// 处理KEY1事件break;default:break;}
}
14.3 与其他子系统集成
将输入事件与其他子系统集成:
// 与LED子系统集成
extern void led_control(int state);static void timer_func(unsigned long arg)
{struct keyinput_dev *dev = (struct keyinput_dev *)arg;int value = gpio_get_value(dev->key[0].gpio);if (value == 0) {input_event(dev->inputdev, EV_KEY, KEY_0, 1);led_control(1); // 点亮LED} else {input_event(dev->inputdev, EV_KEY, KEY_0, 0);led_control(0); // 熄灭LED}input_sync(dev->inputdev);
}
参考文档
- Linux内核源码: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
- 设备树规范: https://www.devicetree.org/
- 《Linux设备驱动程序》第三版
- 《深入理解Linux内核》第三版
https://gitee.com/dream-cometrue/linux_driver_imx6ull