触摸之外,底板上还有几颗用户键。今天把模数转换那两颗、普通输入那一颗对上,丝印编号不是通道号。

底板上三颗键,丝印 KEY3、KEY4、KEY5。第一次写设备树的人,几乎都会把 KEY3 接到 SARADC 的通道 3。节点能 probe,/dev/input/event* 也在,按下 KEY3 却没有任何事件。不是键坏了,是名字骗了你。

原理图上 KEY3 接的是 SARADC VIN0,也就是通道 0。KEY4 接 VIN1(通道 1)。KEY5 根本不走 ADC,是 GPIO0_B0,低有效。KEY3 的「3」是丝印序号,不是通道号。

验证这件事只用一行:

hdc shell "cat /sys/bus/iio/devices/iio:device1/in_voltage0_raw"

没按,读数靠近满量程(参考 1.8 V,raw 往往一千出头到一千七)。按下,电压被按键电阻拉到地附近,raw 掉到接近 0。如果你读的是 in_voltage3_raw,按 KEY3 纹丝不动——那就是通道写成了 3 的现场。

还有两个坑叠在上面。一是 IIO 设备号:iio:device0 经常是波形芯片 AD9833 先注册抢走的,SARADC 的名字是 fe720000.saradc,运行时多半是 iio:device1。二是公版 adc-keys 也挂在 VIN0 上,按 KEY3 时它会同时报音量+、音量-、菜单、返回,od 里出现 0x73 / 0x9e 这些你没声明过的键码。板级要把公版那个节点 disabled

产品写 rk3568_evb,板级 dts:

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

1. 三颗键,两条栈

KEY3 ──分压──► SARADC VIN0 (ch0) ── adc-keys ──► EV_KEY KEY_MENU  (0x8b)
KEY4 ──分压──► SARADC VIN1 (ch1) ── adc-keys ──► EV_KEY KEY_BACK  (0x9e)
KEY5 ──按下接地──► GPIO0_B0      ── gpio-keys ──► EV_KEY KEY_HOME  (0x66)

KEY3/KEY4 是模拟键:没按时 ADC 脚接近 1.8 V,按下被电阻拉到接近 0 V。驱动轮询电压,低于阈值就报按下。KEY5 是数字键:按下把 GPIO 拉低,gpio-keys 走中断或轮询。

不要把它们和 4×4 矩阵键盘混在一起。矩阵是另一组 GPIO、另一个 input 设备,名字通常叫 keypad。三颗板载键是 adc-keys-3adc-keys-1gpio-keys 这类名字。测矩阵的时候按板载键,事件当然对不上 keymap。

也不要和电源键混。RK809 的 pwrkey 会再注册一个 input,那是 PMIC 脚,不是 KEY5。

Linux GPIO 编号:

编号 = bank × 32 + 组内偏移 + 位号
A=0  B=8  C=16  D=24
GPIO0_B0 = 0×32 + 8 + 0 = 8

/sys/kernel/debug/gpio 里 gpio-8 被 gpio-keys 占用,才说明 KEY5 绑对了。


2. 设备树:通道号写对,公版节点关掉

/* KEY5:GPIO0_B0 = linux gpio 8,低有效 */
board_gpio_keys: gpio-keys {
   compatible = "gpio-keys";
   #address-cells = <1>;
   #size-cells = <0>;

   key5 {
       label = "KEY5";
       gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>;
       linux,code = <KEY_HOME>;
       debounce-interval = <20>;
       wakeup-source;
   };
};

/*
* KEY3 → VIN0 → 通道 0。旧稿写成 <&saradc 3> 是把丝印当通道号。
* 按下阈值 100 mV:低于它算按下。抬起阈值 1.8 V:高于它算松开。
*/
board_adc_keys3: adc-keys-3 {
   compatible = "adc-keys";
   io-channels = <&saradc 0>;
   io-channel-names = "buttons";
   keyup-threshold-microvolt = <1800000>;
   poll-interval = <100>;

   key3 {
       label = "KEY3";
       linux,code = <KEY_MENU>;
       press-threshold-microvolt = <100000>;
   };
};

