1. 程式人生 > >我的Android NDK之旅(四),android串列埠通訊-mac+串列埠除錯工具

我的Android NDK之旅(四),android串列埠通訊-mac+串列埠除錯工具

一些關於串列埠的知識

什麼是串列埠

串列埠是計算機上一種非常通用裝置通訊的協議,不要與通用序列匯流排Universal Serial Bus(USB)混淆。大多數計算機包含兩個基於RS232的串列埠。串列埠同時也是儀器儀表裝置通用的通訊協議;很多GPIB相容的裝置也帶有RS-232口。同時,串列埠通訊協議也可以用於獲取遠端採集裝置的資料。

串列埠通訊的概念非常簡單,串列埠按位(bit)傳送和接收位元組。儘管比按位元組(byte)的並行通訊慢,但是串列埠可以在使用一根線傳送資料的同時用另一根線接收資料。它很簡單並且能夠實現遠距離通訊。比如IEEE488定義並行通行狀態時,規定裝置線總常不得超過20米,並且任意兩個裝置間的長度不得超過2米;而對於串列埠而言,長度可達1200米。

典型地,串列埠用於ASCII碼字元的傳輸。通訊使用3根線完成:(1)地線,(2)傳送,(3)接收。由於串列埠通訊是非同步的,埠能夠在一根線上傳送資料同時在另一根線上接收資料。其他線用於握手,但是不是必須的。串列埠通訊最重要的引數是波特率、資料位、停止位和奇偶校驗。對於兩個進行通行的埠,這些引數必須匹配:
a. 波特率:這是一個衡量通訊速度的引數。它表示每秒鐘傳送的bit的個數。例如300波特表示每秒鐘傳送300個bit。當我們提到時鐘週期時,我們就是指波特率。例如如果協議需要4800波特率,那麼時鐘是4800Hz。這意味著串列埠通訊在資料線上的取樣率為4800Hz。通常電話線的波特率為14400,28800和36600。波特率可以遠遠大於這些值,但是波特率和距離成反比。高波特率常常用於放置的很近的儀器間的通訊,典型的例子就是GPIB裝置的通訊。
b. 資料位:這是衡量通訊中實際資料位的引數。當計算機發送一個資訊包,實際的資料不會是8位的,標準的值是5、7和8位。如何設定取決於你想傳送的資訊。比如,標準的ASCII碼是0~127(7位)。擴充套件的ASCII碼是0~255(8位)。如果資料使用簡單的文字(標準 ASCII碼),那麼每個資料包使用7位資料。每個包是指一個位元組,包括開始/停止位,資料位和奇偶校驗位。由於實際資料位取決於通訊協議的選取,術語“包”指任何通訊的情況。
c. 停止位:用於表示單個包的最後一位。典型的值為1,1.5和2位。由於資料是在傳輸線上定時的,並且每一個裝置有其自己的時鐘,很可能在通訊中兩臺裝置間出現了小小的不同步。因此停止位不僅僅是表示傳輸的結束,並且提供計算機校正時鐘同步的機會。適用於停止位的位數越多,不同時鐘同步的容忍程度越大,但是資料傳輸率同時也越慢。
d. 奇偶校驗位:在串列埠通訊中一種簡單的檢錯方式。有四種檢錯方式:偶、奇、高和低。當然沒有校驗位也是可以的。對於偶和奇校驗的情況,串列埠會設定校驗位(資料位後面的一位),用一個值確保傳輸的資料有偶個或者奇個邏輯高位。例如,如果資料是011,那麼對於偶校驗,校驗位為0,保證邏輯高的位數是偶數個。如果是奇校驗,校驗位位1,這樣就有3個邏輯高位。高位和低位不真正的檢查資料,簡單置位邏輯高或者邏輯低校驗。這樣使得接收裝置能夠知道一個位的狀態,有機會判斷是否有噪聲干擾了通訊或者是否傳輸和接收資料是否不同步。

/dev 目錄下面的檔案是什麼

dev是裝置(device)的英文縮寫。/dev這個目錄對所有的使用者都十分重要。因為在這個目錄中包含了所有linux(macOS也類似)系統中使用的外部裝置。但是這裡並不是放的外部裝置的驅動程式,這一點和windows,dos作業系統不一樣。它實際上是一個訪問這些外部裝置的埠。我們可以非常方便地去訪問這些外部裝置,和訪問一個檔案,一個目錄沒有任何區別。比如說我們的串列埠檔案ttyS0就在這個目錄下面詳細請點選

串列埠終端(/dev/ttySn)

串列埠終端(Serial Port Terminal)是使用計算機串列埠連線的終端裝置。計算機把每個串列埠都看作是一個字元裝置。有段時間這些串列埠裝置通常被稱為終端裝置,因為 那時它的最大用途就是用來連線終端。這些串列埠所對應的裝置名稱是/dev/tts/0(或/dev/ttyS0), /dev/tts/1(或/dev/ttyS1)等,裝置號分別是(4,0), (4,1)等,分別對應於DOS系統下的COM1、COM2等。若要向一個埠傳送資料,可以在命令列上把標準輸出重定向到這些特殊檔名上即可。例如, 在命令列提示符下鍵入:echo test > /dev/ttyS1會把單詞”test”傳送到連線在ttyS1(COM2)埠的裝置上。所以說,我們的串列埠通訊實際上也就是往這些檔案中寫入資料或者是接收資料。

