1. 为什么要在Android上折腾GPIO从libgpiod说起大家好我是老李一个在嵌入式圈子里摸爬滚打了十多年的老码农。今天想和大家聊聊一个在RK356X这类高性能安卓开发板上特别实用的技能用libgpiod库来玩转GPIO。我知道一提到在Android系统下搞硬件控制很多朋友第一反应是头大。Android应用层和底层硬件之间好像隔着一座山传统的/sys/class/gpio方式又慢又不稳定写个JNI去调用内核驱动更是复杂。别急我今天分享的libgpiod就是官方推荐的、更现代、更优雅的“登山缆车”。简单说GPIO就是开发板上那些可以让你自由控制高低电平或者读取外部信号状态的引脚。它们是连接软件世界和物理世界的桥梁。你想点亮一个LED、读取一个按键的状态、控制一个继电器或者和传感器通信都离不开它。在RK3566/RK3568这类功能强大的国产芯平台上GPIO资源非常丰富不利用起来就太可惜了。那libgpiod是什么呢你可以把它理解成Linux系统下操作GPIO的“标准答案”。它最早由内核社区推出用来替代老旧的、基于sysfs的GPIO接口。它的核心优势在于通过字符设备/dev/gpiochipX直接与内核交互效率高、功能全而且提供了统一的C库和一套超级好用的命令行工具。这意味着你既可以在adb shell里快速调试硬件也可以在C/C程序中稳定地调用API实现复杂逻辑。对于在Android平台做硬件开发、产品原型验证或者自动化测试的工程师来说掌握它就等于拿到了直接与硬件对话的钥匙。2. 为你的RK356X Android系统装备libgpiod理论说再多不如动手装一个。在RK356X的Android 11或更高版本系统上libgpiod通常不是默认自带的需要我们手动把它“移植”到系统镜像里。别被“移植”这个词吓到其实步骤很清晰我带着你走一遍。2.1 获取源码与集成到AOSP首先你需要有一个完整的RK356X Android SDK编译环境。这个环境怎么搭建网上教程很多这里就不展开了。我们重点看怎么把libgpiod的源码拉下来。通常我们会把第三方库放在AOSP源码的external目录下。打开你的终端进入AOSP根目录然后执行cd external git clone https://github.com/technexion-android/platform_external_libgpiod.git这里我用的这个仓库是经过一些Android化适配的版本用起来更省心。当然你也可以直接用官方的内核源码库kernel.googlesource.com但可能需要自己处理一些编译适配问题。对于快速上手我推荐用上面这个现成的。源码下载好后你会在external目录下看到一个libgpiod或者platform_external_libgpiod的文件夹里面就是所有的库源码和命令行工具源码。2.2 修改编译配置让它参与系统构建光有源码还不行得告诉Android的编译系统“嘿把这个库和工具也一起编译进去”。这个操作是通过修改产品的mk文件实现的。这个文件通常位于device/rockchip/rk356x/或类似的产品目录下名字可能是device.mk或product.mk。用你喜欢的编辑器打开这个文件找到定义PRODUCT_PACKAGES的地方。这个变量列出了所有要打包进系统镜像的应用和模块。我们需要在后面添加上libgpiod的相关内容。添加后的效果大致如下PRODUCT_PACKAGES \ ... [其他已有的包] ... gpiodetect \ gpioinfo \ gpioget \ gpioset \ gpiomon \ gpiofind \ libgpiod这里一共添加了7项。前6个是命令行工具最后一个是动态链接库本身。它们的用途我简单列个表你一看就懂工具名功能描述使用场景举例gpiodetect探测系统中有几个GPIO控制器chip查看你的板子有哪些gpiochip0,gpiochip1gpioinfo列出指定GPIO控制器中所有引脚的状态和功能查看gpiochip0里每个引脚是输入、输出还是被占用了gpioget读取一个或多个GPIO引脚的电平值读取按键是否按下高/低电平gpioset设置一个或多个GPIO引脚的电平值点亮一个LED灯gpiomon监控GPIO引脚的电平变化类似“示波器”监听一个脉冲信号或者抓取按键的按下/释放事件gpiofind根据引脚名查找对应的GPIO控制器和偏移量知道引脚叫“GPIO0_A6”用它查具体位置libgpiod提供C语言API的共享库在你的C/C程序里调用实现复杂控制逻辑保存好mk文件后就可以开始编译了。在AOSP根目录下执行你的标准编译命令比如lunch选择你的产品然后make -j8。编译系统会自动处理libgpiod的依赖和编译。编译成功后你需要的所有工具和库就已经集成到你的系统镜像system.img等里了。烧录这个新镜像到你的RK356X开发板开机后在adb shell里输入gpiodetect如果能看到类似gpiochip0 [gpio0] (32 lines)的输出恭喜你第一步就成功了3. 命令行利器五分钟上手硬件调试工具装好了咱们立刻来玩玩。通过adb连接上你的开发板进入shell环境。这些命令行工具是硬件调试的“瑞士军刀”能极大提升你的效率。3.1 侦察兵gpiodetect 和 gpioinfo首先派侦察兵摸清敌情。输入gpiodetect它会告诉你系统里有多少个GPIO控制器。对于RK356X输出通常类似这样rk3568_r:/ # gpiodetect gpiochip0 [gpio0] (32 lines) gpiochip1 [gpio1] (32 lines) gpiochip2 [gpio2] (32 lines) gpiochip3 [gpio3] (32 lines) gpiochip4 [gpio4] (32 lines)这表示有5个GPIO控制器每个控制器管理32个引脚。名字gpio0到gpio4和芯片手册里的GPIO分组是对应的。接下来用gpioinfo看看某个控制器里引脚的详细信息。比如我想看gpiochip0的所有引脚rk3568_r:/ # gpioinfo gpiochip0 gpiochip0 - 32 lines: line 0: PIN_GPIO0_A0 unused input active-high line 1: PIN_GPIO0_A1 unused input active-high line 2: PIN_GPIO0_A2 framebuffer output active-high [used] line 3: PIN_GPIO0_A3 unused input active-high ... [中间省略] ... line 6: PIN_GPIO0_A6 unused input active-high ... [后续省略] ...这个信息太有用了它显示了每个引脚的编号line、名字、使用者、方向和状态。比如line 2被framebuffer占用了是输出模式。而line 6对应GPIO0_A6是空闲的输入模式。我们待会儿就可以用它来测试。3.2 实战操作gpioget 与 gpioset现在我们让line 6GPIO0_A6动起来。假设我已经用杜邦线把这个引脚连接到了一个LED的正极LED负极接地。第一步读取当前电平。虽然之前gpioinfo显示它是输入但我们还是可以读一下rk3568_r:/ # gpioget gpiochip0 6 1输出1表示当前是高电平3.3V0表示低电平0V。因为引脚悬空读到1是正常的内部上拉。第二步把它设置为输出模式并控制LED。注意一个引脚不能同时被多个进程占用。我们先用gpioset把它设为输出并拉低点亮LED假设LED是低电平点亮rk3568_r:/ # gpioset gpiochip0 60执行这条命令后命令会“卡住”因为它以独占模式占用了这个GPIO线并保持输出低电平。这时你应该能看到LED被点亮。按CtrlC可以终止命令引脚会被释放恢复原状可能变回输入LED熄灭。如果想点灯一段时间后自动释放可以用--modetime和--sec参数rk3568_r:/ # gpioset --modetime --sec3 gpiochip0 60这条命令会让引脚输出低电平3秒钟然后自动退出并释放引脚。非常适合做简单的定时控制。3.3 监听者gpiomon这个工具超级好玩它像是一个逻辑分析仪的基础版。比如我想监听line 6上的电平变化。可以先在电脑端用adb shell进入板子然后执行rk3568_r:/ # gpiomon --num-events5 --rising-edge gpiochip0 6这条命令的意思是监控gpiochip0的line 6只捕获上升沿事件从0变1捕获满5次事件后自动退出。然后你用手动的方式把引脚瞬间接地再松开制造一个上升沿终端就会打印出一行时间戳信息。这在调试按键、脉冲计数等场景下非常直观。4. 深入核心在C程序中调用libgpiod API命令行工具适合调试和简单任务但真正的力量在于编程。在你的C/C应用程序中直接调用libgpiod的API可以实现自动化、复杂的逻辑控制。下面我结合一个真实的GPIO回环测试例子带你一步步写代码。4.1 场景与思路GPIO对测假设我们要测试开发板上几十个GPIO引脚的好坏。一个高效的方法是做“回环测试”把两个GPIO引脚用杜邦线短接起来一个设为输出一个设为输入。程序控制输出引脚交替输出高电平和低电平然后立刻从输入引脚读取电平看两者是否相反考虑到可能直接短路我们期望读到的和输出的相反。如果匹配说明这两个引脚功能都是好的。我们需要一个测试数组来定义哪些引脚两两配对。同时为了测试更充分我们还可以交换输入输出的角色再测一次。这就是一个非常实用的生产测试或板级验证程序。4.2 代码拆解从打开设备到释放资源我来把核心代码掰开揉碎了讲。首先你需要包含头文件并定义你的测试引脚映射数组。#include stdio.h #include unistd.h #include gpiod.h #include gpiotest.h // 这个头文件里定义了 test_gpio 二维数组 #define LEN 45 // 测试数组的总项数根据你的gpiotest.h实际大小修改 #define HIGH 1 int main() { struct gpiod_chip *input_chip, *output_chip; struct gpiod_line *input_line, *output_line; bool output_value, input_value; int ret, test_res 0, total_pass 0;第一步遍历与配置。我们用一个外层循环来实现角色互换测两轮。内层循环遍历每一个引脚对。for (int num 0; num 2; num) { int in_chip_idx 0; // 输入引脚在数组中的控制器索引 int in_line_idx 1; // 输入引脚在数组中的线偏移索引 int out_chip_idx 2; // 输出引脚在数组中的控制器索引 int out_line_idx 3; // 输出引脚在数组中的线偏移索引 // 第二轮测试交换输入输出角色 if (1 num) { in_chip_idx 2; in_line_idx 3; out_chip_idx 0; out_line_idx 1; } for (int i 0; i LEN; i) { // 1. 打开输入引脚所在的GPIO控制器 input_chip gpiod_chip_open_by_number(test_gpio[i][in_chip_idx]); if (!input_chip) { perror(Open input chip failed); return -1; } // 2. 从控制器获取输入引脚线对象 input_line gpiod_chip_get_line(input_chip, test_gpio[i][in_line_idx]); if (!input_line) { perror(Get input line failed); gpiod_chip_close(input_chip); return -1; } // 3. 将输入引脚配置为输入方向并请求占用 ret gpiod_line_request_input(input_line, gpiotest-input); if (ret 0) { perror(Request input line failed); gpiod_chip_close(input_chip); return -1; }关键点解析gpiod_chip_open_by_number: 通过编号打开GPIO控制器。这个编号就是gpiodetect看到的gpiochip0中的那个0。gpiod_chip_get_line: 获取具体的引脚线对象。第二个参数是线偏移量也就是gpioinfo里看到的line 0中的那个0。这里最容易出错很多新手会直接填芯片手册上的GPIO编号如GPIO0_A6但这里必须填在这个控制器内的局部偏移量。GPIO0_A6在gpiochip0里通常就是line 6。gpiod_line_request_input: 以输入模式请求占用这条线。第二个参数是消费者名字可以随便起用于在gpioinfo里标识谁在用这个引脚。输出引脚的配置过程完全类似只是函数换成了gpiod_line_request_output并且需要指定一个初始输出电平。4.3 测试逻辑与资源管理配置好输入输出后就开始核心测试了test_res 1; // 假设本次测试通过 for (int j 0; j 2; j) { // 测试拉高和拉低两种情况 // 获取当前输出值然后取反输出 output_value gpiod_line_get_value(output_line); gpiod_line_set_value(output_line, !output_value); usleep(100000); // 等待一小段时间让电平稳定100毫秒 // 也可以使用 sleep(1) 等待1秒便于观察 input_value gpiod_line_get_value(input_line); // 判断输出的是新值输入读到的应该是旧值因为线是直接短接的 // 所以理论上 (新输出值) ! (输入值) 才是正确的 // 更直观的逻辑我们期望输入值等于输出引脚变化前的值 if ( (output_value) input_value ) { // 注意这里根据你的硬件连接逻辑判断 printf(Test FAIL at array[%d], round %d. Output%d, Input%d\n, i, j, !output_value, input_value); test_res -1; break; } }重要提示上面的判断逻辑(output_value) input_value是一个示例。实际逻辑取决于你的硬件连接方式。如果两个引脚是直接短接那么输出变化后输入几乎立即读到新值。但如果你中间加了反相器或者你的测试逻辑是期望读到相反的值判断条件就需要调整。这是硬件测试的关键一定要根据你的实际电路来写判断逻辑。第四步善后工作。每个引脚对测试完成后必须释放资源否则这个引脚会一直被占用影响后续测试或其他程序。if (1 test_res) { printf(PASS gpio array %d\n, i); total_pass 1; } // 释放资源先关line不对直接关chip就行chip关闭时会自动释放其所有line。 gpiod_chip_close(input_chip); gpiod_chip_close(output_chip); } // 结束单个引脚对测试 } // 结束正反轮测试 printf(Total passed GPIO pairs: %d out of %d\n, total_pass, LEN); return 0; }记住gpiod_chip_close()会关闭控制器设备文件描述符并自动释放通过这个控制器申请的所有GPIO线。所以不需要单独调用gpiod_line_release。4.4 编译与部署实战代码写好了怎么把它变成RK356X安卓系统里能跑的程序呢如果你已经把libgpiod集成到系统里了编译就很简单。方法一使用Android的编译系统。在你的external/libgpiod/目录下或者你自己新建的device/.../gpiotest/目录下编写合适的Android.bp或Android.mk文件。例如一个简单的Android.mk可以这样写LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : gpiotest LOCAL_SRC_FILES : gpiotest.c LOCAL_SHARED_LIBRARIES : libgpiod LOCAL_CFLAGS : -Wall -Wextra include $(BUILD_EXECUTABLE)然后在AOSP根目录下执行source build/envsetup.sh lunch your_target-eng # 选择你的RK356X目标 mmm path/to/your/gpiotest/dir编译成功后可执行文件会在out/target/product/rk3566_r/system/bin/gpiotest路径下。方法二使用NDK交叉编译。如果你不想动整个AOSP可以用Android NDK。首先你需要有libgpiod库的头文件和so库文件可以从编译好的系统中提取。然后编写一个CMakeLists.txt用NDK的工具链进行编译。这种方式更灵活适合应用层开发。部署到设备编译出可执行文件后通过adb推送到开发板并赋予执行权限。adb root adb remount # 可能需要如果/system分区只读 adb push gpiotest /system/bin/ adb shell chmod x /system/bin/gpiotest然后就可以在adb shell里直接运行/system/bin/gpiotest了。看到它逐个测试引脚并打印成功失败信息那种成就感是非常实在的。5. 避坑指南与性能优化踩过不少坑之后我总结了一些在RK356X Android平台上使用libgpiod的注意事项希望能帮你节省时间。第一个大坑GPIO编号映射。这是新手最容易晕的地方。芯片手册上写的GPIO0_A6在软件里到底对应gpiochip几、line几没有统一公式。最可靠的方法是查看内核设备树dts文件。在Linux内核源码中搜索你的板型dts文件找gpio0、gpio1这些节点的reg属性结合pinctrl的配置可以找到映射关系。但这需要一定的内核知识。实践出真知用gpioinfo命令。这是最直接的方法。先在gpioinfo gpiochip0的输出里找有没有名字包含A6的引脚。如果没有再查gpiochip1。找到后记下它的line编号。你的程序里就用这个chip编号和line编号。第二个坑引脚复用冲突。RK356X的引脚功能很多是复用的一个引脚可能既是GPIO又是I2C的SDA还是PWM输出。如果你发现用libgpiod操作某个引脚失败gpiod_line_request_*返回错误首先用gpioinfo看看这个引脚是不是已经被别的驱动占用了显示[used]。如果是你需要去修改设备树把其他功能禁用或者在你的驱动/程序中申请时指定正确的标志。第三个坑电平理解与硬件连接。程序里的1和0对应的是物理电平的“高”和“低”。但“高”是3.3V还是1.8V这要看该GPIO Bank的供电电压。另外有些硬件是低电平有效如低电平点亮LED。你的代码逻辑和判断条件一定要和硬件设计匹配。接上线之前最好用万用表量一下。关于性能libgpiod通过字符设备操作比旧的sysfs方式快得多但对于需要极高频率翻转GPIO的场景比如模拟软件PWM仍然有用户态到内核态切换的开销。如果对实时性要求极高可以考虑编写内核驱动或者使用芯片特定的硬件PWM模块。对于绝大多数控制继电器、读取传感器、扫描键盘等应用libgpiod的性能绰绰有余。最后调试时善用gpiomon这个监听工具。它不仅能帮你抓取信号其输出的事件时间戳也能帮你分析程序的响应时间是否满足要求。当你把命令行工具的灵活性和C程序的强大逻辑结合起来在RK356X这样的安卓平台上进行硬件开发与调试就会变得游刃有余。