--- title: 输入设备与tslib tags: [嵌入式Linux, Linux应用编程, input子系统, 输入设备, 触摸屏, tslib, IMX6ULL] created: 2026-09-18 updated: 2026-09-18 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十七章 输入设备应用编程、第十八章 使用tslib库" --- # 输入设备与tslib > 💡 **关联知识**:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]];延伸阅读:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/08-misc与input子系统]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]] 输入设备(鼠标、键盘、触摸屏、按键……)把用户动作变成数据交给系统。Linux 用 **input 子系统**统一了它们的接口:应用层只要 `open` 一个 `/dev/input/eventX`,再 `read` 出 `struct input_event`,就得到原始输入数据。本篇先讲清 input 子系统的数据格式与解析方法(按键、单点触摸、多点触摸),再引入 **tslib** —— 把"读原始 event + 去噪 + 坐标变换"封装好的触摸屏应用层函数库。 --- ## 1. 输入设备与 input 子系统 **输入设备**(input 设备)指能够产生输入事件的设备:鼠标、键盘、触摸屏、遥控器、画图板等。设备种类繁多、上报数据类型各异,Linux 为统一管理实现了 **input 子系统**: - 驱动人员基于它开发驱动,它屏蔽硬件差异、向应用层提供统一接口;注册成功的设备在 `/dev/input` 下生成 `eventX` 节点,应用层读取节点即可获取数据。 ```mermaid flowchart TB subgraph 用户态 APP["应用程序 open/read /dev/input/eventX"] end subgraph 内核态 CORE["input 子系统核心"] DRV1["按键驱动"] DRV2["触摸屏驱动"] DRV3["USB 键盘/鼠标驱动"] end HW["硬件 GPIO 按键 / 触摸 IC / USB 设备"] APP -->|"read struct input_event"| CORE CORE <--> DRV1 CORE <--> DRV2 CORE <--> DRV3 DRV1 --> HW DRV2 --> HW DRV3 --> HW ``` 读取流程(以触摸屏 `/dev/input/event0` 为例): 1. 应用打开设备文件;2. 发起 `read`,无数据可读时在阻塞 I/O 下休眠;3. 有数据时被唤醒、读操作返回;4. 应用解析数据。 > Tips:设备文件不同于普通文件,读写设备文件之前**无需设置读写位置偏移量**。 --- ## 2. struct input_event:应用层看到的数据单元 应用层每次 `read` 得到一个 `struct input_event`,定义在 ``: ```c struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; }; ``` `time` 是事件发生时间(通常不是重点),重点是其馀三个成员: | 成员 | 含义 | | --------------------------------- | ---------------------------------- | | `type` | 事件类型(哪一大类事件) | | `code` | 事件代码(该类中的哪一个具体事件) | | `value` | 事件值,解释随 `code` 变化 | | 事件类型宏(``): | ```c #define EV_SYN 0x00 //同步类事件 #define EV_KEY 0x01 //按键类事件 #define EV_REL 0x02 //相对位移类事件(譬如鼠标) #define EV_ABS 0x03 //绝对位移类事件(譬如触摸屏) #define EV_MSC 0x04 //其它杂类事件 /* 还有 EV_SW(0x05)、EV_LED(0x11)、EV_SND(0x12)、EV_REP(0x14) 等 */ #define EV_MAX 0x1f #define EV_CNT (EV_MAX+1) ``` 常见 `code`: ```c /* 按键类 */ #define KEY_1 2 //数字 1 键 #define KEY_A 30 //字母 A 键 #define KEY_VOLUMEDOWN 114 //出厂系统 KEY0 使用的键值 /* 相对位移类 */ #define REL_X 0x00 //X 轴 #define REL_Y 0x01 //Y 轴 /* 绝对位移类(触摸屏) */ #define ABS_X 0x00 //X 轴 #define ABS_Y 0x01 //Y 轴 #define ABS_MT_SLOT 0x2f //当前更新的触摸点 slot #define ABS_MT_POSITION_X 0x35 //触摸点 X 坐标 #define ABS_MT_POSITION_Y 0x36 //触摸点 Y 坐标 #define ABS_MT_TRACKING_ID 0x39 //触摸点 ID #define BTN_TOUCH 330 //触摸按下/松开 ``` `value` 的解释随 `code` 变化:按键事件中 `value=1` 按下、`0` 松开、`2` 长按;绝对位移事件中 `code=ABS_X` 时 `value` 就是 X 坐标,`code=ABS_Y` 时就是 Y 坐标。这些宏定义在 `input-event-codes.h`,被 `` 包含。 --- ## 3. 数据同步:EV_SYN 与 SYN_REPORT 一次 `read` 只能读一个 event,而一个触摸点含 X、Y 等多项,需多次 `read` 才读全。内核把本轮数据全部上报后,会再上报一个**同步事件**告知"本轮数据已完整": ```c #define SYN_REPORT 0 // 本轮数据完整 #define SYN_DROPPED 3 // 数据丢失 ``` 所有输入设备都需上报同步事件,通常是 `SYN_REPORT`、`value` 通常为 0。 --- ## 4. 读取 struct input_event 数据(read_input.c) 例程 `read_input.c` 是最基础的读取框架:传参为设备节点路径,用 `open(argv[1], O_RDONLY)` 只读打开,在死循环中每次 `read` 恰好读出一个 event(阻塞 I/O,无数据时在此休眠),再把 `in_ev.type`、`in_ev.code`、`in_ev.value` 打印出来供手工分析。完整程序含 `#include`、传参校验与错误处理,见例程 `17_input/read_input.c`。 ```bash arm-linux-gnueabihf-gcc -o testApp read_input.c scp testApp root@192.168.1.10:/home/root/ ``` ### 4.1 在开发板上验证按键 KEY0 ALPHA/Mini 都有一个用户按键 **KEY0**,出厂系统中其驱动基于 input 子系统。用 `cat /proc/bus/input/devices` 确定设备节点后运行程序,示例中 KEY0 对应 `/dev/input/event2`。按下、松开 KEY0 的输出为: ```text type:1 code:114 value:1 // EV_KEY, code=114=KEY_VOLUMEDOWN, value=1 按下 type:0 code:0 value:0 // EV_SYN/SYN_REPORT,本轮数据完整 type:1 code:114 value:0 // 松开 type:0 code:0 value:0 // 同步 ``` 长按(按住不放)时 `value=2`。 --- ## 5. 按键应用编程(read_key.c) 按键上报流程为 `KEY_A` → `SYN_REPORT`,`value` 1/0/2 分别表示按下/松开/长按: ```c #include #include #include #include #include #include #include int main(int argc, char *argv[]) { struct input_event in_ev = {0}; int fd = -1; int value = -1; /* 校验传参 */ if (2 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 打开文件 */ if (0 > (fd = open(argv[1], O_RDONLY))) { perror("open error"); exit(-1); } for ( ; ; ) { /* 循环读取数据 */ if (sizeof(struct input_event) != read(fd, &in_ev, sizeof(struct input_event))) { perror("read error"); exit(-1); } if (EV_KEY == in_ev.type) { //按键事件 switch (in_ev.value) { case 0: printf("code<%d>: 松开\n", in_ev.code); break; case 1: printf("code<%d>: 按下\n", in_ev.code); break; case 2: printf("code<%d>: 长按\n", in_ev.code); break; } } } } ``` 拿到数据后先判断 `EV_KEY`,再按 `value` 区分松开/按下/长按。 ```bash arm-linux-gnueabihf-gcc -o testApp read_key.c scp testApp root@192.168.1.10:/home/root/ ./testApp /dev/input/event2 # 测试 KEY0 ``` 把 USB 键盘插到开发板 USB HOST 口,也可按同样方法在 `/proc/bus/input/devices` 找到 `eventX` 测试;根据 `code` 查 `input-event-codes.h` 即知按键,如 `code=30` 对应 A 键、`code=48` 对应 B 键。 **读取多个输入设备**:一个进程一次只能阻塞读取一个设备节点。若要同时读取多个设备(如按键 + 触摸屏),可用 `poll()`/`select()` 同时监听多个 fd(见 [[03-外设与高级IO编程/01-高级IO]]),或为每个设备各开一个线程/进程;`input_event.time` 时间戳可用于还原不同设备事件的先后顺序。 --- ## 6. 触摸屏:事件类型与上报流程 触摸屏是**绝对位移设备**,上报 `EV_ABS`: | 类型 | 特点 | 承载事件 | | -------- | -------------------------- | ------------------------------------- | | 单点触摸 | 一轮完整数据只含一个触摸点 | `ABS_X`、`ABS_Y` 等 `ABS_XXX` | | 多点触摸 | 一轮完整数据可含多个触摸点 | `ABS_MT_POSITION_X/Y` 等 `ABS_MT_XXX` | 除位移事件外还会上报: - **同步事件**:几乎每个输入设备都上报,告知本轮数据是否完整; - **按键事件 `BTN_TOUCH`**(`code=0x14a`,即 330):点击触摸屏或手指离开时上报,滑动时不上报。它不支持长按,`value` 不会等于 2;对多点设备,只有第一个点按下时 `value=1`、最后一个点离开时 `value=0`。 单点设备上报顺序: ```text # 点击 # 滑动 # 松开 BTN_TOUCH ABS_X BTN_TOUCH ABS_X ABS_Y SYN_REPORT ABS_Y SYN_REPORT SYN_REPORT ``` 不同设备信息量不同(有的仅 X/Y,有的带按压力、接触面积),这些数据都会在 `SYN_REPORT` 之前上报。 ### 6.1 多点触摸与 MT Type B 协议 Linux 用多点触摸(MT)协议上报各触摸点数据,分 Type A 与 Type B。**Type A 使用很少、几乎淘汰**,开发板配套触摸屏都属于 **Type B**。 Type B 适用于能追踪并区分触摸点的设备,重点是用 `ABS_MT_SLOT` 上报各触摸点信息的更新: - **slot**:硬件概念,触摸点的编号,通常按触碰时间先后分配、从 0 开始;`ABS_MT_SLOT` 的 `value` 告知当前正在更新哪个 slot; - **tracking_id**:软件概念,由 `ABS_MT_TRACKING_ID` 上报,用于触摸点的创建、替换和销毁——`>=0` 为有效触摸点,`-1` 表示被移除,以前不存在的 ID 表示新触摸点; - Type B **只上报发生变更的数据**:若只改变 X 坐标,内核只发新的 `ABS_MT_POSITION_X`。 ```text ABS_MT_SLOT 0 ABS_MT_TRACKING_ID 10 ABS_MT_POSITION_X ABS_MT_POSITION_Y ABS_MT_SLOT 1 ABS_MT_TRACKING_ID 11 ABS_MT_POSITION_X ABS_MT_POSITION_Y SYN_REPORT ``` | 概念 | 层级 | 含义 | | ---- | ---- | --------------------------------------------------- | | slot | 硬件 | 触摸点编号,按触碰先后分配 | | ID | 软件 | 触摸点生命周期的标识;手离开后销毁,再触碰即为新 ID | 同一手指触碰、离开、再触碰,两次都是 slot=0,但生命周期不同,因此 ID 不同。 ### 6.2 上报数据实例 一个手指点击触摸屏不松开,`read_input.c` 打印: 要点:`ABS_MT_TRACKING_ID` 非负表示新建触摸点(按下);多点设备也通过 `ABS_X/ABS_Y` 上报坐标,但通常只有触摸点 0 支持,故可当单点用;**有的屏幕因触摸芯片不同没有 `EV_KEY`,也就没有 `BTN_TOUCH`**。增加第二个触摸点时会先上报 `ABS_MT_SLOT`(`code=47`)`value=1`;手指松开时上报 `ABS_MT_TRACKING_ID` 且 `value=-1`。 --- ## 7. 获取触摸屏信息:ioctl 与 input_absinfo ```c #include int ioctl(int fd, unsigned long request, ...); ``` - `fd` 文件描述符;`request` 请求指令;可变参数随 `request` 决定。 input 设备的请求指令宏(`input.h`): ```c #define EVIOCGNAME(len) _IOC(_IOC_READ, 'E', 0x06, len) /* get device name */ #define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) /* get global key state */ #define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo) /* get abs value/limits */ ``` `EVIOCG`(get)获取信息,`EVIOCS`(set)设置。重点 `EVIOCGABS(abs)` 可获取某 `ABS_XXX` 事件的取值范围,第三个参数为 `struct input_absinfo *`: ```c struct input_absinfo { __s32 value; //最新的报告值 __s32 minimum; //最小值 __s32 maximum; //最大值 __s32 fuzz; __s32 flat; __s32 resolution; }; ``` 例程 `17_input/read_slot.c` 打开设备后执行: ```c struct input_absinfo info; if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info)) perror("ioctl error"); max_slots = info.maximum + 1 - info.minimum; printf("max_slots: %d\n", max_slots); ``` 结果为 `max_slots: 5` 时说明是 5 点触摸屏,该值正是下面多点程序申请 `struct ts_mt` 数组的依据。 ## 8. 单点触摸应用程序(read_ts.c) 把多点触摸屏当单点用:用 x/y 保存坐标,down 记录状态(1 按下、0 松开、-1 滑动),valid 标记本轮关注信息是否更新。解析逻辑为:EV_ABS 事件中,ABS_MT_TRACKING_ID 的 value 判断按下(0)/松开(-1),ABS_MT_POSITION_X/Y 更新坐标并置 valid;收到 SYN_REPORT 时数据完整,按 down 打印"按下/松开/移动"并重置标志。完整程序见例程 17_input/read_ts.c。 ```bash arm-linux-gnueabihf-gcc -o testApp read_ts.c scp testApp root@192.168.1.10:/home/root/ ./testApp /dev/input/event1 ``` ## 9. 多点触摸应用程序(read_mt.c) 按 slot 分别维护每个触摸点的坐标与 ID: ```c #include #include #include #include #include #include #include #include #include /* 用于描述MT多点触摸每一个触摸点的信息 */ struct ts_mt { int x; //X坐标 int y; //Y坐标 int id; //对应ABS_MT_TRACKING_ID int valid; //数据有效标志位(=1表示触摸点信息发生更新) }; /* 一个触摸点的x坐标和y坐标 */ struct tp_xy { int x; int y; }; static int ts_read(const int fd, const int max_slots, struct ts_mt *mt) { struct input_event in_ev; static int slot = 0;//用于保存上一个slot static struct tp_xy xy[12] = {0};//用于保存上一次的x和y坐标值,假设触摸屏支持的最大触摸点数不会超过12 int i; /* 对缓冲区初始化操作 */ memset(mt, 0x0, max_slots * sizeof(struct ts_mt)); //清零 for (i = 0; i < max_slots; i++) mt[i].id = -2;//将id初始化为-2, id=-1表示触摸点删除, id>=0表示创建 for ( ; ; ) { if (sizeof(struct input_event) != read(fd, &in_ev, sizeof(struct input_event))) { perror("read error"); return -1; } switch (in_ev.type) { case EV_ABS: switch (in_ev.code) { case ABS_MT_SLOT: slot = in_ev.value; break; case ABS_MT_POSITION_X: xy[slot].x = in_ev.value; mt[slot].valid = 1; break; case ABS_MT_POSITION_Y: xy[slot].y = in_ev.value; mt[slot].valid = 1; break; case ABS_MT_TRACKING_ID: mt[slot].id = in_ev.value; mt[slot].valid = 1; break; } break; //case EV_KEY://按键事件对单点触摸应用比较有用 // break; case EV_SYN: if (SYN_REPORT == in_ev.code) { for (i = 0; i < max_slots; i++) { mt[i].x = xy[i].x; mt[i].y = xy[i].y; } } return 0; } } } int main(int argc, char *argv[]) { struct input_absinfo slot; struct ts_mt *mt = NULL; int max_slots; int fd; int i; /* 参数校验 */ if (2 != argc) { fprintf(stderr,"usage: %s \n", argv[0]); exit(EXIT_FAILURE); } /* 打开文件 */ fd = open(argv[1], O_RDONLY); if (0 > fd) { perror("open error"); exit(EXIT_FAILURE); } /* 获取触摸屏支持的最大触摸点数 */ if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)) { perror("ioctl error"); close(fd); exit(EXIT_FAILURE); } max_slots = slot.maximum + 1 - slot.minimum; printf("max_slots: %d\n", max_slots); /* 申请内存空间并清零 */ mt = calloc(max_slots, sizeof(struct ts_mt)); /* 读数据 */ for ( ; ; ) { if (0 > ts_read(fd, max_slots, mt)) break; for (i = 0; i < max_slots; i++) { if (mt[i].valid) {//判断每一个触摸点信息是否发生更新(关注的信息发生更新) if (0 <= mt[i].id) printf("slot<%d>, 按下(%d, %d)\n", i, mt[i].x, mt[i].y); else if (-1 == mt[i].id) printf("slot<%d>, 松开\n", i); else printf("slot<%d>, 移动(%d, %d)\n", i, mt[i].x, mt[i].y); } } } /* 关闭设备、退出 */ close(fd); free(mt); exit(EXIT_FAILURE); } ``` - `main` 先用 `ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)` 求最大触摸点数,再按 `max_slots` 申请 `struct ts_mt` 数组:`mt[0]` 对应 slot 0,依次类推; - `ts_read()` 把一轮数据填入数组:`ABS_MT_SLOT` 更新当前 slot,后续 X/Y、TRACKING_ID 都写入该 slot;`SYN_REPORT` 时同步坐标并返回; - `id` 初值 `-2` 表示未上报,`-1` 表示删除(松开),`>=0` 表示创建(按下)。单点靠 `BTN_TOUCH` 判断动作,多点靠 **ID** 判断各手指动作。 ```bash arm-linux-gnueabihf-gcc -o testApp read_mt.c scp testApp root@192.168.1.10:/home/root/ ./testApp /dev/input/event1 ``` ## 10. tslib 库 tslib 是专门为触摸屏开发的 Linux **应用层函数库**,开源。它是触摸屏驱动与应用层之间的适配层: - 把应用层读取 `struct input_event` 并解析的过程**封装**,向上提供 API; - 从触摸屏获得原始坐标,经**去噪、去抖、坐标变换**,转换为屏幕坐标; - 通过配置文件 `ts.conf` 提供配置参数; - 可作为 **Qt 的触摸屏输入插件**(也可用其它插件,但多数选择 tslib)。 ```mermaid flowchart LR HW["触摸屏硬件"] --> DRV["内核 input 驱动"] DRV -->|"/dev/input/eventX 原始 input_event"| TS["tslib 去噪/去抖/坐标变换"] TS -->|"封装 API"| APP["触摸屏应用程序"] TS -->|"ts.conf / pointercal"| CFG["配置与校准文件"] ``` ### 10.1 tslib 移植 进入 git 仓库 `https://github.com/libts/tslib/releases` 下载源码。出厂系统已移植 **1.16**,为统一版本下载 `tslib-1.16.tar.gz`。编译分三步:配置 → 编译 → 安装。 ```bash tar -xzf tslib-1.16.tar.gz mkdir -p ~/tools/tslib # 设置交叉编译 SDK 环境 source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi cd tslib-1.16 ./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/tslib/ make make install ``` - `--host`:库文件运行的平台,通常设为交叉编译器名称前缀(`arm-poky-linux-gnueabi-gcc` 的前缀即 `arm-poky-linux-gnueabi`); - `--prefix`:安装路径;`./configure --help` 可看全部选项。 安装目录:`bin/`(测试工具)、`etc/ts.conf`(配置)、`include/tslib.h`(头文件)、`lib/`(库文件与 `ts/` 插件库)、`share/`(可忽略)。 `ts.conf` 常用配置: | 配置 | 作用 | | --------------------------- | --------------------------------------- | | `module_raw input` | 取消注释后使能支持 input 输入事件 | | `module pthres pmin=1` | 支持按压力测试时启用,`pmin` 调节灵敏度 | | `module dejitter delta=100` | 去噪插件,过滤噪声样本 | | `module linear` | 坐标变换(X/Y 互换、旋转等) | 部署到自己做的根文件系统时:`bin/` 下可执行文件拷到 `/usr/bin`,`ts.conf` 拷到 `/etc`,`lib/` 下库文件拷到 `/usr/lib`。运行时依赖以下环境变量(出厂系统已在 `/etc/profile` 中配置): | 变量 | 含义 | | --------------------- | ------------------------------------- | | `TSLIB_CONSOLEDEVICE` | 控制台设备名,直接为 `none` | | `TSLIB_FBDEVICE` | 显示设备节点(画线测试要在 LCD 显示) | | `TSLIB_TSDEVICE` | 触摸屏设备节点,按实际配置 | | `TSLIB_CONFFILE` | `ts.conf` 路径 | | `TSLIB_PLUGINDIR` | 插件路径 | > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 > > 校准相关变量 `TSLIB_CALIBFILE` 用于指定校准数据文件(默认 `/etc/pointercal`)。工具 `ts_calibrate` 会在屏幕**四角与中心**共五个点提示点击,用采集到的原始坐标与理论屏幕坐标做线性变换拟合,求出参数写入 `pointercal`,运行时 `module linear` 插件读取它完成坐标变换。若设备坐标已稳定,也可不生成该校准文件。 测试工具:`ts_print`/`ts_test`(单点,后者可在 LCD 画线)、`ts_print_mt`/`ts_test_mt`(多点)。执行后在触摸屏上按下、滑动、松开,终端会打印信息或画线。查看设备与版本可用 `ts_finddev`。这些工具源码位于 tslib 源码目录 `tests/` 下,内部最终仍落实到前面的 input event 解析。 ### 10.2 tslib 库函数 使用需包含 `#include `,步骤为:打开设备 → 配置设备 → 读取数据。 | 函数 | 说明 | | ------------ | ------------------------------------------------------------------------- | | `ts_open` | 打开触摸屏设备;成功返回句柄,失败返回 `NULL` | | `ts_setup` | 打开**并配置**设备;`dev_name` 传 `NULL` 时读取 `TSLIB_TSDEVICE` 环境变量 | | `ts_config` | 解析 `ts.conf`、加载插件;成功 0、失败 -1 | | `ts_close` | 关闭设备 | | `ts_read` | 读单点触摸数据 | | `ts_read_mt` | 读多点触摸数据;`max_slots` 为最大触摸点数 | `nonblock`:0 阻塞、非 0 非阻塞。`nr` 为对一个触摸点的采样数,设为 1 即可;出错时 `ts_read` 返回负数。 单点样本结构体: ```c struct ts_sample { int x; //X 坐标 int y; //Y 坐标 unsigned int pressure; //按压力大小 struct timeval tv; //时间 }; ``` 多点样本结构体 `struct ts_sample_mt` 常用字段: | 字段 | 含义 | | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | `x` / `y` | X / Y 坐标 | | `pressure` | 按压力大小 | | `slot` | 触摸点 slot | | `tracking_id` | 触摸点 ID | | `tv` | 时间 | | `pen_down` | `BTN_TOUCH` 状态 | | `valid` | 本次样本是否有效(数据是否更新) | | 其余字段(`tool_type`、`tool_x/y`、`touch_major/minor`、`width_major/minor`、`orientation`、`distance`、`blob_id`)为更细的描述,一般可忽略。 | ## 11. 基于 tslib 的单点触摸程序(ts_read.c) ```c #include #include #include //包含tslib.h头文件 int main(int argc, char *argv[]) { struct tsdev *ts = NULL; struct ts_sample samp; int pressure = 0;//用于保存上一次的按压力,初始为0,表示松开 /* 打开并配置触摸屏设备 */ ts = ts_setup(NULL, 0); if (NULL == ts) { fprintf(stderr, "ts_setup error"); exit(EXIT_FAILURE); } /* 读数据 */ for ( ; ; ) { if (0 > ts_read(ts, &samp, 1)) { fprintf(stderr, "ts_read error"); ts_close(ts); exit(EXIT_FAILURE); } if (samp.pressure) {//按压力>0 if (pressure) //若上一次的按压力>0 printf("移动(%d, %d)\n", samp.x, samp.y); else printf("按下(%d, %d)\n", samp.x, samp.y); } else printf("松开\n");//打印坐标 pressure = samp.pressure; } ts_close(ts); exit(EXIT_SUCCESS); } ``` `ts_setup(NULL, 0)` 内部读取 `TSLIB_TSDEVICE` 得到设备节点并完成配置;通过**按压力**判断状态:`pressure=0` 表示松开,大于 0 时再根据上一次按压力判断"按下/移动"。 ```bash arm-linux-gnueabihf-gcc -I /home/dt/tools/tslib/include -L /home/dt/tools/tslib/lib -lts -o testApp ts_read.c scp testApp root@192.168.1.10:/home/root/ ./testApp ``` | 选项 | 含义 | | ------ | ---------------------------------------------------- | | `-I` | 头文件路径(tslib 的 `include`) | | `-L` | 库文件路径(tslib 的 `lib`) | | `-lts` | 链接 `libts.so`(Linux 动态库命名 `lib`+名字+`.so`) | --- ## 12. 基于 tslib 的多点触摸程序(ts_read_mt.c) ```c #include #include #include #include #include int main(int argc, char *argv[]) { struct tsdev *ts = NULL; struct ts_sample_mt *mt_ptr = NULL; struct input_absinfo slot; int max_slots; unsigned int pressure[12] = {0}; //用于保存每一个触摸点上一次的按压力,初始为0,表示松开 int i; /* 打开并配置触摸屏设备 */ ts = ts_setup(NULL, 0); if (NULL == ts) { fprintf(stderr, "ts_setup error"); exit(EXIT_FAILURE); } /* 获取触摸屏支持的最大触摸点数 */ if (0 > ioctl(ts_fd(ts), EVIOCGABS(ABS_MT_SLOT), &slot)) { perror("ioctl error"); ts_close(ts); exit(EXIT_FAILURE); } max_slots = slot.maximum + 1 - slot.minimum; printf("max_slots: %d\n", max_slots); /* 内存分配 */ mt_ptr = calloc(max_slots, sizeof(struct ts_sample_mt)); /* 读数据 */ for ( ; ; ) { if (0 > ts_read_mt(ts, &mt_ptr, max_slots, 1)) { perror("ts_read_mt error"); ts_close(ts); free(mt_ptr); exit(EXIT_FAILURE); } for (i = 0; i < max_slots; i++) { if (mt_ptr[i].valid) {//有效表示有更新! if (mt_ptr[i].pressure) { //如果按压力>0 if (pressure[mt_ptr[i].slot])//如果上一次的按压力>0 printf("slot<%d>, 移动(%d, %d)\n", mt_ptr[i].slot, mt_ptr[i].x, mt_ptr[i].y); else printf("slot<%d>, 按下(%d, %d)\n", mt_ptr[i].slot, mt_ptr[i].x, mt_ptr[i].y); } else printf("slot<%d>, 松开\n", mt_ptr[i].slot); pressure[mt_ptr[i].slot] = mt_ptr[i].pressure; } } } /* 关闭设备、释放内存、退出 */ ts_close(ts); free(mt_ptr); exit(EXIT_SUCCESS); } ``` - 用 `ts_fd(ts)` 从 tslib 句柄取出底层 fd,再 `ioctl` 获取最大触摸点数; - 按 `max_slots` 申请 `struct ts_sample_mt` 数组;`ts_read_mt(ts, &mt_ptr, max_slots, 1)` 读一轮数据; - 只处理 `valid` 为真的触摸点;每个 slot 用 `pressure[]` 记录上一次按压力,区分"按下/移动"。 ```bash arm-linux-gnueabihf-gcc -I /home/dt/tools/tslib/include -L /home/dt/tools/tslib/lib -lts -o testApp ts_read_mt.c ``` --- ## 13. 实验步骤与调试方法 ### 13.1 实验步骤 1. 将 LCD 屏连接到开发板 LCD 接口,上电启动出厂系统;点击屏幕进入设置页面,点击退出按钮退出出厂系统 GUI 应用; 2. `cat /proc/bus/input/devices` 确认触摸屏设备节点(如 `goodix-ts`); 3. 用 `read_input.c` 打印原始数据,对照第 6.2 节分析事件规则; 4. 用 `read_slot.c` 查看最大触摸点数,再运行 `read_ts.c`、`read_mt.c`; 5. 运行 `ts_print`/`ts_print_mt`/`ts_test`/`ts_test_mt` 验证 tslib; 6. 交叉编译 `ts_read.c`/`ts_read_mt.c`(带 `-I`、`-L`、`-lts`),拷到开发板运行。 ### 13.2 调试方法 | 现象 | 可能原因 | 排查手段 | | --------------------------------------- | -------------------- | ------------------------------------------- | | `open error: No such file or directory` | 设备节点写错 | `cat /proc/bus/input/devices` 确认 `eventX` | | `open error: Permission denied` | 权限不足 | 用 `root` 或检查节点权限 | | `read` 阻塞无输出 | 该设备无事件(正常) | 触发按键/触摸;或改用非阻塞 | | 坐标与屏幕不符 | 未做坐标变换/校准 | 用 tslib `module linear` 或校准 | | 编译找不到 `tslib.h` | 未指定头文件路径 | 加 `-I /include` | | 链接找不到 `libts` | 未指定库路径或库名 | 加 `-L /lib -lts` | ```bash cat /proc/bus/input/devices # 查看所有输入设备及 eventX hexdump /dev/input/event2 # 十六进制查看原始事件流(需触发事件) ts_finddev # 查看 tslib 设备与版本 ``` --- ## 14. 跨平台对比:IMX6ULL vs STM32 vs RK3568 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 | 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 | | ---------- | --------------------------------------- | ---------------------------------- | ---------------------------- | | 输入框架 | Linux input 子系统,`/dev/input/eventX` | 无统一框架,直接读寄存器或自写驱动 | Linux input 子系统,接口一致 | | 数据单元 | `struct input_event` | 自定义结构/寄存器值 | `struct input_event` | | 多点触摸 | MT Type B,slot + tracking_id | 触摸 IC 自定义报文 | MT 协议,接口一致 | | 用户态库 | tslib(去噪/校准/坐标变换,Qt 插件) | 通常无,需自行滤波校准 | tslib 同样可用 | | 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc` | `aarch64-linux-gnu-gcc` | I.MX6ULL 与 RK3568 同属 Linux 应用编程,input 子系统与 tslib 用法基本通用;STM32 裸机没有设备节点与 `input_event`,不能套用本篇模型。 --- ## 15. 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ### Q1:什么是 input 子系统?它对应用层提供了什么? **答**:input 子系统是 Linux 为统一管理各种输入设备(键盘、鼠标、触摸屏、按键等)实现的兼容框架。驱动人员基于它开发输入设备驱动,它屏蔽硬件差异、向应用层提供统一接口。注册成功的设备在 `/dev/input` 下生成 `eventX` 节点,应用层 `open` 后 `read`,每次得到一个 `struct input_event`(time、type、code、value)。 ### Q2:`struct input_event` 的 type、code、value 分别是什么?如何判断按键按下? **答**:`type` 描述事件大类(`EV_KEY` 按键、`EV_REL` 相对位移、`EV_ABS` 绝对位移、`EV_SYN` 同步等);`code` 指明该类中的具体事件;`value` 是事件值,解释随 `code` 变化。对按键事件,`code` 为键值,`value=1` 按下、`0` 松开、`2` 长按;先判断 `type==EV_KEY`,再读 `value` 即可判断状态,`code` 用于区分具体按键。 ### Q3:为什么需要 EV_SYN / SYN_REPORT?一次 read 能读全一个触摸点吗? **答**:一次 `read` 只能读一个 `struct input_event`,而一个触摸点含 X、Y 等多项,需多次 `read` 才读全。内核把本轮数据全部上报后,再上报 `SYN_REPORT`(`type=EV_SYN`、`code=0`)告知本轮数据完整。应用层收到它时才认为信息到齐、可以解析。 ### Q4:多点触摸中 slot 与 tracking_id 有什么区别? **答**:slot 是硬件层面概念,代表触摸点编号,按触碰时间先后分配、从 0 开始,`ABS_MT_SLOT` 告知当前更新哪个 slot。tracking_id 是软件层面概念,标识触摸点生命周期,由 `ABS_MT_TRACKING_ID` 上报:`>=0` 为有效触摸点,`-1` 表示被移除。同一手指触碰、离开、再触碰,两次都是 slot=0,但属于不同生命周期,tracking_id 不同。 ### Q5:tslib 是什么?它做了什么,如何使用? **答**:tslib 是开源的 Linux 触摸屏应用层函数库,是驱动与应用层之间的适配层。它把"读取原始 `input_event` 并解析"封装起来,经去噪、去抖、坐标变换,把原始触摸坐标转换为屏幕坐标,并通过 `ts.conf` 配置和校准文件完成变换;还可作为 Qt 的触摸屏输入插件。使用步骤:`ts_setup`/`ts_open` 打开、`ts_config` 配置(`ts_setup` 已含)、`ts_read`/`ts_read_mt` 读取、`ts_close` 关闭。需设置 `TSLIB_TSDEVICE`、`TSLIB_CONFFILE`、`TSLIB_PLUGINDIR` 等环境变量,编译时用 `-I`、`-L`、`-lts`。 --- ## 延伸阅读 - 驱动侧原理:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/08-misc与input子系统]] - 触摸屏驱动实现:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]] - 本库相关:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]] --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十七章 输入设备应用编程、第十八章 使用tslib库;例程源码 `17_input`(read_input.c、read_key.c、read_slot.c、read_ts.c、read_mt.c)、`18_tslib`(ts_read.c、ts_read_mt.c)