通信协议—Modbus

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。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值