I2C 总线框架下LM75A 温度传感器 Linux驱动开发:

在嵌入式 Linux 开发中,温度传感器是非常常见的外设,LM75A 作为一款低成本、高精度的 I2C 接口温度传感器,被广泛应用于工业控制、消费电子等场景。本文将详细讲解 LM75A 的 Linux I2C 驱动开发、设备树配置、应用层测试全部流程。

一、基础背景:LM75A 与 Linux I2C 驱动框架

1.1 LM75A 核心特性

LM75A 是 NXP(原 TI)推出的数字温度传感器,核心特性如下:

  • 通信接口:I2C(支持 100kHz/400kHz 速率);
  • 测量范围:-55℃ ~ +125℃;
  • 精度:0.5℃步进(即最小分辨率为 0.5℃);
  • 寄存器结构:温度寄存器(0x00,只读)为 16 位数据,高 9 位有效(第 8 位为符号位,其余为温度值),温度值 = 读取值 × 0.5℃。

1.2 Linux I2C 驱动框架

Linux 内核为 I2C 外设提供了成熟的驱动框架,核心分为两层:

  • I2C 适配器层:对应硬件 I2C 控制器(如 SOC 的 I2C1),由内核自带的总线驱动管理,负责底层 I2C 通信时序;
  • I2C 设备驱动层 :对应具体外设(如 LM75A),开发者需实现probe(设备匹配)、remove(设备卸载)等函数,同时通过miscdevice(杂项设备)封装为字符设备,简化用户层访问。

二、设备树配置

Linux 设备树(Device Tree)是描述硬件信息的关键,需为 LM75A 配置对应的 I2C 设备节点,以下是基于 I2C1 控制器的配置示例:

dts

复制代码
&i2c1 {
    clock-frequency = <100000>;  // I2C通信速率:100kHz
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>; // 绑定I2C1引脚配置
    status = "okay";             // 启用I2C1控制器

    // LM75A设备节点
    lm75a@0x48{
        compatible = "pute,putelm75a"; // 驱动匹配关键字,需与驱动中of_match_table一致
        reg = <0x48>;                 // LM75A默认I2C从地址(可通过硬件ADDR引脚修改)
    };
};

关键配置说明:

  1. clock-frequency:指定 I2C 总线通信速率,需与传感器支持的速率匹配(LM75A 支持 100kHz/400kHz);
  2. compatible:设备与驱动匹配的核心标识,驱动中of_match_table需完全匹配该值(本文中为pute,putelm75a);
  3. reg:I2C 外设的从地址,LM75A 默认从地址为 0x48(ADDR 引脚接 GND),若 ADDR 接 VCC 则为 0x49,需根据硬件调整。

三、驱动代码(lm75a_drv.c)

驱动代码是核心,本文驱动基于 Linux 杂项设备框架封装,既复用 I2C 驱动框架,又简化字符设备注册流程,以下逐模块解析:

3.1 头文件与全局变量

c

运行

复制代码
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/i2c.h>

static struct i2c_client *plm75a_client; // 保存匹配后的I2C设备客户端
  • 头文件:linux/i2c.h提供 I2C 驱动核心接口,linux/miscdevice.h提供杂项设备注册接口,asm/uaccess.h提供内核与用户空间数据拷贝函数;
  • 全局变量plm75a_client:保存probe函数匹配成功后的 I2C 设备句柄,后续 I2C 通信需通过该句柄获取适配器、从地址等信息。

3.2 核心读函数:lm75a_read

该函数实现从 LM75A 温度寄存器读取数据,并将结果拷贝到用户空间:

c

运行

