本文將使用一個Github開源的組件庫技術來讀寫三菱PLC和西門子plc數據,使用的是基於以太網的TCP/IP實現,不須要額外的組件,讀取操做只要放到後臺線程就不會卡死線程,本組件支持超級方便的高性能讀寫操做css
github地址:https://github.com/dathlin/HslCommunication 若是喜歡能夠star或是fork,還能夠打賞支持,打賞請認準源代碼項目。html
在Visual Studio 中的NuGet管理器中能夠下載安裝,也能夠直接在NuGet控制檯輸入下面的指令安裝:前端
Install-Package HslCommunication
若是須要教程:Nuget安裝教程:http://www.cnblogs.com/dathlin/p/7705014.htmlgit
聯繫做者及加羣方式(激活碼在羣裏發放):http://www.hslcommunication.cn/Cooperationgithub
組件的完整信息和API介紹參照:http://www.cnblogs.com/dathlin/p/7703805.html 組件的使用限制,更新日誌,都在該頁面裏面。數組
若是你須要在讀取PLC數據以後,還要羣發客戶端來實現遠程辦公室同步監視,能夠參考以下的項目(基於該組件擴展起來的,帶有帳戶驗證,版本控制,數據羣發,公告管理等等功能)安全
https://github.com/dathlin/ClientServerProject服務器
本文將展現如何配置網絡參數及怎樣使用代碼來訪問PLC數據,但願給有須要的人解決一些實際問題。主要對三菱Q系列PLC的X,Y,M,L,B,V,F,S,D,W,R區域的數據讀寫,對西門子PLC的M,Q,I,DB塊的數據讀寫,親測有效。網絡
此處使用了網線直接的方式,若是PLC接進了局域網,就能夠進行遠程讀寫了^_^多線程
此處使用到了2個命名空間:
using HslCommunication; using HslCommunication.Profinet;
當咱們一個上位機須要讀取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端口機制也是這樣的。如下代碼默認使用長鏈接,性能更高,還支持多線程同步。
在短鏈接的模式下,每次請求都是單獨的訪問,因此沒有重連的困擾,在長鏈接的模式下,若是本次請求失敗了,在下次請求的時候,會自動從新鏈接服務器,直到請求成功爲止。另外,儘可能全部的讀寫都對結果的成功進行判斷。
關於日誌記錄
無論是三菱的數據訪問類,仍是西門子的,仍是Modbus tcp訪問類,都有一個LogNet屬性用來記錄日誌,該屬性是一個接口類,ILogNet,凡事繼承該接口的均可以用來記錄日誌,該日誌會在訪問失敗時,尤爲是由於網絡的緣由致使訪問失敗時會進行日誌記錄(若是你爲這個 LogNet 屬性配置了真實的日誌記錄器的話):若是你想使用該記錄日誌的功能,請參照以下的博客進行實例化:
http://www.cnblogs.com/dathlin/p/7691693.html
下面的一個項目是這個組件的訪問測試項目,您能夠進行初步的訪問的測試,免去了您寫測試程序的麻煩,三菱的界面和西門子的界面幾乎是一致的。能夠同時參考。該項目位於本篇文章開始處的Gitbub源代碼裏面的
下載地址爲:HslCommunicationDemo.zip
下面的三篇演示了具體如何去訪問PLC的數據,咱們在訪問完成後,一般須要進行處理,如下的示例項目就演示了後臺從PLC讀取數據後,前臺顯示並推送給全部在線客戶端的功能,客戶端並進行圖形化顯示,具備必定的參考意義,而且推送給網頁前端,項目地址爲:
https://github.com/dathlin/RemoteMonitor
下面的圖片示例中的左邊程序就是服務器程序,它應該和PLC直接鏈接並接入局域網,而後把數據推送給客戶端顯示。注意:一個複雜高級的程序就應該把處理邏輯程序和界面程序分開,好比這裏的服務器程序實現數據採集,推送,存儲。讓客戶端程序去實現數據的整理,分析,顯示,這樣即便客戶端程序由於BUG奔潰,服務器端仍然能夠正常的工做。
三菱PLC篇(下面列舉了三種配置方法,本組件支持二進制和ASCII通信,支持1E幀兼容協議訪問)
Q06UDV Plc的訪問測試感謝:hwdq0012
fx5u plc的訪問測試感謝:山楂
Q02CPU, L02CPU-CM : 本人測試
感謝:小懶豬雨中人 的測試,VB程序也能夠調用本通信庫
環境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個端口來支持讀寫操做:
端口號設置規則:
本文檔僅做組件的測試,因此只用了一個端口做爲讀寫。若是你的程序也使用了一個端口,那麼你在讀取數據時候, 恰好也在寫入(異步操做可能發生這樣的狀況),那麼寫入會失敗!)(在長鏈接模式下沒有這個問題)
三菱PLC的數據主要由兩類數據組成,位數據和字數據,在位數據中,例如X,Y,M,L都是位數據,字數據例如D,W。 兩類的數據在讀取解碼上存在一點小差異。(事實上也能夠先將16個M先賦值給一個D,讀取D數據再進行解析, 在讀取M的數量比較多的時候,這樣操做效率更高)
初始化訪問PLC對象
注意:若是你想採用ASCII來讀寫數據,請使用MelsecMcAsciiNet類,若是想採用1E幀協議,使用MelsecA1ENet類,除了實例化,其餘的數據交互都是同樣的。
若是想使用本組件的數據讀取功能,必須先初始化數據訪問對象,根據實際狀況進行數據的填入。 下面僅僅是測試中的數據:
private MelsecMcNet melsec_net = new MelsecMcNet( "192.168.0.1", 6000 );
如上圖所示,只要指定了IP地址和端口號就完成了初始化的搭建了,固然還支持一些額外的信息配置
melsec_net.ConnectTimeOut = 2000; // 網絡鏈接的超時時間 melsec_net.NetworkNumber = 0x00; // 網絡號 melsec_net.NetworkStationNumber = 0x00; // 網絡站號
打開鏈接,並能夠判斷是否鏈接上
melsec_net.ConnectClose( );
若是須要判斷,那麼按照以下的操做
OperateResult connect = melsec_net.ConnectServer( ); if (connect.IsSuccess) { MessageBox.Show( "鏈接成功!" ); } else { MessageBox.Show( "鏈接失敗!" ); }
說明:對象應該放在窗體類下面,此處僅僅針對讀取一臺設備的plc,也能夠在訪問的方法中實例化局部對象, 初始化數據,而後讀取,該對象幾乎不損耗內存,內存垃圾由CLR進行自動回收。此處測試方便,窗體的多個按鈕均鏈接同一臺PLC 設備,因此本窗體實例化一個對象便可。
關於兩種地址的表示方式
第一種,使用系統的類來標識,好比M200,寫成(MelsecDataType.M, 200)的表示形式,這樣也能夠去MelsecDataType裏面找到全部支持的數據類型。
第二種,使用字符串表示,這個組件裏全部的讀寫操做提供字符串表示的重載方法,全部的支持訪問的類型對應以下,字符串的表示方式存在十進制和十六進制的區別:
展現一些簡單實用基礎數據讀寫,這些數據的讀寫沒有進行嚴格的是否成功判斷(判斷方法參照後面的代碼),通常網絡良好的狀況下都會成功,但不排除失敗,如下代碼僅做測試,全部沒有嚴格判斷是否成功:
bool[] M100 = melsec_net.ReadBool("M100",1).Content; // 讀取M100是否通,十進制地址 bool[] X1A0 = melsec_net.ReadBool("X1A0",1).Content; // 讀取X1A0是否通,十六進制地址 bool[] Y1A0 = melsec_net.ReadBool("Y1A0",1).Content; // 讀取Y1A0是否通,十六進制地址 bool[] B1A0 = melsec_net.ReadBool("B1A0",1).Content; // 讀取B1A0是否通,十六進制地址 short short_D1000 = melsec_net.ReadInt16("D1000").Content; // 讀取D1000的short值 ,W3C0,R3C0 效果是同樣的 ushort ushort_D1000 = melsec_net.ReadUInt16("D1000").Content; // 讀取D1000的ushort值 int int_D1000 = melsec_net.ReadInt32("D1000").Content; // 讀取D1000-D1001組成的int數據 uint uint_D1000 = melsec_net.ReadUInt32("D1000").Content; // 讀取D1000-D1001組成的uint數據 float float_D1000 = melsec_net.ReadFloat("D1000").Content; // 讀取D1000-D1001組成的float數據 long long_D1000 = melsec_net.ReadInt64("D1000").Content; // 讀取D1000-D1003組成的long數據 ulong ulong_D1000 = melsec_net.ReadUInt64( "D1000" ).Content; // 讀取D1000-D1003組成的long數據 double double_D1000 = melsec_net.ReadDouble("D1000").Content; // 讀取D1000-D1003組成的double數據 string str_D1000 = melsec_net.ReadString("D1000", 10).Content; // 讀取D1000-D1009組成的條碼數據 melsec_net.Write("M100", new bool[] { true} ); // 寫入M100爲通 melsec_net.Write( "Y1A0", new bool[] { true } ); // 寫入Y1A0爲通 melsec_net.Write( "X1A0", new bool[] { true } ); // 寫入X1A0爲通 melsec_net.Write( "B1A0", new bool[] { true } ); // 寫入B1A0爲通 melsec_net.Write( "D1000", (short)1234); // 寫入D1000 short值 ,W3C0,R3C0 效果是同樣的 melsec_net.Write( "D1000", (ushort)45678); // 寫入D1000 ushort值 melsec_net.Write( "D1000", 1234566); // 寫入D1000 int值 melsec_net.Write( "D1000", (uint)1234566); // 寫入D1000 uint值 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值
下面再分別講解嚴格的操做,以及批量化的複雜的讀寫操做,假設你要讀取1000個M,循環讀取1千次可能要3秒鐘,若是用了下面的批量化讀取,只須要50ms,可是須要你對字節的原理比較熟悉才能駕輕就熟的處理
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個字節的數據,如下示例數據進行批量化的讀取
private void userButton20_Click(object sender, EventArgs e) { // M200-M209讀取顯示 OperateResult<bool[]> read = melsec_net.ReadBool("M200", 10); if (read.IsSuccess) { // 成功讀取,True表明通,False表明不通 bool M200 = read.Content[0]; bool M201 = read.Content[1]; bool M202 = read.Content[2]; bool M203 = read.Content[3]; bool M204 = read.Content[4]; bool M205 = read.Content[5]; bool M206 = read.Content[6]; bool M207 = read.Content[7]; bool M208 = read.Content[8]; bool M209 = read.Content[9]; // 顯示 } else { //失敗讀取,顯示失敗信息 MessageBox.Show(read.ToMessageShowString()); } } private void userButton21_Click(object sender, EventArgs e) { // X100-X10F讀取顯示 OperateResult<bool[]> read = melsec_net.ReadBool("X200", 16); if (read.IsSuccess) { // 成功讀取,True表明通,False表明不通 bool X200 = read.Content[0]; bool X201 = read.Content[1]; bool X202 = read.Content[2]; bool X203 = read.Content[3]; bool X204 = read.Content[4]; bool X205 = read.Content[5]; bool X206 = read.Content[6]; bool X207 = read.Content[7]; bool X208 = read.Content[8]; bool X209 = read.Content[9]; bool X20A = read.Content[10]; bool X20B = read.Content[11]; bool X20C = read.Content[12]; bool X20D = read.Content[13]; bool X20E = read.Content[14]; bool X20F = read.Content[15]; // 顯示 } else { //失敗讀取,顯示失敗信息 MessageBox.Show(read.ToMessageShowString()); } } private void userButton3_Click(object sender, EventArgs e) { // M100-M104 寫入測試 此處寫入後M100:通 M101:斷 M102:斷 M103:通 M104:通 bool[] values = new bool[] { true, false, false, true, true };// 等同於 byte[] values = new byte[]{0x01,0x00,0x00,0x01,0x01} OperateResult write = melsec_net.Write("M100", values); if (write.IsSuccess) { TextBoxAppendStringLine("寫入成功"); } else { MessageBox.Show(write.ToMessageShowString()); } }
錯誤說明:有可能由於站號網絡號沒有配置正確返回有錯誤代號沒有錯誤信息, 也有可能由於網絡問題致使沒有鏈接上,此時會有鏈接不上的錯誤信息。
下面展現的是後臺線程循環讀取的狀況,事實上在實際的使用過程當中常常會遇見的狀況。下面的方法須要 放到單獨的線程中,同理,訪問D數據時也是按照下面循環就行,此處再也不贅述。
//後臺循環讀取PLC數據 M200開始10個字 也便是M200-M209 while (true) { OperateResult<bool[]> read = melsec_net.ReadFromPLC("M200", 10); if (read.IsSuccess) { //成功讀取,委託顯示 textBox2.BeginInvoke(new Action(delegate { textBox2.Text = "M201:" + (read.Content[1] ? "通" : "斷"); })); } else { //失敗讀取,應該對失敗信息進行日誌記錄,不該該顯示,測試訪問時才適合顯示錯誤信息 LogHelper.save(read.ToMessageShowString()); } System.Threading.Thread.Sleep(1000);//決定了訪問的頻率 }
D,W,R字數據的讀寫操做
此處讀取針對中間存在整數數據的狀況,由於二者讀取方式相同,故而只演示一種數據讀取, 使用該組件讀取數據,一次最多讀取或寫入960個字,超出則失敗。 若是讀取的長度確實超過限制,請考慮分批讀取。
private void userButton2_Click(object sender, EventArgs e) { // D100-D104讀取 OperateResult<byte[]> read = melsec_net.Read("D100", 5); if (read.IsSuccess) { // 成功讀取,提取各自的值,此處的值有個前提假設,假設PLC上的數據是有符號的數據,表示-32768-32767 short D100 = melsec_net.ByteTransform.TransInt16(read.Content, 0); short D101 = melsec_net.ByteTransform.TransInt16( read.Content, 2); short D102 = melsec_net.ByteTransform.TransInt16( read.Content, 4); short D103 = melsec_net.ByteTransform.TransInt16( read.Content, 6); short D104 = melsec_net.ByteTransform.TransInt16( read.Content, 8); TextBoxAppendStringLine("D100:" + D100); TextBoxAppendStringLine("D101:" + D101); TextBoxAppendStringLine("D102:" + D102); TextBoxAppendStringLine("D103:" + D103); TextBoxAppendStringLine("D104:" + D104); } else { //失敗讀取 MessageBox.Show(read.ToMessageShowString()); } } private void userButton4_Click( object sender, EventArgs e ) { short[] values = new short[5] { 1335, 8765, 1234, 4567, -2563 }; // D100爲1234,D101爲8765,D102爲1234,D103爲4567,D104爲-2563 OperateResult write = melsec_net.Write( "D6000", values ); if (write.IsSuccess) { //成功寫入 TextBoxAppendStringLine( "寫入成功" ); } else { MessageBox.Show( write.ToMessageShowString( ) ); } }
ASCII字符串數據的讀寫
在實際項目中,有可能會碰到PLC存儲了規格數據,或是條碼數據,這些數據是以ASCII編碼形式存在, 咱們須要把數據進行讀取出來用於顯示,保存等操做。下面演示讀取指定長度的條碼數據,數據的數據存放在D2000-D2004中, 長度應該爲存儲條碼的最大長度,也便是佔用了5個D,一個D能夠存儲2個ASCII碼字符:
private void button7_Click(object sender, EventArgs e) { //讀取字符串數據,共計10個字節長度 OperateResult<byte[]> read = melsec_net.Read("D2000", 5); if (read.IsSuccess) { //成功讀取 textBox2.Text = Encoding.ASCII.GetString(read.Content); } else { //失敗讀取 MessageBox.Show(read.ToMessageShowString()); } } private void button8_Click(object sender, EventArgs e) { //寫字符串,若是寫入K12345678這9個字符,讀取出來時末尾會補0 OperateResult write = melsec_net.WriteAsciiString("D2000", "K123456789"); if (write.IsSuccess) { textBox2.Text = "寫入成功"; } else { MessageBox.Show(write.ToMessageShowString()); } }
須要注意的是,若是第一次在D2000-D2004中寫入了"K123456789",第二次寫入了"K6666",那麼讀取D2000-D2004的條碼數據會讀取到 K666656789,若是要避免這種狀況,則須要在寫入條碼的時候,指定總長度,該長度必須爲偶數, 否則也會自動補0,小於該長度時,自動補零,大於該長度時,自動截斷數據,具體的使用方法以下:
private void button8_Click(object sender, EventArgs e) { //寫字符串,本次寫入指定了10個長度的字符,其他的D的數據將被清空,是一種安全的寫入方式 OperateResult write = melsec_net.WriteAsciiString("D2000", "K6666", 10); if (write.IsSuccess) { textBox2.Text = "寫入成功"; } else { MessageBox.Show(write.ToMessageShowString()); } }
中文及特殊字符的讀寫
在須要讀寫複雜的字符數據時,上述的ASCII編碼已經不能知足要求,雖然使用讀寫的基礎方法能夠實現任意數據的讀寫, 可是此處爲了方便,仍是提供了一個方便的方法來讀寫中文數據,採用Unicode編碼的字符, 該編碼下的一個字符佔用一個D或W來存儲。以下將演示,讀寫方法,基本用途和上述 ASCII編碼的讀寫一致。
private void button9_Click(object sender, EventArgs e) { //讀中文,存儲在D3000-D3009 OperateResult<byte[]> read = melsec_net.Read("D3000", 10); if (read.IsSuccess) { //解析數據 textBox2.Text = Encoding.Unicode.GetString(read.Content); } else { MessageBox.Show(read.ToMessageShowString()); } } private void button10_Click(object sender, EventArgs e) { //寫中文 D3000-D3009,該10含義爲中文字符數 OperateResult write = melsec_net.WriteUnicodeString("D3000", "測試數據test", 10); if (write.IsSuccess) { textBox2.Text = "寫入成功"; } else { MessageBox.Show(write.ToMessageShowString()); } }
一個實際中複雜的例子演示
實際中可能碰到的狀況會很複雜,一臺設備中須要上傳的數據包含了溫度,壓力,產量,規格等等信息,在一串數據中 會包含各類各樣的不一樣的數據,上述的讀取D,讀取M,讀取條碼的方式不太好用,因此此處作一個完整示例的演示,假設咱們須要讀取 D4000-D4009的數據,假設D4000存放了溫度數據,55.1℃在D中爲551,D4001存放了壓力數據,1.23MPa在D中存放爲123,D4002存放了 設備狀態,0爲中止,1爲運行,D4003存放了產量,1000就是指1000個,D4004備用,D4005-D4009存放了規格,如下代碼演示如何去解析數據:
private void button29_Click(object sender, EventArgs e) { //解析複雜數據 OperateResult<byte[]> read = melsec_net.Read("D4000", 10); if (read.IsSuccess) { double 溫度 = melsec_net.ByteTransform.TransInt16(read.Content, 0) / 10d;//索引很重要 double 壓力 = melsec_net.ByteTransform.TransInt16(read.Content, 2) / 100d; bool IsRun = melsec_net.ByteTransform.TransInt16(read.Content, 4) == 1; int 產量 = BitConverter.ToInt16(read.Content, 6); string 規格 = Encoding.ASCII.GetString(read.Content, 10, 10); } else { MessageBox.Show(read.ToMessageShowString()); } }
究極數據讀取展現,用於測試你本身的報文以及擴展本身的更高級,更變態的API,如下演示,使用這個高級模式,寫入M100,True的操做:
咱們要寫入的字節數組HEX表示形式爲:50 00 00 FF FF 03 00 0D 00 0A 00 01 14 01 00 64 00 00 90 01 00 10
private void userButton23_Click(object sender, EventArgs e) { byte[] buffer = HslCommunication.BasicFramework.SoftBasic.HexStringToBytes("50 00 00 FF FF 03 00 0D 00 0A 00 01 14 01 00 64 00 00 90 01 00 10"); // 直接使用報文進行 OperateResult<byte[]> operate = melsec_net.ReadFromCoreServer(buffer); if(operate.IsSuccess) { // 返回PLC的報文反饋,須要本身對報文進行結果分析 MessageBox.Show(HslCommunication.BasicFramework.SoftBasic.ByteToHexString(operate.Content)); } else { // 網絡緣由致使的失敗 MessageBox.Show(operate.ToMessageShowString()); } }
更詳細的信息,能夠參照源代碼裏面的測試項目。
西門子篇參見另外一篇博客:http://www.cnblogs.com/dathlin/p/8685855.html