环境光会读了。今天同一条总线上换红外测温。这颗芯片对速率有上限,配快了目录还在、驱动却绑不上去。

现场是这样开始的。dts 写了 mlx90614@5a,ls /sys/bus/i2c/devices 能看见 1-005a,应用按路径去读 in_temp_object_raw,文件不存在。有人说芯片坏了,因为「节点都出来了」。节点出来只说明内核根据设备树 实例化了 i2c_client,不说明这颗硅在 400 kHz 下 ACK 过。

ls -l /sys/bus/i2c/devices/1-005a/driver
# ls: cannot access '.../driver': No such file or directory

dmesg | grep -i mlx
# (空,或者 failed to read config)

hexdump -C /proc/device-tree/i2c@fe5a0000/clock-frequency
# 00 06 1a 80

00 06 1a 80 是大端 400000。MLX90614 走 SMBus,规格上限 100 kHz。400 kHz 下读配置寄存器静默失败,驱动放弃 probe,client 目录还在。把 clock-frequency 改成 <100000>,重打 boot_linux 并且刷 resource 里那份 dtb,软链才出现:

ls -l /sys/bus/i2c/devices/1-005a/driver
# .../1-005a/driver -> ../../../../bus/i2c/drivers/mlx90614

hexdump -C /proc/device-tree/i2c@fe5a0000/clock-frequency
# 00 01 86 a0

物体温度在 RAM 0x07:总线上是三字节 LSB、MSB、PEC。换算:

T(°C) = raw * 0.02 - 273.15

环境温度在 RAM 0x06,同一公式。透镜对准手掌或额头 1~3 cm,物体通道会到 32~36 °C 一带;对准桌面,物体通道掉回室温附近。这是验收,不是装饰。


1. 这颗芯片在干什么

MLX90614 是非接触红外温度计。封装顶部那颗透镜把视场里的红外收进热电堆,芯片内部已经做完放大和数字化,用户只读 RAM。I2C 7 位地址 0x5a 出厂写死,改地址要写 EEPROM,bring-up 不要碰。

RAM 里和温度有关的字(命令字节就是地址本身):

命令内容
0x04原始 IR 通道 1
0x05原始 IR 通道 2(单探头型号常不用)
0x06环境温度 Ta
0x07物体温度 Tobj1
0x08物体温度 Tobj2(双探头)

每个字 16 位。读周期在 SMBus 上是:

S  0x5a+W  ACK  cmd  ACK  Sr  0x5a+R  ACK  LSB  ACK  MSB  ACK  PEC  NAK  P

用户态一次组合读 3 字节,就能拿到 LSB、MSB、PEC。PEC 是 CRC-8,多项式 0x07,覆盖「地址+W、命令、地址+R、LSB、MSB」。bring-up 可以先不校验,数字对了再加。内核 i2c_smbus_read_word_data 只要前两个数据字节,PEC 由 SMBus 层处理。

16 位 raw 是开尔文的 50 倍:

T_K = raw * 0.02
T_C = T_K - 273.15 = raw * 0.02 - 273.15

raw = 0x3AEB = 15083 时,15083 × 0.02 − 273.15 ≈ 28.5 °C,像室内桌面。手掌贴到 1~3 cm,物体 raw 会到 0x3B80 一带,大约 34 °C。全 0 或全 1(0xFFFF)优先当总线失败,不要换算成 −273 °C 或 1030 °C 拿去吓人。

上电大约 250 ms 之内读数不稳定。写过 EEPROM 要等 20 ms。睡眠唤醒要把 SDA 拉低约 34 ms,板级没接 wakeup GPIO 就别进睡眠。

视场宽。1~3 cm 对准皮肤,周围墙壁、灯光会掺进去。贴到 0 cm 堵死透镜,或者 20 cm 对着空气,物体通道都会往环境温度靠。现场只认这一条:手掌 1~3 cm 和桌面,两个数对不上,才叫通。


