Learning Objectives
- 說明 Modbus TCP 和 Modbus RTU 的差別:MBAP 標頭、沒有 CRC、埠 502
- 拆解 MBAP 標頭的四個欄位,說出交易識別碼與單元識別碼的用途
- 用 pymodbus 的 ModbusTcpClient 連線、讀寫 PLC,並處理連線失敗與沒有回應
- 說明遠端控制為什麼寫「命令暫存器」,而不是直接改 PLC 程式在控制的輸出
把 Modbus 搬到乙太網路上
RS-485 的設備要一台一台輪流問,速度也有限。現在的 PLC、HMI、電力監控設備大多有乙太網路埠, Modbus TCP 把同樣的請求與回應放進 TCP 連線裡傳送:功能碼、位址、資料的格式都沒變,上一課學的四張表照樣適用。
這一課要回答:
- Modbus TCP 的訊框和 RTU 差在哪裡?
- 用 Python 讀寫 PLC 的完整流程是什麼?連不上、沒回應時怎麼辦?
- 要從遠端啟動一台泵浦,應該寫哪裡?
RTU 與 TCP 的比較
| Modbus RTU | Modbus TCP | |
|---|---|---|
| 實體層 | RS-485(或 RS-232) | 乙太網路 |
| 找到對方 | 站號 1~247 | IP 位址+埠 502(規格保留給 Modbus 的知名埠)+單元識別碼 |
| 訊框開頭 | 站號 | MBAP 標頭(7 個位元組) |
| 錯誤檢查 | CRC-16 | 沒有 CRC:TCP 本身就有檢查與重傳 |
| 訊框的分界 | 3.5 字元的靜默間隔 | MBAP 的長度欄位 |
| 同時通訊 | 匯流排上一次一台 | 每個 client 各自一條 TCP 連線 |
MBAP 標頭
00 01 00 00 00 06 01 │ 03 00 00 00 02
交易 協定 長度 單元 │ 功能碼 位址 數量
識別碼 識別碼 識別碼│ ←─── PDU(和 RTU 一樣)───→
- 交易識別碼:client 每送一筆請求就換一個號碼,server 在回應裡原樣送回,client 才知道這是哪一筆的回應。
- 協定識別碼:Modbus 固定是 0。
- 長度:後面還有幾個位元組(單元識別碼+PDU)。
- 單元識別碼:直接連到 PLC 時通常填 1(有的設備要 0 或 255,以手冊為準);經過閘道器(TCP 轉 RTU)時,閘道器用它當作後面 RS-485 上的站號。
一個 Modbus TCP 訊框最多 260 個位元組(PDU 253+MBAP 7)。
用 ModbusTcpClient 讀寫
程式和 RTU 幾乎一樣,只換了 client:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("192.168.0.10", port=502, timeout=1)
client.connect() # 回傳 True/False
rr = client.read_holding_registers(0, count=2, device_id=1)
client.write_register(0, 1, device_id=1)
client.close()
也可以寫成 with ModbusTcpClient(...) as client:,離開區塊時自動關閉連線。
版本差異(本課以撰寫時的 pymodbus 3.x 為準,最新版是 3.16):
- 3.8 起 client 的參數必須用關鍵字寫:
ModbusTcpClient("192.168.0.10", port=502)可以,ModbusTcpClient("192.168.0.10", 502)不行; 讀取時count=2也要寫出名字。 - 站號的參數 3.10 起叫
device_id=,3.3~3.9 叫slave=,更舊的版本叫unit=。 - 讀寫的回傳值:
rr.registers、rr.bits、rr.isError();例外回應是ExceptionResponse。
兩種常見的錯誤:
| 狀況 | 現象 |
|---|---|
| IP 打錯、網路不通、對方沒開 502 埠 | connect() 回傳 False;硬要讀取會丟出 ConnectionException |
| 連上了,但單元識別碼不對或設備沒回 | 等到逾時,丟出 ModbusIOException |
| 連上了,但位址不存在 | 收到例外回應,rr.isError() 是 True |
逐步看:連線、讀取、看 MBAP
對照通訊紀錄:請求的開頭 00 01 00 00 00 06 01 是 MBAP 標頭,第二筆請求的交易識別碼變成 00 02;回應裡的交易識別碼和請求一樣。
遠端控制:寫命令,不要搶輸出
想從電腦啟動泵浦,最直覺的做法是直接寫泵浦的線圈 Q0.0。但 PLC 的階梯圖每個掃描週期都會重新算 OUT Q0.0,
你剛寫進去的值,下一輪就被程式的結果蓋掉了,就像兩個人搶同一個開關。
正確的做法是讓 PLC 程式自己決定輸出,電腦只寫一個命令暫存器(例如 MW0:1=啟動、0=停止)。PLC 程式把命令和其他條件一起判斷:
‖ CMP MW0 == 1 CMP IW0 < 3276(80%) Q0.0 泵浦
‖────[ == ]──────────────[ < ]─────────────────────────( )────‖
這樣液位到 80% 時,就算電腦一直送「啟動」,PLC 還是會停泵浦;安全條件永遠留在 PLC 裡。 實務上還會加上通訊中斷的處理:例如電腦定期更新一個「心跳」暫存器,PLC 發現太久沒更新就停機。
安全:遠端命令可以讓設備在沒人看著的時候動起來。正式的設備要有現場的急停與機器防護,遠端操作的權限與網路區隔見第 17 課。
逐步看:寫命令與直接寫線圈
看水箱與即時階梯圖:寫 MW0=1 之後第一條 rung 導通,泵浦開始進水。接著直接寫線圈 0=False,讀回來卻還是 True;改寫 MW0=0 才真的停下來。
逐步看:連不上與沒有回應
三種失敗:IP 錯了(connect 回傳 False)、沒連上還硬要讀(ConnectionException)、單元識別碼錯了(等到逾時後 ModbusIOException)。注意每次失敗都要等 timeout 的時間。
小測驗:MBAP 與 TCP
Modbus TCP 請求 00 07 00 00 00 06 01 03 00 0A 00 02 的交易識別碼與要讀的位址是?
MBAP 的長度欄位,在讀保持暫存器的請求裡是 6,指的是哪些位元組?
用 Modbus TCP 經過閘道器讀取後面 RS-485 上站號 5 的電力錶,單元識別碼要填多少?
小測驗:程式與錯誤處理
下面的程式模擬交易識別碼的變化,會印出什麼?
tid = 0
log = []
for fc in [3, 4, 6]:
tid += 1
log.append((tid, fc))
print(log[-1])
client.connect() 回傳 False,最可能的原因是?
重點整理
- Modbus TCP=MBAP 標頭(交易識別碼、協定識別碼 0、長度、單元識別碼)+和 RTU 相同的 PDU;沒有 CRC,用埠 502。
- 交易識別碼讓 client 對上請求與回應;單元識別碼經過閘道器時就是 RS-485 上的站號。
ModbusTcpClient(主機, port=502)→connect()→ 讀寫 →close(),或用with。- 連不上:connect() 回傳 False、讀取丟 ConnectionException;沒回應:ModbusIOException;位址錯:例外回應。
- 遠端控制寫命令暫存器,讓 PLC 程式決定輸出;安全條件留在 PLC 裡。
- pymodbus 3.8 起參數要用關鍵字寫,3.10 起站號叫
device_id。
下一課把讀取放進迴圈:定時輪詢、記錄成 CSV、超過門檻時發出告警。