ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenHarmony下MLX90614红外测温驱动开发实战教程

OpenHarmony下MLX90614红外测温驱动开发实战教程 红外测温方案里MLX90614是少数几颗让我“无脑推”的传感器。一颗TO-39封装的小芯片内置红外热电堆和信号调理电路主控通过I2C总线就能直接读到物体温度常温下精度能做到±0.5℃左右十几块钱的模块在智能门禁、冷链物流、设备过热保护里被反复用到。这次我在开源鸿蒙OpenHarmony系统上给这颗芯片做驱动开发过程比预想中曲折不少涉及SMBus和I2C的兼容性问题、HDF驱动框架的接入流程、还有一堆发射率补偿和滤波的细节。这篇教程会把整个开发流程从头到尾拆开讲从芯片手册怎么读到HCS设备配置怎么填再到能跑通的驱动代码长什么样最后是我实际联调中碰到的那些坑。想在自己板子上接MLX90614的同学无论之前搞没搞过鸿蒙驱动跟着这篇应该都能把温度读出来。1. 项目要解决的问题和整体技术框架1.1 为什么选择MLX90614这颗传感器做非接触测温选型时绕不开这颗芯片。它本质上是一个红外热电堆传感器物体辐射的红外线透过TO-39封装顶部窗口照到热电堆上产生微弱的电压信号芯片内部集成的DSP会做放大、ADC转换、环境温度补偿和线性化最后输出一个代表物体温度的16位数据。主控要干的事情只是通过I2C把数据取回来不需要自己去拟合热电堆的非线性曲线这对驱动开发来说是件非常省心的事。实际项目中这颗芯片的应用场景高度集中智能门禁测温一体机测手腕或额头温度人体皮肤发射率大约0.97~0.98。工业设备轴承、电机外壳的温度异常预警要求非接触、高温范围大MLX90614的物体温度测量范围能到-70℃~380℃视具体型号而定。冷链物流中检测包装箱表面温度不需要打开箱子。选它还有一个原因I2C接口太普及了。任何带I2C控制器的主控都能接无论是单片机还是运行Linux系统的SoC只要把时序调对剩下的都是软件层的活。而且在OpenHarmony上做驱动HDF框架对I2C设备的支持很成熟直接把总线操作封装进驱动就好不需要为这颗芯片搞专用硬件接口。1.2 OpenHarmony驱动开发选型HDF还是直接读写I2COpenHarmony下做外设驱动路径其实有两条。一条是轻量系统/小型系统设备比如Hi3861这种Wi-Fi SoC上的裸驱动直接在LiteOS-M或者LiteOS-A上申请I2C总线资源跑一个简单的业务任务去轮询数据。另一条是标准系统通常跑Linux内核上的HDF驱动框架驱动的加载、配置、服务发布都由HDF来管理这也是目前大多数带屏设备、开发板拿到系统后最常用的一套机制。标题里既然写的是“开源鸿蒙OpenHarmony系统实战开发”我建议直接按标准系统的HDF路线来做。理由有几点HDF是OpenHarmony对外统一驱动框架后续不管是屏幕、触摸、还是各种传感器都会归到这套体系里越早熟悉越省事。HDF的设备管理节点、电源管理、服务发布都是现成的驱动写好之后通过一个服务接口暴露给上层应用比业务代码直接操作内核i2c-dev更规范。驱动配置用HCSHDF Configuration Source文件描述设备挂在哪个I2C控制器、用哪个地址、支持的发射率参数是多少都在配置里维护后续换板子不用改C代码。当然我的实测经验是不要一上来就写完整HDF驱动先用最简单的内核态测试方式确认I2C通路是好的比如在uboot或者临时内核模块里直接发起一次读操作把环境温度读出来再往HDF框架里迁。这样做的好处是分离I2C硬件问题和框架接入问题不然到时候很难定位是总线没通还是HDF服务没配好。整体技术链路就是硬件接线准备 - I2C通路验证 - HCS设备配置 - HDF驱动Bind/Init/Release实现 - I2C读Word封装 - 温度换算与滤波 - HDF服务向上层暴露 - 应用层联调。2. 芯片工作原理与I2C通信细节2.1 MLX90614的寄存器与数据格式芯片内部有两类寄存器驱动开发最常用的是RAM区。RAM地址0x00存放环境温度Ta0x01存放物体温度Tobj0x02存放芯片温度Tdie。环境温度和物体温度是应用层直接关心的两个量。还有一块EEPROM区存放发射率校准系数、温度报警阈值、PWM配置等参数。EEPROM的配置写操作相对敏感上电后如果频繁擦写可能把芯片锁死正常情况下不要去动它。温度数据保存在16位寄存器里格式上是开尔文温标的扩展精度格式最低有效位对应的温度变化是0.02℃。把读到的16位原始值转成摄氏度的公式很好记float celsius (int16_t)rawValue * 0.02f - 273.15f;注意这里一定要先转成int16_t有符号数再乘系数MLX90614低温段输出的是二进制补码直接当uint16_t乘会算出离谱的负值。我在第一次写转换函数时就是因为忽略了这个细节读出来的物体温度变成了六百多度排查了半天发现只是数据截断类型的问题。读取寄存器时遵循SMBus的Read Word协议主机先发送设备的7位写地址和寄存器地址然后重新发起一次起始信号再发送设备读地址从机返回两个字节数据。数据发送顺序是低字节在前、高字节在后。看起来简单但在Linux的I2C控制器上做这种操作要特别注意是不是支持REPEATED START我后面在坑这一章节里会细说。2.2 SMBus跟I2C的区别和实操建议有时候芯片手册上写的是SMBus但我们的主控只有I2C控制器这俩严格说不是一回事。SMBus是I2C体系下的一个子集规定了更严格的电气参数如时钟频率最高100kHz、超时机制和分组协议MLX90614传输层走的是SMBus Read Word。I2C控制器如果实现了标准I2C协议基本也能完成SMBus的读写时序但有三个点需要特别注意。第一SMBus的Read Word要求第二个起始信号是REPEATED START。有些老旧的I2C控制器驱动在拆分读操作时会在两个事务之间插入STOP再重新START这就会导致MLX90614判读失败读回来的数据是全0xFF。解决办法是看控制器的I2C控制器驱动是否支持将写地址和读地址放在同一个I2C transfer的message数组里让控制器硬件连续发起addrreg、repeat-start、addrread。我在标准系统的HDF I2C API里就是这么做的一个调用传两个message让底层控制器自己完成时序。第二SMBus默认会带一个PECPacket Error Checking字节MLX90614在SMBus模式下也会输出这个校验字节。如果我们的主控把MLX90614当成纯I2C设备读两个字节就走人问题也不大但严格SMBus主机可能会等待第三个字节造成总线lock。更省事的办法是把MLX90614切到I2C模式省掉PEC的烦恼。怎么切换呢把SDA引脚拉低一段时间不同批次要求几十毫秒同时给芯片重新上电芯片探测到这种状态会进入I2C模式不再要求PEC。这块有条件的同学可以用逻辑分析仪确认一下。第三时钟频率不要跑太快。MLX90614在SMBus模式标准是100kHz很多项目的I2C总线为了传输速率提高到400kHz甚至1MHz结果要么读错数据要么通信不稳定。实测下来这颗芯片挂100kHz最稳就算整条总线上其他设备支持高速也建议给它单独分一条总线或者用总线速率不高于100kHz的控制器端口来带它。3. 搭建驱动工程HCS配置与驱动框架代码3.1 在OpenHarmony源码里新建驱动目录和编译入口OpenHarmony的HDF驱动模块通常放在drivers/peripheral/或者vendor厂商目录下我习惯在drivers/peripheral/sensor/mlx90614/下面建目录这样驱动归属清晰后续提交到上游也比较方便。目录结构大致是这样mlx90614/ ├── BUILD.gn ├── src/ │ ├── mlx90614_driver.c │ └── mlx90614_driver.h ├── hcs/ │ └── mlx90614_config.hcs └── docs/ └── README.mdBUILD.gn负责把C文件编译成HDF驱动模块。在标准系统里HDF驱动一般是独立的动态库或者内核模块编译类型可以用hdf_driver模板这个模板会处理驱动入口的注册和依赖关系。如果内核开启了HDF框架加载驱动的方式还需要把驱动模块的moduleName和HCS里的moduleName对应上。先把最小的编译骨架跑通我通常只写一个能打印日志的Init函数确认驱动能被系统加载再往里面填功能。不要一上来就写几百行代码再去编译那样出问题不好定位。3.2 设备配置文件的编写要点HCS配置是整个HDF驱动正确加载的关键。配置分两部分一部分是驱动实例的声明告诉HDF框架内核要加载哪个驱动模块另一部分是设备私有属性配置HDF框架会把这些属性解析出来在驱动Init时通过DeviceResourceIface接口读进C代码。驱动实例声明一般在vendor/开发板/vendor/config/里的device_info.hcs中追加内容类似root { device_info { match_attr hdf_manager; device_mlx90614 :: device { device0 :: deviceNode { policy 2; priority 100; preload 0; permission 0660; moduleName mlx90614_driver; serviceName hdf_mlx90614; deviceMatchAttr mlx90614_config; } } } }几个字段要理解清楚。policy 2表示内核态服务对内发布允许用户态通过IPC访问moduleName必须和C代码里HdfDriverEntry的moduleName完全一致否则HDF框架找不到驱动入口deviceMatchAttr是设备属性匹配的关键字HDF会用这个字段去设备私有配置节点里找到对应的match_attr节点把那一串属性绑定给这个设备实例。然后在mlx90614_config.hcs里写私有配置我习惯把硬件相关的东西全部放进来这样软件和硬件参数分离root { mlx90614_config { match_attr mlx90614_config; i2cBus 3; i2cAddr 0x5a; i2cSpeed 100; sampleIntervalMs 100; emissivityCompensation 0; } }match_attr要和上面deviceMatchAttr对应上。i2cBus是控制器编号需要查对应开发板的I2C控制器分配表比如RK3568上I2C3可能对应的是总线3。i2cAddr是芯片的7位地址MLX90614的默认地址是0x5A注意这里的0x5A是7位地址还是8位写地址HDF的I2C接口里用的是7位地址。3.3 驱动入口Bind、Init、Release的职责划分HDF驱动的生命周期由三个回调函数组成这个套路和Linux platform driver很像但职责更明确static int32_t Mlx90614Bind(struct HdfDeviceObject *deviceObject) { // 创建设备私有数据结构初始化互斥锁、读写缓冲区 // 这一步不做硬件资源申请只做软件结构初始化 return HDF_SUCCESS; } static int32_t Mlx90614Init(struct HdfDeviceObject *deviceObject) { // 读取HCS私有配置 // 打开I2C控制器句柄 HdfI2cOpen // 注册HDF服务回调让上层应用可以通过Dispatch访问 return HDF_SUCCESS; } static void Mlx90614Release(struct HdfDeviceObject *deviceObject) { // 关闭I2C句柄释放内存销毁锁 } struct HdfDriverEntry g_mlx90614DriverEntry { .moduleVersion 1, .moduleName mlx90614_driver, .Bind Mlx90614Bind, .Init Mlx90614Init, .Release Mlx90614Release, }; HDF_INIT(g_mlx90614DriverEntry);这里面有个容易被忽略的细节HDF框架加载驱动的顺序是先调Bind再调Init如果Bind返回失败Init不会执行而Release是在驱动卸载时被调用不一定会经过正常的挂载流程所以在Release里释放资源时一定要做好空指针判断。我早期在Release里直接释放一个未初始化的指针导致驱动卸载时内核报oops后来养成习惯Bind就把设备结构体用OsalMemCalloc清零再逐字段初始化Release里只负责逆操作。4. 核心代码实现I2C读写和温度换算4.1 初始化I2C控制器并封装读Word函数HDF标准系统里操作I2C是四个接口HdfI2cOpen、HdfI2cTransfer、HdfI2cClose。在Init里打开对应控制器dev-i2cHandle HdfI2cOpen(dev-i2cBus); if (dev-i2cHandle NULL) { HDF_LOGE(Mlx90614Init: open i2c bus %d failed, dev-i2cBus); return HDF_FAILURE; }读Word的封装是整个驱动的核心。MLX90614的SMBus读操作需要拆成两个HdfI2cMsg放到同一个Transfer调用里第一个是写寄存器地址第二个是读数据static int32_t ReadWord(Mlx90614Device *dev, uint8_t regAddr, uint16_t *value) { struct HdfI2cMsg msgs[2]; uint8_t regBuf[1]; uint8_t dataBuf[2]; regBuf[0] regAddr; msgs[0].addr dev-i2cAddr; msgs[0].flags 0; msgs[0].len 1; msgs[0].writeBuf regBuf; msgs[1].addr dev-i2cAddr; msgs[1].flags I2C_FLAG_READ; msgs[1].len 2; msgs[1].readBuf dataBuf; if (HdfI2cTransfer(dev-i2cHandle, msgs, 2) ! 2) { HDF_LOGE(ReadWord: i2c transfer failed); return HDF_FAILURE; } *value (uint16_t)((uint16_t)dataBuf[0] | ((uint16_t)dataBuf[1] 8)); return HDF_SUCCESS; }这里必须强调很多人会觉得既然是SMBus读先Write一次再Read一次也行但在实际控制器驱动实现里分两次调用HdfI2cTransfer往往会产生STOPSTARTMLX90614就不认。把两个msg放进同一个数组调用控制器才有机会在中间只插REPEATED START而不是STOP。如果驱动跑在内核态还要注意总线访问权限。HDF的I2C接口在标准系统里会做总线访问互斥但同一总线上如果还有别的设备在操作建议驱动里也加一个自己的mutex防止并发读取时寄存器地址和读数据被交叉组装。我就在一个项目里遇到过高频读取时偶发温度跳变最后定位到是另一个driver也在操作同一I2C控制器加上互斥锁后问题消失。4.2 温度数据转换与滤波处理读取RAM区的0x00环境温度和0x01物体温度代码就是连着两次ReadWord然后做转换#define MLX90614_RAW_TO_CELSIUS(raw) ((float)(int16_t)(raw) * 0.02f - 273.15f) static int32_t GetTemperature(Mlx90614Device *dev, float *ta, float *tobj) { uint16_t rawTa 0; uint16_t rawTobj 0; if (ReadWord(dev, MLX90614_REG_TA, rawTa) ! HDF_SUCCESS) { return HDF_FAILURE; } if (ReadWord(dev, MLX90614_REG_TOBJ, rawTobj) ! HDF_SUCCESS) { return HDF_FAILURE; } *ta MLX90614_RAW_TO_CELSIUS(rawTa); *tobj MLX90614_RAW_TO_CELSIUS(rawTobj); return HDF_SUCCESS; }温度数据本身不是完全平滑的传感器内部有热噪声特别是环境温度快速变化时Tobj会有小幅抖动。我的经验是做两层处理一层是硬件层面给传感器远离发热源一层是软件层面做滑动平均滤波。滑动滤波的窗口不建议太大我测试过取4~8次采样平均既不会让响应变迟钝太多又能明显压住抖动。窗口太大会让红外测温失去“快速读数”的意义实际门禁场景要求几百毫秒内出结果8次采样按100ms周期算就是0.8秒可以接受。关于发射率补偿这里有个很多开发者忽略的坑。MLX90614的出厂发射率默认是1.0也就是按黑体标定的。但现实物体的发射率都小于1比如皮肤0.97、塑料0.95、钢铁抛光面0.2。如果直接拿默认参数测读数会比真实温度偏低而且物体发射率越低偏差越大。项目里测人体时这个问题尤其明显。软件层面的补偿思路是如果物体发射率为e芯片默认按黑体辐射反推出的温度会更接近物体表面真实温度减去部分反射环境辐射的影响严格推导需要辐射能量公式但工程上可以做一个线性近似。另一个更规范的方式是把发射率写进芯片EEPROM让芯片内部做补偿缺点是需要操作EEPROM有锁死的风险。我的建议是固定场景、发射率相对稳定的写EEPROM一劳永逸经常变换测量对象的软件层做补偿更灵活只是换测量目标时要调整参数。4.3 通过HDF服务向上层暴露接口驱动把温度读出来了最终要交给应用层。HDF驱动一般通过service方式对外提供能力。在Bind阶段把service注册到设备对象上然后可以在Init里通过HdfDeviceRegisterService或者直接在设备对象的service字段上挂回调进入Dispatch。Dispatch是处理上层消息的统一入口。我习惯给上层提供三个命令字一个是读环境温度一个是读物体温度一个是设置软件发射率补偿系数。上层用户态程序通过HDF的IPC接口发消息过来驱动解析命令字执行对应的内部函数把温度结果通过replyData传回去。static int32_t Mlx90614Dispatch(struct HdfDeviceIoClient *client, int cmdCode, struct HdfSBuf *data, struct HdfSBuf *reply) { switch (cmdCode) { case CMD_MLX90614_GET_TA: { float ta 0.0f; if (GetTemperature(dev, ta, tobj) ! HDF_SUCCESS) { return HDF_FAILURE; } HdfSbufWriteFloat(reply, ta); return HDF_SUCCESS; } case CMD_MLX90614_GET_TOBJ: { float tobj 0.0f; ... } default: return HDF_FAILURE; } }上层应用如果走的是标准系统可以直接用hdf框架的用户态接口拿到设备服务然后发起调用。这个调度链路封装得比较深调试时很容易摸不着头脑我建议在驱动里把Dispatch入口处的cmdCode和返回值打印出来上层每次调用都能在dmesg里看到请求是否到达驱动能省很多联调时间。5. 联调中遇到的问题与排查思路5.1 读不到设备与NACK问题我在不同平台上接MLX90614遇到的第一类问题几乎都是I2C总线通信失败典型表现是HdfI2cTransfer返回负数或者读出来的数据一直是0xFFFF。排查思路分三步走。先确认地址。MLX90614的默认7位地址是0x5A换算成8位写地址是0xB4读地址是0xB5。检查HCS配置里到底填的是0x5A还是0xB4我见过不止一次把0xB4填进配置导致地址不匹配最后总线一直回NACK。另外有些模块的厂商会把AD0或者地址选择引脚拉高拉低来改地址不要想当然认为所有模块都是0x5A用i2cdetect之类的工具扫一下最靠谱。再确认REPEATED START是否被正确执行。这点的典型现象是读时序能在逻辑分析仪上看到START - 写地址 - 寄存器地址但后面跟了一个STOP再接着一个START而不是一个连续的REPEATED START。这种时序下MLX90614本次读操作是失败的。解决办法就是把前面说的两个message放进同一个HdfI2cTransfer调用如果还是不行检查平台的I2C控制器驱动在操作多个msg时是否规范执行了非STOP的地址切换。最后看一下时钟频率和上拉电阻。MLX90614供电3.3V时I2C上拉电阻推荐4.7k如果总线过长或者上拉过小沿会变差导致数据采样错误。实测200ms的轮询周期下这问题会偶发出现很不稳定。另外如果总线上还挂了别的设备各个设备的SDA/SCL引脚寄生电容会叠加走线长的板子建议上拉电阻选小一点比如2.2k但要注意别低于I2C规范的下限导致驱动能力不足。5.2 温度读数偏低、波动大的原因分析读出来有数据、但温度不对这类问题比通信问题更隐蔽。测人体温度时读数比水银体温计低2~3℃大概率是发射率问题。皮肤发射率是0.97~0.98芯片默认按1.0标定黑体辐射公式是非线性的所以高温段偏差更明显。解决办法要么做软件补偿要么用红外黑体源校准后把补偿系数写到EEPROM里。我实际测过在25℃环境里测人的手腕默认发射率下读出来33.5℃左右而实际体温是36.5℃差了3℃这个偏差在门禁测温场景里完全不能接受。软件补偿做一次线性校正后偏差能压到0.3℃以内但要做多点校准才稳。读数波动大从软件层面看两个原因。一是采样频率太接近芯片内部刷新周期。MLX90614的SMBus读操作周期受内部资源限制连续高速读会让寄存器里的值还没来得及刷新就被重复读取数据自然显得“粘滞”或者偶发突变。把采样间隔控制到100ms以上会好很多。二是没有做滤波或者说滤波窗口太短。红外传感器本身具有热噪声特性几十毫秒的瞬时读数跳0.1~0.2℃很正常这在很多场景下可以接受但如果用于精度要求较高的判断至少要取平均值。从硬件层面看波动大的常见原因是传感器前方有遮挡物或者视场角内有多个目标物体。MLX90614不同型号有不同视场角FOV有的35°有的50°甚至90°FOV内的所有物体辐射会混合进入传感器。测体温时需要传感器离目标足够近让目标充满视场角。如果传感器前面加了保护玻璃还要确认这层玻璃在红外波段是透光的普通玻璃对红外线衰减很严重会直接导致读数偏低和波动。5.3 操作EEPROM的翻车经历我早期一个项目里想通过EEPROM把发射率改成0.95结果因为写时序不对芯片进入了异常状态后面读温度都返回错误值。还好那批模块留了备份换一片就好了但也让我意识到EEPROM操作必须谨慎。MLX90614写入EEPROM需要走特定的解锁流程不同批次芯片可能还有细微差异。我的建议是除非你有明确的量产校准需求否则不要在生产环境里做EEPROM写操作。如果一定要写先在备份模块上验证流程记录下出厂默认值万一写坏了还有恢复路径。软件补偿方案在没有严格要求时是更稳妥的选择。6. 一些实测心得和后续扩展6.1 实测数据稳定性与精度表现我拿RK3568开发板跑这套HDF驱动100kHz总线速率、100ms采样周期、8次滑动平均在恒定水温目标上连续测了半小时温度最大波动在±0.2℃以内和环境温度25℃的温差变化趋势对比曲线非常平滑。测人体手腕软件发射率补偿后读数稳定在36.3~36.6℃和水银体温计对照大概±0.3℃。这个精度对非接触测温场景是够用的。需要提醒的是MLX90614在快速改变目标时比如从冷环境移到热物体前芯片内部的响应时间大约0.5秒到1秒加上滤波窗口最终稳定读数可能需要2秒左右。所以做门禁测温时被测者要在传感器前停留一两秒而不是指望瞬间出数。6.2 这个驱动还能往哪个方向扩展目前这套驱动实现了最基本的环境温度和物体温度读取。后续可以扩展的方向有几个。一个是做温度报警阈值管理HDF驱动里把阈值放到HCS配置中上层通过Dispatch动态修改当红外温度超过阈值时通过HDF的事件上报机制主动通知应用层这样应用层不用一直轮询。另一个是多传感器接入。如果一个系统里需要测量多个点的温度可以买MULTI版本或者通过地址引脚来区分不同MLX90614HCS里配置多个deviceNode每个节点绑定不同的I2C地址和总线号驱动里写一个统一的注册表去管理多个实例。还可以把驱动接入系统的sensor框架OpenHarmony标准系统有sensor服务接入之后上层应用可以直接通过系统的Sensor API获取温度数据不用自己写私有服务协议这也是量产方案中更好的接入方式。我个人在实际操作中的体会是MLX90614这颗芯片的驱动开发难度主要不在芯片本身而在宿主系统的驱动框架适应。HDF这套东西文档不够顺滑调试工具也少但一旦把Bind、Init、Dispatch这条链路摸透后面再做其他传感器驱动基本上都是复制粘贴改配置。这个教程里写的代码直接拷到你的开发板上把i2cBus和i2cAddr改成实际值大概率就能跑通。如果卡在某一步优先用逻辑分析仪看I2C时序而不是反复改代码瞎猜。
返回列表