news 2026/10/14 19:38:24

【Linux 驱动开发】I2C 子系统详解 + LM75 温度传感器驱动实战(misc 杂项设备)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux 驱动开发】I2C 子系统详解 + LM75 温度传感器驱动实战(misc 杂项设备)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、Linux I2C 子系统整体架构(对应思维导图)
    • I2C 通讯时序(图下方 4 种传输时序)
  • 二、驱动源码:i2c_lm75_drv.c(内核模块)
    • 驱动逐段解析
    • DTS 设备树片段(必须添加)
  • 三、应用测试代码 i2c_lm75_app.c 用户态程序
    • 应用代码解析
  • 四、整体调用流程(对照思维导图)
  • 五、现有代码 Bug 与优化方案
    • 驱动侧问题
    • 应用侧问题
    • 优化后的应用代码(支持负温度)
  • 六、编译 Makefile 示例
  • 七、测试步骤
  • 八、总结

一、Linux I2C 子系统整体架构(对应思维导图)

这张图是本节课 I2C 驱动完整分层结构,I2C 在内核分为四层,自上而下:

  1. 用户层:应用程序,open/read 系统调用,访问/dev/lm75_misc设备节点
  2. 杂项设备层 (misc):封装字符设备,自动创建设备节点,简化驱动开发
  3. I2C 核心层 (i2c-core):i2c_driver、i2c_client、设备树匹配、i2c_msg消息封装,负责总线管理、驱动和设备匹配
  4. I2C 适配器层(总线驱动):I2C 硬件控制器驱动(SoC 内部 I2C 外设),i2c_adapter+algo硬件操作方法,最终通过master_xfer完成 I2C 硬件收发
  5. 硬件层:I2C 物理总线 + 挂载的 I2C 从设备(LM75 传感器,I2C 从地址 0x48)

I2C 通讯时序(图下方 4 种传输时序)

I2C 读寄存器标准流程(LM75 读取温度就是先写寄存器地址,重复起始再读数据,对应时序 2)

  1. 发送从机地址 + 写位,发送寄存器地址 0x00(温度寄存器)
  2. 重复起始信号
  3. 发送从机地址 + 读位
  4. 读取 2 字节温度数据,主机发送 NACK + 停止位

4 种 I2C 传输模式:

  1. 只写 1 字节
  2. 写寄存器地址,然后读 N 字节(本次 LM75 使用这种)
  3. 连续写多字节
  4. 连续读写多字节

核心结构体对应关系

  • i2c_client:描述I2C 从设备(LM75),保存从机地址、所属适配器
  • i2c_driver:描述I2C 设备驱动,包含 probe、remove、设备匹配表
  • i2c_adapter:I2C 主机适配器,代表 SOC 的 I2C 控制器
  • i2c_msg:I2C 消息,一条消息代表一次收发,可以标记读 / 写

二、驱动源码:i2c_lm75_drv.c(内核模块)

#include <linux/fs.h> #include <linux/module.h> #include <linux/init.h> #include <linux/i2c.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> static struct i2c_client *lm75_client = NULL; // read回调函数:应用调用read触发 static ssize_t lm75_read(struct file *fp, char __user *puser, size_t n, loff_t *off) { int ret; unsigned char tmpbuff[2] = {0}; struct i2c_msg sendmsg; struct i2c_msg recvmsg; // 步骤1:I2C写,发送要读取的寄存器地址0x00(LM75温度寄存器) sendmsg.addr = lm75_client->addr; tmpbuff[0] = 0x00; sendmsg.buf = tmpbuff; sendmsg.len = 1; ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &sendmsg, 1); if(ret != 1){ pr_info("i2c write reg error!\n"); return -1; } // 步骤2:I2C读,读取2字节温度 recvmsg.addr = lm75_client->addr; recvmsg.flags |= I2C_M_RD; //标记为读消息 recvmsg.buf = tmpbuff; recvmsg.len = 2; ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &recvmsg, 1); if(ret != 1){ pr_info("i2c read data error!\n"); return -1; } // 内核空间拷贝数据到用户空间 ret = copy_to_user(puser, tmpbuff, sizeof(tmpbuff)); if (ret) { pr_info("copy_to_user failed\n"); return -1; } return sizeof(tmpbuff); } // 文件操作集合 static struct file_operations fops = { .read = lm75_read, }; // 杂项设备,自动生成/dev/lm75_misc static struct miscdevice lm75_misc = { .minor = MISC_DYNAMIC_MINOR, .name = "lm75_misc", .fops = &fops, }; // probe函数:驱动和设备匹配成功,自动调用 static int lm75_probe(struct i2c_client *pclient, const struct i2c_device_id *pdev) { int ret; lm75_client = pclient; //保存i2c_client ret = misc_register(&lm75_misc); pr_info("lm75 probe ok\n"); return 0; } // remove:设备卸载时调用 static int lm75_remove(struct i2c_client *pclient) { misc_deregister(&lm75_misc); pr_info("lm75 remove\n"); return 0; } // 设备树匹配表 static struct of_device_id lm75_of_match_tables[] = { {.compatible = "pute,putelm75"}, {}, }; // 传统id匹配表 static struct i2c_device_id lm75_id_tables[] = { {.name = "putelm75"}, {}, }; // i2c驱动结构体 struct i2c_driver lm75_drv = { .probe = lm75_probe, .remove = lm75_remove, .driver = { .name = "lm75_drv", .owner = THIS_MODULE, .of_match_table = lm75_of_match_tables, }, .id_table = lm75_id_tables, }; //模块入口出口 static int __init lm75_drv_init(void) { return i2c_add_driver(&lm75_drv); } static void __exit lm75_drv_exit(void) { i2c_del_driver(&lm75_drv); } module_init(lm75_drv_init); module_exit(lm75_drv_exit); MODULE_LICENSE("GPL");

