串口通信分析与实例

串口通信(Serial Communication)是一种逐位传输数据的通信方式,本文主要分析在android中使用串口进行数据通信。

1,在android中使用串口前置设置

首先,在Android Studio集成开发环境的菜单栏选择Tools->SDK Manager,在Languages&Frameworks条目下选择Android SDK,在SDK Tools页面将NDK和CMake选中进行安装。

接着,编写CMakeLists.txt文件,使用 CMake 构建系统为 Android 项目创建共享库。

ini 复制代码
# 指定了项目所需的最低 CMake 版本为 3.4.1。如果使用的 CMake 版本低于此要求,CMake 会报错。
cmake_minimum_required(VERSION 3.4.1)

# add_library 命令用于创建一个库。
add_library( # Sets the name of the library.
        uart-control # 库的名称

        # Sets the library as a shared library.
        SHARED  # SHARED 表示创建的是共享库(动态链接库)。

        # Provides a relative path to your source file(s).
        src/main/java/com/eric/hello/jni/serialPort.cpp;) # 库的源文件路径。不过,将 C++ 源文件放在 java 目录下不太符合常规做法,通常 C++ 源文件应放在 cpp 目录中。

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

# find_library 命令用于查找指定的 NDK 库
find_library( # Sets the name of the path variable.
        log-lib  # log - lib 是一个变量名,用于存储找到的 log 库的路径。

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log) # log 是要查找的 NDK 库的名称,log 库可用于在 Android 项目中输出日志信息。

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

# target_link_libraries 命令用于将目标库与其他库进行链接
target_link_libraries( # Specifies the target library.
        uart-control  # 目标库
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})  # 之前找到的 log 库的路径,将 gpio - control 库与 log 库进行链接。

接着,在模块级的build.gradle.kts(:app)文件中,配置外部原生构建。

通过编写代码告诉 Android Gradle 插件使用 CMake 来构建项目中的原生代码(如 C、C++ 代码),并指定了 CMake 构建脚本(CMakeLists.txt 文件)的路径。

arduino 复制代码
    // externalNativeBuild:这是 Android Gradle 插件提供的一个配置块,
    // 用于配置如何构建项目中的外部原生代码。它可以包含多个子配置块,
    // 例如 cmake 和 ndkBuild,分别对应使用 CMake 和 NDK Build 两种不同的原生代码构建方式。
    externalNativeBuild {
        // cmake:表示使用 CMake 作为原生代码的构建工具。
        // CMake 是一个跨平台的构建系统,能够生成不同平台下的构建文件(如 Makefile、Visual Studio 项目文件等)。
        // path:用于指定 CMake 构建脚本(CMakeLists.txt 文件)的路径。
        // 在提供的代码中,path { "CMakeLists.txt" } 表示 CMakeLists.txt 文件位于项目的根目录下。
        cmake {
            path { "CMakeLists.txt" }
        }
    }

2, 编码底层cpp文件实现串口的打开关闭以及读写

在上面的分析中,涉及到一个serialPort.cpp文件,需要进一步地编码该文件。

scss 复制代码
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#include <termios.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <android/log.h>

#define LOG_TAG "jni_serial_port"
//日志显示的等级
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

