Modbus协议

本来需要用到Modbus,就了解了一下,不过后来用不到了,只作了一些调研,简单记录下。所有内容未实际测试

0.概述

Modbus是一种工业协议,于1979年开发,旨在实现自动化设备之间的通信。最初是作为串行层传输数据的应用层协议实现的,现已扩展到包括串行、TCP和UDP的实现。

 

Modbus使用主从关系实现的请求-响应协议。在​主从关系​中,​通信​总是​成​对​发生 - 一个​设备​必须​发起​请求,​然后​等待​响应 - 并且​发起​设备​(主​设备)​负责​发起​每次​交互。通常,​主​设备​是​人​机​界面​(HMI)​或​监​控​和​数据​采集​(SCADA)​系统,​从​设备​是​传感器、​可​编​程​逻辑​控制器​(PLC)​或可​编​程​自动​化​控制器​(PAC)。这些​请求​和​响应​的​内容​以及​发送​这些​消息​的​网络​层​由​协议​的​不同​层​来​定义。

主从网络关系

英文完整版协议

NI中文版协议介绍

1.名词解释

协议数据单元(PDU)

通俗的理解,协议数据单元就是包含Modbus协议所规定的从机地址,功能码,数据,校验等各个数据部分的数据单元。Modbus应用协议规范主要就是在定义这几个部分数据的含义。

Modbus数据模型

Modbus数据模型包含线圈状态,离散输入,输入寄存器,保持寄存器四种可访问的数据类型。(Modbus协议最开始是用来解决PLC的通信问题,这些名词来源于PLC专业属于,因此对于部分行业的人来说难以理解)。可以理解为这个主从设备构成的系统的公用寄存器,这些寄存器数据类型不同,其处于不同的地址范围,主从设备都可以访问这些寄存器,但主从设备有着不同的访问权限。

内存区块名称

功能码*

数据类型

主设备访问权限

从设备访问权限

地址范围

线圈状态

01H,05H,0FH

读/写

读/写

0x00000-0x0ffff

离散输入

02H

读/写

0x10000-0x1ffff

保持寄存器

03H,06H,10H

无符号双字节整型

读/写

读/写

0x40000-0x4ffff

输入寄存器

04H

无符号双字节整型

读/写

0x30000-0x3ffff

 

功能码

主设备用功能码表示希望从设备执行什么操作。比如,01H功能码表示主设备想读取单个或多个线圈状态。后面会详细描述。

功能码分为三种:

公共功能码(Public Function Codes):在公开文档种有明确定义,并保证唯一的功能码。

用户定义功能码(User-Defined Function Codes):用户可以选择实现未被标准支持的功能码,以支持需要的功能。

保留功能码(Reserved Function Codes):被部分公司使用,作为遗留项目而未公开使用的功能码。

以下是官方文档中对功能码的分类:

2.功能码

reference

功能码概述

功能码是主设备告诉从设备其想执行什么操作。常用的有以下几种:

 

功能码

描述

数据类型

操作数量

寄存器地址

01H

读线圈状态

单个或多个

 

02H

读离散输入

单个或多个

 

03H

读保持寄存器

无符号双字节整型

单个或多个

 

04H

读输入寄存器

无符号双字节整型

单个或多个

 

05H

写单个线圈状态

单个

 

06H

写单个保持寄存器

无符号双字节整型

单个

 

0FH

写多个线圈状态

多个

 

10H

写多个保持寄存器

无符号双字节整型

多个

 

功能码使用示例

1.读多个线圈状态

功能码01H读取Modbus从机中线圈寄存器的状态,可以是单个寄存器,或者多个连续的寄存器。

第一步,主机发送命令

假设从机地址为01H,读取的线圈寄存器的起始地址为0017H,读取38个寄存器,指令如下表所示:

从机地址

功能码

起始地址高位

起始地址低位

寄存器数量高位

寄存器数量低位

CRC高位

CRC低位

01

01

00

17

00

26

0D

D4

第二步,从机发送响应

各线圈的状态与数据内容的每个bit对应,1代表ON,0代表OFF。如果查询的线圈数量不是8的倍数,则在最后一个字节的高位补0。

从机地址

功能码

返回字节数

数据1

数据2

数据3

数据4

数据5

CRC高位

CRC低位

01

01

05

CD

6B

B2

0E

1B

44

EA

响应数据与寄存器对应关系:

