0x6A Logbook

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

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

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

Modbus 通訊協定是工業自動化領域最廣泛應用的通訊協議之一,從 PLC、感測器、馬達驅動器到 SCADA 系統,幾乎所有工業設備都支援 Modbus。它簡單、穩定、開源且跨平台。

本文從 Modbus 通訊協定的歷史與分類出發,深入解析 Modbus RTU 和 Modbus TCP 的幀格式、功能碼、資料模型,並以 ESP32 和 STM32 為平台展示完整的 C 語言實作。

Modbus 協議概述

Modbus 由 Modicon 公司在 1979 年發明,採用 Master/Slave(主從)架構。一個網路上只能有一個 Master,最多 247 個 Slave(RTU 模式使用位址 1~247,0 為廣播)。

Modbus 網路拓撲圖
圖 1:Modbus 網路拓撲 — Master 透過 RS485 匯流排與多個 Slave 通訊,並可透過 TCP Gateway 橋接到乙太網路
Modbus RTU 幀格式
圖 2:Modbus RTU 幀格式 — 共 4 個欄位:位址、功能碼、資料、CRC
欄位 長度 說明
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.65ms)。

Modbus 功能碼
圖 3:常用 Modbus 功能碼 — 讀取類(01~04)與寫入類(05~16)
功能碼 名稱 操作對象 說明
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 寫入多個保持暫存器
Modbus 資料模型
圖 4:Modbus 資料模型 — 四種資料類型各有 65536 個定址空間

Modbus 定義了四種資料類型,每種最多 65536 個地址:

  • Coils (00001~09999):1 bit 可讀寫,對應實體繼電器輸出
  • Discrete Inputs (10001~19999):1 bit 唯讀,對應實體按鈕/開關
  • Input Registers (30001~39999):16 bit 唯讀,對應感測器/ADC 數值
  • Holding Registers (40001~49999):16 bit 可讀寫,對應設定參數/控制值

Modbus RTU 使用 CRC-16/MODBUS 演算法進行錯誤檢驗。以下是以 ESP32/STM32 實作的查表法:

// 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;  // 低位元組在前
}

以下是以 ESP32 搭配 UART2 與 RS485 收發器(MAX485),讀取 Slave 01 的 Holding Register 0 的完整範例:

#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);
}

STM32 作為 Modbus Slave 時,建議使用 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 事件
    }
}
特性 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 橋接。

  1. 差動對:使用雙絞線(Twisted Pair),A(+) 接 A(+),B(-) 接 B(-)
  2. 終端電阻:在匯流排兩端各接 120Ω 電阻,防止訊號反射
  3. 偏壓電阻:在 Master 端加 680Ω 上拉(A→VCC)和下拉(B→GND),確保空閒時為已知狀態
  4. 隔離:長距離或工業環境使用隔離型 RS485 收發器(如 ADM2483)
  5. 接地:設備之間需共地(GND 相連),否則共模電壓會擊穿收發器
  • 檢查 Slave 位址是否正確
  • 檢查 RS485 A/B 線是否接反
  • 檢查 Baud Rate 是否一致
  • 用邏輯分析儀抓取 TX 訊號確認封包格式
  • baud rate 太高導致訊號失真(工業環境建議 ≤ 19200)
  • 終端電阻缺失或位置不對
  • 接地不良導致共模電壓超過收發器容忍範圍
  • 某個 Slave 的 TX 方向控制邏輯錯誤,一直佔用匯流排
  • DE/RE 引腳沒有正確切換
  • Slave 回應時間超過 Master timeout

Modbus 通訊協定雖然問世超過 40 年,仍然是工業自動化領域最核心的通訊標準。它的簡單性和可靠性讓它在 IoT 和 Industry 4.0 時代依然活躍。

在 ESP32 或 STM32 上實作 Modbus 時,先確定你需要的資料類型(Coil、Register),選擇正確的功能碼(03/06/16),然後處理好 RS485 的硬體接線和方向控制。對於複雜的 Slave 應用,直接使用 FreeModbus 庫可以省下大量開發時間。

📖 延伸閱讀:RS485 通訊協定完整教學 · FIFO 完全解析 · PID 演算法

 

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

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