板载这些先告一段落。从今天起换插在排针上的模块。先把 I2C 讲透:两条线、一个地址、一次应答。树上有节点,不代表芯片答应过。

有人第一次摸这块板上的传感器,开口就问:「串口波特率多少?」
我把键盘推过去:termios / 8250 是第 41 章 UART 的事,这章是 I2C。两条线,一个地址,一次应答。你要是拿着串口助手去扫 0x5a,扫到天黑也不会亮。

这章把三条 I2C 总线摊开,把 I2C_RDWR 三件套写完,再带你走一遍「设备树里有节点、芯片却不应答」的现场。后面环境光、红外测温、血氧、温湿度、手势,全靠这章把总线打穿。


1. 先把 UART 的手放下

UART 是点对点:TX、RX、波特率、8N1。I2C 是总线:一根时钟 SCL、一根数据 SDA,总线上每个从设备一个 7 位地址。主机发起,从机 ACK 或 NACK。没有起始位停止位这一套,也没有 1500000 这种调试口神话。

物理上就这样:

  主机 RK3568                  从机 A 0x10        从机 B 0x5a
  ┌──────────┐                 ┌─────────┐        ┌─────────┐
  │  SCL ────┼──── 4.7k 上拉 ──┼── SCL   │        │  SCL    │
  │  SDA ────┼──── 4.7k 上拉 ──┼── SDA   │────────│  SDA    │
  │          │     到 3.3 V    └─────────┘        └─────────┘
  └──────────┘

SCL、SDA 都是开漏。芯片自己只能把线拉低,拉高全靠电阻。上拉没焊、电阻太大、线太长,波形会圆,ACK 会丢。示波器量这两根,比在 ArkTS 里加 log 快。

官方把连线画成了这一张,先看完再往下:

图上主机、从机、SDA、SCL、电源、地,缺一根都别往软件上猜。模块没插紧、排针松了、3.3 V 没供上,后面所有 ioctl 都是 NACK。

软件侧官方另有一张流程:

把它翻译成人话:

  1. 打开总线设备,本产品用户态就是 /dev/i2c-0/dev/i2c-1/dev/i2c-2
  2. 组消息:写寄存器、读寄存器、或者只读不写。
  3. 一次 transfer:内核把消息变成 SCL 上的起止条件和 ACK。
  4. 关闭。应用层不要自己 bit-bang GPIO,除非你在做 SPI 那种位拍,I2C 别作死。

HDF 也有一套 I2C 平台驱动,触摸、部分传感器走那条。本章走的是 内核 i2c-dev + 用户态 ioctl。外设模型那层第 13 章讲过:同一颗芯片不要内核驱动和 HDF 同时 bind。


2. 这块产品三条总线,别扫错号

RK3568 有多路 I2C 控制器。板级设备树把其中三路 okay 了。产品代号 rk3568_evb,文件:

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

你仓库目录名不同,全局当成「板级 dts」理解。

总线设备节点速率典型从设备
i2c0`/dev/i2c-0`100 kHzRTC DS1307 @0x68;CPU 供电芯片 @0x40
i2c1`/dev/i2c-1`**100 kHz**环境光 0x10、温湿度 0x40、血氧 0x57、红外测温 0x5a、手势 0x73
i2c2`/dev/i2c-2`100 kHz触摸 GT911 @0x5d / FT5406 @0x38、相机 OV4689 @0x36

总线号以运行时 /dev/i2c-* 为准。别假设「原理图第一路就是 i2c0」——控制器编号跟 pinctrl 组有关,以 dts 里 &i2cN 为准。

i2c1 必须 100 kHz。 红外测温芯片走 SMBus,规格书上限就是 100 kHz。有人照着公版 dts 写成 400000,读配置寄存器静默失败,驱动不 bind,sysfs 里空空如也。第 35 章会把这个案子拆开。同总线上的其它传感器 100 kHz 也稳,别为了「快一点」去冒险。

i2c1 和 CAN0 共脚。 i2c1_xfer 是 GPIO0_B3 / GPIO0_B4。CAN0 的 TX/RX 也是这两根。设备树里 &can0&i2c1 只能有一个 status = "okay"。默认交付开 i2c1、关 can0。你要测 CAN,第 58 章会出另一份 dtb,传感器整组消失是预期,不是总线坏了。

