串口通信(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
}