温湿度的软件边界标过了。今天换手势芯片。常常没有内核节点,芯片在位不等于会认挥手,还要写初始化表。

现场就卡在这一步。i2cdetect -y -r 1 在 0x73 上有 ACK。写寄存器 0xEF = 0x00 选 bank0,读 0x00 得到 0x20,芯片在。然后把手伸到窗口前面左挥、右挥、上推、下压,读 0x43,回来永远是 0。

不是总线挂了。不是地址写错。是这颗芯片出厂后内部 DSP 处于「没喂过初始化表」的状态。手势引擎不跑,0x43 就不会置位。表有 219 条,按顺序写,中间还要切两次 bank。少写、写乱、写完停在 bank1,现象都是「在位,但不认手势」。

内核没有 PAJ7620 的 IIO 节点,也没有 input 设备。DTS 里通常不声明这颗从设备。整条路是用户态:/dev/i2c-1 + I2C_RDWR。和 MAX30100 那种「驱动绑上了却没数」不一样——这里连驱动都没有,不写表就什么都没有。

距离大约 15~20 cm,动作慢一点。贴着窗口甩,芯片看的是一团糊。


1. 总线上它长什么样

地址 0x73,挂在 i2c1,100 kHz。寄存器空间切成两个 bank,靠 0xEF 选择:

写 0xEF进入
0x00bank0。part id 在 `0x00`,手势位图在 `0x43`,挥手在 `0x44`
0x01bank1。初始化表后半段在这里

part id 低字节,bank0 的 0x00,必须是 0x20。读到别的值,要么没切 bank,要么根本不是这颗料。读失败(ioctl 返 -1,或数据 0xFF)就是 0x73 没有 ACK:模块没插、没上电、总线被 CAN 镜像关掉。

手势结果在 bank0 的 0x43,一个字节八个位:

bit手势
0右
1左
2上
3下
4前(靠近)
5后(远离)
6顺时针
7逆时针

0x44 的 bit0 是挥手。一次动作可能置多位,读完芯片会清,所以要轮询,间隔 50~100 ms 够用,400 ms 也行,再慢容易漏。

读写都用组合事务:写一个寄存器地址,Repeated Start 再读。和 HDC1080 的裸读不是一路。初始化是纯写:寄存器号 + 一个数据字节,219 次。

应用
 │  open("/dev/i2c-1") + ioctl I2C_RDWR
 ▼
i2c1 @ 100 kHz
 │
 ▼
PAJ7620 @ 0x73
 ├─ 0xEF  选 bank
 ├─ 0x00  part id(bank0,期望 0x20)
 ├─ 0x43  手势位图(bank0)
 └─ 0x44  挥手(bank0)

没有 /sys/bus/iio/devices/.../paj*。ls /sys/bus/i2c/devices/ 也常常看不到 1-0073,因为设备树没声明它。i2cdetect 能扫到,用户态就能打。不要去内核里找一份不存在的 paj7620.c。


2. 设备树:只需要 i2c1 活着

板级:

device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
&i2c1 {
   status = "okay";
   clock-frequency = <100000>;
   /* PAJ7620 @0x73 不声明节点。用户态直接打。 */
};

不要画蛇添足写一个 paj7620@73 却配不上任何内核 compatible——probe 失败会在 dmesg 里吵,对用户态没有帮助。若已经写了节点、驱动又找不到,i2cdetect 上显示 UU 或地址空洞,先把节点删掉或 disabled。

/dev/i2c-1 要 chmod 0666。HAP 不是 root,写不了 export,也打不开 0600 的 i2c 节点。放权脚本和 MAX30100、HDC1080 共用这一条。

CAN 和 i2c1 共脚(GPIO0_B3 / B4)。开 CAN 的镜像上 0x73 会从世界上消失,不是手势芯片坏了。


3. 当时那次「在位却没手势」

第一步,确认地址:

hdc shell
chmod 0666 /dev/i2c-1
i2cdetect -y -r 1

73 在。-- 就先查模块、3.3 V、SDA/SCL,别往下写表。

第二步,手写两条 ioctl 等价操作——下面 C 里的 probe 子命令:

写 0xEF = 0x00
读 0x00 → 0x20

