Android中的串口通信

串口通讯

在计算机之间、计算机内部各部分之间,通信可以以串行和并行的方式进行。一个并行连接通过多个通道(例如导线、印制电路布线和光纤)在同一时间内传播多个数据流;而串行在同一时间内只连接传输一个数据流

虽然串行连接单个时钟周期能够传输的数据比并行数据更少,前者传输能力看起来比后者要弱一些,实际的情况却常常是,串行通信可以比并行通信更容易提高通信时钟频率,从而提高数据的传输速率。

串口通讯和并行通讯的区别

可以从上图看到,并行通讯可以一次传输8字节的数据,而串口一次只传输一个字节。但是通常串行通信都凭借其更低廉的部署成本成为更佳的选择,尤其是在远距离传输中。许多集成电路都具有串行通信接口来减少引脚数量,从而节约成本。

串口通讯的接口标准有很多,最常见的为RS-232、RS-485和USB等,下面我们看一下RS-232的接口标准。

串口通讯示意图

在RS-232标准中字符是按byte来一个接一个串列方式传输的,所以配线简单,发送的距离远,上图就是一个和电脑通讯的串口连接方式,通常情况我们将有管脚的端叫公头,将有孔的一端叫母头。

管脚的定义如下:

           DE-9 Male(Pin Side)                   DE-9 Female (Pin Side)
             -------------                          -------------
             \ 1 2 3 4 5 /                          \ 5 4 3 2 1 /
              \ 6 7 8 9 /                            \ 9 8 7 6 /
               ---------                              ---------
脚位简写意义说明
Pin1DCDData Carrier Detect调制解调器通知计算机有载波被侦测到。
Pin2RXDReceiver接收数据。
Pin3TXDTransmit发送数据。
Pin4DTRData Terminal Ready计算机告诉调制解调器可以进行传输。
Pin5GNDGround地线。
Pin6DSRData Set Ready调制解调器告诉计算机一切准备就绪。
Pin7RTSRequest To Send计算机要求调制解调器将数据提交。
Pin8CTSClear To Send调制解调器通知计算机可以传数据过来。
Pin9RIRing Indicator调制解调器通知计算机有电话进来。

串口通讯分为异步串行通信和同步串行通信。

异步串行通信

异步串行通信不需要发送方和接收方的时钟频率一致,这样就需要每个字符附件2~3位的起止、校验位和停止位,各个帧之间还有间隔,所以传输效率不高。

同步串行通信

同步串行通信需要发送方和接收方的时钟频率一致,有开始和结束标志,这种方式传输效率高。

Google官方源码

我们现在市面上的所有Android串口通信的源代码都是Google公司在2011年开源的Google官方源代码。

官方串口通讯源码地址:https://code.google.com/archive/p/android-serialport-api/

官方的源码是Eclipse环境的,幸运的是GitHub上已经有人帮我们做了Android Studio上面的支持,源码地址:https://github.com/lxqxsyu/Android-SerialPort-API

如何使用

第一步:开启串口。 第二步:打开输入输出流。 第三步:开启读线程。

try {
    mSerialPort = new SerialPort(devicePath, baudrate, 0);
    mInputStream = new BufferedInputStream(mSerialPort.getInputStream());
    mOutputStream = new BufferedOutputStream(mSerialPort.getOutputStream());
    mReadThread = new SerialReadThread();
    mReadThread.start();
    return mSerialPort;
} catch (Throwable tr) {
    closeSerial();
    return null;
}

向串口写入数据:

try {
    mOutputStream.write(bytes, off, len);
    mOutputStream.flush();
} catch (Exception e) {
    e.printStackTrace();
}

从串口读取数据:

while(true){
  int available = mInputStream.available();

  if (available > 0) {
      len = mInputStream.read(mRecvBuffer);
      if (len > 0) {
          getReceiver().onReceive(mRecvBuffer, 0, len);
      }
  } else {
      // 暂停一点时间,免得一直循环造成CPU占用率过高
      SystemClock.sleep(1);
  }
}

过程分析

检查devicePath是否具有可读可写的权限,如果没有则通过chmod 666来更改权限, 接下来调用native方法open().

在这个open()方法中首先做了一个波特率的转换。

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;
	}
}

随后调用了linux的open()函数

int open(const char *pathname, int flags);

pathname是文件路径。 flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个:

  • O_RDONLY:只读模式
  • O_WRONLY:只写模式
  • O_RDWR:可读可写

以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。

  • O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
  • O_CREAT 表示如果指定文件不存在,则创建这个文件
  • O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。
  • O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
  • O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
  • O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

函数API:

fd = open(path_utf, O_RDWR | flags);

然后使用通过cfgetispeed函数和cfgetospeed函数来设置输入和输出的波特率。

cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);

这个cfg是一个termios类型的结构体:

struct termios cfg;

termios结构体中,该结构体一般包括如下的成员:

成员说明
c_iflag输入模式标志,控制终端输入方式
c_oflag输出模式标志,控制终端输出方式
c_cflag控制模式标志,指定终端硬件控制信息
c_lflag本地模式标志,控制终端编辑功能

那个这个cfg的数据是从哪来的呢?其实是通过tcgetattr(fd, &cfg)函数获得的,这个函数可以获得与终端相关的参数,参数保存在cfg中。

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);

tcgetattr()可以得到波特率、字符大小、数据位、停止位、奇偶校验位和硬件流控制等。

最后创建了一个java.io.FileDescriptor对象并返回。

	/* 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);
	}

事实上我们的读写流也是通过FileDescriptor对象来获得的。

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);

哈哈,这个时候你是不是觉得这个FileDescriptor很神奇,它到底是什么。

FileDescriptor

FileDescriptor 是在UNIX系统里对文件描述的一个提法。在Window系统里,称为file handle。是指代文件的一种抽象表示法。

操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数。Java虽然在设计上使用了抽象程度更高的流来作为文件操作的模型,但是底层依然要使用文件描述符与操作系统交互,而Java世界里文件描述符的对应类就是FileDescriptor。

Java文件操作的三个类:FileIntputStream,FileOutputStream,RandomAccessFile,打开这些类的源码可以看到都有一个FileDescriptor成员变量。

操作系统中的文件描述符本质上是一个非负整数,其中0,1,2固定为标准输入,标准输出,标准错误输出,程序接下来打开的文件使用当前进程中最小的可用的文件描述符号码,比如3。

文件描述符本身就是一个整数,所以FileDescriptor的核心职责就是保存这个数字:

public final class FileDescriptor {
    private int fd;
}

标准输入,标准输出,标准错误输出是所有操作系统都支持的,对于一个进程来说,文件描述符0,1,2固定是标准输入,标准输出,标准错误输出。

Java对标准输入,标准输出,标准错误输出的支持也是通过FileDescriptor实现的,FileDescriptor中定义了in,out,err这三个静态变量:

public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);