简介一个基于 Arduino 的 Modbus 主站库适用于需要让 Arduino 通过 Modbus RTU 协议与传感器、从站设备通信的开发者。库内封装了多项高层功能可帮助不熟悉 Modbus 协议的用户快速读取设备数据并针对 TTL、RS485/RS322 电气标准及 EnviroDIY Mayfly 等开发板的硬件接口给出了实用建议。资源包共 13 个文件以 C 源码.cpp/.h、Arduino 示例.ino为主另含 README.md、库配置 JSON/INI/Properties 及 CI 配置等结构清晰便于直接导入 Arduino IDE 或 PlatformIO 使用。压缩包整体仅 23KB轻量易部署。目前已有 640 人学习下载。下载后细读 README.md 即可掌握 Modbus 映射规则、支持的数据类型与示例用法代码中包含读写寄存器、扫描寄存器等典型示例适合正在做环境监测、工业数据采集等项目的 Arduino 开发者参考借鉴。整体代码简洁注释充分便于二次开发与协议学习。1. 你手里的传感器从机需要一个能主动说话的 Master很多工程师第一次接触 Modbus是从一块智能电表或温湿度变送器开始的设备上明明标着 RS485 和地址手头的 Arduino UNO 打开串口监视器却只有乱码。原因很简单这类设备不会主动吐数据它们作为从机只会响应主机发来的请求帧。Arduino 要读数就得先把自己变成 Modbus Master在 C 里把请求帧组出来、发出去、再解析从机回的那一串十六进制。这个标题对应的正是这样一个用于 Arduino 的 Modbus Master 库它控制传感器、读取寄存器、写寄存器把 Modbus 的报文细节封装成几个 C 类方法。适合所有用 Arduino 兼容板接工业传感器、PLC、变频器或自定义从机板的开发者。下面我会从协议选型、库的选择、最小可运行代码到排错和轮询策略把这条技术路线完整展开并且让新手能照着走让熟手能避开那些藏在字节序和方向控制里的坑。2. Modbus Master 库选型与协议关键点2.1 为什么单片机侧优先用 Modbus RTU 而不是 TCPModbus 有两个常见载体RTU 和 TCP。RTU 运行在串行线上主机发出一个带 CRC 校验的请求从机在几十毫秒内回一段数据TCP 则跑在以太网上通常由网关或上位机扮演 Master。对 Arduino 这类单片机RTU 更现实因为一根双绞线加一个 RS485 收发器就能把传感器拉到上百米成本不到几块钱。很多智能传感器、仪表、PLC 的从站接口都默认暴露 RS485直接用 RTU 协议可以省去协议转换网关。RS485 是半双工总线同一时刻只有一端在发送所以 Master 代码里必须控制发送方向。Modbus RTU 的帧结构是“地址域 功能码 数据域 CRC16”没有 HTTP 那样的头尾包围多个从机靠地址区分。单片机做 Master 时重点就是正确组装这 4 个部分并处理接收超时和 CRC 校验。虽然用裸串口也能手写报文但一个成熟库能省去对超时、缓冲区、状态机的反复调试。尤其是从机响应时间不固定手写状态机在中断和轮询之间很容易出现竞争库封装好以后你只需要关心业务层的寄存器地址和数值。2.2 功能码与寄存器模型Master 要处理的最小集合从机的数据被组织成 4 张表线圈、离散输入、保持寄存器、输入寄存器。线圈和保持寄存器既可读也可写离散输入和输入寄存器只读。Modbus Master 库至少要覆盖 01、02、03、04、05、06、0x10 这些功能码但实际项目中 03 读保持寄存器和 06 写单个寄存器用得最多。比如智能电表的电压、电流、功率都在保持寄存器里温湿度传感器通常也是把量化后的温度值放进一个 16 位保持寄存器。功能码名称用途库中对应方法0x01读线圈读取开关量输出readCoils0x02读离散输入读取开关量输入readDiscreteInputs0x03读保持寄存器读取模拟量/参数readHoldingRegisters0x04读输入寄存器读取只读测量值readInputRegisters0x05写单线圈控制单路开关writeSingleCoil0x06写单个寄存器设置单个参数writeSingleRegister0x10写多个寄存器批量下发参数writeMultipleRegisters这里有个特别容易踩的地址换算问题。很多传感器手册只给“40001”这类 PLC 地址那是把保持寄存器映射到了 Modbus 的 4xxxx 区段真正写进报文的是最后四位十六进制偏移即 40001 对应协议地址 0x000040010 对应 0x0009。如果照抄手册把 40001 填进 readHoldingRegisters 的第一个参数实际请求的地址就变成 40001 对应的偏移从机根本不认最常见的表现就是反复超时。2.3 两个常见 C 实现的取舍Arduino 库管理器里能装到的 Modbus Master 库主要分两派。一个是 Doc Walker 维护的 ModbusMaster类封装完整提供 preTransmission 和 postTransmission 回调配合 RS485 方向控制很顺支持硬件串口和 SoftwareSerial但 SoftwareSerial 在高波特率下会丢失字节只建议在 9600 波特率以下使用。另一个是 SimpleModbusMaster源码只有一个 .cpp 文件接口更接近底层直接暴露 txBuffer 和 rxBuffer适合想要完全掌控报文或者 Flash 空间极度紧张的板子。我的使用习惯是如果目标是工业传感器优先用 ModbusMaster因为它的错误码枚举更细能从返回值看出是 CRC 错误、超时还是从机异常。如果目标是学习协议或者有一个非常规从机需要自定义报文SimpleModbusMaster 反而更容易改。选库时不要只看下载量关键看是否支持你要的 STM32、ESP32 平台。这两个库都只用标准 C 写在大多数 Arduino 兼容平台上能直接编译通过只是要注意个别板子的硬件串口命名差异。3. 用 Arduino IDE 搭建最小 Master 工程3.1 安装库与 RS485 硬件接线在 Arduino IDE 的库管理器里输入 ModbusMaster安装第一个结果即可。Arduino IDE 2.x 的库默认安装在用户目录的 Documents/Arduino/libraries 下如果之前改过库目录可以在 文件→首选项 里确认。这里不需要额外去网站找“相关文件下载”库管理器里拿到的是可直接编译的源码包。硬件接线是第一个掉坑点。以 ESP32 或 STM32F103C8T6 这类带多个硬件串口的板子为例我一般把 MAX485 模块的 DI 接主板的 TX、RO 接 RXDE 与 RE 并联后接一个普通 GPIO 作为方向控制。注意不能把 DE/RE 接成固定电平否则半双工切不过来。RS485 的 A、B 端接从机A 接 AB 接 B两端都配 120Ω 终端电阻只有一个从机做短距离测试时可以只在主站侧接 120Ω。共地也非常关键如果 Master 和从机各自独立供电必须把 GND 连在一起否则线上共模电压超标表现为偶发乱码。如果用的是最经典的 Arduino UNO需要注意它只有一个硬件串口调试口被占用后只能改用 SoftwareSerial。SoftwareSerial 的引脚电平也是 TTL 电平接 MAX485 的 DI/RO 没问题但中断和时序限制较多大量数据时会丢字节。所以我在 UNO 上做 Modbus 时一般把 Serial 留给 RS485调试输出用另一块板子或改用软串口。3.2 读取从机保持寄存器的完整代码下面这段代码在 ESP32 上通过 Serial2 与一个地址为 1 的 Modbus 从机通信读取起始地址 0x0000 的 4 个保持寄存器并打印到串口监视器。#include ModbusMaster.h #define RS485_DIR_PIN 4 // 方向控制引脚接 DERE ModbusMaster node; void preTransmission() { digitalWrite(RS485_DIR_PIN, HIGH); // 发送模式 } void postTransmission() { digitalWrite(RS485_DIR_PIN, LOW); // 接收模式 } void setup() { Serial.begin(115200); // 调试串口 Serial2.begin(9600, SERIAL_8N1); // RS485 串口8位数据、无校验、1停止位 pinMode(RS485_DIR_PIN, OUTPUT); digitalWrite(RS485_DIR_PIN, LOW); node.begin(1, Serial2); // 从机地址 1 node.preTransmission(preTransmission); node.postTransmission(postTransmission); } void loop() { uint8_t result; uint16_t data[4]; result node.readHoldingRegisters(0x0000, 4); if (result node.ku8MBSuccess) { for (uint8_t i 0; i 4; i) { data[i] node.getResponseBuffer(i); Serial.print(REG[); Serial.print(i); Serial.print(]); Serial.println(data[i]); } } else { Serial.print(Modbus Error Code: ); Serial.println(result, HEX); } delay(500); }这段代码的核心逻辑在 loop 里先调用 readHoldingRegisters 组包并等待响应返回 ku8MBSuccess 表示完整拿到了回复然后把响应缓冲区里的 16 位值按索引取出。Modbus RTU 返回的数据是大端序而 ModbusMaster 库已经在内部做了一次字节序调整所以直接赋值给 uint16_t 就可以不需要自己交换高低字节。几个关键参数要解释清楚。node.begin(1, Serial2)的第一参数是从机地址范围 1…2470 是广播地址广播地址不能用于读操作。第二个参数必须是 Stream 对象硬件串口和 SoftwareSerial 都行但我只有在调试时才会用 SoftwareSerial因为它的定时精度不够波特率超过 9600 容易丢字节。node.ku8MBSuccess是库内核定义的枚举常量不同版本的 ModbusMaster 可能还有其它错误码查看错误码时直接打印十六进制值最方便。3.3 关键函数与参数速查函数第一个参数第二个参数返回值含义node.begin(slaveID, stream)从机地址串口对象无node.readHoldingRegisters(addr, qty)协议地址偏移寄存器个数0 成功非 0 错误码node.readInputRegisters(addr, qty)同上同上同上node.writeSingleRegister(addr, value)寄存器地址16 位值同上node.getResponseBuffer(idx)索引-返回第 idx 个寄存器值调用写寄存器时有一个隐藏细节writeSingleRegister 返回之后需要一个短延时再发下一帧不然从机还在写 EEPROM新的请求会被忽略。我一般会在每次成功写完后加 20ms 延时。批量写多个寄存器用 writeMultipleRegisters但不同从机的单帧最大寄存器数量不同常见上限是 125 个实际使用时建议先查从机文档不要一次写满否则从机会返回异常码 0x83。4. 调参、排错与从机边界4.1 串口参数与超时从机不回应的常见原因如果代码编译通过、接线也正确但一直拿不到数据第一个要查的是串口参数。Modbus RTU 的标准配置是 9600, 8, N, 1很多从机出厂默认就是这个。也有大量变送器默认 4800 或 19200校验位可能设置为 Even。只要 Serial2.begin 里面的参数与从机不一致整帧都会被从机丢弃Master 端表现为一直超时。除了波特率还需要关注帧间隔。Modbus RTU 规定一帧的字符间隔不得超过 1.5 个字符时间超过就算帧结束。使用 Arduino 库时这个间隔由串口硬件和库的接收状态机共同保证问题往往出在延时上。比如从机需要 100ms 才能准备好数据而 Master 在下一轮循环里立刻再发请求就会因为没有等待而从机无响应。我遇到过不少案例把 loop 里的 delay(500) 改成 delay(50) 后异常消失就是因为给了从机足够的恢复时间。这里列一个排查清单1) 用带隔离的 USB 转 RS485 模块直接监听 A/B 电平看请求帧是否发得出来2) 核对地址、功能码、寄存器地址与从机手册的寄存器表3) 核对串口参数和校验位4) 如果总线挂了两个从机检查终端电阻是否两端都接。注意监听工具推荐用带隔离的 USB 转 RS485 模块不要用普通 TTL 电平直接接到 A/B 线上否则可能烧芯片。4.2 广播地址 0 与单播地址的区别地址 0 在 Modbus 协议里是广播地址。主机向地址 0 发送请求所有从机都会执行但都不会回帧。这意味着对广播写操作Master 发出后应该在极短的时间内认为发送成功而不是等待响应。ModbusMaster 库的 readHoldingRegisters 如果传入地址 0会进入一个等待超时的流程表现就是卡住 200ms 左右然后返回错误码看起来像死机。正确做法是只对写单线圈、写单个寄存器这类写操作使用广播而且使用后必须单独延时或者通过返回的错误码判断发送完成而不是读取成功。有些第三方传感器的拨码地址默认是 255 或 247不在标准 1…247 范围内。这时要先把从机地址改到合法范围或者读取厂家地址。如果地址设错总线上所有从机都不会回应因为地址匹配不上表现和串口参数错误很相似排查时容易混淆。我建议在代码里把从机地址定义成宏方便批量替换测试。4.3 常见坑发送间隔、方向控制、数据对齐方向控制是刚接触 RS485 的人最容易摔的跟头。DE 是驱动器使能RE 是接收器使能ModbusMaster 库在发出请求前调用 preTransmission收到完整报文或超时后调用 postTransmission。如果你的板子上 DE 和 RE 是分开的一定要确保 RE 在发送期间也被拉高否则会产生本机回环把收到的字节当成从机响应。更隐蔽的情况是使用自动方向切换模块这时还要在库的回调里避免额外的翻转。数据对齐的坑集中在寄存器位数。很多传感器把 32 位浮点数拆成两个 16 位保持寄存器且字节序可能是 ABCD、CDAB 等各种组合。ModbusMaster 库只会原样返回两个 16 位寄存器不会自动拼成 float。常见的处理方式是把两个寄存器拼成一个 uint32_t再按从机的浮点数文档选择高位在前还是低位在前uint32_t raw ((uint32_t)data[0] 16) | data[1]; float value; memcpy(value, raw, sizeof(float));如果从机文档写的是“4字节大端”上面这行直接可用如果是“4字节小端”则要把 data[0] 和 data[1] 对调。这一步没有任何通用库能替你判断必须根据手册确认。5. 进阶多从机轮询与模拟验证5.1 用本地模拟从机验证 Master 逻辑手头没有从机硬件时不要急着下单。我一般会先在 PC 上用 Python 的 pymodbus 跑一个从机再用 USB 转 RS485 把它和 Arduino 连起来。第一步是写一个最小从机from pymodbus.server import StartSerialServer from pymodbus.datastore import ModbusSlaveContext, ModbusSequentialDataBlock from pymodbus.transaction import ModbusRtuFramer store ModbusSlaveContext( diModbusSequentialDataBlock(0, [1, 2, 3]), coModbusSequentialDataBlock(0, [4, 5, 6]), hrModbusSequentialDataBlock(0, [0x1234, 0xABCD]), irModbusSequentialDataBlock(0, [0x5678])) StartSerialServer( contextstore, framerModbusRtuFramer, portCOM5, baudrate9600, parityN, stopbits1, timeout0.2)这段代码在 PC 上创建了一个 RTU 从机保持寄存器区定义了 0x0000 和 0x0001 两个寄存器。Arduino 端跑 3.2 节的代码就能在串口监视器里看到 0x1234 和 0xABCD。pymodbus 的 StartSerialServer 参数中port 是 PC 上 USB 转 RS485 的串口号baudrate、parity、stopbits 必须与 Arduino 侧一致timeout 控制等待请求的超时数值越大对中途打断越宽容。5.2 用非阻塞轮询提高 Master 的吞吐当你控制多个从机时不能在 loop 里串行等待每一个读任务。读一个 4 寄存器的请求在 9600 波特率下大约耗时 10…20ms10 个从机就是 200ms 的阻塞这段时间如果还要响应按键或显示就会很别扭。常用的做法是把每次读操作看成一个状态机空闲时发送请求收到响应或超时后进入下一个从机。这里给出一个用 millis 控制轮询间隔的简单框架struct ModbusSlave { uint8_t address; uint16_t startAddr; uint8_t qty; uint8_t state; // 0: idle, 1: waiting, 2: done uint32_t lastPoll; }; void pollSlave(ModbusSlave s) { if (s.state 0) { node.begin(s.address, Serial2); uint8_t err node.readHoldingRegisters(s.startAddr, s.qty); s.state (err node.ku8MBSuccess) ? 2 : 1; s.lastPoll millis(); } else if (s.state 1) { if (millis() - s.lastPoll 100) { s.state 2; // 超时退出 } } }注意这里的 node.begin 只在状态切换时调用因为每个从机地址不同。实际工程里还可以把超时时间做成可配置以及确保 preTransmission/postTransmission 回调保持全局生效。最后一个技巧是如果从机支持 0x10 写多个寄存器尽量把多个参数合并到一个请求里这比逐个写单个寄存器少三分之二的帧数与等待时间。本文还有配套的精品资源点击获取