4、Modbus RTU实战:使用Python的 minimalmodbus 库读取温湿度传感器数据

好,咱们进入真正的实战环节了。

前面讲了那么多Modbus协议的理论,说白了,最终目的就是让咱们的工业网关能跟现场设备对上话。今天这一章,我就带你手把手用Python把温湿度数据读回来。

我个人习惯,学任何协议栈,先跑通一个最小系统,再谈优化。所以咱们今天的目标很纯粹:用一根串口线,连上温湿度传感器,用Python把温度和湿度数值打印到屏幕上

4.1 准备工作:硬件与软件环境

先看看你手头需要什么。别急着买贵的,我刚开始做项目时,用几十块钱的USB转485模块一样能调通。

硬件/软件 说明 备注
温湿度传感器 支持Modbus RTU协议,比如建大仁科、RS485型 默认从站地址一般为1
USB转RS485模块 FTDI或CH340芯片的都可以 记得装驱动
双绞线 连接传感器A/B线到转换器 线不要太长,1米内调试没问题
Python环境 Python 3.6以上 建议用虚拟环境
minimalmodbus库 纯Python实现,无需额外依赖 pip install minimalmodbus
小提示: 如果你手头没有温湿度传感器,也可以用Modbus模拟器代替。我早期调试时经常用Modbus Slave软件模拟从站,一样能跑通流程。

4.2 安装minimalmodbus库

这个库非常轻量,说白了就是封装了Modbus RTU的底层串口通信。你不需要关心CRC校验怎么算,它都帮你搞定了。

# 打开终端或命令提示符
pip install minimalmodbus

# 验证安装
python -c "import minimalmodbus; print(minimalmodbus.__version__)"

嗯,这里要注意:如果你用的是Linux系统,记得把当前用户加到dialout组,不然没权限访问串口。

sudo usermod -a -G dialout $USER
# 然后注销重新登录

4.3 连接硬件与确认串口

把USB转485模块插到电脑上,传感器A线接模块的A,B线接B。千万别接反了,我曾经有一次在项目现场,就是因为A/B线接反,折腾了半小时才发现。

然后确认串口号:

  • Windows: 设备管理器 -> 端口(COM和LPT) -> 看是COM几
  • Linux: ls /dev/ttyUSB*ls /dev/ttyACM*
  • macOS: ls /dev/cu.usbserial*
警告: 如果你在Linux下看到的是ttyACM0而不是ttyUSB0,别慌。这只是内核驱动不同,用法完全一样。

4.4 第一个读取程序:读取温湿度

好,核心代码来了。我习惯先写一个最简单的版本,能跑通再说。

import minimalmodbus
import time

# 初始化串口
# 参数1:串口设备路径(Windows下是'COM3')
# 参数2:从站地址(传感器默认一般是1)
instrument = minimalmodbus.Instrument('/dev/ttyUSB0', 1)

# 设置串口参数
instrument.serial.baudrate = 9600      # 波特率
instrument.serial.bytesize = 8         # 数据位
instrument.serial.parity = 'N'         # 无校验
instrument.serial.stopbits = 1         # 停止位
instrument.serial.timeout = 0.5        # 超时时间,单位秒

# 设置Modbus模式
instrument.mode = minimalmodbus.MODE_RTU

print("开始读取温湿度传感器数据...")
print("按 Ctrl+C 停止\n")

try:
    while True:
        # 读取温度:功能码03,起始地址0,读取1个寄存器
        # 注意:有些传感器温度地址是0,有些是1,看手册
        temperature = instrument.read_register(0, 1)  # 1表示1位小数
        
        # 读取湿度:功能码03,起始地址1,读取1个寄存器
        humidity = instrument.read_register(1, 1)     # 1表示1位小数
        
        print(f"温度: {temperature:.1f} °C, 湿度: {humidity:.1f} %")
        
        time.sleep(2)  # 每2秒读一次
        
except KeyboardInterrupt:
    print("\n停止读取")
except Exception as e:
    print(f"读取失败: {e}")
finally:
    instrument.serial.close()

跑一下试试。如果一切正常,你应该能看到类似这样的输出:

开始读取温湿度传感器数据...
按 Ctrl+C 停止

温度: 25.3 °C, 湿度: 60.1 %
温度: 25.4 °C, 湿度: 60.0 %
温度: 25.3 °C, 湿度: 60.2 %
核心要点: read_register(address, decimals, signed) 这个函数是咱们最常用的。address是寄存器地址,decimals是小数位数,signed表示是否有符号。大部分温湿度传感器用无符号整数就够了。