什麼是android串列埠通訊

簡單來說,就是在java層呼叫c或者是c++層的程式碼,來操控底層的串列埠檔案,比如說ttyS0。因為串列埠通訊主要是對串列埠檔案的讀寫操作

什麼是FileDescriptor

檔案描述符,檔案描述符類的例項作為一個不透明控制代碼到底層機器特定的結構表示一個開啟的檔案,一個開放的插座,或位元組的另一個源或宿。對於一個檔案描述符的主要實際用途是建立一個 FileInputStream或FileOutputStream遏制它。上面說了,android串列埠通訊就是操作串列埠檔案,但是串列埠檔案不能直接操作,是要通過FileDescriptor這個物件來操作串列埠檔案。android上的串列埠通訊就是對這個物件進行操作。

串列埠通訊示例

我的裝置 mac + android開發板 + RS232usb轉串列埠的線一根

這裡寫圖片描述

首先,得下載usb轉串列埠驅動下載和安裝驅動請參考這篇部落格(只需要參考安裝驅動即可,不用管後面的安裝SecureCRT串列埠除錯工具),安裝成功會在/dev下看到:

這裡寫圖片描述

然後下載串列埠除錯工具,這個是用來與android開發板進行通訊的,下載請輕參考這篇部落格

串列埠除錯工具用法很簡單,首先點選Options來設定引數

這裡寫圖片描述

接著,如果我們之前usb轉串列埠驅動安裝成功了的話,這裡會顯示有usbserial選項,選擇這個,點選ok,再點選Connect,正常情況下會顯示Connected,也就是連線成功,如果沒有連線成功,那肯定是某些操作沒有做好。

這裡寫圖片描述

連線成功後就可以向android開發板傳送資料了,不過現在我們還沒有完成android的程式碼,所以傳送了,android端也接收不到。

這裡寫圖片描述

這裡寫圖片描述

接下來就是android端程式碼的編寫了,下面是最核心的程式碼。具體的程式碼點選下載

SerialPort.c

這個c檔案是整個串列埠通訊的關鍵,主要是用來開啟串列埠和關閉串列埠,在百度上面搜尋andoid串列埠通訊可以發現基本上所有的示例都是用的這個檔案或者說是程式碼,我也是用的這個,只不過稍微修改了點或者說是簡化了點:

#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <jni.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)

/**
 * 獲取相應波特率
 * @param baudrate 波特率
 * @return
 */
static speed_t getBaudrate(jint baudrate) {
    switch (baudrate) {
        case 9600:
            return B9600;
        case 19200:
            return B19200;
        case 38400:
            return B38400;
        case 115200:
            return B115200;
        default:
            return -1;
    }
}

/*
 * Class:     android_serialport_SerialPort
 * Method:    開啟串列埠,主要就是返回FileDescriptor這個物件,供java層呼叫。
 * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
 */
JNIEXPORT jobject JNICALL Java_com_chenxin_testserialport_utils_SerialPort_open
        (JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags) {
    int fd;
    speed_t speed;
    jobject mFileDescriptor;

    /* 檢查引數 */
    {
        speed = getBaudrate(baudrate);
        if (speed == -1) {
            /* TODO: throw an exception */
            LOGE("Invalid baudrate");
            return NULL;
        }
    }

    /* 開啟裝置 */
    {
        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 );//加上 O_NDELAY 引數,讀取資料是非同步的,不是阻塞的。目前沒有加上此引數,所以說讀取資料是阻塞的方式,具體加不加視情況而定。
        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;
        }

    }

    /* 設定引數 */
    {
        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;
        }
    }

    /* 建立一個 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:    關閉串列埠
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_chenxin_testserialport_utils_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);
}

可以看到,這個段c層的程式碼只有兩個方法,一個是開啟串列埠,一個是關閉串列埠。開啟串列埠就是開啟的串列埠檔案,比如說ttyS0,開啟之後返回FileDescriptor這個物件。接下來就很簡單了,只需要在java層中,宣告這個native層的方法,然後呼叫,獲取到FileDescriptor物件,最後只需要在java層操作FileDescriptor這個物件就可以實現對串列埠的資料讀寫操作了。如果串列埠成功的開啟後,我們就可以用前面提到的串列埠除錯工具來發送資料或者是接收資料,實現mac對anroid開發板的資料通訊。如果對android ndk這塊還不是太瞭解的可以看看我前面的幾篇博文
我的Android NDK之旅(三),使用cmake來構建jni
我的Android NDK之旅(二),使用ndk-build構建Jni
我的Android NDK之旅(一),不使用ndk-build命令來建立jni