提到串口編程,就不得不提到JNI,不得不提到JavaAPI中的檔案描述符類:FileDescriptor。下面我分別對JNI、FileDescriptor以及串口的一些知識點和實現的源碼進行分析說明。這裡主要是參考了開源項目android-serialport-api。
串口編程需要瞭解的基本知識點:對於串口編程,我們只需對串口進行一系列的設定,然後開啟串口,這些操作我們可以參考串口調試助手的源碼進行學習。在Java中如果要實現串口的讀寫功能只需操作檔案裝置類:FileDescriptor即可,其他的事都由驅動來完成不用多管!當然,你想瞭解,那就得看驅動代碼了。這裡並不打算對驅動進行說明,只初略闡述應用程式層的實現方式。 
(一)JNI: 
關於JNI的文章網上有很多,不再多做解釋,想詳細瞭解的朋友可以查看雲中漫步的技術文章,寫得很好,分析也很全面,那麼在這篇拙文中我強調3點: 
1、如何將編譯好的SO檔案打包到APK中?(方法很簡單,直接在工程目錄下建立檔案夾 libs/armeabi,將SO檔案Copy到此目錄即可) 
2、命名要注意的地方?(在編譯好的SO檔案中,將檔案重新命名為:libfilename.so即可。其中filename.so是編譯好後產生的檔案) 
3、MakeFile檔案的編寫(不用多說,可以直接參考package/apps目錄下用到JNI的相關項目寫法) 
這是關鍵的代碼: 複製代碼 代碼如下:<span style="font-size:18px;"> 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; 
} 
} 
</span> 
(二)FileDescritor: 
檔案描述符類的執行個體用作與基礎機器有關的某種結構的不透明控制代碼,該結構表示開放檔案、開放通訊端或者位元組的另一個源或接收者。檔案描述符的主要實際用途是建立一個包含該結構的FileInputStream 或FileOutputStream。這是API的描述,不太好理解,其實可簡單的理解為:FileDescritor就是對一個檔案進行讀寫。 
(三)實現串口通訊細節 
1) 建工程:SerialDemo包名:org.winplus.serial,並在工程目錄下建立jni和libs兩個檔案夾和一個org.winplus.serial.utils,如: 
2) 建立一個類:SerialPortFinder,添加如下代碼: 複製代碼 代碼如下:<span style="font-size:18px;">package org.winplus.serial.utils; 
import java.io.File; 
import java.io.FileReader; 
import java.io.IOException; 
import java.io.LineNumberReader; 
import java.util.Iterator; 
import java.util.Vector; 
import android.util.Log; 
public class SerialPortFinder { 
private static final String TAG = "SerialPort"; 
private Vector<Driver> mDrivers = null; 
public class Driver { 
public Driver(String name, String root) { 
mDriverName = name; 
mDeviceRoot = root; 
} 
private String mDriverName; 
private String mDeviceRoot; 
Vector<File> mDevices = null; 
public Vector<File> getDevices() { 
if (mDevices == null) { 
mDevices = new Vector<File>(); 
File dev = new File("/dev"); 
File[] files = dev.listFiles(); 
int i; 
for (i = 0; i < files.length; i++) { 
if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) { 
Log.d(TAG, "Found new device: " + files[i]); 
mDevices.add(files[i]); 
} 
} 
} 
return mDevices; 
} 
public String getName() { 
return mDriverName; 
} 
} 
Vector<Driver> getDrivers() throws IOException { 
if (mDrivers == null) { 
mDrivers = new Vector<Driver>(); 
LineNumberReader r = new LineNumberReader(new FileReader( 
"/proc/tty/drivers")); 
String l; 
while ((l = r.readLine()) != null) { 
// Issue 3: 
// Since driver name may contain spaces, we do not extract 
// driver name with split() 
String drivername = l.substring(0, 0x15).trim(); 
String[] w = l.split(" +"); 
if ((w.length >= 5) && (w[w.length - 1].equals("serial"))) { 
Log.d(TAG, "Found new driver " + drivername + " on " 
+ w[w.length - 4]); 
mDrivers.add(new Driver(drivername, w[w.length - 4])); 
} 
} 
r.close(); 
} 
return mDrivers; 
} 
public String[] getAllDevices() { 
Vector<String> devices = new Vector<String>(); 
// Parse each driver 
Iterator<Driver> itdriv; 
try { 
itdriv = getDrivers().iterator(); 
while (itdriv.hasNext()) { 
Driver driver = itdriv.next(); 
Iterator<File> itdev = driver.getDevices().iterator(); 
while (itdev.hasNext()) { 
String device = itdev.next().getName(); 
String value = String.format("%s (%s)", device, 
driver.getName()); 
devices.add(value); 
} 
} 
} catch (IOException e) { 
e.printStackTrace(); 
} 
return devices.toArray(new String[devices.size()]); 
} 
public String[] getAllDevicesPath() { 
Vector<String> devices = new Vector<String>(); 
// Parse each driver 
Iterator<Driver> itdriv; 
try { 
itdriv = getDrivers().iterator(); 
while (itdriv.hasNext()) { 
Driver driver = itdriv.next(); 
Iterator<File> itdev = driver.getDevices().iterator(); 
while (itdev.hasNext()) { 
String device = itdev.next().getAbsolutePath(); 
devices.add(device); 
} 
} 
} catch (IOException e) { 
e.printStackTrace(); 
} 
return devices.toArray(new String[devices.size()]); 
} 
} 
</span> 
上面這個類在“android-serialport-api串口工具測試隨筆”中有詳細的說明,我就不多說了。 
3)建立SerialPort類,這個類主要用來載入SO檔案,通過JNI的方式開啟關閉串口 複製代碼 代碼如下:<span style="font-size:18px;">package org.winplus.serial.utils; 
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 { 
/* Check access permission */ 
if (!device.canRead() || !device.canWrite()) { 
try { 
/* Missing read/write permission, trying to chmod the file */ 
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); 
} 
// Getters and setters 
public InputStream getInputStream() { 
return mFileInputStream; 
} 
public OutputStream getOutputStream() { 
return mFileOutputStream; 
} 
// JNI 
private native static FileDescriptor open(String path, int baudrate, 
int flags); 
public native void close(); 
static { 
System.loadLibrary("serial_port"); 
} 
} 
</span> 
4) 建立一個MyApplication 繼承android.app.Application,用來對串口進行初始化和關閉串口 複製代碼 代碼如下:<span style="font-size:18px;">package org.winplus.serial; 
import java.io.File; 
import java.io.IOException; 
import java.security.InvalidParameterException; 
import org.winplus.serial.utils.SerialPort; 
import org.winplus.serial.utils.SerialPortFinder; 
import android.content.SharedPreferences; 
public class MyApplication extends android.app.Application { 
public SerialPortFinder mSerialPortFinder = new SerialPortFinder(); 
private SerialPort mSerialPort = null; 
public SerialPort getSerialPort() throws SecurityException, IOException, InvalidParameterException { 
if (mSerialPort == null) { 
/* Read serial port parameters */ 
SharedPreferences sp = getSharedPreferences("android_serialport_api.sample_preferences", MODE_PRIVATE); 
String path = sp.getString("DEVICE", ""); 
int baudrate = Integer.decode(sp.getString("BAUDRATE", "-1")); 
/* Check parameters */ 
if ( (path.length() == 0) || (baudrate == -1)) { 
throw new InvalidParameterException(); 
} 
/* Open the serial port */ 
mSerialPort = new SerialPort(new File(path), baudrate, 0); 
} 
return mSerialPort; 
} 
public void closeSerialPort() { 
if (mSerialPort != null) { 
mSerialPort.close(); 
mSerialPort = null; 
} 
} 
} 
</span> 
5) 建立一個繼承抽象的Activity類,主要用於讀取串口的資訊 複製代碼 代碼如下:<span style="font-size:18px;">package org.winplus.serial; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.security.InvalidParameterException; 
import org.winplus.serial.utils.SerialPort; 
import android.app.Activity; 
import android.app.AlertDialog; 
import android.content.DialogInterface; 
import android.content.DialogInterface.OnClickListener; 
import android.os.Bundle; 
public abstract class SerialPortActivity extends Activity { 
protected MyApplication mApplication; 
protected SerialPort mSerialPort; 
protected OutputStream mOutputStream; 
private InputStream mInputStream; 
private ReadThread mReadThread; 
private class ReadThread extends Thread { 
@Override 
public void run() { 
super.run(); 
while (!isInterrupted()) { 
int size; 
try { 
byte[] buffer = new byte[64]; 
if (mInputStream == null) 
return; 
/** 
* 這裡的read要尤其注意,它會一直等待資料,等到天荒地老,海枯石爛。如果要判斷是否接受完成,只有設定結束標識,或作其他特殊的處理。 
*/ 
size = mInputStream.read(buffer); 
if (size > 0) { 
onDataReceived(buffer, size); 
} 
} catch (IOException e) { 
e.printStackTrace(); 
return; 
} 
} 
} 
} 
private void DisplayError(int resourceId) { 
AlertDialog.Builder b = new AlertDialog.Builder(this); 
b.setTitle("Error"); 
b.setMessage(resourceId); 
b.setPositiveButton("OK", new OnClickListener() { 
public void onClick(DialogInterface dialog, int which) { 
SerialPortActivity.this.finish(); 
} 
}); 
b.show(); 
} 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
mApplication = (MyApplication) getApplication(); 
try { 
mSerialPort = mApplication.getSerialPort(); 
mOutputStream = mSerialPort.getOutputStream(); 
mInputStream = mSerialPort.getInputStream(); 
/* Create a receiving thread */ 
mReadThread = new ReadThread(); 
mReadThread.start(); 
} catch (SecurityException e) { 
DisplayError(R.string.error_security); 
} catch (IOException e) { 
DisplayError(R.string.error_unknown); 
} catch (InvalidParameterException e) { 
DisplayError(R.string.error_configuration); 
} 
} 
protected abstract void onDataReceived(final byte[] buffer, final int size); 
@Override 
protected void onDestroy() { 
if (mReadThread != null) 
mReadThread.interrupt(); 
mApplication.closeSerialPort(); 
mSerialPort = null; 
super.onDestroy(); 
} 
} 
</span> 
6)編寫string.xml 以及baudrates.xml檔案 
在string.xml檔案中添加: 複製代碼 代碼如下:<span style="font-size:18px;"> <string name="error_configuration">Please configure your serial port first.</string> 
<string name="error_security">You do not have read/write permission to the serial port.</string> 
<string name="error_unknown">The serial port can not be opened for an unknown reason.</string> 
</span> 
在baudrates.xml檔案中添加 複製代碼 代碼如下:<span style="font-size:18px;"><?xml version="1.0" encoding="utf-8"?> 
<resources> 
<string-array name="baudrates_name"> 
<item>50</item> 
<item>75</item> 
<item>110</item> 
<item>134</item> 
<item>150</item> 
<item>200</item> 
<item>300</item> 
<item>600</item> 
<item>1200</item> 
<item>1800</item> 
<item>2400</item> 
<item>4800</item> 
<item>9600</item> 
<item>19200</item> 
<item>38400</item> 
<item>57600</item> 
<item>115200</item> 
<item>230400</item> 
<item>460800</item> 
<item>500000</item> 
<item>576000</item> 
<item>921600</item> 
<item>1000000</item> 
<item>1152000</item> 
<item>1500000</item> 
<item>2000000</item> 
<item>2500000</item> 
<item>3000000</item> 
<item>3500000</item> 
<item>4000000</item> 
</string-array> 
<string-array name="baudrates_value"> 
<item>50</item> 
<item>75</item> 
<item>110</item> 
<item>134</item> 
<item>150</item> 
<item>200</item> 
<item>300</item> 
<item>600</item> 
<item>1200</item> 
<item>1800</item> 
<item>2400</item> 
<item>4800</item> 
<item>9600</item> 
<item>19200</item> 
<item>38400</item> 
<item>57600</item> 
<item>115200</item> 
<item>230400</item> 
<item>460800</item> 
<item>500000</item> 
<item>576000</item> 
<item>921600</item> 
<item>1000000</item> 
<item>1152000</item> 
<item>1500000</item> 
<item>2000000</item> 
<item>2500000</item> 
<item>3000000</item> 
<item>3500000</item> 
<item>4000000</item> 
</string-array> 
</resources> 
</span> 
7)開始編寫介面了:在main.xml布局檔案中添加兩個編輯框,一個用來發送命令,一個用來接收命令: 複製代碼 代碼如下:<span style="font-size:18px;"><?xml version="1.0" encoding="utf-8"?> 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" 
android:orientation="vertical" > 
<EditText 
android:id="@+id/EditTextReception" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" 
android:layout_weight="1" 
android:gravity="top" 
android:hint="Reception" 
android:isScrollContainer="true" 
android:scrollbarStyle="insideOverlay" > 
</EditText> 
<EditText 
android:id="@+id/EditTextEmission" 
android:layout_width="fill_parent" 
android:layout_height="wrap_content" 
android:hint="Emission" 
android:lines="1" > 
</EditText> 
</LinearLayout> 
</span> 
8) SerialDemoActivity類的實現: 複製代碼 代碼如下:<span style="font-size:18px;">package org.winplus.serial; 
import java.io.IOException; 
import android.os.Bundle; 
import android.view.KeyEvent; 
import android.widget.EditText; 
import android.widget.TextView; 
import android.widget.TextView.OnEditorActionListener; 
public class SerialDemoActivity extends SerialPortActivity{ 
EditText mReception; 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.main); 
// setTitle("Loopback test"); 
mReception = (EditText) findViewById(R.id.EditTextReception); 
EditText Emission = (EditText) findViewById(R.id.EditTextEmission); 
Emission.setOnEditorActionListener(new OnEditorActionListener() { 
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 
int i; 
CharSequence t = v.getText(); 
char[] text = new char[t.length()]; 
for (i=0; i<t.length(); i++) { 
text[i] = t.charAt(i); 
} 
try { 
mOutputStream.write(new String(text).getBytes()); 
mOutputStream.write('\n'); 
} catch (IOException e) { 
e.printStackTrace(); 
} 
return false; 
} 
}); 
} 
@Override 
protected void onDataReceived(final byte[] buffer, final int size) { 
runOnUiThread(new Runnable() { 
public void run() { 
if (mReception != null) { 
mReception.append(new String(buffer, 0, size)); 
} 
} 
}); 
} 
} 
</span> 
寫到這裡,代碼基本上寫完了。下面就是要實現JNI層的功能了,要實現JNI,必須首先產生標頭檔,標頭檔的產生方式也很簡單, 我們編譯工程,在終端輸入 javah org.winplus.serial.utils.SerialPort 則會產生標頭檔:org_winplus_serial_utils_SerialPort.h,這個標頭檔的名字可以隨意命名。我們將它命名為:SerialPort.h拷貝到建立的目錄jni中,建立SerialPort.c 檔案,這兩個檔案的代碼就不貼出來了。直接到上傳的代碼中看吧。 
(四)串口的應用,可實現掃描磁頭,指紋識別等外圍USB轉串口的特色應用 
還蠻繁瑣的,以上只是對開源項目android-serialport-api 進行精簡想瞭解此項目請點擊此處!就這樣吧,晚了準備見周公去!