還記得 C# 串列傳輸 這篇文章嗎? 今天要用它來繼續教學
其實要通訊,要注意的事項挺多的
設備端站號、鮑率、停止位元、資料位元、同位元檢查、ASCII/RTU,都要查清楚目前的設定,腳位是否接的正確。
電腦這端要注意serialPort物件的屬性是否也都跟設備一致,只要有一個環節弄錯就會通不起來,這些是需要經驗的累積。
我們就假設:
PortName:COM1
BaudRate : 9600
DataBits:8
StopBits:One
Parity:None
設備端也是以上設定,且:
站號:1
通訊協定:Modbus ASCII
接線:RS485/RS422
這是一個比較簡單的開始
上次在Form 拉了一個 serialPort 設定好屬性(PortName、BaudRate、DataBits、StopBits、Parity)
還有ComboBox選通訊埠、Button 開啟通訊埠、最後是接收。
這次只要先拉一個Button 做測試,輸入以下程式碼:
private void button2_Click(object sender, EventArgs e)
{
if (!serialPort1.IsOpen) return;
try
{
string cmd = ":010300000002FA\r\n";
Byte[] buf = Encoding.ASCII.GetBytes(cmd);
serialPort1.Write(buf, 0, buf.Length);
textBox1.AppendText(cmd);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
複習一下
ModBus ASCII格式,會有表頭、從站(Slave)、功能(Function)、暫存器位址(Address)、長度、資料、檢查碼、表尾。
ModBus ASCII 的檢查碼是 LRC,取1的補數
今天我們要送出一個讀取的指令,而讀取的功能碼是03,因此:
表頭是冒號 ":" (0x3A)、
站號是 "01" (0x30 0x31)、
功能碼是 "03" (0x30 0x33)、
位址是 "0000" (0x30 0x30 0x30 0x30)、
連續長度是 "0002" (0x30 0x30 0x30 0x32)、
檢查碼計算得到 "FA" (0x46 0x42)、
表尾是換行符號 "\r\n" (0x0D 0x0A)
LRC檢查碼計算方式 01 + 03 + 00 + 00 + 00 + 02 = 06 取 1的補數 = FA
因此上面程式碼,我直接先建立一個字串並給指令做測試,
因為是ASCII的格式,我只需要透過微軟內建的方法,將它轉成Byte[] 就行了。
直接將指令傳出去,設備收到後自動就會回碼了。
Modbus ASCII 講完了,是不是很簡單?
接下來困難的來了
ModBus RTU格式,只有從站(Slave)、功能(Function)、暫存器位址(Address)、長度、資料、檢查碼。跟ASCII比起來少了表頭表尾。
Modbus RTU 的檢查碼是 CRC,循環冗餘計算...是個很麻煩的東西... 在 Modbus簡介 有簡單敘述,至於要寫成程式就麻煩了。
解說一下 ASCII 與 RTU 格式的資料差異
比方說我們的資料 00 :
ASCII 視為字串 "01",轉換成Byte 資料 要對照 ASCII表,最後變成Byte數值 { 0x30, 0x31 }
RTU 視為數值 01,直接處理 { 0x01 }
將上面那段ASCII的程式碼修改一下:
private void button3_Click(object sender, EventArgs e)
{
if (!serialPort1.IsOpen) return;
try
{
Byte[] buf = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B};
serialPort1.Write(buf, 0, buf.Length);
foreach(Byte b in buf) textBox1.AppendText(b.ToString("X2"));
textBox1.AppendText("\r\n");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
我們先設計一個欄位,輸入後,按下傳送,將指令丟出去
所以運算校驗碼固定都是傳入字串,到了要serialPort1.Write前,再判斷Modbus 模式(ASCII/RTU)後,要將指令轉成 ASCII數值 還是 16進制數值。
//傳入資料EX: "010300000002"
public String LRC(String Data)
{
//兩兩一組 01 03 00 00 00 02
int count = Data.Length / 2;
//計算總和 01 + 03 + 00 + 00 + 00 + 02
Byte sum = 0;
for (int i = 0; i < count; i++)
{
//讀取兩個
String SubMsg = Data.Substring(i * 2, 2);
//16進制字串轉成16進制數值後加總
sum += Byte.Parse(SubMsg, System.Globalization.NumberStyles.HexNumber);
}
//取1的補數
sum = (Byte)(sum * -1);
//將得到的校驗碼轉成16進制字串
return sum.ToString("X2");
}
//傳入資料EX: "010300000002"
public String CRC(String Data)
{
//總Byte 數
int len = Data.Length / 2;
//初始值
ushort val = 0xffff;
//指令總Byte數
for (int i = 0; i < len; i++)
{
//每個Byte
//互斥或上次的結果
val ^= UInt16.Parse(Data.Substring(0 + 2 * i, 2), System.Globalization.NumberStyles.HexNumber);
//計算CRC-16
for (int j = 0; j < 8; j++)
{
//先取得餘數
int tmp = val & 0x1;
//再右移
val >>= 1;
//如果餘1
if (tmp == 1)
{
//互斥或0XA001
val ^= 0xa001;
}
}
}
String StrCRC = ((int)val).ToString("X4");
//Low Byte 先放
return StrCRC.Substring(2, 2) + StrCRC.Substring(0, 2); ;
}
講完了,是不是很簡單?
接下來困難的部分就交給各位讀者、各為碼農們了。