Modbus 协议二之 ModBus在串行链路下的实现

作者:@登云上人间

目录

一、简述
二、Modbus 数据链路层
2.1 通信原理
2.2 Modbus 地址规则
2.3 Modbus 帧描述
2.4 主站/从站状态
2.4.1 主站状态图
2.4.2 从站状态图
2.4.3 主站/从站通信时序图
2.5 两种串行传输模式
2.5.1 RTU 传输模式
三、物理层
3.3 电气接口
3.3.1 多点串行总线结构
3.3.2 2线-MODBUS
3.3.3 可选的4 线-MODBUS 定义
3.3.3.2 4 线与2 线电缆的兼容性

一、简述

  • Modbus在一个主站和一个或多个从站之间交换Modbus请求,位于OSI 模型的第二层.

  • 在物理层,Modbus 串行链路系统可以使用不同的物理接口(RS485、RS232)

image
image

二、Modbus 数据链路层

2.1 通信原理

  • 同一时刻,只有一个主节点连接于总线一个或多个子节点(最大编号为247 ) 连接于同一个串行总线通信总是由主节点发起子节点在没有收到来自主节点的请求时,从不会发送数据。 子节点之间从不会互相通信主节点在同一时刻只会发起一个Modbus 事务处理。

主节点以两种模式对子节点发出Modbus 请求:

  • 单播模式:主节点以特定地址访问某个子节点,子节点接到并处理完请求后,子节点向主节点返回一个报文(一个'应答')。一个Modbus 事务处理包含2 个报文主节点的请求子节点的应答。每个子节点必须有唯一的地址(1 到247)

image

  • 广播模式:主节点向所有的子节点发送请求。广播请求一般用于写命令子节点没有应答返回所有设备必须接受广播模式的写功能。地址0 专门用于表示广播数据。

image

2.2 Modbus 地址规则

image

  • 主节点没有地址
  • 子节点必须有一个地址,且在总线上唯一
  • 所有的子节点必须识别广播地址。

2.3 Modbus 帧描述

协议数据单元(PDU - Protocol DataUnit):
image

  • 不同总线或网络的 Modbus 协议 在协议数据单元(PDU之外)之外映射了一些附加域,发起Modbus 事务处理的客户端(主机)构造Modbus PDU添加附加的域以构造适当的通信PDU

  • 地址域只含有子节点地址。主节点通过地址域对子节点寻址子节点在应答报文中的地址域放入自己的地址告诉主节点哪个子节点应答

  • 功能码指明服务器要执行的动作

  • 错误检验域是对报文内容执行"冗余校验" 的计算结果

2.4 主站/从站状态

Modbus 由两个不同的子层组成:

  • 主/ 从协议

  • 传输模式( RTU 和ASCII 模式)

状态图词法:。标记法要点如下:
image
当一个系统处于"状态_A"发生"触发"事件,只有当"临界条件" 为真系统转换到"状态_B",之后,一个"动作"被执行。

2.4.1 主站状态图

image

  • 单播请求发送到一个子节点,主节点将进入"等待应答" 状态,"响应超时"启动
  • 收到一个应答,主节点在处理数据之前检验应答。收到来自非期望子节点的应答时,“响应超时”继续计时;检测到帧错时,执行一个“重试”。“响应超时”但没有收到应答时,产生一个错误。主节点进入”空闲” 状态, 并发出一个重试请求。

  • 广播请求发送到串行总线,没有响应从子节点返回。主节点需要进行"转换延迟"(使子节点在发送新的请求处理完当前请求)

2.4.2 从站状态图

image

  • 收到一个请求时,子节点在处理请求中要求的动作前 检验报文包。当检测到错误(请求的格式错,非法动作等),必须向主节点发送应答此情况是该帧数据确实是发给我的,但帧结构中的数据有错误,该情况需要返回给主站)

  • 要求的动作完成后,单播报文要求必须格式化一个应答并发往主节点。

  • 子节点在接收到的帧中检测到错误没有响应返回到主节点。(此情况是该帧不是发给我的,或者发给我的帧结构错误,该情况不需要返回给主站)

帧错误包括: 1) 对每个字符的奇偶校验; 2) 对整个帧的冗余校验。

2.4.3 主站/从站通信时序图

多看一下上方的状态图,就大致了解了主从站通信。
image

2.5 两种串行传输模式

两种串行传输模式:

  • RTU 模式

  • ASCII 模式

作用:定义了报文域的位内容在线路上串行的传送,确定了信息如何打包成报文和解码。

  • Modbus 串行链路上所有设备的传输模式(和串行口参数) 必须相同。设备由用户设成期望的模式:RTU 或ASCII默认设置必须为RTU 模式

