实战:传感器驱动从 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: temp48 { 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; }; };三、驱动代码完整实现头文件和寄存器定义#includelinux/module.h#includelinux/i2c.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/interrupt.h#includelinux/wait.h#includelinux/mutex.h#includelinux/ioctl.h#includelinux/of.h#includelinux/of_irq.h#defineDEV_NAMEtemp_sensor#defineDEV_CLASStemp_sensor_class#defineDEV_COUNT1// 寄存器定义#defineREG_TEMP0x00#defineREG_CONFIG0x01#defineREG_RATE0x02// 采样率#defineRATE_1HZ0#defineRATE_10HZ1#defineRATE_100HZ2// ioctl 命令#defineTEMP_IOC_MAGICt#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 reti2c_smbus_read_byte_data(dev-client,reg);if(ret0)returnret;*valret;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 reti2c_smbus_read_word_data(dev-client,REG_TEMP);if(ret0)returnret;// 大端12位有效单位0.0625度转换为毫摄氏度s16 rawbe16_to_cpu((__force __be16)ret)4;*tempraw*625;// 毫度return0;}中断处理staticirqreturn_tsensor_irq_handler(intirq,void*dev_id){structsensor_dev*devdev_id;// 顶半部标记数据就绪唤醒等待队列dev-data_readytrue;wake_up_interruptible(dev-read_wq);returnIRQ_HANDLED;}file_operations 实现staticintsensor_open(structinode*inode,structfile*filp){structsensor_dev*devcontainer_of(inode-i_cdev,structsensor_dev,cdev);filp-private_datadev;return0;}staticintsensor_release(structinode*inode,structfile*filp){return0;}staticssize_tsensor_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos){structsensor_dev*devfilp-private_data;s16 temp;intret;// 阻塞等待数据就绪可被信号打断retwait_event_interruptible(dev-read_wq,dev-data_ready);if(ret)returnret;mutex_lock(dev-lock);// 读温度retsensor_read_temp(dev,temp);if(ret){mutex_unlock(dev-lock);returnret;}dev-data_readyfalse;// 清标志dev-last_temptemp;mutex_unlock(dev-lock);// 返回给用户int 类型的毫度值if(countsizeof(int))countsizeof(int);if(copy_to_user(buf,temp,count))return-EFAULT;returncount;}staticlongsensor_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){structsensor_dev*devfilp-private_data;intrate,ret;switch(cmd){caseTEMP_IOC_SET_RATE:if(copy_from_user(rate,(int__user*)arg,sizeof(int)))return-EFAULT;if(rateRATE_1HZ||rateRATE_100HZ)return-EINVAL;mutex_lock(dev-lock);retsensor_write_reg8(dev,REG_RATE,rate);if(!ret)dev-sample_raterate;mutex_unlock(dev-lock);returnret;caseTEMP_IOC_GET_RATE:ratedev-sample_rate;if(copy_to_user((int__user*)arg,rate,sizeof(int)))return-EFAULT;return0;default:return-EINVAL;}}staticconststructfile_operationssensor_fops{.ownerTHIS_MODULE,.opensensor_open,.releasesensor_release,.readsensor_read,.unlocked_ioctlsensor_ioctl,};probe 函数staticintsensor_probe(structi2c_client*client){structsensor_dev*dev;u8 id;intret;staticdev_tdevid_base;staticbool major_allocated;devdevm_kzalloc(client-dev,sizeof(*dev),GFP_KERNEL);if(!dev)return-ENOMEM;dev-clientclient;i2c_set_clientdata(client,dev);// 1. 验证芯片读配置寄存器简化实际应该读ID寄存器retsensor_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_readyfalse;dev-sample_rateRATE_10HZ;// 3. 获取中断号dev-irqclient-irq;if(dev-irq0){dev_err(client-dev,无效的中断号\n);return-EINVAL;}// 4. 申请中断retdevm_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){retalloc_chrdev_region(devid_base,0,DEV_COUNT,DEV_NAME);if(ret0)returnret;major_allocatedtrue;}dev-deviddevid_base;// 6. 注册 cdevcdev_init(dev-cdev,sensor_fops);dev-cdev.ownerTHIS_MODULE;retcdev_add(dev-cdev,dev-devid,1);if(ret0)gotoerr_cdev;// 7. 创建 class 和 device自动生成/dev节点dev-classclass_create(THIS_MODULE,DEV_CLASS);if(IS_ERR(dev-class)){retPTR_ERR(dev-class);gotoerr_class;}dev-devicedevice_create(dev-class,NULL,dev-devid,NULL,DEV_NAME);if(IS_ERR(dev-device)){retPTR_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*devi2c_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[]{{.compatibleheipiao,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{.probesensor_probe,.removesensor_remove,.id_tablesensor_id,.driver{.nameDEV_NAME,.of_match_tablesensor_of_match,},};module_i2c_driver(sensor_driver);MODULE_LICENSE(GPL);MODULE_AUTHOR(Heipiao);MODULE_DESCRIPTION(I2C 温度传感器字符设备驱动);四、应用层测试程序#includestdio.h#includestdlib.h#includefcntl.h#includeunistd.h#includesys/ioctl.h#defineTEMP_IOC_MAGICt#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[]){intfdopen(/dev/temp_sensor,O_RDWR);if(fd0){perror(open);return1;}// 设置采样率intrateRATE_10HZ;ioctl(fd,TEMP_IOC_SET_RATE,rate);printf(采样率设置为 %d\n,rate);// 读 10 次温度for(inti0;i10;i){inttemp;intnread(fd,temp,sizeof(temp));if(n0){perror(read);break;}printf(温度: %.3f ℃\n,temp/1000.0);}close(fd);return0;}编译运行gcc sensor_test.c-osensor_test ./sensor_test五、驱动要点回顾1. I2C 驱动框架i2c_driverprobe/removeof_match_table设备树匹配module_i2c_driver简化注册2. 字符设备接口alloc_chrdev_region分配设备号cdev_initcdev_add注册字符设备file_operations实现 open/read/ioctlclass_createdevice_create自动创建设备节点3. 中断 等待队列devm_request_irq申请中断顶半部标记就绪 wake_up_interruptible唤醒read 里wait_event_interruptible阻塞等待没有数据时进程睡眠不占 CPU4. 并发保护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 标志没置位数据不对字节序错、单位换算错、寄存器地址错七、可以继续扩展的方向sysfs 属性加温度、采样率的 sysfs 文件不用字符设备也能读hwmon 子系统注册成 hwmon 设备融入 Linux 硬件监控框架IIO 子系统传感器类驱动推荐走 IIOIndustrial I/O子系统poll 支持实现 .poll 函数支持 select/epoll异步通知fasync 支持数据就绪发 SIGIO 信号低功耗runtime PM不用的时候休眠设备树属性从设备树读取校准参数、采样率默认值八、系列总结这个系列 12 篇把嵌入式 Linux 驱动开发入门需要的知识都覆盖了基础概念驱动分类、内核模块、开发环境字符设备设备号、cdev、file_operations、自动创建设备节点中断处理顶半部底半部、tasklet、workqueue定时器timer_list、hrtimer、延时函数并发同步原子操作、自旋锁、互斥锁、等待队列设备模型platform 总线、设备树 DTS子系统GPIO pinctrl、I2C、SPI实战项目完整的传感器字符设备驱动学习路线建议先把 hello world 模块跑起来熟悉编译加载流程写一个最简单的字符设备能 read/write加上 GPIO 操作控制 LED、读按键加上中断按键中断触发学设备树把驱动改成 platform 方式学 I2C/SPI写真实传感器驱动深入子系统input、IIO、hwmon、framebuffer 等给新手的建议先跑起来再说不要纠结原理先把代码跑通再回头理解多看内核源码内核里有大量现成驱动照着模仿善用 printk驱动调试基本靠打日志哪里不对加打印别怕 panic内核崩溃很正常重启再来看 oops 信息定位从简单外设开始LED、按键、EEPROM由简到难九、本篇小结实战驱动整合了 I2C 通信、字符设备、中断、等待队列、互斥锁、ioctl驱动结构i2c_driver 外壳 cdev 字符设备接口 中断通知 等待队列阻塞probe 里完成I2C 验证 → 中断申请 → 字符设备注册 → class/device 创建read 用等待队列阻塞数据就绪中断唤醒效率高不占 CPUioctl 提供配置接口应用层灵活控制采样率等参数调试步骤先确认硬件 I2C → 确认 probe → 确认中断 → 测试读写嵌入式 Linux 驱动入门系列到这里就全部结束了。入门之后根据你的具体硬件平台和业务场景再深入对应的子系统。驱动开发水很深但基础都是通的把这 12 篇的内容吃透写大部分字符类设备驱动都没问题了。我是黒漂技术佬我们下个系列见。