Linux 驱动开发学习笔记:I2C 子系统从入门到实践

1. 引言

I2C(Inter-Integrated Circuit)总线是一种由 Philips 公司(现 NXP)开发的两线式串行通信总线,广泛应用于嵌入式系统中连接低速外设,如传感器、EEPROM、RTC 时钟芯片、触摸控制器等。在 Linux 内核中,I2C 子系统提供了完整的驱动框架,屏蔽了底层硬件差异,让开发者可以专注于设备驱动的业务逻辑。本文记录笔者学习 Linux I2C 驱动开发的过程,从硬件协议基础到内核框架,再到实际驱动编写,逐步梳理关键知识点。

2. I2C 协议基础

在深入 Linux 驱动之前,先回顾 I2C 总线的硬件协议要点,这对理解内核框架的设计至关重要。

2.1 物理层特性

  • 两线制:SCL(串行时钟线)和 SDA(串行数据线),均为开漏输出,需外接上拉电阻。
  • 多主多从:总线上可挂载多个主机和从机,通过地址区分设备。
  • 7 位或 10 位地址:常用 7 位地址,一个总线上最多挂载 112 个设备(保留地址除外)。
  • 传输速率:标准模式 100 kbit/s,快速模式 400 kbit/s,高速模式 3.4 Mbit/s。

2.2 数据传输格式

一次完整的 I2C 传输包含起始条件(START)、地址帧、数据帧和停止条件(STOP)。读写操作的基本格式如下:

text 复制代码
写操作:START + 从机地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP
读操作:START + 从机地址(写) + ACK + 寄存器地址 + ACK + START + 从机地址(读) + ACK + 数据 + NACK + STOP

理解这个时序是编写驱动的基础,因为 Linux 内核的 I2C 传输接口本质上就是对这些时序的封装。

3. Linux I2C 子系统架构

Linux 内核将 I2C 子系统划分为三个层次:控制器驱动(Adapter)、核心层(Core)和设备驱动(Client Driver)。理解这三层的关系是学习 I2C 驱动的关键。

3.1 三层结构总览

  • I2C Adapter(控制器驱动):直接操作硬件寄存器,负责在总线上产生时序,对应 SoC 上的 I2C 控制器外设。
  • I2C Core(核心层):提供统一的设备模型、总线注册机制、数据传输接口,以及设备与驱动的匹配逻辑,对上层屏蔽控制器差异。
  • I2C Client Driver(设备驱动):针对具体挂在总线上的设备(如某型号传感器)编写,通过核心层提供的 API 与设备通信,不直接操作硬件寄存器。

3.2 关键数据结构

I2C 子系统的核心数据结构主要有以下几个:

数据结构 作用
struct i2c_adapter 描述一个 I2C 控制器,包含总线号、算法、锁等。
struct i2c_algorithm 定义控制器收发数据的主函数(master_xfer)。
struct i2c_client 描述一个挂在总线上的设备,包含地址、驱动名等。
struct i2c_driver 描述一个设备驱动,包含 probe、remove 回调及 id 表。
struct i2c_msg 描述一次传输的消息,包含地址、标志、缓冲区等。

4. 设备驱动开发流程

编写一个 I2C 设备驱动通常遵循固定的步骤。下面以读取一个虚拟传感器为例,梳理完整流程。

4.1 驱动框架搭建

首先定义一个 i2c_driver 结构体,并实现 probe 和 remove 回调:

c 复制代码
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/init.h>
static int my_sensor_probe(struct i2c_client *client)
{
pr_info("my_sensor: probe success, addr=0x%02x\n", client->addr);
return 0;
}
static void my_sensor_remove(struct i2c_client *client)
{
pr_info("my_sensor: remove\n");
}
static const struct i2c_device_id my_sensor_id[] = {
{ "my-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my-sensor",
},
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.id_table = my_sensor_id,
};
module_i2c_driver(my_sensor_driver);
MODULE_LICENSE("GPL");

4.2 数据传输接口

在 probe 中获取设备后,就可以通过 i2c_transfer 或更便捷的 i2c_smbus_read_byte_data 等接口读写数据。以读取传感器寄存器为例:

c 复制代码
static int my_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
    int ret;
    struct i2c_msg msgs[2] = {
        {
            .addr = client->addr,
            .flags = 0,
            .len = 1,
            .buf = &reg,
        },
        {
            .addr = client->addr,
            .flags = I2C_M_RD,
            .len = 1,
            .buf = val,
        },
    };
ret = i2c_transfer(client-&gt;adapter, msgs, 2);
if (ret != 2) {
    dev_err(&amp;client-&gt;dev, "read reg 0x%02x failed, ret=%d\n", reg, ret);
    return ret &lt; 0 ? ret : -EIO;
}
return 0;
}

4.3 设备树匹配

在现代内核中,通常通过设备树描述硬件。在设备树中声明设备节点后,驱动通过 of_match_table 完成匹配:

c 复制代码
static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "vendor,my-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my-sensor",
.of_match_table = my_sensor_of_match,
},
.probe = my_sensor_probe,
.remove = my_sensor_remove,
};

对应的设备树节点示例:

dts 复制代码
&i2c1 {
    my_sensor@48 {
        compatible = "vendor,my-sensor";
        reg = <0x48>;
    };
};

