第 8 篇 RK 平台安卓驱动实战 1:GPIO 输入输出驱动,从内核到 App 全流程打通

目录

[开篇先搞懂: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 层代码)

(1)头文件my_gpio_hal.h

(2)实现文件my_gpio_hal.c

(3)编译脚本Android.bp

[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 核心知识点,小白必记

  1. 引脚分组:RK3568 一共有 5 组 GPIO,分别是 GPIO0、GPIO1、GPIO2、GPIO3、GPIO4,每组最多有 32 个引脚,分为 A、B、C、D 四个端口,每个端口 8 个引脚,比如 GPIO0_A0~A7、GPIO0_B0~B7,以此类推;
  2. 电平标准 :RK3568 的 GPIO 引脚默认是3.3V 电平,绝对不能直接接 5V 的设备,不然会直接烧 CPU,新手红线警告;
  3. 引脚复用:RK3568 的大部分 GPIO 引脚,都有多个功能,比如 GPIO0_B0,默认可以是普通 GPIO,也可以是 I2C 的 SDA 引脚,还可以是 PWM 输出引脚。我们要把引脚当普通 GPIO 用,必须先在设备树里配置引脚复用,把它设为 GPIO 功能,这是新手最常踩的坑;
  4. 上下拉配置:GPIO 引脚可以配置为上拉、下拉或者浮空。输入模式下,一般要配置上拉或者下拉,避免引脚浮空导致电平乱跳,比如按键输入,我们一般配置为上拉,按键按下的时候引脚接地,变成低电平,稳定可靠。

一、实战前的硬件准备

我们这次的实战目标是:

  1. 输出功能:用 GPIO0_A0 引脚控制一个 LED 灯,App 点击按钮,LED 点亮,再点击熄灭;
  2. 输入功能:用 GPIO0_A1 引脚接一个按键,App 实时显示按键是按下还是松开的状态。

硬件清单

  1. RK3568 开发板 1 块;
  2. LED 灯 1 个(直插式,随便什么颜色);
  3. 1K 限流电阻 1 个(LED 必须串电阻,不然直接烧引脚!);
  4. 轻触按键 1 个;
  5. 杜邦线若干;
  6. 面包板 1 个(可选,方便接线)。

硬件接线图,一步都不能错

表格

RK3568 开发板引脚 外接硬件 接线说明
GPIO0_A0 LED 阳极(长脚) LED 阴极(短脚)接 1K 电阻,电阻另一端接 GND
GPIO0_A1 按键一端 按键另一端接 GND
GND 所有硬件的 GND 必须共地,不然电平会乱跳

小白红线警告

  1. LED 必须串联限流电阻!绝对不能直接把 LED 接在 GPIO 和 GND 之间,不然 GPIO 引脚输出 3.3V,电流过大,直接烧芯片引脚,哭都来不及;
  2. 所有硬件必须和开发板共地,不然电平读取会乱跳,完全不准;
  3. 绝对不要把 GPIO 引脚直接接 5V 电源,RK3568 的 GPIO 是 3.3V 的,接 5V 直接烧 CPU。

二、第一步:设备树配置,GPIO 驱动的基础

上一篇我们讲过,设备树是用来描述硬件信息的,我们要使用 GPIO 引脚,必须先在设备树里添加对应的节点,配置引脚复用、GPIO 属性,不然驱动根本匹配不上硬件。

1. 修改板级设备树文件

  1. 进入 Ubuntu 虚拟机,打开终端,进入设备树目录: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/
  2. 打开你的开发板对应的板级.dts 文件,比如rk3568-firefly.dts

    bash

    运行

    复制代码
    vim rk3568-firefly.dts
  3. 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配置为上拉,输入模式更稳定
                };
            };
        };
    };
  4. ESC,输入:wq保存退出。

