Tuto MODBUS RTU et norme IEE754 - Utilisation d'un Arduino pour la conversion

Bonjour à tous,

J’ouvre ce sujet pour ceux qui aurait comme moi des problèmes pour récupéré des valeurs dans des équipements qui communique en MODBUS RTU sous la norme IEE754, en effet il n’est pas possible de convertir les valeurs de manière simple.

J’ai donc trouvé une solution sur la base d’un Arduino Mega, j’ai passé beaucoup de temps à écrire le code et à faire les essais mais celui-ci est désormais fonctionnel.

Vous aurez besoin d’une carte arduino mega, et de deux convertisseurs de signaux MAX485.

Ce code configure une passerelle Modbus RTU entre un compteur d’énergie SDM230 (esclave), un IPX800 V5 (maître) et une carte Arduino (qui agit à la fois en tant que maître et esclave). L’Arduino permet de lire les valeurs du SDM230, puis les transmet à l’IPX800 via Modbus RTU, en utilisant des modules MAX485 pour la communication RS485.

Fonctionnement :

  1. Arduino en tant que maître : L’Arduino se connecte au SDM230 (esclave Modbus) et lit plusieurs registres Modbus (comme la tension, le courant, la puissance, etc.). Ces valeurs sont lues toutes les 200 ms.
  2. Conversion et stockage des données : Les données lues du SDM230 sont converties en valeurs entières adaptées (uint16_t) à l’esclave (IPX800). Par exemple, la tension est multipliée par 100 pour améliorer la précision.
  3. Arduino en tant qu’esclave : L’Arduino agit également comme un esclave Modbus, permettant à l’IPX800 V5 (maître) de lire les registres contenant les données lues depuis le SDM230. L’IPX800 peut ainsi accéder aux informations via Modbus RTU.
  4. Communication via MAX485 : Les modules MAX485 sont utilisés pour gérer la communication série sur RS485, où le maître Arduino envoie des requêtes au SDM230 et l’esclave Arduino expose les valeurs lues au réseau Modbus.

Détails techniques :

  • Arduino en tant que maître (SDM230) : L’Arduino utilise la bibliothèque ModbusMaster pour communiquer avec le compteur d’énergie.
  • Arduino en tant qu’esclave (IPX800) : L’Arduino utilise la bibliothèque ModbusRTUSlave pour exposer les valeurs lues au réseau Modbus, ce qui permet à l’IPX800 de les lire.
  • Contrôle des pins MAX485 : Le MAX485 gère la communication en utilisant les pins DE (Driver Enable) et RE (Receiver Enable) pour activer ou désactiver la transmission.
#include <ModbusMaster.h>
#include <ModbusRTUSlave.h>

ModbusMaster node;
const int MAX485_CONTROL_MASTER = 2;

void preTransmission() { digitalWrite(MAX485_CONTROL_MASTER, HIGH); }
void postTransmission() { digitalWrite(MAX485_CONTROL_MASTER, LOW); }

#define MODBUS_SERIAL Serial2
#define DE_PIN 13
#define RE_PIN 12
#define MODBUS_BAUD 9600
#define MODBUS_CONFIG SERIAL_8N1
#define MODBUS_UNIT_ID 20

uint16_t holdingRegisters[20];
ModbusRTUSlave modbusSlave(MODBUS_SERIAL);

const uint16_t SDM230_REGISTERS[] = {0x0000, 0x0006, 0x000C, 0x0046, 0x0156, 0x0056};
const int NUM_REGISTERS = sizeof(SDM230_REGISTERS) / sizeof(SDM230_REGISTERS[0]);

int currentRegisterIndex = 0;
unsigned long previousMillisRead = 0;
const unsigned long intervalRead = 200;

uint16_t convertToScaledUint16(float value, float scaleFactor) {
  return (uint16_t)(value * scaleFactor);
}

float convertToFloat(uint16_t high, uint16_t low) {
  uint32_t combined = ((uint32_t)high << 16) | low;
  float result;
  memcpy(&result, &combined, sizeof(result));
  return result;
}

