目录
[一、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. 硬件准备
- 开发板:RK3576 核心板 + 底板(野火、正点原子、瑞芯微 EVB 板均可)
- 硬件物料:直插 LED 灯、1k 限流电阻、杜邦线、面包板
- 接线规则 :
- LED 长脚(正极)通过限流电阻接 RK3576 的 GPIO 引脚(示例用 GPIO3_A5,编号 101)
- LED 短脚(负极)接开发板 GND
- 若开发板自带用户 LED,可直接使用板载 LED,无需额外接线,只需确认对应 GPIO 编号即可
- 前置环境 :开发板已开启 adb root、关闭 SELinux、给
/dev/gpiochip*设备节点配置 777 权限,操作步骤见上一章权限配置章节。
二、GPIO 控制的两种实现方式
RK3576 安卓系统下,GPIO 控制主要有两种实现方式,分别适用于不同场景,本章会分别讲解实现逻辑,重点讲解标准 ioctl 方式。
方式 1:sysfs 方式(新手入门级)
sysfs 是 Linux 内核提供的虚拟文件系统,GPIO 子系统会在/sys/class/gpio/目录下暴露操作接口,通过读写文件的方式控制 GPIO,优点是逻辑简单、上手快,缺点是性能低、实时性差,不适合高频操作场景,仅推荐用于调试。
核心操作步骤(adb shell 命令行可直接验证)
- 导出 GPIO 引脚到用户空间:
echo 101 > /sys/class/gpio/export - 设置引脚方向为输出:
echo out > /sys/class/gpio/gpio101/direction - 设置引脚高电平(点亮 LED):
echo 1 > /sys/class/gpio/gpio101/value - 设置引脚低电平(熄灭 LED):
echo 0 > /sys/class/gpio/gpio101/value - 释放 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>
四、编译运行与调试
- 权限确认 :运行前确保开发板已执行
adb root、setenforce 0、chmod 777 /dev/gpiochip* - 编译运行:在 Android Studio 中选择 RK3576 开发板,点击 Run 安装 APK
- 功能验证 :
- 打开 APP 后,会自动初始化 GPIO,提示初始化成功
- 点击「点亮 LED」,LED 灯亮起,Logcat 可看到电平设置日志
- 点击「熄灭 LED」,LED 灯熄灭
- 点击「读取引脚电平」,可查看当前引脚的电平状态
五、新手必避的核心坑点
- GPIO 编号与偏移量混淆:ioctl 方式使用的是 bank 内的偏移量,不是全局编号。比如 GPIO3_A5,全局编号是 101,但在 gpiochip3 中,偏移量是 5,不是 101,这是新手最常见的错误。
- GPIO 芯片节点选错 :GPIOx 对应
/dev/gpiochipx,GPIO3 对应 gpiochip3,不是 gpiochip0。 - 主线程执行硬件操作:所有 GPIO 操作必须放在子线程中,否则会阻塞主线程,导致 ANR。
- 资源未释放:APP 退出时必须调用 gpio_close 释放句柄,否则会导致文件描述符泄漏,多次重启 APP 后无法打开设备节点。
- 引脚被内核占用 :若 ioctl 返回设备忙,需通过
cat /sys/kernel/debug/gpio查看引脚是否被内核其他驱动占用,需在设备树中释放对应引脚。 - 硬件接线错误:LED 必须串联限流电阻,否则会烧毁 GPIO 引脚;正负极不能接反,否则 LED 不亮。
下章预告
下一章将进入 RK3576 实战第二部分:JNI 调用 I2C 驱动读取温湿度传感器数据,讲解 I2C 总线的核心原理、Linux i2c-dev 驱动的 ioctl 操作规范,完成 SHT30 温湿度传感器的数据读取,实现安卓 APP 实时显示传感器数据。