Open source Modbus protocol stack selection guide: deep comparison of seven protocol stacks

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

Why are you reading this article

This question has been asked thousands of times on Stack Overflow, Control.com, and CSDN - 'Which open-source Modbus library should I use?'? . Each answer is a few scattered sentences, and no one has systematically compared them. The Chinese community is even worse off, with half of the posts found posting code for NModbus4 from ten years ago, without even knowing that the NModbus4 main repository has been migrated to 3.0. x.

I spent three days flipping through the repositories of these seven protocol stacks, running the smallest examples of each one, and confirming the maintenance status. Here are the results. If you are in a hurry, jump directly to the section of "Beginner Recommendation Path", select the library corresponding to your platform, and start working.

Overview Comparison Table

protocol stackLanguagelicenseNumber of StarsMaintenance statusmain siteslaveRTUTCPFor whom
FreeModbusCBSD~1.5klow activitypaySTM32 Bare/RTOS
libmodbusCLGPL v2.1+~3.5kactiveLinux industrial computer/gateway
pymodbusPythonBSD~2.2kVery activeUpper computer/test script
NModbusC#MIT~700active. NET/WinForm upper computer
jamodJavaApache 2.0~300stagnationJava legacy system
modbus-tkPythonLGPL~500low activityRapid Prototyping
QtModbusC++LGPL/GPLQt built-inactiveQt Cross Platform Applications

The Star number was captured by me in June 2026, and the approximate order of magnitude is correct. You can check GitHub for the specific number at a glance.

FreeModbus - The Standard Answer for Embedded Bare Metal Devices

GitHub: https://github.com/cwalter-at/freemodbus

FreeModbus was written by Christian Walter, an Austrian embedded engineer. This thing holds the same position in the STM32 community as ext4 in the Linux kernel - you can use other things, but using it is the least error prone.

**Code size * *: Approximately 6-12KB ROM after compilation, depending on which feature codes and transfer modes you have enabled. The RAM overhead is several hundred bytes, mainly consisting of a few data buffers and event queues. Basically, a Cortex-M0 can run.

**The function code supports * *: 03 (read hold register), 04 (read input register), 06 (write single register), 16 (write multiple registers), 01 (read coil), 02 (read discrete input), 05 (write single coil), and 15 (write multiple coils). There are also 17 (report from station ID). Note that there are no 22 (mask write register) and 23 (read/write multiple registers), you need to add them yourself if needed.

**Transmission mode * *: RTU, ASCII, TCP are all supported, and can be switched by compiling macros.

**Master/Slave * *: This is the most easily misunderstood aspect of FreeModbus. The official repository only open-source the code for the secondary site, while the primary site is paid for. There are many community hacks on GitHub that have added main site functionality, such as Armink's FreeModbus_Slave-Master-RTT-STM32, which is open-source and of good quality, but not officially maintained. When using these third-party versions, please note that the timeout handling and retry logic are implemented crudely and should not be directly put into production.

**Code example * * - Initialize an RTU slave on STM32, address 1, baud rate 9600, no checksum:

#include "mb.h"

int main(void) {
    eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE);
    eMBEnable();
    while (1) {
        eMBPoll();
    }
}

