目录
[一、I2C 基础与 RK3576 硬件资源](#一、I2C 基础与 RK3576 硬件资源)
[1. I2C 总线核心原理](#1. I2C 总线核心原理)
[2. RK3576 I2C 硬件资源](#2. RK3576 I2C 硬件资源)
[1. 物料清单](#1. 物料清单)
[2. 接线规则](#2. 接线规则)
[3. 硬件连接验证](#3. 硬件连接验证)
[三、Linux I2C 驱动核心操作规范](#三、Linux I2C 驱动核心操作规范)
[1. 核心依赖头文件](#1. 核心依赖头文件)
[2. 核心 ioctl 命令](#2. 核心 ioctl 命令)
[3. 核心数据结构:i2c_msg](#3. 核心数据结构:i2c_msg)
[4. I2C 读写标准流程](#4. I2C 读写标准流程)
[四、C++ 层 I2C 通用工具函数实现](#四、C++ 层 I2C 通用工具函数实现)
[1. 通用 I2C 工具函数](#1. 通用 I2C 工具函数)
[2. SHT30 专用功能实现](#2. SHT30 专用功能实现)
[五、JNI 层封装与安卓 APP 实现](#五、JNI 层封装与安卓 APP 实现)
[1. Java 层 native 方法定义与 UI 实现](#1. Java 层 native 方法定义与 UI 实现)
[2. JNI 层接口实现](#2. JNI 层接口实现)
[3. CMakeLists.txt 配置](#3. CMakeLists.txt 配置)
[4. 布局文件(activity_main.xml)](#4. 布局文件(activity_main.xml))
本章核心说明
本章基于 Linux 标准i2c-dev驱动框架,通过 JNI+ioctl 系统调用实现 RK3576 的 I2C 总线读写操作,完成工业级常用的 SHT30 温湿度传感器数据采集,最终实现安卓 APP 实时显示温湿度数据。所有代码遵循 Linux I2C 开发规范,适配 RK3576 原厂安卓固件,可直接复用至工业级项目。
一、I2C 基础与 RK3576 硬件资源
1. I2C 总线核心原理
I2C 是一种两线式串行同步总线,仅需两根线即可实现主设备与多个从设备之间的通信,是嵌入式开发中传感器、存储、外设控制最常用的总线协议:
- SDA:串行数据线,用于传输数据
- SCL:串行时钟线,由主设备提供时钟,同步数据传输
- 主从架构:同一总线上只有一个主设备(RK3576),可挂载多个从设备(传感器、外设),每个从设备有唯一的 7 位 / 10 位硬件地址
- 半双工通信:同一时间只能单向传输数据,读写操作通过起始位、读写标志位区分
2. RK3576 I2C 硬件资源
RK3576 芯片内置 10 路独立硬件 I2C 控制器,每路控制器对应一个设备节点,路径为/dev/i2c-0 ~ /dev/i2c-9,其中i2c-2、i2c-3两路为开发板厂商默认引出到排针的常用总线,原厂固件默认开启驱动,无需修改设备树即可直接使用。
总线可用性验证(新手必做)
通过 adb 命令可直接验证 I2C 总线是否可用,提前排除硬件问题:
bash
运行
# 1. 进入开发板shell
adb root
adb shell
# 2. 安装i2c-tools工具(原厂固件一般自带,无则通过apt安装)
apt install i2c-tools
# 3. 扫描i2c-2总线上的设备,-y参数跳过交互确认
i2cdetect -y 2
若扫描结果中出现传感器的硬件地址(SHT30 默认地址为0x44),说明硬件连接正常、总线可用。
二、硬件准备与连接
1. 物料清单
- RK3576 核心板 + 底板(任意厂商量产板均可)
- SHT30 温湿度传感器模块(I2C 接口,工业级常用,精度 ±0.2℃/±2% RH)
- 4P 杜邦线
- 3.3V 电源(开发板排针直接提供)
2. 接线规则
表格
| SHT30 模块引脚 | RK3576 开发板引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 必须接 3.3V,禁止接 5V,否则烧毁传感器芯片 |
| GND | GND | 共地,必须连接,否则会出现通信异常 |
| SDA | I2C2_SDA | 对应 i2c-2 总线的数据线,需与开发板原理图引脚对应 |
| SCL | I2C2_SCL | 对应 i2c-2 总线的时钟线,需与 SDA 同一路总线 |
3. 硬件连接验证
接线完成后,开发板上电,通过上述i2cdetect -y 2命令扫描总线,若输出表格中出现44地址,说明硬件连接正常,可进入代码开发环节。
三、Linux I2C 驱动核心操作规范
Linux 系统将所有 I2C 控制器抽象为/dev/i2c-x字符设备节点,应用层通过open打开设备节点,通过ioctl完成总线配置与数据读写,是安卓环境下 I2C 操作的唯一标准方式。
1. 核心依赖头文件
所有 I2C 相关的 ioctl 命令、结构体均定义在 Linux 标准头文件中,NDK 已内置,直接 include 即可使用,无需额外导入:
cpp
运行
#include <linux/i2c.h> // I2C核心结构体与命令定义
#include <linux/i2c-dev.h> // i2c-dev驱动专用定义
#include <fcntl.h> // open系统调用
#include <unistd.h> // close/read/write系统调用
#include <sys/ioctl.h> // ioctl系统调用
#include <errno.h> // 错误码处理
#include <string.h> // 内存操作
2. 核心 ioctl 命令
表格
| 命令码 | 作用 | 新手使用优先级 |
|---|---|---|
I2C_SLAVE |
设置当前 I2C 总线要通信的从机地址,后续读写操作均针对该地址 | 高,简单读写场景用 |
I2C_RDWR |
批量、复合式读写操作,支持写寄存器后连续读,兼容所有 I2C 传感器,支持重复起始位 | 最高,工业级场景首选,90% 的传感器用此方式 |
I2C_RETRIES |
设置通信失败时的重试次数 | 低,默认值即可 |
I2C_TIMEOUT |
设置通信超时时间 | 低,默认值即可 |
3. 核心数据结构:i2c_msg
I2C_RDWR命令的核心是i2c_msg结构体,每一个结构体对应一次 I2C 总线传输,支持多段读写组合操作,完美适配传感器 "写寄存器地址→读数据" 的标准操作流程:
cpp
运行
struct i2c_msg {
__u16 addr; // 从机7位地址,无需左移
__u16 flags; // 传输方向:0=写,I2C_M_RD=读
__u16 len; // 本次传输的数据长度(字节数)
__u8 *buf; // 数据缓冲区指针,发送/接收数据均存在这里
};
4. I2C 读写标准流程
- 调用
open打开 I2C 设备节点(如/dev/i2c-2),获取文件描述符 fd - 构造
i2c_msg数组,配置写 / 读参数 - 封装
i2c_rdwr_ioctl_data结构体,传入i2c_msg数组 - 调用
ioctl(fd, I2C_RDWR, &rdwr_data)执行复合读写操作 - 解析读取到的原始数据,完成物理量转换
- 调用
close关闭设备节点,释放资源
四、C++ 层 I2C 通用工具函数实现
本节封装通用的 I2C 读写函数,可适配所有 I2C 传感器,同时实现 SHT30 专用的温湿度读取逻辑,所有函数均带详细注释,新手可直接复用。
1. 通用 I2C 工具函数
cpp
运行
#include <jni.h>
#include <string>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <android/log.h>
#define LOG_TAG "RK3576_I2C"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 全局I2C设备文件描述符
static int i2c_fd = -1;
// 打开I2C设备节点
// 参数:i2c_bus_path - I2C设备节点路径,如"/dev/i2c-2"
// 返回值:0成功,-1失败
int i2c_open(const char *i2c_bus_path) {
if (i2c_fd >= 0) {
LOGD("I2C设备已打开,fd=%d", i2c_fd);
return 0;
}
// 读写方式打开设备节点
i2c_fd = open(i2c_bus_path, O_RDWR);
if (i2c_fd < 0) {
LOGE("打开I2C设备失败:%s,原因:%s", i2c_bus_path, strerror(errno));
return -1;
}
LOGD("I2C设备打开成功,fd=%d", i2c_fd);
return 0;
}
// 通用I2C复合读写函数(写+读组合,传感器最常用)
// 参数:
// slave_addr - 从机7位地址
// write_buf - 写入数据缓冲区(一般是寄存器地址)
// write_len - 写入数据长度
// read_buf - 读取数据缓冲区
// read_len - 读取数据长度
// 返回值:0成功,-1失败
int i2c_write_read(uint8_t slave_addr, uint8_t *write_buf, uint8_t write_len,
uint8_t *read_buf, uint8_t read_len) {
if (i2c_fd < 0) {
LOGE("I2C设备未打开");
return -1;
}
// 构造2个i2c_msg:第1个写寄存器,第2个读数据
struct i2c_msg msgs[2];
struct i2c_rdwr_ioctl_data rdwr_data;
// 第1段:写操作,发送寄存器地址/命令
msgs[0].addr = slave_addr;
msgs[0].flags = 0; // 0=写操作
msgs[0].len = write_len;
msgs[0].buf = write_buf;
// 第2段:读操作,读取传感器返回数据
msgs[1].addr = slave_addr;
msgs[1].flags = I2C_M_RD; // 读操作
msgs[1].len = read_len;
msgs[1].buf = read_buf;
// 封装rdwr数据
rdwr_data.msgs = msgs;
rdwr_data.nmsgs = 2; // 2段传输
// 执行ioctl读写操作
int ret = ioctl(i2c_fd, I2C_RDWR, &rdwr_data);
if (ret < 0) {
LOGE("I2C读写失败,原因:%s", strerror(errno));
return -1;
}
LOGD("I2C读写成功,写入%d字节,读取%d字节", write_len, read_len);
return 0;
}
// 关闭I2C设备,释放资源
void i2c_close() {
if (i2c_fd >= 0) {
close(i2c_fd);
i2c_fd = -1;
LOGD("I2C设备已关闭");
}
}
2. SHT30 专用功能实现
SHT30 的标准操作流程:发送单次测量命令0x2C06→等待测量完成→读取 6 字节原始数据→转换为实际温湿度值。其中 6 字节数据结构为:温度高 8 位、温度低 8 位、温度 CRC、湿度高 8 位、湿度低 8 位、湿度 CRC。
核心转换公式
- 温度转换:
Temperature(℃) = -45 + 175 * (原始温度值 / 65535) - 湿度转换:
Humidity(%RH) = 100 * (原始湿度值 / 65535)
cpp
运行
// SHT30默认从机地址
#define SHT30_SLAVE_ADDR 0x44
// SHT30单次高精度测量命令
#define SHT30_MEASURE_CMD 0x2C06
// SHT30读取温湿度数据
// 参数:temperature - 输出温度值,humidity - 输出湿度值
// 返回值:0成功,-1失败
int sht30_read_data(float *temperature, float *humidity) {
if (temperature == NULL || humidity == NULL) {
LOGE("输出参数为空");
return -1;
}
// 1. 构造写入命令:0x2C 0x06
uint8_t write_buf[2] = {SHT30_MEASURE_CMD >> 8, SHT30_MEASURE_CMD & 0xFF};
// 2. 读取缓冲区:6字节数据
uint8_t read_buf[6] = {0};
// 3. 执行写命令+读数据操作
int ret = i2c_write_read(SHT30_SLAVE_ADDR, write_buf, 2, read_buf, 6);
if (ret < 0) {
LOGE("SHT30读取数据失败");
return -1;
}
// 4. 解析原始数据
uint16_t raw_temp = (read_buf[0] << 8) | read_buf[1];
uint16_t raw_hum = (read_buf[3] << 8) | read_buf[4];
// 5. 转换为实际物理量
*temperature = -45.0f + 175.0f * ((float)raw_temp / 65535.0f);
*humidity = 100.0f * ((float)raw_hum / 65535.0f);
LOGD("SHT30读取成功:温度=%.2f℃,湿度=%.2f%%RH", *temperature, *humidity);
return 0;
}
五、JNI 层封装与安卓 APP 实现
1. Java 层 native 方法定义与 UI 实现
MainActivity.java 实现 JNI 接口定义、子线程循环读取传感器数据、UI 实时更新,避免主线程阻塞导致 ANR:
java
运行
package com.heipiao.rk3576.i2c;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
// RK3576 I2C-2总线设备节点
private static final String I2C_BUS_PATH = "/dev/i2c-2";
private TextView tvTemperature;
private TextView tvHumidity;
private boolean isRunning = false;
private Thread readThread;
static {
System.loadLibrary("i2c-native");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvTemperature = findViewById(R.id.tv_temperature);
tvHumidity = findViewById(R.id.tv_humidity);
// 初始化I2C设备
new Thread(() -> {
int ret = i2cInit(I2C_BUS_PATH);
runOnUiThread(() -> {
if (ret == 0) {
Toast.makeText(this, "I2C初始化成功", Toast.LENGTH_SHORT).show();
startReadData();
} else {
Toast.makeText(this, "I2C初始化失败", Toast.LENGTH_SHORT).show();
}
});
}).start();
}
// 启动子线程循环读取传感器数据,1秒刷新一次
private void startReadData() {
isRunning = true;
readThread = new Thread(() -> {
while (isRunning) {
float[] data = new float[2];
int ret = sht30Read(data);
if (ret == 0) {
float temp = data[0];
float hum = data[1];
runOnUiThread(() -> {
tvTemperature.setText(String.format("温度:%.2f ℃", temp));
tvHumidity.setText(String.format("湿度:%.2f %%RH", hum));
});
}
// 1秒刷新一次
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
readThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
isRunning = false;
if (readThread != null) {
readThread.interrupt();
}
i2cClose();
}
// JNI native方法定义
public native int i2cInit(String i2cBusPath);
public native int sht30Read(float[] data);
public native void i2cClose();
}
2. JNI 层接口实现
在 native-lib.cpp 中实现 Java 层定义的 native 方法,封装上述 C++ 工具函数:
cpp
运行
extern "C" JNIEXPORT jint JNICALL
Java_com_heipiao_rk3576_i2c_MainActivity_i2cInit(JNIEnv *env, jobject thiz,
jstring i2c_bus_path) {
const char *path = env->GetStringUTFChars(i2c_bus_path, NULL);
if (path == NULL) {
return -1;
}
int ret = i2c_open(path);
env->ReleaseStringUTFChars(i2c_bus_path, path);
return ret;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_heipiao_rk3576_i2c_MainActivity_sht30Read(JNIEnv *env, jobject thiz,
jfloatArray data) {
float temp, hum;
int ret = sht30_read_data(&temp, &hum);
if (ret == 0) {
// 把数据写入Java数组
float result[2] = {temp, hum};
env->SetFloatArrayRegion(data, 0, 2, result);
}
return ret;
}
extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_i2c_MainActivity_i2cClose(JNIEnv *env, jobject thiz) {
i2c_close();
}
3. CMakeLists.txt 配置
cmake
cmake_minimum_required(VERSION 3.10.2)
project("i2c-native")
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(
i2c-native
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
i2c-native
${log-lib})
4. 布局文件(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:padding="30dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RK3576 SHT30温湿度监测"
android:textSize="28sp"
android:layout_marginBottom="80dp"/>
<TextView
android:id="@+id/tv_temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="温度:-- ℃"
android:textSize="32sp"
android:layout_marginBottom="40dp"/>
<TextView
android:id="@+id/tv_humidity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="湿度:-- %RH"
android:textSize="32sp"/>
</LinearLayout>
六、编译运行与调试
-
权限配置 :运行前执行 adb 命令,配置设备节点权限与 SELinux:
bash
运行
adb root adb remount adb shell chmod 777 /dev/i2c-* adb shell setenforce 0 -
编译运行:Android Studio 中选择 RK3576 开发板,点击 Run 安装 APK
-
功能验证:APP 打开后自动初始化 I2C,每秒刷新一次温湿度数据,Logcat 可查看详细的读取日志
-
调试技巧 :若读取失败,先通过
i2cdetect命令确认硬件地址,再检查接线、权限、从机地址是否正确
七、新手必避的核心坑点
- I2C 从机地址错误 :SHT30 的 7 位地址是
0x44,无需左移一位,i2c_msg的 addr 字段直接填 7 位地址即可,这是新手最常见的错误 - 电平不匹配:SHT30 是 3.3V 器件,必须接 3.3V 电源,IO 口也是 3.3V 电平,接 5V 会直接烧毁芯片
- 共地问题:传感器与开发板必须共地,否则会出现通信时通时断、数据乱码的问题
- 主线程执行硬件操作:I2C 读写是阻塞操作,必须放在子线程中执行,否则会阻塞主线程,导致 ANR
- 设备节点权限不足 :必须给
/dev/i2c-x节点配置 777 权限,同时关闭 SELinux,否则会出现Permission denied错误 - 总线被复用:若 I2C 通信无响应,检查设备树中对应引脚是否被复用为 GPIO 功能,原厂固件默认开启 i2c-2/3,若自行修改过设备树需重新配置
- 数据转换公式错误:原始数据是 16 位无符号整数,转换时必须除以 65535,而非 65536,否则会出现固定的数值偏差
下章预告
下一章将进入 RK3576 实战第三部分:JNI 调用 librga 实现 2D 硬件加速图像处理,讲解瑞芯微 RGA 硬件加速引擎的核心原理、librga 库的 JNI 集成与调用,实现图像缩放、旋转、格式转换等操作,对比 CPU 与硬件加速的性能差异。