JAVA串口編程

序言
說到開源,恐怕很少有人不挑大指稱讚。學生通過開源代碼學到了知識,程序員通過開源類庫獲得了別人的成功經驗及能夠按時完成手頭的工程,商家通過開源軟件賺到了錢……,總之是皆大歡喜。然而開源軟件或類庫的首要缺點就是大多缺乏詳細的說明文檔和使用的例子,或者就是軟件代碼隨便你用,就是文檔,例子和後期服務收錢。這也難怪,畢竟就像某個著名NBA球員說的那樣:“我還要養家,所以千萬美元以下的合同別找我談,否則我寧可待業”。是啊,支持開源的人也要養家,收點錢也不過分。要想既不花錢又學到知識就只能藉助網絡和了,我只是想拋磚引玉,爲開源事業做出點微薄共獻,能爲你的工程解決哪怕一個小問題,也就足夠了。
雖然我的這個系列介紹的東西不是什麼Web框架,也不是什麼開源服務器,但是我相信,作爲一個程序員,什麼樣的問題都會遇到。有時候越是簡單的問題反而越棘手;越是小的地方就越是找不到稱手的傢伙。只要你不是整天只與“架構”、“構件”、“框架”打交道的話,相信我所說的東西你一定會用到。


嵌入式系統或傳感器網絡的很多應用和測試都需要通過PC機與嵌入式設備或傳感器節點進行通信。其中,最常用的接口就是RS-232串口和並口(鑑於USB接口的複雜性以及不需要很大的數據傳輸量,USB接口用在這裏還是顯得過於奢侈,況且目前除了SUN有一個支持USB的包之外,我還沒有看到其他直接支持USB的Java類庫)。SUN的CommAPI分別提供了對常用的RS232串行端口和IEEE1284並行端口通訊的支持。RS-232-C(又稱EIA RS-232-C,以下簡稱RS232)是在1970年由美國電子工業協會(EIA)聯合貝爾系統、調制解調器廠家及計算機終端生產廠家共同制定的用於串行通訊的標準。RS232是一個全雙工的通訊協議,它可以同時進行數據接收和發送的工作。
目前,常見的Java串口包有SUN在1998年發佈的串口通信API:comm2.0.jar(Windows下)、comm3.0.jar(Linux/Solaris);IBM的串口通信API以及一個開源的實現。鑑於在Windows下SUN的API比較常用以及IBM的實現和SUN的在API層面都是一樣的,那個開源的實現又不像兩家大廠的產品那樣讓人放心,這裏就只介紹SUN的串口通信API在Windows平臺下的使用。
到SUN的網站下載javacomm20-win32.zip,包含的東西如下所示:
按照其使用說明(Readme.html)的說法,要想使用串口包進行串口通信,除了設置好環境變量之外,還要將win32com.dll複製到<JDK>/bin目錄下;將comm.jar複製到<JDK>/lib;把javax.comm.properties也同樣拷貝到<JDK>/lib目錄下。然而在真正運行使用串口包的時候,僅作這些是不夠的。因爲通常當運行“java MyApp”的時候,是由JRE下的虛擬機啓動MyApp的。而我們只複製上述文件到JDK相應目錄下,所以應用程序將會提示找不到串口。解決這個問題的方法很簡單,我們只須將上面提到的文件放到JRE相應的目錄下就可以了。
值得注意的是,在網絡應用程序中使用串口API的時候,還會遇到其他更復雜問題。有興趣的話,你可以查看CSDN社區中“關於網頁上Appletjavacomm20讀取客戶端串口的問題”的帖子。
這是用於描述一個被底層系統支持的端口的抽象類。它包含一些高層的IO控制方法,這些方法對於所有不同的通訊端口來說是通用的。SerialPort 和ParallelPort都是它的子類,前者用於控制串行端口而後者用於控這並口,二者對於各自底層的物理端口都有不同的控制方法。這裏我們只關心SerialPort。
這個類主要用於對串口進行管理和設置,是對串口進行訪問控制的核心類。主要包括以下方法
l         確定是否有可用的通信端口
l         爲IO操作打開通信端口
l         決定端口的所有權
l         處理端口所有權的爭用
l         管理端口所有權變化引發的事件(Event)
這個類用於描述一個RS-232串行通信端口的底層接口,它定義了串口通信所需的最小功能集。通過它,用戶可以直接對串口進行讀、寫及設置工作。
大段的文字怎麼也不如一個小例子來的清晰,下面我們就一起看一下串口包自帶的例子---SerialDemo中的一小段代碼來加深對串口API核心類的使用方法的認識。
void listPortChoices() {
            CommPortIdentifier portId;
            Enumeration en = CommPortIdentifier.getPortIdentifiers();
            // iterate through the ports.
            while (en.hasMoreElements()) {
                portId = (CommPortIdentifier) en.nextElement();
                if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {
                    System.out.println(portId.getName());
                }
            }
            portChoice.select(parameters.getPortName());
        }