Just these three lines. `EMBPoll() is a non blocking state machine that runs the entire protocol stack. You need to feed it a 3.5T timeout signal in the timer interrupt. This 3.5T timer is the number one pitfall for FreeModbus porting, which will be discussed in detail below.

**Known issues * *:

**Document * *: There is only one API document in HTML format, which is sufficient but not very good. There are quite a few Chinese community tutorials, CSDN has a bunch to search for.

Libmodbus - the top choice for Linux industrial computers

GitHub: https://github.com/stephane/libmodbus

Maintainer St é phane Raimbault, French. Libmodbus is the most mature Modbus library written in C language, without exception. The 3.5k star is not for nothing.

The design concept of this library is completely different from FreeModbus. FreeModbus is designed for resource constrained MCUs, using callback functions and state machines. Libmodbus is POSIX style, blocking the API. The ` modbus_dead_degisters() ` call will wait until the data returns or times out. People who write industrial computer programs like this style - simple and direct, without worrying about any state machines.

**Function code support * *: Almost all. 01/02/03/04/05/06/07/0F/10/11/16/17, It also supports 22 (mask write) and 23 (read/write multiple). There are ` modbus_det_float() `/` modbus_get_float() ` that can directly process floating-point numbers according to IEEE 754, supporting four byte orders: ABCD/DCBA/BADC/CDAB.

**Transmission mode * *: RTU+TCP fully supported. The same API, 'modbus_new_rtu()' creates a serial context, 'modbus_new_tcp()' creates a TCP context, and then the read and write interfaces are exactly the same.

**Both master and slave stations are supported. The TCP slave uses the combination of 'modbus_tcp_isten()' and 'modbus_tcp'accept()', similar to writing a Linux socket server.

**Code Example * * - Read the hold register 0x0000 from slave station 1 and return a 16 bit value:

#include <modbus.h>
#include <stdio.h>

int main() {
    modbus_t *ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
    modbus_set_slave(ctx, 1);
    modbus_connect(ctx);
    uint16_t val;
    modbus_read_registers(ctx, 0, 1, &val);
    printf("Register 0 = %dn", val);
    modbus_close(ctx);
    modbus_free(ctx);
}

**Known issues * *:

**Document * *: The official website libmodbus.org has a set of manual page style documents, as well as an online version of mkdocs. Clear but without many example projects, just the ones in the 'tests/' directory.

Pymodbus - the factual standard of Python ecosystem

GitHub: https://github.com/pymodbus-dev/pymodbus

Pymodbus is currently the most actively maintained Modbus library, without a doubt. After the Pymodbus dev organization took over, 3. x rewrote the asynchronous architecture, supported asyncio, and continued to release versions. The latest stable version by the end of 2024 is v3.6. x.

**Function code * *: fully supported. 01/02/03/04/05/06/15/16/22/23, even 43 (reading device recognition) are available.

**Transmission mode * *: RTU+TCP+TLS. TLS support is newly added to pymodbus 3. x, which allows you to run ` modbus+tls://` directly. ASCII mode is also supported but marked as obsolete in 3. x.

**Master/Slave * *: Both support and support both synchronous and asynchronous APIs. The slave can run a complete simulator - ` pymodbus. simulator ` can start a virtual device with multiple registers from a JSON configuration file, which is very useful for upper computer development and debugging.

**Code Example * * - Read a hold register in synchronous mode:

from pymodbus.client import ModbusSerialClient

client = ModbusSerialClient(port="/dev/ttyUSB0", baudrate=9600)
client.connect()
rr = client.read_holding_registers(address=0, count=1, slave=1)
print(rr.registers[0])
client.close()

The asynchronous version has a few more lines, using the 'asynchronous with' context manager and running with asyncio.

**Known issues * *:

**Document * *: There is a complete document on readthedocs, and there are dozens of example scripts in the 'examples/' directory. There is no Chinese document, but the English is very clear.

NModbus - the only serious choice for the. NET platform

GitHub: https://github.com/NModbus/NModbus

The history of NModbus is a bit convoluted. The earliest NModbus was a project on Google Code (yes, that Google Code), which later migrated to GitHub and became NModbus4. Later, NModbus4 also stopped updating. The current NModbus/NModbus is the successor to NModbus4, maintained by rquackenbush, actively under development, with the nuget package name 'NModbus', latest version 3.0. x, supported NET 6+。

**Function code * *: fully supported on 01/02/03/04/05/06/15/16. 22 and 23 require custom function code processing.

**Transmission modes * *: RTU, ASCII, TCP, UDP are fully supported. The serial port supports Windows/Linux through the NModbus. Serial package.

