简介:这是一套面向 Arduino 开发者的 Modbus 主站通信库,代码基于 C++ 编写,适用于通过 RS232/485 接口与 RTU 协议驱动 Modbus 从站的工业控制、数据采集与传感器交互等场景。它涵盖线圈读写、离散输入读取、保持/输入寄存器访问、屏蔽写及读写多寄存器等完整的 Modbus 功能,支持全双工/半双工收发器,并提供回调函数灵活切换数据使能(DE)与接收使能(/RE)引脚,安装过程兼容 Arduino IDE 库管理器,整体上手门槛较低。压缩包共 30 个文件、约 266KB,以 md 说明文档、h/cpp 库源码、ino 示例程序、pdf 参考手册为主,附带 Makefile、keywords.txt、library.properties 等工程配套文件;目录中还包含 doc 文档、CHANGELOG 更新记录与 CONTRIBUTING 贡献规范,结构与命名清晰,便于定位与二次开发。目前已有 1481 人学习下载。借助包内 README、示例工程与 Doxygen 生成文档,可快速理解寄存器读写流程与函数调用关系,参考 RS485 半双工、PhoenixContact 等示例适配实际硬件,适合为单片机扩展专业工业通信能力。
1. 让 Arduino 当 Modbus 主站:这块板子到底能干什么
调试台上放着一台只吐 Modbus RTU 协议的温控表,上位机软件还没影,现场催着要数据,你手边只剩一块 Arduino UNO。很多人第一反应是打开串口自己解析帧、手算 CRC16,写两百行代码还调不通。其实用 ModbusMaster 这个库,几行代码就能让 Arduino 成为 Modbus 主站,按你的节奏去读从站设备的寄存器,读写线圈和保持寄存器都不在话下。这篇笔记围绕这个方向拆开讲:先说清楚主站是什么、为什么值得用库,再给出能直接跑通的最小工程,最后把 RS485 通信里那些让人挠头的坑逐个捋顺。适合正在做小型数据采集、实验室台架、设备联动控制的人,新手能跟着步骤走,老手可以直接跳到避坑和轮询章节。
2. 主站原理与选型:协议不复杂,难在别重复造轮子
2.1 Modbus RTU 主从模型与一帧数据的解剖
先把这个标题拆开。“ModbusMaster”是一个 Arduino 库的名字,它把 Arduino 的角色定义成 Modbus 主站(Master)。Modbus 是工业现场用得最广的串行通信协议之一,采用主从模型:总线上只有一个主站,负责发起所有请求;从站是被动的,只能等主站来问,问一句答一句。你的 Arduino 一旦跑起 ModbusMaster,就成了总线上唯一提问的人,那些仪表、变频器、温控器就是回答问题的从站。
为什么强调“唯一”?因为总线上两台设备不能同时开口。RS485 是半双工物理层,同一时刻只有一方能占用线路。主站发请求时所有从站都能听到,但只有地址匹配的那个从站会回应,其他从站保持静默。这个一问一答的模式看起来简单,实际工程里也够用,大量工业设备跑的就是这套逻辑。
一帧 Modbus RTU 报文的结构很固定:从站地址、功能码、数据部分、CRC16 校验。比如主站要读 1 号从站从起始地址 0x0000 开始的 8 个保持寄存器,发出的报文大致是:0x01(从站地址)、0x03(功能码:读保持寄存器)、0x00 0x00(起始地址高字节)、0x00 0x00(起始地址低字节)、0x00 0x08(寄存器数量)、CRC 低字节、CRC 高字节。从站收到后返回的数据区里除了功能码,还带一个字节计数字段,后面跟每个寄存器的两个字节。
这里有两个容易忽略的细节。第一,Modbus RTU 的多字节数据都是高字节在前,但 CRC 例外,CRC 在帧里是低字节在前。很多第一次接触的人把寄存器字节顺序搞反,读出来的数值翻车,后面我会专门讲。第二,帧与帧之间必须保证至少 3.5 个字符时间的静默间隔,否则从站会把相邻两帧当成一帧处理。Arduino 串口在低波特率下跑循环轮询,如果不注意请求之间的间距,通信稳定性会很差。
2.2 为什么用 ModbusMaster 而不自己写协议栈
我在早期项目里犯过轴,觉得 Modbus 协议这么简单,自己写个解析器也就两三百行。结果 CRC16 算完要么对不上,要么超时机制忘了写,从站不回应的时候程序就傻在那里。其实自己写不是不行,但没有必要,ModbusMaster 这个库把三件最繁琐的事都封装好了。
第一件是 CRC16 计算和校验。发送前算一遍挂在帧尾,收到响应后再算一遍验一下,这个逻辑在每个请求里都要重复。库内部把这件事藏得干干净净,你只需要管业务数据。第二件是响应超时和错误码。从站不在线、从站报异常,库会返回对应的错码,比如响应超时、非法功能码、CRC 校验失败,拿到错码就能定位是物理链路问题还是协议问题。如果自己写,这一块最容易漏,漏掉的结果就是程序阻塞在等待串口数据上。
第三件是多个从站实例的轮询支持。你可以声明好几个 ModbusMaster 对象,分别绑定不同的从站地址,在 loop 里轮流读。要是自己写,状态机要自己维护,请求节奏要自己控制,出问题很难排查。库还提供了收发前的回调钩子,可以在发送前把 RS485 的方向引脚拉高,发完再拉低,这几乎是为 RS485 半双工接口量身定做的。
不过我并不建议无脑通读库源码,重点看两处就够了:一处是 begin 函数的签名,确认你的库版本怎么绑定串口;另一处是错误码常量的定义,方便在 Serial.println 里把失败原因打印出来。剩下的按示例代码抄,先跑通再深入。
2.3 RS485 硬件选型与连接:MAX485 模块怎么挑
Modbus RTU 最常用的物理层是 RS485。Arduino 板载串口输出的是 TTL 电平,工业设备的总线是 RS485 差分信号,两者之间必须加一个电平转换模块。市面上最常见的模块是 MAX485 芯片方案,一块小板几块钱,引出 VCC、GND、A、B、RO、DI、RE、DE 八个引脚,有的模块还把 RE 和 DE 合并成一个方向控制脚,用起来更省事。
选模块时有几个点要看清楚。第一,工作电压。3.3V 的 Arduino 板子要选支持 3.3V 的模块,5V 的 UNO 就选 5V 版本,别把 5V 怼到 3.3V 模块上。第二,是否有隔离。普通模块和带隔离的模块价格差好几倍,短距离室内测试用普通模块完全够;如果现场有强干扰、地电位差大,就要考虑隔离型,否则通信玄学频出。第三,看 DE/RE 引脚。有些模块把 DE 和 RE 分开引出,你需要分别接两个数字口,或者直接短接后共用一个数字口。我一般习惯把它们接在一起,一个引脚控制方向就够了,代码也简单。
接线方面,模块的 RO 接 Arduino 的 RX 引脚,DI 接 Arduino 的 TX 引脚,VCC 和 GND 对应接好。DE/RE 合并后接任意一个空闲数字引脚,比如 D2。总线侧的 A、B 两根线分别接从站设备的 A、B,注意从站那边可能标的是 485+ 和 485-,功能对应上去就行。这里有个很容易踩的坑:A 和 B 接反是最常见的通信失败原因,信号是差分对,反了逻辑直接反掉,后面怎么调都调不通,只能乖乖翻线。
长距离传输时还要考虑终端电阻。工程上的经验是总线两端各接一个 120 欧姆电阻,匹配特征阻抗,减少信号反射。室内短距离(几米内)不接也能工作,但如果出现数据时好时坏、丢帧严重,第一件事就是检查终端电阻和 A/B 线有没有接对。
3. 跑通第一个主站程序:读取保持寄存器的最小工程
3.1 安装库:库管理器与手动导入两条路
在 Arduino IDE 里装 ModbusMaster 有两条常见路径。一条是用库管理器:在 Library Manager 里搜索 ModbusMaster,找到后点安装。这个方式最省心,会自动安装到正确目录,版本也不用你管。另一条是从库的官方发布页下载 zip 包,然后通过 Sketch -> Include Library -> Add .ZIP Library 导入。要注意下载的 zip 解压后文件夹名必须是 ModbusMaster,不能带着版本号后缀,否则 Arduino IDE 识别不了,编译直接报错找不到头文件。
装完以后验证一下:打开 File -> Examples,如果库安装成功,会看到 ModbusMaster 的示例菜单。里面有读保持寄存器、写寄存器、多从站等现成例子,我的建议是先打开一个轮询示例编译试试,确认 IDE 环境和库版本能协同工作,再开始写自己的代码。库版本差异要特别注意,2.0 之后的 begin 接口通常是node.begin(slaveId, Serial),而早期版本可能只需要node.begin(slaveId)并且把串口对象单独赋值。如果拿到一份老教程代码编译报错,优先看库自带示例是怎么写的,以你装的那个版本为准。
3.2 接线表:Arduino、MAX485 与从站的 5 根线
下面这个接线表适用于 Arduino UNO + MAX485 普通模块,从站是一个支持 Modbus RTU 的仪表。电源统一用 5V 或外部直流电源,地线必须全部连在一起。
| Arduino UNO | MAX485 模块 | 说明 |
|---|---|---|
| 5V | VCC | 给模块供电 |
| GND | GND | 共地,别省 |
| D0 (RX) | RO | 模块接收 TTL 数据 |
| D1 (TX) | DI | 模块发送 TTL 数据 |
| D2 | DE + RE (或合并脚) | 方向控制,默认拉低 |
MAX485 模块的 A 和 B 两个端子接从站设备的 485 总线。需要提醒的是,有的仪表端子上写的是 A/B,有的写的是 485+/485- 或者 D+/D-,本质是同一对差分线。A 对应同相端,B 对应反相端,接反的后果前面说过,逻辑反相会让你束手无策。接好之后用万用表量一下 A-B 之间的电压,空闲时应该在 -1.5V 到 -6V 之间,如果量出来是正的,说明 A/B 反了。
3.3 最小读寄存器代码与逐行说明
先贴一个可以直接烧录的最小程序,功能是每秒钟读一次 1 号从站的 8 个保持寄存器,起始地址 0x0000,并把结果打到串口监视器上。
#include <ModbusMaster.h> // 引入 ModbusMaster 库 #define MAX485_DE_RE 2 // DE/RE 方向控制引脚 ModbusMaster node; // 创建主站对象,对应一个从站地址 // 发送前回调:把 RS485 切到发送模式 void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); } // 发送后回调:切回接收模式 void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); } void setup() { Serial.begin(9600); // Modbus 使用硬件串口,波特率需与从站一致 pinMode(MAX485_DE_RE, OUTPUT); digitalWrite(MAX485_DE_RE, LOW); // 初始状态为接收 node.begin(1, Serial); // 从站地址 1,绑定硬件串口 node.preTransmission(preTransmission); node.postTransmission(postTransmission); } void loop() { uint8_t result; uint16_t regs[8]; result = node.readHoldingRegisters(0x0000, 8); // 起始地址 0,读 8 个寄存器 if (result == node.ku8MBSuccess) { for (uint8_t i = 0; i < 8; i++) { regs[i] = node.getResponseBuffer(i); Serial.print("Reg["); Serial.print(i); Serial.print("] = "); Serial.println(regs[i]); } } else { Serial.print("Read failed, code=0x"); Serial.println(result, HEX); } delay(1000); }这里有几个参数值得展开。node.begin(1, Serial)里的 1 是从站地址,必须是 1 到 247 之间的整数,0 是广播地址,一般不用。第二个参数是要使用的 Stream 对象,这里是硬件串口,也可以换成 SoftwareSerial 模拟串口,但不推荐,模拟串口在中断频繁时容易丢字节。readHoldingRegisters(0x0000, 8)的第一个参数是寄存器起始地址,第二个参数是数量,上限由协议限制,通常一次最多 125 个寄存器,超过这个数设备会报异常。
getResponseBuffer(i)是读取从站响应数据的核心接口,响应里的每个寄存器按顺序放进缓冲区,通过下标取出来。注意返回值是 uint16_t,本身就是无符号 16 位值,如果你的数据实际是有符号数,需要自己强转成 int16_t。还有,函数返回的result只有等于ku8MBSuccess(值为 0)才代表成功,其他非零值都是错误码,打印出来就能对照排查。
有一点要特别提醒:上面代码里用的是同一个硬件串口,也就是说串口监视器和 Modbus 共用 Serial。你没接上位机调试器时没问题,但如果开了串口监视器,监视器会占住这个串口,和 Modbus 通信抢资源。生产环境里正确的做法是用第二个串口做调试输出,或者干脆把调试信息写在日志里。UNO 只有一个硬件串口,想同时调试就得换 Mega 或用 SoftwareSerial。
3.4 preTransmission 和 postTransmission:半双工的方向闸门
初次接触 RS485 的人最不理解的就是这两个回调。MAX485 模块上 DE 引脚控制驱动器使能,RE 引脚控制接收器使能,两者通常反相关。当 DE/RE 为高时,模块把 DI 上的 TTL 信号差分输出到 A/B 总线上;当 DE/RE 为低时,模块把总线上的差分信号转换成 TTL 电平送到 RO。因为同一时刻不能同时收发,所以每次发送请求前必须把方向切到发送,发完立刻切回接收,等待从站应答。
这两个回调函数的作用,就是让库在发出数据帧之前和之后自动调用你写的方向切换逻辑。如果没有它们,你会遇到一个非常经典的故障:能发出请求但永远收不到响应,因为模块一直停在发送状态,或者一直停在接收状态导致数据根本没发出去。有的模块把 DE/RE 内部已经拉死,做成自动方向切换,那就不需要这两个回调;但大多数廉价普通模块还是需要手动控制。
写回调时注意动作要快,里面不要做耗时操作,什么 Serial.println、delay 都不要放,只做 digitalWrite 最好。有些项目把方向切换放到 begin 之后直接调用一次,希望在发送时是高的、接收时是低的,但那是错的,库每次收发都会调用回调,方向状态应该完全交给回调维护,不要在 main loop 里手动改 DE 引脚。
4. 功能码扩展:从读寄存器到写线圈和批量写
4.1 功能码速查表:你要读什么就找哪个码
Modbus 协议把“读什么、写什么”用功能码区分开。一开始记不住很正常,先抓主干:线圈是 1 位的开关量,寄存器是 16 位的整型量;输入表示只读,保持表示可读可写。这样四个组合就对应四个基本功能码:读线圈 0x01、读离散输入 0x02、读保持寄存器 0x03、读输入寄存器 0x04。写操作相对集中:写单个线圈 0x05、写单个寄存器 0x06、写多个线圈 0x0F、写多个寄存器 0x10。
| 功能码 | 名称 | 数据粒度 | 读写属性 |
|---|---|---|---|
| 0x01 | 读线圈 | 1 位 | 可读可写 |
| 0x02 | 读离散输入 | 1 位 | 只读 |
| 0x03 | 读保持寄存器 | 16 位 | 可读可写 |
| 0x04 | 读输入寄存器 | 16 位 | 只读 |
| 0x05 | 写单个线圈 | 1 位 | 写 |
| 0x06 | 写单个寄存器 | 16 位 | 写 |
| 0x0F | 写多个线圈 | 1 位 x N | 写 |
| 0x10 | 写多个寄存器 | 16 位 x N | 写 |
ModbusMaster 库里每个功能码对应一个方法,命名和功能码一致:readCoils、readDiscreteInputs、readHoldingRegisters、readInputRegisters、writeSingleCoil、writeSingleRegister、writeMultipleCoils、writeMultipleRegisters。实际项目里最常用的是 0x03 和 0x06,因为仪表类设备的核心数据大多暴露在保持寄存器里。先把这个表收藏下来,开发时查一眼就不会把方法用错。
4.2 读输入寄存器和离散输入:只读量的两条路
如果从站设备里有些量是只读的,比如测量值、状态位,通常会映射到输入寄存器(04)或离散输入(02)。读取方式跟读保持寄存器几乎一模一样,区别只是换了个方法名。下面的代码展示了同时读输入寄存器和离散输入的写法。
uint8_t result; // 读 1 号从站的输入寄存器,起始地址 0x0000,读 4 个 result = node.readInputRegisters(0x0000, 4); if (result == node.ku8MBSuccess) { for (uint8_t i = 0; i < 4; i++) { Serial.println(node.getResponseBuffer(i)); } } // 读 1 号从站的离散输入,起始地址 0x0000,读 16 个 bit result = node.readDiscreteInputs(0x0000, 16); if (result == node.ku8MBSuccess) { // 每个离散输入占用 response buffer 里的一个 uint16_t,值为 0 或 1 for (uint8_t i = 0; i < 16; i++) { Serial.print("DI "); Serial.print(i); Serial.print(" = "); Serial.println(node.getResponseBuffer(i)); } }注意一个细节:离散输入虽然只有一个位,但 getResponseBuffer 取出的是一个 uint16_t,取值只有 0 或 1。不要尝试用位操作从某个 uint16_t 里抠 bit,库已经帮你把每一个离散输入翻译成独立的值了,直接判断就行。如果你习惯从设备手册里读地址表,记得确认起始地址是 0 还是 1,有的手册从 1 开始编号,但 Modbus 报文里用的是从 0 开始的偏移地址,写错一个数,读出来的数据会整体错位。
还有一类情况是寄存器地址跨区,比如设备把温度映射到保持寄存器 0x0000,把压力映射到 0x0001,中间不留空。这种连续区段可以一次读多个寄存器减少请求次数,但如果地址中间有大段空洞,分包读反而更稳,避免一次请求长度超过从站允许的最大寄存器数。
4.3 写单个线圈和写单个寄存器:控制类指令怎么发
控制类操作比读操作更讲究“确认”,因为写错了会直接影响设备动作。写单个线圈的典型场景是设备启停,写单个寄存器的典型场景是修改设定值。ModbusMaster 提供了两个对应方法,调用方式和返回判断是一致的。
uint8_t result; // 写 1 号从站的线圈 0x0000,置为 ON(0xFF00 表示 ON,0x0000 表示 OFF) result = node.writeSingleCoil(0x0000, 0xFF00); if (result == node.ku8MBSuccess) { Serial.println("Coil write OK"); } else { Serial.print("Coil write failed: 0x"); Serial.println(result, HEX); } // 写 1 号从站的保持寄存器 0x0100,设定值为 2500 result = node.writeSingleRegister(0x0100, 2500); if (result == node.ku8MBSuccess) { Serial.println("Register write OK"); }写线圈有一个协议层面的细节:线圈的“开”不是传 1 而是传 0xFF00,“关”传 0x0000,中间值比如 0x0001 属于非法值,从站可能报异常。库的 writeSingleCoil 方法里通常直接把这个转换做了,但如果你自己拼帧,一定要写 0xFF00。写寄存器则简单得多,直接传 uint16_t 数值,库内部按高字节在前组帧。
这里要提醒的是,写操作之后最好立刻读回来确认。工业现场写寄存器容易受总线干扰,偶发失败的概率不是零。比较稳的做法是写完调一次读保持寄存器,把刚写的地址读回来比对,不一致就重写一次。这个“写后读回验”的习惯能帮你挡掉不少莫名其妙的控制失效问题。
4.4 写多个寄存器的边界条件
有些设备一次要下发一整组参数,比如 PID 的三组系数,一条条写太慢还容易中途被打断,Modbus 提供了写多个寄存器功能(0x10),一次最多可以写 123 个寄存器。ModbusMaster 的 writeMultipleRegisters 方法需要你准备一个数组。
uint16_t pidParams[3] = {100, 20, 5}; // P、I、D 三组设定值 uint8_t result; result = node.writeMultipleRegisters(0x0200, 3, pidParams); if (result == node.ku8MBSuccess) { Serial.println("Write multiple registers OK"); }三个参数分别是起始地址、寄存器数量、数组指针。注意数组的元素类型必须是 uint16_t,而不是 int,如果数值可能为负数,先确认设备的寄存器是否支持补码表示。另外一个常被忽略的坑是数组长度和寄存器数量不一致:库不会帮你检查越界,它按 count 逐个取数组元素,如果 count 写大了,数组越界会把内存里的随机值发出去,设备侧的行为就不可控了。写多寄存器之前,务必确认 count 和数组长度严格相等。
写完多个寄存器之后,可以再用读保持寄存器把所有值读回来整体校验。批量写看起来省事,但一旦出错影响面也大,所以校验环节别省。
5. 避坑手记:通信失败与数据异常的 5 个高频原因
5.1 现象:每次请求都超时
最常见的问题就是 readHoldingRegisters 返回响应超时错误码。原因排查要从物理层开始:首先量 A/B 电压,空闲时应该在 -1.5V 到 -6V 之间,如果没有电压,检查模块供电和总线连接;然后看 A/B 是否接反,接反会导致请求发出去从站收到的是错误信号,自然不回应;接着看 DE/RE 方向控制,如果发送后没有切回接收模式,从站响应回来了但模块没有把它送到 RO 引脚,程序一样超时;最后看波特率和从站配置是否一致,从站设的是 19200,你这边用 9600 发,从站根本解不出有效帧。按照这个顺序过一遍,多数超时问题都能定位。
注意:排查超时问题时,顺序永远是物理层、方向控制、波特率、软件,不要一上来就改代码。
5.2 现象:数据能读但全是乱码
能读到数据说明通信链路是通的,乱码的来源基本是字节序和数据类型不匹配。Modbus 寄存器是 16 位,高字节在前,但很多设备手册喜欢用字节序来描述内部存储。比如一个 32 位的浮点数占用两个寄存器,哪一个是低 16 位、哪一个是高 16 位,不同设备厂商的约定不同。读出来两个 uint16_t 后,你需要自己拼成 uint32_t 再解析成 float,顺序错了数值就完全不对。另一个常见因素是寄存器数量读多了,比如设备实际只有 2 个有效寄存器,你一次读 4 个,多出来的位置可能是空数据或垃圾位,看起来就像乱码。遇到乱码先检查读的地址区间和数量是否与设备手册完全一致,再确认数据的组合方式。
5.3 现象:读操作正常,写操作没反应
读正常说明物理层和地址匹配都没问题,写没反应一般要从功能码和设备权限两个方向查。先确认你用的功能码是设备支持的:有些设备只有读寄存器功能,根本不实现写操作的代码路径,这时发写请求会收到异常响应,或者干脆没有响应。然后确认寄存器地址是不是可写的,很多设备的保持寄存器是只读映射,写请求会返回非法数据地址异常。还有一类情况是设备带写保护,需要先解锁或写特定的“授权寄存器”才能改参数,这在商业仪表里很常见,手册里通常会写明。最后检查一下写入值是否合法,比如超出量程、超过上下限,设备会拒绝写入并返回异常码,代码里把错误码打出来就能看到。
5.4 现象:多从站时数据串台
总线上挂了好几个从站,读 A 的时候偶尔返回 B 的数据,多半是地址冲突和帧间隔不够两个原因叠加。先确认每个从站的地址拨码没有重复,重复地址会造成两个设备同时响应,总线上信号冲突,读回来的是噪声或错数据。然后把发送请求之间的间隔加大,Modbus RTU 要求帧间隔至少 3.5 个字符时间,在 9600 波特率下大约是 4ms,如果代码里连续调用不同 node 的读方法中间没有停顿,从站可能把上一帧的尾部当成下一帧的开头,导致应答错乱。我一般会在每两个请求之间加 10ms 左右的延时,宁可慢一点也不要让总线出现时序混乱。
5.5 现象:短距离正常,线一长就丢帧
室内两米测试一切正常,拉到车间几十米就随机丢帧,这是 RS485 最常见的物理层问题。原因不外乎三个:一是总线两端没有终端电阻,信号在末端反射产生振铃;二是共地没做好,不同设备地电位差导致共模电压超标,影响收发器稳定性;三是线缆质量差,用了普通跳线而不是双绞屏蔽线。双绞线能有效抑制共模干扰,屏蔽层单端接地。这三个问题按优先级处理:先加终端电阻,把两端 120 欧姆焊上;再检查所有设备是否共地,尤其注意隔离型设备的地如何处理;最后换线,把普通杜邦线换成带屏蔽的双绞线。这一套操作下来,长距离丢帧基本能解决。
6. 进阶:多从站轮询与超时重试的工程化写法
6.1 多从站轮询骨架与失败重试
单从站跑通之后,真实项目里往往是一台 Arduino 带多个从站。ModbusMaster 支持声明多个实例,但工程上更推荐用结构体把每个从站的地址、起始寄存器、数据缓冲登记成一张表,循环统一调度。下面是一个两从站的最小骨架。
typedef struct { ModbusMaster* node; // 对应的主站对象 uint16_t startAddr; // 起始寄存器 uint8_t regCount; // 寄存器数量 uint16_t data[16]; // 读回的数据 uint8_t failCount; // 连续失败计数 } Channel; Channel ch[2] = { { &node1, 0x0000, 8, {0}, 0 }, { &node2, 0x0000, 4, {0}, 0 } }; void loop() { for (uint8_t i = 0; i < 2; i++) { if (ch[i].failCount >= 5) continue; // 连续失败 5 次,暂时跳过 uint8_t result = ch[i].node->readHoldingRegisters( ch[i].startAddr, ch[i].regCount); if (result == ch[i].node->ku8MBSuccess) { ch[i].failCount = 0; for (uint8_t j = 0; j < ch[i].regCount; j++) { ch[i].data[j] = ch[i].node->getResponseBuffer(j); } } else { ch[i].failCount++; } delay(10); // 帧间隔,保证 3.5 字符时间以上 } delay(1000); // 轮询周期 }这个骨架的关键在于失败计数控制跳过逻辑:某个从站断电时,不会拖垮整个轮询循环;恢复后 failCount 归零又能自动重新纳入采集。轮询周期的设定要先实测每个从站的响应时长,一个从站约 10ms 到 50ms,20 个从站一轮可能就要 600ms,硬把周期压到 300ms 只会让请求排队冲突。先用实际测出的响应时间定轮询周期,再决定要不要提高波特率。
6.2 调试技巧:串口监听是最快的定位手段
遇到通信问题先别急着改代码,搭一个最原始的抓包环境往往更快。用一个 USB 转 TTL 模块,A 接总线 A、B 接总线 B,波特率设成和 Modbus 一样,电脑串口助手打开十六进制显示,就能看到主站发出的请求帧和从站返回的响应帧。逐字节对照功能码和数据区,物理层和协议层的问题一眼就能区分开。这个习惯帮我定位过不少“玄学”故障,实际上都不是玄学,只是帧字节错了或者时序没对上。建议你拿到一块从站设备时,先用这套方法把读写帧各抓一遍,留作以后比对基准。希望这篇从接线到轮询的笔记能帮你把这个协议栈跑熟,少走我当初走过的弯路。
本文还有配套的精品资源,点击获取