文章目录

  • 1.modbus概况
  • 2.各语言的推荐 Modbus 库
  • 3. 我对modbus程序的理解
  • 4.tcp/ip 报文解析
  • 4.1 功能码(libmodbus 程序中的定义)
  • 4.2 client向server发送 的request报文
  • 4.2.1 client发送报文,各个功能码对应的数据部分
  • 4.2.2 报文举例
  • 4.3 server的response 报文
  • 5.写个代码(libmodbus) tcp/ip client


1.modbus概况

搜索有大把的概括,感兴趣就搜,我就精简的写。

  • 是一种通讯协议,分客户端/主机(client/master)和服务端/从机(server/slave)。
  • 客户端/主机 向 服务器/从机 发送报文读写数据。
  • 服务器/从机 只能在客户端/主机查询时返回数据,不能主动向客户端/主机发送数据(当然可以改协议)。

2.各语言的推荐 Modbus 库

3. 我对modbus程序的理解

废话不多说,直接看程序。

server设备一直维护四个table:

  • Coils: 保存二进制数据,可读可写
  • Discrete inputs: 保存二进制数据,只可读
  • input registers: 保存16进制数据,只可读
  • holding registers: 保存16进制数据,可读可写

client设备发送报文读/写server数据,报文中需要包含的内容:

  • server地址(因为一个client可以连接多个server,需要指定哪个server)
  • function功能码(对哪个寄存器表,进行读或写)
  • 寄存器具体地址
  • 和function功能码对应的具体数据

4.tcp/ip 报文解析

其实如果不打算做处理报文的程序,单纯只维护寄存器表时,这部分不用看,因为各个开源程序已经封装好处理报文的程序,调用函数就行。

4.1 功能码(libmodbus 程序中的定义)
  • 0x01 :MODBUS_FC_READ_COILS ,读cols
  • 0x02 :MODBUS_FC_READ_DISCRETE_INPUTS,读discrete inputs
  • 0x03 :MODBUS_FC_READ_HOLDING_REGISTERS,读holding register
  • 0x04 :MODBUS_FC_READ_INPUT_REGISTERS, 读input register
  • 0x05 :MODBUS_FC_WRITE_SINGLE_COIL, 写一个 coil
  • 0x06 :MODBUS_FC_WRITE_SINGLE_REGISTER, 写一个register
  • 0x07 :MODBUS_FC_READ_EXCEPTION_STATUS, 读exception status
  • 0x0F :MODBUS_FC_WRITE_MULTIPLE_COILS ,写多个coils
  • 0x10 :MODBUS_FC_WRITE_MULTIPLE_REGISTERS ,写多个register
  • 0x11 :MODBUS_FC_REPORT_SLAVE_ID, 上报地址
  • 0x16 :MODBUS_FC_MASK_WRITE_REGISTER ,对一个数据进行处理
  • 0x17 :MODBUS_FC_WRITE_AND_READ_REGISTERS ,读写registers
4.2 client向server发送 的request报文

报文构成:

帧头

tcp/ip协议表示

该字节以后的长度

设备地址

功能码

数据

2字节

0x00 00

2字节

1字节

1字

根据功能码确定

4.2.1 client发送报文,各个功能码对应的数据部分
  • 读操作: 0x01, 0x02, 0x03, 0x04

2字节

2字节

寄存器起始地址

读寄存器的数目

  • 0x05: 写一个coil

2字节

2字节

寄存器起始地址

数据(0x0000 ,代表0; 0xff00,代表1)

  • 0x06,写一个register

2字节

2字节

寄存器起始地址

数据

  • 0x0f,写多个coils

2字节

2字节

一个字节

写数据个数/8 个字节

寄存器起始地址

写数据个数

写数据个数/8

具体算法看代码即可

例如写9个数据,第三部分数值为0x02

  • 0x10 :写多个register

2字节

2字节

一个字节

2*n 个字节

寄存器起始地址

写数据个数

后面的字节数目

写每个数据两个字节

  • 0x11 : 上报地址

2字节

寄存器起始地址

  • 0x07, 读exception status

2字节

寄存器起始地址

  • 0x16 : 处理数据

2字节

2个字节

