Android Studio集成libgpiod:从交叉编译到JNI控制GPIO实战

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

常见的错误和解决方法:

  1. "Permission denied":需要root权限或修改selinux策略
  2. "Device or resource busy":GPIO线已被其他进程占用
  3. "No such device":GPIO芯片名称错误或驱动未加载

5.3 性能优化建议

在实时控制场景中,性能很重要:

  1. 避免频繁打开关闭:在app启动时打开GPIO芯片,退出时关闭
  2. 批量操作:如果需要控制多个GPIO,尽量使用gpiod_line_request_bulk_output
  3. 缓存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访问能力。

「LLM那些事」系列第 4 篇《上下文窗口的边界》,文章连接:https://blog.csdn.net/houwenjin/article/details/163999753。 演示什么:在「预测」Sheet 的黄色格子里输入一句话(默认「来泡一杯」),四个「模型」——分别只统计最后 1 / 2 / 3 / 4 个字的 n-gram 查表——同时预测下一个字。同一个输入,看的上下文越长,候选越少、预测越确定: ┌────────────────┬──────────┬───────────────┬──────┐ │ 只看最后几个字 │ 用的前缀 │ 候选下一字数 │ 预测 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 1 个 │ 杯 │ 3(茶/子/水) │ 模糊 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 2 个 │ 一杯 │ 2(茶/水) │ 收窄 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 3 个 │ 泡一杯 │ 1(茶) │ 确定 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 4 个 │ 来泡一杯 │ 1(茶) │ 确定 │ └────────────────┴──────────┴───────────────┴──────┘
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值