以上代碼可以列舉出當前系統所有可用的串口名稱,我的機器上輸出的結果是COM1和COM3。
串口一般有如下參數可以在該串口打開以前配置進行配置:
包括波特率,輸入/輸出流控制,數據位數,停止位和齊偶校驗。
SerialPort sPort;
try {
            sPort.setSerialPortParams(BaudRate,Databits,Stopbits,Parity);
                     //設置輸入/輸出控制流
                     sPort.setFlowControlMode(FlowControlIn | FlowControlOut);
        } catch (UnsupportedCommOperationException e) {}
對串口讀寫之前需要先打開一個串口:
CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier(PortName);
try {
            SerialPort  sPort = (SerialPort) portId.open("串口所有者名稱", 超時等待時間);
        } catch (PortInUseException e) {//如果端口被佔用就拋出這個異常
            throw new SerialConnectionException(e.getMessage());
        }
//用於對串口寫數據
OutputStream os = new BufferedOutputStream(sPort.getOutputStream());
os.write(int data);
//用於從串口讀數據
InputStream is = new BufferedInputStream(sPort.getInputStream());
int receivedData = is.read();
讀出來的是int型,你可以把它轉換成需要的其他類型。
這裏要注意的是,由於Java語言沒有無符號類型,即所有的類型都是帶符號的,在由byte到int的時候應該尤其注意。因爲如果byte的最高位是1,則轉成int類型時將用1來佔位。這樣,原本是10000000的byte類型的數變成int型就成了1111111110000000,這是很嚴重的問題,應該注意避免。
終於嘮叨完我最討厭的基礎知識了,下面開始我們本次的重點--串口應用的研究。由於向串口寫數據很簡單,所以這裏我們只關注於從串口讀數據的情況。通常,串口通信應用程序有兩種模式,一種是實現SerialPortEventListener接口,監聽各種串口事件並作相應處理;另一種就是建立一個獨立的接收線程專門負責數據的接收。由於這兩種方法在某些情況下存在很嚴重的問題(至於什麼問題這裏先賣個關子J),所以我的實現是採用第三種方法來解決這個問題。
現在我們來看看事件監聽模型是如何運作的
l        首先需要在你的端口控制類(例如SManager)加上“implements SerialPortEventListener”
l        在初始化時加入如下代碼:
try {
            SerialPort sPort.addEventListener(SManager);
        } catch (TooManyListenersException e) {
            sPort.close();
            throw new SerialConnectionException("too many listeners added");
        }
        sPort.notifyOnDataAvailable(true);
l        覆寫public void serialEvent(SerialPortEvent e)方法,在其中對如下事件進行判斷:
BI -通訊中斷.
  CD -載波檢測.
  CTS -清除發送.
  DATA_AVAILABLE -有數據到達.
  DSR -數據設備準備好.
  FE -幀錯誤.
  OE -溢位錯誤.
  OUTPUT_BUFFER_EMPTY -輸出緩衝區已清空.
  PE -奇偶校驗錯.
