How to Implement Data Communication Using Modbus RTU? (part 2)

Jan 12

So how to arrange data exchange through Modbus RTU?  Answering multiple requests and inspired by @burgua@maxmaxmax and other people I have finally decided to write down a second part of the manual 🙂

inspired

In order to start up data exchange protocol, it is required to implement it both on master (in our case PC) and slave (board RS-485 with port). All this will work with RS-485 – USB adapter.

Let’s have a look at Modbus RTU configuration implementation, where PC is a master (i.e. query sender) and a board is a slave (query responding unit).

For the very beginning, one should define address chart to be completely matching address map set in the controller which we are going to use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace Modbus
{
    public enum ModbusMap
    {
        ADR_STAT = 0,
        ADR_ADC_STATUS = 1,
        ADR_ADC_RESULT = 2,
        ADR_ADC_ENG_VALUES = 12,
        ADR_TIME = 22,
        ADR_BINR_STATE = 25,
        ADR_BINR_ATTEMPTNUM = 26,
        ADR_STATUS_IOBSM = 27,
        ADR_DIVISOR_COUNTER = 28,
        ADR_TIME_SCALE = 29,
        ADR_GSK_RES = 55,
        ADR_OSK_RES = 83,
        ADR_CMD = 111,
        ADR_NAV_TASK_STATE = 112,
        ADR_NAV_TASK_CORRECT_ORB_INDEX = 113,
        ADR_NAV_TASK_CORRECT_ORB_INDEX_ITER = 114,
        ADR_COUNT_GPS_DATA = 115,
        ADR_COUNT_GPS_TASK = 116,
        ADR_FIRST_INDEX_GPS_TASK = 117,
        ADR_IS_CYCLE_READ_MODE = 118,
        ADR_STORE_DIVISOR = 119
    }
}
namespace Modbus
{
    public enum ModbusMap
    {
        ADR_STAT = 0,
        ADR_ADC_STATUS = 1,
        ADR_ADC_RESULT = 2,
        ADR_ADC_ENG_VALUES = 12,
        ADR_TIME = 22,
        ADR_BINR_STATE = 25,
        ADR_BINR_ATTEMPTNUM = 26,
        ADR_STATUS_IOBSM = 27,
        ADR_DIVISOR_COUNTER = 28,
        ADR_TIME_SCALE = 29,
        ADR_GSK_RES = 55,
        ADR_OSK_RES = 83,
        ADR_CMD = 111,
        ADR_NAV_TASK_STATE = 112,
        ADR_NAV_TASK_CORRECT_ORB_INDEX = 113,
        ADR_NAV_TASK_CORRECT_ORB_INDEX_ITER = 114,
        ADR_COUNT_GPS_DATA = 115,
        ADR_COUNT_GPS_TASK = 116,
        ADR_FIRST_INDEX_GPS_TASK = 117,
        ADR_IS_CYCLE_READ_MODE = 118,
        ADR_STORE_DIVISOR = 119
    }
}

Enum is a not according to convention in C#, but in this way it is more comfortable to cope with addresses since in the controller they are defined in the following manner:

1
2
3
#define ADR_STAT   (0)
#define ADR_ADC_STATUS  (1)
#define ADR_ADC_RESULT  (2)
#define ADR_STAT   (0)
#define ADR_ADC_STATUS  (1)
#define ADR_ADC_RESULT  (2)

Slave address is also defined – i.e. address of the unit, which we are working with:

1
#define MB_SLAVE_ADR (1)
#define MB_SLAVE_ADR (1)

Optionally, all these could be saved in the configuration file in case addresses and parameters are frequently changed.

Here basic parameters for data transfer to be defined. Definitely, all this stuff is better now to use in an App.config file, since if you will try to use the other port, nothing will work then ðŸ™‚