i2c2 负载大:触摸 FPC、相机分支都挂上面。400 kHz 下触摸会整片 NACK。跟 Linux 公版一样,锁 100 kHz。


3. 设备树怎么写

把板级 dts 里三段抄过来。你改速率、改 status、加节点,都在这里。改完重打 boot_linux,同时确认 resource 分区那份 dtb 也更新(第 11、12 章:生效 DTB 在 resource 的 rk-kernel.dtb,只刷 Image 不够)。

/* I2C0:RTC */
&i2c0 {
    status = "okay";
    clock-frequency = <100000>;

    rk8600: rk8600@40 {
        compatible = "rockchip,rk860x";
        reg = <0x40>;
        /* 电源节点,省略 regulator 子属性 */
    };

    ds1307: rtc@68 {
        compatible = "dallas,ds1307";
        reg = <0x68>;
    };
};

/* I2C1:传感器。MLX90614 SMBus 上限 100 kHz,禁止 400000 */
&i2c1 {
    pinctrl-0 = <&i2c1_xfer>;
    status = "okay";    /* 开 CAN0 时必须改 disabled,共脚 GPIO0_B3/B4 */
    clock-frequency = <100000>;

    aht20@38 {
        compatible = "aosong,aht20";
        reg = <0x38>;
        status = "disabled";   /* 给 HDF 留的同类芯片,内核先别抢 */
    };

    veml6040: veml6040@10 {
        compatible = "vishay,veml6040";
        reg = <0x10>;
        status = "okay";
    };

    hdc1080: hdc1080@40 {
        compatible = "ti,hdc1080";
        reg = <0x40>;
        status = "okay";
    };

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

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

/* I2C2:触摸 + 相机。负载大,100 kHz */
&i2c2 {
    status = "okay";
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&i2c2m1_xfer>;

    gt911@5d {
        compatible = "goodix,gt911";
        reg = <0x5d>;
        status = "disabled";   /* HDF 接管触摸时,内核 goodix 必须 disabled */
    };

    ov4689: ov4689@36 {
        compatible = "ovti,ov4689";
        reg = <0x36>;
        status = "okay";
    };
};

compatible 必须跟驱动 of_device_id 对得上。写了节点只说明 内核会创建 i2c_client,不说明芯片 ACK 了。这是本章最值钱的一句话,下一节用命令钉死。

还要内核把字符设备露出来:

CONFIG_I2C=y
CONFIG_I2C_CHARDEV=y

三处同步:源码 defconfig、out/ 里那份、板级 build_kernel.sh 的合并段。缺一处,下次重建 /dev/i2c-* 蒸发。第 14 章讲过这个习惯。


4. 设备树出现,不等于芯片 ACK

上板,hdc 进去:

hdc shell
ls -l /dev/i2c-*
ls /sys/bus/i2c/devices/

期望类似:

crw-rw-rw- 1 root root 89, 0 ... /dev/i2c-0
crw-rw-rw- 1 root root 89, 1 ... /dev/i2c-1
crw-rw-rw- 1 root root 89, 2 ... /dev/i2c-2

1-0010  1-0040  1-0057  1-005a  2-0036  2-005d  i2c-0  i2c-1  i2c-2

1-005a适配器 1、地址 0x5a 的 client。它的存在只证明 dts 写了 mlx90614@5a。芯片在不在,看 driver 软链:

ls -l /sys/bus/i2c/devices/1-005a/driver
cat /sys/bus/i2c/devices/1-005a/name
dmesg | grep -i mlx

三种结局:

现象含义你该干什么
`driver` 指向 `mlx90614`probe 成功,芯片 ACK 过去读 IIO 或继续用户态
目录在,`driver` 不存在没 bind:速率错、NACK、compatible 不匹配、驱动没编进`dmesg`,降到 100 kHz,查 CONFIG
连 `1-005a` 目录都没有dts 没编进生效 dtb,或 status 不是 okay反编译 resource / boot_linux 里的 dtb

有人看到 /sys/bus/i2c/devices/1-0040 就宣布温湿度通了。HDC1080 那颗驱动 probe 不检查写配置的返回值,空壳 client 照样建。dmesg 里一行 cannot set seq config 才是真相。第 37 章。别在软件层空转。


5. i2cdetect:三种格子

镜像里若带了 i2cdetect(i2c-tools):

i2cdetect -y -r 1

-y 跳过确认。-r 用 SMBus receive byte 探测,部分只认「读」的芯片比纯写探测老实。不要对正在工作的 RTC、正在出图的触摸狂扫——扫描会打断传输。传感器总线 i2c1 相对安全。

输出长这样:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: 10 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 57 -- -- 5a -- -- -- -- --
70: -- -- -- 73 -- -- -- --

格子三种:

格子意思
`--`这地址 NACK。没芯片,或芯片没上电,或脚不是 I2C
`10` `5a` `73`有 ACK,**当前没有内核驱动占用**。用户态 I2C_RDWR 可以直接打
`UU`内核驱动已经 bind,适配器认为这地址忙。用户态再打可能 EBUSY,也可能被允许,看驱动有没有把 client 锁死

没有 i2cdetect 就用下面那份 C 的 scan 子命令,原理一样:对 0x03~0x77 发一条长度为 0 的写消息,看 ioctl 成不成功。长度为 0 的写就是「只发地址等 ACK」。

现场对照:

# 红外测温应在 0x5a
i2cdetect -y -r 1 | grep 5a

# 若全是 --,先量 3.3 V 和上拉,再查 i2c1 有没有被 can0 挤掉
cat /proc/device-tree/i2c@fe5a0000/status
# 路径以你 dts 里 i2c1 的 reg 为准,或:
find /proc/device-tree -name 'clock-frequency' | while read f; do
  echo $f $(hexdump -C $f | head -1)
done

clock-frequency 在 device-tree 里是大端 u32。00 01 86 a0 是 100000,00 06 1a 80 是 400000。看到后者,就是第 35 章那个坑还在。


6. 用户态只认 I2C_RDWR

Linux 给用户态两条老 ioctl:I2C_SLAVEread/write,以及 I2C_RDWR 一次提交一组 i2c_msg。后者能表达重复起始(Repeated Start),传感器协议几乎都要它。

开源鸿蒙 NDK 的 sysroot 常常 没有 linux/i2c-dev.h。别 include 失败就改方案,自己把结构体抄下来。用户态是 32 位 armv7,指针 4 字节,下面这个布局就是对的:

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

struct i2c_msg {
    uint16_t addr;    /* 7 位地址,不要左移 */
    uint16_t flags;   /* 0 = 写,I2C_M_RD = 读 */
    uint16_t len;
    uint8_t *buf;
};

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

addr0x5a,不是 0xB4。内核自己会在总线上拼 R/W 位。

三件套对应三种线上时序。我建议你把这三行背下来:

i2cWr S addr+W ACK reg ACK data... ACK P

组合读 i2cRdReg S addr+W ACK reg ACK Sr addr+R ACK data... NAK P

中间那个 Sr 是重复起始,不断开总线。MAX30100 读 FIFO、普通寄存器,都走它。

裸读 i2cRdPlain S addr+R ACK data... NAK P

没有寄存器地址字节。从机用内部指针决定吐什么。HDC1080 触发转换之后必须这样读——你要是再组合读一次,等于又写了指针 0x00,芯片重新开始转换,你读到的是未完成的空数据或 NACK。第 37 章会再打一次。


7. 完整工具:i2c_tool.c

把下面整文件保存为 i2c_tool.c。后面几章直接拿这份用。

/* i2c_tool.c — OpenHarmony 4.1 用户态 I2C 工具(32 位 musl)
 * 子命令:
 *   i2c_tool scan  <bus>
 *   i2c_tool wr    <bus> <addr> <reg> [hex bytes...]
 *   i2c_tool rd    <bus> <addr> <reg> <len>     组合读
 *   i2c_tool plain <bus> <addr> <len>           裸读
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.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

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 i2c_open(int bus)
{
    char path[32];
    snprintf(path, sizeof(path), "/dev/i2c-%d", bus);
    int fd = open(path, O_RDWR);
    if (fd < 0)
        fprintf(stderr, "open %s: %s(没节点=缺 I2C_CHARDEV;EACCES=没 chmod 0666)\n",
                path, strerror(errno));
    return fd;
}

/* 写:S addr+W  reg  [data...] P */
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 = addr,
        .flags = 0,
        .len = (uint16_t)(len + 1),
        .buf = b,
    };
    struct i2c_rdwr_ioctl_data d = { .msgs = &m, .nmsgs = 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 = addr, .flags = 0,       .len = 1,              .buf = &reg },
        { .addr = addr, .flags = I2C_M_RD, .len = (uint16_t)len, .buf = data },
    };
    struct i2c_rdwr_ioctl_data d = { .msgs = m, .nmsgs = 2 };
    return ioctl(fd, I2C_RDWR, &d);
}

