Click here to Skip to main content
15,881,248 members
Articles / Programming Languages / C#
Article

Simple Modbus Protocol in C# / .NET 2.0

Rate me:
Please Sign up or sign in to vote.
4.91/5 (110 votes)
18 Oct 20077 min read 863K   55.6K   176   118
A simple implementation of .NET 2.0's SerialPort class for Modbus communications
Screenshot - modbusCS.jpg

Introduction

Often in my day I find myself testing and debugging devices that communicate via the modbus protocol. In an effort to save myself time, I have tried several downloadable applications but always came to the same conclusion - "Why pay money for something when it's not that hard to build myself?" Also, given that modbus is a 30 year old standard, it makes it difficult to find current code that is usable in today's popular languages. What follows is a brief description of the madness behind the modbus and a small application that I find myself using daily when testing RS232 modbus devices. The code is hardly robust, but it gets the job done in the sense that it works and it is easy to use and alter - two features I find the most important in a lab setting. I hope you find this of some use!

Background

Modbus is a serial communications protocol used extensively in the industrial world going all the way back to the 1970s. It is especially useful in situations where one needs to connect to several devices in a network (or slaves) through its implementation of multiple device addresses. But if you found this article, then chances are you already know what modbus is and would rather not be bored to death with the details. For those who want to get their hands dirty with the finer points, here is the place to go for the relevant details.

For the purposes of this article, I'll do a quick rundown of the modbus implementation used and the functions covered.

Modbus Variants

The modbus protocol comes in two flavors: RTU and ASCII. RTU is a binary implementation and is often most desirable. As such, this discussion will pertain solely to the RTU standard.

Modbus Functions

There are numerous functions available in the modbus protocol, yet I haven't found any use for most besides the basic read and write commands. Therefore we will only be looking at implementations of Function 3 - Read Multiple Registers and Function 16 - Write Multiple Registers commands. Other commands can be learned about by following the link provided in the 'Background' portion of this article.

Function 3 - Read Multiple Registers Message Framing

Request
  • Address (one byte denoting the slave ID)
  • Function Code (one byte denoting the function ID, in this case '3')
  • Starting Address (two bytes representing the modbus register at which to begin reading)
  • Register Quantity (two bytes denoting the amount of registers to read)
  • CRC (two bytes containing the cyclical redundancy check checksum for the outgoing message)
Response
  • Address (one byte containing the ID of the slave responding)
  • Function Code (one byte denoting the function to which the slave is responding, in this case '3')
  • Byte Count (one byte representing the quantity of bytes being read. Each modbus register is made up of 2 bytes, so this value would be 2 * N, with N being the quantity of registers being read)
  • Register Values (2 * N bytes representing the values being read)
  • CRC (two bytes containing the CRC checksum for the incoming message)

Function 16 - Write Multiple Registers Message Framing

Request
  • Address (one byte denoting the slave ID)
  • Function Code (one byte denoting the function ID, in this case '16')
  • Starting Address (two bytes representing the modbus register at which to begin writing)
  • Register Quantity (two bytes denoting the amount of registers to write)
  • Byte Count (one byte representing the quantity of bytes being written. Each modbus register is made up of 2 bytes, so this value would be 2 * N, with N being the quantity of registers being written to)
  • Register Values (2 * N bytes containing the actual bytes being written)
  • CRC (two bytes containing the cyclical redundancy check checksum for the outgoing message)
Response
  • Address (one byte containing the ID of the slave responding)
  • Function Code (one byte representing the function being responded to, in this case '16')
  • Starting Address (two bytes stating the starting register address that was first written to)
  • Register Quantity (two bytes denoting the quantity of modbus registers that were written to)
  • CRC (two bytes representing the CRC checksum of the incoming message)

Error and Exception Coding

The modbus protocol describes numerous exception codes available for each function in its implementation guide. All that is handled in this code is a simple CRC evaluation, as this is hardly a full implementation of the entire modbus protocol. If one chooses to add more error checking, additional procedures can easily be added to the provided code.

Using the Code

The class modbus.cs contains several functions for serial port handling and data transfer. All functions make use of the SerialPort class found in the System.IO.Ports namespace of .NET 2.0.

One peculiarity of the SerialPort class exists in its DataReceived event. As has been documented in other articles on this site, the DataReceived event, which should fire each time the serial port receives an incoming event, doesn't "actually do this". This prevents truly event driving communications from working properly without the additional fixes and workarounds.

