Android端串口通信開發整理

​這一段時間作的項目自動售貨機和無線終端設備的通信,都是經過串口進行對接和通信。在Android中進行串口通訊方式能夠用Google官方提供的demo代碼(android-serialport-api),也能夠經過NDK的方式使用C/C++進行實現(Android串口助手,C++實現),其底層原理都是經過調用open函數打開設備文件來進行讀寫操做。對串口接觸下來,發現真的能夠作不少有意思的東西,不少硬件設備均可以經過串口進行通信,好比:打印機、ATM吐卡機、IC/ID卡讀卡等,以及物聯網相關的設備。因此有必有對相關知識進行下梳理和總結。html

串口簡介

串口通訊(Serial Communications)的概念很是簡單,串口按位(bit)發送和接收字節。串口能夠在使用一根線(Tx)發送數據的同時用另外一根線(Rx)接收數據。java

串口參數

**波特率:**串口傳輸速率,用來衡量數據傳輸的快慢,即單位時間內載波參數變化的次數,如每秒鐘傳送240個字符,而每一個字符格式包含10位(1個起始位,1箇中止位,8個數據位),這時的波特率爲240Bd,比特率爲10位*240個/秒=2400bps。波特率與距離成反比,波特率越大傳輸距離相應的就越短。linux

**數據位:**這是衡量通訊中實際數據位的參數。當計算機發送一個信息包,實際的數據每每不會是8位的,標準的值是六、7和8位。如何設置取決於你想傳送的信息。android

**中止位:**用於表示單個包的最後一位。典型的值爲1,1.5和2位。因爲數據是在傳輸線上定時的,而且每個設備有其本身的時鐘,極可能在通訊中兩臺設備間出現了小小的不一樣步。所以中止位不只僅是表示傳輸的結束,而且提供計算機校訂時鐘同步的機會。適用於中止位的位數越多,不一樣時鐘同步的容忍程度越大,可是數據傳輸率同時也越慢。ios

**校驗位:**在串口通訊中一種簡單的檢錯方式。有四種檢錯方式:偶、奇、高和低。固然沒有校驗位也是能夠的。對於偶和奇校驗的狀況,串口會設置校驗位(數據位後面的一位),用一個值確保傳輸的數據有偶個或者奇個邏輯高位。c++

串口地址git

以下表不一樣操做系統的串口地址,Android是基於Linux的因此通常狀況下使用Android系統的設備串口地址爲/dev/ttyS0...github

System Port 1 Port 2
IRIX® /dev/ttyf1 /dev/ttyf2
HP-UX /dev/tty1p0 /dev/tty2p0
Solaris®/SunOS® /dev/ttya /dev/ttyb
Linux® /dev/ttyS0 /dev/ttyS1
Digital UNIX® /dev/tty01 /dev/tty02

Android串口實現

​在Android上使用串口比較快速的方式就是直接套用google官方的串口demo代碼(android-serialport-api),基本上可以應付不少在Android設備使用串口的場景。好比簡單的讀卡號。api

​可是問題來了!數組

​在收發數據頻率很快的狀況下,實際測試這種方式接收數據會有延遲。好比:發送一個命令以後,設備會同時響應兩條命令,一條是結果一條是校驗且兩條命令間隔時間僅1ms,按理兩條命令會幾乎同時收到,可是實際使用該方式會出現10ms的延遲。因此只能着手優化,嘗試使用C/C++的方式進行串口數據的讀寫。

一番查閱下來,使用C/C++實現其實和上面的demo差異不大,一樣是那幾個步驟,設置串口參數,經過調用open方法開啓串口,再進行數據的讀寫操做。出現數據讀取延遲極可能的緣由,就是由於官方demo是經過Java層的文件流(FileInputStream,FileOutputStream)進行讀寫操做引發的。若是有大神懂這塊的能夠說明這種方式致使延遲的緣由。

關於使用C、C++在Android上實現串口通信的源代碼有不少,沒有實際作過C/C++開發,可是也容易看懂。

設置串口波特率、數據位、中止位、校驗位主要操做的就是termios 結構體,對應的頭文件是termios.h。

好比設置波特率代碼:

int SerialPort::setSpeed(int fd, int speed) {
    speed_t b_speed;
    struct termios cfg;
    b_speed = getBaudrate(speed);
    if (tcgetattr(fd, &cfg)) {
        LOGE("tcgetattr invocation method failed!");
        close(fd);
        return FALSE;
    }

    cfmakeraw(&cfg);
    cfsetispeed(&cfg, b_speed);
    cfsetospeed(&cfg, b_speed);

    if (tcsetattr(fd, TCSANOW, &cfg)) {
        LOGE("tcsetattr invocation method failed!");
        close(fd);
        return FALSE;
    }
    return TRUE;
}
複製代碼

