4、I2C驱动框架:i2c_driver注册、probe函数实现、regmap操作
好,我们进入第四章。前面几章我们把触摸屏的硬件接口、I2C协议时序聊透了。这一章,咱们要真正动手写代码了——把I2C驱动框架搭起来。
说白了,Linux内核里的I2C驱动,核心就三件事:注册驱动、匹配设备、读写寄存器。我刚开始接触这块时,总觉得内核框架很神秘,后来发现,其实套路非常固定。你只要掌握了这个套路,换任何一款I2C触摸芯片,都能快速上手。
4.1 i2c_driver的注册与注销
每个I2C设备驱动,都需要一个i2c_driver结构体。它就像你的身份证,告诉内核:“嘿,我能驱动哪款芯片,我的probe函数在哪。”
先看结构体定义:
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/kernel.h>
static struct i2c_device_id ft5x06_idtable[] = {
{ "ft5x06", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ft5x06_idtable);
static int ft5x06_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
// 初始化逻辑
return 0;
}
static int ft5x06_remove(struct i2c_client *client)
{
// 清理逻辑
return 0;
}
static struct i2c_driver ft5x06_driver = {
.driver = {
.name = "ft5x06",
.of_match_table = of_match_ptr(ft5x06_of_match),
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,
.id_table = ft5x06_idtable,
};
module_i2c_driver(ft5x06_driver);
MODULE_LICENSE("GPL");
嗯,这里要注意几个关键点:
- id_table:这是I2C设备ID表,内核通过它来匹配设备和驱动。名字必须和设备树里
compatible属性一致。 - of_match_table:设备树匹配表,现在主流做法。我建议你两个都写上,兼容性更好。
- module_i2c_driver:这是个宏,展开后就是
module_init和module_exit,省得你手写。
i2c_driver结构体放在文件末尾,probe/remove函数放在前面。这样阅读代码时,从上到下,逻辑更顺。
4.2 probe函数——驱动的入口
probe函数什么时候被调用?当内核发现一个I2C设备,并且它的名字或兼容属性跟你注册的驱动匹配时,内核就会调用你的probe函数。
你可以把probe理解为驱动的构造函数。在这里,你要完成:
- 分配并初始化设备私有数据结构
- 获取硬件资源(中断号、GPIO、时钟等)
- 初始化I2C寄存器(通过regmap或直接SMBus)
- 注册输入设备(input_dev)
- 注册中断处理函数
我见过不少新手,把probe写得又臭又长。其实probe应该只做必要的初始化,复杂的逻辑放到工作队列或线程里去做。为什么呢?因为probe执行时,系统可能还没完全准备好,你在这里做太多事,容易出问题。
static int ft5x06_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct ft5x06_data *data;
int ret;
// 1. 分配私有数据
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 2. 初始化regmap
data->regmap = devm_regmap_init_i2c(client, &ft5x06_regmap_config);
if (IS_ERR(data->regmap)) {
dev_err(&client->dev, "regmap init failed\n");
return PTR_ERR(data->regmap);
}
// 3. 读取芯片ID,验证连接
ret = regmap_read(data->regmap, FT5X06_REG_CHIP_ID, &data->chip_id);
if (ret < 0) {
dev_err(&client->dev, "failed to read chip id\n");
return ret;
}
// 4. 注册输入设备
// ... 后续章节展开
return 0;
}
request_irq注册中断,结果中断触发时,输入设备还没注册完,导致空指针访问。后来我改成在probe最后才注册中断,或者在中断处理函数里加个标志位判断。切记,probe里的顺序很重要!
4.3 regmap操作——优雅的寄存器读写
说到I2C读写寄存器,早期驱动都是直接用i2c_master_send和i2c_master_recv,或者i2c_smbus_read_byte_data这些函数。代码写起来很啰嗦,而且缓存管理、并发保护都得自己搞。
后来内核引入了regmap,这东西太好用了。它把寄存器读写抽象成一套统一的API,底层可以是I2C、SPI、MMIO,上层调用完全一样。而且自带缓存和并发保护。
我个人强烈建议:新写的I2C驱动,都用regmap。除非你的芯片有非常特殊的时序要求,否则regmap能省你一半的代码量。
4.3.1 regmap配置
static const struct regmap_config ft5x06_regmap_config = {
.reg_bits = 8, // 寄存器地址宽度:8位
.val_bits = 8, // 寄存器值宽度:8位
.max_register = 0xFF, // 最大寄存器地址
.cache_type = REGCACHE_RBTREE, // 使用红黑树缓存
.read_flag_mask = 0x00, // 读操作掩码
.write_flag_mask = 0x00, // 写操作掩码
};
这里有几个参数需要根据芯片手册来配:
| 参数 | 说明 | 常见值 |
|---|---|---|
reg_bits |
寄存器地址的位数 | 8或16 |
val_bits |
寄存器值的位数 | 8或16 |
max_register |
最大有效寄存器地址 | 根据芯片定 |
cache_type |
缓存策略 | REGCACHE_NONE / REGCACHE_RBTREE |
read_flag_mask |
读操作时,地址是否需要特殊标记 | 一般为0 |
4.3.2 常用regmap API
初始化完成后,读写就非常简单了:
// 读寄存器
int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);
// 写寄存器
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);
// 更新寄存器某几位(读-改-写)
int regmap_update_bits(struct regmap *map, unsigned int reg,
unsigned int mask, unsigned int val);
// 批量读
int regmap_bulk_read(struct regmap *map, unsigned int reg,
void *val, size_t val_count);
// 批量写
int regmap_bulk_write(struct regmap *map, unsigned int reg,
const void *val, size_t val_count);
你看,是不是很清爽?regmap_update_bits尤其好用,比如你要设置某个寄存器的bit3为1,不用先读再改再写,一个函数搞定。
1. 自动处理I2C传输的细节(地址、数据格式)
2. 提供缓存,减少不必要的I2C访问(省电!)
3. 内置锁,多线程访问时不会乱
4.4 整体框架图
下面这张图,把I2C驱动的整个注册流程和probe里的关键步骤串起来了。你写驱动时,就照着这个流程走,基本不会错。
4.5 避坑指南
最后,分享几个我实际项目中遇到的坑:
- regmap缓存问题:如果你用
REGCACHE_RBTREE,第一次读寄存器时,regmap会从硬件读。但之后如果硬件寄存器被其他模块改了(比如固件升级),regmap可能还返回缓存的值。我遇到过触摸屏校准值读不对,查了两天才发现是缓存没刷新。解决方案:关键寄存器读操作前,调用regcache_mark_dirty强制刷新。 - 设备树匹配:
compatible属性一定要和驱动里of_match_table完全一致,大小写、标点都不能错。我曾经把"ft5x06"写成了"ft5x06",少了个下划线,结果驱动死活不probe。 - I2C地址:设备树里写的
reg是7位地址,而芯片手册里给的可能是8位地址(左移了一位)。这个坑我见得太多了,一定要确认清楚。
dev_info打印,把读取到的芯片ID、版本号打出来。这样驱动加载时,看一眼内核日志,就知道I2C通信是否正常。省得后面出问题了再回头查。
好了,这一章的内容就到这里。I2C驱动框架说白了就是三板斧:注册、probe、regmap。你把这章代码敲一遍,跑通,后面触摸数据的读取就水到渠成了。