串口通信分析与实例

串口通信(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
    }
相关推荐
alexhilton13 小时前
端侧RAG实战指南
android·kotlin·android jetpack
Kapaseker1 天前
2026年,我们还该不该学编程?
android·kotlin
Kapaseker2 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
Kapaseker3 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish4 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker4 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker5 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z7 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton8 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream8 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin