第1章串行通訊之.NET SerialPort
.NET 庫中類System.IO.Ports.SerialPort用於串行通訊,本文對其使用進行簡要說明。
1 枚舉串口
函數System.IO.Ports.SerialPort.GetPortNames將獲得系統所有的串口名稱。C#代碼如下:
|   string[] arrPort = System.IO.Ports.SerialPort.GetPortNames(); foreach (string s in arrPort) { }  |  
           
2 打開/關閉串口
下面的 C# 代碼將打開 COM100:1200,N,8,1
|   System.IO.Ports.SerialPort m_sp = new System.IO.Ports.SerialPort(); private void btnOpen_Click(object sender, EventArgs e) { m_sp.PortName = "COM100"; //串口 m_sp.BaudRate = 1200; //波特率:1200 m_sp.Parity = System.IO.Ports.Parity.None; //校驗法:無 m_sp.DataBits = 8; //數據位:8 m_sp.StopBits = System.IO.Ports.StopBits.One; //停止位:1 try { m_sp.Open(); //打開串口 m_sp.DtrEnable = true; //設置DTR為高電平(含義見下文) m_sp.RtsEnable = true; //設置RTS為高電平(含義見下文) } catch (System.Exception ex) {//打開串口出錯,顯示錯誤信息 MessageBox.Show(ex.Message); } }  |  
           
下面的 C# 代碼將關閉打開的串口
|   if(m_sp.IsOpen) //判斷串口是否已經被打開 { m_sp.Close(); //關閉串口 }  |  
           
3 寫數據
System.IO.Ports.SerialPort用於寫串口數據的成員函數有四個,如下所示:
|   函數  |  
              說明  |  
           
|   void Write(byte[] buffer, int offset, int count); void Write(char[] buffer, int offset, int count);  |  
              寫二進制數據  |  
           
|   void Write(string text);  |  
              寫文本數據  |  
           
|   void WriteLine(string text);  |  
              寫一行文本  |  
           
3.1 寫二進制數據
void Write(byte[] buffer, int offset, int count);和void Write(char[] buffer, int offset, int count);用於寫二進制數據。它們的區別僅僅在於第一個參數不同:byte[]是無符號的,char[]是有符號的。對於二進制數據而言,byte、char沒有實質的區別。
下面的C#代碼,將寫1024個00H:
|   try { if(m_sp.IsOpen) { byte[] bt = new byte[1024]; m_sp.Write(bt,0,bt.Length); //寫 1024 個 00H } } catch (System.Exception ex) {//顯示錯誤信息 MessageBox.Show(ex.Message); }  |  
           
注意:
1、Write函數是同步的。以上面的代碼為例,1024個00H在發送完之前,Write函數是不會返回的。波特率1200,發送1024個字節大概要耗時9秒。如果這段代碼在主線程里,那么這9秒內整個程序將處於假死狀態:無法響應用戶的鍵盤、鼠標輸入;
2、WriteTimeout屬性用於控制Write函數的最長耗時。它的默認值為System.IO.Ports.SerialPort.InfiniteTimeout,也就是-1。其含義為:Write函數不將所有數據寫完絕不返回。可以修改此屬性,如下面的代碼:
|   try { if(m_sp.IsOpen) { byte[] bt = new byte[1024]; m_sp.WriteTimeout = 5000; //Write 函數最多耗時 5000 毫秒 m_sp.Write(bt,0,bt.Length); //寫 1024 個 00H } } catch (System.Exception ex) {//顯示錯誤信息 MessageBox.Show(ex.Message); }  |  
           
上面的代碼中,設置WriteTimeout屬性為5秒。所以Write寫數據時最多耗時5秒,超過這個時間未發的數據將被舍棄,Write函數拋出異常TimeoutException后立即返回。
3.2 寫文本數據
下面是void Write(string text)的示例代碼
|   try { if(m_sp.IsOpen) { m_sp.Encoding = System.Text.Encoding.GetEncoding(936); m_sp.Write("串行通訊"); } } catch (System.Exception ex) {//顯示錯誤信息 MessageBox.Show(ex.Message); }  |  
           
首先設置代碼頁為936(即GBK碼),Write(string text)函數根據代碼頁把字符串"串行通訊"轉換為二進制數據,如下所示:
|   字符串  |  
              串  |  
              行  |  
              通  |  
              訊  |  
           
|   內碼  |  
              B4 AE  |  
              D0 D0  |  
              CD A8  |  
              D1 B6  |  
           
然后把二進制數據B4 AE D0 D0 CD A8 D1 B6發送出去。
函數void WriteLine(string text);等價於void Write(text + NewLine)。參考下面的代碼:
|   try { if(m_sp.IsOpen) { m_sp.Encoding = System.Text.Encoding.GetEncoding(936); m_sp.NewLine = "\r\n"; m_sp.WriteLine("串行通訊"); } } catch (System.Exception ex) {//顯示錯誤信息 MessageBox.Show(ex.Message); }  |  
           
代碼m_sp.NewLine = "\r\n";設置行結束符為回車(0DH)換行(0AH)。m_sp.WriteLine("串行通訊");等價於m_sp.Write("串行通訊"+m_sp.NewLine);也就是m_sp.Write("串行通訊\r\n");
最終,發送出去的二進制數據為B4 AE D0 D0 CD A8 D1 B6 0D 0A。
4 讀數據
System.IO.Ports.SerialPort用於讀串口數據的成員函數有七個,如下所示:
|   函數  |  
              說明  |  
           
|   int ReadByte();  |  
              讀取一個字節  |  
           
|   int ReadChar();  |  
              讀取一個字符  |  
           
|   int Read(byte[] buffer, int offset, int count); int Read(char[] buffer, int offset, int count);  |  
              讀取二進制數據  |  
           
|   string ReadExisting();  |  
              讀取全部文本  |  
           
|   string ReadTo(string value);  |  
              讀取文本到某個字符串  |  
           
|   string ReadLine();  |  
              讀取一行文本  |  
           
4.1 讀二進制數據
下面的C#代碼,將讀取 3 個字節的串口數據
|   if(m_sp.IsOpen) { try { byte[] b = new byte[3]; int n = m_sp.Read(b,0,3); //返回值是讀取到的字節數 } catch (System.Exception ex) { MessageBox.Show(ex.Message); } }  |  
           
注意:
1、Read函數是同步的。以上面的代碼為例,3個字節的數據被讀取之前,Read函數是不會返回的。如果這段代碼在主線程里,那么整個程序將處於假死狀態;
2、ReadTimeout屬性用於控制Read函數的最長耗時。它的默認值為System.IO.Ports.SerialPort.InfiniteTimeout,也就是-1。其含義為:Read函數未讀取到串口數據之前是不會返回的。可以修改此屬性,如下面的代碼:
|   if(m_sp.IsOpen) { try { byte[] b = new byte[3]; m_sp.ReadTimeout = 2000; int n = m_sp.Read(b,0,3); //返回值是讀取到的字節數 } catch (System.Exception ex) { MessageBox.Show(ex.Message); } }  |  
           
上面的代碼中,設置ReadTimeout屬性為2秒。所以Read函數讀數據時最多耗時2秒。超過這個時間未讀取到數據,Read函數將拋出異常TimeoutException,然后返回。
4.2 讀一個字節
int ReadByte();與int Read(byte[] buffer, int offset, int count);類似,它的特點就是只讀取一個字節的串口數據。
4.3 讀一個字符
int ReadChar();是讀取一個字符,這個稍微復雜些。它可能讀取1~3個字節的數據,然后合為一個字符。
如:m_sp.Encoding = System.Text.Encoding.GetEncoding(936);即字符串編碼為GBK。給m_sp發送"串"的GBK編碼B4 AE。ReadChar首先讀取一個字節得到B4H。這是一個漢字的區碼,還得讀取一個字節得到位碼。最終ReadChar讀取的是B4 AE。ReadChar的返回值是Unicode編碼,即返回前會把GBK編碼B4 AE轉換為Unicode編碼0x4E32。
再如:m_sp.Encoding = System.Text.Encoding.UTF8;即字符串編碼為UTF8。給m_sp發送"串"的UTF8編碼E4 B8 B2。ReadChar會讀取三個字節的串口數據E4 B8 B2,然后將其轉換為Unicode編碼0x4E32,並返回這個數值。
4.4 讀全部文本
函數string ReadExisting();讀取串口輸入緩沖區中的所有二進制數據,然后將其轉換為字符串,最后返回字符串。
注意:
1、ReadExisting會立即返回。如果輸入緩沖區內沒有數據,直接返回長度為零的空字符串;
2、ReadExisting讀取輸入緩沖區后,有時會留幾個字節。參考下面的代碼:
|   m_sp.Encoding = System.Text.Encoding.GetEncoding(936); string s = m_sp.ReadExisting(); int n = m_sp.BytesToRead; //輸入緩沖區剩余的字節數  |  
           
"串"、"行"的GBK編碼分別為 B4 AE和D0 D0。
首先發送 B4 AE D0 給m_sp,運行上述代碼。ReadExisting將獲得B4 AE D0,"B4 AE"會被解釋為"串",D0是漢字的區碼,所以ReadExisting會將D0保留在輸入緩沖區內。上述代碼的運行結果就是:s為"串",n為1;
然后發送D0 給m_sp,運行上述代碼。ReadExisting將獲得D0 D0,"D0 D0"會被解釋為"行"。上述代碼的運行結果就是:s為"行",n為0。
4.5 讀文本到某個字符串
函數string ReadTo(string value);將在串口輸入緩沖區內查找字符串value。找到了,就返回value之前的字符串,同時清除緩沖區內value及其之前的數據;未找到,就一直等待,直至超時。
4.6 讀一行文本
函數string ReadLine();等價於ReadTo(NewLine)。使用前,請設置NewLine屬性,指定行結束符。
4.7 DataReceived事件
串口輸入緩沖區獲得新數據后,會以DataReceived事件通知System.IO.Ports.SerialPort對象,可以在此時讀取串口數據。請參考下面兩段代碼:
|   m_sp.ReceivedBytesThreshold = 1; m_sp.DataReceived+=new System.IO.Ports.SerialDataReceivedEventHandler(m_sp_DataReceived);  |  
           
|   void m_sp_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { int nRead = m_sp.BytesToRead; if (nRead > 0) { byte[] data = new byte[nRead]; m_sp.Read(data, 0, nRead); } }  |  
           
m_sp.ReceivedBytesThreshold = 1;的含義:串口輸入緩沖區獲得新數據后,將檢查緩沖區內已有的字節數,大於等於ReceivedBytesThreshold就會觸發DataReceived事件。這里設置為1,顯然就是一旦獲得新數據后,立即觸發DataReceived事件。
m_sp.DataReceived+=new System.IO.Ports.SerialDataReceivedEventHandler(m_sp_DataReceived);的含義:對於DataReceived事件,用函數m_sp_DataReceived進行處理。
回調函數m_sp_DataReceived用於響應DataReceived事件,通常在這個函數里讀取串口數據。它的第一個參數sender就是事件的發起者。上面的代碼中,sender其實就是m_sp。也就是說:多個串口對象可以共用一個回調函數,通過sender可以區分是哪個串口對象。
回調函數是被一個多線程調用的,它不在主線程內。所以,不要在這個回調函數里直接訪問界面控件。如下面的代碼將將讀取到的串口數據轉換為字符串,然后顯示在按鈕Open上。紅色代碼處將產生異常。
|   void m_sp_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { int nRead = m_sp.BytesToRead; if (nRead > 0) { byte[] data = new byte[nRead]; m_sp.Read(data, 0, nRead); btnOpen.Text = System.Text.Encoding.Default.GetString(data); } }  |  
           
可使用Invoke或BeginInvoke改進上面的紅色代碼:
|   BeginInvoke(new Action<string>((x)=>{btnOpen.Text = x;}) ,new Object[] {System.Text.Encoding.Default.GetString(data)});  |  
           
5 流控制
串行通訊的雙方,如果有一方反應較慢,另一方不管不顧的不停發送數據,就可能造成數據丟失。為了防止這種情況發生,需要使用流控制。
流控制也叫握手,System.IO.Ports.SerialPort的Handshake屬性用於設置流控制。它有四種取值:
|   取值  |  
              說明  |  
           
|   System.IO.Ports.Handshake.None  |  
              無  |  
           
|   System.IO.Ports.Handshake.XOnXOff  |  
              軟件  |  
           
|   System.IO.Ports.Handshake.RequestToSend  |  
              硬件  |  
           
|   System.IO.Ports.Handshake.RequestToSendXOnXOff  |  
              硬件和軟件  |  
           
5.1 軟件流控制(XON/XOFF)
串口設備A給串口設備B發送數據。B忙不過來時(B的串口輸入緩沖區快滿了)會給A發送字符XOFF(一般為13H),A將暫停發送數據;B的串口輸入緩沖區快空時,會給A發送字符XON(一般為11H),A將繼續發送數據。
軟件流控制最大的問題在於:通訊雙方不能傳輸字符XON和XOFF。
5.2 硬件流控制(RTS/CTS)
RTS/CTS流控制是硬件流控制的一種,需要按下圖連線:

圖1
串口設備A給串口設備B發送數據。B忙不過來時(B的串口輸入緩沖區快滿了)會設置自己的RTS為低電平,這樣A的CTS也變為低電平。A發現自己的CTS為低電平后,會停止發送數據;B的串口輸入緩沖區快空時,會設置自己的RTS為高電平,這樣A的CTS也變為高電平。A發現自己的CTS為高電平后,會繼續發送數據。
相同的道理,DTR/DSR也可以做硬件流控制。
現在再來看看如下代碼:
|   m_sp.Open(); m_sp.DtrEnable = true; m_sp.RtsEnable = true;  |  
           
為什么打開串口時需要設置DTR、RTS為高電平呢?原因就在於:如果對方設置了硬件流控制,而這邊的DTR、RTS為低電平,那么對方就不會給這邊發送數據。
需要注意的是:RTS/CTS硬件流控制下,RTS的電平由系統自行調整。調用m_sp.RtsEnable = true;改變RTS的電平將會導致異常。
6 輸入信號
上一節中,屬性DtrEnable、RtsEnable可以控制輸出信號DTR、RTS。與之相應的,屬性CDHolding、CtsHolding、DsrHolding可讀取輸入信號。
CtsHolding 為 true,說明對方的RTS為高電平(請按圖1所示連線)。
DsrHolding 為 true,說明對方的DTR為高電平(請按圖1所示連線)。
