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 為廣播)。
| 欄位 | 長度 | 說明 |
|---|---|---|
| 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)。
| 功能碼 | 名稱 | 操作對象 | 說明 |
|---|---|---|---|
| 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 定義了四種資料類型,每種最多 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 橋接。
- 差動對:使用雙絞線(Twisted Pair),A(+) 接 A(+),B(-) 接 B(-)
- 終端電阻:在匯流排兩端各接 120Ω 電阻,防止訊號反射
- 偏壓電阻:在 Master 端加 680Ω 上拉(A→VCC)和下拉(B→GND),確保空閒時為已知狀態
- 隔離:長距離或工業環境使用隔離型 RS485 收發器(如 ADM2483)
- 接地:設備之間需共地(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 演算法
文章評論