Protocollo Modbus TCP/IP: tutto quello che c'è da sapere per collegare i propri controllori al cloud
Modbus TCP è il protocollo di acquisizione dati per PLC più diffuso nell'industria francese. È presente in decine di milioni di apparecchiature installate da oltre 40 anni. Se integrate la connettività remota nei vostri progetti, vi imbatterete sicuramente in Modbus TCP: tanto vale comprenderlo a fondo.
Questa guida tratta gli aspetti fondamentali per un professionista: storia, struttura della trama byte per byte, codici funzione, confronto con i protocolli concorrenti, ottimizzazione delle prestazioni, sicurezza ed esempio di codice Python completo e funzionante.
1. Da Modbus RTU a Modbus TCP: 45 anni di evoluzione
La nascita di Modbus nel 1979
Modbus è stato creato nel 1979 da Modicon (oggi acquisita da Schneider Electric) per consentire la comunicazione tra i primi controllori logici programmabili Modicon 084. Si trattava di un protocollo seriale (RS-232 e successivamente RS-485), di tipo master/slave, progettato per un unico scopo: leggere e scrivere valori numerici su apparecchiature di campo.
La semplicità era il principio fondamentale di progettazione:
- Nessun indirizzamento di rete — solo un identificativo slave compreso tra 1 e 247 sul bus
- Nessuna autenticazione: qualsiasi master può interrogare qualsiasi slave
- Nessuna individuazione automatica: l'elenco degli slave viene configurato manualmente
- Nessun meccanismo di sicurezza: il protocollo presuppone un ambiente fisicamente sicuro
Modbus RTU (Remote Terminal Unit): versione binaria compatta per RS-485. I dati vengono trasmessi in formato binario, con un CRC a 16 bit per il rilevamento degli errori. Ancora molto diffuso sugli strumenti da campo (contatori di energia, sensori di pressione, convertitori di frequenza).
Modbus ASCII: versione leggibile in ASCII, per collegamenti RS-232. Meno efficiente (2 byte per ogni byte di dati), ormai obsoleta nelle nuove installazioni.
Modbus TCP: Ethernet senza una rivoluzione del protocollo
Intorno al periodo 1996-1999, con la diffusione capillare di Ethernet negli stabilimenti, Modbus TCP ha incapsulato il protocollo in frame TCP/IP. La modifica è stata minime:
- Stesso modello di dati (coil, ingressi discreti, registri di ingresso, registri di temporizzazione)
- Stessi codici funzione
- Stesso indirizzamento dei registri
- Eliminazione del CRC (il TCP garantisce l’integrità)
- Aggiunta di un’intestazione MBAP di 7 byte per l’instradamento e la correlazione delle richieste
- Porta TCP standard: 502
Il vantaggio: tutte le apparecchiature esistenti compatibili con Modbus RTU potevano passare a Modbus TCP con modifiche minime. Lo svantaggio: le lacune di sicurezza originarie sono state mantenute.
2. Struttura di un frame Modbus TCP — analisi byte per byte
Panoramica della 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 │
└──────────┴──────────┴──────────┴─────────┴────────────┴─────────────┘
Analisi dell'intestazione MBAP
Identificatore della transazione (2 byte): Numero libero scelto dal cliente, restituito identico nella risposta. Consente di correlare le risposte alle richieste nel caso di connessioni persistenti con più richieste in corso. In pratica, la maggior parte dei client utilizza un semplice contatore (00 01, 00 02, 00 03...) o un identificatore fisso (00 00) se opera in modalità richiesta/risposta sequenziale.
Identificatore del protocollo (2 byte):
Sempre 00 00 per Modbus. Questa costante è stata definita per consentire un’eventuale estensione ad altri protocolli tramite la stessa porta, ma non è mai stata utilizzata.
Lunghezza (2 byte):
Numero di byte successivi, in formato big-endian. Include l'ID unità e la PDU, ma non i primi 4 byte dell'intestazione MBAP. Per una richiesta FC=03 di 10 registri: 6 byte (1 ID unità + 1 FC + 2 indirizzi + 2 conteggi) → 00 06.
Identificatore unità (1 byte): Identificatore dello slave Modbus. Nei PLC collegati direttamente (S7-1200 con MB_SERVER, M340 con server Modbus integrato), il valore è in genere 1 o 255. Sui gateway Modbus RTU→TCP, questo identificatore corrisponde all'indirizzo dello slave RTU sul bus RS-485.
Analisi di una richiesta FC=03 completa
Lettura di 10 registri di controllo a partire dall'indirizzo 100 (es.: %MW100 su 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]
Codici di eccezione (messaggi di errore)
Quando il sistema automatizzato non è in grado di elaborare una richiesta, restituisce un codice di eccezione:
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
| Codice | Nome | Significato | Causa comune |
|---|---|---|---|
| 01 | Funzione non consentita | FC non supportata | Il controllore non supporta questa FC |
| 02 | Indirizzo dati non valido | Indirizzo fuori intervallo | Registro > limite del controllore |
| 03 | Valore dati non valido | Valore dati non valido | Conteggio > 125 (FC=03) |
| 04 | Errore del dispositivo server | Errore interno | Server Modbus non inizializzato |
| 05 | Conferma | Elaborazione in corso | Raro nella pratica |
| 06 | Dispositivo server occupato | Troppe connessioni simultanee | |
| 0A | Gateway Path Unavailable | Percorso gateway non disponibile | Slave RTU non raggiungibile |
| 0B | Gateway Target Device Failed | Slave non rispondente | Slave RTU spento o guasto |
3. I codici funzione in dettaglio
FC=01 e FC=02 — Lettura dei bit
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 — Lettura dei registri di mantenimento (il più utilizzato)
Lettura e scrittura di registri a 16 bit. Limite: massimo 125 registri per richiesta.
FC=04 — Lettura dei registri di ingresso
Identico a FC=03, ma per i registri "di ingresso" (sola lettura). Su Schneider M340, corrisponde agli %IW. Sulla maggior parte dei controllori logici, è poco utilizzato poiché i valori analogici vengono copiati negli %MW (Holding Registers).
FC=06 e FC=16 — Scrittura nei registri
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]
Attenzione durante la scrittura (FC=06, FC=16): scrivere nei registri solo se si è certi dell'indirizzamento e dell'impatto sul programma del PLC. Una scrittura su un registro di setpoint modifica immediatamente il comportamento del processo. Effettuare sempre dei test su un PLC di sviluppo o fuori produzione prima della messa in produzione.
4. Modbus TCP vs OPC-UA vs MQTT: quale protocollo scegliere per la vostra architettura?
Questi tre protocolli coesistono nelle moderne architetture IIoT. Servono livelli diversi e non sono in diretta concorrenza tra loro.
| Criterio | Modbus TCP | OPC-UA | MQTT |
|---|---|---|---|
| Anno di introduzione | 1979 (RTU), ~1996 (TCP) | 2008 | 1999 |
| Modello | Client/Server (polling) | Client/Server + Pub/Sub | Pub/Sub (broker) |
| Livello | Comunicazione sul campo | Interoperabilità dei sistemi | Trasporto IoT/cloud |
| Sicurezza nativa | Nessuna | TLS + autenticazione tramite certificato | TLS + autenticazione |
| Carico CPU del PLC | Molto basso | Elevato | Da basso a medio |
| PLC legacy | Universale (40 anni) | Solo PLC moderni | Tramite gateway |
| Rilevamento automatico | No | Sì (namespace OPC) | No |
| Dati strutturati | No (registri piatti) | Sì (tipi, strutture, array) | Sì (JSON/CBOR) |
| Latenza | Bassa (polling) | Molto bassa (pub/sub) | Molto bassa |
Come scegliere?
Utilizzare Modbus TCP se:
- Il PLC supporta Modbus TCP (Siemens S7-1200, Schneider M340/M221/M241, Wago, Allen-Bradley...)
- È necessario acquisire misure semplici (registri a 16 bit)
- La semplicità di configurazione ha la precedenza sulla ricchezza del modello di dati
Utilizzare OPC-UA se:
- Il PLC supporta nativamente OPC-UA server (S7-1500, B&R, Beckhoff, CODESYS 3.x recente)
- È necessario monitorare variabili complesse (strutture, array, tipi personalizzati)
- L'interoperabilità con altri sistemi SCADA o MES è un requisito fondamentale
Utilizzate MQTT per il trasporto dati nel cloud se:
- Disponete di un gateway (come Eziwan) che raccoglie dati in Modbus/OPC-UA e li pubblica in MQTT
- Avete bisogno di un'architettura pub/sub scalabile per migliaia di punti
L'architettura di destinazione per l'IIoT nel cloud:
[Automate] ──Modbus TCP / OPC-UA──→ [Gateway] ──MQTT over TLS──→ [Cloud]
Il gateway converte e trasmette i dati. Funge da ponte tra il mondo OT (Modbus/OPC-UA) e il mondo cloud (MQTT/REST).
5. Sicurezza Modbus TCP: il problema e le soluzioni
La totale assenza di autenticazione
Modbus TCP non dispone di alcun meccanismo di sicurezza nativo:
- Nessuna autenticazione: chiunque sulla rete può leggere e scrivere
- Nessuna crittografia: i dati sono in chiaro (leggibili con Wireshark)
- Nessuna autorizzazione: non esiste il concetto di diritti di lettura/scrittura per utente
Conseguenza pratica: se il vostro controllore è accessibile da Internet (porta 502 aperta), è completamente vulnerabile. Se qualcuno si connette alla vostra rete OT (fisicamente o tramite una falla nella sicurezza), può leggere e modificare tutti i registri.
Le possibili risposte
Segmentazione della rete (misura primaria): Limitare il traffico Modbus TCP a una rete OT isolata. Il gateway Eziwan si trova in questa rete OT; la comunicazione con il cloud avviene tramite un tunnel VPN, non tramite l'esposizione della porta 502.
Filtraggio IP sul PLC (misura secondaria): Alcuni PLC di nuova generazione (Schneider M340 con firmware ≥ 2.60, Siemens S7-1200 con firmware V4.x e funzioni FIREWALL) supportano il filtraggio IP: solo l’indirizzo IP del gateway Eziwan è autorizzato a connettersi alla porta 502.
Modbus Security (misura avanzata): Nel 2018 la Modbus Organization ha pubblicato un'estensione denominata "Modbus Security" (trasporto TLS per Modbus TCP) sulla porta 802. Questa estensione non è ancora ampiamente supportata dai controllori presenti sul mercato, ma è disponibile su alcune apparecchiature di recente produzione.
6. Ottimizzazione delle prestazioni di raccolta
Raggruppamento delle richieste (batching)
La regola d'oro: raggruppare le letture in blocchi contigui. Leggere 100 registri contigui in un'unica query è molto più efficiente che eseguire 100 query, ciascuna su un singolo registro:
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
Il gateway Eziwan raggruppa automaticamente le variabili configurate in intervalli contigui. Per ottimizzare questo raggruppamento, organizzare le variabili di monitoraggio in aree di memoria adiacenti.
Impatto sul ciclo del controllore
Ogni richiesta Modbus TCP ricevuta viene elaborata dal PLC al termine del ciclo di programma. Su un M340 con un ciclo di 10 ms, 10 richieste Modbus simultanee possono allungare il ciclo da 1 a 3 ms.
Raccomandazione: non superare 1 richiesta Modbus per ciclo del PLC nelle applicazioni con requisiti in tempo reale. Ridurre la frequenza di polling se il ciclo si allunga.
7. Esempio di codice Python completo 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()
Questo codice illustra le migliori pratiche per l'acquisizione di dati Modbus TCP in Python: raggruppamento delle letture, gestione delle eccezioni, riconnnessione automatica e decodifica di tipi complessi. In produzione, il gateway Eziwan gestisce tutte queste funzioni in modo nativo, senza richiedere lo script Python in loco.
Domande frequenti — Protocollo Modbus TCP/IP
Perché la porta Modbus TCP è la 502 e non una porta standard come la 80 o la 443?
La porta 502 è stata ufficialmente assegnata a Modbus dall'IANA (Internet Assigned Numbers Authority) in occasione della standardizzazione di Modbus TCP negli anni '90. All’epoca non vi era motivo di riutilizzare le porte applicative IT esistenti. Si tratta di una porta registrata (< 1024), pertanto il suo utilizzo come server richiede i privilegi di root su Linux — cosa che i firmware industriali gestiscono in modo nativo.
Qual è il limite massimo di registri leggibili in una singola richiesta FC=03?
Il limite è di 125 registri per richiesta FC=03 (Read Holding Registers), come definito nella specifica ufficiale Modbus. Ciò corrisponde a un massimo di 250 byte di dati utili per richiesta. Per FC=16 (Write Multiple Registers), il limite è di 123 registri per richiesta. Il gateway Eziwan gestisce automaticamente la suddivisione se un intervallo richiesto supera tali limiti.
Modbus TCP supporta più connessioni client simultanee?
Sì, ma è il PLC a definire il limite. Lo Schneider M340 accetta fino a 16 connessioni simultanee, il Siemens S7-1200 fino a 8 (tramite MB_SERVER). Oltre questo limite, il PLC restituisce un'eccezione 06 (Server Busy). In pratica, un gateway di raccolta dati + uno strumento di diagnostica + un sistema SCADA locale raggiungono facilmente le 3 connessioni simultanee — da tenere sotto controllo.
È possibile crittografare il protocollo Modbus TCP?
Lo standard Modbus TCP nativo (porta 502) non è crittografato. Dal 2018 esiste un’estensione “Modbus Security” sulla porta 802 con TLS, ma è raramente supportata dai controllori attuali. La soluzione pratica consiste nel confinare Modbus TCP in una rete OT isolata e far passare tutte le comunicazioni verso l’esterno tramite un tunnel VPN crittografato (come OpenVPN di Eziwan) — la crittografia è garantita dal livello di trasporto, non da Modbus stesso.
Pymodbus è l'unica libreria Python per Modbus TCP?
No. Le alternative principali sono minimalmodbus (più semplice per Modbus RTU), umodbus (leggero, con un basso consumo di memoria) e pyModbusTCP (puramente TCP, senza dipendenze). Per i progetti industriali in Python, pymodbus >= 3.0 rimane la scelta più completa grazie alla gestione delle eccezioni, alla riconnessione e al supporto di entrambe le modalità (RTU e TCP).
Come eseguire il debug di una comunicazione Modbus TCP senza poter accedere al PLC?
Utilizzare Modbus Slave (Windows) come simulatore di PLC locale, oppure diagslave (riga di comando, multipiattaforma). Questi strumenti simulano un server Modbus TCP che risponde a indirizzi configurabili. Ideale per testare una configurazione di raccolta dati senza il rischio di interferire con un PLC in produzione.
Il vostro controllore utilizza il protocollo Modbus TCP, ma non siete sicuri che sia compatibile con la vostra versione del firmware?
Verifica la compatibilità del tuo controllore →
Vedi anche: Modbus TCP con uno Schneider M340 e la documentazione Siemens S7 per le configurazioni specifiche.