The Noiasca Modbus Server Simple Library (Modbus Slave)

The Noiasca Modbus Server Simple is a lightweight Library to implement a Modbus Server (former Modbus Slave) on an Arduino.

I split this page in following chapters

  • Idea: Why a new Modbus Server Library
  • Holding Registers and additional Addresses
  • Example and Proof of Concept
  • Important Member Functions
  • Caveats & good to know

Idea: Why do we need a new Modbus Server Library?

The Noiasca Modbus Server Library can handle values (2 byte integers) in ascending holding registers. Each additional holding register will expand the array of holding registers. Holding registers will start by default at register 0.

Beside the holding register, you can define additional addresses. This comes handy if you want to read actual values only on demand by the Modbus Client without storing them in a holding registers or if you want switch pins without wasting space for holding register. Further more, additional addresses don't need to be ascending.

Basic Settings of Noiasca Modbus Server Simple

The Modbus is a communication protocol based on top of the RS485.

If you have several Hardware Serial (like on the Arduino MEGA) - you should use one of the Hardware Serials for the Modbus communication. On the Uno you can use Soft-Serial at low baud rate

#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3); // RX, TX

I recommend that you declare some constants:

constexpr byte modbus_slave_id = 2;     // Modbus address of this slave
constexpr uint32_t modbus_baud = 19200; // use only slow speeds with SoftSerial
constexpr byte modbus_enable_pin = 11;  // The GPIO used to control the MAX485 TX pin

Now you can use mySerial when you create the Modbus Server Object:

ModbusServer modbusServer(modbus_slave_id, mySerial);

In Setup you will need to call the begin method of your Serial interface:

mySerial.begin(modbus_baud); // start the Serial for Modbus

Usually you will need to control the DE/RE pins on the MAX485. I propose to connect DE/RE and use only one pin. In the example it is called like "modbus_enable_pin". You should define the two functions to handle the pin before the transmission and after the transmission:

void cbPreTransmission()
{
  digitalWrite(modbus_enable_pin, HIGH);
}

void cbPostTransmission()
{
  digitalWrite(modbus_enable_pin, LOW);
}

Don't forget to set the pinmode to OUTPUT and set callback functions in setup()

  pinMode(modbus_enable_pin, OUTPUT);                   // if you need a TX enable pin, set it to output
  modbusServer.setPreTransmission(cbPreTransmission);   // if you need a TX enable pin, set a callback to your pin handling
  modbusServer.setPostTransmission(cbPostTransmission); // if you need a TX enable pin, set a callback to your pin handling

uint16_t update() is the main function of the Modbus Server and should be called in loop() over and over again.

  modbusServer.update(); // call Modbus in loop

See the Example 0201_Modubs_Server_HelloWorld for a basic example.

Usage of Holding Registers

This Modbus Server holds values in registers of 2 byte length each.

a) define an enumeration with clear text register names. Use the final entry "HOLDING_REGS_SIZE" to determine the size of the array

enum
{
  regButton,         // register 0 - read a button
  regADC0,           // register 1 - analog port 0
  regLED,            // register 2 - set a LED
  regServo,          // register 3 - set a Servo
  HOLDING_REGS_SIZE  // leave this one
};

b) Define an array of a 2 byte variable of size HOLDING_REGS_SIZE

unsigned int holdingRegs[HOLDING_REGS_SIZE]; // function 3 and 16 register array

Use "regButton" instead of a magic number "0" n your sketch. If you need more registers - expand the list.

Each additional register will need 2 bytes of SRAM.

The defined register array makes it very easy to provide values to the client. You just have to update the values in the holdingRegs array:

void updateRegisters()
{
  static uint32_t previousMillis = 0;
  uint32_t currentMillis = millis();
  if (currentMillis - previousMillis > 50)  // this also helps to "debounce" a button
  {
    previousMillis = currentMillis;
    if (digitalRead(buttonPin) == LOW)
      holdingRegs[regButton] = 1;
    else
      holdingRegs[regButton] = 0;
    holdingRegs[regADC0] = analogRead(0);
  }
}

Vice versa you can read the value from the holdingRegs and switch pins or start other activities.

You can set two optional callback function which will be called after the client has read a holding register or after the client has written to a holding register:

  modbusServer.setRegisterWritten(cbRegisterWritten); // FC6/FC16 write: Client has written to a server holding register
  modbusServer.setRegisterRead(cbRegisterRead);       // FC3 read: Client has requested server holding registers

Usage of Additional Addresses

If you need additional addresses which are not ascending and are out of the range of the holding registers you can use following pattern:

a) Define a callback function to make the validation of additional addresses possible. The function must be able to accept a 2 byte address and must return a true (=valid) or false (unvalid address). You can define each valid address or ranges of valid addresses:

