Android串口通信

YuriyShea 2015年07月16日 312次浏览

前段时间因为工作需要研究了一下android的串口通信,网上有很多讲串口通信的文章,我在做的时候也参考了很多文章,现在就将我学习过程中的一些心得分享给大家,希望可以帮助大家在学习的时候少走一些弯路,有的地方可能我的理解有偏差,也请大家见谅。

网上讲的很多案例都是基于手机端的串口通信Demo,但是在实际开发过程中,串口通信主要用于开发板应用的开发,即不是我们常见的手机android应用,而基于开发板的开发和手机应用开发还是有区别的,我在这里就不赘述二者的区别了,因为我们今天讲的重点是串口通信的实现。

那么,android的串口同信到底是如何实现的呢?在实现过程中又有哪些地方是需要我们特别注意的呢?

首先,我们可以参看google官方的源码,但是google源码的下载链接现在已经打不开了,我们可以从github下载到我们的源码:https://github.com/cepr/android-serialport-api,
这个源码里面包含了两个工程,我们应该选择这个android-serialport-api这个工程打开,打开之后,我们可以看到很多类文件,不要看着里面有很多类,其实只有几个类是最关键的:

SerialPort.java是这个项目当中最核心的类,用于通过调用底层C函数,实现串口的打开和关闭。SerialPort.java如下(我将原来的英文注释改为了中文注释):

/*
 * Copyright 2009 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. 
 */

package android_serialport_api;

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;

public class SerialPort {

    private static final String TAG = "SerialPort";

    /*
     * Do not remove or rename the field mFd: it is used by native method close();
     */
    private FileDescriptor mFd;
    private FileInputStream mFileInputStream;
    private FileOutputStream mFileOutputStream;

    public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {

        // 检查是否获取了指定串口的读写权限
        if (!device.canRead() || !device.canWrite()) {
            try {
                // 如果没有获取指定串口的读写权限,则通过挂在到linux的方式修改串口的权限为可读写
                Process su;
                su = Runtime.getRuntime().exec("/system/bin/su");
                String cmd = "chmod 777 " + 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);
    }

    // Getters and setters
    public InputStream getInputStream() {
        return mFileInputStream;
    }

    public OutputStream getOutputStream() {
        return mFileOutputStream;
    }

    // JNI:调用java本地接口,实现串口的打开和关闭
    /**
     * 串口有五个重要的参数:串口设备名,波特率,检验位,数据位,停止位
     * 其中检验位一般默认位NONE,数据位一般默认为8,停止位默认为1
     * @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");// 载入底层C文件
    }
}

那么上面的两个函数open()和close()在C中是怎样实现的呢?我们可以在本工程目录下面看到的jni文件夹有一个SerialPort.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:     android_serialport_SerialPort
 * Method:    open
 * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
 */
JNIEXPORT jobject JNICALL Java_android_1serialport_1api_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:     cedric_serial_SerialPort
 * Method:    close
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_android_1serialport_1api_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);
}

我们在用到自己的项目的时候,这个文件直接copy到我们的项目的jni文件(自己创建)目录下面就可以了,但需要注意的是,我们在建立自己项目的时候,为了避免不必要的错误,建议也按照本工程的结构来创建,即:建立一个名为android_serialport_api的包,然后把两个关键的类SerialPort.java和SerialPortFinder.java直接copy到这个包下面,这样做是为了保持和本地C函数中的方法名同步,如果不这样不这样做也可以,但是你需要根据你自己建立的包的名字来更改SerialPort.c中的函数名,我建议这样做也是为了避免在对JNI不是很了解的情况下陷入不必要的困扰。

细心的你们可能会发现,在android_serialport_api中还有一个类SerialPortFinder.java,那么这个类使用来干嘛的呢?其实这个类我们可用可不用,因为它是用来帮助我们找到设备中所有可用的串口的,即:如果你事先不清楚设备当中有哪些串口,那么你可以通过这个类来查询出设备中所有可用的串口,但是如果已经知道了串口的名字,那么这个类也可以不用,我们只需通过直接指定串口名的方式:SerialPort sp = new SerialPort(new File(path), baudrate, 0)打开串口,其中path就是串口的路径,baudrate为波特率,最后一个参数写0就可以了,如:SerialPort sp = new SerialPort(new File("/dev/ttyS0"), 9600, 0),当设备连接到电脑的时候,我们在DDMS中可以看到,所有的串口均在dev目录下面。如果我们要照出所有的串口,那么我们在建立自己的项目的时候,需要参考的类还有SerialPortPreferences.java,这个类继承了PreferenceActivity,其作用是用来展示出查询出来的所有串口,我们要用到自己的项目里面的时候直接copy即可。

这个项目里面还有一个类Application.java,这个类使用来干嘛的呢,它其实就是通过继承android.app.Application自定义了一个用来进行全局方法调用的Application,使用它我们可以很方便的在任何一个组件当中调用它里面所报看的变量和方法,这是我对其简单的理解,如果你要把这个类用到自己的项目当中来,那么需要注意的是,你需要改动清单文件中的配置,在清单文件的application节点中加入一行代码android:name="Application"即可,Application是我们自己写的Application的名字。这个类我们同样可以不用。

其实,整个项目可以进行极大的简化,我们可以只是用两个类便可实现串口通信,一个是SerialPort.java,另一个则是我们用来打开串口的Activity(在这个Activity中构造SerialPort实例便可),在此,我写了一个简化的Demo供大家参考,Demo地址:https://github.com/monkey1992/AndroidSerialPort