Ir al contenido

Guía Modbus TCP y RTU para PLCs Raspberry Pi: ejemplos con Python y Node-RED

Ejemplos con pymodbus 3.16 y Node-RED para Modbus RTU por RS-485 y Modbus TCP por Ethernet, con nombres de puerto, direccionamiento y diagnóstico de errores.
21 de noviembre de 2023 por
Guía Modbus TCP y RTU para PLCs Raspberry Pi: ejemplos con Python y Node-RED
Boot & Work Corp. S.L, Martí Schnabel

Un Raspberry PLC habla Modbus RTU por sus puertos RS-485 y Modbus TCP por sus puertos Ethernet. En Python se usa la librería pymodbus, y en Node-RED los nodos de node-red-contrib-modbus. Los dos pueden funcionar como maestro que consulta medidores, variadores y sensores, o como esclavo al que consulta un sistema SCADA.

Esta guía da código Python que funciona con la versión actual de pymodbus, la configuración de Node-RED, los nombres de los puertos RS-485 de cada versión de Raspberry PLC y las comprobaciones que hay que hacer cuando un equipo no responde. La parte del protocolo (direcciones, timeouts, códigos de excepción, valores de 32 bits) sirve para cualquier controlador Linux, también si no vas a programar en Python.

Probado Los ejemplos de Python se han ejecutado con pymodbus 3.16.1 (la última versión en octubre de 2026) y Python 3.13 en Linux: Modbus TCP en 127.0.0.1 y Modbus RTU sobre un enlace serie virtual. pymodbus cambia su API a menudo. Si tu PLC tiene una versión anterior, la sección de versiones de pymodbus explica qué hay que cambiar.

¿Cómo funciona Modbus en un Raspberry PLC?

Modbus es un protocolo de petición y respuesta. El maestro (cliente) envía una petición con un código de función, una dirección y una cantidad. El esclavo (servidor) la ejecuta y responde. Un esclavo nunca envía datos por su cuenta. La especificación de Modbus y pymodbus ya dicen cliente y servidor. Esta guía también usa maestro y esclavo, porque la mayoría de manuales de equipos todavía lo hacen.

Modbus RTU y Modbus TCP usan los mismos códigos de función y el mismo modelo de datos. Solo cambia el transporte: una línea serie con una dirección de equipo y un CRC en cada trama, o una conexión TCP con una cabecera MBAP.

Modbus RTUModbus TCP
Capa físicaRS-485, dos hilos (A+ y B-), half duplexEthernet
Puerto en el Raspberry PLCDe /dev/ttySC0 a /dev/ttySC3, según la versióneth0 o eth1, puerto TCP 502
Dirección del equipoDe 1 a 247 en cada trama. 0 es broadcast y no recibe respuestaLa dirección IP. El identificador de unidad de la cabecera selecciona un equipo detrás de una pasarela
Control de erroresCRC-16 en cada tramaChecksum de TCP. Sin CRC de Modbus
MaestrosUn maestro por línea RS-485Varios clientes pueden conectarse a un servidor
Equipos32 por segmento sin repetidorLimitado por la red y por el servidor
Configuración por defecto en la especificación19200 bit/s, 8 bits de datos, paridad par, 1 bit de paradaPuerto 502

Modbus tiene cuatro tablas de datos. Las coils son bits de lectura y escritura (FC01, FC05, FC15). Las entradas discretas son bits de solo lectura (FC02). Los input registers son palabras de 16 bits de solo lectura (FC04). Los holding registers son palabras de 16 bits de lectura y escritura (FC03, FC06, FC16). Los ejemplos de esta guía escriben y leen holding registers, la tabla que tiene casi cualquier equipo.

Puertos RS-485 y Ethernet del Raspberry PLC para Modbus

Los puertos RS-485 del Raspberry PLC aparecen en Linux como dispositivos serie. El nombre depende de la versión de hardware del PLC, no del modelo:

