公司給的一個小任務,這篇文章進行詳細講解編程
主要內容以下:
一、實現使用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#沒有指針,用的怪怪的,沒有從地址角度去考慮數據,數據容易管理很差,我的以爲。
最後一點就是學到了不少東西,文章也慢慢開始寫,須要多積累,多運用,纔是屬於本身的。