复制代码
static ssize_t lm75a_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    struct i2c_msg sendmsg;  // I2C写消息(发送寄存器地址)
    struct i2c_msg recvmsg;  // I2C读消息(读取温度数据)
    unsigned long nret = 0;
    unsigned char tmpbuff[2] = {0}; // 数据缓冲区
    unsigned short tmpval = 0;

    // 初始化I2C消息结构体
    memset(&sendmsg, 0, sizeof(sendmsg));
    memset(&recvmsg, 0, sizeof(recvmsg));

    // 1. 发送要读取的寄存器地址:0x00(温度寄存器)
    sendmsg.addr = plm75a_client->addr; // LM75A从地址(0x48)
    tmpbuff[0] = 0x00;                  // 温度寄存器地址
    sendmsg.buf = tmpbuff;
    sendmsg.len = 1;                    // 发送1字节(寄存器地址)
    // 调用I2C适配器的传输函数发送消息
    plm75a_client->adapter->algo->master_xfer(plm75a_client->adapter, &sendmsg, 1);

    // 2. 读取温度寄存器数据(2字节)
    recvmsg.addr = plm75a_client->addr;
    recvmsg.flags |= I2C_M_RD;          // 标记为读操作
    recvmsg.buf = tmpbuff;
    recvmsg.len = 2;                    // 读取2字节
    plm75a_client->adapter->algo->master_xfer(plm75a_client->adapter, &recvmsg, 1);

    // 3. 数据解析:高9位有效,右移7位得到温度值(0.5℃步进)
    tmpval = ((tmpbuff[0] << 8 | tmpbuff[1]) >> 7);
    // 4. 将内核数据拷贝到用户空间
    nret = copy_to_user(puser, &tmpval, 2);
    if (nret != 0) {
        pr_info("copy_to_user failm75a\n");
        return -1;
    }

    pr_info("Kernel:lm75a read success\n");
    return 0;
}
关键要点:
  • I2C 通信流程:LM75A 读取温度需先发送寄存器地址(0x00),再读取 2 字节数据,这是 I2C 外设的通用读流程;
  • 数据解析:LM75A 温度寄存器为 16 位,高 9 位有效(bit15~bit7),因此将两字节数据合并后右移 7 位,得到以 0.5℃为步进的温度值;
  • copy_to_user:内核空间不能直接访问用户空间地址,需通过该函数完成数据拷贝,返回值为未拷贝的字节数,0 表示成功。

优化建议:原代码直接调用adapter->algo->master_xfer,更推荐使用内核封装的i2c_transfer函数(兼容性更好),示例:

c

运行

复制代码
struct i2c_msg msgs[] = {
    {.addr = plm75a_client->addr, .buf = &reg, .len = 1}, // 写寄存器地址
    {.addr = plm75a_client->addr, .flags = I2C_M_RD, .buf = tmpbuff, .len = 2} // 读数据
};
i2c_transfer(plm75a_client->adapter, msgs, ARRAY_SIZE(msgs));

3.3 字符设备与杂项设备封装

c

运行

复制代码
// 字符设备操作集:仅实现read函数
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = lm75a_read,
};

// 杂项设备结构体:简化字符设备注册(动态分配次设备号)
static struct miscdevice lm75a_misc = {
    .minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
    .name = "lm75a_misc",        // 设备名,对应/dev/lm75a_misc
    .fops = &fops,               // 绑定操作集
};

杂项设备(misc)是 Linux 内核为简化字符设备开发设计的框架,无需手动申请主设备号,只需注册即可生成/dev/lm75a_misc设备文件。

3.4 probe/remove 函数(设备匹配与卸载)

c

运行

复制代码
static int lm75a_probe(struct i2c_client *pclient, const struct i2c_device_id *pid)
{
    int ret = 0;
    plm75a_client = pclient; // 保存I2C客户端句柄

    // 注册杂项设备
    ret = misc_register(&lm75a_misc);
    if (ret != 0) {
        pr_info("misc register failm75a\n");
        goto err_mis_register; // 错误处理:跳转到注销逻辑
    }

    pr_info("Kernel:lm75a probe success\n");
    return 0;

err_mis_register:
    misc_deregister(&lm75a_misc);
    return -1;
}

