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引脚 | 开发板接口 | 备注 |
|---|---|---|
| VDD | 3.3V | 供电正极 |
| GND | GND | 共地 |
| SDA | I2Cx_SDA | 数据线,需上拉 |
| SCL | I2Cx_SCL | 时钟线,需上拉 |
上拉电阻的选择有个经验公式:R = (Vdd - Vol) / Iol,一般I2C总线上拉取4.7k是经过验证的稳妥值。如果你总线上挂了多个I2C设备,可以适当减小到2.2k,但不要低于1k,否则功耗会上去,而且可能拉不低电平。
2.2 I2C通信协议在MLX90614上的具体表现
MLX90614的I2C通信遵循标准的读写时序,但有几个特殊点需要记住。它的设备地址是7位地址0x5A,写操作时发送0xB4,读操作时发送0xB5。芯片内部有一块RAM和一块EEPROM,RAM地址从0x00到0x1F,EEPROM地址从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的驱动框架叫HDF(Hardware 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: mlx90614@5a { 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 = ® // 第二条消息:读数据 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的架构里,驱动层往上是通过HDI(Hardware 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的驱动框架扩展性很好,只要把基础通信调通,后面的业务逻辑就是搭积木的事情了。