测温会了。今天换指尖光电:估心率和血氧。必须指腹按紧,轻触没有有效信号。

第 33 章把 I2C 三条总线讲清楚了,第 34、35 章两颗芯片都能从 IIO 或用户态拿到数。这一章你会碰到一种更气人的情况:驱动绑上了,IIO 节点也在,读出来永远是空的。

芯片是 MAX30100,7 位地址 0x57,挂在 i2c1 上。它不是「写一个寄存器、读两个字节」那种温湿度计,而是一颗带 FIFO 的光电传感器:红外 LED 和红光 LED 轮流打进指腹,光电二极管把反射光变成 16 位样本,每秒一百次推进 FIFO。心率和血氧饱和度都是从这两路波形算出来的,不是芯片直接吐一个百分比。

内核里有现成的 IIO 驱动 drivers/iio/health/max30100.c,CONFIG_MAX30100=y 编进去就能 probe。它的设计假设是:INT 脚会跳,中断来了再去排空 FIFO。 板上这根 INT 如果没接到、接到了但电平不对、或者 GPIO 被别人占用,中断计数永远是 0,FIFO 在芯片里涨,驱动这边一个样本都不拿。你去读 in_temp_raw 得到 EAGAIN,读 IIO buffer 得到空,于是开始怀疑芯片坏了。

这章的结论先说在前面:别跟中断较劲。用户态打开 /dev/i2c-1,自己把 MODE 写成 SpO2,自己按写指针 / 读指针把 FIFO 排空。 中断那条路可以当加分项,主路径是轮询。指腹按紧 5 到 10 秒,IR / RED 数值会明显跳起来。轻触无效。


1. 这颗芯片在干什么

MAX30100 的封装上有一个小窗口。窗口下面是两颗 LED(红外、红光)和一颗光电二极管。指腹按上去以后:

  1. 红外 LED 亮一小段时间,二极管采一次反射,得到 IR。
  2. 红光 LED 再亮,得到 RED。
  3. 这一对 16 位值组成一条 FIFO 记录,4 字节:IR_H, IR_L, RED_H, RED_L。
  4. 默认我们把采样配成 100 Hz,也就是每 10 ms 一条。

血红蛋白对红光和红外的吸收不一样,含氧和不含氧又不一样。所以两路波形的交流分量之比,能估出血氧;IR 波形的峰间距能估心率。芯片自己不算这些,它只负责点灯和填 FIFO。算法在你的 C 里。

没有手指的时候,LED 打到空气里,IR / RED 往往是很小的数,或者在一个很低的平台上抖。手指按紧以后,数值会大一个数量级,并且能看见脉搏起伏。这就是本章的验收铁证:空气一组数,按紧一组数,两组对不上才叫通。


2. 通信怎么走

应用 / 命令行工具
        │  open + ioctl(I2C_RDWR)
        ▼
   /dev/i2c-1          ← CONFIG_I2C_CHARDEV=y
        │
        ▼
   i2c-rk3x 控制器(i2c1,100 kHz)
        │  SDA / SCL
        ▼
   MAX30100 @ 0x57
        │
        ├─ 0xFE PART_ID / 0xFF REV_ID
        ├─ 0x06 MODE(复位、关机、SpO2)
        ├─ 0x07 SPO2_CONFIG(100 Hz)
        ├─ 0x09 LED_CONFIG(电流)
        ├─ 0x01 INT_ENABLE
        ├─ 0x02 FIFO_WR_PTR
        ├─ 0x04 FIFO_RD_PTR
        └─ 0x05 FIFO_DATA(每次读 4 字节弹出一条)

读写方式用第 33 章的 组合读 i2cRdReg:先写寄存器地址,再 Repeated Start 读数据。FIFO 数据口 0x05 也是组合读,连读 4 字节。不要对 0x05 做「裸读」——指针没设对,读回来的就不是你以为的那条样本。

和 HDC1080(下一章)正好相反。那颗芯片触发之后禁止再写指针。这颗芯片每次读 FIFO 都必须先告诉它「我要 0x05」。

INT 脚在原理图上常常接到 GPIO2_D0(linux gpio 编号 80 一带),低电平有效。内核驱动靠它。用户态轮询 完全不看这根脚。所以 INT 没接好,不影响你这章把数读出来。


