这两天把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) |
---|---|---|---|---|
0x05 | 02 | 00 00 | 00 01 | 4E B8 |
- 响应帧
从机地址(1Byte) | 功能号(1Byte) | 数据长度(1Byte) | 数据(1Byte) | CRC校验 |
---|---|---|---|---|
0x05 | 02 | 01 | 01 | 78 61 |
【2】关闭设备5的开关
在写入单个数据后(bool或byte)从设备是要给主设备一个响应帧,已确定从设备已经收到了请求帧。不过参考FreeModbus发现,如果仅仅是写bool或byte的话,响应时会把数据原封不动的返回回来。
- 请求帧
从机地址 | 功能号 | 数据地址 | 写入数据 | CRC校验 |
---|---|---|---|---|
0x05 | 0x05 | 00 00 | 00 01 | 8E 0D |
- 响应帧
从机地址 | 功能号 | 数据地址 | 写入数据 | CRC校验 |
---|---|---|---|---|
0x05 | 0x05 | 00 00 | 00 01 | 8E 0D |
【3】向设备05写入多组数据
- 请求帧格式
从机地址 | 功能号 | 寄存器地址 | 寄存器数量 | 数据字节数 | 写入数据 | CRC校验 |
---|---|---|---|---|---|---|
0x05 | 0x10 | 00 00 | 04 | 08 | 00 01 02 06 07 08 16 19 | BF 60 |
1)寄存器数量:这个表示寄存器数量,用来验证写入数据对齐问题。数据字节数 = 寄存器的位宽 寄存器数量 。目前寄存器的宽度为16bit.
2)数据字节数:即将写入的字节数
其实通讯协议在使用时已经约定好了,数据格式,从设备接收到数据后,是知道接收数据的数据结构。不过这个在开发调试之前,一定要定义好相关的数据结构。
- 响应帧
响应帧与请求帧只是少了写入的具体数据,且重新生成了CRC校验码。
从机地址 | 功能号 | 寄存器地址 | 寄存器数量 | 数据字节数 | CRC校验 |
---|---|---|---|---|---|
0x05 | 0x10 | 00 00 | 04 | 08 | 8B 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;