4. I2C设备驱动:客户端注册、读写操作、用户空间接口
好,咱们进入正题。这一章讲的是I2C设备驱动的核心——怎么把设备挂到总线上,怎么读写数据,最后怎么让用户态程序也能用上。说白了,就是打通从硬件寄存器到应用程序的整条链路。
我在做嵌入式项目时,发现很多新手把I2C驱动想得太复杂。其实Linux内核已经把框架搭好了,你要做的就是填对回调函数。嗯,咱们一步步来。
4.1 客户端注册:让内核认识你的设备
设备驱动第一步,就是注册。你得告诉内核:“嘿,我这儿有个I2C设备,挂在哪个地址上,用什么方式匹配。”
4.1.1 设备树匹配(主流方式)
现在做嵌入式,基本都用设备树。你在dts文件里写一段描述,内核启动时就会自动创建对应的i2c_client。
// 设备树片段
&i2c1 {
status = "okay";
clock-frequency = <100000>;
temperature_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
驱动这边,用of_match_table来匹配:
static const struct of_device_id tmp102_of_match[] = {
{ .compatible = "ti,tmp102" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, tmp102_of_match);
4.1.2 传统板级匹配
老项目或者没有设备树的平台,用i2c_device_id:
static const struct i2c_device_id tmp102_id[] = {
{ "tmp102", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, tmp102_id);
4.1.3 probe函数——驱动的入口
匹配成功后,内核调用probe。这里是你初始化硬件、分配资源的地方。
static int tmp102_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct tmp102_data *data;
int ret;
// 分配私有数据结构
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 检查设备是否存在
ret = i2c_smbus_read_byte_data(client, TMP102_REG_ID);
if (ret < 0) {
dev_err(&client->dev, "device not found\n");
return -ENODEV;
}
// 注册其他子系统(如hwmon、input等)
// ...
return 0;
}
个人经验:probe里一定要做设备存在性检查。我遇到过硬件虚焊的情况,板子回来怎么调都不通,最后发现是芯片根本没焊上。加个读ID的校验,能省很多调试时间。
4.2 读写操作:与硬件对话
I2C读写分两种:SMBus接口和普通I2C接口。SMBus是I2C的子集,大部分传感器都支持。我个人习惯优先用SMBus,代码更简洁。
4.2.1 SMBus读写(推荐)
| 函数 | 用途 | 说明 |
|---|---|---|
i2c_smbus_read_byte() |
读1字节 | 不带寄存器地址 |
i2c_smbus_write_byte() |
写1字节 | 不带寄存器地址 |
i2c_smbus_read_byte_data() |
读指定寄存器 | 最常用 |
i2c_smbus_write_byte_data() |
写指定寄存器 | 最常用 |
i2c_smbus_read_word_data() |
读2字节 | 注意字节序 |
i2c_smbus_read_i2c_block_data() |
读多字节 | 最多32字节 |
举个例子,读温度传感器的温度值:
static int tmp102_read_temp(struct i2c_client *client, int *temp)
{
s16 raw;
int ret;
ret = i2c_smbus_read_word_data(client, TMP102_REG_TEMP);
if (ret < 0)
return ret;
// TMP102返回的是大端数据,需要转换
raw = be16_to_cpu((__be16)ret);
// 12位精度,右移4位
*temp = raw >> 4;
return 0;
}
注意:很多I2C设备返回的数据是大端格式。我踩过这个坑——读出来的温度值忽高忽低,查了半天才发现是字节序没转。记住:be16_to_cpu()是你的好朋友。
4.2.2 普通I2C接口(灵活但复杂)
当SMBus不够用时,比如需要连续读写大量数据,就用i2c_transfer():
static int tmp102_bulk_read(struct i2c_client *client,
u8 reg, u8 *buf, int len)
{
struct i2c_msg msgs[2];
int ret;
// 第一条消息:写寄存器地址
msgs[0].addr = client->addr;
msgs[0].flags = 0; // 写
msgs[0].len = 1;
msgs[0].buf = ®
// 第二条消息:读数据
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; // 读
msgs[1].len = len;
msgs[1].buf = buf;
ret = i2c_transfer(client->adapter, msgs, 2);
if (ret != 2)
return -EIO;
return 0;
}
4.3 用户空间接口:让应用程序访问设备
驱动写好了,怎么让用户态程序用?有三种方式:
- sysfs属性文件——简单,适合单个值
- 字符设备接口——灵活,适合复杂操作
- i2cdev(内核自带)——偷懒,适合调试
4.3.1 sysfs接口(快速实现)
用DEVICE_ATTR宏创建属性文件:
static ssize_t temp_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct i2c_client *client = to_i2c_client(dev);
int temp, ret;
ret = tmp102_read_temp(client, &temp);
if (ret < 0)
return ret;
return sprintf(buf, "%d\n", temp);
}
static DEVICE_ATTR_RO(temp);
// 在probe中创建
static int tmp102_probe(struct i2c_client *client, ...)
{
// ... 其他初始化
ret = device_create_file(&client->dev, &dev_attr_temp);
if (ret)
return ret;
return 0;
}
用户态用起来很简单:
# cat /sys/bus/i2c/devices/1-0048/temp
25
4.3.2 字符设备接口(完整控制)
当需要ioctl、mmap等复杂操作时,注册字符设备:
static int tmp102_open(struct inode *inode, struct file *file)
{
// 从inode获取i2c_client
struct tmp102_data *data = container_of(inode->i_cdev,
struct tmp102_data, cdev);
file->private_data = data;
return 0;
}
static long tmp102_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct tmp102_data *data = file->private_data;
switch (cmd) {
case TMP102_GET_TEMP:
return tmp102_read_temp(data->client, (int __user *)arg);
case TMP102_SET_LIMIT:
// 设置温度阈值
break;
default:
return -ENOTTY;
}
}
static const struct file_operations tmp102_fops = {
.owner = THIS_MODULE,
.open = tmp102_open,
.unlocked_ioctl = tmp102_ioctl,
};
4.3.3 i2cdev(调试利器)
内核配置CONFIG_I2C_CHARDEV后,每个I2C适配器都会生成/dev/i2c-N设备。用户态可以直接用ioctl操作:
// 用户态程序示例
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x48); // 设置从机地址
// 读寄存器
__u8 reg = 0x00;
__u8 data;
struct i2c_msg msgs[2] = {
{ .addr = 0x48, .flags = 0, .len = 1, .buf = ® },
{ .addr = 0x48, .flags = I2C_M_RD, .len = 1, .buf = &data },
};
struct i2c_rdwr_ioctl_data rdwr = {
.msgs = msgs,
.nmsgs = 2,
};
ioctl(fd, I2C_RDWR, &rdwr);
我的建议:产品发布时别用i2cdev,它绕过了驱动层的权限控制和电源管理。但调试阶段特别好用——我曾经用i2cdev配合一个Python脚本,半小时就把一个新传感器的寄存器摸透了。
4.4 知识结构总览
下面这张图把整个流程串起来了,你写驱动时按这个思路走,基本不会乱:
4.5 避坑指南
最后,分享几个我实际项目中踩过的坑:
- 地址左移问题:设备树里写0x48,内核会左移1位变成0x90。别在驱动里再左移一次,否则地址就错了。
- 时钟延展:有些慢速设备会拉低SCL,主控要支持时钟延展。我遇到过一颗PMIC,读寄存器时偶尔卡死,就是时钟延展没处理好。
- 原子上下文:I2C传输可能会睡眠,不能在中断上下文或自旋锁里直接调用。用
i2c_smbus_read_byte_data前,确认当前上下文允许睡眠。 - 电源管理:别忘了在suspend/resume里保存恢复寄存器。我有个项目,休眠唤醒后传感器读数全乱,就是因为没做寄存器保存。
调试小技巧:用i2cget、i2cset命令行工具快速验证硬件。比如:i2cget -y 1 0x48 0x00 w,直接读温度值。先确认硬件没问题,再怀疑驱动代码。
嗯,这一章的内容就到这儿。客户端注册、读写操作、用户空间接口,这三块搞明白了,I2C设备驱动的基本框架你就掌握了。剩下的就是根据具体芯片的数据手册,填寄存器地址和协议细节。