PLC Serial Communication: RS232, RS485, Modbus RTU/TCP & Communication Layers Explained

Understand PLC serial communication from the physical wire up — RS232 vs RS485, TX/RX data flow, Modbus RTU vs TCP framing, and the 7-layer OSI model mapped to industrial protocols.

The physical layer: RS232 and RS485

Before any protocol can run, the bits have to become voltages on copper. RS232 and RS485 define that part: how ones and zeros travel down the wire, how far they can go and how many devices can share it.

The two standards as roads

If RS232 is a country lane between two houses, RS485 is a highway that a whole street of devices can join:

ConceptRS232RS485
Road typeSingle-lane country roadMulti-lane highway
Distance~15 meters maxUp to 1200 meters
Passengers1 driver, 1 passenger (point-to-point)Up to 32 devices on one bus
Speed vs distanceFast but shortFast AND long
Noise immunityLow (single-ended)High (differential signaling)

TX and RX

Every serial connection has two basic signals. TX (transmit) carries data leaving the device, and RX (receive) carries data arriving at it.

Cross TX to RX

One device's TX has to land on the other device's RX, with the grounds joined. Get this wrong and you see no response at all:

Device A          Device B
┌──────┐          ┌──────┐
│  TX ─┼──────────┼─ RX  │
│  RX ─┼──────────┼─ TX  │
│ GND ─┼──────────┼─ GND │
└──────┘          └──────┘

TX always connects to RX (crossover)
Never TX-to-TX or RX-to-RX!

RS232 signal levels

RS232 is single-ended: each signal is measured against ground, and the logic levels are inverted, so a negative voltage is a 1:

Logic 1 (Mark):   -3V to -15V
Logic 0 (Space):  +3V to +15V

    +15V ┤     ┌───┐     ┌───┐
         │     │   │     │   │     ← Logic 0
     +3V ┤─ ─ ─│─ ─│─ ─ ─│─ ─│─ ─ ─
      0V ┤     │   │     │   │
     -3V ┤─ ─ ─│─ ─│─ ─ ─│─ ─│─ ─ ─
         │ ────┘   └─────┘   └────  ← Logic 1
    -15V ┤

RS485 differential signaling

RS485 sends each bit as the voltage difference between two wires, A and B. Noise that hits both wires equally cancels out at the receiver:

            ┌─── Line A (D+) ───┐
Device ─────┤                   ├───── Device
            └─── Line B (D-) ───┘

Logic 1: A > B (differential voltage > +200mV)
Logic 0: A < B (differential voltage < -200mV)

Noise hits BOTH lines equally → receiver subtracts it out!
This is why RS485 works over 1200m in noisy factories.

RS232 or RS485: side by side

FeatureRS232RS485
SignalingSingle-endedDifferential
Max cable length15m (50 ft)1200m (4000 ft)
Max devices2 (point-to-point)32 (multidrop bus)
TopologyPoint-to-pointBus / daisy-chain
Common usePC-to-PLC debug, HMIFieldbus, sensor networks
ConnectorDB9 / DB25Screw terminals (A, B, GND)
Typical baud rates9600–1152009600–921600
Industrial ratingOffice/labFactory floor

When to use each

RS232 → Use when:
  ✓ Short distance (< 15m)
  ✓ Only 2 devices talking
  ✓ Programming port / HMI connection
  ✓ Legacy equipment

RS485 → Use when:
  ✓ Long distance or noisy environment
  ✓ Multiple devices on one cable
  ✓ Modbus RTU network
  ✓ Industrial sensor bus

Modbus: what the bytes mean

Modbus is a protocol. RS232 and RS485 move the bytes; Modbus defines what those bytes mean. If the serial line is the postal truck, Modbus is the format of the letter inside it.

Modbus RTU vs Modbus TCP

