欢迎光临
我们一直在努力

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

目录

开篇先搞懂:GPIO 到底是什么?

大白话定义

RK3568 GPIO 核心知识点,小白必记

一、实战前的硬件准备

硬件清单

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

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

1. 修改板级设备树文件

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

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

1. 创建驱动代码文件

2. 编写驱动代码,全注释详解

3. 关键知识点讲解,小白必懂

4. 配置 Makefile,编译驱动

5. 验证驱动加载成功

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

1. 创建 HAL 层代码文件

2. 编写 HAL 层代码

(1)头文件my_gpio_hal.h

(2)实现文件my_gpio_hal.c

(3)编译脚本Android.bp

3. 编译 HAL 层模块

五、第四步:JNI 接口封装,让 Java 能调用 HAL 层代码

1. 创建 JNI 项目

2. 编写 JNI 实现代码

3. 配置 CMakeLists.txt,编译 JNI 库

4. 编译生成 APK,安装到开发板

六、第五步:安卓 App 开发,实现可视化控制

1. 布局文件activity_main.xml

2. 主界面代码MainActivity.java

3. 最终效果

七、小白必踩的 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 驱动的问题,评论区留言,我都会一一回复。

    赞(0)
    未经允许不得转载:171主机测评 » 第 8 篇 RK 平台安卓驱动实战 1:GPIO 输入输出驱动,从内核到 App 全流程打通
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址