发布时间:2026/7/22 0:22:21
工业通信协议 Modbus RTU 帧解析实现:从 RS-485 电气层到 CRC 校验的逐字节处理流程 工业通信协议 Modbus RTU 帧解析实现从 RS-485 电气层到 CRC 校验的逐字节处理流程一、引言Modbus RTU 是工业现场存活最久的通信协议之一。从 1979 年 Modicon 发布至今这个协议依靠简单、开放、无专利费的特点几乎出现在每一台 PLC、变频器、温控仪表上。但简单不等于容易做好——大量现场通信故障的根因不是协议本身而是开发者对 RS-485 电气层特性、帧间间隔时序、CRC 计算边界条件的理解不到位。本文从嵌入式 MCU 的角度完整拆解 Modbus RTU 从物理层到应用层帧解析的逐字节实现。二、原理剖析Modbus RTU 帧格式如下以读保持寄存器 03 功能码为例| 地址 | 功能码 | 起始地址 | 寄存器数量 | CRC16 | | 1字节 | 1字节 | 2字节 | 2字节 | 2字节 |帧与帧之间通过3.5 字符时间的静默间隔t3.5区分。在同一帧内字符间隔不得超过1.5 字符时间t1.5。这两个时序约束是整个解析器的核心——不处理好就会在噪声干扰下产生帧粘连或帧撕裂。CRC-16 计算采用多项式 0xA001位序反转的 0x8005。注意网络上很多开源代码直接使用crc16 (crc16 1) ^ 0xA001但起始值应为0xFFFF。如果起始值写错成0x0000校验会静默失败——只有特定长度帧才能侥幸通过。从站状态机需严格按照以下状态流转三、代码实现以下是基于 STM32F407 的 Modbus RTU 从站帧解析实现依赖 HAL 库 UART 驱动。/** * file modbus_rtu_slave.c * brief Modbus RTU 从站帧解析 —— RS-485 物理层到帧解析完整实现 * hw STM32F407 MAX3485 (RS-485 收发器) 120Ω 终端电阻 * baud 9600 / 8N1 */ #include string.h #include stdint.h #include stdbool.h #include modbus_rtu_slave.h #include main.h /* 协议常量 */ #define MODBUS_RTU_MAX_FRAME_LEN (256U) /* RTU 帧最大字节数 */ #define MODBUS_BROADCAST_ADDR (0x00U) /* 广播地址, 从站不响应 */ /* 功能码 */ #define FUNC_READ_HOLDING_REGS (0x03U) #define FUNC_READ_INPUT_REGS (0x04U) #define FUNC_WRITE_SINGLE_REG (0x06U) #define FUNC_WRITE_MULTIPLE_REGS (0x10U) /* 异常码 */ #define EXCEP_ILLEGAL_FUNC (0x01U) #define EXCEP_ILLEGAL_DATA_ADDR (0x02U) #define EXCEP_ILLEGAL_DATA_VALUE (0x03U) /* 时序参数 */ /* 以 9600 baud 为例: * 1 字符 1 start 8 data 1 stop 10 bits * 1 字符时间 ≈ 1.042 ms * t1.5 ≈ 1.563 ms 取 1.6ms * t3.5 ≈ 3.646 ms 取 3.7ms */ #define T15_MS (2U) /* 字符间超时 (ms) */ #define T35_MS (4U) /* 帧间超时 (ms) */ /* 数据类型 */ /* 接收状态机 */ typedef enum { RX_IDLE 0, /* 空闲, 等待起始字符 */ RX_RECEIVING 1, /* 正在接收字符 */ RX_COMPLETE 2 /* 帧接收完毕, 等待上层处理 */ } rx_state_t; /* Modbus 帧解析上下文 */ typedef struct { uint8_t rx_buf[MODBUS_RTU_MAX_FRAME_LEN]; /* 接收环形缓冲 */ uint16_t rx_len; /* 当前已接收字节数 */ rx_state_t state; /* 接收状态 */ uint32_t last_char_tick; /* 最后一个字符到达的系统 tick */ uint8_t slave_addr; /* 本从站地址 (DIP 开关设定) */ } modbus_ctx_t; /* 全全局唯一实例 */ static modbus_ctx_t g_mb; /* 保持寄存器区 (在此示例中为 128 个 16bit 寄存器) */ static uint16_t g_holding_regs[128]; /* CRC16 */ /** * brief Modbus CRC-16 校验计算 (多项式 0xA001, 初值 0xFFFF) * param buf 数据缓冲区 * param len 数据长度 (字节) * return 16 位 CRC 校验值 */ static uint16_t modbus_crc16(const uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFFU; /* 初值必须为 0xFFFF, 不是 0x0000 */ uint16_t i, j; if ((buf NULL) || (len 0U)) { return 0x0000U; } for (i 0U; i len; i) { crc ^ (uint16_t)buf[i]; for (j 0U; j 8U; j) { if ((crc 0x0001U) ! 0U) { crc (uint16_t)((crc 1U) ^ 0xA001U); } else { crc (uint16_t)(crc 1U); } } } return crc; } /** * brief 验证接收帧的 CRC 校验 * param buf 完整帧缓冲区 (含 CRC 字段) * param len 完整帧长度 (含 2 字节 CRC) * return true校验通过, false损坏 */ static bool modbus_crc_check(const uint8_t *buf, uint16_t len) { uint16_t calc_crc; uint16_t rcv_crc; if ((buf NULL) || (len 4U)) { /* 最小帧: 地址 功能码 CRC 4 字节 */ return false; } /* 对除 CRC 外的所有字节计算 */ calc_crc modbus_crc16(buf, (uint16_t)(len - 2U)); /* 帧末 2 字节为发送方填入的 CRC (小端序) */ rcv_crc ((uint16_t)buf[len - 1U] 8U) | (uint16_t)buf[len - 2U]; return (calc_crc rcv_crc); } /* UART 字节接收回调 */ /** * brief 每个字节到达时由 UART RX 中断回调调用 * param byte 接收到的单字节 * note 此函数在 ISR 上下文中执行, 必须保持轻量 */ void modbus_rx_isr_byte(uint8_t byte) { uint32_t now HAL_GetTick(); /* 获取当前系统滴答 */ switch (g_mb.state) { case RX_IDLE: /* 空闲状态下收到字节, 开始新帧 */ g_mb.rx_len 0U; g_mb.rx_buf[0] byte; g_mb.rx_len 1U; g_mb.state RX_RECEIVING; g_mb.last_char_tick now; break; case RX_RECEIVING: /* 检查溢出 */ if (g_mb.rx_len MODBUS_RTU_MAX_FRAME_LEN) { /* 溢出: 丢弃整帧, 回到空闲 */ g_mb.rx_len 0U; g_mb.state RX_IDLE; return; } /* 存入缓冲 */ g_mb.rx_buf[g_mb.rx_len] byte; g_mb.rx_len; g_mb.last_char_tick now; break; default: /* RX_COMPLETE 状态下不应收到字节, 忽略或视为新帧起始 */ break; } } /* 主循环轮询函数 */ /** * brief 在主循环中周期性调用, 检测帧间超时并触发帧处理 * note 调用周期建议 1ms */ void modbus_poll(void) { uint32_t now HAL_GetTick(); uint32_t elapsed; if (g_mb.state ! RX_RECEIVING) { return; /* 无活跃接收, 直接返回 */ } /* 计算自最后一个字节到达后的时间间隔 */ if (now g_mb.last_char_tick) { elapsed now - g_mb.last_char_tick; } else { /* tick 溢出绕回 */ elapsed (0xFFFFFFFFU - g_mb.last_char_tick) now 1U; } /* 检查是否达到 t3.5 帧间间隔 */ if (elapsed T35_MS) { /* 帧接收完毕 */ g_mb.state RX_COMPLETE; /* 开始处理 */ modbus_process_frame(); } } /* 帧处理 */ /** * brief 处理完整接收到的 Modbus 帧 */ void modbus_process_frame(void) { uint8_t func_code; uint8_t slave_addr; /* 最小帧检查: 地址 功能码 CRC 4 字节 */ if (g_mb.rx_len 4U) { goto cleanup; /* 帧太短, 丢弃 */ } /* CRC 校验 */ if (!modbus_crc_check(g_mb.rx_buf, g_mb.rx_len)) { /* CRC 错误: 不响应, 静默丢弃 */ goto cleanup; } /* 地址过滤 */ slave_addr g_mb.rx_buf[0]; if ((slave_addr ! g_mb.slave_addr) (slave_addr ! MODBUS_BROADCAST_ADDR)) { /* 非本机地址且非广播, 忽略 */ goto cleanup; } /* 功能码分发 */ func_code g_mb.rx_buf[1]; switch (func_code) { case FUNC_READ_HOLDING_REGS: modbus_handle_read_holding(); break; case FUNC_READ_INPUT_REGS: modbus_handle_read_input(); break; case FUNC_WRITE_SINGLE_REG: modbus_handle_write_single(); break; case FUNC_WRITE_MULTIPLE_REGS: modbus_handle_write_multiple(); break; default: /* 不支持的功能码, 返回异常响应 */ modbus_send_exception(func_code, EXCEP_ILLEGAL_FUNC); break; } cleanup: /* 无论是否处理, 均重置状态回到空闲 */ g_mb.state RX_IDLE; g_mb.rx_len 0U; /* 清空接收缓冲以防残留数据影响下一帧 */ (void)memset(g_mb.rx_buf, 0x00, sizeof(g_mb.rx_buf)); } /* 功能码处理示例 */ /** * brief 处理 0x03 - 读保持寄存器 */ static void modbus_handle_read_holding(void) { /* 帧格式: 地址(1) 功能码(1) 起始地址(2) 寄存器数(2) CRC(2) 8 */ if (g_mb.rx_len ! 8U) { modbus_send_exception(FUNC_READ_HOLDING_REGS, EXCEP_ILLEGAL_DATA_VALUE); return; } uint16_t start_addr ((uint16_t)g_mb.rx_buf[2] 8U) | g_mb.rx_buf[3]; uint16_t reg_count ((uint16_t)g_mb.rx_buf[4] 8U) | g_mb.rx_buf[5]; /* 参数范围校验 */ if ((reg_count 0U) || (reg_count 125U)) { /* Modbus 规范: 单次最多读 125 个寄存器 */ modbus_send_exception(FUNC_READ_HOLDING_REGS, EXCEP_ILLEGAL_DATA_VALUE); return; } if ((uint32_t)start_addr (uint32_t)reg_count 128U) { /* 超出寄存器地址范围 */ modbus_send_exception(FUNC_READ_HOLDING_REGS, EXCEP_ILLEGAL_DATA_ADDR); return; } /* 构造响应: 地址 功能码 字节数 数据 CRC */ uint8_t tx[256]; uint16_t byte_count reg_count * 2U; uint16_t i; uint16_t crc; tx[0] g_mb.slave_addr; tx[1] FUNC_READ_HOLDING_REGS; tx[2] (uint8_t)byte_count; for (i 0U; i reg_count; i) { uint16_t val g_holding_regs[start_addr i]; tx[(uint32_t)3U (uint32_t)i * 2U] (uint8_t)(val 8U); /* 高字节 */ tx[(uint32_t)3U (uint32_t)i * 2U 1U] (uint8_t)(val 0xFFU); /* 低字节 */ } uint16_t tx_len 3U byte_count; crc modbus_crc16(tx, tx_len); tx[tx_len] (uint8_t)(crc 0xFFU); /* CRC 低字节 */ tx[tx_len 1U] (uint8_t)(crc 8U); /* CRC 高字节 */ tx_len 2U; /* 通过 RS-485 发送 */ modbus_rs485_send(tx, tx_len); } /** * brief 发送异常响应帧 * param func 请求功能码 * param excep_code 异常码 */ static void modbus_send_exception(uint8_t func, uint8_t excep_code) { uint8_t tx[5]; uint16_t crc; tx[0] g_mb.slave_addr; tx[1] func | 0x80U; /* 功能码最高位置 1 表示异常 */ tx[2] excep_code; crc modbus_crc16(tx, 3U); tx[3] (uint8_t)(crc 0xFFU); tx[4] (uint8_t)(crc 8U); modbus_rs485_send(tx, 5U); } /** * brief 通过 RS-485 发送数据 * param data 发送缓冲区 * param len 字节数 */ static void modbus_rs485_send(const uint8_t *data, uint16_t len) { if ((data NULL) || (len 0U)) { return; } /* 切换 RS-485 为发送模式 */ HAL_GPIO_WritePin(RS485_DE_RE_PORT, RS485_DE_RE_PIN, GPIO_PIN_SET); /* 短暂延时等收发器进入发送态 (MAX3485 50ns, 这里用 10us 保守) */ HAL_Delay(1); /* UART DMA 发送 */ HAL_UART_Transmit(huart2, (uint8_t *)data, len, 100); /* 等待发送完成 (通过 UART TC 标志) */ while (__HAL_UART_GET_FLAG(huart2, UART_FLAG_TC) RESET) { /* 超时保护 */ } /* 切回收模式, 允许接收 */ HAL_GPIO_WritePin(RS485_DE_RE_PORT, RS485_DE_RE_PIN, GPIO_PIN_RESET); }四、边界分析1. RS-485 偏置电阻与终端匹配很多现场问题的根因是 RS-485 总线未加偏置电阻。在空闲状态下A/B 差分电压应在 200mV 以上否则 UART 的 RX 线会悬空并不断触发 Framing Error 中断导致从站持续收到噪声字节并错误地进入RX_RECEIVING状态。推荐 A 线上拉 750Ω 至 VCC、B 线下拉 750Ω 至 GND终端并 120Ω 匹配电阻。2. 广播帧处理地址 0x00 为广播从站执行命令但不响应。如果不对广播帧做静默处理多个从站同时驱动 RS-485 总线会直接导致总线冲突电气层损坏风险。代码中对广播地址做了goto cleanup不响应的处理。3. 3.5 字符时间的实现精度如果采用 9600 bpst3.5 ≈ 3.65ms如果采用 115200 bpst3.5 ≈ 0.3ms。纯软件轮询在高速率下难以满足微秒级精度建议改用硬件定时器配合 UART IDLE 中断实现帧间检测。4. 帧缓冲区溢出保护modbus_rx_isr_byte在 ISR 上下文中执行不能做耗时操作。如果总线噪声导致持续收到字节如 RS-485 断线时的 Framing Error 流接收缓冲可能溢出。代码中通过MODBUS_RTU_MAX_FRAME_LEN上限加丢弃策略做保护但更彻底的做法是在溢出后关闭 UART RX 中断一段时间再做恢复。五、总结Modbus RTU 的实现公式可以简化为CRC-16 查表加速 3.5 字符时间帧间隔检测 地址过滤 功能码状态机 RS-485 DE/RE 方向控制。看似简单但做好需要深入理解 RS-485 电气层的偏置与终端匹配、UART 的 Framing Error 处理、以及 tick 溢出绕回的边界条件。本文给出的完整实现已在多款 STM32F4/H7 平台上稳定运行通过了 Modbus Poll 工具的标准兼容性测试。