得到 0x20,当时觉得完事了。ArkTS 里每 400 ms 读一次 0x43,挥手十分钟,日志全是 0x43=0x00。

第三步,对照手册才看到「Gesture 模式需要把初始化数组写进 bank0/bank1」。数组在参考代码里叫 initRegisterArray,219 项。不写,DSP 不进手势状态机。

第四步,把表原样写进去,最后再把 0xEF 写回 0x00。同一套轮询,左挥立刻出现 0x43=0x02。

漏掉最后那次切回 bank0,读 0x43 读的是 bank1 的无关寄存器,值乱七八糟或者恒定,不是手势。表的中段有 {0xEF,0x01},末尾附近还有一次切 bank。写完必须显式回 bank0。


4. 完整 C:探测、写 219 条、轮询手势

存成 paj7620_tool.c。用户态 32 位 musl,自带 i2c_msg,不依赖 NDK 里有没有 linux/i2c-dev.h。

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.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

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 0x73
#define REG_BANK 0xEF
#define REG_PART 0x00
#define REG_GES  0x43
#define REG_WAVE 0x44

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

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

/* 219 条。顺序就是状态机,不要排序、不要去重。 */
static const unsigned char PAJ_INIT[][2] = {
   {0xEF,0x00}, {0x32,0x29}, {0x33,0x01}, {0x34,0x00}, {0x35,0x01}, {0x36,0x00},
   {0x37,0x07}, {0x38,0x17}, {0x39,0x06}, {0x3A,0x12}, {0x3F,0x00}, {0x40,0x02},
   {0x41,0xFF}, {0x42,0x01}, {0x46,0x2D}, {0x47,0x0F}, {0x48,0x3C}, {0x49,0x00},
   {0x4A,0x1E}, {0x4B,0x00}, {0x4C,0x20}, {0x4D,0x00}, {0x4E,0x1A}, {0x4F,0x14},
   {0x50,0x00}, {0x51,0x10}, {0x52,0x00}, {0x5C,0x02}, {0x5D,0x00}, {0x5E,0x10},
   {0x5F,0x3F}, {0x60,0x27}, {0x61,0x28}, {0x62,0x00}, {0x63,0x03}, {0x64,0xF7},
   {0x65,0x03}, {0x66,0xD9}, {0x67,0x03}, {0x68,0x01}, {0x69,0xC8}, {0x6A,0x40},
   {0x6D,0x04}, {0x6E,0x00}, {0x6F,0x00}, {0x70,0x80}, {0x71,0x00}, {0x72,0x00},
   {0x73,0x00}, {0x74,0xF0}, {0x75,0x00}, {0x80,0x42}, {0x81,0x44}, {0x82,0x04},
   {0x83,0x20}, {0x84,0x20}, {0x85,0x00}, {0x86,0x10}, {0x87,0x00}, {0x88,0x05},
   {0x89,0x18}, {0x8A,0x10}, {0x8B,0x01}, {0x8C,0x37}, {0x8D,0x00}, {0x8E,0xF0},
   {0x8F,0x81}, {0x90,0x06}, {0x91,0x06}, {0x92,0x1E}, {0x93,0x0D}, {0x94,0x0A},
   {0x95,0x0A}, {0x96,0x0C}, {0x97,0x05}, {0x98,0x0A}, {0x99,0x41}, {0x9A,0x14},
   {0x9B,0x0A}, {0x9C,0x3F}, {0x9D,0x33}, {0x9E,0xAE}, {0x9F,0xF9}, {0xA0,0x48},
   {0xA1,0x13}, {0xA2,0x10}, {0xA3,0x08}, {0xA4,0x30}, {0xA5,0x19}, {0xA6,0x10},
   {0xA7,0x08}, {0xA8,0x24}, {0xA9,0x04}, {0xAA,0x1E}, {0xAB,0x1E}, {0xCC,0x19},
   {0xCD,0x0B}, {0xCE,0x13}, {0xCF,0x64}, {0xD0,0x21}, {0xD1,0x0F}, {0xD2,0x88},
   {0xE0,0x01}, {0xE1,0x04}, {0xE2,0x41}, {0xE3,0xD6}, {0xE4,0x00}, {0xE5,0x0C},
   {0xE6,0x0A}, {0xE7,0x00}, {0xE8,0x00}, {0xE9,0x00}, {0xEE,0x07}, {0xEF,0x01},
   {0x00,0x1E}, {0x01,0x1E}, {0x02,0x0F}, {0x03,0x10}, {0x04,0x02}, {0x05,0x00},
   {0x06,0xB0}, {0x07,0x04}, {0x08,0x0D}, {0x09,0x0E}, {0x0A,0x9C}, {0x0B,0x04},
   {0x0C,0x05}, {0x0D,0x0F}, {0x0E,0x02}, {0x0F,0x12}, {0x10,0x02}, {0x11,0x02},
   {0x12,0x00}, {0x13,0x01}, {0x14,0x05}, {0x15,0x07}, {0x16,0x05}, {0x17,0x07},
   {0x18,0x01}, {0x19,0x04}, {0x1A,0x05}, {0x1B,0x0C}, {0x1C,0x2A}, {0x1D,0x01},
   {0x1E,0x00}, {0x21,0x00}, {0x22,0x00}, {0x23,0x00}, {0x25,0x01}, {0x26,0x00},
   {0x27,0x39}, {0x28,0x7F}, {0x29,0x08}, {0x30,0x03}, {0x31,0x00}, {0x32,0x1A},
   {0x33,0x1A}, {0x34,0x07}, {0x35,0x07}, {0x36,0x01}, {0x37,0xFF}, {0x38,0x36},
   {0x39,0x07}, {0x3A,0x00}, {0x3E,0xFF}, {0x3F,0x00}, {0x40,0x77}, {0x41,0x40},
   {0x42,0x00}, {0x43,0x30}, {0x44,0xA0}, {0x45,0x5C}, {0x46,0x00}, {0x47,0x00},
   {0x48,0x58}, {0x4A,0x1E}, {0x4B,0x1E}, {0x4C,0x00}, {0x4D,0x00}, {0x4E,0xA0},
   {0x4F,0x80}, {0x50,0x00}, {0x51,0x00}, {0x52,0x00}, {0x53,0x00}, {0x54,0x00},
   {0x57,0x80}, {0x59,0x10}, {0x5A,0x08}, {0x5B,0x94}, {0x5C,0xE8}, {0x5D,0x08},
   {0x5E,0x3D}, {0x5F,0x99}, {0x60,0x45}, {0x61,0x40}, {0x63,0x2D}, {0x64,0x02},
   {0x65,0x96}, {0x66,0x00}, {0x67,0x97}, {0x68,0x01}, {0x69,0xCD}, {0x6A,0x01},
   {0x6B,0xB0}, {0x6C,0x04}, {0x6D,0x2C}, {0x6E,0x01}, {0x6F,0x32}, {0x71,0x00},
   {0x72,0x01}, {0x73,0x35}, {0x74,0x00}, {0x75,0x33}, {0x76,0x31}, {0x77,0x01},
   {0x7C,0x84}, {0x7D,0x03}, {0x7E,0x01},
};

