LearnSCADA

Part III · Chapter 13

Modbus RTU over RS-485

Now the bytes leave localhost and travel the wire that defines industrial Modbus. The good news: almost nothing you've learned changes. The new material is practical and physical: an adapter, a port name, a permission, and four serial settings that must match.

By the end of this chapter you can

  • Wire a USB-to-RS-485 adapter to a device and find its port on Linux.
  • Fix the "permission denied" problem with the dialout group.
  • Create a ModbusSerialClient with matching serial settings and read over RTU.
  • Build a two-adapter test rig, and work the silent-bus checklist in order.

The same pymodbus read and write calls work over serial. You create a serial client instead of a TCP one and tell it the port and serial settings, the very details Chapter 5 prepared you for. This is where Modbus stops being a desk exercise and becomes something you could wire into a real plant.

The adapter

A microcomputer has no RS-485 port of its own, so you add one with an inexpensive USB-to-RS-485 adapter: a small dongle that plugs into a USB socket and presents a pair of terminals, usually labeled A and B, for the bus. To the computer it looks like an ordinary serial port; on the bus side it speaks the differential RS-485 signaling from Chapter 5. With it, your board can join a real Modbus RTU network and talk to actual meters, drives, and controllers.

A USB-to-RS-485 adapter gives the Raspberry Pi a serial port. The Pi connects by USB to the adapter, which appears as /dev/ttyUSB0. The adapter's A, B and GND terminals are wired A to A, B to B and ground to ground on a Modbus device. Signals travel along the A and B pair in both directions. Raspberry Pi USB port USB USB → RS-485 adapter /dev/ttyUSB0 ABGND Modbus device meter · unit 1 ABGND A → AB → Bground request → ← response Only silence? Swap A and B: a reversed pair is a common, harmless mistake. Long bus: termination resistors at each end. Short bench link: usually not needed.
A USB-to-RS-485 adapter gives the microcomputer a serial port. It plugs into USB on one side and connects to the bus's A and B wires (and ground) on the other, joining the board to a real RTU network.

Wiring follows Chapter 5 exactly: adapter A to device A, B to device B, and join the grounds. Manufacturers sometimes disagree about which line is which, so if you get only silence, try swapping the two data wires. On a longer bus, remember termination resistors at each end; for a short bench connection between two nearby devices you can usually skip termination and still get a clean signal.

Finding the port

Plug the adapter into the Pi and Linux gives it a device name, almost always /dev/ttyUSB0 for the first adapter. Confirm it by listing USB serial devices, or by checking the kernel log after plugging it in:

TERMINAL — find the serial port
ls /dev/ttyUSB*
dmesg | grep ttyUSB
OUTPUT (first command, one adapter)
/dev/ttyUSB0

The first command lists the USB serial ports present. The second shows the kernel messages that mention them, which is handy for telling two adapters apart: plug them in one at a time and watch which name each gets. Note the name (typically /dev/ttyUSB0); you'll pass it to pymodbus shortly. If nothing appears, the adapter isn't being recognized, and the cable or the adapter itself is the place to look.

Permission to use the port

One small obstacle trips up nearly everyone the first time. On Linux, serial ports are restricted to members of a group called dialout, and your user may not be in it. If your program fails with a permission error opening the port, add yourself to the group once, then log out and back in for the change to take effect:

TERMINAL — grant serial access
sudo usermod -a -G dialout $USER

It's a one-time step, easy to forget and easy to fix. A "permission denied" on /dev/ttyUSB0 almost always means this group membership is missing, not that anything is wrong with your wiring or code.

The serial client

Here's the payoff for all of Part III: it's the Chapter 11 client with the connection swapped. Instead of ModbusTcpClient with an address, create a ModbusSerialClient with a port and Chapter 5's four serial settings: baud rate, parity, stop bits, and byte size:

PYTHON — a serial client
from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(
    port="/dev/ttyUSB0",
    baudrate=9600,
    parity="N",
    stopbits=1,
    bytesize=8)
client.connect()

Those four settings must match at both ends, just as Chapter 5 insisted: baudrate=9600, parity="N" (none), stopbits=1, and bytesize=8, which together spell the familiar "9600 8-N-1." Set them to whatever the device uses; a mismatch here is the leading cause of a silent serial bus. Notice there's no framer to choose: in pymodbus 3.7.4, ModbusSerialClient uses RTU framing by default, which is what you want.

How four settings spell 9600 8-N-1. In turn, each setting lights up with its part of the notation and its part of one character on the wire: baudrate=9600 sets the length of each bit, about 104 microseconds; bytesize=8 gives eight data bits; parity N means no parity bit; stopbits=1 gives one stop bit after the data. baudrate=9600 bytesize=8 parity="N" stopbits=1 9600 8-N-1 ONE CHARACTER ON THE WIRE S D0D1D2D3 D4D5D6D7 none 1 start8 data bitsparitystop 9600 bits per second: each bit lasts 1/9600 s ≈ 104 µs 8: eight data bits per character N: no parity bit is sent 1: one stop bit closes the character
The serial client's four settings (baud rate, byte size, parity, and stop bits) are the same ones from Chapter 5 that must match at both ends. Together they spell a notation like "9600 8-N-1."

Reading is unchanged

With the serial client connected, reading is identical to the TCP case with one addition. A serial bus is multidrop, so you must name which device with its unit address. In pymodbus that's the slave argument (older versions) or device_id (newer ones). A read of four holding registers from unit 1:

PYTHON — read over serial
rr = client.read_holding_registers(
    0, count=4, slave=1)
if not rr.isError():
    print("Registers:", rr.registers)
client.close()
slave= or device_id=?

