I2C 设备驱动开发
I2C 是嵌入式最常用的低速串行总线,传感器、EEPROM、RTC 很多都是 I2C 接口。这篇讲 Linux I2C 子系统架构、i2c_driver 框架、设备树匹配、I2C 读写 API,以及完整的传感器驱动示例。
大家好,我是黒漂技术佬。
I2C 总线用两根线(SCL 时钟、SDA 数据)就能挂一堆设备,地址寻址,非常方便。温度传感器、加速度计、EEPROM、触摸屏......很多外设都是 I2C 接口。
Linux 有完整的 I2C 子系统,驱动不用自己模拟时序,用内核提供的 API 就行。
这篇讲 I2C 驱动怎么写:I2C 子系统架构、i2c_driver、设备树匹配、读写函数、完整示例。
一、I2C 总线简介
特点
- 两线制:SCL(时钟)、SDA(数据)
- 半双工,同步串行
- 主从结构,主机发起通信
- 7 位或 10 位地址,一条总线最多挂 127 个设备(7位地址)
- 标准模式 100kbit/s,快速模式 400kbit/s,高速模式 3.4Mbit/s
通信过程
- 主机发起起始信号(START)
- 主机发从机地址 + 读写位
- 从机应答(ACK)
- 数据传输,每字节一个应答
- 主机发停止信号(STOP)
常见 I2C 设备
- 传感器:温度、湿度、加速度、陀螺仪、光照
- 存储:EEPROM、FRAM
- 时钟:RTC 实时时钟
- 外设:IO 扩展、ADC、DAC、触摸屏控制器
二、Linux I2C 子系统架构
三层结构:
I2C 设备驱动(我们写的)
↓
I2C 核心(i2c-core)
↓
I2C 总线驱动(适配器驱动,SoC厂商提供)
1. I2C 适配器(adapter)
SoC 的 I2C 控制器驱动,由芯片厂商提供,负责实际的 I2C 时序收发。
2. I2C 核心
核心层,提供通用 API,连接适配器和设备驱动。
3. I2C 设备驱动
我们写的,针对具体 I2C 设备(传感器、EEPROM 等)的驱动。
三、I2C 驱动框架
i2c_driver 结构体
跟 platform_driver 类似:
c
#include <linux/i2c.h>
static int my_probe(struct i2c_client *client)
{
// 匹配成功,初始化
return 0;
}
static void my_remove(struct i2c_client *client)
{
// 清理
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-sensor" },
{}
};
MODULE_DEVICE_TABLE(of, my_of_match);
static const struct i2c_device_id my_id[] = {
{ "my-sensor", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, my_id);
static struct i2c_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.id_table = my_id, // 旧的匹配方式
.driver = {
.name = "my-sensor",
.of_match_table = my_of_match, // 设备树匹配
},
};
module_i2c_driver(my_driver);
module_i2c_driver 宏
自动注册注销 i2c_driver,省得写 module_init/exit。
i2c_client
匹配成功后,probe 的参数就是 i2c_client,代表一个 I2C 从设备:
c
struct i2c_client {
unsigned short addr; // 从机地址
struct i2c_adapter *adapter; // 所属适配器
struct device dev;
// ...
};
addr 就是 I2C 设备地址,读写的时候用。
四、设备树中的 I2C 设备
I2C 控制器节点
SoC 的 I2C 控制器:
dts
i2c0: i2c@10000000 {
compatible = "vendor,i2c-controller";
reg = <0x10000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <400000>; // 400kHz
status = "okay";
};
I2C 设备子节点
挂在 I2C 控制器下面:
dts
&i2c0 {
temp_sensor: temp@48 {
compatible = "vendor,temp-sensor";
reg = <0x48>; // I2C 地址
status = "okay";
};
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
reg = <0x48>:I2C 从机地址(7位)- 子节点挂在 I2C 控制器节点下面
- 驱动的 compatible 跟这里匹配
五、I2C 读写 API
1. 简单收发
c
// 发送
int i2c_master_send(const struct i2c_client *client,
const char *buf, int count);
// 接收
int i2c_master_recv(const struct i2c_client *client,
char *buf, int count);
- 返回成功传输的字节数,负数失败
- 自动加 START/STOP
2. 通用消息传输
更灵活,可以组合多次传输(比如先写寄存器地址再读):
c
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
i2c_msg 结构体:
c
struct i2c_msg {
__u16 addr; // 从机地址
__u16 flags; // 标志:I2C_M_RD 读,0 写
__u16 len; // 数据长度
__u8 *buf; // 数据缓冲区
};
3. SMBus 方式
大部分 I2C 设备兼容 SMBus 协议,有更方便的寄存器读写函数:
c
#include <linux/i2c-smbus.h>
// 读/写一个字节
s32 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command);
s32 i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value);
// 读/写字(16位)
s32 i2c_smbus_read_word_data(const struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_data(const struct i2c_client *client, u8 command, u16 value);
// 读/写块数据(最多32字节)
s32 i2c_smbus_read_i2c_block_data(const struct i2c_client *client, u8 command,
u8 length, u8 *values);
s32 i2c_smbus_write_i2c_block_data(const struct i2c_client *client, u8 command,
u8 length, const u8 *values);
command 就是寄存器地址。
传感器驱动最常用这个! 大部分传感器都是「写寄存器地址 → 读数据」的模式,smbus 函数刚好对应。
六、封装寄存器读写
实际驱动里一般封装成读寄存器、写寄存器的函数:
c
// 读一个寄存器
static int reg_read(struct i2c_client *client, u8 reg, u8 *val)
{
s32 ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0) return ret;
*val = ret;
return 0;
}
// 写一个寄存器
static int reg_write(struct i2c_client *client, u8 reg, u8 val)
{
return i2c_smbus_write_byte_data(client, reg, val);
}
// 读多个寄存器
static int reg_read_bulk(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
return i2c_smbus_read_i2c_block_data(client, reg, len, buf);
}
然后业务逻辑就直接调 reg_read / reg_write,清晰很多。
七、完整示例:温度传感器驱动
假设一个温度传感器,I2C 地址 0x48,寄存器 0x00 是温度值(16位),0x01 是配置寄存器。
设备树
dts
&i2c0 {
temp@48 {
compatible = "vendor,temp-sensor";
reg = <0x48>;
status = "okay";
};
};
驱动代码
c
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/hwmon.h>
#define REG_TEMP 0x00
#define REG_CONFIG 0x01
struct temp_data {
struct i2c_client *client;
// ...
};
static int temp_read_reg(struct i2c_client *client, u8 reg, u16 *val)
{
s32 ret = i2c_smbus_read_word_data(client, reg);
if (ret < 0) return ret;
*val = ret;
return 0;
}
static int temp_write_reg(struct i2c_client *client, u8 reg, u16 val)
{
return i2c_smbus_write_word_data(client, reg, val);
}
static int get_temperature(struct temp_data *data, int *temp)
{
u16 raw;
int ret;
ret = temp_read_reg(data->client, REG_TEMP, &raw);
if (ret) return ret;
// 假设寄存器是 12 位,单位 0.0625 度
raw = be16_to_cpu(raw) >> 4;
*temp = raw * 625; // 毫摄氏度
return 0;
}
// sysfs 属性:读温度
static ssize_t temp1_input_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct temp_data *data = dev_get_drvdata(dev);
int temp, ret;
ret = get_temperature(data, &temp);
if (ret) return ret;
return sprintf(buf, "%d\n", temp);
}
static DEVICE_ATTR_RO(temp1_input);
static struct attribute *temp_attrs[] = {
&dev_attr_temp1_input.attr,
NULL
};
ATTRIBUTE_GROUPS(temp);
static int temp_probe(struct i2c_client *client)
{
struct temp_data *data;
struct device *hwmon_dev;
u16 config;
int ret;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_WORD_DATA))
return -ENODEV;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data) return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 读配置寄存器,验证芯片是否存在
ret = temp_read_reg(client, REG_CONFIG, &config);
if (ret) {
dev_err(&client->dev, "读取配置寄存器失败\n");
return ret;
}
// 注册为 hwmon 设备(Linux 硬件监控子系统)
hwmon_dev = devm_hwmon_device_register_with_groups(
&client->dev, client->name, data, temp_groups);
dev_info(&client->dev, "温度传感器 probe 成功\n");
return PTR_ERR_OR_ZERO(hwmon_dev);
}
static void temp_remove(struct i2c_client *client)
{
// devm 自动清理
}
static const struct of_device_id temp_of_match[] = {
{ .compatible = "vendor,temp-sensor" },
{}
};
MODULE_DEVICE_TABLE(of, temp_of_match);
static const struct i2c_device_id temp_id[] = {
{ "temp-sensor", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, temp_id);
static struct i2c_driver temp_driver = {
.probe = temp_probe,
.remove = temp_remove,
.id_table = temp_id,
.driver = {
.name = "temp-sensor",
.of_match_table = temp_of_match,
},
};
module_i2c_driver(temp_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Heipiao");
MODULE_DESCRIPTION("I2C 温度传感器驱动");
八、调试方法
i2cdetect:扫描总线上的设备
bash
# 扫描 i2c-0 总线
i2cdetect -y 0
# 输出:UU 表示被驱动占用了,数字表示检测到的地址
i2cget / i2cset:命令行读写
bash
# 读 0x48 设备的 0x00 寄存器
i2cget -y 0 0x48 0x00
# 写 0x48 设备的 0x01 寄存器为 0x80
i2cset -y 0 0x48 0x01 0x80
i2cdump:dump 所有寄存器
bash
i2cdump -y 0 0x48
调试驱动前先用 i2cdetect 确认设备能扫到,i2cget 能读到数据,再写驱动。
九、常见坑
坑 1:I2C 地址不对
- 有的 datasheet 写的是 8 位地址(包含读写位),Linux 用的是 7 位地址,要右移一位
- 地址对了 i2cdetect 才能扫到
坑 2:引脚复用没配对
SCL/SDA 引脚没配置成 I2C 功能,通信肯定失败。检查 pinctrl 配置。
坑 3:上拉电阻
I2C 总线必须有上拉电阻。硬件没焊上拉或者上拉阻值不对,通信不稳定甚至完全不通。
坑 4:字节序
16位寄存器,有的是大端有的是小端,读出来要转换。用 be16_to_cpu / le16_to_cpu。
坑 5:不检查适配器功能
有的 I2C 控制器不支持 SMBus 某些命令,调用前用 i2c_check_functionality 检查。
坑 6:probe 里不验证设备
上来就初始化,设备不存在也不报错。先读一个已知的寄存器(比如 ID 寄存器)验证芯片真的在总线上。
十、本篇小结
- I2C:两线制串行总线,主从结构,7位地址,100k/400k 速率
- Linux I2C 子系统三层:适配器驱动、核心层、设备驱动
- i2c_driver 框架:probe/remove、of_match_table 设备树匹配、module_i2c_driver 宏
- i2c_client:代表一个 I2C 从设备,包含地址和适配器信息
- 设备树:I2C 设备作为子节点挂在控制器下,reg 是 I2C 地址
- 读写 API:i2c_smbus_read/write_byte_data 最常用,适合寄存器型设备
- i2c_transfer 更灵活,可以组合多次 START
- 调试工具:i2cdetect 扫描、i2cget/set 读写、i2cdump 查看全部
- 常见问题:地址不对、上拉电阻、引脚复用、字节序
下一篇讲 SPI 设备驱动开发:SPI 总线、spi_driver、四种模式、读写传输。
我是黒漂技术佬。