前段時間因為工作需要研究了一下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