1
2
3
4
5
6
7
8
9
<span class="sc3"><span class="re1"><appSettings<span class="re2">></span></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"SlaveAddress"</span> <span class="re0">value</span>=<span class="st0">"1"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"PortName"</span> <span class="re0">value</span>=<span class="st0">"COM3"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"BaudRate"</span> <span class="re0">value</span>=<span class="st0">"115200"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"DataBits"</span> <span class="re0">value</span>=<span class="st0">"8"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"ReadTimeout"</span> <span class="re0">value</span>=<span class="st0">"1000"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"WriteTimeout"</span> <span class="re0">value</span>=<span class="st0">"1000"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"AttemptsModbus"</span> <span class="re0">value</span>=<span class="st0">"3"</span> <span class="re2">/></span></span>
<span class="sc3"><span class="re1"></appSettings<span class="re2">></span></span></span>
<span class="sc3"><span class="re1"><appSettings<span class="re2">></span></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"SlaveAddress"</span> <span class="re0">value</span>=<span class="st0">"1"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"PortName"</span> <span class="re0">value</span>=<span class="st0">"COM3"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"BaudRate"</span> <span class="re0">value</span>=<span class="st0">"115200"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"DataBits"</span> <span class="re0">value</span>=<span class="st0">"8"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"ReadTimeout"</span> <span class="re0">value</span>=<span class="st0">"1000"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"WriteTimeout"</span> <span class="re0">value</span>=<span class="st0">"1000"</span> <span class="re2">/></span></span>
    <span class="sc3"><span class="re1"><add</span> <span class="re0">key</span>=<span class="st0">"AttemptsModbus"</span> <span class="re0">value</span>=<span class="st0">"3"</span> <span class="re2">/></span></span>
<span class="sc3"><span class="re1"></appSettings<span class="re2">></span></span></span>

Parameters:

  • SlaveAddress – slave device address;
  • PortName – port;
  • BaudRate – data transfer rate for serial port;
  • DataBits – standard data bits number in a byte;
  • ReadTimeout – reading timeout;
  • WriteTimeout – writing timeout;
  • AttemptsModbus – attempts number for data transfer through the protocol.

So, basic parameters defined and then it’s time to use library NModbus.dll.  It could be downloaded from here. The library is accessible for different versions of .NET and enables to avoid trying to reinvent the wheel for protocol implementation in C# but to use ready methods and then work with all types of  Modbus (ASCII, RTU, TCP/IP).

Let’s write down a small class to work with library with minimum required functions (only registers reading and writing):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using System;
using System.Configuration;
using System.IO.Ports;
using Modbus.Device;
 
namespace Modbus
{
    public static class Modbus
    {
        // Parameters from App.config
        private static readonly int ReadTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["ReadTimeout"]);
        private static readonly int WriteTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["WriteTimeout"]);
        private static readonly int AttemptsModbus = Convert.ToInt32(ConfigurationManager.AppSettings["AttemptsModbus"]);
 
        // Counters of errors
        public static int ReadErrors { get; private set; }
        public static int WriteErrors { get; private set; }
 
        /// 
        /// Open COM-port.
        /// 
        ///Serial port.
        public static void OpenPort(SerialPort port)
        {
            port.Open();
        }
 
        /// 
        /// Close COM-port.
        /// 
        ///Serial port.
        public static void ClosePort(SerialPort port)
        {
            port.Close();
        }
 
        /// 
        /// Read registers using Modbus RTU.
        /// 
        ///Serial port.
        ///Slave device address.
        ///Start address for reading.
        ///Counter of registers that should be read.
        /// Values of registers.
        public static ushort[] ReadRegisters(SerialPort port, byte slaveAddress, ushort startAddress, ushort numRegisters)
        {
            var registers = new ushort[numRegisters];
            if (port.IsOpen)
            {
                try
                {
                    var master = ModbusSerialMaster.CreateRtu(port);
                    master.Transport.ReadTimeout = ReadTimeout;
                    master.Transport.Retries = AttemptsModbus;
                    registers = master.ReadHoldingRegisters(slaveAddress, startAddress, numRegisters);
                }
                catch
                {
                    ++ReadErrors;
                }
            }
 
            return registers;
        }
 
        /// 
        /// Wrire registers using Modbus RTU.
        /// 
        ///Serial port.
        ///Slave device address.
        ///Start address for writing.
        ///Values of registers.
        public static void WriteRegisters(SerialPort port, byte slaveAddress, ushort startAddress, params ushort[] registers)
        {
            if (port.IsOpen)
            {
                try
                {
                    var master = ModbusSerialMaster.CreateRtu(port);
                    master.Transport.WriteTimeout = WriteTimeout;
                    master.Transport.Retries = AttemptsModbus;
                    master.WriteMultipleRegisters(slaveAddress, startAddress, registers);
                }
                catch
                {
                    ++WriteErrors;
                }
            }
        }
    }
}
using System;
using System.Configuration;
using System.IO.Ports;
using Modbus.Device;