3. 设备树

板级文件:

device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts

i2c1 必须 100 kHz,第 35 章已经为 MLX90614 降过速,这颗芯片在 100 kHz 上很老实。节点:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    max30100: max30100@57 {
        compatible = "maxim,max30100";
        reg = <0x57>;
        interrupt-parent = <&gpio2>;
        interrupts = <RK_PD0 IRQ_TYPE_LEVEL_LOW>;
        status = "okay";
    };
};

compatible 对上内核 max30100.c 的 of_match。interrupts 写了 GPIO2_D0 低电平。写了不等于会来。 probe 成功只说明 I2C 地址 ACK、驱动把 IIO 设备注册了。中断请求是否真的能进 max30100_irq,要看:

  • 这根脚有没有被别的节点抢(显示、按键、别的模块);
  • 模块排针的 INT 有没有真的连到这颗 GPIO;
  • 上拉是不是 1.8 V 逻辑,SoC 这颗脚能不能认。

改完 DTS 要重打 boot_linux 和 resource(dtb 在 resource 分区才生效,第 11、12 章)。CONFIG_MAX30100=y 三处同步,第 14 章。

用户态主路径其实 不依赖这个节点 okay。节点 okay 会让内核驱动占着 0x57。多数内核仍然允许 i2c-dev 对同一地址发 I2C_RDWR,我们实测这条路通。若你发现用户态读写被拒,先把节点改 disabled 卸掉内核驱动,只留 i2c1 本身 okay。


4. 内核 IIO 这条死路,我带你走一遍

上电之后按习惯去找 IIO:

hdc shell
ls /sys/bus/iio/devices/
# iio:device0  iio:device1  ... 名字各板不同

for d in /sys/bus/iio/devices/iio:device*; do
    echo -n "$d  "
    cat $d/name
done

能看到一行 max30100。再看:

ls /sys/bus/i2c/devices/1-0057/driver
# .../drivers/max30100   ← 绑上了

cat /sys/bus/iio/devices/iio:deviceN/in_temp_raw
# 经常是 I/O error 或空

温度通道要先在 MODE 里打开 TEMP_EN,驱动用中断或轮询去取。血氧真正的数据不在 in_intensity_*_raw 这种简单文件里,而在 IIO buffer:使能 scan_elements、echo 1 > buffer/enable,再读 /dev/iio:deviceN。你按文档做完,读出来还是 0 字节。

这时候去看中断:

cat /proc/interrupts | grep -i max
# 次数恒 0

dmesg | grep -i max30100

max30100_irq 一次都没进。FIFO 在芯片侧写指针在走(你用下面的 C 去读 0x02 能看见),驱动侧因为没中断,根本不去 0x05。

有人会改驱动,把中断改成 hrtimer 轮询。能做,但要重编内核、还要处理和 IIO kfifo 的锁。用户态 80 行 C 就能把 FIFO 掏干净。IIO 当诊断用:能看见设备名说明 0x57 ACK;要数据,走 i2c-dev。


5. 寄存器地图(只记会用到的)

地址名字本章怎么用
0x00INT_STATUS可读可清,轮询路径可以不理
0x01INT_ENABLE写成 0xF0,把 FIFO/HR/SpO2 中断源打开;即便脚没接,寄存器还是建议按手册来
0x02FIFO_WR_PTR芯片写到哪
0x03FIFO_OVR_CTR溢出计数,非 0 说明你读太慢
0x04FIFO_RD_PTR你读到哪
0x05FIFO_DATA读 4 字节弹出一条
0x06MODEbit7 复位;bit1+bit0 = 0b11 为 SpO2;0x0b = SpO2+相关位
0x07SPO2_CONFIG0x47:高分辨率 + 100 Hz
0x09LED_CONFIG0x33:红/红外电流,过小波形没起伏,过大容易饱和
0xFEPART_ID有应答时不是 0xFF
0xFFREV_ID修订号

FIFO 深度 16 条。写指针减读指针(模 16 或模 32,看手册实现;下面 C 用模 32 再钳到 16)就是待取条数。一次最多取 16,避免在指针回绕时把旧数据当新的。

