最近在做OpenHarmony(开源鸿蒙)设备端开发,接到的第一个有点含金量的任务,就是给一块开发板适配温湿度传感器驱动。说实话,OpenHarmony的驱动框架和传统的Linux驱动、MCU裸机驱动都不一样,刚上手的时候资料少、坑多,网上的教程要么只讲应用开发,要么贴一堆源码却不解释为什么这么写。这篇就把我从头到尾做温湿度传感器驱动的完整过程写出来,包括框架选型、HDF配置、I2C读写、数据上报、还有我实际踩过的几个坑,给同样想入坑设备驱动的朋友做个参考。
这篇内容适合谁看?一是准备做OpenHarmony设备开发、但还没碰过驱动的人;二是做过Linux驱动想看看鸿蒙HDF有啥区别的人;三是单纯想搞清楚“传感器数据怎么从芯片一路跑到应用层”的好奇派。我尽量讲得直白一点,凡是涉及关键参数和步骤的地方都给出实际做法和原因,保证你按着走能复现。
1. 整体设计与思路拆解
1.1 为什么拿温湿度传感器当练手项目最合适
“万物智能”这个概念喊了很多年,落到硬件层面,第一步永远是感知环境,而温湿度就是最基础、最高频的两个环境参数。仓库监测、农业大棚、机房运维、智能家居,几乎所有场景都要用。从驱动开发角度讲,温湿度传感器又几乎是I2C设备的典型代表,寄存器少、时序简单、数据换算公式固定,非常适合当第一个驱动来写。
等你把这颗传感器彻底跑通,后面再遇到气压计、光照传感器、加速度计、陀螺仪,套路都一样:看手册确认I2C地址和寄存器,按手册时序发命令,读数据然后换算物理量,最后通过HDF上报给上层服务。可以说,温湿度驱动是整个OpenHarmony传感器驱动目录里性价比最高的入门样例,学会它,你就掌握了“HDF驱动框架 + I2C总线操作 + 传感器数据上报”这条主线。
1.2 驱动放用户态还是内核态,我是怎么选的
OpenHarmony的HDF驱动框架(Harmony Driver Foundation)支持两种驱动形态:内核态驱动和用户态驱动。很多人第一次听到这两个概念会纠结,其实选型逻辑不复杂,看你的外设需求和团队能力。
内核态驱动跑在内核空间,访问硬件效率高,但调试难度也高,一崩就是整机死机,对开发者的内核知识要求比较高。用户态驱动跑在用户空间,通过HDF提供的平台接口访问硬件,开发效率高、调试方便,安全性也好,驱动崩溃不至于带崩整个系统。对于一个采样频率每秒几次、数据量只有几个字节的温湿度传感器来说,用户态驱动绰绰有余。
所以我最终选择了用户态驱动。具体对比我放在下面,方便你做个判断:
| 对比维度 | 用户态驱动 | 内核态驱动 |
|---|---|---|
| 开发难度 | 低,按HDF框架接口写业务逻辑即可 | 高,需要懂内核对象、内存管理、并发 |
| 调试方式 | 可以直接打日志,出错后系统不挂 | 出错容易panic,需要串口或内核日志定位 |
| 实时性 | 一般,适合慢速设备 | 高,适合显示、总线控制器等场景 |
| 稳定性 | 驱动故障不会直接影响内核 | 一个指针错误可能导致整机崩溃 |
| 适合设备 | 传感器、外设IO、小屏 | 触摸屏、音频、GPU、DMA控制器 |
一句话总结:能用用户态就别碰内核态,尤其是做产品原型或者学习阶段,省下时间多踩几个业务层面的坑更有价值。
1.3 数据从传感器到应用层要经过哪些环节
很多人只写驱动,却不关心数据后面去哪了,结果驱动写好了不知道咋验证。我建议一开始就把整条链路搞明白,心里有数才不慌。
温湿度数据流向大致是这样的:传感器芯片采集温湿度原始数据 → 通过I2C总线传给开发板 → HDF驱动接收原始数据并换算成摄氏度和百分比 → 驱动把换算后的数据上报给OpenHarmony的传感器服务框架(Sensor Framework) → 应用层调用系统传感器接口拿到数据。
对你来说,驱动开发者最需要搞定的是前三级:I2C通信、数据解析、上报接口。传感器服务框架和应用层接口是系统已经封装好的,你只要按规范上报,应用层就能自动读取,不需要你额外开发应用。
2. 核心概念与框架细节
2.1 先搞懂HDF驱动的“户口本”机制
HDF框架和传统Linux驱动最大的不同,是它有一套自己的驱动描述和加载机制。你写好的驱动不是靠“驱动名+设备树匹配”自动加载,而是要在一个叫device_info.hcs的配置里注册,框架启动时根据这个配置去找对应的驱动入口,然后完成绑定和初始化。
打个比方:device_info.hcs就是驱动进程的“户口本”,你的驱动代码写好了但没有户口,系统根本不会理你。配置文件里需要声明设备ID、服务名称、设备节点名等关键信息,全系统统一编号、统一管理。这个设计的好处是硬件平台的改动能通过配置隔离,换一块板子,功能代码不用动,只改配置就行。
HDF驱动代码的核心入口有三个回调:Bind、Init、Release。Bind负责绑定服务,Init负责初始化硬件和资源,Release负责释放资源,三者的调用顺序和生命周期由HDF框架自动管理。你只需要把业务逻辑填进去,不用操心驱动什么时候被加载、什么时候被卸载。
2.2 I2C地址位数这个天坑,最多人栽在这里
温湿度传感器基本都是I2C接口,I2C通信第一步就是器件寻址。AHT20这类传感器的I2C地址一般是0x38(7位地址)。看到这个地方,新手最容易踩坑。
很多单片机的I2C库在写地址时会用8位地址,也就是把7位地址左移一位,末尾补上读写标志位。比如STM32的习惯就是发送0x70(0x38左移一位)表示写操作。但在OpenHarmony的HDF接口里,I2cMsg结构体中的addr字段用的是7位地址,你直接填0x38就行,绝对不要再左移。
我当时从一份MCU的参考代码移植过来,没注意这个差异,结果I2C通信一直NACK,排查了半天。所以看到任何示例代码,先确认它是7位地址还是8位地址,再填到HDF的I2cMsg里。
2.3 传感器的上电时序和测量时序,一步都不能省
温湿度传感器内部其实是个小状态机,上电后要完成内部初始化才能响应测量命令。AHT20的上电流程是:传感器上电后等待至少40ms,然后发送初始化命令0xBE,再等待约10ms,之后才可以触发测量。
测量流程也有严格顺序:发送触发测量命令0xAC,附带参数0x33和0x00,然后等待至少80ms,让传感器完成一次采集,再读取6个字节的数据。如果等待时间不够,读回来的状态字节最高位(bit[7])会是1,表示测量还在进行中。
这个80ms等待很多人不在意,图省事直接读,结果读出来全是0xFF或者上一轮的旧数据。我在调试时就遇到过类似问题,后来把读取逻辑改成“先发命令,等80ms,再读数据”,数值才稳定下来。传感器这种设备,时序就是硬约束,手册写多少就是多少,别想当然”多等一会应该没事“,多等没问题,少等就出错。
3. 实操过程与核心环节实现
3.1 驱动工程目录结构和编译依赖
在OpenHarmony开源代码里,驱动一般放在drivers/peripheral或者drivers/hdf_core相关的目录中。我习惯把这类独立的传感器驱动放在drivers/peripheral下,这样既能被HDF框架识别,也方便参与系统整体编译。工程结构如下:
drivers/peripheral/temp_humi/ ├── BUILD.gn ├── include/ │ └── temp_humi_driver.h └── src/ ├── temp_humi_driver.c └── temp_humi_sensor.cBUILD.gn是整个驱动的编译脚本,需要声明依赖的框架头文件和库。我这里给出一个精简但能直接跑的版本:
import("//drivers/framework/model/sensor/driver/device/sensor_driver.gni") hdf_driver("temp_humi_driver") { sources = [ "src/temp_humi_driver.c", "src/temp_humi_sensor.c" ] include_dirs = [ "include", "//drivers/framework/include/core", "//drivers/framework/include/platform", "//drivers/framework/include/osal" ] deps = [ "//drivers/framework:drivers_framework", "//drivers/framework/lib:drivers_platform_driver" ] }有个细节需要注意,deps里的drivers_framework和drivers_platform_driver缺一不可。前者提供HDF驱动入口和生命周期管理,后者提供I2C等平台接口的具体实现。漏掉任何一个,编译时会报找不到DeviceI2cOpen这类符号。
3.2 驱动入口:Bind、Init、Release一个都不能少
驱动入口的核心结构其实很简洁,就是在Init里完成硬件初始化和资源分配,在Release里释放,在Bind里把服务接口注册出去。我写的驱动入口大概是这个样子:
#include "device/hdf_driver.h" #include "hdf_device_desc.h" #include "hdf_log.h" #include "osal/osal_mutex.h" #include "platform/i2c_if.h" #define HDF_LOG_TAG temp_humi_driver static struct I2cDevHandle *g_i2cHandle = NULL; static int32_t TempHumiSensorInit(struct HdfDeviceObject *device) { (void)device; int32_t ret; uint8_t initCommand[] = {0xBE}; uint8_t softReset[] = {0xBA}; // 打开I2C控制器,hostId需要和device_info.hcs里配置一致 g_i2cHandle = DeviceI2cOpen(3); if (g_i2cHandle == NULL) { HDF_LOGE("I2C open failed"); return HDF_ERR_DEVICE_NOT_SUPPORT; } // 上电延时,给传感器内部完成初始化 OsalMSleep(40); // 发送软复位命令,让传感器回到可控状态 ret = DeviceI2cWrite(g_i2cHandle, 0x38, softReset, sizeof(softReset)); if (ret != HDF_SUCCESS) { HDF_LOGE("soft reset failed, ret=%d", ret); DeviceI2cClose(g_i2cHandle); g_i2cHandle = NULL; return ret; } OsalMSleep(10); // 发送初始化命令 ret = DeviceI2cWrite(g_i2cHandle, 0x38, initCommand, sizeof(initCommand)); if (ret != HDF_SUCCESS) { HDF_LOGE("sensor init failed, ret=%d", ret); DeviceI2cClose(g_i2cHandle); g_i2cHandle = NULL; return ret; } OsalMSleep(10); HDF_LOGI("temp humi sensor init success"); return HDF_SUCCESS; }Bind函数的主要作用是把驱动服务注册到HDF框架,虽然温湿度传感器本身不对外暴露复杂业务接口,但作为一个合格的驱动,绑定步骤不能省:
static int32_t TempHumiDriverBind(struct HdfDeviceObject *device) { (void)device; // 如果有对外服务接口,在这里把服务实例挂载到device->service return HDF_SUCCESS; }Release函数对应的就是驱动卸载时的资源清理:
static void TempHumiDriverRelease(struct HdfDeviceObject *device) { (void)device; if (g_i2cHandle != NULL) { DeviceI2cClose(g_i2cHandle); g_i2cHandle = NULL; } } struct HdfDriverEntry g_tempHumiDriverEntry = { .moduleVersion = 1, .moduleName = "temp_humi_driver", .Bind = TempHumiDriverBind, .Init = TempHumiSensorInit, .Release = TempHumiDriverRelease, }; HDF_INIT(g_tempHumiDriverEntry);注意看.moduleName的值,是"temp_humi_driver"。这个字符串必须和device_info.hcs里的driverName完全一致,否则HDF框架找不到驱动入口,系统日志里会报service create failed。
3.3 数据读取与温湿度换算的完整实现
驱动初始化之后,剩下就是按测量时序触发采集并读取数据。I2C读温湿度数据的标准流程是:先发送0xAC、0x33、0x00触发一次测量,等待80ms,再从寄存器读取6个字节。这6个字节分别是:1个状态字节、2.5字节湿度数据、2.5字节温度数据。
AHT20的数据换算公式为:
湿度值(%RH)= 湿度原始值 / 2^20 * 100 温度值(℃)= 温度原始值 / 2^20 * 200 - 50换算逻辑里最容易被坑的是字节拼接。传感器输出的是20位无符号整数,分两段存放在不同字节里,直接使用uint8_t数组按位拼回即可。我这里用uint32_t保存拼接结果,避免溢出:
static int32_t TempHumiReadData(float *temperature, float *humidity) { uint8_t command[3] = {0xAC, 0x33, 0x00}; uint8_t data[6] = {0}; uint32_t rawHumi = 0; uint32_t rawTemp = 0; int32_t ret; // 发送触发测量命令 struct I2cMsg msgs[2]; msgs[0].addr = 0x38; msgs[0].buf = command; msgs[0].len = 3; msgs[0].flag = 0; msgs[1].addr = 0x38; msgs[1].buf = data; msgs[1].len = 6; msgs[1].flag = I2C_FLAG_READ; ret = DeviceI2cTransfer(g_i2cHandle, msgs, 2); if (ret != 2) { HDF_LOGE("I2C transfer failed, ret=%d", ret); return HDF_FAILURE; } // 等待测量完成 OsalMSleep(80); // 状态字节bit[7]为1表示测量占用中,需要重新读 if (data[0] & 0x80) { HDF_LOGE("sensor is busy, status=0x%02x", data[0]); return HDF_FAILURE; } // 拼接湿度:前4位 + 中间8位 + 低4位 rawHumi = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | ((uint32_t)data[3] >> 4); // 拼接温度:中4位 + 低8位 + 低4位 rawTemp = (((uint32_t)data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5]; *humidity = (float)rawHumi * 100.0f / (float)(1 << 20); *temperature = (float)rawTemp * 200.0f / (float)(1 << 20) - 50.0f; HDF_LOGI("rawHumi=%u, rawTemp=%u", rawHumi, rawTemp); return HDF_SUCCESS; }这里我把读数据和触发测量放在同一个I2cTransfer里完成,减少了一次函数调用开销,也能保证时序的紧凑性。注意DeviceI2cTransfer返回的是实际传输的消息数量,判断时要和2比较,而不是和HDF_SUCCESS比较,否则错误场景判断不准。
3.4 把数据上报到Sensor框架
驱动测出了温湿度,但如果不上报,应用层永远拿不到。OpenHarmony的传感器框架要求驱动把数据封装成规定的上报结构,然后调用上报接口推给系统服务。
上报逻辑大致是这样,我做了简化:
#include "sensor_service_if.h" static void TempHumiSensorReadAndReport(void) { float temp = 0.0f; float humi = 0.0f; int32_t ret; ret = TempHumiReadData(&temp, &humi); if (ret != HDF_SUCCESS) { return; } // 填充传感器上报数据结构 struct SensorReportData reportData; memset(&reportData, 0, sizeof(reportData)); reportData.sensorTypeId = SENSOR_TYPE_TEMPERATURE; reportData.dataLen = sizeof(float); memcpy(reportData.data, &temp, sizeof(float)); // 调用传感器框架的上报接口 ret = ReportSensorData(&reportData); if (ret != HDF_SUCCESS) { HDF_LOGE("report temp data failed, ret=%d", ret); } reportData.sensorTypeId = SENSOR_TYPE_HUMIDITY; memcpy(reportData.data, &humi, sizeof(float)); ret = ReportSensorData(&reportData); if (ret != HDF_SUCCESS) { HDF_LOGE("report humi data failed, ret=%d", ret); } }这一步需要特别注意传感器类型ID的枚举值,在OpenHarmony头文件里SENSOR_TYPE_TEMPERATURE和SENSOR_TYPE_HUMIDITY是已经定义好的宏,直接用就行,别自己瞎填数字。上报周期不建议太频繁,温湿度本身变化慢,我测试时用1秒一次已经绰绰有余,太频繁反而会增大系统功耗和日志刷屏。
3.5 编译、烧录与验证一次跑通
OpenHarmony的编译是分build目标来做的,我用的命令大致是:
hb build -f -T temp_humi_driver如果只想看驱动模块编译过没过,这种单模块编译方式最省时间。如果要把驱动编进系统固件,要在对应产品的config.json里把temp_humi_driver模块加到subsystem列表,然后执行完整编译。
烧录完成之后,串口终端或者hilog里能看到类似下面的日志:
01-01 08:00:00.001 1234 1234 I 03f00/temp_humi_driver: rawHumi=512000, rawTemp=823296 01-01 08:00:00.001 1234 1234 I 03f00/temp_humi_driver: report temp data success 01-01 08:00:00.001 1234 1234 I 03f00/temp_humi_driver: report humi data success看到report success基本上说明驱动已经通了。之后可以用一个简单的测试应用调用系统传感器接口,试着读取当前温湿度,或者干脆用手捏住传感器,观察几秒后数据是否缓慢上升。我测试的时候用手心捂住传感器,不到5秒温度就升了2度多,说明数据链路是通的。
4. 常见问题与排查技巧实录
4.1 I2C通信一直NACK,从哪里开始查
NACK是最常见也最让人抓狂的问题,现象就是I2C transfer failed。按我排查的经验,顺序应该是:先查硬件连接,再查地址,最后查时序。
第一,确认传感器的SCL和SDA是否接对了I2C控制器的对应引脚,而且最好加上拉电阻,很多传感器模块自己带了上拉,没问题;如果是裸芯片,不加上拉信号根本拉不上去。
第二,确认I2C从设备地址。不同传感器的地址差异很大,AHT20是0x38,但有些厂家会把地址引脚拉高变成0x39。直接用同一个地址强行读,设备没有响应就会NACK。
第三,确认I2C速率。尽量把I2C时钟频率配置在100kHz到400kHz之间,传感器手册都会写最大速率。如果其他代码把I2C总线速率调到了1MHz,传感器跟不上,也会表现为随机性NACK。
实在排查不出来,上逻辑分析仪看波形,这是最直接的手段。SCL上有没有连续的时钟,SDA上有没有ACK位,一看便知。
4.2 读出来的温湿度数值忽大忽小,原因在哪
数据抖动比固定错值更坑人。固定错值往往是公式问题或者字节顺序问题,抖动则很多时候是测量时序没控制好。
一个典型错误是触发测量后马上读数据,传感器还没准备好,读到的状态字节显示busy,但你忽略了状态判断,继续往下解析数据,结果自然是一堆垃圾。另一个常见问题是连续触发测量的间隔太短,传感器的采样周期一般在几十毫秒量级,你如果每10ms触发一次,芯片根本忙不过来。
还有一点是浮点运算精度。在驱动代码里做换算时,尽量先保留原始整数,最后一步再转浮点,不要每一步都转float,累积误差会让最终数值完全不准。
4.3 驱动编译过了但加载失败,报service create failed
这个报错十有八九是device_info.hcs配置和你代码里的moduleName对不上。HDF框架根据device_info.hcs里的driverName去查找模块入口,你的HdfDriverEntry.moduleName必须一字不差。我排查过一个案例,配置里写了temp_humi,模块名却是temp_humi_driver,结果白白折腾了小半天。
另外检查BUILD.gn里是否真的把驱动编进了系统镜像。模块单独编译通过不代表它被打进了最终固件,只有把驱动模块加进产品的config.json配置,编译镜像的时候才会带上。
4.4 驱动正常上报,但应用层始终读不到数据
驱动日志显示report success,说明数据已经上报给传感器服务框架了。这时候应用层还读不到,问题往往出在传感器服务的使能状态。
OpenHarmony的传感器服务默认不会一直把数据推给每个应用,应用必须先调用使能接口,比如通过传感器接口的Enable把指定类型的传感器打开,并设置合理的数据上报间隔,之后应用才能持续收到温湿度数据。简单说,驱动只是供水的水厂,应用得先拧开水龙头。
还有一种情况是上报类型ID填错。你上报的数据类型如果和传感器服务订阅的类型不一致,服务端会过滤掉,表现为驱动在拼命上报,应用却毫无反应。用hilog过滤SENSOR关键字,能看到服务端的接收日志,非常管用。
最后分享一点个人体会
这套温湿度驱动做下来,最大的收获不是代码本身,而是理解了OpenHarmony设备开发里“配置驱动一切”的思路。HDF框架把硬件差异靠hcs配置隔离掉,驱动代码本身可以跨平台复用,这对做产品、出方案来说非常友好。建议你拿到一块新板子的时候,先不要急着写代码,花半天时间把device_info.hcs读懂,把I2C引脚和控制器编号理清楚,后面能省出好几天的调试时间。开了这个头,后面不管接陀螺仪还是接激光测距传感器,都不会再觉得驱动开发是个黑盒子了。