RI - 振鈴指示.
一般最常用的就是DATA_AVAILABLE--串口有數據到達事件。也就是說當串口有數據到達時,你可以在serialEvent中接收並處理所收到的數據。然而在我的實踐中,遇到了一個十分嚴重的問題。
首先描述一下我的實驗:我的應用程序需要接收傳感器節點從串口發回的查詢數據,並將結果以圖標的形式顯示出來。串口設定的波特率是115200,川口每隔128毫秒返回一組數據(大約是30字節左右),週期(即持續時間)爲31秒。實測的時候在一個週期內應該返回4900多個字節,而用事件監聽模型我最多隻能收到不到1500字節,不知道這些字節都跑哪裏去了,也不清楚到底丟失的是那部分數據。值得注意的是,這是我將serialEvent()中所有處理代碼都注掉,只剩下打印代碼所得的結果。數據丟失的如此嚴重是我所不能忍受的,於是我決定採用其他方法。
這個模型顧名思義,就是將接收數據的操作寫成一個線程的形式:
public void startReadingDataThread() {
        Thread readDataProcess = new Thread(new Runnable() {
            public void run() {
                            while (newData != -1) {
                    try {
                                          newData = is.read();
                        System.out.println(newData);
                                          //其他的處理過程
                                          ……….
                                   } catch (IOException ex) {
                        System.err.println(ex);
                        return;
                    }
                     }
              readDataProcess.start();
}
在我的應用程序中,我將收到的數據打包放到一個緩存中,然後啓動另一個線程從緩存中獲取並處理數據。兩個線程以生產者—消費者模式協同工作,數據的流向如下圖所示:
 

這樣,我就圓滿解決了丟數據問題。然而,沒高興多久我就又發現了一個同樣嚴重的問題:雖然這回不再丟數據了,可是原本一個週期(31秒)之後,傳感器節電已經停止傳送數據了,但我的串口線程依然在努力的執行讀串口操作,在控制檯也可以看見收到的數據仍在不斷的打印。原來,由於傳感器節點發送的數據過快,而我的接收線程處理不過來,所以InputStream就先把已到達卻還沒處理的字節緩存起來,於是就導致了明明傳感器節點已經不再發數據了,而控制檯卻還能看見數據不斷打印這一奇怪的現象。唯一值得慶幸的是最後收到數據確實是4900左右字節,沒出現丟失現象。然而當處理完最後一個數據的時候已經快1分半鐘了,這個時間遠遠大於節點運行週期。這一延遲對於一個實時的顯示系統來說簡直是災難!
後來我想,是不是由於兩個線程之間的同步和通信導致了數據接收緩慢呢?於是我在接收線程的代碼中去掉了所有處理代碼,僅保留打印收到數據的語句,結果依然如故。看來並不是線程間的通信阻礙了數據的接收速度,而是用線程模型導致了對於發送端數據發送速率過快的情況下的數據接收延遲。這裏申明一點,就是對於數據發送速率不是如此快的情況下前面者兩種模型應該還是好用的,只是特殊情況還是應該特殊處理。
痛苦了許久(Boss天天催我L)之後,偶然的機會,我聽說TinyOS中(又是開源的)有一部分是和我的應用程序類似的串口通信部分,於是我下載了它的1.x版的Java代碼部分,參考了它的處理方法。解決問題的方法說穿了其實很簡單,就是從根源入手。根源不就是接收線程導致的嗎,那好,我就乾脆取消接收線程和作爲中介的共享緩存,而直接在處理線程中調用串口讀數據的方法來解決問題(什麼,爲什麼不把處理線程也一併取消?----都取消應用程序界面不就鎖死了嗎?所以必須保留)於是程序變成了這樣:
public byte[] getPack(){
       while (true) {
                       // PacketLength爲數據包長度
                    byte[] msgPack = new byte[PacketLength];
                    for(int i = 0; i < PacketLength; i++){
                        if( (newData = is.read()) != -1){
                            msgPack[i] = (byte) newData;
                            System.out.println(msgPack[i]);
                        }
                    }
                    return msgPack;
                            }
}
在處理線程中調用這個方法返回所需要的數據序列並處理之,這樣不但沒有丟失數據的現象行出現,也沒有數據接收延遲了。這裏唯一需要注意的就是當串口停止發送數據或沒有數據的時候is.read()一直都返回-1,如果一旦在開始接收數據的時候發現-1就不要理它,繼續接收,直到收到真正的數據爲止。

本文介紹了串口通信的基本知識,以及常用的幾種模式。通過實踐,提出了一些問題,並在最後加以解決。值得注意的是對於第一種方法,我曾將傳感器發送的時間由128毫秒增加到512毫秒,仍然有很嚴重的數據丟失現象發生,所以如果你的應用程序需要很精密的結果,傳輸數據的速率又很快的話,就最好不要用第一種方法。對於第二種方法,由於是線程導致的問題,所以對於不同的機器應該會有不同的表現,對於那些處理多線程比較好的機器來說,應該會好一些。但是我的機器是Inter 奔四3.0雙核CPU+512DDR內存,這樣都延遲這麼厲害,還得多強的CPU才行啊?所以對於數據量比較大的傳輸來說,還是用第三種方法吧。不過這個世界問題是很多的,而且未知的問題比已知的問題多的多,說不定還有什麼其他問題存在,歡迎你通過下面的聯繫方式和我一起研究 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章