ระดับ 1: พื้นฐาน
ใบงานที่ 1: การสื่อสาร RS485 เบื้องต้นและการทดสอบ Loopback
วัตถุประสงค์: เรียนรู้โครงสร้างสายสัญญาณ RS485, โมดูล MAX485/SP3485 และการควบคุมพิน DE/RE บน ESP32
💡 คำอธิบายเสริมให้เข้าใจง่าย:
Modbus คืออะไร? คือภาษา (Protocol) ที่อุปกรณ์อุตสาหกรรมใช้คุยกันผ่านสายสัญญาณ 2 เส้น (A และ B)
RS485 คืออะไร? คือชนิดของสายสัญญาณ physical (การใช้ความต่างศักย์ไฟฟ้าส่งข้อมูล) สาย A กับ B ทำงานแบบ Differential Signal ทนสัญญาณรบกวนได้สูงมาก ยาวได้ถึง 1.2 กิโลเมตร
ทำไมต้องมีขา DE / RE? สื่อสารแบบ Half-Duplex (ผลัดกันพูด) ขา DE (Driver Enable) ใช้เปิดตอนส่งข้อมูล และ RE (Receiver Enable) ใช้เปิดตอนรับข้อมูล ปกติต่อรวมกันเข้าพิน GPIO เดียวกัน
การต่อวงจร (Hardware Wiring)
ESP32 MAX485 Module
GPIO16 (RX2) -----------> RO (Receiver Output)
GPIO17 (TX2) -----------> DI (Driver Input)
GPIO4 -----------> DE + RE (ต่อขนานกัน)
3.3V / 5V -----------> VCC
GND -----------> GND
โค้ดตัวอย่าง ESP32 (Arduino IDE)
ติดตั้งไลบรารี: ModbusMaster โดย Doc Walker ผ่าน Library Manager
#include <HardwareSerial.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
void setup() {
Serial.begin(115200);
pinMode(MAX485_DE_RE, OUTPUT);
digitalWrite(MAX485_DE_RE, LOW); // เริ่มต้นตั้งเป็นโหมดรับ (Receive Mode)
// เริ่มต้น HardwareSerial2 สำหรับ RS485
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
Serial.println("RS485 Hardware Test Ready");
}
void loop() {
// สลับเป็นโหมดส่ง (Transmit Mode)
digitalWrite(MAX485_DE_RE, HIGH);
delay(1);
Serial2.println("Hello Modbus World!");
Serial2.flush(); // รอส่งข้อมูลเสร็จสิ้น
// สลับกลับเป็นโหมดรับ (Receive Mode)
digitalWrite(MAX485_DE_RE, LOW);
delay(2000);
}
🎯 โจทย์ท้าทาย:
ลองใช้บอร์ด ESP32 สองบอร์ด ต่อสาย A-A และ B-B เข้าหากัน แล้วทดสอบส่งข้อความสวนกันเพื่อเช็คว่าโมดูลสื่อสารได้ถูกต้องหรือไม่
ระดับ 2: เริ่มต้น
ใบงานที่ 2: การอ่านค่าเซนเซอร์อุณหภูมิและความชื้น XY-MD02 (Modbus RTU Master)
วัตถุประสงค์: สื่อสารอ่านค่า Register จาก Modbus Slave จริง ผ่านฟังก์ชัน Read Input Registers (0x04)
💡 คำอธิบายเสริมให้เข้าใจง่าย:
Modbus Register คืออะไร? เปรียบเหมือน "ตู้ไปรษณีย์" ที่มีหมายเลขกำกับ (Address)
01 Read Coils (0x01): อ่านสถานะ Digital Output (ON/OFF)
02 Read Discrete Inputs (0x02): อ่านสถานะ Digital Input (ON/OFF)
03 Read Holding Registers (0x03): อ่าน/เขียนค่าตัวเลข (Read/Write 16-bit)
04 Read Input Registers (0x04): อ่านค่าตัวเลขจากเซนเซอร์ได้อย่างเดียว (Read-only 16-bit)
เซนเซอร์ XY-MD02 มี Address สถิตคือ Slave ID = 1, คำนวณอุณหภูมิที่ Register 0x0001 และความชื้นที่ Register 0x0002 (ต้องหารด้วย 10.0)
โค้ดตัวอย่าง ESP32
#include <ModbusMaster.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
ModbusMaster node;
void preTransmission() {
digitalWrite(MAX485_DE_RE, HIGH);
}
void postTransmission() {
digitalWrite(MAX485_DE_RE, LOW);
}
void setup() {
Serial.begin(115200);
pinMode(MAX485_DE_RE, OUTPUT);
digitalWrite(MAX485_DE_RE, LOW);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
// ตั้งค่า Modbus Slave ID = 1
node.begin(1, Serial2);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
}
void loop() {
uint8_t result;
// อ่าน Input Register ตั้งแต่ 0x0001 จำนวน 2 ตัว
result = node.readInputRegisters(0x0001, 2);
if (result == node.ku8MBSuccess) {
float temperature = node.getResponseBuffer(0) / 10.0;
float humidity = node.getResponseBuffer(1) / 10.0;
Serial.print("Temp: ");
Serial.print(temperature);
Serial.print(" C | Humidity: ");
Serial.print(humidity);
Serial.println(" %");
} else {
Serial.print("Modbus Error: ");
Serial.println(result, HEX);
}
delay(2000);
}
🎯 โจทย์ท้าทาย:
แปลงค่าอุณหภูมิที่อ่านได้เป็นองศาฟาเรนไฮต์ (F) แล้วแสดงผลผ่าน Serial Monitor
ระดับ 3: ปานกลาง
ใบงานที่ 3: การอ่านค่าแร่ธาตุในดิน NPK 7-in-1 Soil Sensor
วัตถุประสงค์: อ่านค่าข้อมูลหลายค่าต่อเนื่องกัน (Multiple Registers) จากเซนเซอร์วัดคุณภาพดินอุตสาหกรรม
💡 คำอธิบายเสริมให้เข้าใจง่าย:
เซนเซอร์ NPK ในเกษตรแม่นยำใช้ Modbus RTU เช่นกัน โดยส่งค่า 7 ค่ารวดเดียว: Moisture, Temp, EC, pH, Nitrogen (N), Phosphorus (P), Potassium (K)
การส่งครั้งเดียวคำสั่งเดียวอ่านได้ 7 Register รวด ช่วยลดความซับซ้อนและประหยัดเวลาส่งข้อมูลในบัส
โค้ดตัวอย่าง ESP32
#include <ModbusMaster.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
ModbusMaster node;
void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }
void setup() {
Serial.begin(115200);
pinMode(MAX485_DE_RE, OUTPUT);
digitalWrite(MAX485_DE_RE, LOW);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
node.begin(1, Serial2); // Slave ID 1
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
}
void loop() {
uint8_t result;
// อ่าน Read Holding Registers (0x03) ตั้งแต่ Address 0x0000 จำนวน 7 ตัว
result = node.readHoldingRegisters(0x0000, 7);
if (result == node.ku8MBSuccess) {
float moisture = node.getResponseBuffer(0) / 10.0;
float temp = node.getResponseBuffer(1) / 10.0;
uint16_t ec = node.getResponseBuffer(2);
float ph = node.getResponseBuffer(3) / 10.0;
uint16_t n = node.getResponseBuffer(4);
uint16_t p = node.getResponseBuffer(5);
uint16_t k = node.getResponseBuffer(6);
Serial.printf("Moist: %.1f%% | Temp: %.1fC | EC: %d us/cm | pH: %.1f
", moisture, temp, ec, ph);
Serial.printf("N: %d mg/kg | P: %d mg/kg | K: %d mg/kg
", n, p, k);
Serial.println("--------------------------------------------------");
} else {
Serial.println("Read Failed!");
}
delay(3000);
}
ระดับ 4: ปานกลาง
ใบงานที่ 4: การสั่งงานเปิด-ปิด RS485 Relay Board (Modbus Write Single Coil)
วัตถุประสงค์: เรียนรู้การส่งคำสั่งเขียนข้อมูล (Write Single Coil 0x05 / Write Single Register 0x06) เพื่อควบคุมเอาต์พุต
💡 คำอธิบายเสริมให้เข้าใจง่าย:
โมดูล Relay RS485 ใช้คำสั่ง Write Coil (0x05) ในการสั่ง ON (0xFF00) หรือ OFF (0x0000) ที่ Coil Address ต่างๆ เช่น Relay 1 = 0x0000, Relay 2 = 0x0001
โค้ดตัวอย่าง ESP32
#include <ModbusMaster.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
ModbusMaster node;
void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }
void setup() {
Serial.begin(115200);
pinMode(MAX485_DE_RE, OUTPUT);
digitalWrite(MAX485_DE_RE, LOW);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
node.begin(1, Serial2); // Relay Board Slave ID 1
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
}
void loop() {
Serial.println("Turning Relay 1 ON...");
// writeSingleCoil(Coil_Address, State)
node.writeSingleCoil(0x0000, true);
delay(3000);
Serial.println("Turning Relay 1 OFF...");
node.writeSingleCoil(0x0000, false);
delay(3000);
}
ระดับ 5: ปานกลางค่อนข้างสูง
ใบงานที่ 5: การอ่านมิเตอร์ไฟฟ้า PZEM-017 AC/DC Energy Meter (Float/32-bit Data)
วัตถุประสงค์: จัดการข้อมูลขนาดใหญ่แบบ 32-bit (Combine 2 Registers) และค่าความแม่นยำสูง
💡 คำอธิบายเสริมให้เข้าใจง่าย:
Modbus Register 1 ตัวมีขนาด 16 บิต (0-65535) แต่ข้อมูลอย่าง กำลังไฟฟ้า (Watt) หรือ พลังงานสะสม (kWh) มีค่าสูงเกิน 65,535 จึงต้องใช้ 2 Registers รวมกันเป็น 32 บิต
การแปลง 32 บิต: (Register High << 16) | Register Low
โค้ดตัวอย่าง ESP32
#include <ModbusMaster.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
ModbusMaster node;
void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }
void setup() {
Serial.begin(115200);
pinMode(MAX485_DE_RE, OUTPUT);
digitalWrite(MAX485_DE_RE, LOW);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
node.begin(1, Serial2);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
}
void loop() {
uint8_t result = node.readInputRegisters(0x0000, 6);
if (result == node.ku8MBSuccess) {
uint16_t voltage_raw = node.getResponseBuffer(0);
uint16_t current_raw = node.getResponseBuffer(1);
// Power ใช้ 32-bit (Reg 2 + Reg 3)
uint32_t power_raw = ((uint32_t)node.getResponseBuffer(3) << 16) | node.getResponseBuffer(2);
float voltage = voltage_raw * 0.1;
float current = current_raw * 0.01;
float power = power_raw * 0.1;
Serial.printf("Voltage: %.1f V | Current: %.2f A | Power: %.1f W
", voltage, current, power);
} else {
Serial.println("PZEM Read Error");
}
delay(2000);
}
ระดับ 6: สูง
ใบงานที่ 6: การสร้าง ESP32 จำลองตัวเองเป็น Modbus Slave (RTU Slave)
วัตถุประสงค์: ทำให้ ESP32 ทำหน้าที่เป็น Slave Device ตอบสนองต่อคำสั่งอ่าน/เขียนจาก Master (เช่น จอ HMI หรือ PLC)
💡 คำอธิบายเสริมให้เข้าใจง่าย:
คราวนี้ ESP32 จะไม่ถามใครแล้ว แต่จะนั่งรอ Master (เช่น จอ HMI หรือ PLC) มาขออ่านค่าเซนเซอร์ที่ต่ออยู่กับ ESP32 หรือส่งคำสั่งมาเปิดไฟที่ ESP32
โค้ดตัวอย่าง ESP32
ติดตั้งไลบรารี: ModbusRTUSlave โดย Carbonell
#include <ModbusRTUSlave.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
ModbusRTUSlave modbus(Serial2, MAX485_DE_RE);
// สร้างตารางข้อมูล Register ของ Slave
bool coils[8];
uint16_t holdingRegisters[8];
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
// ตั้งค่าป้ายชื่อบอก Modbus Slave
modbus.configureCoils(coils, 8);
modbus.configureHoldingRegisters(holdingRegisters, 8);
// เริ่มต้น Slave ID = 2, Baudrate = 9600
modbus.begin(2, 9600);
}
void loop() {
// อัปเดตข้อมูลจำลองใส่ Holding Register 0
holdingRegisters[0] = analogRead(34); // อ่านค่า ADC
holdingRegisters[1] = millis() / 1000; // ค่าเวลา
modbus.poll(); // คอยฟังสัญญาณคำสั่งจาก Master
}
ระดับ 7: สูง
ใบงานที่ 7: การรับ-ส่งข้อมูลระหว่าง ESP32 กับ PLC (Mitsubishi FX3U / Siemens S7)
วัตถุประสงค์: สื่อสารระหว่าง ESP32 (Master) และ PLC (Slave) ผ่าน Modbus RTU เพื่ออ่านค่า Register D และสั่งงาน M Relay
💡 คำอธิบายเสริมให้เข้าใจง่าย:
ในโรงงาน PLC เช่น Mitsubishi FX3U (ติดบอร์ด FX3U-485ADP-MB) มีความเสถียรสูง คุมจักรกลหนัก แต่ขาดฟังก์ชันส่งข้อมูลขึ้น Cloud
เราสามารถใช้ ESP32 ต่อ Modbus RTU ไปดึงข้อมูล Register D (เช่น D0, D100) มาประมวลผลหรือส่งขึ้นระบบ IoT ได้ทันที
ตารางการแมป Address (Mitsubishi FX3U Modbus Map)
D0 -> Holding Register Address 0x0000 (40001)
D1 -> Holding Register Address 0x0001 (40002)
M0 -> Coil Address 0x0000 (00001)
โค้ดตัวอย่าง ESP32 อ่าน D0 จาก PLC
#include <ModbusMaster.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
ModbusMaster plcNode;
void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }
void setup() {
Serial.begin(115200);
pinMode(MAX485_DE_RE, OUTPUT);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
plcNode.begin(1, Serial2); // PLC Station Address = 1
plcNode.preTransmission(preTransmission);
plcNode.postTransmission(postTransmission);
}
void loop() {
// อ่านค่า Data Register D0 จาก PLC (Address 0x0000)
uint8_t result = plcNode.readHoldingRegisters(0x0000, 1);
if (result == plcNode.ku8MBSuccess) {
uint16_t plc_d0_value = plcNode.getResponseBuffer(0);
Serial.printf("PLC Register D0 Value: %d
", plc_d0_value);
} else {
Serial.println("PLC Communication Failed!");
}
delay(1000);
}
ระดับ 8: ขั้นสูง
ใบงานที่ 8: การสื่อสาร Modbus TCP Master ผ่านเครือข่าย Wi-Fi
วัตถุประสงค์: เปลี่ยนจากการใช้สาย RS485 มาใช้การส่งข้อมูล Modbus ผ่านโครงข่าย Ethernet / Wi-Fi (IP Address & Port 502)
💡 คำอธิบายเสริมให้เข้าใจง่าย:
Modbus TCP ต่างจาก Modbus RTU อย่างไร?
- Modbus RTU: วิ่งบนสาย RS485 ต้องคำนวณ Checksum (CRC) ต่อท้ายแพ็กเกจ
- Modbus TCP: วิ่งบน Wi-Fi/LAN (IP Address) ไม่ต้องใช้ CRC (เพราะ TCP/IP จัดการให้อยู่แล้ว) และทำงานเร็วขึ้นสูงมาก
- พอร์ตมาตรฐานของ Modbus TCP คือ Port 502
โค้ดตัวอย่าง ESP32 (Modbus TCP Client/Master)
ติดตั้งไลบรารี: ModbusIP_ESP8266 โดย emelianov
#include <WiFi.h>
#include <ModbusIP_ESP8266.h>
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
IPAddress remoteServer(192, 168, 1, 100); // IP ของ Modbus TCP Slave Server
ModbusIP mb;
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("
WiFi Connected!");
mb.client(); // กำหนดโหมดเป็น Master (Client)
}
void loop() {
if (mb.isConnected(remoteServer)) {
uint16_t res[2];
// อ่าน Holding Register Address 0 จำนวน 1 ค่า จาก Server
mb.readHreg(remoteServer, 0, res, 1);
mb.task(); // ประมวลผลแบ็กกราวด์ Modbus TCP
Serial.printf("Data from TCP Server: %d
", res[0]);
} else {
mb.connect(remoteServer); // พยายามเชื่อมต่อ Server Port 502
Serial.println("Connecting to Modbus TCP Server...");
}
delay(2000);
}
ระดับ 9: ขั้นสูง
ใบงานที่ 9: การสร้าง ESP32 Modbus TCP Server (Slave) สำหรับ SCADA / Node-RED
วัตถุประสงค์: เปิด ESP32 เป็น Server ให้ซอฟต์แวร์ SCADA, Node-RED หรือ HMI มาอ่าน/เขียนข้อมูลผ่าน Wi-Fi
💡 คำอธิบายเสริมให้เข้าใจง่าย:
จำลอง ESP32 เป็น PLC หรือ Smart Device บนเครือข่าย Wi-Fi โดยเปิดพอร์ต 502 รอรับการเชื่อมต่อจากซอฟต์แวร์ เช่น Node-RED, Ignition SCADA, หรือโปรแกรม Modbus Poll
โค้ดตัวอย่าง ESP32 Modbus TCP Server
#include <WiFi.h>
#include <ModbusIP_ESP8266.h>
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// กำหนด Register Offset
const int SENSOR_HREG = 100;
ModbusIP mb;
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
mb.server(); // กำหนดเป็น Server (Slave) Listen Port 502
// เพิ่ม Holding Register ที่ Address 100
mb.addHreg(SENSOR_HREG);
}
void loop() {
mb.task(); // คอยรับ-ส่งข้อมูล TCP
// อัปเดตค่าจำลองเข้าไปที่ Register 100
uint16_t adcVal = analogRead(34);
mb.Hreg(SENSOR_HREG, adcVal);
delay(10);
}
🎯 โจทย์ท้าทาย:
ใช้โปรแกรม Modbus Poll หรือ Node-RED ในคอมพิวเตอร์ ยิง Modbus TCP Read มายัง IP Address ของ ESP32 เพื่อดูค่า ADC 34 Real-time
ระดับ 10: ผู้เชี่ยวชาญ
ใบงานที่ 10: โปรเจกต์ RS485 to Modbus TCP Gateway (Bridge)
วัตถุประสงค์: สร้างอุปกรณ์แปลงโปรโตคอล (Gateway) รับข้อมูลจาก Modbus RTU (RS485) มาแปลงเป็น Modbus TCP ส่งต่อให้ SCADA ผ่าน Wi-Fi
💡 คำอธิบายเสริมให้เข้าใจง่าย:
อุปกรณ์ในโรงงานส่วนใหญ่รุ่นเก่าใช้ RS485 แต่ระบบควบคุมสมัยใหม่ในห้อง Control Room ใช้ Ethernet/Wi-Fi (TCP/IP)
ESP32 จะทำหน้าที่เป็น "ตัวแปลภาษา" (Gateway) โดยฝั่งหนึ่งใช้ RS485 ไปอ่านค่าจากเซนเซอร์ แล้วนำค่านั้นมาพักไว้ใน TCP Register เพื่อให้ SCADA ดึงผ่าน Wi-Fi
ผังโครงสร้าง Gateway System
[RS485 Sensors] <--- Modbus RTU ---> [ ESP32 Gateway ] <--- Modbus TCP (Wi-Fi) ---> [ SCADA / Dashboard ]
(Slave ID 1) (Modbus Server) (Client / PC)
โค้ดตัวอย่าง ESP32 Full Gateway
#include <WiFi.h>
#include <ModbusMaster.h>
#include <ModbusIP_ESP8266.h>
#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
ModbusMaster rtuNode;
ModbusIP tcpServer;
void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }
unsigned long lastUpdate = 0;
void setup() {
Serial.begin(115200);
// 1. Setup RS485 Hardware
pinMode(MAX485_DE_RE, OUTPUT);
Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
rtuNode.begin(1, Serial2); // อ่าน RTU Slave ID 1
rtuNode.preTransmission(preTransmission);
rtuNode.postTransmission(postTransmission);
// 2. Setup Wi-Fi & Modbus TCP Server
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
Serial.print("Gateway IP: ");
Serial.println(WiFi.localIP());
tcpServer.server();
tcpServer.addHreg(0); // Map ค่า Temp จาก RTU
tcpServer.addHreg(1); // Map ค่า Hum จาก RTU
}
void loop() {
tcpServer.task(); // รักษาสายเชื่อมต่อ Modbus TCP
// อ่านค่าจาก RS485 ทุกๆ 2 วินาที แล้วแมปลง TCP Register
if (millis() - lastUpdate > 2000) {
lastUpdate = millis();
uint8_t result = rtuNode.readInputRegisters(0x0001, 2);
if (result == rtuNode.ku8MBSuccess) {
uint16_t tempRaw = rtuNode.getResponseBuffer(0);
uint16_t humRaw = rtuNode.getResponseBuffer(1);
// อัปเดตข้อมูลเข้า Modbus TCP Server Register
tcpServer.Hreg(0, tempRaw);
tcpServer.Hreg(1, humRaw);
Serial.printf("[Gateway Sync] Temp Raw: %d | Hum Raw: %d
", tempRaw, humRaw);
}
}
}