議讀寫與采集方案)
簡介這是面向C#開發(fā)者與工業(yè)自動化從業(yè)者的西門子S7 PLC通信實例源碼由工控老馬整理并驗證可用適合從入門到進階的工程師參考學習。壓縮包共32個文件整體約140KB典型結構包含9個C#源文件、項目工程與解決方案文件、配置文件及編譯輸出文件其中Form1.cs與Form1.Designer.cs主導界面邏輯App.config用于連接參數(shù)配置可直接在Visual Studio中打開S7PLCTest.sln運行調試。已有322人學習使用。資料通過S7.NET庫實現(xiàn)與西門子PLC的數(shù)據(jù)交互覆蓋通信連接、讀寫操作與界面展示等關鍵環(huán)節(jié)并附帶可執(zhí)行exe與調試符號便于對照學習和驗證效果。目錄結構清晰適合希望快速上手.NET方式PLC通信、理解實際封裝思路的開發(fā)人員。1. C# 上位機直連西門子S7 PLC先想清楚用哪條路很多做過設備采集的 C# 工程師第一次接到“把西門子 PLC 的數(shù)據(jù)取出來”的需求時第一反應是裝 WinCC、配 OPC 或者去翻 SIMATIC Manager。其實在 .NET 里通過 S7 以太網(wǎng)協(xié)議可以直接讀寫 S7-1200、S7-1500 和 S7-300/400代碼量并不大。適合的場景很明確寫 C# 上位機界面、做小型數(shù)據(jù)采集網(wǎng)關、或者在 MES 項目里臨時抓幾個 DB 變量。不需要額外授權也不需要 PLC 停機的軟件組態(tài)只要 IP 能通把 Rack、Slot、DB 地址和輪詢線程這四件事理順變量就能出現(xiàn)在 WinForms 或者 WPF 里。下面按原理、實例、循環(huán)采集、排錯這條路線展開一套能直接落地的方案。2. S7 通信原理與 .NET 側選型協(xié)議形態(tài)和庫的取舍在實際寫代碼之前先要弄清楚一條鏈路里有哪些角色。西門子 S7 的以太網(wǎng)通信走的是 TCP 102 端口PLC 作為 TCP 服務器上位機是客戶端。無論是 S7-1200、S7-1500 還是老款的 S7-300/400都允許通過 S7 協(xié)議發(fā)起讀寫請求只是 PLC 側默認不一定放行。TIA Portal 里必須打開“允許來自遠程對象的 PUT/GET 通信”這個選項在 CPU 屬性 - 保護與安全 - 連接機制下面。第一次接 S7-1500 的人最容易在這里卡住因為出廠配置對直接訪問比較嚴Open 能成功Read 卻會拿到訪問拒絕代碼改來改去都找不到原因。一旦開關打開剩下的工作就是“尋址”。PLC 里的 DB1.DBD4在 C# 地址字符串中也寫成DB1.DBD4但讀出來的數(shù)據(jù)類型要你自己聲明成 float、int 還是 bool。這個映射非常固定熟悉之后完全可以不看報文直接寫代碼。2.1 S7 通信不是 HTTP請求/響應和 PDU 協(xié)商S7 協(xié)議和 Modbus TCP 最大的差異在于連接建立后不是馬上就能發(fā)數(shù)據(jù)。plc.Open()會先完成一次 PDU 協(xié)商雙方確認能處理的請求長度隨后讀取每個 DB 區(qū)域時客戶端發(fā)送讀請求服務端返回數(shù)據(jù)或者錯誤碼。這個過程被類庫封裝好但排錯時要知道異常文本對應哪個環(huán)節(jié)。遇到PlcException時常見提示包括目標地址不存在、DB 塊長度不足、連接被拒絕三類分別指向 DB 編號寫錯、偏移超出塊長度、PUT/GET 未放行。對應用層開發(fā)來說這些背景不用背下來卻決定了選型。很多方案里在 C# 和 S7 之間插了一層 OPC UA使用成本立刻高了一截。如果你的上位機只服務一兩臺 PLC直接用 S7 客戶端庫是更省事的做法只有當多個品牌 PLC 或多種系統(tǒng)訪問同一批點位時OPC UA 才值得上線。我一般把 OPC UA 當作最后選項避免為一個小工具裝兩個后臺服務。2.2 三個 .NET 方案的取舍S7.NetPlus、Sharp7 與 OPC方案部署形態(tài)讀取方式適合場景S7.NetPlusNuGet 引用一個 dllplc.Read(DB1.DBD0)字符串地址上位機界面、點位少、開發(fā)快Sharp7單 dll偏底層封裝S7Client.DBRead()按字節(jié)塊讀高頻采集、需要控制報文細節(jié)OPC UA/DA安裝 OPC Server再寫客戶端標準接口統(tǒng)一地址空間異構設備、多客戶端、MES 對接S7.NetPlus 的優(yōu)勢是 API 離 PLC 變量表很近地址怎么寫代碼就怎么寫適合本文的場景。Sharp7 更接近底層當你需要一次讀回 200 字節(jié) DB 時它的 DBRead 更高效。OPC 方案在部署上要額外維護一個服務器如果不是項目里已經(jīng)存在不建議為單臺 PLC 引入。選擇之后就可以開始配置 PLC 側參數(shù)了。2.3 連接前先用一條命令確認 PLC 的 102 端口大多數(shù) S7 連接失敗不是 C# 代碼的問題而是網(wǎng)絡沒通。我會在寫上位機之前先用 PowerShell 探測 PLC 的 TCP 102 端口Test-NetConnection 192.168.0.1 -Port 102這條命令返回TcpTestSucceeded : True才說明 PC 和 PLC 之間的 S7 通道是通的。如果返回 False先檢查 IP 是否同段、物理網(wǎng)卡是否啟用、Windows 防火墻是否放行 TCP 102然后再回 C# 代碼里找原因。PLC 側還需要確認三個參數(shù)一是 CPU 的 IP二是機架號 Rack三是 CPU 所在槽位 Slot。常見的對應關系是S7-1200 和 S7-1500 基本是 Rack0、Slot1S7-300 大多數(shù)是 Rack0、Slot2S7-400 要看硬件組態(tài)通常在 0 或 1 起步。這三個值會直接傳給Plc構造函數(shù)填錯的表現(xiàn)往往是 Open 超時而不是登錄失敗。除地址外要讀取的 DB 塊還需要取消“優(yōu)化的塊訪問”否則從外部訪問時看到的變量表是空的。這條要記住下面實例代碼默認按“非優(yōu)化訪問”處理。3. 用 S7.NetPlus 在 10 分鐘內收發(fā) DB 與 I/O 的實例代碼開始寫代碼前確認項目里通過 NuGet 引用了 S7.NetPlus 包。示例程序以 WinForms 或控制臺為例都沒問題公共邏輯是“創(chuàng)建 Plc - Open - Read/Write - Close”。下面這段代碼會演示 DB、M 區(qū)和 I/O 區(qū)的讀寫地址字符串直接對標 PLC 程序里的 DB 變量。3.1 建立連接new Plc(cpu, ip, rack, slot) 四個參數(shù)要寫對using S7.Net; using System; class S7Client { private readonly string _ip; private readonly short _rack; private readonly short _slot; private Plc _plc; public S7Client(string ip, short rack, short slot) { _ip ip; _rack rack; _slot slot; } public bool Connect() { // CpuType 要和實際 PLC 一致1200、1500、300、400 都有獨立枚舉 _plc new Plc(CpuType.S71200, _ip, _rack, _slot); _plc.Open(); return _plc.IsConnected; } public void Close() { _plc?.Close(); } }代碼里的Open()不是簡單地建立一個 Socket 連接它內部完成了 TCP 連接和 S7 PDU 握手如果 PLC 不存在、IP 不通、端口被防火墻攔都會拋PlcException。參數(shù)中CpuType.S71200表示目標機型庫在解析地址時依賴這個枚舉寫錯型號容易出現(xiàn)連接成功但 Read 得到 PDU 錯誤。Rack 和 Slot 的默認值上文已經(jīng)給過連接 S7-1200 時用 0 和 1 即可。注意Plc對象不要頻繁創(chuàng)建一個 PLC 保持一個長連接采集循環(huán)里復用。3.2 讀寫 DB 和 M 區(qū)地址字符串就是 PLC 坐標讀操作public void ReadDemo() { // DB1 的第 0 字節(jié)第 0 位返回 bool bool startBtn Convert.ToBoolean(_plc.Read(DB1.DBX0.0)); // DB1 第 8 字節(jié)開始的 32 位浮點數(shù) float temp Convert.ToSingle(_plc.Read(DB1.DBD8)); // M 存儲區(qū)的第 100 字節(jié)第 2 位 bool alarm Convert.ToBoolean(_plc.Read(M100.2)); }寫操作public void WriteDemo() { // 把 DB1.DBD8 對應的 Real 變量改成 22.5 _plc.Write(DB1.DBD8, 22.5f); // 啟動信號寫到 M100.2 _plc.Write(M100.2, true); }這段代碼最需要理解的是地址后綴DBX是位DBB是字節(jié)DBW是字DBD是雙字DB 后的第一個數(shù)字是 DB 塊號第二個數(shù)字是字節(jié)偏移。寫值時類型由 C# 值類型決定bool、float、int 都會映射到 PLC 對應類型。S7.NetPlus 內部已經(jīng)做了大端轉換所以完全不用關心 CPU 的字節(jié)序這點和后面自己讀字節(jié)數(shù)組的場景不一樣。常用地址的映射關系如下地址示例C# 類型PLC 數(shù)據(jù)類型DB1.DBX0.0boolBoolDB1.DBB0byteByteDB1.DBW0ushortWord/IntDB1.DBD4uint/floatDWord/RealM100.2boolBool如果目標是整段讀取比如 DB10 中連續(xù) 50 個 Real 變量逐個Read會發(fā)起 50 次請求效率太低。常見做法是ReadBytes(DataType.DataBlock, 10, 0, 200)一次抓回 200 字節(jié)再用BitConverter解析既減少通信次數(shù)也便于寫日志。下面工具類把兩種方式都封裝進去。3.3 帶超時和自動重連的讀寫工具類public class SafeS7Client { private readonly string _ip; private readonly short _rack; private readonly short _slot; private Plc _plc; public SafeS7Client(string ip, short rack, short slot) { _ip ip; _rack rack; _slot slot; } public float ReadFloat(string address) { EnsureConnected(); return Convert.ToSingle(_plc.Read(address)); } public byte[] ReadDbBytes(int db, int start, int length) { EnsureConnected(); return _plc.ReadBytes(DataType.DataBlock, db, start, length); } private void EnsureConnected() { if (_plc ! null _plc.IsConnected) return; _plc?.Close(); _plc new Plc(CpuType.S71200, _ip, _rack, _slot); _plc.Open(); } }EnsureConnected()在每次讀寫前檢查狀態(tài)掉線時重新執(zhí)行 Close 和 Open重建 S7 會話。這里有一個常見誤用很多人以為IsConnected會實時反映 TCP 狀態(tài)實際上它只是一個上次 Open 后的標志網(wǎng)絡斷開后不會自動變成 false。因此更可靠的方式是把 Read 放進 try-catch一旦捕獲PlcException或SocketException調用重連并重新讀取一次。要避免任何異常都立刻重連連續(xù)失敗達到 3 次以上時停 2 秒再試否則會對 PLC 形成一次重連風暴。S7-1200 允許的并發(fā)連接數(shù)有限重連過快反而會把設備拖死。4. 循環(huán)數(shù)據(jù)采集與 UI 刷新把 PLC 輪詢從界面上拆走搜過 C# 上位機的人一定見過這類問題循環(huán)讀取 PLC 數(shù)據(jù)時界面卡死窗口一直轉圈關閉還要卡幾秒。原因幾乎都出在同一個地方把 PLC 的直接讀寫放在了 UI 線程里。下面這套方案把采集和顯示拆成兩段每段各管一個時鐘。4.1 為什么把輪詢放進 UI 線程就會卡多數(shù)初始寫法是在一個 Button 點擊里寫while (true) { label.Text plc.Read(DB1.DBD0).ToString(); }。每一次 Read 都是一次同步的 TCP 請求-響應遇到網(wǎng)絡延時或 PLC 忙單個請求可能阻塞幾百毫秒此時 UI 線程被獨占Windows 消息循環(huán)無法執(zhí)行窗口必然無響應。更隱蔽的問題是如果 Read 失敗后馬上重連UI 線程卡的時間會被重連的超時疊加最后連關閉按鈕都點不動。不要用Application.DoEvents()去緩解它會在一次采集里重入大量 UI 消息界面看起來響應了數(shù)據(jù)反而會亂跳。把采集放到后臺線程只在需要的時候把帶時間戳的快照交還給界面。這里的約束是UI 控件是單線程組件跨線程組件通信必須通過消息封送。與其到處寫Invoke不如用一個 UI 定時器去隊列里取數(shù)據(jù)這樣并發(fā)邊界非常干凈。4.2 用 Channel 做采集隊列后臺任務只管讀using System.Threading.Channels; public class MachineSnapshot { public float Temp { get; set; } public bool Running { get; set; } public DateTime Timestamp { get; set; } } ChannelMachineSnapshot _snapChannel Channel.CreateBoundedMachineSnapshot( new BoundedChannelOptions(200) { FullMode BoundedChannelFullMode.DropOldest });后臺循環(huán)void StartCollectLoop(CancellationToken token) { Task.Run(() { while (!token.IsCancellationRequested) { try { var snap new MachineSnapshot { Temp Convert.ToSingle(_plc.Read(DB10.DBD0)), Running Convert.ToBoolean(_plc.Read(DB10.DBX0.0)), Timestamp DateTime.Now }; _snapChannel.Writer.TryWrite(snap); } catch (Exception ex) { // 連續(xù)異常時記錄日志交給上層決定是否重連 Console.WriteLine(ex.Message); } Thread.Sleep(100); // 采集周期 100ms可按工藝調整 } }, token); }這里的Channel是 .NET Core 3.0 及以上內置的標準庫隊列容量設為 200 幀DropOldest表示隊列滿時丟最舊的數(shù)據(jù)避免 UI 慢時內存無限漲。如果你還在 .NET Framework 4.8需要補一個System.Threading.Channels的 NuGet 包或者直接把隊列換成ConcurrentQueue效果接近。采集周期 100ms 對大多數(shù)設備足夠如果讀的是溫度、壓力這類慢過程可以放到 500ms如果做高速包裝線計數(shù)再降到 50ms。TryWrite 不會阻塞即使 UI 消費跟不上后臺任務也能繼續(xù)按時采下一幀。沒有用 Unbounded是因為無界隊列在 UI 卡死一小時時會積壓幾十萬幀數(shù)據(jù)沒必要。采集周期UI 刷新周期適用場景50ms200ms高速包裝、實時報警100ms500ms通用設備監(jiān)控500ms1s溫度、液位等過程量4.3 UI 端用 Timer 定期取隊列不要一幀刷一次System.Windows.Forms.Timer uiTimer new System.Windows.Forms.Timer(); uiTimer.Interval 500; uiTimer.Tick (s, e) { while (_snapChannel.Reader.TryRead(out MachineSnapshot snap)) { lblTemp.Text snap.Temp.ToString(F2); btnRun.Enabled snap.Running; lblTime.Text snap.Timestamp.ToString(HH:mm:ss.fff); } }; uiTimer.Start();這個 Timer 運行在 UI 線程因此直接賦值給 Text 和 Enabled 是合法的。每隔 500ms 把隊列里剩余的快照全部取出來界面刷新是一幀不會出現(xiàn)逐幀閃爍后臺采集仍然是 100ms 一次數(shù)據(jù)粒度不受刷新頻率影響。若界面只需要顯示最新值也可以把 Channel 換成ConcurrentQueueMachineSnapshot加TryDequeue或者干脆用一個字段只保存最新快照。區(qū)別在于Channel 保留的最近 200 幀可以用于曲線回放單字段只能得到當前值。5. 西門子 S7 連不上、掉線、讀數(shù)不對把這幾處當排查入口S7 通信項目里大部分問題不是 C# 語法而是 PLC 配置和環(huán)境問題。我在現(xiàn)場見過最快解決的問題是防火墻攔截最難發(fā)現(xiàn)的問題是 DB 開了優(yōu)化訪問。下面的排查順序按優(yōu)先級排過照著走一遍比反復改代碼效率高。5.1 掉線先按順序查這幾處排查順序檢查位置具體操作1IP 與物理鏈路先 ping 通 PLC再用 PowerShell 測 102 端口2Windows 防火墻放行出站和入站的 TCP 1023PUT/GET 開關TIA 屬性 - 保護與安全 - 連接機制勾選允許 PUT/GET4DB 優(yōu)化訪問在 DB 屬性中取消“優(yōu)化的塊訪問”重新下載5Rack/Slot 參數(shù)用 CpuType 對應型號按默認值逐一試驗另外把上位機裝在虛擬機里跑時虛擬網(wǎng)卡的網(wǎng)絡模式不要選 NAT 或僅主機改成橋接到物理網(wǎng)卡否則經(jīng)常出現(xiàn) ping 得通但 Open 超時的情況。5.2 Real 和字符串讀出來全是亂碼時處理字節(jié)序如果讀取結果不是明顯報錯而是數(shù)值大小離譜先懷疑字節(jié)序。西門子 S7 的數(shù)據(jù)在 CPU 里是大端工業(yè)庫在返回時大多做了轉換但當你用ReadBytes、拼接多字節(jié)變量或者直接寫 byte[] 時必須自己處理。最樸素的檢查方法是打印原始字節(jié)byte[] raw _plc.ReadBytes(DataType.DataBlock, 1, 4, 4); Console.WriteLine(BitConverter.ToString(raw));如果讀的是 DB1.DBD4期望得到的 float 是 22.5但原始字節(jié)順序不對就按下面的方式反轉if (BitConverter.IsLittleEndian) { Array.Reverse(raw); } float temp BitConverter.ToSingle(raw, 0); Console.WriteLine(temp);注意ReadBytes的參數(shù)順序是 DataType、DB 號、起始字節(jié)、長度這里的DataType.DataBlock, 1, 4, 4表示讀 DB1 從第 4 字節(jié)開始的 4 個字節(jié)也就是 DBD4。把同一個反轉邏輯封裝成一個ToPlcFloat(byte[])方法所有自定義報文都走這一處就不會出現(xiàn)一半數(shù)據(jù)正常一半讀成天文數(shù)字的情況。把字節(jié)序檢查放在數(shù)據(jù)庫記錄之前能省掉大量臟數(shù)據(jù)。本文還有配套的精品資源點擊獲取