For a video demonstration of this project, see:
Bilibili: https://www.bilibili.com/video/BV1cc411m72m/
modbus_rt is a modbus communication library entirely implemented in C that can run on Windows, Linux, MacOS, and RTOS. Its main purpose is for use in control systems and industrial IoT. modbus_rt supports both slave and master modes, based on a socket-like handle concept, facilitating support for multiple instances. It encompasses not only the parsing and implementation of the modbus protocol but also aims to provide a ready-to-use application layer encapsulation, minimizing the need for application-layer porting.
modbus_rt supports nearly all commonly used modbus protocols, including modbus RTU, modbus ASCII, modbus TCP, modbus TCP over UDP, modbus RTU over TCP/UDP, modbus ASCII over TCP/UDP, and can operate in both Slave and Master modes. Additionally, modbus_rt provides endian conversion function interfaces (supporting 4 types of endian modes), facilitating the conversion of modbus registers to various data types (including bytes, strings, ints, longs, floats, doubles) across different platforms.
modbus_rt also offers an interface implementation based on pikapython, integrated into pikapython's official package manager. This allows for the implementation of modbus communication functions using python scripts, seamlessly compatible with the pikapython environment (future consideration may be given to providing interfaces based on micro python and Cpython, depending on familiarity with their underlying packaging, time permitting, and interest).
Furthermore, as modbus_rt is written in pure C, future considerations include supporting its compilation into a dynamic link library for use with other programming languages (primarily for use with C#, with most control HMI platforms based on QT or C#). Currently, no DLL encapsulation is provided, so it is only available based on QT demos.
Modified the SLAVE_DATA_DEVICE_BINDING macro definition to be determined by both the SLAVE_DATA_DEVICE_BINDING macro definition and the dev_binding flag variable, indicating whether to bind SLAVE hardware peripherals to registers. Therefore, when hardware peripherals need to be bound, the modbus_xxx_set_dev_binding(xxx_modbus_device_t dev, int flag) function must be called to implement the binding variable; otherwise, it defaults to no binding and can exist as a completely independent modbus instance. This is done so that, on the interaction end, code based on DTU on the PC can run on embedded devices with almost no porting.
Added file transfer functionality based on modbus (similar to tftp, implemented using custom modbus function codes, which can be modified in mosbus_p2p.h), which can be used to implement firmware upgrades (encrypted transmission and upgrades of firmware can be selected). Additionally, it can implement direct transmission of pikapython bytecode over serial or network to the device end (if the device end supports a file system), allowing the device end to run bytecode directly, making pikapython operation on the device end more efficient and convenient (this feature can be enabled or disabled via macro definition; disabling file transfer functionality maintains compatibility with previous versions).
Integrated into the official repository of pikaPython, allowing the use of pikaPython's package management tools for loading and updating. Additionally, endian conversion, RTU, and TCP were separated to facilitate users who only need to use Modbus TCP or modbus RTU to quickly integrate modbus_rt into their projects (requires RTOS or operating system support, currently does not support bare metal operation of modbus_rt).
Synchronized the protocol parsing part of modbus with agile modbus, updating to version 1.1.4 of agile modbus.
The examples in the example section have not yet been updated to the latest version; I will update them when I have time.
Please note: Considering the first item may cause some users who used the previous version of the code (limited to the device end, where hardware itself was bound to IO peripherals) to encounter compatibility issues after upgrading to the latest version (after creating a modbus slave instance, in addition to setting the SLAVE_DATA_DEVICE_BINDING macro definition to 1 to enable, an additional modbus_xxx_set_dev_binding function is needed to fully implement the hardware binding function). If you do not need the above functionality, you can directly use the betaV0.1 version released (it is recommended to use the latest version, as some minor bugs have been updated and fixed in the latest version, so the betaV0.1 version is limited to users who have already used this communication library and have no additional needs to prevent updates causing incompatibilities).
On the Windows platform, we use QT+MSVC for compiling and testing (theoretically, Visual Studio—based on MSVC, or MiniGW—based on GCC can also be used). We provide two demos: modbus_rt_slave_test and pikapython_test.
example\code_py
, we have prepared modbus test programs. To facilitate testing, we also provide the compiled executable program, located in the example\windows\pikapython_test_release
directory. You can double-click pikapython.exe
to run the REPL environment directly, or enter pikapython xxx.py
in the terminal to run the corresponding python file (note that the terminal run directory should be set to that directory, and the program to be executed should also be copied to that directory).Here, we simply test using the pikapython_test program, running rtu_master.py
, tcp_master.py
, udp_master.py
for basic functionality testing. Detailed testing methods can be referred to in the demonstration video (details are not repeated here):
We can also use pikapython_test for more functionalities, which can be referred to in later cases.
If you need to compile into your own application, simply copy the code in the src directory into the project, install the pthread library and libserialport library, and add the headers as needed, referring to the two QT+MSVC source projects we provide.
The Linux platform can theoretically use any standard Linux system hardware. Here, I use the Wildfire Luban Cat 1 as the testing platform, based on the Rockchip RK3566 platform. I just happened to have this development board on hand, and many ROS robotic systems currently on the market use this development board, which has certain representativeness. modbus_rt is an upper-layer application development API, so it is actually hardware-independent.
We provide two demos with the same functionality as on Windows. We can compile them directly to run. Since setting up a Linux environment may be somewhat difficult for beginners, here is a brief description of the build environment and compilation process. Detailed testing methods can be referred to in the demonstration video, where we provide a case, which is to use two USB to 485 cables to connect Windows and Linux and then conduct mutual communication testing.
Here, we flashed the latest Debian desktop version of firmware provided by Wildfire. Ensure that git, cmake, and gcc are installed. If not installed, you can install them using sudo apt install cmake
and sudo apt install git
(since the system on my development board is installed in EMMC, and the flash space is insufficient, so the code is compiled on a tf card, which I mounted to the /home/cat/sdcard
directory, which requires attention to give /home/cat/sdcard
directory operating permissions):
sudo mount /dev/mmcblk1 /home/cat/sdcard
sudo chown -R cat /home/cat/sdcard
chmod -R 755 /home/cat/sdcard
Note: If you need to compile libserialport under Linux, you also need to install autotools related tools, because libserialport is compiled based on autotools tools under Linux. If you need to compile examples under libserialport, you may also need to install pkg-config tools. Please install the corresponding compilation tools as prompted.
git clone https://github.com/sigrokproject/libserialport.git
git clone https://github.com/SenySunny/modbus_rt.git
sudo apt-get install autotools-dev autoconf automake libtool
sudo apt-get install pkg-config
cd libserialport/
./autogen.sh
./configure
make
sudo make install
sudo ldconfig
cd examples/
make
At this point, you can run the example, such as the following is the result of running `./list_ports`, showing two serial ports, one is the system debug serial port, and the other is my external USB to serial module:
cat@lubancat:~/sdcard/libserialport/examples$ ./list_ports
Getting port list.
Found port: /dev/ttyFIQ0
Found port: /dev/ttyUSB0
Found 2 ports.
Freeing port list.
cd ~/sdcard/modbus_rt/example/linux/modbus_rt_slave_test
sh make.sh
sudo build/modbus_rt_linux
cd ~/sdcard/modbus_rt/example/linux/pikapython_test
sh make.sh
sudo build/modbus_rt_linux
or enter the directory to run a specific file, such as running the test.py file directly below:
cd build
sudo ./pikapython test.py
The testing method is the same as on Windows, with the difference being that Linux terminals and Windows terminals are slightly different. Windows terminals can directly enter the program name to run the program, while Linux terminals check if there is a program with that name under the environment variable and do not directly run the program under the directory, so you need to enter .\pikapython
or run .\pikapython xxx.py
to run the program. Additionally, if the program's socket uses ports below 1024, such as the default modbus 502, you need to run it in administrator mode, entering sudo .\pikapython
or sudo .\pikapython xxx.py
to run the program. This is because Linux systems by default do not allow users to use ports below 1024.
Another point to note: If you use a Windows or Linux system as the device end. When enabling the slave mode based on UDP, if you want to enable the device IP discovery function, the Windows system needs to specify the network card's IP address when creating the slave device, similar to "192.168.28.150" used in Windows testing. BSD socket under Linux or RTOS systems needs to use NULL or an empty string "" to specify the default IP address; otherwise, it will not receive broadcast data packets from 255.255.255.255.
The RT-thread platform demo provides two sets of hardware, one of which is a DIY PLC development board I made earlier. Considering that users do not have this board for verification, and the network part of this board uses the W5500 network chip, I added a case demo based on "Wildfire STM32F407_Jiaoyang Development Board," which is implemented based on the "Wildfire STM32F407_Jiaoyang Development Board." Information about this development board can be referred to at "Wildfire Documentation Download Center: https://doc.embedfire.com/products/link/zh/latest/mcu/stm32_motor/ebf_stm32f407_jiaoyang/download/stm32f407_jiaoyang.html"
The RT-thread platform demo uses a DIY PLC development board I made earlier, with a case completely compatible with Siemens's S7-200 PLC case. The main control here uses a domestic semiconductor's APM32E103VET6 chip (basically compatible with STM32F103VET6, pin-to-pin compatible, but with twice the RAM of STM32, 128K, STM32 only has 64K, and the main frequency of APM32 is 120Mhz), and the network part uses a W5500 network chip. Running the rt-thread system. Circuit diagrams and test code can be seen in example\rt-thread\apm32e103vet6
This hardware has two RS485 interfaces. Our code defaults to using the RS485 closest to the network card for terminal testing. We connect the network cable and RS485 cable (one for modbus communication, one for terminal debugging), power up, compile, and download to the hardware platform. We can control and read the status of IO using modbus poll, or use the previously compiled Windows or Linux application programs to run modbus master examples and communicate with them.
Here, a brief explanation of the code is given. We created three slave examples: one for modbus rtu slave on RS485, one modbus tcp slave, and one based on modbus tcp slave over udp. The udp slave supports device discovery functionality.
Register distribution is as follows:
0x-Register: 20000~ : DO output register
1x-Register: 10000~ :DI input register (read-only)
3x-Register: 8000-8007 : Stores the device's basic information
8008-8015 :Device hardware address (here refers to the MAC address), note that 8008 stores the address length
(here is 6), 8009-8011 stores the MAC address, 8012~8015 are not used here.
8016-8025 : Stores the device's network information, which are IP address, subnet mask, gateway address,
primary DNS server, secondary DNS server. Each occupies 2 addresses.
4x-Register: 2008-2015 : Introduces MAC address matching functionality, specifically for modifying network information
via broadcast. Definition is the same as 8008-8015.
2016-2027 : Modify network information address registers, 2016-2025 addresses are IP address, subnet mask,
gateway address, primary DNS server, secondary DNS server, each occupies 2 addresses.
2016 as modification confirmation identifier, indicates which information to modify, can take values from 0~5.
Default 0 does not modify,
1 indicates only passing ip, subnet mask and gateway use default (subnet mask defaults to 255.255.255.0,
gateway address defaults to ip same segment's xxx.xxx.xxx.1);
2 indicates only passing ip and subnet mask, gateway uses default;
3 indicates ip, subnet mask, and gateway are modified simultaneously)
4 adds modification of the primary DNS server
5 modifies all information.
2017 to enable DHCP mode, setting 1 indicates enabling, only effective for current, setting after DHCP gets
the IP address and other network information will be stored in 3x register's 8016-8025, after reboot, the system
will return to static ip mode, and set the last DHCP IP as static IP, so this mode is used for when the router's available
IP is unknown to let the router automatically assign an ip to the device.
This code and the modbus_rt_slave_test function on Windows and Linux are consistent, i.e., running three modbus slave instances based on RS485, tcp, and udp. For udp, we added network device search and device network information modification functionality. Additionally, we wrote a QT-based upper computer program for finding and modifying device IP information (itself also based on modbus_rt), located in the example\windows\tools\find_device
directory. After we compile the program and download it to the microcontroller, we can use this software to find and modify the device's IP (this software supports multiple devices searching simultaneously, but modifying cross-segment device network information does not support multiple devices by default, mainly because: finding devices is through broadcast search, but if the found device and the computer are not in the same segment, network information will be modified through broadcast, but if the found device itself and the computer are in the same segment, directed IP address information will be sent).
The RT-Thread platform code has been modified to add MAC address matching functionality, thus supporting cross-segment multi-device modification functionality (it is important to note: MAC address matching functionality does not belong to the modbus_rt application framework. It needs to be implemented and matched by the application end, and it is essential to ensure MAC uniqueness. If multiple devices have the same MAC, this functionality may cause issues. The latest version of the firmware code has added MAC address matching functionality.).
Currently, the upper computer for finding and modifying device IP is only available on the Windows platform. The Linux platform is not yet provided, and the usage method will be demonstrated in the video.
For a brief introduction and usage instructions for the software, see Tool Software Usage Instructions
Here, we made two demos based on the Wildfire STM32F407_Jiaoyang development:
import modbus_rt
import modbus_rt_defines as cst
serial_name = "uart4"
ip_addr = ""
rm = modbus_rt.rtu(cst.MASTER)
rm.set_serial(serial_name)
rm.open()
ts = modbus_rt.tcp()
ts.set_net(ip_addr, 502, cst.SOCK_STREAM)
def pre_call(evt) :
slave = evt.slave
function = evt.function
addr = evt.addr
quantity = evt.quantity
if cst.READ_DISCRETE_INPUTS == function:
if addr >= 0 and addr <= 16 :
data = rm.excuse(slave, function, addr + 10000, quantity)
ts.excuse(cst.WRITE, cst.INPUTS, addr, quantity, data)
elif cst.READ_COILS == function:
if addr >= 0 and addr <= 16 :
data = rm.excuse(slave, function, addr + 20000, quantity)
ts.excuse(cst.WRITE, cst.CIOLS, addr, quantity, data)
def done_call(evt) :
slave = evt.slave
function = evt.function
addr = evt.addr
quantity = evt.quantity
if cst.WRITE_SINGLE_COIL == function:
if addr >= 0 and addr <= 16 :
data = ts.excuse(cst.READ, cst.CIOLS, addr, 1)
rm.excuse(slave, function, addr + 20000, data[0])
elif cst.WRITE_MULTIPLE_COILS == function:
if addr >= 0 and addr <= 16 :
data = ts.excuse(cst.READ, cst.CIOLS, addr, quantity)
rm.excuse(slave, function, addr + 20000, quantity, data)
ts.set_strict(0)
ts.set_pre_ans_callback(pre_call)
ts.set_done_callback(done_call)
ts.open()
The effect of executing this code is that if you write to the 20000 coil register, it defaults to a DO output operation. If you write to the address 0 coil register, it will default to writing to the extended IO module's 20000 coil register through RTU. Similarly, if you read the 10000 discrete input register, it defaults to reading the development board's DI input. But if you read the 0 discrete input register, it will default to reading the extended module's 10000 register through RTU. This conveniently allows for the expansion of IO peripherals or supports Ethernet devices as the main controller, with RTU peripherals as the slave, implementing scheduling of the slave by the main controller. Additionally, it supports the functionality of uploading information from the main device to an upper computer or the cloud.
For the FreeRTOS platform, we conducted tests on two hardware platforms. One is PikaPython's open-source hardware PikaPython-OpenHardware, based on the ESP32-S3 platform, developed based on ESP-IDF V5.1 version. Theoretically, it can be compatible with all ESP32 devices using ESP-IDF. The development board is completely open-source, with onboard isolated RS485 interface, and the network part can be tested using wifi. Actually, we can use any ESP32 series development board and minimum system board to run this example. The open-source repository for the development board is: https://gitee.com/Lyon1998/pikapython_openhardware , with the development board schematic shown below:
Another platform is based on the air780E 4G Cat.1 module from AirLink, whose core controller uses the Mxic EC618 chip platform. It can also be compatible with air780EG, air780EP, air780EPV, and other modules using Mxic EC618, EC718.
We use the open-source pikapython-air780e development board, which is completely open-source in both software and hardware. The open-source repository is: https://gitee.com/Lyon1998/pikapython-air780e, with the development hardware schematic shown below:
The test code repository for the platform (developed based on idf V5.1 environment) provides two demos, one running modbus_rt and the other running pikapython. The project code is under the example\FreeRTOS\PikaPython_OpenHardware directory:
The air780e-based case can directly use the official repository code of pikapython-air780e, with modbus_rt instances already synchronized with the official repository. Repository address: https://gitee.com/Lyon1998/pikapython-air780e , please follow the official repository's instructions for compilation and use.
Currently, the air780e platform, considering the 4G Cat.1 module has no LAN environment, only adapts modbus RTU and modbus ASCII, and does not adapt modbus TCP or other network-related modbus protocols.
Since all four platforms use the PikaPython encapsulated API interface and use scripts for communication, it doesn't matter which platform is used for testing. We use the more commonly used S7-1214C PLC platform for Siemens PLC, here only testing modbus TCP. We provide two demos:
The PLC project is in the example/PLC directory, created using TIA Portal V15.1.
Weintek HMI program: We use Weintek's MT8071IP HMI, combined with our RT-Thread platform hardware, for a DO control and DI detection example. Detailed information about the Weintek screen project is in example/WEINVIEW, opened and downloaded using EBproV6.08.02.500. Theoretically, operations on other HMIs are basically the same, so they are not tested one by one here.
Here, we just wrote a simple demo using the Weintek screen to read DI and write into DO to control the development board. Monitoring four input and four output channels. The HMI screen itself can achieve very powerful functions through modbus, which is not elaborated here.
Considering that many cost-sensitive projects or users will not use HMI (mainly because it is expensive). So here, we also used a national color serial screen for a demo example. National color serial screens, many models support modbus protocol, and the price is even only a few tens of yuan. Here, we chose a 2.8-inch serial screen, model: DC24320M028_1110_0T. We implement the same functionality with this serial screen as with the HMI screen, monitoring DI and controlling DO.
The project for the national color serial screen is in example/DACAI, with software using VisualTFT_3.0.0.1232, interface as follows:
Since the national color serial screen I used has an RS232/TTL-UART interface, I used a TTL to RS485 module externally during the demonstration, finally connecting to our RT-thread development board.
Here, we provide a simple example, implementing modbus TCP to modbus RTU functionality. We use a Linux system development board connected to a USB to RS485 module, connected to rt-thread platform hardware (to simulate modbus rtu-based peripherals—such as IO modules, sensor peripherals). Then, we run modbus tcp master on Windows, communicating with Linux to control the IIO on rt-thread platform hardware.
Here, mainly verifying the use of modbus_rt's callback function functionality to implement DTU conversion functionality.
cd ~/sdcard/modbus_rt/example/linux/pikapython_test/build
tcp2rtu_dtu.py
file and copy the following code:import modbus_rt
import modbus_rt_defines as cst
serial_name = "/dev/ttyUSB0"
ip_addr = ""
rm = modbus_rt.rtu(cst.MASTER)
rm.set_serial(serial_name)
rm.open()
ts = modbus_rt.tcp()
ts.set_net(ip_addr, 502, cst.SOCK_STREAM)
def pre_call(evt) :
slave = evt.slave
function = evt.function
addr = evt.addr
quantity = evt.quantity
if cst.READ_HOLDING_REGISTERS == function:
data = rm.excuse(slave, function, addr, quantity)
ts.excuse(cst.WRITE, cst.REGISTERS, addr, quantity, data)
elif cst.READ_DISCRETE_INPUTS == function:
data = rm.excuse(slave, function, addr, quantity)
ts.excuse(cst.WRITE, cst.INPUTS, addr, quantity, data)
def done_call(evt) :
slave = evt.slave
function = evt.function
addr = evt.addr
quantity = evt.quantity
if cst.WRITE_SINGLE_COIL == function:
data = ts.excuse(cst.READ, cst.CIOLS, addr, 1)
rm.excuse(slave, function, addr, data[0])
elif cst.WRITE_SINGLE_REGISTER == function:
data = ts.excuse(cst.READ, cst.REGISTERS, addr, 1)
rm.excuse(slave, function, addr, data[0])
elif cst.WRITE_MULTIPLE_COILS == function:
data = ts.excuse(cst.READ, cst.CIOLS, addr, quantity)
rm.excuse(slave, function, addr, quantity, data)
elif cst.WRITE_MULTIPLE_REGISTERS == function:
data = ts.excuse(0, cst.REGISTERS, addr, quantity)
rm.excuse(slave, function, addr, quantity, data)
ts.add_block("A", 0, 20000, 10)
ts.add_block("B", 1, 10000, 16)
ts.add_block("C", 4, 0, 10)
ts.set_strict(0)
ts.set_pre_ans_callback(pre_call)
ts.set_done_callback(done_call)
ts.open()
We can see that the main functionality of the code is to create a tcp slave and an rtu master, converting write coil and holding register commands into operations for the external rtu master. When receiving commands to read holding registers and discrete input registers, it first obtains device information from the corresponding address through the rtu master to update its own register information, ensuring the information read corresponds to the rtu slave device's information.
.py
filesudo ./pikapython tcp2rtu_dtu.py
We will gradually provide more application demos based on the situation
Create an example through modbus_tcp(mode), modbus_rtu(mode), or modbus_ascii(mode), where mode: 0 represents modbus slave; 1 represents modbus Master. This function is mainly used to create the content of the device block, initialize the mutexes and semaphores needed by the device.
Initialize the corresponding parameters using functions like modbus_xxx_set_net, modbus_xxx_set_serial, etc., details can be found in the API manual.
Start running the device using modbus_xxx_open, this function is mainly used to establish the device's communication port, create communication-related threads.
If the device is temporarily not needed, you can use modbus_xxx_close to close the device, this function is mainly used to end the device communication-related threads, close the device's communication interface.
If you completely stop using the corresponding device, you can use modbus_xxx_destroy to destroy the device, this function is mainly used to destroy the device's mutexes and semaphore-related numbers, destroy the device's data information and device letter. At this point, the device pointer will point to a null pointer.
The slave adds register data block function as the modbus_xxx_add_block function. Parameters are respectively the register type (default 0, 1, 3, 4 types), register start address, storage data space address, register length (not data length).
Use the modbus_xxx_excuse function to get or modify the register value, for slave mode it's for reading or writing its own register values, for master mode it's for reading or writing connected slave device register values.
If dtu or modbus to other protocol conversion content needs to be implemented, you can set two callback functions using modbus_xxx_set_pre_ans_callback and modbus_xxx_set_done_callback, called respectively when the slave device receives data from the master end, and after modbus communication ends, protocol conversion related content can be completed in the callback functions (note that callback functions cannot be blocking functions, otherwise it will cause modbus communication timeout).
If you need to bind the modbus slave device with the device interface, such as implementing DIDO, PWM, ADC, DAC, motor control, modifying PID parameters, etc., peripheral modules, you can modify the SLAVE_DATA_DEVICE_BINDING macro definition in modbus_config.h to 1, and complete the binding work of hardware peripherals with slave registers yourself. Detailed operations can be referred to in the RT-thread platform embedded demo. The general operation is as follows:
You need to add the "device_data.h" header file and complete the following functions:
dev_write_bits: Write bits register
dev_read_bits: Read bits register
dev_write_regs: Write regs register
dev_read_regs: Read regs register
dev_data2modbus_slave: Register initialization (bind slave register and hardware peripheral information)
modbus_xxx_set_strict is used in slave mode, setting strict to 0 means communication will not perform matching checks on the device address sent by the master, i.e., any address can communicate with this slave device, mainly aimed at implementing dtu and protocol conversion related functionalities.
modbus_data_xxx related functions are used for converting registers and various data types to meet communication needs in different scenarios. The device defaults to little-endian mode, and since different devices have different endian modes, different endian modes can be used to convert data.
Little-endian byte swap:
Register 0: 0x0001, Register 1: 0x0002
Represents the number: 0x20001 (131073)
Big-endian byte swap:
Register 0: 0x0001, Register 1: 0x0002
Represents the number: 0x10002000 (16,777,728)
Internal big-end, external little-endian(Little-endian):
Register 0: 0x0001, Register 1: 0x0002
Represents the number: 0x2000100 (33,554,688)
Internal little-end, external big-endian(Big-endian):
Register 0: 0x0001, Register 1: 0x0002
Represents the number: 0x10002 (65538)
Please click the star in the upper right corner to give me support, and feel free to fork.
If this software has helped you, feel free to buy me a coffee~ Thank you very much!
This project references or uses content from the following open-source projects, and I would like to thank the creators of the following projects.
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。