void setup() {
  Serial.begin(9600);
  Serial1.begin(9600, SERIAL_8N1);
  MODBUS_SERIAL.begin(MODBUS_BAUD, MODBUS_CONFIG);

  pinMode(MAX485_CONTROL_MASTER, OUTPUT);
  digitalWrite(MAX485_CONTROL_MASTER, LOW);
  pinMode(DE_PIN, OUTPUT);
  pinMode(RE_PIN, OUTPUT);
  digitalWrite(DE_PIN, LOW);
  digitalWrite(RE_PIN, LOW);

  node.begin(1, Serial1);
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

  modbusSlave.configureHoldingRegisters(holdingRegisters, 20);
  modbusSlave.begin(MODBUS_UNIT_ID, MODBUS_BAUD, MODBUS_CONFIG);

  Serial.println("Passerelle Modbus RTU initialisée.");
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillisRead >= intervalRead) {
    previousMillisRead = currentMillis;
    uint8_t result = node.readInputRegisters(SDM230_REGISTERS[currentRegisterIndex], 2);
    if (result == node.ku8MBSuccess) {
      uint16_t high = node.getResponseBuffer(0);
      uint16_t low = node.getResponseBuffer(1);
      float value = convertToFloat(high, low);

      switch (currentRegisterIndex) {
        case 0: holdingRegisters[currentRegisterIndex] = convertToScaledUint16(value, 100); break;
        case 1: holdingRegisters[currentRegisterIndex] = convertToScaledUint16(value, 1000); break;
        case 2: holdingRegisters[currentRegisterIndex] = convertToScaledUint16(value, 1); break;
        case 3: holdingRegisters[currentRegisterIndex] = convertToScaledUint16(value, 100); break;
        case 4: holdingRegisters[currentRegisterIndex] = convertToScaledUint16(value, 1); break;
        case 5: holdingRegisters[currentRegisterIndex] = convertToScaledUint16(value, 100); break;
      }

      Serial.print("Registre ");
      Serial.print(SDM230_REGISTERS[currentRegisterIndex], HEX);
      Serial.print(": ");
      Serial.println(value);
    }

    currentRegisterIndex = (currentRegisterIndex + 1) % NUM_REGISTERS;
  }

  digitalWrite(DE_PIN, LOW);
  digitalWrite(RE_PIN, LOW);
  modbusSlave.poll();
}

J’espère que cette " passerelle " aidera beaucoup d’entre nous en attendant une mise à jour de l’IPX800 v5 sur la partie Modbus. Bonne lecture à tous.

5 « J'aime »

Bonjour,
c’est une belle solution, bravo, mais je crains que tout le monde ne puisse pas réaliser tout cela.
J’espère que @GCE pourra mettre en œuvre le calcul d’un Float IEE754 nativement sur la V5, ainsi que le complément à 2.
Bonne journée

2 « J'aime »

Bonjour et bonne année 2025 à vous tous !

Je me permets de mettre à jour mon tutoriel, car j’ai remarqué quelques problèmes avec la bibliothèque Arduino qui gère la partie esclave. En effet, il s’avère que le bus RS485 était parfois bloqué en mode émission, ce qui causait un sacré bazar avec les autres équipements présents sur le bus.

Après de nombreux essais de modification de code, de câblage, de passage du bus à l’oscilloscope, etc., j’ai finalement utilisé une autre bibliothèque, qui a résolu le problème.

Voici le code fonctionnel, accompagné de commentaires pour en faciliter la compréhension :

#include <ModbusMaster.h>
#include <SimpleModbusSlave.h>

// Configuration pour le maître (SDM230)
ModbusMaster node;
const int MAX485_CONTROL_MASTER = 2; // Pin de contrôle pour le MAX485 maître

void preTransmission() { digitalWrite(MAX485_CONTROL_MASTER, HIGH); }
void postTransmission() { digitalWrite(MAX485_CONTROL_MASTER, LOW); }

// Configuration pour l'esclave (IPX800)
#define MODBUS_SERIAL Serial2 // Utilise Serial2 pour la communication avec l'IPX800
#define DE_PIN 13            // Driver Enable
#define RE_PIN 12            // Receiver Enable
#define MODBUS_BAUD 9600
#define MODBUS_ID 20
#define MODBUS_CONFIG SERIAL_8N2

enum {
  REGISTER_VOLTAGE, // 0
  REGISTER_CURRENT, // 1
  REGISTER_POWER,   // 2
  REGISTER_FREQUENCY, // 3
  REGISTER_ENERGY_TOTAL, // 4
  REGISTER_POWER_TOTAL, // 5
  TOTAL_REGISTERS
};

unsigned int holdingRegisters[TOTAL_REGISTERS]; // Registres Modbus

// Liste des registres Modbus à lire depuis le SDM230
const uint16_t SDM230_REGISTERS[] = {0x0000, 0x0006, 0x000C, 0x0046, 0x0156, 0x0056};
const int NUM_REGISTERS = sizeof(SDM230_REGISTERS) / sizeof(SDM230_REGISTERS[0]);

// Variables pour la gestion de la lecture
int currentRegisterIndex = 0;
unsigned long previousMillisRead = 0;
const unsigned long intervalRead = 200; // Intervalle entre les lectures (ms)

// Fonction pour convertir float en uint16_t avec un facteur d'échelle
uint16_t convertToScaledUint16(float value, float scaleFactor) {
  return (uint16_t)(value * scaleFactor);
}

// Conversion de deux registres 16 bits en un float IEEE754
float convertToFloat(uint16_t high, uint16_t low) {
  uint32_t combined = ((uint32_t)high << 16) | low;
  float result;
  memcpy(&result, &combined, sizeof(result));
  return result;
}

