Android串口通信:抱歉,學會它真的可以爲所欲爲

引言:

之所以寫這篇文章,一方面是最近工作中對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)

.so文件
二 、 配置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既然有這個幫助類的話,我們就具體分析一波。
  • 具體是這樣的:
  1. 在這個方法中:Vector< Driver > getDrivers() throws IOException 讀取 /proc/tty/drivers 裏面的帶有serial,也就是標識串口的地址,然後保存在一個集合裏面,例如在我的Android設備中:
    drivers的物理地址
    如圖示,有八個不同類型的串口驅動地址。對我而言我需要的是/dev/ttyS

  2. Vector< File > getDevices()方法中,讀取/dev下的地址。但是如圖示,地址有很多,我們需要的是串口,那麼就將drivers中讀取的地址,與/dev中的地址匹配,成功的則存儲到集合中。於是就成功獲取到了所有串口地址了。

dev地址

  1. 上面是具體的思路,我們一般使用的都是下面這個方法:
//獲取在設備目錄下的,所有串口的具體物理地址,並且存入到數組裏面。
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的理解 :

  1. 打開串口的時候,一直很奇怪,爲什麼會存在flag爲0。查詢了很多資料,有的人說是因爲這個是一個校驗位Android串口通信

  2. 有的人說這個並沒有任何的作用和意義。

  3. 我從Google的本身的代碼。去研究發現,其flags在C++裏面的代碼是:
    底層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:

----------------------------------------------------------------------------------------------------

參考:

Android串口通信

android串口通信——android-serialport-api

Android Studio下的串口程序開發實戰

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章