// extern "C" 用于指定函数按照 C 语言的方式进行编译和链接,避免 C++ 的名字修饰问题。
extern"C"
{
static const char *TAG = "SerialPort";

// fd 是一个全局变量用于存储打开的串口文件描述符
int fd;


// getBaudrate 函数用于将 Java 传入的波特率转换为 termios 结构体中使用的波特率常量。
// 根据传入的 Java 整型波特率值,通过 switch 语句返回对应的 termios 结构体中的波特率常量,如果传入的波特率不匹配任何一个 case,则返回 -1。
static speed_t getBaudrate(jint baudrate) {

    switch (baudrate) {
        case 0:
            return B0;
        case 19200:
            return B19200;
        case 115200:
            return B115200; # 在termbits.h文件中,#define B115200 0x00001002
        default:
            return -1;
    }
}


JNIEXPORT jobject JNICALL Java_com_eric_hello_EricJniFile_uartOpen
        (JNIEnv *env, jclass cls, jstring path, jint baudrate) {
    speed_t speed;
    jobject mFileDescriptor;

    printf("init native Check arguments");

    //check arguments
    {
		// 检查传入的波特率是否有效,通过调用 getBaudrate 函数进行验证。
        speed = getBaudrate(baudrate);
        if (speed == -1) {
            printf("Invalid baudrate");
            return NULL;
        }
    }

    printf("init native Opening device!");

    //opening device
    {
        jboolean iscopy;
		// 将 Java 的 jstring 类型的串口路径转换为 C 风格的字符串,使用 env->GetStringUTFChars 函数。
        const char *path_utf = env->GetStringUTFChars(path, &iscopy);
        printf("Opening serial port %s", path_utf);
		// 使用 open 函数打开串口设备,设置为读写、非控制终端、非阻塞和无延迟模式。
        fd = open(path_utf, O_RDWR | O_NOCTTY | O_NONBLOCK | O_NDELAY);
        printf("open() fd = %d", fd);
        env->ReleaseStringUTFChars(path, path_utf);
        if (fd == -1) {
            printf("Cannot open port %d", baudrate);
            return NULL;
        }
    }

    printf("init native configure device!");

    /* Configure device */
    {
	    // 对串口进行配置,包括获取当前串口配置、设置为原始模式、设置波特率、禁用硬件流控制、设置数据位、停止位、校验位等,最后使用 tcsetattr 函数应用配置。
        struct termios cfg;
		// tcgetattr 函数用于获取与文件描述符 fd 相关联的终端(这里是串口)的参数,并将其存储在 termios 结构体 cfg 中。
		// 如果 tcgetattr 函数调用失败(返回非零值),则打印错误信息,关闭串口文件描述符 fd ,并返回 NULL 表示配置失败。
        if (tcgetattr(fd, &cfg)) {
            printf("Configure device tcgetattr() failed 1");
            close(fd);
            return NULL;
        }

		// cfmakeraw 函数用于将 termios 结构体 cfg 设置为原始模式。
		// 在原始模式下,输入数据以字节为单位进行处理,不进行特殊的字符处理(如回车换行转换等),适用于串口通信等需要直接处理字节数据的场景。
        cfmakeraw(&cfg);
		// cfsetispeed 函数设置输入波特率,cfsetospeed 函数设置输出波特率。
		// speed 是之前通过 getBaudrate 函数转换得到的波特率常量,这两个函数将指定的波特率应用到 termios 结构体 cfg 中。
        cfsetispeed(&cfg, speed);
        cfsetospeed(&cfg, speed);

		// cfg.c_cflag &= ~CRTSCTS; 这行代码通过按位与操作,将 c_cflag 中的 CRTSCTS 位清零,从而禁用硬件流控制(RTS/CTS)。
        cfg.c_cflag &= ~CRTSCTS;
		// cfg.c_cflag |= CS8; 将数据位设置为 8 位,CS8 是一个常量表示 8 位数据位。
        cfg.c_cflag |= CS8;
		// cfg.c_cflag &= ~CSTOPB; 清除 CSTOPB 位,设置停止位为 1 位(默认是 1 位停止位,CSTOPB 置位时表示 2 位停止位)。
        cfg.c_cflag &= ~CSTOPB;
		// cfg.c_cflag &= ~PARENB; 清除 PARENB 位,禁用奇偶校验。
        cfg.c_cflag &= ~PARENB;
		// cfg.c_iflag &= ~INPCK; 清除 INPCK 位,在禁用奇偶校验的情况下,同时禁用输入奇偶校验检查。
        cfg.c_iflag &= ~INPCK;
		// cfg.c_cflag |= CLOCAL; 则将 CLOCAL 位置位,表示本地连接(不进行调制解调器控制),忽略 DCD(载波检测)等调制解调器信号。
        cfg.c_cflag |= CLOCAL;

		// cfg.c_oflag &= ~OPOST; 通过按位与操作,清除 OPOST 位,禁用输出处理(如字符转换等),使输出数据以原始形式发送,适合串口原始数据输出的需求。
        cfg.c_oflag &= ~OPOST;  //修改输出模式,原始数据输出
		// 通过按位与操作,清除 c_lflag 中的 ICANON(规范模式)、ECHO(回显)、ECHOE(擦除字符回显)和 ISIG(信号生成)位。
		// 禁用规范模式意味着输入数据不会被解释为行(不会等待换行符等),禁用回显则输入的字符不会在终端上显示,禁用信号生成则不会对一些特殊字符(如 Ctrl+C)产生默认的信号处理。
        cfg.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); //清bit位  关闭流控字符 0x11 0x13
		// cfg.c_cc[VTIME] 用于设置读取字符的超时时间,这里设置为 1,单位是十分之一秒,即读取一个字符等待 1 * (1/10) 秒。
        // 设置等待时间和最小接收字符
        cfg.c_cc[VTIME] = 1;    //读取一个字符等待1*(1/10)s
		// cfg.c_cc[VMIN] 用于设置读取字符的最小个数,这里设置为 1,表示至少读取 1 个字符才返回。
        cfg.c_cc[VMIN] = 1;     //读取字符的最少个数为1s
		// cfg.c_iflag &= ~(IXON | IXOFF | IXANY); 清除 IXON(启用输出软件流控制)、IXOFF(启用输入软件流控制)和 IXANY(允许任何字符重启输出)位,禁用软件流控制,避免在传输特定字符(如 XON/XOFF)时出现问题。
        cfg.c_iflag &= ~(IXON | IXOFF |
                         IXANY);        //c_cc数组的VSTART和VSTOP元素被设定成DC1和DC3,代表ASCII标准的XON和XOFF字符,如果在传输这两个字符的时候就传不过去,需要把软件流控制屏蔽
        // cfg.c_iflag &= ~(INLCR | ICRNL | IGNCR); 清除 INLCR(将输入的换行符转换为回车换行)、ICRNL(将输入的回车转换为换行)和 IGNCR(忽略输入的回车)位,避免在处理回车和换行字符时出现意外的转换。
		cfg.c_iflag &= ~(INLCR | ICRNL |
                         IGNCR);       //发送字符0X0d的时候,往往接收端得到的字符是0X0a,原因是因为在串口设置>中c_iflag和c_oflag中存在从NL-CR和CR-NL的映射,即串口能把回车和换行当成同一个字符,可以进行如下设置屏蔽之
        // cfg.c_oflag &= ~(ONLCR | OCRNL); 清除 ONLCR(将输出的换行符转换为回车换行)和 OCRNL(将输出的回车转换为换行)位,确保输出的字符不进行这些转换。
		cfg.c_oflag &= ~(ONLCR | OCRNL);
		// tcflush 函数用于刷新与文件描述符 fd 相关联的终端的输入或输出缓冲区。TCIFLUSH 表示刷新输入缓冲区,丢弃尚未读取的输入数据,确保串口接收的数据是最新的。
        tcflush(fd, TCIFLUSH);  //在termbits-common.h中,#define TCIFLUSH 0

		// tcsetattr 函数用于将 termios 结构体 cfg 中的配置应用到与文件描述符 fd 相关联的终端(串口)上。TCSANOW 表示立即应用配置。
		// 如果 tcsetattr 函数调用失败(返回非零值),则打印错误信息,关闭串口文件描述符 fd ,并返回 NULL 表示配置失败。
        if (tcsetattr(fd, TCSANOW, &cfg)) {
            printf("Configure device tcsetattr() failed 2");
            close(fd);
            /* TODO: throw an exception */
            return NULL;
        }
    }
	// 通过以上这些设置,串口被配置为符合特定通信需求的工作模式,能够进行可靠的数据传输。

    /* Create a corresponding file descriptor */
    {
		// 创建一个 java.io.FileDescriptor 对象,并将打开的串口文件描述符设置到该对象中,最后返回该对象。
        jclass cFileDescriptor = env->FindClass("java/io/FileDescriptor");
        jmethodID iFileDescriptor = env->GetMethodID(cFileDescriptor, "<init>", "()V");
        jfieldID descriptorID = env->GetFieldID(cFileDescriptor, "descriptor", "I");
        mFileDescriptor = env->NewObject(cFileDescriptor, iFileDescriptor);
        env->SetIntField(mFileDescriptor, descriptorID, (jint) fd);
    }

    return mFileDescriptor;
}


