libmodbus
Présentation de la libmodbus
La libmodbus est une bibliothèque Open Source écrite en Langage C. Elle est très populaire pour communiquer avec les appareils Modbus.
- propose le support du Modbus RTU (serial) et TCP (Ethernet)
- disponible pour Linux, FreeBSD, Mac OS et Windows
- écrite en C
- possède une bonne documentation
- ne nécessite pas de dépendances
Prérequis pour appréhender les différentes démonstrations de cet article :
- Fonctionnement Modbus
- Langage C et la ToolChain
- Les commandes Linux (ls, cd, …)
Installation sous Ubuntu
Si vous démarrez avec une nouvelle installation d’Ubuntu, il faut dans un premier temps installer les compilateurs gcc, g++ et l’outil make. Sous Ubuntu c’est le paquet build-essential
. Il est peut-être déjà installé, vous pouvez vérifier en faisant la commande :
sudo apt list --installed | grep build-essential
Pour installer le paquet build-essential
:
sudo apt install build-essential
Pour installer la libmodbus
:
sudo apt install libmodbus5 libmodbus-dev
On installe également le paquet pkg-config
pour simplifier la commande de compilation.
sudo apt install pkg-config
Cette méthode d’installation devrait être indentique quelle que soit la version d’Ubuntu ou de Debian que vous possédez. Si vous êtes sur une autre distribution, l’installation sera proche mais il faudra l’adapter à votre gestionnaire de paquets, les noms peuvent également un peu varier.
Remarque : L’installation sous Windows est délicate et demande un peu de maîtrise pour générer une DLL. Nous n’en parlerons pas dans cet article.
Modbus TCP avec automate Wago 750-880
Sur le PC portable est installé Ubuntu avec la libmodbus et les outils de compilation.
- @IP du PC : 192.168.1.123
- @IP de l’automate Wago : 192.168.1.128
L’automate possède :
- une carte DI à 4 Inputs, mappée sur %IW0 pour le registre sur 16 bits ou %IX0,%IX1, %IX2 et %IX3 en mode booléen. Les quatre entrées correspondent aux 4 bits de poids faible de %IW0. Les entrées de l’automates sont câblées avec quatre interrupteurs pour forcer des valeurs.
- une carte DO à 4 Outputs, mappée sur %QW0 pour le registre sur 16 bits ou %QX0, %QX1, %QX2 et %QX3. Pareil, les quatres sorties correspondent aux 4 bits de poids faible de %QW0. Quatre Leds sont branchées sur ces sorties.
Lecture des entrées avec FC 0x03
“Read Holding Register”
La documentation de l’automate indique que nous pouvons lire la valeur de %IW0 à l’adresse Modbus 0 avec une fonction FC 0x03
. La documentation de la libmodbus propose un exemple que nous adaptons à notre configuration réseau :
#include <stdio.h>
#include <modbus.h>
int main(void) {
modbus_t *ctx;
uint16_t tab_reg[32];
// create context
ctx = modbus_new_tcp("192.168.1.128", 502); //@IP Wago and Modbus TCP port number
modbus_connect(mb);
/* Read 1 registers from the address 0 */
modbus_read_registers(ctx, 0, 1, tab_reg);
printf("Valeur lue pour IW0 : %d \n",tab_reg[0]);
//free resources
modbus_close(ctx);
modbus_free(ctx);
}
Pour compiler le fichier test-modbus.c
on utilise la commande suivante :
gcc test-modbus.c -o test `pkg-config --cflags --libs libmodbus`
Remarque : c’est le le quote inversé ` qu’il faut utiliser (ALTGR + 7)
Si vous n’avez pas pkg-config installé sur votre machine, vous pouvez tenterla ligne de compilation suivante :
gcc test-modbus.c -o test -I/usr/include/modbus -lmodbus
Avant de faire de lancer le binaire executable crée, j’active les 4 interrupteurs des entrées digitales, puis :
philippe@vm:~/Documents/test-modbus$ ./test
Valeur lue pour IW0 : 15
Le programme renvoit la valeur en décimale 15
qui correspond au code binaire de 1111
. La fonction Read-Holding Register est opérationnelle !
Écriture sur les sorties avec FC 0x06
En consultant la documentation de la librairie libmodbus, on remarque que la fonction modbus_write_register pourrait effectuer la mise à 1 ou 0 de nos sorties digitales.
Extrait de la documentation :
Pour cet essai, nous souhaitons mettre à 1
les trois première DO de l’automate. NOus écrirons la valeur 7 sur l’adresse Modbus 0
qui correspond à la variable %QW0 selon la document de l’automate Wago pour une fonction FC 0x06
#include <stdio.h>
#include <modbus.h>
int main(void) {
modbus_t *ctx;
uint16_t tab_reg[32];
// create context
ctx = modbus_new_tcp("192.168.1.128", 502); //@IP Wago and Modbus TCP port number
modbus_connect(mb);
/* Write 1 register on the address 0 */
modbus_write_register(ctx, 0, 7); // adresse à écrire 0 et value = 7 = 111 pour allumer les 3 première DO
printf("Operation OK\n");
//free resources
modbus_close(ctx);
modbus_free(ctx);
}
On utilise la même ligne de compilation que précédemment :
gcc test-modbus.c -o test `pkg-config --cflags --libs libmodbus`
On exécute le binaire avec ./test
et l’on observe les 3 premières sorties digitales de l’automate sont passées à ON.
Lecture au niveau bit avec la fonction Read Coil 0x01
:
Nous souhaitons modifier le code pour effectuer un accès au niveau bit pour les 4 DI de l’automate. La documentation de libmodbus nous indique que la fonction Read Coil est notée modbus_read_bits dont l’extrait de documentation est la suivante :
Extrait de la documentation :
On remarque que le tableau de variable doit être au format uint_8
et non plus uint_16
comme pour les fonctions travaillant sur registres. Le langage C ne pouvant pas travailler directement sur des booléens, la libmodbus utilise la variable la plus petite en non-signé -> uint_8
.
#include <stdio.h>
#include <modbus.h>
int main(void) {
modbus_t *ctx;
uint8_t tab_bool[32];
ctx = modbus_new_tcp("192.168.1.128", 502); //@IP Wago and Modbus TCP port number
modbus_connect(ctx);
//on lit les 4 premiers Coil à partir de l'adresse Modbus 0
modbus_read_bits(ctx, 0, 4, tab_bool);
printf("DI[0] = %d\n",tab_bool[0]),
printf("DI[1] = %d\n",tab_bool[1]),
printf("DI[2] = %d\n",tab_bool[2]),
printf("DI[3] = %d\n",tab_bool[3]),
modbus_close(ctx);
modbus_free(ctx);
}
Avec les interrupteur 1 et 2 associés à DI1 et DI2 à on
, le résultat à la console est le suivant :
philippe@vm:~/Documents/test-modbus$ ./test
DI[0] = 0
DI[1] = 1
DI[2] = 0
DI[3] = 1
Écriture avec la fonction Write Multiple Coils 0x0F
On trouve dans la documentation de la libmodbus, la fonction modbus_write_bits qui correspond à la fonction 0x0F
Le Synopsis de la fonction est le suivant :
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);
Pour mettre les sorties DO0, DO1, DO2 et DO3 à {1,0,1,0} respectivement, je vais créer un tableau initialisé de la manière suivante :
uint8_t tab_do[4]={1,0,1,0};
et configurer la fonction modbus_write_bits :
int modbus_write_bits(ctx, 0, 4, tab_do);
Les sorties DO0 et D02 se mettent bien à on
.
Modbus RTU avec capteur PKTH100B-CZ1
Capteur PKTH100B-CZ1 | FTDI USB-RS485 |
---|---|
Configuration de la liaison RS-485
Sous Ubuntu ou Debian, un utilisateur non root ne peut avoir accès aux ports série de type ttyS0 ou ttyUSB0 s’il n’est pas membre du groupe dialout ! Autrement dit, il suffit de vous ajouter à ce groupe pour y avoir accès.
Pour cela, il existe une commande très simple:
sudo usermod -aG dialout votre_login
On vérifie que l’adapteur USB-RS485 est bien reconnu par Linux. Classiquement, les convertisseurs USB-Serial sont vus comme /dev/ttyUSB0
, /dev/ttyUSB1
, /dev/ttyACM0
, …
Dans un terminal, on lance la commande : ls /dev
philippe@vm:~$ ls /dev
autofs hpet loop20 ptmx tty1 tty3 tty5 ttyS10 ttyS30 vcs6
block hugepages loop21 pts tty10 tty30 tty50 ttyS11 ttyS31 vcsa
bsg hwrng loop22 random tty11 tty31 tty51 ttyS12 ttyS4 vcsa1
btrfs-control i2c-0 loop23 rfkill tty12 tty32 tty52 ttyS13 ttyS5 vcsa2
bus initctl loop3 rtc tty13 tty33 tty53 ttyS14 ttyS6 vcsa3
cdrom input loop4 rtc0 tty14 tty34 tty54 ttyS15 ttyS7 vcsa4
char kmsg loop5 sda tty15 tty35 tty55 ttyS16 ttyS8 vcsa5
console kvm loop6 sda1 tty16 tty36 tty56 ttyS17 ttyS9 vcsa6
core log loop7 sda2 tty17 tty37 tty57 ttyS18 ttyUSB0 vcsu
cpu loop0 loop8 sda3 tty18 tty38 tty58 ttyS19 udmabuf vcsu1
...
On remarque que le convertisseur est bien vu comme /dev/ttyUSB0
Programmation de requêtes FC 0x03
Read Holding Registers en Modbus RTU
Les caractéristiques du capteurs sont :
- Slave @ = 1
- Bauds = 9600 bps
- Data = 8 bits
- Parity = None
- Stop = 1 bit
Pour récupérer les données de température et d’humidité relative, il faut effectuer un Read Holding Register sur l’adresse mémoire Modbus 0 sur 2 registres (1 pour T°C et 1 pour Hr%)
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
modbus_t *mbrtu;
uint16_t tab_reg[2];
int main(void) {
/* Confiration de la liaison série */
mbrtu=modbus_new_rtu("/dev/ttyUSB0",9600,'N',8,1);
/* Adresse du Slave RTU */
modbus_set_slave(mbrtu,1);
/* Définition de la liaison série RS485 et connexion*/
modbus_rtu_set_serial_mode(mbrtu, MODBUS_RTU_RS485);
modbus_connect(mbrtu);
/* Read 2 registers from the address 0 */
modbus_read_registers(mbrtu, 0, 2, tab_reg);
/* Affichage des résultats */
printf("Temperature: %d\n",tab_reg[0]);
printf("Humidity: %d\n",tab_reg[1]);
/*fermeture de la liaison RTU */
modbus_close(mbrtu);
modbus_free(mbrtu);
return EXIT_SUCCESS;
}
On compile et l’on obtient les résultats suivants :
philippe@vm:~/Documents/test-modbus$ ./test
Temperature: 253
Humidity: 584
Il reste à diviser par 10 les valeurs pour retrouver les valeurs réelles de température et d’Humidité relative.
Créer un Server Modbus avec la libmodbus
Jusqu’à maintenant, nous avons crée des programmes de type Client Modbus TCP ou Master Modbus RTU, qui réalisent les mêmes opérations que QModmaster pour interroger les Servers Modbus TCP ou les Slaves RTU.
Créer un Server Modbus est un peu plus complexe mais très utile quand on voudra créer notre propre système à base de Raspberry Pi ou d’ESP-32, communiquant en Modbus.
Exemple de programme server
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <unistd.h>
const int NB_BITS = 16; //coils
const int NB_INPUT_BITS = 16; //input bits
const int NB_REGISTERS = 16; //holding registers
const int NB_INPUT_REGISTERS = 16; //input registers
const uint8_t UT_INPUT_BITS_TAB[]={0xAF, 0x0D};
int main(void){
int s = -1;
int rc = 0;
int i;
uint8_t buf[MODBUS_TCP_MAX_ADU_LENGTH]={};
modbus_t *ctx;
modbus_mapping_t *map;
//create context (use 1502 port instead 502)
ctx = modbus_new_tcp(NULL, 1502);
//create mapping (start adress = 0 for NB_BITS, NB_INPUT_BITS, NB_REGISTERS, NB_INPUT_REGISTERS)
map= modbus_mapping_new_start_address( 0, NB_BITS, 0, NB_INPUT_BITS, 0, NB_REGISTERS, 0, NB_INPUT_REGISTERS);
if(map == NULL){
fprintf(stderr, "Failed to allocate the mapping: %s\n", modbus_strerror(errno));
return -1;
}
else{
printf("mapping created\n");
}
//set some holding registers values
map->tab_registers[0] = 17;
map->tab_registers[1] = 18;
map->tab_registers[2] = 19;
//set some input registers values
map->tab_input_registers[0] = 13;
map->tab_input_registers[1] = 37;
map->tab_input_registers[2] = 42;
//set some discrete inputs
modbus_set_bits_from_bytes(
map->tab_input_bits, 0, NB_INPUT_BITS, UT_INPUT_BITS_TAB);
//see discrete input bits
for(i = 0; i< NB_INPUT_BITS; i++){
printf("Discrete Input : %d\n", map->tab_input_bits[i]);
}
while(1){
s=modbus_tcp_listen(ctx,1);
modbus_tcp_accept(ctx, &s);
if(s<0){
printf("Error listening for modbus, err %s\n",modbus_strerror(errno));
usleep(100000);
continue;
}
else{
break;
}
}
struct mb_tcp{
uint16_t transact;
uint16_t protocol;
uint16_t length;
uint8_t unit;
uint8_t func;
uint8_t data[];
};
for(;;){
do{
rc=modbus_receive(ctx,buf);
}while (rc==0);
if(rc < 0){
printf("%d\n", rc);
printf("Error %s\n", modbus_strerror(errno));
close(s);
break;
}
printf("Received poll from Master\n");
struct mb_tcp *frame = (struct mb_tcp *)buf;
printf("Function code is: %d\n", frame->func);
printf("Protocol code is: %d\n", frame->protocol);
printf("Transaction is: %d\n", frame->transact);
printf("-----------------------------------------------\n");
rc = modbus_reply(ctx, buf, rc, map);
if (rc < 0){
printf("%d%d\n", rc, rc);
printf("Error %s\n", modbus_strerror(errno));
close(s);
break;
}
}
//free resources
close(s);
modbus_free(ctx);
modbus_mapping_free(map);
return 0;
}
Explications du code
Expliquer ligne par ligne le code serait ennuyant. Je vais placer quelques extraits du code en mettant en lien la documentation officiel des fonctions de la libmodbus qui ont été utilisées.
modbus_new_tcp
On utilise le port 1502 pour éviter d’avoir à lancer le programme avec les droits administrateurs. On place NULL dans la fonction car on crée le contexte pour un Server.
//create context (use 1502 port instead 502)
ctx = modbus_new_tcp(NULL, 1502);
Extraits de la documentation libmodbus pour modbus_new_tcp
t’s convenient to use a port number greater than or equal to 1024 because it’s not necessary to have administrator privileges.
A NULL value can be used to listen any addresses in server mode
modbus_mapping_new_start_address
//create mapping (start adress = 0 for NB_BITS, NB_INPUT_BITS, NB_REGISTERS, NB_INPUT_REGISTERS)
map= modbus_mapping_new_start_address( 0, NB_BITS, 0, NB_INPUT_BITS, 0, NB_REGISTERS, 0, NB_INPUT_REGISTERS);
Extraits de la documentation libmodbus pour modbus_mapping_new_start_address
The newly created mb_mapping will have the following arrays:
- tab_bits allocated to store 16 bits (uint8_t)
- tab_input_bits allocated to store 16 input_bits (uint8_t)
- tab_input_registers allocated to store 16 input_registers (uint16_t)
- tab_registers allocated to store 16 registers (uint16_t)
Affectation de valeurs aux Holding Registers et Input Registers
//set some holding registers values
map->tab_registers[0] = 17;
map->tab_registers[1] = 18;
map->tab_registers[2] = 19;
//set some input registers values
map->tab_input_registers[0] = 13;
map->tab_input_registers[1] = 37;
map->tab_input_registers[2] = 42;
- On affecte les valeurs 17, 18, 19 aux trois premiers Holding Registers sur les 16 au total.
- On affecte les valeurs 13, 37, 42 aux trois premiers Input Registers sur les 16 au total.
Affectations de bit pour les discrete inputs
//set some discrete inputs
modbus_set_bits_from_bytes(
map->tab_input_bits, 0, NB_INPUT_BITS, UT_INPUT_BITS_TAB);
The modbus_set_bits_from_bytes function shall set bits by reading an array of bytes.
Première boucle infinie
On écoute sur le contexte et l’on peut accepter jusqu’à 1 client. Quand on a accepté un client, on sort de la boucle while (break)
while(1){
s=modbus_tcp_listen(ctx,1);
modbus_tcp_accept(ctx, &s);
seconde boucle infinie
On récupère les requêtes modbus
for(;;){
do{
rc=modbus_receive(ctx,buf);
}while (rc==0);
et l’on envoie les valeurs demandées par le client (reply)
rc = modbus_reply(ctx, buf, rc, map);
Résultats
On compile le fichier test-server-tcp.c
et l’on obtient à l’exécution :
philippe@vm:~/Documents/test-modbus$ gcc test-server-tcp.c -o test `pkg-config --cflags --libs libmodbus`
philippe@vm:~/Documents/test-modbus$ ./test
mapping created
Size of holding registers is: 8
Discrete Input : 1
Discrete Input : 1
Discrete Input : 1
Discrete Input : 1
Discrete Input : 0
Discrete Input : 1
Discrete Input : 0
Discrete Input : 1
Discrete Input : 1
Discrete Input : 0
Discrete Input : 1
Discrete Input : 1
Discrete Input : 0
Discrete Input : 0
Discrete Input : 0
Discrete Input : 0
Analyse des valeurs affichées pour les Discrete Inputs
Les Discrete Input ont été configurées aux valeurs hexa 0xAF 0x0D
const uint8_t UT_INPUT_BITS_TAB[]={0xAF, 0x0D};
Si pour chaque octet,
0xAF = 1010 1111
si on transfère au LSB au MSB on obtient :1111 0101
0x0D = 0000 1101
si on transfère au LSB au MSB on obtient :1011 0000
On retrouve ainsi la suite binaire affichée 1111010110110000
Requête FC0x02
avec QModMaster
La fonction FC0x02
permet de lire les valeurs des Discrete Inputs de notre Server. La console nous retourne les valeurs suivantes :
Received poll from Master
Function code is: 2
Protocol code is: 0
Transaction is: 768
-----------------------------------------------
et le logiciel QModMaster nous retourne la suite des bits 1111010110110000
Requête FC0x03
avec QModMaster
Si l’on fait une requête de type FC0x03
avec QModMaster sur le Server la console nous renvoie les informations suivantes :
Received poll from Master
Function code is: 3
Protocol code is: 0
Transaction is: 256
-----------------------------------------------
et sur QModMaster, nous retrouvons les valeurs 17, 18 et 19 associées aux Holding_Registers. La requête a été faite à partir de l’adresse 0
n sur 16 registres.
Requête FC0x04
avec QModMaster
Si l’on fait une requête de type FC0x04
avec QModMaster sur le Server la console nous renvoie les informations suivantes :
Received poll from Master
Function code is: 4
Protocol code is: 0
Transaction is: 1024
-----------------------------------------------
et sur QModMaster, nous retrouvons les valeurs 13, 37 et 42 associées aux Input_Registers. La requête a été faite à partir de l’adresse 0
n sur 16 registres.
Bilan
La libmodbus est une bibliothèque très utile quand on souhaite développer des programmes permettant de communiquer avec des appareillages Modbus mais offre également la possibilité de créer ses propres systèmes communiquant en Modbus.
On peut très facilement créer :
- Un client Modbus TCP
- Un Master RTU
Avec un peu plus de développement :
- Un server TCP
- Un slave RTU (non vu dans cet article)
L’auteur de la bibliothèque a également réalisé un portage pour créer un Slave Modbus RTU sur les cibles Arduino, Modbusino