2019.01.08 update
一般常用Modbus, Memobus、Memobus2.0 是安川自訂的協定(Protocal)
一般比較習慣用ModBus,今天遇到了MemoBus,
兩者有什麼不同呢?
最大的差別,大概就是ModBus有頭尾碼,MemoBus沒有頭尾碼。
對了!我習慣用0x或者數值後面加H來表示十六進制。
之前寫的ModBus頭[0x3A],尾[0x0D0A],
字元表示就是頭[ : ] ,尾[\r\n],
說人話就是頭[冒號],尾[換行符號],
還是不懂...就沒辦法了,
這種通訊協定的方式屬於半雙工,
送一個指令過去,會回一個指令回來,
至於回碼過程中送下一道指令會發生什麼事情,就只能請讀者回去試了。
安川變頻器A1000技術手冊中其實介紹的滿詳細的
A1000 Memobus通訊架構:
比較類似Modbus的RTU的協定
從站:1~31站 (0 在通訊上通常是廣播)
功能碼:
[傳送] 功能碼:03H(Read)、08H(Test)、10H(Write)
[回碼] 功能碼:03H、08H、10H,故障時回83H、89H、90H
一般而言故障碼通常是最高位元 ON,08H的故障回碼竟然是89H ... 家家有本難念的經 (誰是家家?)
錯誤校驗:CRC-16
什麼是CRC-16?可以看Wiki這篇:循環冗餘校驗
多項式為:
可得到0x8005 、0xA001(Memobus用)
可使用線上CRC-16計算,驗證程式計算的結果。
輸入完整個指令(不含校驗碼),
EX:01080000A537,CRC會得到8DDA
到此眼尖的人會問我...
1.為什麼HEX?而不是ASCII?
Q:我也想知道...
2.為什麼表格中高位是DA,低位是8D?
Q:手冊可能寫錯,或者我理解錯誤。
CRC 規則 :
- 初始值為FFFFH
- 算初始值(FFFFH)與從站地址的XOR
- 將步驟2的結果向右移動1位。繼續移動直到剩餘的位為"1"。
- 剩餘的位為"1"後,利用上述步驟3的結果和A001H來計算XOR。
- 重複操作步驟3和4,直到右移8次。
- 利用步驟5的結果和該資訊的下一個資料(功能碼、暫存器位址、數據)來計算XOR。重複步驟3~5的計算,直到得出最後的資料。
- 最後的右移結果或者最後的XOR計算值即為CRC-16的計算結果。
1.將指令用字串的方式儲存 "01080000A537" (我的習慣,不一定要這樣,這樣我方便顯示與傳送)
2.每兩個字組成1組 1Byte 的數值,十六進制字串,轉Byte,再進行CRC-16計算
為什麼要2個字?因為1Byte = 8bit,範圍是00H ~ FFH,暫存器慣用的最小單位。
C# 程式實作:
public static class CRC
{
public static String CRC_16(String Data)
{
//總Byte 數
int len = Data.Length / 2;
//初始值
ushort val = 0xffff;
//指令總Byte數
for (int i = 0; i < len; i++)
{
//每個Byte,但是跟WORD計算,WORD是2Byte,要轉同類型。
//互斥或上次的結果
val ^= (ushort)("0x" + Data.Substring(0 + 2 * i, 2)).ToInt();
//計算CRC-16
for (int j = 0; j < 8; j++)
{
//先取得餘數
int tmp = val & 0x1;
//再右移(除以2)
val >>= 1;
//如果餘1
if (tmp == 1)
{
//互斥或0XA001
val ^= 0xa001;
}
}
}
String StrCRC = ((int)val).ToHex(4);
//手冊寫順序是高低,但手冊上計算結果卻是低高。
return StrCRC.Substring(2, 2) + StrCRC.Substring(0, 2); ;
}
}
.ToInt() 我的函式,string to int, 開頭加"0x"表示要轉的字串為十六進制
.ToHex() 我的函是,int to 十六進制顯示的 string
什麼是RTU?
Remote Terminal Unit
白話的解釋:遠程終端單元,可以點超連結看Wiki怎麼解是,通常泛指PLC,但只要能夠通訊的設備都能稱之為RTU。
通訊的介面有些用串列(serial)、有些用網路(ethernet)。
其通訊的協定常見就Modbus了,
Modbus的編碼方式有兩種:ASCII、RTU。
假設今天有一個指令 :"01100001000102FFFF31A6",要放在Byte 陣列中,
傳送 ASCII 模式:Byte[0]=0x30, Byte[1]=0x31, Byte[2]=0x31 .... 依此類推,Size 22的Buffer,通常開頭會加0x31,LRC校驗,結尾會加0x0D, 0x0A。
傳送RTU模式:Byte[0]=0x01, Byte[1]=0x10, Byte[2]=0x00 .... 依此類推,Size 11的Buffer,通常沒有頭尾,常用CRC校驗。
CRC也是因此有分為ASCII模式與RTU(HEX)模式。
設計MemoBus的通訊軟體
超級懶人的設計方式是:
1.想傳就傳
2.用Timer一直收
天阿!怎麼能這麼懶!連執行序都省!
平凡的設計方式就是:
1.送出一道指令
2.等待送碼與回碼所需要的時間
3.開始讀回碼
但如果等待時間不夠充分,回碼是會有時對有時錯,效率很差。
我的設計方式:
1.準備一個陣列,想傳什麼指令通通丟到陣列排程。
2.清除通訊佇列中的資料。確保傳出與傳入的資料正確性。
3.傳送陣列中第一筆資料。
4.累加讀取的回碼直到超時或錯誤。
程式碼:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.IO.Ports;
using System.Windows.Forms;
using WAPI;
namespace A1000
{
public enum FunctionCode
{
Read = 0x03,
Test = 0x08,
Write = 0x10,
ReadError = 0x83,
TestError = 0x89,
WriteError = 0x90
}
public class TSerial
{
//----定義委派函式----
private delegate void SynchronizeMethod();
public delegate void OnReadEvent(object sender, String R_Cmd, String S_Cmd);
public delegate void OnTimeoutEvent(object sender, String R_Cmd, String S_Cmd);
public delegate void OnRetryEvent(object sender, String R_Cmd, String S_Cmd, int index);
public delegate void OnErrorEvent(object sender, String R_Cmd, String S_Cmd, int ErrorCode);
//----宣告事件(委派)函式----
public event OnReadEvent OnRead;
public event OnTimeoutEvent OnTimeout;
public event OnRetryEvent OnRetry;
public event OnErrorEvent OnError;
//屬性參數
public int Port = 1;
public int BaudRate=9600;
public int DataBit = 8;
public StopBits StopBit = StopBits.One;
public Parity Parity = Parity.None;
public int iTimeout = 1000;
public int Retry = 3;
//狀態
private bool bClose = false;
private bool bTimeout = false;
private int RetryCount = 0;
private bool bError = false;
private bool bStop = false;
private int iErrorCode = 0;
//通訊元件
public SerialPort serialPort = new SerialPort();
//執行序
private Thread SerialThread=null;
//要同部的對像、跟著釋放的對像
private Form Owner = null;
//同步用
private String R_Cmd;
private String S_Cmd;
//指令陣列
private List<String> CmdList = new List<String>();
//建構子
public TSerial(Form Owner)
{
this.Owner = Owner;
}
//讀取(執行序)
private void Execute()
{
while (true)
{
if (bClose) break;
if (bStop)
{
bStop = false;
bTimeout = false;
bError = false;
iErrorCode = 0;
CmdList.Clear();
RetryCount = 0;
}
//降CPU Load
Thread.Sleep(1);
//未開啟就不執行
if (!serialPort.IsOpen) continue;
//沒有資料要傳
if (CmdList.Count == 0) continue;
//清除佇列中的資料,可能會有上次殘留的部分
serialPort.DiscardInBuffer();
serialPort.DiscardOutBuffer();
//寫資料出去,一次只寫一筆
Write();
Thread.Sleep(50);
//等待回碼
Listent();
//超時
if (bTimeout)
{
//處理超時
Synchronize(TimeoutEvent);
}
//錯誤
else if (bError)
{
//處理錯誤
Synchronize(ErrorEvent);
}
//都正常的情況下
if (!bTimeout && !bError)
{
//處理回碼
Synchronize(ReadEvent);
}
else
{
//重傳
RetryCount++;
if (RetryCount <= Retry)
{
//處理重傳
Synchronize(RetryEvent);
Thread.Sleep(50);
continue;
}
RetryCount = 0;
iErrorCode = 3;//傳送多次失敗,放棄此指令
//處理錯誤
Synchronize(ErrorEvent);
}
//刪除指令
CmdList.RemoveAt(0);
}
}
//[事件]收到回碼 <同步模式,避免事件中使用到可視元件>
private void ReadEvent()
{
if (OnRead != null)
{
OnRead(this, R_Cmd, S_Cmd);
}
}
//[事件]錯誤 <同步模式,避免事件中使用到可視元件>
private void ErrorEvent()
{
if (OnError != null)
{
OnError(this, R_Cmd, S_Cmd, iErrorCode);
}
}
//[事件]超時 <同步模式,避免事件使用到可視元件>
private void TimeoutEvent()
{
if (OnTimeout != null)
{
OnTimeout(this, R_Cmd, S_Cmd);
}
}
//[事件]重傳 <同步模式,避免事件中使用到可視元件>
private void RetryEvent()
{
if (OnRetry != null)
{
OnRetry(this, R_Cmd, S_Cmd, RetryCount);
}
}
//等待回碼
private void Listent()
{
//int len = serialPort.ReadBufferSize;
byte[] buf = new byte[1];
String Msg = "";
bool bSlave=false;
bool bFunc=false;
bool bCount=false;
int iSlave=0;
int iFunc=0;
int iCount=0;
int iCmdLength = 0;
String strCRC = "";
//回碼開頭固定為 從站 + 功能碼
//再依功能碼分類
//回碼在CRC之後的不處理,可能是雜訊
while (true)
{
try
{
//讀取(未收到會等到超時才離開)
serialPort.Read(buf, 0, 1);
}
catch (TimeoutException)
{
//超時
bTimeout = true;
return;
}
//本次收到的回碼(可能是不完整的)
//String Tmp=Encoding.ASCII.GetString(buf, 0, len).Trim('\0');
//組合成完整的回碼
// Msg += Tmp;
Msg += ((int)buf[0]).ToHex(2);
R_Cmd = Msg;
//從站
if ((Msg.Length > 2) && !bSlave)
{
String strSlave = Msg.Substring(0, 2);
String checkSlave = CmdList[0].Substring(0,2);
if (strSlave != checkSlave)
{
iErrorCode = 2;
bError = true;
return;
}
bSlave = true;
iSlave = ("0x"+strSlave).ToInt();
if ((iSlave < 0) || (iSlave > 31))
{
iErrorCode = 2;
bError = true;
return;
}
}
//功能碼
if ((Msg.Length > 4) && !bFunc)
{
String strFunc = Msg.Substring(2, 2);
String checkFunc = CmdList[0].Substring(2,2);
if ((checkFunc == "03" && strFunc == "03") ||
(checkFunc == "03" && strFunc == "83") ||
(checkFunc == "08" && strFunc == "08") ||
(checkFunc == "08" && strFunc == "89") ||
(checkFunc == "10" && strFunc == "10") ||
(checkFunc == "10" && strFunc == "90"))
{
bFunc = true;
iFunc = ("0x" + strFunc).ToInt();
}
else
{
iErrorCode = 2;
bError = true;
return;
}
}
//讀取指令
if (iFunc == 0x03)
{
if ((Msg.Length > 6) && (!bCount))
{
String strLength = Msg.Substring(4, 2);
bCount = true;
iCount = ("0x" + strLength).ToInt() / 2;
}
//長度完整
if (Msg.Length >= (6+4*iCount+4))
{
iCmdLength = 6 + 4 * iCount;
strCRC = Msg.Substring(6 + 4 * iCount, 4);
break;
}
}
//錯誤碼
else if ((iFunc == 0x83) || (iFunc == 0x89) ||(iFunc == 0x90))
{
//長度完整
if (Msg.Length >= 10)
{
iCmdLength = 6;
strCRC = Msg.Substring(6, 4);
break;
}
}
//測試碼
else if (iFunc == 0x08)
{
//長度完整
if (Msg.Length >= 16)
{
iCmdLength = 12;
strCRC = Msg.Substring(12, 4);
break;
}
}
//寫入指令
else if (iFunc == 0x10)
{
//長度完整
if (Msg.Length >= 16)
{
iCmdLength = 12;
strCRC = Msg.Substring(12, 4);
break;
}
}
}
//計算收到的資料CRC檢查碼
String CalCRC = CRC.CRC_16(Msg.Substring(0, iCmdLength));
//判斷資料的檢查碼與收到的檢查碼是否一致
if (strCRC != CalCRC)
{
iErrorCode = 1;
bError = true;
return;
}
}
public void WriteData(int slave, int address, int data)
{
int[] arr_data=new int[1];
arr_data[0]=data;
WriteData(slave, address, arr_data, 1);
}
public void WriteData(int slave, int address, int[] data, int count)
{
String cmd = slave.ToHex(2) + "10" + address.ToHex(4) + count.ToHex(4) + (count*2).ToHex(2);
for (int i = 0; i < count; i++)
{
cmd += data[i].ToHex(4);
}
cmd += CRC.CRC_16(cmd);
CmdList.Add(cmd);
}
//加一筆指令(待傳送)
//String轉byte[]
public void AddCmd(String cmd)
{
CmdList.Add(cmd);
}
//將資料傳送出去
private void Write()
{
if (CmdList.Count > 0)
{
try
{
bTimeout = false;
iErrorCode = 0;
bError = false;
String Cmd = CmdList[0];
S_Cmd = Cmd;
R_Cmd = "";
int len = Cmd.Length / 2;
byte[] buf = new byte[len];
for (int i = 0; i < len; i++)
{
buf[i] = (byte)("0x" + Cmd.Substring(i * 2, 2)).ToInt();
}
serialPort.Write(buf, 0, len);
}
catch (Exception) { }
}
}
//停止傳送
public void Stop()
{
bStop = true;
}
//關閉通訊
public void Close()
{
bClose = true;
serialPort.Close();
}
//開啟通訊
public bool Open()
{
//COM Port
serialPort.PortName = "COM" + Port.ToString();
//Baud Rate
serialPort.BaudRate = this.BaudRate;
//Data Bit
serialPort.DataBits = this.DataBit;
//Stop Bit
serialPort.StopBits = this.StopBit;
//Parity
serialPort.Parity = this.Parity;
//Timeout
serialPort.ReadTimeout = iTimeout;
serialPort.WriteTimeout = iTimeout;
try
{
CmdList.Clear();
bTimeout = false;
bError=false;
iErrorCode = 0;
RetryCount = 0;
bClose = false;
serialPort.Open();
SerialThread = new Thread(Execute);
SerialThread.Start();
}
catch (Exception) { }
if (serialPort.IsOpen)
{
return true;
}
else
{
return false;
}
}
public bool IsOpen
{
get{return serialPort.IsOpen;}
}
//使用Invoke 呼叫函式
private void Synchronize(SynchronizeMethod Method)
{
while (!Owner.InvokeRequired)
{
}
Owner.Invoke(Method);
}
}//end class
}//end namespace
下載範例程式
相關文章:
留言列表