Fortunately, using something like the modbus protocol gives us the benefit of known incoming and outgoing message lengths. This allows us to use the ReadByte function as the backbone of any read commands and we can bypass the problem completely. In a perfect world, this would be an event driven class - but in the meantime we'll reject the tradeoff in lost data and tell the port when to read on our own.

SendFc3 - Read Multiple Registers

C#
public bool SendFc3(byte address, ushort start, 
    ushort registers, ref short[] values)
{
    //Ensure port is open:
    if (sp.IsOpen)
    {
        //Clear in/out buffers:
        sp.DiscardOutBuffer();
        sp.DiscardInBuffer();

        //Function 3 request is always 8 bytes:
        byte[] message = new byte[8];

        //Function 3 response buffer:
        byte[] response = new byte[5 + 2 * registers];

        //Build outgoing modbus message:
        BuildMessage(address, (byte)3, start, registers, ref message);

        //Send modbus message to Serial Port:
        try
        {
            sp.Write(message, 0, message.Length);

            GetResponse(ref response);
        }
        catch (Exception err)
        {
            modbusStatus = "Error in read event: " + err.Message;
            return false;
        }

        //Evaluate message:
        if (CheckResponse(response))
        {
            //Return requested register values:
            for (int i = 0; i < (response.Length - 5) / 2; i++)
            {
                values[i] = response[2 * i + 3];
                values[i] <<= 8;
                values[i] += response[2 * i + 4];
            }

            modbusStatus = "Read successful";
            return true;
        }
        else
        {
            modbusStatus = "CRC error";
            return false;
        }
    }
    else
    {
        modbusStatus = "Serial port not open";
        return false;
    }
}

This public function accepts four variables - address (slave ID), start (register to begin read at), registers (quantity of registers to read) and values (reference to a byte array to contain the read values). The function will return true for a successful read or false for an erroneous read.

The BuildMessage() function simply places each byte into the desired modbus message format as well as passes this message to the CRC calculator. GetResponse() reads a byte stream of set length from the serial port and places this result into the response[] array. These functions are very straightforward and can be examined by downloading the sample code. Following the read and write routines, the requested data is handled by placing it into the short[] array passed by reference to this function. These values are then made available to the user application.

The public string modbusStatus contains pertinent information at various points during the modbus communication and makes this information available to the user application during program execution. This can be seen throughout the sample application.

SendFc16 - Write Multiple Registers

C#
public bool SendFc16(byte address, ushort start, 
    ushort registers, short[] values)
{
    //Ensure port is open:
    if (sp.IsOpen)
    {
        //Clear in/out buffers:
        sp.DiscardOutBuffer();
        sp.DiscardInBuffer();
        
        //Message is 1 addr + 1 fcn + 2 start + 2 reg + 1 count + 
            2 * reg vals + 2 CRC
        byte[] message = new byte[9 + 2 * registers];

        //Function 16 response is fixed at 8 bytes
        byte[] response = new byte[8];

        //Add bytecount to message:
        message[6] = (byte)(registers * 2);

        //Put write values into message prior to sending:
        for (int i = 0; i < registers; i++)
        {
            message[7 + 2 * i] = (byte)(values[i] >> 8);
            message[8 + 2 * i] = (byte)(values[i]);
        }

        //Build outgoing message:
        BuildMessage(address, (byte)16, start, registers, ref message);
        
        //Send Modbus message to Serial Port:
        try
        {
            sp.Write(message, 0, message.Length);
            GetResponse(ref response);
        }
        catch (Exception err)
        {
            modbusStatus = "Error in write event: " + err.Message;
            return false;
        }

        //Evaluate message:
        if (CheckResponse(response))
        {
            modbusStatus = "Write successful";
            return true;        
        }
        else
        {
            modbusStatus = "CRC error";
            return false;
        }
    }
    else
    {
        modbusStatus = "Serial port not open";
        return false;
    }
}

This function behaves in a similar fashion to SendFc3. Again we are passing four variables to the function, only this time instead of passing a byte array by reference to receive the read values, we are passing a populated array of values to use when writing the relevant registers. Again, we receive a boolean value depicting the success, or lack thereof, of the write command.

