modbus串口通信C#

簡介

公司給的一個小任務,這篇文章進行詳細講解編程

題目: modbus串口通信

主要內容以下:
一、實現使用modbus通信規約的測試軟件;
二、具備通訊超時功能;
三、分主站從站,並能編輯報文、生成報文等;
四、計算髮送報文次數,接收報文次數,失敗通訊次數;
五、對接收的數據進行解析。數組

下面圖片能夠看出具體的內容:
緩存

知識點講解

該小軟件使用的知識以下:
一、modbus通訊規約;
二、串口通信;
三、定時器;
四、多線程;多線程

一、modbus通信規約

modbus是一個工業上經常使用的通信協議,一個通信約定,包括RTU,ASCII,TCP。該軟件使用的RTU。函數

主站設備查詢:
查詢消腫的功能號告知被選中的設備要執行何種功能。數據段包括了從站設備要執行的功能的任何附加信息。測試

從站設備迴應:
當從站設備正常回應後,在迴應數據裏也包括這功能號,並直接截取從站設備收集的數據。若是發生錯誤,功能號將被修改成用於指出迴應消息爲錯誤消息。並在數據段包括該描述的錯誤信息。錯誤校測域容許主設備確認消息的內容是否可用,是否正確。線程

下面的圖片解釋了modbus的規約的組成:設計


mobus通信規約是由從機地址+功能號+數據地址+數據+CRC校驗。指針

從機地址:該規約是單主站/多從站,主站輪詢向從站請求的方式進行傳輸數據,並使用從機地址的方式區分從機。code

功能號: 某指令是幹啥,一目瞭然。接收方將經過功能號進行相應的執行功能。
下面爲經常使用功能號:

數據地址:意思是數據存儲的地址,從該存儲的地址的獲取數據。

CRC校驗:循環冗餘校驗碼,是數據通訊領域中最經常使用的一種查錯校驗碼,其特徵是信息字段和校驗字段的長度能夠任意選定。

對於校驗,網上資料不少,這裏直接上代碼:

#region  CRC16
    public static byte[] CRC16(byte[] data)
    {
        int len = data.Length;
        if (len > 0)
        {
            ushort crc = 0xFFFF;

            for (int i = 0; i < len; i++)
            {
                crc = (ushort)(crc ^ (data[i]));
                for (int j = 0; j < 8; j++)
                {
                    crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
                }
            }
            byte hi = (byte)((crc & 0xFF00) >> 8);  //高位置
            byte lo = (byte)(crc & 0x00FF);         //低位置

            return new byte[] { hi, lo };
        }
        return new byte[] { 0, 0 };
    }
    #endregion

串口通信

在C#中實現串口通信,因爲C#微軟封裝的很好,提供了SerialPort類,命名空間爲system.IO.Ports.


下面解釋serialPort類編程中經常使用到的關鍵字和方法:

經常使用字段:
PortName 獲取或設置通訊端口
BaudRate 獲取或設置串行波特率
DataBits 獲取或設置每一個字節的標準數據位長度
Parity 獲取或設置奇偶校驗檢查協議
StopBits 獲取或設置每一個字節的標準中止位數

經常使用方法:
Close 關閉端口鏈接,將IsOpen 屬性設置爲false,並釋放內部 Stream 對象
GetPortNames 獲取當前計算機的串行端口名稱數組
Open 打開一個新的串行端口鏈接
Read 從 SerialPort 輸入緩衝區中讀取
Write 將數據寫入串行端口輸出緩衝區


串口通訊簡介

串口是一種能夠接受來自CPU的並行數據字符轉換爲連續的的串行數據流發送出去,同時可將接受的串行數據流轉換爲並行的數據字符供給CPU的器件,也就是說硬件稱爲串行接口電路。

串口通信重要的參數有波特率,數據位,中止位,奇偶校驗。

一、波特率,這是一個衡量符號傳輸速率的參數,指的是信號被調製之後在單位時間內的變化,即單位時間內載波參數變化的次數,如每秒鐘傳960個字符,而每一個字符格式包含10位(1個起始位,1箇中止位,8個數據位)這是波特率爲960Bd,比特率就是9600bps,

二、數據位:這是衡量通訊中實際數據位的參數,當計算機發送一個信息包,實際的數據每每不會是8位,標準的是六、7和8位,標準的ASCII碼是0~127(7位),擴展的ASCII碼是0~255(8位),

三、中止位:用於表示單個包的最後幾位,典型的值爲1,1.5和2位。做用就是數據在傳輸線上定時的,而且每個有其本身的時鐘,極可能在通訊中兩臺設備出現不一樣步的狀況,中止位能夠解決這個問題,它不只表示傳輸結束,還能夠提供計算機矯正同步時鐘的機會。

四、校驗位:在串口通訊中一種簡單的檢錯方式,有四種檢錯方式:奇,偶,高、低。


下面是我寫的串口通信的代碼:

一、加載串口配置