Versión del Raspberry PLCPuertos RS-485 en Linux
V3, y V4 sin placa de expansión/dev/ttySC0 y /dev/ttySC1
V6, y V4 con placa de expansión/dev/ttySC2 y /dev/ttySC3

Para estar seguro, lista los puertos de tu PLC con ls /dev/ttySC*. La guía RS-485 con Raspberry PLC muestra los bornes y una prueba en bucle entre los dos puertos del mismo PLC. Haz esta prueba primero: si la prueba en bucle falla, Modbus también fallará.

  • Conecta A+ con A+ y B- con B- en todos los equipos, con cable de par trenzado.
  • Cablea los equipos en línea, uno detrás de otro, no en estrella. En líneas largas, pon una resistencia de terminación adecuada al cable (normalmente 120 Ω) en los dos extremos de la línea. La hoja técnica del Raspberry PLC V6 indica una resistencia de terminación de 120 Ω en los dos puertos RS-485, así que cablea el PLC en un extremo de la línea.
  • Todos los equipos de una línea deben usar la misma velocidad, paridad y bits de parada. Si no coinciden, no llega ninguna respuesta, ni siquiera un mensaje de error.

Para Modbus TCP, el PLC usa sus puertos Ethernet. Por defecto, eth0 tiene la dirección estática 10.10.10.20. Dos PLC nuevos en la misma red tienen la misma dirección en eth0, así que cambia la dirección de uno de ellos antes de la prueba (consulta cómo configurar una IP estática en un Raspberry PLC). Si usas eth1, tiene la dirección 10.10.11.20 por defecto y, según la hoja técnica, una configuración fija de 10BT half duplex sin autonegociación. Comprueba las direcciones con ip a.

Instalar pymodbus 3.16 en un entorno virtual de Python

Instala pymodbus en un entorno virtual. Raspberry Pi OS basado en Debian 12 (Bookworm) y posteriores bloquea pip install fuera de un entorno virtual, y un entorno virtual además mantiene fija la versión de pymodbus de tu proyecto. La opción [serial] instala pyserial, que Modbus RTU necesita.

python3 --version   # pymodbus 3.9 and later need Python 3.10 or later
python3 -m venv ~/modbus-venv
source ~/modbus-venv/bin/activate
pip install "pymodbus[serial]==3.16.1"
python -c "import pymodbus; print(pymodbus.__version__)"

Ejecuta tus scripts con ~/modbus-venv/bin/python script.py. Usa la misma ruta en un servicio de systemd.

La mayoría de ejemplos de internet se escribieron para versiones antiguas de pymodbus y fallan con la actual. Estos son los cambios que rompen el código antiguo:

Versión de pymodbusCambioQué se escribe ahora
3.0unit= pasa a ser slave=Ver 3.10
3.7FramerType sustituye a ModbusRtuFramer. El ID de equipo por defecto es 1, no 0framer=FramerType.RTU
3.8Los parámetros del cliente después de la dirección son solo por nombre (keyword only)read_holding_registers(0, count=5, ...)
3.10slave= pasa a ser device_id=. ModbusSlaveContext pasa a ser ModbusDeviceContextdevice_id=1
3.12 y 3.13El servidor usa SimData y SimDevice. Las clases antiguas del datastore dan un aviso de obsolescenciaSimDevice(1, simdata=SimData(...))

Fuente: cambios de API de pymodbus. Con pymodbus 3.16.1, un ejemplo antiguo falla con errores como got an unexpected keyword argument 'slave' o cannot import name 'ModbusRtuFramer'.

Maestro y esclavo Modbus RTU en Python

Para esta prueba necesitas dos Raspberry PLC con sus puertos RS-485 conectados entre sí, A+ con A+ y B- con B-. Un PLC ejecuta el esclavo y el otro el maestro. En una instalación real, el esclavo suele ser un medidor, un variador o un sensor, y el Raspberry PLC es el maestro.

Esclavo Modbus RTU con pymodbus

from pymodbus.framer import FramerType
from pymodbus.server import StartSerialServer
from pymodbus.simulator import DataType, SimData, SimDevice

