news 2026/7/10 3:07:56

实战:传感器驱动从 0 到 1

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
实战:传感器驱动从 0 到 1

实战:传感器驱动从 0 到 1

系列收尾实战篇。把前面讲的 platform 驱动、字符设备、中断、等待队列、I2C 通信全部串起来,从零写一个完整的 I2C 传感器字符设备驱动,包含设备树、probe 初始化、read 阻塞读取、ioctl 配置、中断数据就绪通知。

大家好,我是黒漂技术佬。

前面 11 篇把字符设备、中断、定时器、并发同步、设备树、platform 总线、GPIO、I2C、SPI 都讲了。这篇作为系列收尾,把这些知识串起来,从零写一个完整的传感器驱动。

实战目标:写一个 I2C 接口的传感器字符设备驱动,支持:

  • 设备树匹配和 probe 初始化
  • 自动创建 /dev 设备节点
  • read 阻塞读取传感器数据(等待队列)
  • ioctl 配置采样率
  • 数据就绪中断通知
  • 应用层测试程序

一、整体架构

硬件假设

  • I2C 接口的温度传感器,地址 0x48
  • 有一个数据就绪中断引脚(DRDY),新数据就绪触发下降沿
  • 寄存器:0x00 温度值(16位)、0x01 配置寄存器、0x02 采样率设置

驱动结构

应用层 open/read/ioctl ↓ 字符设备 (cdev + file_operations) ↓ I2C 读写 (i2c_smbus_*) ↓ 硬件传感器 ↓ DRDY 中断 → tasklet → 唤醒等待队列

文件组成

  • 设备树节点(dts 片段)
  • 驱动源码:sensor_drv.c
  • 应用层测试程序:sensor_test.c

二、设备树节点

