ESP32 综合教程:通过 RS485 与 Modbus RTU 读取工业传感器
本文由 AI 协助整理。示例用于低压学习环境,不适用于未经评估的生产控制。不同传感器的供电、站号、串口参数、寄存器和字节序完全可能不同,必须以手中设备的说明书为准。涉及市电、变频器或工业现场时,应由具备资质和经验的人员处理接线与隔离。

为什么学了 UART,还要折腾 RS485
ESP32 的 UART 很适合板间短距离通信,但在设备距离较远、电磁环境复杂、总线上挂多个设备时,单端 TTL 信号并不理想。RS485 使用差分信号,常见于温湿度变送器、电表、称重模块、PLC 和变频器。
RS485 只规定电气层,Modbus RTU 则规定“怎么问、怎么答”。可以把 RS485 理解为公路,Modbus RTU 是这条公路上的交通规则。只有公路没有规则,设备听见了电信号却不知道含义;只有规则没有收发器,ESP32 的 3.3V UART 也不能直接接 A/B 差分线。
这篇教程做一个 Modbus 主站:ESP32 通过 RS485 收发器向一个传感器发送读寄存器请求,解析温度和湿度,再从串口打印结果。重点不是某个具体传感器,而是学会阅读寄存器表、处理地址偏移、控制半双工方向,并根据错误码定位问题。
硬件选择:不要忽略逻辑电平
需要:
- ESP32 DevKit。
- 支持 Modbus RTU 的 RS485 传感器或模拟从站。
- 3.3V 逻辑兼容的 RS485 收发器,例如 MAX3485、SP3485 一类模块。
- 与传感器匹配的独立电源。
- 双绞线,以及需要时使用的终端电阻。
常见 MAX485 小模块通常使用 5V 供电,其 RO 输出可能达到接近 5V,不能想当然地直接接 ESP32 的 3.3V GPIO。学习时优先购买明确标注 3.3V 逻辑兼容的收发器模块。名字只差一个数字,电平问题却不讲人情。
工业传感器可能使用 12V 或 24V,绝不能把其电源正极接到 ESP32 的 3V3 或 5V 引脚。传感器电源按说明书独立提供;非隔离通信通常需要共地。现场存在大电位差、强干扰或安全要求时,应使用隔离型 RS485 模块和合适的浪涌保护。
参考接线
以 ESP32 UART2 与 3.3V 收发器为例:
| ESP32 | RS485 模块 | 作用 |
|---|---|---|
| GPIO17 / TX2 | DI | ESP32 发送数据到收发器 |
| GPIO16 / RX2 | RO | 收发器把接收数据送给 ESP32 |
| GPIO4 | DE 与 /RE 并联 |
高电平发送,低电平接收 |
| 3V3 | VCC | 仅适用于 3.3V 模块 |
| GND | GND | 逻辑参考地 |
| - | A、B | 连接传感器 A、B |
不同厂商对 A/B 命名偶尔存在差异。有的还标作 D+、D-。如果串口参数和站号都正确,却始终超时,可以在断电后交换 A/B 再测试,但不要带电随手乱碰。
终端电阻通常用于总线两端,常见值约 120Ω,但是否需要、模块是否自带,要看线长、速率和拓扑。桌面上一米线不应为了“看起来工业”就在每个节点都并一个电阻;总线上电阻过多也会增加负载。
先读懂传感器说明书
在写代码前找到以下信息:
| 参数 | 示例 | 必须以说明书为准 |
|---|---|---|
| 从站地址 | 1 | 可配置范围可能不同 |
| 波特率 | 9600 | 常见但并非固定 |
| 数据格式 | 8N1 | 也可能有偶校验或奇校验 |
| 功能码 | 0x03 或 0x04 | 保持寄存器与输入寄存器不同 |
| 起始寄存器 | 0x0000 | 手册可能写成 40001 |
| 寄存器数量 | 2 | 温度和湿度各一个 |
| 数据类型 | 有符号 16 位 | 可能是无符号、32 位或浮点 |
| 比例系数 | 0.1 | 原始值 253 代表 25.3℃ |
最常见的坑是寄存器地址。手册写 40001,程序库可能要求传入偏移地址 0;手册写 30001,通常表示输入寄存器,应该使用功能码 0x04。不要把手册中的展示编号不加思考地原样塞进函数。
第二个坑是数据类型。负温度若以有符号 16 位整数保存,必须转换成 int16_t;直接按 uint16_t 解释,零下温度可能瞬间变成一个非常热情的五位数。
安装 ModbusMaster 库
Arduino IDE 中打开库管理器,搜索并安装 ModbusMaster。这个库支持常用的线圈、离散输入、保持寄存器和输入寄存器操作,并提供发送前、发送后的回调,正好用于控制 DE 与 /RE。
本文使用 Arduino 的 HardwareSerial:
#include <ModbusMaster.h>
HardwareSerial rs485Serial(2);
ModbusMaster node;
constexpr int RX_PIN = 16;
constexpr int TX_PIN = 17;
constexpr int DIR_PIN = 4;
constexpr uint8_t SLAVE_ID = 1;
半双工方向控制
RS485 两线半双工在同一对线上轮流发送和接收。发送请求前拉高方向脚,发送完成后切回接收:
void preTransmission() {
digitalWrite(DIR_PIN, HIGH);
}
void postTransmission() {
digitalWrite(DIR_PIN, LOW);
}
初始化程序:
void setup() {
Serial.begin(115200);
pinMode(DIR_PIN, OUTPUT);
digitalWrite(DIR_PIN, LOW);
rs485Serial.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN);
node.begin(SLAVE_ID, rs485Serial);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
Serial.println("Modbus master ready");
}
如果设备使用偶校验,可以把格式改成相应配置,例如常见的 SERIAL_8E1。波特率、校验、站号有一个不一致,设备通常就会保持一种非常专业的沉默。
读取两个保持寄存器
假设说明书明确写着:功能码 0x03,偏移地址 0 开始,寄存器 0 为有符号温度乘以 10,寄存器 1 为无符号湿度乘以 10。读取代码如下:
unsigned long lastPollAt = 0;
void pollSensor() {
uint8_t result = node.readHoldingRegisters(0x0000, 2);
if (result != node.ku8MBSuccess) {
Serial.printf("Modbus error: 0x%02X\n", result);
return;
}
int16_t rawTemperature =
static_cast<int16_t>(node.getResponseBuffer(0));
uint16_t rawHumidity = node.getResponseBuffer(1);
float temperature = rawTemperature / 10.0f;
float humidity = rawHumidity / 10.0f;
if (temperature < -40.0f || temperature > 125.0f ||
humidity < 0.0f || humidity > 100.0f) {
Serial.println("Value outside expected range");
return;
}
Serial.printf("Temperature: %.1f C, Humidity: %.1f %%\n",
temperature, humidity);
}
void loop() {
if (millis() - lastPollAt >= 2000) {
lastPollAt = millis();
pollSensor();
}
}
这段代码只是一个寄存器表示例。如果你的设备使用输入寄存器,应改为 readInputRegisters();地址、数量和倍率也要跟着手册修改。

