Page 1 of 1

ESP-Modbus Slave Register Descriptors: A Real Limitation and the Awkward Workarounds It Forces

Posted: Sat Jun 06, 2026 10:08 am
by PETERESP
ESP-Modbus Slave Register Descriptors: A Real Limitation and the Awkward Workarounds It Forces
---------------------------------
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_t
) to define contiguous register regions for each Modbus register type (Holding, Input, Coils, Discrete Inputs). [Structures](https://docs.espressif.com/projects/esp ... structures)]
Each descriptor contains four core fields: You can register multiple descriptors per register type by calling

Code: Select all

mbc_slave_set_descriptor()
multiple times. [Configuring slave data](https://docs.espressif.com/projects/esp ... ata-access)]
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
From the master’s perspective:
  • 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
In short:
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_t
entries per register type. [Configuring slave data](https://docs.espressif.com/projects/esp ... ata-access)]
Each 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
Each functional block can have its own dedicated array and descriptor. The code remains modular, the memory layout is clear, and the address mapping is explicit and maintainable.

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.
  1. Slave-Side Compromise: One Monolithic Descriptor, Fake Logical Blocks
One workaround is to abandon the idea of multiple physical memory blocks for any range the master might want to read continuously. Instead, we define a single large descriptor that covers the entire combined range:

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));
Then, within our own code, we manually treat this large array as if it were split into logical subranges:
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
It’s hard to shake the feeling that we’re bending our entire design around a limitation that should not exist.
  • Master-Side Compromise: Split Reads and Manual Stitching
The other option is to keep the slave side cleanly structured with multiple descriptors, and accept that the master must never issue a read that spans more than one descriptor.

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
Again, this is not a business requirement or a protocol limitation — it’s purely a workaround for how the current ESP-Modbus slave implementation validates register areas.

---------------------------------

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: All of this suggests a flexible, industrial-grade Modbus solution.

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
But as things stand, it doesn’t. And because of that, we end up either:
  • 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
Both sides are making concessions not to the Modbus protocol itself, but to this one implementation detail. That’s where the real frustration comes from.

---------------------------------

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
Specific questions:
  • 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

    Code: Select all

    mb_register_area_descriptor_t
    entries for a single request?
  • 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
Right now, it feels like the framework is 90% of the way to a truly elegant solution, but this remaining 10% forces us into unnecessary design compromises. Any insights, patterns, or even confirmation that "yes, this is a known limitation" would be extremely helpful.