【RK3576 安卓 JNI/NDK 系列 07】RK3576 实战(一):JNI 调用 GPIO 驱动点亮 LED

目录

本章核心说明

[一、RK3576 GPIO 硬件基础](#一、RK3576 GPIO 硬件基础)

[1. RK3576 GPIO 分组规则](#1. RK3576 GPIO 分组规则)

[2. 硬件准备](#2. 硬件准备)

[二、GPIO 控制的两种实现方式](#二、GPIO 控制的两种实现方式)

[方式 1:sysfs 方式(新手入门级)](#方式 1:sysfs 方式(新手入门级))

[核心操作步骤(adb shell 命令行可直接验证)](#核心操作步骤(adb shell 命令行可直接验证))

[C++ 层 sysfs 方式实现代码](#C++ 层 sysfs 方式实现代码)

[方式 2:gpiochip + ioctl 标准方式(工业级推荐)](#方式 2:gpiochip + ioctl 标准方式(工业级推荐))

核心依赖

[核心 ioctl 命令说明](#核心 ioctl 命令说明)

[C++ 层标准 ioctl 方式实现代码](#C++ 层标准 ioctl 方式实现代码)

[三、JNI 层封装与安卓 APP 实现](#三、JNI 层封装与安卓 APP 实现)

[1. Java 层 native 方法定义](#1. Java 层 native 方法定义)

[2. JNI 层接口实现](#2. JNI 层接口实现)

[3. CMakeLists.txt 配置](#3. CMakeLists.txt 配置)

[4. 布局文件(activity_main.xml)](#4. 布局文件(activity_main.xml))

四、编译运行与调试

五、新手必避的核心坑点

下章预告


本章核心说明

本章基于上一章讲解的设备节点与 ioctl 操作,完成 RK3576 平台的 GPIO 输入输出控制实战,实现安卓 APP 通过 JNI 调用底层 GPIO 驱动,完成 LED 点亮 / 熄灭、引脚电平读取功能。全程采用 Linux 标准 GPIO 子系统规范,适配 RK3576 原厂安卓固件,代码可直接复用至工业级项目。

一、RK3576 GPIO 硬件基础

1. RK3576 GPIO 分组规则

RK3576 芯片共分为 4 组 GPIO bank,分别为 GPIO0、GPIO1、GPIO2、GPIO3,每组 bank 包含 32 个独立引脚,又按 A/B/C/D 分为 4 个小组,每组 8 个引脚,对应关系为:A=0、B=1、C=2、D=3。

引脚编号计算公式(核心,新手必须记牢)

plaintext

复制代码
GPIOx_y_z 对应的全局编号 = x * 32 + y * 8 + z

示例:

  • GPIO3_A5:332 + 08 +5 = 101
  • GPIO2_B3:232 + 18 +3 = 75
  • GPIO0_D7:032 + 38 +7 = 31

2. 硬件准备

  1. 开发板:RK3576 核心板 + 底板(野火、正点原子、瑞芯微 EVB 板均可)
  2. 硬件物料:直插 LED 灯、1k 限流电阻、杜邦线、面包板
  3. 接线规则
    • LED 长脚(正极)通过限流电阻接 RK3576 的 GPIO 引脚(示例用 GPIO3_A5,编号 101)
    • LED 短脚(负极)接开发板 GND
    • 若开发板自带用户 LED,可直接使用板载 LED,无需额外接线,只需确认对应 GPIO 编号即可
  4. 前置环境 :开发板已开启 adb root、关闭 SELinux、给/dev/gpiochip*设备节点配置 777 权限,操作步骤见上一章权限配置章节。

二、GPIO 控制的两种实现方式

RK3576 安卓系统下,GPIO 控制主要有两种实现方式,分别适用于不同场景,本章会分别讲解实现逻辑,重点讲解标准 ioctl 方式。

方式 1:sysfs 方式(新手入门级)

sysfs 是 Linux 内核提供的虚拟文件系统,GPIO 子系统会在/sys/class/gpio/目录下暴露操作接口,通过读写文件的方式控制 GPIO,优点是逻辑简单、上手快,缺点是性能低、实时性差,不适合高频操作场景,仅推荐用于调试。

核心操作步骤(adb shell 命令行可直接验证)
  1. 导出 GPIO 引脚到用户空间:echo 101 > /sys/class/gpio/export
  2. 设置引脚方向为输出:echo out > /sys/class/gpio/gpio101/direction
  3. 设置引脚高电平(点亮 LED):echo 1 > /sys/class/gpio/gpio101/value
  4. 设置引脚低电平(熄灭 LED):echo 0 > /sys/class/gpio/gpio101/value
  5. 释放 GPIO 引脚:echo 101 > /sys/class/gpio/unexport
C++ 层 sysfs 方式实现代码

cpp

运行

复制代码
#include <jni.h>
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <android/log.h>

#define LOG_TAG "RK3576_GPIO_SYSFS"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// 导出GPIO引脚
int gpio_export(int gpio_num) {
    char path[64];
    int fd, len;
    char buf[8];

    sprintf(path, "/sys/class/gpio/export");
    fd = open(path, O_WRONLY);
    if (fd < 0) {
        LOGE("export打开失败: %s", strerror(errno));
        return -1;
    }
    len = sprintf(buf, "%d", gpio_num);
    write(fd, buf, len);
    close(fd);
    return 0;
}

// 设置GPIO方向
int gpio_set_direction(int gpio_num, int is_output) {
    char path[64];
    int fd;

    sprintf(path, "/sys/class/gpio/gpio%d/direction", gpio_num);
    fd = open(path, O_WRONLY);
    if (fd < 0) {
        LOGE("direction打开失败: %s", strerror(errno));
        return -1;
    }
    if (is_output) {
        write(fd, "out", 3);
    } else {
        write(fd, "in", 2);
    }
    close(fd);
    return 0;
}

// 设置GPIO电平
int gpio_set_level(int gpio_num, int level) {
    char path[64];
    int fd;
    char buf[8];

    sprintf(path, "/sys/class/gpio/gpio%d/value", gpio_num);
    fd = open(path, O_WRONLY);
    if (fd < 0) {
        LOGE("value打开失败: %s", strerror(errno));
        return -1;
    }
    sprintf(buf, "%d", level);
    write(fd, buf, 1);
    close(fd);
    return 0;
}

// 读取GPIO电平
int gpio_get_level(int gpio_num) {
    char path[64];
    int fd;
    char buf[8];
    int level;

    sprintf(path, "/sys/class/gpio/gpio%d/value", gpio_num);
    fd = open(path, O_RDONLY);
    if (fd < 0) {
        LOGE("value打开失败: %s", strerror(errno));
        return -1;
    }
    read(fd, buf, 1);
    level = atoi(buf);
    close(fd);
    return level;
}

// 释放GPIO引脚
int gpio_unexport(int gpio_num) {
    char path[64];
    int fd, len;
    char buf[8];

    sprintf(path, "/sys/class/gpio/unexport");
    fd = open(path, O_WRONLY);
    if (fd < 0) {
        LOGE("unexport打开失败: %s", strerror(errno));
        return -1;
    }
    len = sprintf(buf, "%d", gpio_num);
    write(fd, buf, len);
    close(fd);
    return 0;
}

方式 2:gpiochip + ioctl 标准方式(工业级推荐)

这是 Linux GPIO 子系统的标准实现方式,基于/dev/gpiochip*设备节点,通过 ioctl 命令完成引脚配置和控制,优点是性能高、实时性好、支持批量操作、无 sysfs 的权限冲突问题,是工业级项目的首选方案,也是本章的核心内容。

核心依赖

所有 ioctl 命令码和结构体均定义在 Linux 标准头文件<linux/gpio.h>中,无需自定义,NDK 已内置该头文件,直接 include 即可使用。

核心 ioctl 命令说明

表格

命令码 作用
GPIO_GET_CHIPINFO_IOCTL 获取 GPIO 芯片信息,验证设备节点有效性
GPIO_GET_LINEINFO_IOCTL 获取指定引脚的信息,查看引脚占用状态
GPIO_GET_LINEHANDLE_IOCTL 获取引脚的操作句柄,配置引脚输入 / 输出方向、默认电平
GPIOHANDLE_GET_LINE_VALUES_IOCTL 读取引脚当前电平
GPIOHANDLE_SET_LINE_VALUES_IOCTL 设置引脚电平
C++ 层标准 ioctl 方式实现代码

cpp

运行

复制代码
#include <jni.h>
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <linux/gpio.h>
#include <android/log.h>

#define LOG_TAG "RK3576_GPIO_IOCTL"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// 全局GPIO芯片设备句柄,RK3576 GPIO3对应/dev/gpiochip3
static int gpio_chip_fd = -1;
// 引脚操作句柄
static int gpio_line_fd = -1;

// 打开GPIO芯片设备节点
int gpio_open_chip(const char *chip_path) {
    if (gpio_chip_fd >= 0) {
        LOGD("GPIO芯片已打开,fd=%d", gpio_chip_fd);
        return 0;
    }
    gpio_chip_fd = open(chip_path, O_RDWR);
    if (gpio_chip_fd < 0) {
        LOGE("打开GPIO芯片失败: %s, 原因: %s", chip_path, strerror(errno));
        return -1;
    }
    LOGD("GPIO芯片打开成功,fd=%d", gpio_chip_fd);
    return 0;
}

// 配置GPIO引脚为输出模式,获取操作句柄
int gpio_config_output(int gpio_offset, int default_level) {
    if (gpio_chip_fd < 0) {
        LOGE("GPIO芯片未打开");
        return -1;
    }
    if (gpio_line_fd >= 0) {
        close(gpio_line_fd);
        gpio_line_fd = -1;
    }

    struct gpiohandle_request req;
    memset(&req, 0, sizeof(req));
    // 配置引脚偏移量,单引脚操作
    req.lineoffsets[0] = gpio_offset;
    req.lines = 1;
    // 配置为输出模式
    req.flags = GPIOHANDLE_REQUEST_OUTPUT;
    // 配置默认电平
    req.default_values[0] = default_level;
    // 给句柄命名,调试用
    strcpy(req.consumer_label, "rk3576_gpio_led");

    // 调用ioctl获取引脚操作句柄
    int ret = ioctl(gpio_chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
    if (ret < 0) {
        LOGE("获取GPIO句柄失败,原因: %s", strerror(errno));
        return -1;
    }
    gpio_line_fd = req.fd;
    LOGD("GPIO引脚配置成功,句柄fd=%d", gpio_line_fd);
    return 0;
}

// 设置GPIO输出电平
int gpio_set_level(int level) {
    if (gpio_line_fd < 0) {
        LOGE("GPIO引脚未配置");
        return -1;
    }
    struct gpiohandle_data data;
    memset(&data, 0, sizeof(data));
    data.values[0] = level;

    int ret = ioctl(gpio_line_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
    if (ret < 0) {
        LOGE("设置GPIO电平失败,原因: %s", strerror(errno));
        return -1;
    }
    LOGD("GPIO电平设置成功: %d", level);
    return 0;
}

// 读取GPIO引脚电平
int gpio_get_level() {
    if (gpio_line_fd < 0) {
        LOGE("GPIO引脚未配置");
        return -1;
    }
    struct gpiohandle_data data;
    memset(&data, 0, sizeof(data));

    int ret = ioctl(gpio_line_fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
    if (ret < 0) {
        LOGE("读取GPIO电平失败,原因: %s", strerror(errno));
        return -1;
    }
    LOGD("GPIO当前电平: %d", data.values[0]);
    return data.values[0];
}

// 关闭GPIO资源
void gpio_close() {
    if (gpio_line_fd >= 0) {
        close(gpio_line_fd);
        gpio_line_fd = -1;
    }
    if (gpio_chip_fd >= 0) {
        close(gpio_chip_fd);
        gpio_chip_fd = -1;
    }
    LOGD("GPIO资源已释放");
}

三、JNI 层封装与安卓 APP 实现

1. Java 层 native 方法定义

在 MainActivity.java 中定义 JNI 接口,同时实现 UI 交互逻辑:

java

运行

复制代码
package com.heipiao.rk3576.gpio;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    // 示例使用GPIO3_A5,对应gpiochip3,偏移量5
    private static final String GPIO_CHIP_PATH = "/dev/gpiochip3";
    private static final int GPIO_OFFSET = 5;

    static {
        System.loadLibrary("gpio-native");
    }

    private Button btnLedOn;
    private Button btnLedOff;
    private Button btnReadLevel;
    private TextView tvLevel;
    private boolean isInitSuccess = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 绑定UI控件
        btnLedOn = findViewById(R.id.btn_led_on);
        btnLedOff = findViewById(R.id.btn_led_off);
        btnReadLevel = findViewById(R.id.btn_read_level);
        tvLevel = findViewById(R.id.tv_level);

        // 初始化GPIO
        new Thread(() -> {
            int ret = gpioInit(GPIO_CHIP_PATH, GPIO_OFFSET, 0);
            runOnUiThread(() -> {
                if (ret == 0) {
                    isInitSuccess = true;
                    Toast.makeText(this, "GPIO初始化成功", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(this, "GPIO初始化失败", Toast.LENGTH_SHORT).show();
                }
            });
        }).start();

        // LED点亮按钮
        btnLedOn.setOnClickListener(v -> {
            if (!isInitSuccess) {
                Toast.makeText(this, "GPIO未初始化", Toast.LENGTH_SHORT).show();
                return;
            }
            new Thread(() -> gpioSetLevel(1)).start();
        });

        // LED熄灭按钮
        btnLedOff.setOnClickListener(v -> {
            if (!isInitSuccess) {
                Toast.makeText(this, "GPIO未初始化", Toast.LENGTH_SHORT).show();
                return;
            }
            new Thread(() -> gpioSetLevel(0)).start();
        });

        // 读取电平按钮
        btnReadLevel.setOnClickListener(v -> {
            if (!isInitSuccess) {
                Toast.makeText(this, "GPIO未初始化", Toast.LENGTH_SHORT).show();
                return;
            }
            new Thread(() -> {
                int level = gpioGetLevel();
                runOnUiThread(() -> tvLevel.setText("当前GPIO电平:" + level));
            }).start();
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        gpioClose();
    }

    // JNI native方法定义
    public native int gpioInit(String chipPath, int gpioOffset, int defaultLevel);
    public native int gpioSetLevel(int level);
    public native int gpioGetLevel();
    public native void gpioClose();
}

2. JNI 层接口实现

在 native-lib.cpp 中实现 Java 层定义的 native 方法,封装上面的 C++ GPIO 工具函数:

cpp

运行

复制代码
#include <jni.h>
#include <string>
// 上面写的GPIO工具函数直接放在这里,或通过头文件引入

extern "C" JNIEXPORT jint JNICALL
Java_com_heipiao_rk3576_gpio_MainActivity_gpioInit(JNIEnv *env, jobject thiz,
                                                      jstring chip_path, jint gpio_offset,
                                                      jint default_level) {
    // 转换字符串路径
    const char *path = env->GetStringUTFChars(chip_path, NULL);
    if (path == NULL) {
        return -1;
    }

    // 打开GPIO芯片
    int ret = gpio_open_chip(path);
    if (ret < 0) {
        env->ReleaseStringUTFChars(chip_path, path);
        return -1;
    }

    // 配置引脚为输出模式
    ret = gpio_config_output(gpio_offset, default_level);
    env->ReleaseStringUTFChars(chip_path, path);
    return ret;
}

extern "C" JNIEXPORT jint JNICALL
Java_com_heipiao_rk3576_gpio_MainActivity_gpioSetLevel(JNIEnv *env, jobject thiz, jint level) {
    return gpio_set_level(level);
}

extern "C" JNIEXPORT jint JNICALL
Java_com_heipiao_rk3576_gpio_MainActivity_gpioGetLevel(JNIEnv *env, jobject thiz) {
    return gpio_get_level();
}

extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_gpio_MainActivity_gpioClose(JNIEnv *env, jobject thiz) {
    gpio_close();
}

3. CMakeLists.txt 配置

cmake

复制代码
cmake_minimum_required(VERSION 3.10.2)
project("gpio-native")

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(
        gpio-native
        SHARED
        native-lib.cpp)

find_library(
        log-lib
        log)

target_link_libraries(
        gpio-native
        ${log-lib})

4. 布局文件(activity_main.xml)

xml

复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp"
    android:gravity="center_horizontal">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="RK3576 GPIO LED控制"
        android:textSize="24sp"
        android:layout_marginBottom="40dp"/>

    <Button
        android:id="@+id/btn_led_on"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="点亮LED"
        android:layout_marginBottom="20dp"/>

    <Button
        android:id="@+id/btn_led_off"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="熄灭LED"
        android:layout_marginBottom="20dp"/>

    <Button
        android:id="@+id/btn_read_level"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="读取引脚电平"
        android:layout_marginBottom="20dp"/>

    <TextView
        android:id="@+id/tv_level"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="当前GPIO电平:--"
        android:textSize="18sp"/>

</LinearLayout>

四、编译运行与调试

  1. 权限确认 :运行前确保开发板已执行adb rootsetenforce 0chmod 777 /dev/gpiochip*
  2. 编译运行:在 Android Studio 中选择 RK3576 开发板,点击 Run 安装 APK
  3. 功能验证
    • 打开 APP 后,会自动初始化 GPIO,提示初始化成功
    • 点击「点亮 LED」,LED 灯亮起,Logcat 可看到电平设置日志
    • 点击「熄灭 LED」,LED 灯熄灭
    • 点击「读取引脚电平」,可查看当前引脚的电平状态

五、新手必避的核心坑点

  1. GPIO 编号与偏移量混淆:ioctl 方式使用的是 bank 内的偏移量,不是全局编号。比如 GPIO3_A5,全局编号是 101,但在 gpiochip3 中,偏移量是 5,不是 101,这是新手最常见的错误。
  2. GPIO 芯片节点选错 :GPIOx 对应/dev/gpiochipx,GPIO3 对应 gpiochip3,不是 gpiochip0。
  3. 主线程执行硬件操作:所有 GPIO 操作必须放在子线程中,否则会阻塞主线程,导致 ANR。
  4. 资源未释放:APP 退出时必须调用 gpio_close 释放句柄,否则会导致文件描述符泄漏,多次重启 APP 后无法打开设备节点。
  5. 引脚被内核占用 :若 ioctl 返回设备忙,需通过cat /sys/kernel/debug/gpio查看引脚是否被内核其他驱动占用,需在设备树中释放对应引脚。
  6. 硬件接线错误:LED 必须串联限流电阻,否则会烧毁 GPIO 引脚;正负极不能接反,否则 LED 不亮。

下章预告

下一章将进入 RK3576 实战第二部分:JNI 调用 I2C 驱动读取温湿度传感器数据,讲解 I2C 总线的核心原理、Linux i2c-dev 驱动的 ioctl 操作规范,完成 SHT30 温湿度传感器的数据读取,实现安卓 APP 实时显示传感器数据。

相关推荐
阿拉斯攀登2 小时前
【RK3576 安卓 JNI/NDK 系列 05】NDK 构建系统:CMakeLists.txt 从入门到精通
cmake·rk3568·瑞芯微·rk安卓驱动·安卓jni·ndk构建系统
阿拉斯攀登5 小时前
【RK3576 安卓 JNI/NDK 系列 10】综合实战:RK3576 智能环境监测系统全实现 + 系列总结
rk3568·瑞芯微·rk安卓驱动·ndk构建系统·嵌入式智能终端
阿拉斯攀登10 小时前
【RK3576 安卓 JNI/NDK 系列 09】RK3576 实战(三):JNI 调用 librga 实现 2D 硬件加速图像处理
android·驱动开发·rk3568·瑞芯微·rk安卓驱动·rk3576 rga加速
阿拉斯攀登1 天前
第 19 篇 驱动性能优化与功耗优化实战
android·驱动开发·瑞芯微·嵌入式驱动·安卓驱动
阿拉斯攀登1 天前
第 20 篇 RK 平台 NPU / 硬件编解码驱动适配与安卓调用
android·驱动开发·瑞芯微·rk安卓驱动
阿拉斯攀登1 天前
第 12 篇 RK 平台安卓驱动实战 5:SPI 设备驱动开发,以 SPI 屏 / Flash 为例
android·驱动开发·rk3568·瑞芯微·嵌入式驱动·安卓驱动·spi 设备驱动
阿拉斯攀登1 天前
【RK3576 安卓 JNI/NDK 系列 02】保姆级环境搭建,从 0 到跑通第一个 JNI 程序
android studio·瑞芯微·嵌入式驱动·安卓驱动·安卓ndk环境搭建 jni入门
我命由我123452 天前
Android 开发 - UriMatcher(一个 URI 分类器)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
阿拉斯攀登2 天前
第 13 篇 输入设备驱动(触摸屏 / 按键)开发详解,Linux input 子系统全解析
android·linux·运维·驱动开发·rk3568·瑞芯微·rk安卓驱动