4.5 避坑指南:我踩过的三个坑

说实话,这个程序看起来简单,但实际项目中我遇到过不少问题。给你列几个最常见的:

  • 坑一:寄存器地址搞错

    我曾经遇到过一款传感器,手册上写温度地址是0x0000,但实际读出来是0。后来发现它用的是协议地址而不是物理地址。说白了,有些厂家从0开始编号,有些从1开始。我的建议是:先用Modbus Poll工具扫一遍地址范围。

  • 坑二:小数位数不对

    read_register的第二个参数decimals,如果你设成0,读出来就是整数。但传感器实际返回的是放大10倍或100倍的值。比如温度25.3,它可能返回253。这时候decimals要设成1。

  • 坑三:串口被占用

    程序跑第二次时报错"Port is already open"。这是因为上一次程序异常退出,没来得及关闭串口。解决办法:要么重启电脑,要么在代码里加try/finally确保关闭。

我的调试习惯: 先用串口调试助手(比如sscom、putty)直接发Modbus报文,看看传感器能不能正常回复。如果能,说明硬件没问题;如果不能,先查接线和参数。这一步能省下大量排查时间。

4.6 进阶:批量读取多个寄存器

如果你需要同时读取温度和湿度,用read_registers批量读效率更高。一次Modbus请求就能拿到两个寄存器的值。

# 批量读取:从地址0开始,连续读2个寄存器
values = instrument.read_registers(0, 2)

temperature = values[0] / 10.0  # 假设1位小数
humidity = values[1] / 10.0

print(f"温度: {temperature:.1f} °C, 湿度: {humidity:.1f} %")

为什么推荐批量读?你想想看,工业现场往往有几十个传感器,如果每个都单独发一次请求,总线压力会很大。批量读能减少通信次数,说白了就是省带宽、省时间。

4.7 完整代码示例:带异常处理的生产级版本

下面这个版本是我在实际项目中用的模板,加了重试机制和日志记录。你可以直接拿去用。

import minimalmodbus
import time
import logging

# 配置日志
logging.basicConfig(level=logging.INFO, 
                    format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class ModbusSensor:
    """Modbus温湿度传感器读取类"""
    
    def __init__(self, port, slave_address, baudrate=9600):
        self.instrument = minimalmodbus.Instrument(port, slave_address)
        self.instrument.serial.baudrate = baudrate
        self.instrument.serial.bytesize = 8
        self.instrument.serial.parity = 'N'
        self.instrument.serial.stopbits = 1
        self.instrument.serial.timeout = 0.5
        self.instrument.mode = minimalmodbus.MODE_RTU
        
    def read_temperature(self):
        """读取温度,带重试机制"""
        for attempt in range(3):
            try:
                return self.instrument.read_register(0, 1)
            except Exception as e:
                logger.warning(f"读取温度失败,第{attempt+1}次重试: {e}")
                time.sleep(0.1)
        raise Exception("温度读取失败,已重试3次")
    
    def read_humidity(self):
        """读取湿度"""
        for attempt in range(3):
            try:
                return self.instrument.read_register(1, 1)
            except Exception as e:
                logger.warning(f"读取湿度失败,第{attempt+1}次重试: {e}")
                time.sleep(0.1)
        raise Exception("湿度读取失败,已重试3次")
    
    def close(self):
        self.instrument.serial.close()

# 使用示例
if __name__ == "__main__":
    sensor = ModbusSensor('/dev/ttyUSB0', 1)
    
    try:
        while True:
            temp = sensor.read_temperature()
            hum = sensor.read_humidity()
            logger.info(f"温度: {temp:.1f}°C, 湿度: {hum:.1f}%")
            time.sleep(5)
    except KeyboardInterrupt:
        logger.info("用户停止")
    finally:
        sensor.close()
        logger.info("串口已关闭")

4.8 本章小结

今天咱们干了三件事:

  1. 装好了minimalmodbus库,连好了硬件
  2. 写了一个最简单的读取程序,跑通了
  3. 加上了异常处理和批量读取,让代码更健壮

说实话,Modbus RTU的实战就这么简单。你只要记住:地址、功能码、数据格式这三个要素,剩下的就是调参。

下一章,咱们会深入Modbus TCP,看看以太网环境下的工业通信怎么玩。到时候你会发现,核心逻辑其实一模一样,只是传输层换成了网络。

课后练习: 试着修改代码,读取传感器上其他寄存器(比如设备地址、版本号)。大部分传感器手册里都有这些信息。读出来之后,你就能确认自己的通信是完全正确的。