本文將使用一個Github開源的組件庫技術來讀寫三菱PLC和西門子plc數據,使用的是基於以太網的TCP/IP實現,不需要額外的組件,讀取操作只要放到后台線程就不會卡死線程,本組件支持超級方便的高性能讀寫操作
本項目目前支持C#語言和java語言,C#語言的功能比較齊全,java版本的庫還在開發及完善中。
C# 版本nuget地址:https://www.nuget.org/packages/HslCommunication/
![]()
C# 版本github地址:https://github.com/dathlin/HslCommunication
如果喜歡可以star或是fork,還可以打賞支持。
聯系作者及加群方式:http://www.hslcommunication.cn/Cooperation
Java版本的Demo代碼 https://github.com/dathlin/HslCommunicationJavaDemo
在Maven里搜索 HslCommunication 即可。
com.github.dathlin:HslCommunication
本文將展示如何配置網絡參數及怎樣使用代碼來訪問PLC數據,希望給有需要的人解決一些實際問題。主要對三菱Q系列PLC的X,Y,M,L,B,V,F,S,D,W,R區域的數據讀寫,對西門子PLC的M,Q,I,DB塊的數據讀寫,親測有效。
此處使用了網線直接的方式,如果PLC接進了局域網,就可以進行遠程讀寫了^_^
此處使用到了2個命名空間:
import HslCommunication.Core.Types.OperateResultExOne;
import HslCommunication.Profinet.Melsec.MelsecMcNet;
訪問測試項目
下載JAVA版本的Demo源代碼,然后運行即可,當然你也可以使用C#版本的來測試。
https://github.com/dathlin/HslCommunicationJavaDemo

隨便聊聊
當我們一個上位機需要讀取100台西門子PLC設備(此處只是舉個例子,凡是都是使用Modbus tcp的都是一樣的)的時候,你采用服務器主動去請求100台設備的機制對性能來說是個極大的考驗,如果開100個線程去輪詢100台設備,那么性能損失將是非常大的,更不用說再增加設備,如果搭建Modbus tcp服務器,就可以完美的解決性能問題,因為連接的壓力將會平均分攤給每一台PLC,服務器端只要新增一個時間戳就可以知道客戶端有沒有連接上。
我們在100台PLC里都增加發送Modbus tcp方法,將數據發送到服務器的ip和端口上去,服務器根據站號來區分設備。這樣就可以搭建一個高性能總站。 本組件支持快速搭建一個高性能的Modbus tcp總站。
http://www.cnblogs.com/dathlin/p/7782315.html
關於兩種模式
在PLC端,包括三菱,西門子,歐姆龍以及Modbus Tcp客戶端的訪問器上,都支持兩種模式,短連接模式和長連接模式,現在就來解釋下什么原理。
短連接:每次讀寫都是一個單獨的請求,請求完畢也就關閉了,如果服務器的端口僅僅支持單連接,那么關閉后這個端口可以被其他連接復用,但是在頻繁的網絡請求下,容易發生異常,會有其他的請求不成功,尤其是多線程的情況下。
長連接:創建一個公用的連接通道,所有的讀寫請求都利用這個通道來完成,這樣的話,讀寫性能更快速,即時多線程調用也不會影響,內部有同步機制。如果服務器的端口僅僅支持單連接,那么這個端口就被占用了,比如三菱的端口機制,西門子的Modbus tcp端口機制也是這樣的。以下代碼默認使用長連接,性能更高,還支持多線程同步。
在短連接的模式下,每次請求都是單獨的訪問,所以沒有重連的困擾,在長連接的模式下,如果本次請求失敗了,在下次請求的時候,會自動重新連接服務器,直到請求成功為止。另外,盡量所有的讀寫都對結果的成功進行判斷。
關於日志記錄
暫時不支持
PLC的配置
環境1:此處以GX Works3為示例,fx5u的配置如下:(感謝 山楂 提供的圖片)


環境2:此處以GX Works2為示例,測試PLC為L02CPU,內置了以太網協議


環境3:此處以GX Works2為示例,添加以太網模塊,型號為QJ71E71-100,組態里添加完成后進行以太網的參數配置,此處需要注意的是:參數的配置對接下來的代碼中配置參數要一一對應

