Help creating a Modbus slave

Hi
Need help whit creating a Modbus slave that sends data in same way as a SDM630 Energy meter.

I have tried a lot of different things and read a lot of things on the net but i cant crack this one on my self.

Status:

  • My layout is as the attached file, layoute_Modbus_SDM630_KAM382.JPG.
  • I can read and print (Serial monitor) values from the two SDM630 (Slav ID:1 and 2) on the Master Arduino (UNO)
  • I can read pulses from KAM382 and calculate power values on the Slav Arduino (Nano), Slav ID:3.
  • I can write/read KAM382 power values to/on a LCD display and to/in the serial monitor, on the Nano.

What to achieve:
Now I want to send the values from Nano (Modbus slave ID:3) to Modbus Master (Uno) in the same format as the SDM630 uses to send its values over Modbus.

Why? So i can use the same Modbus Master for SDM630 and other non Modbus energy meters. In my case two Kamstrup 382.

SDM630 using IEEE 754 format floating-point number, see attached file, Easton Modbus Registers.pdf.

What do i need help whit?

  • Making modifications to my slave sketch, Slave_KAM382.ino, so it can send the power values from the 2x KAM382 to the Modbus Master (Arduino/Uno).

  • Point out which ModbusSlave library to use and help to modify it if needed.

  • All other good modifications on the code

The ZIP file contains sketches and library and some photos.

Regareds // Vind89

Eastron Modbus Registers.pdf (283 KB)

Modbus_SDM630_Kam382.zip (337 KB)

  • Making modifications to my slave sketch, Slave_KAM382.ino, so it can send the power values from the 2x KAM382 to the Modbus Master (Arduino/Uno).

Do you want to support only the two registers that you’re currently requesting in the master sketch? Or do you plan to support all registers to provide a mostly complete support (doesn’t really make sense in my opinion).

  • Point out which ModbusSlave library to use and help to modify it if needed.

I guess in your case the ModbusRTU_slave library (available in the library manager) is the best match. You won’t have to modify the library, just use it as the example shows you.
The library only supports hardware serial and that’s for a good reason. If you use SoftwareSerial you will most probably miss pulses and therefor return wrong values. SoftwareSerial deactivates interrupts for long periods of time (during complete byte transfers, not just of the traffic for the own node but for all traffic on the bus) so pulses will be late and in the case that two pulses occur within one byte transfer you will even loose some.
You might use SoftwareSerial for debugging if you really need a serial interface for that (you have the display).