void setup() {
  // Initialisation des ports série
  Serial.begin(9600); // Debug
  Serial1.begin(9600, SERIAL_8N1); // UART1 pour RS485 maître (SDM230)
  MODBUS_SERIAL.begin(MODBUS_BAUD);

  // Configuration des pins de contrôle
  pinMode(MAX485_CONTROL_MASTER, OUTPUT);
  digitalWrite(MAX485_CONTROL_MASTER, LOW);

  pinMode(DE_PIN, OUTPUT);
  pinMode(RE_PIN, OUTPUT);
  digitalWrite(DE_PIN, LOW);
  digitalWrite(RE_PIN, LOW);

  // Configuration Modbus maître
  node.begin(1, Serial1); // Adresse Modbus 1 (SDM230)
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

  // Configuration Modbus esclave
  modbus_configure(
    &MODBUS_SERIAL,   // Port série utilisé
    MODBUS_BAUD,      // Baud rate
    MODBUS_CONFIG,    // Configuration série (SERIAL_8N1)
    MODBUS_ID,        // ID Modbus esclave
    DE_PIN,           // Broche pour activer la transmission
    TOTAL_REGISTERS,  // Taille des registres
    holdingRegisters  // Pointeur vers les registres
  );

  Serial.println("Passerelle Modbus RTU initialisée.");
}

void loop() {
  unsigned long currentMillis = millis();
  
  // Lecture Modbus maître toutes les 200 ms
  if (currentMillis - previousMillisRead >= intervalRead) {
    previousMillisRead = currentMillis;

    // Lire un registre depuis le SDM230
    uint8_t result = node.readInputRegisters(SDM230_REGISTERS[currentRegisterIndex], 2);
    if (result == node.ku8MBSuccess) {
      uint16_t high = node.getResponseBuffer(0);
      uint16_t low = node.getResponseBuffer(1);
      float value = convertToFloat(high, low);

      // Conversion et stockage dans les registres de l'esclave
      switch (currentRegisterIndex) {
        case 0: holdingRegisters[REGISTER_VOLTAGE] = convertToScaledUint16(value, 100); break;  // Tension x100
        case 1: holdingRegisters[REGISTER_CURRENT] = convertToScaledUint16(value, 1000); break; // Courant x1000
        case 2: holdingRegisters[REGISTER_POWER] = convertToScaledUint16(value, 1); break;    // Puissance active x1
        case 3: holdingRegisters[REGISTER_FREQUENCY] = convertToScaledUint16(value, 100); break;  // Fréquence x100
        case 4: holdingRegisters[REGISTER_ENERGY_TOTAL] = convertToScaledUint16(value, 1); break;    // Énergie totale x1
        case 5: holdingRegisters[REGISTER_POWER_TOTAL] = convertToScaledUint16(value, 100); break;  // Puissance totale x100
      }

      Serial.print("Registre ");
      Serial.print(SDM230_REGISTERS[currentRegisterIndex], HEX);
      Serial.print(": ");
      Serial.println(value);
    } else {
      Serial.print("Erreur lecture registre ");
      Serial.print(SDM230_REGISTERS[currentRegisterIndex], HEX);
      Serial.print(": Code ");
      Serial.println(result);
    }

    // Passer au registre suivant
    currentRegisterIndex = (currentRegisterIndex + 1) % NUM_REGISTERS;
  }

  // Gestion Modbus esclave
  modbus_update();
}

En même temps voici le schéma de câblages de cette passerelle :

projet.pdf (18,1 Ko) PDF

.

En ce qui concerne les modules MAX485, j’ai utilisé ce modèle, qui coûte à peine 10 € pour un lot de 5 modules et est facilement disponible sur de nombreux sites en ligne :

Il est nécessaire de dessouder la résistance R7 si votre module n’est pas en bout de ligne et que vous rencontrez des problèmes avec le bus RS485.

Pour les adresses des registres de l’Arduino configuré en tant qu’esclave, voici un tableau récapitulatif des adresses et des échelles de mesure :

.

Ensuite, il ne reste plus qu’à paramétrer votre objet LECTURE MODBUS comme illustré ici :

Et voilà, une passerelle entièrement fonctionnelle qui permet de lire directement les valeurs du SDM230 via l’IPX800.


N’hésitez pas à me faire un retour si vous avez mis en place cette passerelle ou si vous avez apporté des améliorations :+1: :+1:.

3 « J'aime »

Bonjour dacdac,

avez-vous vu cette annonce :

Bonne journée

1 « J'aime »

Et bah effectivement je suis passé à côté de l’info :person_shrugging:et étant dans l’idée que la mise à jour de l’objet modbus ne ce fasse pas de suite …

Ça m’aura entraîné à de la R&D :grimacing:

En tout cas merci et bonne soirée :+1:

1 « J'aime »