| name | esp-modbus-skill |
| description | AI Skill for Espressif esp-modbus (Modbus RTU / ASCII / TCP) firmware development on ESP-IDF. Used when users need to create, modify, or debug Modbus master/slave applications, configure the Data Dictionary and register area descriptors, map CIDs to Modbus registers, register custom function-code handlers, or resolve Modbus communication errors on ESP32 family chips. Trigger words: "Modbus", "RTU", "ASCII", "Modbus TCP", "RS485", "esp-modbus", "freemodbus", "Modbus 主站", "Modbus 从站", "保持寄存器", "输入寄存器", "线圈", "离散输入", "Holding", "Coil", "mbc_master", "mbc_slave", "Data Dictionary", "数据字典" |
| tags | ["embedded","esp-idf","espressif","esp32","modbus","rtu","ascii","tcp","rs485","industrial","firmware"] |
| license | Apache-2.0 |
| compatibility | Requires ESP-IDF v5.0 or later (idf >= "5.0"); targets ESP32 / ESP32-S2 / ESP32-S3 / ESP32-C3 / ESP32-C2 / ESP32-C5 / ESP32-C6 / ESP32-C61 / ESP32-H2 / ESP32-P4; build via idf.py (CMake). On ESP-IDF releases that still ship the built-in freemodbus component, exclude it with `set(EXCLUDE_COMPONENTS freemodbus)` in the project CMakeLists.txt. |
| metadata | {"author":"Community","version":"1.1.0"} |
esp-modbus-skill
AI Skill for developing firmware with the Espressif esp-modbus library (component version v2.x.x). Covers Modbus RTU, ASCII and TCP, the v2 instance-based master/slave API (mbc_*_create_* returning a handle), the Data Dictionary (CID → register mapping), slave register-area descriptors, custom function-code handlers, the endianness/extended-types conversion API, and the Kconfig configuration keys. Every API name, struct, macro, and code snippet below is grounded in the repository's own docs/, headers under modbus/mb_controller/common/include/, and the examples under examples/.
Core Principles
- v2 is instance-based — Every master/slave object is created by
mbc_master_create_serial / mbc_master_create_tcp / mbc_slave_create_serial / mbc_slave_create_tcp, which fill a void *handle. That handle is the first argument of every later API call for that object. The legacy single-instance global-handle API is gone.
- Never guess APIs — All public functions live in
mbcontroller.h → esp_modbus_master.h / esp_modbus_slave.h. If a function is not there, it does not exist; do not invent it.
- Communication mode is compile-time + per-instance — The serial modes (RTU, ASCII) and TCP must be enabled at compile time (
CONFIG_FMB_COMM_MODE_RTU_EN, CONFIG_FMB_COMM_MODE_ASCII_EN, CONFIG_FMB_COMM_MODE_TCP_EN). The per-instance mode is then set in mb_communication_info_t (.ser_opts.mode = MB_RTU / MB_ASCII, or .tcp_opts.mode = MB_TCP).
- Master needs a Data Dictionary — The master does not read raw registers; it reads CIDs. A
mb_parameter_descriptor_t device_parameters[] table must be registered via mbc_master_set_descriptor() before mbc_master_start(). Each CID maps to a slave UID + register type + start + size + data type.
- Slave needs register-area descriptors — A slave must register at least one
mb_register_area_descriptor_t per register type it wants to serve (Holding/Input/Coil/Discrete) via mbc_slave_set_descriptor(). Unmapped accesses return a Modbus exception.
- Initialization order is fixed — create → (set descriptor / register handlers) →
uart_set_pin + uart_set_mode(UART_MODE_RS485_HALF_DUPLEX) for serial → start. For master, set_descriptor must happen before start.
- RS485 UART setup is the app's job — esp-modbus does not configure pins or half-duplex mode. The app must call
uart_set_pin() and uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX) itself (see serial examples).
- Master timeout governs reliability —
CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND (or per-instance ser_opts.response_tout_ms / tcp_opts.response_tout_ms) must be greater than the worst-case slave round-trip time, otherwise the master drops in-flight responses and the slave logs a race-condition warning.
- Extended data types need a Kconfig flag — The big family of
PARAM_TYPE_U32_ABCD, PARAM_TYPE_FLOAT_CDAB, PARAM_TYPE_DOUBLE_HGFEDCBA, etc., and the mb_get_* / mb_set_* endianness helpers, only exist when CONFIG_FMB_EXT_TYPE_SUPPORT=y. On the slave side they require #include "mb_endianness_utils.h" (pulled in automatically when the flag is set).
- Access shared slave data under lock — When the stack is active, the app must guard direct reads/writes of the mapped register storage with
mbc_slave_lock()/mbc_slave_unlock() (or a FreeRTOS critical section for cross-object sharing).
- Custom handlers run in the controller task —
mbc_set_handler() callbacks execute in the Modbus controller event task and must be short, non-blocking, and never log heavily; a slow handler makes the slave miss the master's response window.
- Destroy on teardown —
mbc_master_delete() / mbc_slave_delete() stop the stack and free all tasks/queues. For TCP, also tear down netif/services afterwards.
When to Use
Applicable:
- Creating a Modbus RTU/ASCII master or slave over RS485 on an ESP32-family chip
- Creating a Modbus TCP master or slave over Wi-Fi/Ethernet
- Mapping physical parameters (temperature, humidity, relay state) to CIDs via the Data Dictionary
- Implementing vendor-specific (non-standard) Modbus function codes via custom handlers
- Reading/writing 32-bit / 64-bit / float / double values with specific byte order (ABCD/CDAB/…)
- Diagnosing Modbus errors (
ESP_ERR_TIMEOUT 0x107, ESP_ERR_NOT_SUPPORTED 0x106, ESP_ERR_INVALID_RESPONSE 0x108, ESP_ERR_INVALID_STATE 0x103)
- Migrating from the legacy built-in freemodbus component to esp-modbus v2
Not applicable:
- Non-Modbus serial protocols (plain UART, DMX, proprietary)
- Bare-metal (non-ESP-IDF) projects — esp-modbus depends on FreeRTOS, UART and lwIP/esp_netif from ESP-IDF
- Other vendors' Modbus stacks (MicroPython
modbus, Linux libmodbus, etc.)
- PCB / RS485 transceiver schematic design (only the firmware side)
Scenario Quick Reference (Recipes)
When the user's intent matches a scenario below, read the corresponding recipe first — it has the full call chain, step-by-step instructions, real code, and a common-errors table.
Serial (RTU / ASCII over RS485)
| recipe | scenario |
|---|
recipes/serial_slave.md | Build a Modbus serial slave (RTU or ASCII): UART + RS485 setup, register-area descriptors, event loop |
recipes/serial_master.md | Build a Modbus serial master: UART + RS485 setup, Data Dictionary, polling loop, teardown |
TCP (over Wi-Fi / Ethernet)
| recipe | scenario |
|---|
recipes/tcp_slave.md | Build a Modbus TCP slave: netif init, mbc_slave_create_tcp, multi-connection, keep-alive |
recipes/tcp_master.md | Build a Modbus TCP master: slave IP address table, MDNS resolution, mbc_master_create_tcp |
Data model & advanced
| recipe | scenario |
|---|
recipes/data_dictionary.md | Author the master Data Dictionary (mb_parameter_descriptor_t), CID enum, OPTS/STR macros, offset macros |
recipes/slave_register_areas.md | Map slave storage structs to Modbus areas via mbc_slave_set_descriptor + HOLD_OFFSET macros |
recipes/custom_handlers.md | Register/override function-code handlers with mbc_set_handler / mbc_get_handler / mbc_delete_handler, including the FC 0x41 echo example |
recipes/extended_types.md | Use CONFIG_FMB_EXT_TYPE_SUPPORT, the PARAM_TYPE_*_ABCD family, and mb_set_float_abcd / mb_get_uint32_dcba helpers |
recipes/slave_device_id.md | Set vendor-specific slave device identification (short UID + running status + vendor data) via mbc_set_slave_id / mbc_get_slave_id for retrieval by master FC 0x11 Report Slave ID |
Modbus Register Types & Data Areas
| Modbus area | mb_param_type_t | Function codes (master) | Direction | Size unit |
|---|
| Holding Registers | MB_PARAM_HOLDING | 0x03 Read, 0x06 Write Single, 0x10 Write Multiple, 0x17 R/W Multiple | read/write | 16-bit register |
| Input Registers | MB_PARAM_INPUT | 0x04 Read Input | read-only | 16-bit register |
| Coils | MB_PARAM_COIL | 0x01 Read, 0x05 Write Single, 0x0F Write Multiple | read/write | 1 bit |
| Discrete Inputs | MB_PARAM_DISCRETE | 0x02 Read Discrete | read-only | 1 bit |
| Custom | MB_PARAM_CUSTOM | vendor code (e.g. 0x41) | n/a | n/a |
The master's mb_size field is in registers (2 bytes) for Holding/Input, and in bits for Coil/Discrete.
Communication Mode / Config Structure Selection
| Mode | Compile flag | mode value | Config union member | Create call |
|---|
| Serial RTU | CONFIG_FMB_COMM_MODE_RTU_EN | MB_RTU | mb_communication_info_t::ser_opts | mbc_master_create_serial / mbc_slave_create_serial |
| Serial ASCII | CONFIG_FMB_COMM_MODE_ASCII_EN | MB_ASCII | …::ser_opts | mbc_master_create_serial / mbc_slave_create_serial |
| TCP/IP | CONFIG_FMB_COMM_MODE_TCP_EN (needs LWIP_ENABLE) | MB_TCP | …::tcp_opts | mbc_master_create_tcp / mbc_slave_create_tcp |
Key Kconfig Quick Reference
| Symbol | Default | Purpose |
|---|
CONFIG_FMB_COMM_MODE_RTU_EN | y | Enable RTU mode |
CONFIG_FMB_COMM_MODE_ASCII_EN | y | Enable ASCII mode |
CONFIG_FMB_COMM_MODE_TCP_EN | y (needs LWIP) | Enable TCP mode |
CONFIG_FMB_TCP_PORT_DEFAULT | 502 | Default Modbus TCP port |
CONFIG_FMB_TCP_PORT_MAX_CONN | 5 | Max simultaneous TCP slave connections |
CONFIG_FMB_TCP_CONNECTION_TOUT_SEC | 2 | TCP connection (accept) timeout |
CONFIG_FMB_TCP_KEEP_ALIVE_TOUT_SEC | 4 | TCP keep-alive probe interval |
CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND | 10000 | Master wait for slave response (ms) |
CONFIG_FMB_MASTER_DELAY_MS_CONVERT | 200 | Master broadcast convert delay (ms) |
CONFIG_FMB_QUEUE_LENGTH | 50 | Event task queue length |
CONFIG_FMB_PORT_TASK_STACK_SIZE | 4096 | Port rx/tx task stack |
CONFIG_FMB_PORT_TASK_PRIO | 10 | Port task priority (controller = prio − 1) |
CONFIG_FMB_PORT_TASK_AFFINITY | CPU0 | Core affinity (NO_AFFINITY / CPU0 / CPU1) |
CONFIG_FMB_BUFFER_SIZE | 260 (TCP) / 256 (serial) | RX/TX buffer size |
CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT | y | Enable Report Slave ID (FC 0x11) |
CONFIG_FMB_CONTROLLER_SLAVE_ID | 0x00112233 | Default slave ID value |
CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE | 20 | Slave parameter-notification queue size |
CONFIG_FMB_EXT_TYPE_SUPPORT | n | Enable extended types + endianness API |
CONFIG_FMB_FUNC_HANDLERS_MAX | 16 | Max registered function handlers per object |
CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD | n | ISR dispatch for timer (selects UART_ISR_IN_IRAM) |
CONFIG_FMB_MDNS_INTEGRATION_ENABLE | y | Resolve MDNS names in TCP master slave table |
Modbus Error Code → esp_err_t Mapping
| Modbus error | esp_err_t | Hex | Meaning |
|---|
MB_ENOERR | ESP_OK | 0x0 | Success |
MB_ENOREG | ESP_ERR_NOT_SUPPORTED | 0x106 | Register not supported / exception from slave |
MB_ETIMEDOUT | ESP_ERR_TIMEOUT | 0x107 | Slave did not respond in time |
MB_EILLFUNC / MB_ERECVDATA | ESP_ERR_INVALID_RESPONSE | 0x108 | Unsupported/incorrect response from slave |
MB_EBUSY / MB_EILLSTATE / MB_ENOCONN | ESP_ERR_INVALID_STATE | 0x103 | FSM busy / not connected / critical failure |
Critical Pitfalls (Must Read)
These are the most common errors. Violating any of these produces non-working Modbus firmware.
1. Master handle is the first parameter of every call
mbc_master_start();
mbc_master_get_parameter(CID_TEMP, temp_data, &type);
static void *master_handle = NULL;
mbc_master_create_serial(&comm, &master_handle);
mbc_master_start(master_handle);
mbc_master_get_parameter(master_handle, CID_TEMP, temp_data, &type);
2. Master: set the descriptor BEFORE start
mbc_master_start(master_handle);
mbc_master_set_descriptor(master_handle, device_parameters, num_device_parameters);
mbc_master_set_descriptor(master_handle, device_parameters, num_device_parameters);
uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
mbc_master_start(master_handle);
3. Serial RS485: the app must set pins and half-duplex mode
mbc_slave_create_serial(&comm, &slave_handle);
mbc_slave_start(slave_handle);
mbc_slave_create_serial(&comm, &slave_handle);
ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, CONFIG_MB_UART_TXD,
CONFIG_MB_UART_RXD, CONFIG_MB_UART_RTS,
UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX));
mbc_slave_start(slave_handle);
4. Slave: register at least one area per type you want to serve
mb_register_area_descriptor_t reg_area = {0};
reg_area.type = MB_PARAM_HOLDING;
reg_area.start_offset = 0;
reg_area.address = &holding_reg_params.holding_data0;
reg_area.size = sizeof(float) * 4;
reg_area.access = MB_ACCESS_RW;
ESP_ERROR_CHECK(mbc_slave_set_descriptor(slave_handle, reg_area));
5. Data Dictionary mb_size is registers (2 bytes), param_size is bytes
{ CID_TEMP, STR("Temp"), STR("C"), 1, MB_PARAM_HOLDING,
0, 4 , 0, PARAM_TYPE_FLOAT, 4, OPTS(0,100,0), PAR_PERMS_READ },
{ CID_TEMP, STR("Temp"), STR("C"), 1, MB_PARAM_HOLDING,
0, 2 , 0, PARAM_TYPE_FLOAT, 4 , OPTS(0,100,0),
PAR_PERMS_READ_WRITE_TRIGGER },
6. Extended types require CONFIG_FMB_EXT_TYPE_SUPPORT=y
{ CID_F, STR("F"), STR("--"), 1, MB_PARAM_HOLDING, 0, 2,
0, PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0,0,0), PAR_PERMS_READ_WRITE_TRIGGER };
7. Master response timeout must beat the slave's round-trip time
.ser_opts.response_tout_ms = 1000;
.ser_opts.response_tout_ms = 0;
8. Guard shared slave storage with lock/unlock
holding_reg_params.holding_data0 += 1.0f;
mbc_slave_lock(slave_handle);
holding_reg_params.holding_data0 += 1.0f;
mbc_slave_unlock(slave_handle);
9. Custom handler must stay short and non-blocking
mb_exception_t my_handler(void *inst, uint8_t *frame, uint16_t *len) {
vTaskDelay(pdMS_TO_TICKS(50));
ESP_LOG_BUFFER_HEXDUMP("HUGE", buf, 4096, ESP_LOG_INFO);
return MB_EX_NONE;
}
mb_exception_t my_handler(void *inst, uint8_t *frame, uint16_t *len) {
MB_RETURN_ON_FALSE((frame && len && *len < (MB_CUST_DATA_LEN - 1)),
MB_EX_ILLEGAL_DATA_VALUE, TAG, "bad frame");
strncpy(my_buf, (char *)&frame[1], MB_CUST_DATA_LEN);
return MB_EX_NONE;
}
10. TCP master slave table must end with NULL
char *slave_ip_address_table[] = {
"01;192.168.1.5;502",
"02;192.168.1.6;502",
};
char *slave_ip_address_table[] = {
"01;mb_slave_tcp_01;502",
"02;192.168.1.6;1502",
NULL
};
11. CID and param_key must be unique
{ 0, STR("Temp"), ... }, { 0, STR("Humid"), ... },
enum { CID_TEMP = 0, CID_HUMID, CID_COUNT };
const mb_parameter_descriptor_t device_parameters[] = {
{ CID_TEMP, STR("Temperature"), ... },
{ CID_HUMID, STR("Humidity"), ... },
};
12. FC 0x17 (Read/Write Multiple) needs the wr_rd_multi_reg_func sub-fields
mb_param_request_t req = { .slave_addr = 1, .command = 0x17,
.reg_start = HOLD_REG_START(holding_data2),
.reg_size = 2 };
mb_param_request_t req = {
.slave_addr = 1,
.command = 0x17,
.reg_start = HOLD_REG_START(holding_data2),
.reg_size = 2,
.wr_rd_multi_reg_func.wr_reg_start = HOLD_REG_START(holding_data0),
.wr_rd_multi_reg_func.wr_reg_size = 6,
};
mbc_master_send_request(master_handle, &req, &read_write_buf);
13. ESP-IDF releases with built-in freemodbus must exclude it
# ❌ WRONG on older ESP-IDF: link error / wrong API, because the built-in
# freemodbus component (old single-instance API) shadows esp-modbus v2
# (no EXCLUDE_COMPONENTS line)
# ✅ CORRECT — in the project CMakeLists.txt, before project()
set(EXCLUDE_COMPONENTS freemodbus)
14. TCP master: netif must exist before create_tcp
mbc_master_create_tcp(&cfg, &master_handle);
example_connect();
example_connect();
mb_communication_info_t cfg = {
.tcp_opts.port = 502,
.tcp_opts.mode = MB_TCP,
.tcp_opts.addr_type = MB_IPV4,
.tcp_opts.ip_addr_table = (void *)slave_ip_address_table,
.tcp_opts.ip_netif_ptr = (void *)get_example_netif(),
...
};
mbc_master_create_tcp(&cfg, &master_handle);
Execution Workflow
| Step | Name | Description |
|---|
| 1 | Plan | Identify role (master/slave), transport (serial RTU/ASCII or TCP), target chip, register map |
| 2 | Recipe | Read the matching recipe in recipes/ and follow its call chain end-to-end |
| 3 | Config | Enable the right CONFIG_FMB_COMM_MODE_*_EN flags; set CONFIG_FMB_EXT_TYPE_SUPPORT=y if extended types are needed; pick UART pins / netif |
| 4 | Author | Write app_main: build mb_communication_info_t, create handle, (master) set descriptor / (slave) set area descriptors, register custom handlers if any, (serial) set UART pin + RS485 mode, start |
| 5 | Validate | Check every API signature in resources/api_reference.md; verify mb_size (registers) vs param_size (bytes); verify slave area byte sizes; verify slave IP table ends with NULL |
| 6 | Build | idf.py set-target <chip> then idf.py build (CMake) |
| 7 | Flash | idf.py -p <PORT> flash monitor |
| 8 | Debug | Watch for ESP_ERR_TIMEOUT (0x107), ESP_ERR_NOT_SUPPORTED (0x106), ESP_ERR_INVALID_RESPONSE (0x108); check CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND; confirm RS485 wiring and direction (RTS) pin |
| 9 | Teardown | Call mbc_master_delete() / mbc_slave_delete(); for TCP also disconnect netif/services |
Step 4 Detail — Project Creation Strategy
When the target directory has no existing project:
- Pick the closest example under
examples/ and copy it wholesale, then edit main/*.c:
- Serial slave →
examples/serial/mb_serial_slave/
- Serial master →
examples/serial/mb_serial_master/
- TCP slave →
examples/tcp/mb_tcp_slave/
- TCP master →
examples/tcp/mb_tcp_master/
- Both master and slave examples depend on the shared
examples/mb_example_common/ component (defines holding_reg_params_t, input_reg_params_t, coil_reg_params_t, discrete_reg_params_t and their extern instances). Keep it as a component or inline the structs.
- Adjust the Data Dictionary / register areas, the UART pins (or netif), and the slave UID to match the device's register map.
- Explain what was copied and why.
When the directory already contains a project: edit files in place; do not overwrite unless asked.
Failure Strategies
| Situation | Action |
|---|
API not found in resources/api_reference.md | Stop; inform the user it does not exist in esp-modbus v2 |
ESP_ERR_TIMEOUT (0x107) on master reads | Raise CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND / per-instance response_tout_ms above worst-case RTT; check RS485 wiring, RTS direction, baud/parity match |
ESP_ERR_NOT_SUPPORTED (0x106) from slave | Slave returned an exception — the requested register is not in any registered area; check Data Dictionary / mbc_slave_set_descriptor coverage |
ESP_ERR_INVALID_RESPONSE (0x108) | Wrong command for register type, or wrong mb_size; verify function-code ↔ mb_param_type pairing |
ESP_ERR_INVALID_STATE (0x103) | Stack not started, FSM busy, or TCP slave not connected; check init order and connection state |
| Slave logs "handling time … exceeds slave response time in master" | Race condition: reduce slave request processing time or increase master timeout (see docs/en/overview_messaging_and_mapping.rst "The Race Condition") |
Extended-type enum / mb_set_float_abcd missing at build | Set CONFIG_FMB_EXT_TYPE_SUPPORT=y in sdkconfig |
TCP master cannot resolve mb_slave_tcp_01 | Either keep CONFIG_FMB_MDNS_INTEGRATION_ENABLE=y and run an mDNS slave, or switch the IP table to literal IP addresses |
| Old ESP-IDF link error / wrong API | Add set(EXCLUDE_COMPONENTS freemodbus) to the project CMakeLists.txt |
References
- Scenario recipes →
recipes/ directory
- API reference (real function signatures) →
resources/api_reference.md
- Kconfig / configuration reference →
resources/config_reference.md
- Consolidated pitfalls →
resources/pitfalls.md
- Example project index →
resources/example_list.md
- Source repo:
D:/esp-skill/espressif-repos/esp-modbus (docs in docs/en/, headers in modbus/mb_controller/common/include/, examples in examples/)