初始化顺序是实测能点亮 LED、能出样本的那一套,不要随便调换:

  1. 读 0xFE / 0xFF,确认不是 0xFF 0xFF。
  2. MODE=0x80 复位,等 50 ms。
  3. MODE=0x00 清复位。
  4. LED_CONFIG=0x33。
  5. SPO2_CONFIG=0x47。
  6. MODE=0x0b 进入 SpO2。这一步如果 NACK,后面全是空谈。
  7. INT_ENABLE=0xF0。
  8. 三个 FIFO 指针写 0。

然后每 10 ms 看一次写/读指针,把差值条数从 0x05 读走。


6. 完整 C:采样 1.2 秒打范围,可选 5 秒算心率血氧

把下面存成 max30100_tool.c。用户态 32 位,动态链接 musl。自己带了 i2c_msg 定义,不依赖 NDK 里有没有 linux/i2c-dev.h。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <math.h>
#include <sys/ioctl.h>

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

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;
};

#define ADDR 0x57
#define REG_INT_EN   0x01
#define REG_FIFO_WR  0x02
#define REG_FIFO_OVR 0x03
#define REG_FIFO_RD  0x04
#define REG_FIFO_DATA 0x05
#define REG_MODE     0x06
#define REG_SPO2     0x07
#define REG_LED      0x09
#define REG_PART     0xFE
#define REG_REV      0xFF

static int i2cWr(int fd, uint8_t addr, uint8_t reg, const uint8_t *data, int len)
{
    uint8_t b[24];
    int i;
    b[0] = reg;
    for (i = 0; i < len && i < 23; i++)
        b[1 + i] = data[i];
    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);
}

/* 组合读:S addr+W reg  Sr addr+R data... P */
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 wr1(int fd, uint8_t reg, uint8_t v)
{
    return i2cWr(fd, ADDR, reg, &v, 1);
}

static int rd1(int fd, uint8_t reg, uint8_t *v)
{
    return i2cRdReg(fd, ADDR, reg, v, 1);
}

static int chip_init(int fd, uint8_t *part, uint8_t *rev)
{
    uint8_t v;
    *part = 0xFF;
    *rev = 0xFF;
    i2cRdReg(fd, ADDR, REG_PART, part, 1);
    i2cRdReg(fd, ADDR, REG_REV, rev, 1);
    if (*part == 0xFF && *rev == 0xFF) {
        fprintf(stderr, "ERR: 0x57 无应答 (PART=FF REV=FF),查模块、i2c1、供电\n");
        return -1;
    }
    wr1(fd, REG_MODE, 0x80);          /* 复位 */
    usleep(50000);
    wr1(fd, REG_MODE, 0x00);
    wr1(fd, REG_LED, 0x33);           /* LED 电流 */
    wr1(fd, REG_SPO2, 0x47);          /* 100 Hz + 高分辨率 */
    if (wr1(fd, REG_MODE, 0x0b) < 0) { /* SpO2 模式 */
        fprintf(stderr, "ERR: 写 MODE=0x0b 被拒,0x57 中途不应答\n");
        return -1;
    }
    wr1(fd, REG_INT_EN, 0xF0);
    wr1(fd, REG_FIFO_WR, 0x00);
    wr1(fd, REG_FIFO_OVR, 0x00);
    wr1(fd, REG_FIFO_RD, 0x00);
    /* 读回 MODE,确认还是 0x0b 一类,不是全 FF */
    if (rd1(fd, REG_MODE, &v) < 0 || v == 0xFF) {
        fprintf(stderr, "ERR: 读 MODE 失败\n");
        return -1;
    }
    printf("PART=0x%02X REV=0x%02X MODE=0x%02X  init ok\n", *part, *rev, v);
    return 0;
}

/* 按指针差排空,最多 16 条。返回新追加的样本数 */
static int drain_fifo(int fd, unsigned *ir, unsigned *red, int cap, int *cnt)
{
    uint8_t wr = 0, rd = 0, buf[4];
    int n, k, added = 0;
    if (rd1(fd, REG_FIFO_WR, &wr) < 0)
        return -1;
    if (rd1(fd, REG_FIFO_RD, &rd) < 0)
        return -1;
    n = (wr - rd + 32) % 32;
    if (n > 16)
        n = 16;
    for (k = 0; k < n; k++) {
        if (i2cRdReg(fd, ADDR, REG_FIFO_DATA, buf, 4) < 0)
            break;
        if (*cnt >= cap)
            break;
        ir[*cnt] = ((unsigned)buf[0] << 8) | buf[1];
        red[*cnt] = ((unsigned)buf[2] << 8) | buf[3];
        if (ir[*cnt] == 0 && red[*cnt] == 0)
            continue;
        (*cnt)++;
        added++;
    }
    return added;
}

