Learning Objectives
- 說明 Modbus 的 client/server(舊稱主站/從站)分工,以及什麼時候需要自己做一個從站
- 用 pymodbus 建立資料區塊、裝置上下文與伺服器上下文,啟動 Modbus TCP 從站
- 說出功能碼對應到哪一個資料區塊,以及請求超出範圍時從站會怎麼回應
- 用 Python 從站模擬一台裝置,讓 HMI 或監控程式在沒有真實設備時也能測試
換個角色:讓 Python 回答別人的問題
前兩課的 Python 程式都是發問的一方:連到 PLC、送出「讀保持暫存器 0~3」,PLC 回答。 這一課反過來,讓 Python 被別人問:它開一個 Modbus TCP 的連接埠,等別的程式來讀寫,照請求回應資料。
Modbus 的兩個角色,舊的文件叫主站(master)與從站(slave);Modbus Organization 在 2020 年 7 月宣布改用 client(客戶端)與 server(伺服器)。兩組名稱的意思一樣,現在的手冊、軟體兩種都看得到:
| 新名稱 | 舊名稱 | 做什麼 | 常見的例子 |
|---|---|---|---|
| client(客戶端) | 主站(master) | 主動送出請求 | HMI、SCADA、上一課的 Python 監控程式 |
| server(伺服器) | 從站(slave) | 等待請求、回應資料 | PLC、變頻器、電錶、溫控器 |
什麼時候需要自己做一個從站?
- 模擬裝置:HMI 畫面或監控程式寫好了,現場的設備還沒到貨。先用 Python 從站假裝成那台設備,把畫面測一遍。
- 測試:故意讓從站回傳極端值、例外回應,看你的監控程式會不會當掉。
- 閘道器:把別的資料來源(感測器、資料庫、另一種協定)轉成 Modbus,讓只會講 Modbus 的設備讀得到。
這一課要回答:
- pymodbus 的從站由哪幾層物件組成?
- 啟動之後,程式停在哪裡?
- 請求的位址超出範圍時,從站回什麼?
逐步看:一個最小的 Modbus 從站
前面幾步在準備資料,最後一步 StartTcpServer 開始等待請求。這個模擬環境照規格的時間表送來 4 個請求,
全部都發生在最後一步:看畫面下方的通訊紀錄,「收到請求」是別的程式問的問題,下一行是從站的回答。
注意第 2 秒有人把保持暫存器 1 改成 650,第 3 秒再讀,讀到的就是新的值。
資料區塊、裝置、伺服器:三層容器
pymodbus 的從站資料一層包一層:
ModbusServerContext(整台伺服器)
└─ ModbusDeviceContext(一台裝置,對應一個站號)
├─ co:線圈 ← 功能碼 1 讀、5/15 寫
├─ di:離散輸入 ← 功能碼 2 讀
├─ hr:保持暫存器 ← 功能碼 3 讀、6/16 寫
└─ ir:輸入暫存器 ← 功能碼 4 讀
收到請求時,伺服器依功能碼決定找哪一個區塊,再依位址與數量取出資料。 如果要的位址不在區塊範圍內(例如區塊只有 10 格,卻要讀位址 12),從站不會亂回一個數字, 而是回例外回應:功能碼加上 0x80,例外碼 2(Illegal Data Address,位址不合法)。第 10 課的資料模型表格就是這樣定義的。
一台伺服器也可以裝好幾台裝置:ModbusServerContext(devices={1: 裝置甲, 2: 裝置乙}, single=False),
依請求的站號分給不同的裝置,常用在一個閘道器後面接了好幾台設備的情況。
使用時要注意的地方
- 一直等待:
StartTcpServer會佔住整個程式。要讓從站的資料自己變化(例如每秒更新溫度), 真實的程式會另外開一個執行緒,或改用非同步版本StartAsyncTcpServer,這一課先不處理。 - 連接埠:Modbus TCP 的標準埠是 502。在 Linux 上,1024 以下的埠需要系統管理員權限, 所以在自己電腦上練習時常改用 5020 之類的埠,客戶端也要連同一個埠。
- 版本差異:本課的寫法適用 pymodbus 3.10 以後的 3.x 版(這個模擬環境對應 3.10)。
較舊的版本把
ModbusDeviceContext叫做ModbusSlaveContext、把device_id=叫做slave=; 3.13 起直接讀寫資料區塊的getValues/setValues改由伺服器物件處理,新版也已把這三個資料類別標示為即將淘汰(v4 改用 SimData/SimDevice)。 開新專案前先查你安裝的版本與官方文件。 - 位址加 1:pymodbus 3.x 的裝置上下文會把請求位址加 1 再去資料區塊找。範例都透過
device.setValues/getValues存取, 兩邊用同一套規則,所以不用自己處理;如果你跳過裝置、直接讀寫資料區塊,就要記得這個差 1。
逐步看:用對照表模擬一台溫控器
做模擬裝置時,先寫一張「暫存器對照表」:哪個位址放什麼、單位怎麼換算,再用迴圈放進資料區塊。 這張表也是你交給寫 HMI 的同事的文件。注意最後一個請求要讀位址 8~11,但區塊只有 0~9,所以得到例外回應。
小測驗:角色與資料區塊
一台 HMI 每秒向變頻器讀取轉速。用 Modbus 現在的名稱,誰是 client、誰是 server?
pymodbus 從站收到功能碼 4(讀輸入暫存器)的請求,會到哪一個資料區塊找資料?
程式在 StartTcpServer(...) 下一行寫了 print("啟動完成"),執行後會怎樣?
小測驗:位址與例外
資料區塊從位址 0 開始、共 10 格。客戶端要讀「位址 8 開始、4 個」保持暫存器,從站會回什麼?
下面的程式模擬「裝置把位址加 1 再去資料區塊找」,會印出什麼?
values = [0, 235, 600, 1]
def get(address, count):
address += 1
return values[address:address + count]
print(get(0, 2))
重點整理
- Modbus 的兩個角色:client(舊稱主站)送出請求,server(舊稱從站)等待並回應;Modbus Organization 在 2020 年改用新名稱。
- pymodbus 從站三層:
ModbusSequentialDataBlock(資料區塊)→ModbusDeviceContext(一台裝置的 co、di、hr、ir)→ModbusServerContext(整台伺服器)。 setValues/getValues用功能碼指定區塊:1→線圈、2→離散輸入、3→保持暫存器、4→輸入暫存器。StartTcpServer會一直等待請求,不會回傳;位址超出範圍時從站回例外碼 2。- 自己做從站的用途:模擬還沒到貨的裝置、測試監控程式、做協定閘道器。寫好暫存器對照表,同時也是給 HMI 的文件。
- pymodbus 的 API 在 3.x 各版之間有改名,開新專案先確認版本。
下一課處理 16 位元裝不下的數值:用兩個暫存器組成 32 位元的整數與浮點數。