C# ModBus Tcp讀寫數據 與服務器進行通訊
前言
本文將使用一個NuGet公開的組件技術來實現一個ModBus TCP的客戶端,方便的對Modbus tcp的服務器進行讀寫,這個服務器可以是電腦端C#設計的,也可以是PLC實現的,也可以是其他任何支持這個通信協議的服務器。
在Visual Studio 中的NuGet管理器中可以下載安裝,也可以直接在NuGet控制台輸入下面的指令安裝:
1
|
Install-Package HslCommunication
|
NuGet安裝教程 http://www.cnblogs.com/dathlin/p/7705014.html
技術支持QQ群:592132877 (組件的版本更新細節也將第一時間在群里發布)組件API地址:http://www.cnblogs.com/dathlin/p/7703805.html
關於兩種模式
在PLC端,包括三菱和西門子篇二以及Modbus Tcp客戶端的訪問器上,都支持兩種模式,短連接模式和長連接模式,現在就來解釋下什么原理。
短連接:每次讀寫都是一個單獨的請求,請求完畢也就關閉了,如果服務器的端口僅僅支持單連接,那么關閉后這個端口可以被其他連接復用,但是在頻繁的網絡請求下,容易發生異常,會有其他的請求不成功,尤其是多線程的情況下。
長連接:創建一個公用的連接通道,所有的讀寫請求都利用這個通道來完成,這樣的話,讀寫性能更快速,即時多線程調用也不會影響,內部有同步機制。如果服務器的端口僅僅支持單連接,那么這個端口就被占用了,比如三菱的端口機制,西門子的Modbus tcp端口機制也是這樣的。以下代碼默認使用短連接,方便測試。
在短連接的模式下,每次請求都是單獨的訪問,所以沒有重連的困擾,在長連接的模式下,如果本次請求失敗了,在下次請求的時候,會自動重新連接服務器,直到請求成功為止。另外,盡量所有的讀寫都對結果的成功進行判斷。
特別感謝
- 網友:陳恩富 對float,int數據的讀取測試,才修復了權重位顛倒的BUG。
- 網友:U4幸福的蝸牛 發現了博客上錯誤的一個方法名稱,已於2018年1月8日13:34:39更新。並反饋了一些特殊設備(modbus tcp服務器)的讀取數據的BUG。已修復。
隨便聊聊
只要是網絡訪問,就會存在主從的區別,此處的設計模式是客戶端主動請求服務器數據,然后接收服務器的反饋數據,支持原生的指令收發,支持其他一些方便的API收發。特殊功能碼需要使用原生收發的API,本組件支持如下的功能操作:
- 0x01 讀取線圈的操作,
- 0x02 讀取離散的操作,
- 0x03 讀取寄存器的值,
- 0x05 寫一個線圈操作,
- 0x06 寫一個寄存器值,
- 0x0F 批量寫線圈操作,
- 0x10 批量寫寄存器值,
如果你的設備需要這些功能之外的數據,可以使用原生API方法,但是這個方法的前提就是你對MODBUS TCP協議非常清晰才可以,如果你不了解這個協議,可以參照下面的博客說明:
http://blog.csdn.net/thebestleo/article/details/52269999
如果你需要搭建自己的ModBus服務器,可以參照這邊文章:http://www.cnblogs.com/dathlin/p/7782315.html
在你開發自己的客戶端程序之前,可以先用MODBUS測試工具進行測試,以下地址的一個開源項目就是基於這個組件開發的Modbus tcp測試工具,可直接用於讀寫測試。
https://github.com/dathlin/ModBusTcpTools
訪問測試項目
下面的一個項目是這個組件的訪問測試項目,您可以進行初步的訪問的測試,免去了您寫測試程序的麻煩,這個項目是和三菱,西門子PLC的訪問寫在一起的。可以同時參考。
https://github.com/dathlin/HslCommunicationDemo
Reference
ModBus組件所有的功能類都在 HslCommunication.ModBus命名空間,所以再使用之前先添加
1
2
|
using
HslCommunication.ModBus;
using
HslCommunication;
|
How to Use
實例化:
在使用讀寫功能之前必須先進行實例化:
1
|
private
ModBusTcpClient busTcpClient =
new
ModBusTcpClient(
"192.168.1.195"
, 502, 0xFF);
// 站號255
|
上面的實例化指定了服務器的IP地址,端口號(一般都是502),以及自己的站號,允許設置為0-255,后面的兩個參數有默認值,在實例化的時候可以省略。
1
|
private
ModBusTcpClient busTcpClient =
new
ModBusTcpClient(
"192.168.1.195"
);
// 端口號502,站號0
|
上面兩個聲明選擇其中一個就行了。然后實例化之后(也可以放在窗體的Load方法中)就可以調用下面的方法切換為長連接了,
1
|
modBusTcpClient.ConnectServer();
|
以下代碼演示常用的讀寫操作,為了方便起見,不再對IsSuccess判斷,一般都是成功的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
private
void
userButton30_Click(
object
sender, EventArgs e)
{
// 讀取操作
bool
coil100 = busTcpClient.ReadBoolCoil(100).Content;
// 讀取線圈100的通斷
short
short100 = busTcpClient.ReadShortRegister(100).Content;
// 讀取寄存器100的short值
ushort
ushort100 = busTcpClient.ReadUShortRegister(100).Content;
// 讀取寄存器100的ushort值
int
int100 = busTcpClient.ReadIntRegister(100).Content;
// 讀取寄存器100-101的int值
uint
uint100 = busTcpClient.ReadUIntRegister(100).Content;
// 讀取寄存器100-101的uint值
float
float100 = busTcpClient.ReadFloatRegister(100).Content;
// 讀取寄存器100-101的float值
long
long100 = busTcpClient.ReadLongRegister(100).Content;
// 讀取寄存器100-103的long值
ulong
ulong100 = busTcpClient.ReadULongRegister(100).Content;
// 讀取寄存器100-103的ulong值
double
double100 = busTcpClient.ReadDoubleRegister(100).Content;
// 讀取寄存器100-103的double值
string
str100 = busTcpClient.ReadStringRegister(100, 5).Content;
// 讀取100到104共10個字符的字符串
// 寫入操作
busTcpClient.WriteOneCoil(100,
true
);
// 寫入線圈100為通
busTcpClient.WriteRegister(100, (
short
)12345);
// 寫入寄存器100為12345
busTcpClient.WriteRegister(100, (
ushort
)45678);
// 寫入寄存器100為45678
busTcpClient.WriteRegister(100, 123456789);
// 寫入寄存器100-101為123456789
busTcpClient.WriteRegister(100, (
uint
)123456778);
// 寫入寄存器100-101為123456778
busTcpClient.WriteRegister(100, 123.456);
// 寫入寄存器100-101為123.456
busTcpClient.WriteRegister(100, 12312312312414L);
//寫入寄存器100-103為一個大數據
busTcpClient.WriteRegister(100, 12634534534543656UL);
// 寫入寄存器100-103為一個大數據
busTcpClient.WriteRegister(100, 123.456d);
// 寫入寄存器100-103為一個雙精度的數據
busTcpClient.WriteRegister(100,
"K123456789"
);
}
|
下面再分別講解嚴格的操作,以及批量化的復雜的讀寫操作,假設你要讀取1000個M,循環讀取1千次可能要3秒鍾,如果用了下面的批量化讀取,只需要50ms,但是需要你對字節的原理比較熟悉才能得心應手的處理
讀取線圈API:
在此處舉例讀取地址為0,長度為10的線圈數量,但是需要注意的是,讀取出來的數據是byte[]類型的,還需要處理一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private
void
userButton8_Click(
object
sender,EventArgs e)
{
HslCommunication.OperateResult<
byte
[]> read = busTcpClient.ReadCoil(0, 10);
if
(read.IsSuccess)
{
// 共返回2個字節,以下展示手動處理位,分別獲取10和線圈的通斷情況
bool
coil_0 = (read.Content[0] & 0x01) == 0x01;
bool
coil_1 = (read.Content[0] & 0x02) == 0x02;
bool
coil_2 = (read.Content[0] & 0x04) == 0x04;
bool
coil_3 = (read.Content[0] & 0x08) == 0x08;
bool
coil_4 = (read.Content[0] & 0x10) == 0x10;
bool
coil_5 = (read.Content[0] & 0x20) == 0x20;
bool
coil_6 = (read.Content[0] & 0x40) == 0x40;
bool
coil_7 = (read.Content[0] & 0x80) == 0x80;
bool
coil_8 = (read.Content[1] & 0x01) == 0x01;
bool
coil_9 = (read.Content[1] & 0x02) == 0x02;
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
當然也可以用組件提供的數據轉換API實現數據提取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private
void
userButton9_Click(
object
sender, EventArgs e)
{
HslCommunication.OperateResult<
byte
[]> read = busTcpClient.ReadCoil(0, 10);
if
(read.IsSuccess)
{
// 共返回2個字節,一次性獲取所有節點的通斷
bool
[] result = HslCommunication.BasicFramework.SoftBasic.ByteToBoolArray(read.Content, 10);
bool
coil_0 = result[0];
bool
coil_1 = result[1];
bool
coil_2 = result[2];
bool
coil_3 = result[3];
bool
coil_4 = result[4];
bool
coil_5 = result[5];
bool
coil_6 = result[6];
bool
coil_7 = result[7];
bool
coil_8 = result[8];
bool
coil_9 = result[9];
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
讀取離散數據:
讀取離散數據和讀取線圈的代碼幾乎是一致的,處理方式也是一致的,只是方法名稱改成了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
private
void
userButton8_Click(
object
sender,EventArgs e)
{
HslCommunication.OperateResult<
byte
[]> read = busTcpClient.ReadDiscrete(0, 10);
if
(read.IsSuccess)
{
// 共返回2個字節,以下展示手動處理位,分別獲取10和線圈的通斷情況
bool
coil_0 = (read.Content[0] & 0x01) == 0x01;
bool
coil_1 = (read.Content[0] & 0x02) == 0x02;
bool
coil_2 = (read.Content[0] & 0x04) == 0x04;
bool
coil_3 = (read.Content[0] & 0x08) == 0x08;
bool
coil_4 = (read.Content[0] & 0x10) == 0x10;
bool
coil_5 = (read.Content[0] & 0x20) == 0x20;
bool
coil_6 = (read.Content[0] & 0x40) == 0x40;
bool
coil_7 = (read.Content[0] & 0x80) == 0x80;
bool
coil_8 = (read.Content[1] & 0x01) == 0x01;
bool
coil_9 = (read.Content[1] & 0x02) == 0x02;
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
private
void
userButton9_Click(
object
sender, EventArgs e)
{
HslCommunication.OperateResult<
byte
[]> read = busTcpClient.ReadDiscrete(0, 10);
if
(read.IsSuccess)
{
// 共返回2個字節,一次性獲取所有節點的通斷
bool
[] result = HslCommunication.BasicFramework.SoftBasic.ByteToBoolArray(read.Content, 10);
bool
coil_0 = result[0];
bool
coil_1 = result[1];
bool
coil_2 = result[2];
bool
coil_3 = result[3];
bool
coil_4 = result[4];
bool
coil_5 = result[5];
bool
coil_6 = result[6];
bool
coil_7 = result[7];
bool
coil_8 = result[8];
bool
coil_9 = result[9];
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
讀取寄存器數據:
假設我們需要讀取地址為0,長度為10的數據,也即是10個數據,每個數據2個字節,總計20個字節的數據。下面解析數據前,先進行了假設,你在解析自己的數據前可以參照下面的解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
private
void
userButton10_Click(
object
sender, EventArgs e)
{
HslCommunication.OperateResult<
byte
[]> read = busTcpClient.ReadRegister(0, 10);
if
(read.IsSuccess)
{
// 共返回20個字節,每個數據2個字節,高位在前,低位在后
// 在數據解析前需要知道里面到底存了什么類型的數據,所以需要進行一些假設:
// 前兩個字節是short數據類型
byte
[] buffer =
new
byte
[2];
buffer[0] = read.Content[1];
buffer[1] = read.Content[0];
short
value1 = BitConverter.ToInt16(buffer, 0);
// 接下來的2個字節是ushort類型
buffer =
new
byte
[2];
buffer[0] = read.Content[3];
buffer[1] = read.Content[2];
ushort
value2 = BitConverter.ToUInt16(buffer, 0);
// 接下來的4個字節是int類型
buffer =
new
byte
[4];
buffer[0] = read.Content[7];
buffer[1] = read.Content[6];
buffer[2] = read.Content[5];
buffer[3] = read.Content[4];
int
value3 = BitConverter.ToInt32(buffer, 0);
// 接下來的4個字節是float類型
buffer =
new
byte
[4];
buffer[0] = read.Content[11];
buffer[1] = read.Content[10];
buffer[2] = read.Content[9];
buffer[3] = read.Content[8];
float
value4 = BitConverter.ToSingle(buffer, 0);
// 接下來的全部字節,共8個字節是規格信息
string
speci = Encoding.ASCII.GetString(read.Content, 12, 8);
// 已經提取完所有的數據
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
寫一個線圈:
寫一個線圈,這個相對比較簡單,假設我們需要寫入線圈0,為通
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private
void
userButton11_Click(
object
sender, EventArgs e)
{
HslCommunication.OperateResult write = busTcpClient.WriteOneCoil(0,
true
);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
寫一個寄存器:
寫一個寄存器的操作也是非常的方便,在這里提供了三個重載的方法,允許使用三種方式寫入:分別寫入,short,ushort,byte三種:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private
void
userButton12_Click(
object
sender, EventArgs e)
{
short
value = -1234;
HslCommunication.OperateResult write = busTcpClient.WriteOneRegister(0, value);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private
void
userButton12_Click(
object
sender, EventArgs e)
{
ushort
value = 56713;
HslCommunication.OperateResult write = busTcpClient.WriteOneRegister(0, value);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private
void
userButton12_Click(
object
sender, EventArgs e)
{
// 0x00為高位,0x10為低位
HslCommunication.OperateResult write = busTcpClient.WriteOneRegister(0, 0x00, 0x10);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
批量寫入線圈:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
void
userButton13_Click(
object
sender, EventArgs e)
{
// 線圈0為True,線圈1為false,線圈2為true.....等等,以此類推,數組長度多少,就寫入多少線圈
bool
[] value =
new
bool
[] {
true
,
false
,
true
,
true
,
false
,
false
};
HslCommunication.OperateResult write = busTcpClient.WriteCoil(0, value);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
批量寫入寄存器:
第一種情況寫入一串short數組,這種情況比較簡單:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private
void
userButton14_Click(
object
sender, EventArgs e)
{
short
[] value =
new
short
[] { -1234, 467, 12345 };
HslCommunication.OperateResult write = busTcpClient.WriteRegister(0, value);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
第二情況寫入一串ushort數組,也是比較簡單:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private
void
userButton14_Click(
object
sender, EventArgs e)
{
ushort
[] value =
new
ushort
[] { 46789, 467, 12345 };
HslCommunication.OperateResult write = busTcpClient.WriteRegister(0, value);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
比較復雜的是寫入自定義的數據,按照上述讀取寄存器,解析的方式反着來就可以實現了,比如我需要寫入寄存器0,寄存器1共同組成的一個int數據,那么我們這么寫:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private
void
userButton15_Click(
object
sender, EventArgs e)
{
int
value = 12345678;
// 等待寫入的一個數據
byte
[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
// 這個是必須的
HslCommunication.OperateResult write = busTcpClient.WriteRegister(0, buffer);
if
(write.IsSuccess)
{
// 寫入成功
textBox1.Text =
"寫入成功"
;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
其他數據參考這個就行,如果有不明白的,可以聯系上面的QQ群。
模式切換(支持熱切換,想什么時候切換都可以):
上面默認都是使用短連接的機制,如果需要使用長連接的話,這種通訊模式更加穩定。多線程已經同步。
1
2
3
4
|
private
void
userButton11_Click(
object
sender, EventArgs e)
{
modBusTcpClient.ConnectServer();
}
|
執行完這一行代碼后,一般在實例化后面就可以切換長連接了,會返回一個OperateResult對象,連接成功IsSuccess為True,后面所有的讀寫操作都調用同一個通信通道。如果想要切換回短連接。
1
|
modBusTcpClient.ConnectClose();
|
究極數據操作,使用原生的報文來操作數據:
傳入一個字節數組,數據內容和原生的數據一致,比如我要通過原生API讀取寄存器地址為0,長度為3的數據,那么字節的HEX標識形式為 00 00 00 00 00 06 00 03 00 00 00 03
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
void
userButton2_Click(
object
sender, EventArgs e)
{
byte
[] data = HslCommunication.BasicFramework.SoftBasic.HexStringToBytes(
"00 00 00 00 00 06 00 03 00 00 00 03"
);
HslCommunication.OperateResult<
byte
[]> read = busTcpClient.ReadFromServerCore(data);
if
(read.IsSuccess)
{
// 獲取結果,並轉化為Hex字符串,方便顯示
string
result = HslCommunication.BasicFramework.SoftBasic.ByteToHexString(read.Content,
' '
);
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
上述代碼在操作時用了一個轉化機制,輸入為十六進制的文本,轉化為byte[]數據,中間的分割符可以為空格,可以為'-',也可以為',','_'等等等等,調用了組件基礎的數據轉化功能。