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 :

test-modbus.c
#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

test-modbus.c
#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.

test-modbus.c
#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
PKTH100-1 Sensor
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%)

test-rtu.c
#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.

⚠️
Cette partie de l’article nécessite une compréhension avancée du langage C.

Exemple de programme server

test-server-tcp.c
#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 0n 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 0n 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