I2C 设备驱动开发

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

通信过程

  1. 主机发起起始信号(START)
  2. 主机发从机地址 + 读写位
  3. 从机应答(ACK)
  4. 数据传输,每字节一个应答
  5. 主机发停止信号(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、四种模式、读写传输。

我是黒漂技术佬。

相关推荐
振南的单片机世界1 小时前
地址总线定门牌号,数据总线搬货量:CPU的“寻址”与“搬运”
arm开发·stm32·单片机·嵌入式硬件
网络小白不怕黑1 小时前
11.虚拟机模拟路由器实验
linux·运维·服务器·网络
Mapleay1 小时前
Linux 内核编程基础
linux·运维·服务器
梁朝辉2 小时前
单片机中ADC直接通道和附加通道什么区别?
单片机·嵌入式硬件
sg_knight3 小时前
Claude Code 安装指南(macOS / Linux / Windows WSL)
linux·windows·macos·claude·code·anthropic·claude-code
oioihoii3 小时前
CentOS运行Teemii实操:Docker Compose部署、漫画导入与公网阅读
linux·docker·centos
恒53911 小时前
Linux文件系统I
linux·运维·服务器
阿成学长_Cain11 小时前
Linux ipcs 命令超全详解:查看共享内存 / 消息队列 / 信号量,IPC 运维必备
linux·运维·服务器·网络·数据库
黯然神伤88811 小时前
AlmaLinux设置软件下载源
linux·alma