---
title: RS-485 and Modbus
description: Multi-drop bus communication, address scanning, and request-response transactions
---

import { Steps, Aside } from '@astrojs/starlight/components';

RS-485 uses a shared differential bus where multiple devices communicate over two wires. Unlike RS-232's point-to-point model, RS-485 is half-duplex -- only one device transmits at a time while all others listen. This makes it the standard for industrial protocols like Modbus RTU.

All of the operations described in this guide are performed through your MCP client. Ask the assistant to open ports, switch modes, scan for devices, and transact on the bus. The tool call notation below shows exactly what the assistant calls on your behalf.

## Switching to RS-485 mode

Ports open in RS-232 mode by default. Switch to RS-485 mode before using any RS-485 tools.

```json
// open_serial_port(port="/dev/ttyUSB0", baudrate=9600)
{
  "success": true,
  "port": "/dev/ttyUSB0",
  "mode": "rs232",
  "baudrate": 9600
}

// set_port_mode(port="/dev/ttyUSB0", mode="rs485")
{
  "success": true,
  "port": "/dev/ttyUSB0",
  "previous_mode": "rs232",
  "current_mode": "rs485",
  "mode_tools": [
    "set_rs485_mode",
    "rs485_transact",
    "rs485_scan_addresses",
    "check_rs485_support"
  ]
}
```

Once in RS-485 mode, the RS-232-specific tools (`get_modem_lines`, `pulse_line`, etc.) are unavailable. Common tools like `read_serial`, `write_serial`, and `configure_serial` work in both modes.

## Configuring RS-485 hardware

If your USB-serial adapter or UART has hardware RS-485 support, `set_rs485_mode` configures the driver to automatically toggle the TX enable (DE/RE) line during transmission. This eliminates the need for manual RTS toggling.

```json
// set_rs485_mode(
//   port="/dev/ttyUSB0",
//   enabled=true,
//   rts_level_for_tx=true,
//   rts_level_for_rx=false,
//   delay_before_tx=0.0,
//   delay_before_rx=0.0
// )
{
  "success": true,
  "port": "/dev/ttyUSB0",
  "rs485_enabled": true,
  "rts_level_for_tx": true,
  "rts_level_for_rx": false,
  "delay_before_tx": 0.0,
  "delay_before_rx": 0.0,
  "loopback": false
}
```

**Parameters explained:**

| Parameter | Default | Purpose |
|-----------|---------|---------|
| `rts_level_for_tx` | `true` | RTS state when transmitting (HIGH enables the driver on most RS-485 transceivers) |
| `rts_level_for_rx` | `false` | RTS state when receiving (LOW disables the driver, allows listening) |
| `delay_before_tx` | `0.0` | Seconds to wait after asserting TX enable before sending data |
| `delay_before_rx` | `0.0` | Seconds to wait after transmission before switching to receive |
| `loopback` | `false` | Echo transmitted data back (for testing) |

## Checking hardware support

Not all USB-serial adapters support hardware RS-485. Use `check_rs485_support` to find out what your hardware can do before configuring.

```json
// check_rs485_support(port="/dev/ttyUSB0")
{
  "success": true,
  "port": "/dev/ttyUSB0",
  "driver": "ftdi_sio",
  "chip": "FT232R",
  "hardware_rs485": true,
  "software_fallback": true,
  "kernel_rs485_ioctl": true,
  "notes": [
    "FTDI chips have hardware RS-485 auto-direction",
    "Kernel TIOCSRS485 ioctl supported"
  ],
  "recommendation": "Use set_rs485_mode() for automatic DE/RE control"
}
```

This tool queries the kernel driver and USB device information to determine:

- Which driver is loaded (ftdi_sio, cp210x, ch341, pl2303)
- Whether the chip supports hardware DE/RE toggling
- Whether the kernel TIOCSRS485 ioctl is available
- A recommendation for how to proceed

### Hardware support by adapter

| Chip | Hardware RS-485 | Notes |
|------|----------------|-------|
| FTDI (FT232R, FT2232, etc.) | Yes | Automatic direction control via driver |
| CP2105 / CP2108 | Yes | Hardware support in these models |
| CP2102 | No | Software RTS control required |
| CH340 / CH341 | No | Timing may be unreliable at high baud rates |
| PL2303 | No | Software RTS control required |
| Native UART (ttyS, ttyAMA) | Depends | Check if RS-485 transceiver is connected |

## Half-duplex transactions

The `rs485_transact` tool handles the complete send-then-receive cycle that RS-485 communication requires. It manages TX/RX turnaround timing automatically, whether your hardware supports it natively or needs software RTS toggling.

```json
// rs485_transact(
//   port="/dev/ttyUSB0",
//   data="\x01\x03\x00\x00\x00\x01\x84\x0A",
//   response_timeout=1.0,
//   response_length=7
// )
{
  "success": true,
  "port": "/dev/ttyUSB0",
  "bytes_sent": 8,
  "data_sent": "\u0001\u0003\u0000\u0000\u0000\u0001\u0084\n",
  "response": "\u0001\u0003\u0002\u0000\u0005\u0085\u0085",
  "response_bytes": 7,
  "response_hex": "01030200058585",
  "hardware_rs485": true
}
```

When hardware RS-485 is **not** configured, `rs485_transact` falls back to manual RTS control:

