提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、Linux I2C 子系统整体架构(对应思维导图)
- I2C 通讯时序(图下方 4 种传输时序)
- 二、驱动源码:i2c_lm75_drv.c(内核模块)
- 驱动逐段解析
- DTS 设备树片段(必须添加)
- 三、应用测试代码 i2c_lm75_app.c 用户态程序
- 应用代码解析
- 四、整体调用流程(对照思维导图)
- 五、现有代码 Bug 与优化方案
- 驱动侧问题
- 应用侧问题
- 优化后的应用代码(支持负温度)
- 六、编译 Makefile 示例
- 七、测试步骤
- 八、总结
一、Linux I2C 子系统整体架构(对应思维导图)
这张图是本节课 I2C 驱动完整分层结构,I2C 在内核分为四层,自上而下:
- 用户层:应用程序,open/read 系统调用,访问
/dev/lm75_misc设备节点 - 杂项设备层 (misc):封装字符设备,自动创建设备节点,简化驱动开发
- I2C 核心层 (i2c-core):
i2c_driver、i2c_client、设备树匹配、i2c_msg消息封装,负责总线管理、驱动和设备匹配 - I2C 适配器层(总线驱动):I2C 硬件控制器驱动(SoC 内部 I2C 外设),
i2c_adapter+algo硬件操作方法,最终通过master_xfer完成 I2C 硬件收发 - 硬件层:I2C 物理总线 + 挂载的 I2C 从设备(LM75 传感器,I2C 从地址 0x48)
I2C 通讯时序(图下方 4 种传输时序)
I2C 读寄存器标准流程(LM75 读取温度就是先写寄存器地址,重复起始再读数据,对应时序 2)
- 发送从机地址 + 写位,发送寄存器地址 0x00(温度寄存器)
- 重复起始信号
- 发送从机地址 + 读位
- 读取 2 字节温度数据,主机发送 NACK + 停止位
4 种 I2C 传输模式:
- 只写 1 字节
- 写寄存器地址,然后读 N 字节(本次 LM75 使用这种)
- 连续写多字节
- 连续读写多字节
核心结构体对应关系
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");驱动逐段解析
- 全局 i2c_client 指针:probe 保存从设备实例,read 函数用来发起 I2C 传输。
lm75_read:用户态 read 系统调用的内核回调- 构造两条
i2c_msg消息,先写寄存器地址,再读 2 字节温度,这是 I2C 读取寄存器的标准方式 master_xfer:I2C 适配器底层硬件传输函数,直接操作 I2C 控制器收发数据copy_to_user:内核缓冲区数据拷贝到用户空间缓冲区(内核态和用户态内存隔离,必须用这个函数)
- 构造两条
miscdevice杂项设备:- 不用手动注册 cdev、手动创建设备节点;
misc_register自动在/dev生成lm75_misc - 简化字符设备驱动开发,适合简单外设
- 不用手动注册 cdev、手动创建设备节点;
- probe/remove:
- probe 触发时机:内核扫描设备树,当 dts 里面
compatible字符串和驱动of_match_table匹配,就调用 probe - probe 中注册 misc 设备,创建设备节点;remove 卸载设备
- probe 触发时机:内核扫描设备树,当 dts 里面
- 匹配表:
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; }应用代码解析
open("/dev/lm75_misc")打开 misc 设备节点,建立用户态与内核驱动的通道- 循环 read,读取驱动返回的 2 字节原始温度数据
- 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 与优化方案
驱动侧问题
- ❌ 直接调用
adapter->algo->master_xfer,属于内核内部私有接口,内核版本升级容易失效。✅推荐使用标准封装函数i2c_transfer()
//替换写法 ret = i2c_transfer(lm75_client->adapter, &sendmsg,1);- ❌ 没有判断全局
lm75_client是否为空,如果 probe 失败,read 会空指针内核崩溃 - ❌ 错误返回值直接 return -1,内核标准规范应该返回负错误码,如
-EIO - ❌ 没有 open/release 回调,多进程同时打开读取,全局变量存在竞态风险
- ❌ read 没有处理用户传入 count 参数,不管用户读多少字节,强制拷贝 2 字节
应用侧问题
- ❌ read 返回值没有判断,I2C 通讯出错,旧脏数据依然参与温度计算,打印错误值
- ❌ 缺少负温度补码处理,零下温度计算结果错误
- ❌ 循环没有错误退出机制,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八、总结
- Linux I2C 子系统分为I2C 设备驱动(我们写的)、I2C 核心层、I2C 适配器总线驱动三层,分离硬件和设备逻辑。
- I2C 设备驱动开发核心:
i2c_driver结构体 + probe 回调,设备树 compatible 匹配。 - misc 杂项设备简化字符设备,自动创建设备节点,适合简单外设。
- I2C 读取寄存器标准方式:先发寄存器地址(写消息),重复起始,再读数据,使用
i2c_msg封装多条消息。 - LM75 原始数据是 11bit 补码,原版应用代码无法处理负温度,开发需要注意传感器 datasheet 寄存器格式。