3. Modbus TCP 服务器模拟

Modbus TCP 服务器模拟 学习卡片

任务目标

本案例将创建一个基于 pyModbus 库的 Modbus TCP 服务器,模拟一个从站设备。该服务器将维护一组寄存器(保持寄存器和线圈),并监听网络端口,供其他 Modbus 客户端(如 Modbus Poll、另一台设备或本机测试脚本)进行读写操作。通过本案例,你将得到一个可独立运行的从站模拟器,用于后续案例中的主站通信测试或边缘网关数据采集开发。

环境与依赖

需要 Python 3.8+ 环境,并安装 pyModbus 库(版本 ≥ 3.0):

pip install pymodbus

完整实现代码

以下代码创建了一个 Modbus TCP 服务器,监听 0.0.0.0:5020 端口。服务器内部维护了 10 个保持寄存器(地址 0-9)和 10 个线圈(地址 0-9),并设置了初始值。客户端可对这些地址进行读写操作。

# modbus_tcp_server.py
from pymodbus.server import StartTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
import logging

# 配置日志(可选,便于观察请求)
logging.basicConfig()
logging.getLogger('pymodbus').setLevel(logging.INFO)

def run_server():
    # 初始化数据块:保持寄存器(3x)地址0-9,线圈(0x)地址0-9
    # 保持寄存器初始值:0-4为100-104,5-9为0
    hr_values = [100, 101, 102, 103, 104, 0, 0, 0, 0, 0]
    # 线圈初始值:地址0-4为True(ON),5-9为False(OFF)
    coil_values = [True, True, True, True, True, False, False, False, False, False]

    # 创建数据块(地址从0开始)
    hr_block = ModbusSequentialDataBlock(0, hr_values)
    coil_block = ModbusSequentialDataBlock(0, coil_values)

    # 创建从站上下文(从站ID=1,仅使用保持寄存器和线圈)
    slave_context = ModbusSlaveContext(
        di=None,              # 不模拟离散输入
        co=coil_block,        # 线圈
        hr=hr_block,          # 保持寄存器
        ir=None               # 不模拟输入寄存器
    )

    # 服务器上下文(单从站模式)
    server_context = ModbusServerContext(slaves=slave_context, single=True)

    # 启动TCP服务器,监听所有网卡的5020端口
    print("Modbus TCP Server 启动,监听端口 5020...")
    print("保持寄存器(3x): 地址0-9, 初始值:", hr_values)
    print("线圈(0x): 地址0-9, 初始值:", coil_values)
    StartTcpServer(context=server_context, address=("0.0.0.0", 5020))

if __name__ == "__main__":
    run_server()

运行与验证方法

1. 启动服务器:

python modbus_tcp_server.py

看到打印信息后,服务器即处于监听状态。

2. 使用 Modbus 客户端工具验证(推荐 Modbus Poll 或 QModMaster):

  • 连接设置:TCP/IP,IP 填 127.0.0.1,端口 5020,从站 ID 设为 1(或 255,取决于工具,通常 1 即可)。
  • 读保持寄存器:功能码 03,起始地址 0,数量 10。预期读到值:100, 101, 102, 103, 104, 0, 0, 0, 0, 0。
  • 写保持寄存器:功能码 06(写单个),地址 5,值设为 200。随后再次读取地址 5,应读到 200。
  • 读线圈:功能码 01,起始地址 0,数量 10。预期读到:地址0-4为 ON,地址5-9为 OFF。
  • 写线圈:功能码 05(写单个),地址 5,值设为 ON。随后读取线圈地址 5,应变为 ON。

3. 使用 Python 脚本验证(无需第三方 GUI 工具):

在另一个终端中运行以下测试脚本(需安装 pymodbus):

# test_client.py
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('127.0.0.1', port=5020)
client.connect()

# 读保持寄存器
rr = client.read_holding_registers(0, 5, slave=1)
print("读取保持寄存器0-4:", rr.registers)  # 预期 [100, 101, 102, 103, 104]

# 写保持寄存器地址5
client.write_register(5, 200, slave=1)
rr = client.read_holding_registers(5, 1, slave=1)
print("写入后读取地址5:", rr.registers)  # 预期 [200]

# 读线圈
cr = client.read_coils(0, 5, slave=1)
print("读取线圈0-4:", cr.bits)  # 预期 [True, True, True, True, True]

# 写线圈地址5
client.write_coil(5, True, slave=1)
cr = client.read_coils(5, 1, slave=1)
print("写入后读取线圈5:", cr.bits)  # 预期 [True]

client.close()

运行测试脚本:

python test_client.py

若输出与注释中的预期值一致,则说明服务器读写功能正常,案例验证通过。

常见问题

  • 端口被占用:若 5020 被占用,可修改代码中的端口号,并同步修改客户端连接端口。
  • 连接失败:检查防火墙是否放行 TCP 5020 端口;若跨设备测试,确保 IP 可达。
  • 从站 ID 不匹配:本服务器使用 single=True,从站 ID 在 0-255 范围内均可访问(pyModbus 3.x 行为)。若客户端指定从站 ID 为 0 或 255 无法访问,可尝试设为 1。