引言:
之所以寫這篇文章,一方面是最近工作中對Android串口通信方面學習的總結。另外一方面也希望能夠幫助到大家,能夠簡單的去理解串口通信方面的知識。
爲什麼學習Android串口通信:
- 距離2008年發佈第一款Android手機已經過去了10年時光了。現在Android的發展是百花齊放,尤其是對於很多公司而言,Android主板與各種傳感器和智能設備之間通信是很常見的事情了,那麼對於開發人員而言,學習串口通信是必要的事情了。
- 學習串口通信,能夠提前瞭解JNI和NDK的知識,算是一個入門預習吧
- Google出品,必屬精品。我們現在市面上的所有Android串口通信的源代碼都是Google公司在2011年開源的Google官方源代碼,學習它也不妨是學習Google的設計思維。
集成串口通信:
導入.so文件
什麼是.so文件:
- .so文件是Unix的動態連接庫,本身是二進制文件,是由C/C++編譯而來的。
- Android調用.so文件的過程也就是所謂的JNI了。在Android中想要調用C/C++中的API的話,也就是調用.so文件了。
一 、 複製圖上所示的三個.so文件的文件夾,到Project -->app -->libs(沒有就自己新建libs)
二 、 配置Gradle文件:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "cn.humiao.myserialport"
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
//這裏是配置JNI的引用地址,也就是引用.so文件
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
總結:就這樣的幾步,配置OK了,就能愉快的在Java裏面直接調用C/C++了。啦啦啦啦 ~
Google串口代碼分析:
SerialPort
/**
* Google官方代碼
*
* 此類的作用爲,JNI的調用,用來加載.so文件的
*
* 獲取串口輸入輸出流
*/
public class SerialPort {
private static final String TAG = "SerialPort";
/*
* Do not remove or rename the field mFd: it is used
* 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();
}
}
System.out.println(device.getAbsolutePath() + "==============================");
//開啓串口,傳入物理地址、波特率、flags值
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);
}
//獲取串口的輸入流
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.out.println("==============================");
//加載庫文件.so文件
System.loadLibrary("serial_port");
System.out.println("********************************");
}
}
一:類作用及介紹
通過打開JNI的調用,打開串口。獲取串口通信中的輸入輸出流,通過操作IO流,達到能夠利用串口接收數據和發送數據的目的
二:類方法介紹
A : 開啓串口的方法:private native static FileDescriptor open(String path, int baudrate,int flags)
- path:爲串口的物理地址,一般硬件工程師都會告訴你的例如ttyS0、ttyS1等,或者通過SerialPortFinder類去尋找得到可用的串口地址。
- baudrate:波特率,與外接設備一致
- flags:設置爲0,原因較複雜,見文章最底下。
B : IO流,如果你不是很明白輸入輸出流的話,可以看看這篇文章史上最簡單的IO流解釋
// 輸入流,也就是獲取從單片機或者傳感器,通過串口傳入到Android主板的IO數據(使用的時候,執行Read方法)
mFileInputStream = new FileInputStream(mFd);
//輸出流,Android將需要傳輸的數據發送到單片機或者傳感器(使用的時候,執行Write方法)
mFileOutputStream = new FileOutputStream(mFd);
獲取了輸入輸出流以後就可以對流進行操作了。
順便科普一下我所理解的IO流:
- 將InputStream和OutputStream的流當做一個管道,所有的byte流(也可以說是二進制流,因爲byte裏面存儲的都是bit)都是流動在這個管道里面的。管道通道兩邊的一邊是內存,一邊是外部存儲。
- 所謂的寫(Write),也就是將內存的數據寫到外部存儲。反之,讀(Read)也就是將外部存儲的數據讀取到內存裏面。
- 對於OutputStream與Write結合使用:
//示例代碼,不能直接運行,我只是講一下我的思路
byte[] sendData = DataUtils.HexToByteArr(data);
outputStream.write(sendData);
outputStream.flush();
首先是獲取Byte數組,然後通過wite()方法將Byte數組放到管道OutputStream中,然後wirte出去,寫出到外部存儲。
- 對於InputStream與Read結合使用:
//示例代碼,不能直接運行,我只是講一下我的思路
byte[] readData = new byte[1024];
int size = inputStream.read(readData);
String readString = DataUtils.ByteArrToHex(readData, 0, size);
首先,new一個byte數組,也就是在內存裏面開闢一個空間用來存儲byte字節。然後管道InputStream中的外部數據通過read方法,讀取到內存裏面,也就是byte[]中,返回值是讀取的大小。然後再將byte[]轉換爲String。
- OutputStream和InputStream兩個管道。輸出流的管道里面是沒有數據的,需要將數據寫入;輸入流的管道里是有數據的,需要讀出來。
SerialPortFinder
- 這個類是用來獲取串口物理地址的,其實一般是用不到這個類的,因爲硬件設備上串口的物理地址,在硬件上都是有具體標識的,你直接使用就可以了。Google既然有這個幫助類的話,我們就具體分析一波。
- 具體是這樣的:
-
在這個方法中:Vector< Driver > getDrivers() throws IOException 讀取 /proc/tty/drivers 裏面的帶有serial,也就是標識串口的地址,然後保存在一個集合裏面,例如在我的Android設備中:
如圖示,有八個不同類型的串口驅動地址。對我而言我需要的是/dev/ttyS -
在Vector< File > getDevices()方法中,讀取/dev下的地址。但是如圖示,地址有很多,我們需要的是串口,那麼就將drivers中讀取的地址,與/dev中的地址匹配,成功的則存儲到集合中。於是就成功獲取到了所有串口地址了。
- 上面是具體的思路,我們一般使用的都是下面這個方法:
//獲取在設備目錄下的,所有串口的具體物理地址,並且存入到數組裏面。
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()]);
}
- 能夠獲取所有的串口的具體地址,然後進行選擇你需要的物理地址就行了。一般來說的話,串口地址爲: /dev/ttyS2、/dev/ttyS1、/dev/ttyS0
SerialPortUtil
public class SerialPortUtil {
private SerialPort serialPort = null;
private InputStream inputStream = null;
private OutputStream outputStream = null;
private ReceiveThread mReceiveThread = null;
private boolean isStart = false;
/**
* 打開串口,接收數據
* 通過串口,接收單片機發送來的數據
*/
public void openSerialPort() {
try {
serialPort = new SerialPort(new File("/dev/ttyS0"), 9600, 0);
//調用對象SerialPort方法,獲取串口中"讀和寫"的數據流
inputStream = serialPort.getInputStream();
outputStream = serialPort.getOutputStream();
isStart = true;
} catch (IOException e) {
e.printStackTrace();
}
getSerialPort();
}
/**
* 關閉串口
* 關閉串口中的輸入輸出流
*/
public void closeSerialPort() {
Log.i("test", "關閉串口");
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
isStart = false;
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 發送數據
* 通過串口,發送數據到單片機
*
* @param data 要發送的數據
*/
public void sendSerialPort(String data) {
try {
byte[] sendData = DataUtils.HexToByteArr(data);
outputStream.write(sendData);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
private void getSerialPort() {
if (mReceiveThread == null) {
mReceiveThread = new ReceiveThread();
}
mReceiveThread.start();
}
/**
* 接收串口數據的線程
*/
private class ReceiveThread extends Thread {
@Override
public void run() {
super.run();
while (isStart) {
if (inputStream == null) {
return;
}
byte[] readData = new byte[1024];
try {
int size = inputStream.read(readData);
if (size > 0) {
String readString = DataUtils.ByteArrToHex(readData, 0, size);
EventBus.getDefault().post(readString);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
一 類的作用:
- 實例化類SerialPort,傳入地址和波特率和flags值,獲取串口的IO流,然後對IO流進行操作。
- 注意一點:我這裏因爲與Android串口連接的設備都是需要的16進制的指令。所以我利用封裝的工具類,將字符"010100000000FFFF"轉換爲16進制的 010100000000FFFF,也就是轉Hex字符串爲字節數組。
二 發送數據:
//轉Hex字符串轉字節數組
byte[] sendData = DataUtils.HexToByteArr(data);
//寫入數組到輸出流
outputStream.write(sendData);
//刷新
outputStream.flush();
三 獲取數據:
因爲是讀取流,所以我專門開一個線程去讀取流:
private class ReceiveThread extends Thread {
@Override
public void run() {
super.run();
//是否開啓串口
while (isStart) {
if (inputStream == null) {
return;
}
//new一個Byte數組
byte[] readData = new byte[1024];
try {
//將流中數據讀到Byte數組中,返回讀的Size大小
int size = inputStream.read(readData);
if (size > 0) {
//將Byte數組轉換了String
String readString = DataUtils.ByteArrToHex(readData, 0, size);
//跨線程通信,利用EventBus將數據傳輸到主線程,也可使用Handler
EventBus.getDefault().post(readString);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 別的都沒什麼可講的了,看註釋就行。其中重點是"布爾值:isStart"。因爲開啓的線程是一直接收串口的數據的,如果不設置isStart的話,那麼接收數據就只會執行一次,因爲開啓串口的時候纔會new線程或者複用線程。而開啓串口肯定只會執行一次,當然接收線程只會一次了。當時我沒有寫這個就被坑了半天時間! 於是進行條件設置用While函數,那麼只要是isStart == True,那麼線程就會一直執行下去!這就是條件判斷的魅力!
一句話總結:
Android串口通信:抱歉,學會它真的可以爲所欲爲
----------------------------------------------------------------------------------------------------
對於串口的Flag爲0的理解 :
-
打開串口的時候,一直很奇怪,爲什麼會存在flag爲0。查詢了很多資料,有的人說是因爲這個是一個校驗位Android串口通信
-
有的人說這個並沒有任何的作用和意義。
-
我從Google的本身的代碼。去研究發現,其flags在C++裏面的代碼是:
關鍵的代碼是:
fd = open(path_utf, O_RDWR | flags) ;
open()函數:
其中O_RDWR 表示可以讀也可以寫 ,爲Linux下的Open函數裏面的值。
根據open各個參數含義,我們可以知道,這是常用的一種用法,fd是設備描述符,linux在操作硬件設備時,屏蔽了硬件的基本細節,只把硬件當做文件來進行操作,而所有的操作都是以open函數來開始,它用來獲取fd,然後後期的其他操作全部控制fd來完成對硬件設備的實際操作。
####讀寫的Hex數含義:
對於O_RDWR 的16進制的值是:02H , 讀寫參數的Hex數值
- #define O_RDONLY 00
- #define O_WRONLY 01
- #define O_RDWR 02
分析這段代碼:fd = open(path_utf, O_RDWR | flags) ;
一 、 在Open函數裏面,傳入flags值爲0,進行按位或運算,得到的結果還是O_RDWR(02H),沒有任何區別。所以說,其flags的值沒有啥意義,也可以是這樣理解的。
二 、 但是,我想Google之所以這樣子去設計,肯定是有意義的,和公司的硬件大佬CJ大佬討論以後。有以下的幾點思考:
a . 對於Open函數本身而言,會根據不同的值,進行不同的操作,如可讀寫(O_RDWR),非阻塞(O_NONBLOCK)等等。因爲Open函數本身不僅僅是操作,還可以操作各種文件。所以需要很多的設定。
b. 在Open函數中,對於串口操作而言,肯定是必須可讀可寫,所以有O_RDWR。而與flags按位或運算。其根本目是,按位或的運算後的結果最後一位必須爲2,例如xx 02 或者xx x2 。因爲對於底層代碼而言,最後一位是判斷你是否可以讀寫的根本。所以Google的意思是最後一位必須固定不變,爲"2",所以只需要保證最後一位是2就好,我試了試傳10H也就是flags爲16(十進制),得到的結果爲12H,最後一位爲2。發現完全是可以正常通信的。bingo ~ 結論正確
結論:我傾向於理解爲flags的值有意義的。
意義:必須保證串口的可讀可寫性,也就是其最後一位爲2的條件下,進行拓展功能,如傳入的flags值爲04000H(O_NONBLOCK非阻塞),進行按位或得到04002,那麼最後串口的open操作既能讀寫,又是非阻塞的。不過一般你使用直接設置爲"0"即可 ~ ~ ~嘻嘻 :)
----------------------------------------------------------------------------------------------------
源代碼Here:
- Github下載地址:!------> SerialPortの源代碼
- CSDN下載地址:!------> SerialPortの源代碼
----------------------------------------------------------------------------------------------------