#region 加載串口配置   

    public bool LoadSerialConfig(string com, string BAUDRATE, string DATABITS, string STOP, string PARITY)
    {
        if (!sp1.IsOpen)   //沒打開
        {
            try
            {
                //設置串口號
                string serialName = com;
                sp1.PortName = serialName;

                //設置各「串口設置」
                string strBaudRate = BAUDRATE;
                string strDateBits = DATABITS;
                string strStopBits = STOP;
                Int32 iBaudRate = Convert.ToInt32(strBaudRate);
                Int32 iDateBits = Convert.ToInt32(strDateBits);

                sp1.BaudRate = iBaudRate;       //波特率
                sp1.DataBits = iDateBits;       //數據位
                switch (STOP)            //中止位
                {
                    case "1":
                        sp1.StopBits = StopBits.One;
                        break;
                    case "1.5":
                        sp1.StopBits = StopBits.OnePointFive;
                        break;
                    case "2":
                        sp1.StopBits = StopBits.Two;
                        break;
                    default:
                        //MessageBox.Show("Error:參數不正確!", "Error");
                        break;
                }

                switch (PARITY)             //校驗位
                {
                    case "NONE":
                        sp1.Parity = Parity.None;
                        break;
                    case "奇校驗":
                        sp1.Parity = Parity.Odd;
                        break;
                    case "偶校驗":
                        sp1.Parity = Parity.Even;
                        break;
                    default:
                        //MessageBox.Show("Error:參數不正確!", "Error");
                        break;
                }

                //若是打開狀態,則先關閉一下
                if (sp1.IsOpen == true)
                {
                    sp1.Close();
                }
                sp1.Open();     //打開串口
                return true;
            }
            catch (System.Exception ex)
            {
                SetSerialOpenFlag(false);
                Form1.ShowThrow(ex);
                return false;
            }
        }
        else   //已經打開
        {
            return true;
        }
    }
    #endregion

二、處理數據的定時器,在定時器裏面對接收到的數據進行壓到隊列裏面,後期對隊列進行再次的處理。

public void StartTimeOutTimer( UInt16 SendDataShowTimer,bool autoFlag)
{
//實例化Timer類,設置間隔時間爲10000毫秒;
timeOutTimer = new System.Timers.Timer(SendDataShowTimer);
timeOutTimer.Elapsed += new System.Timers.ElapsedEventHandler(EndTimeProcess);
timeOutTimer.AutoReset = autoFlag;//設置是執行一次(false)仍是一直執行(rtue);
timeOutTimer.Enabled = true;//是否執行System.Timers.Timer.Elapsed事件;
}


private void EndTimeProcess(Object sender, EventArgs e)
{
    if (GetSerialOpenFlag())
    {
        recvBytesNum = (UInt16)sp1.BytesToRead;

    if (recvBytesNum == 0 && delayTime <= TimeOutFailMaxTime)
    {
        delayTime++;
        timeOutTimer.Start();    //定時器應該執行一次,而後在這重新開始,好比100毫秒後還未接收到數據,就記下數後從新開始定時器
    }
    else    //經過sp1.BytesToRead已經知道串口接收緩存區的大小,使用read函數直接取數,
    {
        if (sp1.BytesToRead > 0)    //有數據,下面接收數據並校驗數據
        {
            //接收16進制
            try
            {
                lock (Recvlock)    //加鎖
                {
                    Byte[] receiveddata = new Byte[sp1.BytesToRead];        //創接建收字節數組
                    sp1.Read(receiveddata, 0, receiveddata.Length);         //讀取數據
                    sp1.DiscardInBuffer();                                  //清空SerialPort控件的Buffer
                    if (receiveddata.Length <= 0)
                        return;
                    DataProcessorQueue.Enqueue(receiveddata);  
                }
                delayTime = 0;
                recvBytesNum = 0;
            }
            catch (Exception ex)
            {
                Form1.ShowThrow(ex);
                return;
            }
        }
        else        //超過屢次定時都爲串口緩衝區的數據都爲空,則說明通信超時
        {
            ConnectFailCount += 1;
        }
    }
}

}


   #region  串口數據接收
void sp1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    if (GetSerialOpenFlag()) //此處可能沒有必要判斷是否打開串口,但爲了嚴謹性,我仍是加上了
    {
        timeOutTimer.Stop();
        timeOutTimer.Start();
    }
    else
    {
        Form1.ShowThrow("串口沒有成功打開");
        return;
    }
}
#endregion

在這裏講解一下爲何須要用到定時器?
因爲要實現通信超時功能,因此我這裏使用定時器的方式,開始接收到數據後開始定時,直到個人數據在定時間內發送過來,我設定了小於3次的定時,若是3次定時都尚未將數據傳輸完畢,則認爲數據傳輸完畢。

三、發送數據

