
1. 为什么要在 OpenHarmony 上折腾一颗红外测温芯片第一次拿到 MLX90614 这颗芯片的时候我其实没太当回事。它长得跟一颗普通的 TO-90 封装三极管差不多四个引脚I2C 接口手册上写着出厂已经校准好了读出来就是摄氏度。看起来比那些需要自己写补偿算法的传感器友好太多。但真正把它接到 OpenHarmony 的板子上跑通中间踩的坑一点都不少——从设备树节点写错导致 I2C 总线扫不到地址到内核态读出来的数据一直是 0xFFFF再到用户态 HDF 框架下服务注册失败每一步都够折腾半天。这篇东西就是把这整条链路完整地捋一遍。核心关键词是MLX90614、OpenHarmony、驱动开发、I2C和设备树我会从硬件接线讲到设备树配置再到内核驱动和 HDF 用户态接口最后给出一个能直接跑通的完整方案。适合谁看如果你已经能在 OpenHarmony 上点亮一个 GPIO或者写过简单的字符设备驱动那这篇就是为你准备的。如果你连 I2C 是什么都还没搞明白建议先去补一下 I2C 时序的基础不然设备树那一堆属性会看得你头皮发麻。MLX90614 的应用场景其实很广非接触式人体测温、工业设备过热预警、智能家居里的环境感知甚至一些 DIY 项目里做红外热成像的单点扫描。它支持 3V 和 5V 供电I2C 地址出厂默认是 0x5A7 位地址测温范围覆盖 -70°C 到 380°C 的物体温度环境温度范围是 -40°C 到 125°C。精度方面人体温度区间内能做到 ±0.5°C这个指标在消费级红外测温里已经相当能打了。OpenHarmony 这边驱动开发走的是 HDFHardware Driver Foundation框架跟传统 Linux 字符设备那套有相似的地方但设备树和驱动注册的流程有自己的规矩。很多人第一次接触 OpenHarmony 驱动开发最容易犯的错就是把 Linux 那套直接搬过来结果发现设备树节点对不上、HDF 服务起不来。我下面会把这些差异点一个个拆开讲。2. 硬件层MLX90614 的 I2C 接线与供电细节2.1 引脚定义与最小系统接线MLX90614 一般有四个引脚VDD、GND、SCL、SDA。有些封装还带一个 PWM 输出引脚但咱们走 I2C 路线那个脚悬空就行。接线本身不复杂但有几个细节不注意就会翻车。VDD 接 3.3V 还是 5V手册上说 3V 到 5V 都能工作但 I2C 电平是跟着 VDD 走的。如果你的主控是 3.3V 电平那 MLX90614 也接 3.3V这样 SDA 和 SCL 上不需要额外的电平转换。我试过接 5V 然后用 3.3V 主控直接拉 I2C结果通信时好时坏后来加了电平转换芯片才稳定。所以供电电压和主控 I2C 电平保持一致这是第一条铁律。上拉电阻的问题。I2C 总线是开漏输出SDA 和 SCL 都必须接上拉电阻。MLX90614 模块板上通常已经自带了 4.7k 或 10k 的上拉但如果你买的是裸芯片自己搭电路那就得自己加。上拉电阻的取值跟总线速率和总线电容有关标准模式 100kHz 下 4.7k 到 10k 都行快速模式 400kHz 下建议用 2.2k 到 4.7k。我一般用 4.7k实测在 100kHz 和 400kHz 下都能稳定工作。电源去耦。MLX90614 对电源噪声比较敏感VDD 和 GND 之间最好并一个 0.1uF 的陶瓷电容位置尽量靠近芯片引脚。我一开始没加读出来的温度偶尔会跳变一两度加了电容之后就稳了。2.2 I2C 地址与总线扫描MLX90614 的出厂默认 7 位 I2C 地址是 0x5A。注意有些资料上写的是 0xB4那是把 7 位地址左移一位之后加上读写位的 8 位地址。在 Linux 和 OpenHarmony 的设备树里填的是 7 位地址也就是 0x5A。在正式写驱动之前我强烈建议先用 i2c-tools 扫一下总线确认芯片能被识别到。OpenHarmony 的标准系统里不一定自带 i2c-tools但你可以交叉编译一个静态版本丢到板子上跑。命令很简单i2cdetect -y 0如果 0x5A 位置显示为 5A说明硬件连接和地址都没问题。如果显示为 --那就要检查接线、上拉电阻和供电。如果显示为 UU说明这个地址已经被某个驱动占用了可能是内核里已经有 MLX90614 的驱动在跑。注意有些 MLX90614 模块出厂时地址被改过尤其是那些带 PCB 的成品模块。如果你扫不到 0x5A试试扫 0x5B 到 0x5F或者用 0x00 地址广播的方式去读它的 EEPROM 里的地址信息。3. 设备树配置把芯片挂到 I2C 总线上3.1 I2C 控制器节点的确认OpenHarmony 的设备树跟 Linux 设备树是同源的但不同芯片平台的 I2C 控制器节点名字和属性可能不一样。以瑞芯微 RK3568 为例I2C 控制器节点通常在rk3568.dtsi里定义比如i2c0、i2c1等等。你要做的第一件事是确认你的 MLX90614 接在哪条 I2C 总线上然后找到对应的控制器节点。假设接在 i2c3 上那么控制器节点大概长这样i2c3: i2cfe5c0000 { compatible rockchip,rk3568-i2c; reg 0x0 0xfe5c0000 0x0 0x1000; interrupts GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH; clocks cru CLK_I2C3, cru PCLK_I2C3; clock-names i2c, pclk; pinctrl-names default; pinctrl-0 i2c3m0_xfer; #address-cells 1; #size-cells 0; status disabled; };你要做的是在板级设备树文件里把status改成okay然后在这个节点下面添加 MLX90614 的子节点。3.2 MLX90614 子节点的编写子节点的写法直接决定了驱动能不能匹配上。我见过太多人在这里写错compatible属性导致驱动加载了但 probe 函数死活不进。正确的写法是这样的i2c3 { status okay; mlx90614: mlx906145a { compatible melexis,mlx90614; reg 0x5a; status okay; }; };compatible属性是驱动匹配的关键。内核驱动里的of_device_id表必须包含melexis,mlx90614这个字符串否则驱动和设备树匹配不上。reg属性填的是 7 位 I2C 地址 0x5A。如果你用的是 HDF 框架下的驱动compatible的匹配规则可能还涉及 HDF 的device_info.hcs配置文件。这个我后面会详细讲。3.3 设备树调试的常见坑设备树写完之后编译烧录然后进系统看/proc/device-tree下面有没有对应的节点。如果没有说明设备树没生效可能是编译没选对 dtb 文件或者节点被其他配置覆盖了。另一个常见的坑是 I2C 控制器被其他驱动占用了。比如有些板子默认把 i2c3 分配给了一个 EEPROM 或者 PMIC你在设备树里再加 MLX90614 就会冲突。这时候要么换一条 I2C 总线要么把原来的驱动关掉。还有一个坑是引脚复用。RK3568 的 I2C 引脚可能跟其他功能复用比如 UART 或者 GPIO。你需要在pinctrl里确认i2c3m0_xfer这个引脚组没有被其他节点引用。如果被引用了要么改引脚组要么把冲突的节点关掉。实操心得设备树改完之后不要急着烧录。先用dtc工具把 dts 编译成 dtb然后反编译回来看看节点是不是你写的那样。命令是dtc -I dtb -O dts -o output.dts input.dtb。这一步能帮你提前发现语法错误和节点覆盖问题。4. 内核驱动开发从 I2C 客户端到温度读取4.1 驱动框架的选择在 OpenHarmony 上开发 MLX90614 驱动有两条路可以走一是传统的 Linux I2C 客户端驱动二是 OpenHarmony 的 HDF 驱动框架。两者不是互斥的HDF 框架底层其实还是基于 Linux 的设备模型但对外提供了统一的硬件接口抽象。如果你只是想让芯片能读温度用传统的 I2C 客户端驱动最快。但如果你要做成一个标准的 OpenHarmony 硬件驱动能被上层应用通过 HDF 接口调用那就得走 HDF 路线。我下面两条路都会讲先讲传统驱动再讲 HDF 封装。4.2 传统 I2C 客户端驱动的实现传统驱动的核心是三个东西i2c_driver结构体、of_device_id匹配表、以及probe函数。probe函数里做初始化然后注册一个字符设备或者 sysfs 接口给用户态读温度。MLX90614 的寄存器地址是 16 位的但 I2C 传输的时候是先发 8 位低地址再发 8 位高地址。比如读环境温度寄存器地址是 0x06实际发送的字节序列是0x06, 0x00。读物体温度寄存器地址是 0x07发送0x07, 0x00。读出来的数据是 16 位的低字节在前高字节在后。温度值的换算公式是温度 原始值 * 0.02 - 273.15单位是摄氏度。这个 0.02 是芯片的分辨率手册上写的。下面是一个简化的驱动代码框架#include linux/module.h #include linux/i2c.h #include linux/fs.h #include linux/uaccess.h #define MLX90614_REG_AMBIENT 0x06 #define MLX90614_REG_OBJECT 0x07 static int mlx90614_read_temp(struct i2c_client *client, u8 reg, int *temp) { u8 buf[2]; s32 ret; u16 raw; ret i2c_smbus_read_word_data(client, reg); if (ret 0) return ret; raw (u16)ret; *temp (int)(raw * 20 - 27315); /* 放大100倍避免浮点 */ return 0; } static ssize_t mlx90614_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { struct i2c_client *client filp-private_data; int temp; char kbuf[16]; int len; if (mlx90614_read_temp(client, MLX90614_REG_OBJECT, temp) 0) return -EIO; len snprintf(kbuf, sizeof(kbuf), %d.%02d\n, temp / 100, temp % 100); if (copy_to_user(buf, kbuf, len)) return -EFAULT; return len; } static int mlx90614_probe(struct i2c_client *client, const struct i2c_device_id *id) { dev_info(client-dev, MLX90614 probed at address 0x%02x\n, client-addr); return 0; } static const struct of_device_id mlx90614_of_match[] { { .compatible melexis,mlx90614 }, { } }; MODULE_DEVICE_TABLE(of, mlx90614_of_match); static struct i2c_driver mlx90614_driver { .driver { .name mlx90614, .of_match_table mlx90614_of_match, }, .probe mlx90614_probe, }; module_i2c_driver(mlx90614_driver); MODULE_LICENSE(GPL);这段代码里i2c_smbus_read_word_data会自动处理 I2C 的读写时序你不需要手动拼字节序列。但要注意有些 I2C 控制器对 SMBus 协议的支持不完整可能会返回-EOPNOTSUPP。如果遇到这种情况就得用i2c_transfer手动构造i2c_msg来读写。4.3 HDF 驱动框架的封装OpenHarmony 的 HDF 框架把驱动分成了几个层次hdf_driver结构体、hdf_device对象、以及Dispatch方法。你要做的是实现一个 HDF 驱动在Bind函数里初始化 I2C 设备在Dispatch里处理上层发来的读温度请求。HDF 驱动的配置文件通常放在vendor/xxx/hdf_config/下面有一个device_info.hcs文件里面定义了设备节点和驱动模块的对应关系。你需要在这个文件里添加 MLX90614 的节点device_mlx90614 :: device { device0 :: deviceNode { policy 2; priority 100; preload 0; permission 0664; moduleName mlx90614_driver; serviceName mlx90614_service; deviceMatchAttr mlx90614_config; }; }moduleName对应驱动编译出来的 ko 文件名serviceName是上层应用通过 HDF 接口调用的服务名。policy为 2 表示对内核态和用户态都提供服务。HDF 驱动的代码结构比传统驱动复杂一些但核心逻辑是一样的读 I2C 寄存器换算温度返回给上层。我建议先用传统驱动把 I2C 通信调通确认能读到正确的温度值然后再套 HDF 的壳。这样出问题的时候容易定位是 I2C 层的问题还是 HDF 层的问题。5. 用户态接口从 sysfs 到 HDF 服务调用5.1 sysfs 接口的快速验证如果你走的是传统驱动路线最简单的用户态接口就是 sysfs。在probe函数里创建一个sysfs属性文件用户态直接cat就能读到温度。static ssize_t temp_show(struct device *dev, struct device_attribute *attr, char *buf) { struct i2c_client *client to_i2c_client(dev); int temp; if (mlx90614_read_temp(client, MLX90614_REG_OBJECT, temp) 0) return -EIO; return sprintf(buf, %d.%02d\n, temp / 100, temp % 100); } static DEVICE_ATTR_RO(temp); static int mlx90614_probe(struct i2c_client *client, ...) { device_create_file(client-dev, dev_attr_temp); return 0; }这样在/sys/bus/i2c/devices/3-005a/temp就能读到温度值。这个方法特别适合调试阶段不用写上层应用就能验证驱动是否工作。5.2 HDF 服务接口的调用如果你走的是 HDF 路线上层应用需要通过hdf_io_service_open打开服务然后通过Dispatch发送读命令。HDF 的服务调用有一套固定的流程先获取服务对象再调用Dispatch方法最后解析返回的数据。struct HdfIoService *service HdfIoServiceBind(mlx90614_service); if (service NULL) { printf(bind service failed\n); return -1; } struct HdfSBuf *data HdfSBufObtainDefaultSize(); struct HdfSBuf *reply HdfSBufObtainDefaultSize(); HdfSbufWriteString(data, read_temp); service-dispatcher-Dispatch(service-object, 0, data, reply); const char *temp HdfSbufReadString(reply); printf(temperature: %s\n, temp);这段代码的关键是HdfIoServiceBind的参数必须跟device_info.hcs里的serviceName一致。如果绑定失败先检查 HDF 驱动有没有加载成功用hdc shell ls /dev/hdf看看设备节点在不在。5.3 数据精度与单位处理MLX90614 返回的温度值分辨率是 0.02°C但实际有效精度受噪声和校准影响人体温度区间内大概能做到 ±0.5°C。在用户态展示的时候我一般保留一位小数再多就没有意义了。单位方面芯片返回的是开尔文乘以 50 的原始值换算成摄氏度是raw * 0.02 - 273.15。如果你要在应用层做显示建议在驱动层就换算好直接返回摄氏度避免上层重复计算。注意MLX90614 读出来的物体温度是视场角内的平均温度如果你的目标物体比视场角小读出来的值会偏低。视场角有 90 度和 35 度两种版本买的时候要看清楚型号后缀。90 度适合近距离大范围测温35 度适合远距离小目标测温。6. 常见问题排查与调试技巧实录6.1 I2C 通信失败的问题排查I2C 通信失败是最常见的问题表现是i2c_transfer返回负值或者读出来的数据全是 0xFF 或 0x00。排查思路是这样的先确认硬件连接。用万用表量一下 SDA 和 SCL 对地的电压正常应该是 VDD 电压。如果只有 0V 或者 1V 左右说明上拉电阻没接或者阻值太大。再量一下 VDD 和 GND 之间的电压确认供电正常。然后确认 I2C 地址。用i2cdetect扫总线如果扫不到 0x5A试试其他地址。有些模块的地址被改过或者地址引脚被拉高了。如果地址能扫到但读写失败那可能是 I2C 速率太高。MLX90614 支持最高 100kHz 的标准模式有些批次支持 400kHz 快速模式但不是所有批次都保证。在设备树里把 I2C 控制器的clock-frequency改成 100000 试试。还有一个隐蔽的坑是 I2C 控制器的引脚复用没配对。RK3568 的 I2C3 有多个引脚组i2c3m0_xfer和i2c3m1_xfer对应的物理引脚不一样。如果你接的是 m1 的引脚但设备树里配的是 m0那肯定通信失败。6.2 温度读数异常的问题排查温度读数异常一般有几种表现读数一直是 0xFFFF、读数跳变很大、读数明显偏离实际温度。读数一直是 0xFFFF通常是 I2C 读到了空数据。检查一下i2c_smbus_read_word_data的返回值如果是负数说明通信失败。如果是 0xFFFF可能是寄存器地址发错了。MLX90614 的寄存器地址是 16 位的但i2c_smbus_read_word_data只发 8 位地址。你需要用i2c_smbus_read_i2c_block_data或者手动构造i2c_msg来发 16 位地址。读数跳变很大一般是电源噪声或者 I2C 总线干扰。加去耦电容缩短接线长度降低 I2C 速率都能改善。另外MLX90614 的读命令之间需要间隔至少 1ms读太快会导致数据不稳定。读数明显偏离实际温度先确认视场角内有没有其他热源。比如你测人体温度但背景有一台电脑显示器那读出来的值会偏高。另外芯片本身有自发热长时间工作后读数会偏高 0.5°C 左右这是正常的。6.3 HDF 服务注册失败的问题排查HDF 服务注册失败的表现是HdfIoServiceBind返回 NULL或者Dispatch调用返回错误码。排查步骤先确认 HDF 驱动有没有编译进系统。用hdc shell ls /dev/hdf看看设备节点在不在。如果不在检查BUILD.gn里有没有把驱动加到编译目标里。再确认device_info.hcs的配置有没有生效。这个文件在编译的时候会被打包进系统镜像如果你改了之后没重新编译那配置还是旧的。还有一个常见问题是moduleName和实际编译出来的 ko 文件名不一致。moduleName填的是mlx90614_driver那编译出来的 ko 文件必须是mlx90614_driver.ko否则 HDF 框架加载不了。6.4 常见问题速查表问题现象可能原因排查方法解决方案i2cdetect 扫不到 0x5A接线错误、供电异常、上拉电阻缺失万用表量电压、检查接线重新接线、加 4.7k 上拉、确认供电读写返回 -EOPNOTSUPPI2C 控制器不支持 SMBus查看控制器驱动改用 i2c_transfer 手动构造消息读数一直 0xFFFF寄存器地址发送错误抓 I2C 波形用 16 位地址读写方式读数跳变电源噪声、总线干扰示波器看电源纹波加去耦电容、降低 I2C 速率HDF 服务绑定失败驱动未加载、配置未生效ls /dev/hdf检查 BUILD.gn 和 device_info.hcs温度偏高芯片自发热、背景热源对比环境温度等待稳定、调整视场角7. 从驱动到应用一个完整的测温小项目7.1 项目结构设计光有驱动还不够得有一个能跑起来的小项目来验证整条链路。我设计了一个简单的测温应用驱动层每 500ms 读一次 MLX90614 的温度通过 HDF 服务上报应用层订阅这个服务把温度显示在 OLED 屏幕上同时通过串口打印出来。OLED 屏幕用的是 SSD1306I2C 接口地址 0x3C。它跟 MLX90614 可以挂在同一条 I2C 总线上只要地址不冲突就行。SSD1306 的驱动 OpenHarmony 社区里已经有现成的直接拿来用就行。7.2 驱动层的定时读取在 HDF 驱动里加一个定时器每 500ms 触发一次温度读取然后把结果缓存起来。上层来读的时候直接返回缓存值避免每次读都去操作 I2C。static void mlx90614_timer_cb(void *arg) { struct mlx90614_dev *dev (struct mlx90614_dev *)arg; int temp; if (mlx90614_read_temp(dev-client, MLX90614_REG_OBJECT, temp) 0) { dev-last_temp temp; } osal_timer_mod(dev-timer, 500); }定时器用 OpenHarmony 的osal_timer接口精度够用而且不占用 I2C 总线太久。7.3 应用层的显示与上报应用层用 HDF 服务接口读到温度后格式化成一个字符串然后调用 SSD1306 的显示接口画到屏幕上。同时通过printf输出到串口方便调试。void temp_display_task(void) { struct HdfIoService *service HdfIoServiceBind(mlx90614_service); while (1) { struct HdfSBuf *data HdfSBufObtainDefaultSize(); struct HdfSBuf *reply HdfSBufObtainDefaultSize(); HdfSbufWriteString(data, read_temp); service-dispatcher-Dispatch(service-object, 0, data, reply); const char *temp HdfSbufReadString(reply); oled_show_string(0, 0, Temp:); oled_show_string(0, 2, temp); printf(current temp: %s\n, temp); HdfSBufRecycle(data); HdfSBufRecycle(reply); osDelay(500); } }这个任务跑起来之后OLED 上就能看到实时的温度值串口也在同步打印。整个链路从 I2C 硬件到驱动到应用就全部打通了。7.4 实测数据与精度对比我用这个方案跟一个医用级电子体温计做了对比在室温 25°C 的环境下测额头温度MLX90614 读数和体温计的偏差在 0.3°C 以内。测 40°C 左右的温水偏差在 0.5°C 以内。这个精度对于非医疗用途来说完全够用。需要注意的是MLX90614 测的是表面温度不是核心温度。测人体温度的时候额头表面温度受环境温度和出汗影响很大所以读出来的值跟体温计的口腔或腋下温度会有差异。如果要做人体测温建议加一个环境温度补偿算法或者直接测手腕内侧这种更接近核心温度的部位。8. 一些踩坑之后的经验总结MLX90614 这颗芯片本身不复杂但在 OpenHarmony 上跑通整条链路涉及的环节比较多硬件接线、设备树、I2C 驱动、HDF 框架、用户态接口。任何一个环节出问题都会导致最终读不到数据。我的建议是分阶段验证。第一阶段用 i2c-tools 确认芯片能被扫描到。第二阶段写一个最简单的 I2C 读写程序确认能读到正确的寄存器值。第三阶段把 I2C 读写封装成内核驱动通过 sysfs 暴露接口。第四阶段套上 HDF 框架通过 HDF 服务暴露接口。第五阶段写上层应用做显示和上报。每个阶段都确认没问题了再进入下一个阶段这样出问题的时候容易定位。另外设备树的调试一定要耐心。OpenHarmony 的设备树跟 Linux 设备树虽然同源但不同芯片平台的实现有差异尤其是引脚复用和时钟配置这部分。多看芯片的参考手册和 SDK 里的示例设备树能少走很多弯路。I2C 的速率不要一上来就设 400kHz先用 100kHz 把通信调通再尝试提速。MLX90614 对时序的容忍度不算高速率太快容易出问题。上拉电阻的阻值也要根据实际总线电容调整线越长上拉电阻要越小。最后说一个 HDF 框架的坑。HDF 驱动的Dispatch方法是同步调用的如果你在Dispatch里做耗时操作比如等 I2C 传输完成会阻塞上层应用的调用。所以我在驱动里加了定时器缓存温度值Dispatch直接返回缓存这样响应就很快了。这个设计在实际项目中很实用尤其是上层有多个应用同时读温度的时候。