Protocolo Modbus TCP/IP: todo lo que necesitas saber para conectar tus controladores lógicos programables a la nube

· 16 min de lectura
16 min read
Équipe Eziwan
Infrastructure IoT

Modbus TCP es el protocolo de recopilación de datos de controladores lógicos programables más extendido en la industria francesa. Se utiliza en decenas de millones de equipos instalados desde hace más de 40 años. Si incorporas la conectividad remota en tus proyectos, te encontrarás con Modbus TCP, así que conviene que lo comprendas a fondo.

Esta guía abarca lo esencial para un profesional: historia, estructura de la trama byte a byte, códigos de función, comparación con los protocolos de la competencia, optimización del rendimiento, seguridad y un ejemplo de código en Python completo y funcional.


1. De Modbus RTU a Modbus TCP: 45 años de evolución

El nacimiento de Modbus en 1979

Modbus fue creado en 1979 por Modicon (hoy absorbida por Schneider Electric) para permitir la comunicación entre los primeros controladores lógicos programables Modicon 084. Se trataba de un protocolo serie (RS-232 y posteriormente RS-485), de tipo maestro/esclavo, diseñado con un único objetivo: leer y escribir valores numéricos en equipos de campo.

La simplicidad era el principio fundamental del diseño:

  • Sin direccionamiento de red: solo un identificador de esclavo del 1 al 247 en el bus
  • Sin autenticación: cualquier maestro puede consultar a cualquier esclavo
  • Sin detección automática: la lista de esclavos se configura manualmente
  • Sin mecanismo de seguridad: el protocolo da por supuesto un entorno físicamente seguro

Modbus RTU (Remote Terminal Unit): versión binaria compacta para RS-485. Los datos se transmiten en formato binario, con un CRC de 16 bits para la detección de errores. Sigue siendo muy habitual en los instrumentos de campo (contadores de energía, sensores de presión, variadores de frecuencia).

Modbus ASCII: versión legible en ASCII, para conexiones RS-232. Menos eficiente (2 octetos por byte de datos), obsoleta en las nuevas instalaciones.

Modbus TCP: Ethernet sin una revolución del protocolo

Entre 1996 y 1999, con la generalización de Ethernet en las fábricas, Modbus TCP encapsuló el protocolo en tramas TCP/IP. La revisión fue minuciosa:

  • Mismo modelo de datos (bobinas, entradas discretas, registros de entrada, registros de retención)
  • Mismos códigos de función
  • Mismo direccionamiento de registros
  • Eliminación del CRC (TCP garantiza la integridad)
  • Incorporación de un encabezado MBAP de 7 bytes para el enrutamiento y la correlación de las solicitudes
  • Puerto TCP estándar: 502

La ventaja: cualquier equipo existente compatible con Modbus RTU podía migrar a Modbus TCP con cambios mínimos. El inconveniente: se mantuvieron las deficiencias de seguridad originales.


2. Estructura de una trama Modbus TCP: análisis byte a byte

Descripción general de la trama

┌─────────────────────────────────────────────────────────────────────┐
│ Trame Modbus TCP │
├──────────────────────────────────────────┬──────────────────────────┤
│ MBAP Header (7 octets) │ PDU │
├──────────┬──────────┬──────────┬─────────┼────────────┬─────────────┤
│ Trans.ID │ Proto.ID │ Length │ Unit ID │ Func.Code │ Data │
│ 2 bytes │ 2 bytes │ 2 bytes │ 1 byte │ 1 byte │ n bytes │
└──────────┴──────────┴──────────┴─────────┴────────────┴─────────────┘

Análisis del encabezado MBAP

Identificador de transacción (2 bytes): Número libre elegido por el cliente, que se devuelve tal cual en la respuesta. Permite correlacionar las respuestas con las solicitudes en el caso de conexiones persistentes con varias solicitudes en curso. En la práctica, la mayoría de los clientes utilizan un contador simple (00 01, 00 02, 00 03...) o un identificador fijo (00 00) si funcionan en modo de solicitud/respuesta secuencial.

