总线会扫了。今天接第一颗练习芯片——环境光。遮一下、照一下,读数必须跟着走。
第 33 章把总线打通了。这章只盯一颗芯片:I2C 地址0x10,RGBW 环境光,挂在i2c1。
我见过的翻车几乎都不是「不会读寄存器」,而是这几件:扫错总线、IIO 设备号每次开机变、HAP 读 sysfs 权限 0444、用手掌盖住数值却不动——其实读的是另一颗 iio:device 的通道。数值会随遮光/照光变,这一条是验收,不是装饰。
两条路都给你:内核 IIO,和用户态I2C_RDWR。IIO 通了用 IIO;驱动没编进来,用第 33 章那份三件套自己读。
1. 这颗芯片在总线上干什么
VEML6040 是 RGBW 四通道环境光。I2C 7 位地址0x10写死,改不了。同一条 i2c1 上还有 0x40 / 0x57 / 0x5a / 0x73,不会撞车。
内部就几个 16 位寄存器,低字节先:
| 寄存器 | 地址 | 作用 |
|---|---|---|
| CONF | 0x00 | 积分时间、触发、关断 |
| R | 0x08 | 红 |
| G | 0x09 | 绿 |
| B | 0x0A | 蓝 |
| W | 0x0B | 白 / clear,对可见光更宽 |
CONF 低字节按数据手册:
- bit0
SD:1 = 关断,0 = 工作。驱动 probe 时写成 0。 - bit1
AF:0 自动,1 强制触发。 - bit2
TRIG:强制模式写 1 启动一次。 - bit6:4
IT:积分时间。000 = 40 ms,越长越灵敏、越慢。
高字节保留,写 0。板级驱动默认CONF = 0x0000:40 ms、自动、开机。
光进窗口,四个通道的 raw 往上爬。你用手盖住,clear 必须掉。掉不了,你读错节点了,或者芯片没在采。
通信路径:
手指遮光 / 台灯 → 窗口光电 → 芯片 0x10 → i2c1 @ 100 kHz → 内核 veml6040.ko(编进 Image) → /sys/bus/iio/devices/iio:deviceN/in_intensity_*_raw → 用户态 read() 或 ArkTS readText()用户态旁路:跳过 IIO,open("/dev/i2c-1")+i2cRdReg读 0x08~0x0B。两条路读到的是同一组寄存器。
2. 设备树和内核开关
板级 dts,产品名rk3568_evb:
&i2c1 { status = "okay"; clock-frequency = <100000>; veml6040: veml6040@10 { compatible = "vishay,veml6040"; reg = <0x10>; status = "okay"; }; };compatible必须和驱动里of_device_id一字不差。驱动不在 mainline 默认配置里,是板级丢进内核树的:
device/board/rk/rk3568_evb/kernel/veml6040.cbuild_kernel.sh在合并 defconfig 时写入:
CONFIG_VEML6040=y并往drivers/iio/light/Makefile加一行obj-$(CONFIG_VEML6040) += veml6040.o,源文件拷进drivers/iio/light/。三处 CONFIG 同步:源码 defconfig、out 里的、脚本合并段。只在 menuconfig 里打开,下次全量重建会丢。
驱动 probe 做两件事:写 CONF 开机,注册四个 IIO_INTENSITY 通道(红、绿、蓝、clear)。read_raw内部是i2c_smbus_read_word_data,就是组合读 16 位。
改 dts 或驱动之后:删内核 checkpoint,重打boot_linux,resource 分区的 dtb 一并刷。只刷 Image、设备树还是旧的,0x10 节点都不会出现。
3. 上板先确认这颗活着
插上传感器模块,i2c1 那排针,3.3 V 有电。hdc:
hdc shell ls -l /dev/i2c-1 i2cdetect -y -r 1 # 没有 i2cdetect 就: # /data/local/tmp/i2c_tool scan 10x10 那一格应该是10或UU。--的话停下来,回到第 33 章:供电、100 kHz、CAN 共脚、扫错总线。
ls -l /sys/bus/i2c/devices/1-0010/driver # 期望指向 .../drivers/veml6040 cat /sys/bus/i2c/devices/1-0010/name # veml6040有目录、没 driver:probe 失败。dmesg | grep -i veml,常见是写 CONF 被 NACK——还是总线问题,不是 IIO 问题。
IIO 侧:
for d in /sys/bus/iio/devices/iio:device*; do echo "$d $(cat $d/name 2>/dev/null)" done你会看到一串名字:saradc、mlx90614、veml6040、可能还有1-0040这种用 i2c 地址当 name 的。不要写死iio:device5。开机枚举顺序随 probe 变化,今天是 5,明天刷完内核变成 3。按名字找。
权限。hdc 是 root,应用不是:
ls -l /sys/bus/iio/devices/iio:device*/in_intensity_*_raw chmod 0666 /sys/bus/iio/devices/iio:device*/in_intensity_*_raw chmod 0666 /sys/bus/iio/devices/iio:device*/name长期放进board-perm.sh,进 vendor。第 33 章写过脚本骨架。
4. 用手验收,不要用想象
找到 veml6040 那颗 IIO:
BASE= for d in /sys/bus/iio/devices/iio:device*; do n=$(cat "$d/name") [ "$n" = "veml6040" ] && BASE=$d done echo "BASE=$BASE" cat $BASE/in_intensity_clear_raw cat $BASE/in_intensity_red_raw cat $BASE/in_intensity_green_raw cat $BASE/in_intensity_blue_raw记下四个数。然后:
- 手掌盖住传感器窗口,再 cat 一次。clear 必须明显下降。
- 拿手机手电筒贴窗口,clear 必须明显上升。
- 红光手电则 red 相对更高;这是定性,不是实验室。
我经历过一次「数值恒定 65535」。那是读到了别的通道的满量程,或者芯片关断位没清、总线全 1。全 1 也出现在没 ACK、读缓冲没刷新的情况。对照 scan:0x10 必须在。
绿通道粗略照度:
lux ≈ green_raw * 0.25168这是积分 40 ms、典型窗口下的经验系数,不是校准证书。室内白天常见几百到两千,手盖住掉到几十或更低。报「lux」的时候写清楚「约」。
5. 完整 C:优先 IIO,没有再走 i2c-dev
保存为veml6040_read.c。先按名字搜 IIO;搜不到再打开/dev/i2c-1组合读 0x08~0x0B。两种路径都要会,IIO 没编进来时还能自己读。
/* veml6040_read.c — 环境光:IIO 优先,失败则 i2c-dev */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <dirent.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 i2cWr(int fd, uint8_t addr, uint8_t reg, const uint8_t *data, int len) { uint8_t b[32]; b[0] = reg; if (data && len > 0) memcpy(b + 1, data, (size_t)len); 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, ® }, { addr, I2C_M_RD, (uint16_t)len, data }, }; struct i2c_rdwr_ioctl_data d = { m, 2 }; return ioctl(fd, I2C_RDWR, &d); } static int read_file_int(const char *path, int *out) { char buf[64]; int fd = open(path, O_RDONLY); if (fd < 0) return -1; ssize_t n = read(fd, buf, sizeof(buf) - 1); close(fd); if (n <= 0) return -1; buf[n] = 0; *out = atoi(buf); return 0; } static int find_iio(const char *want, char *out, size_t outlen) { DIR *d = opendir("/sys/bus/iio/devices"); if (!d) return -1; struct dirent *e; int found = 0; while ((e = readdir(d)) != NULL) { if (strncmp(e->d_name, "iio:device", 10) != 0) continue; char namep[128], name[64]; snprintf(namep, sizeof(namep), "/sys/bus/iio/devices/%s/name", e->d_name); int fd = open(namep, O_RDONLY); if (fd < 0) continue; ssize_t n = read(fd, name, sizeof(name) - 1); close(fd); if (n <= 0) continue; name[n] = 0; char *nl = strchr(name, '\n'); if (nl) *nl = 0; if (strstr(name, want)) { snprintf(out, outlen, "/sys/bus/iio/devices/%s", e->d_name); found = 1; break; } } closedir(d); return found ? 0 : -1; } static int read_iio(void) { char base[128]; if (find_iio("veml6040", base, sizeof(base)) < 0) { fprintf(stderr, "IIO 里没有 veml6040(驱动没 bind 或没编进来)\n"); return -1; } int r, g, b, c; char p[160]; snprintf(p, sizeof(p), "%s/in_intensity_red_raw", base); if (read_file_int(p, &r) < 0) { fprintf(stderr, "读 red 失败: %s %s\n", p, strerror(errno)); return -1; } snprintf(p, sizeof(p), "%s/in_intensity_green_raw", base); read_file_int(p, &g); snprintf(p, sizeof(p), "%s/in_intensity_blue_raw", base); read_file_int(p, &b); snprintf(p, sizeof(p), "%s/in_intensity_clear_raw", base); read_file_int(p, &c); double lux = g * 0.25168; printf("IIO %s\n", base); printf("R=%d G=%d B=%d clear=%d 约 %.0f lux(绿通道粗算)\n", r, g, b, c, lux); printf("用手掌盖住再跑一次,clear 必须下降。\n"); return 0; } static int read_i2c(void) { int fd = open("/dev/i2c-1", O_RDWR); if (fd < 0) { fprintf(stderr, "open /dev/i2c-1: %s\n", strerror(errno)); return -1; } /* 开机:CONF=0x0000,SD=0 */ uint8_t conf[2] = { 0x00, 0x00 }; if (i2cWr(fd, 0x10, 0x00, conf, 2) < 0) { fprintf(stderr, "写 CONF NACK,0x10 不在?\n"); close(fd); return -1; } usleep(50000); /* 40 ms 积分 + 余量 */ uint8_t rb[2]; unsigned ch[4]; const uint8_t regs[4] = { 0x08, 0x09, 0x0A, 0x0B }; const char *nm[4] = { "R", "G", "B", "W" }; for (int i = 0; i < 4; i++) { if (i2cRdReg(fd, 0x10, regs[i], rb, 2) < 0) { fprintf(stderr, "读 0x%02x NACK\n", regs[i]); close(fd); return -1; } ch[i] = rb[0] | (rb[1] << 8); /* 低字节先 */ } close(fd); printf("i2c-dev 0x10\n"); for (int i = 0; i < 4; i++) printf("%s=%u%s", nm[i], ch[i], i == 3 ? "\n" : " "); printf("约 %.0f lux\n", ch[1] * 0.25168); return 0; } int main(int argc, char **argv) { int force_i2c = (argc > 1 && strcmp(argv[1], "i2c") == 0); if (!force_i2c && read_iio() == 0) return 0; return read_i2c() == 0 ? 0 : 1; }编译和上传,和第 33 章同一套工具链:
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 veml6040_read veml6040_read.c hdc file send veml6040_read /data/local/tmp/veml6040_read hdc shell chmod 0755 /data/local/tmp/veml6040_read hdc shell /data/local/tmp/veml6040_read hdc shell /data/local/tmp/veml6040_read i2c第一次跑 IIO,第二次强制 i2c-dev。两串数字应在同一量级。差出一个数量级,检查有没有重复起始、有没有把高低字节倒了。这颗是小端 16 位,rb[0] | (rb[1]<<8)。
6. 现场:数值不动
学生说「我盖住了,还是 1247」。我让他连续 cat:
while true; do date cat $BASE/in_intensity_clear_raw sleep 1 done盖上、拿开、拿手电。数字跳,芯片是活的,应用读错路径。数字死,往下查。
读错 iio 设备。iio:device0经常是 saradc。saradc 的in_voltage0_raw是按键通道,跟光无关,你怎么盖窗口它都不动。findIio("veml6040")就是为这件事存在的。
权限。应用readText返回空或Permission denied。hdc 下 cat 正常。chmod 0666,写进 board-perm。
驱动没 bind,你还在读一个过期路径。重编内核之后 device 序号变了,页面里写死的/sys/bus/iio/devices/iio:device5/in_intensity_clear_raw变成了别的芯片,或者路径 404。每次按名字找。
CONF.SD=1。有人用户态写过关断,又去读 IIO,IIO 那条路径以为自己管着 CONF。别混用。选一条路。
盖错地方。模块上有两颗窗口时,环境光是那颗扁的小芯片,不是红外测温的透镜。盖错颗,clear 当然不动。对着丝印或用手电筒贴每一颗看谁跳。
7. 现场:IIO 没有 veml6040
grep VEML6040 /proc/config.gz # 若没有 config.gz: zcat /proc/config.gz 2>/dev/null | grep VEML ls /sys/bus/i2c/drivers/veml6040 dmesg | grep -i vemlCONFIG_VEML6040没进去:脚本合并段没写,或重建时被 olddefconfig 吃掉。打开,重编内核。
驱动在,client 不 bind:compatible 写错、status disabled、i2c1 没起来。反编译生效 dtb:
# 从板上把 dtb 抽出来看节点,或直接: ls /proc/device-tree/i2c*/veml6040@10 cat /proc/device-tree/i2c*/veml6040@10/statusdisabled就是 dts 没 okay。okay仍不 bind,看 dmesg 写 CONF 那一行。
没有内核驱动也不丢人。veml6040_read i2c照样验收遮光。IIO 是方便 sysfs 的路,不是物理定律。
8. ArkTS 页面
native 导出findIio、readText。页面不要写死 device 号。
import board from 'libboard.so'; @Entry @Component struct LightPage { @State line: string = '点读取。盖住窗口再点一次,clear 必须变小。'; private num(path: string): number { let s: string = board.readText(path).trim(); return parseFloat(s); } readLight() { let base: string = board.findIio('veml6040'); if (base.length === 0) { this.line = '未找到 veml6040。查 i2c1 0x10 / CONFIG_VEML6040 / 权限'; return; } let c: number = this.num(base + '/in_intensity_clear_raw'); if (Number.isNaN(c)) { this.line = '读 clear 失败(chmod 0666 in_intensity_*_raw)'; return; } let g: number = this.num(base + '/in_intensity_green_raw'); let r: number = this.num(base + '/in_intensity_red_raw'); let b: number = this.num(base + '/in_intensity_blue_raw'); let lux: number = Number.isNaN(g) ? 0 : g * 0.25168; this.line = 'clear=' + c.toFixed(0) + ' R=' + r.toFixed(0) + ' G=' + g.toFixed(0) + ' B=' + b.toFixed(0) + ' 约 ' + lux.toFixed(0) + ' lux\n' + base; } build() { Column() { Text('环境光 VEML6040 @0x10').fontSize(18).margin({ bottom: 8 }) Button('读一次').onClick(() => { this.readLight(); }) Text(this.line).margin({ top: 12 }).fontSize(16) }.padding(16) } }findIio的 native 实现和第 33 章、上面 C 的find_iio一样:扫/sys/bus/iio/devices,name子串匹配。匹配veml6040,不要匹配0040——那是另一颗。
轮询的话 200~500 ms 一次够了,积分时间 40 ms,更快没有新数据。不要在 UI 线程里setInterval同时打四次同步读还弹动画,卡了会以为芯片死了。
9. 和同总线其它芯片的关系
i2c1 @ 100 kHz 是全家的约定。你为了「环境光快点」把总线改回 400 kHz,环境光可能仍 ACK,红外测温会掉线。别改。
内核占用 0x10 时 i2cdetect 是UU。用户态再读写 0x10 可能 EBUSY。页面走 IIO 就不要再走 i2cReadReg(1, 0x10, ...)。调试才用veml6040_read i2c。
CAN0 和 i2c1 共脚:开 CAN 的镜像上,这章整章失效。不是芯片坏了。
10. 验收
i2cdetect -y -r 1或i2c_tool scan 1见到 0x10。/sys/bus/i2c/devices/1-0010/driver指向 veml6040(走 IIO 时)。- 按名字找到 IIO,读到四个 raw。
- 手掌盖住,clear 下降;手电筒,clear 上升。两次读取间隔至少 100 ms。
- HAP 里同样能读(0666 已放权),且遮光仍变。
- 你没有在代码里写死
iio:device5。
第 4 条不过,不要标这颗完成。恒定值对应用来说比没节点更危险——它看起来「通了」。
一次遮光实验的终端记录
下面是板上真实会看到的形状。数字随环境变,趋势必须对。
# cat /sys/bus/iio/devices/iio:device3/name veml6040 # cat /sys/bus/iio/devices/iio:device3/in_intensity_clear_raw 1842 # 手掌盖住窗口,等 200 ms # cat /sys/bus/iio/devices/iio:device3/in_intensity_clear_raw 117 # 拿开,手电筒斜打 # cat /sys/bus/iio/devices/iio:device3/in_intensity_clear_raw 6120R/G/B 会一起动,但比例不同。白炽灯偏红通道,阴天窗口偏蓝。不要用「某一个通道没变」判坏——看 clear,再看绿。
同一段用用户态旁路:
# /data/local/tmp/veml6040_read i2c CONF=0x0000 R=210 G=1880 B=940 W=1842 ~lux=473IIO 和 i2c-dev 对同一颗芯片,clear 应对得上,差几个 count 是积分时刻不同,差一个数量级才是读错寄存器或读了邻机。
HAP 读到恒定值、hdc cat 会变:应用打开的不是你以为的那份 sysfs。findIio("veml6040")每次测量都跑,不要在aboutToAppear里缓存iio:device3用一整天——插上 USB 网卡、加载别的 IIO 设备,序号会挤。
CONF 写 0x0001(SD=1 关断)后再读,四个通道接近 0。这是确认「我读写的是 0x10」的最快办法。测完写回 0x0000,否则 IIO 下一次 raw 也是 0,两人会互相指责对方把芯片弄休眠了。
积分时间 IT 拉长,数值变大、更新变慢。默认 40 ms 够遮光实验。别在演示现场改 IT,除非你把等待也改了。
路径清单
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts &i2c1 veml6040@10 compatible = "vishay,veml6040" device/board/rk/rk3568_evb/kernel/veml6040.c 板级 IIO 驱动,CONF=0x0000,四通道 RAW device/board/rk/rk3568_evb/kernel/build_kernel.sh CONFIG_VEML6040=y ,拷贝 .c 进 drivers/iio/light/ kernel/linux/linux-5.10/drivers/iio/light/veml6040.c 编译后的落点(由脚本放入) /sys/bus/iio/devices/iio:deviceN/ name = veml6040 in_intensity_red_raw in_intensity_green_raw in_intensity_blue_raw in_intensity_clear_raw /dev/i2c-1 用户态旁路,地址 0x10,寄存器 0x00 / 0x08 / 0x09 / 0x0A / 0x0B device/board/rk/rk3568_evb/board_perm/board-perm.sh chmod 0666 iio in_* 与 /dev/i2c-1数量级,免得把邻机读数当成环境光
白天靠窗,clear 常见四位数。手掌盖严,掉到两位数或三位数低端。手电筒贴窗口,可以冲到五千以上。夜间只开一盏台灯,几百。这些不是指标,是「我读的是这颗芯片」的气味。
若 clear 恒定 0:CONF 的 SD 位可能被写成 1,或者你读的 IIO 根本不是 veml6040。回去cat name。
若恒定 65535:总线读出来全 1,常见于没 ACK 仍把缓冲当数据、或读到了别的 16 位满量程通道。i2cdetect上 0x10 必须在。用户态读 0x08 若也是ff ff,先量 3.3 V 和上拉。
SARADC 那章的毫伏和这颗无关。有人把in_voltage2_raw的变化当成环境光——那是底板电位器或外部 ADC 通道,遮窗口它不动。名字里没有 intensity 的,不要拿来交差。
绿通道乘 0.25168 得到的「约 lux」只在 IT=40 ms、窗口没贴膜时有点边。IT 改成 80 ms、160 ms,同一个房间数字会翻倍,lux 公式不能再用。演示现场保持默认 CONF=0x0000。
应用层缓存iio:deviceN是下一颗坑。USB 网卡、SARADC、红外测温都可能先 probe,序号被挤。每次读取都findIio("veml6040"),多一次 open,少一晚上假数据。
编译用户态工具仍是 32 位 musl,和第 33 章同一套 clang。编出来file必须看到ARM、interpreter /system/lib/ld-musl-arm.so.1。aarch64 静态编上去,exec 直接失败,现象像「工具没送上去」。
hdc file send veml6040_read /data/local/tmp/veml6040_read hdc shell chmod 0755 /data/local/tmp/veml6040_read hdc shell /data/local/tmp/veml6040_read hdc shell /data/local/tmp/veml6040_read i2c两次输出的 clear 对得上,这一章才能收工。对不上,先看是不是内核占用 0x10 导致用户态 EBUSY——那就只信 IIO,把 i2c 旁路留给驱动没编进来的镜像。
ArkTS 侧不要自己拼/sys/bus/iio/devices/iio:device5/in_intensity_clear_raw。NAPI 提供findIio("veml6040")和readText。页面上两个按钮就够:读一次、连续读。连续读间隔 200 ms 以上,积分时间才 40 ms,读太快数字看起来「不变」,那是你采样比芯片还快。
遮光实验必须在应用里再做一遍。hdc cat 会变、HAP 恒定,十有八九是权限 0444 或读错了 device 序号。board-perm.sh把in_*chmod 0666,进 vendor,不要每次开机手敲。
hdc shell "cat /proc/partitions" hdc shell "ls -l /dev/i2c-1 /dev/ttyS0" hdc shell "dmesg | grep -E 'i2c|mlx|hdc|uart0|vcc-camera' | tail -30"上面三条在换模块、改 dts、刷完分区之后各跑一次,比在应用里加 log 便宜。输出贴进记录,下一轮对照。
i2c1 被 CAN 镜像关掉时,这颗环境光会一起消失。不是 VEML6040 坏了,是整条总线让路给了 CAN0 的 GPIO0_B3/B4。换回默认镜像(i2c1 okay、can0 disabled)再测。触摸在 i2c2,不受影响。
CAN 镜像把 i2c1 关掉时,0x10 会从 i2cdetect 上消失。换回 i2c1 okay 的镜像再测,不要先换环境光模块。
遮光必须肉眼看见数字跳。跳了,这章结束。。
手盖住,clear 必须下降。
系列第 34 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10