iT邦幫忙

2

【技術分享】Modbus 神坑:為什麼 Schneider PLC 的 Float32 讀回來變成 1.4e-41?

  • 分享至 

  • xImage
  •  

為什麼你的 Schneider PLC Float32 讀回來是 1.4e-41 而不是 25.5°C

痛點開頭

你用 Python 寫好 pymodbus 腳本,連上 Schneider M221,讀取一對你明明知道溫度是 25.5°C 的 holding register,結果拿到的是:

1.401298464324817e-45

不是 25.5。連邊都沒沾到。一個極小到接近 0 的非正規化浮點數(denormal float)。

歡迎來到 Modbus float32 byte-swap 陷阱——這是工程師最常以為「pymodbus 壞掉」但其實沒壞的問題。一旦你搞懂發生了什麼事,修正只要 5 行程式碼。這篇文章就是要把這件事講清楚。


為什麼會這樣

Modbus 規範把 register 定義成 16-bit unsigned integer。就這樣。沒了。這個規範是 1979 年寫的,當時這就夠用了。

但現代 PLC 需要傳 float、32-bit integer、double、字串。業界採用的「解法」是:把比較大的值拆開,跨好幾個 16-bit register 存

問題是?Modbus 規範對拆開後的順序沒有任何規定

每家 PLC 廠商各自決定要把高字組(high word)放前面還是低字組(low word)放前面、要不要在 word 裡面交換 bytes、要不要全部翻過來。結果就是同一個 float 有 4 種不同的編碼方式,而且沒辦法從封包本身判斷你拿到的是哪一種

這就是為什麼你 PLC 軟體上明明顯示 25.5°C,但 Python 腳本讀到 1.4e-41——同樣的 bytes,不同的解碼順序


你會遇到的 4 種 byte order

IEEE-754 的 32-bit float 25.5 對應的 byte sequence 是:

0x41 0xCC 0x00 0x00

現在來看不同 PLC 把這 4 個 bytes 塞進 2 個 Modbus register 時會發生什麼:

順序 Register N Register N+1 常見於
ABCD(big-endian,無 swap) 0x41CC 0x0000 Allen-Bradley、ABB、實作乾淨的設備
CDAB(word-swap) 0x0000 0x41CC Schneider M221/M241、部分 Siemens
BADC(byte-swap) 0xCC41 0x0000 少見——某些舊型 controller
DCBA(byte + word swap) 0x0000 0xCC41 多半是 legacy 硬體

如果裝置送的是 CDAB 但你用 ABCD 解碼,結果會是極小的 denormal float(你看到的 1.4e-41)、或 nan、或 inf、或一堆毫無意義的數字。

Schneider 的 CDAB(word-swap)是最坑大多數人的陷阱


程式碼:用 Python 解出全部 4 種順序

純 stdlib,不需要任何依賴。直接拿去用:

import struct

def decode_float32(reg_high: int, reg_low: int, byte_order: str = "ABCD") -> float:
    """把兩個 16-bit Modbus register 解碼成 float32。

    byte_order:
        ABCD - big-endian,無 swap(IEEE-754 預設)
        CDAB - word-swap(Schneider、部分 Siemens)
        BADC - 每個 word 內部 byte-swap
        DCBA - byte + word 同時 swap
    """
    # 把兩個 register 打包成 4 bytes(big-endian)
    raw = struct.pack(">HH", reg_high, reg_low)

    if byte_order == "ABCD":
        return struct.unpack(">f", raw)[0]
    elif byte_order == "CDAB":
        # 交換兩個 16-bit word
        return struct.unpack(">f", raw[2:4] + raw[0:2])[0]
    elif byte_order == "BADC":
        # 每個 word 內部交換 bytes
        return struct.unpack(">f", raw[1:2] + raw[0:1] + raw[3:4] + raw[2:3])[0]
    elif byte_order == "DCBA":
        # 4 bytes 全部反過來
        return struct.unpack(">f", raw[::-1])[0]
    else:
        raise ValueError(f"Unknown byte order: {byte_order}")