static int cmd_quick(int fd)
{
    unsigned ir[256], red[256];
    int cnt = 0, it;
    unsigned irMin = 65535, irMax = 0, redMin = 65535, redMax = 0;
    for (it = 0; it < 120; it++) {          /* ~1.2 s */
        usleep(10000);
        drain_fifo(fd, ir, red, 256, &cnt);
    }
    if (cnt == 0) {
        uint8_t wr = 0;
        rd1(fd, REG_FIFO_WR, &wr);
        printf("无采样 FIFO_WR=%u。指腹按紧再试;仍 0 则 LED/芯片/型号不对\n", wr);
        return 1;
    }
    for (it = 0; it < cnt; it++) {
        if (ir[it] < irMin) irMin = ir[it];
        if (ir[it] > irMax) irMax = ir[it];
        if (red[it] < redMin) redMin = red[it];
        if (red[it] > redMax) redMax = red[it];
    }
    printf("采样 %d  IR[%u~%u]  RED[%u~%u]\n", cnt, irMin, irMax, redMin, redMax);
    printf("空气里 IR 往往几百到一两千;按紧后应到数千以上,且 max-min 有起伏。\n");
    return 0;
}

static int cmd_spo2(int fd)
{
    unsigned ir[800], red[800];
    int cnt = 0, it, peaks = 0, refr = 0;
    double sumIr = 0, sumRed = 0, dcIr, dcRed, acIr = 0, acRed = 0;
    double R, secs;
    int hr, spo2, i;
    const int WIN = 25;

    printf("测量 5 秒:指腹按紧窗口,不要说话不要动。\n");
    for (it = 0; it < 500; it++) {
        usleep(10000);
        drain_fifo(fd, ir, red, 800, &cnt);
    }
    if (cnt < 100) {
        printf("采样不足 (%d)。请按紧,轻触无效。\n", cnt);
        return 1;
    }
    for (i = 0; i < cnt; i++) {
        sumIr += ir[i];
        sumRed += red[i];
    }
    dcIr = sumIr / cnt;
    dcRed = sumRed / cnt;
    for (i = 0; i < cnt; i++) {
        double di = ir[i] - dcIr, dr = red[i] - dcRed;
        acIr += di * di;
        acRed += dr * dr;
    }
    acIr = sqrt(acIr / cnt);
    acRed = sqrt(acRed / cnt);
    secs = (double)cnt / 100.0;
    if (dcIr < 1 || acIr / dcIr < 0.002) {
        printf("脉冲太弱 IR=%.0f±%.0f。按紧、遮环境光、手别冰。\n", dcIr, acIr);
        return 1;
    }
    for (i = WIN; i + 1 < cnt; i++) {
        double ma = 0, cur, prev;
        int k;
        for (k = -WIN; k <= WIN; k++)
            ma += (double)ir[i + k];
        ma /= (2 * WIN + 1);
        cur = (double)ir[i] - ma;
        prev = (double)ir[i - 1] - ma;
        if (refr > 0)
            refr--;
        if (cur > 0.5 * acIr && cur >= prev &&
            (double)ir[i + 1] <= (double)ir[i] && refr == 0) {
            peaks++;
            refr = 40;                 /* 0.4 s 不应期 */
        }
    }
    hr = (int)(60.0 * peaks / secs + 0.5);
    R = (acRed / dcRed) / (acIr / dcIr);
    spo2 = (int)(110.0 - 25.0 * R + 0.5);
    if (spo2 > 100)
        spo2 = 100;
    printf("采样 %d (%.1f 秒)  IR=%.0f±%.0f  RED=%.0f±%.0f  R=%.3f\n",
           cnt, secs, dcIr, acIr, dcRed, acRed, R);
    if (hr < 30 || hr > 220)
        printf("心率: 测不准 (峰 %d)\n", peaks);
    else
        printf("心率: %d 次/分\n", hr);
    if (R > 0.2 && R < 1.0 && spo2 >= 70)
        printf("血氧: %d %%  (参考 95~100,偏低请复测,不是医疗结论)\n", spo2);
    else
        printf("血氧: 本次 R 超经验范围,按紧再测\n");
    return 0;
}