&i2c0 { temp_sensor: temp@48 { compatible = "heipiao,temp-sensor"; reg = <0x48>; // 数据就绪中断引脚 interrupt-parent = <&gpio0>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; // 也可以用 GPIO 方式描述 // drdy-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>; status = "okay"; }; };

三、驱动代码完整实现

头文件和寄存器定义

#include<linux/module.h>#include<linux/i2c.h>#include<linux/cdev.h>#include<linux/device.h>#include<linux/uaccess.h>#include<linux/interrupt.h>#include<linux/wait.h>#include<linux/mutex.h>#include<linux/ioctl.h>#include<linux/of.h>#include<linux/of_irq.h>#defineDEV_NAME"temp_sensor"#defineDEV_CLASS"temp_sensor_class"#defineDEV_COUNT1// 寄存器定义#defineREG_TEMP0x00#defineREG_CONFIG0x01#defineREG_RATE0x02// 采样率#defineRATE_1HZ0#defineRATE_10HZ1#defineRATE_100HZ2// ioctl 命令#defineTEMP_IOC_MAGIC't'#defineTEMP_IOC_SET_RATE_IOW(TEMP_IOC_MAGIC,1,int)#defineTEMP_IOC_GET_RATE_IOR(TEMP_IOC_MAGIC,2,int)// 设备私有数据structsensor_dev{structi2c_client*client;structcdevcdev;structclass*class;structdevice*device;dev_tdevid;intirq;wait_queue_head_tread_wq;bool data_ready;structmutexlock;// 保护并发访问intsample_rate;// 当前采样率s16 last_temp;// 上次读到的温度(毫度)};

I2C 寄存器读写封装

staticintsensor_read_reg8(structsensor_dev*dev,u8 reg,u8*val){s32 ret=i2c_smbus_read_byte_data(dev->client,reg);if(ret<0)returnret;*val=ret;return0;}staticintsensor_write_reg8(structsensor_dev*dev,u8 reg,u8 val){returni2c_smbus_write_byte_data(dev->client,reg,val);}staticintsensor_read_temp(structsensor_dev*dev,s16*temp){s32 ret=i2c_smbus_read_word_data(dev->client,REG_TEMP);if(ret<0)returnret;// 大端,12位有效,单位0.0625度,转换为毫摄氏度s16 raw=be16_to_cpu((__force __be16)ret)>>4;*temp=raw*625;// 毫度return0;}

中断处理

staticirqreturn_tsensor_irq_handler(intirq,void*dev_id){structsensor_dev*dev=dev_id;// 顶半部:标记数据就绪,唤醒等待队列dev->data_ready=true;wake_up_interruptible(&dev->read_wq);returnIRQ_HANDLED;}

file_operations 实现

staticintsensor_open(structinode*inode,structfile*filp){structsensor_dev*dev=container_of(inode->i_cdev,structsensor_dev,cdev);filp->private_data=dev;return0;}staticintsensor_release(structinode*inode,structfile*filp){return0;}staticssize_tsensor_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos){structsensor_dev*dev=filp->private_data;s16 temp;intret;// 阻塞等待数据就绪(可被信号打断)ret=wait_event_interruptible(dev->read_wq,dev->data_ready);if(ret)returnret;mutex_lock(&dev->lock);// 读温度ret=sensor_read_temp(dev,&temp);if(ret){mutex_unlock(&dev->lock);returnret;}dev->data_ready=false;// 清标志dev->last_temp=temp;mutex_unlock(&dev->lock);// 返回给用户(int 类型的毫度值)if(count>sizeof(int))count=sizeof(int);if(copy_to_user(buf,&temp,count))return-EFAULT;returncount;}staticlongsensor_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){structsensor_dev*dev=filp->private_data;intrate,ret;switch(cmd){caseTEMP_IOC_SET_RATE:if(copy_from_user(&rate,(int__user*)arg,sizeof(int)))return-EFAULT;if(rate<RATE_1HZ||rate>RATE_100HZ)return-EINVAL;mutex_lock(&dev->lock);ret=sensor_write_reg8(dev,REG_RATE,rate);if(!ret)dev->sample_rate=rate;mutex_unlock(&dev->lock);returnret;caseTEMP_IOC_GET_RATE:rate=dev->sample_rate;if(copy_to_user((int__user*)arg,&rate,sizeof(int)))return-EFAULT;return0;default:return-EINVAL;}}staticconststructfile_operationssensor_fops={.owner=THIS_MODULE,.open=sensor_open,.release=sensor_release,.read=sensor_read,.unlocked_ioctl=sensor_ioctl,};

probe 函数

staticintsensor_probe(structi2c_client*client){structsensor_dev*dev;u8 id;intret;staticdev_tdevid_base;staticbool major_allocated;dev=devm_kzalloc(&client->dev,sizeof(*dev),GFP_KERNEL);if(!dev)return-ENOMEM;dev->client=client;i2c_set_clientdata(client,dev);// 1. 验证芯片:读配置寄存器(简化,实际应该读ID寄存器)ret=sensor_read_reg8(dev,REG_CONFIG,&id);if(ret){dev_err(&client->dev,"I2C 通信失败\n");returnret;}// 2. 初始化等待队列和锁init_waitqueue_head(&dev->read_wq);mutex_init(&dev->lock);dev->data_ready=false;dev->sample_rate=RATE_10HZ;// 3. 获取中断号dev->irq=client->irq;if(dev->irq<=0){dev_err(&client->dev,"无效的中断号\n");return-EINVAL;}// 4. 申请中断ret=devm_request_irq(&client->dev,dev->irq,sensor_irq_handler,IRQF_TRIGGER_FALLING,DEV_NAME,dev);if(ret){dev_err(&client->dev,"申请中断失败: %d\n",ret);returnret;}// 5. 分配设备号(只分配一次)if(!major_allocated){ret=alloc_chrdev_region(&devid_base,0,DEV_COUNT,DEV_NAME);if(ret<0)returnret;major_allocated=true;}dev->devid=devid_base;// 6. 注册 cdevcdev_init(&dev->cdev,&sensor_fops);dev->cdev.owner=THIS_MODULE;ret=cdev_add(&dev->cdev,dev->devid,1);if(ret<0)gotoerr_cdev;// 7. 创建 class 和 device(自动生成/dev节点)dev->class=class_create(THIS_MODULE,DEV_CLASS);if(IS_ERR(dev->class)){ret=PTR_ERR(dev->class);gotoerr_class;}dev->device=device_create(dev->class,NULL,dev->devid,NULL,DEV_NAME);if(IS_ERR(dev->device)){ret=PTR_ERR(dev->device);gotoerr_device;}// 8. 设置默认采样率sensor_write_reg8(dev,REG_RATE,RATE_10HZ);dev_info(&client->dev,"温度传感器驱动加载成功\n");return0;err_device:class_destroy(dev->class);err_class:cdev_del(&dev->cdev);err_cdev:// 注意:设备号是全局的,最后一个设备卸载时再释放returnret;}

remove 函数

staticvoidsensor_remove(structi2c_client*client){structsensor_dev*dev=i2c_get_clientdata(client);device_destroy(dev->class,dev->devid);class_destroy(dev->class);cdev_del(&dev->cdev);// devm 自动释放中断、内存dev_info(&client->dev,"温度传感器驱动卸载\n");}

驱动注册

staticconststructof_device_idsensor_of_match[]={{.compatible="heipiao,temp-sensor"},{}};MODULE_DEVICE_TABLE(of,sensor_of_match);staticconststructi2c_device_idsensor_id[]={{"temp-sensor",0},{}};MODULE_DEVICE_TABLE(i2c,sensor_id);staticstructi2c_driversensor_driver={.probe=sensor_probe,.remove=sensor_remove,.id_table=sensor_id,.driver={.name=DEV_NAME,.of_match_table=sensor_of_match,},};module_i2c_driver(sensor_driver);MODULE_LICENSE("GPL");MODULE_AUTHOR("Heipiao");MODULE_DESCRIPTION("I2C 温度传感器字符设备驱动");

四、应用层测试程序

#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<sys/ioctl.h>#defineTEMP_IOC_MAGIC't'#defineTEMP_IOC_SET_RATE_IOW(TEMP_IOC_MAGIC,1,int)#defineTEMP_IOC_GET_RATE_IOR(TEMP_IOC_MAGIC,2,int)#defineRATE_1HZ0#defineRATE_10HZ1#defineRATE_100HZ2intmain(intargc,char*argv[]){intfd=open("/dev/temp_sensor",O_RDWR);if(fd<0){perror("open");return1;}// 设置采样率intrate=RATE_10HZ;ioctl(fd,TEMP_IOC_SET_RATE,&rate);printf("采样率设置为 %d\n",rate);// 读 10 次温度for(inti=0;i<10;i++){inttemp;intn=read(fd,&temp,sizeof(temp));if(n<0){perror("read");break;}printf("温度: %.3f ℃\n",temp/1000.0);}close(fd);return0;}

编译运行:

gcc sensor_test.c-osensor_test ./sensor_test

五、驱动要点回顾

1. I2C 驱动框架

  • i2c_driver+probe/remove
  • of_match_table设备树匹配
  • module_i2c_driver简化注册

2. 字符设备接口

  • alloc_chrdev_region分配设备号
  • cdev_init+cdev_add注册字符设备
  • file_operations实现 open/read/ioctl
  • class_create+device_create自动创建设备节点

3. 中断 + 等待队列

  • devm_request_irq申请中断
  • 顶半部标记就绪 +wake_up_interruptible唤醒
  • read 里wait_event_interruptible阻塞等待
  • 没有数据时进程睡眠,不占 CPU

4. 并发保护

  • mutex保护 I2C 读写和共享数据
  • 多个进程同时 read 不会乱

5. ioctl 控制

  • 自定义命令码,设置采样率、读取配置
  • 应用层通过 ioctl 配置驱动参数

6. devm 托管资源

  • 内存、中断用 devm_ 版本,自动释放
  • 减少 remove 里的清理工作,降低泄漏风险

六、调试步骤

第一步:确认硬件和 I2C 通信

# 扫描总线,看能不能扫到 0x48i2cdetect-y0# 读寄存器,确认芯片正常i2cget-y00x48 0x00

第二步:加载驱动看 probe 有没有成功

insmod temp_sensor.kodmesg|tail# 看有没有 "温度传感器驱动加载成功"ls/dev/temp_sensor# 确认设备节点存在

第三步:测试中断

cat/proc/interrupts|greptemp_sensor# 看中断计数有没有增加

第四步:运行应用测试

./sensor_test# 看能不能正常读到温度数据

常见问题排查

现象可能原因
probe 不调用compatible 不匹配、设备树没加、i2c控制器没开
I2C 读失败地址不对、引脚没复用对、上拉电阻没焊
中断不触发中断极性搞反、引脚没配置对、没使能中断
read 一直阻塞中断没进来、data_ready 标志没置位
数据不对字节序错、单位换算错、寄存器地址错

七、可以继续扩展的方向

  1. sysfs 属性:加温度、采样率的 sysfs 文件,不用字符设备也能读
  2. hwmon 子系统:注册成 hwmon 设备,融入 Linux 硬件监控框架
  3. IIO 子系统:传感器类驱动推荐走 IIO(Industrial I/O)子系统
  4. poll 支持:实现 .poll 函数,支持 select/epoll
  5. 异步通知:fasync 支持,数据就绪发 SIGIO 信号
  6. 低功耗:runtime PM,不用的时候休眠
  7. 设备树属性:从设备树读取校准参数、采样率默认值

八、系列总结

这个系列 12 篇,把嵌入式 Linux 驱动开发入门需要的知识都覆盖了:

  1. 基础概念:驱动分类、内核模块、开发环境
  2. 字符设备:设备号、cdev、file_operations、自动创建设备节点
  3. 中断处理:顶半部底半部、tasklet、workqueue
  4. 定时器:timer_list、hrtimer、延时函数
  5. 并发同步:原子操作、自旋锁、互斥锁、等待队列
  6. 设备模型:platform 总线、设备树 DTS
  7. 子系统:GPIO + pinctrl、I2C、SPI
  8. 实战项目:完整的传感器字符设备驱动

学习路线建议

  1. 先把 hello world 模块跑起来,熟悉编译加载流程
  2. 写一个最简单的字符设备,能 read/write
  3. 加上 GPIO 操作,控制 LED、读按键
  4. 加上中断,按键中断触发
  5. 学设备树,把驱动改成 platform 方式
  6. 学 I2C/SPI,写真实传感器驱动
  7. 深入子系统:input、IIO、hwmon、framebuffer 等

给新手的建议

  • 先跑起来再说:不要纠结原理,先把代码跑通,再回头理解
  • 多看内核源码:内核里有大量现成驱动,照着模仿
  • 善用 printk:驱动调试基本靠打日志,哪里不对加打印
  • 别怕 panic:内核崩溃很正常,重启再来,看 oops 信息定位
  • 从简单外设开始:LED、按键、EEPROM,由简到难

九、本篇小结

  • 实战驱动整合了 I2C 通信、字符设备、中断、等待队列、互斥锁、ioctl
  • 驱动结构:i2c_driver 外壳 + cdev 字符设备接口 + 中断通知 + 等待队列阻塞
  • probe 里完成:I2C 验证 → 中断申请 → 字符设备注册 → class/device 创建
  • read 用等待队列阻塞,数据就绪中断唤醒,效率高不占 CPU
  • ioctl 提供配置接口,应用层灵活控制采样率等参数
  • 调试步骤:先确认硬件 I2C → 确认 probe → 确认中断 → 测试读写

嵌入式 Linux 驱动入门系列到这里就全部结束了。入门之后,根据你的具体硬件平台和业务场景,再深入对应的子系统。驱动开发水很深,但基础都是通的,把这 12 篇的内容吃透,写大部分字符类设备驱动都没问题了。

我是黒漂技术佬,我们下个系列见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/10 3:07:23

C++ std::boolalpha 与 std::noboolalpha:3种场景下的布尔值格式化控制

C std::boolalpha 与 std::noboolalpha&#xff1a;3种场景下的布尔值格式化控制在C开发中&#xff0c;布尔值的输出格式化是一个看似简单却经常被忽视的细节。默认情况下&#xff0c;std::cout会将true输出为1&#xff0c;false输出为0&#xff0c;这种数字表示虽然简洁&#…

作者头像 李华
网站建设 2026/7/10 3:06:53

74LS175 D触发器四人抢答器:Multisim 14.0 仿真与面包板实测对比

74LS175 D触发器四人抢答器&#xff1a;从Multisim仿真到面包板落地的全流程实战在数字电路课程设计中&#xff0c;四人抢答器是一个经典的综合实验项目。本文将基于74LS175 D触发器芯片&#xff0c;带你完整走通从Multisim 14.0仿真到面包板实际搭建的全过程。不同于简单的理论…

作者头像 李华
网站建设 2026/7/10 3:06:44

软考高项整合管理7过程:3大基准变更与CCB审批权限实战解析

软考高项整合管理7过程&#xff1a;3大基准变更与CCB审批权限实战解析在项目管理实践中&#xff0c;整合管理如同交响乐团的指挥&#xff0c;协调各个知识领域的过程和活动&#xff0c;确保项目目标的顺利实现。其中&#xff0c;变更控制是整合管理的核心难点&#xff0c;尤其是…

作者头像 李华
网站建设 2026/7/10 3:04:37

广州机电安装施工流程

机电安装施工流程。机电安装是一项复杂且关键的工程&#xff0c;其施工流程涵盖多个重要环节。清晰了解机电安装施工流程&#xff0c;对于保障工程质量和进度至关重要。国冶机电安装公司作为专业从事工程服务的企业&#xff0c;具备丰富经验&#xff0c;能有效应对工期延误难题…

作者头像 李华
网站建设 2026/7/10 3:03:08

PowerPoint 2021 互动游戏开发:3类核心动画与超链接实现沉浸式体验

PowerPoint 2021互动游戏开发实战&#xff1a;从基础动画到沉浸式体验设计在数字化教学与创意展示领域&#xff0c;PowerPoint早已突破传统演示工具的边界。最新发布的PowerPoint 2021版本通过增强的动画窗格、智能触发器和动态超链接功能&#xff0c;为教育工作者和内容创作者…

作者头像 李华