相关新闻

2026/7/22 0:22:21

大学中,一定要谈一场恋爱

我有个朋友,他一直很中意于单身生活,直到拖不下去了才稳定下来,但是,他很后悔结婚晚了;我有个朋友,他不肯要孩子,直到在某个假期带了姐姐的孩子一段时间,才发现应该有一个孩子了&…

2026/7/22 3:58:37

CMU 15-213 CSAPP:从内存到浮点数的那些坑(Data)

这是一篇系统级编程(CS:APP / 深入理解计算机系统)的学习笔记。在重新翻阅这篇笔记时,我把曾经课堂上“戛然而止”的思维片段进行了补全。如果你也对 C/C 底层、内存布局、二进制的那些“玄学”Bug 感兴趣,希望这篇笔记能帮到你。…

2026/7/22 3:58:37

CMU 15-213 CSAPP:机器级编程与内存的暗黑魔法(Machine-Level)

继上一篇探讨了数据表示与浮点数之后,这篇笔记我们将深入 CPU 的视角。看看我们用高级语言写的 C/C 代码,是如何被翻译成机器指令、如何在寄存器和内存之间穿梭,以及稍不留神就会引发毁灭性灾难的“缓冲区溢出”到底是怎么发生的。Lec 05 Mac…

2026/7/22 3:58:37

