拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Modbus协议全解析:从帧结构、CRC校验到现场调试实战

写这篇文章前先交代一句背景我在现场摸爬滚打这些年调过数不清的串口设备、PLC、变频器和仪表Modbus是绕不开的坎。之前有个项目上位机死活读不到从站数据折腾了一天最后发现是CRC校验字节发送顺序搞反了——低字节该先发我后发了。这种细节问题典型的文档里根本不会写明白。所以这篇想把Modbus从帧结构、功能码、工具联调到单片机实现和PLC场景一次讲透。1. 一主多从协议本质和一张数据表就够了先把Modbus的底摸清楚。它不是某个芯片或者特定硬件而是一套应用层报文规范跑在物理层之上。串口也好、以太网也好只要能把字节发出去就能跑Modbus。这也是它活了四十多年还没被淘汰的最根本原因——简单、开放、门槛低。1.1 四种存储对象线圈、离散输入、保持寄存器、输入寄存器Modbus把设备内部的资源抽象成四张表分别是线圈Coil、离散输入Discrete Input、保持寄存器Holding Register、输入寄存器Input Register。理解这四张表整个协议就懂了一半对象名称位/字读写属性典型场景线圈位1 bit可读可写继电器输出、电机启停、阀门开关离散输入位1 bit只读限位开关、按钮状态、光电开关保持寄存器字16 bit可读可写设定温度、运行频率、PID参数输入寄存器字16 bit只读当前温度、压力、电流采样值现场遇到的大部分设备通讯点表里标注的“40001保持寄存器”“30001输入寄存器”就对应上面的表。仪表类产品通常把测量值放在输入寄存器把设定值放在保持寄存器PLC类设备则更灵活你可以自己把内部M区、D区映射到任意一张表上。1.2 为什么这样设计历史包袱和电气隔离需求很多人刚接触时都会问既然都是寄存器为什么要分成输入和保持直接用一个地址空间不行吗这跟Modbus诞生时的工业背景有关系。早期的Modicon PLC里输入通道接按钮、开关和输出通道接接触器、指示灯在硬件上就分开了CPU访问的IO地址空间也不同。协议为了贴合硬件结构干脆在数据模型层就把只读输入和可读写的输出区分开。这样做还有个好处主站从协议层面就能判断某个地址能不能写不用试错。另外在电气上输入寄存器通常来自隔离的模拟量采集卡保持寄存器往往是CPU直接操作的内存映射区两者的访问权限和刷新周期天然不同。所以保持这个划分不是多余是严谨。当然现在的设备很多并不严格区分物理属性有些厂商直接把所有参数都放保持寄存器里一样用。Modbus的灵活就在这里它给了你规则但允许你在规则内自由安排。2. RTU和TCP帧结构以及CRC高低字节那个坑Modbus的主要两种形态RTU串口最常见的是RS485和TCP以太网。TCP本质上就是把RTU的报文包了一层TCP/IP头数据负载部分几乎一致。搞清楚RTUTCP自然就通了。2.1 RTU靠时间间隔分帧不是靠帧头帧尾RTU帧没有Start bit和End marker这是新手最容易懵的地方。它的结构很简单[从站地址1字节] [功能码1字节] [数据N字节] [CRC低字节] [CRC高字节]一帧报文以从站地址开头以CRC结尾。但接收方怎么判断一帧数据结束了靠静默时间。Modbus RTU规定帧内两个字节之间的间隔不能超过1.5个字符时间帧与帧之间的间隔必须大于3.5个字符时间。也就是说总线上静默超过3.5个字符时长就认为上一帧收完了。以9600波特率、8数据位、1停止位为例一个字符要发送10位起始位8数据位停止位无校验时即1.04ms左右。3.5个字符时间大约是3.64ms。所以单片机或者串口服务器在接收时只要检测到总线上空闲超过这个时间就可以把缓冲区里的数据当一整帧来处理。TCP模式就没这么麻烦——TCP是字节流但IP包天然有边界而且Modbus TCP用MBAP头里的长度字段来标识一帧的长度不存在帧边界判断问题。这也是很多人觉得TCP比RTU好调的原因之一。2.2 CRC16-Modbus的计算与发送顺序CRC校验是RTU最容易翻车的地方我在文章开头说的那个低字节先发的坑就在这里。Modbus RTU使用CRC16多项式是0x8005即标准CRC16但反映射reflected后使用的多项式是0xA001初始值为0xFFFF。最常用的实现是查表法或逐位法。逐位法代码很简单uint16_t crc16_modbus(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i, j; for (i 0; i len; i) { crc ^ buf[i]; for (j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }关键是发送顺序。比如计算出来的CRC是0x1234报文里的CRC字段应该是[CRC低字节] 0x34 [CRC高字节] 0x12也就是说先发低字节再发高字节。很多调不通RTU通讯的朋友卡就卡在这一步。有的从站设备做得宽容高低字节反了也能识别但相当一部分严格遵守协议的从站会直接丢弃报文主站表现为“发送有应答但从站没任何回复”。我建议调试时直接用Modbus Poll或者串口助手抓一发一收对一下报文的CRC字节顺序一分钟就能定位是不是这个问题。3. 功能码和地址偏移40001开头的记忆方法一张报文里除了地址和CRC最核心的就是功能码。功能码告诉从站“你要干什么”地址告诉从站“你在哪下手”。3.1 常用功能码一张表现场最常用的功能码并不多绝大多数场景就是读读写写功能码含义操作对象0x01读线圈线圈0x02读离散输入离散输入0x03读保持寄存器保持寄存器0x04读输入寄存器输入寄存器0x05写单个线圈线圈0x06写单个保持寄存器保持寄存器0x0F写多个线圈线圈0x10写多个保持寄存器保持寄存器功能码03和04是日常使用频率最高的几乎所有支持Modbus的设备都支持这两个读操作。写操作里06和10十六进制0x10最常用。还有0x17读混合读写、0x16掩码写等扩展功能码碰到的时候不多但工业仪表里偶尔会用到。3.2 报文地址与数据地址错开1的根源这是Modbus最容易让人困惑的一个点没有之一。数据点表上写的“保持寄存器40001”在报文里对应的地址是0x0000“保持寄存器40002”对应0x0001。也就是说报文里的寄存器地址从0开始编号而人可读的地址从1开始编号所以报文地址是数据地址减1。很多组态软件、触摸屏、网关配置界面里会让你填“寄存器地址”。不同厂家的处理不一样有的直接填40001这种软件内部帮你换算有的只填偏移量让你写0、1、2还有一种容易坑人的软件界面上写着“地址”但实际是以0开始的你必须填0填1就对应到了40002我的习惯是拿到点表后先在Modbus Poll这种工具上验证一遍确定偏移关系再往组态里填省得现场反复试。还有个更简单的记忆技巧看见4xxxx开头报文地址就是这个数字减40001。比如40031报文地址就是30十六进制0x001E。另外要注意的是地址范围和数量的关系。比如读保持寄存器从40001开始读10个寄存器报文里起始地址0x0000寄存器数量0x000A。如果起始地址加数量超出了从站实际地址范围从站会返回异常码02非法数据地址这点后面展开讲。4. Modbus Poll和Modbus Slave从零联调做Modbus开发手头必须有一套趁手的调试工具。Modbus Poll主站模拟器和Modbus Slave从站模拟器是行业最常用的两个我第一次把它们连起来时确实卡了一会这里把完整流程写清楚。4.1 第一次让主站和从站跑起来先建从站。打开Modbus Slave按以下步骤操作菜单Connection Connect选择Connection Mode为Serial Port选好COM口号。设置串口参数Common Options里波特率选9600Data Bits 8Parity NoneStop Bits 1即9600 8N1。从站地址默认1保持不变。菜单Setup Slave DefinitionFunction选03 Holding Register起始地址0长度10确认。这样从站就模拟了一个有10个保持寄存器的设备初始值默认全0可以双击单元格手动改成任意数值。点击OK后从站界面进入监听状态。再开主站。打开Modbus Poll按以下步骤操作菜单Connection Connect连接模式同样选Serial Port选择同一个COM口。关键点补充如果Modbus Slave已经把COM口占用了Modbus Poll是打不开同一个口的。这时可以用虚拟串口软件创建一对互联的COM口比如COM3和COM4从站连COM3主站连COM4两边就能通。很多做单片机和上位机联调的人都会装一个虚拟串口工具就是这个原因。串口参数设置跟从站完全一致9600、8N1。菜单Setup Read/Write DefinitionFunction选03 Holding RegisterSlave ID填1起始地址0长度10轮询周期设1000ms。点击OK主站开始周期性发送读请求。正常情况下主站界面的数据表格里会显示从站里填的数值通讯状态显示OK。看到数据刷出来你的第一个Modbus RTU链路就算通了。4.2 工具选择的几个出口正版试用、开源替代不少人来问Modbus Poll的注册码或密钥文件。我的建议很简单不要图这个省事。Modbus Poll有官方评估期过了之后会限制使用但调试一个项目通常用不了几个月。网上找来的注册码动辄携带木马在工控电脑上中一次毒损失远超几百块授权费。如果确实想用免费工具推荐几个替代方案QModMaster开源支持RTU和TCP界面朴素但功能齐全ModbusPal免费的Modbus从站模拟器可以模拟多个从站适合一主多从测试丹佛斯或西门子官方出的Modbus调试工具也都不错跟自家硬件配合更好另外很多USB转RS485模块会附带调试软件比如CH340芯片方案的厂家就会送一个简单的Modbus调试助手临时用也够了。核心是能读能写、能看到原始报文而不是纠结用什么牌子。5. 从站异常码Exception Response不是错误是情报Modbus有个很好的设计从站收到非预期请求时不会丢帧不吭声而是返回一个异常响应。响应报文的功能码是请求功能码的最高位置1并附带一个异常码。比如你发功能码03去读从站返回0x83数据字节为0x02就是在告诉你“功能码我支持但地址不在我的范围内”。5.1 01/02/03/04四条最常见异常码的排查链路异常码含义现场排查方向01非法功能码从站不支持这个功能码。先确认从站支持哪些功能码有些仪表只支持03/04你发06写寄存器就会收到0102非法数据地址请求的起始地址数量超出从站地址范围。重点检查地址偏移报文地址是不是比点表地址小103非法数据值请求里的数据超出允许范围比如写频率设定值15.00但量程上限只有50Hz写100Hz就会被拒04从站设备故障从站收到请求但无法执行。通常是从站内部状态不对、正在忙、或者外设如采集芯片异常异常码02是现场遇到最多的。我处理过一个水处理项目上位机读PH仪表仪表手册点表上写着保持寄存器40021~40040组态王里填了地址20结果一直报02。后来把报文抓出来一看组态软件自作聪明把地址加1变成了21对应的报文地址0x0014其实指向了40022依然在范围内但数据对不上。这种软件层面的隐性偏移只能在报文级别才能发现。排查异常码的顺序我建议固定为先看功能码再看地址再看值。报文抓出来一个字节一个字节对着点表核比在组态里瞎改要快得多。5.2 通过异常码保护现场超时与重试策略在PLC或者上位机程序里处理Modbus通讯不能只写“发送请求、等待响应”这种简单逻辑必须考虑异常响应、超时、重试三种情况。我最常用的策略是超时时间设置RTU模式下从站正常响应时间一般是10~100ms。超时设为500ms比较合理太短会把慢速从站误判为故障太长会拖慢轮询周期。重试次数连续3次无响应或异常才判定该从站离线或故障。异常码和超时要区分处理收到异常码说明链路通、从站活着是请求内容有问题一直无响应才是链路断了。这两个问题不能混淆成同一个诊断码。很多工程师写轮询程序时一个寄存器读失败就整个站卡住。正确做法是单点失败不影响整站轮询读失败的地址记录错误次数跳过继续读下一个一轮结束后统一报错。否则32个变频器只要有一台掉线后面全部连锁超时这是灾难性的。6. 单片机做RTU从站串口接收状态机和帧间隔判断做嵌入式或者单片机项目经常要用单片机模拟一个Modbus从站接传感器、显示屏或者作为PLC的下位机。RTU帧没有帧头帧尾怎么可靠区分每一帧是写代码的第一个难题。6.1 接收思路串口中断字节间隔定时器一个可靠的接收方案必须在串口接收中断里做两件事存字节、重置定时器。定时器的作用是判断帧结束。每收到一个字节就重置一个定时器定时时长设为3.5字符时间以上兼容1.5字符间隔的扩展。如果定时器溢出说明总线上已经安静了足够长的时间当前接收缓冲区里就是完整的一帧。到9600波特率时这个延时大约是5~10ms。伪代码如下volatile uint8_t rx_buf[256]; volatile uint16_t rx_len; volatile uint8_t rx_complete_flag; void Timer_ISR(void) // 定时器中断2ms进一次也行 { frame_idle_ticks; if (frame_idle_ticks 5) { frame_idle_ticks 0; rx_complete_flag 1; // 空闲超时认为一帧接收完成 } } void UART_ISR(void) // 串口接收中断 { uint8_t byte UART_GetByte(); if (rx_len sizeof(rx_buf)) { rx_buf[rx_len] byte; } frame_idle_ticks 0; // 收到字节重置空闲计时 }收到完整帧后主循环里处理if (rx_complete_flag) { rx_complete_flag 0; // 1. 判断地址是否匹配 // 2. CRC校验 // 3. 解析功能码与数据 // 4. 构造响应帧 rx_len 0; }除了纯粹的定时器方案不少MCU支持串口空闲中断IDLE Line可以在硬件层面检测一帧结束更省CPU资源。还有用DMA空闲中断组合的适合高速率和长数据场景但调试复杂度也高一些新手先拿定时器方案跑通逻辑再优化也不迟。6.2 一帧数据的解析和响应构造解析的核心是状态机。我习惯这样组织地址校验如果报文里的从站地址不等于本机地址直接丢弃不构造任何响应。CRC校验用前面那份代码重新算一遍和报文末尾的CRC比较不一致直接丢弃。功能码分发根据03/04/06/10等分支处理。地址范围检查起始地址加数量是否越界越界返回异常码02。执行读写操作构造响应帧再补上CRC。这里有一个容易忽略的点如果本机地址是0广播地址从站需要执行请求但不返回任何响应。所以在发送响应前记得判断一下地址如果不是0才回帧。响应帧构造好后发送的CRC同样要低字节先发、高字节后发。有的单片机工程师在这里栽过一次我当时也是用串口抓一下回帧跟协议规范一比对秒懂。另外从站的“处理时间”要注意——有些从站收到写请求后执行动作需要几十毫秒比如继电器动作。如果主站紧接着发下一条请求从站可能来不及处理。这种情况下从站可以返回异常码06从站忙或者干脆用延时·响应机制这取决于设备设计但无论如何主站侧的超时和重试逻辑必须兼容这种情况。7. 西门子PLC带32台变频器实时性、接线和轮询这个场景是工控群里的高频问题一台西门子S7-1200通过RS485接32台变频器全部走Modbus RTU可行吗答案是可行但要算清楚轮询周期和通讯压力。7.1 可行性分析9600波特率下的一轮轮询要多久以最常见的“读每台变频器的当前频率和电流各2个保持寄存器”为例一次完整请求响应大约是这样的请求帧地址1字节 功能码3字节含寄存器数量和CRC总共8字节响应帧地址1 功能码1 字节数1 数据4字节2个寄存器 CRC2字节 9字节一收一发共17字节9600波特率下每个字符10位传输一个字符约1.04ms。17字节约需18ms。32台变频器顺序轮询总传输时间约0.6秒。再加上PLC扫描周期、从站响应延迟、帧间间隔实际一轮大约1秒左右。结论很清晰9600波特率下读32台的关键参数实时性在1秒级。如果要在0.5秒内刷新全部数据需要把波特率提高到19200或38400传输时间近似减半减少每台读取的寄存器数量把32台分成两组用两个RS485口并行轮询S7-1200可以扩展通信模块如果实时性要求再高比如伺服轴联动、高速张力控制Modbus RTU这个层级就不太合适了得考虑EtherCAT、Profinet IRT这类实时总线。Modbus的意义在于简单稳定别拿它硬扛高速场景。接线方面RS485的A/B线要接对变频器侧的终端电阻要打开现场强电线和通讯线必须分开走线槽。屏蔽层单端接地这是老生常谈但真的管用。7.2 组态软件对接Modbus的地址偏移陷阱最后聊一下组态软件kingSCADA、组态王、WinCC等连接Modbus TCP时最容易踩的坑地址映射。Modbus TCP连接时通常只用填设备IP和端口502从站地址对应Unit ID。麻烦的是寄存器地址的填写。不同组态软件对“寄存器地址”的起点定义完全不同有的以1开头、有的以0开头、有的让你填40001、有的只填偏移量40001。我在kingSCADA上对接过一个Modbus TCP仪表设备配置里选“保持寄存器”地址填的是1结果读出来是点表第二个寄存器的值差了一个。后来查了官方手册才知道kingSCADA的“地址”是按数据块里第几个寄存器算的从1开始而不是直接填协议地址。所以不管用什么组态软件第一件事永远是拿Modbus Poll对着同样的设备测试一遍确定“点表地址→报文地址→组态地址”三层对应关系再进行组态配置。不要省这个步骤一旦PLC系统里几十个点位全部配错现场恢复的代价比现在多花十分钟验证大得多。说到最后Modbus这套协议之所以到今天还在工控界屹立不倒说白了就是因为它不挑硬件、文档公开、实现简单。掌握了帧结构、CRC、功能码和异常码这四件事无论是调试仪表、单片机从站还是PLC组态都只是套模板的活。那些把Modbus想得高深莫测的人多半是没理解它“一张数据表走天下”的底层逻辑。先把表搞清楚再多抓几次报文这些坑你都能绕过去。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门