驱动逐段解析

  1. 全局 i2c_client 指针:probe 保存从设备实例,read 函数用来发起 I2C 传输。
  2. lm75_read:用户态 read 系统调用的内核回调
    • 构造两条i2c_msg消息,先写寄存器地址,再读 2 字节温度,这是 I2C 读取寄存器的标准方式
    • master_xfer:I2C 适配器底层硬件传输函数,直接操作 I2C 控制器收发数据
    • copy_to_user:内核缓冲区数据拷贝到用户空间缓冲区(内核态和用户态内存隔离,必须用这个函数)
  3. miscdevice杂项设备:
    • 不用手动注册 cdev、手动创建设备节点;misc_register自动在/dev生成lm75_misc
    • 简化字符设备驱动开发,适合简单外设
  4. probe/remove:
    • probe 触发时机:内核扫描设备树,当 dts 里面compatible字符串和驱动of_match_table匹配,就调用 probe
    • probe 中注册 misc 设备,创建设备节点;remove 卸载设备
  5. 匹配表:
    • of_device_id:设备树 DTS 匹配(现代嵌入式平台主流方式)
    • i2c_device_id:旧版非设备树板级信息匹配

DTS 设备树片段(必须添加)

&i2c1 { lm75@48 { compatible = "pute,putelm75"; reg = <0x48>; /* LM75硬件I2C从机地址,硬件决定 */ }; };

reg:I2C 从设备地址;compatible 字符串必须和驱动 of_match_table 完全一致,否则匹配失败,probe 不会执行。


