文章目录
1.modbus概况
搜索有大把的概括,感兴趣就搜,我就精简的写。
- 是一种通讯协议,分客户端/主机(client/master)和服务端/从机(server/slave)。
- 客户端/主机 向 服务器/从机 发送报文读写数据。
- 服务器/从机 只能在客户端/主机查询时返回数据,不能主动向客户端/主机发送数据(当然可以改协议)。
2.各语言的推荐 Modbus 库
- C/C++: libmodbus,Qt Modbus
- Python : pyModbusTCP
- Java: jamod
- C#: NModbus4
- Javascript: modbus-serial
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中。源代码看一下就好,结构很清晰,想处理报文就改这部分。