3、I2C触摸屏驱动:I2C协议基础、I2C设备驱动注册、I2C数据传输与中断处理
说到触摸屏驱动,I2C 协议是绕不开的坎。市面上绝大多数电容式触摸屏都跑在 I2C 总线上。我刚开始调触摸屏时,总觉得 I2C 简单——不就两根线嘛,SDA 和 SCL。但真正上手才发现,坑全藏在细节里。
这一章,咱们就把 I2C 触摸屏驱动的三个核心环节拆开揉碎:协议基础、驱动注册、数据传输与中断。嗯,都是实战中每天要打交道的。
3.1 I2C 协议基础:两根线上的舞蹈
I2C 是飞利浦(现 NXP)搞出来的串行总线协议。它只用两根线:SDA(数据线)和 SCL(时钟线)。所有设备都挂在这两根线上,通过地址区分。
说白了,I2C 就是个主从结构。主设备(一般是 SoC)发起通信,从设备(触摸屏控制器)响应。每次通信都遵循一套固定流程:
- 起始条件:SCL 高电平时,SDA 从高拉低
- 发送从设备地址 + 读写位:7 位地址 + 1 位 R/W
- 等待应答:从设备拉低 SDA 表示 ACK
- 数据传输:每字节 8 位,后面跟 ACK/NACK
- 停止条件:SCL 高电平时,SDA 从低拉高
关键点:触摸屏的 I2C 地址通常是 7 位,比如 0x38 或 0x41。但有些芯片支持地址配置引脚,同一个型号可能有两个地址。我遇到过一块板子,原理图上地址引脚悬空,结果读出来是 0x38,另一批板子拉了高电平,变成 0x39。驱动里写死地址就翻车了。
触摸屏的 I2C 通信有个特点:数据量不大。一次触摸事件通常只传 6~12 字节。但实时性要求高——你手指滑过去,驱动得在几毫秒内把坐标报上去。
下面这张图展示了 I2C 触摸屏驱动的整体知识结构,方便你建立全局认知:
3.2 I2C 设备驱动注册:让内核认识你的触摸屏
驱动注册说白了就是告诉内核:「嘿,我这儿有个 I2C 设备,地址是 0x38,你帮我管着。」在 Linux 内核里,这事通过 i2c_add_driver 完成。
标准的注册流程分三步:
- 定义 i2c_device_id 表:声明设备名称和驱动数据
- 实现 probe 函数:设备被发现时调用,做初始化
- 注册 i2c_driver 结构体:把上面两个东西绑在一起
来看一段典型的触摸屏驱动注册代码:
static const struct i2c_device_id ft5x06_ts_id[] = {
{ "ft5x06", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ft5x06_ts_id);
static const struct of_device_id ft5x06_of_match[] = {
{ .compatible = "focaltech,ft5x06" },
{ }
};
MODULE_DEVICE_TABLE(of, ft5x06_of_match);
static int ft5x06_ts_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct ft5x06_ts_data *ts;
int ret;
// 分配私有数据结构
ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
if (!ts)
return -ENOMEM;
ts->client = client;
i2c_set_clientdata(client, ts);
// 初始化 I2C 传输相关资源
// ... 后续会展开
return 0;
}
static struct i2c_driver ft5x06_ts_driver = {
.driver = {
.name = "ft5x06_ts",
.of_match_table = ft5x06_of_match,
},
.probe = ft5x06_ts_probe,
.id_table = ft5x06_ts_id,
};
module_i2c_driver(ft5x06_ts_driver);
个人习惯:我写 I2C 驱动时,一定会同时提供 i2c_device_id 和 of_match_table。前者兼容老式板级代码,后者用于设备树。两个都写上,不管内核用哪种方式匹配,都能正常工作。
设备树里的节点长这样:
&i2c2 {
status = "okay";
clock-frequency = <400000>;
ft5x06: touchscreen@38 {
compatible = "focaltech,ft5x06";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
};
};
这里有个细节:reg = <0x38> 就是 I2C 从设备地址。probe 时内核会把 client->addr 设成 0x38。你想想看,如果设备树里地址写错了,probe 根本不会触发——我踩过这个坑,查了两天才发现是设备树地址和芯片实际地址对不上。
3.3 I2C 数据传输:把触摸坐标读回来
驱动注册成功只是第一步。真正的硬仗是数据传输——怎么从触摸屏芯片里把坐标数据读出来。
Linux 内核提供了两套 I2C 传输 API:
| API | 说明 | 适用场景 |
|---|---|---|
i2c_master_send / i2c_master_recv |
单次发送或接收,封装了起始/停止条件 | 简单的读写操作 |
i2c_transfer |
支持消息队列,一次调用可组合多个读写操作 | 复合操作(如先写寄存器地址再读数据) |
i2c_smbus_read_xxx / i2c_smbus_write_xxx |
SMBus 协议封装,更高级的接口 | 兼容 SMBus 的设备 |
触摸屏最常用的模式是:先写一个寄存器地址,然后连续读多个字节。用 i2c_transfer 最合适:
static int ft5x06_read_data(struct i2c_client *client, u8 reg,
u8 *buf, int len)
{
struct i2c_msg msg[2];
int ret;
// 消息 0:写寄存器地址
msg[0].addr = client->addr;
msg[0].flags = 0; // 写
msg[0].len = 1;
msg[0].buf = ®
// 消息 1:读数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读
msg[1].len = len;
msg[1].buf = buf;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret == 2)
return 0;
else
return -EIO;
}
我曾经犯过一个低级错误:在中断上下文里直接调用 i2c_transfer。I2C 传输可能会睡眠(尤其是时钟延展时),而中断上下文不允许睡眠。结果内核直接报 BUG: scheduling while atomic。正确的做法是用 threaded IRQ,把 I2C 传输放到进程上下文。
说到时钟延展,这是 I2C 协议里一个容易忽略的点。从设备如果来不及处理数据,会把 SCL 拉低,逼主设备等待。大多数 SoC 的 I2C 控制器支持硬件时钟延展,但有些需要软件配合。嗯,这里要注意:如果遇到 I2C 传输超时,先拿逻辑分析仪看看 SCL 是不是被拉低了。
3.4 中断处理:别让触摸事件丢了
触摸屏的中断处理是驱动性能的关键。用户手指点下去,芯片通过 INT 引脚通知 SoC。SoC 在中断服务程序里发起 I2C 读操作,把坐标取回来。
Linux 里处理这种场景,我推荐用 threaded IRQ:
static irqreturn_t ft5x06_ts_irq_handler(int irq, void *dev_id)
{
struct ft5x06_ts_data *ts = dev_id;
struct i2c_client *client = ts->client;
u8 buf[6];
int ret;
// 读取触摸数据
ret = ft5x06_read_data(client, FT5X06_REG_TD_STATUS, buf, 6);
if (ret < 0) {
dev_err(&client->dev, "read touch data failed: %d\n", ret);
return IRQ_HANDLED;
}
// 解析坐标并上报
ft5x06_report_touch(ts, buf);
return IRQ_HANDLED;
}
static int ft5x06_ts_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
// ... 前面的初始化代码 ...
// 注册 threaded IRQ
ret = devm_request_threaded_irq(&client->dev,
client->irq,
NULL, // 上半部:NULL 表示只用下半部
ft5x06_ts_irq_handler, // 下半部:处理 I2C 传输
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"ft5x06_ts", ts);
if (ret) {
dev_err(&client->dev, "request irq failed: %d\n", ret);
return ret;
}
return 0;
}
这里有几个设计要点:
- IRQF_ONESHOT:保证中断线在 threaded handler 执行期间被屏蔽,防止中断嵌套。触摸屏中断很频繁,不加这个标志容易丢中断。
- 上半部传 NULL:触摸屏中断不需要做紧急的硬件操作,直接在下半部处理即可。省掉一个不必要的上半部函数。
- 中断触发方式:大多数触摸芯片用下降沿触发。但有些芯片支持电平触发,需要根据 datasheet 配置。
避坑指南:我曾经遇到一个 case,触摸屏偶尔会丢失第一个触摸点。查了很久发现是中断触发方式配置错了。芯片手册写的是「下降沿触发」,但实际测试发现芯片在初始化后会先拉高 INT 引脚,然后才拉低产生中断。如果驱动在 probe 时没有清除芯片内部的中断状态寄存器,就会导致第一次中断丢失。解决方案是在 probe 最后加一次虚拟读操作,清空 pending 的中断。
3.5 实战经验:调试 I2C 触摸屏的常用手段
调了这么多年触摸屏驱动,我总结了几条实用经验:
- 先用 i2c-tools 验证硬件:在用户态用
i2cdetect -y <bus>看设备地址是否正确。再用i2cget和i2cset手动读写寄存器,确认通信正常。 - 逻辑分析仪是神器:I2C 协议用逻辑分析仪抓波形最直观。看起始条件、地址、ACK、数据、停止条件,一眼就能发现问题。
- 加调试打印要克制:中断处理里别加
printk,会拖慢系统。用 tracepoint 或 ftrace 替代。 - 注意 I2C 时钟频率:触摸屏通常支持 400kHz(快速模式)。但有些廉价芯片在 400kHz 下不稳定,降到 100kHz 就好了。设备树里配
clock-frequency就行。