board_adc_keys1: adc-keys-1 {
   compatible = "adc-keys";
   io-channels = <&saradc 1>;
   io-channel-names = "buttons";
   keyup-threshold-microvolt = <1800000>;
   poll-interval = <100>;

   key4 {
       label = "KEY4";
       linux,code = <KEY_BACK>;
       press-threshold-microvolt = <100000>;
   };
};

&saradc {
   status = "okay";
   vref-supply = <&vcca_1v8>;
};

/* 公版 adc-keys 挂 VIN0,和 KEY3 抢通道,按下会假报音量 */
&adc_keys {
   status = "disabled";
};

keyup-threshold-microvolt 是「大于这个值 = 没有键按下」。press-threshold-microvolt 是「小于这个值 = 这个子键按下」。ADC 键允许一根通道上串多个不同阈值的键(公版就是这么做音量+ / 音量-的)。本板每根通道只有一颗键,阈值拉到 100 mV 足够:按下接近 0,没按接近 1.8 V,中间不会晃。

poll-interval = <100> 是 100 ms 轮询。adc-keys 走 IIO 轮询,不是边沿中断。按下去到事件出来,最多一个轮询周期。对菜单键够用。不要指望它做连发游戏手柄。

内核要有:

CONFIG_KEYBOARD_ADC=y
CONFIG_KEYBOARD_GPIO=y
CONFIG_IIO=y
CONFIG_ROCKCHIP_SARADC=y

KEYBOARD_ADC 依赖 IIO。SARADC 驱动是 rockchip,rk3568-saradc。节点地址 saradc@fe720000,所以 sysfs 名字是 fe720000.saradc


3. 为什么必须按名字找 IIO,不能写死 device0

板上 IIO 设备按 probe 顺序编号。AD9833 的 spi-gpio 节点往往先起来,抢走 iio:device0。SARADC 变成 iio:device1。再往后还有血氧、温湿度、环境光、红外测温、一个虚拟测试驱动。编号会变,名字不会变

hdc shell "cat /sys/bus/iio/devices/iio:device*/name"

典型输出:

ad9833
fe720000.saradc
max30100
hdc1080
veml6040
mlx90614
adc_linux_virtual_iio_driver

读 KEY3:

for d in /sys/bus/iio/devices/iio:device*; do
   echo -n "$d "
   cat $d/name
done

hdc shell "cat /sys/bus/iio/devices/iio:device1/in_voltage0_raw"
hdc shell "cat /sys/bus/iio/devices/iio:device1/in_voltage1_raw"

按下 KEY3,in_voltage0_raw 接近 0;按下 KEY4,in_voltage1_raw 接近 0。没按都在高位。scale 文件给出 mV/LSB:

hdc shell "cat /sys/bus/iio/devices/iio:device1/in_voltage_scale"

电压 mV ≈ raw × scale。参考 1.8 V、10 bit 时,scale 大约 1.8e3/1024 ≈ 1.76 mV/LSB。满量程 raw≈1023,对应约 1800 mV。有的内核报 12 bit,满量程 4095,先看 in_voltage_scale 和实际满量程,不要背数字。

文档里如果还写着 iio:device0/in_voltage0_raw,那是抄错了。按 name 找。

外部 ADC 端子是 VIN2~VIN7,那是另一件事,见 SARADC 那篇。通道 0/1 给按键,往这两个通道灌 2 V「看 ADC 变不变」,按键会乱报,外部通道纹丝不动。


4. input 子系统:先看 devices,再看事件流

hdc shell "cat /proc/bus/input/devices"

期望能看到类似:

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="adc-keys-3"
H: Handlers=kbd event3

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="adc-keys-1"
H: Handlers=kbd event4

I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="gpio-keys"
H: Handlers=kbd event5

名字以你 dts 节点名为准。现场见过带板级前缀的名字,那只是节点名,认 Handlers 里的 event 号。

同一份列表里可能还有 keypad(矩阵)、rk805 pwrkey、触摸。按 KEY3 却去 od 矩阵那个 event,当然是错的。

