IMX6ULL LM75温度传感器I2C驱动开发:从子系统架构到专属驱动实现
LM75是一款经典的I2C总线数字温度传感器,广泛应用于嵌入式系统的温度采集场景。本次基于IMX6ULL开发板,深入解析Linux I2C子系统 架构,并实现LM75的专属I2C设备驱动,同时对比直接操作I2C适配器 和操作专属设备节点两种应用层访问方式,让开发者理解Linux I2C驱动开发的标准流程和核心要点。
本文所有代码基于Linux 4.1.15内核(NXP官方IMX6ULL适配版本),驱动采用Linux标准I2C驱动框架 开发,结合杂项设备(miscdevice) 简化字符设备注册流程,遵循硬件与驱动分离的设计思想,通过设备树完成驱动与硬件的匹配,是嵌入式Linux I2C驱动开发的典型实战案例。
一、核心知识点回顾
在解析代码前,先梳理本次开发涉及的Linux I2C子系统及驱动开发核心知识点,也是嵌入式I2C驱动开发的高频考点:
1. Linux I2C子系统分层架构
Linux I2C子系统分为四层,自上而下各司其职,核心是将硬件I2C控制器与外设设备驱动分离,简化开发,架构如下(结合实战笔记):
| 层级 | 核心组成 | 功能说明 |
|---|---|---|
| 应用层 | 应用程序、/dev/i2c-x、/dev/xxx | 访问I2C设备,可直接操作I2C适配器节点/dev/i2c-x,或操作专属设备节点/dev/lm75 |
| I2C核心层 | I2C Core | 提供统一的I2C操作接口(i2c_msg/master_xfer),管理I2C适配器和设备,完成驱动匹配 |
| I2C总线驱动层 | I2C Adapter/总线驱动 | 适配硬件I2C控制器(如IMX6ULL的I2C0/I2C1),实现I2C通信的底层时序(起始/停止/读写) |
| 硬件层 | I2C控制器、LM75外设 | 物理硬件,IMX6ULL的I2C控制器作为I2C主设备 ,LM75作为I2C从设备,通过I2C总线通信 |
2. I2C子系统核心概念
- I2C Adapter(I2C适配器) :对应硬件I2C控制器,内核为每个I2C控制器创建一个适配器,生成设备节点
/dev/i2c-x(x为适配器编号),是I2C主设备的软件抽象; - I2C Client(I2C设备):对应I2C从设备(如LM75),由设备树解析或手动创建,关联到指定的I2C适配器,包含从设备地址、设备信息等;
- I2C Msg(I2C消息):Linux描述I2C单次通信的结构体,包含从设备地址、读写标志、数据长度、数据缓冲区,是I2C数据传输的核心载体;
- master_xfer :I2C数据传输的核心函数,由I2C适配器的算法层实现,用于发送
i2c_msg完成实际的I2C读写操作。
3. I2C驱动的两种匹配方式
Linux I2C驱动支持设备树匹配 和ID表匹配 ,优先级为设备树匹配 > ID表匹配,是驱动与硬件绑定的核心:
- 设备树匹配:通过
of_device_id的compatible属性与设备树节点的compatible匹配; - ID表匹配:通过
i2c_device_id的name属性与I2C Client的name匹配。
4. 杂项设备(miscdevice)
本次驱动依然使用杂项设备简化字符设备开发,主设备号固定为10 ,通过MISC_DYNAMIC_MINOR动态分配次设备号,无需手动申请设备号,只需实现file_operations即可快速创建专属设备节点(/dev/lm75),适合LM75这类简单的I2C外设。
二、两种应用层访问LM75的方式对比
LM75的I2C从设备地址默认为0x48 (可通过硬件引脚修改),温度数据为2字节,按特定格式解析即可得到实际温度(解析公式:t = 0.5 * ((temp[0] << 8 | temp[1]) >> 7))。
应用层有两种访问LM75的方式,分别对应无专属驱动 和有专属驱动的场景,以下分别解析代码并对比优缺点。
方式1:直接操作I2C适配器节点/dev/i2c-0
无需为LM75开发专属驱动,通过Linux内核提供的i2c-dev驱动生成的适配器节点/dev/i2c-x直接访问,核心是通过ioctl设置从设备地址,再通过read/write完成I2C通信。
核心代码解析
c
#include <linux/i2c-dev.h> // 必须包含I2C-dev头文件
int main(int argc, const char *argv[])
{
int fd = open("/dev/i2c-0", O_RDWR); // 打开I2C0适配器节点
if(fd < 0) { perror("open iic failed"); return 1; }
ioctl(fd, I2C_SLAVE, 0x48); // 关键:设置I2C从设备地址为0x48
unsigned char temp[2] = {0};
while(1)
{
int ret = read(fd, temp, sizeof temp); // 直接读2字节温度数据
// 解析LM75温度数据
float t = 0.5 * ((temp[0] << 8 | temp[1]) >> 7);
printf("t = %.1f\n", t);
sleep(1);
}
close(fd);
return 0;
}
关键函数说明
ioctl(fd, I2C_SLAVE, 0x48):向I2C适配器节点发送I2C_SLAVE命令,设置后续通信的I2C从设备地址为0x48,是直接操作适配器的核心;read(fd, temp, 2):内核的i2c-dev驱动会将该读操作封装为I2C读消息,完成与LM75的实际I2C通信。
方式2:操作LM75专属设备节点/dev/lm75
为LM75开发专属I2C驱动,驱动创建专属设备节点/dev/lm75,应用层直接对该节点进行read/write操作,无需关心I2C总线细节 ,是嵌入式开发的标准方式。
核心代码解析
c
int main(int argc, const char *argv[])
{
int fd = open("/dev/lm75", O_RDWR); // 打开LM75专属设备节点
if(fd < 0) { perror("open iic failed"); return 1; }
unsigned char temp[2] = {0};
while(1)
{
int ret = read(fd, temp, sizeof temp); // 直接读温度数据,无I2C相关操作
float t = 0.5 * ((temp[0] << 8 | temp[1]) >> 7); // 解析温度
printf("t = %.1f\n", t);
sleep(1);
}
close(fd);
return 0;
}
核心优势
- 应用层屏蔽了I2C总线细节,开发更简洁,无需知道I2C从设备地址、适配器编号等硬件信息;
- 驱动层统一管理I2C通信,便于后续功能扩展(如温度阈值判断、中断处理);
- 符合分层设计思想,应用层专注业务逻辑,驱动层专注硬件操作。
两种方式核心对比
| 特性 | 直接操作/dev/i2c-0 |
操作专属/dev/lm75 |
|---|---|---|
| 开发成本 | 低(无需开发专属驱动) | 稍高(需开发I2C驱动) |
| 应用层复杂度 | 高(需处理I2C总线细节) | 低(屏蔽硬件细节) |
| 可维护性 | 差(硬件信息耦合到应用层) | 好(硬件细节封装在驱动层) |
| 扩展性 | 差(难以扩展硬件相关功能) | 好(驱动层可灵活扩展) |
| 适用场景 | 快速调试、简单测试 | 产品开发、正式项目 |
三、LM75专属I2C驱动核心代码解析
本次LM75驱动基于Linux标准I2C驱动框架 开发,结合杂项设备 简化字符设备注册,核心流程为:I2C驱动注册 → 设备树匹配 → probe函数执行 → 注册杂项设备 → 实现read函数完成I2C数据传输,驱动代码结构清晰,是I2C外设驱动的标准模板。
1. 头文件与全局变量定义
c
#include <linux/i2c.h> // I2C子系统核心头文件,必须包含
// 其他通用头文件(init/printk/fs/miscdevice/of.h等)省略
#define DEV_NAME "lm75"
static struct i2c_client * lm75_client; // 保存I2C Client指针,核心全局变量
关键说明 :i2c_client是I2C设备的软件抽象,包含了LM75的从设备地址、关联的I2C适配器、设备树节点等核心信息,驱动中所有I2C操作都基于该指针。
2. 字符设备操作集实现(核心:read函数)
实现open/read/close函数,read函数是驱动的核心 ,完成与LM75的I2C数据传输,write函数暂未实现(LM75写操作用于配置阈值,可后续扩展)。
c
// open/close仅打印日志,无实际逻辑
static int open(struct inode * inode, struct file * file) {
printk("lm75 open\n");
return 0;
}
static int close(struct inode * inode, struct file * file) {
printk("lm75 close\n");
return 0;
}
// 核心:read函数,完成I2C读温度数据传输
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff) {
int ret = 0;
unsigned char temp[2] = {0};
// 定义I2C消息结构体,描述一次I2C读操作
struct i2c_msg msg = {
.addr = lm75_client->addr, // LM75从设备地址,从i2c_client获取
.flags = I2C_M_RD, // 标志位:I2C_M_RD表示读操作,写操作则置0
.len = 2, // 数据长度:LM75温度数据为2字节
.buf = temp // 数据缓冲区:存储读取的温度数据
};
// 执行I2C数据传输:通过I2C适配器的master_xfer函数发送i2c_msg
lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);
// 将内核空间的温度数据拷贝到用户空间
ret = copy_to_user(buf, temp, sizeof(temp));
return sizeof(temp); // 返回读取的字节数
}
// 字符设备操作集
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = open,
.read = read,
.write = write,
.release = close
};
核心结构体/函数解析
struct i2c_msg:Linux描述I2C单次通信的最小单元 ,是I2C数据传输的核心,关键成员:.addr:I2C从设备地址,从i2c_client中获取,避免硬编码,适配性更强;.flags:I2C_M_RD表示读操作,写操作时置0即可;.len/.buf:指定通信的数据长度和数据缓冲区。
master_xfer:I2C适配器的算法层函数,实际完成I2C硬件时序的操作,由I2C总线驱动实现(如IMX6ULL的I2C控制器驱动),应用层和设备驱动层无需关心底层时序细节,体现了Linux子系统的分层设计优势;copy_to_user:内核空间向用户空间拷贝数据的标准函数,禁止直接访问用户层地址,避免内存访问错误。
3. 杂项设备定义
使用杂项设备快速创建专属设备节点/dev/lm75,无需手动申请设备号,简化开发:
c
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
.name = DEV_NAME, // 设备名,对应/dev/lm75
.fops = &fops // 关联字符设备操作集
};
4. I2C驱动核心实现(probe/remove/匹配表)
Linux I2C驱动的核心是实现probe和remove函数,以及设备树匹配表/ID表,当I2C核心层完成驱动与设备(i2c_client) 的匹配后,自动执行probe函数完成驱动初始化。
c
// probe函数:I2C设备与驱动匹配成功后执行,完成驱动初始化
static int probe(struct i2c_client * client, const struct i2c_device_id * device) {
int ret = misc_register(&misc_dev); // 注册杂项设备,创建/dev/lm75
if(ret < 0)
goto err_misc_register;
lm75_client = client; // 保存I2C Client指针到全局变量,供后续操作使用
printk("lm75 probe\n");
return 0;
// 错误处理:释放已申请资源
err_misc_register:
printk("lm75 probe misc_register failed\n");
return ret;
}
// remove函数:驱动卸载时执行,释放资源
static int remove(struct i2c_client * client) {
misc_deregister(&misc_dev); // 注销杂项设备
printk("lm75 remove\n");
return 0;
}
// 1. ID表匹配:无设备树时使用
static const struct i2c_device_id lm75_table[] = {
{.name = "ti,lm75"},
{} // 空元素结束,必须添加
};
// 2. 设备树匹配:现代Linux驱动标准方式,优先级更高
static const struct of_device_id of_lm75_table[] = {
{.compatible = "ti,lm75"}, // 与设备树的compatible属性匹配
{}
};
// 定义I2C驱动结构体
static struct i2c_driver lm75_driver = {
.probe = probe, // 匹配成功后的初始化函数
.remove = remove, // 驱动卸载函数
.driver = {
.name = DEV_NAME,
.of_match_table = of_lm75_table // 关联设备树匹配表
},
.id_table = lm75_table // 关联ID表匹配
};
关键说明
probe函数 :I2C驱动的初始化入口,核心工作是注册杂项设备 和保存i2c_client指针,后续所有I2C操作都基于该指针;- 双匹配机制 :同时支持设备树匹配 和ID表匹配,兼顾现代设备树驱动和传统无设备树驱动,设备树匹配优先级更高;
of_device_id的compatible:必须与设备树中LM75节点的compatible属性完全一致 ,否则I2C核心层无法完成匹配,probe函数不会执行。
5. 模块入口与出口
遵循Linux内核模块的标准写法,完成I2C驱动的注册与注销,是驱动的入口和出口:
c
// 模块入口:注册I2C驱动
static int __init lm75_driver_init(void) {
int ret = i2c_add_driver(&lm75_driver); // 注册I2C驱动到内核I2C核心层
if(ret < 0)
goto err_i2c_add;
printk("lm75_driver_init ...\n");
return 0;
err_i2c_add:
printk("lm75_driver_init failed...\n");
return ret;
}
// 模块出口:注销I2C驱动
static void __exit lm75_driver_exit(void) {
i2c_del_driver(&lm75_driver); // 从内核注销I2C驱动
printk("lm75_driver_exit ...\n");
}
// 模块入口/出口宏
module_init(lm75_driver_init);
module_exit(lm75_driver_exit);
MODULE_LICENSE("GPL"); // 必须声明GPL协议,否则模块加载失败
关键函数:
i2c_add_driver:将自定义的I2C驱动注册到Linux I2C核心层,核心层会遍历系统中的I2C Client,完成驱动与设备的匹配;i2c_del_driver:从I2C核心层注销驱动,自动调用remove函数释放资源。
四、LM75设备树配置
本次驱动采用设备树匹配 的方式,需在IMX6ULL的设备树文件(如imx6ull-alientek-emmc.dts)中添加LM75的I2C设备节点,指定LM75的所属I2C适配器 、I2C从设备地址 、兼容属性 等硬件信息,内核启动时会解析该节点并创建i2c_client。
核心设备树节点配置
LM75挂载到IMX6ULL的I2C0 适配器下,从设备地址为0x48,配置如下:
dts
&i2c0 { // 关联IMX6ULL的I2C0适配器,需确保该适配器已启用(status = "okay")
clock-frequency = <100000>; // I2C通信速率:100KHz
status = "okay"; // 启用I2C0适配器
lm75@48 { // LM75设备节点,@48表示I2C从设备地址为0x48
compatible = "ti,lm75"; // 与驱动的of_device_id匹配,必须一致
reg = <0x48>; // 明确指定I2C从设备地址,与@48一致
status = "okay"; // 启用该LM75设备节点
};
};
配置关键说明
- 节点必须挂载到实际的I2C适配器节点下(如
&i2c0/&i2c1),与硬件接线一致; compatible = "ti,lm75":必须与驱动中的of_device_id完全一致,是驱动匹配的核心;reg = <0x48>:I2C从设备地址,必须与LM75的硬件地址一致(默认0x48);- 需确保I2C适配器节点的
status = "okay",否则适配器未启用,LM75节点无法被解析。
设备树编译与烧录
设备树修改后,需重新编译设备树并烧录到开发板:
bash
# 进入Linux内核源码目录
cd /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga
# 配置交叉编译环境
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 编译设备树
make dtbs -j4
# 编译后生成的设备树文件:arch/arm/boot/dts/imx6ull-alientek-emmc.dtb
将编译后的dtb文件拷贝到开发板的boot分区,重启开发板即可完成设备树生效。
五、驱动编译与完整测试流程
1. 驱动编译:编写Makefile
编写适用于IMX6ULL的交叉编译Makefile,指定Linux内核源码路径和交叉编译工具链:
makefile
obj-m += lm75_drv.o # 驱动源码文件为lm75_drv.c,生成lm75_drv.ko
# Linux内核源码路径,根据自己的开发环境修改
KERNELDIR := /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga
PWD := $(shell pwd)
# 交叉编译工具链,IMX6ULL为arm-linux-gnueabihf-
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm
# 编译驱动模块
all:
make -C $(KERNELDIR) M=$(PWD) modules ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE)
# 清理编译残留
clean:
make -C $(KERNELDIR) M=$(PWD) clean
编译命令:
bash
make -j4 # 根据CPU核心数调整,加速编译
编译成功后,生成lm75_drv.ko驱动模块文件。
2. 应用层程序编译
交叉编译两种应用层测试程序(lm75_test1.c:操作/dev/i2c-0;lm75_test2.c:操作/dev/lm75):
bash
# 编译方式1的测试程序
arm-linux-gnueabihf-gcc lm75_test1.c -o lm75_test1
# 编译方式2的测试程序
arm-linux-gnueabihf-gcc lm75_test2.c -o lm75_test2
将编译后的lm75_test1、lm75_test2和驱动模块lm75_drv.ko拷贝到开发板的根文件系统(如/home/root/)。
3. 开发板完整测试步骤
步骤1:确认I2C适配器节点存在
开发板重启后,查看I2C适配器节点是否生成,确认I2C控制器驱动正常:
bash
ls /dev/i2c* # 正常输出:/dev/i2c-0 /dev/i2c-1(根据启用的适配器而定)
步骤2:加载LM75专属驱动模块
bash
# 加载驱动模块
insmod lm75_drv.ko
# 查看已加载模块,确认驱动加载成功
lsmod | grep lm75
# 查看专属设备节点,确认misc设备注册成功
ls /dev/lm75
# 查看内核打印,确认probe函数执行成功(驱动与设备树匹配成功)
dmesg | grep lm75
若内核打印lm75 probe,表示驱动与设备树匹配成功,驱动初始化完成。
步骤3:测试方式2(操作专属设备/dev/lm75)
直接运行方式2的测试程序,查看温度打印:
bash
./lm75_test2
正常情况下,终端会每隔1秒打印一次采集到的温度(如t = 25.0、t = 26.5),表示驱动工作正常。
步骤4:测试方式1(直接操作/dev/i2c-0)
无需卸载驱动,直接运行方式1的测试程序,验证直接操作适配器的有效性:
bash
./lm75_test1
终端同样会每隔1秒打印温度数据,两种方式均可正常采集温度。
步骤5:卸载驱动模块
测试完成后,卸载驱动模块,释放资源:
bash
rmmod lm75_drv # 无需加.ko后缀
# 查看内核打印,确认remove函数执行
dmesg | grep lm75
内核打印lm75 remove,表示驱动卸载成功,/dev/lm75设备节点会被自动删除。
六、开发总结与关键注意事项
本次LM75温度传感器I2C驱动开发是嵌入式Linux I2C驱动的标准实战案例,涵盖了I2C子系统的核心知识点和驱动开发的通用流程,以下是开发总结和必须注意的关键事项:
1. 开发总结
- Linux I2C子系统的分层设计 是驱动开发的核心,设备驱动层无需关心底层I2C时序,只需通过
i2c_msg和master_xfer完成数据传输,大幅简化开发; - I2C驱动的双匹配机制 :设备树匹配是现代Linux驱动的标准方式,需保证
compatible属性的一致性,ID表匹配用于兼容无设备树的传统场景; - 杂项设备是简单外设的最佳选择,无需手动申请设备号,快速创建专属设备节点,简化字符设备开发;
- 应用层两种访问方式各有适用场景,快速调试用直接操作适配器,产品开发用专属设备节点,遵循分层设计思想;
i2c_client是I2C设备驱动的核心,驱动初始化时需保存该指针,后续所有操作(从设备地址、适配器关联)都基于该指针。
2. 关键注意事项
- 设备树配置的一致性 :
compatible属性、I2C从设备地址、所属适配器必须与硬件和驱动完全一致,否则驱动无法匹配,probe函数不会执行; - I2C消息的正确配置 :
i2c_msg的addr从i2c_client获取,避免硬编码,flags置I2C_M_RD表示读操作,写操作置0,数据长度需与外设一致; - 内核与用户层数据交互 :必须使用
copy_to_user/copy_from_user,禁止直接访问用户层地址,避免内存访问错误; - GPL协议声明 :内核模块必须添加
MODULE_LICENSE("GPL"),否则模块加载失败; - 硬件接线与I2C速率 :确保LM75的SDA/SCL引脚与IMX6ULL的I2C适配器引脚正确连接,设备树中
clock-frequency需与外设支持的速率一致(LM75支持100KHz/400KHz); - 资源释放 :驱动的
probe和remove函数需成对申请和释放资源,错误处理时从后往前释放,避免内存泄漏和资源占用。
七、拓展延伸
本次驱动仅实现了LM75的基础温度采集功能,可在此基础上进行功能扩展,进一步加深I2C驱动开发的理解:
- 实现LM75的写操作:通过I2C写操作配置LM75的温度阈值(上限/下限),实现超温报警功能;
- 添加中断处理:利用LM75的超温中断引脚,结合Linux中断子系统,实现超温中断触发的报警功能;
- 实现多设备支持:在设备树中添加多个LM75节点(不同地址),修改驱动支持多个LM75设备的同时采集;
- 替换为字符设备:将杂项设备替换为标准字符设备,手动申请设备号,理解字符设备的完整注册流程;
- 添加数据校验:在驱动层添加温度数据的合法性校验,避免无效数据传递到应用层。