血氧心跳传感器MAX30100芯片驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
测温会了。今天换指尖光电:估心率和血氧。必须指腹按紧,轻触没有有效信号。
第 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(红外、红光)和一颗光电二极管。指腹按上去以后:
- 红外 LED 亮一小段时间,二极管采一次反射,得到 IR。
- 红光 LED 再亮,得到 RED。
- 这一对 16 位值组成一条 FIFO 记录,4 字节:
IR_H, IR_L, RED_H, RED_L。 - 默认我们把采样配成 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. 寄存器地图(只记会用到的)
| 地址 | 名字 | 本章怎么用 |
|---|---|---|
| 0x00 | INT_STATUS | 可读可清,轮询路径可以不理 |
| 0x01 | INT_ENABLE | 写成 0xF0,把 FIFO/HR/SpO2 中断源打开;即便脚没接,寄存器还是建议按手册来 |
| 0x02 | FIFO_WR_PTR | 芯片写到哪 |
| 0x03 | FIFO_OVR_CTR | 溢出计数,非 0 说明你读太慢 |
| 0x04 | FIFO_RD_PTR | 你读到哪 |
| 0x05 | FIFO_DATA | 读 4 字节弹出一条 |
| 0x06 | MODE | bit7 复位;bit1+bit0 = 0b11 为 SpO2;0x0b = SpO2+相关位 |
| 0x07 | SPO2_CONFIG | 0x47:高分辨率 + 100 Hz |
| 0x09 | LED_CONFIG | 0x33:红/红外电流,过小波形没起伏,过大容易饱和 |
| 0xFE | PART_ID | 有应答时不是 0xFF |
| 0xFF | REV_ID | 修订号 |
FIFO 深度 16 条。写指针减读指针(模 16 或模 32,看手册实现;下面 C 用模 32 再钳到 16)就是待取条数。一次最多取 16,避免在指针回绕时把旧数据当新的。
初始化顺序是实测能点亮 LED、能出样本的那一套,不要随便调换:
- 读 0xFE / 0xFF,确认不是 0xFF 0xFF。
- MODE=0x80 复位,等 50 ms。
- MODE=0x00 清复位。
- LED_CONFIG=0x33。
- SPO2_CONFIG=0x47。
- MODE=0x0b 进入 SpO2。这一步如果 NACK,后面全是空谈。
- INT_ENABLE=0xF0。
- 三个 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, ® },
{ 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. 验收
i2cdetect -y -r 1能看到 57 或 UU。max30100_tool quick在空气和按紧下 IR 范围明显不同,按紧时max-min不是 0。max30100_tool spo2按紧 5 秒,心率落在 50~120 才算一次合格演示;血氧 95~100 常见,偶尔一次失败允许重测。- 应用点「测量 5 秒」期间 UI 能点别的按钮,不卡死。
- 拔掉模块再跑,工具必须打出
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
更多推荐



所有评论(0)