【RK3576 安卓 JNI/NDK 系列 06】安卓驱动访问基础:设备节点与 ioctl 全解析

目录

前言

[一、先搞懂核心逻辑:安卓 APP 到底是怎么控制硬件的?](#一、先搞懂核心逻辑:安卓 APP 到底是怎么控制硬件的?)

[1. 核心基石:Linux "一切皆文件" 的设计思想](#1. 核心基石:Linux “一切皆文件” 的设计思想)

[2. 什么是设备节点?RK3576 里常见的设备节点有哪些?](#2. 什么是设备节点?RK3576 里常见的设备节点有哪些?)

[3. 安卓应用访问硬件的完整链路(和之前的 JNI 架构呼应)](#3. 安卓应用访问硬件的完整链路(和之前的 JNI 架构呼应))

[二、核心 API 详解:5 个系统调用,搞定 90% 的硬件操作](#二、核心 API 详解:5 个系统调用,搞定 90% 的硬件操作)

[1. open:打开设备节点,获取操作权限](#1. open:打开设备节点,获取操作权限)

作用

参数详解

返回值

新手必记注意事项

示例代码

[2. close:关闭设备节点,释放资源](#2. close:关闭设备节点,释放资源)

作用

参数详解

返回值

新手必记注意事项

[3. read:从设备节点读取数据](#3. read:从设备节点读取数据)

作用

参数详解

返回值

[4. write:向设备节点写入数据](#4. write:向设备节点写入数据)

作用

参数详解

返回值

[5. ioctl:硬件控制的万能接口(重点中的重点)](#5. ioctl:硬件控制的万能接口(重点中的重点))

作用

参数详解

返回值

新手必记注意事项

三、新手头号拦路虎:权限问题根治方案

[1. 权限问题的两个根源](#1. 权限问题的两个根源)

[2. 临时调试方案(开发阶段用,重启失效)](#2. 临时调试方案(开发阶段用,重启失效))

[步骤 1:adb 连接 RK3576 开发板,获取 root 权限](#步骤 1:adb 连接 RK3576 开发板,获取 root 权限)

[步骤 2:修改设备节点的 Linux 文件权限](#步骤 2:修改设备节点的 Linux 文件权限)

[步骤 3:临时关闭 SELinux(最关键的一步)](#步骤 3:临时关闭 SELinux(最关键的一步))

[3. 永久方案(产品开发用,改固件永久生效)](#3. 永久方案(产品开发用,改固件永久生效))

[方案 1:修改 ueventd.rc,永久设置设备节点权限](#方案 1:修改 ueventd.rc,永久设置设备节点权限)

[方案 2:添加 SELinux 规则,允许 APP 访问设备节点](#方案 2:添加 SELinux 规则,允许 APP 访问设备节点)

[四、RK3576 实战:从 JNI 调用驱动,跑通完整流程](#四、RK3576 实战:从 JNI 调用驱动,跑通完整流程)

前置准备

[实战 demo1:基础设备节点操作,验证 open/close 流程](#实战 demo1:基础设备节点操作,验证 open/close 流程)

[步骤 1:Java 层定义 native 方法](#步骤 1:Java 层定义 native 方法)

[步骤 2:C++ 层实现 JNI 方法,操作设备节点](#步骤 2:C++ 层实现 JNI 方法,操作设备节点)

[步骤 3:编译运行,看结果](#步骤 3:编译运行,看结果)

[实战 demo2:ioctl 进阶,获取 GPIO 引脚信息](#实战 demo2:ioctl 进阶,获取 GPIO 引脚信息)

[五、新手踩坑急救站:99% 的硬件操作问题都在这里解决](#五、新手踩坑急救站:99% 的硬件操作问题都在这里解决)

[坑 1:open 设备节点一直报 Permission denied](#坑 1:open 设备节点一直报 Permission denied)

[坑 2:ioctl 一直返回 - 1,报错 Invalid argument](#坑 2:ioctl 一直返回 - 1,报错 Invalid argument)

[坑 3:APP 跑几次之后,open 一直失败,报错 Too many open files](#坑 3:APP 跑几次之后,open 一直失败,报错 Too many open files)

[坑 4:设备节点不存在,报错 No such file or directory](#坑 4:设备节点不存在,报错 No such file or directory)

[坑 5:在主线程里调用 ioctl,APP 卡成 PPT](#坑 5:在主线程里调用 ioctl,APP 卡成 PPT)

[本章总结 + 下章预告](#本章总结 + 下章预告)

【本章总结】

【下章预告】


前言

哈喽各位兄弟们,我是你们的黒漂技术佬!

前面五章咱们把 JNI 语法、NDK 构建系统彻底啃完了,后台一堆兄弟报喜:"佬哥,我已经能成功链接 RK3576 的 librga 库了!" 但同时,90% 的兄弟都卡在了同一个核心门槛上:"佬哥,JNI 我玩明白了,CMake 也会写了,但是到底怎么通过安卓 APP 碰硬件啊?我想控 GPIO、读 I2C,根本不知道从哪下手!""我照着网上的代码写了 open ("/dev/gpiochip0"),结果一直报 Permission denied,权限改了八百遍还是不行,人都麻了!""到处都能看到 ioctl 这个东西,到底是干嘛的?参数怎么写?完全看不懂啊!"

懂了懂了!这是所有安卓底层新手从 "能写 JNI 代码" 到 "能控硬件" 的必经鬼门关。咱们做 RK3576 嵌入式开发,核心就是玩硬件,而设备节点 + ioctl,就是安卓应用层操控底层硬件的唯一入口。前面学的 JNI,只是给咱们搭了 Java 到 C++ 的桥,而这一章的内容,才是教你怎么从 C++ 走到硬件面前,打开控制硬件的大门。

今天这一章,咱们还是老规矩:大白话讲透核心原理 + 保姆级步骤拆解 + RK3576 专属实战 demo + 全场景避坑指南,从 Linux "一切皆文件" 的核心思想,到设备节点操作、ioctl 使用、权限问题根治,全给你扒得明明白白。所有代码全是针对 RK3576 原厂固件适配的,新手跟着走,100% 能跑通,再也不会被 "权限拒绝""硬件没反应" 搞崩心态!


一、先搞懂核心逻辑:安卓 APP 到底是怎么控制硬件的?

很多新手上来就对着设备节点瞎敲代码,根本没搞懂底层的链路逻辑,越写越懵。咱们先把整个流程的底层原理讲透,建立完整的认知,后面的代码就只是按规则填空而已。

1. 核心基石:Linux "一切皆文件" 的设计思想

咱们的 RK3576 安卓系统,底层是基于 Linux 内核的,而 Linux 最核心的设计思想就是一切皆文件 。大白话讲:在 Linux 系统里,不管是普通的文本文件、图片视频,还是键盘、鼠标、GPIO、I2C、显示屏这些硬件设备,内核都会把它抽象成一个文件 ,放在/dev/目录下,这个文件就叫设备节点

你想操控这个硬件,根本不用记一堆复杂的硬件指令,只需要像操作普通文本文件一样:

  • open打开这个设备节点(相当于拿到了硬件的操作权限,打开了硬件的大门)
  • read读取硬件的数据(相当于从硬件里拿东西,比如传感器的温度值)
  • write给硬件写数据(相当于给硬件发指令,比如给 GPIO 写高低电平)
  • ioctl给硬件发特殊控制命令(相当于给硬件调参数,比如设置 GPIO 的输入输出方向、I2C 的通信速率)
  • 用完用close关闭设备节点(相当于用完锁门,释放资源)

用咱们最熟悉的生活类比:

  • 硬件(GPIO/I2C / 传感器)= 你家的洗衣机
  • 设备节点(/dev/gpiochip0)= 你家洗衣机的门
  • 驱动程序 = 洗衣机的内部电路和控制板
  • open/close = 开门 / 关门
  • read/write = 往洗衣机里放衣服 / 拿衣服
  • ioctl = 按洗衣机的面板按钮,设置洗衣模式、转速、水温

你不用懂洗衣机的内部电路是怎么转的,只要会开门、放衣服、按按钮、关门,就能用洗衣机。同理,你不用懂内核驱动是怎么写的,只要会操作设备节点,就能控制 RK3576 的硬件。

2. 什么是设备节点?RK3576 里常见的设备节点有哪些?

设备节点就是 Linux 内核给每个硬件设备创建的一个特殊文件,一般都在/dev/目录下,它是应用层和内核驱动之间的唯一桥梁。驱动程序会监听这个文件的操作,你对这个文件做的任何读写、控制操作,内核都会转发给对应的驱动程序,驱动再去操控实际的硬件。

设备节点分为两大类,咱们嵌入式开发 99% 的场景都只接触第一类:

表格

设备类型 特点 RK3576 常见示例 咱们的用途
字符设备(Character Device) 按字节流读写,数据顺序传输,大多数硬件外设都是字符设备 /dev/gpiochip0(GPIO 驱动)、/dev/i2c-2(I2C 总线)、/dev/ttyS0(串口)、/dev/rga(2D 加速) GPIO 控制、传感器读取、串口通信、硬件加速,咱们系列的核心操作对象
块设备(Block Device) 按块(一般 512 字节 / 4K)读写,支持随机访问,主要是存储设备 /dev/mmcblk0(EMMC 存储)、/dev/sda(U 盘) 存储相关操作,咱们基本用不到

给大家列一下 RK3576 原厂固件里,咱们后续章节会高频用到的设备节点,新手提前记一下:

  • GPIO 控制:/dev/gpiochip0/dev/gpiochip1/dev/gpiochip2/dev/gpiochip3(对应 RK3576 的 4 组 GPIO bank)
  • I2C 总线:/dev/i2c-0/dev/i2c-9(RK3576 有 10 路硬件 I2C,常用的是 i2c-2、i2c-3)
  • 串口:/dev/ttyS0/dev/ttyS9
  • RGA 2D 加速:/dev/rga
  • NPU 推理:/dev/rknpu

3. 安卓应用访问硬件的完整链路(和之前的 JNI 架构呼应)

咱们把之前的分层架构,结合硬件访问的流程,再细化一遍,让你清楚地知道,你写的每一行代码,到底是怎么让硬件动起来的:

plaintext

复制代码
┌─────────────────────────────────────────────────┐
│ 【应用层】Java写的安卓APP,定义native方法      │
├─────────────────────────────────────────────────┤
│ 【JNI层】Java ↔ C++的桥梁,封装硬件操作接口    │
├─────────────────────────────────────────────────┤
│ 【原生层】C++代码,调用Linux系统调用            │
│  open/read/write/ioctl/close 操作设备节点       │
├─────────────────────────────────────────────────┤
│ 【内核层】Linux内核驱动,接收系统调用,操控硬件 │
├─────────────────────────────────────────────────┤
│ 【硬件层】RK3576外设:GPIO、I2C传感器、LED等   │
└─────────────────────────────────────────────────┘

大白话流程拆解(以点亮 LED 为例):

  1. Java 层点击按钮,调用 JNI 的native void ledOn(int gpioNum)方法;
  2. JNI 层接收到调用,把 GPIO 编号传给 C++ 层的硬件控制函数;
  3. C++ 层用open打开/dev/gpiochip0设备节点,拿到文件描述符;
  4. C++ 层用ioctl给驱动发送命令,设置 GPIO 为输出模式、高电平;
  5. 内核驱动收到 ioctl 命令,配置 GPIO 寄存器,把对应引脚置为高电平;
  6. 硬件层的 LED 收到高电平,成功点亮;
  7. C++ 层用close关闭设备节点,释放资源。

看到这里你就懂了:咱们前面学的 JNI,只是打通了 Java 到 C++ 的路,而今天学的设备节点操作,才是打通 C++ 到硬件的路,两者结合,才能实现安卓 APP 控制硬件。


二、核心 API 详解:5 个系统调用,搞定 90% 的硬件操作

Linux 给咱们提供了 5 个核心的系统调用函数,专门用来操作设备节点,咱们一个一个讲,大白话讲透参数、用法、返回值、注意事项,全是咱们开发中天天要用的,新手直接收藏。

所有函数都在<unistd.h><fcntl.h><sys/ioctl.h>这几个头文件里,C++ 代码里直接 include 就能用。

1. open:打开设备节点,获取操作权限

c

运行

复制代码
// 函数原型
int open(const char *pathname, int flags);
作用

打开指定路径的设备节点(或普通文件),返回一个文件描述符(fd),后续所有的读写、控制操作,都靠这个 fd 来完成,相当于你打开门之后拿到的门钥匙。

参数详解
  • pathname:设备节点的路径,比如"/dev/gpiochip0",必须是绝对路径。
  • flags:打开方式,咱们硬件操作常用的有 3 个,多个用|分隔:
    • O_RDONLY:只读方式打开,比如只需要读传感器数据,用这个。
    • O_WRONLY:只写方式打开,比如只需要控制 GPIO 输出电平,用这个。
    • O_RDWR:读写方式打开,最常用,既能读又能写,硬件控制基本都用这个。
    • 补充:O_NONBLOCK:非阻塞模式,一般串口、网络用,硬件操作默认不用。
返回值
  • 成功:返回一个≥0 的整数,就是文件描述符 fd,后续所有操作都要用到它。
  • 失败:返回-1,同时会设置全局变量errno,可以用strerror(errno)打印具体的失败原因(比如权限不足、设备不存在)。
新手必记注意事项
  1. 必须判断返回值!很多新手 open 之后直接用 fd,万一打开失败,fd 是 - 1,后续操作直接崩溃。
  2. 设备路径别写错 !一个字符都不能错,比如/dev/gpiochip0写成/dev/gpiochip00,直接打开失败。
  3. 用完一定要 close!不然会造成文件描述符泄漏,跑久了系统会耗尽文件描述符,再也打不开任何文件。
示例代码

c

运行

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

#define LOG_TAG "Heipiao_RK3576_HW"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// 打开GPIO设备节点
int fd = open("/dev/gpiochip0", O_RDWR);
if (fd < 0) {
    LOGE("打开设备节点失败,原因:%s", strerror(errno));
    return -1;
}
LOGE("设备节点打开成功,fd=%d", fd);

2. close:关闭设备节点,释放资源

c

运行

复制代码
// 函数原型
int close(int fd);
作用

关闭之前用 open 打开的文件描述符,释放系统资源,相当于用完门之后锁门。

参数详解
  • fd:open 返回的文件描述符。
返回值
  • 成功:返回 0
  • 失败:返回 - 1,一般只有 fd 已经关闭过的情况才会失败。
新手必记注意事项
  1. 一个 open 必须对应一个 close!成对出现,哪怕中间业务逻辑失败 return 了,也要先 close 再 return,不然会泄漏。
  2. 关闭之后绝对不能再用这个 fd!不然就是野指针操作,直接崩溃。

3. read:从设备节点读取数据

c

运行

复制代码
// 函数原型
ssize_t read(int fd, void *buf, size_t count);
作用

从 fd 对应的设备里,读取 count 个字节的数据,存到 buf 缓冲区里,比如从传感器里读取温度数据。

参数详解
  • fd:open 返回的文件描述符。
  • buf:用来存读取到的数据的缓冲区,一般是 char 数组。
  • count:要读取的字节数。
返回值
  • 成功:返回实际读取到的字节数(≥0),如果返回 0,说明已经读到文件末尾了。
  • 失败:返回 - 1,errno 会记录失败原因。

4. write:向设备节点写入数据

c

运行

复制代码
// 函数原型
ssize_t write(int fd, const void *buf, size_t count);
作用

把 buf 缓冲区里的 count 个字节数据,写入到 fd 对应的设备里,比如给串口发送数据、给 GPIO 写电平值。

参数详解
  • fd:open 返回的文件描述符。
  • buf:要写入的数据的缓冲区。
  • count:要写入的字节数。
返回值
  • 成功:返回实际写入的字节数(≥0)。
  • 失败:返回 - 1,errno 记录失败原因。

5. ioctl:硬件控制的万能接口(重点中的重点)

c

运行

复制代码
// 函数原型
int ioctl(int fd, unsigned long request, ...);
作用

这是硬件控制里最核心、最常用的函数,没有之一。read/write 只能用来读写数据,而ioctl 是专门用来给硬件发送控制命令的,比如设置 GPIO 的输入输出方向、配置 I2C 的设备地址、设置串口的波特率、控制电机的转速等等,所有没法用 read/write 完成的控制操作,全靠 ioctl。

还是用洗衣机的类比:read/write 是放衣服 / 拿衣服,而 ioctl 就是按洗衣机的所有功能按钮,设置模式、温度、转速,是真正控制硬件行为的核心。

参数详解
  • fd:open 返回的文件描述符。
  • request:控制命令码,也叫 ioctl 号,这个是驱动程序提前定义好的,每个命令对应一个唯一的编号,咱们不能自己瞎写,必须和驱动里的定义一致。
  • ...:可变参数,一般是一个指针,用来传递命令的参数,或者接收驱动返回的数据,具体传什么,由 request 命令决定。
返回值
  • 成功:返回≥0 的整数,具体含义由驱动定义,一般 0 就是成功。
  • 失败:返回 - 1,errno 记录失败原因。
新手必记注意事项
  1. request 命令码必须和驱动一致 !这个是新手最容易踩的坑,命令码写错了,ioctl 要么没反应,要么直接报错。咱们用的 RK3576 标准驱动,命令码都在 Linux 内核的标准头文件里,比如 GPIO 的命令在<linux/gpio.h>里,直接 include 就行,不用自己定义。
  2. 参数类型必须匹配!驱动要求传什么类型的参数,你就传什么,比如驱动要一个结构体指针,你传个 int 值,直接崩溃。
  3. ioctl 是同步阻塞的:调用之后会等驱动执行完才返回,别在主线程里调用耗时的 ioctl 操作,会卡 UI。

三、新手头号拦路虎:权限问题根治方案

90% 的新手第一次操作设备节点,都会遇到Permission denied(权限拒绝)的报错,改了 chmod 还是不行,根本找不到原因。今天佬哥我把这个问题的根源和根治方案全给你讲透,分临时调试方案永久产品方案,新手直接照着做。

1. 权限问题的两个根源

安卓系统是基于 Linux 的,但比原生 Linux 多了两层权限管控,你打不开设备节点,99% 是这两层拦着你:

  1. Linux 文件权限/dev/下的设备节点,默认的所有者是 root,权限是600(只有 root 能读写),普通的安卓 APP 是 system 用户,没有读写权限。
  2. SELinux 安全策略:安卓的强制访问控制(MAC)系统,哪怕你把文件权限改成 777,SELinux 规则里不允许 APP 访问这个设备节点,还是会被拦截,这是新手最容易忽略的点!

2. 临时调试方案(开发阶段用,重启失效)

咱们开发调试的时候,用这个方案最快,一步到位,不用改固件,但是开发板重启之后就会失效,需要重新执行。

步骤 1:adb 连接 RK3576 开发板,获取 root 权限

bash

运行

复制代码
# 连接开发板后,执行
adb root
adb remount
adb shell
步骤 2:修改设备节点的 Linux 文件权限

bash

运行

复制代码
# 比如给gpiochip0设置777权限,所有用户都能读写
chmod 777 /dev/gpiochip0
# 给所有i2c设备设置权限
chmod 777 /dev/i2c-*
步骤 3:临时关闭 SELinux(最关键的一步)

bash

运行

复制代码
# 临时关闭SELinux,重启失效
setenforce 0
# 查看SELinux状态,0是关闭,1是开启
getenforce

执行完这三步,你的 APP 就能正常打开设备节点了,再也不会报权限拒绝。

3. 永久方案(产品开发用,改固件永久生效)

如果是做产品,总不能每次重启都手动改权限,咱们需要修改 RK3576 的安卓固件源码,把权限配置写进固件里,编译之后永久生效。

方案 1:修改 ueventd.rc,永久设置设备节点权限

ueventd.rc 是 Linux 系统启动时,用来设置设备节点权限的配置文件,RK3576 的专属配置文件在固件源码的device/rockchip/common/ueventd.rockchip.rc路径下。

修改方法:在文件末尾添加以下内容,给咱们要用的设备节点设置权限:

rc

复制代码
# 给GPIO设备节点设置权限,所有者system,权限666,所有用户都能读写
/dev/gpiochip*               0666   system   system
# 给I2C设备节点设置权限
/dev/i2c-*                   0666   system   system
# 给串口设备设置权限
/dev/ttyS*                   0666   system   system
# 给RGA设备设置权限
/dev/rga                     0666   system   system

修改之后,重新编译安卓固件,烧录到开发板里,设备节点启动后就会自动设置好权限,重启不会失效。

方案 2:添加 SELinux 规则,允许 APP 访问设备节点

关闭 SELinux 只是调试用,产品开发建议开启 SELinux,只给 APP 开放需要的权限,保证系统安全。RK3576 的 SELinux 规则在固件源码的device/rockchip/common/sepolicy/目录下,给你的 APP 添加对应的权限规则即可。

新手开发阶段,直接用临时方案就行,等做产品的时候再研究 SELinux 规则,不用上来就啃,太劝退。


四、RK3576 实战:从 JNI 调用驱动,跑通完整流程

讲完了理论,咱们直接上实战,分两个 demo,从易到难,新手跟着敲,100% 能跑通,彻底掌握设备节点和 ioctl 的用法。

前置准备

  1. RK3576 开发板已经通过 adb 连接,执行了adb rootsetenforce 0chmod 777 /dev/gpiochip0,权限已经配置好。
  2. 之前的 JNI 项目环境已经搭好,NDK r21e、CMake 3.10.2,abiFilters 只留 arm64-v8a。

实战 demo1:基础设备节点操作,验证 open/close 流程

这个 demo 不用任何外接硬件,只要 RK3576 开发板就能跑,核心是验证咱们能不能正常打开设备节点、获取设备信息,掌握 open/close 的用法,为后面的硬件操作打基础。

步骤 1:Java 层定义 native 方法

在 MainActivity.java 里定义 native 方法,用来打开设备节点,获取 GPIO 芯片信息:

java

运行

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

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.heipiao.rk3576.jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        TextView tv = binding.sampleText;
        // 调用native方法,打开GPIO设备节点,获取信息
        String result = openGpioDevice();
        tv.setText(result);
    }

    // native方法:打开GPIO设备节点,返回操作结果
    public native String openGpioDevice();
}
步骤 2:C++ 层实现 JNI 方法,操作设备节点

打开 native-lib.cpp,编写代码,实现设备节点的打开、信息获取、关闭:

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 "Heipiao_RK3576_HW"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

extern "C" JNIEXPORT jstring JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_openGpioDevice(JNIEnv *env, jobject thiz) {
    char result[512];
    int offset = 0;

    // 步骤1:打开GPIO设备节点
    const char *device_path = "/dev/gpiochip0";
    int fd = open(device_path, O_RDWR);
    if (fd < 0) {
        offset += sprintf(result + offset, "打开设备节点%s失败!\n原因:%s", device_path, strerror(errno));
        LOGE("%s", result);
        return env->NewStringUTF(result);
    }
    offset += sprintf(result + offset, "设备节点%s打开成功!fd=%d\n", device_path, fd);
    LOGD("设备节点打开成功,fd=%d", fd);

    // 步骤2:用ioctl获取GPIO芯片信息,验证ioctl用法
    struct gpiochip_info chip_info;
    // 调用ioctl,发送GPIO_GET_CHIPINFO_IOCTL命令,获取芯片信息
    int ret = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &chip_info);
    if (ret < 0) {
        offset += sprintf(result + offset, "获取GPIO芯片信息失败!\n原因:%s", strerror(errno));
        LOGE("%s", result);
        close(fd); // 失败也要关闭fd
        return env->NewStringUTF(result);
    }

    // 解析获取到的芯片信息
    offset += sprintf(result + offset, "===== GPIO芯片信息 =====\n");
    offset += sprintf(result + offset, "芯片名称:%s\n", chip_info.name);
    offset += sprintf(result + offset, "芯片标签:%s\n", chip_info.label);
    offset += sprintf(result + offset, "GPIO线数量:%d\n", chip_info.lines);
    LOGD("GPIO芯片:%s,线数量:%d", chip_info.name, chip_info.lines);

    // 步骤3:关闭设备节点,释放资源
    close(fd);
    offset += sprintf(result + offset, "\n设备节点已关闭,操作完成!");

    // 返回结果给Java层
    return env->NewStringUTF(result);
}
步骤 3:编译运行,看结果

点击 Run,把 APK 安装到 RK3576 开发板上,如果一切正常,你会在屏幕上看到:

plaintext

复制代码
设备节点/dev/gpiochip0打开成功!fd=xx
===== GPIO芯片信息 =====
芯片名称:gpiochip0
芯片标签:200f0000.gpio
GPIO线数量:32

设备节点已关闭,操作完成!

恭喜你!你已经成功通过 JNI + 系统调用,访问了 RK3576 的内核驱动,拿到了硬件信息,安卓 APP 控制硬件的大门,已经正式为你打开了!

实战 demo2:ioctl 进阶,获取 GPIO 引脚信息

这个 demo 咱们进一步用 ioctl 获取具体 GPIO 引脚的信息,比如 GPIO3_A5 对应的引脚信息,为下一章的 GPIO 点灯实战做铺垫,代码和上面的 demo 结构一致,只需要修改 C++ 层的逻辑:

cpp

运行

复制代码
// 接上面的代码,打开设备节点成功后,添加以下内容
// 要查询的GPIO引脚编号,比如GPIO0_A5,就是第5个引脚
int gpio_line = 5;
struct gpioline_info line_info;
memset(&line_info, 0, sizeof(line_info));
line_info.line_offset = gpio_line;

// 调用ioctl,获取引脚信息
ret = ioctl(fd, GPIO_GET_LINEINFO_IOCTL, &line_info);
if (ret < 0) {
    offset += sprintf(result + offset, "获取GPIO%d信息失败!\n原因:%s", gpio_line, strerror(errno));
    close(fd);
    return env->NewStringUTF(result);
}

// 解析引脚信息
offset += sprintf(result + offset, "\n===== GPIO%d引脚信息 =====\n", gpio_line);
offset += sprintf(result + offset, "引脚名称:%s\n", line_info.name);
offset += sprintf(result + offset, "引脚使用者:%s\n", line_info.consumer);
offset += sprintf(result + offset, "引脚方向:%s\n", (line_info.flags & GPIOLINE_FLAG_IS_OUT) ? "输出" : "输入");

运行之后,你就能看到对应 GPIO 引脚的详细信息,彻底掌握 ioctl 的用法。


五、新手踩坑急救站:99% 的硬件操作问题都在这里解决

佬哥我把 RK3576 安卓底层开发中,设备节点操作的所有高频坑,全整理出来了,遇到问题直接来这里找解决方案,不用到处百度。

坑 1:open 设备节点一直报 Permission denied

99% 的原因

  1. 没给设备节点设置读写权限,chmod 没执行;
  2. SELinux 没关闭,哪怕权限 777 还是被拦截;
  3. 没有 adb root,普通用户没法修改 /dev 目录的权限。

解决方案

  1. 先执行adb rootadb remount
  2. 执行chmod 777 /dev/你的设备节点
  3. 执行setenforce 0关闭 SELinux;
  4. ls -l /dev/你的设备节点查看权限,确认是 777。

坑 2:ioctl 一直返回 - 1,报错 Invalid argument

原因

  1. ioctl 的 request 命令码写错了,和驱动里的定义不一致;
  2. 传递的参数类型不对,比如驱动要结构体指针,你传了个值;
  3. 参数没有初始化,结构体里的内容不对;
  4. 文件描述符 fd 是无效的,open 失败了没判断。

解决方案

  1. 确认命令码是内核标准头文件里的,比如 GPIO 的命令要 include <linux/gpio.h>
  2. 严格按照驱动要求的参数类型传递,结构体要先 memset 清零再赋值;
  3. 必须判断 open 的返回值,fd≥0 才能调用 ioctl。

坑 3:APP 跑几次之后,open 一直失败,报错 Too many open files

原因:文件描述符泄漏,open 了之后没有 close,每次打开都会占用一个 fd,系统的文件描述符是有限的,耗尽之后就再也打不开任何文件了。

解决方案

  1. 一个 open 必须对应一个 close,成对出现,哪怕业务逻辑失败,也要先 close 再 return;
  2. adb shell lsof -p 你的APP进程号查看打开的文件描述符,确认有没有泄漏。

坑 4:设备节点不存在,报错 No such file or directory

原因

  1. 设备节点路径写错了,一个字符都不能错;
  2. 内核没有编译对应的驱动,设备节点没有生成;
  3. 硬件没有接好,驱动没有识别到设备。

解决方案

  1. adb shell ls /dev/查看对应的设备节点是否真的存在;
  2. 确认 RK3576 的固件里已经开启了对应的驱动,比如 GPIO、I2C 驱动都是默认开启的。

坑 5:在主线程里调用 ioctl,APP 卡成 PPT

原因:ioctl 是同步阻塞的,会等驱动执行完才返回,如果驱动里的操作耗时比较长,就会阻塞主线程,导致 UI 卡顿。

解决方案:把所有硬件操作都放到子线程里执行,绝对不要在主线程里做 open/ioctl/close 操作。


本章总结 + 下章预告

【本章总结】

今天这一章,咱们彻底打通了安卓 APP 到底层硬件的最后一公里,核心就 4 件事:

  1. 搞懂了 Linux "一切皆文件" 的核心思想,知道了设备节点就是应用层操控硬件的唯一入口;
  2. 掌握了 5 个核心系统调用:open/close/read/write/ioctl,尤其是 ioctl 的核心用法,搞定了 90% 的硬件控制操作;
  3. 根治了新手头号拦路虎 ------ 权限问题,学会了临时调试方案和永久产品方案,再也不会被 Permission denied 搞崩;
  4. 跑通了 RK3576 的实战 demo,通过 JNI 成功访问了 GPIO 驱动,拿到了硬件信息,为后续的硬件控制打下了坚实的基础。

【下章预告】

下一章,咱们正式进入大家期待已久的RK3576 实战(一):JNI 调用 GPIO 驱动点亮 LED。我会给你讲透 RK3576 的 GPIO 编号计算规则,手把手带你通过 JNI + 设备节点 + ioctl,实现 GPIO 的输入输出控制,亲手点亮开发板上的 LED 灯,真正实现安卓 APP 控制硬件!


我是黒漂技术佬,专注给小白搞懂 RK3576 安卓底层、JNI/NDK、嵌入式开发的保姆级教程,跟着我,保证你不迷路、不踩坑!

兄弟们,跟着本章成功跑通设备节点操作的,麻烦评论区扣个「驱动访问入门成功」!有啥问题、踩了啥坑,评论区直接留言,佬哥我挨个回复!点赞收藏关注不迷路,咱们下一章点灯见!

相关推荐
阿拉斯攀登1 天前
【RK3576 安卓 JNI/NDK 系列 01】概念扫盲 + RK3576 安卓开发架构全解析
ndk·嵌入式安卓·安卓jni·瑞芯微rk3576·底层驱动
阿拉斯攀登2 天前
第 11 篇 RK 平台安卓驱动实战 4:I2C 设备驱动开发,以 OLED 屏为例
android·驱动开发·i2c·瑞芯微·嵌入式驱动·rk3576·嵌入式安卓
阿拉斯攀登2 天前
第 10 篇 RK 平台安卓驱动实战 3:PWM 驱动开发,实现 LED 呼吸灯 + 电机调速
驱动开发·嵌入式硬件·pwm·瑞芯微·嵌入式驱动·rk3576·嵌入式安卓
阿拉斯攀登7 天前
【瑞芯微 RK 系列 + 安卓驱动全栈教程】博客系列
嵌入式硬件·安卓·瑞芯微·rk3576·嵌入式安卓·安卓驱动
大大祥2 个月前
Android FFmpeg集成
android·ffmpeg·kotlin·音视频·jni·ndk·音视频编解码
此去正年少2 个月前
编写adb脚本工具对Android设备上的闪退问题进行监控分析
android·adb·logcat·ndk·日志监控
特立独行的猫a2 个月前
[鸿蒙PC三方库交叉编译] libtool与鸿蒙SDK工具链的冲突解决方案:从glibc污染到参数透传的深度解析
华为·harmonyos·ndk·三方库移植·鸿蒙pc·libtool
hslinux3 个月前
NDK 通过configure 编译C++源码通用脚本
android·c++·ndk·configure
iloveAnd4 个月前
Android NDK入门
android·ndk