/* 裸读:S addr+R [data...] P  —— 不要再写指针 */
static int i2cRdPlain(int fd, uint8_t addr, uint8_t *data, int len)
{
    struct i2c_msg m = {
        .addr = addr,
        .flags = I2C_M_RD,
        .len = (uint16_t)len,
        .buf = data,
    };
    struct i2c_rdwr_ioctl_data d = { .msgs = &m, .nmsgs = 1 };
    return ioctl(fd, I2C_RDWR, &d);
}

static int cmd_scan(int bus)
{
    int fd = i2c_open(bus);
    if (fd < 0)
        return 1;
    printf("     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f\n");
    for (int row = 0; row < 8; row++) {
        printf("%02x:", row * 16);
        for (int col = 0; col < 16; col++) {
            int addr = row * 16 + col;
            if (addr < 0x03 || addr > 0x77) {
                printf("   ");
                continue;
            }
            struct i2c_msg m = {
                .addr = (uint16_t)addr,
                .flags = 0,
                .len = 0,
                .buf = NULL,
            };
            struct i2c_rdwr_ioctl_data d = { .msgs = &m, .nmsgs = 1 };
            int rc = ioctl(fd, I2C_RDWR, &d);
            if (rc >= 0)
                printf(" %02x", addr);
            else
                printf(" --");
        }
        printf("\n");
    }
    close(fd);
    return 0;
}