hdc shell "od -An -tx2 -w16 /dev/input/event3"

Linux input_event 在 64 位内核上是:

timeval (16 字节) + type (2) + code (2) + value (4)

用户态 32 位 musl 读 64 位内核的 event,布局仍是内核那份(time 是 64 位秒+纳秒)。按下去会看到:

typecodevalue含义
0x0001 EV_KEY0x008b KEY_MENU1KEY3 按下
0x0000 EV_SYN00同步
0x0001 EV_KEY0x008b0KEY3 松开

KEY4 的 code 是 0x009e(KEY_BACK),KEY5 是 0x0066(KEY_HOME)。

如果按 KEY3 时同一个或另一个 event 上冒出 0x0073(VOLUMEUP)或 0x0072(VOLUMEDOWN),公版 adc_keys 还活着。回去把 &adc_keys { status = "disabled"; } 写上,刷 resource + boot_linux。

没有 evtest 时 od 足够。有 getevent 更好读,开源鸿蒙用户态不一定带。

dmesg 里成功 probe 类似:

input: adc-keys-3 as /devices/platform/adc-keys-3/input/inputX
input: gpio-keys as /devices/platform/gpio-keys/input/inputY

没有这几行:查 CONFIG_KEYBOARD_ADC、节点 status、SARADC 有没有 okay。


5. 完整 C:按名字找 SARADC,再阻塞读按键

用户态 32 位。下面这个程序两件事都做:打印 VIN0/VIN1 的 raw,以及从指定 event 节点读键。

/* keys_probe.c
* armv7-unknown-linux-ohos-clang --target=arm-linux-ohos \
*   --sysroot=${OHOS_NATIVE}/sysroot \
*   -o keys_probe keys_probe.c \
*   -Wl,--dynamic-linker=/system/lib/ld-musl-arm.so.1
*
* hdc file send keys_probe /data/keys_probe
* hdc shell "chmod 755 /data/keys_probe; /data/keys_probe /dev/input/event3"
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <linux/input.h>

static int read_int_file(const char *path, int *out)
{
   char buf[64];
   int fd = open(path, O_RDONLY);
   if (fd < 0) {
       return -1;
   }
   int n = (int)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_by_name(const char *want, char *out, size_t len)
{
   DIR *d = opendir("/sys/bus/iio/devices");
   struct dirent *e;
   if (!d) {
       return -1;
   }
   while ((e = readdir(d)) != NULL) {
       if (strncmp(e->d_name, "iio:device", 10) != 0) {
           continue;
       }
       char npath[256], name[64];
       snprintf(npath, sizeof(npath),
                "/sys/bus/iio/devices/%s/name", e->d_name);
       int fd = open(npath, O_RDONLY);
       if (fd < 0) {
           continue;
       }
       int n = (int)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, len, "/sys/bus/iio/devices/%s", e->d_name);
           closedir(d);
           return 0;
       }
   }
   closedir(d);
   return -1;
}

static const char *key_name(int code)
{
   if (code == KEY_MENU) return "KEY_MENU (KEY3)";
   if (code == KEY_BACK) return "KEY_BACK (KEY4)";
   if (code == KEY_HOME) return "KEY_HOME (KEY5)";
   if (code == KEY_VOLUMEUP) return "VOLUMEUP (公版假键?)";
   if (code == KEY_VOLUMEDOWN) return "VOLUMEDOWN (公版假键?)";
   return "other";
}

int main(int argc, char **argv)
{
   char base[256];
   if (find_iio_by_name("saradc", base, sizeof(base)) == 0) {
       int v0 = -1, v1 = -1;
       char p0[300], p1[300];
       snprintf(p0, sizeof(p0), "%s/in_voltage0_raw", base);
       snprintf(p1, sizeof(p1), "%s/in_voltage1_raw", base);
       read_int_file(p0, &v0);
       read_int_file(p1, &v1);
       printf("IIO %s\n  VIN0/KEY3 raw=%d  VIN1/KEY4 raw=%d\n",
              base, v0, v1);
       printf("  按下 KEY3 时 VIN0 应接近 0,没按应接近满量程。\n");
   } else {
       printf("未找到名字含 saradc 的 IIO 设备。查 &saradc status。\n");
   }

   const char *dev = (argc > 1) ? argv[1] : "/dev/input/event3";
   int fd = open(dev, O_RDONLY);
   if (fd < 0) {
       perror(dev);
       printf("先 cat /proc/bus/input/devices 找 Handlers。\n");
       return 1;
   }
   printf("listening %s ...\n", dev);

   struct input_event ev;
   while (read(fd, &ev, sizeof(ev)) == (ssize_t)sizeof(ev)) {
       if (ev.type == EV_KEY) {
           printf("KEY code=0x%x %s value=%d\n",
                  ev.code, key_name(ev.code), ev.value);
       }
   }
   close(fd);
   return 0;
}

struct input_event 在 32 位用户态 / 64 位内核上,有的环境 timeval 大小不一致,表现为读出来 type/code 全乱。若 od -tx2 看着对、C 程序解析错,改用按字节解析,或直接把 od 当验收工具。不要在这里怀疑按键硬件。


6. ArkTS:NAPI 读 event,页面高亮

HAP 没有权限直接 open("/dev/input/event3") 时,走 NAPI。native 侧和上面的 C 同一份 read 循环,把 code/value 抛给 ArkTS。

/* napi_keys.c 片段:阻塞读一帧 KEY */
#include "napi/native_api.h"
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

