Follow our official account to keep receiving embedded knowledge!
Introduction
In the development of industrial automation and IoT devices, the Modbus protocol has become the preferred choice for embedded system communication due to its simplicity and reliability.
However, traditional Modbus protocol stacks are often large, with tens of thousands of lines of code, making them too bulky for resource-constrained microcontrollers.
Recently, I discovered an open-source project on GitHub called nanoMODBUS, which implements a complete Modbus RTU/TCP protocol stack with just 2000 lines of C code. This streamlined and efficient design concept impressed me.
Project Overview

nanoMODBUS is a lightweight Modbus protocol stack created by Italian developer debevv, specifically designed for embedded systems. The core features of the project include:
- Extreme Minimalism: The core code is only about 2000 lines, including complete RTU and TCP support.
- No Dynamic Allocation: It uses static memory throughout, avoiding memory fragmentation issues.
- Platform Independence: It only relies on the C99 standard library, making it portable to any platform.
- Complete Functionality: Supports client/server modes, covering major Modbus function codes.
- MIT License: A commercially friendly open-source license.
Project address: https://github.com/debevv/nanoMODBUS
In-Depth Analysis of Core Mechanisms
Ingenious Design of the Platform Abstraction Layer
When analyzing the architecture of nanoMODBUS, I was first attracted by the design of its platform abstraction layer. The author achieved a hardware-independent protocol stack implementation through a simple interface design.
typedef struct {
nmbs_transport transport; // Transport type: RTU or TCP
nmbs_read_func read; // Platform-specific read function
nmbs_write_func write; // Platform-specific write function
void* arg; // User-defined parameters
} nmbs_platform_conf;
The cleverness of this design lies in its complete decoupling of the core logic of the protocol stack from the underlying hardware. When porting to the STM32 platform, only two simple functions need to be implemented:
int32_t my_transport_read(uint8_t* buf, uint16_t count, int32_t timeout_ms, void* arg) {
UART_HandleTypeDef* huart = (UART_HandleTypeDef*)arg;
HAL_StatusTypeDef status = HAL_UART_Receive(huart, buf, count, timeout_ms);
return (status == HAL_OK) ? count : -1;
}
int32_t my_transport_write(const uint8_t* buf, uint16_t count, int32_t timeout_ms, void* arg) {
UART_HandleTypeDef* huart = (UART_HandleTypeDef*)arg;
HAL_StatusTypeDef status = HAL_UART_Transmit(huart, (uint8_t*)buf, count, timeout_ms);
return (status == HAL_OK) ? count : -1;
}
This weak function callback design reminds me of the virtual file system in the Linux kernel, where a unified interface masks underlying differences, representing a very elegant architectural pattern.
State Machine Driven Protocol Parsing Engine
Upon further investigation of the protocol parsing logic in nanoMODBUS, I found that it employs a clever state machine design. Unlike traditional blocking parsing, nanoMODBUS uses a non-blocking state machine to handle the reception and parsing of protocol frames.

