资讯动态

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

发布时间:2026/10/14 19:38:25 来源:尧图企业网站定制
提示文章写完后目录可以自动生成如何生成可参考右边的帮助文档文章目录前言一、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_adapteralgo硬件操作方法最终通过master_xfer完成 I2C 硬件收发硬件层I2C 物理总线 挂载的 I2C 从设备LM75 传感器I2C 从地址 0x48I2C 通讯时序图下方 4 种传输时序I2C 读寄存器标准流程LM75 读取温度就是先写寄存器地址重复起始再读数据对应时序 2发送从机地址 写位发送寄存器地址 0x00温度寄存器重复起始信号发送从机地址 读位读取 2 字节温度数据主机发送 NACK 停止位4 种 I2C 传输模式只写 1 字节写寄存器地址然后读 N 字节本次 LM75 使用这种连续写多字节连续读写多字节核心结构体对应关系i2c_client描述I2C 从设备LM75保存从机地址、所属适配器i2c_driver描述I2C 设备驱动包含 probe、remove、设备匹配表i2c_adapterI2C 主机适配器代表 SOC 的 I2C 控制器i2c_msgI2C 消息一条消息代表一次收发可以标记读 / 写二、驱动源码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; // 步骤1I2C写发送要读取的寄存器地址0x00LM75温度寄存器 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; } // 步骤2I2C读读取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_xferI2C 适配器底层硬件传输函数直接操作 I2C 控制器收发数据copy_to_user内核缓冲区数据拷贝到用户空间缓冲区内核态和用户态内存隔离必须用这个函数miscdevice杂项设备不用手动注册 cdev、手动创建设备节点misc_register自动在/dev生成lm75_misc简化字符设备驱动开发适合简单外设probe/removeprobe 触发时机内核扫描设备树当 dts 里面compatible字符串和驱动of_match_table匹配就调用 probeprobe 中注册 misc 设备创建设备节点remove 卸载设备匹配表of_device_id设备树 DTS 匹配现代嵌入式平台主流方式i2c_device_id旧版非设备树板级信息匹配DTS 设备树片段必须添加i2c1 { lm7548 { compatible pute,putelm75; reg 0x48; /* LM75硬件I2C从机地址硬件决定 */ }; };regI2C 从设备地址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 原始数据5LM75 寄存器低 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 寄存器格式。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