# 範例:用 pymodbus 讀值,4 種順序全部跑過一次
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("192.168.1.10")
client.connect()

response = client.read_holding_registers(address=100, count=2)
reg_high, reg_low = response.registers[0], response.registers[1]

print(f"ABCD: {decode_float32(reg_high, reg_low, 'ABCD')}")
print(f"CDAB: {decode_float32(reg_high, reg_low, 'CDAB')}")
print(f"BADC: {decode_float32(reg_high, reg_low, 'BADC')}")
print(f"DCBA: {decode_float32(reg_high, reg_low, 'DCBA')}")

訣竅:每次接新裝置時,4 種全部印出來。其中唯一一個給你合理數值(例如你知道溫度是 25.5,就會看到 25.5)的那個就是該裝置的編碼方式。確認後鎖定該裝置用那一種。


各廠牌實戰經驗

整合過這麼多年的 PLC,我看到的 byte order 大致是這樣:

  • Schneider M221、M241、M340 → 幾乎都是 CDAB(word-swap)。這是「我 Python 腳本壞了」最常見的兇手第一名
  • Siemens S7-1200、S7-1500 → 看 float 怎麼存。如果是透過 Modbus TCP wrapper 暴露出來,常常也是 CDAB
  • Allen-Bradley CompactLogix → 透過 Prosoft Modbus 模組時通常 ABCD,乾淨
  • Mitsubishi FX 系列 → 大部分配置下是 ABCD
  • Delta DVPABCD
  • 電力錶(Schneider PM5xxx 系列) → 通常 CDAB
  • 變頻器(ABB ACS、Schneider Altivar) → 混亂——一定要 4 種全測

教訓:永遠不要假設。即使同一個廠商,不同產品線都可能挑不同順序。新裝置一律先做「4 種全部印」測試。


常見的雷

1. 有些函式庫會「靜悄悄」做 swap——而且靜悄悄做錯

pymodbus 內建的 BinaryPayloadDecoder 讓你指定 byteorder=Endian.BIG, wordorder=Endian.LITTLE(也就是 CDAB)。但這個 API 真的混亂到我看過有人兩個都設成 BIG 然後 debug 好幾個小時。直接用 struct 手動解碼比較清楚

2.「負數」是紅色警報

如果你解碼出來的值是 -1.5e+38 而你期待的是 25.5幾乎可以確定 byte order 抓錯。在任何正常 scaling 下,個位數的正溫度不會意外被編碼成超大負數。

3. int32 也有同樣問題

這篇文章聚焦在 float32 因為它最痛,但所有跨 register 的型別都有這個問題。int32、uint32、int64、double——全部都會跨 register 拆開存,全部都需要做 byte-order 處理。

4. 有些裝置把 swap 設定存在 register 裡

很煩但真的有:少數高階 PLC 讓使用者自己設定 endianness。跑測試程式前,先翻一下手冊有沒有「byte order」或「word order」這種參數。


結語

Modbus float32 byte-swap 屬於那種「看起來像你程式碼有 bug,其實是協定歷史遺留」的問題。一旦你知道要 4 種都試,新裝置的整合時間會從幾個小時壓縮到幾分鐘。

希望這篇有幫到你少踩一個坑。


如果你想看更多工業 Python 跟協定底層相關的文章,可以追一下我的 GitHub:github.com/PhilYeh1212,有興趣可以聊聊。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
一級屠豬士
iT邦大師 1 級 ‧ 2026-05-07 23:56:56
 raw[1:2] + raw[0:1] + raw[3:4] + raw[2:3]

這段寫法 可以更精簡

PhilYeh iT邦新手 5 級 ‧ 2026-05-08 14:01:51 檢舉

沒錯的喔!!
每個人寫法習慣不同,就看自己順手

我要留言

立即登入留言