**Both master and slave stations are supported. The slave station supports custom data storage and can map registers to memory, databases, and even PLCs.

**Code example * * - TCP master reads a register:

using NModbus;

var client = new TcpClient("192.168.1.100", 502);
var factory = new ModbusFactory();
var master = factory.CreateMaster(client);
ushort[] result = master.ReadHoldingRegisters(1, 0, 1);
Console.WriteLine(result[0]);

**Known issues * *:

**Document * *: README is sufficient, there are several examples in the 'Samples/' directory. Mainly relying on old posts on Stack Overflow.

Jamod - the 'senior' of the Java ecosystem, but I suggest you don't use it

SourceForge: https://sourceforge.net/projects/jamod/

Jamod was written by Dieter Wimberger in 2002, which is longer than many engineers present here. The last substantial update was in 2010, and it has been largely absent since then. The openHAB community fork has fixed several bugs in a version for its own use, but that fork is only passively maintained.

If you are starting a new Java project to do Modbus now, skip jamod and look at j2mod (GitHub: steveohara/j2mod). J2mod is a rewritten version of jamod, licensed under Apache 2.0, Java 8+, Still being updated (last submitted in July 2024), with full support for RTU and TCP, both master and slave stations are available.

But since this article is going to cover Jamod, I will still write it. Jamod supports function codes: 01/02/03/04/05/06/15/16. Transmission mode RTU+ASCII+TCP. Serial communication relies on 'javax. comm' (a very old API that JDK does not come with), and alternative solutions are RXTX or jSerial Comm.

Code example:

import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.io.ModbusTCPTransaction;
import net.wimpi.modbus.msg.ReadInputRegistersRequest;
import net.wimpi.modbus.msg.ReadInputRegistersResponse;
import net.wimpi.modbus.net.TCPMasterConnection;
import java.net.InetAddress;

TCPMasterConnection conn = new TCPMasterConnection(
    InetAddress.getByName("192.168.1.100"));
conn.connect();
ReadInputRegistersRequest req = new ReadInputRegistersRequest(0, 1);
ModbusTCPTransaction trans = new ModbusTCPTransaction(conn);
trans.setRequest(req);
trans.execute();
ReadInputRegistersResponse res = (ReadInputRegistersResponse) trans.getResponse();
System.out.println(res.getRegisterValue(0));
conn.close();

The word 'known problem' is too light for Jamod, it is already a problem in itself. But if you are maintaining an old system from 2012 and have to use it, then openHAB's fork is more reliable than the original version.

Modbus tk - a great helper for rapid prototyping

GitHub: https://github.com/ljean/modbus-tk

Written by Frenchman Luc Jean, tk in the name is an abbreviation for TestKit - positioned very clearly as a testing tool. Not for use in production environments. But in reality, many people use it for production because it is indeed simple.

**Function code * *: 01/02/03/04/05/06/15/16. Not supported on 22/23.

**Transmission mode * *: RTU+TCP. ASCII is not supported.

**Both master and slave stations are supported. The slave can easily create simulated devices using 'add_stave' and 'add-block'. There is a built-in hook function mechanism that can insert custom logic when receiving read and write requests - this design is very clever, and when writing test scripts, fault scenarios can be directly injected into the hook.

**Code Example * * - RTU Master Station Read Register:

import serial
import modbus_tk.defines as cst
from modbus_tk import modbus_rtu

master = modbus_rtu.RtuMaster(
    serial.Serial(port="/dev/ttyUSB0", baudrate=9600))
val = master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 1)
print(val[0])

**Known issues * *:

**Document * *: The examples in the 'examples/' directory are all the documents. Fortunately, the code is small and can be read in just ten minutes.

QtModbus - a native solution for Qt developers

This is not a 'library', it is part of the Qt official 'qtserialbus' module. Qt 5.8 was introduced and is now a standard module for Qt 6.

