总线会扫了。今天接第一颗练习芯片——环境光。遮一下、照一下,读数必须跟着走。

第 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 位寄存器,低字节先:

寄存器地址作用
CONF0x00积分时间、触发、关断
R0x08红
G0x09绿
B0x0A蓝
W0x0B白 / 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.c

build_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 1

0x10 那一格应该是 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

记下四个数。然后:

  1. 手掌盖住传感器窗口,再 cat 一次。clear 必须明显下降。
  2. 拿手机手电筒贴窗口,clear 必须明显上升。
  3. 红光手电则 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, &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 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 veml

CONFIG_VEML6040 没进去:脚本合并段没写,或重建时被 olddefconfig 吃掉。打开,重编内核。

驱动在,client 不 bind:compatible 写错、status disabled、i2c1 没起来。反编译生效 dtb:

# 从板上把 dtb 抽出来看节点,或直接:
ls /proc/device-tree/i2c*/veml6040@10
cat /proc/device-tree/i2c*/veml6040@10/status

disabled 就是 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. 验收

  1. i2cdetect -y -r 1 或 i2c_tool scan 1 见到 0x10。
  2. /sys/bus/i2c/devices/1-0010/driver 指向 veml6040(走 IIO 时)。
  3. 按名字找到 IIO,读到四个 raw。
  4. 手掌盖住,clear 下降;手电筒,clear 上升。两次读取间隔至少 100 ms。
  5. HAP 里同样能读(0666 已放权),且遮光仍变。
  6. 你没有在代码里写死 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
6120

R/G/B 会一起动,但比例不同。白炽灯偏红通道,阴天窗口偏蓝。不要用「某一个通道没变」判坏——看 clear,再看绿。

同一段用用户态旁路:

# /data/local/tmp/veml6040_read i2c
CONF=0x0000  R=210  G=1880  B=940  W=1842  ~lux=473

IIO 和 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