static int hexbyte(const char *s)
{
    return (int)strtol(s, NULL, 16);
}

static int cmd_wr(int argc, char **argv)
{
    if (argc < 5) {
        fprintf(stderr, "wr <bus> <addr> <reg> [bytes...]\n");
        return 1;
    }
    int bus = atoi(argv[2]);
    uint8_t addr = (uint8_t)hexbyte(argv[3]);
    uint8_t reg  = (uint8_t)hexbyte(argv[4]);
    uint8_t data[24];
    int n = argc - 5;
    if (n > 24)
        n = 24;
    for (int i = 0; i < n; i++)
        data[i] = (uint8_t)hexbyte(argv[5 + i]);
    int fd = i2c_open(bus);
    if (fd < 0)
        return 1;
    int rc = i2cWr(fd, addr, reg, data, n);
    close(fd);
    if (rc < 0) {
        fprintf(stderr, "wr NACK/err: %s\n", strerror(errno));
        return 1;
    }
    printf("wr ok addr=0x%02x reg=0x%02x len=%d\n", addr, reg, n);
    return 0;
}

static int cmd_rd(int argc, char **argv)
{
    if (argc < 6) {
        fprintf(stderr, "rd <bus> <addr> <reg> <len>\n");
        return 1;
    }
    int bus = atoi(argv[2]);
    uint8_t addr = (uint8_t)hexbyte(argv[3]);
    uint8_t reg  = (uint8_t)hexbyte(argv[4]);
    int len = atoi(argv[5]);
    if (len <= 0 || len > 32)
        return 1;
    uint8_t buf[32];
    int fd = i2c_open(bus);
    if (fd < 0)
        return 1;
    int rc = i2cRdReg(fd, addr, reg, buf, len);
    close(fd);
    if (rc < 0) {
        fprintf(stderr, "rd NACK/err: %s\n", strerror(errno));
        return 1;
    }
    printf("rd addr=0x%02x reg=0x%02x:", addr, reg);
    for (int i = 0; i < len; i++)
        printf(" %02x", buf[i]);
    printf("\n");
    return 0;
}