2.5.1 RTU 传输模式

  • 报文中每个8 位字节含有两个4 位十六进制字符。每个报文必须以连续的字符流传送

  • RTU 模式每个字节( 11 位) 的格式为:1 起始位,8 数据位, 首先发送最低有效位,1 位作为奇偶校验,停止位。

  • 默认校验模式模式必须为偶校验, 其它模式( 奇校验, 无校验) 也可以使用。使用无校验要求2 个停止位

  • 字符字节发送顺序:
    image
    image

  • 帧检验域: 循环冗余校验(CRC)

  • 帧描述:
    image

2.5.1.1 Modbus 报文RTU 帧

  • 发送设备将Modbus 报文构造带有已知起始和结束标记的帧

  • RTU 模式,报文帧由时长至少为3.5 个字符时间的空闲间隔区分
    image
    image

  • 整个报文帧必须以连续的字符流发送。两个字符之间的空闲间隔大于1.5 个字符时间,则报文帧被认为不完整应该被接收节点丢弃
    image

  • 在通信速率等于或低于19200 Bps 时,这两个定时必须严格遵守;波特率大于19200 Bps 的情形,应该使用2 个定时的固定值:建议的字符间超时时间(t1.5)为750μs帧间的超时时间(t1.5) 为1.750ms

  • RTU 传输模式下状态图描述。"主节点" 和"子节点" 的不同角度均在相同的图中表示:
    image

  • 链路空闲时, 在链路上检测到的任何传输的字符被识别为帧起始,链路上没有字符传输的时间间个达到t3.5 后,被识别为帧结束。检测到帧结束后,完成CRC 计算和检验

2.5.1.2 CRC 校验

不管报文有无奇偶校验,均执行此检验。

  • CRC 包含由两个8 位字节组成的一个16 位值。CRC 域附加在报文之后。计算后,首先附加低字节,然后是高字节。高字节为报文发送的最后一个子节。

  • CRC 的值由发送设备计算,接收设备在接收报文时重新计算CRC,将计算结果于实际接收到的CRC 值相比

  • 只有字符中的8个数据位参与生成CRC 的运算,起始位,停止位和校验位不参与CRC 计算。

生成CRC 的过程为:

  1. 将一个16 位寄存器装入十六进制FFFF (全1). 将之称作CRC 寄存器.
  2. 将报文的第一个8 位字节与16 位CRC 寄存器的低字节异或,结果置于CRC 寄存器.
  3. 将CRC 寄存器右移1 位(向LSB 方向), MSB 充零. 提取并检测LSB.
  4. (如果LSB 为0): 重复步骤3 (另一次移位).
    (如果LSB 为1): 对CRC 寄存器异或多项式值0xA001 (1010 0000 0000 0001).
  5. 重复步骤3 和4,直到完成8 次移位。当做完此操作后,将完成对8 位字节的完整操作。
  6. 对报文中的下一个字节重复步骤2 到5,继续此操作直至所有报文被处理完毕。
  7. CRC 寄存器中的最终内容为CRC 值.
  8. 当放置CRC 值于报文时,如下面描述的那样,高低字节必须交换。

当16 位CRC (2 个8 位字节) 在报文中传送时,低位字节首先发送,然后是高位字节。
例如, CRC 值为十六进制1241 (0001 0010 0100 0001):
image

  • 所有的可能的CRC 值都被预装在两个数组中。一个数组含有16 位CRC 域的所有256 个可能的高位字节,另一个数组含有地位字节的值。此函数返回的是已经经过交换的CRC 值。
    函数使用两个参数:
    unsigned char *puchMsg: 指向含有用于生成CRC 的二进制数据报文缓冲区的指针
    unsigned short usDataLen: 报文缓冲区的字节数.

/* 高位字节的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
} ;

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 char *puchMsg ; /* 用于计算CRC 的报文*/
unsigned short usDataLen ; /* 报文中的字节数*/

unsigned short CRC16 (unsigned char * puchMsg,unsigned short  usDataLen ) /* 函数以unsigned short 类型返回CRC */
{
	unsigned char uchCRCHi = 0xFF ; /* CRC 的高字节初始化*/
	unsigned char uchCRCLo = 0xFF ; /* CRC 的低字节初始化*/
	unsigned uIndex ; /* CRC 查询表索引*/
	
	while (usDataLen--) /* 完成整个报文缓冲区*/
	{
		uIndex = uchCRCLo ^ *puchMsgg++ ; /* 计算CRC */
		uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex} ;
		uchCRCHi = auchCRCLo[uIndex] ;
	}
	return (uchCRCHi << 8 | uchCRCLo) ;
}

2.5.1.3 LRC 校验

  • 纵向冗余校验(LRC)为一个字节,含有8 位二进制值
  • 由发送设备计算,接收设备在接收文时计算LRC, 并将计算的结果与在LRC 接收到的实际值相比

生成一个LRC 的过程为:
1.不包括起始”冒号”和结束CRLF 的报文中的所有字节相加到一个8 位域,故此进位被丢弃。
2.从FF (全1)十六进制中减去域的最终值,产生1 的补码(二进制反码)。
3.加1 产生二进制补码.

