Cet article étant quelque peu long, il a été découpé en plusieurs pages, vous pouvez les lire l'une après l'autre ou les consulter dans leur intégralité
Introduction
Afin de réaliser le communication Modbus depuis un automate WAGO (type PFC100/200), nous avons trois grandes solutions :
- Utiliser le configurateur Modbus présent dans le logiciel e!COCKPIT
- Utiliser les bibliothèques WAGO contenant les blocs fonctionnels permettant la gestion d'une communication Modbus
- Tout redévelopper les communications
Nous ne donnerons ici aucune explication sur la troisième possibilité, car ce n'est pas le but de cet article.
La première solution sera écartée de cet article, car, même si, de prime abord, elle peut sembler facilement utilisable et rapide à mettre en service, elle comporte, néanmoins, de nombreux points qui nous empêchent de l'utiliser. Parmi eux, nous ne citerons que ceux-ci :
- Adresse IP / Adresse Esclave non modifiable par le programme. Nous nous retrouvons donc dans une solution figée pour le projet qui ne peut plus être portable sur une autre application presque identique.
- Diagnostic de la communication très léger.
- Requêtes impossibles à modifier depuis le programme.
- Variables crées directement dans le programme, difficulté d'utiliser des tableaux de variables.
Ces points peuvent ne pas déranger suivant les applications. Mais dans notre cas et notre manière d'organiser nos programmes, ils le sont (voir les articles Conception d'un programme automate (1/3),(2/3),(3/3)).
Nous utiliserons donc les bibliothèques Modbus fournies par WAGO avec le logiciel e!COCKPIT
Les bibliothèques
Où plutôt, la bibliothèque, car il n'y en a qu'une seule.
Cette bibliothèque est, lors de la rédaction de cet article, à sa version 1.1.3.0 et elle comporte les blocs fonctionnels nécessaires à la gestion d'une communication Modbus, que ce soit en mode série ou en mode Ethernet.
Coté Master
Les deux blocs fonctionnels à utiliser seront donc :
- Pour une communication série
- Pour une communication Ethernet TCP
En dehors de tous les paramètres qui sont liés à la communication, nous pouvons remarquer que les deux blocs fonctionnels travaillent avec les mêmes informations, à savoir :
- xTrigger : déclenchement de l'envoi de la requête (cette variable est mise à FALSE par le bloc lui-même lorsque la requête a été exécutée)
- utQuery : une structure contenant les informations de la requête.
- utResponse : une structure contenant les informations renvoyées par l'esclave / serveur
En travaillant avec ces deux blocs fonctionnels, nous pourrons donc arriver à réaliser un programme gérant une communication modbus. Cette communication sera rapidement modifiable au niveau de la couche physique puisqu'il suffira de changer de bloc fonctionnel, sans toucher ni à la génération des requêtes ni au traitement des réponses.
Coté Slave
Les deux blocs fonctionnels à utiliser seront donc :
- Pour une communication série
- Pour une communication Ethernet TCP
Les mêmes constatations que pour les blocs Master peuvent être faites.
En dehors des informations liées à la connexion, les deux blocs fonctionnels travaillent avec 4 tableaux :
Ces quatre tableaux contiennent les informations qui seront utilisées pour les échanges de données et utilisés en fonction des codes fonctions reçus par le Master :
No | Description | Data Type | Access | Access by Functioncodes |
---|---|---|---|---|
1 | Discrete Input Area | ARRAY[..] OF BOOL | Read only | FC02 |
2 | Discrete Output Area | ARRAY[..] OF BOOL | Read / Write | FC01 FC05 FC15 |
3 | Input Register Area | ARRAY[..] OF WORD | Read only | FC04 |
4 | Holding Register Area | ARRAY[..] OF WORD | Read / Write | FC03 FC06 FC16 FC22 FC23 |
Les bornes mini et maxi des tableaux donneront les adresses mini et maxi des registres modbus utilisables.
Par exemple, un tableau défini pour les Holding Register Area allant de 150 à 7500 défini comme suit :
VAR
awHoldingRegisters:ARRAY[150..7500] OF WORD;
END_VAR
permettra un accès en lecture / écriture aux adresses des registres modbus allant de 150 à 7500.
Mise en place
Pour des raisons de simplification au niveau des tests, la communication modbus sera réalisée en TCP. Nous utiliserons donc les blocs fonctionnels TCP de la bibliothèque WagoAppPlcModbus.
Nous allons dans un premier temps créer l'arborescence de notre projet de la manière suivante :
Définition des variables et renseignement du bloc coté esclave :
PROGRAM PLC_MODBUS_SLAVEVAR
FbSlave:WagoAppPlcModbus.FbMbSimpleServerTcp;
xOpen: BOOL;
wPort: WORD;
utKeepAlive: WagoAppPlcModbus.typKeepAlive;
bUnitId: BYTE;
IIdentifyObject: WagoAppPlcModbus.I_IdentifyBaseObject;
axDiscreteInputs:ARRAY[1..10] OF BOOL;
axCoils:ARRAY[1..10] OF BOOL;
awInputRegisters:ARRAY[1..10] OF WORD;
awHoldingRegisters:ARRAY[1..1000] OF WORD;
END_VAR
FbSlave(
xOpen:= xOpen,
wPort:=wPort ,
utKeepAlive:=utKeepAlive ,
xIsOpen=> ,
xError=> ,
oStatus=> ,
udiConnectedClients=> ,
bUnitId:= bUnitId,
IIdentifyObject:= IIdentifyObject,
axDiscreteInputs:= axDiscreteInputs,
axCoils:= axCoils,
awInputRegisters:= awInputRegisters,
awHoldingRegisters:=awHoldingRegisters ,
oMbAccessInfo=> );
Définition des variables et renseignement du bloc coté maitre :
PROGRAM PLC_MODBUS_MASTER
VAR
fbMaster:WagoAppPlcModbus.FbMbMasterTcp;
xConnect: BOOL;
sHost: STRING;
wPort: WORD;
utKeepAlive: WagoAppPlcModbus.typKeepAlive;
eFrameType: WagoAppPlcModbus.eMbFrameType;
tTimeOut: TIME;
utQuery: WagoAppPlcModbus.typMbQuery;
xTrigger: BOOL;
utResponse: WagoAppPlcModbus.typMbResponse;
END_VAR
fbMaster(
xConnect:= xConnect,
sHost:= sHost,
wPort:=wPort ,
utKeepAlive:=utKeepAlive ,
eFrameType:=eFrameType ,
tTimeOut:= tTimeOut,
utQuery:= utQuery,
xTrigger:= xTrigger,
utResponse:=utResponse ,
xIsOpen=> ,
xError=> ,
oStatus=> );
Modification des déclarations de variables coté slave
Afin de pouvoir lire et écrire depuis notre programme dans les données d'échange modbus, les tableaux de variables d'échange vont être déplacés dans la liste de variable globale modbus.
Le nouveau code prend donc la forme suivante :
GVL_MODBUS
VAR_GLOBAL
axDiscreteInputs:ARRAY[1..10] OF BOOL;
axCoils:ARRAY[1..10] OF BOOL;
awInputRegisters:ARRAY[1..10] OF WORD;
awHoldingRegisters:ARRAY[1..1000] OF WORD;
END_VAR
PROGRAM PLC_MODBUS_SLAVE
VAR
FbSlave:WagoAppPlcModbus.FbMbSimpleServerTcp;
xOpen: BOOL;
wPort: WORD;
utKeepAlive: WagoAppPlcModbus.typKeepAlive;
bUnitId: BYTE;
IIdentifyObject: WagoAppPlcModbus.I_IdentifyBaseObject;
END_VAR
FbSlave(
xOpen:= xOpen,
wPort:=wPort ,
utKeepAlive:=utKeepAlive ,
xIsOpen=> ,
xError=> ,
oStatus=> ,
udiConnectedClients=> ,
bUnitId:= bUnitId,
IIdentifyObject:= IIdentifyObject,
axDiscreteInputs:= gvl_modbus.axDiscreteInputs,
axCoils:= gvl_modbus.axCoils,
awInputRegisters:= gvl_modbus.awInputRegisters,
awHoldingRegisters:=gvl_modbus.awHoldingRegisters ,
oMbAccessInfo=> )
;
Initialisation des variables coté slave
Nous allons créer une action afin d'initialiser certaines variables, cette action sera appelée au premier tour de cycle du programme. Il faudra donc aussi créer une variable qui stocke cette information.
Définition de la variable xinit :
xinit:BOOL;
Définition du code appelant l'action init :
IF NOT xinit THEN
ACT_INIT();
xinit:=TRUE;
END_IF
Action ACT_INIT:
xOpen:=TRUE;
wPort:=502;
utKeepAlive.xEnable:=FALSE;
bUnitId:=1;
Modification des déclarations de variables coté master
Afin de pouvoir lire et écrire depuis notre programme dans les données utilisées par le bloc maitre, les informations concernant les requêtes et les réponses des esclaves vont être déplacées dans la liste de variable globale modbus.
Le nouveau code prend donc la forme suivante :
GVL_MODBUS
VAR_GLOBAL
axDiscreteInputs:ARRAY[1..10] OF BOOL;
axCoils:ARRAY[1..10] OF BOOL;
awInputRegisters:ARRAY[1..10] OF WORD;
awHoldingRegisters:ARRAY[1..1000] OF WORD;
utQuery: WagoAppPlcModbus.typMbQuery;
utResponse: WagoAppPlcModbus.typMbResponse;
END_VAR
PROGRAM PLC_MODBUS_MASTER
VAR
fbMaster:WagoAppPlcModbus.FbMbMasterTcp;
xConnect: BOOL;
sHost: STRING;
wPort: WORD;
utKeepAlive: WagoAppPlcModbus.typKeepAlive;
eFrameType: WagoAppPlcModbus.eMbFrameType;
tTimeOut: TIME;
xTrigger: BOOL;
END_VAR
fbMaster(
xConnect:= xConnect,
sHost:= sHost,
wPort:=wPort ,
utKeepAlive:=utKeepAlive ,
eFrameType:=eFrameType ,
tTimeOut:= tTimeOut,
utQuery:= GVL_MODBUS.utQuery,
xTrigger:= xTrigger,
utResponse:=GVL_MODBUS.utResponse ,
xIsOpen=> ,
xError=> ,
oStatus=> );
Initialisation des variables coté master
Nous allons créer une action afin d'initialiser certaines variables, cette action sera appelée au premier tour de cycle du programme. Il faudra donc aussi créer une variable qui stocke cette information.
Définition de la variable xinit :
xinit:BOOL;
Définition du code appelant l'action init :
IF NOT xinit THEN
ACT_INIT();
xinit:=TRUE;
END_IF
Action ACT_INIT:
xConnect:=TRUE;
sHost:='192.168.1.102'; // Adresse IP de l'esclave (ici l'adresse IP de notre PFC)
wPort:=502;
utKeepAlive.xEnable:=FALSE;
eFrameType:=2;
tTimeOut:=T#100MS;
Définition des requêtes
Pour notre exemple, nous allons réaliser, dans un premier temps, une requête de lecture.
Cette requête aura donc pour code fonction 03 et l'adresse du premier registre à lire sera le registre 10 et la quantité à lire 30.
Modification de l'action ACT_INIT pour intégrer la définition de notre requête.
xConnect:=TRUE;
sHost:='192.168.1.102';
wPort:=502;
utKeepAlive.xEnable:=FALSE;
eFrameType:=2;
tTimeOut:=T#100MS;
GVL_MODBUS.utQuery.bUnitId:=1;
GVL_MODBUS.utQuery.bFunctionCode:=03;
GVL_MODBUS.utQuery.uiReadAddress:=10;
GVL_MODBUS.utQuery.uiReadQuantity:=30;
Création des taches pour appel des programmes Modbus
Afin d'appeler les deux programmes que nous avons crées, nous allons définir deux taches d'appel.
Création de la tâche pour le programme Master
Création de la tâche pour le programme Slave
Nous allons paramétrer ces tâches pour qu'elles évoluent toutes les 25ms avec une priorité de 14 (voir le tableau des priorités de tâches dans l'article suivant) et qu'elles appellent nos programmes PLC_MODBUS_MASTER et PLC_MODBUS_SLAVE.
Une fois nos deux tâches créées, nous allons pouvoir tester notre programme.