MODBUS
/ JBUS : Bien communiquer pour bien contrôler
PLAN du CHAPITRE
1/ Configuration matérielle Schéma
de câblage
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.2 / Exemple de calcul développé
en binaire
3.3 / Exemple de calcul du CRC en
DELPHI
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.
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
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.
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.
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 :
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 :
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;
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.