1. 环境准备与交叉编译基础
在开始Android Studio集成libgpiod之前,我们需要先准备好交叉编译环境。这个过程就像是给不同国家的电器准备转换插头,让PC上的代码能在ARM架构的开发板上正常运行。我当初做rk3568开发板项目时,花了整整两天时间折腾环境配置,这里把踩过的坑都给你总结好了。
1.1 安装必要的编译工具
首先确保你的Ubuntu系统已经安装了基础编译工具。打开终端,依次执行以下命令:
sudo apt update
sudo apt-get install -y autoconf automake libtool autoconf-archive m4 pkg-config
这里有个常见坑点:如果遇到"Repository does not have a Release file"错误,通常是因为软件源配置有问题。检查/etc/apt/sources.list和/etc/apt/sources.list.d/目录下的文件,删除或注释掉无效的软件源。我上次就遇到一个陈旧的Java PPA源导致的错误,用vim编辑删除后立即恢复正常。
1.2 获取libgpiod源码
推荐使用v1.6.x稳定版本,兼容性最好:
git clone -b v1.6.x https://github.com/brgl/libgpiod.git
cd libgpiod
如果网络问题导致git克隆失败,可以直接在GitHub页面下载zip包,然后解压:
unzip libgpiod-1.6.x.zip
cd libgpiod-1.6.x
2. 交叉编译libgpiod库
交叉编译是整个流程中最关键的一步,就像是为Android设备定制专属的工具箱。这里需要配置正确的环境变量和编译参数。
2.1 设置交叉编译环境
根据你的NDK安装路径调整环境变量。我使用的是NDK 26.2,路径可能需要相应修改:
export NDK=/home/your_username/Android/Sdk/ndk/26.2.11394342
export TOOLCHAIN=${NDK}/toolchains/llvm/prebuilt/linux-x86_64
export TARGET=aarch64-linux-android
export API=29
export AR=${TOOLCHAIN}/bin/llvm-ar
export CC=${TOOLCHAIN}/bin/${TARGET}${API}-clang
export CXX=${TOOLCHAIN}/bin/${TARGET}${API}-clang++
export LD=${TOOLCHAIN}/bin/ld
export RANLIB=${TOOLCHAIN}/bin/llvm-ranlib
export STRIP=${TOOLCHAIN}/bin/llvm-strip
注意:TARGET变量要根据你的设备架构选择。rk3568是arm64-v8a架构,所以用aarch64-linux-android。如果是32位ARM设备,应该用armv7a-linux-androideabi。
2.2 配置和编译
首先运行autogen.sh生成配置脚本:
bash autogen.sh
成功后你会看到configure文件生成。接下来是关键步骤:编辑configure文件,删除所有包含"rpl_malloc"的语句。如果不做这一步,后面运行时会出现"dlopen failed: cannot locate symbol rpl_malloc"错误。
sudo ./configure CC=${CC} CXX=${CXX} AR=${AR} RANLIB=${RANLIB} STRIP=${STRIP} \
--prefix=/home/android_libgpiod \
--host=${TARGET} \
--enable-tools=no \
--enable-bindings-cxx=no
这里--enable-tools=no和--enable-bindings-cxx=no可以避免编译不必要的工具和C++绑定,减少依赖问题。
编译和安装:
make -j$(nproc)
sudo make install
编译完成后,在--prefix指定的目录中就能找到libgpiod.so和gpiod.h文件。用file命令验证一下架构是否正确:
file /home/android_libgpiod/lib/libgpiod.so
应该显示"ELF 64-bit LSB shared object, ARM aarch64"之类的信息。
3. Android Studio项目集成
现在进入Android开发环境集成阶段。这部分需要仔细配置CMake和Gradle,确保库文件正确链接。
3.1 项目结构配置
在Android Studio中,切换到Project视图,按照以下结构放置文件:
app/
├── src/
│ ├── main/
│ │ ├── cpp/
│ │ │ ├── include/ # 放置gpiod.h
│ │ │ ├── jni/
│ │ │ │ ├── arm64-v8a/ # 放置libgpiod.so
│ │ │ │ └── x86_64/ # 模拟器用的库(可选)
│ │ │ └── native-lib.cpp
│ │ └── java/
└── build.gradle.kts
记得右键点击so文件,选择"Link C++ Project with Gradle"时勾选"Android.mk"或"CMakeLists.txt"。
3.2 CMakeLists.txt配置
CMake的配置是整个集成过程的核心,需要正确定义导入库:
cmake_minimum_required(VERSION 3.22.1)
project("myapplication")
# 设置包含目录
include_directories(src/main/cpp/include)
# 导入预编译的libgpiod库
add_library(libgpiod SHARED IMPORTED)
set_target_properties(libgpiod PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/jni/${CMAKE_ANDROID_ARCH_ABI}/libgpiod.so
)
# 添加你的原生库
add_library(native-lib SHARED
src/main/cpp/native-lib.cpp
)
# 链接库
target_link_libraries(native-lib
libgpiod
log
)
提示:IMPORTED_LOCATION路径一定要正确,这里的${CMAKE_ANDROID_ARCH_ABI}会根据构建的ABI自动替换为arm64-v8a等值。
3.3 Gradle配置
在app/build.gradle.kts的android块中添加NDK配置:
android {
compileSdk = 34
defaultConfig {
// ... 其他配置
ndk {
abiFilters.add("arm64-v8a")
// 如果需要其他ABI,可以添加
// abiFilters.add("armeabi-v7a")
// abiFilters.add("x86_64")
}
}
externalNativeBuild {
cmake {
path = file("CMakeLists.txt")
version = "3.22.1"
}
}
}
同步项目后,如果配置正确,应该能在"Build"输出中看到CMake生成成功的信息。
4. JNI接口实现与GPIO控制
现在来到最有趣的部分:通过JNI调用libgpiod控制实际的GPIO引脚。我会用一个完整的LED控制示例来说明。
4.1 Java层Native方法定义
首先在Java类中定义native方法:
package com.example.gpiocontrol;
public class GpioController {
private static final String TAG = "GpioController";
// 加载库 - 注意顺序!
static {
System.loadLibrary("gpiod");
System.loadLibrary("native-lib");
}
// Native方法声明
public static native boolean openGpioChip(String chipName);
public static native void setGpioValue(int lineOffset, boolean value);
public static native void closeGpioChip();
// 简化版的LED控制方法
public static void setLed(boolean on) {
setGpioValue(123, on); // 123是GPIO线偏移量,根据实际硬件修改
}
}
重要:System.loadLibrary()的调用顺序很关键!必须先加载依赖库(libgpiod),再加载你自己的库。
4.2 JNI实现代码
在native-lib.cpp中实现具体的GPIO控制逻辑:
#include <jni.h>
#include <string>
#include <gpiod.h>
#include <android/log.h>
#define LOG_TAG "GPIO_NATIVE"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
static struct gpiod_chip *chip = nullptr;
static struct gpiod_line *line = nullptr;
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_gpiocontrol_GpioController_openGpioChip(
JNIEnv *env,
jclass clazz,
jstring chipName) {
const char *c_chipName = env->GetStringUTFChars(chipName, nullptr);
// 打开GPIO芯片
chip = gpiod_chip_open_by_name(c_chipName);
if (!chip) {
LOGE("Failed to open GPIO chip: %s", c_chipName);
env->ReleaseStringUTFChars(chipName, c_chipName);
return JNI_FALSE;
}
LOGI("Successfully opened GPIO chip: %s", c_chipName);
env->ReleaseStringUTFChars(chipName, c_chipName);
return JNI_TRUE;
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_gpiocontrol_GpioController_setGpioValue(
JNIEnv *env,
jclass clazz,
jint lineOffset,
jboolean value) {
if (!chip) {
LOGE("GPIO chip not opened!");
return;
}
// 获取GPIO线
line = gpiod_chip_get_line(chip, lineOffset);
if (!line) {
LOGE("Failed to get GPIO line: %d", lineOffset);
return;
}
// 设置为输出模式
if (gpiod_line_request_output(line, "android_app", 0) < 0) {
LOGE("Failed to set line as output");
return;
}
// 设置电平值
int ret = gpiod_line_set_value(line, value ? 1 : 0);
if (ret < 0) {
LOGE("Failed to set GPIO value");
} else {
LOGI("Set GPIO %d to %s", lineOffset, value ? "HIGH" : "LOW");
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_gpiocontrol_GpioController_closeGpioChip(
JNIEnv *env,
jclass clazz) {
if (line) {
gpiod_line_release(line);
line = nullptr;
}
if (chip) {
gpiod_chip_close(chip);
chip = nullptr;
}
LOGI("GPIO chip closed");
}
这段代码实现了完整的GPIO控制流程:打开芯片、设置引脚为输出模式、控制电平和释放资源。我在实际项目中发现,一定要在不再使用时及时释放GPIO线,否则其他进程无法访问这些资源。
5. 硬件调试与常见问题解决
集成完成后,硬件调试阶段可能会遇到各种问题。这里分享一些实战经验。
5.1 权限问题处理
在Android上访问GPIO需要root权限或者相应的selinux策略。在调试阶段,可以先使用adb root和adb shell setenforce 0来获取完整权限。
在设备上测试权限:
adb shell
su
ls -l /dev/gpiochip* # 检查GPIO设备是否存在
cat /sys/kernel/debug/gpio # 查看GPIO状态(需要内核支持)
如果设备没有root权限,需要在init.rc或者sepolicy中添加相应的权限规则。对于rk3568,通常需要修改device/rockchip/rk3568/sepolicy目录下的相关文件。
5.2 调试技巧与日志查看
启用详细的日志输出有助于调试:
// 在JNI代码中添加详细日志
LOGI("Trying to open chip: %s", c_chipName);
// ...
LOGI("Chip opened successfully, number of lines: %d", gpiod_chip_num_lines(chip));
使用adb logcat查看日志:
adb logcat -s GPIO_NATIVE:V *:S
常见的错误和解决方法:
- "Permission denied":需要root权限或修改selinux策略
- "Device or resource busy":GPIO线已被其他进程占用
- "No such device":GPIO芯片名称错误或驱动未加载
5.3 性能优化建议
在实时控制场景中,性能很重要:
- 避免频繁打开关闭:在app启动时打开GPIO芯片,退出时关闭
- 批量操作:如果需要控制多个GPIO,尽量使用gpiod_line_request_bulk_output
- 缓存GPIO线:重复使用的GPIO线可以缓存起来,避免重复查找
// 批量操作示例
struct gpiod_line_bulk bulk;
gpiod_line_bulk_init(&bulk);
// 添加多个线
gpiod_line_bulk_add(&bulk, line1);
gpiod_line_bulk_add(&bulk, line2);
// 批量设置值
int values[2] = {1, 0};
gpiod_line_set_value_bulk(&bulk, values);
6. 实际应用案例:LED呼吸灯控制
现在我们来一个实际案例:实现PWM呼吸灯效果。虽然libgpiod本身不直接支持PWM,但我们可以用软件模拟。
6.1 PWM实现原理
通过快速切换GPIO电平来模拟PWM效果:
extern "C" JNIEXPORT void JNICALL
Java_com_example_gpiocontrol_GpioController_startPwm(
JNIEnv *env,
jclass clazz,
jint lineOffset,
jint frequency,
jfloat dutyCycle) {
const int periodUs = 1000000 / frequency; // 周期(微秒)
const int highTimeUs = periodUs * dutyCycle;
const int lowTimeUs = periodUs - highTimeUs;
while (!shouldStopPwm) {
setGpioValue(lineOffset, true);
usleep(highTimeUs);
setGpioValue(lineOffset, false);
usleep(lowTimeUs);
}
}
注意:这种软件PWM会占用CPU资源,对于精确的PWM控制建议使用硬件PWM模块。
6.2 完整的LED控制类
public class LedController {
private static final int LED_GPIO_LINE = 123;
private static Thread pwmThread;
private static volatile boolean running = false;
public static void init() {
GpioController.openGpioChip("gpiochip0");
}
public static void setBrightness(float brightness) {
// brightness范围0.0f-1.0f
if (brightness <= 0) {
GpioController.setGpioValue(LED_GPIO_LINE, false);
} else if (brightness >= 1) {
GpioController.setGpioValue(LED_GPIO_LINE, true);
} else {
startPwm(brightness);
}
}
private static void startPwm(final float dutyCycle) {
stopPwm();
running = true;
pwmThread = new Thread(() -> {
nativeStartPwm(LED_GPIO_LINE, 100, dutyCycle);
});
pwmThread.start();
}
private static native void nativeStartPwm(int line, int freq, float dutyCycle);
public static void stopPwm() {
running = false;
if (pwmThread != null) {
try {
pwmThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void release() {
stopPwm();
GpioController.closeGpioChip();
}
}
这个案例展示了如何通过JNI和libgpiod实现复杂的硬件控制功能。在实际项目中,我还添加了异常处理、状态回调等功能,让Java层能够更好地监控硬件状态。
整个集成过程虽然有些复杂,但一旦打通了这条路,就能在Android上实现各种有趣的硬件交互项目。从简单的LED控制到复杂的传感器数据采集,libgpiod提供了稳定可靠的GPIO访问能力。

977

被折叠的 条评论
为什么被折叠?