PORT = "/dev/ttySC0"  # RS-485 port, check the name for your PLC version
DEVICE_ID = 1         # Modbus address of this slave (1 to 247)

# 100 registers (addresses 0 to 99), all at 0
device = SimDevice(
    DEVICE_ID,
    simdata=SimData(0, count=100, values=0, datatype=DataType.REGISTERS),
)

StartSerialServer(
    device,
    framer=FramerType.RTU,
    port=PORT,
    baudrate=9600,
    bytesize=8,
    parity="N",
    stopbits=1,
)

Este esclavo usa el ID de equipo 1. Sus 100 registros (direcciones de 0 a 99) responden a peticiones de holding registers y de input registers. Una petición fuera de este rango recibe el código de excepción 02 (dirección de datos no válida). En nuestra prueba, pymodbus 3.16.1 también respondió con el código de excepción 04 a las peticiones dirigidas a otros ID de equipo, así que usa este esclavo de prueba solo en la línea, sin otros esclavos.

Maestro Modbus RTU con pymodbus

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 = 1         # address of the slave, never 0 (0 is broadcast)

client = ModbusSerialClient(
    port=PORT,
    framer=FramerType.RTU,
    baudrate=9600,
    bytesize=8,
    parity="N",
    stopbits=1,
    timeout=1,
)
if not client.connect():
    raise SystemExit(f"Cannot open {PORT}")

try:
    # FC16: write 5 holding registers from address 0
    rr = client.write_registers(0, [10, 20, 30, 40, 50], device_id=DEVICE_ID)
    if rr.isError():
        print("Write error:", rr)
    else:
        print("Write successful")

    # FC03: read the same 5 holding registers
    rr = client.read_holding_registers(0, count=5, device_id=DEVICE_ID)
    if rr.isError():
        print("Read error:", rr)
    else:
        print("Read successful:", rr.registers)
except ModbusException as exc:
    # No answer, CRC error or wrong device ID
    print("Modbus error:", exc)
finally:
    client.close()

Salida esperada cuando la comunicación funciona:

Write successful
Read successful: [10, 20, 30, 40, 50]

Dos detalles hacen que este ejemplo funcione. El ID de equipo es 1, nunca 0: la dirección 0 es broadcast, y los esclavos ejecutan una escritura broadcast pero nunca la responden. Y solo un programa puede usar un puerto serie a la vez: dos programas en el mismo puerto /dev/ttySC* se quitan bytes el uno al otro, y aparecen timeouts y errores de CRC. Detén los flujos de Node-RED que usan el puerto antes de ejecutar el script.

Modbus RTU en Node-RED con node-red-contrib-modbus

Si Node-RED todavía no está instalado en tu PLC, sigue cómo instalar Node-RED en un Raspberry PLC. Después instala node-red-contrib-modbus desde Menú, Manage palette, Install. La versión 5.62.0 declara Node-RED 4 o posterior y Node.js 22 o posterior, así que comprueba antes node -v.

Este flujo es un maestro Modbus RTU. Escribe un contador en el registro 0 del esclavo Python y lo vuelve a leer cada 5 segundos.

Flujo maestro en Node-RED: un nodo Inject y un nodo Function alimentan Modbus-Write (FC 6). Modbus-Read consulta el mismo registro cada 5 segundos.
  1. Nodo Inject: repetir cada 5 segundos.
  2. Nodo Function: un contador que suma 1 en cada mensaje.
    if (context.hasOwnProperty('num')) {
        context.num = context.num + 1;
    } else {
        context.num = 0;
    }
    msg.payload = context.num;
    return msg;
  3. Nodo Modbus-Write. En Server, crea un Modbus-Client con Type Serial, Serial port /dev/ttySC0 (o el puerto de tu versión de PLC), Serial type RTU, Baud rate 9600, Data bits 8, Parity None, Stop bits 1 y Unit-Id 1. Después pon FC en FC 6: Preset Single Register, Address 0, Quantity 1.
  4. Nodo Modbus-Read con el mismo Modbus-Client: FC FC 3: Read Holding Registers, Address 0, Quantity 1, Poll Rate 5 segundos.
  5. Nodos Debug o Modbus-Response en las salidas.