其中,第一个字节CDH对应线圈0017H到001E的状态,转为二进制是11001101,其中bit0对应0017H,bit7对应001E,具体对应关系如下:

线圈0017H到001EH的状态

001EH

001DH

001CH

001BH

001AH

0019H

0018H

0017H

1

1

0

0

1

1

0

1

ON

ON

OFF

OFF

ON

ON

OFF

ON

最后一个字节为1BH,对应线圈0037H到003CH的状态,转为二进制是00011011,其中bit0对应0037H,bit5对应003CH,其余两位用0填充,如下表所示:

线圈0037H到003CH的状态

003CH

003BH

003AH

0039H

0038H

0037H

0036H

0035H

0

0

0

1

1

0

1

1

填充

填充

OFF

ON

ON

OFF

ON

ON

2.写入单个线圈寄存器

功能码05H写单个线圈寄存器,FF00H请求线圈处于ON状态,0000H请求线圈处于OFF状态。

第一步,主机发送命令

假设从机地址为01H,线圈寄存器的地址为00ACH,使其处于ON状态的指令如下表所示:

从机地址

功能码

寄存器地址高位

寄存器地址低位

数据高位

数据低位

CRC高位

CRC低位

01

05

00

AC

FF

00

4C

1B

第二步,从机返回发送的指令

如果写入成功,返回发送的指令,即010500ACFF004C1B。

3.libmodbus源码解析

libmodbus官网

libmodbus github

这里对libmodbus源码做简要解析,详细解析可参考猪哥博客

libmodbus使用:https://zhuge.blog.csdn.net/article/details/89185837

libmodbus源码解析: https://zhuge.blog.csdn.net/article/details/104088091

数据结构定义

功能码定义

// modbus.h
/* Modbus function codes */
#define MODBUS_FC_READ_COILS                0x01
#define MODBUS_FC_READ_DISCRETE_INPUTS      0x02
#define MODBUS_FC_READ_HOLDING_REGISTERS    0x03
#define MODBUS_FC_READ_INPUT_REGISTERS      0x04
#define MODBUS_FC_WRITE_SINGLE_COIL         0x05
#define MODBUS_FC_WRITE_SINGLE_REGISTER     0x06
#define MODBUS_FC_READ_EXCEPTION_STATUS     0x07
#define MODBUS_FC_WRITE_MULTIPLE_COILS      0x0F
#define MODBUS_FC_WRITE_MULTIPLE_REGISTERS  0x10
#define MODBUS_FC_REPORT_SLAVE_ID           0x11
#define MODBUS_FC_MASK_WRITE_REGISTER       0x16
#define MODBUS_FC_WRITE_AND_READ_REGISTERS  0x17

modbus设备上下文modbus_t,里面包含了从机地址,socket(TCP/UDP)或文件描述符(串口),超时时间,以及消息处理方法等成员。

// modbus.h
typedef struct _modbus modbus_t;

//modbus-private.h
struct _modbus {
   /* Slave address */
   int slave;
   /* Socket or file descriptor */
   int s;
   int debug;
   int error_recovery;  // 用户是否允许自动重连
   struct timeval response_timeout;
   struct timeval byte_timeout;
   struct timeval indication_timeout;
   const modbus_backend_t *backend;
   void *backend_data;
};

上下文种比较重要的一个定义是后端 modbus_backend_t,该数据结构位struct,里面存储了对于modbus数据的处理函数,用于屏蔽不同的底层协议(RTU、TCP、UDP等)

//modbus-private.h
typedef struct _modbus_backend {
   unsigned int backend_type;
   unsigned int header_length;
   unsigned int checksum_length;
   unsigned int max_adu_length;
   int (*set_slave) (modbus_t *ctx, int slave);
   int (*build_request_basis) (modbus_t *ctx, int function, int addr,
                               int nb, uint8_t *req);
   int (*build_response_basis) (sft_t *sft, uint8_t *rsp);
   int (*prepare_response_tid) (const uint8_t *req, int *req_length);
   int (*send_msg_pre) (uint8_t *req, int req_length);
   ssize_t (*send) (modbus_t *ctx, const uint8_t *req, int req_length);
   int (*receive) (modbus_t *ctx, uint8_t *req);
   ssize_t (*recv) (modbus_t *ctx, uint8_t *rsp, int rsp_length);
   int (*check_integrity) (modbus_t *ctx, uint8_t *msg,
                           const int msg_length);
   int (*pre_check_confirmation) (modbus_t *ctx, const uint8_t *req,
                                  const uint8_t *rsp, int rsp_length);
   int (*connect) (modbus_t *ctx);
   void (*close) (modbus_t *ctx);
   int (*flush) (modbus_t *ctx);
   int (*select) (modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length);
   void (*free) (modbus_t *ctx);
} modbus_backend_t;

