I2C多点触控驱动开发详解
I2C多点触控驱动开发详解
1. 多点触控技术概述
1.1 触控技术发展历程
触控技术作为人机交互的重要方式,经历了从单点触控到多点触控的演进过程。早期的电阻式触控屏只能实现单点触控,限制了用户体验。随着电容式触控技术的发展,多点触控成为可能,极大地丰富了人机交互方式。
在嵌入式Linux系统中,多点触控技术的应用日益广泛,从智能手机、平板电脑到工业控制面板,都离不开多点触控的支持。GT9147作为一款高性能的电容式触控芯片,为嵌入式设备提供了可靠的多点触控解决方案。
1.2 GT9147芯片特性
GT9147是Goodix公司推出的一款电容式触控控制器芯片,具有以下主要特性:
- 高精度触控检测:支持5点同时触控,提供精确的触控坐标信息
- 低功耗设计:多种工作模式可选,满足不同功耗需求
- I2C通信接口:采用标准I2C协议进行数据通信,简化硬件连接
- 智能手势识别:支持多种手势识别功能,提升用户体验
- 抗干扰能力强:内置噪声抑制算法,提高触控稳定性
GT9147通过I2C接口与主控芯片通信,将触控面板的原始数据转换为数字信号,再通过I2C总线传输给主控芯片进行处理。
1.3 多点触控系统架构
在Linux系统中,多点触控系统的架构分为硬件层、驱动层和应用层三个层次:
+------------------+
| 应用程序层 |
| (触摸事件处理) |
+------------------+
| 输入子系统层 |
| (事件分发处理) |
+------------------+
| GT9147驱动层 |
| (硬件抽象与控制) |
+------------------+
| 硬件层 |
| (触控面板与IC) |
+------------------+
驱动层作为连接硬件和系统的关键部分,负责将硬件的原始数据转换为系统可识别的输入事件,并通过Linux输入子系统向上层提供统一的接口。
2. I2C总线协议基础
2.1 I2C总线工作原理
I2C(Inter-Integrated Circuit)总线是一种串行通信协议,由Philips公司开发,用于连接低速外围设备。I2C总线采用两线制设计,包括:
- SDA(Serial Data Line):串行数据线,用于传输数据
- SCL(Serial Clock Line):串行时钟线,用于同步数据传输
I2C总线支持多主多从架构,通过设备地址进行设备寻址。在Linux系统中,I2C总线由I2C适配器(Adapter)管理,每个连接到总线的设备都有唯一的7位或10位地址。
2.2 I2C通信时序
I2C通信遵循严格的时序要求,主要包括以下几个阶段:
- 起始条件(Start Condition):SCL保持高电平,SDA从高电平变为低电平
- 地址传输:发送7位或10位设备地址和读写位
- 应答信号(ACK):接收方在第9个时钟周期拉低SDA线表示应答
- 数据传输:按字节传输数据,每个字节后跟随应答信号
- 停止条件(Stop Condition):SCL保持高电平,SDA从低电平变为高电平
// I2C消息结构体定义
struct i2c_msg {__u16 addr; // 设备地址__u16 flags; // 读写标志__u16 len; // 数据长度__u8 *buf; // 数据缓冲区
};
2.3 Linux I2C子系统架构
Linux I2C子系统采用分层架构设计,主要包括:
- I2C核心层:提供I2C总线操作的通用接口
- I2C适配器层:实现具体I2C控制器的硬件操作
- I2C设备驱动层:实现特定I2C设备的驱动逻辑
这种分层设计使得I2C设备驱动可以独立于具体的硬件平台,提高了代码的可移植性和复用性。
3. GT9147驱动框架分析
3.1 驱动模块初始化
GT9147驱动作为Linux内核模块,遵循标准的模块初始化和退出流程。驱动的入口函数gt9147_init
和出口函数gt9147_exit
分别完成模块的注册和注销。
static int __init gt9147_init(void)
{int ret = 0;ret = i2c_add_driver(>9147_driver);if (ret < 0){ret = -EINVAL;goto fail_i2c_add;}return 0;
}static void __exit gt9147_exit(void)
{i2c_del_driver(>9147_driver);
}
i2c_add_driver
函数将I2C驱动注册到I2C核心,当系统检测到匹配的I2C设备时,会自动调用驱动的probe
函数进行设备初始化。
3.2 I2C驱动结构体
I2C驱动的核心是i2c_driver
结构体,它定义了驱动的基本属性和操作方法:
struct i2c_driver gt9147_driver = {.probe = gt9147_probe,.remove = gt9147_remove,.id_table = gt9147_i2c_id,.driver = {.name = "gt9147",.owner = THIS_MODULE,.of_match_table = of_match_ptr(gt9147_of_id),},
};
其中:
probe
:设备探测函数,在设备匹配时调用remove
:设备移除函数,在设备卸载时调用id_table
:传统设备匹配表of_match_table
:设备树匹配表
3.3 设备树匹配机制
现代Linux系统广泛采用设备树(Device Tree)来描述硬件配置。GT9147驱动通过设备树匹配表与硬件设备关联:
gt9147: gt9147@14 {compatible = "goodix,gt9147", "goodix,gt9xx";reg = <0x14>;interrupt-parent = <&gpio1>;interrupts = <9 0>;reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;status = "okay";
};
驱动中的匹配表:
static struct of_device_id gt9147_of_id[] = {{.compatible = "goodix,gt9147",},{/* sentinel */}
};
当设备树中的compatible
属性与驱动中的of_match_table
匹配时,内核会调用驱动的probe
函数初始化设备。
4. GT9147硬件初始化
4.1 引脚资源获取
在设备初始化过程中,首先需要获取设备所需的GPIO资源。GT9147需要两个关键引脚:中断引脚和复位引脚。
gt9147dev.irq_pin = of_get_named_gpio(client->dev.of_node, "interrupt-gpios", 0);
gt9147dev.reset_pin = of_get_named_gpio(client->dev.of_node, "reset-gpios", 0);
of_get_named_gpio
函数从设备树中解析指定名称的GPIO引脚编号,为后续的GPIO操作做准备。
4.2 硬件复位时序
GT9147芯片需要按照特定的时序进行硬件复位,确保芯片处于已知的初始状态:
gpio_set_value(dev->reset_pin, 0); /* 复位GT9147 */
msleep(20);
gpio_set_value(dev->reset_pin, 1); /* 停止复位GT9147 */
msleep(50);
gpio_set_value(dev->irq_pin, 0); /* 拉低INT引脚 */
msleep(50);
gpio_direction_input(dev->irq_pin); /* INT引脚设置为输入 */
这个复位时序严格遵循GT9147芯片手册的要求,确保芯片能够正确初始化。
4.3 软件复位操作
在硬件复位完成后,还需要进行软件复位,通过I2C接口向GT9147的控制寄存器写入特定值:
data = 0x02;
gt9147_write_regs(>9147dev, GT_CTRL_REG, &data, 1); /* 软复位 */
mdelay(100);
data = 0x00;
gt9147_write_regs(>9147dev, GT_CTRL_REG, &data, 1); /* 停止软复位 */
mdelay(100);
软件复位确保芯片内部的寄存器和状态机都恢复到默认状态,为后续的配置做好准备。
5. I2C通信实现
5.1 I2C读操作实现
GT9147驱动通过gt9147_read_regs
函数实现I2C读操作。该函数采用I2C消息传输机制,分两步完成读取操作:
static int gt9147_read_regs(struct gt9147_dev *dev, u16 reg, void *val, int len)
{u8 regdata[2];struct i2c_client *client = dev->client;struct i2c_msg msg[2];regdata[0] = reg >> 8;regdata[1] = reg & 0xFF;msg[0].addr = client->addr;msg[0].flags = 0;msg[0].buf = ®data[0];msg[0].len = 2;msg[1].addr = client->addr;msg[1].flags = I2C_M_RD;msg[1].buf = val;msg[1].len = len;return i2c_transfer(client->adapter, msg, 2);
}
读操作流程:
- 第一个消息:发送要读取的寄存器地址
- 第二个消息:读取指定长度的数据
5.2 I2C写操作实现
写操作通过gt9147_write_regs
函数实现,将寄存器地址和数据打包发送:
static int gt9147_write_regs(struct gt9147_dev *dev, u16 reg, u8 *buf, int len)
{u8 b[256];struct i2c_msg msg;struct i2c_client *client;client = dev->client;b[0] = reg >> 8;b[1] = reg & 0XFF;memcpy(&b[1], buf, len);msg.addr = client->addr;msg.flags = 0;msg.buf = b;msg.len = len + 2;return i2c_transfer(client->adapter, &msg, 1);
}
写操作将寄存器地址和数据合并为一个连续的数据包,通过单次I2C传输完成。
5.3 通信错误处理
I2C通信可能因各种原因失败,驱动需要实现适当的错误处理机制:
ret = i2c_transfer(client->adapter, msg, 2);
if (ret != 2) {dev_err(&client->dev, "I2C transfer failed: %d\n", ret);return -EREMOTEIO;
}
常见的错误包括:
- 设备未响应(NACK)
- 总线忙
- 通信超时
- 数据校验错误
6. 输入子系统集成
6.1 输入设备分配
GT9147驱动使用Linux输入子系统向上层提供统一的输入接口。首先需要分配输入设备结构体:
gt9147dev.input = devm_input_allocate_device(&client->dev);
if (gt9147dev.input == NULL)
{goto fail_probe;
}
devm_input_allocate_device
函数分配输入设备结构体,并自动管理其生命周期。
6.2 输入事件类型配置
配置输入设备支持的事件类型和按键类型:
__set_bit(EV_SYN, gt9147dev.input->evbit);
__set_bit(EV_KEY, gt9147dev.input->evbit);
__set_bit(EV_ABS, gt9147dev.input->evbit);
__set_bit(BTN_TOUCH, gt9147dev.input->keybit);
这里配置了三种事件类型:
EV_SYN
:同步事件,用于标记事件包的结束EV_KEY
:按键事件,用于报告触摸状态EV_ABS
:绝对坐标事件,用于报告触摸坐标
6.3 绝对坐标参数设置
设置触摸屏的坐标范围参数:
gt9147dev.max_x = 480;
gt9147dev.max_y = 272;input_set_abs_params(gt9147dev.input, ABS_X, 0, gt9147dev.max_x, 0, 0);
input_set_abs_params(gt9147dev.input, ABS_Y, 0, gt9147dev.max_y, 0, 0);
input_set_abs_params(gt9147dev.input, ABS_MT_POSITION_X, 0, gt9147dev.max_x, 0, 0);
input_set_abs_params(gt9147dev.input, ABS_MT_POSITION_Y, 0, gt9147dev.max_y, 0, 0);
这些参数定义了触摸屏的物理尺寸和坐标范围,确保触摸坐标能够正确映射到显示区域。
6.4 多点触控槽初始化
初始化多点触控槽,支持最多5个同时触控点:
ret = input_mt_init_slots(gt9147dev.input, MAX_SUPPORT_POINTS, 0);
if (ret)
{goto fail_probe;
}
多点触控槽机制允许系统跟踪多个独立的触控点,每个槽对应一个触控点的状态。
7. 中断处理机制
7.1 中断申请
GT9147通过中断引脚通知主机有新的触摸事件发生。驱动需要申请中断并注册中断处理函数:
ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,gt9147_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,client->name, dev);
这里使用了线程化中断处理机制(threaded IRQ),将中断处理分为两个部分:
- 顶半部(top half):快速执行,仅做必要的硬件操作
- 底半部(bottom half):在内核线程上下文中执行,可以进行较长时间的操作
7.2 中断触发方式
中断触发方式配置为下降沿触发(IRQF_TRIGGER_FALLING
),因为GT9147的中断引脚在有触摸事件时会从高电平变为低电平。
IRQF_ONESHOT
标志表示这是一个不可共享的中断,确保中断处理的独占性。
7.3 中断处理函数
中断处理函数gt9147_irq_handler
负责处理触摸事件:
static irqreturn_t gt9147_irq_handler(int irq, void *dev_id)
{int touch_num = 0;int input_x, input_y;int id = 0;int ret = 0;u8 data;u8 touch_data[5];struct gt9147_dev *dev = dev_id;ret = gt9147_read_regs(dev, GT_GSTID_REG, &data, 1);if (data == 0x00){ /* 没有触摸数据,直接返回 */goto fail;}else{ /* 统计触摸点数据 */touch_num = data & 0x0f;}// ... 触摸数据处理
}
中断处理函数首先读取触摸状态寄存器,判断是否有新的触摸事件,然后读取触摸点数据。
8. 触摸数据处理
8.1 触摸状态寄存器
GT9147通过GT_GSTID_REG
寄存器报告当前的触摸状态:
ret = gt9147_read_regs(dev, GT_GSTID_REG, &data, 1);
if (data == 0x00)
{ /* 没有触摸数据,直接返回 */goto fail;
}
else
{ /* 统计触摸点数据 */touch_num = data & 0x0f;
}
寄存器的低4位表示当前检测到的触摸点数量,高4位保留。
8.2 触摸点数据读取
根据触摸点数量,读取相应的触摸点数据:
gt9147_read_regs(dev, GT_TP1_REG, touch_data, 5);
id = touch_data[0] & 0x0F;
if (id == 0)
{input_x = touch_data[1] | (touch_data[2] << 8);input_y = touch_data[3] | (touch_data[4] << 8);
}
每个触摸点数据包含5个字节:
- 字节0:触摸点ID和状态
- 字节1-2:X坐标(低字节在前)
- 字节3-4:Y坐标(低字节在前)
8.3 坐标值解析
GT9147返回的坐标值为16位无符号整数,采用小端字节序:
input_x = touch_data[1] | (touch_data[2] << 8);
input_y = touch_data[3] | (touch_data[4] << 8);
通过位操作将两个字节组合成一个16位整数,得到实际的触摸坐标。
9. 多点触控事件报告
9.1 多点触控槽管理
Linux输入子系统使用多点触控槽机制管理多个触控点:
input_mt_slot(dev->input, id);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, true);
input_mt_slot
函数选择当前要操作的触控槽,input_mt_report_slot_state
函数报告该槽的触控状态。
9.2 触摸坐标报告
报告触控点的坐标信息:
input_report_abs(dev->input, ABS_MT_POSITION_X, input_x);
input_report_abs(dev->input, ABS_MT_POSITION_Y, input_y);
这些函数将触摸坐标作为绝对值事件报告给输入子系统。
9.3 事件同步
在报告完所有触控点数据后,需要发送同步事件:
input_mt_report_pointer_emulation(dev->input, true);
input_sync(dev->input);
input_sync
函数发送EV_SYN
事件,标记当前事件包的结束,确保事件的原子性。
10. 驱动注册与注销
10.1 驱动注册
驱动通过module_i2c_driver
宏简化注册过程:
module_i2c_driver(gt9147_i2c_driver);
这个宏自动展开为模块初始化和退出函数,简化了驱动代码。
10.2 设备探测函数
probe
函数是驱动的核心,负责设备的完整初始化:
static int gt9147_probe(struct i2c_client *client, const struct i2c_device_id *id)
{// 硬件初始化// 输入设备配置// 中断注册// 返回0表示成功
}
如果probe
函数执行成功,设备就被认为是正常工作的。
10.3 设备移除函数
remove
函数在设备卸载时调用,负责清理资源:
static int gt9147_remove(struct i2c_client *client)
{input_unregister_device(gt9147dev.input);return 0;
}
这里注销了输入设备,释放相关资源。
11. 设备树配置详解
11.1 设备节点定义
设备树中GT9147设备节点的完整定义:
gt9147: gt9147@14 {compatible = "goodix,gt9147", "goodix,gt9xx";reg = <0x14>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_tsc>;interrupt-parent = <&gpio1>;interrupts = <9 0>;reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;status = "okay";
};
11.2 关键属性说明
compatible
:兼容性字符串,用于驱动匹配reg
:I2C设备地址interrupt-parent
:中断控制器引用interrupts
:中断配置reset-gpios
:复位引脚配置interrupt-gpios
:中断引脚配置
11.3 引脚控制组
引脚控制组定义了GT9147相关引脚的电气特性:
pinctrl_tsc: tscgrp {fsl,pins = <MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x10B0MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x10B0>;
};
这些配置确保引脚工作在正确的模式和电平。
12. 性能优化与调试
12.1 中断处理优化
中断处理函数应尽量精简,避免长时间操作:
// 顶半部:快速响应
static irqreturn_t gt9147_irq_handler(int irq, void *dev_id)
{// 仅读取状态并唤醒底半部schedule_work(&dev->work);return IRQ_WAKE_THREAD;
}// 底半部:详细处理
static irqreturn_t gt9147_irq_thread(int irq, void *dev_id)
{// 详细的数据读取和事件报告
}
12.2 电源管理
实现电源管理功能,降低系统功耗:
#ifdef CONFIG_PM
static int gt9147_suspend(struct device *dev)
{// 进入低功耗模式
}static int gt9147_resume(struct device *dev)
{// 恢复正常工作模式
}static const struct dev_pm_ops gt9147_pm_ops = {.suspend = gt9147_suspend,.resume = gt9147_resume,
};
#endif
12.3 调试信息输出
添加适当的调试信息,便于问题排查:
dev_dbg(&client->dev, "Touch point: %d, X: %d, Y: %d\n", id, input_x, input_y);
使用不同级别的日志输出,平衡调试信息和系统性能。
13. 错误处理与恢复
13.1 I2C通信错误
处理I2C通信失败的情况:
if (ret < 0) {dev_err(&client->dev, "I2C communication failed: %d\n", ret);// 尝试重新初始化gt9147_ts_reset(dev);return ret;
}
13.2 硬件复位恢复
在通信持续失败时,尝试硬件复位:
static int gt9147_recovery(struct gt9147_dev *dev)
{gt9147_ts_reset(dev);// 重新初始化序列return gt9147_probe(dev->client, NULL);
}
13.3 看门狗机制
实现看门狗机制,防止驱动死锁:
struct timer_list watchdog_timer;static void gt9147_watchdog(unsigned long data)
{struct gt9147_dev *dev = (struct gt9147_dev *)data;if (time_after(jiffies, dev->last_activity + HZ * 5)) {// 5秒无活动,尝试恢复gt9147_recovery(dev);}mod_timer(&dev->watchdog_timer, jiffies + HZ * 5);
}
14. 驱动测试与验证
14.1 基本功能测试
验证驱动的基本功能:
# 检查设备节点
ls /dev/input/# 查看输入设备信息
cat /proc/bus/input/devices# 监听输入事件
getevent
源码仓库位置:https://gitee.com/dream-cometrue/linux_driver_imx6ull