Ejecuta el esclavo Python de la sección anterior en el otro PLC. La salida de Modbus-Write muestra { address: 0, value: 8 } y la de Modbus-Read muestra [8], con el valor actual del contador. Para leer varios esclavos de la misma línea desde un flujo, consulta cómo usar varios esclavos Modbus RTU en Node-RED.

Servidor y cliente Modbus TCP en Python

Puedes probar Modbus TCP en un solo PLC. Ejecuta el servidor en un terminal y el cliente en un segundo terminal, con la dirección 127.0.0.1. Después lleva el cliente a un segundo PLC o a un PC y usa la dirección IP del PLC que ejecuta el servidor.

Servidor Modbus TCP

from pymodbus.server import StartTcpServer
from pymodbus.simulator import DataType, SimData, SimDevice

# Port 502 is the Modbus TCP standard. Linux opens ports under 1024
# only for root, so this test uses 5020.
PORT = 5020
DEVICE_ID = 1

device = SimDevice(
    DEVICE_ID,
    simdata=SimData(0, count=100, values=0, datatype=DataType.REGISTERS),
)

# 0.0.0.0 listens on all interfaces: eth0, eth1 and localhost
StartTcpServer(device, address=("0.0.0.0", PORT))

En un sistema Linux estándar, solo root puede abrir puertos TCP por debajo de 1024. Un usuario normal que arranca un servidor en el puerto Modbus 502 recibe un PermissionError. Por eso la prueba usa el puerto 5020. En producción, ejecuta el servicio con los permisos que necesita o redirige el puerto 502 al puerto de tu servidor.

Cliente Modbus TCP

from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException

HOST = "127.0.0.1"  # IP address of the slave, or 127.0.0.1 on the same PLC
PORT = 5020
DEVICE_ID = 1       # unit identifier, see the device manual (often 1 or 255)

client = ModbusTcpClient(HOST, port=PORT, timeout=2)
if not client.connect():
    raise SystemExit(f"No connection to {HOST}:{PORT}")

try:
    rr = client.write_registers(0, [10, 20, 30, 40, 50], device_id=DEVICE_ID)
    if rr.isError():
        print("Write error:", rr)
    else:
        print("Write successful")

    rr = client.read_holding_registers(0, count=5, device_id=DEVICE_ID)
    if rr.isError():
        print("Read error:", rr)
    else:
        print("Read successful:", rr.registers)
except ModbusException as exc:
    print("Modbus error:", exc)
finally:
    client.close()

En Modbus TCP, la dirección IP selecciona el equipo. El identificador de unidad (device_id) solo selecciona un equipo detrás de una pasarela, por ejemplo un esclavo RTU detrás de una pasarela Modbus TCP a RTU. Muchos equipos lo ignoran, y otros solo responden a 1 o a 255. Usa el valor del manual del equipo.

Modbus TCP en Node-RED

Usa el mismo flujo que para Modbus RTU y cambia solo la configuración del Modbus-Client:

  • Type: TCP, TCP type DEFAULT.
  • Host: dirección IP del servidor Modbus TCP, o 127.0.0.1 si el servidor se ejecuta en el mismo PLC.
  • Port: 5020 para el servidor Python de prueba, 502 para la mayoría de equipos.
  • Unit-Id: 1, o el valor del manual del equipo.

Modbus-Write (FC 6, dirección 0) y Modbus-Read (FC 3, dirección 0, cantidad 1, poll rate de 5 segundos) no cambian.

Timeouts, reintentos y reconexión en un bucle de lectura Modbus

