75、 IMX6ULL LM75温度传感器I2C驱动开发

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_idcompatible属性与设备树节点的compatible匹配;
  • ID表匹配:通过i2c_device_idname属性与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中获取,避免硬编码,适配性更强;
    • .flagsI2C_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驱动的核心是实现proberemove函数,以及设备树匹配表/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表匹配
};
关键说明
  1. probe函数 :I2C驱动的初始化入口,核心工作是注册杂项设备保存i2c_client指针,后续所有I2C操作都基于该指针;
  2. 双匹配机制 :同时支持设备树匹配ID表匹配,兼顾现代设备树驱动和传统无设备树驱动,设备树匹配优先级更高;
  3. of_device_idcompatible :必须与设备树中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设备节点
    };
};

配置关键说明

  1. 节点必须挂载到实际的I2C适配器节点下(如&i2c0/&i2c1),与硬件接线一致;
  2. compatible = "ti,lm75":必须与驱动中的of_device_id完全一致,是驱动匹配的核心;
  3. reg = <0x48>:I2C从设备地址,必须与LM75的硬件地址一致(默认0x48);
  4. 需确保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-0lm75_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_test1lm75_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.0t = 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. 开发总结

  1. Linux I2C子系统的分层设计 是驱动开发的核心,设备驱动层无需关心底层I2C时序,只需通过i2c_msgmaster_xfer完成数据传输,大幅简化开发;
  2. I2C驱动的双匹配机制 :设备树匹配是现代Linux驱动的标准方式,需保证compatible属性的一致性,ID表匹配用于兼容无设备树的传统场景;
  3. 杂项设备是简单外设的最佳选择,无需手动申请设备号,快速创建专属设备节点,简化字符设备开发;
  4. 应用层两种访问方式各有适用场景,快速调试用直接操作适配器,产品开发用专属设备节点,遵循分层设计思想;
  5. i2c_client是I2C设备驱动的核心,驱动初始化时需保存该指针,后续所有操作(从设备地址、适配器关联)都基于该指针。

2. 关键注意事项

  1. 设备树配置的一致性compatible属性、I2C从设备地址、所属适配器必须与硬件和驱动完全一致,否则驱动无法匹配,probe函数不会执行;
  2. I2C消息的正确配置i2c_msgaddri2c_client获取,避免硬编码,flagsI2C_M_RD表示读操作,写操作置0,数据长度需与外设一致;
  3. 内核与用户层数据交互 :必须使用copy_to_user/copy_from_user,禁止直接访问用户层地址,避免内存访问错误;
  4. GPL协议声明 :内核模块必须添加MODULE_LICENSE("GPL"),否则模块加载失败;
  5. 硬件接线与I2C速率 :确保LM75的SDA/SCL引脚与IMX6ULL的I2C适配器引脚正确连接,设备树中clock-frequency需与外设支持的速率一致(LM75支持100KHz/400KHz);
  6. 资源释放 :驱动的proberemove函数需成对申请和释放资源,错误处理时从后往前释放,避免内存泄漏和资源占用。

七、拓展延伸

本次驱动仅实现了LM75的基础温度采集功能,可在此基础上进行功能扩展,进一步加深I2C驱动开发的理解:

  1. 实现LM75的写操作:通过I2C写操作配置LM75的温度阈值(上限/下限),实现超温报警功能;
  2. 添加中断处理:利用LM75的超温中断引脚,结合Linux中断子系统,实现超温中断触发的报警功能;
  3. 实现多设备支持:在设备树中添加多个LM75节点(不同地址),修改驱动支持多个LM75设备的同时采集;
  4. 替换为字符设备:将杂项设备替换为标准字符设备,手动申请设备号,理解字符设备的完整注册流程;
  5. 添加数据校验:在驱动层添加温度数据的合法性校验,避免无效数据传递到应用层。
相关推荐
之歆2 小时前
Linux命令完全指南
linux·运维·服务器
甄心爱学习2 小时前
【python】list的底层实现
开发语言·python
独自破碎E2 小时前
BISHI41 【模板】整除分块
java·开发语言
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Springboot图书管理系统为例,包含答辩的问题和答案
java·spring boot·后端
小趴菜不能喝2 小时前
Spring AI 基础实践
数据库·人工智能·spring
Flash.kkl2 小时前
MySQL访问
数据库·mysql
Lsir10110_2 小时前
【Linux】线程初步——线程概念以及接口认识
linux·运维·服务器
edisao2 小时前
第三章 合规的自愿
jvm·数据仓库·python·神经网络·决策树·编辑器·动态规划