static int lm75a_remove(struct i2c_client *pdevice)
{
    misc_deregister(&lm75a_misc); // 注销杂项设备
    pr_info("Kernel:lm75a remove success\n");
    return 0;
}
  • probe函数:当设备树中的compatible与驱动匹配时触发,核心逻辑是注册杂项设备;
  • remove函数:驱动卸载时触发,核心逻辑是注销杂项设备,释放资源。

3.5 驱动匹配表与模块入口 / 出口

c

运行

复制代码
// 设备树匹配表:与设备树compatible匹配
static const struct of_device_id lm75a_of_match_table[] = {
    {.compatible = "pute,putelm75a"},
    {},
};

// 非设备树匹配表(备用)
static const struct i2c_device_id lm75a_id_table[] = {
    {.name = "putelm75a"},
    {},
};

// I2C驱动结构体:绑定核心函数与匹配表
static struct i2c_driver lm75a_drv = {
    .probe = lm75a_probe,
    .remove = lm75a_remove,
    .driver = {
        .name = "putelm75a",
        .of_match_table = lm75a_of_match_table, // 设备树匹配
    },
    .id_table = lm75a_id_table, // 非设备树匹配
};

// 模块初始化:注册I2C驱动
static int __init lm75a_drv_init(void)
{
    i2c_add_driver(&lm75a_drv);
    pr_info("lm75a_drv_init success!\n");
    return 0;
}

// 模块退出:注销I2C驱动
static void __exit lm75a_drv_exit(void)
{
    i2c_del_driver(&lm75a_drv);
    pr_info("lm75a_drv_exit success!\n");
    return;
}

module_init(lm75a_drv_init);
module_exit(lm75a_drv_exit);

MODULE_LICENSE("GPL"); // 开源许可证(必须,否则内核加载失败)
MODULE_AUTHOR("pute");
  • 匹配表:of_match_table用于设备树匹配(主流方式),id_table用于非设备树匹配(备用);
  • 模块入口 / 出口:module_init/module_exit是 Linux 内核模块的标准入口,分别注册 / 注销 I2C 驱动;
  • MODULE_LICENSE("GPL"):必须声明开源许可证(如 GPL),否则内核会拒绝加载模块。

四、应用层代码(lm75a_app.c)解析

应用层通过访问/dev/lm75a_misc设备文件读取温度:

c

运行

复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <linux/input.h>

#define KEY_ON  1
#define KEY_OFF 0

void delay_ms(int ms)
{
    usleep(ms * 1000);
}

int main(void)
{
    int fd = 0;
    int ret = 0;
    float temp = 0;
    unsigned short value = 0;

    // 打开杂项设备文件
    fd = open("/dev/lm75a_misc", O_RDONLY);
    if (-1 == fd)
    {
        printf("open error\n");
        return -1;
    }

    // 循环读取温度
    while (1)
    {
        ret = read(fd, &value, sizeof(value)); // 读取2字节温度值
        if (ret < 0)
        {
            printf("read error\n");
            break;
        }
        temp = value * 0.5; // 换算为实际温度(0.5℃步进)
        printf("temp = %.1f\n", temp);
        sleep(1); // 1秒读取一次
    }
    
    close(fd);
    return 0;
}

关键要点:

  • 设备文件打开:O_RDONLY以只读方式打开/dev/lm75a_misc,该文件由驱动中miscdevice注册自动生成;
  • 温度换算:驱动层返回的value是 0.5℃步进的整数值,因此乘以 0.5 得到实际温度(如 value=50 → 25.0℃);
  • 循环读取:通过while(1)实现持续测温,sleep(1)控制读取频率,避免频繁访问内核。

五、编译与测试流程

5.1 驱动编译(Makefile)

编写 Makefile,指定内核源码路径,编译为内核模块:

makefile

复制代码
#目标文件
OBJ := lm75a_drv


#内核路径
kerdir := (填自己的内核路径)

#当前驱动工程目录
curdir := $(shell pwd)

#代码添加到工程编译选项中
obj-m += $(OBJ).o 

all:
	make -C $(kerdir) M=$(curdir) modules
	cp $(OBJ).ko ~/nfs/rootfs

.PHONY:

