1. 程式人生 > >串列埠通訊開發

串列埠通訊開發

一開始做串列埠通訊開發時,覺得並不難,無非就是傳送,然後等一會,再接收就完事了。其實裡面的水很深,特別是在各種裝置都有的情況下。我們在整個開發過程中,遇到了以下的幾個主要問題:

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,而使用另外一種方法來區分採數和反控。方法流程圖如下圖所示:

這裡需要指出的是,這種方法對於多裝置共用串列埠的情況並不適用。所以在多裝置共用串列埠時,我們沒辦法使用多執行緒採數。