A lightweight, portable, and robust Modbus slave implementation written in C for embedded systems. libModbus provides a complete RTU protocol stack with minimal memory footprint and comprehensive test coverage.
✅ Vast Modbus RTU Protocol Support
- Read Coils (0x01)
- Read Discrete Inputs (0x02)
- Read Holding Registers (0x03)
- Read Input Registers (0x04)
- Write Single Coil (0x05)
- Write Single Register (0x06)
- Write Multiple Coils (0x0F)
- Write Multiple Registers (0x10)
- Mask Write Register (0x16)
- Read/Write Multiple Registers (0x17)
- Read Device Identification (0x2B / MEI 0x0E)
🚀 Optimized for Embedded Systems
- Minimal memory footprint
- No dynamic memory allocation
- ISR-safe design
- Portable code
🧪 Comprehensive Testing
- 100% test coverage
- Unity test framework
- Unit tests for all functions
- Integration tests
- Timing and state machine tests
🔧 Easy Integration
- Simple callback interface
- Flexible configuration
- Platform-agnostic design
- Well-documented API
git clone --recurse-submodules https://github.com/OpenModbus/libModbus.git
cd libModbus#include "modbus_slave.h"
// Define your application callbacks
ModbusExceptionCode read_holding_registers(uint16_t addr, uint16_t count, uint8_t *dest) {
for (int i = 0; i < count; i++) {
modbus_be16_set(&dest[i * 2], your_register_data[addr + i]);
}
return MODBUS_EX_NONE;
}
void transmit_data(const uint8_t *data, uint16_t length) {
// Send data via UART, SPI, etc.
uart_write(data, length);
}
int main() {
ModbusSlave slave;
ModbusSlaveConfig config = {
.address = 0x01,
.write = transmit_data,
.read_holding_registers = read_holding_registers,
// Add other callbacks as needed
};
// Initialize the Modbus slave
if (modbus_slave_init(&slave, &config) != 0) {
return -1;
}
while (1) {
modbus_slave_poll(&slave);
// Your application code here
}
}// Initialization
int modbus_slave_init(ModbusSlave *slave, const ModbusSlaveConfig *cfg);
// Reception (call from UART ISR)
void modbus_slave_rx_byte(ModbusSlave *slave, uint8_t byte);
// Timing (call from timer ISR)
void modbus_slave_1_5t_elapsed(ModbusSlave *slave);
void modbus_slave_3_5t_elapsed(ModbusSlave *slave);
// Polling (call from main loop)
void modbus_slave_poll(ModbusSlave *slave);typedef struct {
uint8_t address; // Slave address (0 for broadcast)
// Required: Transmit callback
void (*write)(const uint8_t *data, uint16_t length);
// Optional context pointer passed to user-defined function callbacks
void *user_ctx;
// Optional user-defined function handlers
const ModbusUserFunctionHandler *user_functions;
uint8_t user_function_count;
// Optional Read Device Identification objects
const ModbusDeviceIdObject *device_id_objects;
uint8_t device_id_object_count;
// Optional callbacks for supported functions
ModbusReadCoilsCb read_coils;
ModbusReadDiscreteInputsCb read_discrete_inputs;
ModbusReadHoldingRegistersCb read_holding_registers;
ModbusReadInputRegistersCb read_input_registers;
ModbusWriteSingleCoilCb write_single_coil;
ModbusWriteSingleRegisterCb write_single_register;
ModbusWriteMultipleCoilsCb write_multiple_coils;
ModbusWriteMultipleRegistersCb write_multiple_registers;
ModbusMaskWriteRegisterCb mask_write_register;
ModbusReadWriteMultipleRegistersCb read_write_multiple_registers;
} ModbusSlaveConfig;Read Device Identification (0x2B / MEI 0x0E) is configured with a static object
table. Values are sent as ASCII strings.
static const ModbusDeviceIdObject device_id[] = {
{ MODBUS_DEVICE_ID_VENDOR_NAME, "OpenModbus" },
{ MODBUS_DEVICE_ID_PRODUCT_CODE, "ExampleBoard" },
{ MODBUS_DEVICE_ID_MAJOR_MINOR_REVISION, "1.0" },
{ MODBUS_DEVICE_ID_PRODUCT_NAME, "Example Modbus Device" },
};
ModbusSlaveConfig config = {
.address = 0x01,
.write = transmit_data,
.device_id_objects = device_id,
.device_id_object_count = sizeof(device_id) / sizeof(device_id[0]),
};The first three objects are the mandatory Basic Device Identification objects from the Modbus specification. The library also supports individual object access and segmented stream responses when the full list does not fit into one Modbus PDU.
Unknown function codes can be handled by registering a small dispatch table:
static ModbusExceptionCode custom_handler(
uint8_t function_code,
const uint8_t *request,
uint16_t request_len,
uint8_t *response,
uint16_t response_max,
uint16_t *response_len,
bool *send_response,
void *user_ctx
) {
(void)function_code;
(void)user_ctx;
if (request_len != 2u) return MODBUS_EX_ILLEGAL_DATA_VALUE;
if (response_max < 2u) return MODBUS_EX_SLAVE_DEVICE_FAILURE;
response[0] = request[0];
response[1] = request[1];
*response_len = 2u;
*send_response = true;
return MODBUS_EX_NONE;
}
static const ModbusUserFunctionHandler user_functions[] = {
{ 0x41u, custom_handler },
};
ModbusSlaveConfig config = {
.address = 0x01,
.write = transmit_data,
.user_functions = user_functions,
.user_function_count = sizeof(user_functions) / sizeof(user_functions[0]),
};The callback receives only the request payload after the function code. It writes only the response payload after the function code. libModbus still adds the slave address, function code, CRC, and exception response framing.
Set *send_response = false when a user-defined function should execute without
replying. If the handler does not change it, the default behavior is to send a
normal response.
-
CMake (>= 3.10)
-
GCC or any C99-compatible compiler
-
Unity test framework (included)
# Run all tests
make# Run only CRC tests
./test_runner -g modbus_crc16
# Run only handler tests
./test_runner -g modbus_handler_read_coils
# Run integration tests
./test_runner -g modbus_integrationYou need to provide two hardware-specific functions:
void your_transmit_function(const uint8_t *data, uint16_t length) {
HAL_GPIO_WritePin(RS_DIR_GPIO_Port, RS_DIR_Pin, GPIO_PIN_SET); // Set DE pin
HAL_UART_Transmit(&huart4, data, length, HAL_MAX_DELAY); // Transmit data
while (__HAL_UART_GET_FLAG(&huart4, UART_FLAG_TC) == RESET); // Wait until transfer completes
HAL_GPIO_WritePin(RS_DIR_GPIO_Port, RS_DIR_Pin, GPIO_PIN_RESET); // Reset DE pin
}-
Configure a timer to call the timing functions based on your baud rate:
-
modbus_slave_1_5t_elapsed()after 1.5 character times -
modbus_slave_3_5t_elapsed()after 3.5 character times
-
-
Integrate with Your Application
// In UART receive ISR
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance != UART4) return;
__HAL_TIM_SET_COUNTER(&htim8, 0); // Reset the timer
timer_counter = 0;
modbus_slave_rx_byte(&modbus, uart_rx_buffer[0]); // Provide libModbus slave with the received byte
HAL_UART_Receive_IT(&huart4, uart_rx_buffer, 1); // Start listening on the next byte
}
// In timer ISR
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if (htim->Instance != TIM8) return;
if (modbus.state != RECEPTION && modbus.state != CONTROL_AND_WAITING) return; // No need to increment the timer counter if not in RECEPTION or CONTROL_AND_WAITING state
timer_counter++;
if (timer_counter == 3) modbus_slave_1_5t_elapsed(&modbus);
if (timer_counter == 7) modbus_slave_3_5t_elapsed(&modbus);
}We welcome contributions! Please see our Contributing Guide for details.
-
Fork the repository
-
Create a feature branch
-
Add tests for new functionality
-
Ensure all tests pass
-
Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.