FeatureModbus RTUModbus TCP
TransportRS232 / RS485 serialEthernet (TCP/IP)
FramingBinary with CRC-16TCP packet with MBAP header
AddressingStation ID (1–247)IP address + Unit ID
SpeedUp to 115200 baud10/100 Mbps Ethernet
Max devices247 addresses; 32 unit loads per RS-485 segment without repeatersUnlimited (network limit)
Error checkCRC-16 checksumTCP built-in + optional
Cable2-wire (RS485) or 3-wire (RS232)Cat5/Cat6 Ethernet

Modbus RTU frame structure

An RTU frame is the slave ID, the function code, the data and a CRC-16. A gap of 3.5 character times of silence on the line marks the end of a frame.

┌──────────┬───────────┬──────────────┬──────────┐
│ Slave ID │ Function  │    Data      │  CRC-16  │
│ (1 byte) │ (1 byte)  │ (N bytes)    │ (2 bytes)│
└──────────┴───────────┴──────────────┴──────────┘
     │           │            │              │
     │           │            │              └─ Error detection
     │           │            └─ Register addresses & values
     │           └─ Read (03), Write Single (06), Write Multi (16)
     └─ Which device responds (1–247)

Frame gap: 3.5 character times of silence = end of frame

Modbus TCP frame (MBAP header)

Modbus TCP drops the CRC and puts a 7-byte MBAP header in front of the function code. The Unit ID in that header plays the part of the RTU slave ID:

┌──────────────┬────────────┬────────┬─────────┬───────────┬──────────┐
│ Transaction  │ Protocol   │ Length │ Unit ID │ Function  │   Data   │
│ ID (2 bytes) │ ID (2 bytes│(2 bytes│(1 byte) │ (1 byte)  │(N bytes) │
│              │  = 0x0000) │       │         │           │          │
└──────────────┴────────────┴────────┴─────────┴───────────┴──────────┘
                                         │
                                         └─ Same as RTU slave ID
No CRC needed — TCP handles error detection!

Common Modbus function codes

CodeNameDescription
01Read CoilsRead digital outputs (ON/OFF)
02Read Discrete InputsRead digital inputs
03Read Holding RegistersRead analog/config values (most common)
04Read Input RegistersRead analog input values
05Write Single CoilSet one digital output
06Write Single RegisterSet one register value
15Write Multiple CoilsSet multiple digital outputs
16Write Multiple RegistersSet multiple register values

The OSI model for industrial communication

The OSI model is why you need both RS485 and Modbus: RS485 sits at the physical layer and Modbus at the application layer. Each of the seven layers has its own job:

Layer 7 │ APPLICATION  │ Modbus commands, read/write registers
────────┤──────────────┤─────────────────────────────────────
Layer 6 │ PRESENTATION │ Data encoding (big-endian, ASCII vs RTU)
────────┤──────────────┤─────────────────────────────────────
Layer 5 │ SESSION      │ Request/response pairing, timeouts
────────┤──────────────┤─────────────────────────────────────
Layer 4 │ TRANSPORT    │ TCP (reliable) or UDP (fast)
────────┤──────────────┤─────────────────────────────────────
Layer 3 │ NETWORK      │ IP addressing, routing between subnets
────────┤──────────────┤─────────────────────────────────────
Layer 2 │ DATA LINK    │ Framing, MAC addresses, CRC checks
────────┤──────────────┤─────────────────────────────────────
Layer 1 │ PHYSICAL     │ RS232/RS485 voltages, cables, connectors

Where industrial protocols sit in the model

Protocol          │ Layer 1      │ Layer 2-4        │ Layer 7
──────────────────┤──────────────┤──────────────────┤──────────────
Modbus RTU        │ RS232/RS485  │ Serial framing   │ Modbus
Modbus TCP        │ Ethernet     │ TCP/IP           │ Modbus
EtherNet/IP       │ Ethernet     │ TCP/UDP + IP     │ CIP
PROFINET          │ Ethernet     │ TCP/UDP + RT     │ PROFINET IO
PROFIBUS DP       │ RS485        │ Token passing    │ PROFIBUS
EtherCAT          │ Ethernet     │ On-the-fly       │ CoE/SoE
OPC UA            │ Ethernet     │ TCP/IP           │ OPC UA Binary

Modbus RTU master in Structured Text

The program below configures the serial port, then runs a state machine: wait for the 500 ms poll interval, read 10 holding registers with function 03, give up after three failed attempts (error or 2 s timeout), and after a good read write 5 registers back with function 16:

PROGRAM ModbusRTUMaster
VAR
    // Serial port configuration
    SerialConfig : COM_CONFIG;
    
    // Modbus request
    mbRequest : MODBUS_REQUEST;
    mbSend : BOOL := FALSE;
    mbBusy : BOOL;
    mbDone : BOOL;
    mbError : BOOL;
    
    // Data buffers
    HoldingRegs : ARRAY[0..9] OF WORD;  // Read from slave
    WriteRegs   : ARRAY[0..4] OF WORD;  // Write to slave
    
    // Communication state machine
    CommState : INT := 0;
    PollTimer : TON;
    TimeoutTimer : TON;
    RetryCount : INT := 0;
    MAX_RETRIES : INT := 3;
END_VAR

// ─── Serial Port Setup (done once) ───
SerialConfig.Port := 1;           // COM1
SerialConfig.BaudRate := 19200;   // Match slave setting
SerialConfig.DataBits := 8;
SerialConfig.StopBits := 1;
SerialConfig.Parity := 'E';      // Even parity (Modbus standard)
SerialConfig.Timeout := T#1S;     // 1 second timeout

// ─── Communication State Machine ───
CASE CommState OF
    0: // IDLE — Wait for poll interval
        PollTimer(IN := TRUE, PT := T#500MS);
        IF PollTimer.Q THEN
            PollTimer(IN := FALSE);
            CommState := 10;
        END_IF;
    
    10: // SEND READ REQUEST
        mbRequest.SlaveID := 1;           // Target device address
        mbRequest.FunctionCode := 3;      // Read Holding Registers
        mbRequest.StartAddress := 40001;  // First register
        mbRequest.Quantity := 10;         // Read 10 registers
        mbSend := TRUE;
        TimeoutTimer(IN := FALSE);
        CommState := 20;
    
    20: // WAIT FOR RESPONSE
        TimeoutTimer(IN := TRUE, PT := T#2S);
        
        IF mbDone THEN
            // Success — copy data
            HoldingRegs := mbRequest.ResponseData;
            RetryCount := 0;
            CommState := 30;  // Next: write
            
        ELSIF mbError OR TimeoutTimer.Q THEN
            // Failed — retry logic
            RetryCount := RetryCount + 1;
            IF RetryCount >= MAX_RETRIES THEN
                // Log alarm, skip to next cycle
                RetryCount := 0;
                CommState := 0;
            ELSE
                CommState := 10;  // Retry
            END_IF;
        END_IF;
    
    30: // SEND WRITE REQUEST
        mbRequest.SlaveID := 1;
        mbRequest.FunctionCode := 16;     // Write Multiple Registers
        mbRequest.StartAddress := 40101;
        mbRequest.Quantity := 5;
        mbRequest.WriteData := WriteRegs;
        mbSend := TRUE;
        CommState := 40;
    
    40: // WAIT FOR WRITE CONFIRMATION
        TimeoutTimer(IN := TRUE, PT := T#2S);
        IF mbDone OR mbError OR TimeoutTimer.Q THEN
            CommState := 0;  // Back to idle
        END_IF;
END_CASE;

Modbus TCP client in Structured Text

The TCP client builds a request with a fresh transaction ID each time, reads 16 holding registers from a remote I/O module on port 502, and keeps counters for packets sent, received and lost. It only accepts a response whose transaction ID matches the request:

PROGRAM ModbusTCPClient
VAR
    // TCP connection
    tcpConnect : TCP_CONNECT;
    tcpConnected : BOOL;
    ServerIP : STRING := '192.168.1.100';
    ServerPort : UINT := 502;  // Standard Modbus TCP port
    
    // Modbus TCP transaction
    TransactionID : UINT := 0;
    mbTCPRequest : MODBUS_TCP_REQUEST;
    
    // Read data from remote I/O module
    RemoteInputs  : ARRAY[0..15] OF WORD;
    RemoteOutputs : ARRAY[0..7] OF WORD;
    
    // Diagnostics
    CommOK : BOOL;
    ResponseTime : TIME;
    RequestTimestamp : TIME;
    PacketsSent : UDINT;
    PacketsReceived : UDINT;
    PacketsLost : UDINT;
END_VAR

// ─── Build Modbus TCP Request ───
TransactionID := TransactionID + 1;

mbTCPRequest.TransactionID := TransactionID;
mbTCPRequest.ProtocolID := 0;        // Always 0 for Modbus
mbTCPRequest.UnitID := 1;            // Slave/unit identifier
mbTCPRequest.FunctionCode := 3;      // Read Holding Registers
mbTCPRequest.StartAddress := 0;
mbTCPRequest.Quantity := 16;

// ─── Send and Track ───
IF tcpConnected THEN
    RequestTimestamp := TIME();
    // ... send mbTCPRequest over TCP socket ...
    PacketsSent := PacketsSent + 1;
END_IF;

// ─── Response Handling ───
IF ResponseReceived THEN
    ResponseTime := TIME() - RequestTimestamp;
    PacketsReceived := PacketsReceived + 1;
    CommOK := TRUE;
    
    // Validate transaction ID matches
    IF Response.TransactionID = TransactionID THEN
        RemoteInputs := Response.Data;
    END_IF;
ELSIF ResponseTimeout THEN
    PacketsLost := PacketsLost + 1;
    CommOK := FALSE;
END_IF;

Common communication errors and fixes

SymptomLikely CauseFix
No response at allTX/RX wired wrongSwap TX and RX wires
Garbled dataBaud rate mismatchMatch all devices to same baud
CRC errorsParity mismatchSet all devices to same parity (Even/None)
Intermittent timeoutCable too long for RS232Switch to RS485 for >15m
Multiple slaves conflictSame address on busAssign unique IDs 1–247
Modbus TCP refusedWrong port or firewallUse port 502, check firewall rules
Data swapped (byte order)Endianness mismatchCheck big-endian vs little-endian

Choosing by distance

Start with the distance to the device. Under 15 m with a single device (a debug or HMI link), RS232 is fine. For up to 32 sensors or VFDs on one cable, use RS485 with Modbus RTU. For SCADA and MES connections, use Ethernet with Modbus TCP:

                    ┌─────────────────┐
                    │  How far is the  │
                    │    device?       │
                    └────────┬────────┘
                             │
                 ┌───── < 15m ──────┐──── > 15m ────┐
                 │                  │                │
            ┌────▼────┐       ┌────▼────┐      ┌────▼────┐
            │ RS232   │       │ RS485   │      │ Ethernet│
            │ OK      │       │ Modbus  │      │ Modbus  │
            │         │       │ RTU     │      │ TCP     │
            └─────────┘       └─────────┘      └─────────┘
                 │                  │                │
            1 device          Up to 32         Unlimited
            Debug/HMI         Sensors/VFDs     SCADA/MES

Commissioning checklist

This is the error table above, in the order you would check it on site:

  • Check the wiring. On RS232, TX goes to RX, RX goes to TX, and the grounds are joined.
  • Check the distance. RS232 is good for about 15 m; past that, move to RS485.
  • Set every device on the bus to the same baud rate and the same parity (Modbus defaults to even parity).
  • Give every slave a unique address from 1 to 247.
  • For Modbus TCP, use port 502 and check the firewall.
  • If values arrive but look wrong, check the byte order (big-endian or little-endian).