// JNIEXPORT 和 JNICALL 是 JNI 定义的宏,用于指定函数的导出和调用约定。
// JNIEnv *env:这是一个指向 JNI 环境的指针,通过它可以调用各种 JNI 函数来操作 Java 对象、调用 Java 方法等。
// jclass cls:表示调用该本地方法的 Java 类的引用。
// jobject thiz:这是从 Java 层传递过来的对象引用,通常是调用该本地方法的 Java 对象实例(在这个场景中,可能是表示串口相关的 Java 对象)。
JNIEXPORT void JNICALL
Java_com_eric_hello_EricJniFile_uartClose(JNIEnv *env, jclass cls, jobject thiz) {
	// 通过 JNIEnv 的 GetObjectClass 函数获取 thiz 所代表的 Java 对象的类引用,并将其存储在 SerialPortClass 变量中。
    jclass SerialPortClass = env->GetObjectClass(thiz);
	// 使用 JNIEnv 的 FindClass 函数查找 java/io/FileDescriptor 类的引用,并将其存储在 FileDescriptorClass 变量中。这个类在 Java 中用于表示文件描述符。
    jclass FileDescriptorClass = env->FindClass("java/io/FileDescriptor");
	// 通过 JNIEnv 的 GetFieldID 函数获取 SerialPortClass 类中名为 mFd 且类型为 java/io/FileDescriptor 的字段的 ID,并将其存储在 mFdID 变量中。这里的 mFd 字段可能是用来存储串口对应的文件描述符对象的。
    jfieldID mFdID = env->GetFieldID(SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
	// 获取 FileDescriptorClass 类中名为 descriptor 且类型为 int(在 JNI 中用 I 表示)的字段的 ID,并将其存储在 descriptorID 变量中。descriptor 字段通常用于存储实际的文件描述符整数值。
    jfieldID descriptorID = env->GetFieldID(FileDescriptorClass, "descriptor", "I");
	// 使用 JNIEnv 的 GetObjectField 函数从 thiz 对象中获取 mFdID 所代表的字段的值,即获取到文件描述符对象,并将其存储在 mFd 变量中。
    jobject mFd = env->GetObjectField(thiz, mFdID);
	// 通过 JNIEnv 的 GetIntField 函数从 mFd 对象(java/io/FileDescriptor 类型)中获取 descriptorID 所代表的字段的值,即获取到实际的文件描述符整数值,并将其存储在 descriptor 变量中。
    jint descriptor = env->GetIntField(mFd, descriptorID);

    // 调用系统的 close 函数,传入获取到的文件描述符整数值 descriptor,从而关闭对应的串口设备文件描述符,完成串口的关闭操作。
    close(descriptor); //如果close函数返回0则表示成功关闭串口
}


JNIEXPORT void JNICALL
Java_com_eric_hello_EricJniFile_uartWrite(JNIEnv *env, jclass cls, jbyteArray data,
                                                  jint size) {
    int i;
	// 使用 JNIEnv 的 GetByteArrayElements 函数从 data 字节数组对象中获取指向实际字节数据的指针,并将其存储在 jdata 变量中。
	// GetByteArrayElements 函数的第二个参数 NULL 表示不关心字节数组的拷贝情况(这里简单使用 NULL 是常见的做法)。
    jbyte * jdata = env->GetByteArrayElements(data, NULL);

    for(i =0;i < size;i++){
        LOGI("write uart buf[%d]: %d len: %d", i, jdata[i] , size);
    }
	// 调用系统的 write 函数,将 jdata 指向的数据写入到文件描述符 fd 所代表的串口设备中,写入的字节数为 size。
	// write 函数的返回值存储在 ret 变量中,ret 表示实际写入的字节数,如果返回值小于 0,则表示写入操作失败。
    int ret = write(fd, jdata, size);
	// 根据 write 函数的返回值 ret 判断写入操作是否成功。如果 ret 小于 0,则使用 LOGI 宏输出写入失败的日志信息;否则输出写入成功的日志信息。
    if (ret < 0) {
        LOGI("write uart dev fail!");
    }else{
        LOGI("write uart dev success!");
    }
	// 使用 JNIEnv 的 ReleaseByteArrayElements 函数释放之前通过 GetByteArrayElements 函数获取的字节数组数据指针 jdata。
	// ReleaseByteArrayElements 函数的第三个参数 JNI_FALSE 表示不将修改后的数据拷贝回 Java 层的字节数组对象(如果在获取指针后对数据进行了修改且希望反映到 Java 层的数组中,可以使用 JNI_TRUE)。
    env->ReleaseByteArrayElements(data, jdata, JNI_FALSE);
}


JNIEXPORT jstring JNICALL
Java_com_eric_hello_EricJniFile_uartRead(JNIEnv *env, jclass cls) {

	// 使用 malloc 函数分配了 1024 字节的内存空间,用于存储从串口读取的数据。
	size_t buffer_size = 1024;
  char *data = (char *)malloc(buffer_size);
	if (data == NULL) {
      LOGI("Memory allocation failed !!!");
	    return NULL;
	}
	// 调用 read 函数从文件描述符 fd 所代表的串口设备读取最多 1024 字节的数据,返回值 ret 表示实际读取的字节数。
    int ret = read(fd, data, buffer_size);

    if (ret < 0) {
        LOGI("read uart dev fail !!!");
        free(data);  // 释放内存
        return NULL;
    }

    // 检查数据是否为有效的 UTF-8 编码
    // 这里简单处理,若不是 UTF-8 编码,将数据转换为十六进制字符串表示
	// 初始化一个布尔型变量 validUtf8 为 1,表示假设数据是有效的 UTF - 8 编码。
    int validUtf8 = 1;
	// 通过一个 for 循环遍历读取到的数据,根据 UTF - 8 编码的规则检查每个字节及其后续字节是否构成有效的 UTF - 8 字符。
    for (int i = 0; i < ret; ++i) {
        unsigned char c = (unsigned char)data[i];
		// 如果当前字节小于 0x80,说明是单字节的 ASCII 字符,继续检查下一个字节。
        if (c < 0x80) {
            continue;
        } else if ((c & 0xE0) == 0xC0) {
			 // 如果当前字节以 110 开头(即 (c & 0xE0) == 0xC0),
			 // 表示这是一个双字节的 UTF - 8 字符,需要检查下一个字节是否以 10 开头,如果不满足条件或者越界,则将 validUtf8 置为 0 并跳出循环。
            if (i + 1 >= ret || ((unsigned char)data[i + 1] & 0xC0) != 0x80) {
                validUtf8 = 0;
                break;
            }
            ++i;
        } else if ((c & 0xF0) == 0xE0) {
			// 如果当前字节以 1110 开头(即 (c & 0xF0) == 0xE0),
			// 表示这是一个三字节的 UTF - 8 字符,需要检查后续两个字节是否都以 10 开头,如果不满足条件或者越界,则将 validUtf8 置为 0 并跳出循环。
            if (i + 2 >= ret || ((unsigned char)data[i + 1] & 0xC0) != 0x80 || ((unsigned char)data[i + 2] & 0xC0) != 0x80) {
                validUtf8 = 0;
                break;
            }
            i += 2;
        } else {
			// 如果当前字节不满足上述任何一种情况,则将 validUtf8 置为 0 并跳出循环。
            validUtf8 = 0;
            break;
        }
    }

    jstring result;
    if (validUtf8) {
		// 如果 validUtf8 为 1,说明数据是有效的 UTF - 8 编码,使用 JNIEnv 的 NewStringUTF 函数将数据直接转换为 Java 字符串。
        result = env->NewStringUTF(data);
    } else {
        // 将非 UTF-8 数据转换为十六进制字符串
		// 使用 malloc 函数分配 ret * 3 + 1 字节的内存空间,用于存储十六进制字符串,每个字节用 2 个十六进制字符和 1 个空格表示,再加一个字符串终止符。
        char *hexData = (char *)malloc(ret * 3 + 1);  // 每个字节用 2 个十六进制字符表示,再加一个终止符
        int hexIndex = 0;
		// 通过一个 for 循环,使用 sprintf 函数将每个字节转换为两位十六进制字符串并存储到 hexData 中。
        for (int i = 0; i < ret; ++i) {
            hexIndex += sprintf(hexData + hexIndex, "%02X ", (unsigned char)data[i]);
        }
		// 去掉最后一个多余的空格,将其替换为字符串终止符 '\0'。
        hexData[hexIndex - 1] = '\0';  // 去掉最后一个空格
		// 使用 JNIEnv 的 NewStringUTF 函数将十六进制字符串转换为 Java 字符串。
        result = env->NewStringUTF(hexData);
		// 使用 free 函数释放存储十六进制字符串的内存。
        free(hexData);
    }
	// 使用 free 函数释放之前分配的存储读取数据的内存。
    free(data);
	// 返回最终的 Java 字符串结果给 Java 层。
    return result;
 }
// 上面的uartRead函数,有如下问题,
//缓冲区大小固定:代码中使用固定大小的缓冲区(1024 字节)来存储读取的数据,可能会导致数据丢失。可以考虑动态调整缓冲区大小,或者使用循环读取的方式来处理大数据量。
//错误处理和日志记录:虽然代码中对读取失败的情况进行了日志记录,但对于其他可能的错误(如内存分配失败、sprintf 调用失败等)没有进行详细的错误处理和日志记录。可以增加更多的错误处理逻辑,提高代码的健壮性。        

//优化后的代码如下:
JNIEXPORT jstring JNICALL
Java_com_eric_hello_EricJniFile_uartread(JNIEnv *env, jclass cls) {
    // 初始缓冲区大小
    size_t buffer_size = 1024;
    char *data = (char *)malloc(buffer_size);
    if (data == NULL) {
        LOGI("Memory allocation failed for data buffer !!!");
        return NULL;
    }

    // 读取数据
    int ret = read(fd, data, buffer_size);
    if (ret < 0) {
        LOGI("read uart dev fail !!!");
        free(data);
        return NULL;
    }

    // 如果读取的数据量达到缓冲区大小,尝试动态扩展缓冲区继续读取
    while (ret == (int)buffer_size) {
        // 扩展缓冲区大小
        buffer_size *= 2;
        char *new_data = (char *)realloc(data, buffer_size);
        if (new_data == NULL) {
            LOGI("Memory reallocation failed while expanding data buffer !!!");
            free(data);
            return NULL;
        }
        data = new_data;

        // 继续读取数据
        int additional_ret = read(fd, data + ret, buffer_size - ret);
        if (additional_ret < 0) {
            LOGI("read uart dev fail during dynamic buffer expansion !!!");
            free(data);
            return NULL;
        }
        ret += additional_ret;

        // 如果没有更多数据可读,退出循环
        if (additional_ret == 0) {
            break;
        }
    }

    // 检查数据是否为有效的 UTF - 8 编码
    int validUtf8 = 1;
    for (int i = 0; i < ret; ++i) {
        unsigned char c = (unsigned char)data[i];
        if (c < 0x80) {
            continue;
        } else if ((c & 0xE0) == 0xC0) {
            if (i + 1 >= ret || ((unsigned char)data[i + 1] & 0xC0) != 0x80) {
                validUtf8 = 0;
                break;
            }
            ++i;
        } else if ((c & 0xF0) == 0xE0) {
            if (i + 2 >= ret || ((unsigned char)data[i + 1] & 0xC0) != 0x80 || ((unsigned char)data[i + 2] & 0xC0) != 0x80) {
                validUtf8 = 0;
                break;
            }
            i += 2;
        } else {
            validUtf8 = 0;
            break;
        }
    }

    jstring result = NULL;
    if (validUtf8) {
        result = env->NewStringUTF(data);
        if (result == NULL) {
            LOGI("Failed to create Java string from valid UTF-8 data !!!");
        }
    } else {
        // 将非 UTF - 8 数据转换为十六进制字符串
        char *hexData = (char *)malloc(ret * 3 + 1);
        if (hexData == NULL) {
            LOGI("Memory allocation failed for hexData buffer !!!");
        } else {
            int hexIndex = 0;
            for (int i = 0; i < ret; ++i) {
                hexIndex += sprintf(hexData + hexIndex, "%02X ", (unsigned char)data[i]);
            }
            hexData[hexIndex - 1] = '\0'; // 去掉最后一个空格
            result = env->NewStringUTF(hexData);
            if (result == NULL) {
                LOGI("Failed to create Java string from hexadecimal data !!!");
            }
            free(hexData);
        }
    }

    free(data);
    return result;
}
//上面代码的优化点说明如下:
// 优化点说明
//内存管理方面
//内存分配失败检查:在使用 malloc 和 realloc 进行内存分配和重新分配时,增加了对返回值是否为 NULL 的检查。如果分配失败,会输出错误日志并释放已分配的内存,避免后续操作导致程序崩溃。
//动态缓冲区扩展:原代码使用固定大小的缓冲区,可能会导致数据丢失。优化后的代码在读取的数据量达到缓冲区大小时,会动态扩展缓冲区大小(每次将缓冲区大小翻倍),并继续读取数据,直到没有更多数据可读。
//错误处理方面
//读取错误处理:在读取数据过程中,如果 read 函数返回 -1,表示读取失败,会输出错误日志并释放已分配的内存。
//JNI 函数调用错误处理:在调用 env->NewStringUTF 函数将数据转换为 Java 字符串时,增加了对返回值是否为 NULL 的检查。如果转换失败,会输出错误日志。
//通过这些优化,代码的健壮性得到了显著提高,能够更好地处理内存分配失败和其他可能出现的错误情况。
}

