
前言:1979 年的協定,為什麼現在還在跑
Modbus 是 Modicon 在 1979 年為 PLC 設計的協定,簡單到有點過時 —— 但它今天還活在你家附近的工廠、大樓機房與太陽能電廠裡。
原因很務實:它夠簡單、夠開放、實作成本夠低。一台變頻器只要宣稱支援 Modbus RTU,就能被任何 SCADA 系統讀到。這種「誰都做得到」的特性,比任何華麗的規格都長壽。
規矩只有一條:只能有一個 Master

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 是為了對應真實世界的接線而設計的:線圈對繼電器、離散輸入對開關、輸入暫存器對感測器、保持暫存器對設定值。
記法很簡單: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 的「通訊不穩」有九成發生在硬體層,而且都是同一批原因:

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

這三類症狀幾乎涵蓋所有現場狀況。我的建議是先看 CRC 錯誤率高不高:如果 CRC 大量出錯,那是訊號或接線問題;如果封包乾淨但 Slave 不回,那才是位址或設定的問題。
總結
Modbus 的價值在於「簡單到不會壞,壞了也很好查」。四個重點:
- 單一 Master:所有通訊都由 Master 輪詢發起,Slave 不主動說話
- 四個欄位 + CRC:訊框精簡,校驗自己做
- 四種資料模型:3 開頭唯讀、4 開頭可寫,別再記錯
- RS485 硬體決定成敗:雙絞線、終端電阻、偏壓電阻、共地,缺一項就會有鬼故事
把這四點弄懂,之後不管是接變頻器、電表還是 PLC,你都只需要查它的暫存器對照表,不用重新學一套協定。
文章評論