打開串口就是簡單的調用open函數,設置相關讀寫參數,這個和官方推薦的demo一致,代碼以下:

int SerialPort::openSerialPort(SerialPortConfig config) {
    LOGD("Open device!");
    isClose = false;
    fd = open(path, O_RDWR);
    if (fd < 0) {
        LOGE("Error to read %s port file!", path);
        return FALSE;
    }

    if (!setSpeed(fd, config.baudrate)) {
        LOGE("Set Speed Error!");
        return FALSE;
    }
    if (!setParity(fd, config.databits, config.stopbits, config.parity)) {
        LOGE("Set Parity Error!");
        return FALSE;
    }
    LOGD("Open Success!");
    return TRUE;
}
複製代碼

串口數據讀取涉及兩個函數 select和read ,函數相關的含義暫且沒去深究,屬於C/C++範湊了,讀取數據代碼以下:

int SerialPort::readData(BYTE *data, int size) {

    int ret, retval;
    fd_set rfds;
    ret = 0;

    if (isClose) return 0;
    for (int i = 0; i < size; i++) {
        data[i] = static_cast<char>(0xFF);
    }
    FD_ZERO(&rfds);     //清空集合
    FD_SET(fd, &rfds);  //把要檢測的句柄fd加入到集合裏
    // TODO Async operation. Thread blocking.
    if (FD_ISSET(fd, &rfds)) {
        FD_ZERO(&rfds);
        FD_SET(fd, &rfds);
        retval = select(fd + 1, &rfds, NULL, NULL, NULL);
        if (retval == -1) {
            LOGE("Select error!");
        } else if (retval) {
            LOGD("This device has data!");
            ret = static_cast<int>(read(fd, data, static_cast<size_t>(size)));
        } else {
            LOGE("Select timeout!");
        }
    }
    if (isClose) close(fd);
    return ret;
}

複製代碼

串口寫數據就是調用write函數了,代碼以下:

int SerialPort::writeData(BYTE *data, int len) {
    int result;
    result = static_cast<int>(write(fd, data, static_cast<size_t>(len)));
    return TRUE;
}
複製代碼

由於不熟悉C/C++,因此就參考網上相關源代碼,依葫蘆畫瓢實現了一個基於C++的Android串口通信庫,並對相關串口控制作了優化,詳細見gayhub,地址:github.com/freyskill/S… ,歡迎star。

經過該庫,完美解決串口數據讀取延遲的問題。

阻塞與非阻塞

​在項目初期使用google官方的串口demo代碼調試設備串口是否能正常通訊的時候,遇到在串口讀數據的線程中會卡死在inputStream.read(buffer);這個時候就讓人疑惑了,不知道問題是出在硬件仍是在串口讀取上,在沒有了解串口相關知識前,但願的場景是讀數據的線程可以不阻塞,一直輪詢讀取數據。

​出現讀取數據線程卡死的狀況是由於在 fd = open(path_utf, O_RDWR | flags); 設置相關參數,讀取默認爲阻塞模式,若在open操做中設置O_NONBLOCK則是非阻塞模式。在阻塞模式中,read沒有讀到數據會阻塞住,直到收到數據;非阻塞模式read沒有讀到數據會返回-1不會阻塞。

修改open方法:

fd = open(path_utf, O_RDWR | flags | O_NONBLOCK | O_NOCTTY | O_NDELAY);
複製代碼

讀取線程就不會再出現卡死了,這個時候仍然接收不到串口設備反饋的數據,就能夠判定是串口設備的問題了。

關於串口文件打開方式,可採用下面的文件打開模式,具體說明以下:

​ O_RDONLY:以只讀方式打開文件

​ O_WRONLY:以只寫方式打開文件

​ O_RDWR:以讀寫方式打開文件

​ O_APPEND:寫入數據時添加到文件末尾

​ O_CREATE:若是文件不存在則產生該文件,使用該標誌須要設置訪問權限位mode_t

​ O_EXCL:指定該標誌,而且指定了O_CREATE標誌,若是打開的文件存在則會產生一個錯誤

​ O_TRUNC:若是文件存在而且成功以寫或者只寫方式打開,則清除文件全部內容,使得文件長度變爲0

​ O_NOCTTY:若是打開的是一個終端設備,這個程序不會成爲對應這個端口的控制終端,若是沒有該標誌,任何一個輸入,例如鍵盤停止信號等,都將影響進程。

​ O_NONBLOCK:該標誌與早期使用的O_NDELAY標誌做用差很少。程序不關心DCD信號線的狀態,若是指定該標誌,進程將一直在休眠狀態,直到DCD信號線爲0。

實際應用中,都會選擇阻塞模式,這樣更節省資源。可是若是但願在一個線程中同時進行讀寫操做,沒數據反饋時,線程就會阻塞等待,就沒法進行寫數據了。

