恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

从零吃透 Modbus 通信|第 2 章:Modbus RTU 帧结构超精讲,每一字节都要彻底读懂

  • 首页
  • 资讯中心
  • /
  • 从零吃透 Modbus 通信|第 2 章:Modbus RTU 帧结构超精讲,每一字节都要彻底读懂

相关资讯

EventBus源码赏析一 —— 基本使用 2026/8/27 12:24:39
二叉搜索树(全) 2026/8/27 12:24:39
【性能优化】大厂OOM优化和监控方案 2026/8/27 12:24:39

最新资讯

【TDengine】JDBC 连接 TDengine 的 URL 格式是什么?如何配置连接池?
【TDengine】TDengine 支持哪些编程语言的客户端驱动(Java、Python、Go 等)?
阿里云 MaxCompute通过python脚本调用odps流程和执行sql
【TDengine】如何通过系统表诊断慢查询?
VecDB第五篇:从问题到答案:一个完整的RAG系统是如何工作的
免费激活 Adobe 全家桶 2019–2023:Adobe-GenP 3.0 完整上手指南

今日推荐

Go语言构建企业级AI服务网关:统一管理英伟达等AI接口调用
LeetCode Hot100(51-60)算法精解与面试技巧
CRC校验实战:从模2除法到HJ212协议排错

本周热门

Nextcloud 桌面客户端:把同步交给它,你只管改文件
如何将 HTML 转成 Word 文档且格式不丢失?html-to-docx 使用教程
Anki 批量操作卡片完整指南:一次搞定上千张,不再逐张修改

本月精选

如何用DamaiHelper实现演唱会门票的智能自动化抢购:完整技术解决方案指南
第4篇:59 倍性能差距的索引瓶颈定位——一次教科书级的全表扫描调优
终极歌词批量下载神器:5分钟解决离线音乐库歌词同步难题

从零吃透 Modbus 通信|第 2 章:Modbus RTU 帧结构超精讲,每一字节都要彻底读懂

