Linux I²C 总线驱动开发:从架构到实战的完整指南
在嵌入式 Linux 开发中,I²C(Inter-Integrated Circuit)总线因简洁的硬件设计和可靠的通信性能,被广泛应用于连接温度传感器、EEPROM、触摸屏等外设。掌握 I²C 驱动开发,是嵌入式工程师必备的核心技能之一。本文将基于 Linux 内核的分层设计思想,系统讲解 I²C 驱动的架构原理、开发流程(含应用层验证、驱动编写、调试技巧),帮助开发者快速上手。
一、I²C 驱动体系架构:分层分离的设计智慧
Linux I²C 子系统采用 "分层 + 分离 " 的设计理念,将复杂的驱动逻辑拆解为不同层级,既降低了开发难度,又提升了代码的复用性。结合下方的五层架构图,可直观理解各层的协作关系:

(注:图中清晰标注了应用层、IIC 设备驱动层、IIC 核心层、IIC 总线驱动层、IIC 硬件层,以及 LM75、AT24C08 等设备在各层的组件映射)
1. 应用层
直接面向用户,可直接调用 I²C 接口,操作 I²C 总线,或通过驱动创建的设备节点 (如/dev/lm75
、/dev/at24c08
)与硬件交互,无需关注底层通信细节。
例如,lm75_app
通过打开/dev/lm75
设备节点,调用read
函数即可获取温度传感器的原始数据,再由应用层完成数据解析与展示。
2. IIC 设备驱动层(Client Driver)
针对具体 I²C 外设(如 LM75 温度传感器、AT24C08 EEPROM)实现功能逻辑,是开发者的核心开发对象。该层通过调用底层总线能力完成数据收发,不直接操作硬件。
每个设备驱动细分为 **"驱动(Driver)"和"客户端(Client)"** 两个核心组件:
- Driver :实现外设的功能逻辑(如 LM75 的温度读取流程、AT24C08 的 EEPROM 读写时序),并向上提供文件操作接口(
open
、read
、write
等)。 - Client:保存外设的关键信息(如 I²C 从设备地址、所属适配器),是驱动与硬件通信的 "身份凭证"。
以 LM75 为例,lm75 driver
实现温度读取的核心逻辑,lm75 client
则关联到具体的 I²C 总线(如 IIC1)和设备地址(如 0x48),确保驱动能精准找到目标硬件。
3. IIC 核心层(I²C Core)
由 Linux 内核提供,承担 "中间人 " 角色:负责匹配 I²C 设备与驱动、管理适配器(Adapter)和从设备(Client)的绑定关系,并提供统一的 API 接口(如master_xfer(),i2c_transfer()
)供设备驱动调用。
其核心作用包括:
- 维护驱动 - 设备匹配表 ,确保
lm75 driver
能找到对应的lm75 client
; - 封装底层总线差异,让设备驱动无需关心是 IIC1 还是 IIC2 在处理通信;
- 提供错误处理、并发控制等通用能力,简化设备驱动的开发复杂度。
4. IIC 总线驱动层(Adapter Driver)
实现 I²C 物理总线的底层通信能力,包括 SCL/SDA 引脚控制、时序生成等,操作对象是 I²C 控制器硬件(如 iic0、iic1)。
该层包含三个核心组件:
- Adapter :如
IIC1 adapter
、IIC2 adapter
,对应物理上的 I²C 控制器(如 SOC 的 I2C0、I2C1 外设),负责生成 SCL、SDA 时序,执行数据收发。 - Device :如
IIC1 device
、IIC2 device
,是总线上挂载的物理外设的 "硬件描述",包含设备地址、通信参数等信息。 - IIC driver :实现适配器的底层驱动逻辑(如寄存器配置、时序生成),向上对接
iic core
,向下操作硬件寄存器。
例如,IIC1 adapter
驱动会初始化 SOC 的 I2C1 控制器,配置通信频率(如 100kHz),并实现master_xfer
函数来完成实际的位级通信。
5. IIC 硬件层
硬件层是 I²C 通信的物理载体,以touch screen
、lm75
、at24c08
为代表,涵盖了各类 I²C 外设。这些设备通过 SCL、SDA 引脚与适配器连接,是数据的最终产生或存储端。
核心实体解析
理解 I²C 驱动的关键在于掌握以下核心数据结构和函数:
i2c_adapter
:即 I²C 适配器,对应物理上的 I²C 控制器,管理一条 I²C 总线的通信。i2c_algorithm
:定义通信规则(如时序),是master_xfer()
函数的实现依据。master_xfer()
:数据传输的核心执行函数,通过解析i2c_msg
结构体完成实际的读写操作。i2c_client
:代表 I²C 从设备,包含设备地址、所属适配器等关键信息。i2c_msg
:描述单次 I²C 传输的消息结构,包含地址、传输方向、数据长度和缓冲区等信息。
核心逻辑关系 :设备驱动通过client->adapter->algo->master_xfer()
的调用链,间接使用总线驱动的底层能力,实现与硬件的解耦。
二、I²C 设备驱动开发实战:以 LM75 和 AT24C08 为例
开发 I²C 设备驱动需遵循 "硬件验证→设备声明→驱动编写→用户层测试" 的流程,以下结合 LM75 温度传感器和 AT24C08 EEPROM 详细说明。
1. 步骤 0:应用层直接操作 I²C 总线(硬件验证阶段)
在编写驱动前,必须先用应用层程序验证硬件通信是否正常,确保总线、设备连接及基本读写逻辑无误。
cs
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <sys/ioctl.h>
int main(int argc, const char *argv[])
{
// 打开I²C总线设备节点(根据硬件连接调整,如I2C1对应/dev/i2c-1)
int fd = open("/dev/i2c-1", O_RDWR);
if(fd < 0)
{
perror("open i2c-1 failed");
return -1;
}
// 设置目标从设备地址(LM75为0x48)
ioctl(fd, I2C_SLAVE, 0x48);
while(1)
{
unsigned char buf[2] = {0};
// 第一步:写入温度寄存器地址(LM75温度寄存器地址为0x00)
int ret = write(fd, buf, 1); // buf[0]默认值为0x00
if(ret != 1)
{
perror("write register address failed");
break;
}
// 第二步:读取2字节温度数据
ret = read(fd, buf, sizeof(buf));
if(ret != 2)
{
perror("read temperature data failed");
break;
}
// 解析温度值:(16位数据 >> 7) * 0.5°C
float temp = (((buf[0] << 8) | buf[1]) >> 7) * 0.5;
printf("Current Temperature: %.1f°C\n", temp);
sleep(3); // 每3秒读取一次
}
close(fd);
return 0;
}
编译与运行:
arm-linux-gnueabihf-gcc i2c_app_test.c -o i2c_app_test
./i2c_app_test # 若输出温度值,说明硬件通信正常
2. 步骤 1:在设备树(DTS)中声明设备
设备树用于描述硬件信息,Linux 内核通过设备树匹配对应的驱动。需在对应 I²C 总线节点下添加设备信息:
pt.dts
cs
/* IIC1适配器与LM75设备配置 */
&i2c1 {
clock-frequency = <100000>; // I²C总线频率100kHz
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>; // 绑定I²C1的引脚配置
status = "okay";
lm75@48 {
compatible = "ti,lm75"; // 兼容性标识,需与驱动匹配
reg = <0x48>; // LM75的I²C从设备地址
};
};
/* IIC2适配器与AT24C08设备配置 */
&i2c2 {
clock-frequency = <100000>;
pinctrl-0 = <&pinctrl_i2c2>;
status = "okay";
at24c08@50 {
compatible = "atmel,at24c08";
reg = <0x50>; // AT24C08的I²C从设备地址
};
};
编写完成后,编译设备树并烧写到开发板:
make pt.dtb # 编译设备树
cp arch/arm/boot/dts/pt.dtb ~/tftpboot # 拷贝到TFTP目录供开发板加载
注意 :compatible
字符串必须与驱动中的匹配表完全一致,否则驱动的probe
函数无法触发。
3. 步骤 2:编写内核设备驱动
驱动的核心是实现probe
(设备匹配成功时调用)、remove
(设备移除时调用)以及文件操作接口(open
、read
等)。
(1)LM75 温度传感器驱动实现
cs
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#define DEV_NAME "lm75"
static struct i2c_client *lm75_client; // 保存从设备指针
// 读取温度数据的核心逻辑
static ssize_t lm75_read(struct file *file, char __user *buf, size_t len, loff_t *loff) {
int ret;
unsigned char data[2] = {0};
struct i2c_msg msg[2] = {
// 第一步:写入温度寄存器地址(0x00)
{
.addr = lm75_client->addr,
.flags = 0, // 写操作
.len = 1,
.buf = data // data[0]默认为0x00
},
// 第二步:读取2字节温度数据
{
.addr = lm75_client->addr,
.flags = I2C_M_RD, // 读操作
.len = 2,
.buf = data
}
};
// 调用核心层API完成数据传输
ret = i2c_transfer(lm75_client->adapter, msg, ARRAY_SIZE(msg));
if (ret != ARRAY_SIZE(msg))
return -EIO;
// 将内核空间数据拷贝到用户空间
if (copy_to_user(buf, data, 2))
return -EFAULT;
return 2;
}
// 文件操作结构体
static const struct file_operations lm75_fops = {
.owner = THIS_MODULE,
.open = default_open,
.read = lm75_read,
.release = default_release,
};
// misc设备(自动分配次设备号,简化设备节点创建)
static struct miscdevice lm75_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEV_NAME,
.fops = &lm75_fops,
};
// 设备匹配成功时调用
static int lm75_probe(struct i2c_client *client, const struct i2c_device_id *id) {
int ret;
lm75_client = client; // 保存从设备指针
// 注册misc设备,创建/dev/lm75节点
ret = misc_register(&lm75_misc);
if (ret < 0) {
pr_err("misc_register failed\n");
return ret;
}
pr_info("lm75 probe success, slave addr: 0x%x\n", client->addr);
return 0;
}
// 设备移除时调用
static int lm75_remove(struct i2c_client *client) {
misc_deregister(&lm75_misc);
pr_info("lm75 device removed\n");
return 0;
}
// 设备匹配表(与设备树compatible字段对应)
static const struct i2c_device_id lm75_id_table[] = {
{DEV_NAME, 0},
{}
};
MODULE_DEVICE_TABLE(i2c, lm75_id_table);
// I²C驱动结构体
static struct i2c_driver lm75_driver = {
.probe = lm75_probe,
.remove = lm75_remove,
.driver = {
.name = DEV_NAME,
.of_match_table = of_match_ptr(lm75_of_match), // 设备树匹配
},
.id_table = lm75_id_table,
};
module_i2c_driver(lm75_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("LM75 Temperature Sensor I2C Driver");
(2)AT24C08 EEPROM 驱动开发要点
AT24C08 是 1KB 的 I²C EEPROM,其驱动开发需关注以下特殊点:
- 地址处理:需 10 位地址,当地址≥256 时,需分 2 字节发送(高 8 位 + 低 8 位)。
- 写操作流程:发送设备地址(写标志)→ 发送内存地址 → 发送数据 → 等待 5ms(内部写周期)。
- 读操作流程:先发送设备地址(写标志)和内存地址,再发送设备地址(读标志),最后读取数据。
开发前建议先通过应用层程序验证通信可行性,例如实现 "写入数据→读取数据→比对验证" 的闭环测试,确保硬件连接和通信逻辑无误后,再封装为内核驱动。
4. 步骤 3:编写用户层测试程序(驱动验证)
(1)LM75 温度读取测试程序
cs
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("/dev/lm75", O_RDONLY);
if (fd < 0) {
perror("open /dev/lm75 failed");
return -1;
}
unsigned char data[2];
while (1) {
memset(data, 0, sizeof(data));
if (read(fd, data, 2) != 2) {
perror("read failed");
break;
}
// LM75温度计算:(16位数据 >> 7) * 0.5°C
float temp = (((data[0] << 8) | data[1]) >> 7) * 0.5;
printf("Current Temperature: %.1f°C\n", temp);
sleep(2); // 每2秒读取一次
}
close(fd);
return 0;
}
交叉编译后在开发板运行:
arm-linux-gnueabihf-gcc lm75_test.c -o lm75_test
./lm75_test # 若输出温度值,说明驱动工作正常
三、调试技巧与常见问题排查
1. 必备调试工具
i2cdetect -l
:列出系统中所有可用的 I²C 总线。i2cdetect -y 1
:扫描 I²C-1 总线上的从设备,验证设备是否被正确识别。dmesg
:查看内核日志,定位驱动加载失败、probe
未触发等问题。- 示波器:抓取 SCL/SDA 引脚波形,排查时序错误或硬件连接问题。
2. 常见问题解决方案
问题现象 | 可能原因 | 解决方法 |
---|---|---|
probe 函数未触发 |
compatible 字符串不匹配 |
确保设备树与驱动的匹配表一致 |
读取数据异常 | 未写寄存器地址、总线编号错误 | 按流程先写地址;确认设备连接的 I²C 总线编号 |
模块加载失败 | 模块已加载或符号冲突 | 用rmmod 卸载旧模块,必要时重启开发板 |
EEPROM 写入无效 | 未等待内部写周期 | 写操作后添加msleep(5) 延时 |
二次通信失败 | 未处理重复起始条件 | 确保读操作前完整执行 "写地址" 流程 |
四、工程实践与最佳实践
1. 分层设计的价值
- 总线驱动由 SoC 厂商提供,开发者无需关注底层时序,专注于设备功能逻辑。
- 核心层提供统一 API,降低设备驱动的开发难度,提升代码复用性。
2. 驱动封装的优势
相比应用层直接操作 I²C 总线,内核驱动封装具有明显优势:
对比维度 | 应用层直接操作 | 内核驱动封装 |
---|---|---|
硬件知识要求 | 高(需掌握时序和地址) | 低(调用统一 API) |
复用性 | 差(需重复编写通信逻辑) | 高(可被多个应用调用) |
维护成本 | 高(硬件变化需修改所有应用) | 低(仅需修改驱动) |
3. 内核开发注意事项
- 避免在核内使用浮点数:内核无 FPU 支持,浮点数运算会导致性能下降或崩溃,建议返回原始整型数据,由用户层解析。
- 严格检查返回值 :对
i2c_transfer()
等函数的返回值进行判断,确保数据传输成功。 - 遵循 GPL 协议 :内核模块需声明
MODULE_LICENSE("GPL")
,避免版权问题。
五、总结
Linux I²C 驱动开发的核心是理解 "分层分离 " 的架构思想,遵循 "硬件验证→设备树声明→驱动编写→测试调试" 的流程。开发者无需重复实现底层总线逻辑,只需聚焦于设备的特定功能,通过内核提供的 API 即可快速完成驱动开发。
记住一个核心原则:"先通逻辑,再写驱动;硬件问题优先排查"。在实际开发中,善用调试工具,遵循最佳实践,就能高效解决各类问题,开发出稳定可靠的 I²C 设备驱动。