Modbus协议简介

这两天把Modubs重新看了一下,之前只是简单的使用没有系统总结。关于Modbus协议的讲解,官方文档讲解的非常清楚,不多说。下面记录下学习笔记

1.Modbus数据封包格式

Modbus有​三​种​数据封包格式​是,分别是TCP、​远程​终端​单元​(RTU)​和​ASCII。 RTU​和​ASCII,通常​用于​串​行​线路,​而​TCP​则​用于​现TCP/​IP​或​UDP/​IP​网络。其中RTU和ASCII的起始有些差别,其它基本一样。而TCP在原来的基础上又加了其它字段。下面报文的结构摘录自FreeModbus源码

  • TCP报文格式
/* ----------------------- MBAP Header --------------------------------------*/
/*
 *
 * <------------------------ MODBUS TCP/IP ADU(1) ------------------------->
 *              <----------- MODBUS PDU (1') ---------------->
 *  +-----------+---------------+------------------------------------------+
 *  | TID | PID | Length | UID  |Code | Data                               |
 *  +-----------+---------------+------------------------------------------+
 *  |     |     |        |      |                                           
 * (2)   (3)   (4)      (5)    (6)                                          
 *
 * (2)  ... MB_TCP_TID          = 0 (Transaction Identifier - 2 Byte) 
 * (3)  ... MB_TCP_PID          = 2 (Protocol Identifier - 2 Byte)
 * (4)  ... MB_TCP_LEN          = 4 (Number of bytes - 2 Byte)
 * (5)  ... MB_TCP_UID          = 6 (Unit Identifier - 1 Byte)
 * (6)  ... MB_TCP_FUNC         = 7 (Modbus Function Code)
 *
 * (1)  ... Modbus TCP/IP Application Data Unit
 * (1') ... Modbus Protocol Data Unit
 */
  • RTU和ASCII
    这两种的结构基本一致,asciil是以‘:'开始,回车换行结束而RTU没有。
 * Constants which defines the format of a modbus frame. The example is
 * shown for a Modbus RTU/ASCII frame. Note that the Modbus PDU is not
 * dependent on the underlying transport.
 *
 * <code>
 * <------------------------ MODBUS SERIAL LINE PDU (1) ------------------->
 *              <----------- MODBUS PDU (1') ---------------->
 *  +-----------+---------------+----------------------------+-------------+
 *  | Address   | Function Code | Data                       | CRC/LRC     |
 *  +-----------+---------------+----------------------------+-------------+
 *  |           |               |                                   |
 * (2)        (3/2')           (3')                                (4)
 *
 * (1)  ... MB_SER_PDU_SIZE_MAX = 256
 * (2)  ... MB_SER_PDU_ADDR_OFF = 0
 * (3)  ... MB_SER_PDU_PDU_OFF  = 1
 * (4)  ... MB_SER_PDU_SIZE_CRC = 2
 *
 * (1') ... MB_PDU_SIZE_MAX     = 253
 * (2') ... MB_PDU_FUNC_OFF     = 0
 * (3') ... MB_PDU_DATA_OFF     = 1
 * </code>

2.Modbus数据类型

Modbus是一种简单的软件协议,说它简单,可能大家在日常工作学习中也有这样类似的用法,只是没有形成体系软件而已。Modbus包含4种数据结构,当然在实际使用中,不局限于‘线圈状态’,用户可以根据自己需要和数据类型选择可是的功能码。

内存区块数据类型主设备访问从设备访问
线圈状态布尔读/写读/写
离散输入布尔只读读/写
保持寄存器无符号双字节整型读/写读/写
输入寄存器无符号双字节整型只读读/写

关于这4种数据类型的描述,下面这段引用自百度知道上面的一个回答,觉得更形象,记录下来。

简单点说,modbus有四种数据,DI、DO、AI、AO
DI: 数字输入,离散输入,一个地址一个数据位,用户只能读取它的状态,不能修改。比如面板上的按键、开关状态,电机的故障状态。
DO: 数字输出,线圈输出,一个地址一个数据位,用户可以置位、复位,可以回读状态,比如继电器输出,电机的启停控制信号。
AI: 模拟输入,输入寄存器,一个地址16位数据,用户只能读,不能修改,比如一个电压值的读数。
AO: 模拟输出,保持寄存器,一个地址16位数据,用户可以写,也可以回读,比如一个控制变频器的电流值。
无论这些东西被叫做什么名字,其内容不外乎这几种,输入的信号用户只能看不能改,输出的信号用户控制,并可以回读。离散的数据只有一位,模拟的数据有16位。

3.Modbus RTU请求帧结构

rtu和ascii的请求帧基本一致的,区别就在起始和结束符。

  <--------------------------- MODBUS SERIAL LINE PDU (1) -------------------->
               <----------------- MODBUS PDU (1') ---------------------------->
   +-----------+---------------+-------------+------------------+-------------+
   | Address   | Function Code |   Reg_addr  |     Reg_len      |  CRC/LRC    |
   +-----------+---------------+-------------+------------------+-------+-----+
   |  1byte    |    1(byte)    |     2bytes  |     2bytes       |   2bytes    |
   +-----------+---------------+-------------+------------------+-------------+
  • Address:地址码,8bit的地址码,总共能表示256个从设备,其中地址0为广播地址。
  • Fuction Code:功能码,常用的功能码就那么几个,可以理解成读写数据类型。
  • Reg_addr:寄存器地址,这个一开始我也是有些疑问,其实可以把它理解成命令,哪一个地址对应什么数据,子设备是非常清楚。不过这个要在一开始定义好对应的宏,方便编程时根据地址直接读写对应的数据。
  • Reg_len:定义读写数据的长度。
  • CRC/LRC:校验结果,其中RTU使用CRC校验,而ASCII使用LRC校验。

4.Modbus RTU响应帧结构

下面是一段摘自FreeModbus源码RTU打包响应帧的代码片段,后面在分析代码时,在详细说明下。可以看到响应报文有2种,一种是正常响应帧,一种是异常响应。

        case EV_FRAME_RECEIVED:
            eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );//获取响应帧buffer
            ....
            break;
        case EV_EXECUTE:
            ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF]; //由于接收缓存中已经包含了地址和功能码
            eException = MB_EX_ILLEGAL_FUNCTION;
            for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )
            {
                /* No more function handlers registered. Abort. */
                if( xFuncHandlers[i].ucFunctionCode == 0 )
                {
                    break;
                }//对应功能码服务函数,在处理命令的同时,打包响应包,返回usLength 为当前响应帧长度
                else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
                {
                    eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
                    break;
                }
            }

            /* If the request was not sent to the broadcast address we
             * return a reply. */
            if( ucRcvAddress != MB_ADDRESS_BROADCAST )
            { //如果下面返回的有异常码,则会返回一个错误的响应包
                if( eException != MB_EX_NONE )
                {
                    /* An exception occured. Build an error frame. */
                    usLength = 0;
                    ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
                    ucMBFrame[usLength++] = eException;
                }//发送响应报文给主机
                eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );
            }
            break;

【1】正常响应
返回的数据

  <------------------------- MODBUS SERIAL LINE PDU (1) ---------------------->
               <-------------- MODBUS PDU (1') ------------------------------->
   +-----------+---------------+-------------+------------------+-------------+
   | Address   | Function Code |   dat_len   |     data         |  CRC/LRC    |
   +-----------+---------------+-------------+------------------+-------+-----+
   |  1byte    |    1(byte)    |     1bytes  |     最多251bytes |   2bytes    |
   +-----------+---------------+-------------+------------------+-------------+
  • Address: 从设备地址,由于Modbus是主从的网络,当子设备发送响应帧的时,也只有主设备能处理。
  • Function Code:功能码和请求帧中的功能码一致
  • dat_len:读写的数据长度,这个一般和请求包中的长度一致。
  • data:即为请求的数据,高位在前,低位在后。请求多少就返回多少

【2】异常响应
从上面代码中能够看到异常发生时,状态回滚。且在功能码中或上了MB_FUNC_ERROR这也可以理解成把功能码设置为MB_FUNC_ERROR,应为异常功能码的区间为128 ~ 255 保留 用于异常应答,所以ucFunctionCode | MB_FUNC_ERROR == MB_FUNC_ERROR

ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
  <------------------ MODBUS SERIAL LINE PDU (1) ------------------>
               <------------- MODBUS PDU (1') --------------------->
   +-----------+---------------+---------------------+-------------+
   | Address   | Function Code |   eException_number |  CRC/LRC    |
   +-----------+---------------+---------------------+-------------+
   |  1byte    |    1(byte)    |       1bytes        |     2bytes  |
   +-----------+---------------+---------------------+-------------+
  • Function Code:异常功能码
  • eException_number :异常编码数,用于异常上报,调试等。

5.Modbus RTU报文举例

【1】 读取设备5的开关状态
CRC高位在左,低位在右。

  • 请求帧
从机地址(1Byte)功能号(1Byte)数据地址(2Bytes)数据长度(2Bytes)CRC校验(2Bytes)
0x050200 0000 014E B8
  • 响应帧
从机地址(1Byte)功能号(1Byte)数据长度(1Byte)数据(1Byte)CRC校验
0x0502010178 61

【2】关闭设备5的开关
在写入单个数据后(bool或byte)从设备是要给主设备一个响应帧,已确定从设备已经收到了请求帧。不过参考FreeModbus发现,如果仅仅是写bool或byte的话,响应时会把数据原封不动的返回回来。

  • 请求帧
从机地址功能号数据地址写入数据CRC校验
0x050x0500 0000 018E 0D
  • 响应帧
从机地址功能号数据地址写入数据CRC校验
0x050x0500 0000 018E 0D

【3】向设备05写入多组数据

  • 请求帧格式
从机地址功能号寄存器地址寄存器数量数据字节数写入数据CRC校验
0x050x1000 00040800 01 02 06 07 08 16 19BF 60

1)寄存器数量:这个表示寄存器数量,用来验证写入数据对齐问题。数据字节数 = 寄存器的位宽 寄存器数量 。目前寄存器的宽度为16bit.
2)数据字节数:即将写入的字节数

其实通讯协议在使用时已经约定好了,数据格式,从设备接收到数据后,是知道接收数据的数据结构。不过这个在开发调试之前,一定要定义好相关的数据结构。

  • 响应帧
    响应帧与请求帧只是少了写入的具体数据,且重新生成了CRC校验码。
从机地址功能号寄存器地址寄存器数量数据字节数CRC校验
0x050x1000 0004088B C2

6.Modbus编码方式

从上面了解到,Modbus读写取数据时需要有功能码和寄存器地址。这样的话可以把具体的数据映射到对应的虚拟地址上。

  • 由功能码可以把数据划分到不同的区段上去。
  • 也可以把寄存器的16位地址也进行划分,一般的需要访问的数据是非常少的。

例如在从设备上我们可以把寄存器地址划分成16个区段,每一个区段使用12bit可以最大表示4096种数据,目前是够用了。如下是简单的距离介绍,在开发开始之前一定要定义好命令,再开始编码。

//常用功能码
#define MB_FUNC_NONE                          (  0 )
#define MB_FUNC_READ_COILS                    (  1 )
#define MB_FUNC_READ_DISCRETE_INPUTS          (  2 )
#define MB_FUNC_WRITE_SINGLE_COIL             (  5 )
#define MB_FUNC_WRITE_MULTIPLE_COILS          ( 15 )
#define MB_FUNC_READ_HOLDING_REGISTER         (  3 )
#define MB_FUNC_READ_INPUT_REGISTER           (  4 )
#define MB_FUNC_WRITE_REGISTER                (  6 )
#define MB_FUNC_WRITE_MULTIPLE_REGISTERS      ( 16 )
#define MB_FUNC_READWRITE_MULTIPLE_REGISTERS  ( 23 )
#define MB_FUNC_DIAG_READ_EXCEPTION           (  7 )
#define MB_FUNC_DIAG_DIAGNOSTIC               (  8 )
#define MB_FUNC_DIAG_GET_COM_EVENT_CNT        ( 11 )
#define MB_FUNC_DIAG_GET_COM_EVENT_LOG        ( 12 )
#define MB_FUNC_OTHER_REPORT_SLAVEID          ( 17 )
#define MB_FUNC_ERROR                         ( 128 )

//数据采集命令打包
#define CMD_DATA(func, data_type, offset) (func << 16 | data_type << 12 | (offset&0xFFF))

//加入温度采集设备上有3个传感器,那么可以直接使用下面的方式
#define CMD_GET_TEMP1_DATA CMD_DATA(MB_FUNC_READ_DISCRETE_INPUTS, TEMP_DATA_TYPE, 0)
#define CMD_GET_TEMP2_DATA CMD_DATA(MB_FUNC_READ_DISCRETE_INPUTS, TEMP_DATA_TYPE, 1)
#define CMD_GET_TEMP3_DATA CMD_DATA(MB_FUNC_READ_DISCRETE_INPUTS, TEMP_DATA_TYPE, 2)

typedef enum {
    TEMP_DATA_TYPE,     //温度
    CURRENT_DATA_TYPE,  //电流数据
    VOLTAGE_DATA_TYPE,  //电压数据
    // this is the last data maxcount = 16
    DATAN_TYPE_MAX_COUNT
}edate_type;

//解析命令时,如下面数据结构
typedef struct {
	uint16_t data_type : 4;
	uint16_t adr_offset: 12;
} dev_addr_t;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值