Un maestro en producción debe seguir funcionando cuando un esclavo se reinicia, se desconecta un cable o llega una trama con un CRC erróneo. pymodbus informa de los problemas de dos formas diferentes, y el bucle de lectura debe tratar las dos:

  • Respuesta de excepción. El esclavo ha respondido, pero ha rechazado la petición. rr.isError() es verdadero y el código de excepción dice por qué.
  • Sin respuesta válida. Timeout, conexión cerrada o trama rota. El cliente lanza una ModbusException.
import time

from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException

HOST = "127.0.0.1"
PORT = 5020
DEVICE_ID = 1
POLL_S = 1.0  # read every second

client = ModbusTcpClient(HOST, port=PORT, timeout=2, retries=1)

while True:
    if not client.connected and not client.connect():
        print("Slave not reachable, new try in 5 s")
        time.sleep(5)
        continue
    try:
        rr = client.read_holding_registers(0, count=5, device_id=DEVICE_ID)
        if rr.isError():
            print("Exception response:", rr)  # the slave answered with an error code
        else:
            print(rr.registers)
    except ModbusException as exc:
        print("No valid answer:", exc)  # timeout or broken frame
        client.close()  # force a clean reconnect on the next cycle
    time.sleep(POLL_S)

El cliente espera timeout segundos en cada intento y repite la petición retries veces. Con timeout=1 y el valor por defecto retries=3, un esclavo RTU ausente bloqueó la llamada unos 4 segundos en nuestra prueba. En una línea RS-485 con varios esclavos, un esclavo caído ralentiza la lectura de todos los demás. Mantén bajos el timeout y los reintentos, y salta un esclavo durante algunos ciclos después de varios fallos seguidos.

Código de excepciónNombreCausa habitual
01Illegal function (función no válida)El equipo no admite este código de función, por ejemplo FC 16 en un equipo que solo acepta FC 6
02Illegal data address (dirección no válida)Dirección o cantidad fuera de rango, a menudo una dirección 1 por encima de la correcta
03Illegal data value (valor no válido)Valor fuera de rango o cantidad errónea en la petición
04Server device failure (fallo del equipo)El equipo no ha podido ejecutar la petición
0B (11)Gateway target device failed to respond (el equipo detrás de la pasarela no responde)Una pasarela Modbus TCP a RTU no ha recibido respuesta del esclavo RTU que tiene detrás

El registro 40001 es la dirección 0: direccionamiento Modbus y valores de 32 bits

Muchos manuales de equipos escriben los holding registers como 40001, 40002, etc. Es una convención de documentación: el primer dígito 4 indica holding register, y la numeración empieza en 1. La dirección en la trama empieza en 0. Así que el registro 40001 es la dirección 0 en pymodbus y en Node-RED, y el 40010 es la dirección 9. Algunos manuales ya numeran desde 0: comprueba el primer registro del mapa. Si lees un registro de más, obtienes el valor siguiente o el código de excepción 02.

Un registro tiene 16 bits. Valores como una tensión en float32 o un contador de energía de 32 bits ocupan dos registros, y los equipos no coinciden en qué registro va primero (el orden de palabras). pymodbus convierte los registros por ti:

from pymodbus.client import ModbusTcpClient

DT = ModbusTcpClient.DATATYPE

# Two holding registers read from a meter, for example with
# rr = client.read_holding_registers(100, count=2, device_id=1)
regs = [17254, 32768]  # 0x4366 0x8000

print(ModbusTcpClient.convert_from_registers(regs, DT.FLOAT32))
# 230.5
print(ModbusTcpClient.convert_from_registers(regs, DT.FLOAT32, word_order="little"))
# -2.4178003703460394e-41 (wrong word order for this device)

Un valor muy pequeño, muy grande o negativo donde no puede serlo suele indicar que el orden de palabras es el contrario. Prueba con word_order="little".

Diagnóstico de problemas Modbus en un Raspberry PLC