2个字节

寄存器起始地址

and

or

最终数据为

data = (data & and) | (or & (~and));
  • 0x17 : 读写

2字节

2个字节

2个字节

2字节

1字节

2*(写寄存器数目n) 字节

读寄存器起始地址

读寄存器数目

写寄存器起始地址

写寄存器数目

之后的字节数

写寄存器数值

4.2.2 报文举例

举例:读holding register

03 6D

00 00

00 06

01

03

00 00

00 0A

主机发送的检验信息(client发送的request的次序)

表示tcp/ip的协议是modbus协议

表示该字节以后的长度

设备地址

功能码

寄存器起始地址

读寄存器数目

4.3 server的response 报文

报文构成

帧头

tcp/ip协议表示

长度

设备地址

功能码

数据长度

数据

2字节

0x00 00

2字节

1字节

1字节

1字节

由具体功能决定

举例:返回holding register数据

03 6D

00 00

00 17

01

03

14

00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

检验信息

tcp/ip为modbus协议

表示该字节后的长度

设备地址

功能码

该字节后的字节数

具体返回的数据部分看代码,懒得写了

5.写个代码(libmodbus) tcp/ip client

了解完就开干。 github搞到源代码,其中还有test例程可以参考。

#include "modbus/modbus.h"
class myclass:{
public:
    myclass();
	virtual ~myclass();
	void listenDevice();
private:
    std::string ip_;
	int port_;
	int server_address_;
	modbus_t* ctx_;
	modbus_mapping_t* mb_mapping_;
	std::shared_ptr<std::thread> revThread_;
	bool timer_run_;
}
myclass::myclass()
{
	ip_ = "127.0.0.1"; //ip
	port_ = 502;  //端口号
	server_address_ = 1;  //地址
	timer_run_ = true;
	ctx_ = modbus_new_tcp(ip_.c_str(),port_);  //初始化
	mb_mapping_ = NULL;   //数据字典
	if(ctx_ == NULL){
		printf("Unable to allocate libmodbus context :%s",modbus_strerror(errno));
	}
	printf("finished_init");
	revThread_ =  std::make_shared<std::thread>(std::bind(&myclass::listenDevice,this));
}

myclass::~myclass() {
	timer_run_ = false;
	if(ctx_ != NULL){
		modbus_free(ctx_);
	}
	if(mb_mapping_ !=NULL){
		modbus_mapping_free(mb_mapping_);
	}
}

void myclass::listenDevice(){
	while(1){
		printf("begin listen device");
		if(!timer_run_){
			return;
		}
		if(ctx_ == NULL){
			printf("Unable to allocate libmodbus context");
			return;
		}
		modbus_set_debug(ctx_,true);
		
		//server需要listen
		int server_socket = modbus_tcp_listen(ctx_,10);
		if(server_socket == -1){
			printf("Unable to listen TCP: %s",modbus_strerror(errno));
			continue;
		}

		modbus_tcp_accept(ctx_, &server_socket);
		//初始数据字典
		mb_mapping_ = modbus_mapping_new(MODBUS_MAX_READ_BITS,
										MODBUS_MAX_WRITE_BITS,
										MODBUS_MAX_READ_REGISTERS,
										MODBUS_MAX_WRITE_REGISTERS);
		if (mb_mapping_ == NULL) {
			printf("Failed to allocate the mapping: %s",modbus_strerror(errno));
			continue;
		}
		//设置字典的数据
	    for (int i=0;i<20;i++)
	    {
	    	mb_mapping_->tab_input_registers[i] = i;
	    	mb_mapping_->tab_registers[i] = i;
	    }

	    while(1){
	    	uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
	    	//接受client报文
	    	int rc = modbus_receive(ctx_, query);
	    	if (rc > 0) {
	    	    //处理client报文
	    		modbus_reply(ctx_, query, rc, mb_mapping_);
	    	} else{
	    		printf("error %s\n", modbus_strerror(errno));
	    		break;
	    	}
			if(!timer_run_){
				return;
			}
	    }
		close(server_socket);
	}
}

来自client的报文解析在modbus_reply中。源代码看一下就好,结构很清晰,想处理报文就改这部分。