簡介
公司給的一個小任務,這篇文章進行詳細講解
題目: 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#沒有指針,用的怪怪的,沒有從地址角度去考慮數據,數據容易管理不好,個人覺得。
最后一點就是學到了很多東西,文章也慢慢開始寫,需要多積累,多運用,才是屬於自己的。