0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

Modbus 通訊協定完整教學:從 RTU 到 TCP,ESP32/STM32 實作

2026 年 5 月 22 日 944點熱度 0人點贊 0條評論

Modbus 通訊協定完整教學封面

前言:1979 年的協定,為什麼現在還在跑

Modbus 是 Modicon 在 1979 年為 PLC 設計的協定,簡單到有點過時 —— 但它今天還活在你家附近的工廠、大樓機房與太陽能電廠裡。

原因很務實:它夠簡單、夠開放、實作成本夠低。一台變頻器只要宣稱支援 Modbus RTU,就能被任何 SCADA 系統讀到。這種「誰都做得到」的特性,比任何華麗的規格都長壽。

規矩只有一條:只能有一個 Master

Modbus 主從架構拓撲

Master 負責發問,Slave 只負責回答 —— Slave 永遠不會主動開口。RTU 模式使用位址 1~247,位址 0 是廣播(所有 Slave 都收到但不回應)。

訊框本身也非常精簡,只有四個欄位:

欄位 長度 說明
Slave Address 1 byte Slave 位址(1~247),0 表示廣播
Function Code 1 byte 功能碼(01~127),高位元為 1 表示異常回應
Data Field N bytes 依功能碼而異(起始位址、數量、資料值)
CRC16 2 bytes CRC-16/MODBUS 校驗,Low byte 在前

這裡有個容易被忽略的設計:RTU 不靠長度判斷封包結束,而是靠時間 —— 兩個封包之間要留至少 3.5 個字元時間的靜默。以 9600 bps 來說約 3.65 ms。

常用功能碼:記住 03 與 06 就夠用

功能碼 名稱 操作對象 說明
01 (0x01) Read Coils Coils 讀取多個線圈狀態(1 bit)
02 (0x02) Read Discrete Inputs Discrete Inputs 讀取多個離散輸入(1 bit)
03 (0x03) Read Holding Registers Holding Registers 讀取多個保持暫存器(16 bit)
04 (0x04) Read Input Registers Input Registers 讀取多個輸入暫存器(16 bit)
05 (0x05) Write Single Coil Coils 寫入單一線圈
06 (0x06) Write Single Register Holding Registers 寫入單一保持暫存器
15 (0x0F) Write Multiple Coils Coils 寫入多個線圈
16 (0x10) Write Multiple Registers Holding Registers 寫入多個保持暫存器

功能碼分成讀(01~04)與寫(05~16)兩類。實務上九成的應用只會用到兩個:03(讀保持暫存器)與 06(寫單一保持暫存器)。其他都是為了特殊硬體或歷史包袱存在的。

四種資料模型:新手最容易搞混的地方

Modbus 四種資料模型

為什麼要分成四種?因為 Modbus 是為了對應真實世界的接線而設計的:線圈對繼電器、離散輸入對開關、輸入暫存器對感測器、保持暫存器對設定值。

記法很簡單:3 開頭的是唯讀(感測器來的),4 開頭的是可讀可寫(你要設定的)。

CRC-16/MODBUS:兩個位元組的保命符

RTU 沒有 TCP 那樣的校驗層,所以每個訊框最後都帶 2 bytes 的 CRC,而且低位元組在前。計算方式用查表法最快:

// CRC-16/MODBUS 查表法
static const uint16_t modbus_crc_table[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
    0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
    // ... (完整 256 項查表)
};

uint16_t modbus_crc16(const uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc = modbus_crc_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
    }
    return crc;  // 低位元組在前
}

這個 CRC 是現場除錯的好工具 —— 當你懷疑「是不是線路問題」時,先算一次 CRC,就知道封包本身有沒有壞。

ESP32 當 Master:讀一台溫度控制器

只要有一顆 RS485 收發器(例如 MAX485)加上 UART,ESP32 就能當 Modbus Master。流程就是「組封包 → 送出 → 等回應 → 驗 CRC → 取出資料」:

#include <HardwareSerial.h>

#define TX2_PIN  17
#define RX2_PIN  16
#define DE_RE_PIN 4   // RS485 方向控制 (DE=1 TX, RE=0 RX)

HardwareSerial ModbusSerial(2);

void setup() {
    Serial.begin(115200);
    ModbusSerial.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
    pinMode(DE_RE_PIN, OUTPUT);
    digitalWrite(DE_RE_PIN, LOW);  // 預設接收模式
}

// 發送 Modbus RTU 請求
void modbus_send(uint8_t *frame, uint16_t len) {
    digitalWrite(DE_RE_PIN, HIGH);   // 切換為 TX
    delay(5);
    ModbusSerial.write(frame, len);
    ModbusSerial.flush();
    delay(5);
    digitalWrite(DE_RE_PIN, LOW);    // 切換為 RX
}