32 位数值与字节序
一个 Modbus 寄存器是 16 位。32 位整数或浮点数需要两个连续寄存器,厂商还可能采用不同的字节或字顺序。
假设设备明确使用高字在前的 IEEE 754 浮点:
float registersToFloatABCD(uint16_t highWord, uint16_t lowWord) {
uint32_t bits = (static_cast<uint32_t>(highWord) << 16) | lowWord;
float value;
memcpy(&value, &bits, sizeof(value));
return value;
}
但有些设备会交换两个寄存器,或者交换寄存器内部字节。解析方式必须依据手册或已知测试值确认。看到一个浮点数像 -1.7e38 时,不要急着认为传感器正在测量宇宙边缘,先检查字节序。
错误码与排查思路
readHoldingRegisters() 返回成功以外的值时,要记录十六进制错误码。常见方向包括:
- 超时:设备没回应,检查供电、A/B、共地、站号和串口参数。
- CRC 错误:线路干扰、波特率不匹配或数据被破坏。
- 非法功能:设备不支持当前功能码。
- 非法地址:起始地址或读取数量不在设备允许范围。
- 非法值:写入数据或请求数量不符合设备要求。
排错时一次只读一个已知寄存器,降低波特率,缩短线缆,只保留一个从站。先把最小链路跑通,再恢复完整总线。不要一开始就接五个设备、两百米线和云平台,然后从 MQTT 仪表盘倒推为什么寄存器没有响应。
用 USB-RS485 工具交叉验证
准备一个可靠的 USB-RS485 转换器和 Modbus 主站调试软件很有帮助。先用电脑读取设备:
- 确认站号、波特率和校验位。
- 验证功能码、起始地址和寄存器数量。
- 记录原始十六进制寄存器值。
- 对照设备显示或已知环境值确认倍率与符号。
- 再把相同参数搬到 ESP32。
如果电脑也读不到,问题多半不在 ESP32 代码;如果电脑能读、ESP32 不能读,就重点检查逻辑电平、方向控制、串口引脚和程序参数。这种交叉验证能把一个大问题切成两个小问题。
多个从站怎样接
RS485 总线适合菊花链式连接,所有设备并在 A/B 总线上,并使用不同站号。避免从主干拉出很长的星形支线。总线两端根据实际条件配置终端电阻,偏置电阻和屏蔽接地也应按现场设计处理。
程序端轮询不同站号,可以为每个从站创建对象或在合适的库接口中切换目标。每次请求之间留出合理间隔,处理超时后再继续下一个设备,避免某个坏节点拖死整个采集循环。
数据结构最好统一包含:设备编号、采集时间、原始寄存器、换算值和读取状态。上报 MQTT 时不要只发一个 temperature,否则出现异常时很难判断是通信失败、解析错误还是传感器本身超量程。
与 WiFi、MQTT 集成时的注意事项
ModbusMaster 的请求是同步等待的,设备不响应时会占用一段时间。项目同时承担网页、MQTT 或其他实时任务时,应降低无效请求频率,记录连续失败次数,并考虑把 Modbus 轮询放入独立 FreeRTOS 任务。
读取成功后再更新“最近有效值”,失败时发布状态而不是把零当成测量值:
{
"device": 1,
"temperature": 25.3,
"humidity": 61.8,
"modbus_ok": true,
"error_count": 0
}
连续失败时可以保留上次值,但必须同时带时间戳或数据年龄,不能让后端误以为它是刚采到的新数据。
安全边界
在变频器、锅炉、泵阀和生产设备上写寄存器,可能造成设备动作。初学阶段只做只读测试,确认寄存器定义与现场安全条件后,再考虑写线圈或保持寄存器。
控制类项目还必须设计失联状态、上电默认状态、急停、权限与物理保护。ESP32 和一段示例代码不能替代 PLC 安全设计,更不能替代合格的电气施工。
常见故障表
| 现象 | 优先检查 |
|---|---|
| 一直超时 | 供电、A/B、共地、站号、波特率和校验 |
| 偶尔成功、偶尔 CRC 错误 | 线缆、终端、干扰、供电和接地 |
| 返回非法地址 | 40001/30001 展示地址与程序偏移混淆 |
| 数值固定但明显不对 | 功能码、倍率、有符号类型或字节序 |
| 只能发送不能接收 | DE 与 /RE 没有及时切回接收 |
| ESP32 发热或异常 | 使用了不兼容的 5V 逻辑模块 |
总结
ESP32 读取 Modbus RTU 设备,代码本身并不复杂,真正的工作集中在四个地方:电气接口是否安全、串口参数是否一致、寄存器地址是否正确、原始数据应该怎样解释。
先用一个从站和一个寄存器建立最小通信,再增加多个参数、多个站号和 MQTT 上报。保留原始值和错误码,遇到问题用电脑工具交叉验证。这样做出来的不是“偶尔能读到一个数字”的演示,而是一条能够说明数据从哪里来、失败发生在哪里的采集链路。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号