2. 通信怎么走

应用 / 命令行工具
        │  open + ioctl(I2C_RDWR)     或者 read(sysfs)
        ▼
   /dev/i2c-1                         /sys/bus/iio/devices/iio:deviceN
        │                                    │
        ▼                                    ▼
   i2c-rk3x(i2c1,必须 100 kHz) ← 内核 mlx90614.c
        │  SDA / SCL
        ▼
   MLX90614 @ 0x5a
        ├─ RAM 0x06 Ta
        └─ RAM 0x07 Tobj1   LSB MSB PEC

两条路读的是同一组 RAM。IIO 通了用 IIO,省得自己拆 PEC。IIO 没 bind,就用户态组合读 3 字节。不要两条路同时改 EEPROM。

组合读 i2cRdReg:先写命令字节 0x07,Repeated Start 再读。不要对 0x07 做裸读——内部指针不一定停在物体温度。HDC1080 触发之后必须裸读,那是另一颗芯片的规矩,别混。

i2c1 上还有 0x10 / 0x40 / 0x57 / 0x73。总线允许共存。CAN0 和 i2c1 共脚 GPIO0_B3/B4,开 CAN 的镜像上 0x5a 从世界上消失,先确认 /dev/i2c-1 对应的适配器还活着。


3. 设备树和内核开关

板级文件:

device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
&i2c1 {
    pinctrl-0 = <&i2c1_xfer>;
    status = "okay";              /* 开 CAN0 时必须 disabled */
    clock-frequency = <100000>;   /* 禁止 400000 */

    mlx90614: mlx90614@5a {
        compatible = "melexis,mlx90614";
        reg = <0x5a>;
        status = "okay";
    };
};

compatible 必须对上 drivers/iio/temperature/mlx90614.c 的 of_match。CONFIG_MLX90614=y 写进 defconfig 三处:源码、out/ 里那份、板级 build_kernel.sh 合并段。只在 menuconfig 打开,下次重建会丢。

改 clock-frequency 是改 dts,不是改驱动。生效 dtb 在 resource 分区 的 rk-kernel.dtb,boot_linux 的 extlinux 里往往还有一份。两处都刷。只刷 Image、设备树还是 400 kHz,现象不变:1-005a 目录在,driver 软链没有。

内核驱动用 i2c_smbus_read_word_data 读 0x06 / 0x07,把 raw 暴露成:

in_temp_ambient_raw
in_temp_object_raw

IIO 的 scale 在这颗驱动里按毫开尔文处理过,板级实测 raw 仍是那份 16 位字,用户态乘 0.02 减 273.15 和手册一致。不要再乘一遍 sysfs 的 _scale 除非你核对过当前内核把 scale 写成了什么。现场以 raw 公式为准。


4. 当时那次 400 kHz,命令是这么走的

模块插在 i2c1 排针,3.3 V 有电。hdc:

hdc shell
ls -l /dev/i2c-1
# crw-rw-rw-  1 root root  89, 1 ... /dev/i2c-1

ls /sys/bus/i2c/devices/
# 1-0010  1-0040  1-0057  1-005a  i2c-0  i2c-1  i2c-2

1-005a 在。以为稳了。

ls -l /sys/bus/i2c/devices/1-005a/driver
# No such file or directory

cat /sys/bus/i2c/devices/1-005a/name
# mlx90614

name 来自 dts,不是 ACK。扫总线:

i2cdetect -y -r 1
# 没有 i2cdetect 就 /data/local/tmp/i2c_tool scan 1

400 kHz 下格子可能是 --,也可能时有时无。不要用一次 ACK 当结论。看生效速率:

find /proc/device-tree -name 'clock-frequency' | while read f; do
  echo "$f"
  hexdump -C "$f" | head -1
done

i2c1 那条若是 00 06 1a 80,就是 400 kHz。改 dts:

clock-frequency = <100000>;

