《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 20 章 Linux 输入设备驱动
参考:宋宝华 著,机械工业出版社,2015年版
20.1 输入设备驱动简介
20.1.1 输入子系统的概念
Linux 输入子系统(Input Subsystem)是专门为人机交互输入设备设计的驱动框架,统一管理键盘、鼠标、触摸屏、游戏手柄、传感器等各类输入设备:
输入子系统的设计目标:
问题:输入设备种类繁多,接口各异
- 键盘:PS/2、USB、I2C
- 鼠标:PS/2、USB、蓝牙
- 触摸屏:I2C、SPI、USB
- 游戏手柄:USB、蓝牙
解决方案:输入子系统提供统一的框架
✓ 驱动只需上报事件,无需关心如何传递给应用程序
✓ 应用程序通过统一的 /dev/input/eventN 接口读取事件
✓ 支持事件过滤、转换、合并等处理
✓ 支持热插拔(USB 设备插入时自动注册)
20.1.2 输入子系统的层次结构
Linux 输入子系统架构:
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 │
│ 应用程序读取 /dev/input/event0、event1... │
│ Qt / GTK / Android InputReader / evtest │
└──────────────────────────┬──────────────────────────────────┘
│ read(fd, &event, sizeof(event))
┌──────────────────────────▼──────────────────────────────────┐
│ 事件处理层(Event Handler) │
│ evdev(通用事件接口)← 最常用,/dev/input/eventN │
│ mousedev(鼠标接口)← /dev/input/mice │
│ keybdev(键盘接口)← 已废弃 │
│ joydev(游戏手柄接口)← /dev/input/jsN │
└──────────────────────────┬──────────────────────────────────┘
│ input_event()
┌──────────────────────────▼──────────────────────────────────┐
│ 输入核心层(Input Core) │
│ input_register_device() / input_unregister_device() │
│ input_report_key() / input_report_abs() / input_sync() │
│ 事件分发、设备管理 │
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────┐
│ 输入设备驱动层 │
│ 键盘驱动 / 触摸屏驱动 / 鼠标驱动 / 传感器驱动 │
│ 通过 input_dev 结构体向核心层上报事件 │
└──────────────────────────┬──────────────────────────────────┘
│ 操作硬件
┌──────────────────────────▼──────────────────────────────────┐
│ 硬件层 │
│ 键盘矩阵 / 触摸屏控制器 / 鼠标传感器 / 加速度计 │
└─────────────────────────────────────────────────────────────┘
20.1.3 输入事件类型
Linux 输入子系统定义了多种事件类型,每种类型对应不同的输入数据:
c
/* 事件类型(input_event.type)*/
#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 /* LED 事件(键盘 LED)*/
#define EV_SND 0x12 /* 声音事件(蜂鸣器)*/
#define EV_REP 0x14 /* 重复事件(按键重复)*/
#define EV_FF 0x15 /* 力反馈事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 力反馈状态事件 */
/* EV_KEY 事件的 code(按键码)*/
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
/* ... */
#define KEY_ENTER 28
#define KEY_LEFTCTRL 29
#define KEY_A 30
/* ... */
#define BTN_LEFT 0x110 /* 鼠标左键 */
#define BTN_RIGHT 0x111 /* 鼠标右键 */
#define BTN_MIDDLE 0x112 /* 鼠标中键 */
#define BTN_TOUCH 0x14A /* 触摸事件 */
/* EV_ABS 事件的 code(绝对坐标轴)*/
#define ABS_X 0x00 /* X 轴 */
#define ABS_Y 0x01 /* Y 轴 */
#define ABS_Z 0x02 /* Z 轴 */
#define ABS_RX 0x03 /* X 轴旋转 */
#define ABS_RY 0x04 /* Y 轴旋转 */
#define ABS_RZ 0x05 /* Z 轴旋转 */
#define ABS_PRESSURE 0x18 /* 压力 */
#define ABS_MT_SLOT 0x2F /* 多点触控槽位 */
#define ABS_MT_TOUCH_MAJOR 0x30 /* 触摸椭圆长轴 */
#define ABS_MT_POSITION_X 0x35 /* 多点触控 X 坐标 */
#define ABS_MT_POSITION_Y 0x36 /* 多点触控 Y 坐标 */
#define ABS_MT_TRACKING_ID 0x39 /* 触摸点追踪 ID */
/* EV_REL 事件的 code(相对坐标轴)*/
#define REL_X 0x00 /* X 轴相对移动 */
#define REL_Y 0x01 /* Y 轴相对移动 */
#define REL_WHEEL 0x08 /* 滚轮 */
20.1.4 input_event 结构体
用户空间读取到的事件格式:
c
#include <linux/input.h>
/*
* input_event:输入事件结构体
* 用户空间通过 read(/dev/input/eventN) 读取此结构体
*/
struct input_event {
struct timeval time; /* 事件时间戳 */
__u16 type; /* 事件类型(EV_KEY/EV_ABS/EV_REL 等)*/
__u16 code; /* 事件代码(具体的键码或坐标轴)*/
__s32 value; /* 事件值 */
};
/*
* value 的含义:
* EV_KEY:0=释放,1=按下,2=重复
* EV_ABS:绝对坐标值
* EV_REL:相对移动量(正/负)
* EV_SYN:0(SYN_REPORT,标志一组事件结束)
*/
/* 用户空间读取事件示例 */
int fd = open("/dev/input/event0", O_RDONLY);
struct input_event ev;
while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
if (ev.type == EV_KEY) {
printf("按键事件:code=%u, value=%d\n", ev.code, ev.value);
} else if (ev.type == EV_ABS) {
printf("绝对坐标:code=%u, value=%d\n", ev.code, ev.value);
} else if (ev.type == EV_SYN) {
printf("同步事件\n");
}
}
20.2 输入设备驱动的结构
20.2.1 input_dev 结构体
c
#include <linux/input.h>
/*
* input_dev:输入设备描述符
* 每个输入设备对应一个 input_dev
*/
struct input_dev {
const char *name; /* 设备名称 */
const char *phys; /* 设备物理路径 */
const char *uniq; /* 设备唯一标识 */
struct input_id id; /* 设备 ID(bustype/vendor/product/version)*/
/* 支持的事件类型位图 */
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)];
/* 支持的 LED 位图 */
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
/* 支持的开关位图 */
unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
/* 绝对坐标参数 */
struct input_absinfo absinfo[ABS_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 device dev; /* 内嵌设备结构体 */
/* ... */
};
/*
* input_absinfo:绝对坐标轴参数
*/
struct input_absinfo {
__s32 value; /* 当前值 */
__s32 minimum; /* 最小值 */
__s32 maximum; /* 最大值 */
__s32 fuzz; /* 噪声过滤(模糊值)*/
__s32 flat; /* 死区(中心区域)*/
__s32 resolution; /* 分辨率(单位/毫米)*/
};
20.2.2 输入设备驱动的注册流程
输入设备驱动注册流程:
module_init() / probe()
↓
1. input_allocate_device()
← 分配 input_dev 结构体
↓
2. 设置设备信息:
dev->name = "My Input Device"
dev->phys = "i2c/0-0038/input0"
dev->id.bustype = BUS_I2C
↓
3. 设置支持的事件类型:
set_bit(EV_KEY, dev->evbit) ← 支持按键事件
set_bit(EV_ABS, dev->evbit) ← 支持绝对坐标事件
set_bit(EV_SYN, dev->evbit) ← 支持同步事件
↓
4. 设置具体的事件代码:
set_bit(BTN_TOUCH, dev->keybit) ← 支持触摸按键
set_bit(ABS_X, dev->absbit) ← 支持 X 坐标
set_bit(ABS_Y, dev->absbit) ← 支持 Y 坐标
↓
5. 设置绝对坐标范围:
input_set_abs_params(dev, ABS_X, 0, 800, 0, 0)
input_set_abs_params(dev, ABS_Y, 0, 480, 0, 0)
↓
6. input_register_device(dev)
← 注册到内核,创建 /dev/input/eventN
↓
7. 申请中断/启动轮询
← 等待硬件事件
module_exit() / remove()
↓
1. input_unregister_device(dev)
2. input_free_device(dev)
20.2.3 事件上报 API
c
#include <linux/input.h>
/* ── 上报按键事件 ──────────────────────────────────────────── */
/*
* input_report_key(dev, code, value)
* code:按键码(KEY_A、BTN_TOUCH 等)
* value:1=按下,0=释放
*/
input_report_key(dev, KEY_ENTER, 1); /* 按下 Enter 键 */
input_report_key(dev, KEY_ENTER, 0); /* 释放 Enter 键 */
input_report_key(dev, BTN_TOUCH, 1); /* 触摸按下 */
/* ── 上报绝对坐标事件 ──────────────────────────────────────── */
/*
* input_report_abs(dev, code, value)
* code:坐标轴(ABS_X、ABS_Y、ABS_PRESSURE 等)
* value:坐标值
*/
input_report_abs(dev, ABS_X, 320); /* X 坐标 = 320 */
input_report_abs(dev, ABS_Y, 240); /* Y 坐标 = 240 */
input_report_abs(dev, ABS_PRESSURE, 100); /* 压力 = 100 */
/* ── 上报相对坐标事件 ──────────────────────────────────────── */
/*
* input_report_rel(dev, code, value)
* code:坐标轴(REL_X、REL_Y、REL_WHEEL 等)
* value:相对移动量(正/负)
*/
input_report_rel(dev, REL_X, 5); /* X 方向移动 +5 */
input_report_rel(dev, REL_Y, -3); /* Y 方向移动 -3 */
input_report_rel(dev, REL_WHEEL, 1); /* 滚轮向上 */
/* ── 同步事件(必须在一组事件结束后调用)──────────────────── */
/*
* input_sync(dev)
* 通知内核一组事件已完整上报
* 内核将这组事件打包发送给用户空间
*/
input_sync(dev);
/* ── 完整的触摸事件上报示例 ────────────────────────────────── */
/* 触摸按下 */
input_report_key(dev, BTN_TOUCH, 1);
input_report_abs(dev, ABS_X, x);
input_report_abs(dev, ABS_Y, y);
input_report_abs(dev, ABS_PRESSURE, pressure);
input_sync(dev);
/* 触摸移动 */
input_report_abs(dev, ABS_X, new_x);
input_report_abs(dev, ABS_Y, new_y);
input_sync(dev);
/* 触摸释放 */
input_report_key(dev, BTN_TOUCH, 0);
input_report_abs(dev, ABS_PRESSURE, 0);
input_sync(dev);
20.2.4 多点触控(Multi-Touch)协议
Linux 输入子系统支持两种多点触控协议:
多点触控协议 A(Type A):
- 不追踪触摸点,每次上报所有触摸点
- 使用 SYN_MT_REPORT 分隔不同触摸点
- 适合不能追踪触摸点的硬件
多点触控协议 B(Type B):
- 追踪每个触摸点(通过 slot 和 tracking_id)
- 使用 ABS_MT_SLOT 切换当前操作的触摸点
- 适合能追踪触摸点的硬件(如 GT911)
- 推荐使用
协议 B 的事件序列(两个触摸点):
触摸点0按下:
ABS_MT_SLOT = 0
ABS_MT_TRACKING_ID = 1 ← 分配 ID(非负值表示按下)
ABS_MT_POSITION_X = 100
ABS_MT_POSITION_Y = 200
SYN_REPORT
触摸点1按下:
ABS_MT_SLOT = 1
ABS_MT_TRACKING_ID = 2
ABS_MT_POSITION_X = 300
ABS_MT_POSITION_Y = 400
SYN_REPORT
触摸点0移动:
ABS_MT_SLOT = 0
ABS_MT_POSITION_X = 110
ABS_MT_POSITION_Y = 210
SYN_REPORT
触摸点0释放:
ABS_MT_SLOT = 0
ABS_MT_TRACKING_ID = -1 ← -1 表示释放
SYN_REPORT
20.3 触摸屏设备驱动实例
20.3.1 GT911 触摸屏控制器简介
GT911 是汇顶科技(Goodix)的一款电容式多点触控控制器,广泛用于嵌入式系统:
GT911 特性:
- 支持最多 5 点同时触控
- I2C 接口(地址:0x5D 或 0x14)
- 中断引脚(INT):触摸时产生中断
- 复位引脚(RST):硬件复位
- 支持手势识别
- 分辨率:最大 4096×4096
- 工作电压:1.8V/2.8V
GT911 寄存器(部分):
0x8047:产品 ID("911\0")
0x8048:固件版本
0x804A:X 轴分辨率(低字节)
0x804B:X 轴分辨率(高字节)
0x804C:Y 轴分辨率(低字节)
0x804D:Y 轴分辨率(高字节)
0x814E:状态寄存器(触摸点数量)
0x8150:触摸点0数据(8字节)
0x8158:触摸点1数据(8字节)
...
触摸点数据格式(每个触摸点8字节):
Byte 0:触摸点 ID(0~4)
Byte 1:X 坐标低字节
Byte 2:X 坐标高字节
Byte 3:Y 坐标低字节
Byte 4:Y 坐标高字节
Byte 5:触摸面积低字节
Byte 6:触摸面积高字节
Byte 7:保留
20.3.2 完整的 GT911 触摸屏驱动
c
/*
* gt911.c ------ GT911 电容式触摸屏驱动
*
* 支持:
* - 多点触控(最多5点,协议B)
* - 中断驱动
* - 硬件复位
* - 设备树配置
*
* 参考:宋宝华《Linux设备驱动开发详解》第20章
*/
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/input/mt.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/firmware.h>
/* GT911 寄存器地址 */
#define GT911_REG_PRODUCT_ID 0x8140 /* 产品 ID(4字节)*/
#define GT911_REG_FW_VERSION 0x8144 /* 固件版本(2字节)*/
#define GT911_REG_X_MAX 0x8048 /* X 轴最大值(2字节)*/
#define GT911_REG_Y_MAX 0x804A /* Y 轴最大值(2字节)*/
#define GT911_REG_TOUCH_NUM 0x804C /* 最大触摸点数 */
#define GT911_REG_STATUS 0x814E /* 状态寄存器 */
#define GT911_REG_TOUCH_DATA 0x8150 /* 触摸数据起始地址 */
#define GT911_REG_CONFIG 0x8047 /* 配置起始地址 */
/* 状态寄存器位 */
#define GT911_STATUS_BUFFER_READY BIT(7) /* 数据就绪 */
#define GT911_STATUS_TOUCH_NUM 0x0F /* 触摸点数量掩码 */
/* 最大触摸点数 */
#define GT911_MAX_TOUCH 5
/* 触摸点数据结构 */
struct gt911_touch_data {
u8 id; /* 触摸点 ID */
u8 x_low; /* X 坐标低字节 */
u8 x_high; /* X 坐标高字节 */
u8 y_low; /* Y 坐标低字节 */
u8 y_high; /* Y 坐标高字节 */
u8 area_low; /* 触摸面积低字节 */
u8 area_high; /* 触摸面积高字节 */
u8 reserved; /* 保留 */
} __packed;
/* 驱动私有数据 */
struct gt911_ts {
struct i2c_client *client;
struct input_dev *input_dev;
/* GPIO */
int int_gpio; /* 中断 GPIO */
int rst_gpio; /* 复位 GPIO */
int irq; /* 中断号 */
/* 触摸屏参数 */
u16 x_max; /* X 轴最大值 */
u16 y_max; /* Y 轴最大值 */
u8 max_touch; /* 最大触摸点数 */
/* 当前触摸状态 */
bool touch_active[GT911_MAX_TOUCH];
};
/* ── I2C 读写辅助函数 ──────────────────────────────────────── */
static int gt911_i2c_read(struct gt911_ts *ts, u16 reg,
u8 *buf, int len)
{
struct i2c_client *client = ts->client;
u8 addr_buf[2] = { reg >> 8, reg & 0xFF };
struct i2c_msg msgs[2] = {
{
.addr = client->addr,
.flags = 0,
.len = 2,
.buf = addr_buf,
},
{
.addr = client->addr,
.flags = I2C_M_RD,
.len = len,
.buf = buf,
},
};
int ret = i2c_transfer(client->adapter, msgs, 2);
return (ret == 2) ? 0 : (ret < 0 ? ret : -EIO);
}
static int gt911_i2c_write(struct gt911_ts *ts, u16 reg,
const u8 *buf, int len)
{
struct i2c_client *client = ts->client;
u8 *write_buf;
int ret;
write_buf = kmalloc(len + 2, GFP_KERNEL);
if (!write_buf)
return -ENOMEM;
write_buf[0] = reg >> 8;
write_buf[1] = reg & 0xFF;
memcpy(write_buf + 2, buf, len);
ret = i2c_master_send(client, write_buf, len + 2);
kfree(write_buf);
return (ret == len + 2) ? 0 : (ret < 0 ? ret : -EIO);
}
/* ── 硬件复位 ──────────────────────────────────────────────── */
static void gt911_reset(struct gt911_ts *ts)
{
/* 复位序列:
* 1. INT 低,RST 低,等待 10ms
* 2. RST 高,等待 10ms
* 3. INT 高(释放),等待 50ms
*/
gpio_direction_output(ts->int_gpio, 0);
gpio_direction_output(ts->rst_gpio, 0);
msleep(10);
gpio_set_value(ts->rst_gpio, 1);
msleep(10);
/* 设置 I2C 地址(INT 低 = 0x5D,INT 高 = 0x14)*/
/* 这里使用 0x5D(INT 保持低电平)*/
gpio_direction_input(ts->int_gpio);
msleep(50);
}
/* ── 读取触摸数据 ──────────────────────────────────────────── */
static void gt911_read_touch_data(struct gt911_ts *ts)
{
struct input_dev *input = ts->input_dev;
struct gt911_touch_data touch[GT911_MAX_TOUCH];
u8 status;
int touch_num;
int i;
int ret;
/* 读取状态寄存器 */
ret = gt911_i2c_read(ts, GT911_REG_STATUS, &status, 1);
if (ret) {
dev_err(&ts->client->dev, "读取状态寄存器失败\n");
return;
}
/* 检查数据是否就绪 */
if (!(status & GT911_STATUS_BUFFER_READY))
return;
/* 获取触摸点数量 */
touch_num = status & GT911_STATUS_TOUCH_NUM;
if (touch_num > GT911_MAX_TOUCH)
touch_num = GT911_MAX_TOUCH;
/* 读取触摸点数据 */
if (touch_num > 0) {
ret = gt911_i2c_read(ts, GT911_REG_TOUCH_DATA,
(u8 *)touch,
touch_num * sizeof(struct gt911_touch_data));
if (ret) {
dev_err(&ts->client->dev, "读取触摸数据失败\n");
goto clear_status;
}
}
/* 处理触摸点数据(多点触控协议 B)*/
for (i = 0; i < touch_num; i++) {
int id = touch[i].id;
int x = touch[i].x_low | (touch[i].x_high << 8);
int y = touch[i].y_low | (touch[i].y_high << 8);
int area = touch[i].area_low | (touch[i].area_high << 8);
if (id >= GT911_MAX_TOUCH)
continue;
/* 切换到对应的触摸槽位 */
input_mt_slot(input, id);
/* 上报触摸点按下 */
input_mt_report_slot_state(input, MT_TOOL_FINGER, true);
input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
input_report_abs(input, ABS_MT_TOUCH_MAJOR, area);
ts->touch_active[id] = true;
}
/* 处理已释放的触摸点 */
for (i = 0; i < GT911_MAX_TOUCH; i++) {
if (!ts->touch_active[i])
continue;
/* 检查此触摸点是否在当前数据中 */
bool found = false;
int j;
for (j = 0; j < touch_num; j++) {
if (touch[j].id == i) {
found = true;
break;
}
}
if (!found) {
/* 触摸点已释放 */
input_mt_slot(input, i);
input_mt_report_slot_state(input, MT_TOOL_FINGER, false);
ts->touch_active[i] = false;
}
}
/* 上报单点触控兼容事件 */
input_mt_report_pointer_emulation(input, true);
/* 同步事件 */
input_sync(input);
clear_status:
/* 清除状态寄存器(写 0 清除 buffer ready 标志)*/
status = 0;
gt911_i2c_write(ts, GT911_REG_STATUS, &status, 1);
}
/* ── 中断处理函数 ──────────────────────────────────────────── */
static irqreturn_t gt911_irq_handler(int irq, void *dev_id)
{
struct gt911_ts *ts = dev_id;
gt911_read_touch_data(ts);
return IRQ_HANDLED;
}
/* ── 初始化触摸屏 ──────────────────────────────────────────── */
static int gt911_init_device(struct gt911_ts *ts)
{
u8 buf[6];
int ret;
/* 读取产品 ID */
ret = gt911_i2c_read(ts, GT911_REG_PRODUCT_ID, buf, 4);
if (ret) {
dev_err(&ts->client->dev, "读取产品 ID 失败\n");
return ret;
}
buf[4] = '\0';
dev_info(&ts->client->dev, "产品 ID:%s\n", buf);
/* 读取固件版本 */
ret = gt911_i2c_read(ts, GT911_REG_FW_VERSION, buf, 2);
if (ret == 0) {
dev_info(&ts->client->dev, "固件版本:0x%04X\n",
buf[0] | (buf[1] << 8));
}
/* 读取 X 轴最大值 */
ret = gt911_i2c_read(ts, GT911_REG_X_MAX, buf, 2);
if (ret) return ret;
ts->x_max = buf[0] | (buf[1] << 8);
/* 读取 Y 轴最大值 */
ret = gt911_i2c_read(ts, GT911_REG_Y_MAX, buf, 2);
if (ret) return ret;
ts->y_max = buf[0] | (buf[1] << 8);
/* 读取最大触摸点数 */
ret = gt911_i2c_read(ts, GT911_REG_TOUCH_NUM, buf, 1);
if (ret) return ret;
ts->max_touch = min_t(u8, buf[0], GT911_MAX_TOUCH);
dev_info(&ts->client->dev, "分辨率:%u×%u,最大触摸点:%u\n",
ts->x_max, ts->y_max, ts->max_touch);
return 0;
}
/* ── probe 函数 ──────────────────────────────────────────────── */
static int gt911_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct gt911_ts *ts;
struct input_dev *input;
struct device_node *np = client->dev.of_node;
int ret;
/* 分配私有数据 */
ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
if (!ts)
return -ENOMEM;
ts->client = client;
i2c_set_clientdata(client, ts);
/* 从设备树获取 GPIO */
ts->int_gpio = of_get_named_gpio(np, "interrupt-gpios", 0);
ts->rst_gpio = of_get_named_gpio(np, "reset-gpios", 0);
if (!gpio_is_valid(ts->int_gpio) || !gpio_is_valid(ts->rst_gpio)) {
dev_err(&client->dev, "无效的 GPIO\n");
return -EINVAL;
}
/* 申请 GPIO */
ret = devm_gpio_request(&client->dev, ts->int_gpio, "gt911-int");
if (ret) return ret;
ret = devm_gpio_request(&client->dev, ts->rst_gpio, "gt911-rst");
if (ret) return ret;
/* 硬件复位 */
gt911_reset(ts);
/* 初始化设备,读取参数 */
ret = gt911_init_device(ts);
if (ret) {
dev_err(&client->dev, "初始化 GT911 失败\n");
return ret;
}
/* 分配 input_dev */
input = devm_input_allocate_device(&client->dev);
if (!input)
return -ENOMEM;
ts->input_dev = input;
/* 设置设备信息 */
input->name = "GT911 Touchscreen";
input->phys = "i2c/gt911";
input->id.bustype = BUS_I2C;
input->id.vendor = 0x0416; /* Goodix */
input->id.product = 0x0911;
input->id.version = 0x0001;
/* 设置支持的事件类型 */
set_bit(EV_SYN, input->evbit);
set_bit(EV_KEY, input->evbit);
set_bit(EV_ABS, input->evbit);
/* 设置按键事件 */
set_bit(BTN_TOUCH, input->keybit);
/* 设置绝对坐标参数(单点触控兼容)*/
input_set_abs_params(input, ABS_X, 0, ts->x_max, 0, 0);
input_set_abs_params(input, ABS_Y, 0, ts->y_max, 0, 0);
input_set_abs_params(input, ABS_PRESSURE, 0, 255, 0, 0);
/* 初始化多点触控(协议 B)*/
ret = input_mt_init_slots(input, ts->max_touch,
INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
if (ret) {
dev_err(&client->dev, "初始化多点触控失败\n");
return ret;
}
/* 设置多点触控坐标参数 */
input_set_abs_params(input, ABS_MT_POSITION_X, 0, ts->x_max, 0, 0);
input_set_abs_params(input, ABS_MT_POSITION_Y, 0, ts->y_max, 0, 0);
input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
/* 注册 input_dev */
ret = input_register_device(input);
if (ret) {
dev_err(&client->dev, "注册 input_dev 失败\n");
return ret;
}
/* 获取中断号并申请中断 */
ts->irq = gpio_to_irq(ts->int_gpio);
ret = devm_request_threaded_irq(&client->dev, ts->irq,
NULL,
gt911_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"gt911", ts);
if (ret) {
dev_err(&client->dev, "申请中断失败\n");
return ret;
}
dev_info(&client->dev, "GT911 触摸屏驱动加载成功\n");
return 0;
}
static int gt911_remove(struct i2c_client *client)
{
dev_info(&client->dev, "GT911 驱动卸载\n");
return 0;
}
/* ── 设备 ID 表 ──────────────────────────────────────────────── */
static const struct i2c_device_id gt911_id[] = {
{ "gt911", 0 },
{ "gt9271", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, gt911_id);
static const struct of_device_id gt911_of_match[] = {
{ .compatible = "goodix,gt911" },
{ .compatible = "goodix,gt9271" },
{}
};
MODULE_DEVICE_TABLE(of, gt911_of_match);
static struct i2c_driver gt911_driver = {
.driver = {
.name = "gt911",
.of_match_table = gt911_of_match,
},
.probe = gt911_probe,
.remove = gt911_remove,
.id_table = gt911_id,
};
module_i2c_driver(gt911_driver);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("GT911 电容式触摸屏驱动");
20.3.3 设备树配置
dts
/* 设备树中的 GT911 触摸屏配置 */
&i2c1 {
clock-frequency = <400000>;
status = "okay";
gt911: touchscreen@5d {
compatible = "goodix,gt911";
reg = <0x5d>; /* I2C 地址 */
/* 中断和复位 GPIO */
interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
/* 中断配置 */
interrupt-parent = <&gpio1>;
interrupts = <9 IRQ_TYPE_EDGE_FALLING>;
/* 触摸屏分辨率(可选,驱动会自动读取)*/
touchscreen-size-x = <800>;
touchscreen-size-y = <480>;
/* 触摸屏方向(可选)*/
/* touchscreen-inverted-x; */
/* touchscreen-inverted-y; */
/* touchscreen-swapped-x-y; */
};
};
20.3.4 触摸屏驱动测试
bash
# 查看输入设备
cat /proc/bus/input/devices
# I: Bus=0018 Vendor=0416 Product=0911 Version=0001
# N: Name="GT911 Touchscreen"
# P: Phys=i2c/gt911
# S: Sysfs=/devices/platform/soc/2028000.i2c/i2c-1/1-005d/input/input0
# U: Uniq=
# H: Handlers=event0
# B: EV=b
# B: KEY=400 0 0 0 0 0 0 0 0 0 0
# B: ABS=2658000 3
# 使用 evtest 测试触摸事件
sudo evtest /dev/input/event0
# Input driver version is 1.0.1
# Input device ID: bus 0x18 vendor 0x416 product 0x911 version 0x1
# Input device name: "GT911 Touchscreen"
# Supported events:
# Event type 0 (EV_SYN)
# Event type 1 (EV_KEY)
# Event code 330 (BTN_TOUCH)
# Event type 3 (EV_ABS)
# Event code 0 (ABS_X)
# Value 0
# Min 0
# Max 800
# Event code 1 (ABS_Y)
# Value 0
# Min 0
# Max 480
# Event code 53 (ABS_MT_POSITION_X)
# Event code 54 (ABS_MT_POSITION_Y)
# Testing ... (interrupt to exit)
# Event: time 1234567890.123456, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 320
# Event: time 1234567890.123456, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 240
# Event: time 1234567890.123456, type 1 (EV_KEY), code 330 (BTN_TOUCH), value 1
# Event: time 1234567890.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0
# 使用 ts_calibrate 校准触摸屏
ts_calibrate
# 使用 ts_test 测试
ts_test
20.4 键盘驱动
20.4.1 键盘驱动的类型
Linux 键盘驱动类型:
1. 矩阵键盘(Matrix Keyboard)
- 按键排列成行列矩阵
- 通过扫描行列检测按键
- 常用于嵌入式系统(如 4×4 数字键盘)
- Linux 提供 matrix_keypad 驱动框架
2. GPIO 按键(GPIO Keys)
- 每个按键连接一个 GPIO
- 通过 GPIO 中断检测按键
- Linux 提供 gpio-keys 驱动(无需编写代码)
3. USB 键盘
- 通过 USB HID 协议通信
- Linux 的 usbhid 驱动自动处理
4. PS/2 键盘
- 通过 PS/2 接口通信
- Linux 的 atkbd 驱动处理
5. I2C/SPI 键盘
- 通过 I2C 或 SPI 接口的键盘控制器
- 需要编写专用驱动
20.4.2 GPIO 按键驱动(gpio-keys)
Linux 内核提供了 gpio-keys 驱动,通过设备树配置即可使用,无需编写代码:
dts
/* 设备树中配置 GPIO 按键 */
/ {
gpio-keys {
compatible = "gpio-keys";
autorepeat; /* 使能按键重复 */
/* 按键1:音量加 */
key-volume-up {
label = "Volume Up";
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>; /* 低电平有效 */
linux,code = <KEY_VOLUMEUP>; /* 按键码 */
debounce-interval = <20>; /* 去抖时间(ms)*/
};
/* 按键2:音量减 */
key-volume-down {
label = "Volume Down";
gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
linux,code = <KEY_VOLUMEDOWN>;
debounce-interval = <20>;
};
/* 按键3:电源键 */
key-power {
label = "Power";
gpios = <&gpio5 1 GPIO_ACTIVE_LOW>;
linux,code = <KEY_POWER>;
gpio-key,wakeup; /* 可以唤醒系统 */
};
/* 按键4:用户自定义按键 */
key-user {
label = "User Key";
gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
linux,code = <KEY_F1>;
};
};
};
20.4.3 矩阵键盘驱动
c
/*
* matrix_keypad_simple.c ------ 简单的矩阵键盘驱动
* 实现一个 4×4 矩阵键盘(16个按键)
*
* 硬件连接:
* 行(ROW):GPIO1_IO00 ~ GPIO1_IO03(输出)
* 列(COL):GPIO1_IO04 ~ GPIO1_IO07(输入,上拉)
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#define ROWS 4
#define COLS 4
#define KEYS (ROWS * COLS)
/* 键盘映射表(行×列 → 按键码)*/
static const unsigned int keymap[ROWS][COLS] = {
{ KEY_1, KEY_2, KEY_3, KEY_F1 }, /* 第0行 */
{ KEY_4, KEY_5, KEY_6, KEY_F2 }, /* 第1行 */
{ KEY_7, KEY_8, KEY_9, KEY_F3 }, /* 第2行 */
{ KEY_STAR, KEY_0, KEY_POUND, KEY_F4 }, /* 第3行 */
};
/* 驱动私有数据 */
struct matrix_keypad {
struct input_dev *input_dev;
int row_gpios[ROWS]; /* 行 GPIO */
int col_gpios[COLS]; /* 列 GPIO */
int col_irqs[COLS]; /* 列中断号 */
struct delayed_work scan_work; /* 扫描工作 */
unsigned int last_key_state; /* 上次按键状态 */
spinlock_t lock;
};
/* ── 键盘扫描函数 ──────────────────────────────────────────── */
static unsigned int matrix_scan(struct matrix_keypad *keypad)
{
unsigned int key_state = 0;
int row, col;
for (row = 0; row < ROWS; row++) {
/* 拉低当前行 */
gpio_set_value(keypad->row_gpios[row], 0);
udelay(10); /* 等待信号稳定 */
/* 扫描所有列 */
for (col = 0; col < COLS; col++) {
if (!gpio_get_value(keypad->col_gpios[col])) {
/* 列为低电平,说明此位置有按键按下 */
key_state |= BIT(row * COLS + col);
}
}
/* 恢复行为高电平 */
gpio_set_value(keypad->row_gpios[row], 1);
}
return key_state;
}
/* ── 扫描工作处理函数 ──────────────────────────────────────── */
static void matrix_scan_work(struct work_struct *work)
{
struct matrix_keypad *keypad = container_of(work,
struct matrix_keypad,
scan_work.work);
struct input_dev *input = keypad->input_dev;
unsigned int key_state, changed;
unsigned long flags;
int i;
spin_lock_irqsave(&keypad->lock, flags);
/* 扫描当前按键状态 */
key_state = matrix_scan(keypad);
/* 找出状态变化的按键 */
changed = key_state ^ keypad->last_key_state;
/* 上报变化的按键事件 */
for (i = 0; i < KEYS; i++) {
if (changed & BIT(i)) {
int row = i / COLS;
int col = i % COLS;
int pressed = (key_state >> i) & 1;
input_report_key(input, keymap[row][col], pressed);
pr_debug("matrix_keypad: 按键[%d][%d]=%s\n",
row, col, pressed ? "按下" : "释放");
}
}
if (changed)
input_sync(input);
keypad->last_key_state = key_state;
/* 如果还有按键按下,继续扫描 */
if (key_state) {
schedule_delayed_work(&keypad->scan_work,
msecs_to_jiffies(20)); /* 20ms 后再次扫描 */
} else {
/* 所有按键释放,重新使能列中断 */
int col;
for (col = 0; col < COLS; col++)
enable_irq(keypad->col_irqs[col]);
}
spin_unlock_irqrestore(&keypad->lock, flags);
}
/* ── 列中断处理函数 ──────────────────────────────────────────── */
static irqreturn_t matrix_col_irq_handler(int irq, void *dev_id)
{
struct matrix_keypad *keypad = dev_id;
int col;
/* 禁止所有列中断(防止重复触发)*/
for (col = 0; col < COLS; col++)
disable_irq_nosync(keypad->col_irqs[col]);
/* 调度扫描工作(延迟 10ms 去抖)*/
schedule_delayed_work(&keypad->scan_work, msecs_to_jiffies(10));
return IRQ_HANDLED;
}
/* ── probe 函数 ──────────────────────────────────────────────── */
static int matrix_keypad_probe(struct platform_device *pdev)
{
struct matrix_keypad *keypad;
struct input_dev *input;
struct device_node *np = pdev->dev.of_node;
int i, ret;
keypad = devm_kzalloc(&pdev->dev, sizeof(*keypad), GFP_KERNEL);
if (!keypad)
return -ENOMEM;
spin_lock_init(&keypad->lock);
INIT_DELAYED_WORK(&keypad->scan_work, matrix_scan_work);
/* 从设备树获取行 GPIO */
for (i = 0; i < ROWS; i++) {
keypad->row_gpios[i] = of_get_named_gpio(np, "row-gpios", i);
if (!gpio_is_valid(keypad->row_gpios[i]))
return -EINVAL;
ret = devm_gpio_request_one(&pdev->dev, keypad->row_gpios[i],
GPIOF_OUT_INIT_HIGH, "keypad-row");
if (ret) return ret;
}
/* 从设备树获取列 GPIO */
for (i = 0; i < COLS; i++) {
keypad->col_gpios[i] = of_get_named_gpio(np, "col-gpios", i);
if (!gpio_is_valid(keypad->col_gpios[i]))
return -EINVAL;
ret = devm_gpio_request_one(&pdev->dev, keypad->col_gpios[i],
GPIOF_IN, "keypad-col");
if (ret) return ret;
/* 获取列中断号 */
keypad->col_irqs[i] = gpio_to_irq(keypad->col_gpios[i]);
/* 申请列中断(下降沿触发)*/
ret = devm_request_irq(&pdev->dev, keypad->col_irqs[i],
matrix_col_irq_handler,
IRQF_TRIGGER_FALLING,
"keypad-col", keypad);
if (ret) return ret;
}
/* 分配 input_dev */
input = devm_input_allocate_device(&pdev->dev);
if (!input)
return -ENOMEM;
keypad->input_dev = input;
/* 设置设备信息 */
input->name = "Matrix Keypad";
input->phys = "gpio/matrix-keypad";
input->id.bustype = BUS_HOST;
/* 设置支持的事件类型 */
set_bit(EV_KEY, input->evbit);
set_bit(EV_REP, input->evbit); /* 支持按键重复 */
/* 设置支持的按键 */
for (i = 0; i < ROWS; i++) {
int j;
for (j = 0; j < COLS; j++)
set_bit(keymap[i][j], input->keybit);
}
/* 注册 input_dev */
ret = input_register_device(input);
if (ret) return ret;
platform_set_drvdata(pdev, keypad);
dev_info(&pdev->dev, "矩阵键盘驱动加载成功(%d×%d)\n", ROWS, COLS);
return 0;
}
static int matrix_keypad_remove(struct platform_device *pdev)
{
struct matrix_keypad *keypad = platform_get_drvdata(pdev);
cancel_delayed_work_sync(&keypad->scan_work);
return 0;
}
static const struct of_device_id matrix_keypad_of_match[] = {
{ .compatible = "myvendor,matrix-keypad" },
{}
};
MODULE_DEVICE_TABLE(of, matrix_keypad_of_match);
static struct platform_driver matrix_keypad_driver = {
.probe = matrix_keypad_probe,
.remove = matrix_keypad_remove,
.driver = {
.name = "matrix-keypad",
.of_match_table = matrix_keypad_of_match,
},
};
module_platform_driver(matrix_keypad_driver);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("矩阵键盘驱动");
矩阵键盘的设备树配置:
dts
/* 4×4 矩阵键盘设备树配置 */
/ {
matrix-keypad {
compatible = "myvendor,matrix-keypad";
/* 行 GPIO(输出)*/
row-gpios = <&gpio1 0 GPIO_ACTIVE_LOW>,
<&gpio1 1 GPIO_ACTIVE_LOW>,
<&gpio1 2 GPIO_ACTIVE_LOW>,
<&gpio1 3 GPIO_ACTIVE_LOW>;
/* 列 GPIO(输入,上拉)*/
col-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>,
<&gpio1 5 GPIO_ACTIVE_LOW>,
<&gpio1 6 GPIO_ACTIVE_LOW>,
<&gpio1 7 GPIO_ACTIVE_LOW>;
/* 去抖时间(ms)*/
debounce-delay-ms = <10>;
/* 按键映射 */
linux,keymap = <
MATRIX_KEY(0, 0, KEY_1)
MATRIX_KEY(0, 1, KEY_2)
MATRIX_KEY(0, 2, KEY_3)
MATRIX_KEY(0, 3, KEY_F1)
MATRIX_KEY(1, 0, KEY_4)
MATRIX_KEY(1, 1, KEY_5)
MATRIX_KEY(1, 2, KEY_6)
MATRIX_KEY(1, 3, KEY_F2)
MATRIX_KEY(2, 0, KEY_7)
MATRIX_KEY(2, 1, KEY_8)
MATRIX_KEY(2, 2, KEY_9)
MATRIX_KEY(2, 3, KEY_F3)
MATRIX_KEY(3, 0, KEY_STAR)
MATRIX_KEY(3, 1, KEY_0)
MATRIX_KEY(3, 2, KEY_POUND)
MATRIX_KEY(3, 3, KEY_F4)
>;
};
};
20.4.4 键盘驱动测试
bash
# 查看输入设备
cat /proc/bus/input/devices
# I: Bus=0019 Vendor=0001 Product=0001 Version=0100
# N: Name="Matrix Keypad"
# P: Phys=gpio/matrix-keypad
# H: Handlers=kbd event1
# B: EV=100003
# B: KEY=...
# 使用 evtest 测试按键事件
sudo evtest /dev/input/event1
# Testing ... (interrupt to exit)
# Event: time ..., type 1 (EV_KEY), code 2 (KEY_1), value 1 ← 按下
# Event: time ..., type 0 (EV_SYN), code 0 (SYN_REPORT), value 0
# Event: time ..., type 1 (EV_KEY), code 2 (KEY_1), value 0 ← 释放
# Event: time ..., type 0 (EV_SYN), code 0 (SYN_REPORT), value 0
# 使用 showkey 查看按键码
showkey
# 使用 xev 在 X11 环境下测试
xev
# 查看 GPIO 按键状态
cat /sys/class/input/input1/name
# Matrix Keypad
# 使用 hexdump 查看原始事件数据
hexdump -C /dev/input/event1
# 00000000 xx xx xx xx xx xx xx xx 02 00 01 00 01 00 00 00 |................|
# 时间戳(8B) + type(2B) + code(2B) + value(4B)
本章小结
| 章节 | 核心知识点 | 关键 API |
|---|---|---|
| 20.1 输入设备驱动简介 | 输入子系统设计目标;完整层次架构图(用户空间→事件处理层→输入核心→驱动层→硬件);事件类型(EV_KEY/EV_ABS/EV_REL/EV_SYN);按键码/坐标轴码;input_event结构体;用户空间读取示例 | /dev/input/eventN、evtest |
| 20.2 输入设备驱动结构 | input_dev/input_absinfo核心结构体;注册流程(input_allocate_device→set_bit→input_set_abs_params→input_register_device);事件上报API(input_report_key/abs/rel/sync);多点触控协议A/B详解 | input_allocate_device()、input_register_device()、input_report_key()、input_sync() |
| 20.3 触摸屏设备驱动实例 | GT911特性(5点触控/I2C/中断/复位);I2C读写辅助函数;硬件复位序列;完整GT911驱动(init/中断处理/多点触控协议B/input_mt_slot);设备树配置;evtest测试 | input_mt_init_slots()、input_mt_slot()、input_mt_report_slot_state() |
| 20.4 键盘驱动 | 键盘驱动类型(矩阵/GPIO/USB/PS2);gpio-keys设备树配置(无需编写代码);完整矩阵键盘驱动(行列扫描/列中断/去抖/延迟工作);矩阵键盘设备树配置;showkey/evtest测试 | gpio-keys、MATRIX_KEY()、schedule_delayed_work() |
输入设备驱动开发要点
1. 事件类型和代码的正确设置
使用 set_bit 设置支持的事件类型和代码
触摸屏:EV_KEY(BTN_TOUCH) + EV_ABS(ABS_X/Y)
键盘:EV_KEY + EV_REP(按键重复)
鼠标:EV_KEY(BTN_LEFT/RIGHT) + EV_REL(REL_X/Y)
2. 绝对坐标范围的设置
input_set_abs_params 必须设置正确的 min/max
fuzz:噪声过滤(触摸屏通常设为 0)
flat:死区(摇杆中心区域)
3. 多点触控协议 B 的使用
input_mt_init_slots 初始化槽位
input_mt_slot 切换当前槽位
input_mt_report_slot_state 上报触摸状态
input_mt_report_pointer_emulation 兼容单点触控
4. 中断处理
触摸屏:使用 threaded_irq(可以睡眠,进行 I2C 读取)
键盘:使用普通中断 + 工作队列去抖
5. 去抖处理
硬件去抖:RC 滤波电路
软件去抖:延迟工作(delayed_work)
6. 调试工具
evtest:查看原始事件
showkey:查看按键码
cat /proc/bus/input/devices:查看设备信息
hexdump /dev/input/eventN:查看原始数据
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年