MODBUS / JBUS : Bien communiquer pour bien contrôler


PLAN du CHAPITRE

0 / Introduction

1/ Configuration matérielle Schéma de câblage

2 / Fonctionnement de MODBUS

2.1 / Trame de lecture

2.2 / Trame d'écriture

2.3 / Exemple de lecture d'un mot

2.4 / Exemple de codage en Visual Basic

2.5 / Exemple de codage en Delphi

2.6 / Exemple de codage avec l'API

3 / Calcul du CRC

3.1 / Algorithme du principe

3.2 / Exemple de calcul développé en binaire

3.3 / Exemple de calcul du CRC en DELPHI

4 / Recommandations générales

 

 

 

 

0/ Introduction

C'est quoi ce binz ??

Je suis informaticien et je pratique (le + possible) la communication avec des appareils divers et variés.(Centrales météo, Centrales de mesures, analyseurs, programmateurs de prom/eprom et automates...).

Cet aspect de l'informatique est passionnant mais dans certains domaines, on se sent étrangement seul face à l'autisme de l'appareil. J'ai ressenti dûrement ce problème avec le protocole MODBUS qui est très répandu dans le monde de la communication industrielle et notament sur les automates.

Il est maintenant rendu totalement transparent par des serveurs OPC du commerce qui sont vendus dans les 10KF et qui en rajoutent une couche par dessus les nombreuses couches du système d'exploitation.

Mais quand on ne veut pas débourser 10KF, qu'on veut faire simple et qu'on veut comprendre le mécanisme, la solution est ailleurs. Encore faut-il la trouver.

C'est pourquoi, j'ai voulu faire moi-même une page détaillée qui explique de long en large la pratique de ce protocole qui est simple comme l'étaient les Hiéroglyphes de la pierre de Rosette.

Bien expliqué, c'est mieux.

Tu trouveras sur cette page tous les renseignements nécessaires à l'utilisation de ce protocole.

Pour résumer brièvement ce qu'est MODBUS, on peut dire qu'il émane de la société GOULD MODICON, que c'est un protocole de communication basé sur un principe Maître/esclave. Un seul maître et plusieurs esclaves. 255 esclaves maxi sur RS485 et 2 maxi sur RS232. Ce standard a été implémenté sur de nombreux appareils, car il est indépendant du matériel et s'adapte parfaitement aux architectures ouvertes. On retrouve aussi ce protocole sous le nom JBUS.

MODBUS peut converser en ASCII 7 bits ou en binaire RTU 8bits

L'avantage du mode RTU est que les données à transmettre prennent moins de place donc moins de temps. En effet, on adresse plus de données en 8 qu'en 7 bits.

On développera uniquement le mode RTU.

MODBUS/RTU est un protocole sécurisé basé sur le calcul d'un CRC (cyclical Redundancy check) ou test de redondance cyclique. Ce CRC calculé sur 16 bits est partie intégrante du message et il est vérifié par le destinataire. Il est calculé sur tous les octets de la trame à part lui-même bien-entendu.

Cet entier de type WORD (2 octets) sera calculé dans la gamme 0 à 65535 mais sera ramené dans la gamme -32768 à 32767.

Le maître (PC) envoie des requêtes à l'esclave qui lui répondra si le message lui convient.

Pour que le message convienne il doit être rédigé selon des règles édictées plus bas.

 

 

 

1/ Configuration matérielle

En premier lieu, s'assurer que les 2 appareils sont configurés de la même façon : exemple :

Vérification des paramètres sur la carte de communication de l'esclave.

Le schéma de câblage est classique et sera imposé par l'esclave. Le PC quant à lui gèrera principaleùment les lignes RD (2) et TD (3), le contraire des interfaces à 25 broches

 

 

2/ Fonctionnement de MODBUS

Il existe diverses fonctions MODBUS mais on ne s'intéressera qu'aux fonctions de lecture (03H) et écriture (06H)

La trame MODBUS est constituée d'une suite de caractères hexadécimaux. et contient les informations suivantes :

La nature des informations de la trame peut varier selon que l'on fera de la lecture, de l'écriture, de mots, de bits ....

On ne développera que les fonctions de lecture/écriture de mots. consécutifs. ON considèrera que l'on s'adresse uniquement à l'automate 1

La trame MODBUS est définie de la façon suivante :

PF/pf signifie 2 octets l'octet de poids Fort mis avant l'octet de poids faible

 

 

2.1 / Trame de lecture (ex : on veut connaître la valeur du mot 800)

 Esclave

 Fonction

 Adresse du 1er mot

 Nombre de mots

 CRC16

 01H

 03H

 *PF/pf

 01H (PF/pf)

 **PF/pf

