开发环境:

  • AndroidStudio3.1.4
  • JDK_1.8.0_152_release
  • Compile Sdk Version API28
  • Gradle 4.4
  • NDK 17.1.4828580

Android与下位机通信,串口通信是比较常见的一种方案。Google官方提供了一个项目android-serialport-api用来读取和写入Linux TTY串行端口
支持的功能包括:列出设备上可用的串行端口,包括USB到串行适配器配置串行端口(波特率,停止位,权限,…)*提供标准InputStream和OutputStream Java接口
代码库地址https://github.com/cepr/android-serialport-api

AndroidStudio3.1使用CMake来编译jni

1.下载https://github.com/cepr/android-serialport-api到本地。
在项目名/app/src/main/java下创建android_serialport_api目录(目录名不要改)
将android-serialport-api/android-serialport-api/project/src/android_serialport_api/目录下的SerialPort.java和SerialPortFinder.java复制到android_serialport_api目录下
SerialPortFinder.java 用于查找串口设备
SerialPort.java 用于jni方法调用

2.创建JNI目录,复制c文件
在main目录右键New—Folder—JNI Floder—Finish
将SerialPort.c和SerialPort.h复制到生成的cpp目录下
使用CMake只需要修改cpp文件和CMakeLists.txt。
CMake生成的cpp文件位于app/src/main/cpp目录,并且cpp文件不需要再引入java类的h文件。
CMakeLists.txt位于app目录下
特别注意SerialPort.h文件的生成格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class android_serialport_api_SerialPort */

#ifndef _Included_android_serialport_api_SerialPort
#define _Included_android_serialport_api_SerialPort
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: android_serialport_api_SerialPort
* Method: open
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
*/ // 命名格式: Java_包名_类目录_类名_接口
// 通过执行 `javah -jni android_serialport_api_SerialPort`生成的.h文件
JNIEXPORT jobject JNICALL Java_android_1serialport_1api_SerialPort_open
(JNIEnv *, jclass, jstring, jint, jint);

/*
* Class: android_serialport_api_SerialPort
* Method: close
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_android_1serialport_1api_SerialPort_close
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

3.修改modue的build.gradle,设置JNI
在defaultConfig段落添加cmake设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.ex.serialport"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// 添加CMake设置 start //
externalNativeBuild {
cmake {
cppFlags ""
}
}
// 添加CMake设置 end//
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 添加CMake设置 start //
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
// 添加CMake设置 end //
}

4.由于API 19之后的 termios.h 里面的函数有调整,因此调试过程中,出现

1
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "tcgetattr" referenced by "libserialport.so"...

解决方法:

将API 19 的 termios.h 拷贝到 jni 目录下
termios.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*
* Copyright (C) 2008 The Android Open Source Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#ifndef _TERMIOS_H_
#define _TERMIOS_H_

#include <sys/cdefs.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdint.h>
#include <linux/termios.h>

__BEGIN_DECLS

/* Redefine these to match their ioctl number */
#undef TCSANOW
#define TCSANOW TCSETS

#undef TCSADRAIN
#define TCSADRAIN TCSETSW

#undef TCSAFLUSH
#define TCSAFLUSH TCSETSF

static __inline__ int tcgetattr(int fd, struct termios *s)
{
return ioctl(fd, TCGETS, s);
}

static __inline__ int tcsetattr(int fd, int __opt, const struct termios *s)
{
return ioctl(fd, __opt, (void *)s);
}

static __inline__ int tcflow(int fd, int action)
{
return ioctl(fd, TCXONC, (void *)(intptr_t)action);
}

static __inline__ int tcflush(int fd, int __queue)
{
return ioctl(fd, TCFLSH, (void *)(intptr_t)__queue);
}

static __inline__ pid_t tcgetsid(int fd)
{
pid_t _pid;
return ioctl(fd, TIOCGSID, &_pid) ? (pid_t)-1 : _pid;
}