至此,在底层完成了对串口的打开、读、写、关闭。

3,编码实现对本地代码的调用

java 复制代码
// 导入了 java.io.FileDescriptor 类,该类用于表示文件描述符。在串口操作中,文件描述符可用于标识打开的串口设备,方便后续的读写和关闭操作。
import java.io.FileDescriptor;

// 通过 Java 本地接口(JNI)来调用本地(通常是 C 或 C++)代码,实现对串口设备的控制操作,包括打开串口、关闭串口、读取串口数据以及向串口写入数据。
public class EricJniFile {
	// 静态代码块,在类加载时会自动执行。其作用是加载名为 uart-control 的本地库。
	// System.loadLibrary 方法会在系统的库搜索路径中查找指定名称的本地库文件(在不同操作系统中,库文件的扩展名不同,如在 Linux 中通常是 .so 文件,在 Windows 中是 .dll 文件),并将其加载到 Java 虚拟机中。后续的本地方法调用将依赖于这个库中的实现。
    static {
        try {
			System.loadLibrary("uart-control");
		} catch (UnsatisfiedLinkError e) {
			System.err.println("Failed to load uart-control library: " + e.getMessage());
		}
    }

	  // 返回值:FileDescriptor 类型,返回一个表示打开的串口设备的文件描述符,后续的读写和关闭操作将使用这个文件描述符。
    public static native FileDescriptor uartOpen(String path, int baudrate);
    public static native void uartClose(Object thiz);
    public static native String uartRead();
    public static native void uartWrite(byte data[], int size);

}

