【RK3576 安卓 JNI/NDK 系列 08】RK3576 实战(二):JNI 调用 I2C 驱动读取传感器数据

目录

本章核心说明

[一、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 读写标准流程

  1. 调用open打开 I2C 设备节点(如/dev/i2c-2),获取文件描述符 fd
  2. 构造i2c_msg数组,配置写 / 读参数
  3. 封装i2c_rdwr_ioctl_data结构体,传入i2c_msg数组
  4. 调用ioctl(fd, I2C_RDWR, &rdwr_data)执行复合读写操作
  5. 解析读取到的原始数据,完成物理量转换
  6. 调用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>

六、编译运行与调试

  1. 权限配置 :运行前执行 adb 命令,配置设备节点权限与 SELinux:

    bash

    运行

    复制代码
    adb root
    adb remount
    adb shell chmod 777 /dev/i2c-*
    adb shell setenforce 0
  2. 编译运行:Android Studio 中选择 RK3576 开发板,点击 Run 安装 APK

  3. 功能验证:APP 打开后自动初始化 I2C,每秒刷新一次温湿度数据,Logcat 可查看详细的读取日志

  4. 调试技巧 :若读取失败,先通过i2cdetect命令确认硬件地址,再检查接线、权限、从机地址是否正确

七、新手必避的核心坑点

  1. I2C 从机地址错误 :SHT30 的 7 位地址是0x44,无需左移一位,i2c_msg的 addr 字段直接填 7 位地址即可,这是新手最常见的错误
  2. 电平不匹配:SHT30 是 3.3V 器件,必须接 3.3V 电源,IO 口也是 3.3V 电平,接 5V 会直接烧毁芯片
  3. 共地问题:传感器与开发板必须共地,否则会出现通信时通时断、数据乱码的问题
  4. 主线程执行硬件操作:I2C 读写是阻塞操作,必须放在子线程中执行,否则会阻塞主线程,导致 ANR
  5. 设备节点权限不足 :必须给/dev/i2c-x节点配置 777 权限,同时关闭 SELinux,否则会出现Permission denied错误
  6. 总线被复用:若 I2C 通信无响应,检查设备树中对应引脚是否被复用为 GPIO 功能,原厂固件默认开启 i2c-2/3,若自行修改过设备树需重新配置
  7. 数据转换公式错误:原始数据是 16 位无符号整数,转换时必须除以 65535,而非 65536,否则会出现固定的数值偏差

下章预告

下一章将进入 RK3576 实战第三部分:JNI 调用 librga 实现 2D 硬件加速图像处理,讲解瑞芯微 RGA 硬件加速引擎的核心原理、librga 库的 JNI 集成与调用,实现图像缩放、旋转、格式转换等操作,对比 CPU 与硬件加速的性能差异。

相关推荐
赶路人儿3 小时前
常见的mcp配置
android·adb
符哥20083 小时前
充电桩 WiFi 局域网配网(Android/Kotlin)流程、指令及实例说明文档
android·开发语言·kotlin
没有了遇见4 小时前
Android 项目架构之<用户信息模块>
android
Georgewu5 小时前
如何判断应用在鸿蒙卓易通或者出境易环境下?
android·harmonyos
localbob6 小时前
Pico 4XVR 1.10.13安装包下载与安装教程 ico 4XVR最新版下载、4XVR 1.10.13 APK安装包、Pico VR看电影软件、4XVR完整版安装教程、Pico 4播放器推荐、V
android·vr·vr播放器·vr眼镜播放器下载·pico 4xvr·4xvr下载·pico 4xvr最新版安装包
峥嵘life6 小时前
Android16 EDLA【CTS】CtsConnectivityMultiDevicesTestCases存在fail项
android·学习
大傻^7 小时前
SpringAI2.0 Null Safety 实战:JSpecify 注解体系与 Kotlin 互操作
android·开发语言·人工智能·kotlin·springai
游戏开发爱好者87 小时前
React Native iOS 代码如何加密,JS 打包 和 IPA 混淆
android·javascript·react native·ios·小程序·uni-app·iphone
kcuwu.7 小时前
Python判断及循环
android·java·python