Because it is an official Qt, the API design is completely Qt style: signal slots, event loops, and 'QModbusReply'. Cross platform natural support - the same set of code can run on Windows, Linux, macOS, and embedded Linux (Boot2Qt).

**Function code * *: Supports all standard types through the enumeration of 'QModbusDataUnit:: LoadType': Coils、DiscreteInputs、InputRegisters、HoldingRegisters。 The underlying layer can send custom function codes.

**Transmission mode * *: Only TCP (QModbusTcpClient/QModbusTcpServer). **There is no RTU * *. This is an important limitation - Qt official does not have built-in Modbus RTU support, so you have to use 'QSerilPort' to write RTU frame parsing at the application layer yourself, or find a third-party implementation. Some developers use 'QModbusRtuSerialMaster' (a community project) to fill this gap.

**Both master and slave stations are supported. TCP slaves can be implemented through QModbusTcpServer to map custom data to the registry of QModbusServer.

**Code example * * - TCP client reads a register:

#include <QModbusTcpClient>
#include <QModbusDataUnit>

auto client = new QModbusTcpClient(this);
client->setConnectionParameter(
    QModbusDevice::NetworkAddressParameter, "192.168.1.100");
client->setConnectionParameter(
    QModbusDevice::NetworkPortParameter, 502);
client->connectDevice();

QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, 0, 1);
auto *reply = client->sendReadRequest(unit, 1);
connect(reply, &QModbusReply::finished, this, [reply]() {
    qDebug() << reply->result().values().at(0);
});

Asynchronous, connect the results using signal slots. Note that the lifecycle of QModbusReply * returned by sendReadRequest is managed by Qt and should not be manually deleted.

**Known issues * *:

**Document * *: Qt official documentation is top-notch. The example project can be found on the Qt Creator welcome page.

Recommended path for beginners

Don't worry, choose according to your technology stack:

-STM32 bare metal/FreeRTOS → FreeModbus. You have no other choice, it's the standard answer. Transplanting takes half a day, connect the 3.5T timer and add another half, then you don't have to worry about it anymore

Classic combination scheme

**FreeModbus Slave+Pymodbus Host Computer Test "* *: Embedded devices run FreeModbus as a slave, and during the development phase, Python is used to write Pymodbus scripts to read and write registers for functional verification. More flexible than using Modbus Poll - you can add data validation, boundary testing, and stress testing to the script. After debugging, switch to a real upper computer (C # or Qt).

**Libmodbus gateway+pymodbus configuration tool "* *: The industrial computer runs libmodbus to connect with dozens of Modbus RTU devices below, collects data, and transfers it to MQTT for cloud deployment. Using Pymodbus for device parameter reading and writing in the web backend - without requiring real-time performance, Python's development efficiency crushes everything.

**QtModbus for interface+libmodbus for underlying "* *: Qt is used for cross platform desktop SCADA, human-computer interaction, and charts. However, Modbus communication does not directly use QtModbus (as there is no RTU), but instead uses libmodbus's C API for underlying data collection, with Qt only responsible for display. This combination is very common in the factory monitoring system.

Avoiding Pit List

**The first pit: FreeModbus's 3.5T timer**

The Modbus RTU protocol specifies a minimum interval of 3.5 characters between frames. At a baud rate of 9600, one character takes approximately 1ms, and 3.5T ≈ 3.5ms. Many people directly set the timer to interrupt with a 3.5ms cycle when transplanting. Wrong. The usage of FreeModbus's 3.5T timer is to reset the timer every byte received. If the timer overflows (indicating that there are no new bytes in the 3.5T), it is considered that a frame has ended. So the timer should be set to single time mode, not periodic mode. Call 'vMBPortTimersEnable ()' in the interrupt received to restart the timer.