此处的EricJniFile中的函数的实现在上面的serialPort.cpp中,可以根据路径找到相应的函数。

4, 编写应用层代码文件调用JNI接口文件

在android应用层定义新类EricSerialPort进行串口的操作

java 复制代码
public class EricSerialPort {
    private static final String TAG = "EricSerialPort";
    private static final EricSerialPort mInstance = new EricSerialPort();
    private FileDescriptor mFd;
    private InputStream mInputStream;
    private boolean isStop = true;
    public EricSerialPort() {
        mFd = null;
    }

    public static EricSerialPort getInstance() {
        return mInstance;
    }

    public boolean ericUartOpen(){
        Log.v(TAG, "ericUartOpen...");
        mFd = EricJniFile.uartOpen("/dev/ttyS3",115200);
        Log.v(TAG, "ericUartRead...mFd:" + mFd);
        if (mFd != null)
            mInputStream = new FileInputStream(mFd);
        isStop = false;
        return mFd != null;
    }
	
    public void ericUartClose(){
        Log.v(TAG, "ericUartClose...");
        try {
            isStop = true;
            mInputStream.close();
            mInputStream = null;
            EricJniFile.uartClose(this);
            mFd = null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
	
    public int ericUartRead(byte[] bBuffers) {
        Log.v(TAG, "ericUartRead...");
        if (mInputStream == null) {
            Log.v(TAG, "ericUartRead...stream is null");
            return -1;
        }
        try {
            return mInputStream.read(bBuffers);
        } catch (IOException e) {
            Log.v(TAG, "ericUartRead...exception:" + e);
            return -1;
        }
    }
	
    public void ericUartWrite(byte data[], int size) {
        Log.v(TAG, "ericUartWrite...");
        EricJniFile.uartwrite(data, size);
    }

}

5, 编码实现对串口数据的接收与发送

首先编写类实现帧序列的构建:

kotlin 复制代码
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicInteger

object EricSerialControl {
    private const val TAG = "EricSerialControl"
    private val FRAME_HEADER = byteArrayOf(0x55, 0xAA.toByte())
    private const val MAX_FRAME_SEQUENCE = 250
    private val serialPort = EricSerialPort.getInstance()
    private var frameSequence = 1
    private var frameDataList: MutableList<MutableList<Byte>> = mutableListOf()
    private lateinit var cmd: ByteArray
    private var data: ByteArray? = null
    private val frameSeq = AtomicInteger(1)

    // 设置命令字
    private fun setCommand(command: ByteArray) {
        this.cmd = command
    }

    // 设置数据区
    private fun setData(data: ByteArray?) {
        this.data = data
    }
	
	// 生成帧数据
	// @OptIn(ExperimentalUnsignedTypes::class):表示启用 Kotlin 中关于无符号类型的实验性功能,因为在计算 CRC16 时使用了无符号类型。
    @OptIn(ExperimentalUnsignedTypes::class)
    private fun makeFrame(): ByteArray {
        Log.v(TAG, "makeFrame...")
        // 帧头
        val frameHeader = FRAME_HEADER
        // 长度
        val dataSize = data?.size ?: 0
        val dataLength = 1 + 2 + dataSize + 2 // 帧序号 + 命令字 + 数据区 + CRC16
        val lengthBytes = byteArrayOf((dataLength shr 8).toByte(), dataLength.toByte())
        // 帧序号
        val sequenceByte = frameSequence.toByte()
        Log.v(TAG, "makeFrame...dataLength:${dataLength},frameSeq:${frameSequence}")
        incrementFrameSequence()
        // 合并数据
        val dataToCheck = mutableListOf<Byte>()
        dataToCheck.addAll(lengthBytes.toList())
        dataToCheck.add(sequenceByte)
        dataToCheck.addAll(cmd.toList())
        data?.toList()?.also {
            dataToCheck.addAll(it)
        }
        Log.v(TAG, "makeFrame...dataToCheck:${dataToCheck.map { it.toUByte() }}")
        // CRC16 校验
        val crc = calculateCRC16(dataToCheck.map { it.toUByte() }.toUByteArray())
        val crcBytes = byteArrayOf((crc shr 8).toByte(), crc.toByte())
        // 合并所有部分
        val frame = mutableListOf<Byte>()
        frame.addAll(frameHeader.toList())
        frame.addAll(dataToCheck)
        frame.addAll(crcBytes.toList())
        return frame.toByteArray()
    }
	
	private fun incrementFrameSequence() {
        frameSequence = if (frameSequence == MAX_FRAME_SEQUENCE) 1 else frameSequence + 1
		//采用多线程安全的方法如下:
          //frameSeq.updateAndGet {
         //    if (it == MAX_FRAME_SEQUENCE) 1 else it + 1
         //}
    }

接着,需要执行循环冗余校验:

kotlin 复制代码
    @OptIn(ExperimentalUnsignedTypes::class)
    val auchCRCLo = ubyteArrayOf(
        0x00u, 0xC0u, 0xC1u,...
	)
	
	@OptIn(ExperimentalUnsignedTypes::class)
    val auchCRCHi = ubyteArrayOf(
        0x00u, 0xC1u, 0x81u,...
	)
        
    @OptIn(ExperimentalUnsignedTypes::class)
    private fun checkCRC16(puchMsg: UByteArray, usDataLen: UShort): UShort {
        var uchCRCHi: UByte = 0xFFu // CRC 的高字节初始化
        var uchCRCLo: UByte = 0xFFu // CRC 的低字节初始化
        var uIndex: UByte // CRC 查询表索引
        var dataLen = usDataLen.toInt()
        var index = 0
        while (dataLen-- > 0) { // 完成整个报文缓冲区
            uIndex = uchCRCLo xor puchMsg[index++] // 计算 CRC
            uchCRCLo = uchCRCHi xor auchCRCHi[uIndex.toInt()]
            uchCRCHi = auchCRCLo[uIndex.toInt()]
        }
        return (((uchCRCHi.toInt() shl 8) or uchCRCLo.toInt()) and 0xFFFF).toUShort()
    }
    
    @OptIn(ExperimentalUnsignedTypes::class)
    private fun calculateCRC16(data: UByteArray): Int {
        val crc = checkCRC16(data, data.size.toUShort())
        return crc.toInt() and 0xFFFF
    }

接着,需要向串口写入数据:

kotlin 复制代码
fun setEricSerialPortControl(labelId: Byte, funcId: Byte, attrId: Byte, attrValue: Byte) {
        Log.v(TAG, "setEricSerialPortControl...labelId:${labelId},funcId:${funcId},attrId:${attrId},attrValue::${attrValue}")
        val cmdBit = 0x0001
        val cmdByte = byteArrayOf((cmdBit shr 8).toByte(), cmdBit.toByte())
        setCommand(cmdByte)
        // 设置数据区
        val data = byteArrayOf(labelId, funcId, attrId, attrValue)
        setData(data)
        // 生成帧数据
        val frameSend = makeFrame()
        val frameSendHexValue = frameSend.joinToString("") { byte ->
            "%02X".format(byte.toInt() and 0xFF)
        }
        Log.v(TAG, "setEricSerialPortControl...frameSend:${frameSendHexValue}")

        EricJniFile.EricUartWrite(frameSend, frameSend.size)
        Thread.sleep(100);
    }

串口双方进行通信,一般需要发送心跳信息,示例如下:

scss 复制代码
    private suspend fun sendHeartbeat() {
        Log.v(TAG, "sendHeartbeat...")
        val heartbeatCmd = 0x1010
        val hbCmdByte = byteArrayOf((heartbeatCmd shr 8).toByte(), heartbeatCmd.toByte())
        setCommand(hbCmdByte)
        setData(null)
        val frameSend = makeFrame()
        val frameSendHexValue = frameSend.joinToString("") { byte ->
            "%02X".format(byte.toInt() and 0xFF)
        }
        Log.v(TAG, "sendHeartbeat...frameSend:${frameSendHexValue}")
        EricJniFile.uartwrite(frameSend, frameSend.size)
        delay(5000)
    }

接收串口信息如下示例:

kotlin 复制代码
    private suspend fun receiveJackyData(): MutableList<Byte> {
        delay(50)
        val dataList = mutableListOf<Byte>()
        val buffer = ByteArray(1024)
        val readSize = serialPort.EricUartRead(buffer)
        if (readSize > 0) {
            Log.v(TAG, "receiveJackyData...readSize:${readSize}")
            dataList.addAll(buffer.take(readSize))
        }
        return dataList
    }

接下来,启动协程开始串口数据的发送和接收:

scss 复制代码
    fun startSerialPortCom() {
        Log.v(TAG, "startSerialPortCom...")
        val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
        serialPort?.let {
            val opened = it.EricUartOpen()
            Log.v(TAG, "startSerialPortCom...,opened:${opened}")
            if (opened) {
                coroutineScope.launch {
                    while (isActive) {
                        sendHeartbeat()
                    }
                }

                coroutineScope.launch {
                    while (isActive) {
                        val receivedJackyData = receiveJackyData()
                        if (receivedJackyData.size > 0) {
                            val receivedJackyHexValue = receivedJackyData.joinToString("") { byte ->
                                "%02X".format(byte.toInt() and 0xFF)}
                            Log.v(TAG, "startSerialPortCom...,receivedJackyHexValue:${receivedJackyHexValue}")
                            processReceivedData(receivedJackyData)
                        }
                    }
                }

            }
        }
    }

下面对接收到的数据进行解析:

kotlin 复制代码
    private suspend fun processReceivedData(recvData: MutableList<Byte>) {
        Log.v(TAG, "processReceivedData...")
        if (recvData != null) {
            val dataHexValue = recvData.joinToString("") { byte ->
                "%02X".format(byte.toInt() and 0xFF)}
            Log.v(TAG, "processReceivedData,Received data: $dataHexValue")
        }
        if (recvData.isEmpty()) {
            Log.v(TAG, "processReceivedData...recvData is empty.")
            return
        }
        val length = checkReceivedData(recvData)
        Log.v(TAG, "processReceivedData...length:${length}")
        if (length > 1) {
           for (i in 0 until length) {
               parseReceivedData(frameDataList.get(i))
           }
        } else {
            parseReceivedData(recvData)
        }

    }

    private fun parseReceivedData(recvData: MutableList<Byte>) {
        Log.v(TAG, "parseReceivedData...")
        val lengthBytes = recvData.slice(2 until 4).toByteArray()
        val frameLength = ((lengthBytes[0].toInt() and 0xFF) shl 8) or (lengthBytes[1].toInt() and 0xFF)
        Log.v(TAG, "parseReceivedData...frameLength:${frameLength}")
        val oneFrameSize = 4 + frameLength
        if (recvData.size < 2 + frameLength) {
            Log.v(TAG, "frame data not complete or timeout")
            return
        }
        if (recvData.size < oneFrameSize) {
            Log.v(TAG, "frame data not complete,length < oneFrameSize")
            return
        }

        // 解析帧数据
        val frameNumber = recvData.slice(4 until 5).toByteArray()[0].toInt() and 0xFF
        Log.v(TAG, "parseReceivedData...frameNumber:${frameNumber}")
        val cmdBytes = recvData.slice(5 until 7).toByteArray()
        val cmdHex = cmdBytes.joinToString("") { byte ->
            "%02X".format(byte.toInt() and 0xFF)}
        Log.v(TAG, "parseReceivedData...cmdHex:${cmdHex}")
        val dataByte = recvData.slice(7 until oneFrameSize -2).toByteArray()
        val dataByteHex = dataByte.joinToString("") { byte ->
            "%02X".format(byte.toInt() and 0xFF)}
        Log.v(TAG, "parseReceivedData...dataByteHex:${dataByteHex}")
        val crcBytes = recvData.slice(oneFrameSize -2  until oneFrameSize).toByteArray()
        val crcHex = crcBytes.joinToString("") { byte ->
            "%02X".format(byte.toInt() and 0xFF)}
        Log.v(TAG, "parseReceivedData...crcHex:${crcHex}")

        // 计算 CRC16 校验值
        val recvResult = recvData.slice(0 until recvData.size-2).toByteArray()
        val recvResultHex = recvResult.joinToString("") { byte ->
            "%02X".format(byte.toInt() and 0xFF)}
        Log.v(TAG, "parseReceivedData...recvResultHex:${recvResultHex}")
        val calculatedCrc = calculateCRC16(recvResult.map { it.toUByte() }.toUByteArray())
        val calculatedCrcHex = String.format("%04X", calculatedCrc)
        Log.v(TAG, "parseReceivedData...calculatedCrcHex:${calculatedCrcHex}")

        // 校验 CRC16
        if (calculatedCrcHex == crcHex) {
            Log.v(TAG, "parseReceivedData...CRC16 check ok")
            val cmdInt: Int = cmdHex.toInt(16)
            if (cmdInt == 0x1010) {
                Log.v(TAG, "parseReceivedData,cmd:0x1010...")
            } else {
                Log.v(TAG, "parseReceivedData,cmd:else...")
            }
        } else {
            Log.v(TAG, "parseReceivedData...CRC16 check error")
        }
    }

    private fun checkReceivedData(recvData: MutableList<Byte>): Int {
        Log.v(TAG, "checkReceivedData...")
        val frameIndices = mutableListOf<Int>()
        for (i in 0 until recvData.size - 1) {
            if (recvData[i] == 0xAA.toByte() && recvData[i + 1] == 0x55.toByte()) {
                frameIndices.add(i)
            }
        }
        if (frameDataList != null) {
            frameDataList.clear()
        }
        frameIndices.forEachIndexed { index, startIndex ->
            // 找到下一帧的起始位置,若没有则为数据末尾
            val nextStartIndex = frameIndices.getOrNull(index + 1) ?: recvData.size
            // 提取当前帧的数据
            val frameData = recvData.subList(startIndex, nextStartIndex)
            frameDataList.add(frameData)
            // 将帧数据转换为十六进制字符串
            val hexStr = frameData.joinToString(" ") { "%02X".format(it.toInt() and 0xFF) }
            Log.v(TAG,"checkReceivedData... ${index + 1}: $hexStr")
        }
        return frameIndices.size
    }
相关推荐
帅次6 小时前
Flutter 边框按钮:OutlinedButton 完全手册与设计最佳实践
android·flutter·macos·ios·kotlin·android studio
QING61812 小时前
Android AIDL 开发指南:包含注意事项、兼容性问题
android·kotlin·app
DonnyCoy12 小时前
Kotlin语言基础笔记
kotlin
帅次13 小时前
Flutter FloatingActionButton 从核心用法到高级定制
android·flutter·macos·ios·kotlin·android-studio
moz与京15 小时前
【记】如何理解kotlin中的委托属性?
android·开发语言·kotlin
左少华15 小时前
Kotlin-inline函数特效
android·开发语言·kotlin
氦客16 小时前
Kotlin知识体系(一) : Kotlin的五大基础语法特性
android·开发语言·kotlin·基础语法·特性·知识体系
每次的天空1 天前
kotlin中的行为组件
android·开发语言·kotlin
wangz761 天前
Kotlin,jetpack compose,Android,MPAndroidChart,折线图示例
android·kotlin·mpandroidchart