namespace Modbus
{
    public static class Modbus
    {
        // Parameters from App.config
        private static readonly int ReadTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["ReadTimeout"]);
        private static readonly int WriteTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["WriteTimeout"]);
        private static readonly int AttemptsModbus = Convert.ToInt32(ConfigurationManager.AppSettings["AttemptsModbus"]);

        // Counters of errors
        public static int ReadErrors { get; private set; }
        public static int WriteErrors { get; private set; }

        /// 
        /// Open COM-port.
        /// 
        ///Serial port.
        public static void OpenPort(SerialPort port)
        {
            port.Open();
        }

        /// 
        /// Close COM-port.
        /// 
        ///Serial port.
        public static void ClosePort(SerialPort port)
        {
            port.Close();
        }

        /// 
        /// Read registers using Modbus RTU.
        /// 
        ///Serial port.
        ///Slave device address.
        ///Start address for reading.
        ///Counter of registers that should be read.
        /// Values of registers.
        public static ushort[] ReadRegisters(SerialPort port, byte slaveAddress, ushort startAddress, ushort numRegisters)
        {
            var registers = new ushort[numRegisters];
            if (port.IsOpen)
            {
                try
                {
                    var master = ModbusSerialMaster.CreateRtu(port);
                    master.Transport.ReadTimeout = ReadTimeout;
                    master.Transport.Retries = AttemptsModbus;
                    registers = master.ReadHoldingRegisters(slaveAddress, startAddress, numRegisters);
                }
                catch
                {
                    ++ReadErrors;
                }
            }

            return registers;
        }

        /// 
        /// Wrire registers using Modbus RTU.
        /// 
        ///Serial port.
        ///Slave device address.
        ///Start address for writing.
        ///Values of registers.
        public static void WriteRegisters(SerialPort port, byte slaveAddress, ushort startAddress, params ushort[] registers)
        {
            if (port.IsOpen)
            {
                try
                {
                    var master = ModbusSerialMaster.CreateRtu(port);
                    master.Transport.WriteTimeout = WriteTimeout;
                    master.Transport.Retries = AttemptsModbus;
                    master.WriteMultipleRegisters(slaveAddress, startAddress, registers);
                }
                catch
                {
                    ++WriteErrors;
                }
            }
        }
    }
}

In the case of the data reading/writing error, the counter increases the error number. It could be also Exception, if even tiny data loss is an exceptional situation. For me, it was enough to demonstrate that the errors number is minimal.

Class is required to operate COM port connection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System.IO.Ports;
 
namespace Modbus
{
    public class Connection
    {
        public SerialPort Port { get; set; }
 
        private bool PortIsOpen
        {
            get { return Port != null && Port.IsOpen; }
        }
 
        public void OpenPort()
        {
            ClosePort();
            Modbus.OpenPort(Port);
        }
 
        public void ClosePort()
        {
            if (PortIsOpen)
            {
                Modbus.ClosePort(Port);
            }
        }
    }
}
using System.IO.Ports;

namespace Modbus
{
    public class Connection
    {
        public SerialPort Port { get; set; }

        private bool PortIsOpen
        {
            get { return Port != null && Port.IsOpen; }
        }

        public void OpenPort()
        {
            ClosePort();
            Modbus.OpenPort(Port);
        }

        public void ClosePort()
        {
            if (PortIsOpen)
            {
                Modbus.ClosePort(Port);
            }
        }
    }
}

Then what is left, is to look at this wonder in action. Operation example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.Configuration;
using System.IO.Ports;
 
