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 🙂
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:
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!
P.S. Happy New Year all. Let the year 2014 to be cooler than 2013! Be happy 🙂