这次我们直接切入一个很具体的问题:手里有一块 RK3588 开发板,想在 ARM Linux 环境下把“用户自接按键”和“USB 键盘”这两种输入方式都跑通,并且为后续嵌入式 AI 应用留好输入接口。很多 RK3588 教程要么只讲跑模型,要么只讲看门狗和 PWM,真正把 GPIO 按键 -> input 子系统 -> 用户态读取事件这条链路讲完整的并不多。
这篇文章会把这条路从头走到尾:先搞清楚 RK3588 的硬件输入结构,再分别用 libgpiod 和 evdev 实现用户按键与 USB 键盘的读取,最后给出交叉编译、功能验证、性能观察和常见排错方法。无论你是要把按键接到 AI 盒子上做物理交互,还是想给 RK3588 边缘设备接一个键盘做调试控制,这篇都适合先收藏备用。
文章默认的环境是 RK3588 开发板运行标准 ARM64 Linux 系统,开发主机使用 Ubuntu/Debian 系 Linux,目标板通过串口、SSH 或 ADB 访问。部分命令需要按你的实际板卡、内核版本和交叉工具链路径调整,但整体思路通用。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 目标平台 | RK3588(4×Cortex-A76 + 4×Cortex-A55,Mali-G610 GPU,6 TOPS NPU) |
| 开发类型 | ARM 嵌入式 Linux 输入子系统编程 |
| 输入设备 | 独立 GPIO 用户按键、USB HID 键盘 |
| 编程语言 | C(libgpiod + input-event API) |
| 开发方式 | 交叉编译或板上直接 gcc 编译 |
| 用户态调试 | evtest、/proc/interrupts、top、free |
| 扩展方向 | RKNN NPU 推理联动、语音/屏幕交互、类 Android 按键映射 |
| 适合场景 | 边缘 AI 盒子、智能终端按键控制、工业控制器输入采集 |
这里先说明一个概念:无论是自接按键还是 USB 键盘,进入 RK3588 的 ARM Linux 系统后,最终都会汇聚到 Linux input 子系统。用户态的读法本质上是一致的。区别只在于硬件链路不同,一个走 GPIO,一个走 USB HID,一个通常在设备树里被描述为 gpio-keys,另一个直接由内核 usbhid 驱动接管。把这一点先立住,后面写代码就不会乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RK3588 编程与嵌入式 AI 开发的关系
RK3588 是目前瑞芯微面向边缘 AI 场景的主力 SoC,CPU 是大核 + 小核的 ARM 架构,集成 Mali GPU 和最高约 6 TOPS 算力的 NPU。它经常出现在 RKNN 模型部署、YOLOv8 目标检测、多路视频分析、智能座舱、边缘服务器等场景里。
但实际做产品时,你会发现 AI 推理只是其中一环。设备需要开机、需要配置、需要人工调试,通常还要有一些物理输入方式。按键和键盘就是最基础但也最容易忽略的一环。举个典型场景:一台 RK3588 AI 盒子部署了目标检测模型,测试人员需要在不接显示器的情况下控制程序启动或切换模型,一个 GPIO 按键比串口命令更方便;再比如你在板子上跑 RKNN 示例,开发阶段插一个 USB 键盘,直接在终端里敲命令、改参数,效率比反复烧写 SD 卡高得多。
所以本文虽然是从按键和键盘切入,实际解决的是 RK3588 ARM Linux 开发里“人怎么和设备交互”的问题。这部分能力理清之后,后面接 NPU 推理、接屏幕显示、接音频输入输出都会顺畅很多。
3. 环境准备与平台前置条件
3.1 硬件准备
- 一块 RK3588 开发板,系统建议为 ARM64 Debian/Ubuntu 或 Buildroot/Yocto 生成的系统镜像。
- 一个独立按键模块或普通轻触开关,用于 GPIO 输入测试。
- 一个标准 USB 键盘,建议先在有线模式下测试,排除蓝牙和 2.4G 接收器干扰因素。
- 串口调试模块(USB-TTL)或网络/SSH 通道,用于登录板卡。
- 开发主机一台,本文使用 Linux 作为交叉编译主机。
3.2 软件与工具
开发主机需要:
- GCC 交叉编译工具链:
gcc-aarch64-linux-gnu - libgpiod 开发库及工具:交叉版本或目标板版本均可
- 目标板 Linux 内核需要支持 input 子系统和 USB HID,通常默认支持
安装交叉编译工具的参考命令:
bash复制sudo apt update
sudo apt install gcc-aarch64-linux-gnu libgpiod-dev
如果你的开发机是 Ubuntu 系,也可以安装交叉编译用的头文件:
bash复制sudo apt install linux-libc-dev-arm64-cross
目标板上的基础工具尽量使用 BusyBox 自带的最小集之外再加 evtest。如果没有 evtest,可以通过板卡自身的包管理器安装,也可以在上位机交叉编译一个静态版本推送过去:
bash复制# 以 Debian/Ubuntu 目标板为例
sudo apt install evtest libgpiod-utils
3.3 确认板卡系统信息
开发板登录后,先确认架构、内核版本和输入设备情况:
bash复制uname -a
cat /proc/version
ls /dev/input/
如果 /dev/input/ 下能看到 event0、event1、mice 等节点,说明内核 input 子系统正常工作。如果看不到 USB 键盘节点,优先检查 USB 口供电和内核 USB HID 配置。
4. 用户按键编程:基于 libgpiod 读取 GPIO 输入
4.1 GPIO 按键的软件链路
RK3588 的所有 GPIO 都是由 GPIO 控制器管理的。用户自己接的按键,通常一端接 GPIO,另一端接 GND,内部或者外部加上拉电阻。按键按下时引脚电平变化,软件可以通过两种方式感知:
- 轮询模式:循环读取 GPIO 电平,判断是否被按下。
- 中断模式:通过设备树配置 gpio-keys 或者内核中断机制,在电平跳变时由内核上报。
现代 Linux 推荐使用 libgpiod,它取代了老旧的 /sys/class/gpio 接口。libgpiod 提供命令行工具和 C API,非常适合编写嵌入式用户态程序。
4.2 确认 GPIO 编号
GPIO 编号需要根据你在开发板上实际连接的引脚来确定。RK3588 的 GPIO 分为多个 bank,每个 bank 下有多路引脚。不同开发板的原理图、丝印和默认复用关系不一样,因此这里不写死某个编号。
你可以在板卡上先用 libgpiod 工具查看当前可用的 chip 和 line:
bash复制gpiodetect
gpioinfo
gpiodetect 会列出类似于 gpiochip0、gpiochip1 的控制器,gpioinfo 会打印每个 controller 下所有 GPIO line 的方向、当前值等信息。找到你接按键的那一路后,记住 chip 名和 line 编号。
如果板卡没有启用 GPIO 的复用配置,可能还需要改设备树或者使用板卡厂商提供的配置接口,这一步务必参照开发板的 GPIO 复用表。
4.3 编写 libgpiod 按键读取程序
下面是一个标准的 libgpiod 用户态按键读取示例。这里以 gpiochip0 的 line 17 为例,实际请按你的板卡调整。
c复制#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static volatile int running = 1;
void stop(int sig)
{
(void)sig;
running = 0;
}
int main(void)
{
struct gpiod_chip *chip;
struct gpiod_line *line;
int ret, val;
signal(SIGINT, stop);
chip = gpiod_chip_open_by_name("gpiochip0");
if (!chip) {
perror("gpiod_chip_open_by_name");
return 1;
}
line = gpiod_chip_get_line(chip, 17);
if (!line) {
perror("gpiod_chip_get_line");
gpiod_chip_close(chip);
return 1;
}
ret = gpiod_line_request_input(line, "user-key-demo");
if (ret < 0) {
perror("gpiod_line_request_input");
gpiod_chip_close(chip);
return 1;
}
printf("Waiting for key...\n");
while (running) {
val = gpiod_line_get_value(line);
if (val == 0) {
printf("KEY PRESSED\n");
/* 等待释放,同时做简单消抖 */
while (running && gpiod_line_get_value(line) == 0) {
usleep(50000);
}
printf("KEY RELEASED\n");
}
usleep(20000);
}
gpiod_chip_close(chip);
return 0;
}
编译命令(开发机交叉编译):
bash复制aarch64-linux-gnu-gcc -O2 -Wall -o key_demo key_demo.c -lgpiod
如果 libgpiod 头文件和库在你开发机上的路径比较特殊,需要手动指定:
bash复制aarch64-linux-gnu-gcc -O2 -Wall -I/usr/aarch64-linux-gnu/include \
-o key_demo key_demo.c -L/usr/aarch64-linux-gnu/lib -lgpiod
将生成的二进制推送到目标板:
bash复制scp key_demo root@<板卡IP>:/root/
或者用 ADB:
bash复制adb push key_demo /data/local/tmp/
运行:
bash复制chmod +x ./key_demo
./key_demo
按下按键,终端会出现 KEY PRESSED,松开后出现 KEY RELEASED。
4.4 用命令行工具快速验证 GPIO
如果不想写 C 程序,也可以先用命令验证硬件链路是否通畅。例如读取某个 line 的值:
bash复制gpioget gpiochip0 17
监控某个 line 的电平变化:
bash复制gpiomon gpiochip0 17
gpiomon 会阻塞并打印事件,适合快速判断硬件接线是否正确。常见错误是接反了 VCC 和 GND,或者 GPIO 被复用成其他功能,这两种情况都会导致电平值没有变化。
5. USB 键盘编程:基于 input 子系统读取键盘事件
5.1 USB 键盘在 Linux 中的处理方式
USB 键盘插到 RK3588 开发板后,内核的 USB 核心会识别设备描述符,匹配到 HID 类驱动,然后注册成为一个 input 设备。用户态可以从 /dev/input/eventX 节点读取标准化的事件。
每个事件是一个 struct input_event 结构体,定义在 Linux 内核头文件 linux/input.h 中。它包含时间戳、事件类型、事件编码和事件值:
c复制struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
常用的事件类型:
EV_KEY:键盘或按键按下/释放EV_REL:相对位移,如鼠标移动EV_ABS:绝对坐标,如触摸屏EV_SYN:同步事件,表示一组事件的结束
键盘按下时,内核会先发一个 EV_KEY 事件,code 是按键扫描码,value 为 1 表示按下,0 表示释放,2 表示连发。随后会跟随一个 EV_SYN 同步事件。
5.2 确定键盘对应的 event 节点
插入 USB 键盘后,通过以下命令寻找设备:
bash复制cat /proc/bus/input/devices
输出中会列出所有 input 设备,包括设备名、物理位置、系统路径等。一般能看到类似 Name="USB Keyboard" 的条目,下面的 Handlers=sysrq kbd eventX 中就是对应事件节点。
也可以直接看目录变化:
bash复制ls -l /dev/input/
插拔键盘前后对比,新增的 eventX 就是键盘节点。常见的是 event0、event1 或 event3,不同内核枚举顺序不一定。
5.3 用 evtest 快速验证 USB 键盘
先用 evtest 验证硬件链路,比直接写代码快得多:
bash复制evtest
它会列出所有 input 设备,输入对应编号后进入监听状态。按下键盘任意键,屏幕会实时打印 Event type (EV_KEY)... code ... value ...。看到这些打印,说明 USB 键盘从内核到用户态的链路已经通了。
5.4 编写 C 语言键盘读取程序
下面是一个不依赖第三方库、直接使用系统 read() 读取 input 事件的示例:
c复制#include <linux/input.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
struct input_event ev;
int fd;
const char *dev = "/dev/input/event0";
if (argc > 1) {
dev = argv[1];
}
fd = open(dev, O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
printf("Listening on %s\n", dev);
while (1) {
ssize_t n = read(fd, &ev, sizeof(ev));
if (n < (ssize_t)sizeof(ev)) {
continue;
}
if (ev.type == EV_KEY) {
if (ev.value == 1) {
printf("Key pressed, code=%d\n", ev.code);
} else if (ev.value == 0) {
printf("Key released, code=%d\n", ev.code);
}
}
if (ev.type == EV_SYN) {
/* 一组事件处理完毕,可以在这里做业务逻辑 */
}
}
close(fd);
return 0;
}
编译:
bash复制aarch64-linux-gnu-gcc -O2 -Wall -o usb_kbd usb_kbd.c
如果没有交叉工具链,也可以在板卡上直接执行:
bash复制gcc -O2 -Wall -o usb_kbd usb_kbd.c
运行:
bash复制./usb_kbd /dev/input/event1
按下一个按键,终端会输出 Key pressed, code=...。
这里的 code 是内核标准输入事件的键码,和 USB 键盘的物理扫描码不同,也和 ASCII 码不同。需要按键名称映射时,可以引入 libinput,或者自己维护一张键码-键名映射表。对于嵌入式设备,通常直接处理 code 就够了。
6. 功能测试与效果验证
6.1 测试用例设计
建议按下面几个维度设计测试用例:
| 测试项 | 操作 | 预期结果 | 判断标准 |
|---|---|---|---|
| GPIO 按键按下 | 按下用户按键 | 打印 KEY PRESSED | 输出稳定无反复横跳 |
| GPIO 按键释放 | 松开用户按键 | 打印 KEY RELEASED | 释放后电平恢复正常 |
| USB 键盘单键 | 按字母键 A | 打印对应 code | evtest 和自编程序输出一致 |
| USB 键盘组合键 | 同时按 Ctrl+C | 打印两个按键 code | 两个 code 都能收到 |
| 多设备共存 | 同时接按键和键盘 | 两个程序互不干扰 | 各自事件通道独立 |
| 长时间运行 | 连续按压 10 分钟 | 无卡死、无丢失 | 事件计数与按压次数对得上 |
6.2 判断输入链路是否正常的方法
最直接的判断标准是:evtest 的输出和自编程序输出一致,且长时间运行没有系统崩溃或内存持续增长。如果按键消息偶发丢失,优先怀疑 USB 键盘连接线质量、供电不稳或者 GPIO 抖动,而不是代码逻辑。
还可以查看系统中断信息,确认设备确实有产生硬件中断:
bash复制cat /proc/interrupts
搜索包含键盘设备名或者 USB 控制器名的行,观察中断计数是否在按键后增长。
7. 输入事件与 RK3588 嵌入式 AI 应用的联动
7.1 按键驱动 RKNN 推理
在嵌入式 AI 场景里,最自然的使用方式是把按键事件作为 AI 推理的触发信号。例如设备处于低功耗待机状态,用户按下 GPIO 按键后唤醒 NPU,执行一次 RKNN 模型推理;推理完成后再次按键,切换模型。整个过程可以用一个循环程序串起来:
text复制等待按键事件
-> 触发 RKNN 推理
-> 获取推理结果
-> 更新显示或输出
-> 再次等待按键
这种模式在工业检测、边缘计算盒子、AI 门禁等产品里很常见。按键的作用不是处理业务逻辑,而是提供一个人机交互的“开关”。
7.2 USB 键盘作为调试控制接口
另一个实际用法是把 USB 键盘接到 RK3588 开发板上,作为本地调试终端。开发板不接显示器、只用串口时,调试体验有限;接上键盘后,可以运行终端程序,配合板载 GUI 或者直接操作控制台程序。对 AI 模型部署来说,键盘输入的典型用途包括:
- 修改推理脚本参数
- 切换执行不同模型的命令
- 查看 CPU 和内存占用
- 手动启停视频流服务
在这种场景里,键盘不需要自己写代码,内核已经做好了全部驱动工作。真正要保证的是 USB 口的供电质量、设备节点权限和终端模拟器的稳定性。
7.3 输入事件在用户态统一处理
如果你要同时接收按键和键盘事件,建议用一个统一的事件循环。Linux 下可以使用 epoll 同时监听多个 /dev/input/eventX 文件描述符,也可以使用 libinput 这类库统一管理。这样 GPIO 按键和 USB 键盘在应用层看起来没有任何区别,都是标准 input 事件。
伪代码思路如下:
c复制/* 伪代码,示意多设备事件处理 */
int epfd = epoll_create(1);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd_key, ...);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd_usb, ...);
while (1) {
int n = epoll_wait(epfd, events, 8, -1);
for (int i = 0; i < n; i++) {
read(events[i].data.fd, &ev, sizeof(ev));
/* 根据 events[i].data.fd 区分设备 */
handle_input(events[i].data.fd, &ev);
}
}
8. 资源占用与性能观察
8.1 观察 CPU 和内存占用
在板卡上运行按键读取程序时,用 top 查看进程占用:
bash复制top -p $(pidof key_demo)
- 轮询模式下,程序占用一个 CPU 核的一部分时间,取决于 sleep 间隔。示例代码中
usleep(20000)大概是 20ms 轮询一次,CPU 占用很低。 - 使用
gpiomon或者设备树中断方式,GPIO 事件由内核中断上报,用户态基本不消耗额外 CPU,占用比轮询更低。 - USB 键盘事件由中断驱动,用户态阻塞在
read()上,CPU 使用率可以忽略。
8.2 内存占用观察
用 free -m 查看系统可用内存。按键读取程序本身只占用很小内存,如果程序出现内存持续增长,多半是没有正确释放动态分配的对象,属于代码问题而不是硬件问题。
长时间运行时,建议观察进程的 VmRSS:
bash复制cat /proc/<pid>/status | grep VmRSS
标准压在几十 MB 以下都算正常,具体取决于你的完整业务逻辑。
8.3 降低资源占用的方法
- GPIO 输入优先考虑设备树配置为中断方式,而不是应用层 busy loop。
- 应用层轮询间隔不要小于 10ms,否则消抖效果变差,CPU 占用反而升高。
- USB 键盘读取使用阻塞式
read(),不要使用非阻塞方式空转。 - 不需要同时打开所有 input 节点,只打开业务实际用到的设备即可。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
gpiod_chip_open_by_name 报错 |
libgpiod 库未安装或 chip 名不对 | 执行 gpiodetect 确认 chip 名 |
安装 libgpiod,改用实际 chip 名 |
| GPIO 电平无变化 | 引脚复用冲突或接线错误 | 检查原理图和复用表,用 gpioget 读值 |
修改设备树复用为 GPIO,重新接线 |
| 按键按下后打印多次 | 抖动未消抖 | 用示波器或 gpiomon 观察波形 |
加消抖延时,软件滤波 |
| 找不到 USB 键盘事件节点 | USB 口供电不足或内核无 usbhid | 插到其他 USB 口,查看 dmesg |
检查内核配置,换口测试 |
open /dev/input/eventX 权限不足 |
当前用户不在 input 组 | ls -l /dev/input/ 查看权限 |
添加 udev 规则或使用 root 运行 |
evtest 未安装 |
目标板文件系统裁剪 | 查看包管理器是否可用 | 交叉编译静态版推送到板卡 |
交叉编译时报 linux/input.h 缺失 |
缺少 arm64 头文件 | 检查开发机库文件 | 安装 linux-libc-dev-arm64-cross |
| 按一次键输出多个不同 code | 键盘开启了多媒体键或自动重复 | 用 evtest 查看原始事件 | 在应用层做去重处理 |
| 同时接按键和键盘时互相干扰 | 两个程序同时轮询设备 | 确认使用不同 event 节点 | 用 epoll 统一管理 |
最常见的坑集中在两个地方:一个是 GPIO 复用了其他功能,导致用户态读到的是固定电平;另一个是 USB 键盘插入后 /dev/input/ 下出现了多个事件节点,读错节点导致没有输出。排错时先确认 evtest 能正常工作,再跑自编程序。
10. 最佳实践与合规提醒
10.1 工程化建议
- 先在命令行工具层验证硬件,再写业务代码,节省排查时间。
- GPIO 编号、事件节点路径不要硬编码在程序里,建议通过配置校验后传入。
- 设备树中配置好的 gpio-keys 内核驱动会让某些按键自动映射为标准键盘事件,此时应用层直接读 input 节点即可,不需要再用 libgpiod。
- 批量生产或长期运行前,做一次 24 小时稳定性测试,关注中断计数、CPU 占用和内存变化。
- 输出日志建议加时间戳,方便回看按键时序和定位问题。
- 开发阶段保留串口登录通道,防止调试程序把系统卡死。
10.2 合规与安全边界
- 自接按键和 USB 键盘事件采集只能用于自己拥有或获得授权的设备,不得用于录制他人键盘输入、窃取账号信息或非法监控。
- 涉及人脸识别、声音采集、摄像头联动等 AI 功能时,必须遵守当地法律法规和平台安全规范,明确告知用户并取得合法授权。
- 在公开网络环境下开放设备端口或输入事件服务时,要做好访问控制,避免被未授权终端接入。
- 调试阶段不要使用弱密码或默认 SSH 口令,输入事件日志如果涉及敏感内容,要及时加密清理。
- 模型和代码的二次分发需要注意开源协议,尤其是 RKNN 工具链相关组件的授权范围。
11. 总结与下一步
RK3588 的用户按键和 USB 键盘编程,核心不是某个晦涩的硬件寄存器,而是理解 Linux input 子系统这套标准输入链路。先把 GPIO 按键用 libgpiod 跑通,再把 USB 键盘用 evdev 读出来,剩下的事情就是业务逻辑如何消费这些事件。
这篇文章最值得记住的两个结论:第一,GPIO 按键优先配置为中断或使用 gpiomon 验证,不要长期用 busy loop 硬顶;第二,USB 键盘由内核默认驱动接管,用户态读 eventX 即可,不要把时间浪费在写 HID 驱动上。
接下来建议做的两件事:第一,用 evtest 跑一次你板卡上所有 input 设备的完整事件流,确认每个设备硬件工作正常;第二,用 epoll 把多个输入源合并成一个事件循环,然后接一个简单的 RKNN 推理任务,让按键真正控制一次 NPU 推理。这样你就把 RK3588 的输入层和应用层打通了,后面再接摄像头、显示器、音频等设备,整体思路完全一致。