5. SMBus 接口与辅助函数

对于大多数简单设备,不必每次都构造 i2c_msg 数组。内核提供了 SMBus 风格的封装函数,使用更加简洁。

5.1 常用 SMBus API

函数 功能
i2c_smbus_read_byte() 读取一个字节,不带寄存器地址。
i2c_smbus_write_byte() 写入一个字节,不带寄存器地址。
i2c_smbus_read_byte_data() 读取指定寄存器的值。
i2c_smbus_write_byte_data() 向指定寄存器写入值。
i2c_smbus_read_word_data() 读取 16 位寄存器值。
i2c_smbus_write_word_data() 写入 16 位寄存器值。

5.2 使用示例

使用 SMBus 接口后,前面的读寄存器函数可以简化为:

c 复制代码
static int my_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
    int ret;
ret = i2c_smbus_read_byte_data(client, reg);
if (ret &lt; 0) {
    dev_err(&amp;client-&gt;dev, "read reg 0x%02x failed, ret=%d\n", reg, ret);
    return ret;
}
*val = (u8)ret;
return 0;
}

6. 用户空间访问

除了编写内核驱动,Linux 还提供了用户空间访问 I2C 设备的接口,方便调试和快速验证。通过 /dev/i2c-N 设备节点,配合 i2c-dev 内核模块,可以在应用层直接读写设备。

6.1 使用 i2c-tools

在开发板上安装 i2c-tools 后,可以快速扫描总线和读写设备:

bash 复制代码
# 扫描总线上的设备
i2cdetect -y 1
读取 0x48 设备 0x00 寄存器的值
i2cget -y 1 0x48 0x00
向 0x48 设备 0x01 寄存器写入 0xff
i2cset -y 1 0x48 0x01 0xff

6.2 应用层编程

在 C 程序中,通过 ioctl 系统调用与 I2C_RDWR 命令完成读写:

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
int main(void)
{
int fd = open("/dev/i2c-1", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
struct i2c_msg msgs[2];
unsigned char reg = 0x00;
unsigned char val = 0;

msgs[0].addr = 0x48;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = &amp;reg;

msgs[1].addr = 0x48;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 1;
msgs[1].buf = &amp;val;

struct i2c_rdwr_ioctl_data data = {
    .msgs = msgs,
    .nmsgs = 2,
};

if (ioctl(fd, I2C_RDWR, &amp;data) &lt; 0) {
    perror("ioctl");
    close(fd);
    return -1;
}

printf("reg 0x00 = 0x%02x\n", val);
close(fd);
return 0;
}

7. 调试与常见问题

I2C 驱动开发中遇到问题,可以从以下几个方向排查。

7.1 常用调试手段

  • 开启内核调试日志 :在 drivers/i2c 目录下开启 CONFIG_I2C_DEBUG_CORE 和 CONFIG_I2C_DEBUG_ALGO,观察传输细节。
  • 逻辑分析仪:抓取 SCL/SDA 波形,确认时序是否符合协议。
  • i2cdetect 扫描:确认设备地址是否正确,设备是否真的挂在总线上。
  • 检查上拉电阻:SDA/SCL 必须有上拉电阻,否则通信不稳定。

7.2 常见错误码

错误码 含义
-ENXIO 无此设备,通常是没有 ACK 应答,地址错误或设备未上电。
-ETIMEDOUT 传输超时,总线可能被拉死或时钟异常。
-EBUSY 总线忙,可能被其他主机占用。
-EINVAL 参数无效,检查消息长度和标志位。

8. 总结

Linux I2C 驱动开发的核心在于理解三层架构:控制器驱动负责时序,核心层负责统一抽象,设备驱动负责业务逻辑。实际开发中,大部分工作集中在编写 i2c_driver 和利用 SMBus 辅助函数读写寄存器。建议初学者先在用户空间用 i2c-tools 验证硬件通路,再编写内核驱动,这样能有效缩小问题排查范围。后续可以进一步学习 I2C 的 10 位地址、多主机仲裁、DMA 传输等进阶内容。

相关推荐
xier_ran1 小时前
【infra之路】AI 编译器学习笔记(八):向量化——把小块变成 SIMD 指令
人工智能·笔记·学习
敲上瘾1 小时前
分布式系统——Redis哨兵:自动故障转移与选举原理
linux·数据库·redis·分布式·缓存·微服务
倔强的石头1061 小时前
【Linux指南】进程通信系列(二):匿名管道 —— pipe 的本质、fork 继承与四种读写规则
java·linux·运维
一条破秋裤1 小时前
09-读写多个sys属性文件
linux
小张成长计划..2 小时前
【Linux】25:线程概念和控制
java·linux·jvm
zhangdaliangmvp2 小时前
四元数的学习理解
学习
硅基手札2 小时前
【Linux 内核专栏 18】进程间通信 IPC(下)— Socket·Netlink·fd 三件套·futex·线程同步
linux·运维·驱动开发·单片机·嵌入式硬件·mcu·架构
djjjx.2 小时前
Linux进程篇(二):深入浅出 [进程概念与进程父子关系]:从底层原理到实战应用
linux·运维·服务器
贵州诚鑫致达科技有限公司10 小时前
Linux的yum源怎么重启?源不是服务
linux·运维·服务器