Your slave code has some drawbacks:

    if (currentImpulse > lastImpulse) {

Remove that if, it’s not necessary as the unsigned integer arithmetic will calculate the result correctly even if an overflow occurred.

if (currentWatt < HighGain){ // Om det nyligen mätta värdet skiljer sig för mycket från föregående förkastas det nya värdet.
if (currentWatt > LowGain){ // Om det nyligen mätta värdet skiljer sig för mycket från föregående förkastas det nya värdet.

These two if can be removed too because the HighGain and the LowGain are calculated a few lines before from the currentWatt value and both conditions are always true.

Thanks for the reply.

About the registers:

  • Yes, for the moment I’m just interested of the two registers.

About the library:
I have tried that one and i didn’t make it work. I think maybe i tried to hard and maybe made it to complex. I will try again and see if i can get it working. I will come back whit some modified sketch for you to look at.

About serial:
I will follow your recommendation and use the hardware serial for Modbus.

About the drawbacks:
I will remove those and see if its still runs good. If it does those lines will stay in the trash bin :slight_smile:

Some small questions on the road:

If a have the value of say 9560.34 or -1223.23 and want to send it. How to get it in the IEEE 754 format.
What i can see the ordinary Modbus RTU is 16 bit and 754 is 32 bit. 754 need then two address fields.

How do i convert the values it and how to put it in to two address fields 0x0000 and 0x0001 for 9560.34 and 0x0002 and 0x0003 for -1223.23?

// Vind89

If a have the value of say 9560.34 or -1223.23 and want to send it. How to get it in the IEEE 754 format.
What i can see the ordinary Modbus RTU is 16 bit and 754 is 32 bit. 754 need then two address fields.

If I remember correctly the Arduino is using the same float format, so you can use a simple union structure to split the float into two unsigned integers (16bit) and put provide them as two register values for Modbus.

Thanks

This is a bit over may knowledge base.

I assume that i need to change my "volatile long currentWatt" to "float currentWatt", or?

And then could someone provide some examples how to:

  • split my "float currentWatt" into two unsigned integers (16bit) and then put it into two different registers 0x0000 and 0x0001?

Please

// Vind89

This shows how to handle unions:

union fconv
{
    struct
    {
        uint8_t lolo;
        uint8_t lohi;
        uint8_t hilo;
        uint8_t hihi;
    } b;
    float fl;
} fl2b;

union iconv
{
    struct
    {
        uint8_t hi;
        uint8_t lo;
    } b;
    uint16_t i;
} int2byte;

fl2b currentWatt;
int2byte regs[2];

currentWatt.fl = 240.57;
regs[0].b.hi = currentWatt.int.hihi;
regs[0].b.lo = currentWatt.int.hilo;
regs[1].b.hi = currentWatt.int.lohi;
regs[1].b.lo = currentWatt.int.lolo;

uint16_t register0 = regs[0].i;
uint16_t register1 = regs[1].i;

I hope this explains enough to show you how to implement your problem.

Now I have tried this but cant get it to work.

Could you pylon ore some one explain the code, pylon provided, above? Please i want to understand.

Most critical is this line:

regs[0].b.hi = currentWatt.int.hihi;

Explain it for me? int?

In a union you have multiple ways to access the same bytes of RAM. "regs" store 16bit values that you can access as 16 bit integers or as two 8bit byte values.
The "currentWatt" value is similar. It's a 32bit values that you can access as either a float value or as the 4 individual (8bit) bytes that are used to store that value in RAM.

The code I posted takes another "feature" of processors into account: the endianess. The AVR processor uses the little-endian system to store values (as Intel processors do) but ModBus values are defined to be big-endian (network-order). Little endian store the lowest valued byte in the lowest address. Big endian systems store the highest valued byte in the lowest address.

And sorry, the code contains some errors, it should be:

union fconv
{
    struct
    {
        uint8_t lolo;
        uint8_t lohi;
        uint8_t hilo;
        uint8_t hihi;
    } b;
    float fl;
} fl2b;

union iconv
{
    struct
    {
        uint8_t hi;
        uint8_t lo;
    } b;
    uint16_t i;
} int2byte;

fl2b currentWatt;
int2byte regs[2];

currentWatt.fl = 240.57;
regs[0].b.hi = currentWatt.b.hihi;
regs[0].b.lo = currentWatt.b.hilo;
regs[1].b.hi = currentWatt.b.lohi;
regs[1].b.lo = currentWatt.b.lolo;

uint16_t register0 = regs[0].i;
uint16_t register1 = regs[1].i;

Solved.

Now i have the two SDM630 and the two Arduino Nano slaves on the same Modbus network and they all send data to my Modbus Master, Arduino Uno. See attached layout image.

Modbus Master

#include <SoftwareSerial.h>                                              
#include <ModbusM485RTU.h>    
#include <LiquidCrystal_I2C.h>

SoftwareSerial swSerSDM(6, 7);  
SDM sdm(swSerSDM, 9600, 5);   

LiquidCrystal_I2C lcd(0x3f, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

void setup() {

  Serial.begin (9600);
  
  sdm.begin();  

    lcd.begin(20,4);

    lcd.setCursor(1,0);
    lcd.print("Home");
    lcd.setCursor(1,1);
    lcd.print("Version_M_v927");
    lcd.setCursor(1,2);
    lcd.print("Mr. ZYX");
    lcd.setCursor(0,3);
    lcd.print("--------------------");
    
    delay(5000);
    lcd.clear();
    
}

void loop() {
     
     lcd.clear();
     lcd.setCursor(0,0);
     lcd.print ("KAM-ID3(W):");
     lcd.setCursor(11,0);
     lcd.print(sdm.readVal(KAM382_POWERTOTAL_1, 0x03), 2);
     lcd.print("  ");

     delay (200);

     lcd.setCursor(0,1);
     lcd.print ("KAM-ID3(W):");
     lcd.setCursor(11,1);
     lcd.print(sdm.readVal(KAM382_POWERTOTAL_2, 0x03), 2);
     lcd.print("  ");
     
     delay (200);
     
     lcd.setCursor(0,2);
     lcd.print ("SDM-ID1(W):");
     lcd.setCursor(11,2);
     lcd.print(sdm.readVal(SDM630_POWERTOTAL, 0x01), 2);
     lcd.print("  ");
     
     delay (200);

     lcd.setCursor(0,3);
     lcd.print ("SDM-ID2(W):");
     lcd.setCursor(11,3);
     lcd.print(sdm.readVal(SDM630_POWERTOTAL, 0x02), 2);
     lcd.print("  ");
     
     delay (2000);

     lcd.clear();
     lcd.setCursor(0,0);
     lcd.print ("KAM-ID4(W):");
     lcd.setCursor(11,0);
     lcd.print(sdm.readVal(KAM382_POWERTOTAL_1, 0x04), 2);
     lcd.print("  ");

     delay (200);
     
     lcd.setCursor(0,1);
     lcd.print ("KAM-ID4(W):");
     lcd.setCursor(11,1);
     lcd.print(sdm.readVal(KAM382_POWERTOTAL_2, 0x04), 2);
     lcd.print("  ");

     delay (2000);

}

Modbus Slav ID:3

#include <LiquidCrystal_I2C.h>                                        
#include <ModbusS485RTU.h>

#define TXEN  5                                 // RE/DE Pin for MAX485
#define ID   3                                  // Set slave ID on Mudbus network 

Modbus slave(ID,0,TXEN);                        // 0 stands for Hardwareserial

uint16_t au16data[6];                           // Array for Calculated IEEE745 values.   
                                                // [0] and [1] is not in use.
                                                // [2] and [3] is for value from IRQ 0 = pin2 / D2. Master query: start address 0x0002 and read two adresses.
                                                // [4] and [5] is for value from IRQ 1 = pin3 / D3. Master query: start address 0x0004 and read two adresses.

volatile unsigned long counter = 0;
volatile unsigned long lastImpulse = 0;
volatile unsigned long diffImpulse = 0;
float currentWatt = 0;
float LcdcurrentWatt = 0;
int byte_1_0 = 0;
int byte_1_1 = 0;
int byte_1_2 = 0;
int byte_1_3 = 0;

volatile unsigned long counter2 = 0;
volatile unsigned long lastImpulse2 = 0;
volatile unsigned long diffImpulse2 = 0;
float currentWatt2 = 0;
float LcdcurrentWatt2 = 0;
int byte_2_0 = 0;
int byte_2_1 = 0;
int byte_2_2 = 0;
int byte_2_3 = 0;

LiquidCrystal_I2C lcd(0x3f, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

void setup() 
{                
    slave.begin( 9600 );
 
    lcd.begin(16,2);

    lcd.setCursor(0,0);
    lcd.print("Home S_v67");
    lcd.setCursor(0,1);
    lcd.print("Kam382 Slave:");
    lcd.setCursor(13,1);
    lcd.print(ID);
    
    delay(3000);
    lcd.clear();
    
    attachInterrupt(0, onPulse, FALLING);       // KWH interrupt attached to IRQ 0 = pin2 / D2

    attachInterrupt(1, onPulse2, FALLING);      // KWH interrupt attached to IRQ 1 = pin3 / D3

}

void loop() 
{ 
    noInterrupts();
      
      LcdcurrentWatt = currentWatt;
      LcdcurrentWatt2 = currentWatt2;
      
    interrupts();
     
     lcd.setCursor(0,0);
     lcd.print("EL-1(W):");
     lcd.setCursor(8,0);
     lcd.print(LcdcurrentWatt);
     lcd.print("   ");

     lcd.setCursor(0,1);
     lcd.print ("EL-2(W):");
     lcd.setCursor(8,1);
     lcd.print(LcdcurrentWatt2);
     lcd.print("  ");


Bytebreakout1 (LcdcurrentWatt);                                     // Make IEEE 745 Bytes of calculated energy meater value

  au16data[2] = ((unsigned int)byte_1_0 << 8) + byte_1_1;           // merge two bytes into oa 16-bit value and put it in the array.
  au16data[3] = ((unsigned int)byte_1_2 << 8) + byte_1_3;           
  
Bytebreakout2 (LcdcurrentWatt2);                                    

  au16data[4] = ((unsigned int)byte_2_0 << 8) + byte_2_1;
  au16data[5] = ((unsigned int)byte_2_2 << 8) + byte_2_3;

slave.poll(au16data ,6 );                                          // Modbus recieve query and send back data.

}
void Bytebreakout1(float f1) 
  {
  byte * b = (byte *) &f1;
  byte_1_3 = (b[0]);
  byte_1_2 = (b[1]);
  byte_1_1 = (b[2]);
  byte_1_0 = (b[3]);
  }
  
void Bytebreakout2(float f2) 
  {
  byte * b = (byte *) &f2;
  byte_2_3 = (b[0]);
  byte_2_2 = (b[1]);
  byte_2_1 = (b[2]);
  byte_2_0 = (b[3]);
  } 
   
void onPulse() 
{
  unsigned long currentImpulse = millis();
  
  // Increment counter
  counter++;
    
  // Calculate current Watt usage by measuring the time difference between two impulses
  if (lastImpulse != 0) { // we can now calculcate
      diffImpulse = currentImpulse - lastImpulse;
      currentWatt = long((3600UL * 1000UL) / diffImpulse); // 2500UL for energy meters whit 400imp/kWh and 1000UL for energy meters whit 1000 imp/kWh.
  }
  lastImpulse = currentImpulse; 
}

void onPulse2() 
{
  unsigned long currentImpulse2 = millis();
  
  // Increment counter
  counter2++;
    
  // Calculate current Watt usage by measuring the time difference between two impulses
  if (lastImpulse2 != 0) { // we can now calculcate
      diffImpulse2 = currentImpulse2 - lastImpulse2;
      currentWatt2 = long((3600UL * 1000UL) / diffImpulse2); // 2500UL for energy meters whit 400imp/kWh and 1000UL for energy meters whit 1000 imp/kWh. 
  }
  lastImpulse2 = currentImpulse2;
}

As you can see, there is still some improvement to do. But not critical.

  • Replace delays in the Master sketch.
  • Replace duplicate use of void functions in the slave sketch.
  • Use Control Structure “for” to reduce the amount of code.

Next step is to:

  • Implement WiFi on the Uno.
  • Send data to KepServerEX
  • Publish data on a WEB page.

//--------------------------------------//
A little heads up about this line:

LiquidCrystal_I2C lcd(0x3f, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

Bee aware that the address “0x3f” could be different between LCD displays of the same type. I had one with the address 0x3f (Slave ID:3) and one with 0x27 (Slave ID:4). Very time consuming.
//-------------------------------------//

Thanks all for the support.

All files is attached down below.

// Vind

sdm630kam382_Master_v927.ino (1.76 KB)

Slave_KAM382v67.ino (4.08 KB)

Slave_KAM382v68.ino (4.08 KB)

ModbusM485RTU.zip (4.35 KB)

ModbusS485RTU.zip (5.42 KB)