I2C 总线怎么用?怎么排障?—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
板载这些先告一段落。从今天起换插在排针上的模块。先把 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。
软件侧官方另有一张流程:

把它翻译成人话:
- 打开总线设备,本产品用户态就是
/dev/i2c-0、/dev/i2c-1、/dev/i2c-2。 - 组消息:写寄存器、读寄存器、或者只读不写。
- 一次 transfer:内核把消息变成 SCL 上的起止条件和 ACK。
- 关闭。应用层不要自己 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 kHz | RTC 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_SLAVE 再 read/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;
};
addr 填 0x5a,不是 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 = ® },
{ .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@5a,ls /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 位。abiFilters 带 armeabi-v7a。so 用同一套 musl 工具链编进 hap。第 67、68 章。
同步 ioctl 不要放在 200 ms 的 setInterval 里死循环——I2C 通常很快,但总线卡住时 ioctl 会睡到超时,UI 线程跟着冻。传感器轮询放到 native 线程,或至少别在 aboutToAppear 里扫全总线。
13. 一张排障表,贴显示器边上
按这个顺序,不要跳:
- 模块插在 I2C 排针上,3.3 V、GND、SDA、SCL 四根都在。不是 UART 那排。
ls /dev/i2c-1。没有:CONFIG_I2C_CHARDEV、dts&i2c1 status、CAN 共脚。chmod 0666。hdc 能读、应用不能读,就是它。i2cdetect -y -r 1或i2c_tool scan 1。目标地址--:供电、速率、虚焊、扫错总线。/sys/bus/i2c/devices/1-00xx/driver。有目录无 driver:probe 失败,dmesg。- 组合读还是裸读,对照芯片手册。HDC1080 禁止组合读触发后的数据。
- 示波器:SCL 100 kHz、ACK 位从机把 SDA 拉低。波形圆、没 ACK,别再改 C。
常见误判:
- dts 有节点 = 芯片活着。不是。
- hdc shell 读通 = 应用也能读。不是,差权限。
- 改了 dts 只刷了 Image。生效 dtb 在 resource。
- 扫 i2c0 找传感器。传感器在 i2c1。
- 同时 okay can0 和 i2c1。共脚。
14. 验收
这章过关的硬标准,不要自我感觉:
/dev/i2c-0i2c-1i2c-2都在,权限 0666。i2c_tool scan 1能看到传感器地址(至少 0x10、0x5a 里有活的)。i2c_tool rd 1 0x10 0x00 2返回两个字节,不是 NACK。ls -l /sys/bus/i2c/devices/1-005a/driver在 100 kHz 下能绑上红外测温驱动(模块插着的前提下)。- 你能口述组合读和裸读的时序差别,以及 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
更多推荐




所有评论(0)