With pymodbus 3.7.4, pinned in Chapter 10, slave=1 is correct. device_id= is the spelling from release 3.10 onward and won't work here. If a device ignores you, check this keyword before suspecting the wiring.

That's the whole difference between Ethernet and serial in your code: a different client object, the serial settings, and a unit address on each call. read_holding_registers, read_input_registers, write_register, and write_coil all behave exactly as they did over TCP. Everything from Chapters 11 and 12 carries straight over to the serial world.

PYTHON — over TCP (Chapter 11)
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient(
    "127.0.0.1", port=5020)

client.connect()
rr = client.read_holding_registers(
    0, count=4)
if not rr.isError():
    print("Registers:", rr.registers)
client.close()
PYTHON — over RS-485 (this chapter)
from pymodbus.client import ModbusSerialClient
client = ModbusSerialClient(
    port="/dev/ttyUSB0", baudrate=9600,
    parity="N", stopbits=1, bytesize=8)
client.connect()
rr = client.read_holding_registers(
    0, count=4, slave=1)
if not rr.isError():
    print("Registers:", rr.registers)
client.close()
The same program, two transports. Only the highlighted parts change: the client class, its connection settings, and the unit address. The read and write calls are the same because the PDU is the same.

Testing without a field device

No industrial device on hand? Here are three ways to practice serial Modbus on your bench:

  1. A real but humble device: an inexpensive RS-485 temperature or energy sensor gives you genuine hardware to read.
  2. A two-adapter rig: plug two USB-to-RS-485 adapters into the same board, wire them A-to-A and B-to-B, run a serial server on one port and your client on the other, and let the board talk to itself over real RS-485.
  3. A software loopback, for testing framing alone, though the two-adapter rig is far more realistic.
A self-contained serial test rig. One Raspberry Pi has two USB-to-RS-485 adapters: /dev/ttyUSB0 runs the client and /dev/ttyUSB1 runs the server. Their A, B and ground terminals are wired together. An RTU request, 01 03 00 00 00 04 44 09, travels from adapter 0 to adapter 1, and the response, 01 03 08 00 64 00 C8 01 2C 01 90 90 08, travels back. Raspberry Pi client.py → /dev/ttyUSB0 server.py → /dev/ttyUSB1 adapter 0 ttyUSB0 · client adapter 1 ttyUSB1 · server ABG ABG A–A B–B GND request 01 03 00 00 00 04 44 09 response 01 03 08 00 64 00 C8 01 2C 01 90 90 08 real ports · real RS-485 · real CRCs
A self-contained serial test rig. Two USB-to-RS-485 adapters on one board, wired A-to-A and B-to-B, let a serial server on one port and a client on the other exchange real RTU frames with no field hardware.

The two-adapter rig is worth the small expense: it exercises the entire serial path (real ports, real differential signaling, real RTU framing and CRCs) in a setup you fully control. A serial server mirrors Chapter 12's TCP server. Build the same data blocks and contexts, then start it with StartSerialServer instead of StartTcpServer, giving it a port and baud rate:

PYTHON — a serial server
from pymodbus.server import StartSerialServer

StartSerialServer(context=ctx,
                  port="/dev/ttyUSB1",
                  baudrate=9600)

Point the serial server at one adapter (/dev/ttyUSB1) and your serial client at the other (/dev/ttyUSB0), match their settings, and you have a complete RTU conversation running over copper on your desk. Watch it work and you've closed the loop between Chapter 5's theory and a live serial bus.

When the bus stays silent

Serial brings back Chapter 3's silence outcome in full force, and its checklist is short and worth memorizing. If a serial read returns nothing, work down these in order: the port name; your permission (the dialout group); the serial settings, baud and parity first; the unit address; and the wiring, including an A/B swap. Almost every silent serial bus is one of these five. Chapter 9's diagnostic counters, especially a climbing CRC-error count, then tell an electrical problem apart from a configuration one.

Silent-bus checklist

  1. Run ls /dev/ttyUSB* and compare with the port= in your code. Unplug and replug while watching dmesg | grep ttyUSB if you have two adapters. Nothing listed? Suspect the cable or the adapter.

  2. Run groups and look for dialout. If it's missing: sudo usermod -a -G dialout $USER, then log out and back in. A "permission denied" error points here.

  3. Baud and parity first, then stop bits and byte size. Compare baudrate, parity, stopbits, bytesize with the device's configuration (for example 9600 8-N-1). Every device on the bus must agree.

  4. Check the device's configured address against slave= in your read call (correct for pymodbus 3.7.4). An address with no device behind it draws silence, not an exception.

  5. A to A, B to B, grounds joined. Labels disagree between manufacturers, so swap the two data wires and try again. On a long bus, check termination at each end.

You can now speak Modbus over both Ethernet and serial, reading and writing, with real or simulated devices. Before a full project, one thing remains: data that doesn't fit in a single 16-bit register (signed numbers, decimals, counts above 65535, and true floating-point readings). Decoding those correctly is the subject of Chapter 14.

Check your understanding

1. Opening /dev/ttyUSB0 fails with "permission denied." What's the usual fix?

Linux limits serial ports to the dialout group. The error is about access, not wiring or settings.

2. Moving a working TCP client to RS-485, what changes in the code?

The PDU is the same, so the read and write calls are the same. Only the transport details and the unit address change.

3. What does "9600 8-N-1" mean?

It's the shorthand for the four serial settings: baudrate=9600, bytesize=8, parity="N", stopbits=1.

4. A serial read is silent. You've confirmed the port name and your dialout membership. What do you check next?

The order is port name, permission, serial settings, unit address, wiring. A settings mismatch is the leading cause of a silent bus.