clean:
	make -C $(kerdir) M=$(curdir) modules clean

distclean:
	make -C $(kerdir) M=$(curdir) modules clean
	rm ~/nfs/rootfs/$(OBJ).ko

执行编译:

bash

运行

复制代码
make 

编译成功后生成lm75a_drv.ko模块文件。

5.2 应用层编译(交叉编译)

若目标平台为 ARM 架构,需使用交叉编译器:

bash

运行

复制代码
自己的交叉编译工具 lm75a_app.c -o lm75a_app

5.3 测试步骤

  1. 加载设备树 :将修改后的设备树编译为.dtb文件,烧录到目标板或动态加载;

  2. 加载驱动模块

    bash

    运行

    复制代码
    insmod lm75a_drv.ko

    查看内核日志(dmesg),若输出lm75a_drv_init success!Kernel:lm75a probe success,说明驱动匹配成功;

  3. 查看设备文件

    bash

    运行

    复制代码
    ls /dev/lm75a_misc -l

    若输出crw-rw---- 1 root root 10, 59 Jan 1 00:00 /dev/lm75a_misc,说明杂项设备注册成功;

  4. 运行应用程序

    bash

    运行

    复制代码
    ./lm75a_app

    正常输出示例:

    plaintext

    复制代码
    temp = 25.0
    temp = 25.0
    temp = 25.5
    ...
  5. 卸载驱动

    bash

    运行

    复制代码
    rmmod lm75a_drv

    查看内核日志,输出Kernel:lm75a remove successlm75a_drv_exit success!,说明卸载成功。

六、常见问题与优化建议

6.1 常见问题排查

  1. probe 函数不执行
    • 检查设备树compatible是否与驱动of_match_table完全一致;
    • 检查 I2C 控制器状态(status = "okay");
    • 检查 LM75A 从地址(reg)是否与硬件一致。
  2. 读取数据错误
    • 用示波器抓 I2C 总线,检查寄存器地址和数据传输是否正确;
    • 检查copy_to_user返回值,确认用户空间数据拷贝是否成功;
    • 核对温度换算逻辑(右移 7 位、乘以 0.5)。
  3. 设备文件不存在
    • 检查misc_register返回值,确认杂项设备注册成功;
    • 检查驱动加载是否有错误日志(dmesg)。。

七、总结

本文完整实现了 LM75A 温度传感器的 Linux I2C 驱动开发,从设备树配置、驱动框架实现、应用层测试全部的流程讲解,核心要点如下:

  1. Linux I2C 驱动开发需遵循 "设备树匹配 + probe 函数初始化 + 字符设备封装" 的核心逻辑;
  2. LM75A 的 I2C 通信需先发送寄存器地址,再读取数据,数据解析需结合传感器寄存器规范;
  3. 杂项设备(misc)是简化字符设备开发的高效方式,无需手动管理主设备号;
  4. 驱动与应用层的数据交互需通过copy_to_user/copy_from_user,避免直接访问用户空间。
相关推荐
专注VB编程开发20年1 小时前
工控成套控制柜厂家 / 自动化小工厂 对外市场价
运维·自动化·工控·上位机开发
片酷2 小时前
【Isaacsim&Isaaclab】安装教程
linux·开发语言·python
Magic@2 小时前
Redis学习[1] ——基本概念和数据类型
linux·开发语言·数据库·c++·redis·学习
microxiaoxiao2 小时前
Aeroshell:2026 年,支持AI的SSH 终端
运维·人工智能·ssh
大腕先生2 小时前
通用分页超详细介绍(附带源代码解析&页面展示效果)
xml·java·linux·服务器·开发语言·前端·idea
feng14562 小时前
稳定性-风险文化建设和风险意识培养
运维·人工智能
红茶要加冰2 小时前
如何安装ubuntu-24.04-live-server-amd64系统
linux·运维·ubuntu
念恒123062 小时前
进程控制---进程等待
linux·c语言
feng14562 小时前
稳定性-资金安全和资损防控
运维·网络·安全