注意:在PLC的以太網模塊的配置中,無法設置網絡號為0,也無法設置站號為0, 所以此處均設置為1,在C#程序中也使用上述的配置,在代碼中均配置為0,如果您自定義設置為網絡2, 站號8,那么在代碼中就要寫對應的數據。如果仍然通信失敗,重新測試0,0。
打開設置:在上圖中的打開設置選項,進行其他參數的配置,下圖只是舉了一個例子,開通了4個端口來支持讀寫操作:
端口號設置規則:
- 為了不與原先存在的系統發生沖突,您在添加自己的端口時盡量使用您自己的端口。
- 如果讀寫都需要,盡可能的將讀取端口和寫入端口區分開來,這樣做比較高性能。
- 如果您的網絡狀態不是特別穩定,讀取端口使用2個,一個受阻切換另一個讀取可以提升系統的穩定性。
本文檔僅作組件的測試,所以只用了一個端口作為讀寫。如果你的程序也使用了一個端口,那么你在讀取數據時候, 剛好也在寫入(異步操作可能發生這樣的情況),那么寫入會失敗!)(在長連接模式下沒有這個問題)
三菱PLC的數據主要由兩類數據組成,位數據和字數據,在位數據中,例如X,Y,M,L都是位數據,字數據例如D,W。 兩類的數據在讀取解碼上存在一點小差別。(事實上也可以先將16個M先賦值給一個D,讀取D數據再進行解析, 在讀取M的數量比較多的時候,這樣操作效率更高)
初始化訪問PLC對象
如果想使用本組件的數據讀取功能,必須先初始化數據訪問對象,根據實際情況進行數據的填入。 下面僅僅是測試中的數據:
MelsecMcNet melsec_net = new MelsecMcNet("192.168.1.192",6001);
然后你可以指定一些參數,網絡號,網絡站號之類的,通常的情況都是不需要指定的
melsec_net.setNetworkNumber((byte) 0x00);
melsec_net.setNetworkStationNumber((byte) 0x00);
melsec_net.setConnectTimeOut(1000);
打開連接
melsec_net.ConnectServer();
如果想知道有沒有連接上去
OperateResult connectResult = melsec_net.ConnectServer();
if(connectResult.IsSuccess){
System.out.print("連接成功");
}
else {
System.out.print("連接失敗:"+connectResult.Message);
}
關於地址的表示方式
使用字符串表示,這個組件里所有的讀寫操作提供字符串表示的重載方法,所有的支持訪問的類型對應如下,字符串的表示方式存在十進制和十六進制的區別:
- 輸入繼電器:"X100","X1A0" // 字符串為十六進制機制
- 輸出繼電器:"Y100" ,"Y1A0" // 字符串為十六進制機制
- 內部繼電器:"M100","M200" // 字符串為十進制
- 鎖存繼電器:"L100" ,"L200" // 字符串為十進制
- 報警器: "F100", "F200" // 字符串為十進制
- 邊沿繼電器:"V100" , "V200" // 字符串為十進制
- 鏈接繼電器:"B100" , "B1A0" // 字符串為十六進制
- 步進繼電器:"S100" , "S200" // 字符串為十進制
- 數據寄存器:"D100", "D200" // 字符串為十進制
- 鏈接寄存器:"W100" ,"W1A0" // 字符串為十六進制
- 文件寄存器:"R100","R200" // 字符串為十進制
關於數據分類
以上地址的數據是分為位數據和字數據的,位數據只能調用ReadBool,字數據用Read及其擴展的方法
簡單讀寫的示例
boolean[] M100 = melsec_net.ReadBool("M100",(short) 1).Content; // 讀取M100是否通,十進制地址
boolean[] X1A0 = melsec_net.ReadBool("X1A0",(short) 1).Content; // 讀取X1A0是否通,十六進制地址
boolean[] Y1A0 = melsec_net.ReadBool("Y1A0",(short) 1).Content; // 讀取Y1A0是否通,十六進制地址
boolean[] B1A0 = melsec_net.ReadBool("B1A0",(short) 1).Content; // 讀取B1A0是否通,十六進制地址
short short_D1000 = melsec_net.ReadInt16("D1000").Content; // 讀取D1000的short值 ,W3C0,R3C0 效果是一樣的
int int_D1000 = melsec_net.ReadInt32("D1000").Content; // 讀取D1000-D1001組成的int數據
float float_D1000 = melsec_net.ReadFloat("D1000").Content; // 讀取D1000-D1001組成的float數據
long long_D1000 = melsec_net.ReadInt64("D1000").Content; // 讀取D1000-D1003組成的long數據
double double_D1000 = melsec_net.ReadDouble("D1000").Content; // 讀取D1000-D1003組成的double數據
String str_D1000 = melsec_net.ReadString("D1000", (short) 10).Content; // 讀取D1000-D1009組成的條碼數據
melsec_net.Write("M100", new boolean[] { true} ); // 寫入M100為通
melsec_net.Write( "Y1A0", new boolean[] { true } ); // 寫入Y1A0為通
melsec_net.Write( "X1A0", new boolean[] { true } ); // 寫入X1A0為通
melsec_net.Write( "B1A0", new boolean[] { true } ); // 寫入B1A0為通
melsec_net.Write( "D1000", (short)1234); // 寫入D1000 short值 ,W3C0,R3C0 效果是一樣的
melsec_net.Write( "D1000", 1234566); // 寫入D1000 int值
melsec_net.Write( "D1000", 123.456f); // 寫入D1000 float值
melsec_net.Write( "D1000", 123.456d); // 寫入D1000 double值
melsec_net.Write( "D1000", 123456661235123534L); // 寫入D1000 long值
melsec_net.Write( "D1000", "K123456789"); // 寫入D1000 string值
X,Y,M,L,F,V,B,S位數據的讀寫說明
- X 輸入繼電器
- Y 輸出繼電器
- M 內部繼電器
- L 鎖存繼電器
- F 報警器
- V 邊沿繼電器
- B 鏈接繼電器
- S 步進繼電器
本小節將展示八種位數據的讀取,雖然更多的時候只是讀取D數據即可,或者是將位數據批量挪到D數據中, 但是在此處仍然進行介紹單獨的讀取X,Y,M,L,F,V,B,S,由於這八種讀取手法一致,故針對M數據進行介紹,其他的您可以自己測試。
如下方法演示讀取了M200-M209這10個M的值,注意:讀取長度必須為偶數,即時寫了奇數,也會補齊至偶數,讀取和寫入的最大長度為7168,否則報錯。如需實際需求確實大於7168的,請分批次讀取。
返回值解析:如果讀取正常則共返回10個字節的數據,以下示例數據進行批量化的讀取
OperateResultExOne<boolean[]> read = melsec_net.ReadBool("M100",(short)10);
if(read.IsSuccess){
boolean m100 = read.Content[0];
boolean m101 = read.Content[1];
boolean m102 = read.Content[2];
boolean m103 = read.Content[3];
boolean m104 = read.Content[4];
boolean m105 = read.Content[5];
boolean m106 = read.Content[6];
boolean m107 = read.Content[7];
boolean m108 = read.Content[8];
boolean m109 = read.Content[9];
}
else {
System.out.print("讀取失敗:"+read.Message);
}
// 寫入測試,M100-M104 寫入測試 此處寫入后M100:通 M101:斷 M102:斷 M103:通 M104:通
boolean[] values = new boolean[]{true,false,false,true,true};
OperateResult write = melsec_net.Write("M100",values);
if(write.IsSuccess){
System.out.print("寫入成功");
}
else {
System.out.print("寫入失敗:"+write.Message);
}
D,W,R字數據的讀寫操作
此處讀取針對中間存在整數數據的情況,因為兩者讀取方式相同,故而只演示一種數據讀取, 使用該組件讀取數據,一次最多讀取或寫入960個字,超出則失敗。 如果讀取的長度確實超過限制,請考慮分批讀取。
OperateResultExOne<byte[]> read1 = melsec_net.Read("D100",(short)5);
if(read1.IsSuccess){
short D100 = melsec_net.getByteTransform().TransByte(read1.Content,0);
short D101 = melsec_net.getByteTransform().TransByte(read1.Content,2);
short D102 = melsec_net.getByteTransform().TransByte(read1.Content,4);
short D103 = melsec_net.getByteTransform().TransByte(read1.Content,6);
short D104 = melsec_net.getByteTransform().TransByte(read1.Content,8);
}
else {
System.out.print("讀取失敗:"+read1.Message);
}
// D100為1234,D101為8765,D102為1234,D103為4567,D104為-2563
short[] values2 = new short[]{1335, 8765, 1234, 4567, -2563 };
OperateResult write = melsec_net.Write("M100",values2);
if(write.IsSuccess){
System.out.print("寫入成功");
}
else {
System.out.print("寫入失敗:"+write.Message);
}
一個實際中復雜的例子演示
實際中可能碰到的情況會很復雜,一台設備中需要上傳的數據包含了溫度,壓力,產量,規格等等信息,在一串數據中 會包含各種各樣的不同的數據,上述的讀取D,讀取M,讀取條碼的方式不太好用,所以此處做一個完整示例的演示,假設我們需要讀取 D4000-D4009的數據,假設D4000存放了溫度數據,55.1℃在D中為551,D4001存放了壓力數據,1.23MPa在D中存放為123,D4002存放了 設備狀態,0為停止,1為運行,D4003存放了產量,1000就是指1000個,D4004備用,D4005-D4009存放了規格,以下代碼演示如何去解析數據:
//解析復雜數據
OperateResultExOne<byte[]> read3 = melsec_net.Read("D4000", (short) 10);
if (read3.IsSuccess)
{
double 溫度 = melsec_net.getByteTransform().TransInt16(read3.Content, 0) / 10d;//索引很重要
double 壓力 = melsec_net.getByteTransform().TransInt16(read3.Content, 2) / 100d;
boolean IsRun = melsec_net.getByteTransform().TransInt16(read3.Content, 4) == 1;
int 產量 =melsec_net.getByteTransform().TransInt32(read3.Content, 6);
String 規格 = melsec_net.getByteTransform().TransString(read3.Content, 10, 10,"ascii");
}
else
{
System.out.print("讀取失敗:"+read3.Message);
}
更詳細的信息,可以參照源代碼里面的測試項目。
