RS485 and Modbus
The same UART, sent as the difference between two wires instead of a voltage against ground. That one change buys a kilometre of cable in an electrically filthy building - and adds one pin that ruins most first attempts.
One pin, one bug
Serial.write() returns as soon as the bytes are in the FIFO, not when they are on the wire — that is the trap. Call Serial.flush() first, and every Modbus reply becomes a CRC error until you do.What the wiring looks like
The ESP32 does not speak RS485 itself. A transceiver — MAX485, SP3485, or one of the automatic ones — sits between the UART and the twisted pair.
| ESP32 | Transceiver |
|---|---|
| TX | DI |
| RX | RO |
| Any GPIO | DE and RE, tied together |
| 3V3 and GND | VCC and GND |
Then A and B go to every device on the line, in parallel, with 120 Ω across the pair at each far end.
Modbus in one paragraph
Every device has an address, 1 to 247. The master sends address, function code, register number, count, and a CRC. The slave replies with the data and its own CRC. There is no discovery — you get the register map from the meter's manual — and there is exactly one master, which is why the direction pin matters so much.
Libraries do the framing and the CRC; the DE timing is still yours.
The code
The whole discipline is in the send function - raise DE, write, flush, drop DE. flush is the line everyone leaves out, and it is why the last byte gets cut off.
const int DE = 4; // driver enable, often tied to RE as well
void sendFrame(const uint8_t *buf, size_t n) {
digitalWrite(DE, HIGH); // we are talking
Serial2.write(buf, n);
Serial2.flush(); // wait for the last bit to leave
digitalWrite(DE, LOW); // now listen
}
void setup() {
Serial.begin(115200);
pinMode(DE, OUTPUT);
digitalWrite(DE, LOW);
Serial2.begin(9600, SERIAL_8N1, 16, 17);
// read 1 holding register at 0x0000 from slave 1
uint8_t req[] = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A };
sendFrame(req, sizeof(req));
}
void loop() {
while (Serial2.available()) Serial.printf("%02X ", Serial2.read());
}Serial.write returns as soon as the bytes are in the FIFO, not when they are on the wire. Without flush you drop the driver mid-character and every reply is a CRC error.
Same sequence. MicroPython's UART has a flush too, and the same rule applies - drop DE only after it returns.
from machine import UART, Pin
import time
de = Pin(4, Pin.OUT, value=0)
uart = UART(2, baudrate=9600, tx=Pin(17), rx=Pin(16))
def send(frame):
de.value(1)
uart.write(frame)
uart.flush() # wait for the last bit
de.value(0)
send(bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A]))
time.sleep_ms(100)
print(uart.read())Some transceiver boards handle the direction automatically from the data line. If yours has no DE pin, it is one of those, and this whole problem disappears.
When it does not work
You dropped DE before the last byte finished sending, so the frame is truncated. Call flush between write and dropping DE.
You held DE too long and were still driving the line when the slave answered. Two drivers on one pair means neither message survives.
Termination. Put 120 Ω across A and B at each end of the run, and only at the ends. A resistor at every node loads the bus into silence.
A and B swapped somewhere, or no common ground reference. RS485 is differential but not magic - most transceivers need the two ends within a few volts of each other.
That is every bus. Next: where the data lands once it is on the board, and why flash is not simply a disk.
Filesystem on flash →Edit this page — content/esp32/rs485-and-modbus.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.