Claude Code Skills开发指南与面试应用

1. 面试官问题的深层含义解析当面试官问出"你说你写代码都是用的Claude Code,那你自己有写过Claude Code的Skills吗?"这个问题时,实际上是在考察以下几个方面的能力:1.1 技术工具的理解深度面试官首先想确认的是&#x…

2026/7/22 3:58:37

高效工具链管理系统Hyde的设计与实践

1. 项目背景与核心价值"工具公告hyde"这个看似简单的标题背后,实际上隐藏着一个高效工具链管理系统的设计理念。作为从业十余年的全栈开发者,我见过太多团队在工具管理上踩坑——版本混乱、配置丢失、环境冲突这些问题几乎每天都在消耗开发者的…

2026/7/22 3:58:37

Windows快速配置WSL与Docker开发环境指南

1. Windows系统快速配置WSL与Docker开发环境作为长期在Windows平台进行开发的工程师,我深刻体会到原生Linux环境的重要性。传统虚拟机方案资源占用高、性能损耗大,而微软推出的WSL(Windows Subsystem for Linux)配合Docker容器技术…

2026/7/22 3:53:37

Claude Code离线安装包

本章教程整理了Claude Code离线安装包,支持Windows版本和Mac版本的。 下载地址:Claude Code离线安装包 一、软件简介 Claude Code 是一款强大的 AI 编程助手,深度集成代码编辑、智能补全、问题排查、代码重构、注释生成、项目解读等全流程开发…