Trame de réponse de l'esclave :

Esclave

 Fonction

 Nombre octets

 Valeur 1er mot

............

 Valeur dernier mot

 CRC16

 01H

 03H

 1 octet

 PF/pf

 

 PF/pf

 **PF/pf

 

 

2.2 Trame d'écriture d'un mot (ex : on veut fixer la valeur 0 au mot dont l'adresse est 800)

 Esclave

 Fonction

 Adresse du mot

 Valeur du mot

 CRC16

 01H

 06H

 *PF/pf

 00H (PF/pf)

 **PF/pf

Trame de réponse de l'esclave :

Esclave

 Fonction

 Adresse du mot

 Valeur du mot

 CRC16

 01H

06H

 PF/pf

 PF/pf

 **PF/pf

(*) Dans cet exemple, 800 décimal doit être converti en HEXA sur 2 octets Poids Fort puis poids faible. Il faut savoir qu'un PC parle intuitivement en HEXA. C'est à dire que si on prend le décimal 65 qui a pour équivalent caractère 'A', quand on va passer chr(65) sur la ligne, on transmettra 'A' qui est en fait 41H ,Eh oui !. De la même manière, quand on passera le décimal 0 qui a pour équivalent caractère NUL, on transmettra NUL qui est en fait 00H.

(**) Le CRC calculé subit le même type de conversion. Son calcul est développé ultérieurement.

 

 

2.3 / Exemple de lecture d'un mot sur l'esclave 1

La trame qui sera envoyée est la suivante :

01 03 0320 0001 8584 (voir trame de lecture précédemment, se munir d'une table de conversion ASCII/DEC/HEX)

Mais comme 01 fait 2 caractères on enverra chr(01) qui est le caractère SOH donc l'HEXA 01H et ainsi de suite...

Par conséquent, la trame définitive qui sera transmise est la suivante :

chr(01)+chr(03)+chr(03)+chr(20)+chr(00)+chr(01)+chr(85)+chr(84)

 

 

2.4 / Exemple de codage en Visual Basic au travers de l'ActiveX MSComm (composant comm série)

requete = chr(01)+chr(03)+chr(03)+chr(20)+chr(00)+chr(01)+chr(85)+chr(84)

mscomm1.Output = requete

 

 

2.5 / Exemple de codage en DELPHI au travers d'un ActiveX Comport Library (dispo gratuit sur internet)

requete := chr(01)+chr(03)+chr(03)+chr(20)+chr(00)+chr(01)+chr(85)+chr(84);

comport1.Writestr(requete);

 

 

2.6 / Exemple de codage avec les fonctions de l'API Windows

Parce-que l'entité de base sur un système d'exploitation est le fichier, le port série COM1 est aussi un fichier qu'il faut ouvrir pour écrire ou lire. Pour ce faire, on utilisera la fonction CreateFile de l'API

hCom: = CreateFile("COM1",GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,0, NULL );

Bien-sûr, on n'est pas dans le cadre de l'ActiveX et on devra remplir la structure de configuration COMMCONFIG et mettre à jour avec les fonctions SetCommConfig/GetCommConfig mais c'est bien documenté dans Win32.hlp

Ainsi ouvert, le fichier peut être lu, écrit. Et il sera refermé après usage (à cause des courants d'air)

Et maintenant qu'on a un fichier, on utilise tout simplement les fonctions de lecture (ReadFile) et d'écriture (WriteFile) de l'API.

attention aux déclarations soit en include(C) en uses(DELPHI) ou en declare(VB) selon le langage de programmation. Les déclarations de fonctions de l'API sont obligatoires en VB Il y a d'ailleurs un utilitaire livré avec VB qui fabrique automatiquement la syntaxe de déclaration des fonctions de l'API.

 

 

 

3 / Calcul du CRC

Le CRC est une technique utilisée pour assurer une fiabilité proche de 100%. Il est donc superflu d'utiliser les contrôles de flux et de parité.CRC signifie (cyclical Redundancy check) ou test de redondance cyclique. Ce CRC calculé sur 16 bits est partie intégrante du message et il est vérifié par le destinataire. Il est calculé sur tous les octets de la trame à part lui-même bien-entendu.

 

3.1 / Algorithme du principe

C'est à ce moment qu'il faut raisonner en HEXA et uniquement en HEXA. Car le CRC sera codé sur 2 octets et aboutira à 4 quartets. Chaque quartet vaudra entre 0 et F. Si le CRC calculé aboutit à -31356, sa séparation en 2 octets donne 85 PF et 84 pf. par conséquent : les quartets 8,5,8 et 4 autrement dit 08H 05H 08H et 04H. Laissons tomber cette considération pour le moment : Elle sera fort utile plus tard.

L'algorithme de calcul est le suivant :

Description : Description : Description : Description : C:\Users\BT\Desktop\site\algo crc.jpg

 

 

 

3.2 / Exemple de calcul développé en binaire :

On pose au préalable :

Poly (polynôme arbitraire) = A001 HEXA

On envoie

la chaîne 02 07 c'est à dire chr(02)+chr(07)

Cela constitue le début d'une trame ou l'on demanderait la fonction 07 à l'esclave 02

 

Développement

CRC

 

 

 

 

 

 

 

 

 

 

 

FLAG (retenue)

Init CRC

 

1111

1111

1111

1111

 

1er octet

XOR

 

 

0000

0010

 

Résultat

 

1111

1111

1111

1101

 

Décalage 1

 

0111

1111

1111

1110

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1101

1111

1111

1111

 

Décalage 2

 

0110

1111

1111

1111

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1100

1111

1111

1110

 

Décalage 3

 

0110

0111

1111

1111

0

Décalage 4

 

0011

0011

1111

1111

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1001

0011

1111

1110

 

Décalage 5

 

0100

1001

1111

1111

0

Décalage 6

 

0010

0100

1111

1111

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1000

0100

1111

1110

 

Décalage 7

 

0100

0010

0111

1111

0

Décalage 8

 

0010

0001

0011

1111

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1000

0001

0011

1110

 

 

 

 

 

 

 

 

2nd octet

XOR

 

 

0000

0111

 

Résultat

 

1000

0001

0011

1001

 

Décalage 1

 

0100

0000

1001

1100

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1110

0000

1010

1101

 

Décalage 2

 

0111

0000

0100

1110

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

résultat

 

1101

0000

0100

1111

 

Décalage 3

 

0110

1000

0010

0111

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1100

1000

0010

0110

 

Décalage 4

 

0110

0100

0001

0011

0

Décalage 5

 

0011

0010

0000

1001

1

Flag=1, poly

XOR

1010

0000

0000

0001

 

Résultat

 

1001

0010

0000

1000

 

Décalage 6

 

0100

1001

0000

0100

0

Décalage 7

 

0010

0100

1000

0010

0

Décalage 8

 

0001

0010

0100

0001

0

quartets

 

1

2

4

1

 

Le résultat de ce calcul aboutit à CRC16 = 0001 0010 0100 0001 soit 1241. Eh non ! parce-que le CRC sera swappé pf puis PF. Par conséquent sa valeur est de 4112 qu'on transmettra PF/pf. C'est comme çà !

 

 

 

3.3 / Exemple concret en DELPHI

Voici l'extrait de fiche (form1) intéressant le code qui va suivre :

Description : Description : Description : Description : C:\Users\BT\Desktop\site\fiche crc.jpg

les codes HEXA sont respectivement Edit7,7,9 et 10

Et voici le CODE DELPHI que je vais commenter

//Déclarations publiques (globales)

 

var bytes :array[1..8] of byte;
tabresult: array[1..4] of integer;
textresult: array [1..4] of string[2];
icrc,n,flag:integer;
crc16 ,oldcrc16:word;
crcreal : real;
crcfinal : smallint;
tbuf : string[5];
adresse ,valeur: word;
loadresse,hiadresse,lovaleur,hivaleur : word; //smallint ;
polynome : word;//smallint;
motrequete : integer; //Valeur du mot automate
ilec:integer; // indice de lecture automate

 

// Début du calcul

 

procedure TForm1.Button29Click(Sender: TObject);
begin
crc16:=$ffff;

// on peut s'aider d'un memo qui sert de buffer d'évolution du CRC
//memo1.Text:=inttostr(crc16);
poids;
// Attention ordre des données pour calcul
bytes[1]:=strtoint(edit3.text);
bytes[2]:=strtoint(edit4.text);
bytes[3]:=hiadresse;
bytes[4]:=loadresse;
bytes[5]:=hivaleur;
bytes[6]:=lovaleur;
bytes[7]:=0;
bytes[8]:=0;
polynome := $A001;

for icrc:=1 to 6 do begin

// memo1.text:=memo1.text+chr(13)+chr(10)+'Caractere n° '+inttostr(icrc);

crc16:=crc16 xor bytes[icrc];
// memo1.text:=memo1.text + chr(13)+chr(10)+inttostr(crc16);
for n:=0 to 7 do begin

// on regarde d'abord si 2 puissance 0 =1 ou 0 (se termine ou non par 1, ce sera le flag
if (crc16 mod 2=0) then flag:=0 else flag:=1;

//Decalage a droite de CRC16
oldcrc16:=crc16 ; // on le garde en mémoire pour voir s'il perd son signe (2^15)
crc16 :=crc16 shr 1;

 

// memo1.text:=memo1.text+chr(13)+chr(10) + 'Décalage '+ inttostr(n+1)+' '+inttostr(crc16)+' flag '+inttostr(flag)+ ' ';
if flag=1 then crc16:=crc16 xor polynome ; //retenue
// if flag = 1 then memo1.text :=memo1.text + ' XOR polynome ' +inttostr(crc16)
// else memo1.text :=memo1.text + ' Pas de polynome ' +inttostr(crc16);
end;{for n}

end; {for i}

 

 

 

edit7.text:=inttostr(lo(crc16)shr 4); //PF du poids faible
edit7.text:= IntToHex(StrToInt(Edit7.Text), 1);
textresult[1]:=edit7.text;
edit8.text:=inttostr(lo((crc16)shl 4)shr 4); //pf du poids faible
edit8.text:= IntToHex(StrToInt(Edit8.Text), 1);
textresult[2]:=edit8.text;
edit9.text:=inttostr(hi(crc16) shr 4); //PF du Poids fort
edit9.text:= IntToHex(StrToInt(Edit9.Text), 1);
textresult[3]:=edit9.text;
edit10.text:=inttostr(hi((crc16)shl 4)shr 4); // etc...
edit10.text:= IntToHex(StrToInt(Edit10.Text), 1);
textresult[4]:=edit10.text;

for icrc:=1 to 4 do begin // L'Hexa ne veut rien dire pour lui alors on met en décimal
if textresult[icrc]='A' then textresult[icrc]:='10';
if textresult[icrc]='B' then textresult[icrc]:='11';
if textresult[icrc]='C' then textresult[icrc]:='12';
if textresult[icrc]='D' then textresult[icrc]:='13';
if textresult[icrc]='E' then textresult[icrc]:='14';
if textresult[icrc]='F' then textresult[icrc]:='15';

tabresult[icrc]:=strtoint(textresult[icrc]);
end;

 

 

crc16:=0;
for icrc:=1 to 4 do begin
crc16:=crc16 + trunc(intpower(16,4-icrc)*tabresult[icrc]) ;
//showmessage(inttostr(trunc(intpower(16,4-i)*tabresult[i]))+' '+inttostr(tabresult[i]));

end;

if crc16 >32767 then //s'il est négatif >32767
crcreal:=crc16 - 65536
//showmessage (currtostr(crcreal));

else crcreal:=crc16 ;
// maintenant, il faut faire rentrer crcreal dans crcfinal
edit11.text:=currtostr(crcreal);
// memo1.text :=memo1.text + ' CRC16 : '+inttostr(crc16);
//showmessage (edit9.text);
crcfinal:=strtoint(edit11.text);
//showmessage(inttostr(crcfinal));
edit12.text:=inttostr(hi(crcfinal));
edit13.text:=inttostr(lo(crcfinal));
edit14.text:=inttostr(lo(strtoint(edit5.text)));
edit15.text:=inttostr(hi(strtoint(edit5.text)));
edit16.text:=inttostr(lo(strtoint(edit6.text)));
edit17.text:=inttostr(hi(strtoint(edit6.text)));

//memo1.text:=memo1.text +' >>> '+edit11.text;
trame.text :=edit3.text+' '+edit4.text+' '+edit5.text+' '+edit6.text+' '+edit11.text;

end;

et voici la routine poids

procedure poids;
begin
try
adresse:=strtoint(form1.edit5.text);
valeur:= strtoint(form1.edit6.text);
loadresse:=lo(adresse);
lovaleur:=lo(valeur);
hiadresse:=hi(adresse);
hivaleur:=hi(valeur);
//form1.label7.caption:=inttostr(hiadresse);
//form1.label8.caption:=inttostr(loadresse);
//form1.label9.caption:=inttostr(hivaleur);
//form1.label10.caption:=inttostr(lovaleur);
except
end;
end;

 

 

 

4 / Recommandations générales

Attention à la vitesse de l'ordinateur. Si des rafales de requêtes arrivent trop vite, l'esclave n'a pas le temps de répondre. Il faut alors temporiser les requêtes.