在嵌入式 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引脚修改)
};
};
关键配置说明:
clock-frequency:指定 I2C 总线通信速率,需与传感器支持的速率匹配(LM75A 支持 100kHz/400kHz);compatible:设备与驱动匹配的核心标识,驱动中of_match_table需完全匹配该值(本文中为pute,putelm75a);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 = ®, .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 测试步骤
-
加载设备树 :将修改后的设备树编译为
.dtb文件,烧录到目标板或动态加载; -
加载驱动模块 :
bash
运行
insmod lm75a_drv.ko查看内核日志(
dmesg),若输出lm75a_drv_init success!和Kernel:lm75a probe success,说明驱动匹配成功; -
查看设备文件 :
bash
运行
ls /dev/lm75a_misc -l若输出
crw-rw---- 1 root root 10, 59 Jan 1 00:00 /dev/lm75a_misc,说明杂项设备注册成功; -
运行应用程序 :
bash
运行
./lm75a_app正常输出示例:
plaintext
temp = 25.0 temp = 25.0 temp = 25.5 ... -
卸载驱动 :
bash
运行
rmmod lm75a_drv查看内核日志,输出
Kernel:lm75a remove success和lm75a_drv_exit success!,说明卸载成功。
六、常见问题与优化建议
6.1 常见问题排查
- probe 函数不执行 :
- 检查设备树
compatible是否与驱动of_match_table完全一致; - 检查 I2C 控制器状态(
status = "okay"); - 检查 LM75A 从地址(
reg)是否与硬件一致。
- 检查设备树
- 读取数据错误 :
- 用示波器抓 I2C 总线,检查寄存器地址和数据传输是否正确;
- 检查
copy_to_user返回值,确认用户空间数据拷贝是否成功; - 核对温度换算逻辑(右移 7 位、乘以 0.5)。
- 设备文件不存在 :
- 检查
misc_register返回值,确认杂项设备注册成功; - 检查驱动加载是否有错误日志(
dmesg)。。
- 检查
七、总结
本文完整实现了 LM75A 温度传感器的 Linux I2C 驱动开发,从设备树配置、驱动框架实现、应用层测试全部的流程讲解,核心要点如下:
- Linux I2C 驱动开发需遵循 "设备树匹配 + probe 函数初始化 + 字符设备封装" 的核心逻辑;
- LM75A 的 I2C 通信需先发送寄存器地址,再读取数据,数据解析需结合传感器寄存器规范;
- 杂项设备(misc)是简化字符设备开发的高效方式,无需手动管理主设备号;
- 驱动与应用层的数据交互需通过
copy_to_user/copy_from_user,避免直接访问用户空间。