一块很常见的温湿度传感器模块一根杜邦线一块能跑OpenHarmony的开发板再加一个下午的调试时间就能把温度数据从I2C总线一路读到系统上层。这篇内容就是把这套过程完整复盘——从驱动为什么放在HDF框架里到配置怎么写、接口怎么调、日志怎么抓再到哪些坑需要避开一起讲清楚。适合正在接触OpenHarmony驱动开发、手头有开发板想接传感器的同学也适合想把Linux驱动经验迁移到OpenHarmony平台上的嵌入式开发者。即使你之前没写过任何驱动代码从这篇文章的配置和代码片段里也能把一个温湿度传感器驱动从无到有跑通。1. 为什么OpenHarmony驱动开发绕不开HDF框架刚接触OpenHarmony的人最容易产生一个疑问我以前在Linux上写驱动直接注册一个platform_driver或者用i2c_driver结构体注册然后就能在/dev下看到设备节点了到了OpenHarmony这里为什么非要学一个新框架这个疑问我也有过。但真正看完HDFHardware Driver Foundation硬件驱动框架的代码结构再对比传统方式之后就会发现它不是为了换名字而换名字而是整个设备管理思路变了。1.1 传统驱动模式在OpenHarmony场景下的短板传统嵌入式Linux的驱动模型本质上是一套以“设备节点”为锚点的方案。驱动在初始化时向内核注册内核帮你生成设备节点应用层通过open/read/write/ioctl操作这个节点。这个模式运行了很多年本身很成熟但如果放到一个面向多设备、多场景、动态加载的物联网操作系统里就会暴露出几个问题。第一个问题是驱动和内核强耦合。驱动一旦加载就是在内核空间运行出问题直接影响整个系统稳定性而且不同芯片平台之间驱动代码复用率不高换个板子经常要改一堆平台相关代码。第二个问题是设备管理是“静态”的。传统驱动主要靠设备树或者板级文件来描述硬件硬件变化时要改配置重新编译内核做不到运行时的动态感知和管理。第三个问题是应用层和驱动之间的服务化通道比较弱。传统方式里应用拿到的只是一个文件描述符中间缺少一套统一的、可追踪、可管控的服务治理机制。在OpenHarmony的设计语境里系统要跑在从轻量设备到富设备的多个形态上芯片平台非常多外设种类更是千奇百怪。如果每个驱动都各自为政上面这些问题会被无限放大。所以HDF的定位就很明确了它是一套统一的驱动开发、加载、管理和服务发布框架。1.2 HDF框架的核心设计Host、Device、Service三层关系HDF框架其实不复杂核心就三样东西Host驱动宿主、Device设备对象、Service驱动服务。Host可以理解为一类设备的“容器”。比如所有传感器驱动挂在一个sensor_host下面所有显示驱动挂在display_host下面。Host本身也是一个驱动它负责管理自己名下的所有Device的加载、卸载和调度。Device就是具体的一个硬件设备抽象对应到我们这次项目里就是一个温湿度传感器实例。而Service是驱动对外提供能力的出入口应用或者其他系统服务不直接操作Device而是通过Service来调用驱动能力。三者之间的协作关系可以类比一个物业体系Host是整栋楼的物业办公室Device是楼里的每一户Service是每户门口的访客对讲机。你要办事不是直接踹门进去而是先按对讲机Service里面的人Device确认后提供服务调用驱动接口整个过程有统一的登记和管理逻辑。这种设计的直接好处是驱动被加载后系统可以统一管理它的生命周期可以在运行时按需加载、卸载驱动模块也可以动态感知设备插入和拔出而且上层服务不用关心底层硬件到底用的是哪个芯片、哪条总线因为Service接口是统一的。对于传感器这类需要频繁数据交互的设备这套机制的稳定性和可维护性明显比传统裸内核驱动要好。1.3 那是不是所有驱动都非要用HDF也不是。如果只是临时调试或者硬件非常固定、永远不会变化用传统内核驱动方式也能跑。但OpenHarmony的驱动仓结构里推荐的做法是把设备驱动都放在HDF框架下尤其面向量产、需要反复升级维护的产品。我的实际建议是只要是准备长期维护、后续要接多款同类型传感器的模块尽量从一开始就写成HDF的host-device-service结构。虽然前期多写几个文件但后面加一个新传感器只需要增加一个device实例配置几行HCS驱动代码复用率会非常高。这篇文章后面写的驱动也是按照这个结构展开的。2. 环境准备与工程配置先把地基打好驱动开发这件事代码写得再好环境配不对也白搭。OpenHarmony的源码树和编译工具链跟常见的单片机工程不太一样第一次接触时容易在目录结构上迷路。我把这次开发用到的东西以及配置过程完整列出来。2.1 硬件选型与连接要点这次用的开发板是某款基于3568芯片方案的通用评估板标准系统版本本身带I2C控制器。传感器模块选的是一颗AHT20温湿度芯片的成品小板模块上已经集成了I2C上拉电阻和稳压电路引脚直接引出四根线VCC、GND、SCL、SDA。连接关系如下表传感器模块引脚开发板接口说明VCC3.3VAHT20工作电压范围是2.0V到5.5V建议稳定在3.3VGNDGND共地必须接好否则I2C通信会不稳定SCLI2C3_SCL对应板上丝印时钟线由主机控制SDAI2C3_SDA数据线双向传输选I2C3是因为这块开发板把I2C3默认引到了扩展排针上不用额外配置引脚复用方便。项目里如果要用其他I2C控制器方法一样只要把驱动里的bus编号对上就行。提示如果你买的是裸芯片而不是模块一定要自己在SCL和SDA上加4.7kΩ左右的上拉电阻到VCC。I2C是开漏结构没有上拉电阻总线根本拉不上去通信会一直超时。模块的话一般已经加好了直接用即可。2.2 源码、工具链与产品配置准备OpenHarmony的驱动开发源码编译是绕不开的。本地准备一个Linux环境我用的是Ubuntu 20.04的容器8核16G源码用官方开源仓库的标准系统分支。拿到源码后几个关键目录要心里有数drivers/framework/HDF框架本身包括内核态的驱动管理、接口定义一般不需要改。drivers/peripheral/外设驱动仓传感器、显示、音频等驱动都在这下面分类。vendor/xxx/具体产品的配置目录里面放产品形态定义、组件打包配置和启动脚本。device/xxx/硬件平台适配层一般由芯片厂商提供。写驱动时驱动实体代码放在drivers/peripheral/sensor/下比较合适因为OpenHarmony的Sensor组件本身有一套独立的HDF驱动管理逻辑。如果只是想快速跑通也可以建一个独立的drivers模块把驱动代码挂进产品编译清单里。为了贴近实际传感器开发流程我建议按标准的sensor peripheral结构放。编译工具是hbOpenHarmony Build。首次使用要安装hb工具链python3 -m pip install --user build/lite然后在源码根目录执行hb set会进入一个交互式界面选择对应的产品目录。如果没有现成的产品适配自己的板子可以用已有的通用产品配置做模板复制一份到vendor/自己的目录下。选好产品后执行hb build -f首次全量编译时间比较长8核机器大概需要三十分钟到一个小时。编译完成后镜像输出在out/产品名目录下。烧录方式各板子不同一般通过烧录工具把boot和system分区镜像写进板子这一步按开发板的官方烧录说明做即可。2.3 工程里驱动代码的放置方式驱动代码在源码树里的位置直接决定了编译系统能不能找到它。以BUILD.gn为例如果我把驱动放在drivers/peripheral/sensor/temp_humi/下文件结构是这样drivers/peripheral/sensor/temp_humi/ ├── BUILD.gn ├── src/ │ └── temp_humi_driver.c ├── include/ │ └── temp_humi_driver.h └── config/ └── temp_humi_device.hcsBUILD.gn里要把源文件、头文件目录和依赖的HDF库声明清楚。一个最简的BUILD.gn示例import(//build_config/basic.gni) hdf_driver(temp_humi_driver) { sources [ src/temp_humi_driver.c ] include_dirs [ include, ] deps [ //drivers/framework/core/manager:hdf_core, //drivers/framework/support/platform/include:hdf_platform, ] }这样编译系统就会把驱动编成一个独立的动态库或者ko文件再通过HCS配置在系统启动时加载。这个配置方法我实际试过多次最容易出错的地方是deps依赖少写。HDF驱动如果依赖了平台层的I2C能力却没有把对应的hdf_platform库加进依赖链接阶段会报一大堆符号找不到的错很烦。第一次写驱动时建议直接参考drivers/peripheral下已有的传感器驱动BUILD.gn把deps段抄过来改比自己从零写靠谱得多。3. 传感器原理与数据解析先把数据算对再谈驱动驱动的核心职责是“把硬件的信号变成系统能识别的数据结构”。写驱动之前先要搞懂传感器本身的数据手册尤其是数据位长、计算公式和时序要求。不懂这些代码写得再漂亮读出来的数据也是错的。3.1 AHT20的内部逻辑与I2C读写流程AHT20是一颗数字温湿度传感器内部有测温单元、测湿单元和ADC通过I2C接口输出数字结果。它的I2C从设备地址是0x38支持标准模式和快速模式。初始化流程分三步上电后等待至少100ms让芯片内部完成上电复位。发送0xBE命令执行初始化校准有些版本叫软复位校准芯片收到命令后会重新校准内部参数。再等至少10ms之后可以发出测量命令。触发测量时主机发送0xAC同时附带两个字节0x33、0x00。芯片收到后开始测量典型测量时间约80ms。等待80ms后主机从设备地址读回6个字节数据。这6个字节的排布如下第1字节状态字节其中bit[3]表示是否已校准读到1说明校准完成。第2到第4字节的高20位湿度原始数据。第4字节的低4位加上第5、第6字节温度原始数据。这里有个常见的坑很多人直接把读到的字节拼成24位整数结果算出来的温度湿度高得离谱或低得离谱原因就是没注意AHT20的原始值只有20位数据位里混入了4个保留的“0”位拼接时必须做右移处理。3.2 数据位拼接与计算公式以AHT20为例读取到的6个字节分别记为b0到b5。湿度原始值的正确拼法uint32_t hum_raw ((uint32_t)(b1 0x0F) 16) | ((uint32_t)b2 8) | b3;等一下这个写法依赖具体的字节含义不同驱动示例里定义不完全一样。参考我手头这款传感器的DataSheet实际更严谨的写法是// b0为状态字节b1为湿度高位b2为湿度中位b3集合了湿度低位和温度高位 uint32_t hum_raw ((uint32_t)b1 12) | ((uint32_t)b2 4) | (b3 4); uint32_t temp_raw ((uint32_t)(b3 0x0F) 16) | ((uint32_t)b4 8) | b5;这个写法把两个20位原始值都拼出来了。拿到原始值后按公式换算RH hum_raw / 2^20 * 100% T temp_raw / 2^20 * 200 - 50比如实测读到一组数据湿度原始值hum_raw 0x80000524288温度原始值temp_raw 0x60000393216那么湿度 524288 / 1048576 * 100% 50.0%RH温度 393216 / 1048576 * 200 - 50 25.0°C驱动代码里对应做一次整数运算即可注意用uint32_t避免溢出最后以浮点数输出float humidity (float)hum_raw / 1048576.0f * 100.0f; float temperature (float)temp_raw / 1048576.0f * 200.0f - 50.0f;3.3 不要忽略测量间隔对精度的影响传感器本身会发热尤其是持续、高频率地发起测量时芯片自热会影响温度读数。AHT20的手册建议测量间隔最好不要小于1秒我看到不少项目为了“实时性”把读取频率调到10Hz、20Hz结果温度越读越高偏差能到2°C以上。所以驱动程序里不要在每次温度请求时都立刻打I2C事务而应该在驱动内部做一个简单的节流控制或者由上层限频调用。我这边驱动实现了一个最小间隔判断距离上次测量不足1000ms直接返回最近一次缓存的数据。这个细节看起来不起眼但实测对数据稳定性帮助很大。4. HDF驱动核心实现手把手写出可上线的驱动现在进入正题。以HDF框架实现温湿度传感器驱动整体包括驱动入口、I2C读写逻辑、数据换算、服务发布和HCS配置几个部分。4.1 驱动入口三件套Bind、Init、ReleaseHDF框架要求每个驱动实现三个核心接口Bind、Init、Release。Bind负责把驱动服务绑定到设备对象上Init做硬件初始化和资源分配Release做反向清理。一个标准的驱动入口结构如下#include hdf_base.h #include hdf_device_desc.h #include hdf_log.h #define HDF_LOG_TAG temp_humi_driver static int32_t TempHumiBind(struct HdfDeviceObject *deviceObject) { if (deviceObject NULL) { return HDF_ERR_INVALID_PARAM; } // Bind阶段主要做服务注册为后续Init做准备 return HDF_SUCCESS; } static int32_t TempHumiInit(struct HdfDeviceObject *deviceObject) { if (deviceObject NULL) { return HDF_ERR_INVALID_PARAM; } // 1. 打开I2C控制器句柄 // 2. 发送0xBE完成传感器初始化校准 // 3. 创建并注册数据读取服务 return HDF_SUCCESS; } static void TempHumiRelease(struct HdfDeviceObject *deviceObject) { if (deviceObject NULL) { return; } // 关闭I2C句柄释放缓存注销服务 } struct HdfDriverEntry g_tempHumiDriverEntry { .moduleVersion 1, .moduleName temp_humi_driver, .Bind TempHumiBind, .Init TempHumiInit, .Release TempHumiRelease, }; HDF_INIT(g_tempHumiDriverEntry);关于Bind和Init的区别刚开始写HDF驱动时容易混淆。Bind阶段设备对象已经出现但硬件资源还没有准备好它主要做服务映射和后续接口准备工作。Init阶段驱动正式开始工作可以打开总线、配置寄存器。Release则是逆流程哪怕Init失败系统也会调用Release来做清理所以Release里一定要做空指针判断。4.2 I2C读写与温湿度数据读取HDF框架对平台总线访问做了一层统一接口封装I2C相关接口在i2c_if.h里。读写I2C的示例代码如下#include i2c_if.h #define AHT20_ADDR 0x38 #define AHT20_MEASURE_CMD 0xAC #define AHT20_MEASURE_CMD_1 0x33 #define AHT20_MEASURE_CMD_2 0x00 #define AHT20_INIT_CMD 0xBE static DevHandle g_i2cHandle NULL; static int32_t TempHumiOpenI2c(void) { g_i2cHandle I2cOpen(3); // 打开I2C控制器3按实际板卡配置 if (g_i2cHandle NULL) { HDF_LOGE(I2cOpen failed); return HDF_FAILURE; } return HDF_SUCCESS; } static int32_t TempHumiReadData(uint8_t *buf, uint32_t len) { struct I2cMsg msgs[2]; uint8_t cmd[3] { AHT20_MEASURE_CMD, AHT20_MEASURE_CMD_1, AHT20_MEASURE_CMD_2 }; // 第一条消息发送测量命令 msgs[0].addr AHT20_ADDR; msgs[0].flags 0; msgs[0].len sizeof(cmd); msgs[0].buf cmd; // 第二条消息读取测量结果 msgs[1].addr AHT20_ADDR; msgs[1].flags I2C_FLAG_READ; msgs[1].len len; msgs[1].buf buf; if (I2cTransfer(g_i2cHandle, msgs, 2) ! 2) { HDF_LOGE(I2cTransfer failed); return HDF_FAILURE; } return HDF_SUCCESS; }这里要注意I2cTransfer是按“消息条数”返回成功的而不是字节数。很多从Linux i2c_transfer迁移过来的开发者会习惯性地判断返回值小于0结果在OpenHarmony上即使读写失败也可能返回正数导致错误被吞掉。严格做法是判断返回值是否等于消息条数。初始化命令在驱动Init阶段调用一次static int32_t TempHumiSensorInit(void) { uint8_t initCmd[3] { AHT20_INIT_CMD, 0x08, 0x00 }; struct I2cMsg msg; msg.addr AHT20_ADDR; msg.flags 0; msg.len sizeof(initCmd); msg.buf initCmd; if (I2cTransfer(g_i2cHandle, msg, 1) ! 1) { HDF_LOGE(Sensor init cmd failed); return HDF_FAILURE; } OsalMSleep(20); // 等待校准完成 return HDF_SUCCESS; }数据读取完成后把6字节按前面讲的拼接方式换算成温湿度存到驱动内部的缓存结构体里。4.3 device_info.hcs配置要点HDF驱动能不能在启动时被正确加载取决于HCS配置。HCS是OpenHarmony的配置描述语言类似带缩进的键值标记最终的解析和校验是在编译阶段完成的。一个最简的传感器设备配置如下device_info :: sensor_host { device :: tempHumi { deviceInfo :: deviceInfo { policy 2; // 服务发布策略2表示同时发布给内核态和用户态 priority 100; // 加载优先级 permission 0660; // 节点权限 moduleName temp_humi_driver; serviceName temp_humi_service; deviceMatchAttr temp_humi_config; } } }这里几个字段的作用分别是policy 2表示驱动服务对用户态可见这样上层Sensor服务才能通过HDF服务接口调用到底层驱动。priority 100加载顺序数字越小越先加载。如果多个驱动之间没有依赖关系保持默认即可。moduleName必须和驱动入口结构体里的moduleName字段完全一致否则框架匹配不到驱动。deviceMatchAttr用于匹配设备匹配属性和驱动的私有配置。HCS配置写错驱动加载时会直接报“no device matched”日志里看不到任何你的驱动打印。遇到这种情况第一反应应该是去查HCS的拼写和层级而不是怀疑代码问题。4.4 服务接口与数据访问方式驱动内部把温湿度读出来后要开放给上层使用。HDF的服务分发方式有两种一种是用HDF的io service把驱动注册为系统服务另一种是直接放到Sensor框架的sensor_device接口下。对我们这次的目标——跑通数据链路——可以用一个简单的策略在驱动里实现一个GetTempHumiData方法然后把它挂到HDF的sensor接口回调列表里。实际项目里更完整的方式是通过HDIHardware Device Interface定义接口生成服务代理。但那个流程涉及接口定义文件和代码生成工具篇幅很长。这里为了讲清楚驱动本身围绕核心读写逻辑展开。5. 编译烧录与全链路验证从日志到APP数据驱动代码写完后编译、烧录、验证每一步都有对应的检查点。这一环节最重要的是形成一套“出现问题我能定位到哪一层”的排查思路。5.1 编译命令与驱动加载日志在源码根目录执行编译hb build -f --target temp_humi_driver这条命令只编译drivers/peripheral/sensor/temp_humi这一个模块速度快适合开发调试。如果驱动代码有语法错误或依赖缺失在这个阶段就能暴露出来。驱动加载成功后系统日志里应该能看到对应的HDF框架日志。通过hilog抓取hilog | grep HDF正常情况下会出现类似这样的输出[HDF] temp_humi_driver init success如果看不到这行日志说明驱动根本没有被加载。按顺序排查先查HCS配置的moduleName是否与驱动入口一致再查BUILD.gn是否被打进了产品镜像最后查设备节点是否在对应的host下注册成功。5.2 应用层读取数据的链路验证驱动跑通后数据要能到应用层。OpenHarmony标准系统上Sensor服务会自动把符合Sensor规范的驱动数据封装成对应用层可见的能力。应用侧可以通过TypeScript接口订阅数据import sensor from ohos.sensor; sensor.on(sensor.SensorId.ENVIRONMENT_TEMPERATURE, (data) { console.info(temperature ${data.temperature}); });这里要注意SensorId.ENVIRONMENT_TEMPERATURE是系统预定义的传感器类型之一如果系统没有把我们的温湿度驱动映射到这个类型上上层是订阅不到的。我自己调试时先在应用里打印原始上报事件确认事件触发再去打印数据字段。如果事件都不触发问题大概率出在驱动和Sensor服务之间的HDI接口层而不是传感器本身。5.3 实测数据与一致性校验驱动调通后我拿它和实验室的水银温度计做了对比测试。室内环境下温度读取稳定在24.8°C到25.3°C之间湿度在46%到51%之间。把传感器拿在手里搓热温度会在两三秒内往上跳湿度则基本不变这说明温度的响应正常。再对着传感器哈一口气湿度立刻能爬到60%以上过十几秒又回落。这种快速改变的测试方法比只看静态数据更能判断传感器到底有没有正常工作。如果吹一口气湿度都没变化那大概率是数据位拼接出了问题或者读取的是同一个缓存值。6. OpenHarmony传感器驱动常见问题排查与避坑实录代码只是驱动开发的一半另一半是排查和调试。我把实际开发里频繁踩到的问题整理成了一张速查表按这个顺序排查能省大量时间。现象可能原因排查方法日志里看不到驱动加载信息HCS里moduleName拼写不一致BUILD.gn没有打进镜像对比驱动入口的moduleName与HCS配置检查生成镜像里是否包含ko文件驱动加载了但I2C通信失败I2C控制器编号错误总线复用冲突地址写错用牙签或万用表核对SCL/SDA是否接对换一个I2C控制器测试确认设备地址是0x38还是0x39读回来的温湿度全是0或者固定值传感器没有完成校准等待时间不够读取的数据未正确拼位Init后加20ms延时触发测量后保证80ms等待打印6字节原始数据逐个比特排查温度数据越读越高测量间隔太短芯片自热驱动里加最小查询间隔限制建议1000ms以上湿度偏差大数据拼接时掩码处理错误传感器受潮或被手指接触对照DataSheet检查拼位逻辑避免用手直接捏传感器用户态应用订阅不到数据SensorId类型未匹配HDI接口没注册先确认服务端是否注册成功再核对上层订阅的SensorId编译报符号找不到BUILD.gn缺少HDF平台依赖参考已有传感器驱动的BUILD.gn补全deps声明除了这张表还有一个隐藏的坑要特别提醒很多开发板默认把同一个I2C控制器分配给了多个功能比如调试串口或者其它外设。驱动里I2cOpen(3)能返回句柄不代表总线上的物理引脚真接上了传感器。我在一次调试中就是被这种“假成功”卡了半天驱动日志一切正常数据却完全不动最后发现I2C3_SCL引脚被复用成了别的功能。所以在正式测试之前先用检测工具扫描I2C总线上的设备地址确认0x38这个地址能被枚举出来再往下调试。没有检测工具的话最简单的办法是写一个读传感器状态字节的小命令读回0x80开头的状态值说明物理链路是通的。另外传感器模块的供电也很关键。AHT20本身支持较宽电压但如果模块的VCC从开发板的排针引出排针电流有限长时间灌电流可能导致电压跌落表现出来就是偶发I2C无响应。碰到驱动偶尔失败、日志又找不到明显规律的场景可以拿万用表量一下VCC在读取瞬间的电压。这是我实际处理过的一个概率型故障换了独立供电的排针口就稳定了。从这一节往后还能怎么扩展驱动跑通只是万里长征第一步。这套温湿度传感器驱动后续可以直接扩展成多传感器节点比如把光照、气压传感器都挂在同一个sensor_host下每个device的驱动代码结构几乎一样只改数据手册相关的寄存器操作即可。也可以把上报的数据走系统Sensor服务接入数据融合让上层去做温度补偿、湿度校准这些更智能的逻辑。硬件上如果要做低功耗可以在驱动Release里主动把传感器切到休眠指令然后再通过外部中断或者定时唤醒的方式重新打开。驱动开发就是这样原理不复杂但每个环节都藏着一堆“你不试不知道”的细节。我写过Linux驱动也写过单片机裸机驱动到了OpenHarmony的HDF框架下最大的体会是心态上不要怕多学一套框架——host、device、service这条线摸清楚之后再回看传统的驱动模式反而会觉得蛮朴素的。如果是从零开始接触OpenHarmony驱动我的建议是先拿GPIO点亮一个LED跑通整个HDF链路再去搞I2C这种带时序的总线设备。底层框架跑通了剩下的就是传感器数据手册的阅读理解问题跟单片机开发没有本质差别。希望这篇分享能帮你少踩几个坑。
阅读完成 · 觉得有帮助?