【嵌入式Linux - 应用开发】输入设备
【目录】
【参考资料】
一、【理论】什么是输入系统
二、【理论】输入系统框架
三、【总结】Linux 输入事件处理流程
四、【理解】Linux 开发输入设备知识
(一) 【内核态】内核如何表示一个输入设备(struct input_dev)
1. 基本信息字段
2. 属性位图
3. 事件支持位图
(二) 【用户态】应用程序可以得到什么样的数据?(struct input_event)
(三) 【用户态】同步事件:类似数据包包尾,实现传输不定长数据(X、Y、Z、压力……)
五、【开发】调试方法
(一) 确定设备信息
1. 查看设备节点(ls /dev/input/* -l)
2. 查看设备节点对应的硬件信息(cat /proc/bus/input/devices)
【高阶】分析对应的硬件信息——devices字段含义解释(I、N、P、S、U、H、B)
(二) 使用命令读取数据(hexdump /dev/input/event*)
六、【开发】APP 访问(输入设备)硬件驱动的四种方式(查询/休眠唤醒/poll 方式/异步 IO)
【补充】ioctl 系统调用
函数原型
request 参数格式:_IOC 宏定义
位域布局
参数含义
(一) 【核心】输入设备的 ioctl 命令
(二) 【核心】读取输入设备上报的事件(struct input_event)
【开发方式】查询方式(非阻塞 IO,O_NONBLOCK)
【开发方式】休眠-唤醒方式(阻塞 IO)
【开发方式】POLL/SELECT方式
1. POLL 方式
2. SELECT 方式
【开发方式】异步通知方式(异步 IO):当输入设备的fd,发生事件/可操作时,内核通过SIGIO信号通知进程,进程进行信号处理
【参考资料】
- 主要看韦东山、正点原子
一、【理论】什么是输入系统
- 输入设备:
常见的有键盘、鼠标、遥控器、书写板、触摸屏等。用户通过这些设备与 Linux 系统进行数据交换。 - 输入系统的作用:
-
- 输入设备种类繁多,如果每种设备都用不同的接口,开发和使用都会很复杂。
- Linux 提供了一套 统一的输入系统框架,用来兼容和管理所有输入设备。
- 驱动开发者:基于该框架开发驱动程序。
- 应用开发者:通过统一的 API 使用各种输入设备,而不必关心底层差异。
👉 简单来说:Linux 的输入系统就是一个 抽象层/中间层,它屏蔽了不同输入设备的差异,让驱动和应用都能用统一的方式交互。
二、【理论】输入系统框架
Linux 输入子系统通过 统一框架 将不同输入设备抽象为统一接口:
- 事件抽象:
struct input_event
(time, type, code, value) - 用户层:APP 通过
/dev/input/eventX
或库函数读取事件
-
- 同步机制:
EV_SYN
确保 APP 能识别一组完整的数据
- 同步机制:
- 核心层:统一管理 (
input_dev
+ handler) - 驱动层:负责采集硬件事件
三、【总结】Linux 输入事件处理流程
- 用户操作
用户通过键盘、鼠标、触摸屏等输入设备产生事件 → 触发硬件中断。 - 驱动层
-
- 驱动程序捕获中断,读取数据帧。
- 将事件传递给 输入子系统核心层(
struct input_dev
结构体管理)。 - 核心层将事件分发给对应的 事件处理 handler(如
evdev_handler
、kbd_handler
、joydev_handler
等)。
- 用户空间接口
-
- 每个 handler 都会对应一个设备节点(如
/dev/input/event0
)。 - 应用程序(APP)可以直接读取这些设备节点获取事件。
- 每个 handler 都会对应一个设备节点(如
- 应用层获取方式
-
- 直接读取:APP 直接访问
/dev/input/eventX
。 - 通过库:使用
tslib
、libinput
等库,这些库会封装底层设备节点的访问,提供统一接口,屏蔽不同设备差异。
- 直接读取:APP 直接访问
👉 总结一句话:
Linux 输入系统通过 驱动 → 核心层 → handler → 设备节点 → APP 的链路,把底层硬件事件统一抽象出来,应用程序既可以直接读设备节点,也可以通过库获得统一接口。
四、【理解】Linux 开发输入设备知识
(一) 【内核态】内核如何表示一个输入设备(struct input_dev
)
1. 基本信息字段
const char *name;
设备名称(字符串描述,例如 "Logitech USB Mouse")。const char *phys;
物理路径(如"usb-0000:00:14.0-1/input0"
)。const char *uniq;
唯一标识符(通常是序列号)。struct input_id id;
设备 ID(包含总线类型、厂商 ID、产品 ID、版本号)。
2. 属性位图
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
输入设备属性(如是否是指点设备、是否是直接输入设备等)。
3. 事件支持位图
evbit
→ 支持的事件类型(如EV_KEY
,EV_REL
,EV_ABS
)。keybit
→ 支持的按键集合(如KEY_A
,KEY_ENTER
)。relbit
→ 支持的相对坐标事件(如REL_X
,REL_Y
,典型于鼠标)。absbit
→ 支持的绝对坐标事件(如ABS_X
,ABS_Y
,典型于触摸屏/手柄摇杆)。mscbit
→ 支持的杂项事件(如MSC_SCAN
)。ledbit
→ 支持的 LED 指示灯(如LED_CAPSL
,键盘大写锁定灯)。sndbit
→ 支持的声音事件(如蜂鸣器)。ffbit
→ 支持的力反馈(Force Feedback)功能。swbit
→ 支持的开关事件(如SW_LID
,笔记本合盖检测)。
📌 总结:
struct input_dev
是 Linux 输入子系统的核心数据结构,用来描述一个输入设备的能力。通过这些位图,内核和用户空间可以快速判断设备支持哪些事件类型和功能。
(二) 【用户态】应用程序可以得到什么样的数据?(struct input_event
)
驱动层上报的数据
- timeval:事件发生时间(自系统启动以来的秒/微秒)
- type:事件类型
-
EV_KEY
→ 按键EV_REL
→ 相对位移(鼠标移动)EV_ABS
→ 绝对位置(触摸屏)
- code:具体事件编号(如哪个键、哪个坐标轴)
-
- 对于按键类事件,它表示键盘。键盘上有很多按键
-
- 对于触摸屏事件,它提供的是绝对位置信息,有 X 方向、 Y 方向,还有压力值
- value:事件值(按下/释放/坐标/压力等)
-
- 对于按键,它的 value 可以是 0(表示按键被按下)、 1(表示按键被松开)、2(表示长按);
- 对于触摸屏,它的 value 就是坐标值、压力值。
(三) 【用户态】同步事件:类似数据包包尾,实现传输不定长数据(X、Y、Z、压力……)
- 问题:
-
- 一个输入操作可能包含多个数据,如不同输入设备:
-
-
- 触摸屏 A 会上报:X、Y 坐标和压力值
- 触摸屏 B 会上报:X、Y 坐标
-
-
- APP 如何知道一组数据是否完整?(ACK 机制)
- 解决方案:
-
- 使用 同步事件 (EV_SYN) 表示一次数据上报的结束
- 当 APP 读取到
EV_SYN
时,说明当前这组数据已完整,可以处理
struct input_event xxx;
.type = 0; (EV_SYN == 0)
.code = 0;
.value = 0;
- 说明:
-
- 同步事件本质上也是一个
input_event
- 具有
type
、code
、value
三个字段 - 常见定义:
EV_SYN = 0x00
- 同步事件本质上也是一个
五、【开发】调试方法
(一) 确定设备信息
1. 查看设备节点(ls /dev/input/* -l
)
要确定输入设备的设备节点,其名称通常为/dev/input/eventX(或/dev/eventX),其中X代表数字0、1、2等。
可以通过执行以下命令来查看设备节点:
ls /dev/input/* -l
ls /dev/event* -l
2. 查看设备节点对应的硬件信息(cat /proc/bus/input/devices
)
cat /proc/bus/input/devices
这条指令的含义是获取与event
对应的相关设备信息。
执行cat /proc/bus/input/devices
命令后的输出示例,列出了两个输入设备的详细信息:
// 设备 1: Goodix Capacitive TouchScreen (触摸屏)
I: Bus=0018 Vendor=0416 Product=038f Version=1060
N: Name="Goodix Capacitive TouchScreen" (名称为"Goodix 电容触摸屏")
P: Phys=input/ts
S: Sysfs=/devices/platform/soc/5c002000.i2c/i2c-2/2-005d/input/input0
U: Uniq=
H: Handlers=kbd event0 (设备节点为/dev/input/event0)
B: PROP=2
B: EV=b
B: KEY=400 0 0 0 0 0 0 20000000 0 0 0
B: ABS=2658000 3// 设备 2: Joystick (GPIO按键)
I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="Joystick" (名称为"操纵杆")
P: Phys=gpio-keys/input0 (物理路径为GPIO按键)
S: Sysfs=/devices/platform/joystick/input/input1
U: Uniq=
H: Handlers=kbd event1 (设备节点为/dev/input/event1)
B: PROP=0
B: EV=3
B: KEY=50000000
【高阶】分析对应的硬件信息——devices
字段含义解释(I
、N
、P
、S
、U
、H
、B
)
在cat /proc/bus/input/devices
的输出中,I
、N
、P
、S
、U
、H
、B
对应的每一行都有特定的含义:
设备 ID (I)
该参数由结构体struct input_id
来描述:
// include/uapi/linux/input.h
struct input_id {__u16 bustype;__u16 vendor;__u16 product;__u16 version;
};
这个结构体包含了总线类型(bustype)、供应商ID(vendor)、产品ID(product)和版本号(version)。
其他字段含义
- N: Name (设备名称)
- P: Phys (物理路径)
- S: Sysfs (sysfs路径)
- U: Uniq (唯一标识符)
- H: Handlers (处理程序和对应的设备节点)
- B: PROP, EV, KEY, ABS (设备支持的属性、事件类型、按键和绝对坐标等信息)
位图解释(EV位图示例)
B位图,如"B: EV=b",用于指示设备支持哪些类型的输入事件。
B: EV=b // 设备支持哪些类型的输入事件
B: KEY=400 0 0 0 0 0 0 20000000 0 0 0 // 支持的KEY类型事件中的哪些具体事件
B: ABS=2658000 3 // 支持的ABS类型事件中的哪些具体事件
'b'的二进制表示为1011
。
bit0
、bit1
和bit3
为1
,表示设备支持这三种类型的事件:
EV_SYN == 0
EV_KEY == 1
EV_ABS == 3
【EV_ABS】ABS 位图示例(绝对位置)
另一个例子是"B: ABS=2658000 3"。
这表示设备在EV_ABS
事件类别中支持哪些特定事件。
这是两个32位数字:0x2658000
和0x3
。
- 高位在前,低位在后,形成64位数字:"0x2658000,00000003"。
- 值为
1
的位位于位置:0
、1
、47
、48
、50
、53
、54
。 - 这些对应以下十六进制值:
0
、1
、0x2f
、0x30
、0x32
、0x35
、0x36
。
绝对位置事件宏定义
// include/uapi/linux/input.h
#define ABS_X 0x00
#define ABS_Y 0x01#define ABS_MT_SLOT 0x2f /* MT slot being modified */
#define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */
#define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */
#define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */
#define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */
#define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */
#define ABS_MT_POSITION_X 0x35 /* Center X touch position */
#define ABS_MT_POSITION_Y 0x36 /* Center Y touch position */
支持的绝对位置事件
该输入设备支持上述列出的绝对位置事件:ABS_X
、ABS_Y
、ABS_MT_SLOT
、ABS_MT_TOUCH_MAJOR
、ABS_MT_WIDTH_MAJOR
、ABS_MT_POSITION_X
和ABS_MT_POSITION_Y
。
它们的具体含义将在后续讨论电容屏时详细阐述。
(二) 使用命令读取数据(hexdump /dev/input/event*
)
hexdump /dev/input/event*
【自己实测的触摸屏】
六、【开发】APP 访问(输入设备)硬件驱动的四种方式(查询/休眠唤醒/poll 方式/异步 IO)
【补充】ioctl
系统调用
函数原型
int ioctl(int fd, unsigned long request, ...);
其中:
fd
:设备的文件描述符request
:请求参数,对于某些驱动程序有特定的格式要求...
:可变参数,用于传递数据
request
参数格式:_IOC 宏定义
request
参数的格式由_IOC
宏定义,该宏在include/uapi/asm-generic/ioctl.h
头文件中定义:
#define _IOC(dir,type,nr,size) \(((dir) << _IOC_DIRSHIFT) | \ // _IOC_DIRSHIFT == bit29((type) << _IOC_TYPESHIFT) | \ // bit8((nr) << _IOC_NRSHIFT) | \ // bit0((size) << _IOC_SIZESHIFT)) // bit16
位域布局
- dir:被左移到第29位 (_IOC_DIRSHIFT)
- type:被左移到第8位 (_IOC_TYPESHIFT)
- nr:被左移到第0位 (_IOC_NRSHIFT)
- size:被左移到第16位 (_IOC_SIZESHIFT)
这个宏将四个部分组合成一个unsigned long
类型的request
值:
参数含义
dir
(方向):表示数据传输的方向
-
_IOC_NONE
(0):无数据传输_IOC_WRITE
(1):应用程序要向设备写入数据_IOC_READ
(2):应用程序要从设备读取数据_IOC_READ|_IOC_WRITE
(3):双向数据传输
type
(类型):表示设备类型或命令组,通常用单个字符表示nr
(编号):表示具体的命令编号size
(大小):表示这个ioctl能传输数据的最大字节数
(一) 【核心】输入设备的 ioctl
命令
// include/uapi/linux/input.h// 获取设备信息
#define EVIOCGID _IOR('E', 0x02, struct input_id) /* get device ID */err = ioctl(fd, EVIOCGID, &id);
if (err == 0) {printf("bustype = 0x%x\n", id.bustype);printf("vendor = 0x%x\n", id.vendor);printf("product = 0x%x\n", id.product);printf("version = 0x%x\n", id.version);
}// 获取事件位图
#define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len)len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
if (len > 0 && len <= sizeof(evbit)) {printf("support ev type: ");for (i = 0; i < len; i++) {byte = ((unsigned char*)evbit)[i];for (bit = 0; bit < 8; bit++) {if (byte & (1 << bit)) {printf("%s ", ev_names[i * 8 + bit]);}}}printf("\n");
}// 获取设备名称
#define EVIOCGNAME(len) _IOC(_IOC_READ, 'E', 0x06, len)// 获取物理路径
#define EVIOCGPHYS(len) _IOC(_IOC_READ, 'E', 0x07, len)// 获取唯一标识符
#define EVIOCGUNIQ(len) _IOC(_IOC_READ, 'E', 0x08, len)// 获取设备属性
#define EVIOCGPROP(len) _IOC(_IOC_READ, 'E', 0x09, len)// 获取全局按键状态
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)// 获取所有LED状态
#define EVIOCGLED(len) _IOC(_IOC_READ, 'E', 0x19, len)// 获取所有声音状态
#define EVIOCGSND(len) _IOC(_IOC_READ, 'E', 0x1a, len)// 获取所有开关状态
#define EVIOCGSW(len) _IOC(_IOC_READ, 'E', 0x1b, len)
(二) 【核心】读取输入设备上报的事件(struct input_event
)
struct input_event event;
while (read(fd, &event, sizeof(event)) == sizeof(event))
{printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);
}
【开发方式】查询方式(非阻塞 IO,O_NONBLOCK
)
fd = open(argv[1], O_RDWR | O_NONBLOCK);
当APP调用read函数读取数据时:
- 如果驱动程序中有数据,read函数会立即返回数据
- 否则,read函数会立刻返回错误,不会等待数据
【开发方式】休眠-唤醒方式(阻塞 IO)
fd = open(argv[1], O_RDWR);
当APP调用read函数读取数据时:
- 如果驱动程序中有数据,read函数会立即返回数据
- 否则,APP会进入内核态休眠【进程会进入休眠状态】
- 当有数据可用时,驱动程序会唤醒APP
- read函数会恢复执行并返回数据给APP
【开发方式】POLL
/SELECT
方式
1. POLL
方式
int fd;
int ret;
int bit;
struct input_event event;
struct input_id id;struct pollfd fds[1];
nfds_t nfds = 1;fd = open(argv[1], O_RDWR | O_NONBLOCK);while (1)
{fds[0].fd = fd;fds[0].events = POLLIN;fds[0].revents = 0;ret = poll(fds, nfds, 5000);if (ret > 0){if (fds[0].revents & POLLIN){while (read(fd, &event, sizeof(event)) == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);}}}else if (ret == 0){printf("time out\n");}else{printf("poll err\n");}
}
2. SELECT
方式
int fd;
int err;
int len;
int ret;
int i;
unsigned char byte;
int bit;
struct input_id id;
unsigned int evbit[2];
struct input_event event;
int nfds;
struct timeval tv;
fd_set readfds;while (1)
{/* 设置超时时间 */tv.tv_sec = 5;tv.tv_usec = 0;/* 想监测哪些文件? */FD_ZERO(&readfds); /* 先全部清零 */ FD_SET(fd, &readfds); /* 想监测fd *//* 函数原型为:int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);* 我们为了"read"而监测, 所以只需要提供readfds*/nfds = fd + 1; /* nfds 是最大的文件句柄+1, 注意: 不是文件个数, 这与poll不一样 */ ret = select(nfds, &readfds, NULL, NULL, &tv);if (ret > 0) /* 有文件可以提供数据了 */{/* 再次确认fd有数据 */if (FD_ISSET(fd, &readfds)){while (read(fd, &event, sizeof(event)) == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);}}}else if (ret == 0) /* 超时 */{printf("time out\n");}else /* -1: error */{printf("select err\n");}
}
【开发方式】异步通知方式(异步 IO):当输入设备的fd,发生事件/可操作时,内核通过SIGIO信号通知进程,进程进行信号处理
int fd;// 当输入设备的fd,发生事件/可操作时,内核通过SIGIO信号
void my_sig_handler(int sig)
{if (sig != SIGIO)return ;struct input_event event;while (read(fd, &event, sizeof(event)) == sizeof(event)) // 操作输入设备fd{printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); }
}int main(int argc, char **argv)
{/* 注册信号处理函数 */signal(SIGIO, my_sig_handler);/* 打开驱动程序 */fd = open(argv[1], O_RDWR | O_NONBLOCK);/* 把APP的进程号告诉驱动程序 */fcntl(fd, F_SETOWN, getpid());/* 使能"异步通知" */flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | FASYNC);while (1) {printf("main loop count = %d\n", count++);sleep(2);}...
}