免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

OpenHarmony驱动开发实战:MLX90614红外温度传感器HDF驱动移植

OpenHarmony驱动开发实战:MLX90614红外温度传感器HDF驱动移植 1. 从零上手为什么要在OpenHarmony上折腾MLX90614第一次拿到这个需求的时候我脑子里冒出来的第一个念头是红外测温这东西不是早就烂大街了吗随便找个单片机跑个I2C读一下寄存器不就完事了。但真正把MLX90614放到OpenHarmony的驱动框架里跑通才发现事情远没有想象中那么简单。裸机时代你只需要关心时序对不对、地址有没有配错而在OpenHarmony这种带HDF驱动框架、带设备树管理、带HDI接口抽象的系统里你要考虑的东西一下子多了好几倍。MLX90614是一颗非接触式红外温度传感器核心原理是通过热电堆探测目标物体辐射的红外能量再结合芯片内部的环境温度做补偿计算最终输出目标温度和芯片自身温度两个值。它出厂就做了校准通过I2C接口读取默认地址是0x5A测温范围覆盖-70到380摄氏度精度在人体温度区间能做到正负0.5度左右。这颗芯片在额温枪、工业测温、智能家居里用得非常多属于经典中的经典。那为什么非要把它搬到OpenHarmony上原因很直接现在大量智能硬件项目在往OpenHarmony生态上迁移尤其是RK3568、Hi3861这类芯片平台很多场景需要本地化的温度感知能力。比如智能门禁要测人体温度、工业网关要监控设备热状态、智能家电要根据环境温度调节运行策略。这些场景下你不可能再外挂一个单片机专门读传感器而是希望主控直接通过OpenHarmony的驱动框架把数据拿上来交给上层应用去处理。这篇文章适合谁看如果你已经写过Linux驱动对I2C协议不陌生但还没在OpenHarmony上完整走过一遍驱动开发流程那这篇内容就是给你准备的。如果你是完全的新手也没关系我会把设备树配置、HDF驱动框架、I2C通信协议这些基础概念用大白话讲清楚让你能跟着一步步做下来。整个流程我会按照实际项目的开发顺序来展开先搞清楚硬件怎么接再配设备树然后写驱动代码接着编译烧录最后调试验证。每一步我都会说明为什么这么做以及我踩过哪些坑。2. 动手之前先把硬件链路和通信协议理清楚2.1 MLX90614的引脚定义与硬件连接方案MLX90614常见封装是TO-39金属壳四个引脚VDD、GND、SDA、SCL。供电范围宽3.3V和5V都能跑但注意SDA和SCL的上拉电压要跟主控IO电平匹配。我用的RK3568开发板IO是3.3V电平所以直接把传感器接在3.3V供电上SDA和SCL各挂一个4.7k的上拉电阻到3.3V。这里有个细节很多人容易忽略MLX90614的I2C接口支持标准模式和快速模式最高频率可以到100kHz标准模式或者400kHz快速模式。但实际用下来我建议在OpenHarmony上先跑100kHz等驱动稳定了再尝试提速。原因后面讲调试的时候会详细说。接线方式很简单MLX90614引脚开发板接口备注VDD3.3V供电正极GNDGND共地SDAI2Cx_SDA数据线需上拉SCLI2Cx_SCL时钟线需上拉上拉电阻的选择有个经验公式R (Vdd - Vol) / Iol一般I2C总线上拉取4.7k是经过验证的稳妥值。如果你总线上挂了多个I2C设备可以适当减小到2.2k但不要低于1k否则功耗会上去而且可能拉不低电平。2.2 I2C通信协议在MLX90614上的具体表现MLX90614的I2C通信遵循标准的读写时序但有几个特殊点需要记住。它的设备地址是7位地址0x5A写操作时发送0xB4读操作时发送0xB5。芯片内部有一块RAM和一块EEPROMRAM地址从0x00到0x1FEEPROM地址从0x20到0x3F。我们最关心的两个数据寄存器是0x06环境温度Ta即芯片自身温度0x07目标温度Tobj即被测物体温度读出来的原始数据是16位的低8位和高8位分两个字节传输。温度换算公式是T raw * 0.02 - 273.15单位是摄氏度。这个0.02是芯片的分辨率也就是每个LSB代表0.02K。I2C的读时序是这样的先发送起始条件然后发送设备地址加写标志0xB4接着发送要读的寄存器地址比如0x07然后重新发送起始条件发送设备地址加读标志0xB5接着读取两个字节的数据最后发送停止条件。整个过程需要严格遵守时序要求尤其是在OpenHarmony的驱动框架下I2C控制器的配置会直接影响通信成功率。注意MLX90614在读取数据时有一个PEC校验字节如果你在驱动里开启了PEC校验需要多读一个字节。我建议初期先关闭PEC等基本通信调通后再考虑加上。2.3 OpenHarmony驱动框架对I2C设备的支持方式OpenHarmony的驱动框架叫HDFHardware Driver Foundation它把驱动分成内核态和用户态两部分。对于I2C设备通常的做法是在内核态实现一个HDF驱动通过I2C控制器提供的统一接口去读写传感器。这样做的好处是驱动代码可以复用HDF提供的I2C访问API不需要直接操作寄存器。HDF的I2C接口主要有几个关键函数I2cOpen()打开I2C控制器I2cTransfer()执行一次I2C传输可以组合多个消息I2cClose()关闭I2C控制器在驱动初始化的时候你需要通过HDF的配置解析机制拿到设备树里配的I2C总线号和从设备地址然后打开对应的I2C控制器。之后每次读温度就构造一个I2cMsg数组把写寄存器地址和读数据两个操作组合成一次传输。这种设计的好处是驱动代码跟具体的I2C控制器硬件解耦了你换一个芯片平台只要设备树改一下驱动代码基本不用动。但代价是你需要理解HDF的驱动模型包括驱动入口、设备管理、配置解析这些概念。我刚开始接触的时候也觉得绕但写过一个完整驱动之后回头看这套框架确实比裸机时代自己撸寄存器要规范得多。3. 设备树配置让内核认识你的传感器3.1 设备树在OpenHarmony中的角色设备树Device Tree这个东西简单说就是一份硬件描述文件告诉内核“板子上有什么设备、它们挂在哪条总线上、地址是多少、用什么驱动”。在OpenHarmony里设备树的作用跟Linux是一样的但配置方式略有不同。OpenHarmony的设备树通常放在kernel/linux/config/或者device/目录下具体路径取决于你用的芯片平台。对于RK3568平台设备树文件一般在kernel/linux/arch/arm64/boot/dts/rockchip/下面你会看到一堆rk3568-xxx.dts和rk3568-xxx.dtsi文件。你需要找到你实际使用的板级设备树文件然后在里面添加MLX90614的节点。3.2 添加MLX90614设备树节点的完整步骤第一步找到I2C控制器的节点。RK3568有多个I2C控制器比如i2c0到i2c5你需要确认你的传感器实际接在哪条总线上。假设接在i2c3上那就在设备树里找到i2c3这个节点。第二步在i2c3节点下面添加子节点。代码大概长这样i2c3 { status okay; clock-frequency 100000; mlx90614: mlx906145a { compatible melexis,mlx90614; reg 0x5a; status okay; }; };这里有几个关键点要解释status okay表示启用这条I2C总线。很多板子的I2C默认是disabled状态你不改的话驱动根本找不到设备。clock-frequency 100000设置I2C总线频率为100kHz。前面说了初期建议用标准模式。compatible这是驱动匹配的关键字段格式一般是“厂商,型号”。你写的驱动里要有一个匹配表里面包含这个字符串内核才能把设备和驱动绑在一起。reg 0x5a从设备地址MLX90614的7位地址就是0x5A。第三步检查引脚复用配置。RK3568的I2C引脚通常跟其他功能复用你需要在pinctrl节点里确认I2C3的SDA和SCL引脚被正确配置为I2C功能。这部分一般在板级dtsi文件里已经配好了但如果你用的是自定义板子可能需要自己改。提示设备树改完之后编译内核的时候会自动把dtb文件更新。你可以用fdtdump工具反编译dtb确认你的节点确实被包含进去了。3.3 设备树配置中容易踩的坑我在这部分踩过两个坑这里分享一下。第一个坑是I2C总线号搞错了。RK3568的I2C控制器编号和实际物理接口的对应关系不同板子可能不一样。我一开始以为丝印上写的I2C3就是i2c3节点结果实际对应的是i2c5。后来用示波器量了波形才确认。所以建议你在配置之前先确认原理图或者用工具扫描一下I2C总线。第二个坑是上拉电阻没焊。有些开发板的I2C接口自带上拉有些没有。如果你接的传感器模块上没有上拉电阻而开发板也没有那I2C总线就是浮空的通信肯定失败。我当时的现象是I2cTransfer一直返回超时错误查了半天才发现是硬件问题。第三个坑是地址冲突。如果你总线上还挂了其他I2C设备要确认它们的地址不冲突。MLX90614的地址可以通过EEPROM修改但出厂默认是0x5A。我遇到过一块OLED屏也是0x5A地址的情况后来把OLED换到另一条总线上才解决。4. HDF驱动开发从驱动入口到数据读取4.1 HDF驱动的基本结构和入口函数OpenHarmony的HDF驱动有一套固定的模板你需要实现几个关键的回调函数。整个驱动代码大概分成三部分驱动入口、设备初始化和业务逻辑。驱动入口用HDF_INIT宏来注册代码结构如下#include hdf_device_desc.h #include hdf_log.h #include i2c_if.h #define HDF_LOG_TAG mlx90614_driver static int32_t MlX90614Bind(struct HdfDeviceObject *device) { if (device NULL) { HDF_LOGE(device is null); return HDF_ERR_INVALID_PARAM; } static struct IDeviceIoService service { .object {0}, }; device-service service; return HDF_SUCCESS; } static int32_t MlX90614Init(struct HdfDeviceObject *device) { if (device NULL || device-property NULL) { HDF_LOGE(device or property is null); return HDF_ERR_INVALID_PARAM; } // 解析设备树配置打开I2C控制器 // ... return HDF_SUCCESS; } static void MlX90614Release(struct HdfDeviceObject *device) { // 释放资源 } struct HdfDriverEntry g_mlx90614DriverEntry { .moduleVersion 1, .moduleName mlx90614_driver, .Bind MlX90614Bind, .Init MlX90614Init, .Release MlX90614Release, }; HDF_INIT(g_mlx90614DriverEntry);这段代码里Bind函数负责把驱动服务挂到HDF设备上Init函数做实际的初始化工作Release函数在驱动卸载时清理资源。moduleName要和驱动配置文件里的名字一致否则驱动加载不起来。4.2 解析设备树配置并打开I2C控制器在Init函数里你需要从device-property中解析出设备树里配的I2C总线号和从设备地址。HDF提供了一套配置解析API常用的有HdfDeviceGetNodeUint32()读取整型配置HdfDeviceGetNodeString()读取字符串配置假设你的驱动配置文件里这样写device_mlx90614 :: device { device0 :: deviceNode { policy 2; priority 100; preload 0; permission 0664; moduleName mlx90614_driver; serviceName mlx90614_service; deviceMatchAttr mlx90614_config; }; }然后在mlx90614_config对应的配置节点里你需要指定I2C总线号。不过更常见的做法是直接在设备树里通过reg属性拿到从设备地址通过父节点的bus-number拿到总线号。打开I2C控制器的代码大概是这样static int32_t MlX90614Init(struct HdfDeviceObject *device) { struct MlX90614DrvData *drvData NULL; drvData (struct MlX90614DrvData *)OsalMemCalloc(sizeof(*drvData)); if (drvData NULL) { HDF_LOGE(malloc drvData failed); return HDF_ERR_MALLOC_FAIL; } // 从设备树解析I2C总线号 int32_t busNum 3; // 假设是i2c3 drvData-i2cHandle I2cOpen(busNum); if (drvData-i2cHandle NULL) { HDF_LOGE(open i2c%d failed, busNum); OsalMemFree(drvData); return HDF_FAILURE; } drvData-slaveAddr 0x5A; device-priv drvData; HDF_LOGI(mlx90614 init success); return HDF_SUCCESS; }这里I2cOpen返回一个句柄后续所有的I2C读写都通过这个句柄来操作。slaveAddr就是MLX90614的从设备地址。4.3 实现温度读取的核心业务逻辑读温度是整个驱动最核心的部分。前面说了MLX90614的目标温度寄存器地址是0x07环境温度是0x06。读一次数据的流程是先写寄存器地址再读两个字节。用HDF的I2C接口实现的话代码大概是这样static int32_t MlX90614ReadReg(struct MlX90614DrvData *drvData, uint8_t reg, uint8_t *buf, uint16_t len) { int32_t ret; struct I2cMsg msg[2] {0}; // 第一条消息写寄存器地址 msg[0].addr drvData-slaveAddr; msg[0].flags 0; msg[0].len 1; msg[0].buf reg; // 第二条消息读数据 msg[1].addr drvData-slaveAddr; msg[1].flags I2C_FLAG_READ; msg[1].len len; msg[1].buf buf; ret I2cTransfer(drvData-i2cHandle, msg, 2); if (ret ! 2) { HDF_LOGE(i2c transfer failed, ret %d, ret); return HDF_FAILURE; } return HDF_SUCCESS; } static int32_t MlX90614ReadTemp(struct MlX90614DrvData *drvData, float *temp) { uint8_t buf[2] {0}; int32_t ret; ret MlX90614ReadReg(drvData, 0x07, buf, 2); if (ret ! HDF_SUCCESS) { return ret; } uint16_t raw (buf[1] 8) | buf[0]; *temp raw * 0.02f - 273.15f; return HDF_SUCCESS; }这里有个细节要注意MLX90614返回的数据是低字节在前、高字节在后所以拼接的时候是(buf[1] 8) | buf[0]不要搞反了。我一开始就是搞反了读出来的温度一直是负的几百度查了半天才发现是字节序问题。另外I2cTransfer的返回值是实际传输的消息数量如果返回2说明两条消息都成功了。如果返回小于2说明某条消息失败了需要检查硬件连接和时序配置。4.4 驱动配置文件的编写要点HDF驱动需要在hdf_devhost的配置文件里注册这个文件通常在vendor/目录下名字类似device_info.hcs。你需要在里面添加你的设备节点device_mlx90614 :: device { device0 :: deviceNode { policy 2; priority 100; preload 0; permission 0664; moduleName mlx90614_driver; serviceName mlx90614_service; deviceMatchAttr mlx90614_config; }; }policy字段决定驱动是内核态还是用户态加载2表示内核态。moduleName要和驱动代码里的moduleName一致。serviceName是上层应用访问驱动时用的服务名。配置改完之后需要重新编译HDF驱动框架和你的驱动模块然后烧录到板子上。编译命令取决于你的项目构建系统OpenHarmony标准系统一般用./build.sh --product-name rk3568这样的命令。5. 编译、烧录与调试把驱动跑起来5.1 编译环境的搭建和驱动模块的编译OpenHarmony的编译环境搭建是个体力活我这里不展开讲整个环境的安装只说你编译驱动模块需要关注的部分。首先你需要确保hb命令能用这是OpenHarmony的构建工具。然后进入你的项目根目录执行hb set # 选择你的产品比如 rk3568 hb build -f如果你只想编译驱动模块可以用hb build -f --target //drivers/hdf_core/framework/model/sensor/mlx90614:mlx90614_driver编译成功后会生成.ko文件这个就是你的驱动模块。然后你需要把.ko文件推到板子上用insmod加载insmod mlx90614_driver.ko如果加载成功用lsmod能看到模块信息。如果加载失败用dmesg看内核日志通常会告诉你失败原因比如符号未定义、版本不匹配之类的。5.2 用HDF工具验证驱动是否加载成功OpenHarmony提供了一个HDF调试工具可以查看当前加载的驱动列表。在串口终端里执行hdf_devhost -l如果看到你的mlx90614_driver出现在列表里说明驱动加载成功了。然后你可以用hdf_devhost -i mlx90614_service查看服务的详细信息。另一个验证方法是直接读/dev下面的设备节点。如果你的驱动创建了设备节点应该能在/dev下面看到对应的文件。不过HDF驱动不一定创建设备节点这取决于你的驱动实现方式。5.3 实际读取温度数据的完整测试流程驱动加载成功之后你需要写一个简单的测试程序来验证数据读取。最直接的方式是在驱动里加一个调试接口通过ioctl或者sysfs暴露给用户态。但更简单的做法是直接在驱动初始化的时候读一次温度打印到内核日志里。我在调试阶段就是在Init函数最后加了一段float temp 0; if (MlX90614ReadTemp(drvData, temp) HDF_SUCCESS) { HDF_LOGI(current temperature: %.2f C, temp); } else { HDF_LOGE(read temperature failed); }然后加载驱动之后用dmesg | grep mlx90614就能看到温度值。如果读出来是25度左右说明环境温度正常如果读出来是-273度说明数据没读对如果读出来是几百度的离谱值可能是字节序或者换算公式有问题。5.4 常见通信失败原因和排查方法调试I2C设备最怕的就是通信失败而且失败的现象往往很模糊。我整理了一个排查清单按优先级排序现象可能原因排查方法I2cTransfer返回超时上拉电阻缺失或阻值不对用万用表量SDA/SCL对VCC的电阻返回NACK从设备地址错误用i2cdetect扫描总线数据全0或全FF寄存器地址错误确认寄存器地址和读写标志温度值明显偏差字节序或换算公式错误手动计算raw值验证驱动加载失败moduleName不匹配检查hcs配置文件和驱动代码还有一个容易被忽略的问题I2C总线的时钟频率。有些MLX90614模块在400kHz下工作不稳定尤其是走线比较长的时候。如果你在100kHz下能读通换到400kHz就不行那大概率是信号完整性问题不是驱动代码的问题。实操心得调试I2C设备的时候示波器是最有用的工具。没有示波器的话至少准备一个逻辑分析仪几十块钱的那种就能用。能看到波形很多问题一眼就能定位。6. 进阶优化让驱动更稳定、更实用6.1 增加温度读取的滤波和校准逻辑MLX90614本身的精度已经不错了但在实际使用中读出来的温度会有小幅波动大概在正负0.2度左右。如果你的应用对稳定性要求高可以在驱动层加一个简单的滑动平均滤波。比如维护一个长度为8的环形缓冲区每次读到的温度放进去输出的时候取平均值。代码实现很简单#define FILTER_LEN 8 static float MlX90614Filter(struct MlX90614DrvData *drvData, float newTemp) { drvData-filterBuf[drvData-filterIdx] newTemp; drvData-filterIdx (drvData-filterIdx 1) % FILTER_LEN; float sum 0; for (int i 0; i FILTER_LEN; i) { sum drvData-filterBuf[i]; } return sum / FILTER_LEN; }这个滤波逻辑会增加一点内存开销但换来的稳定性提升是值得的。尤其是在测温目标距离较远或者环境温度变化较快的时候滤波效果很明显。另外MLX90614的EEPROM里可以校准发射率Emissivity。默认发射率是0.95适合大多数物体表面。如果你测的是高反射率金属表面需要把发射率调低否则读数会偏低。发射率寄存器的地址是0x24修改的时候要注意EEPROM写入有次数限制不要频繁写。6.2 通过HDI接口向上层暴露温度数据在OpenHarmony的架构里驱动层往上是通过HDIHardware Device Interface接口跟上层服务通信的。如果你想让应用层能直接拿到温度数据需要实现一个HDI接口。不过对于大多数场景更简单的做法是通过sysfs或者ioctl暴露一个字符设备节点上层应用直接读文件就行。用sysfs的方式你需要在驱动里创建一个属性文件static ssize_t TempShow(struct device *dev, struct device_attribute *attr, char *buf) { struct MlX90614DrvData *drvData dev_get_drvdata(dev); float temp 0; MlX90614ReadTemp(drvData, temp); return sprintf(buf, %.2f\n, temp); } static DEVICE_ATTR_RO(Temp);然后应用层用cat /sys/class/mlx90614/temp就能读到温度值。这种方式简单直接适合快速验证和轻量级应用。6.3 低功耗场景下的驱动适配思路如果你的设备是电池供电的那MLX90614的功耗就需要考虑。芯片正常工作电流大概1.5mA待机电流只有几微安。在OpenHarmony的电源管理框架下你可以让驱动支持运行时挂起和恢复。具体做法是实现HdfDeviceObject的Suspend和Resume回调在系统进入低功耗模式的时候关闭I2C控制器唤醒的时候重新打开。不过这个功能需要跟系统的电源管理策略配合不是所有场景都需要。另一个省电的思路是降低采样频率。MLX90614支持单次测量模式你可以在需要读温度的时候才发起一次测量读完就让芯片进入睡眠。这样平均功耗可以降到几十微安级别。6.4 多传感器组网时的地址管理和总线扩展一个I2C总线上理论上可以挂多个MLX90614但它们的默认地址都是0x5A会冲突。解决办法有两个一是通过EEPROM修改其中一个的地址二是用I2C多路复用器扩展总线。修改地址的方法是通过写EEPROM的0x2E寄存器SMBus地址寄存器把地址改成其他值。但注意EEPROM写入次数有限而且改完之后要重新上电才生效。我一般建议用I2C多路复用器比如TCA9548A一片可以扩展8条I2C总线每条总线上挂一个MLX90614地址都不用改。在OpenHarmony的设备树里多路复用器的配置稍微复杂一点需要把复用器本身作为一个I2C设备然后在它下面再挂子设备。这部分内容展开讲篇幅会很长有机会单独写一篇。7. 我踩过的坑和最后分享几个实用技巧整个项目做下来前前后后花了大概一周时间其中大部分时间不是在写代码而是在调试硬件和排查配置问题。这里把几个印象最深的坑分享一下希望能帮你少走弯路。第一个坑是设备树里I2C总线号写错了。我一开始看原理图上标的是I2C3就在设备树里配了i2c3结果怎么都读不到数据。后来用逻辑分析仪抓波形发现SDA和SCL上根本没有信号。查了RK3568的引脚复用表才发现原理图上的I2C3对应的是i2c5节点。所以原理图上的标号和设备树里的节点号不一定一致一定要交叉验证。第二个坑是驱动加载顺序的问题。HDF驱动有preload属性如果设成0表示不预加载需要手动加载。我一开始设成0然后忘了手动insmod结果一直找不到设备。后来改成1让它随系统启动自动加载问题就解决了。第三个坑是温度换算的浮点运算。OpenHarmony的内核态默认可能不支持浮点运算如果你在驱动里直接用float做计算可能会触发异常。解决办法是用定点数运算把温度值乘以100用整数传输上层再除以100。或者把浮点运算放到用户态去做驱动只负责传原始数据。最后分享一个小技巧如果你手头没有逻辑分析仪可以用GPIO模拟I2C的方式来验证传感器是否正常工作。先把SDA和SCL配成普通GPIO手动翻转电平模拟I2C时序如果能读到正确的数据说明传感器和硬件连接没问题问题出在I2C控制器配置上。这个方法虽然原始但在没有专业工具的时候非常管用。这个驱动后续还可以继续扩展比如加上温度阈值报警功能、支持多个传感器同时采集、通过MQTT把数据上传到云端等等。OpenHarmony的驱动框架扩展性很好只要把基础通信调通后面的业务逻辑就是搭积木的事情了。
返回列表