When the baud rate is different, the corresponding time for 3.5T is different: 9600bps →~3.65ms, 19200bps →~1.83ms, 115200bps →~304 μ s. Under 115200, the 3.5T has only about 300 microseconds. If your timer has a minimum granularity of 1ms and cannot be calibrated so finely, frame interval detection will fail. At this point, either lower the baud rate to 38400 or use the high-precision mode of the hardware timer.

**Second pitfall: libmodbus's connect vs new_tcp vs listen semantics**

`Modbus_new_tcp ("192.168.1.100", 502) created context, but has not yet connected.

The most common error: habitually calling 'modbus_comnect()' when writing TCP slave, and then realizing that no request from the master can be received no matter what. Because 'connect()' is to connect with others, not to wait for them to connect with you.

Another pitfall: the 'ctx' pointer remains after 'modbus_free (ctx)'. If you want to reconnect in a loop (such as when the network is down and then restored), you must first 'ctx=NULL' and then 'modbus_new_tcp()', otherwise there is a probability of accessing the released memory. Wireshark packet capture will see TCP SYN resending tens of thousands of times - that's' modbus_comnect() 'accessing the wild pointer.

**The third pitfall: Pymodbus synchronous vs asynchronous mode mixed use**

Pymodbus 3. x has two sets of client APIs: synchronous Modbus Serial Client/Modbus TcpClient, and asynchronous` AsyncModbusSerialClient` / `AsyncModbusTcpClient`。

If you use a synchronous client in a process that already has an asyncio event loop, the underlying socket I/O will block the event loop, causing all asynchronous tasks to pause. On the other hand, using an asynchronous client in a purely synchronous script will result in an 'await' syntax error.

The rule is simple: either fully synchronous or fully asynchronous. Testing scripts usually require synchronization. If a production environment requires a process to connect dozens of devices simultaneously, asynchronous mode must be used, otherwise one device will timeout and drag the other 49 devices together.

**The fourth pitfall: Byte order issue - not the pot of the library, but always a pitfall**

The Modbus protocol itself only defines the transmission format for 16 bit registers - Big Endian, with high bytes first. But when you use two registers to transfer a 32-bit floating-point number, the protocol doesn't say who comes first or who comes back. Different manufacturers have different handling methods:

-Schneider PLC uses large terminal double word (ABCD): register N stores high 16 bits, N+1 stores low 16 bits

Libmodbus provides' modbus_det_float() 'and four types of byte order constants, pymodbus has' Binary Payload Decoder' to specify byte order, while FreeModbus has nothing - you can use 'union' or 'memcpy' to spell it yourself.

The lesson of blood: After adjusting for two days, I found that the temperature reading was an astronomical number, and in the end, the byte order was reversed. First, confirm if the manual of the other party's device specifies the storage format for Float. If you haven't written it, read two registers and spell it yourself. If you change the four arrangements, one of them will always be correct.

**The fifth pit: the pit of broadcasting address 0**

Modbus specifies that address 0 is a broadcast address. The master station sends a broadcast frame, and all slave stations execute it but do not reply. But in reality, many slave devices do not support broadcasting at all, and there is no response when sending to address 0. Some devices, although supporting broadcasting, do not fully support it -06 (writing a single register) broadcasting can be executed, 16 (writing multiple registers) broadcasting is ignored.

If you are broadcasting with 'libmodbus_det_slave (ctx, 0)', don't expect a response. `The behavior of modbus_dead_degisters() in broadcast mode is undefined.

Quick search for selection decision

If you don't want to read thousands of words on it, here's a decision tree:

1. The target platform is MCU bare metal → FreeModbus

I have pasted the minimum runnable examples for all the libraries, and by copying, pasting, and changing the device address, they can be run. When communication cannot be adjusted, first use Modbus Poll or pymodbus to set up a pure software loop to confirm that there is no problem with the hardware link, and then suspect a bug in the protocol stack.

If you have any questions, please chat on the modbus.cn forum.

Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support

Leave a Reply

Your email address will not be published. Required fields are marked *.