Guía completa: Modbus RTU y TCP con Python y Node-RED
Para escribir y leer un coil por Modbus RTU desde un Raspberry PLC en Python, instala pymodbus en un entorno virtual, abre el puerto RS-485 con ModbusSerialClient(port, framer=FramerType.RTU, ...) y llama a write_coil(address, value, device_id=...) (código de función 05) y a read_coils(address, count=1, device_id=...) (código de función 01). Los ejemplos de este post se han probado con pymodbus 3.16.1.
Esta es la parte 3 de la serie Modbus y Raspberry PLC. Las partes 1 y 2 usan Node-RED, y esta parte y la parte 4 usan Python. Todos los detalles, como Modbus TCP, timeouts, direccionamiento y ajustes de Node-RED, están en la guía de Modbus para PLC con Raspberry Pi.
Qué necesitas
- Un Raspberry PLC. Es el maestro Modbus RTU.
- Un esclavo Modbus RTU: cualquier equipo con RS-485, u otro PLC de Industrial Shields que trabaje como esclavo, por ejemplo un PLC basado en Arduino o ESP32 como en Modbus RTU con PLC basados en Arduino y ESP32.
- Cableado RS-485: A+ con A+ y B- con B-, con terminación en los extremos del bus.
- Los parámetros serie del esclavo: dirección (device ID), velocidad, paridad y bits de parada. Este post usa el device ID 31 a 38400 bit/s, 8N1. Cámbialos para que coincidan con tu esclavo.
Instala pymodbus
Instala pymodbus con pip en un entorno virtual y fija la versión. Raspberry Pi OS Bookworm y posteriores bloquean pip install fuera de un entorno virtual, y la versión que instala apt depende de la versión del sistema. pymodbus cambia su API entre versiones, así que la versión decide si un ejemplo funciona.
python3 -m venv ~/modbus-venv
source ~/modbus-venv/bin/activate
pip install "pymodbus[serial]==3.16.1"
ls /dev/ttySC*
El último comando lista los puertos serie. En el Raspberry PLC v3, y en el v4 sin expansión, el RS-485 es /dev/ttySC0 y /dev/ttySC1. En el v6, y en el v4 con expansión, es /dev/ttySC2 y /dev/ttySC3. Este post usa /dev/ttySC0.
Escribe un coil con Python (FC05)
Guarda este código como rtu_write_coil.py:
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from pymodbus.framer import FramerType
PORT = "/dev/ttySC0" # RS-485 port, check the name for your PLC version
DEVICE_ID = 31 # Modbus address of the slave (1 to 247, never 0)
COIL = 0 # coil address (coil 00001 in the device manual is address 0)
client = ModbusSerialClient(
port=PORT,
framer=FramerType.RTU,
baudrate=38400,
bytesize=8,
parity="N",
stopbits=1,
timeout=1,
)
if not client.connect():
raise SystemExit(f"Cannot open {PORT}")
try:
rr = client.write_coil(COIL, True, device_id=DEVICE_ID) # FC05
if rr.isError():
print("Exception response:", rr)
else:
print(f"Coil {COIL} set to ON")
except ModbusException as exc:
print("No valid answer:", exc) # timeout, CRC error or wrong device ID
finally:
client.close()
- Importaciones.
ModbusSerialClientes el maestro RTU,FramerTypeselecciona la trama RTU yModbusExceptioncaptura los casos en que el esclavo no responde. - Puerto y esclavo.
DEVICE_IDes la dirección Modbus del esclavo. La dirección 0 es broadcast: los esclavos no le responden. - Cliente. La velocidad, los bits de datos, la paridad y los bits de parada deben ser los mismos que en el esclavo.
timeout=1espera un segundo cada respuesta. - Escritura.
write_coil(COIL, True, device_id=DEVICE_ID)envía el código de función 05. UsaFalsepara apagar el coil. - Dos tipos de error.
rr.isError()es una respuesta de excepción: el esclavo ha respondido pero rechaza la petición, por ejemplo porque la dirección no existe.ModbusExceptionsignifica que no ha llegado ninguna respuesta válida.
Ejecútalo con el entorno virtual activo:
source ~/modbus-venv/bin/activate
python rtu_write_coil.py
Coil 0 set to ON

