前言
本文將使用一個NuGet公開的組件技術(shù)來實現(xiàn)一個ModBus TCP的客戶端,方便的對Modbus tcp的服務(wù)器進行讀寫,這個服務(wù)器可以是電腦端C#設(shè)計的,也可以是PLC實現(xiàn)的,也可以是其他任何支持這個通信協(xié)議的服務(wù)器。
github地址:https://github.com/dathlin/HslCommunication 如果喜歡可以star或是fork,還可以打賞支持。
在Visual Studio 中的NuGet管理器中可以下載安裝,也可以直接在NuGet控制臺輸入下面的指令安裝:
1 | Install-Package HslCommunication
|
NuGet安裝教程 http://www.cnblogs.com/dathlin/p/7705014.html
技術(shù)支持QQ群:群1:592132877(滿) 群2:948305931 (組件的版本更新細(xì)節(jié)也將第一時間在群里發(fā)布)組件API地址:http://www.cnblogs.com/dathlin/p/7703805.html
關(guān)于兩種模式
在PLC端,包括三菱和西門子,歐姆龍以及Modbus Tcp客戶端的訪問器上,都支持兩種模式,短連接模式和長連接模式,現(xiàn)在就來解釋下什么原理。
短連接:每次讀寫都是一個單獨的請求,請求完畢也就關(guān)閉了,如果服務(wù)器的端口僅僅支持單連接,那么關(guān)閉后這個端口可以被其他連接復(fù)用,但是在頻繁的網(wǎng)絡(luò)請求下,容易發(fā)生異常,會有其他的請求不成功,尤其是多線程的情況下。
長連接:創(chuàng)建一個公用的連接通道,所有的讀寫請求都利用這個通道來完成,這樣的話,讀寫性能更快速,即時多線程調(diào)用也不會影響,內(nèi)部有同步機制。如果服務(wù)器的端口僅僅支持單連接,那么這個端口就被占用了,比如三菱的端口機制,西門子的Modbus tcp端口機制也是這樣的。以下代碼默認(rèn)使用短連接,方便測試。
在短連接的模式下,每次請求都是單獨的訪問,所以沒有重連的困擾,在長連接的模式下,如果本次請求失敗了,在下次請求的時候,會自動重新連接服務(wù)器,直到請求成功為止。另外,盡量所有的讀寫都對結(jié)果的成功進行判斷。
特別感謝
- 網(wǎng)友:陳恩富 對float,int數(shù)據(jù)的讀取測試,才修復(fù)了權(quán)重位顛倒的BUG。
- 網(wǎng)友:U4幸福的蝸牛 發(fā)現(xiàn)了博客上錯誤的一個方法名稱,已于2018年1月8日13:34:39更新。并反饋了一些特殊設(shè)備(modbus tcp服務(wù)器)的讀取數(shù)據(jù)的BUG。已修復(fù)。
隨便聊聊
只要是網(wǎng)絡(luò)訪問,就會存在主從的區(qū)別,此處的設(shè)計模式是客戶端主動請求服務(wù)器數(shù)據(jù),然后接收服務(wù)器的反饋數(shù)據(jù),支持原生的指令收發(fā),支持其他一些方便的API收發(fā)。特殊功能碼需要使用原生收發(fā)的API,本組件支持如下的功能操作:
- 0x01 讀取線圈的操作,
- 0x02 讀取離散的操作,
- 0x03 讀取寄存器的值,
- 0x05 寫一個線圈操作,
- 0x06 寫一個寄存器值,
- 0x0F 批量寫線圈操作,
- 0x10 批量寫寄存器值,
如果你的設(shè)備需要這些功能之外的數(shù)據(jù),可以使用原生API方法,但是這個方法的前提就是你對MODBUS TCP協(xié)議非常清晰才可以,如果你不了解這個協(xié)議,可以參照下面的博客說明:
http://blog.csdn.net/thebestleo/article/details/52269999
如果你需要搭建自己的ModBus服務(wù)器,可以參照這邊文章:http://www.cnblogs.com/dathlin/p/7782315.html
在你開發(fā)自己的客戶端程序之前,可以先用MODBUS測試工具進行測試,以下地址的一個開源項目就是基于這個組件開發(fā)的Modbus tcp測試工具,可直接用于讀寫測試。
ModbusTcpServer.zip
訪問測試項目
下面的一個項目是這個組件的訪問測試項目,您可以進行初步的訪問的測試,免去了您寫測試程序的麻煩,這個項目是和三菱,西門子PLC的訪問寫在一起的??梢酝瑫r參考。
下載地址為:HslCommunicationDemo.zip

