简单描述下Modbus-RTU协议通信特点、过程:
一主机多从机;仅主机可发起请求;同一时间,只有一个数据在传输,即(单播时)主机发送请求,从机响应,主机不发送,没有数据在传输(广播时,主机发送,所有从机不需要发送响应)
下面具体介绍一些内容,介绍具体过程前需要先介绍一下帧
协议数据单元(PDU)包括1字节的功能码域,和不定长度的数据域
应用数据单元(ADU)在此之上引入了1字节地址域和2字节差错检验
地址域1字节。广播地址为0,每一个从机都有一个对应的地址在1-255,主机没有地址,在从机返回的ADU中,地址域返回的是从机的地址
功能码1字节。功能码通知从机需要进行哪种操作
数据域n字节。数据域是一些附加信息,针对功能码进行的操作给一些补充
差错检验2字节。是针对整个帧进行CRC检验(RTU)
在RTU传输模式中,每个字节(11位)的格式为:
1起始位+8数据位,先发送最低有效位+1奇偶校验位+1停止位
如果无奇偶校验,则为2停止位
下面介绍一个具体的通信过程,分别用主机和从机的状态图来表示
当没有等待的请求时,主机处于空闲状态,这是上电后的初始状态,只有处于空闲状态才能发送请求。当一个请求被发送后,主机就会离开空闲状态,也就不能继续发送请求了,也就是说不能同时发送第二个请求
如果此次发送的请求为广播,则主机在发送后进入会启动转换延迟从而进入等待转换延迟状态,当转换延迟时间到达之后(这个时间足够所有的从机完成对请求内容的执行),主机会回到空闲状态
如果发送的请求为单播,主机发送请求到对应从机之后主机就会启动响应超时(这个时间足够从机进行响应并发回响应,如果超时则表示出现错误,具体应用的超时时间不同),同时进入等待应答状态。如果应答超时没有收到响应,就会进入出错处理状态,主机回到空闲状态并发出一个重试请求,重试的最大次数取决于主节点的设置
如果在响应时间前收到了应答,主机会在处理数据前,对应答进行检验,如果收到非期望的从机应答,响应超时继续计时,如果是产生了帧错误(包括对字符的奇偶校验或对整个帧的冗余校验),则发起一个重试
如果主机收到了正确的应答,主机在处理完应答后,会回到空闲状态,等待下一个请求
在从机端,当没有收到请求时,从机处于空闲状态,当从主机收到一个请求后,从机首先对报文进行检验,如果检验没有问题,则进行处理,完成请求动作,发出正常应答
如果检验过程或处理过程出现错误,则需要不响应或发出异常应答,具体情况如下:
(协议里介绍的是所有异常响应情况)
如果从机没有收到请求,就没有返回响应,主机处理超时状态
如果从机收到请求,但检验出一个通信错误(奇偶校验、CRC...),那么不返回响应,主机处理超时状态
如果从机收到没有通信错误的请求,但是并不能处理这个请求,将会返回一个异常响应
下面介绍三种PDU,包括主机请求,从机响应和从机异常响应
主机请求PDU,1字节功能码+n字节数据
从机正常响应,1字节功能码+n字节数据
从机异常响应,1字节(功能码+0x80(也就是最高位置为1))+1字节异常码(告诉主机哪里异常)
这个图简单介绍了事务处理中,异常码的基本情况
那么,到底实现哪些功能呢,modbus建立了这几个数据模型,基本上功能是围绕它们展开的,包括离散量输入,线圈,输入寄存器和保持寄存器
然后介绍一下功能码,简单看下协议吧
举个例子吧,串联一下
比如说,功能码0x03读保持寄存器,协议会列出几个PDU的组成,然后给你举例具体实现
可以看到,请求PDU是由功能码(1字节)+起始地址(2字节)从0x0000到0xFFFF+寄存器数量(2字节)1-125组成,例子中,功能码03,起始地址高和低各占一个字节,先高后低,寄存器编号也是一样,右侧响应给我们的是返回了左侧请求的从006B也就是108开始,108、109、110这三个寄存器的值,同样是高位在前
modbus里面还比较重要的还有1.5字符时间和3.5字符时间:
modbus报文RTU帧,报文帧由至少为3.5字符时间的空闲间隔区分,整个报文帧必须以连续的字符流发送,如果两个字符之间的空闲间隔大于1.5个字符时间,则报文帧被认为不完整应该被接收节点丢弃
CRC校验
看下协议吧,我再附一个代码
协议给出的这个代码并不是按照计算过程来的,给出了解释,因为这种索引的办法比每个新字符都计算新的CRC更快
//CRC 生成函数
/* 高位字节的 CRC 值 */
static unsigned char auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80,0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00,0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01,0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,0x00, 0xC1, 0x81,
0x40 };
/* 低位字节的 CRC 值 */
static char auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB,0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE,0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2,0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E,0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B,0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27,0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD,0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8,0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4,0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94,0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59,0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D,0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,0x41, 0x81, 0x80,
0x40 };
// 函数以unsigned short 类型返回CRC
//puchMsg : 用于计算CRC的报文
//usDataLen : 报文中的字节数
unsigned short CRC16(unsigned char *puchMsg, unsigned short usDataLen)
{
unsigned char uchCRCHi = 0xFF; //CRC的高字节初始化
unsigned char uchCRCLo = 0xFF; //CRC的低字节初始化
unsigned uIndex; //CRC查询表索引
while (usDataLen--) //完成整个报文缓冲区
{
uIndex = uchCRCLo^*puchMsg++; //计算CRC
uchCRCLo = uchCRCHi^auchCRCHi[uIndex];
uchCECHi = auchCRCLo[uIndex];
}
return (uchCRCHi << 8 | uchCRCLo);
}