In terms of code implementation, the core logic of the state machine is very simple:
nmbs_error nmbs_server_poll(nmbs_t* nmbs) {
switch (nmbs->state) {
case NMBS_SERVER_STATE_LISTENING:
return server_receive_request(nmbs);
case NMBS_SERVER_STATE_PROCESSING:
return server_process_request(nmbs);
case NMBS_SERVER_STATE_RESPONDING:
return server_send_response(nmbs);
default:
nmbs->state = NMBS_SERVER_STATE_LISTENING;
return NMBS_ERROR_INVALID_STATE;
}
}
The advantage of this design is that it avoids blocking waits, making it particularly suitable for use in RTOS environments. In actual projects, I found that this non-blocking design allows the main loop to run efficiently without being affected by communication blocking, thus maintaining system real-time performance.
Using a state machine for protocol parsing is a common method. Previously, we shared a lightweight communication protocol tool for embedded systems that also uses a state machine for protocol data reception and parsing.
The Minimalist Philosophy of Memory Management
In embedded development, memory management is often the most troublesome issue. Dynamic allocation can easily lead to memory fragmentation, while static allocation may cause memory waste. nanoMODBUS solves this problem in a clever way.
typedef struct nmbs {
uint8_t msg[NMBS_PDU_MAX_SIZE]; // Fixed-size message buffer
uint16_t msg_length; // Current message length
nmbs_state_t state; // Current state
nmbs_platform_conf platform; // Platform configuration
// ... other necessary fields
} nmbs_t;
I noticed that the author uses a fixed-size buffer to store Modbus messages. This design seems simple but is actually very clever:
- Avoid Dynamic Allocation: All memory is determined at compile time, and there are no memory allocation operations at runtime.
- Reasonable Size: The maximum PDU size of the Modbus protocol is fixed (253 bytes), and the buffer size is just right.
- Efficient Reuse: The same buffer is used for both receiving and sending, maximizing memory utilization.
An important principle in embedded system design is: to simplify the design as much as possible while meeting functionality.
Practice and Reflection
Porting to the ESP32 Platform
To verify the portability of nanoMODBUS, I ported it to the ESP32 platform. The entire process was surprisingly smooth, with the main steps as follows:
- Configure the UART Interface:
void init_uart() {
uart_config_t uart_config = {
.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_param_config(UART_NUM_1, &uart_config);
uart_driver_install(UART_NUM_1, 256, 256, 0, NULL, 0);
}
- Implement Transport Functions:
int32_t esp32_transport_read(uint8_t* buf, uint16_t count, int32_t timeout_ms, void* arg) {
int len = uart_read_bytes(UART_NUM_1, buf, count, timeout_ms / portTICK_RATE_MS);
return len > 0 ? len : -1;
}
int32_t esp32_transport_write(const uint8_t* buf, uint16_t count, int32_t timeout_ms, void* arg) {
int len = uart_write_bytes(UART_NUM_1, (const char*)buf, count);
return len == count ? len : -1;
}
- Initialize the Protocol Stack:
nmbs_platform_conf platform_conf = {
.transport = NMBS_TRANSPORT_RTU,
.read = esp32_transport_read,
.write = esp32_transport_write,
.arg = NULL
};
nmbs_t nmbs;
nmbs_server_create(&nmbs, 1, &platform_conf); // Slave address is 1
The entire porting process took less than 50 lines of code, which fully demonstrates the excellence of the nanoMODBUS design.
Considerations for Thread Safety
When using nanoMODBUS in a multithreaded environment, special attention must be paid to thread safety issues. Although the protocol stack itself uses static memory, access to state variables still needs protection:
// Using mutex to protect in FreeRTOS
SemaphoreHandle_t modbus_mutex;
void modbus_task(void* param) {
while (1) {
if (xSemaphoreTake(modbus_mutex, portMAX_DELAY) == pdTRUE) {
nmbs_server_poll(&nmbs); // Handle Modbus communication
xSemaphoreGive(modbus_mutex);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
Conclusion
nanoMODBUS implements the most complete functionality with the simplest code, embodying the design philosophy of “Less is More.” For us embedded developers, this project provides several important insights:
- The Importance of Platform Abstraction: Through reasonable abstraction layer design, code can be easily ported across different platforms.
- The Power of State Machines: State machines are a very effective design pattern for handling complex protocols.
- The Art of Memory Management: In embedded systems, static memory allocation is often more reliable than dynamic allocation.
- The Value of Simplicity: Complex functionality does not necessarily require complex implementations.
You might also like:
A Lightweight Circular Buffer Management Library for Embedded Systems!
Git Interactive Rebase to Modify Commit Descriptions
Singleton Pattern: The Guardian of Global State Consistency in Embedded Systems
Embedded Field: The Ultimate Showdown between Linux and RTOS!
Advanced Guide to Embedded Software, Let’s Level Up Together!