发布时间:2026/8/27 12:29:39
从零吃透 Modbus 通信|第 2 章:Modbus RTU 帧结构超精讲,每一字节都要彻底读懂 前言上一章我们建立了 Modbus 的全局认知主从一问一答、地址识别、功能码操作、寄存器读写、CRC 校验。但真正写代码时这些概念还不够。你必须能看懂每一帧数据长什么样子主机发出去的请求帧到底是怎么组成的从机返回的应答帧每一字节代表什么为什么有时候设备会返回异常码CRC 校验错了是哪一环节出了问题本章我们进入 Modbus RTU 的核心帧结构。读完本章你不需要再靠 “猜协议” 调试而是能直接根据串口数据判断这帧是读寄存器、写寄存器、异常响应还是 CRC 错误。1. Modbus RTU 帧总览1.1 一帧数据到底由什么组成Modbus RTU 一帧通常由以下几部分组成字节字段作用第 1 字节从站地址告诉总线要和哪个设备通信第 2 字节功能码告诉设备要执行什么操作第 3~4 字节寄存器起始地址从哪个寄存器开始读 / 写第 5~6 字节寄存器数量 / 写入数据读多少个寄存器或写入什么数据最后 2 字节CRC 校验校验整帧数据是否正确注意 不同功能码的帧结构会有差异。 其中最常见、最容易踩坑的是03 功能码读保持寄存器06 功能码写单个寄存器16 功能码写多个寄存器异常响应帧2. 最经典案例03 功能码读保持寄存器2.1 应用场景03 功能码是工程中使用频率最高的功能码。作用读取从设备的保持寄存器数据。例如读温度读电压读电流读转速读设备状态读 PID 参数2.2 主机请求帧格式主机发送一帧字节含义示例1从站地址0x012功能码0x033起始地址高字节0x004起始地址低字节0x005寄存器数量高字节0x006寄存器数量低字节0x027CRC 低字节0x048CRC 高字节0x0B示例解析01 03 00 00 00 02 04 0B含义从站地址0x01功能码0x03读保持寄存器起始寄存器地址0x0000读取寄存器数量0x0002CRC 校验0x0B04注意 Modbus 中多字节数据通常采用大端模式也就是高字节在前低字节在后。所以00 00代表寄存器起始地址0x0000而不是 0x0000 反过来。2.3 从机正常应答帧格式如果从机收到合法请求会返回数据。应答帧格式如下字节含义示例1从站地址0x012功能码0x033字节数0x044~5寄存器 1 数据0x00 0x0A6~7寄存器 2 数据0x00 0x148CRC 低字节0xXX9CRC 高字节0xXX示例01 03 04 00 0A 00 14 XX XX解析从站地址0x01功能码0x03后续数据字节数0x04第 1 个寄存器0x000A第 2 个寄存器0x0014最后两字节为 CRC 校验这里非常关键读 N 个寄存器返回的数据字节数 N × 2。因为一个寄存器是 2 字节。例如读 1 个寄存器 → 返回 2 字节数据读 2 个寄存器 → 返回 4 字节数据读 10 个寄存器 → 返回 20 字节数据这一点写代码时必须用循环处理不能写死长度。3. 06 功能码写单个寄存器3.1 应用场景06 功能码用于写入单个寄存器。常见用途修改设备地址修改波特率设置转速设置 PID 参数控制设备启停修改报警阈值3.2 主机写请求帧格式字节含义示例1从站地址0x012功能码0x063寄存器地址高字节0x004寄存器地址低字节0x015写入数据高字节0x006写入数据低字节0x0A7CRC 低字节0xXX8CRC 高字节0xXX示例01 06 00 01 00 0A XX XX含义从站地址0x01功能码0x06寄存器地址0x0001写入数据0x000ACRC 校验3.3 从机正常应答写成功后从机通常会回显相同内容01 06 00 01 00 0A XX XX这代表我已经收到并执行了你的写入命令。注意 不是所有设备都会严格回显但标准 Modbus 设备通常会返回原写帧作为确认。4. 16 功能码写多个寄存器4.1 应用场景06 功能码只能写单个寄存器。如果需要批量写入多个参数就要用16 功能码。典型场景批量写入 PID 参数批量写入控制指令批量写入配置参数批量校准设备参数4.2 主机请求帧格式字节含义1从站地址2功能码0x103起始地址高字节4起始地址低字节5寄存器数量高字节6寄存器数量低字节7写入数据字节数8~多组寄存器数据最后 2 字节CRC 校验示例01 10 00 10 00 02 04 00 01 00 02 XX XX解析从站地址0x01功能码0x10起始地址0x0010写入寄存器数量0x0002后续数据字节数0x04第 1 个寄存器写入0x0001第 2 个寄存器写入0x0002CRC 校验4.3 从机应答写成功后从机通常返回01 10 00 10 00 02 XX XX含义已确认从地址 0x0010 开始写入 2 个寄存器。5. 异常响应帧设备为什么报错5.1 异常响应长什么样当主机发送非法指令时从机不会正常返回数据而是返回异常帧。异常帧格式字节含义1从站地址2功能码最高位置 13异常码4CRC 低字节5CRC 高字节例如01 83 02 XX XX解析从站地址0x01功能码0x83说明原来的 0x03 请求发生异常异常码0x02CRC 校验注意 异常响应的功能码 原功能码 0x80。例如原功能码 0x03 → 异常码 0x83原功能码 0x06 → 异常码 0x86原功能码 0x10 → 异常码 0x90这是写代码时判断异常响应的重要依据。5.2 常见异常码对照表异常码含义常见原因0x01非法功能码设备不支持该功能码0x02非法数据地址寄存器地址超出范围0x03非法数据值写入数值不合法0x04从设备故障设备内部错误0x05确认设备正在处理需要稍后重试0x06从设备忙设备忙主机稍后再发0x08内存奇偶校验错误存储数据异常0x0A不可用网关路径网关路径不可用0x0B网关目标设备响应失败目标设备无响应工程中最常见的三个异常码0x02非法数据地址通常说明你读的寄存器地址设备没有寄存器地址越界起始地址 读取数量超过设备寄存器范围例如设备只支持地址 0x0000~0x0009你却读了 0x000A就会返回 0x02。0x03非法数据值通常说明写入的数据超出设备允许范围参数格式错误设备不接受该数值例如设备转速范围是 0~3000你写入 6000可能返回 0x03。0x06从设备忙说明设备正在处理其他任务主机应该稍后重试。这也是主机程序必须做超时和重试机制的原因。6. CRC16 校验Modbus 的数据安全底线6.1 CRC 在帧中的位置Modbus RTU 每一帧最后两字节是 CRC 校验。例如01 03 00 00 00 02 04 0B最后两字节04 0B但注意Modbus RTU 中 CRC 存储方式是低字节在前高字节在后。所以实际 CRC 值是0x0B04而不是 0x040B。这一点新手极容易写错。6.2 CRC16-MODBUS 标准算法Modbus 使用标准 CRC16-MODBUS 算法。初始值0xFFFF多项式0xA001计算范围从从站地址开始到 CRC 之前的所有字节。6.3 直接可用的 C 语言 CRC 代码#include stdint.h uint16_t Modbus_CRC16(uint8_t *data, uint16_t length) { uint16_t crc 0xFFFF; uint16_t i, j; for(i 0; i length; i) { crc ^ data[i]; for(j 0; j 8; j) { if(crc 0x0001) { crc 1; crc ^ 0xA001; } else { crc 1; } } } return crc; }6.4 如何使用例如你构造了一帧请求uint8_t tx_buf[] {0x01, 0x03, 0x00, 0x00, 0x00, 0x02};计算 CRCuint16_t crc Modbus_CRC16(tx_buf, sizeof(tx_buf));然后把 CRC 低字节和高字节追加到帧尾tx_buf[6] crc 0xFF; tx_buf[7] (crc 8) 0xFF;注意顺序tx_buf[6] crc_low tx_buf[7] crc_high这是 Modbus RTU 标准要求。7. 主机如何判断一帧数据是否合法7.1 主机接收判断流程主机收到从机应答后必须按以下顺序判断第一步判断帧长度是否足够Modbus RTU 最短帧至少也要包含从站地址功能码CRC 低字节CRC 高字节如果长度不足直接丢弃。第二步判断从站地址是否匹配收到的地址必须等于目标从站地址或为广播地址。第三步判断是否为异常响应如果功能码最高位为 1说明是异常响应。例如if(rx_buf[1] 0x80) { // 异常响应 }第四步重新计算 CRC 并对比主机必须重新计算接收帧的 CRC再和帧尾 CRC 对比。只有相等才认为数据有效。第五步根据功能码解析数据如果是 03 功能码按读寄存器解析。如果是 06 功能码按写确认解析。7.2 主机解析 03 功能码示例代码uint8_t Modbus_Parse_Read_Holding(uint8_t *rx_buf, uint16_t rx_len, uint16_t *reg_data, uint16_t reg_num) { uint16_t crc_calc; uint16_t crc_rx; uint16_t i; if(rx_len 5) { return 0; } if(rx_buf[0] ! 0x01) { return 0; } if(rx_buf[1] 0x83) { // 异常响应 return rx_buf[2]; } if(rx_buf[1] ! 0x03) { return 0; } if(rx_buf[2] ! reg_num * 2) { return 0; } crc_calc Modbus_CRC16(rx_buf, rx_len - 2); crc_rx (uint16_t)rx_buf[rx_len - 2] | ((uint16_t)rx_buf[rx_len - 1] 8); if(crc_calc ! crc_rx) { return 0; } for(i 0; i reg_num; i) { reg_data[i] ((uint16_t)rx_buf[3 i * 2] 8) | rx_buf[4 i * 2]; } return 1; }这段代码已经具备工程雏形。它判断了帧长度地址异常响应功能码数据字节数CRC 校验寄存器数据解析8. 从机如何处理主机请求8.1 从机处理流程从机收到一帧后通常按以下流程处理1. 判断地址是否匹配如果不是本机地址也不是广播地址直接忽略。2. 计算 CRC如果 CRC 错误直接丢弃。3. 判断功能码根据功能码进入不同处理函数。4. 判断寄存器地址是否合法如果地址越界返回异常码 0x02。5. 判断写入数据是否合法如果数据范围错误返回异常码 0x03。6. 执行读 / 写操作读取寄存器或写入寄存器。7. 构造应答帧按照 Modbus 格式返回应答。8.2 从机处理 03 功能码示例代码uint8_t Modbus_Slave_Process(uint8_t *rx_buf, uint16_t rx_len, uint8_t *tx_buf, uint16_t *tx_len) { uint16_t crc_calc; uint16_t crc_rx; uint16_t start_addr; uint16_t quantity; uint16_t i; if(rx_len 8) { return 0; } if(rx_buf[0] ! 0x01) { return 0; } crc_calc Modbus_CRC16(rx_buf, rx_len - 2); crc_rx (uint16_t)rx_buf[rx_len - 2] | ((uint16_t)rx_buf[rx_len - 1] 8); if(crc_calc ! crc_rx) { return 0; } if(rx_buf[1] 0x03) { start_addr ((uint16_t)rx_buf[2] 8) | rx_buf[3]; quantity ((uint16_t)rx_buf[4] 8) | rx_buf[5]; if(start_addr quantity 10) { tx_buf[0] 0x01; tx_buf[1] 0x83; tx_buf[2] 0x02; *tx_len 5; Modbus_Append_CRC(tx_buf, tx_len); return 1; } tx_buf[0] 0x01; tx_buf[1] 0x03; tx_buf[2] quantity * 2; for(i 0; i quantity; i) { tx_buf[3 i * 2] 0x00; tx_buf[4 i * 2] Holding_Reg[start_addr i]; } *tx_len 3 quantity * 2; Modbus_Append_CRC(tx_buf, tx_len); return 1; } return 0; }其中补充一个追加 CRC 的函数void Modbus_Append_CRC(uint8_t *buf, uint16_t *len) { uint16_t crc Modbus_CRC16(buf, *len); buf[*len] crc 0xFF; buf[*len 1] (crc 8) 0xFF; *len 2; }9. 新手最容易写错的 5 个点9.1 CRC 顺序错了Modbus RTU 中CRC低字节在前 CRC高字节在后很多人写成tx_buf[6] (crc 8) 0xFF; tx_buf[7] crc 0xFF;这会导致从机校验失败不响应。9.2 寄存器地址没有加偏移很多设备手册里写的寄存器地址是1 起始地址。例如手册写温度寄存器地址00001实际代码里通常要减 1变成0x0000否则会多读一个地址偏移。9.3 没有处理异常响应从机返回异常码后主机如果继续按正常帧解析会导致数据错误。9.4 写死帧长度读 1 个寄存器和读 10 个寄存器的帧长度不同。代码必须根据数据字节数动态解析。9.5 没有超时机制从机离线后主机如果一直等待程序会卡死。主机必须设置超时#define MODBUS_TIMEOUT_MS 100超过 100ms 没有应答判定失败。10. 本章总结本章我们彻底拆解了 Modbus RTU 最核心的帧结构。你现在应该能读懂03 功能码读保持寄存器请求帧03 功能码应答帧06 功能码写单个寄存器16 功能码写多个寄存器异常响应帧CRC 校验帧核心记忆点Modbus RTU 帧 地址 功能码 数据 CRC03 功能码最常用用于读传感器、电压、转速、参数06 功能码用于写单个寄存器16 功能码用于批量写多个寄存器异常响应功能码 原功能码 0x80CRC 必须低字节在前高字节在后从机不主动说话主机必须做超时判断

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号