Identificador de protocolo (2 bytes): Siempre es 00 00 para Modbus. Esta constante existe para permitir una posible ampliación a otros protocolos a través del mismo puerto, pero nunca se ha utilizado.

Longitud (2 octetos): Número de octetos que siguen, en formato big-endian. Incluye el ID de unidad y la PDU, pero no los 4 primeros octetos del encabezado MBAP. Para una solicitud FC=03 de 10 registros: 6 octetos (1 ID de unidad + 1 FC + 2 direcciones + 2 recuentos) → 00 06.

Identificador de unidad (1 byte): Identificador del esclavo Modbus. En los controladores conectados directamente (S7-1200 con MB_SERVER, M340 con servidor Modbus integrado), el valor suele ser 1 o 255. En las pasarelas Modbus RTU→TCP, este identificador corresponde a la dirección del esclavo RTU en el bus RS-485.

Análisis de una solicitud FC=03 completa

Lectura de 10 registros de holding a partir de la dirección 100 (por ejemplo: %MW100 en el Schneider M340):

Requête client (hex) : 00 01 00 00 00 06 FF 03 00 64 00 0A

Décodage byte par byte :
00 01 → Transaction ID = 1
00 00 → Protocol ID = 0 (Modbus standard)
00 06 → Length = 6 (Unit ID + FC + 2 adresse + 2 count)
FF → Unit ID = 255 (0xFF = CPU locale M340)
03 → Function Code = Read Holding Registers
00 64 → Starting Address = 100 (0x0064 en big-endian)
00 0A → Quantity = 10 registres (0x000A)
Réponse automate (hex) : 00 01 00 00 00 17 FF 03 14 [20 octets]