namespace Modbus
{
    internal class Program
    {
        private static void Main()
        {
            try
            {
                // Create connection using port parameters
                var connection = new Connection
                    {
                        Port = new SerialPort
                            {
                                PortName = ConfigurationManager.AppSettings["PortName"],
                                BaudRate = Convert.ToInt32(ConfigurationManager.AppSettings["BaudRate"]),
                                DataBits = Convert.ToInt32(ConfigurationManager.AppSettings["DataBits"]),
                                ReadTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["ReadTimeout"]),
                                WriteTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["WriteTimeout"]),
                                Parity = Parity.None,
                                StopBits = StopBits.One
                            }
                    };
 
                var slaveAddress = Convert.ToByte(ConfigurationManager.AppSettings["SlaveAddress"]);
 
                try
                {
                    // Open connection
                    connection.OpenPort();
 
                    // Write into registers
                    Modbus.WriteRegisters(connection.Port, slaveAddress, (ushort) ModbusMap.ADR_STAT, 1);
                    Modbus.WriteRegisters(connection.Port, slaveAddress, (ushort) ModbusMap.ADR_ADC_STATUS, 1);
                    Console.WriteLine(Modbus.WriteErrors);
 
                    // Read multiply registers
                    var registers = Modbus.ReadRegisters(connection.Port, slaveAddress, (ushort) ModbusMap.ADR_STAT, 2);
 
                    // Check results and do with registers what you want
                    Console.WriteLine(registers);
                    Console.WriteLine(Modbus.ReadErrors);
                }
                finally
                {
                    connection.ClosePort();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}
using System;
using System.Configuration;
using System.IO.Ports;

namespace Modbus
{
    internal class Program
    {
        private static void Main()
        {
            try
            {
                // Create connection using port parameters
                var connection = new Connection
                    {
                        Port = new SerialPort
                            {
                                PortName = ConfigurationManager.AppSettings["PortName"],
                                BaudRate = Convert.ToInt32(ConfigurationManager.AppSettings["BaudRate"]),
                                DataBits = Convert.ToInt32(ConfigurationManager.AppSettings["DataBits"]),
                                ReadTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["ReadTimeout"]),
                                WriteTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["WriteTimeout"]),
                                Parity = Parity.None,
                                StopBits = StopBits.One
                            }
                    };

                var slaveAddress = Convert.ToByte(ConfigurationManager.AppSettings["SlaveAddress"]);

                try
                {
                    // Open connection
                    connection.OpenPort();

                    // Write into registers
                    Modbus.WriteRegisters(connection.Port, slaveAddress, (ushort) ModbusMap.ADR_STAT, 1);
                    Modbus.WriteRegisters(connection.Port, slaveAddress, (ushort) ModbusMap.ADR_ADC_STATUS, 1);
                    Console.WriteLine(Modbus.WriteErrors);

                    // Read multiply registers
                    var registers = Modbus.ReadRegisters(connection.Port, slaveAddress, (ushort) ModbusMap.ADR_STAT, 2);

                    // Check results and do with registers what you want
                    Console.WriteLine(registers);
                    Console.WriteLine(Modbus.ReadErrors);
                }
                finally
                {
                    connection.ClosePort();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

Well, perhaps that’s all.  I’ve tried  to make an example as simple as possible sources could be found here. Enjoy cuties!

New Year Cat

P.S. Happy New Year all. Let the year 2014 to be cooler than 2013! Be happy 🙂

We #StandWithUkraine.
Learn how you can help too!

#Stand­With­Ukraine

We don't know how long the war will last. But what we do know is that we can't stand aside and watch.

The fastest way you can help too is to support Ukraine financially. The National Bank of Ukraine (NBU) has opened a multi-currency account for that purpose. Learn more

This account accepts donations in US, Canadian and Australian dollars, euros, British pounds, Swiss francs, yuan and yen.

UA823000010000032302338301027

Also accepting cryptocurrency donations – the fastest way to help. Learn more

BTC – 357a3So9CbsNfBBgFYACGvxxS6tMaDoa1P

ETH, USDT (ERC-20) – 0x165CD37b4C644C2921454429E7F9358d18A45e14

If you want to volunteer in the army of Ukraine and help with deeds, then here are specific instructions how to do this: Learn more

Spread the word!