National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 11

Modbus TCP 與 pymodbus

用 Python 讀寫 PLC 的資料
Interaction 30 minute(s)

Learning Objectives

  • 說明 Modbus TCP 和 Modbus RTU 的差別:MBAP 標頭、沒有 CRC、埠 502
  • 拆解 MBAP 標頭的四個欄位,說出交易識別碼與單元識別碼的用途
  • 用 pymodbus 的 ModbusTcpClient 連線、讀寫 PLC,並處理連線失敗與沒有回應
  • 說明遠端控制為什麼寫「命令暫存器」,而不是直接改 PLC 程式在控制的輸出

把 Modbus 搬到乙太網路上

RS-485 的設備要一台一台輪流問,速度也有限。現在的 PLC、HMI、電力監控設備大多有乙太網路埠, Modbus TCP 把同樣的請求與回應放進 TCP 連線裡傳送:功能碼、位址、資料的格式都沒變,上一課學的四張表照樣適用。

這一課要回答:

  1. Modbus TCP 的訊框和 RTU 差在哪裡?
  2. 用 Python 讀寫 PLC 的完整流程是什麼?連不上、沒回應時怎麼辦?
  3. 要從遠端啟動一台泵浦,應該寫哪裡?

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)。

互動練習:組 MBAP 訊框

依序完成任務:

  1. 組出「交易識別碼 1,讀取單元 1 的保持暫存器,從位址 0 讀 2 個」的 TCP 請求。
  2. 下一筆請求:交易識別碼 2,把保持暫存器 0 寫成 1(寫單一暫存器)。注意長度欄位的值。
  3. 按「貼上範例」解讀一筆例外回應。
  4. 切到 RTU,組出和任務 1 相同內容的請求,比較兩種訊框。
  5. 回答一個問題。

用 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 的時間。

互動練習:遠端啟停水箱泵浦

PLC 在 192.168.0.10,階梯圖:命令 MW0=1、而且液位低於 80% 時開泵浦(Q0.0)。水箱從 30% 開始,一直有少量用水流出。依序完成任務:

  1. 直接執行起始程式,讀到液位。
  2. 寫命令 MW0=1 啟動泵浦,讓液位升到 50% 以上。
  3. 用 for 迴圈每秒讀一次液位,印成百分比,至少印 5 行。
  4. 結束前把命令寫回 0,讓泵浦在程式結束時是停的。
  5. 回答一個問題。

小測驗:MBAP 與 TCP

1.

Modbus TCP 請求 00 07 00 00 00 06 01 03 00 0A 00 02 的交易識別碼與要讀的位址是?

2.

MBAP 的長度欄位,在讀保持暫存器的請求裡是 6,指的是哪些位元組?

3.

用 Modbus TCP 經過閘道器讀取後面 RS-485 上站號 5 的電力錶,單元識別碼要填多少?

第 1 次作答

小測驗:程式與錯誤處理

1.

下面的程式模擬交易識別碼的變化,會印出什麼?

tid = 0
log = []
for fc in [3, 4, 6]:
    tid += 1
    log.append((tid, fc))
print(log[-1])
2.

client.connect() 回傳 False,最可能的原因是?

第 1 次作答

重點整理

  • 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、超過門檻時發出告警。

這一課完成了!
Next lesson:用 Python 監控 PLC
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。