static int probe(int fd, uint8_t *id)
{
   uint8_t zero = 0x00;
   *id = 0xFF;
   if (wr1(fd, REG_BANK, zero) < 0)
       return -1;
   if (rd1(fd, REG_PART, id) < 0)
       return -1;
   return 0;
}

static int load_init(int fd)
{
   unsigned i, n = (unsigned)(sizeof(PAJ_INIT) / sizeof(PAJ_INIT[0]));
   uint8_t v;
   for (i = 0; i < n; i++) {
       v = PAJ_INIT[i][1];
       if (i2cWr(fd, ADDR, PAJ_INIT[i][0], &v, 1) < 0) {
           fprintf(stderr, "ERR: init[%u] 写 0x%02X 失败(共 %u 条)\n",
                   i, PAJ_INIT[i][0], n);
           return -1;
       }
   }
   if (wr1(fd, REG_BANK, 0x00) < 0) {
       fprintf(stderr, "ERR: 切回 bank0 失败\n");
       return -1;
   }
   printf("init %u 条已写,bank0\n", n);
   return 0;
}

static void decode(uint8_t g, uint8_t w, char *out, int cap)
{
   int n = 0;
   out[0] = 0;
   if (g & 0x01) n += snprintf(out + n, cap - n, "右 ");
   if (g & 0x02) n += snprintf(out + n, cap - n, "左 ");
   if (g & 0x04) n += snprintf(out + n, cap - n, "上 ");
   if (g & 0x08) n += snprintf(out + n, cap - n, "下 ");
   if (g & 0x10) n += snprintf(out + n, cap - n, "前 ");
   if (g & 0x20) n += snprintf(out + n, cap - n, "后 ");
   if (g & 0x40) n += snprintf(out + n, cap - n, "顺时针 ");
   if (g & 0x80) n += snprintf(out + n, cap - n, "逆时针 ");
   if (w & 0x01) n += snprintf(out + n, cap - n, "挥手 ");
   if (n == 0)
       snprintf(out, cap, "(无)");
}