// 讀取 Holding Register (Func 03)
bool read_holding_register(uint8_t slave, uint16_t reg_addr, uint16_t *value) {
    uint8_t req[] = {slave, 0x03, (reg_addr>>8)&0xFF, reg_addr&0xFF, 0x00, 0x01};
    uint16_t crc = modbus_crc16(req, 6);
    req[6] = crc & 0xFF;
    req[7] = (crc >> 8) & 0xFF;

    modbus_send(req, 8);

    // 讀取回應 (timeout 100ms)
    uint8_t resp[8];
    uint32_t t = millis();
    uint8_t idx = 0;
    while (millis() - t < 100 && idx < 8) {
        if (ModbusSerial.available()) {
            resp[idx++] = ModbusSerial.read();
        }
    }
    if (idx < 8) return false;  // 逾時

    // 驗證 CRC
    crc = modbus_crc16(resp, 6);
    if (resp[6] != (crc & 0xFF) || resp[7] != ((crc>>8)&0xFF)) return false;

    *value = (resp[3] << 8) | resp[4];
    return true;
}

void loop() {
    uint16_t temp;
    if (read_holding_register(0x01, 0x0000, &temp)) {
        // 假設暫存器值為溫度(縮放 0.1°C)
        float celsius = temp * 0.1;
        Serial.printf("溫度: %.1f°C\n", celsius);
    } else {
        Serial.println("Modbus 讀取失敗");
    }
    delay(1000);
}

寫這種 Master 有兩個實務重點:方向控制(DE/RE)要在送完立刻切回接收,以及等待回應一定要設 timeout。少了前者會撞線,少了後者程式會卡死。

STM32 當 Slave:用 FreeModbus 少走冤枉路

要讓 MCU 當 Slave,自己實作 RTU 狀態機(尤其是那個 3.5 字元靜默判斷)很花時間。FreeModbus 這類開源實作可以省下大半功夫,你只需要寫回呼函式:

// FreeModbus 回呼函數:讀取 Holding Register
eMBErrorCode eMBRegHoldingCB(
    UCHAR *pucRegBuffer, USHORT usAddress,
    USHORT usNRegs, eMBRegisterMode eMode)
{
    // usAddress 從 0 開始(對應 40001)
    uint16_t reg_addr = usAddress;

    if (eMode == MB_REG_READ) {
        // 讀取:將內部變數填入緩衝區
        for (int i = 0; i < usNRegs; i++) {
            uint16_t val = get_sensor_value(reg_addr + i);
            pucRegBuffer[i * 2]     = (val >> 8) & 0xFF;     // High byte
            pucRegBuffer[i * 2 + 1] = val & 0xFF;            // Low byte
        }
    } else {
        // 寫入:從緩衝區讀取並更新內部變數
        for (int i = 0; i < usNRegs; i++) {
            uint16_t val = (pucRegBuffer[i * 2] << 8) | pucRegBuffer[i * 2 + 1];
            set_actuator_value(reg_addr + i, val);
        }
    }
    return MB_ENOERR;
}

int main(void) {
    // 初始化 USART2 (RS485)
    eMBInit(MB_RTU, 0x01, 2, 9600, MB_PAR_NONE);
    eMBEnable();
    while (1) {
        eMBPoll();  // 不斷輪詢 Modbus 事件
    }
}

回呼的邏輯很單純:把 Master 要讀的暫存器位址對應到你內部的變數,其他交給協定堆疊處理。

RTU 還是 TCP?先看你接的是什麼

特性 Modbus RTU Modbus TCP
傳輸層 RS232 / RS485 TCP/IP (Ethernet/WiFi)
最大節點 32 (RS485) / 247 (理論) 無限制
Slave 位址 1 byte (1~247) Unit ID (通常 1)
錯誤檢驗 CRC-16 TCP/IP 層處理
Header 無 MBAP (7 bytes)
傳輸距離 ~1200m (RS485) 取決於網路
實時性 確定性(Polling) 依賴網路 QoS

一句話決策:現場設備走 RTU over RS485,上位機與 SCADA 走 TCP。需要遠端監控時,就在中間掛一台 RTU-to-TCP Gateway,兩邊都不用改。

RS485 接線五要點

Modbus RTU 的「通訊不穩」有九成發生在硬體層,而且都是同一批原因:

RS485 接線五要點

其中最容易被忽略的是偏壓電阻。少了它,匯流排空閒時的電位不確定,接收端會收到莫名其妙的位元,症狀看起來就像「偶發的 CRC 錯誤」。

常見問題:先分類,再查線

Modbus 常見問題與追查方向

這三類症狀幾乎涵蓋所有現場狀況。我的建議是先看 CRC 錯誤率高不高:如果 CRC 大量出錯,那是訊號或接線問題;如果封包乾淨但 Slave 不回,那才是位址或設定的問題。

總結

Modbus 的價值在於「簡單到不會壞,壞了也很好查」。四個重點:

  • 單一 Master:所有通訊都由 Master 輪詢發起,Slave 不主動說話
  • 四個欄位 + CRC:訊框精簡,校驗自己做
  • 四種資料模型:3 開頭唯讀、4 開頭可寫,別再記錯
  • RS485 硬體決定成敗:雙絞線、終端電阻、偏壓電阻、共地,缺一項就會有鬼故事

把這四點弄懂,之後不管是接變頻器、電表還是 PLC,你都只需要查它的暫存器對照表,不用重新學一套協定。

標籤: 工業通訊 教學 生產力
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang