目录
[一、先搞懂核心逻辑:安卓 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 为例):
- Java 层点击按钮,调用 JNI 的
native void ledOn(int gpioNum)方法; - JNI 层接收到调用,把 GPIO 编号传给 C++ 层的硬件控制函数;
- C++ 层用
open打开/dev/gpiochip0设备节点,拿到文件描述符; - C++ 层用
ioctl给驱动发送命令,设置 GPIO 为输出模式、高电平; - 内核驱动收到 ioctl 命令,配置 GPIO 寄存器,把对应引脚置为高电平;
- 硬件层的 LED 收到高电平,成功点亮;
- 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)打印具体的失败原因(比如权限不足、设备不存在)。
新手必记注意事项
- 必须判断返回值!很多新手 open 之后直接用 fd,万一打开失败,fd 是 - 1,后续操作直接崩溃。
- 设备路径别写错 !一个字符都不能错,比如
/dev/gpiochip0写成/dev/gpiochip00,直接打开失败。 - 用完一定要 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 已经关闭过的情况才会失败。
新手必记注意事项
- 一个 open 必须对应一个 close!成对出现,哪怕中间业务逻辑失败 return 了,也要先 close 再 return,不然会泄漏。
- 关闭之后绝对不能再用这个 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 记录失败原因。
新手必记注意事项
- request 命令码必须和驱动一致 !这个是新手最容易踩的坑,命令码写错了,ioctl 要么没反应,要么直接报错。咱们用的 RK3576 标准驱动,命令码都在 Linux 内核的标准头文件里,比如 GPIO 的命令在
<linux/gpio.h>里,直接 include 就行,不用自己定义。 - 参数类型必须匹配!驱动要求传什么类型的参数,你就传什么,比如驱动要一个结构体指针,你传个 int 值,直接崩溃。
- ioctl 是同步阻塞的:调用之后会等驱动执行完才返回,别在主线程里调用耗时的 ioctl 操作,会卡 UI。
三、新手头号拦路虎:权限问题根治方案
90% 的新手第一次操作设备节点,都会遇到Permission denied(权限拒绝)的报错,改了 chmod 还是不行,根本找不到原因。今天佬哥我把这个问题的根源和根治方案全给你讲透,分临时调试方案 和永久产品方案,新手直接照着做。
1. 权限问题的两个根源
安卓系统是基于 Linux 的,但比原生 Linux 多了两层权限管控,你打不开设备节点,99% 是这两层拦着你:
- Linux 文件权限 :
/dev/下的设备节点,默认的所有者是 root,权限是600(只有 root 能读写),普通的安卓 APP 是 system 用户,没有读写权限。 - 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 的用法。
前置准备
- RK3576 开发板已经通过 adb 连接,执行了
adb root、setenforce 0、chmod 777 /dev/gpiochip0,权限已经配置好。 - 之前的 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% 的原因:
- 没给设备节点设置读写权限,chmod 没执行;
- SELinux 没关闭,哪怕权限 777 还是被拦截;
- 没有 adb root,普通用户没法修改 /dev 目录的权限。
解决方案:
- 先执行
adb root、adb remount; - 执行
chmod 777 /dev/你的设备节点; - 执行
setenforce 0关闭 SELinux; - 用
ls -l /dev/你的设备节点查看权限,确认是 777。
坑 2:ioctl 一直返回 - 1,报错 Invalid argument
原因:
- ioctl 的 request 命令码写错了,和驱动里的定义不一致;
- 传递的参数类型不对,比如驱动要结构体指针,你传了个值;
- 参数没有初始化,结构体里的内容不对;
- 文件描述符 fd 是无效的,open 失败了没判断。
解决方案:
- 确认命令码是内核标准头文件里的,比如 GPIO 的命令要 include
<linux/gpio.h>; - 严格按照驱动要求的参数类型传递,结构体要先 memset 清零再赋值;
- 必须判断 open 的返回值,fd≥0 才能调用 ioctl。
坑 3:APP 跑几次之后,open 一直失败,报错 Too many open files
原因:文件描述符泄漏,open 了之后没有 close,每次打开都会占用一个 fd,系统的文件描述符是有限的,耗尽之后就再也打不开任何文件了。
解决方案:
- 一个 open 必须对应一个 close,成对出现,哪怕业务逻辑失败,也要先 close 再 return;
- 用
adb shell lsof -p 你的APP进程号查看打开的文件描述符,确认有没有泄漏。
坑 4:设备节点不存在,报错 No such file or directory
原因:
- 设备节点路径写错了,一个字符都不能错;
- 内核没有编译对应的驱动,设备节点没有生成;
- 硬件没有接好,驱动没有识别到设备。
解决方案:
- 用
adb shell ls /dev/查看对应的设备节点是否真的存在; - 确认 RK3576 的固件里已经开启了对应的驱动,比如 GPIO、I2C 驱动都是默认开启的。
坑 5:在主线程里调用 ioctl,APP 卡成 PPT
原因:ioctl 是同步阻塞的,会等驱动执行完才返回,如果驱动里的操作耗时比较长,就会阻塞主线程,导致 UI 卡顿。
解决方案:把所有硬件操作都放到子线程里执行,绝对不要在主线程里做 open/ioctl/close 操作。
本章总结 + 下章预告
【本章总结】
今天这一章,咱们彻底打通了安卓 APP 到底层硬件的最后一公里,核心就 4 件事:
- 搞懂了 Linux "一切皆文件" 的核心思想,知道了设备节点就是应用层操控硬件的唯一入口;
- 掌握了 5 个核心系统调用:open/close/read/write/ioctl,尤其是 ioctl 的核心用法,搞定了 90% 的硬件控制操作;
- 根治了新手头号拦路虎 ------ 权限问题,学会了临时调试方案和永久产品方案,再也不会被 Permission denied 搞崩;
- 跑通了 RK3576 的实战 demo,通过 JNI 成功访问了 GPIO 驱动,拿到了硬件信息,为后续的硬件控制打下了坚实的基础。
【下章预告】
下一章,咱们正式进入大家期待已久的RK3576 实战(一):JNI 调用 GPIO 驱动点亮 LED。我会给你讲透 RK3576 的 GPIO 编号计算规则,手把手带你通过 JNI + 设备节点 + ioctl,实现 GPIO 的输入输出控制,亲手点亮开发板上的 LED 灯,真正实现安卓 APP 控制硬件!
我是黒漂技术佬,专注给小白搞懂 RK3576 安卓底层、JNI/NDK、嵌入式开发的保姆级教程,跟着我,保证你不迷路、不踩坑!
兄弟们,跟着本章成功跑通设备节点操作的,麻烦评论区扣个「驱动访问入门成功」!有啥问题、踩了啥坑,评论区直接留言,佬哥我挨个回复!点赞收藏关注不迷路,咱们下一章点灯见!