static int cmd_probe(int fd)
{
   uint8_t id = 0;
   if (probe(fd, &id) < 0) {
       fprintf(stderr, "ERR: 0x73 无应答,查 i2c1 / 模块 / 供电\n");
       return 1;
   }
   if (id != 0x20) {
       fprintf(stderr, "ERR: part id=0x%02X,期望 0x20(没切 bank0?料号不对?)\n", id);
       return 2;
   }
   printf("part id=0x%02X  在位\n", id);
   return 0;
}

static int cmd_init(int fd)
{
   uint8_t id = 0;
   int rc = cmd_probe(fd);
   if (rc != 0)
       return rc;
   if (load_init(fd) < 0)
       return 1;
   if (probe(fd, &id) < 0 || id != 0x20) {
       fprintf(stderr, "ERR: 写表之后 part id 丢了\n");
       return 1;
   }
   return 0;
}

static int cmd_poll(int fd, int seconds)
{
   uint8_t g, w;
   char buf[80];
   int i, n = seconds * 10;
   if (wr1(fd, REG_BANK, 0x00) < 0) {
       fprintf(stderr, "ERR: 选 bank0 失败,先跑 init\n");
       return 1;
   }
   printf("轮询 %d 秒,窗口上方 15~20cm 慢速挥手\n", seconds);
   for (i = 0; i < n; i++) {
       g = 0;
       w = 0;
       if (rd1(fd, REG_GES, &g) < 0) {
           fprintf(stderr, "ERR: 读 0x43 失败\n");
           return 1;
       }
       rd1(fd, REG_WAVE, &w);
       if (g != 0 || (w & 0x01)) {
           decode(g, w, buf, (int)sizeof(buf));
           printf("0x43=0x%02X 0x44=0x%02X  %s\n", g, w, buf);
       }
       usleep(100000);
   }
   return 0;
}

static void usage(const char *argv0)
{
   fprintf(stderr,
       "用法: %s probe | init | poll [秒]\n"
       "  probe  读 part id,期望 0x20\n"
       "  init   写 219 条并切回 bank0\n"
       "  poll   先 init 再轮询手势,默认 20 秒\n",
       argv0);
}

int main(int argc, char **argv)
{
   const char *dev = "/dev/i2c-1";
   const char *cmd = "probe";
   int fd, sec = 20;
   if (argc >= 2)
       cmd = argv[1];
   if (argc >= 3)
       sec = atoi(argv[2]);
   if (sec < 1)
       sec = 20;

   fd = open(dev, O_RDWR);
   if (fd < 0) {
       fprintf(stderr, "open %s: %s (chmod 0666?)\n", dev, strerror(errno));
       return 1;
   }
   if (strcmp(cmd, "probe") == 0) {
       int rc = cmd_probe(fd);
       close(fd);
       return rc;
   }
   if (strcmp(cmd, "init") == 0) {
       int rc = cmd_init(fd);
       close(fd);
       return rc;
   }
   if (strcmp(cmd, "poll") == 0) {
       int rc = cmd_init(fd);
       if (rc == 0)
           rc = cmd_poll(fd, sec);
       close(fd);
       return rc;
   }
   usage(argv[0]);
   close(fd);
   return 1;
}

上面用了 atoi,补一行头文件:

#include <stdlib.h>

放进 #include <stdio.h> 那一组。漏了会警告,部分工具链当错误。

编译、送上板:

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 paj7620_tool paj7620_tool.c

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