高位字符首先发送,然后是低位字符。
例如,LRC 值为十六进制61 (0110 0001):
image

函数带有两个参数:
unsigned char *auchMsg; 指向含有用于生成LRC 的二进制数据报文缓冲区的指针,
unsigned short usDataLen; 报文缓冲区的字节数.


unsigned char *auchMsg ; /* 要计算LRC 的报文*/
unsigned short usDataLen ; /* 报文的字节数*/

static unsigned char LRC(auchMsg, usDataLen) /* 函数返回unsigned char 类型的LRC 结果
*/
{
	unsigned char uchLRC = 0 ; /* LRC 初始化*/
	
	while (usDataLen--) /* 完成整个报文缓冲区*/
		uchLRC += *auchMsg++ ; /* 缓冲区字节相加,无进位*/

	return ((unsigned char)(-((char)uchLRC))) ; /* 返回二进制补码*/
}

三、物理层

3.3 电气接口

3.3.1 多点串行总线结构

image

  • 主干间接口:ITr
  • 设备和无源接口:IDv(分支)
  • 设备和有源接口:AUI(附加)

3.3.2 2线-MODBUS

image

  • 任何时刻只有一个驱动器有权力发送数据。

image

3.3.3 可选的4 线-MODBUS 定义

  • 主对总线(RXD1-RXD2)上的数据只能由从站接收,从对总线(TXD0-TXD1)上的数据只能由主站接收。

  • 任何时刻只有一个驱动器有权力发送数据。
    image
    image

  • 主站应该:

    • 从对总线(TXD1-TXD0)上接收来自从站的数据,
    • 主对总线(RXD1-RXD0)上发送数据,由从站接收,

3.3.3.2 4 线与2 线电缆的兼容性

  • 4 线电缆系统可以按下述修改:
    • TXD0 信号应与RXD0 信号连接,使之成为D0 信号。
    • TXD1 信号应与TXD0 信号连接,使之成为D1 信号。
    • 上拉,下拉电阻和线路终端电阻应重新安排以正确地适应D0,D1 信号。

image

posted @   登云上人间  阅读(130)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
阅读排行:
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 易语言 —— 开山篇
· Trae初体验
  1. 1 原来你也在这里 周笔畅
  2. 2 世间美好与你环环相扣 柏松
  3. 3 起风了 吴青峰
  4. 4 极恶都市 夏日入侵企划
  5. 5 パレード ヨルシカ
极恶都市 - 夏日入侵企划
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 王星

作曲 : 灰鸿啊/皮皮

编曲 : 夏日入侵企画

制作人 : 邢硕

节奏吉他 : 肯尼

主音吉他 : 张伟楠

贝斯 : 皮皮

鼓 : 海鑫

和声 : 邢硕

音效制作 : 邢硕

录音 : 邢硕/夏国兴

混音 : 于昊

特别鸣谢 : 张伟楠

这城市的车流和这地表的颤抖

像一颗石子落入地心之后泛起的温柔

暗涌

河水流过转角她的楼

被梦魇

轻声呓语唤醒身后的幼兽

失效感官焦灼只剩下

麻木愚钝无从感受

共同支撑全都瓦解

只是我们现在都

已忘记到底是

谁隐藏春秋

谁在大雨之后

把旗帜插在最高的楼

过去陈旧的还在坚守

内心已腐朽

摇摇欲坠不停退后

毁灭即拯救

夏日掠夺春秋

结局无法看透

眼看这情节开始变旧

所有的城池已失守

最终无法占有

无眠辗转

伴着人间破碎的旧梦

像繁星

退却后只剩下混沌的夜空

炙热

掩盖风声鹤唳的担忧

把所有失落无助反手推入

无尽的白昼

失效感官焦灼只剩下

麻木愚钝无从感受

共同支撑全都瓦解

只是我们现在都已经忘记到底是

谁隐藏春秋

谁在大雨之后

把旗帜插在最高的楼

过去的陈旧还在坚守

内心已腐朽

摇摇欲坠不停退后

毁灭即拯救

夏日掠夺春秋

结局无法看透

眼看这情节开始变旧

所有的城池早已失守

惶恐难以接受

缠绵往复不肯放手

最终无法占有

谁隐藏春秋

谁在大雨之后

把旗帜插在最高的楼

过去的陈旧还在坚守

内心已腐朽

摇摇欲坠不停退后

毁 灭 即 拯 救

谁掠夺春秋

谁在大雨之后

把旗帜插在最高的楼

过去的陈旧还在坚守

内心已腐朽

摇摇欲坠不停退后

毁灭即拯救

夏日掠夺春秋

结局无法看透

明知城池已失守

缠绵往复不肯放手

最终无法占有

点击右上角即可分享
微信分享提示