删内核 checkpoint(改 board dts 不会自动让 GN 重跑内核动作),重打 boot_linux.img,刷 p5;resource.img 刷 p4。刷之前:

hdc shell "cat /proc/partitions"
hdc shell "mount | grep ' on / '"

SD 启动常见 mmcblk1,eMMC 是 mmcblk0。dd 必须 /dev/block/mmcblkXpY,不要写成 /dev/mmcblkXpY 在 tmpfs 里建一个假文件。

复位后再看:

hexdump -C /proc/device-tree/i2c@fe5a0000/clock-frequency
# 00 01 86 a0

ls -l /sys/bus/i2c/devices/1-005a/driver
# .../drivers/mlx90614

dmesg | grep -i mlx
# mlx90614 1-005a: ...

IIO 名字按名字找,不要写死 iio:device5:

for d in /sys/bus/iio/devices/iio:device*; do
  echo "$d  $(cat $d/name 2>/dev/null)"
done

找到 mlx90614 那颗:

BASE=...   # 上一步的路径
cat $BASE/in_temp_object_raw
cat $BASE/in_temp_ambient_raw

手算一遍。假设物体 raw = 15280:

15280 * 0.02 - 273.15 = 32.45 °C

对准手掌 1~3 cm 再 cat 一次,物体通道必须往上走;对准桌面,往回掉。环境通道几乎不动,它看的是芯片自身温度。

权限。hdc 是 root。HAP 读 sysfs 要 0666,放进 board-perm.sh 进 vendor。开发期:

chmod 0666 $BASE/in_temp_object_raw $BASE/in_temp_ambient_raw $BASE/name
chmod 0666 /dev/i2c-1

5. 用户态三字节:LSB MSB PEC

IIO 没 bind 时走 /dev/i2c-1。I2C 三件套里这里只用组合读。命令 0x07,长度 3。

uint8_t buf[3];
if (i2cRdReg(fd, 0x5a, 0x07, buf, 3) < 0)
    /* NACK:速率、供电、地址、CAN 抢脚 */
raw = buf[0] | (buf[1] << 8);   /* LSB 在前 */
pec = buf[2];
t = raw * 0.02 - 273.15;

PEC 计算(CRC-8,多项式 0x07,初值 0)覆盖 5 个字节:{0xB4, cmd, 0xB5, LSB, MSB}。0xB4 是 0x5a 左移加写位,0xB5 是读位。bring-up 阶段 PEC 错了先打印出来,不要直接当温度失败——有的适配器在读第 3 字节时已经发了 NAK,PEC 会脏。温度数字随手掌变化,PEC 以后再严。

不要用 I2C_SLAVE + read() 去读这颗。没有 Repeated Start,芯片会把命令字节理解错。I2C_RDWR 两条 msg:第一条写 cmd,第二条带 I2C_M_RD 读 3 字节。

读 0x06 环境温度,同一套,把 cmd 换成 0x06。


6. mlx90614_read.c

整文件可编。优先 IIO;加参数 i2c 走三字节;loop 每秒打一行,方便对准、移开。