As previously mentioned, the CheckResponse function does nothing besides a simple CRC check. The CRC is recalculated for the incoming message and this value is compared against that passed as part of the message itself - hence checking the validity of the response. Any message framing error should result in a CRC exception.

Points of Interest

Again, it's important to reiterate the downfalls of the event handling in the SerialPort DataReceived event. A simple Google search will locate several frustrated coders explaining identical situations with few solutions suggested. This site contains one such solution and whether or not to pursue this line of coding is up to the individual. A better implementation of the modbus protocol would contain an override for this event and a more finely tuned data reception function.

Note that the public functions found in this modbus class were built with the intention that they can be used in dedicated threads in a user-made application. The sample application demonstrates the use of these functions in a thread created by the System.Timers.Timer.Elapsed event. It should be easy to find other uses that will fit your specific needs.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer BLAMO! Media
United States United States
http://www.blamomedia.ca

Comments and Discussions

 
QuestionProblem with decimal values Pin
txirrita17-Jan-14 5:10
txirrita17-Jan-14 5:10 
Questionwould you please share your source Pin
Member 103143478-Oct-13 17:24
Member 103143478-Oct-13 17:24 
QuestionSendFc3 Problem Pin
Paul148-Oct-13 1:31
Paul148-Oct-13 1:31 
QuestionKeep connection Pin
ZoingZoing29-Aug-13 22:27
professionalZoingZoing29-Aug-13 22:27 
Questionproblem Pin
pedram198119-Aug-13 4:28
pedram198119-Aug-13 4:28 
AnswerRe: problem Pin
ZoingZoing29-Aug-13 22:12
professionalZoingZoing29-Aug-13 22:12 
BugReading bug with float value Pin
Lycian30-Jul-13 3:46
Lycian30-Jul-13 3:46 
GeneralMy vote of 5 Pin
Owen 88829-Jul-13 20:05
Owen 88829-Jul-13 20:05 
Excellent code. It works. Thanks a lot
QuestionCompile problem Pin
Korpus9130-Jun-13 20:10
Korpus9130-Jun-13 20:10 
AnswerRe: Compile problem Pin
Owen 88829-Jul-13 20:04
Owen 88829-Jul-13 20:04 
GeneralMy vote of 5 Pin
Tejas_Pathak11-Jun-13 18:11
Tejas_Pathak11-Jun-13 18:11 
Generalwriting problems in the Modbus Poll cs (C#) Pin
Member 1037070412-Jan-14 7:24
Member 1037070412-Jan-14 7:24 
GeneralRe: writing problems in the Modbus Poll cs (C#) Pin
Member 114377575-May-15 22:08
Member 114377575-May-15 22:08 
Questiondisplay value in datagridview .... Pin
EmmaFinn3-Jun-13 19:57
EmmaFinn3-Jun-13 19:57 
AnswerRe: display value in datagridview .... Pin
Owen 88829-Jul-13 20:27
Owen 88829-Jul-13 20:27 
QuestionTCP/IP Pin
caioshin29-May-13 4:38
caioshin29-May-13 4:38 
GeneralMy vote of 5 Pin
Member 66486830-Apr-13 22:58
Member 66486830-Apr-13 22:58 
QuestionRegarding the response from serial port from modbus functon Pin
Jagadisha_Ingenious14-Apr-13 21:02
Jagadisha_Ingenious14-Apr-13 21:02 
AnswerRe: Regarding the response from serial port from modbus functon Pin
Member 103707043-Jan-14 10:21
Member 103707043-Jan-14 10:21 
QuestionOther functons in Modbus Pin
Jagadisha_Ingenious2-Apr-13 21:16
Jagadisha_Ingenious2-Apr-13 21:16 
AnswerRe: Other functons in Modbus Pin
Member 1157936613-Apr-15 6:25
Member 1157936613-Apr-15 6:25 
Questionit is working for only 100 address values 251 to 351, pls help how to read 500 addresses Pin
reddy santosh10-Jan-13 16:01
reddy santosh10-Jan-13 16:01 
AnswerRe: it is working for only 100 address values 251 to 351, pls help how to read 500 addresses Pin
Owen 88829-Jul-13 20:12
Owen 88829-Jul-13 20:12 
QuestionModBus device emulator Pin
Instead srl25-Oct-12 10:46
Instead srl25-Oct-12 10:46 
QuestionVisual Basic Code, anyone? Pin
juita23-Oct-12 4:19
juita23-Oct-12 4:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.