static __inline__ int tcsendbreak(int fd, int __duration)
{
return ioctl(fd, TCSBRKP, (void *)(uintptr_t)__duration);
}

static __inline__ speed_t cfgetospeed(const struct termios *s)
{
return (speed_t)(s->c_cflag & CBAUD);
}

static __inline__ int cfsetospeed(struct termios *s, speed_t speed)
{
s->c_cflag = (s->c_cflag & ~CBAUD) | (speed & CBAUD);
return 0;
}

static __inline__ speed_t cfgetispeed(const struct termios *s)
{
return (speed_t)(s->c_cflag & CBAUD);
}

static __inline__ int cfsetispeed(struct termios *s, speed_t speed)
{
s->c_cflag = (s->c_cflag & ~CBAUD) | (speed & CBAUD);
return 0;
}

static __inline__ void cfmakeraw(struct termios *s)
{
s->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
s->c_oflag &= ~OPOST;
s->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
s->c_cflag &= ~(CSIZE|PARENB);
s->c_cflag |= CS8;
}

__END_DECLS

#endif /* _TERMIOS_H_ */

5.编辑 CMake 脚本CMakeLists.txt
每次创建一个新的库,需要添加另一个 add_library() 脚本,每个add_library 脚本语句只能导入一个库

1
2
3
add_library(serial_port
SHARED
src/main/cpp/SerialPort.c)

如果同步报 Log 日志异常则需要给 .c 文件导入 Log 库

1
2
3
4
5
6
7
8
9
# 该语句可以给一个 .c 库文件导入多个依赖库
target_link_libraries( # Specifies the target library.
# add_library 生成的
serialport

# Links the target library to the log library
# included in the NDK.
# find_library 找到的系统库
${log-lib} )

默认是已经有导入 log-lib 库的,如果没有的话则需要在上面代码之前导入 log-lib 库

1
2
3
4
5
6
7
8
9
# find_library 定义当前代码库需要依赖的系统或者第三方库文件
find_library( # Sets the name of the path variable.
# 指定要查找的系统库, 给一个名字
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
# 真正要查找的liblog.so或者liblog.a
log )

6.编译运行,生成.so文件
.so文件在 \项目\app\build\intermediates\cmake\下release或者debug的obj目录下arm64-v8a、armeabi-v7a、x86、x86_64

串口的参数

串口中有五个重要的参数:串口设备名、波特率、奇偶校验位、数据位、停止位。

设备名称:串口的名称。
波特率:传输速率的参数,波特率和传输距离成反比。
校验位:在串口通信中一种简单的检错方式,有四种检错方式:偶、奇、高和低,允许无校验位。
数据位:通信中实际数据位的参数
停止位:用于表示单个包的最后一位。

其中检验位一般默认位NONE,数据位一般默认为8,停止位默认为1,校验位是为了减少误差的会根据奇、偶进行补位操作

Android 串口通信

Android 主板在与其它硬件进行串口通信时,串口作为底层实现,Android 系统把设备作为一个文件,与其他设备进行串口通信就相当于读写此文件。
所以,串口通信其实就是对系统根目录下 /proc/tty/drivers 文件进行流的读写,因此,串口读写需要 Android 系统 Root 权限。
用 串口线 连接开发板 和 PC,然后在串口调试工具中,打开对应位置的端口。如果提示失败,就要检查串口线的端口号是否正确。

用数据线连接开发板至 PC,用 adb 命令打开 Android 系统对应的串口文件。
以 root 权限进入系统,

1
2
adb shell
su

然后,更改串口文件ttyS3的读写权限,

1
chmod 777 /dev/ttyS3

最后,写入信息 1111 到串口文件ttyS3里

1
echo 1111 > /dev/ttyS3

观察 PC 上的串口调试工具,如果 PC 上能收到信息,说明调通了,那么就可以进行应用开发
源码地址
PC端调试工具友善串口调试工具