3. 硬件接口与电气基础
好,我们进入触摸驱动实战的第一个硬核环节——I2C驱动。说实话,嵌入式Linux里90%的触摸屏都挂在I2C总线上。你搞懂了I2C驱动模型,就等于拿到了触摸驱动的入场券。
这一节,我会带着你把I2C适配器、设备驱动模型、probe函数、设备树匹配这几个核心概念串起来。不搞学院派那套,咱们直接说清楚:代码怎么写、设备树怎么配、probe怎么被调用。
3.1 I2C子系统三层架构
Linux的I2C子系统,说白了就是三层:
- I2C核心层:提供总线注册、设备注册、数据传输的通用API。你不需要动它。
- I2C适配器层:对应硬件上的I2C控制器。比如STM32MP1的I2C1、I2C2。这一层由SoC厂商写。
- I2C设备驱动层:对应挂在总线上的具体外设。比如触摸芯片GT911、FT5x06。这一层,就是我们要写的。
我刚开始接触这个模型时,总觉得分层太多。后来在项目里调试一个I2C时钟芯片,发现适配器层和设备驱动层完全解耦,换一颗主控芯片,设备驱动一行代码都不用改。嗯,这时候才体会到分层的好处。
核心认知:你写触摸驱动,99%的工作量在设备驱动层。适配器层通常由BSP工程师或SoC厂商提供,你只需要在设备树里引用它。
下面这张图,把三层关系画清楚了:
3.2 设备驱动模型:i2c_driver 结构体
在Linux里,写一个I2C设备驱动,核心就是填充一个 i2c_driver 结构体。这个结构体定义在 include/linux/i2c.h 里。我挑几个关键字段说:
| 字段 | 类型 | 作用 |
|---|---|---|
driver.name |
const char * | 驱动名称,用于和设备树中的 compatible 匹配 |
driver.of_match_table |
struct of_device_id * | 设备树匹配表,核心中的核心 |
probe |
函数指针 | 匹配成功后自动调用,初始化硬件 |
remove |
函数指针 | 驱动卸载或设备拔出时调用 |
id_table |
struct i2c_device_id * | 传统I2C设备ID表(非设备树方式) |
来看一个实际的触摸驱动骨架。这是我之前做GT911驱动时的简化版:
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/of.h>
/* 设备树匹配表 */
static const struct of_device_id gt911_of_match[] = {
{ .compatible = "goodix,gt911" },
{ /* 结束标志 */ }
};
MODULE_DEVICE_TABLE(of, gt911_of_match);
/* probe 函数 - 核心初始化 */
static int gt911_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct device *dev = &client->dev;
int ret;
dev_info(dev, "GT911 probe start\n");
/* 1. 检查设备是否存在 */
ret = i2c_smbus_read_byte_data(client, 0x40);
if (ret < 0) {
dev_err(dev, "device not respond\n");
return -ENODEV;
}
/* 2. 初始化触摸芯片 */
/* ... 配置寄存器、申请中断、注册输入设备 ... */
dev_info(dev, "GT911 probe success\n");
return 0;
}
/* 驱动结构体 */
static struct i2c_driver gt911_driver = {
.driver = {
.name = "gt911",
.of_match_table = gt911_of_match,
},
.probe = gt911_probe,
.id_table = NULL, /* 使用设备树,不需要传统ID表 */
};
module_i2c_driver(gt911_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
个人习惯:我写I2C驱动时,probe函数里第一件事一定是读一个寄存器确认设备在线。别问我为什么——有一次设备树地址写错了,probe照样被调用,结果后面所有操作都超时。加个寄存器校验,能省你半天调试时间。
3.3 probe函数:驱动与设备的握手时刻
probe函数什么时候被调用?简单说:当内核发现一个I2C设备和一个I2C驱动匹配成功时,就会调用驱动的probe函数。
匹配过程是这样的:
- 系统启动时,I2C核心层扫描设备树,为每个I2C子节点创建一个
i2c_client对象。 - 当你的驱动调用
i2c_add_driver()时,核心层遍历所有已注册的i2c_client。 - 核心层拿驱动的
of_match_table和每个i2c_client的of_node里的compatible属性做比较。 - 匹配上了?好,调用你的
probe函数,把i2c_client指针传进去。
probe函数里,你通常要做这几件事:
- 读取设备ID寄存器,确认硬件存在
- 初始化设备(配置寄存器、设置工作模式)
- 申请中断(触摸屏通常用中断通知CPU)
- 注册输入子系统(把触摸事件上报给上层)
- 初始化可能用到的定时器或工作队列
我曾经踩过的坑:probe函数里不要做耗时操作,比如msleep(1000)。内核在probe期间是持有锁的,你睡一秒,整个I2C总线都卡住。如果设备初始化确实需要等待,用异步方式或者放到工作队列里去做。
3.4 设备树匹配:compatible 属性的艺术
设备树匹配,说白了就是让内核知道:这个硬件节点,应该由哪个驱动来管。
匹配的核心是 compatible 属性。设备树里这样写:
&i2c2 {
status = "okay";
clock-frequency = <400000>; /* 400KHz 快速模式 */
gt911: touch@5d {
compatible = "goodix,gt911";
reg = <0x5d>; /* I2C设备地址 */
interrupt-parent = <&gpioa>;
interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpiob 12 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <1024>;
touchscreen-size-y = <600>;
};
};
驱动里的 of_match_table 必须和设备树的 compatible 完全一致。我见过有人把"goodix,gt911"写成"goodix,gt911s",结果probe死活不调用——查了两天才发现是拼写错误。
匹配优先级是这样的:
| 匹配方式 | 优先级 | 说明 |
|---|---|---|
| 设备树 compatible | 最高 | 现代内核首选,支持多平台 |
| i2c_device_id 表 | 中等 | 传统方式,用于非设备树平台 |
| ACPI 匹配 | 低 | x86平台使用,嵌入式很少见 |
关键点:设备树里的 reg 属性就是I2C设备地址。触摸芯片的地址通常是7位,比如GT911默认是0x5d或0x14。注意:设备树里写的是7位地址,不是8位。我见过有人把0xBA(8位地址)写进去,结果内核怎么都扫描不到设备。
3.5 完整匹配流程:从设备树到probe
我把整个流程串起来,你感受一下:
- Bootloader加载设备树(.dtb文件)到内存。
- 内核解析设备树,为每个I2C控制器创建
i2c_adapter。 - 内核遍历I2C控制器下的子节点,为每个节点创建
i2c_client,并记录compatible和reg。 - 你的驱动模块加载,调用
module_i2c_driver()注册i2c_driver。 - I2C核心层遍历所有
i2c_client,用驱动的of_match_table逐一匹配。 - 匹配成功?调用
probe,传入i2c_client指针。 - probe里,你通过
i2c_client->addr拿到设备地址,开始通信。
嗯,流程不复杂,但每一步都有坑。我建议你刚开始做的时候,在probe函数里加一句 dev_info(dev, "client addr: 0x%02x\n", client->addr);,确认内核拿到的地址和你设备树里写的一致。
3.6 调试技巧:怎么看匹配有没有成功
匹配不成功,probe不调用,这是最常见的入门问题。我的排查步骤:
- 检查设备树编译:
dtc -I dtb -O dts xxx.dtb反编译,确认节点存在。 - 检查compatible字符串:驱动和设备树必须一字不差,包括大小写和逗号。
- 检查I2C地址:
reg属性是否和芯片数据手册一致?7位地址别写成8位。 - 检查I2C控制器状态:
status = "okay"写了吗?我忘过三次。 - 看内核日志:
dmesg | grep i2c或dmesg | grep gt911,看有没有错误信息。
我的调试利器:在驱动里加 #define DEBUG,然后打开 i2c-core 的调试日志。内核里 i2c-core.c 有很多 dev_dbg() 输出,能告诉你匹配过程的每一步。具体做法:echo 'file i2c-core.c +p' > /sys/kernel/debug/dynamic_debug/control。