Lightweight Modbus RTU Driver
Synchronous Modbus RTU implementation with 92% ROM and 92% RAM reduction vs ESP-Modbus. FSM-based frame parsing with WiFi/BLE coexistence optimization
Context
- System: ESP32-S3 based IoT controller (WiFi + BLE + RS485)
- Requirement: Control ISV2-RS servo motors via Modbus RTU while using WiFi/BLE simultaneously
- Constraint: ESP-Modbus library competing with WiFi for resources, limited RAM environment
Core Problem
Running Modbus and WiFi together on ESP32-S3 causes several issues:
- ESP-Modbus uses 50KB ROM and 20KB RAM, starving WiFi buffer space
- Separate task and queue create context switching overhead
- RS485 transceiver switching generates garbage bytes (0xFE, 0xFF)
- WiFi transmission conflicts with Modbus timing
Why this was hard: Modbus RTU relies on T3.5 (3.5 character time) for frame delimiting, but WiFi interrupts disrupt this timing. Meanwhile, RS485 half-duplex switching generates noise that must be filtered.
Key Insight
Synchronous API with FSM-based receiver eliminates task/queue overhead. Inter-frame delay setting yields CPU time to WiFi. Result: 92% ROM and 92% RAM reduction.
Why FSM? RS485 transceiver switching generates garbage bytes before actual responses. Simple timeout-based receivers mistake these as frame starts. FSM scans for address+function code patterns to extract only valid frames.
Approach
1) Resource Comparison
| Item | ESP-Modbus | Modbus Lite | Reduction |
|---|---|---|---|
| ROM | 50KB | 4KB | 92% |
| RAM | 20KB | 1.5KB | 92% |
| Task Stack | 4KB | 0 | 100% |
| Queue Memory | 2KB | 0 | 100% |
2) FSM-Based Frame Receiver
Skips garbage bytes and extracts only valid frames:
State transitions:
RX_SYNC → [addr match] → RX_GOT_ADDR_FUNC → [3rd byte] → RX_COLLECT_REST
↑ ↓
└────────────────── [CRC fail: resync] ─────────────────────┘
3-layer validation:
- Address + function code pattern matching
- Calculate expected length per function code
- Accept after CRC16 verification
// Sliding window to find addr+func pattern
if (prev == expected_addr && is_valid_func_response(expected_func, b)) {
rx[0] = prev;
rx[1] = b;
st = RX_GOT_ADDR_FUNC;
}
3) Modbus Standard Timing Implementation
Calculate T3.5 and T1.5 character times accurately per baud rate:
// Below 19200 bps: actual calculation
if (baudrate <= 19200) {
t15_us = (bit_time_us * bits_per_char * 15) / 10; // 1.5 chars
t35_us = (bit_time_us * bits_per_char * 35) / 10; // 3.5 chars
} else {
// High speed: fixed values (ESP-Modbus standard)
t15_us = 750;
t35_us = 1750;
}
4) WiFi/BLE Coexistence Optimization
Inter-frame delay yields CPU time to WiFi:
// Dynamic adjustment based on WiFi load
void modbus_lite_set_frame_delay(uint32_t delay_ms);
// Usage example
if (wifi_is_busy()) {
modbus_lite_set_frame_delay(20); // WiFi priority
} else {
modbus_lite_set_frame_delay(5); // Normal mode
}
Timing sequence:
[T3.5 wait] → [frame delay] → [bus idle check] → [TX] → [wait TX done] → [RX]
5) RS485 Hardware Mode
Leverages ESP32-S3’s hardware RS485 half-duplex mode for automatic RTS control:
// Hardware controls RTS automatically
uart_set_mode(uart_num, UART_MODE_RS485_HALF_DUPLEX);
// RX timeout for frame end detection
uart_set_rx_timeout(uart_num, tout_thresh);
uart_set_always_rx_timeout(uart_num, true);
6) Synchronous API
Simple blocking calls without tasks and callbacks:
// Initialization
modbus_lite_config_t config = {
.baudrate = 115200,
.uart_num = UART_NUM_1,
.tx_pin = GPIO_NUM_17,
.rx_pin = GPIO_NUM_18,
.rts_pin = GPIO_NUM_14,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_2, // 8N2
.timeout_ms = 100
};
modbus_lite_init(&config);
// Read/write registers
uint16_t position;
modbus_lite_read_registers(motor_id, REG_POSITION, 1, &position);
modbus_lite_write_register(motor_id, REG_SPEED, speed_value);
Tradeoffs
| Decision | Rationale | Tradeoff |
|---|---|---|
| Synchronous API | Eliminates task/queue, 92% resource savings | Blocks caller |
| FSM receiver | Filters garbage bytes, CRC failure recovery | Slightly more complex code |
| Static buffers | No dynamic allocation overhead | 512 bytes fixed allocation |
| 3x retry | Auto recovery on CRC errors | Up to 300ms delay worst case |
| Frame delay | WiFi/BLE coexistence | Reduced Modbus throughput |
Results
Resource Savings:
- ROM: 50KB → 4KB (92% reduction)
- RAM: 20KB → 1.5KB (92% reduction)
- Init time: 100ms → 10ms (90% reduction)
Performance:
- Single request latency: 15-20ms → 5-10ms (50% reduction)
- CPU usage: 5-10% → <1% (90% reduction)
- CRC error rate: ~10% → <0.1% (FSM + retry effect)
Supported Functions:
| Function Code | Operation |
|---|---|
| 0x03 | Read Holding Registers |
| 0x04 | Read Input Registers |
| 0x06 | Write Single Register |
| 0x10 | Write Multiple Registers |
Key Takeaway
The key to lightweight design is “what to remove.” By eliminating ESP-Modbus’s general-purpose features (master/slave, TCP/RTU, async) and implementing only the required master RTU functionality, we achieved 92% resource reduction. The FSM-based receiver enables stable frame parsing even in noisy RS485 environments, and inter-frame delays solved WiFi/BLE coexistence issues. The result is 92% less resource usage with more reliable communication.