/* mlx90614_read.c — OpenHarmony 4.1,32 位 musl
 *   mlx90614_read           # IIO,没有则 i2c-dev
 *   mlx90614_read i2c       # 强制 3 字节 RAM
 *   mlx90614_read loop      # 每秒一行
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>

#ifndef I2C_RDWR
#define I2C_RDWR 0x0707
#endif
#ifndef I2C_M_RD
#define I2C_M_RD 0x0001
#endif

#define ADDR        0x5a
#define CMD_TA      0x06
#define CMD_TOBJ1   0x07

struct i2c_msg {
    uint16_t addr;
    uint16_t flags;
    uint16_t len;
    uint8_t *buf;
};

struct i2c_rdwr_ioctl_data {
    struct i2c_msg *msgs;
    uint32_t nmsgs;
};

static int i2cWr(int fd, uint8_t addr, uint8_t reg, const uint8_t *data, int len)
{
    uint8_t b[32];
    if (len < 0 || len > 31)
        return -1;
    b[0] = reg;
    if (data && len > 0)
        memcpy(b + 1, data, (size_t)len);
    struct i2c_msg m = { addr, 0, (uint16_t)(len + 1), b };
    struct i2c_rdwr_ioctl_data d = { &m, 1 };
    return ioctl(fd, I2C_RDWR, &d);
}

static int i2cRdReg(int fd, uint8_t addr, uint8_t reg, uint8_t *data, int len)
{
    struct i2c_msg m[2] = {
        { addr, 0, 1, &reg },
        { addr, I2C_M_RD, (uint16_t)len, data },
    };
    struct i2c_rdwr_ioctl_data d = { m, 2 };
    return ioctl(fd, I2C_RDWR, &d);
}

static int i2cRdPlain(int fd, uint8_t addr, uint8_t *data, int len)
{
    struct i2c_msg m = { addr, I2C_M_RD, (uint16_t)len, data };
    struct i2c_rdwr_ioctl_data d = { &m, 1 };
    return ioctl(fd, I2C_RDWR, &d);
}

static uint8_t crc8(const uint8_t *p, int n)
{
    uint8_t crc = 0;
    int i, b;
    for (i = 0; i < n; i++) {
        crc ^= p[i];
        for (b = 0; b < 8; b++) {
            if (crc & 0x80)
                crc = (uint8_t)((crc << 1) ^ 0x07);
            else
                crc <<= 1;
        }
    }
    return crc;
}

static int pec_ok(uint8_t cmd, uint8_t lsb, uint8_t msb, uint8_t pec)
{
    uint8_t s[5] = { (uint8_t)(ADDR << 1), cmd, (uint8_t)((ADDR << 1) | 1), lsb, msb };
    return crc8(s, 5) == pec;
}

static double raw_to_c(unsigned raw)
{
    return raw * 0.02 - 273.15;
}

static int read_file_int(const char *path, int *out)
{
    char buf[64];
    int fd = open(path, O_RDONLY);
    if (fd < 0)
        return -1;
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    close(fd);
    if (n <= 0)
        return -1;
    buf[n] = 0;
    *out = atoi(buf);
    return 0;
}

static int find_iio(const char *want, char *out, size_t outlen)
{
    DIR *d = opendir("/sys/bus/iio/devices");
    struct dirent *e;
    if (!d)
        return -1;
    while ((e = readdir(d)) != NULL) {
        char namep[160], name[64];
        int fd;
        ssize_t n;
        char *nl;
        if (strncmp(e->d_name, "iio:device", 10) != 0)
            continue;
        snprintf(namep, sizeof(namep), "/sys/bus/iio/devices/%s/name", e->d_name);
        fd = open(namep, O_RDONLY);
        if (fd < 0)
            continue;
        n = read(fd, name, sizeof(name) - 1);
        close(fd);
        if (n <= 0)
            continue;
        name[n] = 0;
        nl = strchr(name, '\n');
        if (nl)
            *nl = 0;
        if (strstr(name, want)) {
            snprintf(out, outlen, "/sys/bus/iio/devices/%s", e->d_name);
            closedir(d);
            return 0;
        }
    }
    closedir(d);
    return -1;
}

static int read_iio_once(void)
{
    char base[128], p[160];
    int obj, amb;
    if (find_iio("mlx90614", base, sizeof(base)) < 0)
        return -1;
    snprintf(p, sizeof(p), "%s/in_temp_object_raw", base);
    if (read_file_int(p, &obj) < 0) {
        fprintf(stderr, "读 object 失败: %s (%s)\n", p, strerror(errno));
        return -1;
    }
    snprintf(p, sizeof(p), "%s/in_temp_ambient_raw", base);
    if (read_file_int(p, &amb) < 0)
        amb = -1;
    printf("IIO %s\n", base);
    printf("物体 raw=%d  %.2f °C\n", obj, raw_to_c((unsigned)obj));
    if (amb >= 0)
        printf("环境 raw=%d  %.2f °C\n", amb, raw_to_c((unsigned)amb));
    printf("对准手掌 1-3cm 再跑,物体通道必须升高。\n");
    return 0;
}

static int read_ram(int fd, uint8_t cmd, unsigned *raw, uint8_t *pec, int *pok)
{
    uint8_t buf[3];
    if (i2cRdReg(fd, ADDR, cmd, buf, 3) < 0)
        return -1;
    *raw = buf[0] | ((unsigned)buf[1] << 8);
    *pec = buf[2];
    *pok = pec_ok(cmd, buf[0], buf[1], buf[2]);
    return 0;
}

static int read_i2c_once(void)
{
    unsigned obj = 0, amb = 0;
    uint8_t pec_o = 0, pec_a = 0;
    int ok_o = 0, ok_a = 0;
    int fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open /dev/i2c-1: %s\n", strerror(errno));
        return -1;
    }
    /* i2cWr / i2cRdPlain 在这颗芯片上用不到,留着和工具链其它传感器共用 */
    (void)i2cWr;
    (void)i2cRdPlain;
    if (read_ram(fd, CMD_TOBJ1, &obj, &pec_o, &ok_o) < 0) {
        fprintf(stderr, "读 0x07 NACK(100kHz?模块?CAN 抢 i2c1?)\n");
        close(fd);
        return -1;
    }
    if (read_ram(fd, CMD_TA, &amb, &pec_a, &ok_a) < 0) {
        fprintf(stderr, "读 0x06 NACK\n");
        close(fd);
        return -1;
    }
    close(fd);
    if (obj == 0 || obj == 0xFFFF) {
        fprintf(stderr, "物体 raw=0x%04x,当总线失败,不换算\n", obj);
        return 1;
    }
    printf("i2c-dev 0x5a  RAM 0x07 LSB/MSB/PEC\n");
    printf("物体 raw=%u PEC=0x%02x %s  %.2f °C\n",
           obj, pec_o, ok_o ? "crc_ok" : "crc_skip_or_bad", raw_to_c(obj));
    printf("环境 raw=%u PEC=0x%02x %s  %.2f °C\n",
           amb, pec_a, ok_a ? "crc_ok" : "crc_skip_or_bad", raw_to_c(amb));
    return 0;
}

