目录
[开篇先搞懂:GPIO 到底是什么?](#开篇先搞懂:GPIO 到底是什么?)
[RK3568 GPIO 核心知识点,小白必记](#RK3568 GPIO 核心知识点,小白必记)
[二、第一步:设备树配置,GPIO 驱动的基础](#二、第一步:设备树配置,GPIO 驱动的基础)
[1. 修改板级设备树文件](#1. 修改板级设备树文件)
[2. 编译设备树,烧录验证](#2. 编译设备树,烧录验证)
[三、第二步:GPIO 内核驱动开发,完整字符设备驱动实现](#三、第二步:GPIO 内核驱动开发,完整字符设备驱动实现)
[1. 创建驱动代码文件](#1. 创建驱动代码文件)
[2. 编写驱动代码,全注释详解](#2. 编写驱动代码,全注释详解)
[3. 关键知识点讲解,小白必懂](#3. 关键知识点讲解,小白必懂)
[4. 配置 Makefile,编译驱动](#4. 配置 Makefile,编译驱动)
[5. 验证驱动加载成功](#5. 验证驱动加载成功)
[四、第三步:安卓 HAL 层适配,连接内核驱动和上层 App](#四、第三步:安卓 HAL 层适配,连接内核驱动和上层 App)
[1. 创建 HAL 层代码文件](#1. 创建 HAL 层代码文件)
[2. 编写 HAL 层代码](#2. 编写 HAL 层代码)
[3. 编译 HAL 层模块](#3. 编译 HAL 层模块)
[五、第四步:JNI 接口封装,让 Java 能调用 HAL 层代码](#五、第四步:JNI 接口封装,让 Java 能调用 HAL 层代码)
[1. 创建 JNI 项目](#1. 创建 JNI 项目)
[2. 编写 JNI 实现代码](#2. 编写 JNI 实现代码)
[3. 配置 CMakeLists.txt,编译 JNI 库](#3. 配置 CMakeLists.txt,编译 JNI 库)
[4. 编译生成 APK,安装到开发板](#4. 编译生成 APK,安装到开发板)
[六、第五步:安卓 App 开发,实现可视化控制](#六、第五步:安卓 App 开发,实现可视化控制)
[1. 布局文件activity_main.xml](#1. 布局文件activity_main.xml)
[2. 主界面代码MainActivity.java](#2. 主界面代码MainActivity.java)
[3. 最终效果](#3. 最终效果)
[七、小白必踩的 GPIO 驱动坑,提前规避](#七、小白必踩的 GPIO 驱动坑,提前规避)
大家好,我是黒漂技术佬。上一篇我们完成了 hello world 字符设备驱动的开发,正式踏入了安卓驱动开发的大门。后台很多兄弟已经摩拳擦掌:
"佬,纯软件的驱动搞定了,什么时候带我们玩真的硬件?我已经把 LED 灯和杜邦线都准备好了!"
安排!今天这篇,我们就从嵌入式开发最经典的「LED 亮灭 + 按键读取」入手,手把手带你完成GPIO 输入输出驱动开发,并且彻底打通「设备树配置→内核驱动→HAL 硬件抽象层→JNI 接口→安卓 App」的完整全链路。学完这篇,你就能用安卓 App 的按钮,直接控制开发板上的 LED 亮灭,实时读取按键的状态,真正实现从软件到硬件的完整闭环。
开篇先搞懂:GPIO 到底是什么?
大白话定义
GPIO 的全称是 General-Purpose Input/Output,通用输入输出口。说白了,就是芯片上一个个可以由我们软件自由控制的引脚,它有两个核心功能:
- 输出模式:我们可以通过软件,控制这个引脚输出高电平(3.3V)或者低电平(0V),比如接个 LED 灯,高电平点亮,低电平熄灭,这就是输出功能;
- 输入模式:我们可以通过软件,读取这个引脚当前的电平状态,是高还是低,比如接个按键,按键按下的时候引脚变成低电平,松开是高电平,我们就能通过读取电平状态,判断按键有没有按下,这就是输入功能。
GPIO 是嵌入式开发的基础中的基础,不管是 LED、按键、传感器、电机,还是 I2C、SPI 这些总线,本质上都是基于 GPIO 引脚实现的,把 GPIO 玩明白,你就掌握了嵌入式开发的半壁江山。
RK3568 GPIO 核心知识点,小白必记
- 引脚分组:RK3568 一共有 5 组 GPIO,分别是 GPIO0、GPIO1、GPIO2、GPIO3、GPIO4,每组最多有 32 个引脚,分为 A、B、C、D 四个端口,每个端口 8 个引脚,比如 GPIO0_A0~A7、GPIO0_B0~B7,以此类推;
- 电平标准 :RK3568 的 GPIO 引脚默认是3.3V 电平,绝对不能直接接 5V 的设备,不然会直接烧 CPU,新手红线警告;
- 引脚复用:RK3568 的大部分 GPIO 引脚,都有多个功能,比如 GPIO0_B0,默认可以是普通 GPIO,也可以是 I2C 的 SDA 引脚,还可以是 PWM 输出引脚。我们要把引脚当普通 GPIO 用,必须先在设备树里配置引脚复用,把它设为 GPIO 功能,这是新手最常踩的坑;
- 上下拉配置:GPIO 引脚可以配置为上拉、下拉或者浮空。输入模式下,一般要配置上拉或者下拉,避免引脚浮空导致电平乱跳,比如按键输入,我们一般配置为上拉,按键按下的时候引脚接地,变成低电平,稳定可靠。
一、实战前的硬件准备
我们这次的实战目标是:
- 输出功能:用 GPIO0_A0 引脚控制一个 LED 灯,App 点击按钮,LED 点亮,再点击熄灭;
- 输入功能:用 GPIO0_A1 引脚接一个按键,App 实时显示按键是按下还是松开的状态。
硬件清单
- RK3568 开发板 1 块;
- LED 灯 1 个(直插式,随便什么颜色);
- 1K 限流电阻 1 个(LED 必须串电阻,不然直接烧引脚!);
- 轻触按键 1 个;
- 杜邦线若干;
- 面包板 1 个(可选,方便接线)。
硬件接线图,一步都不能错
表格
| RK3568 开发板引脚 | 外接硬件 | 接线说明 |
|---|---|---|
| GPIO0_A0 | LED 阳极(长脚) | LED 阴极(短脚)接 1K 电阻,电阻另一端接 GND |
| GPIO0_A1 | 按键一端 | 按键另一端接 GND |
| GND | 所有硬件的 GND | 必须共地,不然电平会乱跳 |
小白红线警告:
- LED 必须串联限流电阻!绝对不能直接把 LED 接在 GPIO 和 GND 之间,不然 GPIO 引脚输出 3.3V,电流过大,直接烧芯片引脚,哭都来不及;
- 所有硬件必须和开发板共地,不然电平读取会乱跳,完全不准;
- 绝对不要把 GPIO 引脚直接接 5V 电源,RK3568 的 GPIO 是 3.3V 的,接 5V 直接烧 CPU。
二、第一步:设备树配置,GPIO 驱动的基础
上一篇我们讲过,设备树是用来描述硬件信息的,我们要使用 GPIO 引脚,必须先在设备树里添加对应的节点,配置引脚复用、GPIO 属性,不然驱动根本匹配不上硬件。
1. 修改板级设备树文件
-
进入 Ubuntu 虚拟机,打开终端,进入设备树目录: bash
运行
cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/ -
打开你的开发板对应的板级.dts 文件,比如
rk3568-firefly.dts:bash
运行
vim rk3568-firefly.dts -
按
Shift+G跳到文件末尾,在根节点/ { ... };里面,添加我们的 GPIO 设备节点,每一行都加了注释,小白能看懂每一行的意义:dts
/ { // 前面是文件原有内容,不要动,在根节点内添加下面的代码 my_gpio: gpio_demo@0 { compatible = "my-gpio,demo"; // 兼容匹配属性,必须和驱动里的完全一致 status = "okay"; // 启用设备 // LED输出引脚:GPIO0_A0,高电平有效 led-gpio = <&gpio0 RK_PA0 GPIO_ACTIVE_HIGH>; // 按键输入引脚:GPIO0_A1,低电平有效(按键按下接GND) key-gpio = <&gpio0 RK_PA1 GPIO_ACTIVE_LOW>; // 引脚复用配置 pinctrl-names = "default"; pinctrl-0 = <&gpio_demo_pins>; }; // 引脚复用节点,配置GPIO0_A0和GPIO0_A1为普通GPIO功能 &pinctrl { gpio_demo { gpio_demo_pins: gpio-demo-pins { // 配置GPIO0_A0为GPIO功能,无上下拉 rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>, <0 RK_PA1 RK_FUNC_GPIO &pcfg_pull_up>; // GPIO0_A1配置为上拉,输入模式更稳定 }; }; }; }; -
按
ESC,输入:wq保存退出。
2. 编译设备树,烧录验证
-
进入 SDK 根目录,单独编译设备树: bash
运行
cd ~/RK3568_Android11_SDK ./build.sh -D -
编译成功后,打包 boot.img: bash
运行
./build.sh -K -
开发板进入 Loader 模式,烧录新的 boot.img,重启开发板;
-
重启完成后,进入 ADB shell,验证设备树节点是否生效: bash
运行
adb shell su ls /proc/device-tree/gpio_demo@0能看到我们添加的
led-gpio、key-gpio等属性,就说明设备树配置成功了!
三、第二步:GPIO 内核驱动开发,完整字符设备驱动实现
设备树配置完成后,我们来写内核驱动代码,实现 GPIO 的输入输出控制,并且封装成字符设备驱动,给上层 HAL 层调用。
1. 创建驱动代码文件
-
进入我们之前创建的驱动目录: bash
运行
cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers -
创建 GPIO 驱动文件
gpio_drv.c:bash
运行
touch gpio_drv.c
2. 编写驱动代码,全注释详解
打开gpio_drv.c,写入下面的完整代码,每一行都有详细注释,小白能看懂每一个函数的作用:
c
运行
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
// 驱动信息声明
MODULE_LICENSE("GPL");
MODULE_AUTHOR("黒漂技术佬");
MODULE_DESCRIPTION("RK3568 Android GPIO Input/Output Driver");
MODULE_VERSION("1.0");
// 宏定义
#define GPIO_BUF_SIZE 64
#define DEVICE_NAME "gpio_drv" // 设备文件名 /dev/gpio_drv
#define CLASS_NAME "gpio_class"
// 控制命令定义,ioctl用
#define CMD_LED_ON 0x1001 // 点亮LED
#define CMD_LED_OFF 0x1002 // 熄灭LED
#define CMD_KEY_READ 0x1003 // 读取按键状态
// 全局变量
static dev_t gpio_devno;
static struct cdev gpio_cdev;
static struct class *gpio_class;
static struct device *gpio_device;
static int led_gpio; // LED对应的GPIO编号
static int key_gpio; // 按键对应的GPIO编号
// ====================== 核心GPIO操作函数 ======================
// 从设备树中获取GPIO信息,初始化GPIO
static int gpio_hw_init(struct device *dev)
{
int ret;
// 1. 从设备树中获取LED的GPIO编号
led_gpio = of_get_named_gpio(dev->of_node, "led-gpio", 0);
if (!gpio_is_valid(led_gpio)) {
dev_err(dev, "获取LED GPIO失败\n");
return -EINVAL;
}
// 2. 从设备树中获取按键的GPIO编号
key_gpio = of_get_named_gpio(dev->of_node, "key-gpio", 0);
if (!gpio_is_valid(key_gpio)) {
dev_err(dev, "获取按键GPIO失败\n");
return -EINVAL;
}
// 3. 申请LED GPIO,设置为输出模式,默认低电平(熄灭)
ret = gpio_request(led_gpio, "led_gpio");
if (ret) {
dev_err(dev, "LED GPIO申请失败\n");
return ret;
}
gpio_direction_output(led_gpio, 0); // 输出模式,默认低电平
dev_info(dev, "LED GPIO初始化成功,编号:%d\n", led_gpio);
// 4. 申请按键GPIO,设置为输入模式
ret = gpio_request(key_gpio, "key_gpio");
if (ret) {
dev_err(dev, "按键GPIO申请失败\n");
gpio_free(led_gpio); // 申请失败,释放已经申请的GPIO
return ret;
}
gpio_direction_input(key_gpio); // 输入模式
dev_info(dev, "按键GPIO初始化成功,编号:%d\n", key_gpio);
return 0;
}
// 释放GPIO资源
static void gpio_hw_exit(void)
{
gpio_set_value(led_gpio, 0); // 熄灭LED
gpio_free(led_gpio); // 释放GPIO
gpio_free(key_gpio);
}
// ====================== 字符设备核心函数 ======================
static int gpio_open(struct inode *inode, struct file *filp)
{
printk("【gpio_drv】设备被打开\n");
return 0;
}
// ioctl函数:处理上层的控制命令,GPIO驱动最常用的方式
static long gpio_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int key_state;
int ret = 0;
switch (cmd) {
case CMD_LED_ON:
// 点亮LED:设置GPIO输出高电平
gpio_set_value(led_gpio, 1);
printk("【gpio_drv】LED点亮\n");
break;
case CMD_LED_OFF:
// 熄灭LED:设置GPIO输出低电平
gpio_set_value(led_gpio, 0);
printk("【gpio_drv】LED熄灭\n");
break;
case CMD_KEY_READ:
// 读取按键状态,返回给用户空间
key_state = gpio_get_value(key_gpio);
ret = copy_to_user((int __user *)arg, &key_state, sizeof(int));
if (ret) {
printk("【gpio_drv】按键状态拷贝失败\n");
return -EFAULT;
}
printk("【gpio_drv】读取按键状态:%d\n", key_state);
break;
default:
printk("【gpio_drv】无效命令\n");
return -EINVAL;
}
return 0;
}
static int gpio_release(struct inode *inode, struct file *filp)
{
printk("【gpio_drv】设备被关闭\n");
return 0;
}
// file_operations结构体
static const struct file_operations gpio_fops = {
.owner = THIS_MODULE,
.open = gpio_open,
.unlocked_ioctl = gpio_ioctl, // 对应ioctl命令
.release = gpio_release,
};
// ====================== platform驱动框架 ======================
// 为什么用platform驱动?因为GPIO是平台设备,和设备树匹配必须用platform框架
static int gpio_probe(struct platform_device *pdev)
{
int ret;
printk("【gpio_drv】驱动和设备树匹配成功,开始probe\n");
// 硬件初始化
ret = gpio_hw_init(&pdev->dev);
if (ret) {
dev_err(&pdev->dev, "硬件初始化失败\n");
return ret;
}
// 1. 动态申请设备号
ret = alloc_chrdev_region(&gpio_devno, 0, 1, DEVICE_NAME);
if (ret < 0) {
dev_err(&pdev->dev, "设备号申请失败\n");
goto err_devno;
}
// 2. 初始化字符设备
cdev_init(&gpio_cdev, &gpio_fops);
gpio_cdev.owner = THIS_MODULE;
// 3. 注册字符设备
ret = cdev_add(&gpio_cdev, gpio_devno, 1);
if (ret < 0) {
dev_err(&pdev->dev, "字符设备注册失败\n");
goto err_cdev_add;
}
// 4. 创建设备类
gpio_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(gpio_class)) {
ret = PTR_ERR(gpio_class);
dev_err(&pdev->dev, "设备类创建失败\n");
goto err_class_create;
}
// 5. 创建设备节点
gpio_device = device_create(gpio_class, NULL, gpio_devno, NULL, DEVICE_NAME);
if (IS_ERR(gpio_device)) {
ret = PTR_ERR(gpio_device);
dev_err(&pdev->dev, "设备创建失败\n");
goto err_device_create;
}
dev_info(&pdev->dev, "GPIO驱动加载成功!\n");
return 0;
// 错误处理
err_device_create:
class_destroy(gpio_class);
err_class_create:
cdev_del(&gpio_cdev);
err_cdev_add:
unregister_chrdev_region(gpio_devno, 1);
err_devno:
gpio_hw_exit();
return ret;
}
static int gpio_remove(struct platform_device *pdev)
{
printk("【gpio_drv】驱动开始卸载\n");
// 释放资源
device_destroy(gpio_class, gpio_devno);
class_destroy(gpio_class);
cdev_del(&gpio_cdev);
unregister_chrdev_region(gpio_devno, 1);
gpio_hw_exit();
dev_info(&pdev->dev, "GPIO驱动卸载成功!\n");
return 0;
}
// 设备树匹配表,必须和设备树里的compatible完全一致
static const struct of_device_id gpio_of_match[] = {
{ .compatible = "my-gpio,demo" },
{ /* 结束标志 */ }
};
MODULE_DEVICE_TABLE(of, gpio_of_match);
// platform驱动结构体
static struct platform_driver gpio_driver = {
.probe = gpio_probe,
.remove = gpio_remove,
.driver = {
.name = "gpio_demo_driver",
.of_match_table = gpio_of_match,
},
};
// ====================== 驱动入口和出口 ======================
static int __init gpio_drv_init(void)
{
printk("【gpio_drv】GPIO驱动开始加载\n");
return platform_driver_register(&gpio_driver);
}
static void __exit gpio_drv_exit(void)
{
platform_driver_unregister(&gpio_driver);
}
module_init(gpio_drv_init);
module_exit(gpio_drv_exit);
3. 关键知识点讲解,小白必懂
- platform 驱动框架:这是 Linux 内核里专门用来管理平台设备的驱动框架,和设备树强绑定。内核启动的时候,会扫描设备树里的节点,根据 compatible 属性,找到对应的 platform 驱动,执行 probe 函数,完成驱动的初始化。这是 Linux 驱动开发的标准框架,必须掌握;
- GPIO 核心 API :
of_get_named_gpio():从设备树里获取 GPIO 编号;gpio_request():向内核申请 GPIO 引脚,避免多个驱动同时使用同一个引脚;gpio_direction_output():设置 GPIO 为输出模式,指定默认电平;gpio_direction_input():设置 GPIO 为输入模式;gpio_set_value():设置 GPIO 输出的电平,1 是高电平,0 是低电平;gpio_get_value():读取 GPIO 当前的电平状态;gpio_free():释放 GPIO 引脚,驱动卸载的时候必须调用;
- ioctl 函数:这是字符设备驱动里,用来处理自定义控制命令的核心函数,比 read/write 更灵活,适合 GPIO 这种需要多种控制命令的场景。我们定义了点亮 LED、熄灭 LED、读取按键三个命令,上层应用只需要调用对应的命令,就能实现对应的功能。
4. 配置 Makefile,编译驱动
-
修改
my_drivers目录下的 Makefile,添加 GPIO 驱动的编译:bash
运行
vim Makefile把内容改成:
makefile
obj-y += hello_drv.o obj-y += gpio_drv.o -
保存退出,进入 SDK 根目录,编译内核: bash
运行
cd ~/RK3568_Android11_SDK ./build.sh -CK -
编译成功后,烧录新的 boot.img 到开发板,重启开发板。
5. 验证驱动加载成功
-
进入 ADB shell,查看驱动日志: bash
运行
adb shell su dmesg | grep gpio_drv能看到「GPIO 驱动加载成功」的日志,说明驱动和设备树匹配成功,正常加载了;
-
查看设备文件: bash
运行
ls -l /dev/gpio_drv能看到
/dev/gpio_drv设备文件,说明驱动完全正常; -
给设备文件设置权限,方便上层调用: bash
运行
chmod 777 /dev/gpio_drv
四、第三步:安卓 HAL 层适配,连接内核驱动和上层 App
驱动写好了,但是安卓 App 不能直接访问/dev/gpio_drv设备文件,必须通过 HAL 层来封装接口,这是安卓驱动开发的核心环节。
1. 创建 HAL 层代码文件
-
进入 SDK 的 HAL 层目录,创建我们的 HAL 模块目录: bash
运行
cd ~/RK3568_Android11_SDK/hardware/rockchip/ mkdir my_gpio_hal cd my_gpio_hal -
创建 HAL 层头文件
my_gpio_hal.h:bash
运行
touch my_gpio_hal.h -
创建 HAL 层实现文件
my_gpio_hal.c:bash
运行
touch my_gpio_hal.c -
创建 Android.bp 编译脚本,安卓 11 用 Android.bp 代替 Android.mk: bash
运行
touch Android.bp
2. 编写 HAL 层代码
(1)头文件my_gpio_hal.h
c
运行
#ifndef MY_GPIO_HAL_H
#define MY_GPIO_HAL_H
#ifdef __cplusplus
extern "C" {
#endif
// 函数声明,给JNI层调用
int led_on(void); // 点亮LED
int led_off(void); // 熄灭LED
int key_read(int *state); // 读取按键状态
#ifdef __cplusplus
}
#endif
#endif // MY_GPIO_HAL_H
(2)实现文件my_gpio_hal.c
c
运行
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "my_gpio_hal.h"
// 设备文件路径
#define DEVICE_PATH "/dev/gpio_drv"
// 和驱动里对应的命令
#define CMD_LED_ON 0x1001
#define CMD_LED_OFF 0x1002
#define CMD_KEY_READ 0x1003
static int fd = -1;
// 打开设备文件,初始化
static int gpio_dev_init(void)
{
if (fd < 0) {
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
printf("【gpio_hal】打开设备文件失败\n");
return -1;
}
printf("【gpio_hal】设备文件打开成功,fd=%d\n", fd);
}
return 0;
}
// 点亮LED
int led_on(void)
{
int ret;
if (gpio_dev_init() < 0) return -1;
ret = ioctl(fd, CMD_LED_ON, 0);
if (ret < 0) {
printf("【gpio_hal】LED点亮失败\n");
return -1;
}
return 0;
}
// 熄灭LED
int led_off(void)
{
int ret;
if (gpio_dev_init() < 0) return -1;
ret = ioctl(fd, CMD_LED_OFF, 0);
if (ret < 0) {
printf("【gpio_hal】LED熄灭失败\n");
return -1;
}
return 0;
}
// 读取按键状态
int key_read(int *state)
{
int ret;
if (gpio_dev_init() < 0) return -1;
ret = ioctl(fd, CMD_KEY_READ, state);
if (ret < 0) {
printf("【gpio_hal】按键读取失败\n");
return -1;
}
return 0;
}
(3)编译脚本Android.bp
json
cc_library_shared {
name: "libmygpio",
srcs: ["my_gpio_hal.c"],
local_include_dirs: ["."],
vendor: true,
cflags: [
"-Wall",
"-Werror",
],
}
这个脚本告诉安卓编译系统,把我们的 HAL 层代码,编译成libmygpio.so动态库,放在 vendor 分区里,给上层调用。
3. 编译 HAL 层模块
-
进入 SDK 根目录,设置编译环境: bash
运行
cd ~/RK3568_Android11_SDK source build/envsetup.sh lunch rk3568_r-userdebug # 换成你的开发板对应的lunch配置 -
编译 HAL 层模块: bash
运行
mmm hardware/rockchip/my_gpio_hal -
编译成功后,会生成
out/target/product/rk3568_r/vendor/lib64/libmygpio.so动态库; -
把新的 vendor.img 烧录到开发板,或者直接把 so 文件推到开发板的 /vendor/lib64 / 目录下,重启开发板。
五、第四步:JNI 接口封装,让 Java 能调用 HAL 层代码
安卓 App 是用 Java 写的,而 HAL 层是 C/C++ 写的,JNI(Java Native Interface)就是连接 Java 和 C/C++ 的桥梁,我们需要通过 JNI,把 HAL 层的函数封装成 Java 能调用的接口。
1. 创建 JNI 项目
我们用 Android Studio 创建一个安卓项目,包名设为com.heipiao.gpiodemo,创建一个专门的 JNI 类GpioJni.java:
java
运行
package com.heipiao.gpiodemo;
public class GpioJni {
// 加载我们的JNI动态库
static {
System.loadLibrary("gpio_jni");
}
// 声明native方法,对应HAL层的函数
public native int ledOn();
public native int ledOff();
public native int keyRead(int[] state);
}
2. 编写 JNI 实现代码
创建gpio_jni.cpp文件,实现 JNI 方法,调用 HAL 层的函数:
cpp
运行
#include <jni.h>
#include <string>
#include "my_gpio_hal.h"
extern "C"
JNIEXPORT jint JNICALL
Java_com_heipiao_gpiodemo_GpioJni_ledOn(JNIEnv *env, jobject thiz) {
return led_on();
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_heipiao_gpiodemo_GpioJni_ledOff(JNIEnv *env, jobject thiz) {
return led_off();
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_heipiao_gpiodemo_GpioJni_keyRead(JNIEnv *env, jobject thiz, jintArray state) {
int *p_state = env->GetIntArrayElements(state, NULL);
int ret = key_read(p_state);
env->ReleaseIntArrayElements(state, p_state, 0);
return ret;
}
3. 配置 CMakeLists.txt,编译 JNI 库
cmake
cmake_minimum_required(VERSION 3.18.1)
project("gpio_jni")
# 头文件路径
include_directories(
${CMAKE_SOURCE_DIR}/include
)
# 链接HAL层的libmygpio.so库
add_library(libmygpio SHARED IMPORTED)
set_target_properties(libmygpio PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libmygpio.so)
# 编译JNI库
add_library(
gpio_jni
SHARED
gpio_jni.cpp
)
target_link_libraries(
gpio_jni
libmygpio
android
log
)
4. 编译生成 APK,安装到开发板
编译完成后,生成gpio_demo.apk,通过 ADB 安装到 RK3568 开发板上:
bash
运行
adb install gpio_demo.apk
六、第五步:安卓 App 开发,实现可视化控制
最后,我们写一个简单的安卓 App 界面,实现两个功能:
- 两个按钮,分别控制 LED 的点亮和熄灭;
- 一个文本框,实时显示按键的状态(按下 / 松开)。
1. 布局文件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:gravity="center"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RK3568 GPIO驱动演示"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="50dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginBottom="30dp">
<Button
android:id="@+id/btn_led_on"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点亮LED"
android:textSize="20sp"
android:layout_marginRight="20dp"/>
<Button
android:id="@+id/btn_led_off"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="熄灭LED"
android:textSize="20sp"/>
</LinearLayout>
<TextView
android:id="@+id/tv_key_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按键状态:松开"
android:textSize="24sp"
android:textColor="@color/black"
android:layout_marginTop="30dp"/>
</LinearLayout>
2. 主界面代码MainActivity.java
java
运行
package com.heipiao.gpiodemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private GpioJni gpioJni;
private Button btnLedOn, btnLedOff;
private TextView tvKeyState;
private Handler handler;
private boolean isRunning = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化JNI对象
gpioJni = new GpioJni();
handler = new Handler(Looper.getMainLooper());
// 绑定控件
btnLedOn = findViewById(R.id.btn_led_on);
btnLedOff = findViewById(R.id.btn_led_off);
tvKeyState = findViewById(R.id.tv_key_state);
// 点亮LED按钮点击事件
btnLedOn.setOnClickListener(v -> {
gpioJni.ledOn();
});
// 熄灭LED按钮点击事件
btnLedOff.setOnClickListener(v -> {
gpioJni.ledOff();
});
// 开启线程,实时读取按键状态
new Thread(() -> {
int[] state = new int[1];
while (isRunning) {
gpioJni.keyRead(state);
// 更新UI
handler.post(() -> {
if (state[0] == 0) {
tvKeyState.setText("按键状态:按下");
} else {
tvKeyState.setText("按键状态:松开");
}
});
try {
Thread.sleep(100); // 100ms刷新一次
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
isRunning = false;
}
}
3. 最终效果
把 APK 安装到开发板上,打开 App:
- 点击「点亮 LED」按钮,开发板上的 LED 灯立刻点亮;
- 点击「熄灭 LED」按钮,LED 灯立刻熄灭;
- 按下按键,App 上的按键状态立刻变成「按下」,松开按键,变成「松开」,实时刷新。
恭喜你!你已经彻底打通了从内核驱动到安卓 App 的完整全链路,真正实现了用安卓 App 控制硬件!
七、小白必踩的 GPIO 驱动坑,提前规避
- 坑 1:引脚复用没配置,GPIO 操作没反应这是新手最常踩的坑,RK3568 的引脚默认可能是其他功能,必须在 pinctrl 里配置为 RK_FUNC_GPIO 功能,不然引脚根本不能当 GPIO 用,操作了也没反应;
- 坑 2:GPIO 申请失败 同一个 GPIO 引脚,已经被设备树里的其他外设占用了,比如你用的 GPIO0_A0,已经被官方的 LED 驱动占用了,必须先把官方的节点禁用,才能申请成功。解决方法:
dmesg | grep gpio,看哪个驱动占用了你的引脚,在设备树里把对应的节点 status 设为 disabled; - 坑 3:按键输入电平乱跳输入模式下,没有配置上下拉,引脚浮空,导致电平乱跳。解决方法:在设备树的 pinctrl 里配置上拉或者下拉,按键输入一般配置上拉,按下接 GND,稳定可靠;
- 坑 4:LED 不亮,或者直接烧引脚没有串联限流电阻,直接把 LED 接在 GPIO 和 GND 之间,轻则 LED 不亮,重则烧芯片引脚。必须给 LED 串联 1K 左右的限流电阻;
- 坑 5:App 调用 HAL 层失败,提示找不到 so 库 没有把 HAL 层的 libmygpio.so 放到正确的目录,或者权限不够。解决方法:把 so 库推到 /vendor/lib64 / 目录下,设置 644 权限,关闭 SELinux 测试:
setenforce 0。
结尾说两句
这篇文章,我们从硬件接线、设备树配置、内核驱动开发、HAL 层适配、JNI 封装,到最终的安卓 App 开发,一步不落的打通了 GPIO 驱动的完整全链路。你现在已经掌握了嵌入式开发最核心的 GPIO 操作,能独立完成从底层驱动到上层 App 的完整开发了。
下一篇,我们进入进阶内容:中断驱动开发,用按键中断的方式,实现按键事件的实时响应,解决轮询读取按键占用 CPU 资源的问题,并且教你怎么把内核中断事件,实时上报到安卓 App。
我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何 GPIO 驱动的问题,评论区留言,我都会一一回复。