1. 项目概述为什么我要写一个温湿度传感器驱动如果你接触过开源鸿蒙OpenHarmony系统你会发现它和传统的 RTOS、Linux 嵌入式方案不太一样系统层的组件划分非常清晰驱动侧的代码也不是你想怎么放就怎么放而是要遵循一套名叫 HDFHardware Driver Foundation的驱动框架。这个项目要解决的就是把一个常见的温湿度传感器比如 I2C 接口的 AHT20、SHT30或者 ADC 接口的模拟量传感器接入 OpenHarmony让上层应用能够通过系统标准的 Sensor API 拿到温度、湿度数据。这件事适合谁一类是刚把 OpenHarmony 跑起来、想给自己的开发板加传感器的同学另一类是从 Linux 驱动转过来、对 HDF 这套东西还不太熟的开发者。你可能会想传感器驱动网上不是有大把例程吗抄一抄改一改不就行了实际上OpenHarmony 的传感器驱动在设备树配置、上报通道、权限管理上都有自己的规矩直接套用 STM32 或者 Linux 的写法大概率跑不通。这篇教程会从“为什么要这样写”讲起把驱动模型的底层逻辑和实操步骤一起讲清楚让你看完之后不只是会抄代码还能自己鼓捣其他类型的传感器。我最初接到这个需求时手里只有一块某厂家的开发板和一个 I2C 温湿度传感器模块。整个开发过程花了大概一周中间踩了不少坑比如设备没有枚举成功、数据读出来全是 0xFF、上报频率不稳定等等。把过程复盘整理出来就是这篇文章。为了照顾新手我会把环境搭建、硬件接线、驱动代码、编译烧录、上层验证全部过一遍老手可以直接跳到第 3 章看代码实现第 5 章有一些不太容易查到的调试技巧。2. 整体设计与核心思路拆解2.1 OpenHarmony 传感器子系统的“四层蛋糕”在动手写代码之前得先搞清楚你写的驱动到底在系统里处于什么位置。我自己习惯把 OpenHarmony 的传感器功能分成四层理解了这个分层后面遇到问题才知道去哪一层排查。最底下是传感器硬件本身也就是 I2C 从设备或者是 ADC 通道上的传感器。往上一层是驱动层这一层运行在 HDF 框架里负责跟硬件打交道初始化传感器、配置采集模式、读取寄存器数据。再往上是传感器服务层它通过 HDF 的消息机制和驱动打交道把驱动上报的原始数据转成系统统一的数据结构同时管理“谁在监听传感器”“多久上报一次”这类事情。最上面就是应用层了应用开发的同学只需要调用sensor.on(temperature)这类接口就能拿到数值完全不用关心底层用的是 I2C 还是 SPI。这个设计有一个很直接的好处驱动和应用彻底解耦。你今天用的是 AHT20明天想换 SHT30只需要改驱动层和设备配置上层的应用一行代码都不用动。我在项目里就遇到过这种情况初期用的传感器缺货临时换了一个兼容芯片结果因为驱动模型封装得比较干净只改了驱动文件里的寄存器地址和转换公式应用侧完全不受影响。2.2 为什么一定要用 HDF 框架而不是直接写内核驱动很多从 Linux 转过来的朋友第一反应是能不能像写 platform_driver 那样直接在内核里注册一个驱动可以但在 OpenHarmony 体系里这样做并不划算。HDF 框架不仅仅是驱动框架它还负责电源管理、消息传递、配置解析这些脏活累活。比如说系统进入低功耗模式时HDF 会按照配置从上到下通知驱动做掉电处理你不需要自己在驱动里处理各种 PM 回调。而且在 OpenHarmony 的生态里传感器服务层默认只会通过 HDF 的 Sensor 接口模型去发现和管理驱动。如果你绕开 HDF 自己写就得自己实现一套上报通道还要对接服务层的协议浪费大量时间还不稳定。HDF 就好比公司里的项目管理系统你当然可以发邮件跟同事沟通但如果大家都用统一系统状态流转、任务分配、日报汇总都会自动完成你只需要把工作内容填进去就行。传感器驱动在 OpenHarmony 里走 HDF也是同样的道理。HDF 的驱动模型对设备描述采用了 HCS 配置语言而不是传统设备树 DTS。这个差异是很多新手最初看不懂的地方。在 DTS 里我们用compatible vendor,device来匹配驱动在 HCS 里则是通过module_name和device_match_attr来关联驱动和设备节点。理解了这个对应关系配置写起来就不难。2.3 技术选型I2C 接口还是 ADC 接口温湿度传感器的接入方式主要有三种。最常见的是 I2C 接口的数字传感器比如 AHT20、SHT30、SHTC3它们内部有 ADC、有校准逻辑主机只需要通过 I2C 发命令、读寄存器就能拿到温湿度精度和一致性都比较好驱动代码也相对简单。第二种是单总线传感器比如 DHT11、DHT22时序要求严苛在 OpenHarmony 的 HDF 框架下需要自己处理 GPIO 电平时序而且在系统调度繁忙时容易出错我个人不建议第一个项目选这种。第三种是模拟量传感器输出的是电压信号需要用 ADC 通道读取驱动要负责查表或者用公式换算通常精度不高。如果你是从零开始我推荐先用 I2C 接口的传感器。原因很实际I2C 协议是标准化的逻辑分析仪一挂就能看到通信过程调试难度比单总线低一个数量级同时 I2C 传感器的寄存器地址和数据结构都有明确手册驱动代码的思路清晰。选定方案之后后续的 HDF 节点配置、驱动编写、上报流程都是通用的以后你再写 SPI 或者 ADC 传感器思路一模一样。3. 核心原理HDF 驱动模型与传感器数据通路3.1 HDF 驱动必须实现的三个函数Bind、Init、ReleaseHDF 框架对驱动有一套统一的生命周期管理绕不开的就是Bind、Init、Release三个回调。第一次接触时我觉得这跟 Linux 的 probe/release 很像但 HDF 有自己的特点。Bind的作用是把驱动实例和服务接口绑定起来。在传感器驱动里通常会在Bind中创建一个服务实例然后通过IDriverEntry的绑定接口把它发布到 HDF 服务管理器中。如果这一步没做好上层服务就找不到你这个驱动。Init是驱动真正初始化的入口传感器上电、复位、配置采集模式都在这里完成。Release是驱动卸载时的清理入口要释放中断、关闭 I2C 设备、释放申请的资源。我见过很多人把初始化逻辑一股脑塞到Init里比如在Init里做 I2C 通信测试、读取设备 ID其实这样并不是最优做法。合理的做法是Init只做最基础的数据结构初始化和资源申请把跟硬件真正通信的部分放到“首次上报数据之前”或者单独的工作队列里去执行。这样做的好处是驱动加载速度更快也能避免因为硬件未就绪导致驱动起不来。HDF 驱动还有一个很重要的概念叫serviceName。这个字符串是驱动对外暴露的名字传感器服务层会通过这个名字找到你的驱动。配置里写错一个字母驱动就算加载成功服务层也连不上。我调试那会儿折腾了一晚上后来发现就是配置里 serviceName 跟驱动实现里的宏定义大小写不一致。3.2 HCS 配置HDF 驱动的“设备树”HCSHDF Configuration Source是 OpenHarmony 自己搞的一套配置语言编译的时候会被解析成二进制配置驱动运行时通过 HDF 配置接口读取。一个完整的传感器节点通常长这样device_sensor_aht20 { device_info { match_attr sensor_aht20_info; // 驱动匹配的关键字段 moduleName sensor_aht20_driver; // 驱动注册时的模块名 serviceName sensor_aht20_service; // 服务发布名 } sensorInfo { sensorName aht20_temperature_humidity; sensorTypeId 1; // 1 表示温度2 表示湿度 chipType 0; } i2cConfig { busNum 3; // 控制器编号不同平台不同 deviceAddr 0x38; // 传感器 I2C 从机地址 freq 400; // I2C 频率单位 kHz } }moduleName对应驱动代码里的HDF_INIT注册名称match_attr则是设备节点和板级配置关联的桥梁。如果你在开发板上新增了一个外设节点需要在板级配置里加上对应的device_sensor_aht20段否则系统根本不会去加载你的驱动。这块很容易犯的错误是把device_info里的字段跟sensorInfo里的字段混在一起。我一开始把sensorTypeId塞到device_info里编译不报错运行的时候传感器服务层拿不到类型信息上报通道一直无法建立。后来老老实实按照 Sensor 子系统的文档把device_info、sensorInfo分开写才正常。3.3 数据从寄存器一路走到应用层的完整链路很多教程直接讲“读寄存器、换算、上报”但是对数据通路的中间环节介绍得很少。这里我用自己项目里的实际路径梳理一遍传感器服务层启动后通过 HDF 接口按照serviceName找到驱动。应用注册监听时服务层会把采样频率、缓存策略等参数通过 HDF 消息传给驱动。驱动收到消息后在ReadSensorData回调里通过 I2C 读传感器寄存器。驱动把原始数据填进SensorData结构体调用上报接口把数据发出去。服务层拿到数据后按照传感器的量程和单位做换算缓存到共享内存。应用层通过订阅回调拿到数据经过ohos.sensor接口抛出温度/湿度数值。驱动开发者最需要关心的就是第 3 步和第 4 步。只要能把合理的SensorData数据上报上去服务层和应用层的工作就都交给系统完成了。也就是说驱动就像一个翻译把硬件的数据翻译成系统听得懂的格式。很多刚学的人会在“换算公式”上纠结太久比如温度数据要除 1024、再乘 200这当然重要但更重要的是理解什么时候触发采样、什么时候上报、上报的格式是否匹配。4. 实操从零编写并接入一个温湿度传感器驱动4.1 环境准备与源码目录定位开始之前假设你已经搭好了 OpenHarmony 的编译环境。如果没有建议先按照官方文档把标准系统镜像编译跑通再回来做驱动开发。因为我用的是标准系统开发板所以整个开发流程涉及drivers/peripheral/sensor目录和vendor下的板级配置目录。我个人建议把驱动代码放在drivers/peripheral/sensor/chipset下面每个芯片一个子目录这样后续维护比较清晰。比如我新建了drivers/peripheral/sensor/chipset/aht20/ ├── BUILD.gn ├── sensor_aht20_driver.c └── sensor_aht20_driver.hBUILD.gn是 OpenHarmony 的构建脚本里面要声明哪些源文件参与编译以及依赖了哪些 HDF 头文件和库。一个简单的BUILD.gn长这样import(//drivers/hdf_core/adapter/uhdf/uhdf.gni) import(//build/ohos.gni) ohos_driver_entry(sensor_aht20_driver) { sources [ sensor_aht20_driver.c ] include_dirs [ //drivers/hdf_core/framework/include, //drivers/hdf_core/framework/include/core, //drivers/hdf_core/framework/include/osal, //drivers/peripheral/sensor/include, //drivers/peripheral/sensor/chipset, ] deps [ //drivers/hdf_core/adapter/uhdf/uhdf:libhdf ] }关于目录位置有一种观点认为芯片驱动放在drivers/peripheral/sensor/chipset更规范因为 OpenHarmony 上游也是按这个套路管理的。不要把驱动写在device/下某个具体开发板的config里那样耦合太重换一块板子就废了。4.2 硬件接线与 I2C 探测我用的传感器是 I2C 接口的四根线VCC、GND、SCL、SDA。开发板上一般会有多个 I2C 控制器哪个能用要看原理图或者跑一下i2cdetect类似的工具。OpenHarmony 里没有现成的i2cdetect但你可以临时写一个读寄存器的小程序来测试或者直接在驱动里打印 I2C 读写返回值。接线时特别要注意上拉电阻。很多 I2C 传感器模块板载已经有上拉电阻可以直接用如果是裸芯片必须在 SCL 和 SDA 上接 4.7kΩ 左右的上拉电阻到 VCC否则通信会时好时坏。我刚开始图省事直接用杜邦线连接结果 10 次读取有 3 次超时后来加上拉电阻才稳定。确认 I2C 总线编号的办法一个是查开发板的原理图另一个是在系统起来后去看/dev下的 I2C 设备节点。OpenHarmony 标准系统里 I2C 控制器一般会注册为/dev/i2c-xx 就是总线编号。搞清楚总线和设备地址HCS 配置里的busNum和deviceAddr才能填对。4.3 编写驱动主流程绑定、初始化、读取、上报下面这段代码是我的驱动核心逻辑经过简化但保留了完整的流程骨架。先说一下整体设计Bind里创建服务、注册回调Init里解析 HCS 配置、拿到 I2C 设备句柄、对传感器做软复位ReadData里通过 I2C 读取温湿度寄存器、换算后填到SensorData中。#include hdf_device_desc.h #include hdf_iotype.h #include sensor_device_if.h #include sensor_config_parser.h #include i2c_if.h #define AHT20_ADDR 0x38 #define AHT20_INIT_CMD 0xBE #define AHT20_TRIGGER_CMD 0xAC #define AHT20_SOFT_RESET 0xBA /* 驱动私有数据结构 */ typedef struct { struct SensorDeviceCfg *cfg; // 从 HCS 解析出的传感器配置 DevHandle i2cHandle; // I2C 控制器句柄 uint8_t addr; // I2C 从机地址 } Aht20Driver; static Aht20Driver g_aht20Driver; /* 读取温湿度原始数据保存在 data 数组中 */ static int32_t Aht20ReadRawData(uint8_t *data, uint8_t len) { uint8_t trigger[3] { AHT20_TRIGGER_CMD, 0x33, 0x00 }; uint8_t status; int32_t ret; /* 发送触发测量命令 */ ret I2cWrite(g_aht20Driver.i2cHandle, g_aht20Driver.addr, trigger, sizeof(trigger)); if (ret ! HDF_SUCCESS) { return ret; } /* 等待测量完成最多轮询 100 次 */ for (int i 0; i 100; i) { ret I2cRead(g_aht20Driver.i2cHandle, g_aht20Driver.addr, status, 1); if (ret ! HDF_SUCCESS) { return ret; } if ((status 0x80) 0) { // bit7 为 0 表示测量完成 break; } OsalMDelay(10); } if ((status 0x80) ! 0) { return HDF_FAILURE; // 测量超时 } return I2cRead(g_aht20Driver.i2cHandle, g_aht20Driver.addr, data, len); } /* 换算并上报温湿度数据 */ static int32_t Aht20ReadData(struct SensorDeviceCfg *cfg, struct SensorData *data) { uint8_t buf[6] { 0 }; int32_t ret Aht20ReadRawData(buf, sizeof(buf)); if (ret ! HDF_SUCCESS) { return ret; } /* 传感器数据格式状态字节、湿度高8位、湿度低8位、湿度低4位温度高4位、温度中8位、温度低8位 */ uint32_t humidityRaw ((uint32_t)buf[1] 12) | ((uint32_t)buf[2] 4) | ((uint32_t)buf[3] 4); uint32_t tempRaw (((uint32_t)buf[3] 0x0F) 16) | ((uint32_t)buf[4] 8) | buf[5]; float humidity (float)humidityRaw * 100.0f / 1048576.0f; float temperature (float)tempRaw * 200.0f / 1048576.0f - 50.0f; /* 按传感器类型填充上报数据区分温度与湿度两个逻辑传感器 */ if (cfg-sensorTypeId SENSOR_TYPE_TEMPERATURE) { >