What is Modbus Communication Protocol & How to Implement Modbus RTU with Arduino
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 RS-485 & How to Use MAX485 with Arduino for Reliable Long-Distance Serial Communication
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.
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.
- Modbus is a messaging protocol that sits at the application layer, the top layer of the OSI model.
- The messaging is based on a request/response scheme between a client and a server.
- Modbus can use any underlying layers for transporting data in binary or ASCII formats.
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 Initiation | Action & Response |
---|---|
Client | Server |
Central | Peripheral |
Master | Slave |
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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.
Field | Start | Device Address | Function Code | Data | CRC Code | End |
---|---|---|---|---|---|---|
Length (bits) | 3.5 * 8 | 8 | 8 | Variable | 16 | 3.5 * 8 |
Byte Order | – | = | = | Hi first | Lo 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,
- Request PDU – request made by a client node.
- Response PDU – response generated by a server node.
- 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 – Public, User-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.
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 Table | Access | Size | Address Space |
---|---|---|---|
Coil | Read-Write | 1 bit | 0 – 65535 |
Discrete Input | Read-Only | 1 bit | 0 – 65535 |
Input Register | Read-Only | 16 bits | 0 – 65535 |
Holding Register | Read-Write | 16 bits | 0 – 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.
Name | Description | Range |
---|---|---|
INT16 | 16-bit signed integer (1 word) | -32768…+32767 |
UINT16 | 16-bit unsigned integer (1 word) | 0…65535 |
INT32 | 32-bit signed integer (2 words) | -2 147 483 648…+2 147 483 647 |
UINT32 | 32-bit unsigned integer (2 words) | 0…4 294 967 295 |
INT64 | 64-bit signed integer (4 words) | -9 223 372 036 854 775 808…9 223 372 036 854 775 807 |
UINT64 | 64-bit unsigned integer (4 words) | 0 to 18 446 744 073 709 600 000 |
Float32 | 32-bit value (2 words) | -3.4028E+38… +3.4028E+38 |
ASCII | 8-bit alphanumeric character | Table of ASCII Characters |
BITMAP | 16-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.
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.
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.
- 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.
- 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.
- Reserved Function Codes
- Functions codes currently being used by legacy implementations and are not available for public use.
Function Type | Function Name | Function Code (Dec) | Comment | ||
---|---|---|---|---|---|
Data Access | Bit access | Physical Discrete Inputs | Read Discrete Inputs | 2 | |
Internal Bits or Physical Coils | Read Coils | 1 | |||
Write Single Coil | 5 | ||||
Write Multiple Coils | 15 | ||||
16-bit access | Physical Input Registers | Read Input Registers | 4 | ||
Internal Registers or Physical Output Registers | Read Multiple Holding Registers | 3 | |||
Write Single Holding Register | 6 | ||||
Write Multiple Holding Registers | 16 | ||||
Read/Write Multiple Registers | 23 | ||||
Mask Write Register | 22 | ||||
Read FIFO Queue | 24 | ||||
File Record Access | Read File Record | 20 | |||
Write File Record | 21 | ||||
Diagnostics | Read Exception Status | 7 | serial only | ||
Diagnostic | 8 | serial only | |||
Get Com Event Counter | 11 | serial only | |||
Get Com Event Log | 12 | serial only | |||
Report Server ID | 17 | serial only | |||
Read Device Identification | 43 | ||||
Other | Encapsulated Interface Transport | 43 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 1 | 0x01 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Coil Count | 2 | 1 – 2000 | 0x01 – 0x07D0 |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 1 | 0x01 |
Byte Count | 1 | 1 – 251 (N) | 0x01 – 0xFB (N) |
Coil Status | n | N or N+1 | N or N+1 |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 129 | 0x01 + 0x80 = 0x81 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
Below is an example that demonstrates the actual use of function code 01 to read coils from addresses 19 – 37 (address starting at 0).
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x01 | Function Code | 0x01 |
Starting Address Hi | 0x00 | Byte Count | 0x03 |
Starting Address Lo | 0x13 | Coil Status 26 – 19 | 0xCD |
Coil Count Hi | 0x00 | Coil Status 34 – 27 | 0x6B |
Coil Count Lo | 0x13 | Coil Status 37 – 35 | 0x05 |
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 Position | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Bit Value | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
Coil Address | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 |
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 Position | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Bit Value | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
Coil Address | – | – | – | – | – | 37 | 36 | 35 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 2 | 0x02 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Input Count | 2 | 1 – 2000 | 0x01 – 0x07D0 |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 2 | 0x02 |
Byte Count | 1 | 1 – 251 (N) | 0x01 – 0xFB (N) |
Input Status | n | N or N+1 | N or N+1 |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 130 | 0x02 + 0x80 = 0x82 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
Below is an example of request and response frames to read discrete inputs from 196 – 217.
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x02 | Function Code | 0x02 |
Starting Address Hi | 0x00 | Byte Count | 0x03 |
Starting Address Lo | 0xC4 | Input Status 203 – 196 | 0xAC |
Input Count Hi | 0x00 | Input Status 211 – 204 | 0xDB |
Input Count Lo | 0x16 | Input Status 217 – 212 | 0x35 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 3 | 0x03 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Count | 2 | 1 – 125 (N) | 0x01 – 0x7D |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 3 | 0x03 |
Byte Count | 1 | 2 * N | 0x02 – 0xFA |
Register Value | n | 2 * N | 2 * N |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 131 | 0x03 + 0x80 = 0x83 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
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.
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x03 | Function Code | 0x03 |
Starting Address Hi | 0x00 | Byte Count | 0x06 |
Starting Address Lo | 0x6B | Register Value Hi (107) | 0x02 |
Register Count Hi | 0x00 | Register Value Low (107) | 0x2B |
Register Count Lo | 0x03 | Register 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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 4 | 0x04 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Count | 2 | 1 – 125 (N) | 0x01 – 0x7D |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 4 | 0x04 |
Byte Count | 1 | 2 * N | 0x02 – 0xFA |
Register Value | n | 2 * N | 2 * N |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 132 | 0x04 + 0x80 = 0x84 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
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.
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x04 | Function Code | 0x04 |
Starting Address Hi | 0x00 | Byte Count | 0x02 |
Starting Address Lo | 0x08 | Register Value Hi (107) | 0x02 |
Register Count Hi | 0x00 | Register Value Low (107) | 0x0A |
Register Count Lo | 0x01 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 5 | 0x05 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Coil State | 2 | 0 or 65280 | 0x0000 or 0xFF00 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 133 | 0x05 + 0x80 = 0x85 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
Below is an example that sets the status of the coil at 172 to 1
(ON).
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x05 | Function Code | 0x05 |
Output Address Hi | 0x00 | Output Address Hi | 0x00 |
Output Address Lo | 0xAC | Output Address Lo | 0xAC |
Output Value Hi | 0xFF | Output Value Hi | 0xFF |
Output Value Lo | 0x00 | Output Value Lo | 0x00 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 6 | 0x06 |
Register Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Value | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 134 | 0x06 + 0x80 = 0x86 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
The below example shows writing the value 0x0003
to address 2.
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x06 | Function Code | 0x06 |
Register Address Hi | 0x00 | Register Address Hi | 0x00 |
Register Address Lo | 0x02 | Register Address Lo | 0x02 |
Register Value Hi | 0x00 | Register Value Hi | 0x00 |
Register Value Lo | 0x03 | Register Value Lo | 0x03 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 15 | 0x0F |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Count | 2 | 1 – 1968 | 0x01 – 0x07B0 |
Byte Count | 1 | N | N |
Output Values | N | 0 – 255 | 0x00 – 0xFF |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 15 | 0x0F |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Count | 2 | 1 – 1968 | 0x0001 or 0x07B0 |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 143 | 0x0F + 0x80 = 0x8F |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
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.
Bytes | 0xCD | 0x01 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit Position | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Bit Value | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Coil Address | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 34 | 33 | 32 | 31 | 30 | 29 | 28 | 27 |
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x0F | Function Code | 0x0F |
Starting Address Hi | 0x00 | Starting Address Hi | 0x00 |
Starting Address Lo | 0x13 | Starting Address Lo | 0x13 |
Register Count Hi | 0x00 | Register Count Hi | 0x00 |
Register Count Lo | 0x0A | Register Count Lo | 0x0A |
Byte Count | 0x02 | ||
Output Values Hi | 0xCD | ||
Output Values Lo | 0x01 |
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.
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 16 | 0x10 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Count | 2 | 1 – 123 (N) | 0x01 – 0x7B |
Byte Count | 1 | 2 * N | 2 * N |
Register Values | N * 2 | 0 – 255 | 0x00 – 0xFF |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | 16 | 0x10 |
Starting Address | 2 | 0 – 65535 | 0x00 – 0xFFFF |
Register Count | 2 | 1 – 123 | 0x01 or 0x7B |
Field | Length (Bytes) | Value/Range (Dec) | Value/Range (Hex) |
---|---|---|---|
Function Code | 1 | Function Code + 128 = 144 | 0x10 + 0x80 = 0x90 |
Exception Code | 1 | 1/2/3/4 | 0x01 /0x02 /0x03 /0x04 |
Below is an example request to write two registers starting at address 0x01
, with values 0x000A
and 0x0102
.
Request | Response | ||
---|---|---|---|
Field Name | Value (Hex) | Field Name | Value (Hex) |
Function Code | 0x10 | Function Code | 0x10 |
Starting Address Hi | 0x00 | Starting Address Hi | 0x00 |
Starting Address Lo | 0x01 | Starting Address Lo | 0x01 |
Register Count Hi | 0x00 | Register Count Hi | 0x00 |
Register Count Lo | 0x02 | Register Count Lo | 0x02 |
Byte Count | 0x04 | ||
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) | Text | Details |
---|---|---|
1 | Illegal Function | The function code received in the query is not recognized or allowed by the server. |
2 | Illegal Data Address | Data addresses of some or all the required entities are not allowed or do not exist on the server. |
3 | Illegal Data Value | Value is not accepted by the server. |
4 | Server Device Failure | An unrecoverable error occurred while the server was attempting to perform the requested action. |
5 | Acknowledge | The 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. |
6 | Server Device Busy | The server is engaged in processing a long-duration command; the client should retry later. |
7 | Negative Acknowledge | The server cannot perform the programming functions; the client should request diagnostic or error information from the server. |
8 | Memory Parity Error | The server detected a parity error in memory; the client can retry the request. |
10 | Gateway Path Unavailable | Specialized for Modbus gateways: indicates a misconfigured gateway. |
11 | Gateway Target Device Failed to Respond | Specialized for Modbus gateways: sent when the server fails to respond. |
- 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 RS-485 & How to Use MAX485 with Arduino for Reliable Long-Distance Serial Communication
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.inoCSE_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.inoIn 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.inoAs 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,
- The server with the address is not present on the bus.
- There are no coils with the specified address on the server.
- 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.
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,
- By connecting the RS-485 serial port to the PC through a Serial-USB adapter. An RS-485 converter is not needed for this.
- 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.
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.
Links
- Modbus Application Protocol Specification V1.1b3 [PDF]
- Modbus – Wikipedia
- What is Modbus and How does it work? – Schneider
- Modbus Organization – Official Site
- What is Modbus? Types of Modbus – RealPars
- What is RS-485 & How to Use MAX485 with Arduino for Reliable Long-Distance Serial Communication – CIRCUITSTATE Electronics
- MAX485-CD4069 RS-485 Module with Auto Data Direction Control – Pinout Diagram & Reference
Short Link
- Short URL to this page – https://www.circuitstate.com/modbusard