Escribe y lee un coil en un bucle (FC05 y FC01)
Esta versión cambia el coil cada segundo y lo vuelve a leer para comprobar el valor:
import time
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from pymodbus.framer import FramerType
PORT = "/dev/ttySC0" # RS-485 port, check the name for your PLC version
DEVICE_ID = 31
COIL = 0
client = ModbusSerialClient(
port=PORT,
framer=FramerType.RTU,
baudrate=38400,
bytesize=8,
parity="N",
stopbits=1,
timeout=1,
)
if not client.connect():
raise SystemExit(f"Cannot open {PORT}")
state = True
try:
while True:
try:
client.write_coil(COIL, state, device_id=DEVICE_ID) # FC05
rr = client.read_coils(COIL, count=1, device_id=DEVICE_ID) # FC01
if rr.isError():
print("Exception response:", rr)
else:
print(f"Wrote {state}, read back {rr.bits[0]}")
except ModbusException as exc:
print("No valid answer:", exc)
state = not state
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
client.close()
read_coils(COIL, count=1, device_id=DEVICE_ID)envía el código de función 01. La respuesta agrupa los coils en bytes, así querr.bitstiene 8 valores aunque leas un solo coil. El primero,rr.bits[0], es el coil que has pedido.- El cliente abre el puerto una vez y lo mantiene abierto. Cada ciclo captura sus propios errores, así que una respuesta que falta no detiene el bucle.
- Pulsa Ctrl+C para parar. El puerto se cierra en
finally.
Salida de nuestra prueba:
Wrote True, read back True
Wrote False, read back False
Wrote True, read back True
Wrote False, read back False

Errores con ejemplos antiguos
La primera versión de este post usaba ModbusSerialClient(method='rtu', ...) y el device ID como argumento posicional. La ejecutamos contra un esclavo simulado con varias versiones de pymodbus: funciona con la 3.0.2 y la 3.6.9, y falla con la 3.7 y posteriores.
| Error | Causa | Solución |
|---|---|---|
unexpected keyword argument 'method' | pymodbus 3.7 y posteriores seleccionan la trama con framer | framer=FramerType.RTU |
unexpected keyword argument 'slave' | pymodbus 3.10 y posteriores lo llaman device_id | device_id=31 |
cannot import name 'ModbusSerialClient' from 'pymodbus.client' | pymodbus 2.x | Instala pymodbus 3 en un entorno virtual |
No response received | El esclavo no responde | Device ID, velocidad, paridad, A y B sin cruzar, terminación |
| Respuesta de excepción con código 02 | La dirección del coil no existe en el esclavo | La tabla de coils del manual del esclavo |
Qué hemos probado. Python 3.13 y pymodbus 3.16.1 en Linux, con un esclavo Modbus RTU simulado (device ID 31, 38400 8N1) en un enlace serie virtual. Los dos scripts se ejecutaron y dieron la salida que se muestra arriba. El código original se ejecutó de la misma forma con pymodbus 2.5.3, 3.0.2, 3.6.9, 3.7.4, 3.8.6 y 3.16.1. No hemos ejecutado esta actualización en un Raspberry PLC real: las dos capturas son de la primera versión del post.
Modbus RTU en un Raspberry PLC en una planta real. En el caso de estudio de la planta de biogás, un Raspberry PLC 58 controla cuatro secciones del proceso, y los contadores con Modbus RTU van en uno de sus dos puertos RS-485.
Lee el caso de la planta de biogásAbre la guía completa de Modbus
Preguntas frecuentes sobre Modbus RTU con Python
El manual dice coil 00001. ¿Qué dirección uso?
La dirección 0. pymodbus usa la dirección de la trama, que empieza en 0, y muchos manuales numeran los coils desde 1.
¿Por qué read_coils devuelve 8 valores?
Porque la respuesta lleva los coils en bytes enteros. pymodbus devuelve todos los bits de esos bytes. Usa solo los primeros count valores.
¿Puedo instalar pymodbus con apt?
Puedes, pero obtienes la versión de tu sistema, que puede tener una API distinta de la de los ejemplos. Un entorno virtual con una versión fija da el mismo resultado en todos los PLC.
¿Puedo usar el device ID 0?
Solo para escrituras broadcast. Todos los esclavos las ejecutan y ninguno responde, así que no puedes leer con el device ID 0.
¿Cómo hago lo mismo por Ethernet?
Con ModbusTcpClient y la dirección IP del esclavo. Consulta la parte 4: Modbus TCP y Python.
Más sobre Modbus en el Raspberry PLC
- Guía de Modbus TCP y RTU para PLC con Raspberry Pi: ejemplos con Python y Node-RED
- Cómo usar pymodbus con un PLC Raspberry Pi: ejemplos asíncronos
- Parte 1: Modbus RTU y Node-RED
- Parte 2: Modbus TCP y Node-RED
- Parte 4: Modbus TCP y Python
- Trama Modbus RTU, códigos de función y CRC
- Cómo elegir un controlador industrial: PLC, controlador embebido, IPC o gateway
Elige el modelo y la combinación de E/S en la familia Raspberry PLC.