1. 为什么要在 OpenHarmony 上折腾一颗红外测温芯片
第一次拿到 MLX90614 这颗芯片的时候,我其实没太当回事。它长得跟一颗普通的 TO-90 封装三极管差不多,四个引脚,I2C 接口,手册上写着出厂已经校准好了,读出来就是摄氏度。看起来比那些需要自己写补偿算法的传感器友好太多。但真正把它接到 OpenHarmony 的板子上跑通,中间踩的坑一点都不少——从设备树节点写错导致 I2C 总线扫不到地址,到内核态读出来的数据一直是 0xFFFF,再到用户态 HDF 框架下服务注册失败,每一步都够折腾半天。
这篇东西就是把这整条链路完整地捋一遍。核心关键词是MLX90614、OpenHarmony、驱动开发、I2C和设备树,我会从硬件接线讲到设备树配置,再到内核驱动和 HDF 用户态接口,最后给出一个能直接跑通的完整方案。适合谁看?如果你已经能在 OpenHarmony 上点亮一个 GPIO,或者写过简单的字符设备驱动,那这篇就是为你准备的。如果你连 I2C 是什么都还没搞明白,建议先去补一下 I2C 时序的基础,不然设备树那一堆属性会看得你头皮发麻。
MLX90614 的应用场景其实很广:非接触式人体测温、工业设备过热预警、智能家居里的环境感知,甚至一些 DIY 项目里做红外热成像的单点扫描。它支持 3V 和 5V 供电,I2C 地址出厂默认是 0x5A(7 位地址),测温范围覆盖 -70°C 到 380°C 的物体温度,环境温度范围是 -40°C 到 125°C。精度方面,人体温度区间内能做到 ±0.5°C,这个指标在消费级红外测温里已经相当能打了。
OpenHarmony 这边,驱动开发走的是 HDF(Hardware 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: i2c@fe5c0000 { 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: mlx90614@5a { 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 上拉、确认供电 |
| 读写返回 -EOPNOTSUPP | I2C 控制器不支持 SMBus | 查看控制器驱动 | 改用 i2c_transfer 手动构造消息 |
| 读数一直 0xFFFF | 寄存器地址发送错误 | 抓 I2C 波形 | 用 16 位地址读写方式 |
| 读数跳变 | 电源噪声、总线干扰 | 示波器看电源纹波 | 加去耦电容、降低 I2C 速率 |
| HDF 服务绑定失败 | 驱动未加载、配置未生效 | ls /dev/hdf | 检查 BUILD.gn 和 device_info.hcs |
| 温度偏高 | 芯片自发热、背景热源 | 对比环境温度 | 等待稳定、调整视场角 |
7. 从驱动到应用:一个完整的测温小项目
7.1 项目结构设计
光有驱动还不够,得有一个能跑起来的小项目来验证整条链路。我设计了一个简单的测温应用:驱动层每 500ms 读一次 MLX90614 的温度,通过 HDF 服务上报;应用层订阅这个服务,把温度显示在 OLED 屏幕上,同时通过串口打印出来。
OLED 屏幕用的是 SSD1306,I2C 接口,地址 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直接返回缓存,这样响应就很快了。这个设计在实际项目中很实用,尤其是上层有多个应用同时读温度的时候。