static int cmd_plain(int argc, char **argv)
{
    if (argc < 5) {
        fprintf(stderr, "plain <bus> <addr> <len>\n");
        return 1;
    }
    int bus = atoi(argv[2]);
    uint8_t addr = (uint8_t)hexbyte(argv[3]);
    int len = atoi(argv[4]);
    if (len <= 0 || len > 32)
        return 1;
    uint8_t buf[32];
    int fd = i2c_open(bus);
    if (fd < 0)
        return 1;
    int rc = i2cRdPlain(fd, addr, buf, len);
    close(fd);
    if (rc < 0) {
        fprintf(stderr, "plain NACK/err: %s\n", strerror(errno));
        return 1;
    }
    printf("plain addr=0x%02x:", addr);
    for (int i = 0; i < len; i++)
        printf(" %02x", buf[i]);
    printf("\n");
    return 0;
}

int main(int argc, char **argv)
{
    if (argc < 3) {
        fprintf(stderr,
            "usage:\n"
            "  %s scan  <bus>\n"
            "  %s wr    <bus> <addr> <reg> [bytes hex...]\n"
            "  %s rd    <bus> <addr> <reg> <len>\n"
            "  %s plain <bus> <addr> <len>\n",
            argv[0], argv[0], argv[0], argv[0]);
        return 1;
    }
    if (strcmp(argv[1], "scan") == 0)
        return cmd_scan(atoi(argv[2]));
    if (strcmp(argv[1], "wr") == 0)
        return cmd_wr(argc, argv);
    if (strcmp(argv[1], "rd") == 0)
        return cmd_rd(argc, argv);
    if (strcmp(argv[1], "plain") == 0)
        return cmd_plain(argc, argv);
    fprintf(stderr, "unknown cmd %s\n", argv[1]);
    return 1;
}

编译必须走开源鸿蒙那套 32 位 musl,不要用主机的 aarch64-linux-gnu-gcc。板上只有 /system/lib/ld-musl-arm.so.1

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 i2c_tool i2c_tool.c
file i2c_tool
# 期望:ELF 32-bit LSB executable, ARM, EABI5, dynamically linked, interpreter /system/lib/ld-musl-arm.so.1

hdc file send i2c_tool /data/local/tmp/i2c_tool
hdc shell chmod 0755 /data/local/tmp/i2c_tool
hdc shell /data/local/tmp/i2c_tool scan 1

扫到 0x10 / 0x57 / 0x5a,这章硬件层过关。读一颗环境光的 CONF 寄存器(第 34 章会解释含义):

hdc shell /data/local/tmp/i2c_tool rd 1 0x10 0x00 2

有两个字节回来,总线、地址、组合读三条一次齐。


8. 权限:HAP 不是 root

hdc shell 是 root,open("/dev/i2c-1") 怎么都成功。ArkTS 应用不是。节点默认 0600 root,应用一 open 就是 EACCES,页面上显示「读失败」,有人就开始改地址。

开机服务里放权。板级脚本建议放:

device/board/rk/rk3568_evb/board_perm/board-perm.sh

init.rk30board.cfg 拉起(硬件名是 rk30board,改错 cfg 文件开机不理你,第 11 章)。脚本里:

#!/system/bin/sh
# 轮询几秒:i2c 适配器 probe 可能比 init 晚
i=0
while [ $i -lt 12 ]; do
    chmod 0666 /dev/i2c-0 /dev/i2c-1 /dev/i2c-2 2>/dev/null
    chmod 0666 /sys/bus/iio/devices/iio:device*/in_* 2>/dev/null
    chmod 0666 /sys/bus/iio/devices/iio:device*/name 2>/dev/null
    i=$((i + 1))
    sleep 1
done
ls -l /dev/i2c-*

进 vendor 分区,重打 vendor.img 刷 p8。单装 HAP 不够。开发期也可以手动:

hdc shell chmod 0666 /dev/i2c-1

重启就没了,所以要进脚本。SELinux 开发镜像是 permissive;交付若 enforce,还得补 policy,光 chmod 不够。


9. 现场一:400 kHz,红外测温不 bind

学生描述:dts 里写了 mlx90614@5als /sys/bus/i2c/devices 看得到 1-005a,就是没有 in_temp_object_raw

我让他跑:

ls -l /sys/bus/i2c/devices/1-005a/driver
# 没有这个软链

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

# 看生效 dtb 里的速率
hexdump -C /proc/device-tree/i2c@*/clock-frequency

00 06 1a 80 = 400000。芯片按 SMBus 100 kHz 设计,400 kHz 下 ACK 时有时无,驱动读配置失败就放弃 bind。client 目录还在,因为 dts 实例化不依赖 ACK。

改:

&i2c1 {
    clock-frequency = <100000>;
};

删内核 checkpoint,重打 boot_linux同时刷 resource(p4)。只刷 Image、dtb 还是旧的那份,现象不变。刷完:

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

cat /sys/bus/iio/devices/iio:device*/name
# 其中一行 veml6040 / mlx90614 / ...

第 35 章用同一颗芯片把温度公式写完。这里只要你记住:NACK 先看速率,再看供电,最后才怀疑 C 代码。


10. 现场二:开了 CAN,传感器集体失踪

另一位把 &can0 { status = "okay"; } 打开,想顺手测 CAN。i2cdetect 扫 i2c1 全 --。触摸还在,相机还在——它们在 i2c2。

GPIO0_B3 / B4 这一对,原理图上既是 I2C1 又是 CAN0。pinctrl 谁先申请到谁赢。can 驱动 probe 成功之后,i2c1 再申请同一 group,失败,适配器不起来,/dev/i2c-1 可能还在(控制器设备在),但线上已经不是 I2C 波形。

处理:两份 dtb、两份镜像,不要在同一时刻 okay。测完 CAN 改回去。应用层检测 /dev/i2c-1 扫不到 0x10,提示「当前镜像开了 CAN,传感器总线让位」,比让用户以为模块坏了要厚道。


11. 现场三:内核占着,用户态 EBUSY

环境光走 IIO 时,1-0010 的 driver 是 veml6040,i2cdetect 这一格是 UU。此时用户态再 i2cRdReg(fd, 0x10, ...),有的内核允许(没把适配器锁死),有的返回 EBUSY。

规矩:

  • 芯片已经有内核驱动并且工作,读 IIO sysfs,不要再抢 i2c-dev。
  • 内核驱动走不通(血氧中断不来、温湿度写配置 NACK),再改用户态轮询。必要时:
echo 1-0057 > /sys/bus/i2c/drivers/max30100/unbind

unbind 之后格子从 UU 变成 57,i2c-dev 才能独占。别两个路径同时打 FIFO 指针。

PAJ7620 没有内核节点,纯用户态,不存在这场架。第 38 章。


12. ArkTS 怎么接到这套 C

应用不自己 ioctl。NAPI 模块做成 libboard.so,把三件套包一层。声明:

// libboard.so
export const i2cReadReg: (bus: number, addr: number, reg: number) => number;
export const i2cWriteReg: (bus: number, addr: number, reg: number, val: number) => number;
export const readText: (path: string) => string;
export const findIio: (nameSubstr: string) => string;

native 里 i2cReadReg 就是 i2cRdReg 读 1 字节,失败返回 -1。页面上:

import board from 'libboard.so';

@Entry
@Component
struct I2cProbe {
  @State msg: string = '点扫描';

  build() {
    Column() {
      Button('读环境光 CONF').onClick(() => {
        let v: number = board.i2cReadReg(1, 0x10, 0x00);
        this.msg = v < 0 ? 'NACK(查 /dev/i2c-1 权限、模块、100kHz)'
                         : ('CONF 低字节 = 0x' + v.toString(16));
      })
      Text(this.msg).margin({ top: 12 })
    }
  }
}