在初始化具体的modbus_t上下文时,modbus_backend_t将会被赋值为对应不同底层协议的结构体。

两个关键步骤解析

以RTU为例,比较重要的两个步骤,解析:

1.初始化modbus上下文

modbus_t *ctx = NULL;                // modbus_t 指针定义
ctx = modbus_new_rtu("/dev/ttySP0", 9600, 'N', 8, 1);  // 初始化上下文,以RTU为传输协议

// modbus_new_rtu函数中,进行了以下操作:为backend结构体赋值,_modbus_rtu_backend是在相应的协议文件(modbus-rtu.c)中定义好的,从而屏蔽具体协议,可以看出,_modbus将backend_data定义为void*类型,也是因为不同的协议有不同的特性参数,因此需要各自定义数据结构,分别初始化

>   ctx->backend = &_modbus_rtu_backend;  
>   ctx->backend_data = (modbus_rtu_t *)malloc(sizeof(modbus_rtu_t));
>   ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
>   ctx_rtu->device = xxx
>   ctx_rtu->baud = xxx
....

2.操作寄存器,以读保持寄存器为例

uint16_t tab_reg[64] = {0}; // 定义存放数据的数组
modbus_read_registers(ctx, 0, 10, tab_reg); // 从地址0开始读取10个寄存器,存入tab_reg
// 这里,不同功能码被分为了几类操作,读寄存器之类的操作被封装到read_registers函数中
>   read_registers(ctx, MODBUS_FC_READ_HOLDING_REGISTERS, addr, nb, dest); // 这个函数实现了各种功能码定义的操作,只需要把相应功能码传给它,就可以实现不同的功能
>>    req_length = ctx->backend->build_request_basis(ctx, function, addr, nb, req); // 打包请求消息体
>>    rc = send_msg(ctx, req, req_length); // 发送请求消息体
>>>     msg_length = ctx->backend->send_msg_pre(msg, msg_length); // 发送前填充校验
>>>     rc = ctx->backend->send(ctx, msg, msg_length); // 发送
>>    rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION); // 这里按照不同的消息格式,读不同长度的response。如果receive出现故障,在错误重连被允许的情况下,将自动重连。
>>>     ctx->backend->check_integrity(ctx, msg, msg_length); // CRC检查
 
>>    rc = check_confirmation(ctx, req, rsp, rc); // 里面区分各种功能码,检查不同的返回值是否符合要求,并进行错误检查
>>    // 填充数据到tab_reg, read_registers中的dest

完整的RTU master读寄存器程序示例

// modbus 从机示例代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <modbus.h>
 
int main(int argc, char *argv[])
{
  uint16_t tab_reg[64] = {0}; // 定义存放数据的数组
  modbus_t *ctx = NULL;       // modbus_t 指针定义

  int rc;
  int i;
  // 以串口的方式创建libmobus实例,并设置参数
  // 使用UART1,对应的设备描述符为ttySP0
  ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); //相 当于初始化 modbus_t
  if (ctx == NULL) {
    fprintf(stderr, "Unable to allocate libmodbus contex\n");
    return -1;
  }
 
  modbus_set_debug(ctx, 1); // 设置1可看到调试信息
  modbus_set_slave(ctx, 1); // 设置slave ID
 
  if (modbus_connect(ctx) == -1) { // 等待连接设备
    fprintf(stderr, "Connection failed:%s\n", modbus_strerror(errno));
    return -1;
  }
 
  while (1) {
    printf("\n----------------\n");
    rc = modbus_read_registers(ctx, 0, 10, tab_reg);
    if (rc == -1) { // 读取保持寄存器的值,可读取多个连续输入保持寄存器
      fprintf(stderr, "%s\n", modbus_strerror(errno));
      return -1;
    }
    for (i = 0; i < 10; i++) {
      printf("reg[%d] = %d(0x%x)\n", i, tab_reg[i], tab_reg[i]);
    }
    sleep(1);
  }
  modbus_close(ctx); // 关闭modbus连接
  modbus_free(ctx);  // 释放modbus资源,使用完libmodbus需要释放掉
 
  return 0;
 }

 