public void SendTextdelegate(byte[] buf)
    {
        SetSendText( buf);
        StartSendThread();
    }
    public void SetSendText(byte[] buf)
    {
        strSend = System.Text.Encoding.Default.GetString(buf);
    }

爲了本身封裝一個類,並與UI進行分離,我使用的是C#經常使用的委託方式,從Form類中傳入數據,

定時器

先上代碼

public System.Timers.Timer timeOutTimer;    //定義定時器

public void StartDataProcessorTimer( bool autoFlag)
    {
        //實例化Timer類,設置間隔時間爲10000毫秒;
        timeOutTimer = new System.Timers.Timer(DataProcessorTimer);
        timeOutTimer.Elapsed += new System.Timers.ElapsedEventHandler(DataProcess);
        timeOutTimer.AutoReset = autoFlag;//設置是執行一次(false)仍是一直執行(true);
        timeOutTimer.Enabled = true;//是否執行System.Timers.Timer.Elapsed事件;
    }

定時器是在通信方面是常用到的,下面我講解一下我這個小軟件使用到定時器位置
一、超時通訊功能
二、定時發送功能
三、接收功能
三、定時顯示某些數據,好比發送次數,接收次數,失敗通訊次數等。

多線程

在定時器使用過程當中,也會使用到線程。好比,有些地方爲了與其餘功能分離開來。

下面給出開啓線程的代碼

public void StartSendThread()
    {
        Thread SendThread = new Thread(SendMsg);
        SendThread.Start();
    }

軟件思路

一、界面

因爲須要作兩個軟件(主從站),我將兩個軟件融合在一塊兒,使用選擇站點的方式進行開啓主站或者從站。 主站的界面和從站的界面很類似,爲了讓用戶操做一致。

二、在上述給出了知識講解中,基本包含了軟件的設計思路,主從站之分在於報文擬製不一樣,串口發送過程相同,所使用的方式也相同,就不具體討論,下面對重要設計思想進行描述。

(a)使用鎖,因爲某些數據須要進行同步,我選擇的是加鎖的方式。
給出一部分的代碼以下:

lock (Recvlock)    //加鎖
{
    Byte[] receiveddata = new Byte[sp1.BytesToRead];        //創接建收字節數組
    sp1.Read(receiveddata, 0, receiveddata.Length);         //讀取數據
    sp1.DiscardInBuffer();                                  //清空SerialPort控件的Buffer
    if (receiveddata.Length <= 0)
        return;
    DataProcessorQueue.Enqueue(receiveddata);  
}

實現數據同步的方式不少,數據同步,爲了讓多線程同時操做同一個緩存區時,可以保證數據一致性,

(b)隊列,因爲考慮到發送方發送數據過快時,我使用的是隊列將接收的數據進行存儲下來,而後再開啓另一個定時器和線程去隊列取數,並將數據,分析,校驗以及顯示等等。這樣的方式能夠不用考慮對方什麼時候發送,發送速度的問題,但有一個問題就是隊列的大小有限制,我選擇的隊列是System.Collections.Generic.Queue,C#中隊列不少,這種隊列能夠解決隊列大小限制的問題。

(C)配置文件
爲了讓軟件在初始化串口參數,我使用的是配置文件對串口參數進行設置。

下面爲配置文件的代碼:

private static IniFile _file;//內置了一個對象

public static void LoadProfile_Serial()
    {
        string strPath = AppDomain.CurrentDomain.BaseDirectory;
        _file = new IniFile(strPath + "Cfg.ini");
        G_BAUDRATE = _file.ReadString("CONFIG", "BaudRate", "4800");    //讀數據,下同
        G_DATABITS = _file.ReadString("CONFIG", "DataBits", "8");
        G_STOP = _file.ReadString("CONFIG", "StopBits", "1");
        G_PARITY = _file.ReadString("CONFIG", "Parity", "NONE");

    }

(d)數據轉換

下面對數據轉換作一個總結:
作通信軟件,數據轉換是必要的,在string,2進制,10進制,16進制,byte之間作轉換。

一、string轉byte[]
byte[] buf = BitConverter.GetBytes(short.Parse(str));

二、byte[]轉string
System.Text.Encoding.Default.GetString(buf);

三、byte[]轉16進制的string

public static string ByteToString(byte[] InBytes)
    {
        string StringOut = "";
        foreach (byte InByte in InBytes)
        {
            StringOut = StringOut + String.Format("{0:X2}", InByte) + " ";
        }
        return StringOut.Trim();
    }

四、int 轉 string
str = i.ToString()

五、string轉int
UInt16 i= UInt16.Parse(str)

總結

通過這個軟件的練習,我對C#語言有必定的瞭解,須要多實踐,多編程。

C#語言和C++語言仍是有不少不同的地方,C#沒有指針,用的怪怪的,沒有從地址角度去考慮數據,數據容易管理很差,我的以爲。

最後一點就是學到了不少東西,文章也慢慢開始寫,須要多積累,多運用,纔是屬於本身的。

相關文章
相關標籤/搜索