modbus串口通訊C#


簡介

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

題目: modbus串口通訊

主要內容如下:
1、實現使用modbus通訊規約的測試軟件;
2、具有通信超時功能;
3、分主站從站,並能編輯報文、生成報文等;
4、計算發送報文次數,接收報文次數,失敗通信次數;
5、對接收的數據進行解析。

下面圖片可以看出具體的內容:

知識點講解

該小軟件使用的知識如下:
1、modbus通信規約;
2、串口通訊;
3、定時器;
4、多線程;

1、modbus通訊規約

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

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

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

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


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

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

功能號: 某指令是干啥,一目了然。接收方將通過功能號進行相應的執行功能。
下面為常用功能號:

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

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的器件,也就是說硬件稱為串行接口電路。

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

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

2、數據位:這是衡量通信中實際數據位的參數,當計算機發送一個信息包,實際的數據往往不會是8位,標准的是6、7和8位,標准的ASCII碼是0127(7位),擴展的ASCII碼是0255(8位),

3、停止位:用於表示單個包的最后幾位,典型的值為1,1.5和2位。作用就是數據在傳輸線上定時的,並且每一個有其自己的時鍾,很可能在通信中兩台設備出現不同步的情況,停止位可以解決這個問題,它不僅表示傳輸結束,還可以提供計算機矯正同步時鍾的機會。

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


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

1、加載串口配置

   #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

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

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次定時都還沒有將數據傳輸完畢,則認為數據傳輸完畢。

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事件;
    }

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

多線程

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

下面給出開啟線程的代碼

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

軟件思路

1、界面

由於需要做兩個軟件(主從站),我將兩個軟件融合在一起,使用選擇站點的方式進行開啟主站或者從站。 主站的界面和從站的界面很相似,為了讓用戶操作一致。

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

(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之間做轉換。

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

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

3、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();
    }

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

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

總結

經過這個軟件的練習,我對C#語言有一定的了解,需要多實踐,多編程。

C#語言和C++語言還是有很多不一樣的地方,C#沒有指針,用的怪怪的,沒有從地址角度去考慮數據,數據容易管理不好,個人覺得。

最后一點就是學到了很多東西,文章也慢慢開始寫,需要多積累,多運用,才是屬於自己的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM