Modbus

Emploi de la librarie de communication TCP/IP Linux<->PLC

 

Attention, sur les CX8090, il faut acheter la licence lors de l'achat (plus simple)

Les sources sous http://libmodbus.org/download

Download de la dernière version 3.1.1 sous ~weber/src/modbus

Instalation: unzip, tar xvf, cd libmodbus-3.1.1, ./configure, make et sous root: make install

tout va sous

  • /usr/local/lib (libmodbus.so, libmodbus.la?, pgkconfig?)
  • /usr/local/include/modbus (modbus.h et modbus-*.h)
  • /usr/local/include/libmodbus (modbus.h (identique à modbus/modbus.h))

 

Mode d'emploi sous http://libmodbus.org/site_media/html/libmodbus.html

Le synopsis est:

#include <modbus.h>

cc `pkg-config --cflags --libs libmodbus` files

en gros inclure /usr/local/lib dans le LD_LIBRARY_PATH (rem: ce n'était pas le cas et j'ai modifié nos scripts de démarrage sous /etc/envv.d/40t120.[c]sh et sous ~weber/src/administration/t4_beta.csh etc...

La libraire fonctionne parfaitement bien, je l'ai également installée sur les LCU car il tournent en 32bits

 

ZONE MEMOIRE

Comme exemple: ~weber/src/spectro_src/libspePLC.c ou ~weber/src/modbus

La zone mémoire accessible en R/W est située en 0x3000 pour le CX8090 (pas facile de la trouver en suivant la doc Beckhoff), et en 0x4000 sur le BC9120 (doc Greg). Donc prévoir une variable pour la baseMemory si un programme doit etre portable.

Mefiance: la mémoire est persistante même au changement de projet. Il est donc préférable de faire  un Reset ou Reset All pour ces tests et pour les variable "Forced" un un Release Force.

Egalement les valeurs d'intialisation (lors de la déclaration des variables) n'est mise en place qu'après un Reset ainsi si l'on souhaite de nouvelles valeurs d'initialisation, il faut absolument faire un Reset.

 

ALIGNEMENT MEMOIRE

Sur la PLC les adresses sont comptées en BYTE et par exemple l'adresse en %Mx0 (avec x==X,B,W,D) équivaut à l'adresse 0x3000 (voir remarques plus haut).

La librarie modus travaille en adresse WORD (16bits), ainsi les adresses PLC impaires ne sont pas accessibles depuis une machine distante.

La PLC compte en adresse byte: 1, 2, 3 ..

  • Les BYTES 8 bits s'alignent normalement sur ces adresses: %MB0, %MB1, %MB3, ...
  • Les WORDs 16 bits doivent s'aligner sur les adresses paires: %MW0, %MW2, %MW4, ...
  • Les DWORDS 32 bits doivent s'aligner sur les les adresses quadruples: %MD0, %MD4, %MD8, ...
  • Les LREAL 64 bits doivent s'aligner sur les adresses octuples : %MD0, %MD8, %MD16, ...

 

En cas de melange de type le compilateur laisse des zones mémoires inoccupées.

Remarque: les temps (ex: t#1s) sont codés sur 32bit non signé en [ms]

Le compilateur informe sur le désalignement mais pas sur l'overlapping. L'overlapping ne semble pas etre une erreur, plusieurs variables peuvent pointer sur le meme espace mémoire.

Le test de l'overlapping se fait avec: TC-PLC-Control->Project->Check->Overlapping memory Areas

Les exemples suivants fonctionnent sous Linux 32 et 64bits.

EXEMPLES

Exemple en C pour lire 10 word(16 bits)  mémoire:

[weber@argos1 modbus]$ more testzero.c
#include 
#include 

main(){

	modbus_t *mbTdf;
	struct timeval response_timeout;
	uint16_t tab_reg[64];
	int rc;
	int i;
	int setpoint;

	mbTdf = modbus_new_tcp("10.10.132.61", MODBUS_TCP_DEFAULT_PORT);
	if(mbTdf == NULL){
		fprintf(stderr, "modbus_new_tcp: can not create Context TCP/IP");
		exit(-1);
	}
	if (modbus_connect(mbTdf) == -1) {
		fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
		modbus_free(mbTdf);
		exit(-1);
	}	
	printf("Connected\n");
	modbus_set_debug(mbTdf, TRUE);

	printf("Offset  0x3000 --------READ--------------------\n");
	rc = modbus_read_registers(mbTdf, 0x3000, 10, tab_reg);
	if (rc == -1) {
    		fprintf(stderr, "%s\n", modbus_strerror(errno));
    		return -1;
	}
	for (i=0; i < rc; i++) {
    		printf("reg[%d]=%d (0x%X)\n", i, tab_reg[i], tab_reg[i]);
	}
	
	modbus_close(mbTdf);
	modbus_free(mbTdf);
	printf("Connection closed\n");
}

Exemple avec Boolean:

a AT %MB0 : BOOL;
b AT %MB1 : BOOL;
c AT %MB2 : BOOL;
d AT %MB3 : BOOL;

a := FALSE;
b := TRUE;
c := TRUE;
d := FALSE;

Donne:

reg[0]=256 (0x100)
reg[1]=1 (0x1)

 

Exemple avec des Byte. Attention, rappel, il faut faire un Reset car il sont intialisés au démarrage uniquement
a AT %MB0 : BYTE := 16#10;
b AT %MB1 : BYTE := 16#20;
c AT %MB2 : BYTE := 16#30;
d AT %MB3 : BYTE := 16#40;

Donne:

reg[0]=8208 (0x2010)
reg[1]=16432 (0x4030)

 

Exemple avec des Bytes et Word. Attention il faut faire un Reset:
Notons que les adresses impaires pour les WORD sont interdites.
a AT %MB0 : BYTE := 16#10;
b AT %MB1 : BYTE := 16#20;
c AT %MB2 : BYTE := 16#30;
d AT %MW4 : WORD := 16#DDFF;

Donne:

reg[0]=8208 (0x2010)
reg[1]=16432 (0x4030)  <-- il reste le 0x40 de l'exemple precedent
reg[2]=56831 (0xDDFF)

Reset et Reset All n'efface pas 0x40
Apres un On/Off de la PLC on a:

reg[0]=8208 (0x2010)
reg[1]=48 (0x30)
reg[2]=56831 (0xDDFF)

Un peu plus complexe avec des variables de type TIME

a AT %MB0 : BYTE := 16#10;
b AT %MB1 : BYTE := 16#20;
c AT %MB2 : BYTE := 16#30;
d AT %MW4 : WORD := 16#DDFF;
e AT %MD8 : TIME := t#1s;
f AT %MD12: TIME := t#23s456ms;
g AT %MD16: TIME := t#65s535ms;
h AT %MD20: TIME := t#65s536ms;
i AT %MW24: WORD := 16#AABB

Donne:

reg[0]=8208 (0x2010)
reg[1]=48 (0x30)         <== byte padding added
reg[2]=56831 (0xDDFF)

reg[3]=0 (0x0)           <== word padding added

reg[4]=1000 (0x3E8)      <== 1[s]000[ms]
reg[5]=0 (0x0)

reg[6]=23456 (0x5BA0)    <== 23[s]456[ms]
reg[7]=0 (0x0)

reg[8]=65535 (0xFFFF)    <== 65[s]535[ms]
reg[9]=0 (0x0)

reg[10]=0 (0x0)          <== 65[s]536[ms]
reg[11]=1 (0x1)

reg[12]=52445 (0xCCDD)

 

TRANSFERT PAR STRUCTURE EN C

 

L'idée est de récupérer un ensemble de valeurs en un seul appel.

Coté C il convient de créer une structure équivalente à la déclaration des variables sur la PLC.
ATTENTION: sur les machines 64 bits, les doubles doivent etre alignés sur une adresse divisible par 8. Les machines 32 bits elles ne sont pas sensible à cela (rem il y a surement une option du compilateur qui change ce comportement par défaut, mais le processeur est plus efficace ainsi).
Ainsi il convient lors du design de créer une zone mémoire avec des variables ayant un bon alignement. Il absolument nécessaire de créer des chaines de caractères  ayant une longueur multiple de 8. Dans l'idéal (pour minimiser les bugs) il faudrait placer dans l'ordre  (notation des dénominations "C|PLC"):
  1. les chaines de caractères: char|STRING (longueur == multiple de 8 (64bits))
  2. les reels double précision: double|LREAL (64bits)
  3. les reels simple precision ou entier: float|REAL, int|DINT,UDINT,DWORD (32bits)
  4. les entiers 16 bits: short|INT,UINT,WORD (16bits)
  5. les bytes: char|SINT,USINT,BYTE (8bits)

 

Attention: selon les machines 32 ou 64bits en C certain types ont une définitions différente:

Ainsi il ne faut pas utiliser le type long (4bytes sur les machines 32bits et 8bytes sur les machines 64bits), mais toujours utiliser le type int (4bytes)



L'exemple montre le code C permettant de lire la PLC avec les éléments de l'exemple précédant:
main(){

	modbus_t *mbTdf;
	int rc;
	
	struct plcMemStruct {
		unsigned char   byte1;
		unsigned char   byte2;
		unsigned char   byte3;
		unsigned char   bytePAD;
		unsigned short  short1;
		unsigned short  shortPAD;
		unsigned int	time1;
		unsigned int	time2;
		unsigned int	time3;
		unsigned int	time4;
		unsigned short  short2;
	} plcMem;
...
	printf("Offset  0x3000 --------READ---STRUCTURE----------------\n");
	rc = modbus_read_registers(mbTdf, 0x3000, sizeof(plcMem), (uint16_t *)&plcMem);
	if (rc == -1) {
    		fprintf(stderr, "%s\n", modbus_strerror(errno));
    		return -1;
	}
    	printf("byte1=%d (0x%X)\n", plcMem.byte1, plcMem.byte1);
    	printf("byte2=%d (0x%X)\n", plcMem.byte2, plcMem.byte2);
    	printf("byte3=%d (0x%X)\n", plcMem.byte3, plcMem.byte3);
    	printf("short1=%d (0x%X)\n", plcMem.short1, plcMem.short1);
    	printf("time1=%d (0x%X)\n", plcMem.time1, plcMem.time1);
    	printf("time1=%d (0x%X)\n", plcMem.time2, plcMem.time2);
    	printf("time1=%d (0x%X)\n", plcMem.time3, plcMem.time3);
    	printf("time1=%d (0x%X)\n", plcMem.time4, plcMem.time4);
    	printf("short2=%d (0x%X)\n", plcMem.short2, plcMem.short2);
...
}

Resultat:

byte1=16 (0x10)
byte2=32 (0x20)
byte3=48 (0x30)
short1=56831 (0xDDFF)
time1=1000 (0x3E8)
time1=23456 (0x5BA0)
time1=65535 (0xFFFF)
time1=65536 (0x10000)
short2=52445 (0xCCDD)

Ecriture de structure

	plcMem.byte1++;
	plcMem.byte2++;
	plcMem.byte3++;
	plcMem.short1++;
	plcMem.time1++; 
	plcMem.time2++; 
	plcMem.time3++; 
	plcMem.time4++; 
	plcMem.short2++;
	
	rc = modbus_write_registers(mbTdf, 0x3000, sizeof(plcMem), (uint16_t *)&plcMem);
	if (rc == -1) {
    		fprintf(stderr, "%s\n", modbus_strerror(errno));
    		return -1;
	}

 

A PROPOS DES DATES

Ce sont tous des entiers 32 bits non signés:

Type PLCExempleDefinitionLimitesType C
DATE D#2014-02-01 temps unix UT [s] à cette date à 00H00M00S 0=1/1/1970-0:0:0 unsigned integer 32 bits
TIME_OF_DAY TOD#20:12:34:56.789 temps du jour [ms]

(mini) 00:00:00.000=0

(maxi) 23:59:59.999 = 86399999

unsigned integer 32 bits
DATE_AND_TIME DT#2014-02-01:-12:34:56 temps unix UT [s] à cette date à l'heure donnée unsigned integer 32 bits
EXEMPLE DE PROGRAMMATION EN C

Juste une idée pour faire un code portable entre BC9120 et PC embarquées et plus simple à debugger:
La limite du début mémoire est fixée par BASE et PLC(add) permet de mettre l'adresse tel qu'elle apparait dans le code PLC (ceci permet une relecture plus aisée du code C vis à vis du code PLC, (sans la division par 2, ni l'erreur de mettre 0x3012 pour l'adresse PLC %MW24 qui est (hexa) #300C .. oui je l'ai fais!)
        Par exemple dans le code PLC on a:

        date AT %MD48 : DATE;


        Le code C aura:

        #define BASE 0x3000
        #define PLC(add) (BASE+(int)(ceil(add/2.)))
        ...
        unsigned int date;
        ...
        rc = modbus_read_registers(mbTdf, PLC(48), sizeof(date), (unin16_t *)&date);