RS232-485串行接口是一種非常成熟的通信接口,曾幾何時,我們用的鼠標是串口的,Modem是串口的,還有早期的一些數(shù)碼相機都是串口的,時過境遷,家用電腦現(xiàn)在已是USB時代,串口這種東西逐漸淡出了我們的視線。 但是,在工業(yè)控制上,串行接口依然有著不可替代的優(yōu)勢,首先是電氣連接簡單,雖說速率不高,但抗干擾能力強,通訊距離很遠,甚至可以鋪設(shè)幾百米的電纜,這些都是USB不能取代的。
對串行接口的操作,微軟公司很早前就提供了一個通用控件,她就是大名鼎鼎的MSCOMM,這個控件可以嵌入幾乎所有宿主語言,包括主流的VC VB DELPHI C++ Build等等。通過這個控件,我們可以極其輕易地對串口進行操作。但是,這個控件依然不是完美的,因為微軟在寫這個控件的時候,考慮的都是一般性的常規(guī)的操作,不過,一旦遇到非常規(guī)操作,控件立刻就顯示出它的局限性,正如可視化編程下控件濫用的壞習(xí)慣一樣,沒有人再去花心思研究程序的內(nèi)部原理,鼠標拖一下,鍵盤敲幾個調(diào)用,甚至一個程序就出來了,這并不是好事,一旦遇到非常規(guī)事務(wù),立刻就會束手無策。
把話題拉回串行接口,串型數(shù)據(jù)RS232接口的基本概念是以高低脈沖來區(qū)分0或者1,以一個字節(jié)(Byte)為最小單位進行發(fā)送,一個Byte為8個二進制位(BIT),另外附加三個位作為起始位、停止位和奇偶校驗位。在選擇不使用奇偶校驗的情況下,串口一次最小傳送10個BIT,如果需要奇偶校驗,則是11個BIT,排列如下:
[起始位] [數(shù)據(jù)位1到8] [奇偶校驗位] [停止位]
奇偶校驗的原理是,計算數(shù)據(jù)位內(nèi)上升沿的個數(shù),也就是BIT=1的次數(shù),然后再根據(jù)這個個數(shù)據(jù)決定奇偶校驗位是0還是1,比如說發(fā)送1這個數(shù),并且現(xiàn)在我們選用奇校驗,則奇偶校驗位是0,因為(原始數(shù)據(jù)=1,奇偶校驗位=0,1+0=1),1是奇數(shù)。 如果選用偶校驗,則奇偶校驗位會自動變成1,(原始數(shù)據(jù)=1,奇偶校驗位=1,1+1=2),2是偶數(shù)。發(fā)送方將數(shù)據(jù)和奇偶校驗位一起發(fā)送,接受方開始接收數(shù)據(jù),并且核對奇偶校驗位,一旦發(fā)現(xiàn)奇偶校驗位有誤,則立刻報錯,因為這說明數(shù)據(jù)傳輸受到了干擾。
奇偶校驗位一般被稱為串口的“第九位”,這個位其實除了校驗數(shù)據(jù)外,還有別的另類玩法。在主機上利用串行接口對多設(shè)備進行控制的時候,主機發(fā)送到每一條命令,必須要編上地址才行,否則就變成廣播操作了,就像老大一聲吼,底下的小弟們?nèi)空癖劭窈簦@在某些時候確實有用,但如果老大只點了一個小弟的名字讓他單獨回答,就會出問題了,人類于是有了名字,而在工業(yè)控制上,模塊都需要編上地址,這跟名字其實沒什么本質(zhì)上的區(qū)別。串行數(shù)據(jù)流里面,往往利用第九位來區(qū)分是地址包還是數(shù)據(jù)包,大家約定,凡是第九位為1的BYTE,說明這是地址,凡是第九位為0的BYTE,那是數(shù)據(jù)。主機控制下的各分機只有在接受到第九位為1的時候,才進行地址識別,如果確實與主機呼叫的地址一致,才開始識別接下來的數(shù)據(jù)(第九位為0)??梢钥闯?,這樣的方式是很聰明的,各分機沒有必要頻繁地接收主機發(fā)送到數(shù)據(jù)流,只有收到第九位為1并且符合自己地址之后,才進行接收,效率不言而喻。
如果采用第九位作為地址/數(shù)據(jù)的區(qū)分,那么串口將喪失奇偶校驗功能,這是沒有辦法的事,魚與熊掌不可兼得嘛。所以在Windows串行接口規(guī)范里,對這個位有5種設(shè)置,分別是:
NOPARITY = 無校驗
ODDPARITY = 偶校驗
EVENPARITY = 奇校驗
MARKPARITY = 第九位強設(shè)為1
SPACEPARITY = 第九位強設(shè)為0
在發(fā)地址包的時候, 可以把Parity設(shè)置成MARKPARITY. 則第九位常為1.
在發(fā)數(shù)據(jù)包的時候, 可以把Parity設(shè)置成SPACEPARITY.則第九位常為0.
看起來不困難,無非就是改變第九位的狀態(tài)而已嘛。但是,很快,可怕的事情來了,使用MSCOMM控件的話,如果頻繁地改動奇偶校驗操作,則通訊將會出現(xiàn)丟包等莫名其妙的問題!但我們?yōu)榱藚^(qū)分數(shù)據(jù)和地址,這種頻繁改動又是必須的,怎么辦?只能扔掉MSCOMM,另尋他途了。
利用API搭建一個串口通訊程序,是一個好辦法,API程序直接作用于Windows,效率很高,VC++用的類庫MFC無非也就是將成千上萬的API函數(shù)集中起來并加以聚合,抽象?,F(xiàn)在我們直接使用API,當然是可行的,但是,因為Visual Basic本身的缺陷,她沒辦法像VC那樣創(chuàng)建多線程程序(至少實現(xiàn)起來極其困難),在以下的例子里我們只能采用同步的方法來獲得串口的數(shù)據(jù)而不能實現(xiàn)異步接收,等等,到底什么叫同步?異步?簡單地說,比如你拖一個1G的文件從C盤到D盤,這需要大量的時間,如果這段時間系統(tǒng)一直等著它完成COPY的操作,其他什么都不管理,那么這就叫同步(回憶一下DOS時代不就是這樣的嗎)。但是,如果系統(tǒng)只是給它這么一條指令,然后你該什么時候COPY完后通知我一聲,讓我知道你COPY完了就行了,系統(tǒng)在這段時間內(nèi)不會死等這個操作完成,而是釋放開給別的有需要的程序(在Windows時代,你可以邊COPY邊聽歌),這就叫異步。很顯然,異步操作聰明得多,也比較合理,最大的優(yōu)勢是榨干了CPU的效能,但鑒于VB這方面完全不行,所以也只好采用同步的方法了。
以下是源代碼:
API聲明:
Option Explicit
'奇偶校驗常數(shù)
Public Const NOPARITY = 0
Public Const ODDPARITY = 1
Public Const EVENPARITY = 2
Public Const MARKPARITY = 3
Public Const SPACEPARITY = 4
'-------------------------------------------------------------------------------
' 文件操作常數(shù)
'-------------------------------------------------------------------------------
Public Const ERROR_IO_INCOMPLETE = 996&
Public Const ERROR_IO_PENDING = 997
Public Const GENERIC_READ = &H80000000
Public Const GENERIC_WRITE = &H40000000
Public Const FILE_ATTRIBUTE_NORMAL = &H80
Public Const FILE_FLAG_OVERLAPPED = &H40000000
Public Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Public Const OPEN_EXISTING = 3
' 通訊常數(shù)
Public Const MS_CTS_ON = &H10&
Public Const MS_DSR_ON = &H20&
Public Const MS_RING_ON = &H40&
Public Const MS_RLSD_ON = &H80&
Public Const PURGE_RXABORT = &H2
Public Const PURGE_RXCLEAR = &H8
Public Const PURGE_TXABORT = &H1
Public Const PURGE_TXCLEAR = &H4
'-------------------------------------------------------------------------------
'通訊結(jié)構(gòu)
'-------------------------------------------------------------------------------
Public Type COMSTAT
fBitFields As Long ' See Comment in Win32API.Txt
cbInQue As Long
cbOutQue As Long
End Type
Public Type COMMTIMEOUTS
ReadIntervalTimeout As Long
ReadTotalTimeoutMultiplier As Long
ReadTotalTimeoutConstant As Long
WriteTotalTimeoutMultiplier As Long
WriteTotalTimeoutConstant As Long
End Type
'
'DCB結(jié)構(gòu),用于串口的設(shè)置
Public Type DCB
DCBlength As Long
BaudRate As Long
fBitFields As Long
wReserved As Integer
XonLim As Integer
XoffLim As Integer
ByteSize As Byte
Parity As Byte
StopBits As Byte
XonChar As Byte
XoffChar As Byte
ErrorChar As Byte
EofChar As Byte
EvtChar As Byte
wReserved1 As Integer 'Reserved; Do Not Use
End Type
'各種API函數(shù)的聲明:
'建立通訊連接
Public Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, ByVal lpSecurityAttributes As Long, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Long) As Long
'關(guān)閉通訊連接
Public Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
'發(fā)送數(shù)據(jù)
Public Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, lpOverlapped As Long) As Long
'讀取數(shù)據(jù)
Public Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, ByVal lpOverlapped As Long) As Long
'獲取DCB串口設(shè)置狀態(tài)
Public Declare Function GetCommState Lib "kernel32" (ByVal nCid As Long, lpDCB As DCB) As Long
'構(gòu)建DCB串口設(shè)置狀態(tài)
Public Declare Function BuildCommDCB Lib "kernel32" Alias "BuildCommDCBA" (ByVal lpDef As String, lpDCB As DCB) As Long
'設(shè)置DCB串口設(shè)置狀態(tài)
Public Declare Function SetCommState Lib "kernel32" (ByVal hCommDev As Long, lpDCB As DCB) As Long
'設(shè)置串口的緩沖區(qū)
Public Declare Function SetupComm Lib "kernel32" (ByVal hFile As Long, ByVal dwInQueue As Long, ByVal dwOutQueue As Long) As Long
'清除串口緩沖區(qū)的數(shù)據(jù)
Public Declare Function PurgeComm Lib "kernel32" (ByVal hFile As Long, ByVal dwFlags As Long) As Long
'設(shè)置串口的超時狀態(tài)
Public Declare Function SetCommTimeouts Lib "kernel32" (ByVal hFile As Long, lpCommTimeouts As COMMTIMEOUTS) As Long
'獲取錯誤狀態(tài)
Public Declare Function GetLastError Lib "kernel32" () As Long
'產(chǎn)生一個系統(tǒng)延時,單位毫秒
Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
============================================================
以下是程序代碼:
'全局變量hCF為通訊句柄
Dim hCF As Long
Private Sub Form_Load()
'建立通訊連接
hCF = CreateFile("COM1", _
GENERIC_READ Or GENERIC_WRITE, 0, ByVal 0&, _
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
End Sub
Private Sub Command1_click()
Dim Ret As Long
Dim Buffer(30) As Byte
Dim I As Long
Dim typCommStat As COMSTAT '定義串口狀態(tài)結(jié)構(gòu)
Dim lngError As Long '定義串口狀態(tài)錯誤
Dim flag As Long '定義回傳值
Dim typDCB As DCB '定義DCB串口設(shè)置塊
Dim strSettings As String
flag = SetupComm(hCF, 1024, 1024) '設(shè)置緩沖區(qū)大小,1K
'強制清空讀寫緩沖區(qū)
flag = PurgeComm(hCF, PURGE_RXABORT Or PURGE_RXCLEAR Or PURGE_TXABORT Or PURGE_TXCLEAR)
'定義超時結(jié)構(gòu)體
Dim typCommTimeouts As COMMTIMEOUTS
typCommTimeouts.ReadIntervalTimeout = 0 '相鄰兩字節(jié)讀取最大時間間隔(為0表示不使用該超時間隔)
typCommTimeouts.ReadTotalTimeoutMultiplier = 10 '一個讀操作的時間常數(shù)
typCommTimeouts.ReadTotalTimeoutConstant = 10 '讀超時常數(shù)
typCommTimeouts.WriteTotalTimeoutMultiplier = 0 '一個寫操作的時間常數(shù)(為0表示不使用該超時間隔)
typCommTimeouts.WriteTotalTimeoutConstant = 0 '寫超時常數(shù)(為0表示不使用該超時間隔)
'超時設(shè)置
flag = SetCommTimeouts(hCF, typCommTimeouts)
Dim addressByte(0 To 1) As Byte '地址位,兩個字節(jié)
Dim dataByte(0 To 3) As Byte '數(shù)據(jù)位,四個字節(jié)
flag = GetCommState(hCF, typDCB)
strSettings = "baud=19200 parity=m data=8 stop=1" '首先將奇偶校驗位調(diào)節(jié)到M模式,則強制設(shè)為1
flag = BuildCommDCB(strSettings, typDCB) '構(gòu)建DCB塊
flag = SetCommState(hCF, typDCB) '設(shè)置DCB塊
addressByte(0) = &H0 '分機編號0000,占用兩個字節(jié)
addressByte(1) = &H0
Ret = WriteFile(hCF, addressByte(0), 2, flag, ByVal 0&) '發(fā)送
flag = GetCommState(hCF, typDCB)
strSettings = "baud=19200 parity=s data=8 stop=1" '首先將奇偶校驗位調(diào)節(jié)到S模式,則強制設(shè)為0
flag = BuildCommDCB(strSettings, typDCB)
flag = SetCommState(hCF, typDCB)
flag = GetCommState(hCF, typDCB)
dataByte(0) = &H3 '這是數(shù)據(jù),我的數(shù)據(jù)為4個字節(jié),這個依據(jù)實際情況自行定義
dataByte(1) = &H20
dataByte(2) = &H0
dataByte(3) = &H23
Ret = WriteFile(hCF, dataByte(0), 4, flag, ByVal 0&) '發(fā)送
Sleep 50 '延時50毫秒
'同步接收來自串口的數(shù)據(jù),數(shù)據(jù)存到Buffer數(shù)組里,我這里取30字節(jié),這個可以按實際情況自定
Ret = ReadFile(hCF, Buffer(0), 30, 0, 0)
For I = 0 To 30
Debug.Print Hex(Buffer(I)) '在DEBUG窗口顯示接收過來的數(shù)據(jù)
Next I
End Sub
Private Sub Form_Unload(Cancel As Integer)
CloseHandle hCF '關(guān)閉通訊連接
End Sub