---------------------------------
We’ve been using the ESP-Modbus slave stack on ESP-IDF and ran into a behavior that, while consistent with the current implementation, feels like a genuine design limitation. It directly impacts how we structure our register maps and how Modbus masters are permitted to access them.
This post is based on explicit assumptions confirmed by our own testing and the API definition:
The slave uses register area descriptors (
Code: Select all
mb_register_area_descriptor_tEach descriptor contains four core fields:
- – Modbus start address for the region
Code: Select all
start_offset - – Region size in bytes
Code: Select all
size - – Register type (
Code: Select all
type,Code: Select all
MB_PARAM_HOLDING, etc.)Code: Select all
MB_PARAM_INPUT - – Pointer to the underlying memory buffer
Code: Select all
address
Code: Select all
mbc_slave_set_descriptor()If a master accesses an undefined region, the stack returns a Modbus exception. [Configuring slave data](https://docs.espressif.com/projects/esp ... ata-access)]
However, the official documentation does not state that a single Modbus request cannot span multiple descriptors and be automatically stitched together. This undocumented constraint is the source of significant practical frustration.
---------------------------------
The Concrete Symptom: 0–9 Works, 10–20 Works, 0–20 Fails
Our setup is conceptually straightforward:
On the slave, we register two separate holding register areas using
Code: Select all
mbc_slave_set_descriptor()- Descriptor A: Offsets 0–9
- Descriptor B: Offsets 10–20
- Reading 0–9 in a single request works perfectly
- Reading 10–20 in a single request works perfectly
- Reading 0–20 in a single continuous request returns no response or a Modbus exception
Two separate reads, each fully contained within a single descriptor → OK
One continuous read spanning both descriptors (with the entire range fully defined) → Rejected as invalid
From the Modbus protocol perspective, a continuous read of 0–20 is completely valid as long as the device implements all those registers. From our perspective as developers, we have indeed defined all of them. But from the current ESP-Modbus slave implementation’s perspective, this is treated as an invalid out-of-bounds access.
The documentation correctly states that the stack will generate an exception for access to undefined regions. [Configuring slave data](https://docs.espressif.com/projects/esp ... ata-access)] The problem is: in our case, the region is not undefined at all — it’s just split across two separate descriptors.
This is where the feeling of a genuine design drawback arises.
---------------------------------
Why This Is a Significant Drawback in Practice
On paper, ESP-Modbus provides an elegant abstraction:
We can define multiple
Code: Select all
mb_register_area_descriptor_tEach descriptor maps a contiguous block of Modbus addresses to a dedicated memory region.
The stack then uses these descriptors to handle master requests automatically.
This naturally encourages a clean, modular design where we split the register map into logical blocks:
- 0–9: System status registers
- 10–20: Device configuration parameters
- 100–199: Historical log data
But the moment a master performs a standard industry practice — like reading 0–20 in one request to get both status and configuration data together — we hit a hard wall:
The slave has:
Descriptor A: 0–9
Descriptor B: 10–20
The master sends a single read request for 0–20.
The current stack implementation only accepts a request if it can be fully covered by exactly one descriptor.
Since no single descriptor covers the full 0–20 range, the entire request is rejected as if the addresses were never defined.
From the outside, this looks like:
“I carefully defined every register from 0 to 20, but I’m not allowed to read them all in one go.”
It’s hard not to feel frustrated here: the framework is so close to being flexible and elegant, but this one arbitrary behavior forces us into awkward, suboptimal design patterns.
---------------------------------
The Forced Workarounds: Compromise and Frustration
Because of this limitation, we are forced to choose between two undesirable options. Both work, but both feel like compromises we should not have to make.
- Slave-Side Compromise: One Monolithic Descriptor, Fake Logical Blocks
Code: Select all
// Example: Cover 0..20 in one monolithic block
uint16_t holding_reg_area[21] = {0};
mb_register_area_descriptor_t reg_area = {
.type = MB_PARAM_HOLDING,
.start_offset = 0,
.address = (void*)holding_reg_area,
.size = sizeof(holding_reg_area) << 1, // As shown in official examples
};
ESP_ERROR_CHECK(mbc_slave_set_descriptor(reg_area));
Indices 0–9: Status registers
Indices 10–20: Configuration registers
From the master’s perspective, this is perfect:
It can read 0–9, 10–20, or 0–20 in a single request.
Everything is continuous and works as expected.
But from the slave developer’s perspective, this is a clear step backward:
- We lose the clean "one array per logical block" structure that multiple descriptors were designed to provide
- We have to manually manage offsets within a single large array, which is error-prone
- Any future changes (e.g., inserting new registers in the middle) become much more fragile, as we must keep the entire region continuous and consistent
- The "multiple descriptors per register type" feature becomes largely useless for continuous access patterns, defeating its original purpose
- Master-Side Compromise: Split Reads and Manual Stitching
In practice, this means:
The master reads 0–9 in one request.
Then reads 10–20 in a second request.
Then manually stitches the two chunks together in its own logic.
This preserves a clean design on the slave side, but pushes all the awkwardness to the master:
- The master now has to know about the slave’s internal descriptor boundaries — something Modbus should never require
- Any change to the slave’s register layout (e.g., merging or splitting blocks) can force corresponding changes to the master’s read strategy
- The master code becomes more complex and less intuitive, even though the register addresses themselves are perfectly continuous
---------------------------------
Why This Feels Like "So Close, Yet Not Quite There"
What makes this particularly frustrating is that ESP-Modbus is otherwise a very capable and thoughtfully designed framework:
- It supports all four standard Modbus register types and allows flexible mapping via descriptors. [Messaging & mapping](https://docs.espressif.com/projects/esp ... ta-mapping)]
- It explicitly allows multiple area descriptors per register type. [Configuring slave data](https://docs.espressif.com/projects/esp ... ata-access)]
- It provides event APIs like and
Code: Select all
mbc_slave_check_event()for structured handling of register access. [Slave communication](https://docs.espressif.com/projects/esp ... munication)]Code: Select all
mbc_slave_get_param_info() - It even supports mapping complex data types and automatic endianness conversion for 32-bit/64-bit values. [Complex data types](https://docs.espressif.com/projects/esp ... data-types)]
And yet, right in the middle of this otherwise solid design, the descriptor lookup logic effectively says:
"Each request must be fully contained within exactly one descriptor. If it spans two contiguous, fully defined descriptors, we treat it as invalid."
It’s not that the stack couldn’t support stitching multiple descriptors together for a single request — conceptually, it would "just" need to:
- Find all descriptors that cover the requested range
- Verify there are no gaps between them
- Then read/write across them in order
- Contorting the slave’s memory layout into one big block to keep the master happy, or
- Contorting the master’s access pattern into multiple smaller reads to keep the slave’s descriptors happy
---------------------------------
Questions for the Community
Given all of the above, I’d really like to hear from other developers who:
- Have encountered this same limitation with ESP-Modbus slave descriptors, and
- Have found better patterns or workarounds than the two compromises described above
- Is there any officially recommended method (or hidden configuration) to allow a single Modbus request to span multiple registered areas, as long as they are contiguous and fully defined?
- Has anyone patched or extended the ESP-Modbus slave implementation to support automatic stitching across multiple entries for a single request?
Code: Select all
mb_register_area_descriptor_t - How are you structuring your register maps in real-world projects to balance:
- Logical clarity (separate blocks for different functions)
- Master convenience (support for continuous bulk reads)
- The current descriptor validation behavior