slave端在linux端应用应该较少,更多使用小嵌入式设备实现传感器数据的发送,据说freemodbus工程更适合slave端应用,有机会再读相关源码吧。

4.异常处理

协议

从​设备​使用​异常​来​指示​各种​不良​状况,​比如​错误​请求​或​不​正确​输入。 但是,​异常​也可以​作为​对​无效​请求​的​应用​程序​级​响应。 从​设备​不​响应​发出​异常​的​请求。 相反,​从​设备​忽略​不​完整​或​损坏​的​请求,​并​开始​等待​新的​消息​传​入。

异常​以​定义​好的​数据​包​格式​报告​给​用户。 首先​将​一个​功能​代码​返回​给​等​同​于​与​原始​功能​代码​的​请求​主​设备,​除了​设置​了​最高​有效​位。 这​等​同​于​为​原始​功能​代码​的​值​加上​0x80。 异常​响应​包括​一个​异常​代码​来​代替​与​给​定​函数​响应​相关​的​正常​数据。

在​标准​内,​四​种​最​常见​的​异常​代码​是​01,02,03​和​04。​表下​​介绍​了​这些​代码​以及​每​种​功能​的​标准​含义。

异常​代码

含义

01

不​支持​接收​到​功能​代码。 要​确认​原始​功能​代码,​请​从​返回​值​中​减去​0x80。

02

尝试​访问​的​请求​是​一个​无效​地址。 在​标准​中,​只有​起始​地址​和​请求​的​数值​超过216时​才​会​发生​这种​情况。 但是,​有些​设备​可能​会​限制​其​数据​模型​中的​地址​空间。

03

请求​包含​不​正确​的​数据。 在​某些​情况​下,​这​意味​着​参数​不​匹配,​例如​发送​的​寄存器​的​数量​与“字​节​数”字​段​之间​的​参数​不​匹配。 更​常见​的​情况​是,​主机​请求​的​数据​比​从​机​或​协议​允许​的​要​多。 例如,​主​设备​一次​只能​读​取​125​个​保持​寄存器,​而​资源​受限​的​设备​可能​会​将​此​值​限制​为​更少​的​寄存器。 例如,​主​设备​一次​只能​读​取​125​个​保持​寄存器,​而​资源​受限​的​设备​可能​会​将​此​值​限制​为​更少​的​寄存器。

04

尝试​处理​请求​时​发生​不可​恢复​的​错误。 这​是​一个​异常​的​代码,​表示​请求​有效,​但从​设备​无法​执行​该​请求。

libmodbus

// modbus.h定义了多种异常类型
/* Protocol exceptions */
enum {
   MODBUS_EXCEPTION_ILLEGAL_FUNCTION = 0x01,// 前四个错误码对应上表中的四个异常代码
   MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS,
   MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,
   MODBUS_EXCEPTION_SLAVE_OR_SERVER_FAILURE,
   MODBUS_EXCEPTION_ACKNOWLEDGE,
   MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY,
   MODBUS_EXCEPTION_NEGATIVE_ACKNOWLEDGE,
   MODBUS_EXCEPTION_MEMORY_PARITY,
   MODBUS_EXCEPTION_NOT_DEFINED,
   MODBUS_EXCEPTION_GATEWAY_PATH,
   MODBUS_EXCEPTION_GATEWAY_TARGET,
   MODBUS_EXCEPTION_MAX
};

这些错误类型由从机发送,发送时,从机首先仍发送从机地址和功能码(不是真正的功能码,是功能码+0x80,以和功能码区分开来)字节,接下来直接发送错误码。

那么主机如何检查到错误呢?还记得上面的check_confirmation函数吗,该函数将进行错误检查,即检查发送功能码和接收功能码是否一致,,如果不一致,则报错,并给errno赋值为相应的错误码。

因此用户需要在modbus_read_registers之后执行modbus_strerror(errno),检查是否出现错误。

// modbus.c
const char *modbus_strerror(int errnum) {
   switch (errnum) {
   case EMBXILFUN:
       return "Illegal function";
   case EMBXILADD:
       return "Illegal data address";
   case EMBXILVAL:
       return "Illegal data value";
   ....
   default:
       return strerror(errnum); // modbus_strerror不光能检查自定义错误,还能检查标准错误,原因是其直接使用对errno赋值的方式实现的
   }
}
 
modbus_strerror(errno);


 

  • 3
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值