int main(int argc, char **argv)
{
    const char *dev = "/dev/i2c-1";
    const char *cmd = "quick";
    uint8_t part = 0, rev = 0;
    int fd, rc;
    if (argc >= 2)
        cmd = argv[1];
    fd = open(dev, O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open %s: %s (chmod 0666 ?)\n", dev, strerror(errno));
        return 1;
    }
    if (chip_init(fd, &part, &rev) < 0) {
        close(fd);
        return 1;
    }
    if (strcmp(cmd, "spo2") == 0)
        rc = cmd_spo2(fd);
    else
        rc = cmd_quick(fd);
    close(fd);
    return rc;
}

编译、送上板、跑:

OHOS=$HOME/code/OpenHarmony          # 改成你的源码根
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 max30100_tool max30100_tool.c -lm

hdc file send max30100_tool /data/local/tmp/max30100_tool
hdc shell chmod 0755 /data/local/tmp/max30100_tool
hdc shell chmod 0666 /dev/i2c-1

先空测,再按手指:

hdc shell /data/local/tmp/max30100_tool quick
# 空气,示意:
# PART=0x11 REV=0x02 MODE=0x0b  init ok
# 采样 110  IR[120~400]  RED[80~260]

hdc shell /data/local/tmp/max30100_tool quick
# 指腹按紧:
# 采样 118  IR[8000~14000]  RED[6000~11000]

hdc shell /data/local/tmp/max30100_tool spo2
# 按紧 5 秒:
# 心率: 72 次/分
# 血氧: 98 %

两组 IR 范围对得上「空气 vs 按紧」,这一章的硬件通路就过了。心率血氧是经验公式,手指凉、按得死、环境光直射窗口,数字会飘。它是演示,不是监护仪。


7. 排障过程:从「FIFO 全 0」走到「按紧有起伏」

把下面当成一次真实对话。你按这个顺序做,不要跳去改驱动。

第一步:0x57 在不在。

hdc shell
i2cdetect -y -r 1

57 或 UU 都算芯片在。-- 空着:模块没插好、没供电、i2c1 被 CAN 镜像关掉了(第 33、58 章,CAN 和 i2c1 共脚)。先把总线救回来。

第二步:PART_ID。

hdc shell /data/local/tmp/max30100_tool quick

打印 PART=0xFF REV=0xFF:地址扫描也许扫到了残影,真正读写被拒。查 3.3 V、地、SDA/SCL 有没有接反。

第三步:写 MODE 被拒。

ERR: 写 MODE=0x0b 被拒 出现在 init 后半段。前面读 ID 成功、复位也写进去了,说明中途总线挂了,或者内核驱动同时在复位芯片。临时:

echo 1-0057 > /sys/bus/i2c/drivers/max30100/unbind

再跑工具。能通就把 DTS 节点改 disabled,避免每次开机抢。

第四步:有 ID、init ok、采样 0。

看打印的 FIFO_WR。若写指针也是 0,芯片没在产样本:MODE 没进 SpO2、LED 电流为 0、或者窗口被贴纸挡住。若写指针在走、读出来全 0,是 0x05 的组合读长度不对,确认 i2cRdReg(..., 4) 不是 1。

第五步:有数,但空气和按紧差不多。

典型是「轻触」。必须指腹把窗口盖严,压力中等,等 1 秒让 LED 稳定再采样。指甲、太冷、窗口上有胶,都会让交流分量没了。spo2 模式会明确说「脉冲太弱」。

第六步:数很大且不再变,max 等于 min。

饱和了。把 LED_CONFIG 从 0x33 改小,例如 0x22。或者手指按得把窗口压变形,光全反射。松一点。

第七步:以为自己在读 MAX30100,其实模块是 MAX30102。

30102 地址经常也是 0x57,FIFO 宽度和寄存器不完全一样。PART_ID 对不上手册时,别硬套 4 字节 IR/RED。换模块或换寄存器表。本章代码只对 MAX30100 负责。