static int g_fd = -1;

static napi_value OpenKeys(napi_env env, napi_callback_info info)
{
   size_t argc = 1;
   napi_value args[1];
   napi_get_cb_info(env, info, &argc, args, NULL, NULL);
   char path[64] = {0};
   size_t len = 0;
   napi_get_value_string_utf8(env, args[0], path, sizeof(path), &len);
   if (g_fd >= 0) {
       close(g_fd);
   }
   g_fd = open(path, O_RDONLY | O_NONBLOCK);
   napi_value ret;
   napi_create_int32(env, g_fd, &ret);
   return ret;
}

static napi_value PollKey(napi_env env, napi_callback_info info)
{
   napi_value arr;
   napi_create_array(env, &arr);
   if (g_fd < 0) {
       return arr;
   }
   struct input_event ev;
   while (read(g_fd, &ev, sizeof(ev)) == (ssize_t)sizeof(ev)) {
       if (ev.type != EV_KEY) {
           continue;
       }
       napi_value obj, code, val;
       napi_create_object(env, &obj);
       napi_create_int32(env, ev.code, &code);
       napi_create_int32(env, ev.value, &val);
       napi_set_named_property(env, obj, "code", code);
       napi_set_named_property(env, obj, "value", val);
       napi_set_element(env, arr, 0, obj);
       break;
   }
   return arr;
}

ArkTS 页面(API 11):

import evbio from 'libevbio.so';

@Entry
@Component
struct BoardKeys {
 @State k3: string = '抬起';
 @State k4: string = '抬起';
 @State k5: string = '抬起';
 @State log: string = '打开 /dev/input/eventN 后按键';
 private timer: number = -1;

 build() {
   Column() {
     Text('板载 KEY3 / KEY4 / KEY5').fontSize(22).fontWeight(FontWeight.Bold)
     Row() {
       this.chip('KEY3 MENU', this.k3)
       this.chip('KEY4 BACK', this.k4)
       this.chip('KEY5 HOME', this.k5)
     }.margin({ top: 16 })
     Text(this.log).fontSize(14).margin({ top: 12 })
   }.width('100%').padding(20)
 }

 @Builder chip(title: string, st: string) {
   Column() {
     Text(title).fontSize(16)
     Text(st).fontSize(20).fontColor(st === '按下' ? '#C62828' : '#2E7D32')
   }.padding(12).margin(8).backgroundColor('#EEEEEE').borderRadius(8)
 }