2. 编译设备树,烧录验证

  1. 进入 SDK 根目录,单独编译设备树: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK
    ./build.sh -D
  2. 编译成功后,打包 boot.img: bash

    运行

    复制代码
    ./build.sh -K
  3. 开发板进入 Loader 模式,烧录新的 boot.img,重启开发板;

  4. 重启完成后,进入 ADB shell,验证设备树节点是否生效: bash

    运行

    复制代码
    adb shell
    su
    ls /proc/device-tree/gpio_demo@0

    能看到我们添加的led-gpiokey-gpio等属性,就说明设备树配置成功了!


三、第二步:GPIO 内核驱动开发,完整字符设备驱动实现

设备树配置完成后,我们来写内核驱动代码,实现 GPIO 的输入输出控制,并且封装成字符设备驱动,给上层 HAL 层调用。

1. 创建驱动代码文件

  1. 进入我们之前创建的驱动目录: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers
  2. 创建 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. 关键知识点讲解,小白必懂

  1. platform 驱动框架:这是 Linux 内核里专门用来管理平台设备的驱动框架,和设备树强绑定。内核启动的时候,会扫描设备树里的节点,根据 compatible 属性,找到对应的 platform 驱动,执行 probe 函数,完成驱动的初始化。这是 Linux 驱动开发的标准框架,必须掌握;
  2. 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 引脚,驱动卸载的时候必须调用;
  3. ioctl 函数:这是字符设备驱动里,用来处理自定义控制命令的核心函数,比 read/write 更灵活,适合 GPIO 这种需要多种控制命令的场景。我们定义了点亮 LED、熄灭 LED、读取按键三个命令,上层应用只需要调用对应的命令,就能实现对应的功能。

4. 配置 Makefile,编译驱动

  1. 修改my_drivers目录下的 Makefile,添加 GPIO 驱动的编译:

    bash

    运行

    复制代码
    vim Makefile

    把内容改成:

    makefile

    复制代码
    obj-y += hello_drv.o
    obj-y += gpio_drv.o
  2. 保存退出,进入 SDK 根目录,编译内核: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK
    ./build.sh -CK
  3. 编译成功后,烧录新的 boot.img 到开发板,重启开发板。

5. 验证驱动加载成功

  1. 进入 ADB shell,查看驱动日志: bash

    运行

    复制代码
    adb shell
    su
    dmesg | grep gpio_drv

    能看到「GPIO 驱动加载成功」的日志,说明驱动和设备树匹配成功,正常加载了;

  2. 查看设备文件: bash

    运行

    复制代码
    ls -l /dev/gpio_drv

    能看到/dev/gpio_drv设备文件,说明驱动完全正常;

  3. 给设备文件设置权限,方便上层调用: bash

    运行

    复制代码
    chmod 777 /dev/gpio_drv

四、第三步:安卓 HAL 层适配,连接内核驱动和上层 App

驱动写好了,但是安卓 App 不能直接访问/dev/gpio_drv设备文件,必须通过 HAL 层来封装接口,这是安卓驱动开发的核心环节。

1. 创建 HAL 层代码文件

  1. 进入 SDK 的 HAL 层目录,创建我们的 HAL 模块目录: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/hardware/rockchip/
    mkdir my_gpio_hal
    cd my_gpio_hal
  2. 创建 HAL 层头文件my_gpio_hal.h

    bash

    运行

    复制代码
    touch my_gpio_hal.h
  3. 创建 HAL 层实现文件my_gpio_hal.c

    bash

    运行

    复制代码
    touch my_gpio_hal.c
  4. 创建 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 层模块

  1. 进入 SDK 根目录,设置编译环境: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK
    source build/envsetup.sh
    lunch rk3568_r-userdebug  # 换成你的开发板对应的lunch配置
  2. 编译 HAL 层模块: bash

    运行

    复制代码
    mmm hardware/rockchip/my_gpio_hal
  3. 编译成功后,会生成out/target/product/rk3568_r/vendor/lib64/libmygpio.so动态库;

  4. 把新的 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 界面,实现两个功能:

  1. 两个按钮,分别控制 LED 的点亮和熄灭;
  2. 一个文本框,实时显示按键的状态(按下 / 松开)。

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:

  1. 点击「点亮 LED」按钮,开发板上的 LED 灯立刻点亮;
  2. 点击「熄灭 LED」按钮,LED 灯立刻熄灭;
  3. 按下按键,App 上的按键状态立刻变成「按下」,松开按键,变成「松开」,实时刷新。

