解码Linux文件IO之触摸屏原理及应用
触摸屏基本概念
核心定义与作用
触摸屏是一种人机交互输入设备,通过检测手指(或触控笔)的按压、滑动等动作,将物理位置转换为数字坐标,实现 “点击屏幕操作界面” 的功能。常见应用场景:智能设备(手机、平板)、工业控制屏、车载中控、开发板人机界面(如 LCD 配套触摸屏)。

Linux 下的输入设备管理逻辑
Linux 系统中,输入设备(键盘、鼠标、触摸屏、摇杆等)种类繁多,为统一管理,设计了输入子系统(Input Subsystem)。核心思路:用 “中间层” 屏蔽不同硬件的细节,让应用程序无需关注硬件差异,只需通过统一接口读取输入事件(如触摸坐标、按键按下)。

Linux 输入子系统架构
输入子系统从下到上分为 3 层,每层职责明确,协同完成 “硬件事件→应用可识别数据” 的转换:
设备驱动层(最下层)
- 核心职责:直接操作触摸屏硬件,完成两件事:
- 读写硬件寄存器:初始化触摸屏、检测触摸动作(如手指按压时硬件产生中断);
- 转换硬件数据:将硬件输出的原始坐标(如电压值)转换为标准 “输入事件”(如 X 轴坐标 = 512),并提交给上层核心层。
- 示例:触摸屏驱动程序会监听硬件中断,当手指滑动时,实时读取触摸点的原始坐标,转换为
input_event结构体格式。
核心层(中间层)
- 核心职责:“承上启下” 的桥梁,提供标准化接口:
- 对下:给设备驱动层提供 “提交事件” 的接口(如
input_report_abs()用于上报绝对坐标); - 对上:给事件层提供 “接收事件” 的接口,同时规整驱动层提交的事件(确保格式统一)。
- 对下:给设备驱动层提供 “提交事件” 的接口(如
- 作用:让驱动层无需关心 “如何将事件传给应用”,事件层无需关心 “硬件如何产生事件”,降低耦合。
事件层(最上层)
- 核心职责:给应用程序提供统一的访问接口:
- 生成设备文件:驱动加载后,在
/dev/input/目录下生成设备文件(如event0); - 分发事件:将核心层传来的事件,通过设备文件分发给正在读取该文件的应用程序。
- 生成设备文件:驱动加载后,在
- 应用视角:只需打开
/dev/input/event0,读取文件中的input_event结构体,就能获取触摸屏的所有动作(按压、滑动、松开)。
核心结构体:input_event(事件封装)
所有输入设备的事件(触摸、按键、鼠标移动)都被统一封装为input_event结构体,定义在/usr/include/linux/input.h头文件中。应用程序读取到的触摸屏数据,本质就是一个个input_event结构体。
结构体定义与字段说明
#include <linux/input.h>
/**
* @brief 输入事件结构体(所有输入设备的事件都用此格式封装)
* @note 大小固定,不同设备仅填充的字段值不同
*/
struct input_event {
// 字段1:事件发生的时间戳(由内核自动填充)
#if (__BITS_PER_LONG != 32 || !defined(__USE_TIME_BITS64)) && !defined(_KERNEL_)
struct timeval time; // 32位系统/用户态:秒+微秒
#define input_event_sec time.tv_sec // 秒(自1970年1月1日起)
#define input_event_usec time.tv_usec // 微秒(0~999999)
#else
__kernel_ulong_t __sec; // 64位系统:秒
__kernel_ulong_t __usec; // 64位系统:微秒
#define input_event_sec __sec#define input_event_usec __usec#endif
__u16 type; // 字段2:事件类型(区分是触摸、按键还是鼠标移动)
__u16 code; // 字段3:事件代码(对类型的进一步细分,如X轴/Y轴坐标)
__s32 value; // 字段4:事件值(具体数据,如坐标值、按键状态)
};
关键字段详解(触摸屏核心)
type:事件类型(区分事件大类)
| type 值(宏定义) | 含义 | 触摸屏关联度 |
|---|---|---|
| EV_SYN | 事件同步标志 | 高(必用) |
| EV_ABS | 绝对位移事件 | 高(核心) |
| EV_KEY | 按键 / 触摸按压事件 | 高(核心) |
| EV_REL | 相对位移事件(如鼠标) | 低(无关联) |
| EV_LED | LED 灯控制事件 | 低(无关联) |
- EV_SYN:用于分割连续事件(如手指滑动时会产生多个坐标事件,
EV_SYN表示 “这一组事件已完整”); - EV_ABS:触摸屏坐标是 “绝对位置”(如 X 轴 0~1023),所以用此类型;
- EV_KEY:触摸屏的 “按压 / 松开” 动作,相当于 “按键按下 / 弹起”。
code:事件代码(细分事件类型)
需结合type值使用,触摸屏常用组合:
| type 值 | code 值(宏定义) | 含义 |
|---|---|---|
| EV_ABS | ABS_X | X 轴坐标事件(横向位置) |
| EV_ABS | ABS_Y | Y 轴坐标事件(纵向位置) |
| EV_KEY | BTN_TOUCH | 触摸按压 / 松开事件(相当于 “触摸键”) |
value:事件值(具体数据)
需结合type和code值判断,触摸屏常用组合:
| type 值 | code 值 | value 值含义 | 示例 |
|---|---|---|---|
| EV_ABS | ABS_X | X 轴坐标值(硬件支持的范围,如 0~1023) | 512(X 轴中点) |
| EV_ABS | ABS_Y | Y 轴坐标值(硬件支持的范围,如 0~599) | 300(Y 轴中点) |
| EV_KEY | BTN_TOUCH | 触摸状态:>0 = 按压,0 = 松开 | 1(手指按下)、0(松开) |
| EV_SYN | SYN_REPORT | 同步标志值(固定为 0) | 0(表示事件组完整) |
触摸屏事件序列示例(手指按压→滑动→松开)
当手指在触摸屏上操作时,内核会连续发送多个input_event,典型序列如下:
type=EV_KEY, code=BTN_TOUCH, value=1→ 手指按压;type=EV_ABS, code=ABS_X, value=300→ X 轴坐标 = 300;type=EV_ABS, code=ABS_Y, value=200→ Y 轴坐标 = 200;type=EV_SYN, code=SYN_REPORT, value=0→ 这组事件(按压 + 坐标)完整;type=EV_ABS, code=ABS_X, value=350→ 手指滑动,X 轴 = 350;type=EV_ABS, code=ABS_Y, value=250→ 手指滑动,Y 轴 = 250;type=EV_SYN, code=SYN_REPORT, value=0→ 滑动事件完整;type=EV_KEY, code=BTN_TOUCH, value=0→ 手指松开;type=EV_SYN, code=SYN_REPORT, value=0→ 松开事件完整。
触摸屏设备文件
设备文件路径与类型
触摸屏属于字符设备(按字节流读写事件),驱动加载后,内核会在/dev/input/目录下自动生成设备文件,命名格式为eventX(X 为 0~31,如event0、event1)。
- 常见路径:
/dev/input/event0(开发板默认触摸屏设备文件,不同板卡可能不同,如event1); - 确认方法:通过
cat /proc/bus/input/devices查看设备信息,找到 “Touchscreen” 对应的eventX。
设备文件权限与访问
- 权限:默认是
crw-rw----(root 用户和 input 组可读写),普通用户需用sudo运行程序,或修改权限(如chmod 666 /dev/input/event0); - 访问方式:通过
open打开设备文件,read读取input_event事件,close关闭文件(与操作普通文件一致)。
如何判断设备是否为触摸屏?
Linux 输入子系统中没有专门的 “触摸屏类型数字”,而是通过设备支持的事件组合来判断。触摸屏的核心特征是同时支持以下事件:
EV_KEY类型下的BTN_TOUCH(触摸按压 / 松开事件);EV_ABS类型下的ABS_X和ABS_Y(X/Y 轴绝对坐标事件)。
代码示例:检测设备是否为触摸屏
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/input.h>
#include <stdlib.h>
/**
* @brief 判断设备是否为触摸屏
* @param fd 设备文件描述符
* @return 1=是触摸屏;0=不是;-1=错误
*/
int is_touchscreen(int fd) {
unsigned char key_bits[16] = {0}; // 存储EV_KEY类型支持的事件代码
unsigned char abs_bits[16] = {0}; // 存储EV_ABS类型支持的事件代码
// 检查是否支持EV_KEY事件类型,且包含BTN_TOUCH(触摸按压事件)
/**
* @param fd 设备文件描述符
* @param EVIOCGBIT(EV_KEY, sizeof(key_bits)) 命令:获取EV_KEY类型支持的事件代码
* @param key_bits 缓冲区:存储事件代码掩码(每个bit代表一种事件)
* @return 成功返回0;失败返回-1
*/
if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), key_bits) == -1) {
perror("ioctl 获取EV_KEY事件失败");
return -1;
}
// 判断是否支持BTN_TOUCH:检查key_bits中对应bit是否为1
int has_btn_touch = (key_bits[BTN_TOUCH / 8] & (1 << (BTN_TOUCH % 8))) != 0;
// 检查是否支持EV_ABS事件类型,且包含ABS_X和ABS_Y(坐标事件)
/**
* @param fd 设备文件描述符
* @param EVIOCGBIT(EV_ABS, sizeof(abs_bits)) 命令:获取EV_ABS类型支持的事件代码
* @param abs_bits 缓冲区:存储事件代码掩码
* @return 成功返回0;失败返回-1
*/
if (ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(abs_bits)), abs_bits) == -1) {
perror("ioctl 获取EV_ABS事件失败");
return -1;
}
// 判断是否支持ABS_X和ABS_Y
int has_abs_x = (abs_bits[ABS_X / 8] & (1 << (ABS_X % 8))) != 0;
int has_abs_y = (abs_bits[ABS_Y / 8] & (1 << (ABS_Y % 8))) != 0;
// 同时满足:支持触摸按压+X/Y坐标 → 是触摸屏
return (has_btn_touch && has_abs_x && has_abs_y) ? 1 : 0;
}
int main(int argc, char const *argv[]) {
if (argc != 2) {
printf("用法:%s <设备文件>\n", argv[0]);
printf("示例:%s /dev/input/event0\n", argv[0]);
exit(1);
}
int fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open 设备失败");
exit(1);
}
int result = is_touchscreen(fd);
if (result == 1) {
printf("%s 是触摸屏\n", argv[1]);
} else if (result == 0) {
printf("%s 不是触摸屏\n", argv[1]);
}
close(fd);
return 0;
}
运行结果:
# 检测触摸屏设备
sudo ./check_touch /dev/input/event0
/dev/input/event0 是触摸屏
# 检测鼠标设备
sudo ./check_touch /dev/input/mouse0
/dev/input/mouse0 不是触摸屏
用 ioctl 读取触摸屏参数
触摸屏的硬件参数(如坐标范围、支持的事件类型)可通过ioctl系统调用动态获取,无需硬编码,适配不同设备更灵活。
常用 ioctl 命令
| 命令 | 命令缩写含义 | 作用 | 参数类型 | 用途 |
|---|---|---|---|---|
EVIOCGID |
EVIOC(EVent device I/O Control,事件设备输入输出控制) + G(Get,获取) + ID(Identifier,设备标识) | 获取设备 ID(厂商、产品型号、版本等信息) | struct input_id * |
识别触摸屏具体型号,区分不同硬件 |
EVIOCGBIT |
EVIOC(事件设备输入输出控制) + G(Get,获取) + BIT(Bitmask,位掩码) | 获取设备支持的事件类型 / 事件代码的位掩码 | unsigned long, void *, size_t |
检查是否支持EV_KEY(触摸按压)、EV_ABS(坐标)等核心事件 |
EVIOCGABS |
EVIOC(事件设备输入输出控制) + G(Get,获取) + ABS(Absolute,绝对坐标) | 获取绝对坐标(X/Y 轴)的详细参数 | int, struct input_absinfo * |
读取 X/Y 轴的最小值、最大值、分辨率等,用于坐标转换 |
结构体:input_absinfo(坐标范围参数)
#include <linux/input.h>
/**
* @brief 绝对坐标参数结构体(存储X/Y轴的范围、误差等信息)
*/
struct input_absinfo {
__s32 value; // 当前实时坐标值
__s32 minimum; // 该轴最小值(如X轴0)
__s32 maximum; // 该轴最大值(如X轴1023)
__s32 fuzz; // 坐标误差允许范围(硬件波动值)
__s32 flat; // 死区范围(小于此值的变化被忽略)
__s32 resolution; // 分辨率(单位:坐标单位/毫米)
};
代码示例:读取触摸屏参数
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/input.h>
#include <sys/ioctl.h>
int main(int argc, char const *argv[]) {
if (argc != 2) {
printf("用法:%s <触摸屏设备文件>\n", argv[0]);
printf("示例:%s /dev/input/event0\n", argv[0]);
exit(1);
}
// 打开触摸屏设备文件
int ts_fd = open(argv[1], O_RDONLY);
if (ts_fd == -1) {
perror("open 触摸屏失败");
exit(1);
}
// 用EVIOCGBIT获取设备支持的事件类型
unsigned char event_bits[16] = {0}; // 事件类型掩码(16字节足够覆盖所有类型)
/**
* @param ts_fd 触摸屏文件描述符
* @param EVIOCGBIT(0, sizeof(event_bits)) 命令:获取所有事件类型掩码(0表示不限制类型)
* @param event_bits 缓冲区:存储掩码(每个bit代表一种事件类型)
* @return 成功返回0;失败返回-1
*/
if (ioctl(ts_fd, EVIOCGBIT(0, sizeof(event_bits)), event_bits) == -1) {
perror("ioctl 获取事件类型失败");
close(ts_fd);
exit(1);
}
// 解析事件类型:检查是否支持触摸核心事件
printf("支持的事件类型:\n");
if (event_bits[EV_KEY / 8] & (1 << (EV_KEY % 8))) {
printf(" - EV_KEY(触摸按压/松开事件)\n");
}
if (event_bits[EV_ABS / 8] & (1 << (EV_ABS % 8))) {
printf(" - EV_ABS(绝对坐标事件)\n");
}
if (event_bits[EV_SYN / 8] & (1 << (EV_SYN % 8))) {
printf(" - EV_SYN(事件同步标志)\n");
}
// 用EVIOCGABS获取X轴坐标范围
struct input_absinfo x_abs;
/**
* @param ts_fd 触摸屏文件描述符
* @param EVIOCGABS(ABS_X) 命令:获取X轴参数
* @param &x_abs 结构体:存储X轴的min/max等参数
* @return 成功返回0;失败返回-1
*/
if (ioctl(ts_fd, EVIOCGABS(ABS_X), &x_abs) == -1) {
perror("ioctl 获取X轴参数失败");
close(ts_fd);
exit(1);
}
// 用EVIOCGABS获取Y轴坐标范围
struct input_absinfo y_abs;
if (ioctl(ts_fd, EVIOCGABS(ABS_Y), &y_abs) == -1) {
perror("ioctl 获取Y轴参数失败");
close(ts_fd);
exit(1);
}
// 打印X/Y轴参数(坐标转换的关键依据)
printf("X轴参数:\n");
printf(" 最小值:%d\n 最大值:%d\n 分辨率:%d(坐标单位/毫米)\n",
x_abs.minimum, x_abs.maximum, x_abs.resolution);
printf("Y轴参数:\n");
printf(" 最小值:%d\n 最大值:%d\n 分辨率:%d(坐标单位/毫米)\n",
y_abs.minimum, y_abs.maximum, y_abs.resolution);
// 关闭设备
close(ts_fd);
return 0;
}
运行结果:
sudo ./ts_params /dev/input/event0
支持的事件类型:
- EV_KEY(触摸按压/松开事件)
- EV_ABS(绝对坐标事件)
- EV_SYN(事件同步标志)
X轴参数:
最小值:0
最大值:1023
分辨率:15(坐标单位/毫米)
Y轴参数:
最小值:0
最大值:599
分辨率:15(坐标单位/毫米)
坐标转换原理(触摸屏→LCD)
为什么需要转换?
触摸屏和 LCD 的分辨率不同,直接使用触摸屏原始坐标会导致 “点击位置与显示位置不匹配”。
- 示例:开发板触摸屏原始分辨率为
1024×600(X:01023,Y:0599),LCD 分辨率为800×480(X:0799,Y:0479);若触摸触摸屏 X=1023(最右),对应 LCD 应显示 X=799(最右),而非 1023(超出 LCD 范围)。
转换公式(等比例缩放)
核心原则:触摸屏坐标与 LCD 坐标 “成比例”,公式如下:
- LCD_X(LCD 上的 X 坐标)= 触摸屏原始 X × LCD 宽度 ÷ 触摸屏宽度;
- LCD_Y(LCD 上的 Y 坐标)= 触摸屏原始 Y × LCD 高度 ÷ 触摸屏高度。
示例计算:
- 触摸屏原始坐标(X=512,Y=300),触摸屏分辨率 1024×600,LCD 分辨率 800×480;
- LCD_X = 512 × 800 ÷ 1024 = 400;
- LCD_Y = 300 × 480 ÷ 600 = 240;
- 结果:触摸屏中点(512,300)对应 LCD 中点(400,240),点击位置匹配。
代码实现(坐标转换函数)
/**
* @brief 触摸屏坐标→LCD坐标转换函数
* @param ts_x 触摸屏原始X坐标(0~1023)
* @param ts_y 触摸屏原始Y坐标(0~599)
* @param lcd_x 输出参数:转换后的LCD X坐标(0~799)
* @param lcd_y 输出参数:转换后的LCD Y坐标(0~479)
* @note 触摸屏分辨率1024×600,LCD分辨率800×480,需根据实际硬件调整
*/
void ts_to_lcd(int ts_x, int ts_y, int *lcd_x, int *lcd_y) {
// 等比例缩放公式
*lcd_x = ts_x * 800 / 1024;
*lcd_y = ts_y * 480 / 600;
// 边界保护:确保坐标不超出LCD范围(防止硬件误差导致坐标超界)
if (*lcd_x < 0) *lcd_x = 0;
if (*lcd_x >= 800) *lcd_x = 799;
if (*lcd_y < 0) *lcd_y = 0;
if (*lcd_y >= 480) *lcd_y = 479;
}
触摸屏实战程序设计(读取坐标)
核心功能
打开触摸屏设备文件,循环读取触摸事件,解析 X/Y 坐标,转换为 LCD 坐标并输出。
完整代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/input.h>
#include <unistd.h>
// 触摸屏与LCD分辨率(根据实际硬件调整)
#define TS_WIDTH 1024 // 触摸屏X轴范围:0~1023
#define TS_HEIGHT 600 // 触摸屏Y轴范围:0~599
#define LCD_WIDTH 800 // LCD X轴范围:0~799
#define LCD_HEIGHT 480 // LCD Y轴范围:0~479
/**
* @brief 坐标转换函数(触摸屏→LCD)
* @param ts_x 触摸屏原始X
* @param ts_y 触摸屏原始Y
* @param lcd_x 输出:LCD X
* @param lcd_y 输出:LCD Y
*/
void ts_to_lcd(int ts_x, int ts_y, int *lcd_x, int *lcd_y) {
*lcd_x = ts_x * LCD_WIDTH / TS_WIDTH;
*lcd_y = ts_y * LCD_HEIGHT / TS_HEIGHT;
// 边界保护
*lcd_x = (*lcd_x < 0) ? 0 : (*lcd_x >= LCD_WIDTH ? LCD_WIDTH-1 : *lcd_x);
*lcd_y = (*lcd_y < 0) ? 0 : (*lcd_y >= LCD_HEIGHT ? LCD_HEIGHT-1 : *lcd_y);
}
int main(int argc, char const *argv[]) {
// 定义变量:存储设备文件描述符、输入事件、坐标
int ts_fd; // 触摸屏设备文件描述符
struct input_event ts_event; // 输入事件结构体
int ts_x = 0, ts_y = 0; // 触摸屏原始坐标
int lcd_x = 0, lcd_y = 0; // 转换后的LCD坐标
int cnt = 0; // 计数:累计读取到的坐标数量(X+Y=2个)
// 打开触摸屏设备文件(读写模式)
/**
* @param "/dev/input/event0" 触摸屏设备文件路径(需根据实际板卡确认)
* @param O_RDWR 打开模式:读写(也可O_RDONLY,只读事件)
* @return 成功返回文件描述符(非负);失败返回-1
*/
ts_fd = open("/dev/input/event0", O_RDWR);
if (ts_fd == -1) {
perror("open 触摸屏失败"); // 打印错误原因(如文件不存在、权限不足)
exit(1); // 退出程序,状态码1表示异常
}
printf("触摸屏设备打开成功,fd=%d\n", ts_fd);
// 循环读取触摸事件(阻塞式:无事件时会等待)
while (1) {
/**
* @brief read函数:从设备文件读取输入事件
* @param ts_fd 触摸屏文件描述符
* @param &ts_event 存储事件的结构体地址
* @param sizeof(ts_event) 读取的字节数(input_event固定大小)
* @return 成功返回读取的字节数(=sizeof(ts_event));失败返回-1
*/
int ret = read(ts_fd, &ts_event, sizeof(ts_event));
if (ret != sizeof(ts_event)) {
perror("read 触摸事件失败");
close(ts_fd);
exit(1);
}
// 解析事件:区分事件类型,提取坐标
// 若为绝对位移事件(EV_ABS),提取X/Y坐标
if (ts_event.type == EV_ABS) {
// 提取X轴坐标
if (ts_event.code == ABS_X) {
ts_x = ts_event.value; // 存储原始X坐标
cnt++; // 计数+1(已获取X)
}
// 提取Y轴坐标
else if (ts_event.code == ABS_Y) {
ts_y = ts_event.value; // 存储原始Y坐标
cnt++; // 计数+1(已获取Y)
}
}
// 若为触摸松开事件(EV_KEY+BTN_TOUCH+value=0),重置计数
else if (ts_event.type == EV_KEY && ts_event.code == BTN_TOUCH && ts_event.value == 0) {
cnt = 0; // 松开后,下一次按压重新计数
}
// 当同时获取到X和Y坐标(cnt>=2),转换并输出
if (cnt >= 2) {
ts_to_lcd(ts_x, ts_y, &lcd_x, &lcd_y); // 坐标转换
printf("触摸屏原始坐标:(%d, %d) → LCD坐标:(%d, %d)\n",
ts_x, ts_y, lcd_x, lcd_y);
cnt = 0; // 重置计数,等待下一次触摸
}
}
// 关闭设备文件(循环不会退出,此句仅为语法完整)
close(ts_fd);
return 0;
}
编译与运行
# 编译代码(假设文件名为ts_read.c)
gcc ts_read.c -o ts_read
# 运行程序(需root权限,否则无权限访问/dev/input/event0)
sudo ./ts_read
运行结果(手指触摸屏幕时输出):
触摸屏设备打开成功,fd=3
触摸屏原始坐标:(512, 300) → LCD坐标:(400, 240)
触摸屏原始坐标:(1023, 599) → LCD坐标:(799, 479)
触摸屏原始坐标:(0, 0) → LCD坐标:(0, 0)
触摸屏实战程序设计(滑动检测)
核心功能
适配触摸屏分辨率1024×600,实时读取触摸「按下起点」和「松开终点」坐标;通过等比例转换适配 LCD 屏幕(默认 800×480,可修改);计算 X/Y 方向坐标差值,优先判断差值绝对值大的方向(横向 / 纵向),最终输出左滑 / 右滑 / 上滑 / 下滑结果,仅在完整滑动(按下→滑动→松开)后触发检测,避免无效数据。
完整代码
#include <stdio.h> // 输入输出(printf)
#include <sys/types.h> // 文件类型定义
#include <sys/stat.h> // 文件状态定义
#include <fcntl.h> // 文件操作(open)
#include <stdlib.h> // 标准库(exit)
#include <linux/input.h> // 输入事件结构体/宏(核心)
#include <unistd.h> // 系统调用(read/close)
#include <math.h> // 数学函数(abs,计算绝对值)
// 硬件分辨率配置(重点:触摸屏1024×600,LCD可按需修改)
#define TS_WIDTH 1024 // 触摸屏原始X范围:0~1023(必须与硬件一致)
#define TS_HEIGHT 600 // 触摸屏原始Y范围:0~599(必须与硬件一致)
#define LCD_WIDTH 800 // LCD目标X范围:0~799(适配显示的分辨率)
#define LCD_HEIGHT 480 // LCD目标Y范围:0~479(适配显示的分辨率)
/**
* @brief 触摸屏坐标→LCD坐标转换(核心:等比例缩放,解决分辨率不匹配)
* @param ts_x 输入:触摸屏原始X坐标
* @param ts_y 输入:触摸屏原始Y坐标
* @param lcd_x 输出:转换后LCD的X坐标
* @param lcd_y 输出:转换后LCD的Y坐标
*/
void ts_to_lcd(int ts_x, int ts_y, int *lcd_x, int *lcd_y) {
// 等比例公式:LCD坐标 = 触摸屏坐标 × LCD分辨率 / 触摸屏分辨率
*lcd_x = ts_x * LCD_WIDTH / TS_WIDTH;
*lcd_y = ts_y * LCD_HEIGHT / TS_HEIGHT;
// 边界保护:防止转换后坐标超出LCD范围(避免后续显示/判断异常)
if (*lcd_x < 0) *lcd_x = 0;
else if (*lcd_x >= LCD_WIDTH) *lcd_x = LCD_WIDTH - 1;
if (*lcd_y < 0) *lcd_y = 0;
else if (*lcd_y >= LCD_HEIGHT) *lcd_y = LCD_HEIGHT - 1;
}
int main(int argc, char const *argv[]) {
// 定义核心变量
int ts_fd; // 触摸屏设备文件描述符(类似设备"ID")
struct input_event ts_event; // 存储读取到的输入事件(Linux标准结构体)
// 滑动坐标:press=按下起点,release=松开终点(均为转换后的LCD坐标)
int press_lcd_x = 0, press_lcd_y = 0;
int release_lcd_x = 0, release_lcd_y = 0;
// 临时变量:存储触摸屏原始坐标、标记是否已记录起点
int ts_x = 0, ts_y = 0;
int has_press = 0; // 1=已记录按下起点,0=未记录(避免重复记录)
// 打开触摸屏设备(只读模式,仅需读取触摸事件)
/**
* 设备路径说明:/dev/input/event0是常见路径,若失败需通过以下命令确认:
* cat /proc/bus/input/devices → 查找"Touchscreen"对应的"Handlers=eventX"(X为数字)
*/
ts_fd = open("/dev/input/event0", O_RDONLY);
if (ts_fd == -1) { // 错误处理:打开失败(权限/路径问题)
perror("open 触摸屏失败"); // 打印具体错误(如"Permission denied"或"No such file")
exit(1); // 异常退出,状态码1表示运行失败
}
printf("✅ 触摸屏设备打开成功(文件描述符:%d)\n", ts_fd);
printf("📌 触摸屏分辨率:1024×600 → LCD适配分辨率:%d×%d\n", LCD_WIDTH, LCD_HEIGHT);
printf("提示:手指在屏幕滑动(左/右/上/下),按Ctrl+C退出\n");
printf("-------------------------\n");
// 循环读取触摸事件(阻塞式:无事件时等待,不占用CPU资源)
while (1) {
/**
* read系统调用:从设备文件读取事件数据
* 返回值:成功=读取的字节数(固定=sizeof(ts_event)),失败=-1
*/
int read_ret = read(ts_fd, &ts_event, sizeof(ts_event));
if (read_ret != sizeof(ts_event)) { // 错误处理:读取事件失败
perror("read 触摸事件失败");
close(ts_fd); // 先关闭设备,避免资源泄漏
exit(1);
}
// 解析事件:分3类处理(坐标更新、触摸按下、触摸松开)
// 情况1:事件类型=绝对位移事件(EV_ABS)→ 更新当前触摸坐标
if (ts_event.type == EV_ABS) {
if (ts_event.code == ABS_X) { // 子类型=X轴坐标 → 记录原始X
ts_x = ts_event.value;
} else if (ts_event.code == ABS_Y) { // 子类型=Y轴坐标 → 记录原始Y
ts_y = ts_event.value;
}
}
// 情况2:事件类型=按键事件(EV_KEY)+ 触摸按下(value=1)→ 记录起点坐标
else if (ts_event.type == EV_KEY && ts_event.code == BTN_TOUCH && ts_event.value == 1) {
if (!has_press) { // 仅在未记录起点时执行(避免重复触发)
// 将触摸屏原始坐标转换为LCD坐标,作为滑动起点
ts_to_lcd(ts_x, ts_y, &press_lcd_x, &press_lcd_y);
has_press = 1; // 标记:已记录起点,等待松开
// (可选)调试用:打印起点坐标
// printf("按下起点(LCD):(%d, %d)\n", press_lcd_x, press_lcd_y);
}
}
// 情况3:事件类型=按键事件(EV_KEY)+ 触摸松开(value=0)→ 计算滑动方向
else if (ts_event.type == EV_KEY && ts_event.code == BTN_TOUCH && ts_event.value == 0) {
if (has_press) { // 仅在已记录起点时执行(确保是完整滑动)
// 将松开时的原始坐标转换为LCD坐标,作为滑动终点
ts_to_lcd(ts_x, ts_y, &release_lcd_x, &release_lcd_y);
// 计算滑动差值(终点 - 起点)→ 差值正负表示方向
int diff_x = release_lcd_x - press_lcd_x; // X方向差值(正=右,负=左)
int diff_y = release_lcd_y - press_lcd_y; // Y方向差值(正=下,负=上)
// 输出坐标信息(原始+转换后,方便调试)
printf("📌 按下起点(LCD):(%d, %d) | 松开终点(LCD):(%d, %d)\n",
press_lcd_x, press_lcd_y, release_lcd_x, release_lcd_y);
printf("📊 滑动差值:X=%d,Y=%d\n", diff_x, diff_y);
// 判断滑动方向(优先横向/纵向,再判断具体方向)
if (abs(diff_x) > abs(diff_y)) { // 横向差值绝对值大 → 横向滑动
if (diff_x > 0) {
printf("✅ 滑动方向:右滑\n");
} else {
printf("✅ 滑动方向:左滑\n");
}
} else { // 纵向差值绝对值大 → 纵向滑动
if (diff_y > 0) {
printf("✅ 滑动方向:下滑\n");
} else {
printf("✅ 滑动方向:上滑\n");
}
}
printf("-------------------------\n");
// 重置标记,等待下一次完整滑动
has_press = 0;
}
}
}
// 关闭设备(注:while(1)无限循环,需按Ctrl+C终止,此句仅为语法完整)
close(ts_fd);
return 0;
}
编译与运行
编译命令
假设代码文件名为ts_slide_detect.c,使用 GCC 编译(嵌入式开发需替换为交叉编译器,如arm-linux-gcc):
# 编译:生成可执行文件ts_slide_detect(-lm链接数学库,用于abs函数)
gcc ts_slide_detect.c -o ts_slide_detect -lm
运行命令
触摸屏设备文件默认仅 root 用户可访问,需用sudo获取权限:
# 运行程序(按Ctrl+C终止)
sudo ./ts_slide_detect
预期运行结果
手指在屏幕滑动后,终端输出示例(具体数值随滑动位置变化):
✅ 触摸屏设备打开成功(文件描述符:3)
📌 触摸屏分辨率:1024×600 → LCD适配分辨率:800×480
提示:手指在屏幕滑动(左/右/上/下),按Ctrl+C退出
-------------------------
📌 按下起点(LCD):(100, 80) | 松开终点(LCD):(600, 80)
📊 滑动差值:X=500,Y=0
✅ 滑动方向:右滑
-------------------------
📌 按下起点(LCD):(500, 300) | 松开终点(LCD):(500, 100)
📊 滑动差值:X=0,Y=-200
✅ 滑动方向:上滑
-------------------------
综合练习:触摸屏控制界面(开机→登录→主界面)
需求分析
- 流程:开机动画(显示 3 秒)→ 登录界面(含 “登录”“退出” 按钮)→ 主界面(含 “返回登录” 按钮);
- 交互:触摸 “登录” 进入主界面,触摸 “退出” 关闭程序,触摸 “返回登录” 回到登录界面;
- 限制:不使用
goto语句(用 “状态机” 实现界面切换)。
核心设计:状态机
用枚举类型定义界面状态,主循环根据当前状态执行对应逻辑,触摸事件触发状态切换:
// 定义界面状态
typedef enum {
STATE_BOOT, // 状态1:开机动画
STATE_LOGIN, // 状态2:登录界面
STATE_MAIN, // 状态3:主界面
STATE_EXIT // 状态4:退出程序
} UI_STATE;
完整代码(含 LCD 显示与触摸控制)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/input.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
// 硬件分辨率定义
#define TS_WIDTH 1024
#define TS_HEIGHT 600
#define LCD_WIDTH 800
#define LCD_HEIGHT 480
// 颜色定义(32位ARGB,A=0x00不透明)
#define COLOR_BLACK 0x00000000 // 黑色
#define COLOR_WHITE 0x00FFFFFF // 白色
#define COLOR_RED 0x00FF0000 // 红色
#define COLOR_BLUE 0x000000FF // 蓝色
#define COLOR_GREEN 0x0000FF00 // 绿色
// 界面状态枚举
typedef enum {
STATE_BOOT,
STATE_LOGIN,
STATE_MAIN,
STATE_EXIT
} UI_STATE;
// 全局变量(LCD内存映射地址、触摸屏文件描述符)
unsigned int *lcd_mem = NULL;
int ts_fd = -1;
/**
* @brief 坐标转换(触摸屏→LCD)
*/
void ts_to_lcd(int ts_x, int ts_y, int *lcd_x, int *lcd_y) {
*lcd_x = ts_x * LCD_WIDTH / TS_WIDTH;
*lcd_y = ts_y * LCD_HEIGHT / TS_HEIGHT;
*lcd_x = (*lcd_x < 0) ? 0 : (*lcd_x >= LCD_WIDTH ? LCD_WIDTH-1 : *lcd_x);
*lcd_y = (*lcd_y < 0) ? 0 : (*lcd_y >= LCD_HEIGHT ? LCD_HEIGHT-1 : *lcd_y);
}
/**
* @brief LCD初始化(打开设备、内存映射)
* @return 成功返回0;失败返回-1
*/
int lcd_init(void) {
int lcd_fd = open("/dev/fb0", O_RDWR);
if (lcd_fd == -1) { perror("open LCD失败"); return -1; }
struct fb_var_screeninfo lcd_var;
if (ioctl(lcd_fd, FBIOGET_VSCREENINFO, &lcd_var) == -1) {
perror("ioctl 获取LCD参数失败"); close(lcd_fd); return -1;
}
size_t lcd_map_len = lcd_var.xres * lcd_var.yres * (lcd_var.bits_per_pixel / 8);
lcd_mem = (unsigned int *)mmap(NULL, lcd_map_len, PROT_READ | PROT_WRITE,
MAP_SHARED, lcd_fd, 0);
if (lcd_mem == MAP_FAILED) {
perror("mmap LCD失败"); close(lcd_fd); return -1;
}
close(lcd_fd); // 映射后可关闭文件描述符
return 0;
}
/**
* @brief 在LCD指定位置画矩形(填充颜色)
* @param x1,y1 矩形左上角坐标
* @param x2,y2 矩形右下角坐标
* @param color 填充颜色(32位ARGB)
*/
void lcd_draw_rect(int x1, int y1, int x2, int y2, unsigned int color) {
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
if (x >= 0 && x < LCD_WIDTH && y >= 0 && y < LCD_HEIGHT) {
lcd_mem[y * LCD_WIDTH + x] = color;
}
}
}
}
/**
* @brief 开机动画(显示3秒,黑色背景+白色文字提示)
*/
void boot_animation(void) {
// 清屏(黑色背景)
lcd_draw_rect(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1, COLOR_BLACK);
// 显示文字提示(简化:用矩形模拟“开机中...”,实际项目用字体库)
lcd_draw_rect(300, 200, 500, 240, COLOR_WHITE); // 文字背景
lcd_draw_rect(310, 210, 490, 230, COLOR_BLACK); // 文字区域
printf("开机动画中...\n");
sleep(3); // 显示3秒后进入登录界面
}
/**
* @brief 读取触摸坐标(阻塞,直到触摸按压)
* @param lcd_x 输出:LCD X坐标
* @param lcd_y 输出:LCD Y坐标
*/
void ts_read_press(int *lcd_x, int *lcd_y) {
struct input_event ts_event;
int ts_x = 0, ts_y = 0;
int cnt = 0;
// 等待触摸按压(EV_KEY+BTN_TOUCH+value>0)
while (1) {
read(ts_fd, &ts_event, sizeof(ts_event));
if (ts_event.type == EV_KEY && ts_event.code == BTN_TOUCH && ts_event.value > 0) {
break; // 检测到按压,退出等待
}
}
// 读取按压时的坐标
while (cnt < 2) {
read(ts_fd, &ts_event, sizeof(ts_event));
if (ts_event.type == EV_ABS) {
if (ts_event.code == ABS_X) { ts_x = ts_event.value; cnt++; }
else if (ts_event.code == ABS_Y) { ts_y = ts_event.value; cnt++; }
}
}
ts_to_lcd(ts_x, ts_y, lcd_x, lcd_y); // 转换为LCD坐标
}
/**
* @brief 登录界面(显示“登录”“退出”按钮,处理触摸)
* @return 下一个界面状态(STATE_MAIN/STATE_EXIT)
*/
UI_STATE login_ui(void) {
// 绘制登录界面
lcd_draw_rect(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1, COLOR_WHITE); // 白色背景
// 绘制“登录”按钮(蓝色,300,200 → 500,260)
lcd_draw_rect(300, 200, 500, 260, COLOR_BLUE);
// 绘制“退出”按钮(红色,300,300 → 500,360)
lcd_draw_rect(300, 300, 500, 360, COLOR_RED);
// 提示信息
printf("登录界面:触摸蓝色按钮登录,红色按钮退出\n");
// 读取触摸坐标,判断点击区域
int lcd_x, lcd_y;
ts_read_press(&lcd_x, &lcd_y); // 等待触摸按压
printf("触摸坐标:(%d, %d)\n", lcd_x, lcd_y);
// 判断是否点击“登录”按钮(300≤x≤500,200≤y≤260)
if (lcd_x >= 300 && lcd_x <= 500 && lcd_y >= 200 && lcd_y <= 260) {
printf("点击登录,进入主界面\n");
return STATE_MAIN;
}
// 判断是否点击“退出”按钮(300≤x≤500,300≤y≤360)
else if (lcd_x >= 300 && lcd_x <= 500 && lcd_y >= 300 && lcd_y <= 360) {
printf("点击退出,程序关闭\n");
return STATE_EXIT;
}
// 点击其他区域,重新显示登录界面
else {
printf("点击区域无效,请重新点击\n");
return STATE_LOGIN;
}
}
/**
* @brief 主界面(显示“返回登录”按钮,处理触摸)
* @return 下一个界面状态(STATE_LOGIN)
*/
UI_STATE main_ui(void) {
// 绘制主界面
lcd_draw_rect(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1, COLOR_GREEN); // 绿色背景
// 绘制“返回登录”按钮(白色,300,400 → 500,460)
lcd_draw_rect(300, 400, 500, 460, COLOR_WHITE);
// 提示信息
printf("主界面:触摸白色按钮返回登录界面\n");
// 读取触摸坐标,判断点击区域
int lcd_x, lcd_y;
ts_read_press(&lcd_x, &lcd_y); // 等待触摸按压
printf("触摸坐标:(%d, %d)\n", lcd_x, lcd_y);
// 判断是否点击“返回登录”按钮(300≤x≤500,400≤y≤460)
if (lcd_x >= 300 && lcd_x <= 500 && lcd_y >= 400 && lcd_y <= 460) {
printf("点击返回,回到登录界面\n");
return STATE_LOGIN;
}
// 点击其他区域,保持主界面
else {
printf("点击区域无效,请重新点击\n");
return STATE_MAIN;
}
}
int main(int argc, char const *argv[]) {
// 初始化LCD和触摸屏
if (lcd_init() != 0) { exit(1); }
ts_fd = open("/dev/input/event0", O_RDONLY);
if (ts_fd == -1) { perror("open 触摸屏失败"); exit(1); }
// 状态机主循环(根据当前状态执行对应逻辑)
UI_STATE current_state = STATE_BOOT; // 初始状态:开机动画
while (current_state != STATE_EXIT) {
switch (current_state) {
case STATE_BOOT:
boot_animation();
current_state = STATE_LOGIN; // 动画结束→登录界面
break;
case STATE_LOGIN:
current_state = login_ui(); // 登录界面返回下一个状态
break;
case STATE_MAIN:
current_state = main_ui(); // 主界面返回下一个状态
break;
default:
current_state = STATE_EXIT; // 异常状态→退出
break;
}
}
// 释放资源
munmap(lcd_mem, LCD_WIDTH * LCD_HEIGHT * 4); // 解除LCD映射
close(ts_fd);
printf("程序退出\n");
return 0;
}
注意事项
- 设备文件路径确认:不同开发板的触摸屏设备文件可能不是
event0,可通过cat /proc/bus/input/devices查看,找到 “Touchscreen” 对应的eventX; - 权限问题:
/dev/input/event0默认只有 root 用户可读写,普通用户需用sudo运行程序,或修改权限(sudo chmod 666 /dev/input/event0); - 坐标边界保护:触摸屏硬件可能存在误差,导致原始坐标超出
0~1023范围,转换后需判断 LCD 坐标是否在0~799和0~479内,避免数组越界; - 触摸事件同步:手指滑动时会产生多个
input_event,需通过EV_SYN判断事件组是否完整(简化程序可忽略,但精准处理需关注); - 界面绘制效率:示例中用双重循环画矩形效率较低,实际项目可使用 “显存批量写入” 或硬件加速,避免界面卡顿。

浙公网安备 33010602011771号