简介面向 STM32 嵌入式开发者的 RS485-Modbus 通信控制工程基于 STM32 HAL 库实现 RS485 接口驱动与 Modbus RTU 协议并用于中菱轮毂电机 ZLAC8015D 的控制开发环境为 Clion。整个压缩包共 187 个文件、约 1015KB包含 .c/.h 源码、CMake 构建脚本、.ioc 工程配置以及 .bin/.elf/.hex 等可直接烧录的编译产物另有 .map/.ld 链接文件与编译中间文件便于核对工程结构和学习底层配置。项目完整展示了从串口参数设置、RS485 收发器驱动到 Modbus 请求报文构造与解析的实现过程还涉及错误处理和重试机制适合希望深入掌握 STM32 工业通信、Modbus 协议栈移植或 Clion 嵌入式工作流的开发者参考。目前已有 1614 人学习下载对于工业控制、电机驱动和嵌入式协议开发场景具有不错的借鉴价值。1. 一主多从的 RS485 采集场景这个 Modbus 从机工程能做什么如果你手头的设备需要和上位机对话现场已经铺好了两根线的 RS485 总线那最省事的协议就是 Modbus RTU。这个项目把一块 STM32F103 通过 SP3485 收发器挂到 RS485 总线上固件完整实现了 Modbus 从机协议——读保持寄存器、写单个寄存器、写多个寄存器都支持CRC 校验和异常响应也齐全。整套代码用 CLion 管理CMake 编译OpenOCD 烧录和调试不用在多个工具之间来回切。适合刚接触 STM32 通信的新手也适合做采集节点想快速上 Modbus 的工程师。2. 硬件链路与工程骨架RS485 方向切换为什么不能靠延时2.1 RS485 半双工的硬件接法SP3485 与 STM32 的信号映射RS485 和串口不一样它是半双工的差分总线同一时刻只能有一个节点往总线上发数据。STM32 的 UART 是单端信号所以要加一颗收发器把 TTL 电平转成差分电平。项目里用 SP3485它工作在 3.3V正好直接对接 STM32 的 IO不需要额外电平转换。接线关系不复杂我把连接关系列成一张表方便你对照着自己的板子检查SP3485 引脚功能接到 STM32备注RO接收输出PA10USART1_RX总线上的差分信号转换成 TTL 给 MCU 读DI发送数据输入PA9USART1_TXMCU 发送的数据从这里进收发器RE接收使能低有效与 DE 并接到 PA8拉低进入接收模式DE发送使能高有效与 RE 并接到 PA8拉高进入发送模式A差分信号正端接 USB 转 485 模块的 A接反会导致通信失败B差分信号负端接 USB 转 485 模块的 B接反会导致通信失败DE 和 RE 在 SP3485 内部是互补逻辑一个高有效一个低有效把它们并在一起后一个 GPIO 就能控制方向PA8 输出高电平就是发送输出低电平就是接收。这是个很关键的设计点后面避坑章节里出现的“连续请求超时”一半以上和这个引脚的切换时机有关。另外注意总线末端最好放一颗 120 欧姆终端电阻尤其是当现场线缆超过几十米或者波特率跑到 38400 以上的时候。没加终端电阻的表现很恶心——短距离测试一切正常拉长线后就偶发丢帧而且用示波器看不出明显异常只有 A-B 差分信号边沿变圆润了。2.2 一发一收的方向切换发送完成回调里拉低 DE方向切换最朴素的做法是发送前拉高 DE发送函数执行完再拉低。刚开始我确实这么干过代码长这样void rs485_send_frame_blocking(uint8_t *buf, uint16_t len) { HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_SET); HAL_UART_Transmit(huart1, buf, len, 100); HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, GPIO_PIN_RESET); }看起来没毛病但实际用 Modbus Poll 连续读写时第二次请求就超时。原因是 HAL_UART_Transmit 只要把数据交给发送移位寄存器就返回了最后一个字节可能还在移位寄存器里没真正从 TX 引脚发完可我已经把 DE 拉低收发器切回接收模式帧尾被截断。后来改成中断发送在发送完成回调里拉低方向引脚逻辑就顺了static volatile uint8_t tx_busy 0; void rs485_set_dir(uint8_t tx) { HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, tx ? GPIO_PIN_SET : GPIO_PIN_RESET); } void rs485_send_frame(uint8_t *buf, uint16_t len) { while (tx_busy) {} // 上一帧还没发完等 tx_busy 1; rs485_set_dir(1); // 切到发送 HAL_UART_Transmit_IT(huart1, buf, len); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { tx_busy 0; rs485_set_dir(0); // 发完立刻回到接收 } }这里 HAL_UART_Transmit_IT 是非阻塞发送函数调用后立刻返回真正发完时进入 TxCpltCallback。此时拉低 DE 才保证整帧完整地送到了总线上。tx_busy 是一个标志量为避免重入发送前要等它为 0简单场景下也可以不加但加了这个标志后续接轮询逻辑会省心很多。2.3 工程目录一棵树从 main 到 modbus 模块的调用边界这个项目虽然是 CubeMX 生成的标准结构但 modbus 协议部分单独抽了一套文件没有和 main.c 混在一起。目录结构大致如下STM32-RS485-Modbus/ ├── CMakeLists.txt ├── openocd.cfg ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── modbus.h │ │ └── crc16.h │ └── Src/ │ ├── main.c │ ├── modbus.c │ └── crc16.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ └── build/main.c 里只做三件事初始化时钟和 GPIO、初始化 UART 和 DMA 接收、在 while(1) 主循环里调用 modbus_poll()。真正的协议解析全部在 modbus.c 里CRC 计算在 crc16.c 里。这种分层的思路对开发很有用uart 接收到的原始字节是“物理层”的事情modbus_poll 的解析是“协议层”的事情registermap 又是一个独立话题。改业务逻辑时你基本不用碰 DMA 和中断改通信细节时也不用担心碰坏协议。我第一次写的时候图省事把 CRC 和功能码处理全塞进中断回调里后来查问题查到怀疑人生才意识到模块边界一开始就要划清楚。3. Modbus 核心实现从一帧数据进入到响应返回的完整链路3.1 帧格式与 CRC16先算对校验后面都是水到渠成Modbus RTU 的帧格式很紧凑地址 1 字节、功能码 1 字节、数据 N 字节、CRC16 低字节在前。比如上位机读从机保持寄存器的请求帧是01 03 00 00 00 02这 6 个字节最后必须拼上 CRC 才是完整的一帧。我习惯先用 CRC 计算器验证一组已知数据01 03 00 00 00 02的 CRC 是0x0BC4发送顺序是C4 0B这一条能帮你确认移植的代码没写反字节序。CRC16-Modbus 用的是多项式 0xA001初值 0xFFFF逐位法实现如下uint16_t crc16_modbus(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; // Modbus 规定初值为 0xFFFF while (len--) { crc ^ *data; // 异或到最低位 for (uint8_t i 0; i 8; i) { if (crc 0x0001) // LSB 为 1右移后异或多项式 crc (crc 1) ^ 0xA001; else crc 1; } } return crc; } uint8_t check_crc16(const uint8_t *frame, uint16_t len) { uint16_t crc crc16_modbus(frame, len - 2); // 对前面所有字节算 uint16_t recv (uint16_t)frame[len - 1] 8 | frame[len - 2]; // 低字节在前 return (crc recv); }注意这里的两个细节校验时不包含帧尾的 CRC 本身计算范围是 len - 2接收帧里的 CRC 是低字节在前所以组合 recv 时要让最后一个字节成为高 8 位。很多新手上手踩的第一个坑就是把 CRC 异或顺序搞反结果调出“时通时不通”的玄学问题。另一个容易被忽略的点收到 CRC 错误的帧时从机应当直接丢弃不回任何数据。Modbus 协议里只有 CRC 正确的帧才需要响应校验失败还硬要回帧会让对端解析卡死。3.2 帧边界判定用串口空闲中断代替 1.5/3.5 字符定时Modbus RTU 是帧之间的间隔来划分消息的协议里要求帧内字符间隔不超过 1.5 个字符时间帧与帧之间至少 3.5 个字符时间。常规做法是用一个定时器去量字符间隔但我更推荐用串口的空闲中断IDLE来判断一帧结束USART 检测到总线上一个字节之后持续空闲就触发空闲中断此时说明上位机已经把这一帧发完了。项目里用的是 DMA 接收加空闲中断代码核心就一段#define RX_BUFFER_SIZE 256 uint8_t rx_buf[RX_BUFFER_SIZE]; volatile uint16_t rx_len 0; volatile uint8_t frame_ready 0; void USART1_IRQHandler(void) { if (USART1-SR USART_SR_IDLE) // 总线进入空闲状态一帧结束 { USART1-SR ~USART_SR_IDLE; // 清除空闲标志 rx_len RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); frame_ready 1; // 置位交给主循环处理 } HAL_UART_IRQHandler(huart1); }逻辑说明DMA 一直在往 rx_buf 里搬数据当你发现串口空闲时DMA 计数器相比初始值减少的数量就是实际收到的字节数。用这种方式无论上位机发 6 字节还是 20 字节从机都能在整帧到达后立刻知道帧长度。参数上要留意 RX_BUFFER_SIZE 必须大于单条 Modbus 报文最大长度常见从机就 256 足够如果寄存器数量多导致单帧超过缓冲区长度DMA 会溢出丢数据那就得改大。另一种备选方案是逐字节中断加定时器超时但代码复杂度更高DMA 加空闲中断已经能覆盖绝大多数场景。帧准备好后要在主循环里尽快处理而不是在中断里处理这关系到第四节会细说的并发问题。主循环里只需做一件事if (frame_ready) modbus_poll();3.3 功能码分发与寄存器映射0x03、0x06、0x10 的完整分支modbus.c 的核心是一个分发函数。我把它写成了独立函数帧校验、地址匹配、功能码处理分成三层每层只关心一件事void modbus_poll(void) { uint8_t *req (uint8_t *)rx_buf; uint16_t req_len rx_len; if (req_len 4) // 最短帧地址 功能码 2 字节 CRC return; if (req[0] ! slave_addr) // 地址不匹配说明不是发给自己的直接丢 return; if (!check_crc16(req, req_len)) // CRC 不过丢弃 return; switch (req[1]) // 根据功能码分发 { case 0x03: handle_read_holding_registers(req); break; case 0x06: handle_write_single_register(req); break; case 0x10: handle_write_multiple_registers(req); break; default: build_exception(0x01); break; // 非法功能码 } }check_crc16 的逻辑前面讲过这里多说一句地址匹配从机地址是 0x01但上位机下发时第二字节是功能码所以地址匹配不要写成req[0] 0x01 req[1] 0x03这样把功能码也卷进来独立判断更容易维护。寄存器表我直接用一个数组承载数组下标对应 Modbus 寄存器地址偏移#define REG_HOLDING_COUNT 16 uint16_t holding_regs[REG_HOLDING_COUNT]; // 保持寄存器可读可写 void handle_read_holding_registers(uint8_t *req) { uint16_t start (req[2] 8) | req[3]; // 起始地址 uint16_t count (req[4] 8) | req[5]; // 寄存器数量 if (start count REG_HOLDING_COUNT) // 越界回异常码 0x02 { build_exception(0x02); // 非法数据地址 return; } uint8_t *rsp tx_buf; rsp[0] slave_addr; rsp[1] 0x03; rsp[2] count * 2; // 字节数每个寄存器占 2 字节 for (uint16_t i 0; i count; i) { rsp[3 i * 2] holding_regs[start i] 8; // 高字节在前 rsp[3 i * 2 1] holding_regs[start i] 0xFF; } uint16_t len 3 count * 2; uint16_t crc crc16_modbus(rsp, len); rsp[len] crc 0xFF; rsp[len 1] crc 8; rs485_send_frame(rsp, len 2); }这里有个 Modbus 的硬性规定多字节数据在帧里都是高字节在前所以写响应时要先放高 8 位再放低 8 位一个翻转都会让上位机读到完全不对的数据。写单个寄存器的处理思路类似只是数据字段变成 4、5 两个字节并且要对地址和值做校验寄存器地址越界回异常码 0x02写入值非法比如控制字只允许 0 或 0x0001回异常码 0x03也就是非法数据值。写多个寄存器 0x10 要多解析一个“字节数”字段并且要检查字节数是否等于寄存器数量乘以 2不等就说明这个帧结构本身有问题直接丢弃比回异常更安全。3.4 响应帧的拼接顺序从地址到 CRC 为什么不能乱响应帧的拼装顺序看起来简单但我见过不少人在 CRC 后面又补了多余字节或者把 CRC 的高低位放反。响应帧一定是地址、功能码、数据区、CRC 低字节、CRC 高字节。以 0x06 写单个寄存器的成功响应为例void handle_write_single_register(uint8_t *req) { uint16_t reg_addr (req[2] 8) | req[3]; uint16_t reg_val (req[4] 8) | req[5]; if (reg_addr REG_HOLDING_COUNT) { build_exception(0x02); // 地址越界 return; } holding_regs[reg_addr] reg_val; // 成功响应原样回显请求帧 for (int i 0; i 6; i) tx_buf[i] req[i]; uint16_t crc crc16_modbus(tx_buf, 6); tx_buf[6] crc 0xFF; tx_buf[7] crc 8; rs485_send_frame(tx_buf, 8); }0x06 的协议规定成功响应的数据区就是请求的完整回显所以直接把接收到的 6 字节拷到发送缓冲即可。这样写有两个好处不用再单独构造地址和数据区避免手误同时天然保证了响应和请求的一致性上位机拿着请求一比对就能确认写入成功。build_exception 的拼装逻辑也值得一提异常响应帧是地址、功能码或 0x80、异常码、CRC。比如功能码 0x03 的非法功能码响应就是01 83 01 C0 0B这样的结构。这里功能码最高位置 1上位机才能区分“正常响应”和“异常响应”。4. CLion 环境配置与烧录联调从 CMake 到 Modbus Poll 一条龙4.1 从 STM32CubeMX 生成 CMake 工程再交给 CLionCLion 本身不直接认识 .ioc 文件需要先用 STM32CubeMX 生成工程。新版的 CubeMX 在 Project Manager 里能把 Toolchain 选成 CMake直接生成带 CMakeLists.txt 的标准工程如果版本生成的还是 Makefile 工程CLion 也能导入本质都是调用交叉编译工具链。生成后的 CMakeLists.txt 有几个关键位置。打开后你会看到类似这样的片段add_compile_definitions(STM32F103xB USE_HAL_DRIVER) add_executable(STM32-RS485-Modbus Core/Src/main.c Core/Src/modbus.c Core/Src/crc16.c Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.c ... ) target_link_libraries(STM32-RS485-Modbus m)STM32F103xB 这个宏决定了芯片型号的外设寄存器定义必须和实际芯片对应USE_HAL_DRIVER 是 HAL 库的开关没有它整个 HAL 头文件都编不过。如果你的项目是 F103 的其他型号比如大容量的 ZET6这里是 STM32F103xE不要照抄。CLion 打开工程后先在 Settings 的 Toolchain 里选到 arm-none-eabi-gcc。Windows 下我直接装了 STM32CubeCLT它把编译器和 OpenOCD 一起打包了省去手工配环境变量的时间。工具链配好后CLion 左上角的 build 图标亮起来就能正常编译了。第一次编会拉一堆 HAL 源文件进来可能有一两个告警不影响生成 elf。4.2 OpenOCD 烧录与 GDB 调试一条命令跑通烧录这件事在 CLion 里可以配置成 Run Configuration 自动执行但为了排查问题我经常直接在终端里手动敲 OpenOCD 命令openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c program build/STM32-RS485-Modbus.elf verify reset exit参数说明interface/stlink.cfg 告诉 OpenOCD 调试器是 ST-Linktarget/stm32f1x.cfg 指定目标芯片是 STM32F1 系列program 后面的 elf 文件路径要按实际 build 目录调整verify 在烧录后自动校验固件是否写对reset 让芯片复位运行exit 执行完退出 OpenOCD。如果这块报错先别急着改代码check 两处SWDIO 和 SWCLK 两根线是不是接对了ST-Link 的 3.3V 和 GND 有没有和开发板连一起。经常有新手只接了两根信号线没接地调试器根本枚举不到目标芯片。想在 CLion 里打断点调试就新建一个 Embedded GDB Server 类型的 Run ConfigurationDebugger 选 arm-none-eabi-gdbGDB Server 选 openocd 的可执行文件其余参数和命令行一致。设好之后点调试按钮就能直接看 modbus_poll 里每个变量的值比加串口打印高效太多。这也是我推荐用 CLion 做嵌入式开发的核心理由——代码、编译、调试都在一个窗口里。4.3 上位机联调Modbus Poll 的串口参数和寄存器地址怎么填固件烧进去以后第一件事不是写应用而是用 Modbus Poll 验证从机行为。Modbus Poll 是 Windows 上调试 Modbus 从机最常用的上位机免费版够用配置项不多配置项推荐值说明Serial Port实际 USB 转 485 的 COM 口设备管理器里看别选错Baudrate9600与固件里的波特率一致Data Bits8Modbus RTU 固定 8 位数据位ParityEven很多现场使用 8E1也可以按需改 NoneStop Bits1同上Slave ID1对应固件里 slave_addrFunction03先读保持寄存器验证基本通路Address0起始寄存器地址Quantity8一次读 8 个寄存器连接后点 Read 按钮如果看到寄存器值稳定显示说明物理链路、协议栈、CRC 校验整个通路是通的。先别急着测写功能把读功能跑稳再去点 Write 写单个寄存器验证 0x06 分支最后测 0x10 写多个寄存器。调试的第一步能稳定读后面的问题大多能在软件层定位不用再怀疑硬件接错。5. 避坑指南调试 Modbus 从机时反复踩过的四个问题5.1 问题一能收到回帧但上位机超时先查 A/B 和共地现象逻辑分析仪挂在 RS485 总线上能看到从机有响应波形电平跳变也正常但 Modbus Poll 一直显示超时。原因最常见的是 A/B 接反了。SP3485 的 A 端对应 USB 转 485 模块的 A有些廉价转换线的丝印不标准接反后从机发出的差分信号极性颠倒上位机的收发器把逻辑 1 读成了逻辑 0。解决用万用表量空闲状态下 A 对 B 的电压。正常情况下 A 比 B 高 200mV 以上如果量出来是负值把 A/B 两根线对调。还有一次是开发板和转换器没共地RS485 是差分传输理论上抗共模干扰强但收发器的输入范围有限地电位差太大照样解不出数据。把两边 GND 接一根线问题立刻消失。5.2 问题二上电第一次能通、连续操作超时方向引脚拉低太早现象每次给从机上电后第一次写保持寄存器成功第二次开始所有请求全部超时重新上电又只成功一次。原因前面在 2.2 里举过具体例子——用阻塞式 HAL_UART_Transmit函数返回时最后一个字节还在发送移位寄存器里DE 被提前拉低帧尾的 CRC 没有完整发出去。第一次成功是因为时序刚好处在边界第二次数据稍有抖动就触发丢帧。解决把发送切成中断方式在 HAL_UART_TxCpltCallback 里拉低 DE。如果暂时不想改代码想快速验证这个问题在阻塞发送后加一句等待 TC 标志HAL_UART_Transmit(huart1, buf, len, 100); while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET); rs485_set_dir(0);TC 和 TXE 的区别就在这TXE 表示数据进入了移位寄存器TC 表示移位寄存器发完了整个字节。很多人只知道等 TXE忽略了 TC这是一个非常典型的嵌入式串口翻车点。5.3 问题三帧校验通过却不进功能码分支地址和帧间隔都别忽略现象CRC 校验已经 OK但是程序始终不走 switch 里的 0x03 分支。用串口抓包工具看请求帧就是最标准的01 03 00 00 00 02 C4 0B。原因第一层地址匹配没过。检查代码里 slave_addr 的初始化如果从机地址还是默认的 0x02而上位机发的是 0x01那这条帧会在校验之前就被丢弃。另一个隐蔽原因是帧被拆成两段如果串口接收用的是逐字节定时器而不是空闲中断当两个字节间隔超过 1.5 字符时间从机会误认为是两帧导致 rx_len 只包含半截数据CRC 校验自然过不了。解决先把 slave_addr 打印出来确认再把上位机发送间隔调大一般上位机轮询间隔不会太小但手动发帧测试时容易连发导致帧粘连。用空闲中断加 DMA 接收的方案能有效避免这种拆帧误判。5.4 问题四在串口中断里解析帧导致程序跑飞接收和处理必须分离现象从机单次查询没问题一但上位机用 100ms 周期连续轮询偶尔出现程序跳到 HardFault或者寄存器数据随机变错。原因把 modbus_poll 直接放在 USART1_IRQHandler 里调用CRC 计算和寄存器写操作在中断上下文执行时间长于两个字节的接收间隔新的 DMA 数据又覆盖了正在解析的缓冲区产生数据竞争。更严重的是如果解析函数里调用了 HAL_UART_Transmit 阻塞等待中断嵌套会进一步恶化。解决中断里只做两件事——保存 DMA 剩余计数置位 frame_ready 标志。modbus_poll 放到主循环 while(1) 里调用。这样接收中断的占用时间只有几个时钟周期解析逻辑再长也不会影响下次接收。从那以后我写任何串口协议栈都强制要求“中断只置标志处理全在主循环”再没遇到过这种跑飞问题。6. 二次开发把从机改造成你的采集节点6.1 从寄存器表改造入手把业务数据映射到 Modbus 地址项目里 holding_regs 是一个纯数组实际产品里寄存器应该和业务数据一一对应。我通常会在 modbus.h 里写一个寄存器映射表让业务代码和协议代码各取所需Modbus 地址变量名含义读写属性0x0000sensor_humidity湿度值只读0x0001sensor_temperature温度值只读0x0002control_word控制字读写0x0003status_word状态字只读实现上不用搞复杂结构体直接定义宏并让业务代码写数组对应下标即可#define REG_HUMIDITY 0 #define REG_TEMPERATURE 1 #define REG_CONTROL_WORD 2 #define REG_STATUS_WORD 3 // 业务层更新传感器值 holding_regs[REG_TEMPERATURE] read_temp(); holding_regs[REG_HUMIDITY] read_humidity();这样协议栈不用关心里面的数据来自 ADC 还是 I2C 传感器反正它只负责把数组里的值按 Modbus 帧格式发出去。业务代码加新变量时只需要在数组里预留下标并更新映射宏。6.2 新增功能码与多从机地址顺手把可维护性也做了要支持 0x05 写线圈或者 0x04 读输入寄存器只需在 modbus_poll 的 switch 里加一个 case再写对应的 handler。注意 Modbus 每个功能码的数据组织方式不一样比如 0x05 写线圈的值规定只有 0xFF00 和 0x0000 两种写进协议栈时按协议手册逐字段核对别想当然。另一个容易忽略的改动是寄存器数组要加越界保护异常响应码不要用错地址越界是 0x02值不合法是 0x03。6.3 多从机与拨码地址一组拨码开关带来的现场可维护性如果现场要挂多台从机地址不能写死。常见做法是把 4 位拨码开关接到 GPIO上电时读取成从机地址uint8_t slave_addr 0; for (uint8_t i 0; i 4; i) slave_addr | (HAL_GPIO_ReadPin(GPIOB, 0x01 i) GPIO_PIN_SET) ? (1 i) : 0; if (slave_addr 0) slave_addr 1; // 全 0 时给个默认地址地址 0x00 在 Modbus 里是广播地址现场设备不要占用。多从机挂在同一条 RS485 总线上时每个从机地址必须唯一否则两个从机同时响应同一帧请求总线冲突会把数据搅得一团糟。6.4 改完怎么验证手工组帧到双节点联调每加一个功能码我习惯先用串口调试助手手工发一帧已知数据验证比如读寄存器就发01 03 00 00 00 02 C4 0B看回包字节是不是和预期一致再换 Modbus Poll 循环读取观察长时间运行有无丢帧。多从机场景下至少要挂两个节点反复轮询确认地址互不干扰。这套流程看着原始但定位问题非常快能直接区分是协议栈的错还是业务层的错。从那以后我每换一个项目都会先把寄存器表和地址定义写成一页注释放在 modbus.c 文件头再开始写业务代码二次开发基本不会再从头翻第二遍。希望帮到你。本文还有配套的精品资源点击获取