Chapter 178: Modbus RTU Slave Implementation
Chapter Objectives
After completing this chapter, you will be able to:
- Understand the role and responsibilities of a Modbus RTU Slave.
- Configure the ESP-IDF
freemodbus
component for Slave mode operation. - Initialize and start the Modbus Slave stack on an ESP32.
- Define and manage the data areas (coils, registers) exposed by the ESP32 slave.
- Implement callback functions to handle read/write requests from a Modbus Master.
- Formulate appropriate responses, including data and exception responses.
- Develop a practical Modbus RTU Slave application on an ESP32.
Introduction
Following our exploration of the Modbus RTU Master in the previous chapter, we now turn our attention to the other essential half of the Modbus pairing: the Slave. A Modbus Slave (or server, in Modbus TCP terminology) is a device that responds to requests initiated by a Modbus Master. It holds data in various register types (coils, discrete inputs, input registers, holding registers) and makes this data available to the master. It can also perform actions, like changing the state of a coil, upon the master’s command.
Implementing a Modbus Slave on an ESP32 enables your device to act as a sensor node, an actuator controller, or any peripheral device that needs to be monitored or controlled by a central system (the Master, which could be a PLC, SCADA software, or another microcontroller). This chapter will provide a comprehensive guide to developing a Modbus RTU Slave application on ESP32 platforms using the ESP-IDF freemodbus
component.
Theory
Role of the Modbus Slave
The Modbus Slave is a passive entity in the Modbus communication model. Its primary functions are:
- Listening for Requests: The Slave continuously listens on the Modbus network for messages addressed to its unique Slave ID.
- Address Recognition: When a message is received, the Slave checks if the Slave Address field in the message matches its own configured address. If not, it ignores the message (unless it’s a broadcast address 0, which specific slaves might process for certain functions without responding).
- Request Processing: If the address matches, the Slave parses the request to determine:
- The Function Code (e.g., Read Holding Registers, Write Single Coil).
- The Data Address (e.g., starting register number).
- The Quantity of data items (e.g., number of registers to read/write).
- Any Data Values provided by the Master (for write operations).
- Data Access: Based on the request, the Slave accesses its internal data storage (coils, discrete inputs, input registers, holding registers).
- Response Formulation:
- Normal Response: If the request is valid and can be processed, the Slave constructs a response frame containing its Slave Address, the original Function Code, the requested data (for read operations) or an echo of the request (for write operations), and a CRC.
- Exception Response: If the Slave cannot process the request due to an error (e.g., illegal function code, invalid data address, invalid data value), it constructs an exception response. This includes its Slave Address, the original Function Code with the most significant bit set, an exception code indicating the error type, and a CRC.
- Transmission: The Slave transmits the normal or exception response frame back to the Master.
graph TD subgraph "Modbus Slave Device" direction LR A[Listen for Frame]; A --> B{Frame Received}; B --> C{CRC Check}; C -- Invalid CRC --> A; C -- Valid CRC --> D{Address Match?}; D -- No --> A; D -- Yes --> E{Parse Request}; E --> F{Is Function<br>Supported?}; F -- No --> G["Formulate<br>Exception Response<br>(Code 01: Illegal Function)"]; F -- Yes --> H{Is Data Address<br>Valid?}; H -- No --> I["Formulate<br>Exception Response<br>(Code 02: Illegal Data Address)"]; H -- Yes --> J{"Is Data Value<br>Valid (for Writes)?"}; J -- No --> K["Formulate<br>Exception Response<br>(Code 03: Illegal Data Value)"]; J -- Yes --> L["Access Data Storage<br>(Read/Write)"]; L --> M{Operation<br>Successful?}; M -- No --> N["Formulate<br>Exception Response<br>(Code 04: Slave Device Failure)"]; M -- Yes --> O[Formulate<br>Normal Response]; G --> P[Transmit Response]; I --> P; K --> P; N --> P; O --> P; P --> A; end subgraph "External Modbus Master" Master_Req[Master sends<br>Request Frame] --> A; P --> Master_Resp[Master receives<br>Response Frame]; end classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class A,Master_Req,Master_Resp primary; class B,C,D,F,H,J,M decision; class E,L,G,I,K,N,O process; class P success;
Slave Data Storage and Management
A Modbus Slave maintains four primary types of data tables, as discussed in Chapter 176:
- Coils (Discrete Outputs): Read/Write single bits.
- Discrete Inputs (Status Inputs): Read-Only single bits.
- Input Registers: Read-Only 16-bit words.
- Holding Registers: Read/Write 16-bit words.
When implementing a slave, you need to decide:
- Which of these data types your device will support.
- The range of addresses for each type.
- How this data will be stored in the ESP32’s memory (e.g., arrays, structures).
- How this data will be updated (e.g., by sensor readings, application logic) and how master writes will affect it.
Handling Master Requests
The core of a slave implementation involves a loop or event-driven mechanism that:
- Receives a request.
- Validates the request (CRC, Slave ID, supported function code, valid address range, valid data values for writes).
- Performs the requested action:
- For read requests (Read Coils, Read Discrete Inputs, Read Holding Registers, Read Input Registers): Retrieve the data from the slave’s internal storage.
- For write requests (Write Single Coil, Write Single Holding Register, Write Multiple Coils, Write Multiple Holding Registers): Update the slave’s internal storage with the data provided by the master.
- Sends the response.
flowchart TD A["Start: Request for <i>Read Holding Registers</i> Received"] --> B{Validate Request}; subgraph B [Validation Checks] direction LR B1[Check Slave ID] --> B2[Check CRC] --> B3[Check Function Code] --> B4[Check Register Range]; end B -- All OK --> C{Calculate Memory Access}; C --> D["start_addr = Request Start Address<br>num_regs = Request Quantity"]; D --> E["array_index = start_addr - HOLDING_REG_START_ADDR"]; D --> F["bytes_to_read = num_regs * 2"]; E --> G{"Is (array_index + num_regs) > HOLDING_REG_COUNT?"}; G -- Yes --> H["Formulate Exception<br>(Illegal Data Address)"]; G -- No --> I[Loop: For each requested register]; subgraph I [Data Retrieval from Application Array] direction TB I1["reg_value = holding_registers[array_index]"] --> I2["Copy reg_value to Response Buffer"] --> I3["array_index++"]; end I --> J{All registers copied?}; J -- No --> I; J -- Yes --> K[Build Final Response Frame]; subgraph K [Response Assembly] direction TB K1["Slave ID"] --> K2["Function Code"] --> K3["Byte Count"] --> K4["Register Data..."] --> K5["Calculate and Append CRC"]; end K --> L[Transmit Response to Master]; H --> L; L --> Z[End]; classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class A,Z primary; class L success; class B,G,J decision; class C,D,E,F,H,I,K process; class B4,H check;
Using ESP-IDF freemodbus
for Slave Mode
The ESP-IDF freemodbus
component (also referred to as esp_modbus_slave
for slave-specific APIs) provides the necessary tools to create a Modbus Slave. Key aspects include:
- Initialization: Functions to initialize the Modbus stack in slave mode, setting the slave ID and serial communication parameters.
- Data Area Definition: A mechanism to define the slave’s data areas (coils, discrete inputs, input registers, holding registers) that are accessible to the master. This often involves registering these areas with the Modbus stack.
- Callback System (
mb_register_area_descriptor_t
): Theesp_modbus_slave
API uses a callback-based approach. You define “register area descriptors” (mb_register_area_descriptor_t
) that specify a memory region (e.g., an array in your application) for each data type (coils, discrete inputs, etc.). The Modbus stack then directly accesses these memory regions when a master requests data or writes data.- The
mb_register_area_descriptor_t
typically includes:start_offset
: The starting Modbus address for this area (0-based).type
: The Modbus data type (e.g.,MB_PARAM_HOLDING
,MB_PARAM_INPUT
,MB_PARAM_COIL
,MB_PARAM_DISCRETE
).address
: A pointer to the actual data storage buffer in your application’s memory.size
: The size of the data storage buffer in bytes.access_opts
(Optional): Fine-grained access control or custom handling via callbacks, though direct memory mapping is common for basic use.
- The
The Modbus stack handles the low-level protocol details (frame parsing, CRC, timing), allowing you to focus on managing the application data.
Practical Examples with ESP32
This section details how to configure and implement a Modbus RTU Slave on an ESP32 using the esp_modbus_slave
API.
1. Project Setup and Component Configuration
a. CMakeLists.txt in your main component:
Ensure freemodbus is listed as a requirement.
# main/CMakeLists.txt
idf_component_register(SRCS "app_main.c"
INCLUDE_DIRS "."
REQUIRES esp_event esp_log freemodbus)
b. sdkconfig.defaults (Optional but Recommended):
For a slave, you’ll need to define its Slave ID.
SDKConfig Parameter | Description | Example Value |
---|---|---|
CONFIG_FMB_COMM_MODE_RTU_EN |
Enables Modbus RTU communication mode. Must be set to ‘y’. | y |
CONFIG_FMB_UART_PORT_NUM |
Selects the UART peripheral to use for Modbus communication. | 1 (for UART1) |
CONFIG_FMB_UART_BAUD_RATE_xxxx |
Sets the communication speed. Only one baud rate option should be enabled. | CONFIG_FMB_UART_BAUD_RATE_9600=y |
CONFIG_FMB_UART_PARITY_xxx |
Sets the parity bit configuration (None, Even, or Odd). | CONFIG_FMB_UART_PARITY_NONE=y |
CONFIG_FMB_UART_TXD_PIN |
Specifies the GPIO pin number for the UART Transmit (TX) line. | 17 |
CONFIG_FMB_UART_RXD_PIN |
Specifies the GPIO pin number for the UART Receive (RX) line. | 16 |
CONFIG_FMB_UART_RTS_PIN |
Specifies the GPIO for Request-To-Send, used to control the DE/RE pins on an RS-485 transceiver. | 18 |
CONFIG_MB_CONTROLLER_SLAVE_ID_SUPPORT |
Enables support for the device to act as a Modbus slave. | y |
CONFIG_MB_CONTROLLER_SLAVE_ID |
Sets the unique Modbus address for this slave device on the bus (1-247). | 0x01 (for Slave ID 1) |
# sdkconfig.defaults
CONFIG_FMB_COMM_MODE_RTU_EN=y
CONFIG_FMB_UART_PORT_NUM=1 # Example: UART1
CONFIG_FMB_UART_BAUD_RATE_9600=y # Example: 9600 baud
CONFIG_FMB_UART_PARITY_NONE=y # Example: No parity
CONFIG_FMB_UART_TXD_PIN=17 # Example: GPIO17 for TX
CONFIG_FMB_UART_RXD_PIN=16 # Example: GPIO16 for RX
CONFIG_FMB_UART_RTS_PIN=18 # Example: GPIO18 for RS485 DE/RE
CONFIG_MB_CONTROLLER_SLAVE_ID_SUPPORT=y
CONFIG_MB_CONTROLLER_SLAVE_ID=0x01 # Default Slave ID for this device
CONFIG_MB_CONTROLLER_NOTIFY_TIMEOUT_MS=1000 # Notification timeout
Run idf.py reconfigure
after changes.
c. menuconfig Configuration:
Use idf.py menuconfig:
- Navigate to
Component config
—>Modbus configuration (FreeModbus)
—>- Ensure
Modbus communication mode (RTU)
is selected. - Set UART parameters (
UART port number
,baud rate
,parity
,TXD pin
,RXD pin
,RTS pin
). - Enable
Modbus controller slave ID support
. - Set
Modbus slave ID (0x00-0xF7)
to your desired slave address (e.g.,1
). - Configure
Modbus controller notification timeout (ms)
.
- Ensure
2. Hardware Setup
Identical to the Master setup (Chapter 177), as the physical layer (RS-485) is the same:
- ESP32 development board.
- RS-485 transceiver module.
- Connections: ESP32 TX to DI, RX to RO, RTS (configured GPIO) to DE/RE.
- Connect A/B lines to the RS-485 bus.
- Ensure proper bus termination if this slave is at an end of the bus.
3. Modbus Slave Implementation (app_main.c
)
This example demonstrates initializing the Modbus slave and defining data areas for holding registers, input registers, coils, and discrete inputs. The slave will allow a master to read these values and write to holding registers and coils.
stateDiagram-v2 direction TB [*] --> Uninitialized Uninitialized --> Initializing: slave_init() Initializing: mbc_slave_init()<br>mbc_slave_setup()<br>mbc_slave_set_descriptor() Initializing --> Setup_Error: Initialization Fails Initializing --> Ready: Initialization OK Ready --> Running: mbc_slave_start() Running: Listening for Master requests<br>Responding to requests Running --> Uninitialized: mbc_slave_destroy() Running --> Communication_Error: UART/Bus Error Communication_Error --> Running: Error resolved state "Error States" as Errors { Setup_Error Communication_Error } style Uninitialized fill:#FEE2E2,stroke:#DC2626,color:#991B1B style Initializing fill:#FEF3C7,stroke:#D97706,color:#92400E style Ready fill:#DBEAFE,stroke:#2563EB,color:#1E40AF style Running fill:#D1FAE5,stroke:#059669,color:#065F46 style Errors fill:#FED7D7,stroke:#9B2C2C,color:#9B2C2C
#include <stdio.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_event.h"
#include "nvs_flash.h" // For NVS initialization
// Modbus includes
#include "esp_modbus_common.h"
#include "esp_modbus_slave.h"
static const char *TAG = "MODBUS_SLAVE_APP";
// Define Modbus communication parameters (these will be overridden by menuconfig if set there)
#define MB_PORT_NUM CONFIG_FMB_UART_PORT_NUM // UART port number
#define MB_SLAVE_ADDR CONFIG_MB_CONTROLLER_SLAVE_ID // Configured Slave ID
#define MB_DEVICE_SPEED CONFIG_FMB_UART_BAUD_RATE // Baud rate
// Define the size of the Modbus data areas
#define HOLDING_REG_START_ADDR 0
#define HOLDING_REG_COUNT 10 // 10 Holding Registers (16-bit each)
#define INPUT_REG_START_ADDR 0
#define INPUT_REG_COUNT 5 // 5 Input Registers (16-bit each)
#define COIL_START_ADDR 0
#define COIL_COUNT 8 // 8 Coils (1-bit each)
#define DISCRETE_INPUT_START_ADDR 0
#define DISCRETE_INPUT_COUNT 4 // 4 Discrete Inputs (1-bit each)
// Data storage for Modbus registers/coils
// These arrays will be directly accessed by the Modbus stack
static uint16_t holding_registers[HOLDING_REG_COUNT];
static uint16_t input_registers[INPUT_REG_COUNT];
// For coils and discrete inputs, FreeModbus expects them packed into bytes.
// Each bit represents one coil/discrete input.
// (COIL_COUNT + 7) / 8 calculates bytes needed.
static uint8_t coils[(COIL_COUNT + 7) / 8];
static uint8_t discrete_inputs[(DISCRETE_INPUT_COUNT + 7) / 8];
// Initialize some default values for our slave's data areas
static void initialize_slave_data(void)
{
ESP_LOGI(TAG, "Initializing slave data areas...");
for (int i = 0; i < HOLDING_REG_COUNT; i++) {
holding_registers[i] = (i + 1) * 10; // Example: 10, 20, 30...
}
for (int i = 0; i < INPUT_REG_COUNT; i++) {
input_registers[i] = (i + 1) * 100; // Example: 100, 200, 300...
}
// Initialize coils (all off)
for (int i = 0; i < (COIL_COUNT + 7) / 8; i++) {
coils[i] = 0x00;
}
// Example: Set coil 0 ON (bit 0 of coils[0])
// coils[0] |= (1 << 0);
// Initialize discrete inputs (example pattern)
for (int i = 0; i < (DISCRETE_INPUT_COUNT + 7) / 8; i++) {
discrete_inputs[i] = 0x00;
}
// Example: Set discrete input 0 and 2 ON
// discrete_inputs[0] |= (1 << 0); // DI 0
// discrete_inputs[0] |= (1 << 2); // DI 2
}
// Modbus slave initialization function
static esp_err_t slave_init(void)
{
mb_communication_info_t comm_info;
comm_info.port = MB_PORT_NUM;
comm_info.mode = MB_MODE_RTU;
comm_info.slave_addr = MB_SLAVE_ADDR; // Set the slave address
comm_info.baudrate = MB_DEVICE_SPEED;
comm_info.parity = MB_PARITY_NONE; // Adjust as needed
void* slave_handler = NULL;
esp_err_t err = mbc_slave_init(MB_PORT_SERIAL_SLAVE, &slave_handler);
MB_RETURN_ON_FALSE((slave_handler != NULL), ESP_ERR_INVALID_STATE, TAG,
"Modbus slave init failed.");
MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG,
"Modbus slave init failed, error code: 0x%x.", (int)err);
err = mbc_slave_setup((void*)slave_handler, (void*)&comm_info);
MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG,
"Modbus slave setup failed, error code: 0x%x.", (int)err);
// Define the Modbus register areas that the slave will expose
// The Modbus stack will read from/write to these memory locations directly.
mb_register_area_descriptor_t reg_area_descriptors[] = {
// Holding Registers
{ .type = MB_PARAM_HOLDING, .start_offset = HOLDING_REG_START_ADDR, .address = (void*)holding_registers, .size = sizeof(holding_registers) },
// Input Registers
{ .type = MB_PARAM_INPUT, .start_offset = INPUT_REG_START_ADDR, .address = (void*)input_registers, .size = sizeof(input_registers) },
// Coils
{ .type = MB_PARAM_COIL, .start_offset = COIL_START_ADDR, .address = (void*)coils, .size = sizeof(coils) },
// Discrete Inputs
{ .type = MB_PARAM_DISCRETE,.start_offset = DISCRETE_INPUT_START_ADDR,.address = (void*)discrete_inputs, .size = sizeof(discrete_inputs) }
};
uint16_t num_reg_area_descriptors = (sizeof(reg_area_descriptors) / sizeof(reg_area_descriptors[0]));
err = mbc_slave_set_descriptor((void*)slave_handler, reg_area_descriptors, num_reg_area_descriptors);
MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG,
"Modbus slave set descriptor failed, error code: 0x%x.", (int)err);
ESP_LOGI(TAG, "Modbus slave stack initialized.");
// Start Modbus controller stack
err = mbc_slave_start((void*)slave_handler);
MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG,
"Modbus slave stack start failed, error code: 0x%x.", (int)err);
// UART driver is installed and configured by mbc_slave_start()
ESP_LOGI(TAG, "Modbus slave stack started with Slave ID: %d.", MB_SLAVE_ADDR);
return ESP_OK;
}
// Example task to simulate sensor updates or internal logic affecting slave data
static void simulate_sensor_updates_task(void *arg)
{
ESP_LOGI(TAG, "Sensor simulation task started.");
uint16_t counter = 0;
while(1) {
// Update an input register (e.g., a simulated sensor value)
// This demonstrates that the application can modify the data areas
// that the Modbus stack exposes.
input_registers[0] = counter++;
if (counter > 1000) counter = 0;
// Update a discrete input (e.g., a switch status)
// Toggle discrete input 1 (bit 1 of discrete_inputs[0])
if ((counter % 20) == 0) { // Toggle every 10 seconds if task delay is 500ms
discrete_inputs[0] ^= (1 << 1); // Toggle bit 1
ESP_LOGI(TAG, "Toggled Discrete Input 1. Current DI byte 0: 0x%02X", discrete_inputs[0]);
}
// Print current value of a holding register that master might change
ESP_LOGD(TAG, "Holding Register 0 (addr %d): %u", HOLDING_REG_START_ADDR, holding_registers[0]);
// Print current value of a coil that master might change
ESP_LOGD(TAG, "Coil 0 (addr %d) status: %s", COIL_START_ADDR, (coils[0] & (1 << 0)) ? "ON" : "OFF");
vTaskDelay(pdMS_TO_TICKS(500)); // Simulate update every 500ms
}
}
void app_main(void)
{
ESP_LOGI(TAG, "Initializing ESP32 Modbus RTU Slave...");
// Initialize NVS (needed for Wi-Fi, Bluetooth, but good practice)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// Initialize the event loop (not strictly needed for basic Modbus slave, but often used)
ESP_ERROR_CHECK(esp_event_loop_create_default());
// Initialize some data for the slave
initialize_slave_data();
// Initialize Modbus slave
ESP_ERROR_CHECK(slave_init());
// Create a task to simulate sensor updates or other application logic
// that might change the slave's data.
xTaskCreate(simulate_sensor_updates_task, "sensor_sim_task", 2048, NULL, 5, NULL);
ESP_LOGI(TAG, "Modbus Slave Application Initialized and Running.");
// The Modbus stack now handles incoming requests in the background.
}
sequenceDiagram participant App as app_main participant SimTask as simulate_sensor_updates_task participant Modbus as Modbus Slave Stack participant Master as Modbus Master App->>App: nvs_flash_init(), esp_event_loop_create_default() App->>App: initialize_slave_data() App->>Modbus: slave_init() Modbus->>Modbus: mbc_slave_init(), mbc_slave_setup() Modbus->>App: returns success App->>Modbus: mbc_slave_set_descriptor(...) App->>Modbus: mbc_slave_start() Modbus->>Modbus: Starts internal tasks, listens on UART App->>SimTask: xTaskCreate(simulate_sensor_updates_task) loop Application Logic SimTask->>SimTask: vTaskDelay() SimTask->>Modbus: Updates input_registers[0]++ Note right of SimTask: App logic directly modifies<br>the shared data array. SimTask->>Modbus: Toggles a bit in discrete_inputs end loop Master Communication Master->>Modbus: Sends "Read Input Registers" Request Modbus->>Modbus: Receives request, validates CRC, ID, etc. Modbus->>Modbus: Reads directly from input_registers array Modbus->>Master: Sends Response with updated values Master->>Modbus: Sends "Write Holding Register" Request Modbus->>Modbus: Receives request, validates Modbus->>Modbus: Writes new value to holding_registers array Modbus->>Master: Sends Write Acknowledgment Response SimTask->>SimTask: (in its own loop) Reads holding_registers array Note right of SimTask: App logic can now use the<br>value written by the Master. end
Important Notes on the Code:
- Data Storage: Global static arrays (
holding_registers
,input_registers
,coils
,discrete_inputs
) are used to store the Modbus data. The Modbus slave stack directly reads from and writes to these arrays based on master requests.mb_register_area_descriptor_t
: This structure is key. It maps Modbus address ranges and types to your application’s data buffers.
.type
: SpecifiesMB_PARAM_HOLDING
,MB_PARAM_INPUT
,MB_PARAM_COIL
, orMB_PARAM_DISCRETE
..start_offset
: The 0-based Modbus address for this block..address
: A pointer to your data buffer (e.g.,(void*)holding_registers
)..size
: The total size of your data buffer in bytes.- Coil and Discrete Input Packing: Coils and Discrete Inputs are bit-packed.
coils[0]
byte holds coils 0-7,coils[1]
holds 8-15, etc. Thefreemodbus
stack handles the bit manipulation.- Slave ID: The slave ID is configured via
menuconfig
(CONFIG_MB_CONTROLLER_SLAVE_ID
) and used duringmbc_slave_setup
.- Application Logic: The
simulate_sensor_updates_task
shows how your application can independently update the data areas (e.g.,input_registers
) that the Modbus master reads. Similarly, when a master writes toholding_registers
orcoils
via Modbus, your application can then read these updated values from the arrays and act upon them.
Field | Type | Description |
---|---|---|
.type |
mb_param_type_t |
Defines the Modbus data area type. Can be MB_PARAM_HOLDING , MB_PARAM_INPUT , MB_PARAM_COIL , or MB_PARAM_DISCRETE . |
.start_offset |
uint16_t |
The first Modbus address (0-based) for this data block. For example, if this is 0, it corresponds to Modbus address 40001 for holding registers. |
.address |
void* |
A pointer to the actual data buffer in the application’s memory. This is where the slave’s data is stored (e.g., an array like holding_registers ). |
.size |
uint16_t |
The total size of the data buffer in bytes. Crucially, this is not the number of registers. For an array of 10 uint16_t registers, size would be 10 * 2 = 20 . |
.access_opts |
void* |
Optional. Used for advanced scenarios like defining custom access control callbacks. For direct memory mapping, this is typically set to NULL . |
4. Build, Flash, and Observe
a. Build the Project:
idf.py build
b. Flash to ESP32:
idf.py -p /dev/ttyUSB0 flash monitor
(Replace /dev/ttyUSB0
with your ESP32’s serial port).
c. Observation:
To test this Modbus Slave, you need a Modbus RTU Master device or simulator:
- Modbus Master Simulator Software: Use software like Modbus Poll, QModMaster, or Simply Modbus on a PC. Connect your PC to the RS-485 bus using a USB-to-RS485 adapter.
- Configuration in Master Simulator:
- Set the correct serial port, baud rate (e.g., 9600), parity (None), data bits (8), stop bits (1).
- Set the Slave ID to what you configured for the ESP32 (e.g.,
1
). - Read Operations:
- Try reading Holding Registers starting at address
0
, quantity10
. You should see values like 10, 20, 30… - Try reading Input Registers starting at address
0
, quantity5
. You should see values like 100, 200… and register 0 should update due tosimulate_sensor_updates_task
. - Try reading Coils starting at address
0
, quantity8
. - Try reading Discrete Inputs starting at address
0
, quantity4
. Discrete Input 1 should toggle.
- Try reading Holding Registers starting at address
- Write Operations:
- Try writing a new value to Holding Register 0 (address
0
). Observe in the ESP32 log (ifESP_LOGD
is enabled for the tag) that the value inholding_registers[0]
changes. - Try writing to Coil 0 (address
0
) to turn it ON (0xFF00
) or OFF (0x0000
). Observe in the ESP32 log.
- Try writing a new value to Holding Register 0 (address
Expected Output in ESP32 Monitor:
You should see initialization logs, logs from simulate_sensor_updates_task, and potentially logs from the Modbus stack if verbose logging is enabled (though the stack is often quiet on successful operations).
I (MODBUS_SLAVE_APP): Initializing ESP32 Modbus RTU Slave...
I (MODBUS_SLAVE_APP): Initializing slave data areas...
I (MODBUS_SLAVE_APP): Modbus slave stack initialized.
I (MODBUS_SLAVE_APP): Modbus slave stack started with Slave ID: 1.
I (MODBUS_SLAVE_APP): Sensor simulation task started.
I (MODBUS_SLAVE_APP): Modbus Slave Application Initialized and Running.
D (MODBUS_SLAVE_APP): Holding Register 0 (addr 0): 10
D (MODBUS_SLAVE_APP): Coil 0 (addr 0) status: OFF
... (sensor task updates input registers and discrete inputs) ...
When the master writes, the data in the corresponding arrays (holding_registers
, coils
) will change, which your application can then use.
Variant Notes
The Modbus RTU Slave implementation using esp_modbus_slave
is highly consistent across the ESP32 family.
- ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2:
- All these variants support UART-based Modbus RTU slave operation effectively. The
esp_modbus_slave
API and the underlyingfreemodbus
stack operate identically. - UART Pin Selection: The primary consideration is the correct configuration of UART TXD, RXD, and RTS pins in
menuconfig
to match your hardware connections to the RS-485 transceiver. - RS-485 Transceiver: An external RS-485 transceiver is always necessary.
- Performance: All variants are more than capable of handling Modbus slave duties, responding to master requests promptly even with other application tasks running. The resource usage of the Modbus slave stack is minimal.
- All these variants support UART-based Modbus RTU slave operation effectively. The
The main factor across variants is ensuring the chosen UART peripheral and GPIO pins are correctly configured and not conflicting with other peripherals in use.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect Slave ID | Master gets a timeout error. ESP32 slave shows no sign of receiving a request. |
Verify CONFIG_MB_CONTROLLER_SLAVE_ID in menuconfig matches the slave ID configured in the Master software/device.
|
Mismatched Serial Parameters | Garbled data in logs (if any). Master reports timeout or CRC errors. No valid responses. | Ensure Baud Rate, Parity, Data Bits (8), and Stop Bits (1) are identical on both the Master and the ESP32 slave configuration. |
RS-485 Wiring Swap | No communication at all. The RS-485 transceiver’s LEDs may not blink as expected. | Swap the ‘A’ and ‘B’ lines on one end of the connection. The A line on the master should connect to the A line on the slave. |
Incorrect RTS Pin for DE/RE | Slave might receive data but cannot send a response, causing master timeouts. Or, the slave might not receive anything if the transceiver is stuck in transmit mode. |
Double-check that the GPIO pin assigned to CONFIG_FMB_UART_RTS_PIN is correctly wired to both the DE and RE pins of the RS-485 transceiver.
|
Wrong Descriptor Size | Master requests for addresses near the end of a range fail with an “Illegal Data Address” exception. Or the slave may crash with a memory access error. |
The .size field must be in bytes. For uint16_t registers, size is count * 2 . For bit-packed coils, size is (count + 7) / 8 .
|
Invalid Pointer in Descriptor | Slave crashes immediately on a Modbus request with Guru Meditation Error, often related to LoadProhibited or StoreProhibited. |
Ensure the .address pointer in mb_register_area_descriptor_t points to a valid, globally-scoped array, not a local variable that has gone out of scope.
|
Forgetting Bus Termination | Communication is unreliable, especially on longer cables or at higher baud rates. Frequent CRC errors. | Add a 120 Ohm resistor across the ‘A’ and ‘B’ lines at the two physical ends of the RS-485 bus. |
Exercises
- Expand Holding Registers and Test:
- Increase
HOLDING_REG_COUNT
to 20. Initialize all with distinct values. - Use a Modbus Master simulator to read all 20 holding registers. Verify all values are correct.
- Write to several different holding registers (e.g., address 5, 10, 15) and verify the ESP32’s internal
holding_registers
array is updated by logging their values fromsimulate_sensor_updates_task
or another diagnostic function.
- Increase
- Implement Writeable Coils Control:
- In the
simulate_sensor_updates_task
(or a new task), monitor the state ofcoils[0]
. - If
coils[0]
bit 0 is set by the master, turn on the ESP32’s built-in LED (if available, or an external LED). If cleared, turn it off. - Test by writing to Coil 0 from your Modbus Master simulator.
- In the
- Implement Custom Input Register Logic:
- Modify
input_registers[1]
to represent a calculated value, e.g.,holding_registers[0] + holding_registers[1]
. - This calculation should happen in
simulate_sensor_updates_task
or a similar application logic loop. - Use the Master to write to
holding_registers[0]
andholding_registers[1]
, then readinput_registers[1]
to verify the sum is correctly calculated and exposed.
- Modify
- Slave ID Configuration Test:
- Change the
CONFIG_MB_CONTROLLER_SLAVE_ID
inmenuconfig
to a different value (e.g.,5
). Rebuild and reflash. - Verify that your Modbus Master can only communicate with the ESP32 slave when addressing the new Slave ID. Attempts to use the old ID should result in timeouts for the master.
- Change the
- Exception Handling (Conceptual):
- Although the
esp_modbus_slave
component handles sending standard exception responses for out-of-bounds access automatically, discuss how you would conceptually handle a request for an “Illegal Function” if you were implementing the Modbus stack from scratch. What would the slave need to check, and what would the response frame look like? (No coding needed, just a description).
- Although the
Summary
- The Modbus Slave responds to requests from a Master, providing data or performing actions.
- ESP-IDF’s
freemodbus
component (esp_modbus_slave
) simplifies Modbus RTU Slave development. - Slave configuration involves setting the Slave ID, UART parameters, and defining data areas using
mb_register_area_descriptor_t
. - These descriptors map Modbus register types and addresses to memory buffers within the ESP32 application.
- The Modbus stack directly accesses these buffers for read/write operations based on Master requests.
- Application logic is responsible for populating read-only data (Input Registers, Discrete Inputs) and acting upon data written by the Master (Holding Registers, Coils).
- All ESP32 variants can effectively function as Modbus RTU Slaves, requiring an external RS-485 transceiver.
Further Reading
- ESP-IDF Modbus Documentation:
- Official ESP-IDF Programming Guide: Sections on “Modbus Controller” or
esp_modbus_slave
. (Search on https://docs.espressif.com/projects/esp-idf/) - Header files in ESP-IDF:
esp_modbus_common.h
,esp_modbus_slave.h
.
- Official ESP-IDF Programming Guide: Sections on “Modbus Controller” or
- Modbus Organization:
- Modbus Application Protocol Specification V1.1b3: http://www.modbus.org/specs.php
- FreeMODBUS Library:
- Understanding the underlying FreeMODBUS library (https://www.embedded-experts.at/en/freemodbus/) can provide deeper insights, although ESP-IDF provides a higher-level API.