HAP 进程 32 位。abiFiltersarmeabi-v7a。so 用同一套 musl 工具链编进 hap。第 67、68 章。

同步 ioctl 不要放在 200 ms 的 setInterval 里死循环——I2C 通常很快,但总线卡住时 ioctl 会睡到超时,UI 线程跟着冻。传感器轮询放到 native 线程,或至少别在 aboutToAppear 里扫全总线。


13. 一张排障表,贴显示器边上

按这个顺序,不要跳:

  1. 模块插在 I2C 排针上,3.3 V、GND、SDA、SCL 四根都在。不是 UART 那排。
  2. ls /dev/i2c-1。没有:CONFIG_I2C_CHARDEV、dts &i2c1 status、CAN 共脚。
  3. chmod 0666。hdc 能读、应用不能读,就是它。
  4. i2cdetect -y -r 1i2c_tool scan 1。目标地址 --:供电、速率、虚焊、扫错总线。
  5. /sys/bus/i2c/devices/1-00xx/driver。有目录无 driver:probe 失败,dmesg。
  6. 组合读还是裸读,对照芯片手册。HDC1080 禁止组合读触发后的数据。
  7. 示波器:SCL 100 kHz、ACK 位从机把 SDA 拉低。波形圆、没 ACK,别再改 C。

常见误判:

  • dts 有节点 = 芯片活着。不是。
  • hdc shell 读通 = 应用也能读。不是,差权限。
  • 改了 dts 只刷了 Image。生效 dtb 在 resource。
  • 扫 i2c0 找传感器。传感器在 i2c1。
  • 同时 okay can0 和 i2c1。共脚。

14. 验收

这章过关的硬标准,不要自我感觉:

  1. /dev/i2c-0 i2c-1 i2c-2 都在,权限 0666。
  2. i2c_tool scan 1 能看到传感器地址(至少 0x10、0x5a 里有活的)。
  3. i2c_tool rd 1 0x10 0x00 2 返回两个字节,不是 NACK。
  4. ls -l /sys/bus/i2c/devices/1-005a/driver 在 100 kHz 下能绑上红外测温驱动(模块插着的前提下)。
  5. 你能口述组合读和裸读的时序差别,以及 HDC1080 为什么必须裸读。

做不到第 4 条,先别跳第 35 章。做不到第 3 条,第 34 章的 IIO 也会是空的。


本章小结

I2C 不是串口。三条总线、一个 100 kHz、一次 ACK。用户态只走 I2C_RDWR 三件套。设备树是「内核会创建 client」的声明,芯片在不在看 driver 软链和 i2cdetect 格子。权限、共脚、速率,这三件事干掉过的人比写错寄存器的人多。

下一章用 0x10 那颗环境光,把 IIO 和用户态两条路都跑通。工具还是今天这份 i2c_tool


路径清单

device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
    &i2c0 / &i2c1 / &i2c2 ,clock-frequency = <100000>
device/board/rk/rk3568_evb/kernel/build_kernel.sh
    合并 CONFIG_I2C_CHARDEV=y 、各 IIO 传感器 CONFIG
device/board/rk/rk3568_evb/board_perm/board-perm.sh
    chmod 0666 /dev/i2c-*
kernel/linux/linux-5.10/drivers/i2c/i2c-dev.c
    /dev/i2c-N 字符设备
kernel/linux/linux-5.10/drivers/i2c/busses/i2c-rk3x.c
    控制器
kernel/linux/linux-5.10/include/uapi/linux/i2c.h
    I2C_M_RD 、i2c_msg(内核头;用户态 NDK 可能没有,本章已内嵌)
vendor/rk/rk3568_evb/hdf_config/
    触摸若走 HDF,不要和内核 goodix 同时占用 i2c2
out/.../packages/phone/images/boot_linux.img
    dtb + Image
out/.../packages/phone/images/resource.img
    生效 rk-kernel.dtb ,改 dts 必刷

官方文档对照:驱动子系统 I2C 接口说明在开源鸿蒙 docs 的 device-dev/driver/ 下,和本章那两张官方图是同一套。HDF 平台 I2C 是另一条腿,传感器作业优先 i2c-dev。


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

Logo

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

更多推荐