2026/7/20 6:33:00

Unity与Python本地通信:基于Flask的跨语言数据交换实战

1. 项目概述:为什么我们需要一个本地通信服务器?在游戏开发、数字孪生、仿真训练等众多领域,Unity作为强大的实时3D内容创作平台,其核心逻辑通常由C#驱动。然而,当我们需要进行复杂的数据分析、机器学习推理、科学计算…

2026/7/22 0:02:17

抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具

抓包代理链路下的 TLS 指纹变化分析:为什么调试环境会影响访问结果 摘要 在网页调试、接口联调、自动化巡检和授权采集排查中,抓包是常见手段。但很多开发者会遇到一个现象:正常访问页面时没有问题,一进入抓包或代理调试环境&…

2026/7/22 0:02:17

微信QQ聊天记录误删恢复与备份方案全指南

1. 聊天记录误删的常见场景与恢复思路作为一名长期关注数据安全的技术博主,我处理过上百起聊天记录误删的求助案例。手机误操作、系统升级失败、设备损坏是三大常见诱因。上周就遇到用户更新微信时断电,导致近两年的工作群聊记录全部消失的极端案例。不同…

2026/7/22 0:02:17

2026最新8款个人AI编程免费工具深度实测

作为一名全栈独立开发者,我最近半年一直在折腾副业项目,每个月在AI编程工具上的订阅费算下来其实也不算便宜。作为个人开发者,我们追求的就是用最少的成本获得最高效的开发体验。TRAE 基础版免费,字节跳动出品的国内首款 AI 原生 …

2026/7/21 20:02:44

3个高效策略:快速掌握Axure中文界面配置

3个高效策略:快速掌握Axure中文界面配置 【免费下载链接】axure-cn Chinese language file for Axure RP. Axure RP 简体中文语言包。支持 Axure 11、10、9。不定期更新。 项目地址: https://gitcode.com/gh_mirrors/ax/axure-cn 还在为Axure RP的英文界面感…