Reference
ModBus組件所有的功能類都在 HslCommunication.ModBus命名空間,所以再使用之前先添加
1 2 | using HslCommunication.ModBus;
using HslCommunication;
|
How to Use
實例化:
在使用讀寫功能之前必須先進行實例化:
1 | private ModbusTcpNet busTcpClient = new ModbusTcpNet( "192.168.1.195" , 502, 0x01); // 站號1
|
上面的實例化指定了服務(wù)器的IP地址,端口號(一般都是502),以及自己的站號,允許設(shè)置為0-255,后面的兩個參數(shù)有默認(rèn)值,在實例化的時候可以省略。
1 | private ModbusTcpNet busTcpClient = new ModbusTcpNet( "192.168.1.195" ); // 端口號502,站號1
|
注意:在Modbus服務(wù)器的設(shè)備里,大部分的設(shè)備都是從地址0開始的,有些特殊的設(shè)備是從地址1開始的,所以本組件里面,默認(rèn)從地址0開始,如果想要從地址1開始,那么就需要如下的配置:
1 | busTcpClient.AddressStartWithZero = False;
|
上面兩個聲明選擇其中一個就行了。然后實例化之后(也可以放在窗體的Load方法中)就可以調(diào)用下面的方法切換為長連接了,
1 | busTcpClient.ConnectServer();
|
關(guān)閉的話,調(diào)用如下的方法
1 | busTcpClient.ConnectClose( );
|
以下代碼演示常用的讀寫操作,為了方便起見,不再對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.ReadCoil( "100" ).Content; // 讀取線圈100的通斷
short short100 = busTcpClient.ReadInt16( "100" ).Content; // 讀取寄存器100的short值
ushort ushort100 = busTcpClient.ReadUInt16( "100" ).Content; // 讀取寄存器100的ushort值
int int100 = busTcpClient.ReadInt32( "100" ).Content; // 讀取寄存器100-101的int值
uint uint100 = busTcpClient.ReadUInt32( "100" ).Content; // 讀取寄存器100-101的uint值
float float100 = busTcpClient.ReadFloat( "100" ).Content; // 讀取寄存器100-101的float值
long long100 = busTcpClient.ReadInt64( "100" ).Content; // 讀取寄存器100-103的long值
ulong ulong100 = busTcpClient.ReadUInt64( "100" ).Content; // 讀取寄存器100-103的ulong值
double double100 = busTcpClient.ReadDouble( "100" ).Content; // 讀取寄存器100-103的double值
string str100 = busTcpClient.ReadString( "100" , 5).Content; // 讀取100到104共10個字符的字符串
// 寫入操作
busTcpClient.WriteCoil( "100" , true ); // 寫入線圈100為通
busTcpClient.Write( "100" , ( short )12345); // 寫入寄存器100為12345
busTcpClient.Write( "100" , ( ushort )45678); // 寫入寄存器100為45678
busTcpClient.Write( "100" , 123456789); // 寫入寄存器100-101為123456789
busTcpClient.Write( "100" , ( uint )123456778); // 寫入寄存器100-101為123456778
busTcpClient.Write( "100" , 123.456); // 寫入寄存器100-101為123.456
busTcpClient.Write( "100" , 12312312312414L); //寫入寄存器100-103為一個大數(shù)據(jù)
busTcpClient.Write( "100" , 12634534534543656UL); // 寫入寄存器100-103為一個大數(shù)據(jù)
busTcpClient.Write( "100" , 123.456d); // 寫入寄存器100-103為一個雙精度的數(shù)據(jù)
busTcpClient.Write( "100" , "K123456789" );
}
|
下面再分別講解嚴(yán)格的操作,以及批量化的復(fù)雜的讀寫操作,假設(shè)你要讀取1000個M,循環(huán)讀取1千次可能要3秒鐘,如果用了下面的批量化讀取,只需要50ms,但是需要你對字節(jié)的原理比較熟悉才能得心應(yīng)手的處理
讀取線圈API:
在此處舉例讀取地址為0,長度為10的線圈數(shù)量,讀取出來的數(shù)據(jù)已經(jīng)自動轉(zhuǎn)化成了bool數(shù)組,方便的進行二次處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void userButton8_Click( object sender,EventArgs e)
{
HslCommunication.OperateResult< bool []> read = busTcpClient.ReadCoil( "0" , 10);
if (read.IsSuccess)
{
bool coil_0 = read.Content[0];
bool coil_1 = read.Content[1];
bool coil_2 = read.Content[2];
bool coil_3 = read.Content[3];
bool coil_4 = read.Content[4];
bool coil_5 = read.Content[5];
bool coil_6 = read.Content[6];
bool coil_7 = read.Content[7];
bool coil_8 = read.Content[8];
bool coil_9 = read.Content[9];
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
當(dāng)然也可以用組件提供的數(shù)據(jù)轉(zhuǎn)換API實現(xiàn)數(shù)據(jù)提?。?/p>
讀取離散數(shù)據(jù):
讀取離散數(shù)據(jù)和讀取線圈的代碼幾乎是一致的,處理方式也是一致的,只是方法名稱改成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void userButton8_Click( object sender,EventArgs e)
{
HslCommunication.OperateResult< bool []> read = busTcpClient.ReadDiscrete( "0" , 10);
if (read.IsSuccess)
{
bool coil_0 = read.Content[0];
bool coil_1 = read.Content[1];
bool coil_2 = read.Content[2];
bool coil_3 = read.Content[3];
bool coil_4 = read.Content[4];
bool coil_5 = read.Content[5];
bool coil_6 = read.Content[6];
bool coil_7 = read.Content[7];
bool coil_8 = read.Content[8];
bool coil_9 = read.Content[9];
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
讀取寄存器數(shù)據(jù):
假設(shè)我們需要讀取地址為0,長度為10的數(shù)據(jù),也即是10個數(shù)據(jù),每個數(shù)據(jù)2個字節(jié),總計20個字節(jié)的數(shù)據(jù)。下面解析數(shù)據(jù)前,先進行了假設(shè),你在解析自己的數(shù)據(jù)前可以參照下面的解析
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 | private void userButton10_Click( object sender, EventArgs e)
{
HslCommunication.OperateResult< byte []> read = busTcpClient.Read( "0" , 10);
if (read.IsSuccess)
{
// 共返回20個字節(jié),每個數(shù)據(jù)2個字節(jié),高位在前,低位在后
// 在數(shù)據(jù)解析前需要知道里面到底存了什么類型的數(shù)據(jù),所以需要進行一些假設(shè):
// 前兩個字節(jié)是short數(shù)據(jù)類型
short value1 = busTcpClient.ByteTransform.TransInt16(read.Content, 0);<br>
// 接下來的2個字節(jié)是ushort類型
ushort value2 = busTcpClient.ByteTransform.TransUInt16(read.Content, 2);<br>
// 接下來的4個字節(jié)是int類型
int value3 = busTcpClient.ByteTransform.TransInt32(read.Content, 4);<br>
// 接下來的4個字節(jié)是float類型
float value4 = busTcpClient.ByteTransform.TransFloat(read.Content, 8);<br>
// 接下來的全部字節(jié),共8個字節(jié)是規(guī)格信息
string speci = Encoding.ASCII.GetString(read.Content, 12, 8);
// 已經(jīng)提取完所有的數(shù)據(jù)
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
寫一個線圈:
寫一個線圈,這個相對比較簡單,假設(shè)我們需要寫入線圈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.WriteCoil( "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.....等等,以此類推,數(shù)組長度多少,就寫入多少線圈
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數(shù)組,這種情況比較簡單:
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.Write( "0" , value);
if (write.IsSuccess)
{
// 寫入成功
textBox1.Text = "寫入成功" ;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
第二情況寫入一串ushort數(shù)組,也是比較簡單:
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.Write( "0" , value);
if (write.IsSuccess)
{
// 寫入成功
textBox1.Text = "寫入成功" ;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
比較復(fù)雜的是寫入自定義的數(shù)據(jù),按照上述讀取寄存器,比如我需要寫入寄存器0,寄存器1共同組成的一個int數(shù)據(jù),那么我們這么寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private void userButton15_Click( object sender, EventArgs e)
{
int value = 12345678; // 等待寫入的一個數(shù)據(jù)
HslCommunication.OperateResult write = busTcpClient.Write( "0" , value);
if (write.IsSuccess)
{
// 寫入成功
textBox1.Text = "寫入成功" ;
}
else
{
MessageBox.Show(write.ToMessageShowString());
}
}
|
其他數(shù)據(jù)參考這個就行,如果有不明白的,可以聯(lián)系上面的QQ群。
模式切換(支持熱切換,想什么時候切換都可以):
上面默認(rèn)都是使用短連接的機制,如果需要使用長連接的話,這種通訊模式更加穩(wěn)定。多線程已經(jīng)同步。
1 2 3 4 | private void userButton11_Click( object sender, EventArgs e)
{
modBusTcpClient.ConnectServer();
}
|
執(zhí)行完這一行代碼后,一般在實例化后面就可以切換長連接了,會返回一個OperateResult對象,連接成功IsSuccess為True,后面所有的讀寫操作都調(diào)用同一個通信通道。如果想要切換回短連接。
1 | modBusTcpClient.ConnectClose();
|
究極數(shù)據(jù)操作,使用原生的報文來操作數(shù)據(jù):
傳入一個字節(jié)數(shù)組,數(shù)據(jù)內(nèi)容和原生的數(shù)據(jù)一致,比如我要通過原生API讀取寄存器地址為0,長度為3的數(shù)據(jù),那么字節(jié)的HEX標(biāo)識形式為 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.ReadFromCoreServer(data);
if (read.IsSuccess)
{
// 獲取結(jié)果,并轉(zhuǎn)化為Hex字符串,方便顯示
string result = HslCommunication.BasicFramework.SoftBasic.ByteToHexString(read.Content, ' ' );
}
else
{
MessageBox.Show(read.ToMessageShowString());
}
}
|
上述代碼在操作時用了一個轉(zhuǎn)化機制,輸入為十六進制的文本,轉(zhuǎn)化為byte[]數(shù)據(jù),中間的分割符可以為空格,可以為'-',也可以為',','_'等等等等,調(diào)用了組件基礎(chǔ)的數(shù)據(jù)轉(zhuǎn)化功能。
|