1. Asserts RTS HIGH (enable driver / TX mode)
2. Sends the data and waits for transmission to complete
3. Drops RTS LOW (disable driver / RX mode)
4. Waits for the turnaround delay
5. Reads the response

The `turnaround_delay` parameter (default 5ms) controls the pause between sending and listening. Increase it for slow devices or long cable runs.

## Scanning for devices

`rs485_scan_addresses` probes a range of addresses and reports which ones respond. This is the fastest way to discover what is connected to a bus.

```json
// rs485_scan_addresses(
//   port="/dev/ttyUSB0",
//   start_address=1,
//   end_address=30,
//   probe_template="{addr:02x}03000001",
//   response_timeout=0.1
// )
{
  "success": true,
  "port": "/dev/ttyUSB0",
  "addresses_scanned": 30,
  "devices_found": 3,
  "responding_addresses": [
    {"address": 1, "response_length": 7, "response_hex": "01030200058585"},
    {"address": 5, "response_length": 7, "response_hex": "05030200124478"},
    {"address": 16, "response_length": 7, "response_hex": "10030200009930"}
  ],
  "hardware_rs485": true
}
```

The `probe_template` uses Python's string formatting. The placeholder `{addr}` is replaced with the current address being scanned. The default template `{addr:02x}03000001` formats the address as a two-character hex string followed by a Modbus "read holding register" frame.

<Aside type="caution" title="Bus termination and biasing">
RS-485 buses longer than a few meters need proper termination resistors (120 ohm) at both ends of the cable. Without termination, reflections cause bit errors, especially at higher baud rates.

The bus also requires biasing resistors to hold the differential lines in a known state when no device is transmitting. Without biasing, the idle bus floats and receivers may interpret noise as valid data. Many RS-485 adapters include built-in biasing -- check your hardware documentation.
</Aside>

## Practical Modbus RTU workflow

This example walks through a complete Modbus RTU session: discovering devices, then reading a holding register.

<Steps>

1. **Open the port and switch to RS-485 mode**

   Modbus RTU typically runs at 9600 baud with 8N1 framing.

   ```json
   // open_serial_port(port="/dev/ttyUSB0", baudrate=9600)
   // set_port_mode(port="/dev/ttyUSB0", mode="rs485")
   ```

2. **Configure hardware RS-485 (if supported)**

   If your adapter supports hardware direction control, enable it. If not, `rs485_transact` handles RTS toggling in software.

   ```json
   // set_rs485_mode(port="/dev/ttyUSB0", enabled=true, rts_level_for_tx=true, rts_level_for_rx=false)
   ```

3. **Scan the bus for responding devices**

   Scan addresses 1 through 247 (the Modbus valid range). Use a short timeout per address to keep the scan fast.

   ```json
   // rs485_scan_addresses(
   //   port="/dev/ttyUSB0",
   //   start_address=1,
   //   end_address=247,
   //   response_timeout=0.1
   // )
   {
     "success": true,
     "addresses_scanned": 247,
     "devices_found": 2,
     "responding_addresses": [
       {"address": 1, "response_length": 7, "response_hex": "01030200058585"},
       {"address": 10, "response_length": 7, "response_hex": "0a030200003071"}
     ]
   }
   ```

4. **Read a holding register from a device**

   Construct a Modbus RTU "Read Holding Registers" frame for address 1, register 0, quantity 1. The frame format is: `[address][function 0x03][start_hi][start_lo][qty_hi][qty_lo][CRC_lo][CRC_hi]`.

   ```json
   // rs485_transact(
   //   port="/dev/ttyUSB0",
   //   data="\x01\x03\x00\x00\x00\x01\x84\x0A",
   //   response_length=7,
   //   encoding="latin-1"
   // )
   {
     "success": true,
     "bytes_sent": 8,
     "response_hex": "01030200058585",
     "response_bytes": 7,
     "hardware_rs485": true
   }
   ```

   The response `01 03 02 00 05 85 85` decodes as: address 1, function 3, 2 bytes of data, value 0x0005 (decimal 5), followed by a CRC.

5. **Close the port when finished**

   ```json
   // close_serial_port(port="/dev/ttyUSB0")
   ```

</Steps>

### Understanding probe_template format

The `probe_template` parameter in `rs485_scan_addresses` is a Python format string. It supports the `{addr}` placeholder with standard format specifiers:

| Template | Address 16 becomes | Use case |
|----------|-------------------|----------|
| `{addr:02x}03000001` | `1003000001` | Modbus-style hex address prefix |
| `{addr:d}` | `16` | Decimal address prefix |
| `{addr:c}` | (ASCII char 16) | Single-byte binary address |

If the template does **not** contain `{addr`, the raw address byte is prepended automatically. This means a template of `03000001` with address 16 sends byte 0x10 followed by `03000001`.

## Software vs hardware RS-485

When `check_rs485_support` reports that your adapter lacks hardware RS-485, mcserial uses **software emulation**:

- Before transmitting, RTS is asserted HIGH to enable the transceiver's driver
- After transmission completes, RTS is dropped LOW to switch back to receive mode
- A configurable turnaround delay prevents missed response bytes

Software emulation works reliably at baud rates up to about 115200. At higher speeds, the timing of the RTS toggle may not be precise enough, causing the first byte of a response to be corrupted or missed. If you need high-speed RS-485, use an adapter with hardware support (FTDI-based adapters are widely available and well-supported).
