1、modbus简介
Modbus服务器:接收处理来自客户端的请求,并返回相应的响应;
Modbus客户端:向Modbus服务器发送请求,并接收服务器返回的响应的设备或程序;
2、modbus poll调试工具下载
modbus poll用于测试和调试Modbus从设备;Modbus Slave用来模拟从站设备,接收主站的命令包,回送数据包。
官网地址 Modbus test and simulation
使用方法参考 Modbus Slave和Modbus Poll的使用说明-CSDN博客
3、windows系统libmodbus库编译与安装
libmodbus库的编译与安装参考博客 [开源库的使用]libModbus编译及使用_libmodbus库-CSDN博客
注意:根据自己电脑配置,编译对应版本的modbus库,作者第一次编译win32位的,导致拷贝项目中运行时出现库计算机类型“win32”与目标计算机类型“×64”冲突以及无法解析外部链接库的错误。
4、libmodbus库的使用
步骤一:*.h、*.lib、*.dll文件拷贝
将libmodbus-master\src目录下所有.h文件、libmodbus-master\src\win32目录下的config.h文件、编译生成的modbus.dll与modbus.lib文件复制到项目对应文件夹下。
步骤二:导入lib库
QT中配置方式:
VS中配置方式:导入附加库目录及附加依赖项。
步骤三:libmodbus库的使用
参考链接 Modbus通讯开发随记1——LibModbus库的学习-CSDN博客
modbus方法:
- 接受指令请求
int modbus_receive(modbus_t *‘ctx’, uint8_t *‘req’)
该函数用于从机设备ctx接收的Modbus主机指令请求,接收到的指令保存在req所指的缓存区。
函数返回值:成功将返回请求指令的长度(包括无请求将返回0),否则将返回-1。
- 返回请求
int modbus_reply(modbus_t *‘ctx’, const uint8_t *‘req’, int ‘req_length’, modbus_mapping_t *‘mb_mapping’)
该函数用于从机设备ctx对请求进行响应,req为请求指令,req_length为请求指令的长度,mb_mapping为从机设备ctx的存储镜像空间。如果请求指示读取或写入值,则操作将根据操作数据的类型在modbus映射_mb_mapping_中进行。
函数返回值:成功将返回响应指令的长度,否者将返回-1。
从机测试代码:
//----------------从机--------------------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "modbus.h"
#include <string>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char *argv[]) {
int socket = -1;
uint8_t device = 1;
//无符号8位整数类型
uint8_t *query;
modbus_t *mb;
int ret;
_modbus_mapping_t *mb_mapping;
mb = modbus_new_tcp("127.0.0.1", 502);
if (mb == NULL)
{
modbus_free(mb);
printf("new tcp failed:%s\n", modbus_strerror(errno));
return 0;
}
query = (uint8_t *)malloc(MODBUS_TCP_MAX_ADU_LENGTH);
//modbus_mapping_new_start_address:分配寄存器阵列
if ((mb_mapping = modbus_mapping_new_start_address(0, 0, 0, 0, 0, 10, 0, 0)) == NULL)
{
modbus_free(mb);
printf("new map failed: %s\n", modbus_strerror(errno));
return 0;
}
vector<uint16_t> totaldata;
totaldata.push_back(5);
totaldata.push_back(287);
totaldata.push_back(2);
totaldata.push_back(7);
totaldata.push_back(3);
totaldata.push_back(639);
totaldata.push_back(255);
totaldata.push_back(711);
totaldata.push_back(57);
totaldata.push_back(672);
//最大值
mb_mapping->tab_registers[0] = totaldata[0];
mb_mapping->tab_registers[1] = totaldata[1];
//最小值
mb_mapping->tab_registers[2] = totaldata[2];
mb_mapping->tab_registers[3] = totaldata[3];
//平均值
mb_mapping->tab_registers[4] = totaldata[4];
mb_mapping->tab_registers[5] = totaldata[5];
//体积
mb_mapping->tab_registers[6] = totaldata[6];
mb_mapping->tab_registers[7] = totaldata[7];
//体积百分比
mb_mapping->tab_registers[8] = totaldata[8];
mb_mapping->tab_registers[9] = totaldata[9];
//modbus_set_slave(mb, device);
socket = modbus_tcp_listen(mb, 1);
modbus_tcp_accept(mb, &socket);
printf("create modbus slave success\n");
//轮询串口数据
while (1)
{
do {
//modbus_receive:接收请求
ret = modbus_receive(mb, query);
} while (ret == 0);
if (ret > 0)
{
printf("len=%02d\t", ret); /*%02d格式限定符,其中0表示变量宽度不足时以0作为填充,2表示显示宽度至少为2,d表示十进制整数*/
for (int i = 0; i < ret; i++)
{
//X 表示以十六进制形式输出,02 表示不足两位,前面补0输出;
printf("%02x", query[i]);
}
printf("\n");
//modbus_reply:返回请求
modbus_reply(mb, query, ret, mb_mapping);
}
else
{
modbus_mapping_free(mb_mapping);
printf("quit the loop: %s\n", modbus_strerror(errno));
break;
}
}
modbus_close(mb);
free(query);
modbus_free(mb);
return 0;
}
modbus poll测试:
主机测试代码:
//----------------主机--------------------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "modbus.h"
int main(int argc, char *argv[]) {
uint16_t table[3];
modbus_t *mt;
mt = modbus_new_tcp("127.0.0.1", 502);
if (modbus_connect(mt) == -1) {
modbus_free(mt);
printf("connect failed: %s\n", modbus_strerror(errno));
return 0;
}
while (1) {
//modbus_read_registers:读取寄存器数据
if (modbus_read_registers(mt, 0X0F, 3, table) == 3)
printf("read success: 0x%04x 0x%04x 0x%04x \n", table[0], table[1], table[2]);/*%04x格式限定符
,其中0表示变量宽度不足时以0作为填充,4表示显示宽度至少为4,x表示十六进制整数*/
else {
printf("read error: %s\n", modbus_strerror(errno));
break;
}
for (int i = 0; i < 3; ++i)
table[i]++;
//modbus_write_registers:写多个寄存器
if (modbus_write_registers(mt, 0X0F, 3, table) == 3)
printf("write success: 0x%04x 0x%04x 0x%04x \n", table[0], table[1], table[2]);
else {
printf("write error: %s\n", modbus_strerror(errno));
break;
}
Sleep(1000);
}
modbus_close(mt);
modbus_free(mt);
system("pause");
return 0;
}
这里推荐一个数据转换工具 tools
经测试,上述示例代码只能单主机连接,多主机连接示例代码如下:
rdsmodbusslave.cpp
#include "RDSModbusSlave.h"
#include <string.h>
#include <mainwindow.h>
#include <QDebug>
#ifdef WIN32
typedef int socklen_t;
#endif
#include <QDebug>
bool RDSModbusSlave::initModbus(std::string Host_Ip = "127.0.0.1", int port = 502, bool debugging = true)
{
ctx = modbus_new_tcp(Host_Ip.c_str(), port);
modbus_set_debug(ctx, debugging);
if (ctx == NULL)
{
fprintf(stderr, "There was an error allocating the modbus\n");
throw - 1;
}
m_modbusSocket = modbus_tcp_listen(ctx, 1);
qDebug() << "listen to 502";
/*设置线圈, 离散输入, 输入寄存器, 保持寄存器个数(数组元素个数))*/
//mapping = modbus_mapping_new(m_numBits, m_numInputBits, m_numInputRegisters, m_numRegisters);
//if (mapping == NULL)
//{
// fprintf(stderr, "Unable to assign mapping:%s\n", modbus_strerror(errno));
// //释放modbus对象
// modbus_free(ctx);
// m_initialized = false;
// return false;
//}
if ((mapping = modbus_mapping_new_start_address(0, 0, 0, 0, 0, 12, 0, 0)) == NULL) {
modbus_free(ctx);
printf("new map failed: %s\n", modbus_strerror(errno));
m_initialized = false;
return false;
}
m_initialized = true;
return true;
}
RDSModbusSlave::RDSModbusSlave(string host, uint16_t port)
{
initModbus(host, port, false);
//TODO:
}
RDSModbusSlave::~RDSModbusSlave()
{
modbus_mapping_free(mapping);
modbus_close(ctx);
modbus_free(ctx);
}
void RDSModbusSlave::loadFromConfigFile()
{
return;
}
void RDSModbusSlave::run()
{
std::thread loop([this]()
{
while (true)
{
if (m_initialized)
{
recieveMessages();
}
else
{
m_initialized = true;
}
}
});
loop.detach();
return;
}
//设置从机号(RTU)
bool RDSModbusSlave::modbus_set_slave_id(int id)
{
int rc = modbus_set_slave(ctx, id);
if (rc == -1)
{
fprintf(stderr, "Invalid slave id\n");
modbus_free(ctx);
return false;
}
return true;
}
bool RDSModbusSlave::setInputRegisterValue(int registerStartaddress, uint16_t Value)
{
if (registerStartaddress > (m_numRegisters - 1))
{
return false;
}
slavemutex.lock();
mapping->tab_input_registers[registerStartaddress] = Value;
slavemutex.unlock();
return true;
}
/***************************************************************
* @brief setRegisterValue(设置保存寄存器的值,类型为uint16_t)
* @param registerStartaddress(保存寄存器的起始地址)
* @param Value(写入到保存寄存器里的值)
**************************************************************/
bool RDSModbusSlave::setHoldingRegisterValue(int registerStartaddress, uint16_t Value)
{
if (registerStartaddress > (m_numRegisters - 1))
{
return false;
}
slavemutex.lock();
mapping->tab_registers[registerStartaddress] = Value;
slavemutex.unlock();
return true;
}
/***************************************************************
* @brief getRegisterValue(获取保存寄存器的值)
* @param registerStartaddress(保存寄存器的起始地址)
**************************************************************/
uint16_t RDSModbusSlave::getHoldingRegisterValue(int registerStartaddress)
{
if (!m_initialized)
{
return -1;
}
return mapping->tab_registers[registerStartaddress];
}
/***************************************************************
* @brief setTab_Input_Bits(设置输入寄存器某一位的值)
* @param NumBit(输入寄存器的起始地址)
* @param Value(输入寄存器的值)
**************************************************************/
bool RDSModbusSlave::setTab_Input_Bits(int NumBit, uint8_t Value)
{
if (NumBit > (m_numInputBits - 1))
{
return false;
}
slavemutex.lock();
mapping->tab_input_bits[NumBit] = Value;
slavemutex.unlock();
return true;
}
/***************************************************************
* @brief getTab_Input_Bits(获取输入寄存器某一位的值)
* @param NumBit(输入寄存器相应的bit位)
**************************************************************/
uint8_t RDSModbusSlave::getTab_Input_Bits(int NumBit)
{
if (!m_initialized)
{
return -1;
}
return mapping->tab_input_bits[NumBit];
}
/***************************************************************
* @brief setRegisterFloatValue(设置浮点值)
* @param (Value:浮点值,registerStartaddress寄存器起始地址)
**************************************************************/
bool RDSModbusSlave::setHoldingRegisterValue(int registerStartaddress, float Value)
{
if (registerStartaddress > (m_numRegisters - 2))
{
return false;
}
/*小端模式*/
slavemutex.lock();
modbus_set_float(Value, &mapping->tab_registers[registerStartaddress]);
slavemutex.unlock();
return true;
}
bool RDSModbusSlave::setInputRegisterValue(int registerStartaddress, float Value)
{
if (registerStartaddress > (m_numRegisters - 2))
{
return false;
}
/*小端模式*/
slavemutex.lock();
modbus_set_float(Value, &mapping->tab_input_registers[registerStartaddress]);
slavemutex.unlock();
return true;
}
/***************************************************************
* @brief 获取寄存器里的浮点数
* @param registerStartaddress寄存器起始地址
* @return 两个uint16_t拼接而成的浮点值
**************************************************************/
float RDSModbusSlave::getHoldingRegisterFloatValue(int registerStartaddress)
{
if (!m_initialized)
{
return -1.0f;
}
return modbus_get_float_badc(&mapping->tab_registers[registerStartaddress]);
}
/***************************************************************
* @brief 支持多个master同时连接
**************************************************************/
void RDSModbusSlave::recieveMessages()
{
uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
int master_socket;
int rc;
fd_set refset;
fd_set rdset;
/* Maximum file descriptor number */
int fdmax;
/* Clear the reference set of socket */
FD_ZERO(&refset);
/* Add the server socket */
FD_SET(m_modbusSocket, &refset);
/* Keep track of the max file descriptor */
fdmax = m_modbusSocket;
while (true)
{
rdset = refset;
if (select(fdmax + 1, &rdset, NULL, NULL, NULL) == -1)
{
perror("Server select() failure.");
break;
}
/* Run through the existing connections looking for data to be
* read */
for (master_socket = 0; master_socket <= fdmax; master_socket++)
{
if (!FD_ISSET(master_socket, &rdset))
{
continue;
}
if (master_socket == m_modbusSocket)
{
/* A client is asking a new connection */
socklen_t addrlen;
struct sockaddr_in clientaddr;
int newfd;
/* Handle new connections */
addrlen = sizeof(clientaddr);
memset(&clientaddr, 0, sizeof(clientaddr));
newfd = accept(m_modbusSocket, (struct sockaddr *)&clientaddr, &addrlen);
qDebug() << newfd;
if (newfd == -1)
{
perror("Server accept() error");
}
else
{
FD_SET(newfd, &refset);
qDebug() << newfd;
if (newfd > fdmax)
{
/* Keep track of the maximum */
fdmax = newfd;
qDebug() << fdmax;
}
qDebug() << "New connection";
printf("New connection from %s:%d on socket %d\n", inet_ntoa(clientaddr.sin_addr), clientaddr.sin_port, newfd);
}
}
else
{
modbus_set_socket(ctx, master_socket);
rc = modbus_receive(ctx, query);
if (rc > 0)
{
if (query[0] == 255 && query[1] == 255)
{
//modbus_reply:返回请求
int id = query[6];
qDebug() << "-------------------";
qDebug() << id;
//modbus_response为自定义响应数据处理方法
modbus_response(ctx, query, rc, mapping, id);
//modbus_reply(mb, query, ret, mb_mapping);
}
//modbus_reply(ctx, query, rc, mapping);
}
else if (rc == -1)
{
/* This example server in ended on connection closing or
* any errors. */
printf("Connection closed on socket %d\n", master_socket);
#ifdef _WIN32
closesocket(master_socket);
#else
close(master_socket);
#endif
/* Remove from reference set */
FD_CLR(master_socket, &refset);
if (master_socket == fdmax)
{
fdmax--;
}
}
}
}
}
m_initialized = false;
}
rdsmodbusslave.h
#ifndef RDSMODBUSSLAVE_H
#define RDSMODBUSSLAVE_H
#include <iostream>
#include <thread>
#include <stdlib.h>
#include <iostream>
#include <mutex>
#include <string>
using namespace std;
/*如果是windows平台则要加载相应的静态库和头文件*/
#ifdef _WIN32
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <windows.h>
#include <modbus.h>
#pragma comment(lib, "Ws2_32.lib")
//#pragma comment(lib, "modbus.lib")
/*linux平台*/
#else
#include <modbus/modbus.h>
#include <unistd.h>
#include <error.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#endif
//#define MAX_POINT 50000
class RDSModbusSlave
{
public:
RDSModbusSlave(string host = "0.0.0.0", uint16_t port = 502);
~RDSModbusSlave();
public:
void recieveMessages();
bool modbus_set_slave_id(int id);
bool initModbus(std::string Host_Ip, int port, bool debugging);
uint8_t getTab_Input_Bits(int NumBit);
bool setTab_Input_Bits(int NumBit, uint8_t Value);
uint16_t getHoldingRegisterValue(int registerNumber);
float getHoldingRegisterFloatValue(int registerStartaddress);
bool setHoldingRegisterValue(int registerNumber, uint16_t Value);
bool setHoldingRegisterValue(int registerNumber, float Value);
bool setInputRegisterValue(int registerNumber, uint16_t Value);
bool setInputRegisterValue(int registerNumber, float Value);
private:
std::mutex slavemutex;
int m_errCount{ 0 };
int m_modbusSocket{ -1 };
bool m_initialized{ false };
// 声明变量
modbus_t* ctx{ nullptr };
modbus_mapping_t* mapping{ nullptr };
/*Mapping*/
int m_numBits{ 60000 };
int m_numInputBits{ 60000 };
int m_numRegisters{ 60000 };
int m_numInputRegisters{ 60000 };
public:
void loadFromConfigFile();
void run();
};
/*annotation:
(1)https://www.jianshu.com/p/0ed380fa39eb
(2)typedef struct _modbus_mapping_t
{
int nb_bits; //线圈
int start_bits;
int nb_input_bits; //离散输入
int start_input_bits;
int nb_input_registers; //输入寄存器
int start_input_registers;
int nb_registers; //保持寄存器
int start_registers;
uint8_t *tab_bits;
uint8_t *tab_input_bits;
uint16_t *tab_input_registers;
uint16_t *tab_registers;
}modbus_mapping_t;*/
#endif // RDSMODBUSSLAVE_H
mainwindow.cpp
#include "modbus.h"
#include "rdsmodbusslave.h"
void modbusRunner(RDSModbusSlave* server)
{
server->recieveMessages();
qDebug() << "----------------------";
}
DWORD ThreadProc(LPVOID lpParameter);
...
HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
RDSModbusSlave modServer("127.0.0.1", 502);
std::thread modServerThread(modbusRunner, &modServer);
modServerThread.join();
}
由于本项目涉及多端口、多传感器通信,会遇到消息堵塞的问题,故将modbus tcp启动程序放在windows线程中。
5、Modbus TCP 通信协议
Modbus TCP数据帧包含报文头、功能代码和数据三部分 。
MBAP报头文:
- Modbus TCP 请求帧
Transaction Identifier: 0x1234 (随意选择的标识符,用于匹配响应)
Protocol Identifier: 0x0000 (Modbus协议标识符)
Length: 0x0006 (后续字节的长度,包括单元标识符、功能码、起始地址和寄存器数量长度)
Unit Identifier: 0x01 (设备或从站标识符)
Function Code: 0x03 (读取多个保持寄存器的功能码)
Starting Address: 0x0000 (起始地址,温度数据存储的地址)
Quantity of Registers: 0x0002 (要读取的寄存器数量,这里为2个寄存器)
将上述信息转换成十六进制表示的请求报文如下:
12 34 00 00 00 06 01 03 00 00 00 02
- Modbus TCP 响应帧
假设服务器正确响应,响应帧包括以下字段:
Transaction Identifier: 0x1234 (与请求中的事务标识符相匹配)
Protocol Identifier: 0x0000 (Modbus协议标识符)
Length: 0x0007 (后续字节的长度)
Unit Identifier: 0x01 (设备或从站标识符)
Function Code: 0x03 (读取多个保持寄存器的功能码)
Byte Count: 0x04 (后续数据字节的数量,因为读取了2个寄存器,所以总共4个字节)
Register Data: xx xx xx xx (实际的寄存器数据)
将上述响应报文转换成十六进制可能如下:
12 34 00 00 00 07 01 03 04 xx xx xx xx
注:无论发送帧为十六进制或十进制数据,响应数据帧都为对应十六进制数据。
示例:
响应数据:13 22,十进制为4898,故为4.4898。