Décodage :
00 01 → Transaction ID = 1 (même que la requête — corrélation)
00 00 → Protocol ID = 0
00 17 → Length = 23 (1 UID + 1 FC + 1 ByteCount + 20 données)
FF → Unit ID = 255
03 → Function Code = 3 (confirme que c'est une réponse normale)
14 → Byte Count = 20 (10 registres × 2 octets = 0x14 hex)
[suivi de 20 octets de données — 10 valeurs uint16 big-endian]

Códigos de excepción (respuestas de error)

Cuando el autómata no puede procesar una solicitud, devuelve un código de excepción:

Réponse exception (hex) : 00 01 00 00 00 03 FF 83 02

83 → FC + 0x80 = 3 + 128 = 131 → indique une réponse d'exception sur FC=3
02 → Exception code : Illegal Data Address
CódigoNombreSignificadoCausa habitual
01Función no válidaFC no compatibleEl autómata no admite esta FC
02Dirección de datos no válidaDirección fuera de rangoRegistro > límite del autómata
03Valor de datos no válidoValor de datos no válidoRecuento > 125 (FC=03)
04Fallo del dispositivo servidorError internoServidor Modbus no inicializado
05ConfirmaciónProcesamiento prolongado en cursoPoco frecuente en la práctica
06Dispositivo servidor ocupadoDemasiadas conexiones simultáneas
0ARuta de la pasarela no disponibleRuta de la pasarela no disponibleEsclavo RTU inaccesible
0BFallo del dispositivo de destino de la pasarelaEsclavo que no respondeEsclavo RTU apagado o averiado

3. Los códigos de función en detalle

FC=01 y FC=02 — Lectura de bits

FC=01 : Read Coils → bits de sortie (écriture autorisée)
FC=02 : Read Discrete Inputs → bits d'entrée (lecture seule)

Requête : FC AddrH AddrL QuantH QuantL
01 00 00 00 00 00 10 (lecture 16 coils depuis @0)

Réponse : FC ByteCount Coil_Data
01 02 [2 bytes = 16 bits d'état]

Note : les coils sont packés en bits, LSB first.
Byte1 bit0 = Coil@0, Byte1 bit1 = Coil@1, etc.

FC=03 — Lectura de los registros de retención (el más utilizado)

Lectura y escritura de registros de 16 bits. Límite: 125 registros como máximo por consulta.

FC=04 — Lectura de registros de entrada

Idéntico a FC=03, pero para los registros de «entrada» (solo lectura). En el Schneider M340, corresponde a los %IW. En la mayoría de los autómatas, se utiliza poco, ya que las mediciones analógicas se copian en los %MW (registros de retención).

FC=06 y FC=16 — Escritura en registros

FC=06 : Write Single Register
Requête : FC AddrH AddrL ValueH ValueL
06 00 64 00 64 01 F4 (écriture 500 à l'adresse 100)

FC=16 : Write Multiple Registers (jusqu'à 123 registres par requête)
Requête : FC AddrH AddrL CountH CountL ByteCount Values...
10 00 64 00 05 01 2C [données]

Precaución al escribir (FC=06, FC=16): solo escriba en los registros si está seguro de la direccionamiento y del impacto en el programa del autómata. Una escritura en un registro de consigna modifica inmediatamente el comportamiento del proceso. Prueba siempre en un autómata de desarrollo o fuera de producción antes de la implementación.


4. Modbus TCP, OPC-UA y MQTT: ¿qué protocolo elegir para tu arquitectura?

Estos tres protocolos coexisten en las arquitecturas modernas de IIoT. Sirven a diferentes capas y no compiten directamente entre sí.

CriterioModbus TCPOPC-UAMQTT
Antigüedad1979 (RTU), ~1996 (TCP)20081999
ModeloCliente/Servidor (sondeo)Cliente/Servidor + Pub/SubPub/Sub (broker)
CapaComunicación de campoInteroperabilidad de sistemasTransporte IoT/nube
Seguridad nativaNingunaTLS + autenticación por certificadoTLS + autenticación
Carga de CPU del autómataMuy bajaAltaBaja a media
Autómatas antiguosUniversal (40 años)Solo autómatas modernosA través de una pasarela
Detección automáticaNoSí (espacio de nombres OPC)No
Datos estructuradosNo (registros planos)Sí (tipos, estructuras, matrices)Sí (JSON/CBOR)
LatenciaBaja (sondeo)Muy baja (pub/sub)Muy baja

¿Cómo elegir?

Utiliza Modbus TCP si:

  • El controlador es compatible con Modbus TCP (Siemens S7-1200, Schneider M340/M221/M241, Wago, Allen-Bradley...)
  • Necesita recopilar mediciones sencillas (registros de 16 bits)
  • La simplicidad de la configuración prima sobre la riqueza del modelo de datos

Utiliza OPC-UA si:

  • El controlador es compatible de forma nativa con el servidor OPC-UA (S7-1500, B&R, Beckhoff, CODESYS 3.x reciente)
  • Necesita supervisar variables complejas (estructuras, matrices, tipos personalizados)
  • La interoperabilidad con otros sistemas SCADA o MES es un requisito previo

Utiliza MQTT para el transporte en la nube si:

  • Tienes una pasarela (como Eziwan) que recoge datos en Modbus/OPC-UA y los publica en MQTT
  • Necesitas una arquitectura pub/sub escalable para miles de puntos

La arquitectura de destino para el IIoT en la nube:

[Automate] ──Modbus TCP / OPC-UA──→ [Gateway] ──MQTT over TLS──→ [Cloud]

La pasarela convierte y transmite datos. Actúa como puente entre el mundo OT (Modbus/OPC-UA) y el mundo de la nube (MQTT/REST).


5. Seguridad de Modbus TCP: el problema y las soluciones

La ausencia total de autenticación

Modbus TCP no cuenta con ningún mecanismo de seguridad nativo:

  • Sin autenticación: cualquier usuario de la red puede leer y escribir
  • Sin cifrado: los datos se transmiten en claro (legibles con Wireshark)
  • Sin autorización: no existe el concepto de derechos de lectura/escritura por usuario

Consecuencia práctica: si se puede acceder a tu controlador desde Internet (puerto 502 abierto), es totalmente vulnerable. Si alguien se conecta a tu red OT (físicamente o a través de una brecha de seguridad), puede leer y modificar todos los registros.

Las posibles respuestas

Segmentación de la red (medida primaria): Limitar el tráfico Modbus TCP a una red OT aislada. La pasarela Eziwan se encuentra en esta red OT; la comunicación con la nube se realiza a través de un túnel VPN, no mediante la exposición del puerto 502.

Filtrado de IP en el controlador (medida secundaria): Algunos controladores recientes (Schneider M340 con firmware ≥ 2.60, Siemens S7-1200 con firmware V4.x y funciones FIREWALL) admiten el filtrado de IP: solo la dirección IP de la pasarela Eziwan está autorizada a conectarse al puerto 502.

Seguridad Modbus (medida avanzada): La Modbus Organization publicó en 2018 una extensión denominada «Modbus Security» (transporte TLS para Modbus TCP) en el puerto 802. Esta extensión aún no es ampliamente compatible con los controladores lógicos programables (PLC) del mercado, pero está disponible en algunos equipos recientes.


6. Optimización del rendimiento de la recogida

Agrupación de consultas (batching)

La regla de oro: agrupar las lecturas en bloques contiguos. Leer 100 registros contiguos en una sola consulta es mucho más eficaz que realizar 100 consultas de un registro cada una:

Configuration inefficace (100 requêtes) :
Lire %MW0 → 1 requête (2 octets de données)
Lire %MW50 → 1 requête
Lire %MW99 → 1 requête
... 97 requêtes supplémentaires ...
Latence totale : 100 × (aller-retour réseau) ≈ 100 × 5ms = 500ms

Configuration optimisée (1 requête) :
Lire %MW0 à %MW99 → 1 requête (200 octets de données)
Latence totale : 1 × 5ms = 5ms

La pasarela Eziwan agrupa automáticamente las variables configuradas en rangos contiguos. Para aprovechar al máximo esta agrupación, organiza tus variables de supervisión en zonas de memoria adyacentes.

Repercusión en el ciclo del autómata

El controlador procesa cada solicitud Modbus TCP recibida al final del ciclo de programa. En un M340 con un ciclo de 10 ms, 10 solicitudes Modbus simultáneas pueden alargar el ciclo entre 1 y 3 ms.

Recomendación: no superar una solicitud Modbus por ciclo del autómata en aplicaciones con requisitos de tiempo real. Reducir la frecuencia de sondeo si el ciclo se alarga.


7. Ejemplo de código completo en Python con pymodbus

"""
Collecte Modbus TCP industrielle avec gestion d'erreurs robuste
Compatible Schneider M340, Siemens S7-1200, Wago, Allen-Bradley...
Testé avec pymodbus >= 3.0

Installation :
pip install pymodbus loguru
"""

from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException, ConnectionException
from loguru import logger
import struct
import time
from dataclasses import dataclass
from typing import Optional


# ─── Configuration ────────────────────────────────────────────────────────────

HOST = "192.168.1.20" # IP automate Schneider M340
PORT = 502 # Port Modbus TCP
UNIT_ID = 255 # 255 pour CPU locale M340, 1 pour S7-1200
TIMEOUT = 3 # Timeout en secondes
RETRIES = 3 # Nombre de tentatives avant abandon


# ─── Structure de données ─────────────────────────────────────────────────────

@dataclass
class VariablesProcess:
temperature_c: float = 0.0
debit_lmin: int = 0
pression_bar: float = 0.0
pompe_1_marche: bool = False
pompe_2_marche: bool = False
code_defaut: int = 0
compteur_production: int = 0
timestamp: float = 0.0


# ─── Client Modbus avec reconnexion automatique ───────────────────────────────

class ModbusCollecteur:
"""Collecteur Modbus TCP avec gestion de reconnexion et retry."""

def __init__(self, host: str, port: int = 502, unit_id: int = 1):
self.host = host
self.port = port
self.unit_id = unit_id
self.client: Optional[ModbusTcpClient] = None
self._connexions_ok = 0
self._connexions_echec = 0

def connecter(self) -> bool:
"""Établit la connexion TCP vers l'automate."""
try:
self.client = ModbusTcpClient(
self.host,
port=self.port,
timeout=TIMEOUT,
retries=1, # Retries gérés par notre code
reconnect_delay=0, # Pas de reconnexion automatique
)
if self.client.connect():
self._connexions_ok += 1
logger.info(f"Connecté à {self.host}:{self.port}")
return True
else:
self._connexions_echec += 1
logger.warning(f"Connexion refusée par {self.host}:{self.port}")
return False
except Exception as e:
logger.error(f"Erreur connexion : {e}")
return False

def deconnecter(self):
if self.client:
self.client.close()
self.client = None

def lire_holding_registers(
self, adresse: int, count: int
) -> Optional[list[int]]:
"""Lit des registres avec retry automatique."""
for tentative in range(1, RETRIES + 1):
try:
if not self.client or not self.client.is_socket_open():
if not self.connecter():
time.sleep(1)
continue

result = self.client.read_holding_registers(
address=adresse, count=count, slave=self.unit_id
)

if result.isError():
exception_code = getattr(result, 'exception_code', '?')
logger.warning(
f"Erreur Modbus addr={adresse} count={count} "
f"exception_code={exception_code}"
)
# Exception 04 (server busy) → attendre avant retry
if exception_code == 6:
time.sleep(0.5)
self.deconnecter()
continue

return result.registers

except ConnectionException:
logger.warning(f"Connexion perdue (tentative {tentative}/{RETRIES})")
self.deconnecter()
time.sleep(0.5)
except Exception as e:
logger.error(f"Erreur inattendue : {e}")
self.deconnecter()
time.sleep(1)

logger.error(f"Échec lecture après {RETRIES} tentatives — addr={adresse}")
return None

@staticmethod
def registres_to_float32_abcd(reg_high: int, reg_low: int) -> float:
"""Décode 2 registres 16-bit en float32 IEEE754 big-endian (ABCD)."""
raw = struct.pack(">HH", reg_high, reg_low)
return struct.unpack(">f", raw)[0]

@staticmethod
def registres_to_int32(reg_high: int, reg_low: int) -> int:
"""Décode 2 registres en entier 32-bit signé big-endian."""
raw = struct.pack(">HH", reg_high, reg_low)
return struct.unpack(">i", raw)[0]


# ─── Lecture des variables process ───────────────────────────────────────────

def lire_variables(collecteur: ModbusCollecteur) -> Optional[VariablesProcess]:
"""
Lecture optimisée : 1 seule requête Modbus pour toutes les variables.

Plan mémoire (M340 EcoStruxure) :
%MW100 : Temperature (int16, ×0.1 → °C)
%MW101 : Débit (int16, L/min)
%MW102-103 : Pression (float32 ABCD, bar)
%MW104 : États machine (bits 0-7)
%MW105 : Code défaut actif
%MW106-107 : Compteur production (int32)
"""
# 1 requête pour %MW100 à %MW107 (8 registres)
registres = collecteur.lire_holding_registers(adresse=100, count=8)
if registres is None:
return None

vars_process = VariablesProcess(
temperature_c = registres[0] * 0.1, # %MW100
debit_lmin = registres[1], # %MW101
pression_bar = ModbusCollecteur.registres_to_float32_abcd( # %MW102+103
registres[2], registres[3]),
pompe_1_marche = bool(registres[4] & 0x0001), # %MW104 bit0
pompe_2_marche = bool(registres[4] & 0x0002), # %MW104 bit1
code_defaut = registres[5], # %MW105
compteur_production = ModbusCollecteur.registres_to_int32( # %MW106+107
registres[6], registres[7]),
timestamp = time.time(),
)

return vars_process


# ─── Boucle principale ────────────────────────────────────────────────────────

def main():
collecteur = ModbusCollecteur(HOST, PORT, UNIT_ID)
INTERVALLE = 5 # secondes entre chaque collecte

logger.info(f"Démarrage collecte Modbus TCP — {HOST}:{PORT} — cycle {INTERVALLE}s")

while True:
try:
vars_process = lire_variables(collecteur)

if vars_process:
logger.info(
f"T={vars_process.temperature_c:.1f}°C "
f"Q={vars_process.debit_lmin}L/min "
f"P={vars_process.pression_bar:.2f}bar "
f"P1={'ON' if vars_process.pompe_1_marche else 'OFF'} "
f"Défaut={vars_process.code_defaut}"
)
# Ici : publier via MQTT, API REST, ou base time-series
# mqtt_client.publish("usine/ligne3/variables", vars_process)
else:
logger.warning("Collecte échouée — attente 10s avant retry")
time.sleep(10)
continue

time.sleep(INTERVALLE)

except KeyboardInterrupt:
logger.info("Arrêt demandé.")
collecteur.deconnecter()
break


if __name__ == "__main__":
main()
info

Este código ilustra las buenas prácticas para la recopilación de datos Modbus TCP en Python: agrupación de lecturas, gestión de excepciones, reconexión automática y decodificación de tipos complejos. En producción, la pasarela Eziwan gestiona todas estas funciones de forma nativa, sin necesidad de un script de Python in situ.


Preguntas frecuentes — Protocolo Modbus TCP/IP

¿Por qué el puerto Modbus TCP es el 502 y no un puerto estándar como el 80 o el 443?

El puerto 502 fue asignado oficialmente a Modbus por la IANA (Autoridad de Asignación de Números de Internet) durante la estandarización de Modbus TCP en la década de 1990. En aquel momento no había motivos para reutilizar puertos de aplicaciones informáticas ya existentes. Se trata de un puerto registrado (< 1024), por lo que su uso como servidor requiere derechos de root en Linux, algo que los firmwares industriales gestionan de forma nativa.

¿Cuál es el límite de registros que se pueden leer en una sola consulta FC=03?

El límite es de 125 registros por solicitud FC=03 (Read Holding Registers), tal y como se define en la especificación oficial de Modbus. Es decir, un máximo de 250 bytes de datos útiles por solicitud. Para FC=16 (Write Multiple Registers), el límite es de 123 registros por solicitud. La pasarela Eziwan gestiona automáticamente la división si un rango solicitado supera estos límites.

¿Admite Modbus TCP varias conexiones de cliente simultáneas?

Sí, pero es el controlador el que define el límite. El Schneider M340 admite hasta 16 conexiones simultáneas, mientras que el Siemens S7-1200 admite hasta 8 (a través de MB_SERVER). Si se supera este límite, el controlador devuelve una excepción 06 (Server Busy). En la práctica, una pasarela de recopilación + una herramienta de diagnóstico + un SCADA local alcanzan fácilmente las 3 conexiones simultáneas, por lo que hay que estar atento.

¿Se puede cifrar Modbus TCP?

El estándar Modbus TCP nativo (puerto 502) no está cifrado. Desde 2018 existe una extensión denominada «Modbus Security» en el puerto 802 con TLS, pero los controladores lógicos actuales rara vez la admiten. La solución práctica consiste en confinar Modbus TCP a una red OT aislada y hacer pasar toda la comunicación hacia el exterior a través de un túnel VPN cifrado (como OpenVPN de Eziwan); el cifrado lo garantiza la capa de transporte, no el propio Modbus.

¿Es pymodbus la única biblioteca de Python para Modbus TCP?

No. Las principales alternativas son minimalmodbus (más sencillo para Modbus RTU), umodbus (ligero, con bajo consumo de memoria) y pyModbusTCP (puramente TCP, sin dependencias). Para proyectos industriales en Python, pymodbus >= 3.0 sigue siendo la opción más completa, con gestión de excepciones, reconexión y compatibilidad con ambos modos (RTU y TCP).

¿Cómo depurar una comunicación Modbus TCP sin acceso al controlador lógico programable?

Utiliza Modbus Slave (Windows) como simulador de controlador lógico local, o diagslave (línea de comandos, multiplataforma). Estas herramientas simulan un servidor Modbus TCP que responde a direcciones configurables. Es ideal para probar una configuración de recopilación de datos sin correr el riesgo de afectar a un controlador en producción.


👉 Siguiente paso

¿Tu controlador utiliza Modbus TCP, pero no estás seguro de si es compatible con tu versión de firmware?

Comprueba la compatibilidad de tu controlador →

Véase también: Modbus TCP con un Schneider M340 y la documentación de Siemens S7 para configuraciones específicas.