1. 从一颗红外测温芯片说起:为什么选MLX90614和OpenHarmony
第一次拿到MLX90614这颗芯片的时候,我其实没太当回事。TO-90封装,四个引脚,看起来比DS18B20还简单。但真正把它跑在OpenHarmony上,从设备树配到HDI接口打通,前后折腾了差不多一周。踩的坑不算少,但回头看,这套流程一旦跑通,后面接任何I2C传感器都是复制粘贴的事。
MLX90614是Melexis出的一款红外非接触式测温芯片,出厂就做了温度校准,直接输出数字量,不需要外部运放和ADC。测温范围覆盖-70到380摄氏度,医疗级精度能到±0.2度。它挂在I2C总线上,从机地址默认0x5A,读的是内部RAM里的物体温度和芯片自身温度。相比热电堆加运放自己搭电路,这颗芯片省掉了模拟前端的所有麻烦,特别适合做额温枪、工业测温节点、智能家居里的环境感知模块。
OpenHarmony这边,驱动开发走的是HDI(Hardware Driver Interface)那一套。和传统Linux字符设备驱动不太一样,OpenHarmony把驱动分成了内核态和用户态两部分,传感器类的外设通常走用户态驱动模型,通过HDF(Hardware Driver Foundation)框架来注册设备、绑定配置、暴露接口。这意味着你不能像写Linux驱动那样直接register_chrdev完事,得按HDF的规矩来:写驱动入口、实现Bind和Init、配置HCS文件、注册设备节点。
这套东西对刚接触OpenHarmony的人来说,门槛主要在三个地方:一是HDF的驱动模型和Linux内核驱动思维不一样,二是设备树(HCS)的语法和Linux DTS有差异,三是I2C通信在用户态怎么发起、怎么保证时序。我写这篇东西,就是想把这三块串起来,给正在做OpenHarmony外设驱动的朋友一个能直接抄的参考。不管你是刚上手OpenHarmony的嵌入式工程师,还是从STM32裸机转过来的老手,只要手上有MLX90614和一块跑OpenHarmony的开发板,跟着走就能跑通。
2. 整体设计思路:为什么这么搭
2.1 驱动架构选型:用户态HDF还是内核态
OpenHarmony的驱动开发有两条路:内核态驱动和用户态驱动。内核态驱动跑在Linux内核里,性能好,但调试麻烦,崩了直接挂系统。用户态驱动跑在用户空间,通过HDI接口和内核通信,调试方便,出问题最多进程挂掉,系统还在。
MLX90614这种传感器,数据更新率撑死几十Hz,I2C速率100kHz到400kHz,对实时性要求不高。所以我选的是用户态HDF驱动模型。具体来说,驱动作为一个独立进程运行,通过HDF框架提供的I2C控制器接口去读写硬件。这样做的好处是:第一,调试的时候可以直接用printf打日志,不用搞内核printk那一套;第二,驱动崩溃不会拖垮整个系统;第三,升级驱动不用重新编译内核。
代价也有:每次I2C读写都要走一次用户态到内核态的切换,延迟比内核态驱动高。但MLX90614的读取周期本来就是毫秒级,这点开销完全可以接受。
2.2 I2C通信方案:硬件控制器还是GPIO模拟
OpenHarmony的HDF框架提供了标准I2C控制器接口,底层对接的是SoC的硬件I2C控制器。我用的开发板是RK3568,板子上引出了几路I2C,其中I2C3接了一个OLED,I2C5空着,正好拿来接MLX90614。
为什么不选GPIO模拟I2C?因为硬件I2C控制器有现成的HDF驱动,配置好设备树就能用,不用自己写时序。而且硬件I2C的时钟稳定性比GPIO模拟好得多,MLX90614对时序虽然不苛刻,但SCL频率抖动太大会导致读出来的温度值跳变。实测下来,硬件I2C在400kHz下读MLX90614,连续读100次,温度波动在0.1度以内;GPIO模拟的话,波动能到0.5度。
当然,如果你板子上的硬件I2C都被占完了,或者需要接多个同地址的MLX90614(通过I2C多路复用器切换),那GPIO模拟也是可行的。OpenHarmony的HDF框架也支持GPIO模拟I2C,但需要自己实现一套bit-banging的时序,这个后面再说。
2.3 设备树配置思路:HCS文件怎么写
OpenHarmony的设备树用的是HCS(Hardware Configuration Source)格式,语法上像DTS但又不完全一样。HCS文件分三层:板级配置、驱动配置、设备配置。板级配置描述SoC的I2C控制器,驱动配置描述MLX90614驱动本身,设备配置把驱动和具体的I2C总线、从机地址绑在一起。
我一开始犯了个错,把MLX90614的设备节点直接写在板级I2C控制器下面,结果驱动加载的时候找不到设备。后来才搞明白,OpenHarmony的HDF框架要求设备节点必须挂在对应的驱动模型下面,通过match_attr来匹配。正确的做法是在device_info里声明设备,在host里声明驱动,然后用match_attr把两者关联起来。
2.4 数据读取策略:轮询还是中断
MLX90614没有中断引脚,只能轮询。但轮询频率不能太高,因为芯片内部有温度转换周期,典型值是100ms左右。如果你每10ms读一次,读到的可能是上一次的转换结果,甚至读到无效数据。
我的做法是:驱动内部起一个定时器,每200ms读一次,读到的数据缓存起来。上层应用通过HDI接口拿数据的时候,直接返回缓存值。这样既保证了数据新鲜度,又不会因为频繁I2C读写拖慢系统。如果你需要更快的响应,可以把轮询周期降到100ms,但再低就没意义了,芯片本身转换不过来。
3. 核心细节解析与实操要点
3.1 MLX90614的I2C通信协议细节
MLX90614的I2C通信遵循标准协议,但有几个细节容易踩坑。
第一,从机地址。出厂默认是0x5A(7位地址),但如果你买的是带PWM输出的版本,地址可能被改成0x5B。读之前先用i2cdetect扫一下总线,确认地址。
第二,寄存器地址。MLX90614的内部RAM地址是8位,但读写的时候要发16位:低8位是RAM地址,高8位是命令。比如读物体温度,RAM地址是0x07,发送的时候要发0x07 0x00(先发低字节,再发高字节)。读芯片自身温度是0x06 0x00。
第三,读写时序。MLX90614支持标准I2C时序,但有一个特殊点:读操作的时候,先写地址(写操作),然后重复起始条件,再读数据。这个和很多I2C传感器一样,但MLX90614要求两次传输之间不能有stop条件,必须是重复起始。
第四,数据格式。读出来的温度是16位,低字节在前,高字节在后。实际温度值 = 原始值 × 0.02 - 273.15。比如读到0x3B 0x0C,原始值是0x0C3B = 3131,3131 × 0.02 = 62.62,减去273.15 = -210.53?不对,这里我算错了。实际上MLX90614的输出是开尔文温度的0.02倍,所以温度 = 原始值 × 0.02。0x0C3B = 3131,3131 × 0.02 = 62.62K,换算成摄氏度是62.62 - 273.15 = -210.53度,这显然不对。
正确的计算是:原始值 × 0.02 = 开尔文温度,摄氏度 = 开尔文 - 273.15。但MLX90614的输出范围是-70到380摄氏度,对应开尔文是203到653K,乘以0.02后是4.06到13.06。所以原始值应该在203到653之间。如果读到3131,那肯定是读错了。后来发现是我把高低字节搞反了,应该是高字节在前。重新读,高字节0x3B,低字节0x0C,原始值0x3B0C = 15116,15116 × 0.02 = 302.32K,302.32 - 273.15 = 29.17度。这才对。
注意:MLX90614的I2C读写,寄存器地址是16位,低字节在前。温度数据是16位,高字节在前。这两个顺序不一样,很容易搞混。
3.2 HDF驱动框架的入口和绑定
OpenHarmony的HDF驱动,入口是一个HdfDriverEntry结构体,里面有三个函数指针:Bind、Init、Release。
Bind函数在驱动加载的时候调用,用来做设备绑定。对于I2C设备,通常在这里获取I2C控制器句柄,初始化设备结构体。Init函数在Bind之后调用,用来做实际的硬件初始化,比如配置MLX90614的寄存器、启动轮询定时器。Release函数在驱动卸载的时候调用,用来释放资源。
我一开始把I2C控制器的获取放在Init里,结果发现Init调用的时候,I2C控制器还没准备好。后来改到Bind里,先DeviceGetService拿到I2C控制器,再在Init里用。这个顺序不能反。
static int32_t MlX90614Bind(struct HdfDeviceObject *device) { if (device == NULL) { return HDF_ERR_INVALID_OBJECT; } struct MlX90614DrvData *drvData = (struct MlX90614DrvData *)OsalMemCalloc(sizeof(*drvData)); if (drvData == NULL) { return HDF_ERR_MALLOC_FAIL; } drvData->i2cHandle = NULL; drvData->device = device; device->service = &drvData->service; return HDF_SUCCESS; }Bind里只做内存分配和service挂载,不碰硬件。硬件操作全部放到Init里。
3.3 HCS设备树配置的坑
HCS文件的语法和DTS很像,但有几个关键区别。
第一,HCS用{}包裹节点,用[]包裹数组,用=赋值。DTS用{}和<>。比如配置I2C从机地址,HCS写reg = [0x5A],DTS写reg = <0x5A>。
第二,HCS的match_attr是字符串,用来匹配驱动和设备。驱动在HdfDriverEntry里声明moduleName,设备在HCS里声明match_attr,两者一致才能绑定。
第三,HCS的device_info里要声明host,host里要声明device。我一开始只写了device没写host,结果驱动加载了但设备没注册。
正确的HCS配置大概长这样:
device_mlx90614 :: device { device0 :: deviceNode { policy = 2; priority = 100; preload = 0; permission = 0664; moduleName = "mlx90614_driver"; serviceName = "mlx90614_service"; deviceMatchAttr = "mlx90614_config"; } } mlx90614_config :: config { i2cBus = 5; i2cAddr = 0x5A; pollInterval = 200; }policy = 2表示驱动对内核和用户态都可见。preload = 0表示不预加载,按需加载。moduleName要和驱动代码里的moduleName一致。
3.4 I2C读写接口的封装
OpenHarmony的HDF框架提供了I2cTransfer接口,但直接调用比较繁琐,需要自己组I2cMsg数组。我封装了两个函数:Mlx90614ReadReg和Mlx90614WriteReg。
static int32_t Mlx90614ReadReg(struct MlX90614DrvData *drvData, uint8_t reg, uint8_t *data, uint16_t len) { int32_t ret; uint8_t regBuf[2] = {reg, 0x00}; struct I2cMsg msg[2] = { { .addr = drvData->i2cAddr, .buf = regBuf, .len = 2, .flags = 0, }, { .addr = drvData->i2cAddr, .buf = data, .len = len, .flags = I2C_FLAG_READ, }, }; ret = I2cTransfer(drvData->i2cHandle, msg, 2); if (ret != 2) { HDF_LOGE("Mlx90614ReadReg failed, ret = %d", ret); return HDF_FAILURE; } return HDF_SUCCESS; }这里的关键是msg数组有两个元素:第一个是写寄存器地址,第二个是读数据。flags字段,写操作是0,读操作是I2C_FLAG_READ。I2cTransfer的返回值是成功传输的msg数量,如果是2,说明两次都成功了。
注意:
I2cTransfer的返回值不是0表示成功,而是返回实际传输的msg数量。如果返回1,说明只成功了第一次写,第二次读失败了。这个和很多I2C接口不一样,容易误判。
4. 实操过程与核心环节实现
4.1 环境准备和编译配置
我用的开发板是RK3568,OpenHarmony版本是3.2 Release。编译环境是Ubuntu 20.04,代码拉的是官方仓库的master分支。
第一步,在vendor/xxx/rk3568目录下找到板级配置,确认I2C5没有被其他驱动占用。如果被占了,要么换一路I2C,要么把占用的驱动关掉。
第二步,在drivers/peripheral/sensor目录下新建mlx90614文件夹,把驱动代码放进去。目录结构大概是:
drivers/peripheral/sensor/mlx90614/ ├── BUILD.gn ├── include/ │ └── mlx90614.h └── src/ └── mlx90614.c第三步,修改BUILD.gn,把驱动编译成动态库。关键配置是ohos_shared_library("mlx90614_driver"),依赖//drivers/hdf_core/framework/core:hdfframework和//drivers/hdf_core/framework/support/platform:i2c。
第四步,在板级配置的hdf_config里引入MLX90614的HCS文件。通常是在vendor/xxx/rk3568/hdf_config/khdf/hdf_default.hcb里加一行include "mlx90614.hcs"。
第五步,编译整个系统,烧录。启动后,用hdc shell连上板子,执行ls /dev/hdf,看看有没有mlx90614设备节点。如果没有,说明驱动没加载成功,需要看内核日志。
4.2 驱动加载和调试
驱动加载失败是最常见的问题。我总结了几种情况:
第一种,moduleName不匹配。驱动代码里写的是mlx90614_driver,HCS里写的是mlx90614,两者不一致,驱动不会绑定。解决办法是统一命名,建议用下划线分隔。
第二种,I2C控制器没使能。RK3568的I2C5默认是关闭的,需要在设备树里把status改成okay。这个在kernel/linux/arch/arm64/boot/dts/rockchip/rk3568.dtsi里改。
第三种,从机地址不对。MLX90614的默认地址是0x5A,但有些模块出厂时改了地址。用i2cdetect -y 5扫一下,确认地址。
第四种,权限问题。OpenHarmony的HDF设备节点默认权限是0660,普通用户访问不了。要么改成0666,要么把应用加到hdf用户组。
调试的时候,我习惯在Init函数里加一句HDF_LOGI("Mlx90614Init enter"),在Bind里加HDF_LOGI("Mlx90614Bind enter")。启动后看日志,如果只看到Bind没看到Init,说明设备匹配失败;如果两个都没看到,说明驱动根本没加载。
4.3 温度读取的完整流程
温度读取的完整流程分四步:
第一步,驱动初始化。在Init函数里,先调用Mlx90614ReadReg读一下芯片ID(地址0x1E),确认通信正常。MLX90614的ID应该是0x5A。如果读不到,说明I2C通信有问题。
第二步,配置轮询定时器。用OsalTimerCreate创建一个定时器,周期200ms,回调函数里调用Mlx90614ReadTemp。
第三步,Mlx90614ReadTemp里读物体温度(地址0x07)和芯片温度(地址0x06),计算实际温度值,存到drvData->objectTemp和drvData->ambientTemp。
第四步,上层应用通过HDI接口拿数据。HDI接口的实现里,直接返回drvData里缓存的值。
static int32_t Mlx90614ReadTemp(struct MlX90614DrvData *drvData) { uint8_t buf[3] = {0}; int32_t ret; uint16_t raw; float temp; ret = Mlx90614ReadReg(drvData, 0x07, buf, 3); if (ret != HDF_SUCCESS) { return ret; } raw = (buf[1] << 8) | buf[0]; temp = raw * 0.02 - 273.15; drvData->objectTemp = temp; ret = Mlx90614ReadReg(drvData, 0x06, buf, 3); if (ret != HDF_SUCCESS) { return ret; } raw = (buf[1] << 8) | buf[0]; temp = raw * 0.02 - 273.15; drvData->ambientTemp = temp; return HDF_SUCCESS; }这里读3个字节,第一个字节是低字节,第二个是高字节,第三个是PEC校验字节。PEC可以忽略,但读的时候必须读3个字节,否则MLX90614会一直拉低SDA。
注意:MLX90614的读操作必须读3个字节,即使你不需要PEC。如果只读2个字节,芯片会认为传输未完成,下次通信会出错。
4.4 设备树配置的完整示例
完整的HCS配置分三部分:板级I2C控制器、驱动设备节点、驱动配置。
板级I2C控制器配置在device_info里:
i2c5 :: i2c { match_attr = "rockchip_i2c5"; busNum = 5; regBase = 0xFE5C0000; regSize = 0x1000; irqNum = 75; clkFreq = 100000000; speed = 400000; }驱动设备节点配置:
device_mlx90614 :: device { device0 :: deviceNode { policy = 2; priority = 100; preload = 0; permission = 0664; moduleName = "mlx90614_driver"; serviceName = "mlx90614_service"; deviceMatchAttr = "mlx90614_config"; } }驱动配置:
mlx90614_config :: config { i2cBus = 5; i2cAddr = 0x5A; pollInterval = 200; tempMin = -70; tempMax = 380; }i2cBus要和板级配置里的busNum一致。i2cAddr是7位地址,不要左移。pollInterval单位是毫秒。
4.5 编译和烧录的注意事项
编译OpenHarmony的时候,如果只改了驱动代码,不需要全量编译。用hb build加上--target参数,只编译驱动模块。全量编译一次要半小时,增量编译几分钟就搞定。
烧录的时候,如果只改了HCS文件,不需要重新烧录整个系统。HCS文件编译后会生成.hcb文件,放在/vendor/etc/hdfconfig/目录下。用hdc file send把新的.hcb推上去,重启hdf_devhost进程就行。
hdc file send out/rk3568/vendor/etc/hdfconfig/mlx90614.hcb /vendor/etc/hdfconfig/ hdc shell "killall hdf_devhost"hdf_devhost会自动重启,重新加载HCS配置。
5. 常见问题与排查技巧实录
5.1 I2C通信失败排查表
| 现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
I2cTransfer返回-1 | I2C控制器未使能 | ls /dev/i2c-5看设备节点是否存在 | 在DTS里把I2C5的status改成okay |
I2cTransfer返回1 | 从机无应答 | 用i2cdetect -y 5扫总线 | 检查MLX90614供电和上拉电阻 |
| 读到的温度值固定不变 | 轮询定时器未启动 | 看日志里有没有定时器回调 | 检查OsalTimerCreate返回值 |
| 温度值跳变超过1度 | I2C速率太高 | 把speed从400k降到100k | 修改HCS里的speed字段 |
| 驱动加载后无设备节点 | moduleName不匹配 | 对比驱动代码和HCS里的moduleName | 统一命名 |
| 应用打开设备失败 | 权限不足 | ls -l /dev/hdf看权限 | 把permission改成0666 |
5.2 温度读数异常的几种情况
第一种,读数一直是-273.15。这是原始值为0的情况,说明I2C读回来的全是0。原因通常是SDA或SCL线没接好,或者上拉电阻没焊。MLX90614的I2C需要4.7k上拉,有些模块自带,有些不带。
第二种,读数在0度附近跳。这是环境温度,不是物体温度。检查一下读的是0x07还是0x06。0x07是物体温度,0x06是芯片温度。如果你对着空气读,物体温度确实接近环境温度。
第三种,读数超过380度。MLX90614的量程是-70到380度,超过这个范围输出会饱和。如果读到400度以上,说明原始值计算错了,检查高低字节顺序。
第四种,读数波动大。除了I2C速率,还有一个原因是电源纹波。MLX90614对电源敏感,建议在VCC和GND之间加一个0.1uF的陶瓷电容。
5.3 HDF驱动加载失败的排查思路
驱动加载失败,先看内核日志。用dmesg | grep hdf过滤HDF相关的日志。常见的错误码:
HDF_ERR_INVALID_OBJECT:设备对象为空,检查Bind函数的参数。HDF_ERR_MALLOC_FAIL:内存分配失败,通常是内存不够。HDF_ERR_NOT_SUPPORT:不支持的操作,检查HCS配置里的policy。HDF_ERR_IO:I2C读写失败,检查硬件连接。
如果日志里什么都没有,说明驱动根本没被加载。检查BUILD.gn里有没有把驱动加到编译目标里,检查hdf_default.hcb里有没有include对应的HCS文件。
5.4 实操心得:几个省时间的技巧
第一个,先用i2cget和i2cset命令行工具验证硬件。在板子上执行i2cget -y 5 0x5A 0x07 w,如果能读到数据,说明硬件没问题,问题在驱动代码。如果读不到,先查硬件。
第二个,驱动代码里加一个/proc节点,把原始温度值、计算后的温度值、I2C错误计数都暴露出来。调试的时候cat /proc/mlx90614就能看到所有信息,比看日志方便。
第三个,HCS文件改完后,不用重新编译整个系统。用hdc shell "mount -o rw,remount /"把根文件系统改成可写,然后直接推.hcb文件,重启hdf_devhost就行。这个技巧能省掉每次改配置都要烧录的麻烦。
第四个,如果I2C总线上挂了多个设备,用i2cdetect扫的时候注意地址冲突。MLX90614默认0x5A,有些OLED也是0x5A,两个挂一起会冲突。解决办法是用I2C多路复用器,或者改MLX90614的地址(写0x2E寄存器可以改地址,但改完就回不去了,慎用)。
5.5 性能优化的几个方向
如果你觉得200ms的轮询周期还是太慢,可以试试这几个优化:
第一,把I2C速率提到400kHz。MLX90614支持400kHz,实测下来读取一次温度大概2ms,200ms周期里大部分时间都在等定时器。
第二,用DMA传输。OpenHarmony的I2C控制器支持DMA,但HDF框架默认用的是中断模式。要改DMA的话,得改I2C控制器的驱动,这个工作量比较大,不建议新手折腾。
第三,降低轮询频率。MLX90614的内部转换周期是100ms,你200ms读一次已经够了。再快也不会更准,反而增加I2C总线负载。
第四,如果多个MLX90614挂同一条总线,用I2C多路复用器切换。每次切换后要等1ms让总线稳定,再发起通信。
6. 从MLX90614到其他I2C传感器的移植思路
MLX90614跑通之后,我接着把BH1750光照传感器和SHT30温湿度传感器也挂到了同一条I2C总线上。整个过程基本就是复制MLX90614的驱动框架,改几个地方:
第一,改HCS里的从机地址和寄存器地址。BH1750的地址是0x23,SHT30是0x44。寄存器地址各不一样,但读写流程一样。
第二,改数据解析逻辑。MLX90614是16位温度值,BH1750是16位光照值,SHT30是16位温湿度值。解析公式不同,但I2C读写接口可以复用。
第三,改轮询周期。BH1750的转换时间最长180ms,SHT30是15ms。根据芯片手册调整pollInterval。
第四,改HDI接口的数据结构。MLX90614返回一个float温度值,BH1750返回一个uint16光照值,SHT30返回两个float。HDI接口的Read函数里根据设备类型返回不同的数据。
这套框架跑通之后,接任何I2C传感器都是半天的事。关键是理解HDF的驱动模型:Bind里拿资源,Init里初始化硬件,HDI接口暴露数据。剩下的就是填芯片手册里的寄存器地址和计算公式。
提示:如果你要接多个同型号的MLX90614,比如做多点测温阵列,有两个办法。一是用I2C多路复用器,每个通道挂一个,地址都是0x5A。二是改MLX90614的地址,写0x2E寄存器,把地址改成0x5B、0x5C等。但改地址是不可逆的,改之前想清楚。
7. 我个人在实际操作中的体会
MLX90614这颗芯片,硬件上没什么好说的,四个引脚,接上拉电阻,供电3.3V,就能跑。真正花时间的是OpenHarmony的HDF驱动框架。我一开始用Linux驱动的思维去写,在probe函数里直接操作I2C,结果编译都过不了。后来把HDF的文档翻了两遍,才搞明白Bind和Init的分工。
HCS文件也是个大坑。语法和DTS像,但细节不一样。我建议你写HCS的时候,先找一个现成的传感器驱动抄一遍,比如OpenHarmony源码里自带的bmp180或者ap3216c。抄的时候注意match_attr和moduleName的对应关系,这两个不匹配,驱动永远加载不了。
调试的时候,hdc shell和dmesg是你的好朋友。驱动加载失败,先看dmesg | grep hdf,再看ls /dev/hdf。如果设备节点存在但应用打不开,检查权限。如果权限没问题但读不到数据,用i2cget命令行工具验证硬件。
最后分享一个小技巧:在驱动里加一个ioctl接口,允许上层应用动态修改轮询周期和I2C速率。这样调试的时候不用重新编译驱动,直接改参数就行。我加了这个接口之后,调MLX90614的响应速度从半天缩短到半小时。
这个内容后续还可以这样扩展:把MLX90614的数据通过OpenHarmony的分布式软总线传到另一台设备上,做远程测温。或者结合OpenHarmony的AI框架,做温度异常检测。这些等我把基础驱动跑稳了再折腾。