一,串口介绍
1 串口简介
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口;
串行接口(SerialInterface)是指数据一位一位地顺序传送。其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢;
2 串口使用场景
串口是一种用于android开发板对硬件设备通信的一种协议,通过发送某种指令控制硬件设备,通常用于物联网设备的信息传输,比如切割器,打印机,ATM吐卡机、IC/ID卡读卡等。
3 波特率
波特率表示串口传输速率,用来衡量数据传输的快慢,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps。波特率与距离成反比,波特率越大传输距离相应的就越短;
4 数据位
这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。如何设置取决于你想传送的信息;
5 停止位
用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢;
6 校验位
在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位;
7 串口地址
不同操作系统的串口地址,Android是基于Linux的所以一般情况下使用Android系统的设备串口地址为/dev/ttyS0;
- /dev的串口包括:虚拟串口,真实串口,USB转串口
- 真实串口:/dev/tty0..tty1这个一般为机器自带COM口
- 虚拟串口:/dev/ttyS1...ttyS2...ttyS3...均为虚拟console,同样可以作为输入输出口
- USB转串口:/dev/tty/USB0
二 Android中串口的实践
1 由于串口底层需要调用C代码,所以需要用jni来进行C交互,下面是全部的C代码,以及JNI调用
1 SerialPort.h
#include #ifndef _Included_android_serialport_SerialPort#define _Included_android_serialport_SerialPort#ifdef __cplusplusextern "C" {#endifJNIEXPORT jobject JNICALL Java_android_serialport_SerialPort_open (JNIEnv *, jobject, jstring, jint, jint, jint, jint, jint);JNIEXPORT void JNICALL Java_android_serialport_SerialPort_close (JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif#ifndef _Included_android_serialport_SerialPort_Builder#define _Included_android_serialport_SerialPort_Builder#ifdef __cplusplusextern "C" {#endif#ifdef __cplusplus}#endif#endif
2 SerialPort.c
#include #include #include #include #include #include #include #include "SerialPort.h"#include "android/log.h"static const char *TAG = "serial_port";#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)static speed_t getBaudrate(jint baudrate) { switch (baudrate) { case 0: return B0; case 50: return B50; case 75: return B75; case 110: return B110; case 134: return B134; case 150: return B150; case 200: return B200; case 300: return B300; case 600: return B600; case 1200: return B1200; case 1800: return B1800; case 2400: return B2400; case 4800: return B4800; case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; case 460800: return B460800; case 500000: return B500000; case 576000: return B576000; case 921600: return B921600; case 1000000: return B1000000; case 1152000: return B1152000; case 1500000: return B1500000; case 2000000: return B2000000; case 2500000: return B2500000; case 3000000: return B3000000; case 3500000: return B3500000; case 4000000: return B4000000; default: return -1; }}JNIEXPORT jobject JNICALL Java_android_serialport_SerialPort_open (JNIEnv *env, jobject thiz, jstring path, jint baudrate, jint dataBits, jint parity, jint stopBits, jint flags) { int fd; speed_t speed; jobject mFileDescriptor; { speed = getBaudrate(baudrate); if (speed == -1) { LOGE("Invalid baudrate"); return NULL; } } { jboolean iscopy; const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy); LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags); fd = open(path_utf, O_RDWR | flags); LOGD("open() fd = %d", fd); (*env)->ReleaseStringUTFChars(env, path, path_utf); if (fd == -1) { LOGE("Cannot open port"); return NULL; } } { struct termios cfg; LOGD("Configuring serial port"); if (tcgetattr(fd, &cfg)) { LOGE("tcgetattr() failed"); close(fd); return NULL; } cfmakeraw(&cfg); cfsetispeed(&cfg, speed); cfsetospeed(&cfg, speed); cfg.c_cflag &= ~CSIZE; switch (dataBits) { case 5: cfg.c_cflag |= CS5; //使用5位数据位 break; case 6: cfg.c_cflag |= CS6; //使用6位数据位 break; case 7: cfg.c_cflag |= CS7; //使用7位数据位 break; case 8: cfg.c_cflag |= CS8; //使用8位数据位 break; default: cfg.c_cflag |= CS8; break; } switch (parity) { case 0: cfg.c_cflag &= ~PARENB; //无奇偶校验 break; case 1: cfg.c_cflag |= (PARODD | PARENB); //奇校验 break; case 2: cfg.c_iflag &= ~(IGNPAR | PARMRK); // 偶校验 cfg.c_iflag |= INPCK; cfg.c_cflag |= PARENB; cfg.c_cflag &= ~PARODD; break; default: cfg.c_cflag &= ~PARENB; break; } switch (stopBits) { case 1: cfg.c_cflag &= ~CSTOPB; //1位停止位 break; case 2: cfg.c_cflag |= CSTOPB; //2位停止位 break; default: cfg.c_cflag &= ~CSTOPB; //1位停止位 break; } if (tcsetattr(fd, TCSANOW, &cfg)) { LOGE("tcsetattr() failed"); close(fd); return NULL; } } { jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor"); jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "", "()V"); jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I"); mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor); (*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint) fd); } return mFileDescriptor;}JNIEXPORT void JNICALL Java_android_serialport_SerialPort_close (JNIEnv *env, jobject thiz) { jclass SerialPortClass = (*env)->GetObjectClass(env, thiz); jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor"); jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;"); jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I"); jobject mFd = (*env)->GetObjectField(env, thiz, mFdID); jint descriptor = (*env)->GetIntField(env, mFd, descriptorID); LOGD("close(fd = %d)", descriptor); close(descriptor);}
3 gen_SerialPort_h.sh 生成java的文件目录
#!/bin/shjavah -o SerialPort.h -jni -classpath ../java android.serialport.SerialPort
4 SerialPort.java jni对应的java文件
package android.serialport;import android.util.Log;import androidx.annotation.NonNull;import androidx.annotation.Nullable;import java.io.DataOutputStream;import java.io.File;import java.io.FileDescriptor;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;public final class SerialPort { private static final String TAG = "SerialPort"; public static final String DEFAULT_SU_PATH = "/system/bin/su"; private static String sSuPath = DEFAULT_SU_PATH; private File device; private int baudrate; private int dataBits; private int parity; private int stopBits; private int flags; public static void setSuPath(@Nullable String suPath) { if (suPath == null) { return; } sSuPath = suPath; } @NonNull public static String getSuPath() { return sSuPath; } private FileDescriptor mFd; private FileInputStream mFileInputStream; private FileOutputStream mFileOutputStream; public SerialPort(@NonNull File device, int baudrate, int dataBits, int parity, int stopBits, int flags) throws SecurityException, IOException { this.device = device; this.baudrate = baudrate; this.dataBits = dataBits; this.parity = parity; this.stopBits = stopBits; this.flags = flags; Log.e(TAG, "SerialPort: canRead" + device.canRead()); if (!device.canRead() || !device.canWrite()) { try { Process su; su = Runtime.getRuntime().exec(sSuPath); String cmd = "chmod 666 " + device.getAbsolutePath() + "\n" + "exit\n"; su.getOutputStream().write(cmd.getBytes()); if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) { throw new SecurityException(); } } catch (Exception e) { e.printStackTrace(); throw new SecurityException(); } } mFd = open(device.getAbsolutePath(), baudrate, dataBits, parity, stopBits, flags); if (mFd == null) { Log.e(TAG, "native open returns null"); throw new IOException(); } mFileInputStream = new FileInputStream(mFd); mFileOutputStream = new FileOutputStream(mFd); } public SerialPort(@NonNull File device, int baudrate) throws SecurityException, IOException { this(device, baudrate, 8, 0, 1, 0); } public SerialPort(@NonNull File device, int baudrate, int dataBits, int parity, int stopBits) throws SecurityException, IOException { this(device, baudrate, dataBits, parity, stopBits, 0); } // Getters and setters @NonNull public InputStream getInputStream() { return mFileInputStream; } @NonNull public OutputStream getOutputStream() { return mFileOutputStream; } @NonNull public File getDevice() { return device; } public int getBaudrate() { return baudrate; } public int getDataBits() { return dataBits; } public int getParity() { return parity; } public int getStopBits() { return stopBits; } public int getFlags() { return flags; } // JNI private native FileDescriptor open(String absolutePath, int baudrate, int dataBits, int parity, int stopBits, int flags); public native void close(); public void tryClose() { try { mFileInputStream.close(); } catch (IOException e) { //e.printStackTrace(); } try { mFileOutputStream.close(); } catch (IOException e) { //e.printStackTrace(); } try { close(); } catch (Exception e) { //e.printStackTrace(); } } static { System.loadLibrary("serial_port"); } public static Builder newBuilder(File device, int baudrate) { return new Builder(device, baudrate); } public static Builder newBuilder(String devicePath, int baudrate) { return new Builder(devicePath, baudrate); } public final static class Builder { private File device; private int baudrate; private int dataBits = 8; private int parity = 0; private int stopBits = 1; private int flags = 0; private Builder(File device, int baudrate) { this.device = device; this.baudrate = baudrate; } private Builder(String devicePath, int baudrate) { this(new File(devicePath), baudrate); } public Builder dataBits(int dataBits) { this.dataBits = dataBits; return this; } public Builder parity(int parity) { this.parity = parity; return this; } public Builder stopBits(int stopBits) { this.stopBits = stopBits; return this; } public Builder flags(int flags) { this.flags = flags; return this; } public SerialPort build() throws SecurityException, IOException { return new SerialPort(device, baudrate, dataBits, parity, stopBits, flags); } } public static void checkFilePermission(File file) { Log.e(TAG, "canRead: " + file.canRead()); Log.e(TAG, "canWrite: " + file.canWrite()); if (!file.canRead() || !file.canWrite()) { try { Process su; su = Runtime.getRuntime().exec(sSuPath); String cmd = "chmod 7777 " + file.getAbsolutePath() + "\n" + "exit\n"; su.getOutputStream().write(cmd.getBytes()); Log.e(TAG, "checkFilePermission: " + file.getAbsolutePath()); if ((su.waitFor() != 0) || !file.canRead() || !file.canWrite()) { throw new SecurityException(); } } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "checkFilePermission: Exception:" + e.getMessage()); throw new SecurityException(); } } } //隐藏系统导航栏 public void hideBottomNavation() { chmod("mount -o remount -w /system"); chmod("chmod 777 /system"); chmod("echo qemu.hw.mainkeys=1 >> /system/build.prop"); } public void chmod(String instruct) { try { Process process = null; DataOutputStream os = null; process = Runtime.getRuntime().exec("su"); os = new DataOutputStream(process.getOutputStream()); os.writeBytes(instruct); os.flush(); os.close(); } catch (Exception ex) { ex.printStackTrace(); } }}
5 CMakeLists.txt,编译c或c++程序的规则文件
Cmake在Jni那篇讲过,这个地方在讲下
CMake是一个可以跨平台的编译工具,可以用简单的语句来描述所有平台的编译过程。他能够输出各种各样的 makefile 或者工程文件。和make与makefile类似,我们在使用CMake时同样也需要一个文件来提供规则,这个文件就是CMakeLists
使用CMake编写跨平台工程的流程如下:
(1)编写源文件
(2)编写CMakeLists.txt
(3)由CMake根据CMakeLists.txt来生成相应的makefile文件
(4)使用make并根据makefile调用gcc来生成相应的可执行文件。
# Sets the minimum version of CMake required to build your native library.# This ensures that a certain set of CMake features is available to# your build.cmake_minimum_required(VERSION 3.4.1)# Specifies a library name, specifies whether the library is STATIC or# SHARED, and provides relative paths to the source code. You can# define multiple libraries by adding multiple add.library() commands,# and CMake builds them for you. When you build your app, Gradle# automatically packages shared libraries with your APK.add_library( # Specifies the name of the library. serial_port # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/SerialPort.c ) find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log )# 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( # Specifies the target library. serial_port # Links the target library to the log library # included in the NDK. ${log-lib} )
5 调用串口-连接,并获取输入输出流
Runnable serialConnectRunnable = new Runnable() { @Override public void run() { try { if (mSerialPort == null) { mSerialPort = new SerialPort(new File(path), baudrate); mOutputStream = mSerialPort.getOutputStream(); mInputStream = mSerialPort.getInputStream(); } } catch (SecurityException e) { ToastUtil.showToast(App.getInstance(), "You do not have read/write permission to the serial port."); } catch (IOException e) { ToastUtil.showToast(App.getInstance(), "The serial port can not be opened for an unknown reason."); } catch (InvalidParameterException e) { ToastUtil.showToast(App.getInstance(), "Please configure your serial port first."); } //Serial结束 } };
2.6 读取串口消息
private class ReadThread extends Thread { @Override public void run() { super.run(); while (!isInterrupted()) { int size; try { byte[] buffer = new byte[512]; if (mInputStream == null) return; size = mInputStream.read(buffer); if (size > 0) { String mReception=new String(buffer, 0, size); String msg = mReception.toString().trim(); Log.e(TAG, "接收短消息:" + msg); } } catch (IOException e) { e.printStackTrace(); return; } } } }
7 发送串口指令
private class WriteRunnable implements Runnable { @Override public void run() { try { String cmd="KZMT;"; Log.e(TAG, "发送短消息:" + cmd); mOutputStream.write(cmd.getBytes()); mOutputStream.flush(); } catch (IOException e) { } } }
8 断开关闭串口
public void closeSerialPortStream() {try {if (mOutputStream != null) {mOutputStream.close();mOutputStream = null;}if (mInputStream != null) {mInputStream.close();mInputStream = null;}if (mSerialPort != null) {mSerialPort.close();mSerialPort = null;} } catch (Exception e) {e.printStackTrace();}}
三 google官方串口工具类
1 除了上面自己编程C底层文件,也可以直接用google官方的串口工具SDK(android-serialport-api),Github串口Demo地址:https://github.com/licheedev/Android-SerialPort-API
2 依赖:
allprojects { repositories { ... jcenter() mavenCentral() // since 2.1.3 }}dependencies { implementation 'com.licheedev:android-serialport:2.1.3'}
3 使用
// 默认8N1(8数据位、无校验位、1停止位)// Default 8N1 (8 data bits, no parity bit, 1 stop bit)SerialPort serialPort = new SerialPort(path, baudrate);// 可选配置数据位、校验位、停止位 - 7E2(7数据位、偶校验、2停止位)// or with builder (with optional configurations) - 7E2 (7 data bits, even parity, 2 stop bits)SerialPort serialPort = SerialPort .newBuilder(path, baudrate)// 校验位;0:无校验位(NONE,默认);1:奇校验位(ODD);2:偶校验位(EVEN)// Check bit; 0: no check bit (NONE, default); 1: odd check bit (ODD); 2: even check bit (EVEN)// .parity(2) // 数据位,默认8;可选值为5~8// Data bit, default 8; optional value is 5~8// .dataBits(7) // 停止位,默认1;1:1位停止位;2:2位停止位// Stop bit, default 1; 1:1 stop bit; 2: 2 stop bit// .stopBits(2) .build(); // read/write to serial port - needs to be in different thread!InputStream in = serialPort.getInputStream();OutputStream out = serialPort.getOutputStream();// closeserialPort.tryClose();
四 总结
串口通讯使用到进程、Linux指令、JNI等,但本质最终还是获得一个输入输出流去进行读写操作;
串口通讯对于Android开发者来说,仅需关注如何连接、操作(发送指令)、读取数据。
大部分的物联网通信本质上都是获取io流,通过io流进行数据的传输和读取,比如蓝牙,wifi等,只不过蓝牙,wifi是通过Socket协议维持一个长连接进行通信
来源地址:https://blog.csdn.net/qq_29848853/article/details/130258387