8. 算法你要心里有数,别对外号称医疗级

spo2 子命令做了三件事。

直流:IR、RED 各自对时间取平均。它代表「窗口里有多少稳定反射」,手指按上去直流会升高。

交流:去掉直流之后的均方根。它代表脉搏起伏。交流比直流小于千分之二,当没脉搏。

比值:

R = (AC_red / DC_red) / (AC_ir / DC_ir)
SpO2 ≈ 110 - 25 * R

这是课本里那条经验直线,不是芯片标定曲线。R 落在 0.2~1.0 才信;算出超过 100 就钳到 100。低于 70 直接判失败,免得吓自己。

心率:IR 减 51 点滑动均值去掉呼吸和基线漂移,找超过 0.5 * AC 的局部峰,峰之间至少 0.4 秒。5 秒窗口里峰数乘 12 就是次/分。手抖会多峰,按死会没峰。

把这些限制写进应用的文案里。用户问「为什么和医院的差 2%」,你就指这段。


9. ArkTS 页面:开始测量,轮询结果

NAPI 不要在 UI 线程里采 5 秒。C 侧起一个线程,ArkTS 每秒问一次状态。模块名 libboard.so,和第 33 章同一份 so。

entry/src/main/cpp/types/libboard/index.d.ts 增加:

export const spo2Start: () => string;
export const spo2Result: () => string;
export const max30100Quick: () => string;

页面(节选,可直接放进一个 @Entry):

import board from 'libboard.so';

@Entry
@Component
struct Spo2Page {
  @State line: string = '指腹按紧窗口后点「测量 5 秒」';
  @State busy: boolean = false;
  private timer: number = -1;

  aboutToDisappear(): void {
    if (this.timer >= 0) {
      clearInterval(this.timer);
      this.timer = -1;
    }
  }

  build() {
    Column() {
      Text('MAX30100 血氧').fontSize(20).margin({ bottom: 8 })
      Text('地址 0x57,用户态轮询 FIFO,不靠 INT')
        .fontSize(14).fontColor('#666').margin({ bottom: 12 })
      Row() {
        Button(this.busy ? '测量中...' : '测量 5 秒')
          .enabled(!this.busy)
          .onClick(() => { this.start(); })
        Button('快速原始值').margin({ left: 8 })
          .enabled(!this.busy)
          .onClick(() => { this.line = board.max30100Quick(); })
      }
      Text(this.line)
        .fontSize(16)
        .margin({ top: 16 })
        .fontColor('#1565C0')
    }
    .padding(16)
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
  }

  start(): void {
    let r: string = board.spo2Start();
    if (r === 'BUSY') {
      this.line = '上一次还在采,等它结束';
      return;
    }
    this.busy = true;
    this.line = '测量中 0/5 秒,按紧、别动';
    let n: number = 0;
    this.timer = setInterval(() => {
      n++;
      let s: string = board.spo2Result();
      if (s === 'RUNNING') {
        this.line = '测量中 ' + n.toString() + '/5 秒,按紧、别动';
        return;
      }
      clearInterval(this.timer);
      this.timer = -1;
      this.busy = false;
      this.line = s;
    }, 1000);
  }
}

NAPI 侧把第 6 节的 chip_init / drain_fifo / cmd_spo2 挪进 so,用 std::thread 跑 cmd_spo2 那套,状态用原子变量:0 空闲、1 跑、2 完成、3 错。spo2Start 把状态从 0 换成 1 并 detach 线程,返回 "OK"。spo2Result 在 1 时返回 "RUNNING",在 2/3 时把字符串交出并回到 0。max30100Quick 走 1.2 秒那条,同步返回也可以,因为只卡一秒,调试按钮能忍。

权限:HAP 不是 root,/dev/i2c-1 必须开机 chmod 0666。放到 device/board/rk/rk3568_evb/ 下的放权脚本里,init 的 rk30board 配置拉起来。只改应用、不重打 vendor,板上会一直 open: Permission denied。


10. 和 IIO 共存时的权限、扫描

调试阶段用 hdc 的 root shell 跑 C 工具,不依赖放权。应用阶段:

hdc shell ls -l /dev/i2c-1
# 期望 crw-rw-rw-

