原作者:賽迪網作者 shihuchen ,我在他的基礎上進行了部分修改
【賽迪網訊】串口, RS-232-C(又稱EIA RS-232-C,以下簡稱RS232)是在1970年由美國電子工業協會(EIA)聯合貝爾系統、調制解調器廠家及計算機終端生產廠家共同制定的用於串行通訊的標准。RS232是一個全雙工的通訊協議,它可以同時進行數據接收和發送的工作。串口是計算機上一種非常通用設備通信的協議。以前,大多數計算機包含兩個基於RS232的串口。串口同時也是儀器儀表設備通用的通信協議;很多GPIB兼容的設備也帶有RS-232口。同時,串口通信協議也可以用於獲取遠程采集的數據。
串口通信的概念非常簡單,串口按位(bit)發送和接收字節。盡管比按字節(byte)的並行通信慢,但是串口可以在使用一根線發送數據的同時用另一根線接收數據。它很簡單並且能夠實現遠距離通信。比如IEEE488定義並行通行狀態時,規定設備線總常不得超過20米,並且任意兩個設備間的長度不得超過2米;而對於串口而言,長度可達1200米。
說到串口,也許很多人都要懷疑,這都什么年代了,USB設備已經如此普及了,誰還能想起這個老掉牙的通信端口呀?其實,在現實生活中,串口正因為它老,才會在實際使用中經常用到它。舉個簡單的例子:工業制造及其設備與計算機之間的通訊、還有在各種電子工廠測試設備的過程中,都無一例外使用的串口。因為它可以不用像USB設備一樣必須有軟件驅動的支持才可以進行工作。
Java對串口通信的支持
常見的Java串口包
SUN的CommAPI分別提供了對常用的RS232串行端口和IEEE1284並行端口通訊的支持。目前,常見的Java串口包有SUN在1998年發布的串口通信API:comm2.0.jar(Windows下)、comm3.0.jar(Linux/Solaris);IBM的串口通信API以及一個開源的實現。鑒於在Windows下SUN的API比較常用以及IBM的實現和SUN的在API層面都是一樣的,那個開源的實現又不像兩家大廠的產品那樣讓人放心,這里就只介紹SUN的串口通信API在Windows平台下的使用。
串口包的安裝(Windows下)
到SUN的網站下載javacomm20-win32.zip,包含的東西如下所示:
需要進行以下設置(要不然串口不通):
<jdk> refers to the root directory of your JDK installation. If you installed JDK in c:\jdk1.2 then replace all reference to <jdk> with c:\jdk1.2.
1.place the win32com.dll in <jdk>\jre\bin directory.
2.Place the comm.jar in <jdk>\jre\lib\ext.
3.Place the javax.comm.properties in <jdk>\jre\lib .
4.Do not alter the CLASSPATH.
串口API介紹
javax.comm.CommPort
這是用於描述一個被底層系統支持的端口的抽象類。它包含一些高層的IO控制方法,這些方法對於所有不同的通訊端口來說是通用的。SerialPort 和ParallelPort都是它的子類,前者用於控制串行端口而后者用於控這並口,二者對於各自底層的物理端口都有不同的控制方法。這里我們只關心SerialPort。
javax.comm.CommPortIdentifier
這個類主要用於對串口進行管理和設置,是對串口進行訪問控制的核心類。主要包括以下方法
確定是否有可用的通信端口
為IO操作打開通信端口
決定端口的所有權
處理端口所有權的爭用
管理端口所有權變化引發的事件(Event)
javax.comm.SerialPort
這個類用於描述一個RS-232串行通信端口的底層接口,它定義了串口通信所需的最小功能集。通過它,用戶可以直接對串口進行讀、寫及設置工作。
串口API實例
壓縮包中除了api,還包括了幾個小例子,下面我們就一起看一下串口包自帶的例子---SerialDemo中的一小段代碼來加深對串口API核心類的使用方法的認識。
列舉出本機所有可用串口
1
2
3
4
5
6
7
8
9
10
11
12
|
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。
串口參數的配置
串口一般有如下參數可以在該串口打開以前配置進行配置:
包括波特率,輸入/輸出流控制,數據位數,停止位和奇偶校驗。
1
2
3
4
5
6
|
SerialPort sPort;
try
{
sPort.setSerialPortParams(BaudRate,Databits,Stopbits,Parity);
//設置輸入/輸出控制流
sPort.setFlowControlMode(FlowControlIn | FlowControlOut);
}
catch
(UnsupportedCommOperationException e) {}
|
串口的讀寫
對串口讀寫之前需要先打開一個串口:
1
2
3
4
5
6
7
8
9
10
11
12
|
CommPortIdentifier portId =CommPortIdentifier.getPortIdentifier(PortName);
try
{
SerialPort sPort = (SerialPort) portId.open(
"串口所有者名稱"
, 超時等待時間);
}
catch
(PortInUseException e) {
//如果端口被占用就拋出這個異常
throw
new
SerialConnectionException(e.getMessage());
}
//用於對串口寫數據
OutputStream os = newBufferedOutputStream(sPort.getOutputStream());
os.write(
int
data);
//用於從串口讀數據
InputStream is = newBufferedInputStream(sPort.getInputStream());
int
receivedData = is.read();
|
讀出來的是int型,你可以把它轉換成需要的其他類型。
這里要注意的是,由於Java語言沒有無符號類型,即所有的類型都是帶符號的,在由byte到int的時候應該尤其注意。因為如果byte的最高位是1,則轉成int類型時將用1來占位。這樣,原本是10000000的byte類型的數變成int型就成了1111111110000000,這是很嚴重的問題,應該注意避免。
串口通信的通用模式及其問題
下面開始我們本次的重點--串口應用的研究。由於向串口寫數據很簡單,所以這里我們只關注於從串口讀數據的情況。通常,串口通信應用程序有兩種模式,一種是實現SerialPortEventListener接口,監聽各種串口事件並作相應處理;另一種就是建立一個獨立的接收線程專門負責數據的接收。由於這兩種方法在某些情況下存在很嚴重的問題,所以我的實現是采用第三種方法來解決這個問題。
事件監聽模型
現在我們來看看事件監聽模型是如何運作的:
首先需要在你的端口控制類(例如SManager)加上“implementsSerialPortEventListener”
在初始化時加入如下代碼:
1
2
3
4
5
6
7
|
try
{
SerialPort sPort.addEventListener(SManager);
}
catch
(TooManyListenersException e) {
sPort.close();
throw
new
SerialConnectionException(
"too many listenersadded"
);
}
sPort.notifyOnDataAvailable(
true
);
|
覆寫publicvoid 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()中所有處理代碼都注掉,只剩下打印代碼所得的結果。數據丟失的如此嚴重是我所不能忍受的,於是我決定采用其他方法。
串口讀數據的線程模型
這個模型顧名思義,就是將接收數據的操作寫成一個線程的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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分半鍾了,這個時間遠遠大於節點運行周期。這一延遲對於一個實時的顯示系統來說簡直是災難!
后來我想,是不是由於兩個線程之間的同步和通信導致了數據接收緩慢呢?於是我在接收線程的代碼中去掉了所有處理代碼,僅保留打印收到數據的語句,結果依然如故。看來並不是線程間的通信阻礙了數據的接收速度,而是用線程模型導致了對於發送端數據發送速率過快的情況下的數據接收延遲。這里申明一點,就是對於數據發送速率不是如此快的情況下前面者兩種模型應該還是好用的,只是特殊情況還是應該特殊處理。
第三種方法
TinyOS中有一部分是和我的應用程序類似的串口通信部分,於是我下載了它的1.x版的Java代碼部分,參考了它的處理方法。解決問題的方法說穿了其實很簡單,就是從根源入手。根源不就是接收線程導致的嗎,那好,我就干脆取消接收線程和作為中介的共享緩存,而直接在處理線程中調用串口讀數據的方法來解決問題,於是程序變成了這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
byte
[] getPack(){
while
(
true
){
//PacketLength為數據包長度
byte
[] msgPack = newbyte[PacketLength];
for
(
int
i =
0
; i <PacketLength; i++){
if
((newData = is.read()) != -
1
){
msgPack = (
byte
) newData;
System.out.println(msgPack);
}
}
return
msgPack;
}
}
|
在處理線程中調用這個方法返回所需要的數據序列並處理之,這樣不但沒有丟失數據的現象行出現,也沒有數據接收延遲了。這里唯一需要注意的就是當串口停止發送數據或沒有數據的時候is.read()一直都返回-1,如果一旦在開始接收數據的時候發現-1就不要理它,繼續接收,直到收到真正的數據為止。
結束語
本文介紹了串口通信的基本知識,以及常用的幾種模式。通過實踐,提出了一些問題,並在最后加以解決。希望能對需要操作串口數據的程序員能有所幫助。