int main(int argc, char **argv)
{
    int force_i2c = 0, loop = 0;
    int i;
    for (i = 1; i < argc; i++) {
        if (strcmp(argv[i], "i2c") == 0)
            force_i2c = 1;
        if (strcmp(argv[i], "loop") == 0)
            loop = 1;
    }
    do {
        int rc;
        if (!force_i2c) {
            rc = read_iio_once();
            if (rc < 0) {
                fprintf(stderr, "IIO 没有 mlx90614,改走 i2c-dev\n");
                rc = read_i2c_once();
            }
        } else {
            rc = read_i2c_once();
        }
        if (!loop)
            return rc == 0 ? 0 : 1;
        sleep(1);
        printf("----\n");
    } while (loop);
    return 0;
}

编译必须 32 位 musl:

OHOS=<源码根>
CLANG=$OHOS/prebuilts/clang/ohos/linux-x86_64/llvm/bin/clang
SYSROOT=$OHOS/prebuilts/ohos-sdk/linux/11/native/sysroot
$CLANG --target=arm-linux-ohos --sysroot=$SYSROOT -O2 -Wall \
  -Wl,--dynamic-linker=/system/lib/ld-musl-arm.so.1 \
  -o mlx90614_read mlx90614_read.c
file mlx90614_read
# ELF 32-bit LSB executable, ARM, ... interpreter /system/lib/ld-musl-arm.so.1

hdc file send mlx90614_read /data/local/tmp/mlx90614_read
hdc shell chmod 0755 /data/local/tmp/mlx90614_read