三、应用测试代码 i2c_lm75_app.c 用户态程序

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> int main(void) { int fd = 0; unsigned char tmpbuff[2] = {0}; fd = open("/dev/lm75_misc", O_RDWR); if (-1 == fd) { perror("fail to open"); return -1; } while (1) { read(fd, tmpbuff, sizeof(tmpbuff)); //LM75计算公式 float temp = ((tmpbuff[0] << 8 | tmpbuff[1]) >> 5) * 0.125; printf("temp = %.2lf ℃\n", temp); sleep(1); } close(fd); return 0; }

应用代码解析

  1. open("/dev/lm75_misc")打开 misc 设备节点,建立用户态与内核驱动的通道
  2. 循环 read,读取驱动返回的 2 字节原始温度数据
  3. LM75 温度计算公式详解
    • tmpbuff[0] <<8 | tmpbuff[1]:高低字节拼成 16bit 原始数据
    • >>5:LM75 寄存器低 5bit 无效,右移 5bit 取出有效的 11bit
    • *0.125:传感器 LSB = 0.125℃,换算成真实温度

⚠️当前公式缺陷:没有处理补码负温度!当温度小于 0℃,11bit 是补码,这个公式会算出错误的正数,只能正确读取 0℃以上温度。

四、整体调用流程(对照思维导图)

graph TD A[用户应用app] -->|open/read系统调用| B[misc杂项设备层] B -->|file_operations.read| C[驱动lm75_read] C -->|构造i2c_msg消息| D[i2c-core I2C核心子系统] D -->|调用adapter->algo->master_xfer| E[I2C适配器硬件驱动] E -->|I2C SDA/SCL硬件信号| F[硬件LM75传感器] F -->返回2字节原始数据 F -->E -->D -->C -->copy_to_user -->B -->A

五、现有代码 Bug 与优化方案

驱动侧问题

  1. ❌ 直接调用adapter->algo->master_xfer,属于内核内部私有接口,内核版本升级容易失效。✅推荐使用标准封装函数i2c_transfer()
//替换写法 ret = i2c_transfer(lm75_client->adapter, &sendmsg,1);
  1. ❌ 没有判断全局lm75_client是否为空,如果 probe 失败,read 会空指针内核崩溃
  2. ❌ 错误返回值直接 return -1,内核标准规范应该返回负错误码,如-EIO
  3. ❌ 没有 open/release 回调,多进程同时打开读取,全局变量存在竞态风险
  4. ❌ read 没有处理用户传入 count 参数,不管用户读多少字节,强制拷贝 2 字节

应用侧问题

  1. ❌ read 返回值没有判断,I2C 通讯出错,旧脏数据依然参与温度计算,打印错误值
  2. ❌ 缺少负温度补码处理,零下温度计算结果错误
  3. ❌ 循环没有错误退出机制,read 持续失败不会退出程序

优化后的应用代码(支持负温度)

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> int main(void) { int fd = 0; unsigned char tmpbuff[2] = {0}; short raw; fd = open("/dev/lm75_misc", O_RDWR); if (-1 == fd) { perror("fail to open"); return -1; } while (1) { int ret = read(fd, tmpbuff, sizeof(tmpbuff)); if(ret !=2) { perror("read error"); sleep(1); continue; } raw = (tmpbuff[0] <<8) | tmpbuff[1]; raw = raw >> 5; //取出11bit有效数据 float temp = raw * 0.125f; printf("temp = %.2lf ℃\n", temp); sleep(1); } close(fd); return 0; }

六、编译 Makefile 示例

KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) obj-m += i2c_lm75_drv.o all: make -C $(KERNELDIR) M=$(PWD) modules gcc i2c_lm75_app.c -o app_lm75 clean: make -C $(KERNELDIR) M=$(PWD) clean rm app_lm75

七、测试步骤

#1.编译 make #2.加载驱动模块 insmod i2c_lm75_drv.ko #3.查看设备节点 ls /dev/lm75_misc #4.运行测试程序 ./app_lm75 #5.卸载驱动 rmmod i2c_lm75_drv

八、总结

  1. Linux I2C 子系统分为I2C 设备驱动(我们写的)、I2C 核心层、I2C 适配器总线驱动三层,分离硬件和设备逻辑。
  2. I2C 设备驱动开发核心:i2c_driver结构体 + probe 回调,设备树 compatible 匹配。
  3. misc 杂项设备简化字符设备,自动创建设备节点,适合简单外设。
  4. I2C 读取寄存器标准方式:先发寄存器地址(写消息),重复起始,再读数据,使用i2c_msg封装多条消息。
  5. LM75 原始数据是 11bit 补码,原版应用代码无法处理负温度,开发需要注意传感器 datasheet 寄存器格式。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 19:37:15

单臂路由原理与实战:VLAN间通信的底层逻辑解析

1. 为什么“单臂路由”不是权宜之计&#xff0c;而是中小网络的底层通信逻辑你有没有遇到过这样的场景&#xff1a;公司刚搬进新办公室&#xff0c;IT预算只够买一台三层交换机和两台二层接入交换机&#xff0c;但业务部门已经急着要划分财务、人事、研发三个独立网段&#xff…

作者头像 李华
网站建设 2026/10/14 19:35:16

【C++】 priority_queue知识点总结

一、基础概念priority_queue 叫优先队列&#xff0c;底层不是队列&#xff0c;是堆&#xff08;大顶堆 / 小顶堆&#xff09;&#xff0c;属于容器适配器。适配器&#xff1a;不自己实现底层存储&#xff0c;复用别的容器&#xff08;vector/deque&#xff09;做底层&#xff0…

作者头像 李华
网站建设 2026/10/14 19:34:37

CPU中断机制实战:从硬件信号到eBPF观测

1. 这不是教科书里的“CPU和中断”&#xff0c;而是我拆了三台服务器、重装过十七次内核后才敢写的实操笔记你点开这个标题&#xff0c;大概率正被“中断响应时间抖动”卡在项目验收前夜&#xff0c;或是刚在嵌入式调试中看到一串无法复位的IRQ异常&#xff0c;又或者——你只是…

作者头像 李华
网站建设 2026/10/14 19:34:26

Android录屏直播推流全解析:MediaProjection采集到RTMP发送

简介&#xff1a;面向中高级 Android 开发者的录屏直播实战方案&#xff0c;用于解决在移动端完成屏幕画面与音频同步采集、编码并通过 RTMP 协议推送到流媒体服务器的完整链路问题&#xff1b;资源围绕 Android 原生技术展开&#xff0c;涵盖 MediaProjection 录屏、AudioReco…

作者头像 李华
网站建设 2026/10/14 19:34:20

plugins插件加载失败:根因排查与系统设计指南

先说个实话&#xff1a;plugins 这个单词看起来简单&#xff0c;但几乎每个和工程打过交道的人都在它身上浪费过至少一下午。要么是 IAR 里装了个插件没反应&#xff0c;要么是构建工具启动时直接抛 failed to load plugins web boot&#xff0c;要么是测试 harness 组件加载失…

作者头像 李华
网站建设 2026/10/14 19:34:19

box-shadow 属性值详解

语法 box-shadow: offset-x offset-y blur-radius spread-radius color inset;只有 offset-x 和 offset-y 是必填的&#xff0c;其余参数可选&#xff0c;且顺序不能乱。 参数说明参数是否必填说明offset-x必填水平偏移量&#xff0c;正值向右&#xff0c;负值向左offset-y必填…

作者头像 李华