串口數據校驗方式

通常狀況下串口通信協議都會在數據幀或者說命令格式裏定義一個校驗方式,經常使用的有異或校驗、和校驗、CRC校驗LRC校驗

**注意:**這裏說的校驗和上面說的校驗位是不一樣的,校驗位針對的是單個字節,校驗類型針對的是單個數據幀。

校驗方式通常放在命令最後,能夠是一個byte,也能夠是兩個byte或者其餘,具體看協議設計。

好比命令格式以下,採用和校驗:

addr command data_length data1 data2 datan checksum
0x01 0x52 0x05 0x11 0xBA ... 8E

其中,獲取校驗碼(checksum)就是將命令中的數據進行相加生成,Checksum=256-(data1+data2+datan)算出校驗碼爲:8E。具體計算方式就是經過將十六進制進行相加算出校驗碼的十進制字符,詳細代碼以下:

/** * 獲取校驗碼(計算方式以下:cs= 256-(data1+data2+data3+data4+datan)) */
public static String getCheckSum(String data){
    Integer in = Integer.valueOf(makeChecksum(data),16);
    String st = Integer.toHexString(256 -in).toUpperCase();
    st = String.format("%2s",st);
    return st.replaceAll(" ","0");
}
複製代碼

十六進制進行相加代碼:

/** * 生成校驗碼,十六進制相加 * @param data * @return */
public static String makeChecksum(String data) {
    if (data == null || data.equals("")) {
        return "00";
    }
    int iTotal = 0;
    int iLen = data.length();
    int iNum = 0;

    while (iNum < iLen){
        String s = data.substring(iNum, iNum + 2);
        System.out.println(s);
        iTotal += Integer.parseInt(s, 16);
        iNum = iNum + 2;
    }

    /** * 用256求餘最大是255,即16進制的FF */
    int iMod = iTotal % 256;
    String sHex = Integer.toHexString(iMod);
    iLen = sHex.length();
    //若是不夠校驗位的長度,補0,這裏用的是兩位校驗
    if (iLen < 2){
        sHex = "0" + sHex;
    }
    return sHex;
}
複製代碼

再好比使用CRC校驗(有CRC8,CRC16,CRC32),關於CRC校驗的原理能夠參考:blog.csdn.net/u011854789/…

/** * 獲取CRC檢驗 * @param command 命令集 * @param len 命令長度 * @return */
public static int CalCrc(byte[] command,int len){
    long MSBInfo;
    int i,j ;
    int nCRCData;
    nCRCData = 0xffff;
    for(i = 0; i < len ;i++) {
        int temp = (int)(command[i]&0xff);
        nCRCData = nCRCData ^ temp ;
        for(j= 0 ; j < 8 ;j ++){
            MSBInfo = nCRCData & 0x0001;
            nCRCData = nCRCData  >> 1;
            if(MSBInfo != 0 )
                nCRCData = nCRCData ^ 0xa001;
        }
    }
    return nCRCData;
}
複製代碼

串口設備問題排查

​在對接串口設備的過程當中,負責硬件的同事說在PC上經過串口助手收發數據沒有問題,然鵝我在Android設備上,經過串口就是沒法接收到數據,因而乎雙方僵持,對方就差說:「若是我硬件有問題我吃xiang...」 堅稱是Android板子串口問題或者是我讀寫數據的代碼有問題。在沒有示波器的狀況下,如何定位問題呢?各方打聽嘗試了以下方式:

  1. 直接短路Tx 與 Rx 兩條線

    不接設備,先肯定Android設備(開發板)上的串口是否可通,檢查方式:直接短路板子上的Tx和Rx兩個針腳,而後經過Android的串口demo或者相關串口助手進行命令發送,看串口是否可以接收響應。也就是檢查板子串口是否能夠自發自收。

  2. 直接與PC對接

    操做方式是將Android板子上的串口經過USB轉接頭直接插入PC,而後在PC和Android設備上同時打開串口助手,波特率等參數保持一致。對接以後打開串口,PC發命令看Android端是否能接收到,反之Android端發看PC端是否能接收到。

​在嘗試了上面方法以後,發現Android端的串口是通的,那緣由就只能出在要使用串口的設備(無線通信模塊)上了,又是一段時間僵持以後,我說這東西是否是要接電才行?結果一試,果真是沒有接電的緣由,崩潰。爲何PC上不須要接電能通,然道是由於USB已經帶電?不得而知。

以上,只是提供一種在沒有示波器狀況下,檢查串口是否正常的方式,僅作參考。

數據轉換工具類

串口開發中比較常見進制與進制,進制與字節間的轉換,好比:十六進制轉十進制,字節數組轉十六進制字符串等。

相關代碼以下:

package top.keepempty.serialdemo;
/** * 數據轉換工具類 * @author frey */
public class DataConversion {

    /** * 判斷奇數或偶數,位運算,最後一位是1則爲奇數,爲0是偶數 * @param num * @return */
    public static int isOdd(int num) {
        return num & 0x1;
    }

    /** * 將int轉成byte * @param number * @return */
    public static byte intToByte(int number){
        return hexToByte(intToHex(number));
    }

    /** * 將int轉成hex字符串 * @param number * @return */
    public static String intToHex(int number){
        String st = Integer.toHexString(number).toUpperCase();
        return String.format("%2s",st).replaceAll(" ","0");
    }

    /** * 字節轉十進制 * @param b * @return */
    public static int byteToDec(byte b){
        String s = byteToHex(b);
        return (int) hexToDec(s);
    }

    /** * 字節數組轉十進制 * @param bytes * @return */
    public static int bytesToDec(byte[] bytes){
        String s = encodeHexString(bytes);
        return (int)  hexToDec(s);
    }

    /** * Hex字符串轉int * * @param inHex * @return */
    public static int hexToInt(String inHex) {
        return Integer.parseInt(inHex, 16);
    }

    /** * 字節轉十六進制字符串 * @param num * @return */
    public static String byteToHex(byte num) {
        char[] hexDigits = new char[2];
        hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
        hexDigits[1] = Character.forDigit((num & 0xF), 16);
        return new String(hexDigits).toUpperCase();
    }

    /** * 十六進制轉byte字節 * @param hexString * @return */
    public static byte hexToByte(String hexString) {
        int firstDigit = toDigit(hexString.charAt(0));
        int secondDigit = toDigit(hexString.charAt(1));
        return (byte) ((firstDigit << 4) + secondDigit);
    }

    private static int toDigit(char hexChar) {
        int digit = Character.digit(hexChar, 16);
        if(digit == -1) {
            throw new IllegalArgumentException(
                    "Invalid Hexadecimal Character: "+ hexChar);
        }
        return digit;
    }

    /** * 字節數組轉十六進制 * @param byteArray * @return */
    public static String encodeHexString(byte[] byteArray) {
        StringBuffer hexStringBuffer = new StringBuffer();
        for (int i = 0; i < byteArray.length; i++) {
            hexStringBuffer.append(byteToHex(byteArray[i]));
        }
        return hexStringBuffer.toString().toUpperCase();
    }

    /** * 十六進制轉字節數組 * @param hexString * @return */
    public static byte[] decodeHexString(String hexString) {
        if (hexString.length() % 2 == 1) {
            throw new IllegalArgumentException(
                    "Invalid hexadecimal String supplied.");
        }
        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < hexString.length(); i += 2) {
            bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
        }
        return bytes;
    }

    /** * 十進制轉十六進制 * @param dec * @return */
    public static String decToHex(int dec){
        String hex = Integer.toHexString(dec);
        if (hex.length() == 1) {
            hex = '0' + hex;
        }
        return hex.toLowerCase();
    }

    /** * 十六進制轉十進制 * @param hex * @return */
    public static long hexToDec(String hex){
        return Long.parseLong(hex, 16);
    }

    /** * 十六進制轉十進制,並對卡號補位 */
    public static String setCardNum(String cardNun){
        String cardNo1= cardNun;
        String cardNo=null;
        if(cardNo1!=null){
            Long cardNo2=Long.parseLong(cardNo1,16);
            //cardNo=String.format("%015d", cardNo2);
            cardNo = String.valueOf(cardNo2);
        }
        return cardNo;
    }
}

複製代碼

其餘

串口中相關引腳說明以下表,通常在開發板子上能夠看到Tx,Rx這兩個針腳,分別標識串口的發送和接收。

序號 信號名稱 符號 流向 功能
2 發送數據 TXD DTE→DCE DTE 發送串行數據
3 接收數據 RXD DTE←DCE DTE 接收串行數據
4 請求發送 RTS DTE→DCE DTE 請求 DCE 將線路切換到發送方式
5 容許發送 CTS DTE←DCE DCE 告訴 DTE 線路已接通能夠發送數據
6 數據設備準備好 DSR DTE←DCE DCE 準備好
7 信號地 信號公共地
8 載波檢測 DCD DTE←DCE 表示 DCE 接收到遠程載波
20 數據終端準備好 DTR DTE→DCE DTE 準備好
22 振鈴指示 RI DTE←DCE 表示 DCE 與線路接通,出現振鈴

關於串口更多的相關知識能夠參考這篇文章

參考:

www.cnblogs.com/hackfun/p/7…

blog.csdn.net/tianruxishu…

以上,只是我的學習整理,歡迎學習交流,若有披露歡迎指出,大神略過。

相關文章
相關標籤/搜索