 aboutToAppear() {
   let fd: number = evbio.openKeys('/dev/input/event3');
   this.log = 'open fd=' + fd + '(按 devices 换成 KEY3 那个 event)';
   this.timer = setInterval(() => {
     let arr = evbio.pollKey() as Array<Record<string, number>>;
     if (!arr || arr.length === 0) {
       return;
     }
     let code: number = arr[0]['code'];
     let val: number = arr[0]['value'];
     let st: string = val ? '按下' : '抬起';
     if (code === 0x8b) { this.k3 = st; }
     if (code === 0x9e) { this.k4 = st; }
     if (code === 0x66) { this.k5 = st; }
     if (code === 0x73 || code === 0x72) {
       this.log = '出现音量键:公版 adc_keys 还在 VIN0 上';
     }
   }, 50);
 }

 aboutToDisappear() {
   if (this.timer >= 0) {
     clearInterval(this.timer);
   }
 }
}

应用非 root 读 /dev/input/event* 会 EACCES。开机脚本里:

chmod 0666 /dev/input/event*

SELinux 若是 enforcing,chmod 也不够,开发期先 permissive。这不是按键驱动的问题。

系统桌面自己也会收 KEY_HOME / KEY_BACK。你的 HAP 和 Launcher 可能同时看到同一颗键。测的时候别惊讶「按 KEY5 桌面回了主页」——那是 gpio-keys 上报 HOME 的正常结果。测按键时可以先把 keymap 改成 KEY_F13 这类桌面不认的码,测完再改回来。


7. 改 DTS 之后刷哪

adc-keys 的通道号在设备树。生效 DTB 在 resource 分区 的 rk-kernel.dtb,不是 boot_linux 里那份玩具名字。改通道却只刷 p5,按键还走旧的 VIN3。

hdc shell "cat /proc/partitions"
hdc shell "dd if=/data/resource.img of=/dev/block/mmcblk1p4 bs=4M conv=fsync"
hdc shell "dd if=/data/boot_linux.img of=/dev/block/mmcblk1p5 bs=4M conv=fsync"

介质以 cat /proc/partitions 为准。禁止 of=/dev/mmcblk1p4:路径不存在时 dd 会建一个普通文件,md5 还对。

刷完看 dmesg,再 cat /proc/bus/input/devices 确认名字。名字对了,才去看事件流。有人一上来就 od 乱码,其实读错了 event 号。


8. 现象对照

现象原因处理
按 KEY3 无事件,VIN3 有跳动通道写成了 3`io-channels = <&saradc 0>`
VIN0 会跳,仍无 KEY 事件adc-keys 没 probe,或读错 event看 devices;查 KEYBOARD_ADC
按 KEY3 冒出音量键公版 adc_keys 共 VIN0`&adc_keys { status="disabled"; }`
`iio:device0` 没有 in_voltagedevice0 是 AD9833按 name=saradc 找
KEY5 恒 0 或恒 1极性反了,或脚被别的节点占ACTIVE_LOW;debugfs 看 gpio-8
HAP 读 event 失败权限chmod 0666;开发期 permissive
和矩阵键盘事件对不上根本不是同一设备矩阵看 `keypad` 那个 event
只刷了 boot_linux 通道仍错生效 DTB 在 resource刷 p4
flowchart TD
 A[按 KEY3 无事件] --> B{in_voltage0_raw 按下近 0?}
 B -->|否, ch3 在跳| C[通道写成了 3]
 B -->|否, 全不跳| D[saradc 没 okay / 读错 iio 号]
 B -->|是| E{devices 里有 adc-keys?}
 E -->|无| F[KEYBOARD_ADC / 节点 status]
 E -->|有| G{od 的是这个 event?}
 G -->|否| H[换 Handlers 里的号]
 G -->|是, 还有音量码| I[禁公版 adc_keys]

丝印是给人看的,通道号是给 SARADC 看的。KEY3 接 VIN0,KEY4 接 VIN1,KEY5 接 GPIO0_B0。IIO 按名字找 fe720000.saradc,input 按 /proc/bus/input/devices 对 event。公版那份挂在 VIN0 上的音量键,关掉。


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

Logo

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

更多推荐