Protocole Modbus TCP/IP : tout comprendre pour connecter vos automates au cloud
Modbus TCP est le protocole de collecte de données automate le plus répandu dans l'industrie française. Il équipe des dizaines de millions d'équipements installés depuis plus de 40 ans. Si vous intégrez la connectivité distante dans vos projets, vous allez rencontrer Modbus TCP — autant le comprendre en profondeur.
Ce guide couvre l'essentiel pour un praticien : histoire, structure de trame byte par byte, Function Codes, comparatif avec les protocoles concurrents, optimisation des performances, sécurité, et exemple de code Python complet et fonctionnel.
1. De Modbus RTU à Modbus TCP : 45 ans d'évolution
La naissance de Modbus en 1979
Modbus a été créé en 1979 par Modicon (aujourd'hui absorbé par Schneider Electric) pour faire communiquer les premiers automates programmables Modicon 084. C'était un protocole série (RS-232 puis RS-485), maître/esclave, conçu pour une chose unique : lire et écrire des valeurs numériques sur des équipements de terrain.
La simplicité était le principe fondamental de conception :
- Pas d'adressage réseau — juste un identifiant esclave de 1 à 247 sur le bus
- Pas d'authentification — n'importe quel maître peut interroger n'importe quel esclave
- Pas de découverte automatique — la liste des esclaves est configurée manuellement
- Pas de mécanisme de sécurité — le protocole suppose un environnement physiquement sécurisé
Modbus RTU (Remote Terminal Unit) : version binaire compacte pour RS-485. Les données sont transmises en binaire, avec un CRC 16 bits pour la détection d'erreurs. Toujours très répandu sur les instruments de terrain (compteurs d'énergie, capteurs de pression, variateurs de fréquence).
Modbus ASCII : version lisible en ASCII, pour les liaisons RS-232. Moins efficace (2 octets par byte de données), obsolète dans les nouvelles installations.
Modbus TCP : Ethernet sans révolution du protocole
Vers 1996-1999, avec la généralisation d'Ethernet dans les usines, Modbus TCP a encapsulé le protocole dans des trames TCP/IP. La révision a été chirurgicale :
- Même modèle de données (coils, discrete inputs, input registers, holding registers)
- Mêmes Function Codes
- Même adressage des registres
- Suppression du CRC (TCP garantit l'intégrité)
- Ajout d'un MBAP Header de 7 octets pour le routage et la corrélation des requêtes
- Port TCP standard : 502
L'avantage : tout équipement existant connaissant Modbus RTU pouvait migrer vers Modbus TCP avec des changements minimes. L'inconvénient : les lacunes de sécurité originales ont été conservées.
2. Structure d'une trame Modbus TCP — analyse byte par byte
Vue d'ensemble de la trame
┌─────────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────┴──────────┴──────────┴─────────┴────────────┴─────────────┘
Dissection du MBAP Header
Transaction Identifier (2 octets) : Numéro libre choisi par le client, retourné à l'identique dans la réponse. Permet de corréler les réponses aux requêtes dans le cas de connexions persistantes avec plusieurs requêtes en vol. En pratique, la plupart des clients utilisent un compteur simple (00 01, 00 02, 00 03...) ou un identifiant fixe (00 00) s'ils fonctionnent en mode requête/réponse séquentiel.
Protocol Identifier (2 octets) :
Toujours 00 00 pour Modbus. Cette constante existe pour permettre une éventuelle extension à d'autres protocoles via le même port, mais n'a jamais été utilisée.
Length (2 octets) :
Nombre d'octets qui suivent, en big-endian. Inclut le Unit ID et le PDU, mais pas les 4 premiers octets du MBAP Header. Pour une requête FC=03 de 10 registres : 6 octets (1 Unit ID + 1 FC + 2 adresse + 2 count) → 00 06.
Unit Identifier (1 octet) : Identifiant de l'esclave Modbus. Sur les automates directement connectés (S7-1200 avec MB_SERVER, M340 avec serveur Modbus intégré), la valeur est typiquement 1 ou 255. Sur les passerelles Modbus RTU→TCP, cet identifiant correspond à l'adresse de l'esclave RTU sur le bus RS-485.
Analyse d'une requête FC=03 complète
Lecture de 10 registres holding à partir de l'adresse 100 (ex: %MW100 sur 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]
Codes d'exception (réponses d'erreur)
Quand l'automate ne peut pas traiter une requête, il renvoie un code d'exception :
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
| Code | Nom | Signification | Cause courante |
|---|---|---|---|
| 01 | Illegal Function | FC non supporté | Automate ne supporte pas cette FC |
| 02 | Illegal Data Address | Adresse hors plage | Registre > limite automate |
| 03 | Illegal Data Value | Valeur de données invalide | Count > 125 (FC=03) |
| 04 | Server Device Failure | Erreur interne | Serveur Modbus pas initialisé |
| 05 | Acknowledge | Traitement long en cours | Rare en pratique |
| 06 | Server Device Busy | Serveur occupé | Trop de connexions simultanées |
| 0A | Gateway Path Unavailable | Chemin passerelle indisponible | Esclave RTU non joignable |
| 0B | Gateway Target Device Failed | Esclave non répondant | Esclave RTU éteint ou en panne |
3. Les Function Codes en détail
FC=01 et FC=02 — Lecture 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 — Read Holding Registers (le plus utilisé)
Lecture de registres de 16 bits en lecture/écriture. Limite : 125 registres maximum par requête.
FC=04 — Read Input Registers
Identique à FC=03 mais pour les registres "entrée" (lecture seule). Sur Schneider M340, correspond aux %IW. Sur la plupart des automates, peu utilisé car les mesures analogiques sont copiées dans des %MW (Holding Registers).
FC=06 et FC=16 — Écriture de registres
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]
Attention en écriture (FC=06, FC=16) : n'écrire des registres que si vous êtes certain de l'adressage et de l'impact sur le programme automate. Une écriture sur un registre de consigne modifie immédiatement le comportement du process. Tester toujours sur un automate de développement ou hors production avant déploiement.
4. Modbus TCP vs OPC-UA vs MQTT : quel protocole pour votre architecture ?
Ces trois protocoles coexistent dans les architectures IIoT modernes. Ils servent des couches différentes et ne sont pas en compétition directe.
| Critère | Modbus TCP | OPC-UA | MQTT |
|---|---|---|---|
| Ancienneté | 1979 (RTU), ~1996 (TCP) | 2008 | 1999 |
| Modèle | Client/Serveur (polling) | Client/Serveur + Pub/Sub | Pub/Sub (broker) |
| Couche | Communication terrain | Interopérabilité systèmes | Transport IoT/cloud |
| Sécurité native | Aucune | TLS + authentification certificat | TLS + authentification |
| Charge CPU automate | Très faible | Élevée | Faible à moyenne |
| Automates anciens | Universel (40 ans) | Automates modernes seulement | Via gateway |
| Découverte automatique | Non | Oui (namespace OPC) | Non |
| Données structurées | Non (registres plats) | Oui (types, structs, arrays) | Oui (JSON/CBOR) |
| Latence | Faible (polling) | Très faible (pub/sub) | Très faible |
Comment choisir ?
Utilisez Modbus TCP si :
- L'automate supporte Modbus TCP (Siemens S7-1200, Schneider M340/M221/M241, Wago, Allen-Bradley...)
- Vous avez besoin de collecter des mesures simples (registres 16 bits)
- La simplicité de configuration prime sur la richesse du modèle de données
Utilisez OPC-UA si :
- L'automate supporte nativement OPC-UA serveur (S7-1500, B&R, Beckhoff, CODESYS 3.x récent)
- Vous devez superviser des variables complexes (structures, tableaux, types personnalisés)
- L'interopérabilité avec d'autres systèmes SCADA ou MES est un prérequis
Utilisez MQTT pour le transport cloud si :
- Vous avez une gateway (comme Eziwan) qui collecte en Modbus/OPC-UA et publie en MQTT
- Vous avez besoin d'une architecture pub/sub scalable pour des milliers de points
L'architecture cible pour l'IIoT cloud :
[Automate] ──Modbus TCP / OPC-UA──→ [Gateway] ──MQTT over TLS──→ [Cloud]
La gateway convertit et transporte. Elle fait le pont entre le monde OT (Modbus/OPC-UA) et le monde cloud (MQTT/REST).
5. Sécurité Modbus TCP : le problème et les réponses
L'absence totale d'authentification
Modbus TCP n'a aucun mécanisme de sécurité natif :
- Pas d'authentification : n'importe qui sur le réseau peut lire et écrire
- Pas de chiffrement : les données sont en clair (lisibles par Wireshark)
- Pas d'autorisation : pas de notion de droits lecture/écriture par utilisateur
Conséquence pratique : si votre automate est accessible depuis Internet (port 502 ouvert), il est totalement vulnérable. Si quelqu'un se connecte à votre réseau OT (physiquement ou via une brèche), il peut lire et modifier tous les registres.
Les réponses possibles
Segmentation réseau (mesure primaire) : Confiner le trafic Modbus TCP à un réseau OT isolé. La gateway Eziwan est dans ce réseau OT ; la communication vers le cloud passe par un tunnel VPN, pas par une exposition du port 502.
Filtrage IP sur l'automate (mesure secondaire) : Certains automates récents (Schneider M340 firmware ≥ 2.60, Siemens S7-1200 firmware V4.x avec fonctions FIREWALL) supportent un filtrage IP : seule l'adresse IP de la gateway Eziwan est autorisée à se connecter sur le port 502.
Modbus Security (mesure avancée) : La Modbus Organization a publié en 2018 une extension "Modbus Security" (transport TLS pour Modbus TCP) sur le port 802. Cette extension n'est pas encore largement supportée par les automates du marché, mais est disponible sur certains équipements récents.
6. Optimisation des performances de collecte
Regroupement des requêtes (batching)
La règle d'or : regrouper les lectures en plages contiguës. Lire 100 registres contigus en une requête est beaucoup plus efficace que 100 requêtes d'un registre chacune :
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 gateway Eziwan groupe automatiquement les variables configurées en plages contiguës. Pour maximiser ce grouping, organiser vos variables de supervision dans des zones mémoire adjacentes.
Impact sur le cycle automate
Chaque requête Modbus TCP reçue est traitée par l'automate en fin de cycle programme. Sur un M340 avec un cycle de 10 ms, 10 requêtes Modbus simultanées peuvent allonger le cycle de 1 à 3 ms.
Recommandation : ne pas dépasser 1 requête Modbus par cycle automate pour les applications avec contrainte temps-réel. Réduire la fréquence de polling si le cycle s'allonge.
7. Exemple de code Python complet avec 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()
Ce code illustre les bonnes pratiques pour une collecte Modbus TCP en Python : regroupement des lectures, gestion des exceptions, reconnexion automatique, et décodage des types complexes. En production, la gateway Eziwan gère toutes ces fonctions nativement — sans nécessiter de script Python sur site.
FAQ — Protocole Modbus TCP/IP
Pourquoi le port Modbus TCP est-il 502 et non un port standard comme 80 ou 443 ?
Le port 502 a été officiellement assigné à Modbus par l'IANA (Internet Assigned Numbers Authority) lors de la standardisation de Modbus TCP dans les années 1990. Il n'y avait alors pas de raison de réutiliser des ports applicatifs IT existants. C'est un port enregistré (< 1024), donc son usage en serveur nécessite des droits root sur Linux — ce que les firmwares industriels gèrent nativement.
Quelle est la limite de registres lisibles en une seule requête FC=03 ?
La limite est de 125 registres par requête FC=03 (Read Holding Registers), définie dans la spécification Modbus officielle. Soit 250 octets de données utiles maximum par requête. Pour FC=16 (Write Multiple Registers), la limite est de 123 registres par requête. La gateway Eziwan gère automatiquement le découpage si une plage demandée dépasse ces limites.
Modbus TCP supporte-t-il plusieurs connexions client simultanées ?
Oui, mais c'est l'automate qui définit la limite. Le Schneider M340 accepte jusqu'à 16 connexions simultanées, le Siemens S7-1200 jusqu'à 8 (via MB_SERVER). Au-delà, l'automate renvoie une exception 06 (Server Busy). En pratique, une gateway de collecte + un outil de diagnostic + un SCADA local atteignent facilement 3 connexions simultanées — à surveiller.
Modbus TCP peut-il être chiffré ?
Le standard Modbus TCP natif (port 502) n'est pas chiffré. Une extension "Modbus Security" sur port 802 avec TLS existe depuis 2018, mais est rarement supportée par les automates actuels. La réponse pratique est de confiner Modbus TCP à un réseau OT isolé et de faire passer toute communication vers l'extérieur via un tunnel VPN chiffré (comme OpenVPN d'Eziwan) — le chiffrement est assuré par la couche de transport, pas par Modbus lui-même.
Est-ce que pymodbus est la seule bibliothèque Python pour Modbus TCP ?
Non. Les alternatives principales sont minimalmodbus (plus simple pour Modbus RTU), umodbus (léger, faible empreinte mémoire), et pyModbusTCP (purement TCP, sans dépendances). Pour des projets industriels en Python, pymodbus >= 3.0 reste le choix le plus complet avec gestion des exceptions, reconnexion et support des deux modes (RTU et TCP).
Comment déboguer un échange Modbus TCP sans accès à l'automate ?
Utiliser Modbus Slave (Windows) comme simulateur d'automate local, ou diagslave (ligne de commande, multiplateforme). Ces outils simulent un serveur Modbus TCP répondant à des adresses configurables. Idéal pour tester une configuration de collecte sans risquer de toucher un automate en production.
Votre automate parle Modbus TCP mais vous n'êtes pas sûr de la compatibilité avec votre version firmware ?
Tester la compatibilité de votre automate →
Voir aussi : Modbus TCP avec un Schneider M340 et la documentation Siemens S7 pour les configurations spécifiques.