What is Modbus Communication Protocol & How to Implement Modbus RTU with Arduino

Learn everything about the industry's favorite Modbus serial communication protocol and use Arduino to implement your first Modbus RTU project.
What is Modbus Serial Communication Protocol and How to Implement Modbus RTU with Arduino by CIRCUITSTATE Electronics Featured Image

In our RS-485 tutorial, we learned about everything that made the interface the de facto standard for industrial automation and communication everywhere around the world. It is the reliability and simplicity of the RS-485 physical layer that made it such a favorite interface. However, since the RS-485 standard did not come with a data format or data transport protocols, something was needed to fill that gap and bring out the full potential of the interface. That thing was Modbus. Modbus is an open data protocol originally developed by Modicon (now Schneider) in 1979 for PLCs (Programmable Logic Controllers. PLCs are electronic devices used in industries to control and monitor various equipment and processes. Modbus became popular due to its royalty-free nature, simplicity, and ease of maintenance. Today, Modbus and its improved versions are used by all types of industrial equipment and controllers. So it is really important to learn Modbus if you want to develop electronic products for industrial applications. In this tutorial, we are going to learn everything about Modbus, and show how you can implement Modbus protocol in any Arduino-supported boards.

What is RS485 and how to use MAX485 and Arduino for long distance serial communication featured image

What is RS-485 & How to Use MAX485 with Arduino for Reliable Long-Distance Serial Communication

Learn about the industry-favorite RS-485 (EIA-485) wired communication interface standard and learn how to interface the MAX485 module with Arduino.

Need RS-485 or Modbus for your project?

CIRCUITSTATE can design and develop complete electronic products incorporating communication interfaces and protocols like RS-485, RS-232, 4-20mA, HART, Ethernet, MODBUS, etc. on all hardware platforms and frameworks, with robust performance. Contact us today to share your requirements.

Electronics Networking Vector Image

What is Modbus

Modbus is a request/response-based messaging protocol. While the messaging sits at the application layer of the OSI model, the physical interface being used can be Serial (UART, RS-485, RS-232, etc.), TCP (Ethernet), and others. Let’s summarise the most important features and aspects of Modbus in a list, and later we will explain them in detail.

  1. Modbus is a messaging protocol that sits at the application layer, the top layer of the OSI model.

  2. The messaging is based on a request/response scheme between a client and a server.

  3. Modbus can use any underlying layers for transporting data in binary or ASCII formats.
Modbus example netwrok vector illustration by CIRCUITSTATE Electronics
Modbus network example

Topology

Modbus assumes a bus topology for its communication network. For physical interfaces like RS-485, it is practically true, since they are usually multi-drop buses. But for interfaces like Ethernet (Modbus TCP/IP), it doesn’t have to be. Every device connected to the bus can be considered as a node. Data transmitted by a node is visible to all other nodes on the bus. If two nodes try to transmit at the same time, there can be data collisions. Therefore, there should be always a single node that is responsible for carrying out collision-free communication. Such a node is called a Client (master in obsolete terms). A client is responsible for initiating a new request. Every other node listening to such requests is called Server (slave in obsolete terms), because it is the node that responds to the request by serving the requested data. Modbus uses this Client-Server hierarchy for messaging. There should be at least two nodes in the bus for it to be possible. The words client and server are replaced by other words in some circumstances. They are shown in the table below. The terms master and slave are obsolete and no longer preferred by new implementations.

Request InitiationAction & Response
ClientServer
CentralPeripheral
MasterSlave
The different terms used in Modbus

A client can initiate a request whenever the bus is free. Only the server with the correct address can respond to a request from a client. All other servers must remain silent and must not interfere with the communication. But what if a server with the address the client is trying to communicate is not present on the bus? In such a case, the client can wait for a certain time period for a valid response from the addressed client and retry the transmission if required. If a response is not received within a timeout period, the client can assume that the server is not present on the bus or there is an issue with the communication. That said, the client should have a list of addresses of all servers on the bus, because only then can the client find the devices on the bus faster.

Physical Interfaces

Since Modbus is an application layer protocol, any physical interface that supports carrying digital data can be used to deploy Modbus. However, due to the prevalence of Modbus in industrial environments, the types of interfaces mainly used for Modbus are the ones that are the most robust.

  1. RS-232 – RS-232 is a legacy serial communication interface standard. Even though modern serial interfaces like USB have replaced it, many of the terms are still used today. Since RS-232 is a serial interface, we can use it for Modbus RTU and Modbus ASCII.

  2. RS-485 – Compared to RS-232, the RS-485 has higher speeds, longer cables, and the signaling is more robust. Modbus over RS-485 is probably the most popular way of deploying Modbus protocol. We can use either Modbus RTU or Modbus ASCII.

  3. Ethernet – Ethernet is undoubtedly one of the best when it comes to high-volume data transfer, second to only optical fibers. The versatility of the Ethernet makes it the favorite of all industries. We can implement virtually any data protocol over Ethernet. It’s a matter of encapsulating the data and sending it to a destination. Modbus TCP/IP is tailored for Ethernet implementations.

  4. Wireless – As long as a communication interface allows reliable and addressable data transfer and a multi-drop scheme, it is suited for Modbus. Depending on the capabilities of the wireless link, any mode of Modbus can be implemented. However, industries prefer wired links over wireless due to the unpredictability and the complex nature of wireless transmission and networks.

Protocol Versions

As many new physical interface standards were introduced over the years, the Modbus protocol has been adapted to suit those interfaces to make communication efficient.

  1. Modbus RTU – RTU stands for Remote Terminal Unit and is mainly used for serial communication. Serial communication interfaces include UART, RS-485, RS-232, etc. In Modbus RTU, the data is grouped into individual frames and represented in binary format. RTU messages can be commands or data (request/response). Each data frame ends with a basic CRC code to detect errors in transmission.

  2. Modbus ASCII – This is similar to the Modbus RTU except all data is sent as ASCII equivalents using only ASCII characters. For example, an integer value like 153 is sent as three characters “1”, “5” and “3” instead of the direct representation of the number in binary format. This has the advantage that you can view the messages in human-readable formats and makes it easy to debug communication. But as a side effect, it doubles the data bandwidth required in most cases.

  3. Modbus TCP/IP – As the name suggests, this is an adaption for the Ethernet interface. In this, the data is transported as TCP/IP packets through Ethernet cables by taking advantage of the speed and addressing capabilities of TCP/IP. Ethernet is also an efficient, fast, and robust communication interface which makes it ideal for industrial applications.

  4. Modbus UDP – TCP/IP is not the only protocol that can be used in Ethernet. Some Modbus implementations take advantage of the low latency and low overhead of the UDP.

There are a few other application-tailored implementations from various companies and organizations. You can find more about them on the Wikipedia page.

Message Frame Format

Modbus message frame format by CIRCUITSTATE Electronics
Modbus message frame format

Despite the different ways Modbus can transfer data over a physical medium, all they have in common is the message format. As we said earlier, Modbus employs a request/response scheme for all communication and it is based on frames. A Modbus message frame is the smallest data unit you can send or receive. The format of the Modbus RTU message frame is as follows.

FieldStartDevice AddressFunction CodeDataCRC CodeEnd
Length (bits)3.5 * 888Variable163.5 * 8
Byte Order==Hi firstLo first

The function code and data are together called a Protocol Data Unit or PDU. The PDU + Address + CRC is called an Application Data Unit (ADU). Modbus protocol defines three types of PDUs,

  1. Request PDU – request made by a client node.
  2. Response PDU – response generated by a server node.
  3. Exception PDU – exception response generated by a server node.

There are size limitations to Modbus ADU and PDU due to constraints applied to legacy implementations. The maximum ADU size in bytes is 256. This makes 253 bytes available for the PDU after a 1 byte address and 2 bytes CRC. After the 1-byte function code, we get 252 bytes for the actual data. All PDUs have the same constituents regardless of their type. Let’s now examine the parts of a Modbus message frame.

Start

Start indicates the start of the frame. Every frame should start with at least 3.5 * 8 bits (25 bits) of silence on the bus, which is also called a Mark condition.

Device Address

Also called a station address, it is used to select one device from all the devices connected to a bus. Only the device whose address matches the address in the frame can reply to a request from a central device. So the address should be unique. The length of the address field is 8 bits which yields a possible combination of a maximum of 256 devices. But practically, the number of devices on the bus will be limited by the type of physical interface being used. For example, the default limit on the number of nodes on an RS-485 bus is 32.

When a client sends a request to the server, the ADU contains the device address of the server. Similarly, when the server responds it should also use its own device address instead of the client’s address. That means, both a request and a subsequent response packets should have the same device address. The client uses the device address to tell which device it wants to talk to, and the server uses the device address to tell which device the response is originating from.

Function Code

This is similar to a command. It is a single byte that indicates what action has to be performed upon receiving the message. There are three types of function codes – PublicUser-Defined, and Reserved. Function code 0 is not valid. So you can not use that. Function codes from 128 to 255 are reserved for indicating exception conditions. We will see how exceptions are conveyed further in this tutorial. We will also explain in detail the main types of function codes you will use typically.

Modbus function code categories diagram
Modbus function code categories

Data

The data field is variable and depending on the function code and the response, the data length, type, and format can vary. But regardless of the type of data, Modbus always uses BIG-Endian format for the byte order. That means the Hi byte is sent first, followed by the Lo byte. For example, a 16-bit value 0x1234 is sent as 0x12 followed by 0x34. But that does not mean that the most significant bit (MSB) of a data byte is sent first. Which bit to send first depends on the type of physical interface used. For example, a UART, often used to drive an RS-485 bus, sends LSB first. The data field can contain exception codes (during exceptions), address ranges, and actual user data.

CRC Code

Cyclic Redundancy Check (CRC) code is a 16-bit data that can be used to indicate bit errors in the data. The sending node will calculate a CRC based on the frame data it has and append the frame with it. If even only one bit is corrupted during transmission, the receiving node will fail to calculate the same CRC. So a mismatching CRC is an indication of an error in the message. Unlike other data in the packet, the CRC uses small-Endian for the byte order. So the Lo byte is sent first followed by the Hi byte.

End

Similar to the start condition, an end condition is also indicated by 28 bits of silence on the bus. This will allow the next message frame to start immediately after an end condition.

Data Structure

Modbus is purely a software-defined protocol. This means, there are no specific types of hardware or memory associated with it. A device must manage its own memory and provide an abstraction layer to the Modbus application in order to manage the data. The Modbus protocol defines how you can effectively organize your data in your system’s memory. Modbus data is organized into tables and there are four primary tables.

Primary TableAccessSizeAddress Space
CoilRead-Write1 bit0 – 65535
Discrete InputRead-Only1 bit0 – 65535
Input RegisterRead-Only16 bits0 – 65535
Holding RegisterRead-Write16 bits0 – 65535

Coil

What is a coil you ask? At the time Modbus was created, the main things the inventors wanted to control were PLCs that had relays inside them. You turn on a relay by energizing its coil. So a coil can remain in one of the two states, ON and OFF. In digital terms that can be represented as a binary bit which can be either 0 or 1. So historically, the data used to save single bits in Modbus started to be known as a coil. Coils are also sometimes called contacts.

Coils store a single bit of data and they have both read and write accesses. So you can set the coil to either 0 or 1 or read its current state. The address space defines how much coil data can be in your system. As per Modbus protocol version V1.1b3, you can have 216 individual coil data items in a Modbus data table and they can be addressed from 0 to 65535. The reason why we can have 65536 items is because, obviously, all of them have a 16-bit address. Not to be confused with the coil data object and its address.

But how do we actually store coils in system memory? Do we use a single byte for each coil, only to store 0 or 1? The answer is it is up to you. The Modbus protocol doesn’t explain how you should store the data tables in your system’s memory because different systems have different properties. If you only have a handful of coil data to manage, you can use plain bytes to store them. Otherwise, you can store each coil as bits of a byte. So a single byte can store 8 coils. We will show how you can do this further down in this tutorial.

Discrete Input

Discrete Input is the same as coil data but it is read-only. It can be used to read, for example, a switch, GPIO pin, sensor, etc. Discrete input also has an address range of 0 – 65535.

Input Register

This is a 16-bit data register. It can only be used to store inputs from a system and therefore it is read-only. Writing you an input-only register will cause an exception. Input register data is stored as two bytes and the high-byte is always sent first. Input registers have an address range of 0 – 65535. Just because the address range is very large doesn’t mean you have to allocate memory for all of them. You just need to allocate memory for just enough data your system needs.

Holding Register

A Holding Register is a general-purpose data register that you can read or write. It is also 16-bit wide and stored as a pair of bytes. Holding registers also have an address range of 0 – 65535.

The data blocks of Modbus don’t necessarily be separate in the memory. Instead, they can also reside in the memory in a shared form. For example, a Coil data can be part of a Holding Register and such. You are free to implement them as you wish.

Data Types

So we know we can send data using the data field of the Modbus message frame. But what kind of data can we send? For example, how do we send a floating point number? How do we send an integer? How do we send a boolean value? Well, it is possible for a user to send data in any format they like, as long as their systems are interoperable.

NameDescriptionRange
INT1616-bit signed integer (1 word)-32768…+32767
UINT1616-bit unsigned integer (1 word)0…65535
INT3232-bit signed integer (2 words)-2 147 483 648…+2 147 483 647
UINT3232-bit unsigned integer (2 words)0…4 294 967 295
INT6464-bit signed integer (4 words)-9 223 372 036 854 775 808…9 223 372 036 854 775 807
UINT6464-bit unsigned integer (4 words)0 to 18 446 744 073 709 600 000
Float3232-bit value (2 words)-3.4028E+38… +3.4028E+38
ASCII8-bit alphanumeric characterTable of ASCII Characters
BITMAP16-bit field (1 word)

Messaging

Let’s now talk about the steps carried out by Modbus devices in order to send and receive data. As we said earlier, Modbus employs a client/server scheme for its messaging. Only one device can act as a client/central node at a time. All other devices have to remain as server/peripheral nodes. Only a client device can initiate a message in the form of a request. The request will have the address of the server it wants to communicate with. The address should be a valid one and a node with that address must reply to the client’s request. The below diagram illustrates the steps involved in successful communication between a client and server.

The function code in the client’s request will have information about the action to be performed by the server. It can be writing or reading the data registers, for example. After initiating a request, a client must wait for the response from the server. If no response is received within a reasonable time, the communication should be assumed to have failed. The client can retry the request in that case. To prevent the client from waiting for a response indefinitely, a timeout is always implemented.

Modbus client-server error-free message transaction illustration
Modbus error-free message transaction

In a successful (error-free) communication, the server will perform the action instructed by the client and return a valid response. But if the client’s request is invalid or it has any error, the server will only return an exception code as a response. The exception code is calculated by making the MSB of the requested function code to 1. This has the effect of adding 128 to the function code. For example, if the function code is 3, then the exception code will be 3 + 128 = 131. Modifying the function code in the response message can only tell there was an exception. The actual exception code is sent just after the function code in the data field of the message frame. We will learn more about exception codes further below.

Modbus client-server exception message transaction illustration
Modbus exception message transaction

Function Codes

In this section, we will try to learn more about function codes. There are three types of function codes as we discussed earlier.

  1. Public Function Codes
    • Well defined by the Modbus protocol.
    • Codes that are guaranteed to be unique across systems and implementations.
    • Publicly documented.
    • Availability of conformal test.
  2. User-Defined Function Codes
    • There are two ranges of user-defined function codes – from 65 – 72 and 100 – 110 (decimal).
    • The user is free to define new function codes that are not specified by the protocol.
    • There is no guarantee the function codes will be unique across implementations.
  3. Reserved Function Codes
    • Functions codes currently being used by legacy implementations and are not available for public use.
Function TypeFunction NameFunction Code (Dec)Comment
Data AccessBit accessPhysical Discrete InputsRead Discrete Inputs2
Internal Bits or Physical CoilsRead Coils1
Write Single Coil5
Write Multiple Coils15
16-bit accessPhysical Input RegistersRead Input Registers4
Internal Registers or Physical Output RegistersRead Multiple Holding Registers3
Write Single Holding Register6
Write Multiple Holding Registers16
Read/Write Multiple Registers23
Mask Write Register22
Read FIFO Queue24
File Record AccessRead File Record20
Write File Record21
DiagnosticsRead Exception Status7serial only
Diagnostic8serial only
Get Com Event Counter11serial only
Get Com Event Log12serial only
Report Server ID17serial only
Read Device Identification43
OtherEncapsulated Interface Transport43
Public function codes

01 – Read Coils

This function code allows you to read one or more coils in a server node. In the data field of the request PDU, you have to specify the start address of the coil register you want to read and then the number of contiguous coil registers to read. The number of coils you can read at a time with a single PDU is limited by the size constraint of the PDU. Since we know the maximum PDU size is limited to 253, and 2 bytes will be used for the response overhead, only 251 bytes can be used to return the coil data. 251 * 8 = 2008 is the maximum number of coils you can read at a time. The formats for the request and normal response are given below.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code110x01
Starting Address20 – 655350x00 – 0xFFFF
Coil Count21 – 20000x01 – 0x07D0
Request PDU

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code110x01
Byte Count11 – 251 (N)0x01 – 0xFB (N)
Coil StatusnN or N+1N or N+1
Response PDU

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1290x01 + 0x80 = 0x81
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

Below is an example that demonstrates the actual use of function code 01 to read coils from addresses 19 – 37 (address starting at 0).

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x01Function Code0x01
Starting Address Hi0x00Byte Count0x03
Starting Address Lo0x13Coil Status 26 – 190xCD
Coil Count Hi0x00Coil Status 34 – 270x6B
Coil Count Lo0x13Coil Status 37 – 350x05

Since the starting address is a 16-bit value, we have to split it into two individual bytes Hi and Lo, and send them separately. The same has to be done for the coil count we want to read. Remember that we always send the Hi bytes first. The address range 19 – 37 tells that we need to read 19 coil states, including the addresses 19 and 37. So the decimal value 19 is converted to 0x13. We use hex representation throughout this tutorial to make showing them easier. You just need to keep in mind that regardless of the method we use to represent numbers in the documentation, all values have the same binary representation.

In order to return 19 bits (states of coils), we need at least 3 bytes. So the byte count in the response message is 0x03. The data bytes will follow the byte count field as shown in the table. Coil statuses from 26 – 19 hold 8 bits. The MSB (Most Significant Bit) of the data byte holds the value of coil with address 26 and the LSB (Least Significant Bit) of the data byte holds the value of 19. The first data byte 0xCD can be written as 1100 1101 in binary. The table below shows which coil address each bit corresponds to.

Bit Position76543210
Bit Value11001101
Coil Address2625242322212019
Value 0xCD

The last value 0x05 contains only 3 actual data bits of coils from 37 – 35. The remaining bits to the left of the byte are padded with 0 as shown in the table below.

Bit Position76543210
Bit Value00000101
Coil Address373635
Value 0x05

Now let’s see what the complete request frame and response frame look like. Suppose the server address we want the data from is 0x0C (Dec 12). The request will look like the following, represented as hex octets.

Request ADU = 0x0C 0x01 0x00 0x13 0x00 0x13 0x8D 0x1F

Let’s break that down.

    Server ID:              12 (decimal) |   0C (hexadecimal)
    Function:                1 (decimal) |   01 (hexadecimal)
    Register Offset:        19 (decimal) | 0013 (hexadecimal)
    Number Of Registers:    19 (decimal) | 0013 (hexadecimal)

    CRC:                  8077 (decimal) | 8D1F (hexadecimal)
    CRC Should Be:        8077 (decimal) | 8D1F (hexadecimal)

    Request: [0C] [01] [0013] [0013] [8D1F]
             |    |    |      |      |-> CRC16 (8077)
             |    |    |      |-> Number Of Coils (19)
             |    |    |-> Starting Address
             |    |-> Function Code (1)
             |-> Server ID (12)

As you can see, except for the first, and last two bytes, the data is the same as we have shown in the request and response frames. The server address can vary depending on where you want to get the data from and in our example, it is 0x0C and it is the first byte of the request frame. Remember that, the first byte can only be sent after a start condition. Since they are predefined, we won’t show them in the data tables.

The last two bytes are the CRC code and they are calculated automatically when the frame is generated. Note that the CRC is always sent Lo byte first. The CRC 8077 (dec) is 0x1F8D in hexadecimal. But it is sent as 0x8D and then 0x1F. The server upon receiving this request message will calculate the CRC using the data in the frame and compare it with the CRC in the message. If they are the same, the server can assume that there were no errors in the received request. To generate Modbus request messages and extract responses, you can use this online tool from N-Pulse.

02 – Read Discrete Inputs

Reading discrete input registers is similar to reading coils because, in both, data is represented as bits. In a request frame, we just need to specify the starting address (addresses start at 0) and the number of inputs we want to read. The returned value in the response is packed the same as the same way, as bits.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code120x02
Starting Address20 – 655350x00 – 0xFFFF
Input Count21 – 20000x01 – 0x07D0
Request PDU
FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code120x02
Byte Count11 – 251 (N)0x01 – 0xFB (N)
Input StatusnN or N+1N or N+1
Response PDU
FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1300x02 + 0x80 = 0x82
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

Below is an example of request and response frames to read discrete inputs from 196 – 217.

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x02Function Code0x02
Starting Address Hi0x00Byte Count0x03
Starting Address Lo0xC4Input Status 203 – 1960xAC
Input Count Hi0x00Input Status 211 – 2040xDB
Input Count Lo0x16Input Status 217 – 2120x35

Since you already know how request and response frames are generated, we won’t be showing them here.

03 – Read Holding Registers

Holding registers are 16-bit data registers that can be read or written. The function code 03 can be used to read a single or a contiguous set of holding registers. The request PDU must specify the starting address and the number of registers to read. You can read up to 125 registers (total 250 bytes) at a time.

The register data in the response message are packed as two bytes per register, with the binary contents right-justified within each byte. For each register, the first byte contains the high-order bits and the second contains the low-order bits.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code130x03
Starting Address20 – 655350x00 – 0xFFFF
Register Count21 – 125 (N)0x01 – 0x7D
Request PDU
FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code130x03
Byte Count12 * N0x02 – 0xFA
Register Valuen2 * N2 * N
Response PDU

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1310x03 + 0x80 = 0x83
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

The below table shows the request frame and a normal response frame PDUs for function code 03. We are requesting 3 holding registers starting with the address 0x6B (Dec 107). Since each holding register is 16-bit (a word), the response PDU will have 6 bytes representing the data. The data format is the same as explained before.

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x03Function Code0x03
Starting Address Hi0x00Byte Count0x06
Starting Address Lo0x6BRegister Value Hi (107)0x02
Register Count Hi0x00Register Value Low (107)0x2B
Register Count Lo0x03Register Value Hi (108)0x00
Register Value Low (108)0x00
Register Value Hi (109)0x00
Register Value Low (109)0x64
Request ADU = 0x0C 0x03 0x00 0x6B 0x00 0x03 0x75 0x0A
    Server ID:              12 (decimal) |   0C (hexadecimal)
    Function:                3 (decimal) |   03 (hexadecimal)
    Register Offset:       107 (decimal) | 006B (hexadecimal)
    Number Of Registers:     3 (decimal) | 0003 (hexadecimal)

    CRC:                  2677 (decimal) | 750A (hexadecimal)
    CRC Should Be:        2677 (decimal) | 750A (hexadecimal)

    Request: [0C] [03] [006B] [0003] [750A]
             |    |    |      |      |-> CRC16 (2677)
             |    |    |      |-> Number Of Registers (3)
             |    |    |-> Starting Address
             |    |-> Function Code (3)
             |-> Server ID (12)

04 – Read Input Registers

Input registers are read-only 16-bit registers. The function code 04 can be used to read one or more contiguous registers from a server. You can read up to 125 registers (250 bytes) at a time. The format is shown below.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code140x04
Starting Address20 – 655350x00 – 0xFFFF
Register Count21 – 125 (N)0x01 – 0x7D
Request PDU

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code140x04
Byte Count12 * N0x02 – 0xFA
Register Valuen2 * N2 * N
Response PDU

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1320x04 + 0x80 = 0x84
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

The below table shows the request frame and a normal response frame PDUs for function code 04. We are requesting a single input register at the address 0x08. Since each holding register is 16-bit (a word), the response PDU will have 2 bytes representing the data. The data format is the same as explained before.

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x04Function Code0x04
Starting Address Hi0x00Byte Count0x02
Starting Address Lo0x08Register Value Hi (107)0x02
Register Count Hi0x00Register Value Low (107)0x0A
Register Count Lo0x01
Request ADU = 0x0C 0x04 0x00 0x08 0x00 0x01 0xB1 0x15
    Server ID:              12 (decimal) |   0C (hexadecimal)
    Function:                4 (decimal) |   04 (hexadecimal)
    Register Offset:         8 (decimal) | 0008 (hexadecimal)
    Number Of Registers:     1 (decimal) | 0001 (hexadecimal)

    CRC:                  5553 (decimal) | B115 (hexadecimal)
    CRC Should Be:        5553 (decimal) | B115 (hexadecimal)

    Request: [0C] [04] [0008] [0001] [B115]
             |    |    |      |      |-> CRC16 (5553)
             |    |    |      |-> Number Of Registers (1)
             |    |    |-> Starting Address
             |    |-> Function Code (4)
             |-> Server ID (12)

05 – Write Single Coil

We know that coils are single-bit data that can be written or read. The function code 01 can be used to read them and 05 can be used to write to them. The format is a little different from the reading PDU. You can only write a single coil at a time. The starting address has to be specified in the PDU. The value of the coil can be sent as a 16-bit value. 0x0000 is for putting the coil state to 0 (OFF) and 0xFF00 is for putting the coil to state 1 (ON). Any other value is invalid and doesn’t affect the coil state.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code150x05
Starting Address20 – 655350x00 – 0xFFFF
Coil State20 or 652800x0000 or 0xFF00
Request and response PDUs

In a successful write to the coil, the response PDU will be an echo of the request PDU. A different PDU is sent as a response only during exceptions.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1330x05 + 0x80 = 0x85
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

Below is an example that sets the status of the coil at 172 to 1 (ON).

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x05Function Code0x05
Output Address Hi0x00Output Address Hi0x00
Output Address Lo0xACOutput Address Lo0xAC
Output Value Hi0xFFOutput Value Hi0xFF
Output Value Lo0x00Output Value Lo0x00
Request and response PDUs are the same for normal operation

06 – Write Single Register

The function code 06 can be used to write a single 16-bit register in the server. The normal response is an echo of the request PDU.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code160x06
Register Address20 – 655350x00 – 0xFFFF
Register Value20 – 655350x00 – 0xFFFF
Request and response PDUs

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1340x06 + 0x80 = 0x86
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

The below example shows writing the value 0x0003 to address 2.

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x06Function Code0x06
Register Address Hi0x00Register Address Hi0x00
Register Address Lo0x02Register Address Lo0x02
Register Value Hi0x00Register Value Hi0x00
Register Value Lo0x03Register Value Lo0x03
Request and response PDUs are the same for normal operation

15 – Write Multiple Coils

The function code 15 (0x0F) can be used to write multiple coil registers at once instead of using the single coil writing many times. The request PDU has to specify the starting address, the number of registers to write, the byte count, and the data. Since coil data is only 1 bit, they are packed into bytes and each bit position will correspond to one of the addressed coil registers.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1150x0F
Starting Address20 – 655350x00 – 0xFFFF
Register Count21 – 19680x01 – 0x07B0
Byte Count1NN
Output ValuesN0 – 2550x00 – 0xFF
Request PDU

N is the number of bytes you will need to pack the number of coil states you want to send. For example, if you have 8 coil states to send, you only need 1 byte. But if you have 9 states to send, then you need an extra byte. Any unused bit has to be made to 0. If you have N number of bytes to send, then you must include them at the end of the frame, with the Hi byte added first. Examine the example shown next.

The response PDU simply returns the number of registers written apart from the starting address.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1150x0F
Starting Address20 – 655350x00 – 0xFFFF
Register Count21 – 19680x0001 or 0x07B0
Response PDU

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1430x0F + 0x80 = 0x8F
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

In the following example, we want to write 10 coils starting from address 19 (0x13). For that, we need two bytes to carry the 10 bits of coil data. The remaining 6 bits will be 0. The data we are sending is 0xCD01 (0b1100 1101 0000 0001). The table below shows what each bit corresponds to.

Bytes0xCD0x01
Bit Position1514131211109876543210
Bit Value1100110100000001
Coil Address26252423222120193433323130292827
RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x0FFunction Code0x0F
Starting Address Hi0x00Starting Address Hi0x00
Starting Address Lo0x13Starting Address Lo0x13
Register Count Hi0x00Register Count Hi0x00
Register Count Lo0x0ARegister Count Lo0x0A
Byte Count0x02
Output Values Hi0xCD
Output Values Lo0x01

16 – Write Multiple Registers

Oftentimes you want to write multiple registers in one go. You can use the function code 16 (0x10) for that. You can write up to 123 contiguous 16-bit registers.

FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1160x10
Starting Address20 – 655350x00 – 0xFFFF
Register Count21 – 123 (N)0x01 – 0x7B
Byte Count12 * N2 * N
Register ValuesN * 20 – 2550x00 – 0xFF
Request PDU
FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1160x10
Starting Address20 – 655350x00 – 0xFFFF
Register Count21 – 1230x01 or 0x7B
Response PDU
FieldLength (Bytes)Value/Range (Dec)Value/Range (Hex)
Function Code1Function Code + 128 = 1440x10 + 0x80 = 0x90
Exception Code11/2/3/40x01/0x02/0x03/0x04
Exception PDU

Below is an example request to write two registers starting at address 0x01, with values 0x000A and 0x0102.

RequestResponse
Field NameValue (Hex)Field NameValue (Hex)
Function Code0x10Function Code0x10
Starting Address Hi0x00Starting Address Hi0x00
Starting Address Lo0x01Starting Address Lo0x01
Register Count Hi0x00Register Count Hi0x00
Register Count Lo0x02Register Count Lo0x02
Byte Count0x04
Register Value Hi (0x01)0x00
Register Value Lo (0x01)0x0A
Register Value Hi (0x01)0x01
Register Value Lo (0x01)0x02

There are still more function codes. There is no point in documenting all of them here when the official Modbus specification document has them all. We just wanted to give you the most important function codes and their use. Many of the examples shown here are taken from the official document.

Exception Codes

Exception codes are used to indicate exception conditions when a normal operation can not be carried out as expected. Exceptions can arise due to an issue in the client’s request, for example, asking for data in the invalid range, or be an issue on the server side. Exceptions are generated almost always by a server. During an exception, the server modifies the function code field in the incoming message by adding 128 to it. For example, if the original function code was 3, adding 128 produces 131. When this message is received by the client, it will know that there was an exception. But this only indicates the exception, We also need to tell what the exception was. This is achieved with a single byte that comes after the function code called Exception Code. The following list has the most important exception codes.

Code (Dec)TextDetails
1Illegal FunctionThe function code received in the query is not recognized or allowed by the server.
2Illegal Data AddressData addresses of some or all the required entities are not allowed or do not exist on the server.
3Illegal Data ValueValue is not accepted by the server.
4Server Device FailureAn unrecoverable error occurred while the server was attempting to perform the requested action.
5AcknowledgeThe server has accepted the request and is processing it, but a long duration of time is required. This response is returned to prevent a timeout error from occurring in the client. The client can next issue a Poll Program Complete message to determine whether the processing is completed.
6Server Device BusyThe server is engaged in processing a long-duration command; the client should retry later.
7Negative AcknowledgeThe server cannot perform the programming functions; the client should request diagnostic or error information from the server.
8Memory Parity ErrorThe server detected a parity error in memory; the client can retry the request.
10Gateway Path UnavailableSpecialized for Modbus gateways: indicates a misconfigured gateway.
11Gateway Target Device Failed to RespondSpecialized for Modbus gateways: sent when the server fails to respond.
Most used exception codes
  • If the server device receives the request without a communication error, and can handle the query normally, it returns a normal response.

  • If the server does not receive the request due to a communication error, no response is returned. The client program will eventually process a timeout condition for the request.

  • If the server receives the request, but detects a communication error (parity, LRC, CRC, etc.), no response is returned. The client program will eventually process a timeout condition for the request.

  • If the server receives the request without a communication error, but cannot handle it (for example, if the request is to read a non–existent output or register), the server will return an exception response informing the client of the nature of the error.

CSE_ArduinoRS485

CSE_ArduinoRS485 is an Arduino library from CIRCUITSTATE Electronics for implementing RS-485 communication on all Arduino-compatible microcontroller boards. It is a fork of the official ArduinoRS485 library from Arduino. We have added many improvements to it and removed many of the hardware dependencies. Unlike the official library, CSE_ArduinoRS485 supports both hardware and software serial (UART) interfaces. The open-source library is available on our GitHub and it is well documented on our RS-485 tutorial. You can check it out to learn more about the library.

What is RS485 and how to use MAX485 and Arduino for long distance serial communication featured image

What is RS-485 & How to Use MAX485 with Arduino for Reliable Long-Distance Serial Communication

Learn about the industry-favorite RS-485 (EIA-485) wired communication interface standard and learn how to interface the MAX485 module with Arduino.

Below is a sample code for printing incoming messages on a serial port used for RS-485. You need two serial ports for the demo. The code is written and tested on an ESP32 board, but you can adapt it to any Arduino-compatible board. For the RS-485 interface, we used the MAX485+CD4069 module. You first need to complete this test to make sure RS-485 is working with your board correctly.

//===================================================================================//

#include <Arduino.h>
#include <CSE_ArduinoRS485.h>

//===================================================================================//

#define PIN_RS485_RX        16
#define PIN_RS485_TX        17

#define PORT_USB            Serial
#define PORT_RS485          Serial2

//===================================================================================//
// Globals

// Create a new RS-485 port with (serial port, DE pin, RE pin, TX pin)
RS485Class RS485 (PORT_RS485, -1, -1, PIN_RS485_TX);

//===================================================================================//
// Forward declaration

void setup();
void loop();
void readRS485();

//===================================================================================//

void setup() {
  // Initialize default serial port
  PORT_USB.begin (115200);
  delay (1000);

  // Initialize the RS485 port manually
  PORT_RS485.begin (9600, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);

  // Initialize the RS485 object.
  RS485.begin();
  delay (1000);

  PORT_USB.println ("--- RS485 Test ---");
  delay (1000);
}

//===================================================================================//

void loop() {
  readRS485();

  // Get data from the USB port and send it to the RS485 port.
  if (PORT_USB.available()) {
    PORT_USB.print ("TX: ");
    String msg = PORT_USB.readStringUntil ('\n');
    PORT_USB.print (msg);
    RS485.print (msg);
  }

  delay (5);
}

//===================================================================================//
/**
 * @brief Read data from the RS485 port and print it to the USB port.
 * 
 */
void readRS485() {
  if (RS485.available()) {
    // Read incoming message from the RS-485 port
    String msg = RS485.readString();

    // Print the message as a string
    PORT_USB.print ("RX String: ");
    PORT_USB.println (msg);
    
    // Print the message as decimal values
    PORT_USB.print ("RX Dec: ");
    for (int i = 0; i < msg.length(); i++) {
      PORT_USB.print (msg [i], DEC);
      PORT_USB.print (" ");
    }
    PORT_USB.println();

    // Print the message as hex values
    PORT_USB.print ("RX Hex: ");
    for (int i = 0; i < msg.length(); i++) {
      if (msg [i] < 0x10) PORT_USB.print ("0");
      PORT_USB.print (msg [i], HEX);
      PORT_USB.print (" ");
    }
    PORT_USB.println();
    PORT_USB.println();
  }
}

//===================================================================================//
RS485-Print.ino

CSE_ModbusRTU

CSE_ModbusRTU is another open-source Arduino library from CIRCUITSTATE Electronics. The library helps you implement Modbus RTU protocol on all Arduino-compatible development boards. The library depends on the CSE_ArduinoRS485 library to create and manage the RS-485 physical interface. So you need to install both libraries to try out the sample Arduino code provided here. Arduino has an official library for Modbus RTU called ArduinoModbus but it is limited in functionalities, hard to understand, hard to maintain, and has narrow hardware compatibility. In contrast, the CSE_ModbusRTU library offers wider hardware compatibility, and the code is easy to write and maintain.

There are two example Arduino sketches provided in the library called ModbusRTU_Client_LED and ModbusRTU_Server_LED. We will demonstrate how actual Modbus RTU communication is carried out with the help of these two examples.

Arduino Code

ModbusRTU_Server_LED

ModbusRTU_Server_LED demonstrates the server node functionality of Modbus RTU. It can listen to incoming Modbus requests to write a single coil and turn an LED ON/OFF based on the coil value. Let’s see how the code works line by line. The example is written for ESP32 board but with a few modifications it will work on other boards as well.

//===================================================================================//
/**
 * @file ModbusRTU_Server_LED.ino
 * @brief This example demonstrates the use of a Modbus RTU server to listen for Modbus
 * requests and turn an LED on and off. The request can come from any Modbus RTU client
 * including the ModbusRTU_Client_LED example. We define 4 coils but only use the 0x00
 * one to turn the LED on and off. We are using hardware serial port for the RS-485
 * interface.
 * @date +05:30 04:45:28 PM 02-08-2023, Wednesday
 * @author Vishnu Mohanan (@vishnumaiea)
 * @par GitHub Repository: https://github.com/CIRCUITSTATE/CSE_ModbusRTU
 * @par MIT License
 * 
 */
//===================================================================================//

#include <CSE_ArduinoRS485.h>
#include <CSE_ModbusRTU.h>

//===================================================================================//

// You can define the serial port pins here.
#define PIN_RS485_RX        16
#define PIN_RS485_TX        17

#define PORT_RS485          Serial2 // The hardware serial port for the RS-485 interface

//===================================================================================//

const int ledPin = LED_BUILTIN;

// Declare the RS485 interface here with a hardware serial port.
RS485Class RS485 (PORT_RS485, -1, -1, PIN_RS485_TX); // (Serial Port, DE, RE, TX)

// Create a Modbus RTU node instance with the RS485 interface.
CSE_ModbusRTU modbusRTU (&RS485, 0x01, "modbusRTU-0x01"); // (RS-485 Port, Device Address, Device Name)

// Create a Modbus RTU server instance with the Modbus RTU node.
CSE_ModbusRTU_Server modbusRTUServer (modbusRTU, "modbusRTUServer"); // (CSE_ModbusRTU, Server Name)

//===================================================================================//

void setup() {
  // Initialize the default serial port for debug messages
  Serial.begin (115200);
  delay (1000);
  Serial.println ("CSE_ModbusRTU - Modbus RTU Server LED");

  // Initialize the RS485 port manually
  PORT_RS485.begin (9600, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);

  // Initialize the RS485 interface. If you are initializing the RS485 interface
  // manually, then the parameter can be empty.
  RS485.begin();

  // Initialize the Modbus RTU server
  modbusRTUServer.begin();

  // Configure the LED
  pinMode (ledPin, OUTPUT);
  digitalWrite (ledPin, LOW);

  // Configure four coils starting at address 0x00
  modbusRTUServer.configureCoils (0x00, 4);
}

//===================================================================================//

void loop() {
  // Poll for Modbus RTU requests
  int requestReceived = modbusRTUServer.poll();

  if ((requestReceived != -1) && (requestReceived < 0x80)) {
    Serial.println ("Request received");
    // Read the current value of the coil
    int coilValue = modbusRTUServer.readCoil (0x00);
  
    if (coilValue == 1) {
      // Coil value set, turn LED on
      digitalWrite (ledPin, HIGH);
    }
    else if (coilValue == 0) {
      // Coil value clear, turn LED off
      digitalWrite (ledPin, LOW);
    }
    else {
      Serial.println ("Error reading coil");
    }
  }
}

//===================================================================================//
ModbusRTU_Server_LED.ino

In order to use the CSE_ModbusRTU library, you need to add two header files. You need to install these libraries if you haven’t already. The libraries can be installed by downloading them as ZIP files and extracting them to the libraries folder of your Arduino IDE. They can also be installed from the Arduino library manager.

#include <CSE_ArduinoRS485.h>
#include <CSE_ModbusRTU.h>
C++

Next, we need to define the serial port pins and instances. Not all boards support customizing the serial port (UART) pins. Boards that use ESP32 (DevKit, FireBeetle, etc.) or RP2040 supports changing the pins used for UART. Boards like Uno do not support this feature. If your board supports custom UART pins, then you can define them in the code and use a proper initialization function to initialize the interface. In this case, we are using GPIO pins 16 and 17 for RX and TX respectively.

// You can define the serial port pins here.
#define PIN_RS485_RX        16
#define PIN_RS485_TX        17

#define PORT_RS485          Serial2 // The hardware serial port for the RS-485 interface
C++

ESP32 has multiple serial port instances. So we are using Serial2 (UART1) as the RS-485 port. This is where we have to connect the RS-485 module.

In the next line, we define the GPIO pins to be used for the LED. The LED_BUILTIN is available on most Arduino boards and it is connected to a debug LED. We will use this LED to indicate the coil status.

const int ledPin = LED_BUILTIN;
C++

In order to use CSE_ModbusRTU library, we need a CSE_ArduinoRS485 port. It can be created with the following line. The RS485Class constructor can accept four parameters – the serial port instance, DE pin, RE pin, and TX pin. Except for the serial port and DE pin, everything else is optional. If not sent, those parameters are set to -1 which disables any functions associated with them. Since we are using the MAX485+CD4069 RS-485 module with auto-direction control, we don’t need to send DE and RE pins, because we are not using them. This saves us precious GPIO pins. The TX pin can be used to apply predelays and post-delays. We are creating an RS-485 port instance called RS485.

// Declare the RS485 interface here with a hardware serial port.
RS485Class RS485 (PORT_RS485, -1, -1, PIN_RS485_TX); // (Serial Port, DE, RE, TX)
C++

In the following lines, we are creating a new CSE_ModbusRTU instance. We have to send three parameters to the constructor – the RS-485 port, device/node address, and a name. The name is not part of the Modbus protocol but it can be used for debugging purposes. modbusRTU is a Modbus protocol device instance only. We have not yet defined what its role is.

// Create a Modbus RTU node instance with the RS485 interface.
CSE_ModbusRTU modbusRTU (&RS485, 0x01, "modbusRTU-0x01"); // (RS-485 Port, Device Address, Device Name)
C++

The device role can be defined with the following line. Here, we are creating a new Modbus RTU server instance called modbusRTUServer. We have to send the CSE_ModbusRTU instance and name for the server.

// Create a Modbus RTU server instance with the Modbus RTU node.
CSE_ModbusRTU_Server modbusRTUServer (modbusRTU, "modbusRTUServer"); // (CSE_ModbusRTU, Server Name)
C++

In the setup() function, we will initialize the default Serial port for printing debugging messages. You can initialize it with any baudrate. After that, we can initialize the serial port used for the RS-485 which is PORT_RS485 (Serial2). The first two parameters are the baudrate and the UART configuration. ESP32 supports defining the pins during the initialization. So we are also sending the GPIO pins we need.

// Initialize the RS485 port manually with custom pins
PORT_RS485.begin (9600, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);
C++

If you are using Arduino Uno for running this example, you can initialize the port as shown below.

// Initialize the RS485 port manually without custom pins
PORT_RS485.begin (9600, SERIAL_8N1);
C++

Next, we can initialize the RS485 port with the code below. The begin() call can also accept serial port parameters except custom pins, and initialize the port. But we are not using that feature here. An empty begin() call sets the baudrate to 0 which disables the serial port initialization from within the library code.

// Initialize the RS485 interface. If you are initializing the RS485 interface
// manually, then the parameter can be empty.
RS485.begin();
C++

Next, we can initialize the Modbus RTU server with a single line.

// Initialize the Modbus RTU server
modbusRTUServer.begin();
C++

After initializing the LED pin, the next important step is to add data to the Modbus RTU server we just created. Since we only need coil data for this example, we are creating 4 coil data in the server. The configureCoils() function accepts two parameters – the starting address of the coils and the number of coils to be added. The memory for this data is automatically allocated, and if memory is not available, the operation can fail.

// Configure four coils starting at address 0x00
modbusRTUServer.configureCoils (0x00, 4);
C++

In the loop() function, we will listen for incoming Modbus requests using the polling method. With each call to poll(), the function waits for a timeout period. If no requests are received within the timeout, the function returns -1. If a valid request is received within the timeout, then the function code of the request will be returned and saved to requestReceived variable. So if the value of requestReceived is not -1 and less than 0x80 (anything >= 0x80 is an exception code and we don’t need to respond to it), then we can assume that we have received a valid Modbus request. We can also know which coils have been written, but for simplicity, we are omitting it.

// Poll for Modbus RTU requests
int requestReceived = modbusRTUServer.poll();
C++

We can then check the new value of the coil present at 0x00 using the following line.

// Read the current value of the coil
int coilValue = modbusRTUServer.readCoil (0x00);
C++

readCoil() will return 0 if the coil value is 0x00, 1 if the coil value is 0x00FF, and -1 if the coil is not present on the server. This is why coilValue is defined as a signed integer. In the lines following that we set the LED state depending on the value of coilValue.

Now how do we send valid Modbus requests to the server? We can do that by uploading the ModbusRTU_Client_LED example to another ESP32 board and hooking it up to the server ESP32 via two MAX485+CD4069 RS-485 modules as shown below. So let’s see how the client example works.

ModbusRTU_Client_LED

The ModbusRTU_Server_LED example is exactly the opposite of the ModbusRTU_Server_LED example. Instead of listening for Modbus requests, it periodically generates requests to write a single coil on a server on the bus. So if we hook up the client to the server, the LED on the server will be blinking periodically. Try uploading the following example to your second ESP32 board.

//===================================================================================//
/**
 * @file ModbusRTU_Client_LED.ino
 * @brief This example demonstrates the use of a Modbus RTU Client to send periodic
 * requests to a server at 0x01 to toggle the value of a single coil. We are using
 * hardware serial port for the RS-485
 interface.
 * @date +05:30 04:45:28 PM 02-08-2023, Wednesday
 * @author Vishnu Mohanan (@vishnumaiea)
 * @par GitHub Repository: https://github.com/CIRCUITSTATE/CSE_ModbusRTU
 * @par MIT License
 * 
 */
//===================================================================================//

#include <CSE_ArduinoRS485.h>
#include <CSE_ModbusRTU.h>

//===================================================================================//

// You can define the serial port pins here.
#define PIN_RS485_RX        16
#define PIN_RS485_TX        17

#define PORT_RS485          Serial2 // The hardware serial port for the RS-485 interface

//===================================================================================//

// Declare the RS485 interface here with a hardware serial port.
RS485Class RS485 (PORT_RS485, -1, -1, PIN_RS485_TX); // (Serial Port, DE, RE, TX)

// Create a Modbus RTU node instance with the RS485 interface.
CSE_ModbusRTU modbusRTU (&RS485, 0x02, "modbusRTU-0x02"); // (RS-485 Port, Device Address, Device Name)

// Create a Modbus RTU server instance with the Modbus RTU node.
CSE_ModbusRTU_Client modbusRTUClient (modbusRTU, "modbusRTUClient"); // (CSE_ModbusRTU, Client Name)

//===================================================================================//

void setup() {
  // Initialize the default serial port for debug messages
  Serial.begin (115200);
  delay (1000);
  Serial.println ("CSE_ModbusRTU - Modbus RTU Client LED");

  // Initialize the RS485 port manually
  PORT_RS485.begin (9600, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);

  // Initialize the RS485 interface. If you are initializing the RS485 interface
  // manually, then the parameter can be empty.
  RS485.begin();

  // Initialize the Modbus RTU client
  modbusRTUClient.begin();
  modbusRTUClient.setServerAddress (0x01); // Set the server address to 0x01
}

//===================================================================================//

void loop() {
  if (modbusRTUClient.writeCoil (0x00, 0xFF00) == -1) { // Turn the LED on
    Serial.println ("Turning LED on failed.");
  }
  else {
    Serial.println ("Turning LED on success.");
  }

  delay (1000);

  if (modbusRTUClient.writeCoil (0x00, 0x0000) == -1) { // Turn the LED off
    Serial.println ("Turning LED off failed.");
  }
  else {
    Serial.println ("Turning LED off success.");
  }
  delay (1000);
}

//===================================================================================//
ModbusRTU_Client_LED.ino

As we have already explained most of the code before, we will only explain the differences here. In the server example, we created a new Modbus RTU server called modbusRTUServer. But in this case, we are creating a new client instance called modbusRTUClient. The parameters are the same here also. Since this client will be the second device on the RS-485 bus, we will set the device/node address of the client to 0x02 during the instantiation of modbusRTU.

In the setup() function, we can initialize the modbusRTUClient and set the address of the server we can send requests to. Remember the difference between a node address (client address, 0x02) and a server address (0x01) in the context of a Modbus RTU client.

// Initialize the Modbus RTU client
modbusRTUClient.begin();
modbusRTUClient.setServerAddress (0x01); // Set the server address to 0x01
C++

In a Modbus RTU client, a coil on the server can be written using the following code.

modbusRTUClient.writeCoil (0x00, 0xFF00)
C++

The function will return the function code for a successful write, and -1 for a failed write. The operation can fail if,

  1. The server with the address is not present on the bus.
  2. There are no coils with the specified address on the server.
  3. The server did not process the request timely.

You can upload the server example to one board and the client example to the other board, and connect them together via RS-485 link. The requests generated by the client will be sent to the server and the server will respond by blinking its LED. You can also monitor the debug messages printed by both the server and the client using two separate serial monitor windows.

Connecting two FireBeetle ESP32 boards via MAX485+CD4069 RS-485 module
Connect two ESP32 boards like this and upload the server example to one board and the client example to the other board. The LED on the server will start to blink periodically.

The following screenshots show how requests generated by the client are received by the server and how they are processed.

USB-RS485 Adapter Dongle

The USB-to-RS485 adapter dongle is a generic product with a USB serial port on one end and a single RS-485 port on the other end. You can use it to send serial data through the RS-485 interface. The particular dongle we have uses CH340 as the USB to serial converter chip and MAX485 as the RS-485 transceiver.

Modbus Emulators

Modbus emulators are software that can emulate Modbus client and server devices. From a client emulator, you can send Modbus requests to a real Modbus server and get the responses from it. Similarly, a server emulator can respond to requests made by an actual Modbus server device. These functionalities can be used to test and debug Modbus devices and networks. There are quite a few Modbus emulators out there, some paid and some free. Unfortunately, most such applications have very ancient designs and don’t work properly on all devices, even the paid ones.

Modbus Mechanic is an open-source Modbus Client/Server emulator application by SciFiDryer (Matt). The application is written in Java and should run fine on all operating systems with Java RE. You can download the latest version from GitHub and run the JAR executable. Before opening the application, you must connect the development board to the computer. You can do this in two ways,

  1. By connecting the RS-485 serial port to the PC through a Serial-USB adapter. An RS-485 converter is not needed for this.

  2. By connecting the RS-485 serial port to an RS-485 converter and then connecting it to a USB-RS-485 converter adapter.

In both cases, you will get a COM port opened in the OS. For this demonstration, we connected the USB of the ESP32 board to the PC which created a COM14 serial port, and the second serial port used for RS-485 to the PC via USB-Serial adapter (COM19). So this created two serial ports. COM14 can be used to monitor the server/client and the USB-Serial adapter can be used to send and receive Modbus messages.

When you open the application, you will be able to see the list of COM ports currently available. Select the COM port used by the server device. The application will open as a client emulator by default so you must flash the ModbusRTU_Server_LED example to your ESP32. After selecting the COM port, simply set the configuration as below. The server address entered here is 0x01. You can select a function code, enter valid parameters, and click on the Transmit packet button. If everything is working, you will get the value back from the server as seen in the screenshots.

Modbus Mechanic Client Emulaotor Screenshot CIRCUITSTATE Electronics
Modbus Mechanic client emulator window
Modbus Mechanic Client Emulator Reading Coils from Server by CIRCUITSTATE Electronics
Reading a single coil from the server. Observe the debug messages printed by the server.
Modbus Mechanic Client Emulator Writing Coils to Server by CIRCUITSTATE Electronics
Writing a single coil with address 0x00 on a server.
Modbus Mechanic Client Emulator Illegal Data Request to Server by CIRCUITSTATE Electronics
Example of an illegal data request. Since there are no discrete input registers present on our server example, the server responds with a proper exception message which is decoded and displayed by the emulator application.

Similarly, you can upload the ModbusRTU_Client_LED example to the Arduino board and use the server functionality of Modbus Mechanic to test it.

Hope you find our tutorial helpful. Your feedback is very important to us. Please let us know how we can improve this tutorial in the comments.

  1. Modbus Application Protocol Specification V1.1b3 [PDF]
  2. Modbus – Wikipedia
  3. What is Modbus and How does it work? – Schneider
  4. Modbus Organization – Official Site
  5. What is Modbus? Types of Modbus – RealPars
  6. What is RS-485 & How to Use MAX485 with Arduino for Reliable Long-Distance Serial Communication – CIRCUITSTATE Electronics
  7. MAX485-CD4069 RS-485 Module with Auto Data Direction Control – Pinout Diagram & Reference
Share to your friends
Vishnu Mohanan

Vishnu Mohanan

Founder and CEO at CIRCUITSTATE Electronics

Articles: 88

Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.