hdc shell
for d in /sys/bus/iio/devices/iio:device*; do
    echo -n "$d "
    cat $d/name
done

name 是 max30100 只说明内核驱动在。应用不要 findIio("max30100") 再去读 raw,那是死路。扫描结果拿来做「模块在位」的绿勾可以,做波形不行。

若你坚持走 IIO buffer,先确认:

cat /proc/interrupts | grep -i max
# 按手指时计数要涨

不涨就停。不要在 ArkTS 里对空的 /dev/iio:deviceN 做 setInterval。


11. 坑,每一个都是真事

轻触当按紧。 窗口只有几毫米。指侧、指尖、悬空 1 毫米,IR 都像空气。验收口令是「指腹把窗口盖住,看到肉贴上去,再数 5 秒」。

空气和按紧的范围写进笔记。 不要只记「通了」。下次模块换批次,LED 电流差一档,你还能对比。

INT 脚当救命稻草。 原理图画了 INT,DTS 也写了,驱动还是空 FIFO。量 GPIO2_D0 在采样时有没有低脉冲。没有脉冲就轮询,不要为了「用上中断」卡两天内核。

MODE 位含义记混。 0x80 是复位不是 SpO2。只写 0x03 而不走 0x0b 那条实测序列,有的批次 LED 不亮。先照抄本章序列,再谈优化电流。

和红外测温同时摸。 同在 i2c1。总线本身允许共存。手却只有一只:测血氧时按窗口,测体温时要对准 1~3 cm,别指望一个动作两颗芯片都满意。

CAN 镜像。 打开 CAN 会 i2c1 disabled,0x57 从世界上消失。先确认 /sys/bus/i2c/devices/i2c-1 还在。

SELinux enforcing。 开发镜像 permissive。一旦 enforce,HAP 访问 /dev/i2c-1 会 403,chmod 也不够,要策略。交付前再收,现在别给自己加戏。

公式当真理。 算出 87% 先复测三次。三次都低,再看手指是不是按出白印(缺血)或窗口进了日光。


12. 验收

  1. i2cdetect -y -r 1 能看到 57 或 UU。
  2. max30100_tool quick 在空气和按紧下 IR 范围明显不同,按紧时 max-min 不是 0。
  3. max30100_tool spo2 按紧 5 秒,心率落在 50~120 才算一次合格演示;血氧 95~100 常见,偶尔一次失败允许重测。
  4. 应用点「测量 5 秒」期间 UI 能点别的按钮,不卡死。
  5. 拔掉模块再跑,工具必须打出 0x57 无应答,不许打印一组假的 98%。

第 5 条是防「上次缓冲还在」。每次 chip_init 都清 FIFO 指针。应用线程结束要 close(fd)。


本章小结

  • MAX30100 是 FIFO 光电传感器,不是「读一个 raw 就是血氧」的 IIO 简单通道。
  • 内核驱动绑得上,只说明地址 ACK。中断不来,IIO 永远空。用户态轮询 0x02/0x04/0x05 才是稳的。
  • 初始化按复位 → LED → 100 Hz → MODE 0x0b → 清指针。写 MODE 被拒就停,别往下算。
  • 验收看空气 vs 按紧的 IR/RED,不看第一次算出来的百分比。
  • 算法是演示用经验公式。按紧 5~10 秒,轻触无效。

路径清单

路径用途
`device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts``&i2c1` 下 `max30100@57`
`kernel/linux/linux-5.10/drivers/iio/health/max30100.c`内核 IIO,靠 INT
`kernel/linux/linux-5.10/Documentation/devicetree/bindings/iio/health/max30100.txt`绑定文档
defconfig 三处 `CONFIG_MAX30100=y`驱动编进内核
`/dev/i2c-1`用户态 I2C_RDWR
`/sys/bus/i2c/devices/1-0057/driver`看有没有绑 max30100
`/sys/bus/iio/devices/iio:deviceN/name`诊断用,不要当数据源
`/proc/interrupts`INT 来没来
`max30100_tool.c`(本章)命令行验收
`entry/src/main/cpp/` 里 `libboard.so`NAPI:`spo2Start` / `spo2Result` / `max30100Quick`
vendor 放权脚本`chmod 0666 /dev/i2c-1`

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

Logo

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

更多推荐