SíntomaQué comprobar
RTU: timeout en todas las peticionesVelocidad, paridad y bits de parada del equipo. A+ y B- invertidos. ID de equipo. El nombre /dev/ttySC* de tu versión de PLC
RTU: algunas peticiones fallan, errores de CRCTerminación en los dos extremos de una línea larga. Cableado en estrella. Cable que no es de par trenzado. Dos programas en el mismo puerto
Código de excepción 02Desfase de dirección (40001 es la dirección 0) o una cantidad que pasa del último registro
unexpected keyword argument 'slave' o 'unit'El ejemplo es para un pymodbus antiguo. Usa device_id=
cannot import name 'ModbusRtuFramer' o 'ModbusSlaveContext'El ejemplo es para un pymodbus antiguo. Usa FramerType y SimDevice
TCP: connection refused (conexión rechazada)El servidor no se está ejecutando, el puerto es incorrecto o un firewall lo bloquea
Servidor TCP: PermissionError en el puerto 502Los puertos por debajo de 1024 necesitan root. Usa el puerto 5020 para las pruebas
TCP funciona en 127.0.0.1 pero no desde el otro PLCLos dos PLC tienen 10.10.10.20 en eth0, los PLC están en subredes diferentes o el servidor solo escucha en 127.0.0.1

Ejemplos de Modbus con Raspberry PLC en proyectos reales

Elige el controlador para tu proyecto Modbus. La familia Raspberry PLC funciona con Linux, Python y Node-RED, con RS-485 para Modbus RTU y Ethernet para Modbus TCP. Compara las E/S y los puertos de cada modelo.

Ver la familia Raspberry PLC

Preguntas frecuentes sobre Modbus en PLC Raspberry Pi

¿Puede un Raspberry PLC ser maestro y esclavo Modbus a la vez?

Sí. Ejecuta el maestro y el servidor como dos programas en puertos diferentes. Una configuración habitual lee medidores como maestro Modbus RTU por RS-485 y da los valores a un sistema SCADA como servidor Modbus TCP por Ethernet. Cada puerto serie solo puede tener un programa.

¿Uso Modbus RTU o Modbus TCP?

Usa el protocolo del equipo que conectas. Muchos medidores, variadores pequeños y sensores solo tienen RS-485 con Modbus RTU. Variadores, inversores, PLC y pasarelas suelen tener Modbus TCP. Con RTU hay un maestro por línea. Con TCP varios clientes pueden leer el mismo servidor por la red existente.

¿Por qué pymodbus dice "unexpected keyword argument 'slave'"?

Desde pymodbus 3.10, el parámetro slave= es device_id=. La versión 2.x usaba unit=. Cambia el parámetro o instala la versión de pymodbus para la que se escribió el ejemplo.

¿El holding register 40001 es la dirección 0 o la 1?

La dirección 0. La notación 4xxxx numera los registros desde 1, y la dirección en la trama Modbus empieza en 0. El registro 40001 es la dirección 0 y el 40100 es la dirección 99.

¿Por qué mi servidor Modbus TCP no arranca en el puerto 502?

En Linux, solo root puede abrir puertos por debajo de 1024. Arranca el servidor como root, dale los permisos que necesita o usa un puerto como el 5020 y configura los clientes con el mismo puerto.

¿Cuántos equipos Modbus RTU puede leer un puerto RS-485?

Modbus RTU tiene 247 direcciones de esclavo (de 1 a 247). La especificación de Modbus sobre línea serie permite 32 equipos en un segmento RS-485 sin repetidor. El límite real suele ser el tiempo de lectura: cada esclavo añade su petición, su respuesta y, si falla, su timeout.

Buscar en nuestro blog

Guía Modbus TCP y RTU para PLCs Raspberry Pi: ejemplos con Python y Node-RED
Boot & Work Corp. S.L, Martí Schnabel 21 de noviembre de 2023
Compartir esta publicación
Etiquetas

¿Estás buscando tu Controlador Lógico Programable ideal?

Echa un vistazo a esta comparativa de producto de varios controladores industriales basados en Arduino.

Comparamos entradas, salidas, comunicaciones y otras especificaciones con las de los equipos de otras marcas destacadas.

Comparativa de PLCs