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 規則 : 

  1. 初始值為FFFFH
  2. 算初始值(FFFFH)與從站地址的XOR
  3. 將步驟2的結果向右移動1位。繼續移動直到剩餘的位為"1"。
  4. 剩餘的位為"1"後,利用上述步驟3的結果和A001H來計算XOR。
  5. 重複操作步驟3和4,直到右移8次。
  6. 利用步驟5的結果和該資訊的下一個資料(功能碼、暫存器位址、數據)來計算XOR。重複步驟3~5的計算,直到得出最後的資料。
  7. 最後的右移結果或者最後的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
 

 

下載範例程式

相關文章:

arrow
arrow
    文章標籤
    程式語言 RS232 MemoBus
    全站熱搜
    創作者介紹
    創作者 史克威爾凱特 的頭像
    史克威爾凱特

    史克威爾凱特的部落格

    史克威爾凱特 發表在 痞客邦 留言(1) 人氣()