bool cbIsValidAddress(uint16_t addr)
{
  switch(addr)
  {
    case 100 :             // a valid address
    case 150 :
    case 200 ... 210 :     // a range of valid addresses
    case 1000 ... 1003 :
      return true;
  }
  return false;            // all other addresses will be unvalid
}

b) In setup() inform the library to check the requested registers with this function:

modbusServer.setIsValidAddress(cbIsValidAddress);

c) Define a callback function for incoming write requests. The function must accept a address and the value which should be written to that register.

void cbModbusRegisterSent(const uint16_t address, const uint16_t value)
{
  switch (address)
  {
    case 100: digitalWrite(ledPin, value); break;
    case 150: digitalWrite(motorPin, value); break;
    case 200: aVariable = value; break;
    case 201: anotherVariable = value; break;
    case 200: aThirdVariable = value; break;
  }
}

d) In setup() inform the library to call the function when one of these addresses was used

modbusServer.setRegisterSent(cbRegisterSent); // FC6/FC16 write: Client has sent value to an additional address

e) Vice versa you can define a callback function for read requests. The function must accept an address and must return the value requested from that address.

 uint16_t cbRegisterViewed(const uint16_t addr)
{
  Serial.print(F("requested additional address:")); Serial.println(addr);
  switch (addr)
  {
    case 1000: return analogRead(0);
    case 1001: return analogRead(1);
    case 1002: return analogRead(2);
    case 1003: return analogRead(3);
    case 1010: return analogRead(4);
    default: return 0xEEEE;
   }
}

f) and set the function as callback

 modbusServer.setRegisterViewed(cbRegisterViewed); // FC3 read: Client has requested an additional address

Coils (FC1, FC5, FC15)

Coils can be compared with controller pins defined as outputs. Modbus provides the FC1, FC5 and FC15 to handle outputs. See the example "Modbus Server FC1 FC5 FC15 Read Write Coils".

Discrete Inputs (FC2)

Discrete Inputs can be compared with Arduino pins defined as inputs. Modbus provides the FC2 to read from inputs. See the example "Modbus Server - FC2 Read Discrete Inputs".

Input Registers (FC4)

If you read input register you will get 2 byte values. In terms of Arduino this can be compared with analog pins readed with analogRead. Another example is a temperature sensor or a distance measurement. See the example "Modbus Server FC4 Input Registers".

More Methods in Noiasca Modbus Server Simple

void setStreamEnd(const CallBack cbStreamEnd) will be called when the transmission of several registers has ended.

void setOnClientActivity(void (*cbOnClientActivitiy)()) will be called when a valid client communication (Master) was recognized.

void modbus_update_comms(long baud, unsigned char byteFormat) can be used to modify Modbus parameters during runtime.

Examples and Proof of Concepts

The examples are splitted into "server" and "client". This library focuses on Modbus Server (Modbus Slave). The sketches in the folder "client" are based on the Modbus Master (Library 2.0.0) which can be downloaded with the library manager.

Example Basic Hello World

The library comes with a basic "Hello World" examples. It shows how to define a Modbus Server object.

Example for the supported Function Codes

For each group of supported Function Codes you find a sketch in the example folder. There is even one example how to set up an Arduino to support FC1, FC2, FC3, FC4, FC5, FC6, FC15 and FC16 in one sketch.

Proof of Concept: Transmit LCD Data Over Long Distances Using Modbus RTU

I2C LCDs are widely used with Arduino. There are several examples how to extend the wires between a "Modbus Client for LCD" and a "Modbus Server for LCD". Using Modbus you can achieve distances up two 1500m between your main device (client) and your LCD (server).

On client side you just use a "LCD library" which sends all LCD commands via Modbus. All lcd.begin, lcd.print commands can be reused. Instead of the LCD I2C Extender you must connect the RS485 interface. On the server side you install the LCD Receiver.

Caveats

Currently the NoiascaModbusServerSimple.h supports the major Modbus RTU function codes FC1, FC2, FC3, FC4, FC5, FC6, FC15 and FC16.

FC3 Read Multiple Holding register: start with 4 and span from 40001 to 49999
FC6 Write Single Holding Register: start with 4 and span from 40001 to 49999
FC16 Write Multiple Holding Register: start with 4 and span from 40001 to 49999

FC2 Read Discrete Inputs: start with 1 and span from 10001 to 19999

FC1 Read Coils: start with 0 and span from 00001 to 09999
FC5 Write Single Coi: start with 0 and span from 00001 to 09999
FC15 Write Multiple Coils: start with 0 and span from 00001 to 09999

FC4 Read input register: start with 3 and span from 30001 to 39999

Summary

-

Links

(*) Disclosure: Some of the links above are affiliate links, meaning, at no additional cost to you I will earn a (little) comission if you click through and make a purchase. I only recommend products I own myself and I'm convinced they are useful for other makers.

History

First upload: 2021-12-22 | Version: 2022-01-02