Day67 Linux I²C 总线与设备驱动架构、开发流程与调试
day67 Linux I²C 总线与设备驱动架构、开发流程与调试
一、I²C 驱动体系结构总览
Linux I²C 子系统采用 分层 + 分离 的设计思想,整体架构如下:
+---------------------+
| 应用层 (User) | ← 可直接调用 I²C 接口(如 i2c-tools)
+---------------------+
| I²C 设备驱动 | ← 如 lm75_driver、at24c08_driver
| (Client Driver) |
+---------------------+
| I²C 核心层 | ← 内核提供,负责匹配 device 与 driver,
| (I²C Core) | 管理 adapter 与 client 的绑定
+---------------------+
| I²C 总线驱动 | ← 如 i2c-s3c2410、i2c-gpio
| (Adapter Driver) |
+---------------------+
| 硬件 (HW) | ← I²C 控制器 + 外设(如 LM75 接在 I²C1 上)
+---------------------+
核心实体说明
实体 | 说明 |
---|---|
I²C 总线驱动(Adapter / Bus Driver) | 实现 I²C 物理总线的读写能力(如 SCL/SDA 控制、时序生成),操作对象是 I²C 总线硬件(如 iic0、iic1、iic2) |
I²C 设备驱动(Client Driver) | 针对具体 I²C 外设(如 lm75、at24c08)实现其功能逻辑,操作对象是 I²C 从设备 |
platform driver / device | 基于 platform bus 框架,用于描述和驱动 I²C 控制器硬件(即 I²C adapter) |
i2c_adapter | I²C 适配器(也称 I²C 控制器或主机控制器),负责管理一条 I²C 总线的通信 |
i2c_algorithm | 定义 I²C 总线通信规则(如时序生成方式),是 master_xfer() 的“说明书” |
master_xfer() | 基于 algorithm 实现具体的数据收发,是“执行器”,通过解析 i2c_msg 数组完成传输 |
i2c_client | 代表 I²C 从设备,包含设备地址、所属 adapter 等信息 |
i2c_msg | 描述一次 I²C 传输的消息结构体 |
✅ 关键理解:
- 总线驱动操作“总线”(如 iic0、iic1)
- 设备驱动操作“设备”(如 lm75、at24c08)
- 设备驱动通过
client->adapter->algo->master_xfer()
调用底层总线能力
二、I²C 设备驱动开发流程(以 LM75 为例)
步骤 1:设备树(DTS)中声明设备
&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";lm75@48 {compatible = "ti,lm75"; // 必须包含厂商前缀reg = <0x48>; // I²C 从设备地址};
};
操作命令:
vim arch/arm/boot/dts/pt.dts
make pt.dtb
cp arch/arm/boot/dts/pt.dtb ~/tftpboot
⚠️ 注意:
compatible
字符串必须与驱动中的匹配表一致,否则 probe 不会触发。
步骤 2:编写内核设备驱动(lm75_driver1.c)
#include <linux/init.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/string.h>
#include <asm/uaccess.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/of.h>#define DEV_NAME "lm75"
static struct i2c_client * lm75_client; // 保存 client 指针,供 read/write 使用// 打开设备文件时调用
static int open(struct inode * node, struct file * file)
{return 0;
}// 读取温度数据
static ssize_t read(struct file * file, char __user * buf, size_t len, loff_t * loff)
{// iic read/writeint ret = 0;unsigned char data[2] = {0};struct i2c_msg msg;// 第一步:写寄存器地址(0x00,温度寄存器)msg.addr = lm75_client->addr; // 从 client 获取设备地址msg.flags = 0; // 写操作msg.len = 1; // 写1字节地址msg.buf = data; // data[0] 默认为0,即寄存器0ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);if(ret < 0)return ret;// 第二步:读取2字节温度数据msg.addr = lm75_client->addr;msg.flags = I2C_M_RD; // 读操作标志msg.len = 2; // 读2字节msg.buf = data; // 接收缓冲区ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);if(ret < 0)return ret;// 将内核数据拷贝到用户空间ret = copy_to_user(buf, data, 2); return ret;
}// 写操作(本例未实现)
static ssize_t write(struct file * file, const char __user * buf, size_t len, loff_t * loff)
{return 0;
}// 关闭设备文件
static int close(struct inode * node, struct file * file)
{return 0;
}// 文件操作结构体
static struct file_operations fops =
{.owner = THIS_MODULE,.open = open,.read = read,.write = write,.release = close
};// misc 设备结构体(自动分配次设备号)
static struct miscdevice misc =
{.minor = MISC_DYNAMIC_MINOR,.name = DEV_NAME,.fops = &fops
};// 设备匹配成功时调用(probe)
static int probe(struct i2c_client * pclient, const struct i2c_device_id * device_id)
{int ret = misc_register(&misc);if(ret < 0)goto err_misc;lm75_client = pclient; // 保存 client 指针printk("lm75 probe slave addr = 0x%x ...\n", lm75_client->addr);return 0;err_misc:printk("misc_register failed\n");return ret;
}// 设备移除时调用
static int remove(struct i2c_client * pclient)
{misc_deregister(&misc);printk("lm75 remove ...\n");return 0;
}// 支持的设备列表(用于匹配)
static const struct i2c_device_id lm75_table[] =
{{.name = "lm75"},{}
};// I²C 驱动结构体
static struct i2c_driver lm75_driver =
{.probe = probe,.remove = remove,.driver = {.name = DEV_NAME,},.id_table = lm75_table
};// 模块初始化
static int __init lm75_driver_init(void)
{int ret = i2c_add_driver(&lm75_driver);if(ret < 0)goto err_i2c_add;printk("lm75_driver_init ...\n");return 0;err_i2c_add:return ret;
}// 模块退出
static void __exit lm75_driver_exit(void)
{i2c_del_driver(&lm75_driver);printk("lm75_driver_exit ...\n");
}module_init(lm75_driver_init);
module_exit(lm75_driver_exit);
MODULE_LICENSE("GPL");
代码功能说明:
- 通过
miscdevice
注册/dev/lm75
设备节点 - 在
probe
中保存i2c_client
指针 read
函数实现标准 I²C 读流程:先写寄存器地址 0x00,再读 2 字节温度数据- 使用
master_xfer()
直接调用底层总线驱动的传输函数(不推荐,应使用i2c_transfer()
,但此处保留原代码)
编译与加载命令:
make modules
cp drivers/char/lm75_driver1.ko ~/nfs/imx6/rootfs
insmod lm75_driver1.ko
理想运行结果:
/ # insmod lm75_driver1.ko
lm75 probe slave addr = 0x48 ...
lm75_driver_init ...
/ # ./lm75_app1
temp = 27.5
temp = 28.0
temp = 28.5
...
/ # rmmod lm75_driver1
lm75 remove ...
lm75_driver_exit ...
步骤 3:编写用户空间测试程序(lm75_app1.c)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>int main(int argc, const char *argv[])
{int fd = open("/dev/lm75", O_RDWR); // 打开驱动创建的设备节点if(fd < 0){perror("open lm75 ");return -1;}unsigned char data[2] = {0};while(1){int ret = read(fd, data, 2); // 读取2字节原始温度数据// 解析:LM75 温度 = (16位值 >> 7) * 0.5°Cfloat temp = (((data[0] << 8) | data[1]) >> 7) * 0.5;printf("temp = %.1f\n", temp);sleep(2);}close(fd);return 0;
}
编译命令:
arm-linux-gnueabihf-gcc lm75_app1.c -o lm75_app1
💡 注意:此程序依赖内核驱动创建的
/dev/lm75
。若驱动未加载,open 会失败。
三、应用层直接操作 I²C 总线(验证阶段)
在编写驱动前,必须先用应用层程序验证硬件通信。
#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[])
{int fd = open("/dev/i2c-0", O_RDWR); // 注意:此处应为 i2c-1(若接在 I2C1)if(fd < 0){perror("open i2c-0");return -1;}ioctl(fd, I2C_SLAVE, 0x48); // 设置从设备地址为 0x48while(1){unsigned char buf[2] = {0};// 第一步:写寄存器地址(0x00)int ret = write(fd, buf, 1); // buf[0]=0printf("ret = %d\n", ret);// 第二步:读取2字节数据read(fd, buf, sizeof(buf));float temp = (((buf[0] << 8) | buf[1]) >> 7) * 0.5;printf("temp = %.1f\n", temp);sleep(3);}close(fd);return 0;
}
编译命令:
arm-linux-gnueabihf-gcc lm75_app1.c -o lm75_app1
⚠️ 常见错误:
- 总线编号错误(如设备接在 i2c-1,却打开 i2c-0)
- 未先写寄存器地址(直接 read 会读到不确定位置的数据)
- 硬件焊接错误(如 LM75 引脚反接)
调试工具:
i2cdetect -l # 列出所有 I²C 总线
i2cdetect -y 1 # 扫描 i2c-1 上的设备
dmesg # 查看内核日志
四、AT24C08 EEPROM 驱动开发要点
关键通信逻辑
-
地址长度:
- AT24C08 有 1024 字节(1K)空间,需 10 位地址
- 地址 ≤ 255(0xFF):发送 1 字节地址
- 地址 ≥ 256(如 0x100):必须发送 2 字节地址(高8位 + 低8位)
-
写操作流程:
- 发送设备地址 + 写标志
- 发送内存地址(1 或 2 字节)
- 发送数据(注意页写限制,通常一页 8/16 字节)
- 等待 5ms(EEPROM 内部写周期)
-
读操作流程(随机读):
- 先写:设备地址 + 写 → 内存地址
- 再读:设备地址 + 读 → 读取数据
调试建议
- 先在应用层验证:
vim i2c1_at24c08.c arm-linux-gnueabihf-gcc i2c1_at24c08.c -o at24c08_app
- 闭环测试:写入 0x55 → 读回 → 比对
- 加入延时:写后
msleep(5)
- 检查地址字节数:高地址必须发 2 字节
✅ 经验总结:
“逻辑都没通,你怎么写驱动?写多少条都没用。”
—— 先通逻辑,再写驱动;先验证通信,再封装功能
五、常见问题与排查
问题 | 可能原因 | 解决方法 |
---|---|---|
probe 未触发 | compatible 不匹配 | DTS 中写 "ti,lm75" ,驱动中匹配表写 "lm75" (或完整字符串) |
读取数据为 0 | 地址未写、总线错误、硬件问题 | 用 i2cdetect 扫描;检查焊接;示波器抓波形 |
模块加载失败 | 已加载、符号冲突 | rmmod 后重试;必要时重启 |
第二次通信失败 | 未正确处理 Repeated START | 确保读操作前完整执行“写地址”流程 |
写入无效 | 未等待 EEPROM 写完成 | 写后加 msleep(5) |
六、工程理念与最佳实践
1. 分层思想
- 总线驱动(adapter):SoC 厂商提供,无需重写
- 核心层(i2c-core):内核提供统一 API(如
i2c_transfer()
) - 设备驱动(client):开发者只需实现设备特定逻辑
2. 驱动封装价值
对比项 | 应用层直接操作 | 内核驱动封装 |
---|---|---|
硬件知识要求 | 高 | 低 |
复用性 | 差 | 高 |
抽象程度 | 无 | 高 |
维护成本 | 高 | 低 |
🌟 核心理念:驱动的目标是 让不懂硬件的人也能正确使用设备
3. 内核开发注意事项
- 避免浮点数:内核中不应使用
float
(无 FPU 或性能差) - 推荐返回原始整型数据(如 16 位有符号整数),由用户空间解析
- 务必检查
i2c_transfer()
返回值(应等于消息数量)
七、总结
- I²C 驱动 = 总线驱动 + 设备驱动
- 开发流程:设备树声明 → 应用层验证 → 编写驱动 → 测试
- 关键匹配:DTS 中
compatible
必须与驱动id_table
一致 - 数据读取:先写寄存器地址,再读数据(LM75);先写内存地址,再读/写(AT24C08)
- 调试原则:先通逻辑,再写驱动;硬件问题优先排查
“不是重复造轮子,而是把轮子装到车上。”
—— 内核提供 I²C 通信“轮子”,开发者只需将设备逻辑“装上车”