串列埠通訊開發
一開始做串列埠通訊開發時,覺得並不難,無非就是傳送,然後等一會,再接收就完事了。其實裡面的水很深,特別是在各種裝置都有的情況下。我們在整個開發過程中,遇到了以下的幾個主要問題:
1、裝置出現嚴重的延遲。
2、接收過程出現數據粘包或截斷。
3、多裝置共用一個串列埠。
4、使用RTU的情況,接受到資料傳給所有程式處理。
5、採數和反控不能相互影響。
6、多執行緒併發採數。
一、裝置出現嚴重的延遲
正常的裝置,在100ms之內就會返回,但某些裝置,因為硬體原因,響應指令的速度非常慢,有可能達到10s。
對於這種情況,我們需要延長等待的時間。但直接延長,對於響應快的裝置,就不公平。我們可以採用以下的方法:
int count = 0;
while (count < 20)
{
Thread.Sleep(WRITE_READ_INTERVAL);
//接收資料
//如果資料合法,跳出迴圈
}
具體程式碼我們結合第二個問題給出。
二、接收過程出現數據粘包或截斷
呼叫一次接收函式,收到的資料不一定是完整的,特別是位元組數比較多的情況。我們需要把幾次接收到的內容,拼成一個包,再判斷這個包是否合法的。根據第一、第二個問題,我們給出以下程式碼解決:
int count = 0;//迴圈次數 byte[] total = new byte[1024];//總接收到的內容 int pointer = 0;//新內容的起始指標 while (count < 20)//迴圈20次,一次100ms,也就是最長等待2s { Thread.Sleep(WRITE_READ_INTERVAL); byte[] temp = ReceiveByte(control);//接收資料,存放在一個臨時變數中 if (temp != null && temp.Length > 0) { Array.Copy(temp, 0, total, pointer, temp.Length);//合併資料 pointer += temp.Length; if (ModbusHelper.CheckRecvValid(total, pointer))//判斷資料是否合法,一旦合法,說明一條完整的資料已經接收完成 { break; } } count++; } if (pointer < 5) { return null; } byte[] recv = new byte[pointer]; Array.Copy(total, recv, pointer); return recv;
上述方法是專門針對Modbus協議的,如果是其他協議的裝置,還需要修改判斷合法的條件。
三、多裝置共用一個串列埠
一開始,我們給每一臺裝置分配一個串列埠,這臺裝置負責串列埠的開啟、傳數和關閉等操作。但多裝置共同串列埠的情況下,這種方法就行不通了。串列埠不再屬於某一臺裝置。基於這種情況,我們把串列埠從裝置類裡抽離出來,轉而使用一個串列埠管理類去操作串列埠。
串列埠管理類的示例程式碼如下:
private static Dictionary<string, ComPortHelper> ComPortDict = new Dictionary<string, ComPortHelper>();//串列埠字典 public static bool IsInit(string PortName)//串列埠是否就緒 { if (ComPortDict.ContainsKey(PortName)) { return ComPortDict[PortName].IsInit(); } return false; } public static void Send(string PortName, byte[] cmd)//傳送資料 { long nowTime = DateTime.Now.Ticks; if (ComPortDict.ContainsKey(PortName)) { ComPortDict[PortName].Send(cmd); } else { //初始化之後再進行傳送 } }
其基本思路很簡單,就是使用一個字典記錄串列埠,裝置在每次操作串列埠時,在字典裡尋找,如果能找到,執行操作,如果找不到,就先初始化串列埠。
共用串列埠需要注意兩個問題:
(1)從機地址必須不一樣。
(2)對於某些裝置,上一個裝置接收完,需要等一會再發送,才能接收成功。還有一種辦法是用其他裝置把這些共用串列埠的裝置隔開,這樣他們有休息的時間,也可以接收成功。
四、使用RTU的情況,接受到資料傳給所有程式處理
正常情況下,工控機與裝置的通訊流程是這樣的:
裝置1傳送資料 裝置1接收資料,處理 裝置2傳送資料 裝置2接收資料,處理 …… |
但在使用RTU的時候,這種方法就不適用了。如果添加了幾個平臺,由於接收資料是非同步的,程式1可能接收到平臺2的資料,而且,程式1接收之後,程式2不會再接收到資料,那資料就丟失了。
對於這種情況,我們使用複製資料的方法。如果是同一個串列埠接收到的資料,複製給其他共用串列埠的裝置。
if (ComPortDict.ContainsKey(PortName))
{
byte[] recv = ComPortDict[PortName].Receive();//接收資料,儲存到臨時變數
if (recv != null && recv.Length != 0)//資料不為空
{
ComDataDict[PortName] = recv;//把資料儲存起來
ComCounterDict[PortName].count = 0;//重置計數器
return recv;
}
else//資料為空
{
ComCounterDict[PortName].count++;//計數器加1
if (ComCounterDict[PortName].count < ComCounterDict[PortName].total)//如果計數超過了共用這個串列埠的裝置,說明這一次的通訊確實失敗了
{
return ComDataDict[PortName];
}
else
{
ComDataDict[PortName] = recv;//使用上一次儲存的資料
ComCounterDict[PortName].count = 0;
return recv;
}
}
}
return null;
這裡我們需要注意:
(1)通訊是有可能失敗的,不能一直使用儲存起來的資料。需要使用一個計數器,在所有共用串列埠的裝置都取完數後,就重置這個計數器。
(2)可以看出,這種方法在處理第三個問題的時候,是有衝突的。所以我們需要分開處理,第三個問題和第四個問題使用不同的處理方法。
五、採數和反控不能相互影響
採集資料是在一個執行緒裡面迴圈完成的。使用者隨時會插入一個反控操作。執行反控時,傳送和接收必須跟採集區分開,一個工作完成了才能做另外一個工作。我們採用以下方法:
採集和反控都放在一個類裡面,然後一個工作正在執行的時候,使用Monitor,讓另外一個工作進行等待。
六、多執行緒併發採數
在處理第六個問題的時候,我們推翻了第五個問題的方法。我們發現,Monitor會鎖住所有執行緒,而不是一個執行緒內容的鎖定。我們把每臺裝置的採集和反控放到不同的類裡面,每個類獨立佔據一個執行緒進行執行。如果沒有Monitor,執行緒執行正常,各個執行緒互不干擾。但Monitor的加入,破壞了這種秩序。一個執行緒使用了Monitor.Enter,其他執行緒都不動了,即使他們鎖的物件並不一樣。
對於這個問題,我們選擇拋棄Monitor,而使用另外一種方法來區分採數和反控。方法流程圖如下圖所示:
這裡需要指出的是,這種方法對於多裝置共用串列埠的情況並不適用。所以在多裝置共用串列埠時,我們沒辦法使用多執行緒採數。