板载按键key的ADC转换和信号控制—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
触摸之外,底板上还有几颗用户键。今天把模数转换那两颗、普通输入那一颗对上,丝印编号不是通道号。
底板上三颗键,丝印 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-3、adc-keys-1、gpio-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 位秒+纳秒)。按下去会看到:
| type | code | value | 含义 |
|---|---|---|---|
| 0x0001 EV_KEY | 0x008b KEY_MENU | 1 | KEY3 按下 |
| 0x0000 EV_SYN | 0 | 0 | 同步 |
| 0x0001 EV_KEY | 0x008b | 0 | KEY3 松开 |
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_voltage | device0 是 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
更多推荐



所有评论(0)