恭喜你!你已经彻底打通了从内核驱动到安卓 App 的完整全链路,真正实现了用安卓 App 控制硬件!


七、小白必踩的 GPIO 驱动坑,提前规避

  1. 坑 1:引脚复用没配置,GPIO 操作没反应这是新手最常踩的坑,RK3568 的引脚默认可能是其他功能,必须在 pinctrl 里配置为 RK_FUNC_GPIO 功能,不然引脚根本不能当 GPIO 用,操作了也没反应;
  2. 坑 2:GPIO 申请失败 同一个 GPIO 引脚,已经被设备树里的其他外设占用了,比如你用的 GPIO0_A0,已经被官方的 LED 驱动占用了,必须先把官方的节点禁用,才能申请成功。解决方法:dmesg | grep gpio,看哪个驱动占用了你的引脚,在设备树里把对应的节点 status 设为 disabled;
  3. 坑 3:按键输入电平乱跳输入模式下,没有配置上下拉,引脚浮空,导致电平乱跳。解决方法:在设备树的 pinctrl 里配置上拉或者下拉,按键输入一般配置上拉,按下接 GND,稳定可靠;
  4. 坑 4:LED 不亮,或者直接烧引脚没有串联限流电阻,直接把 LED 接在 GPIO 和 GND 之间,轻则 LED 不亮,重则烧芯片引脚。必须给 LED 串联 1K 左右的限流电阻;
  5. 坑 5:App 调用 HAL 层失败,提示找不到 so 库 没有把 HAL 层的 libmygpio.so 放到正确的目录,或者权限不够。解决方法:把 so 库推到 /vendor/lib64 / 目录下,设置 644 权限,关闭 SELinux 测试:setenforce 0

结尾说两句

这篇文章,我们从硬件接线、设备树配置、内核驱动开发、HAL 层适配、JNI 封装,到最终的安卓 App 开发,一步不落的打通了 GPIO 驱动的完整全链路。你现在已经掌握了嵌入式开发最核心的 GPIO 操作,能独立完成从底层驱动到上层 App 的完整开发了。

下一篇,我们进入进阶内容:中断驱动开发,用按键中断的方式,实现按键事件的实时响应,解决轮询读取按键占用 CPU 资源的问题,并且教你怎么把内核中断事件,实时上报到安卓 App。

我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何 GPIO 驱动的问题,评论区留言,我都会一一回复。

相关推荐
常利兵2 小时前
告别SharedPreferences!DataStore+Android Keystore构建安全存储新防线
android·安全
2501_915918412 小时前
网站抓包解析,掌握浏览器请求和 HTTPS 数据分析的流程
android·ios·小程序·https·uni-app·iphone·webview
黄林晴2 小时前
Android 17 要下狠手了:无障碍服务 API 将被严格限制
android
冰语竹2 小时前
Android学习之表格布局
android
00后程序员张2 小时前
iOS开发者工具有哪些?Xcode、AppUploader(开心上架)、Fastlane如何使用
android·macos·ios·小程序·uni-app·iphone·xcode
Kapaseker3 小时前
一杯 Kotlin 美式学透 enum class
android·kotlin
实时云渲染dlxyz66883 小时前
点盾云安卓版手机/平板播放器使用教程
android·点盾云播放·点盾云安卓使用·点盾云安卓播放器·加密播放
nix.gnehc3 小时前
OpenClaw 安卓设备接入指南:从零开始配置你的移动节点
android·openclaw
RDCJM13 小时前
【MySQL】在MySQL中STR_TO_DATE()以及其他用于日期和时间的转换
android·数据库·mysql