1、什么是串口?
串行端口(Serial port),或称串列埠、序列埠、串口,主要用于串列式逐位元数据传输(简单来讲就是按顺序一位一位地传输数据)。
常见的串口有25针和9针(RS-232标准),我们PC机上主要使用的是9针的串口。
2、串口通信原理
串口通信的概念很好理解,串口按位(bit)发送和接收字节。
我们常用的9针串口中有3针是有连接线的,分别是地线(1针),发送(2针),接收(3针),其他线用于握手的,可要可不要。大家有兴趣的可以拿起串口线接口看一下,串口母口的针孔附近一般都标有阿拉伯数字,标有1,2,3的那三个针孔就是上面说的接入连接线的。
有时也有只使用一根线来完成数据收发的,即发送数据,又接收数据。但是因为只有一根线,所以发数据的时候不能接数据,接数据的时候不能发数据,也就是通信双方不能同时收发数据,我们把这种称为半双工通信;
还有一种情况也只有一根线,且只允许上位机(发送数据的一方)向下位机(接收数据的一方)发送数据,我们把这种情况称为单工通信,比如我们电视机的有线就是单工通信;
而接入3针连接线的,通信双方可同时进行发送和接收数据,我们把这种称为全双工通信。
硬件层次上的串口通信的实现理解起来比较复杂,涉及的细节也比较多,如握手、奇偶位校验等等。我们不妨从软件实现上来看,就简单多了。我们可以认为系统为每个串口创建了一个虚拟文件(事实上系统也是这么做的),我们去打开串口的时候就相当于打开了一个文件。当我们去从串口接收数据时,就相当于把文件里的数据按位取出(这与读取普通的文件有一点不同,串口读取数据时,同时会擦除掉虚拟文件中被读取的数据,也就是你从虚拟文件中读取一段数据后,那段数据就在虚拟文件中消失了);同样,我们向串口写入数据时,就相当于把数据按位写入虚拟文件中,至于数据怎么传送给另一方,那么就交给系统去完成了。
3、Android应用上串口通信如何实现?
Android SDK并没有在Framework层实现封装关于串口通信的类库。但是,Android是基于Linux kernel 2.6上的,所以我们可以像在Linux系统上一样来使用串口。因为Framework层中并没有封装关于串口通信的类库,所以我们需要通过Android NDK来实现打开、读写串口,然后提供接口供JAVA本地调用。
我们实现串口通信的基本步骤如下:
1、设置串口名(虚拟文件的绝对路径),并打开串口;
2、配置串口参数(无特殊要求,一般使用默认参数);
3、读写串口;
4、关闭串口。
步骤2中提到的串口参数主要包括波特率、数据位停止位和奇偶校验等(其他的参数如写缓存大小、读缓存大小...)。对于两个进行通信的端口,这些参数必须匹配。
a.波特率:这是一个衡量通信速度的参数。它表示每秒钟传送的bit的个数,例如1000波特表示每秒钟发送1000个bit。当我们提到时钟周期时,我们就是指波特率,如果协议需要4800波特率,那么时钟是4800Hz。这意味着串口通信在数据线上的采样率为4800Hz。通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信。
b.数据位:这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据不会是8位的,标准的值是6、7和8位,如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位),扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据取决于通信协议的选取,术语“包”指任何通信的情况。
c.停止位:用于表示单个包的最后一位。典型的值为1、1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
d.奇偶位校验:在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低,当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个;如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不真正地检查数据,简单置位逻辑高或者逻辑低校验,这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。
4、实战演练
1、打开eclipse,新建一个Android APP项目,并在对话框中勾选“Mark this project as a library”,表示新建一个库项目;
注意:在选择API时不能大于API20,不然项目运行时会提示“无法连接到该库”。
2、项目上右键->Android Tools->Add Native Support...添加原生支持;
3、jin文件夹下新建SerialPort.h、SerialPort.c文件,并编码。
SerailPort.h文件源码:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_luoye_serialport_SerialPort */ #ifndef _Included_com_luoye_serialport_SerialPort #define _Included_com_luoye_serialport_SerialPort #ifdef __cplusplus extern "C" { #endif /* * Class: com_luoye_serialport_SerialPort * Method: open * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor; */ JNIEXPORT jobject JNICALL Java_com_luoye_serialport_SerialPort_open (JNIEnv *, jclass, jstring, jint, jint); /* * Class: com_luoye_serialport_SerialPort * Method: close * Signature: ()V */ JNIEXPORT void JNICALL Java_com_luoye_serialport_SerialPort_close (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endifSerialPort.c文件源码:
/* * Copyright 2009-2011 Cedric Priscal * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include <termios.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <jni.h> #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; } } /* * Class: com_luoye_serialport_SerialPort * Method: open * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor; */ JNIEXPORT jobject JNICALL Java_com_luoye_serialport_SerialPort_open (JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags) { int fd; speed_t speed; jobject mFileDescriptor; /* Check arguments */ { speed = getBaudrate(baudrate); if (speed == -1) { /* TODO: throw an exception */ LOGE("Invalid baudrate"); return NULL; } } /* Opening device */ { 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) { /* Throw an exception */ LOGE("Cannot open port"); /* TODO: throw an exception */ return NULL; } } /* Configure device */ { struct termios cfg; LOGD("Configuring serial port"); if (tcgetattr(fd, &cfg)) { LOGE("tcgetattr() failed"); close(fd); /* TODO: throw an exception */ return NULL; } cfmakeraw(&cfg); cfsetispeed(&cfg, speed); cfsetospeed(&cfg, speed); if (tcsetattr(fd, TCSANOW, &cfg)) { LOGE("tcsetattr() failed"); close(fd); /* TODO: throw an exception */ return NULL; } } /* Create a corresponding file descriptor */ { jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor"); jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V"); jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I"); mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor); (*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd); } return mFileDescriptor; } /* * Class: com_luoye_serialport_SerialPort * Method: close * Signature: ()V */ JNIEXPORT void JNICALL Java_com_luoye_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); }在SerialPort.c文件中,我们可以看到在Java_com_luoye_serialport_SerialPort_open函数中有这么一段代码:
/* Opening device */ { 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) { /* Throw an exception */ LOGE("Cannot open port"); /* TODO: throw an exception */ return NULL; } }我们可以看到打开串口就像我们平常打开普通文件一样,调用C库的open函数来打开串口。我们接着往下看:
/* Configure device */ { struct termios cfg; LOGD("Configuring serial port"); if (tcgetattr(fd, &cfg)) { LOGE("tcgetattr() failed"); close(fd); /* TODO: throw an exception */ return NULL; } cfmakeraw(&cfg); cfsetispeed(&cfg, speed); cfsetospeed(&cfg, speed); if (tcsetattr(fd, TCSANOW, &cfg)) { LOGE("tcsetattr() failed"); close(fd); /* TODO: throw an exception */ return NULL; } }这段代码是关于配置串口参数,我们可以看到tcgetattr()函数从我们打开的串口中读取串口的默认配置参数信息,然后通过cfsetispeed()函数和cfsetospeed()函数来修改配置信息中串口读写的波特率,最后通过调用tcsetattr()函数将修改后的配置信息设置于串口。
接着我们再往下看:/* Create a corresponding file descriptor */ { jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor"); jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V"); jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I"); mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor); (*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd); } return mFileDescriptor;在这段代码中,我们可以看到jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");这句,获取一个JAVA类FileDescriptor的引用,我们知道FileDescriptor类是JAVA库中的文件描述符类,后面的代码不用我多说,相信大家都看的明白,创建一个FileDescriptor类的对象,绑定串口的文件句柄,并返回对象。
而在Java_com_luoye_serialport_SerialPort_close函数中我们可以看到:
/* * Class: com_luoye_serialport_SerialPort * Method: close * Signature: ()V */ JNIEXPORT void JNICALL Java_com_luoye_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); }同样地,从FileDescriptor类的对象中获取到文件句柄,并调用C库中close()函数关闭串口。
4、在jin文件夹下创建Android.mk文件,并填写如下脚本:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) TARGET_PLATFORM := android-3 LOCAL_MODULE := serial_port LOCAL_SRC_FILES := SerialPort.c LOCAL_LDLIBS := -llog include $(BUILD_SHARED_LIBRARY)如果你想让你的库文件能够运行于不同的CPU架构下,那么可以在jin文件夹下创建一个Application.mk文件,并填写如下脚本:
APP_ABI := armeabi armeabi-v7a x86那么编译成功时,会在libs文件夹下分别创建armeabi、armeabi-v7a和x86三个文件夹,每个文件夹下拥有相同名称的so库文件(但是支持不同的CPU架构)。若没有添加上面的Application.mk文件,那么只会在libs文件夹下创建armeabi文件夹和生成相应的so库文件。
5、在项目中创建java类文件,并编码:
SerialPort.java文件源码:
package com.luoye.serialport; 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; import android.util.Log; /** * 串口类 * * @author LUOYE * @data 2015-07-05 11:03:15 */ public class SerialPort { /** Log日志输出标识 */ private static final String TAG = "SerialPort"; /** 串口文件描述符,禁止删除或重命名,因为native层关闭串口时需要使用 */ private FileDescriptor mFd; /** 输入流,用于接收串口数据 */ private FileInputStream mFileInputStream; /** 输出流,用于发送串口数据 */ private FileOutputStream mFileOutputStream; /** * 构造函数 * * @param device 串口名 * @param baudrate 波特率 * @param flags 操作标识 * @throws SecurityException 安全异常,当串口文件不可读写时触发 * @throws IOException IO异常,开启串口失败时触发 */ public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException { /* 检测设备管理权限,即文件的权限属性 */ if (!device.canRead() || !device.canWrite()) { try { /* 若没有读/写权限,试着chmod该设备 */ Process su; su = Runtime.getRuntime().exec("/system/bin/su"); 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, flags); if (mFd == null) { Log.e(TAG, "native open returns null"); throw new IOException(); } mFileInputStream = new FileInputStream(mFd); mFileOutputStream = new FileOutputStream(mFd); } /** * 获取输入流 * * @return 串口输入流 */ public InputStream getInputStream() { return mFileInputStream; } /** * 获取输出流 * * @return 串口输出流 */ public OutputStream getOutputStream() { return mFileOutputStream; } /** * 原生函数,开启串口虚拟文件 * * @param path 串口虚拟文件路径 * @param baudrate 波特率 * @param flags 操作标识 * @return */ private native static FileDescriptor open(String path, int baudrate, int flags); /** * 原生函数,关闭串口虚拟文件 */ public native void close(); static { System.loadLibrary("serial_port"); } }在SerialPort类中的FileDescriptor类型成员域mFd,就是我们SerialPort.c文件中的Java_com_luoye_serialport_SerialPort_close函数中要获取的“mFd”字段。在SerialPort类文件中后半部分我们可以看到关于原生函数的声明和原生库serial_port的加载。
6、新建一个Android App项目,引用我们上面生成的库项目,并填写如下代码测试:
mSerialPort = new SerialPort(new File("/dev/ttyS1"), mBaudRate, 0); mOutputStream = mSerialPort.getOutputStream(); mInputStream = mSerialPort.getInputStream();其他的关于如何读写串口,就跟如何读写文件是一样的方式,大家*发挥咯。
关于上面代码中的/dev/ttyS1串口名,我们可以在接入真机或运行模拟器时,打开eclipse上的DDMS,在“File Explorer”选项卡中查看整个文件系统的目录,找到dev文件夹,点击打开就可以看到啦!