现场:

hdc shell /data/local/tmp/mlx90614_read
# IIO /sys/bus/iio/devices/iio:device4
# 物体 raw=15120  29.25 °C
# 环境 raw=15090  28.65 °C

hdc shell /data/local/tmp/mlx90614_read i2c
# i2c-dev 0x5a  RAM 0x07 LSB/MSB/PEC
# 物体 raw=15118 PEC=0x3c crc_ok  29.21 °C

hdc shell /data/local/tmp/mlx90614_read loop

loop 开着,手掌伸到透镜前 2 cm,物体通道往 33~36 °C 走;拿开,掉回室温。环境通道几乎不动。两路数字贴在一起,多半对准距离不对,或者读成了同一通道。

IIO 和 i2c-dev 同一时刻的 raw 允许差几个字,芯片内部一直在刷新。差出一个数量级,查有没有把高低字节倒了:必须 LSB | (MSB<<8)。


7. ArkTS 按名字找 IIO

libboard.so 提供 findIio、readText。不要写死 iio:device4。

import board from 'libboard.so';

@Entry
@Component
struct IrTempPage {
  @State line: string = '对准手掌 1-3cm 再读。桌面和手掌必须是两个数。';

  private num(path: string): number {
    return parseFloat(board.readText(path).trim());
  }

  readMlx() {
    let base: string = board.findIio('mlx90614');
    if (base.length === 0) {
      this.line = '未找到 mlx90614。查 i2c1 100kHz、1-005a/driver、resource 是否刷了';
      return;
    }
    let obj: number = this.num(base + '/in_temp_object_raw');
    if (Number.isNaN(obj)) {
      this.line = '读 in_temp_object_raw 失败(chmod 0666?)';
      return;
    }
    let tObj: number = obj * 0.02 - 273.15;
    let amb: number = this.num(base + '/in_temp_ambient_raw');
    let tAmb: string = Number.isNaN(amb) ? '-' : (amb * 0.02 - 273.15).toFixed(1);
    this.line = '物体 ' + tObj.toFixed(1) + ' °C    环境 ' + tAmb + ' °C\n' + base;
  }

  build() {
    Column() {
      Text('红外测温 MLX90614  @0x5a').fontSize(18).margin({ bottom: 8 })
      Button('读一次').onClick(() => { this.readMlx(); })
      Text(this.line).margin({ top: 12 }).fontSize(16)
    }.padding(16)
  }
}

用户态三字节也可以从 NAPI 包一层 mlxRead(),内部就是 i2cRdReg(fd, 0x5a, 0x07, buf, 3)。IIO 已经 bind 时优先 sysfs,少一次和内核抢 0x5a。

HAP 不是 root。in_temp_*_raw 默认 0444 或 0640,应用读空。开机脚本 chmod 0666,进 vendor。只装 hap 不够。

透镜不是接触式热电偶。贴到 0 cm 读数乱跳,30 cm 对着房间就是室温。1~3 cm 正对皮肤才是设计用法。手掌对桌面来回切,数字必须跟着切。


9. 还踩过的坑

只刷 Image:/proc/device-tree 里 clock-frequency 仍是 00 06 1a 80,生效 dtb 在 resource。扫错总线:芯片在 i2c1,i2c0 是 RTC,i2c2 是触摸。CAN 镜像会 disabled i2c1。不要写死 iio:deviceN,用名字找。IIO 暴露的是 raw 字,不要再乘一次 0.02。总线上先出 LSB。必须组合读命令 0x07,裸读会读到环境温度。PEC 脏时先看手掌数字。HAP 要 chmod 0666。0xFFFF 当未连接,不要显示 1030 °C。EEPROM 别擦。模块 3.3 V。

验收:100 kHz;手掌与桌面两个数;拔模块 NACK。CONFIG_MLX90614=y 三处同步。刷 boot_linux 和 resource。


系列第 35 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