先探测,再写表,再挥:

hdc shell /data/local/tmp/paj7620_tool probe
# part id=0x20  在位

hdc shell /data/local/tmp/paj7620_tool init
# init 219 条已写,bank0

hdc shell /data/local/tmp/paj7620_tool poll 20
# 轮询 20 秒,窗口上方 15~20cm 慢速挥手
# 0x43=0x02 0x44=0x00  左
# 0x43=0x01 0x44=0x00  右
# 0x43=0x04 0x44=0x00  上

只跑 probe、不跑 init,poll 里读 0x43 会一直安静。这就是开头那个现场。poll 子命令内部会先 init 一次,避免「以为在测手势,其实表没写」。

sizeof(PAJ_INIT)/sizeof(PAJ_INIT[0]) 必须是 219。改表的时候用:

grep -o '{0x' paj7620_tool.c | wc -l

工具源码里除了表还有几处 { addr, 结构体,数字会略大于 219,大致对上即可。真正准的是 C 里那句 printf("init %u 条。


5. 表为什么不能「精简」

219 条看起来像魔法。里面混着增益、阈值、手势窗口、DSP 时钟,以及两次 0xEF 切 bank。没有一份公开的「最小 20 条就能用手势」的裁剪表在这块产品上验证过。

现场试过三种自作聪明,全失败:

只写 bank0 前 40 条。part id 还在,0x43 仍 0。后半段在 bank1,手势状态机的参数在那里。

按寄存器地址排序再写。表里同一个地址出现多次,后写覆盖前写,而且中间夹着切 bank。排序等于打乱时序。

写完不切回 bank0。随后读 0x43 读到 bank1 的 0x43,值是初始化表里写进去的 0x30,于是应用解码出「上+右」的鬼手势,没有人动。

所以表是整块搬运,写完 0xEF=0x00。不要在生产固件里对它做「优化」。

上电后表在芯片 RAM 里,掉电丢失。每次进程起来都要写一遍。应用 aboutToAppear 调一次 init,不要每个手势读之前写 219 条,I2C 会卡 100 ms 量级,轮询就抖。


6. 动作怎么做,芯片才置位

窗口朝上或朝外,看模块丝印。手掌张开,在窗口上方 15~20 cm,一次一个动作,做完停半秒。

左 / 右:手掌平面平行窗口,横向扫过。太快变成挥手(0x44),太贴变成「前」。

上 / 下:同样距离,纵向扫。

前 / 后:手掌正对窗口,靠近或拉远,不要横着切。

顺时针 / 逆时针:在窗口上方画圈,半径大约一个手掌,慢。画得小,DSP 当噪声丢掉。

日光灯正对窗口、或者旁边有红外测温的 LED 在闪,误触发会多。用手遮一下环境光再试。

一次读到多 bit 是正常的。解码按位拼字符串,不要 switch 只认单 bit,否则「右上」会被丢掉。


7. ArkTS:先 init,再连续读

libboard.so 三个接口:

export const paj7620Init: () => number;     // 0 成功 / -1 无应答 / -2 part id 不是 0x20
export const i2cWriteReg: (bus: number, addr: number, reg: number, val: number) => number;
export const i2cReadReg: (bus: number, addr: number, reg: number) => number;  // 0..255 或 -1

NAPI 的 paj7620Init 就是 C 里的 cmd_init:probe + 写表 + 回 bank0。i2cWriteReg / i2cReadReg 走同一套 i2cWr / i2cRdReg,bus 传 1,addr 传 0x73。

import board from 'libboard.so';

@Entry
@Component
struct PajPage {
 @State line: string = '先「初始化」,再「连续读」,窗口上方 15~20cm 慢挥';
 @State ready: boolean = false;
 private timer: number = -1;

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

 build() {
   Column() {
     Text('PAJ7620 手势').fontSize(20).margin({ bottom: 8 })
     Text('i2c1 地址 0x73,无内核节点。不写 219 条则 0x43 恒 0。')
       .fontSize(14).fontColor('#666').margin({ bottom: 12 })
     Row() {
       Button('在位自检').onClick(() => { this.probe(); })
       Button('初始化').margin({ left: 8 }).onClick(() => { this.init(); })
       Button(this.timer >= 0 ? '停止' : '连续读')
         .margin({ left: 8 })
         .onClick(() => { this.toggle(); })
     }
     Text(this.line).fontSize(16).margin({ top: 16 }).fontColor('#1565C0')
   }
   .padding(16)
   .width('100%')
   .alignItems(HorizontalAlign.Start)
 }

 probe(): void {
   board.i2cWriteReg(1, 0x73, 0xEF, 0x00);
   let id: number = board.i2cReadReg(1, 0x73, 0x00);
   if (id < 0) {
     this.line = '0x73 无应答(模块、i2c1、chmod 0666)';
     this.ready = false;
     return;
   }
   this.line = 'part id=0x' + id.toString(16) + (id === 0x20 ? ' 在位' : ' 不是 0x20');
 }

 init(): void {
   let r: number = board.paj7620Init();
   if (r === 0) {
     this.ready = true;
     this.line = '219 条已写。连续读,慢挥。';
   } else if (r === -1) {
     this.ready = false;
     this.line = '无应答';
   } else if (r === -2) {
     this.ready = false;
     this.line = 'part id 不是 0x20';
   } else {
     this.line = 'init 返回 ' + r.toString();
   }
 }

 readOnce(): void {
   board.i2cWriteReg(1, 0x73, 0xEF, 0x00);
   let g: number = board.i2cReadReg(1, 0x73, 0x43);
   let w: number = board.i2cReadReg(1, 0x73, 0x44);
   if (g < 0) {
     this.line = '读 0x43 失败,先初始化';
     return;
   }
   let s: string = '';
   if (g & 0x01) s += '右 ';
   if (g & 0x02) s += '左 ';
   if (g & 0x04) s += '上 ';
   if (g & 0x08) s += '下 ';
   if (g & 0x10) s += '前 ';
   if (g & 0x20) s += '后 ';
   if (g & 0x40) s += '顺时针 ';
   if (g & 0x80) s += '逆时针 ';
   if (w > 0 && (w & 0x01)) s += '挥手 ';
   if (s.length === 0) {
     this.line = '无手势 0x43=0' + (this.ready ? '' : '  (表还没写?)');
   } else {
     this.line = s + '  (0x43=0x' + g.toString(16) + ')';
   }
 }

 toggle(): void {
   if (this.timer >= 0) {
     clearInterval(this.timer);
     this.timer = -1;
     return;
   }
   this.readOnce();
   this.timer = setInterval(() => { this.readOnce(); }, 200);
 }
}

连续读走 setInterval 200 ms。i2cReadReg 是同步 ioctl,单次几十毫秒,UI 扛得住。不要把 219 条 init 放进 interval。

NAPI 里 paj7620Init 的返回值约定写死在 index.d.ts 旁边的注释里:0 / -1 / -2。ArkTS 不要把 -1 当无符号 4294967295,napi_create_int32 即可。


8. 和 PIR 不要搅在一块

同一块模块上经常还有人体感应,那是一根 GPIO,linux 编号实测是 73(GPIO2_B1),不是 74。74 拔掉模块仍恒 1,悬空上拉。手势是 0x73 这个 I2C 地址。一个数字 73、一个地址 0x73,现场口头容易说乱。

测手势时 PIR 的电平会跟着人晃,那是另一条路径,别在手势页里读 GPIO。测 PIR 时也不要抱怨 0x43 是 0——表没写,手势页本来就该是 0。

HAP 读 GPIO 同样要开机预 export。那是另一篇的事。这里只保证 /dev/i2c-1 的 0666。


9. 排障

probe 得 0xFF:先看 i2cdetect 有没有 73,再看 1-0073/driver 是否被占。poll 无手势:确认 219 条写完、距离 15~20 cm、动作慢。恒定 0x30:停在 bank1。应用读失败:工具和应用不要共用一个 fd;0666 在 reload 后要重做。i2c_msg 自己定义,不要拿 64 位主机头文件。表放 C 里,不要 JSON 一条条从 ArkTS 写。

过关:part id=0x20;不 init 时 0x43 恒 0;init 后左/右/上能置位;拔模块必须失败。/dev/i2c-1 chmod 0666。


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

Logo

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

更多推荐