Python modbus库

68

我需要用串口控制一个Modbus设备,对Modbus我没有经验,但是我进行了简短的研究,发现有几个Modbus库:

这些库有什么优缺点?是否有更好的替代品?

4个回答

156

大约在同一时间,我面临了相同的问题 - 选择哪个python modbus master库来实现串行通信(modbus RTU),因此我的观察仅适用于modbus RTU。

在我的研究中,我并没有太关注文档,但是对于modbus-tk,串行RTU主站的示例最容易找到,但仍然在源代码而非维基等地方。

简而言之:

MinimalModbus:

  • 优点:
    • 轻量级模块
    • 对于读取 ~10 个寄存器的应用程序,性能可能可接受
  • 缺点:
    • 当读取 ~64 个寄存器时,性能无法接受(对于我的应用程序)
    • 相对较高的CPU负载

pymodbus:

特点:依赖于串行流 (作者的帖子) ,必须动态设置串行超时,否则性能将很低(串行超时必须针对最长可能的响应进行调整)。

  • 优点:
    • CPU负载较低
    • 性能可接受
  • 缺点:
    • 即使超时被动态设置,性能也比modbus-tk慢2倍;如果超时保持在恒定值,性能会更差(但查询时间恒定)
    • 对硬件敏感(可能是由于依赖于来自串行缓冲区的处理流)或者可能存在事务内部问题:如果每秒执行不同的读取或读取/写入操作20次或更多次,则可能会获取混乱的响应。延长超时有所帮助,但并不总是使pymodbus RTU实现足够稳健以用于生产。
    • 添加对动态串行端口超时设置的支持需要进行额外的编程:继承基本同步客户端类并实现套接字超时修改方法
    • 响应验证不像modbus-tk那样详细。例如,在总线衰减的情况下,仅抛出异常,而modbus-tk在相同的情况下返回错误的从站地址或CRC错误,这有助于识别问题的根本原因(可能是超时时间太短、总线终端不正确/缺失或浮动接地等)

modbus-tk:

特点:探测串行缓冲区中的数据,快速组装并返回响应。

  • 优点:
    • 最佳性能;比带有动态超时的pymodbus快2倍左右
  • 缺点:
    • CPU负载大约比pymodbus高4倍,但这个问题可以通过对EDIT部分的改进来大大改善,使得这一点不再成立
    • 随着请求大小的增加,CPU负载会增加,但这个问题可以通过对EDIT部分的改进来大大改善,使得这一点不再成立
    • 代码不如pymodbus那么优雅

我使用pymodbus超过6个月,因为它的性能和CPU负载比例最佳,但在更高的请求速率下,不可靠的响应成为了一个严重的问题,最终我转向了更快的嵌入式系统并添加了对modbus-tk的支持,这对我来说效果最好。

对于那些对细节感兴趣的人

我的目标是达到最小响应时间。

设置:

  • 波特率:153600
    • 与实现modbus从站的微控制器的16MHz时钟同步
    • 我的RS-485总线只有50m
  • FTDI FT232R转换器以及串行转TCP桥接器(使用RFC2217模式下的com4com作为桥接器)
  • USB到串口转换器的最低超时和缓冲区大小配置为串行端口(以降低延迟)
  • 自动发送RS-485适配器(总线处于支配状态)

用例场景:

  • 每秒轮询5、8或10次,支持异步访问
  • 读/写10到70个寄存器的请求

典型长期(几周)性能:

  • MinimalModbus:初步测试后放弃
  • pymodbus:读取64个寄存器需要约30毫秒;有效速率高达30个请求/秒
    • 但响应不可靠(在多个线程同步访问的情况下)
    • 可能有一个线程安全的分支在github上,但它落后于主干,我还没有尝试过(https://github.com/xvart/pymodbus/network
  • modbus-tk:读取64个寄存器需要约16毫秒;对于较小的请求,有效速率高达70-80个请求/秒

基准测试

代码:

import time
import traceback
import serial
import modbus_tk.defines as tkCst
import modbus_tk.modbus_rtu as tkRtu

import minimalmodbus as mmRtu

from pymodbus.client.sync import ModbusSerialClient as pyRtu

slavesArr = [2]
iterSp = 100
regsSp = 10
portNbr = 21
portName = 'com22'
baudrate = 153600

timeoutSp=0.018 + regsSp*0
print "timeout: %s [s]" % timeoutSp


mmc=mmRtu.Instrument(portName, 2)  # port name, slave address
mmc.serial.baudrate=baudrate
mmc.serial.timeout=timeoutSp

tb = None
errCnt = 0
startTs = time.time()
for i in range(iterSp):
  for slaveId in slavesArr:
    mmc.address = slaveId
    try:
        mmc.read_registers(0,regsSp)
    except:
        tb = traceback.format_exc()
        errCnt += 1
stopTs = time.time()
timeDiff = stopTs  - startTs

mmc.serial.close()

print mmc.serial

print "mimalmodbus:\ttime to read %s x %s (x %s regs): %.3f [s] / %.3f [s/req]" % (len(slavesArr),iterSp, regsSp, timeDiff, timeDiff/iterSp)
if errCnt >0:
    print "   !mimalmodbus:\terrCnt: %s; last tb: %s" % (errCnt, tb)



pymc = pyRtu(method='rtu', port=portNbr, baudrate=baudrate, timeout=timeoutSp)

errCnt = 0
startTs = time.time()
for i in range(iterSp):
  for slaveId in slavesArr:
    try:
        pymc.read_holding_registers(0,regsSp,unit=slaveId)
    except:
        errCnt += 1
        tb = traceback.format_exc()
stopTs = time.time()
timeDiff = stopTs  - startTs
print "pymodbus:\ttime to read %s x %s (x %s regs): %.3f [s] / %.3f [s/req]" % (len(slavesArr),iterSp, regsSp, timeDiff, timeDiff/iterSp)
if errCnt >0:
    print "   !pymodbus:\terrCnt: %s; last tb: %s" % (errCnt, tb)
pymc.close()


tkmc = tkRtu.RtuMaster(serial.Serial(port=portNbr, baudrate=baudrate))
tkmc.set_timeout(timeoutSp)

errCnt = 0
startTs = time.time()
for i in range(iterSp):
  for slaveId in slavesArr:
    try:
        tkmc.execute(slaveId, tkCst.READ_HOLDING_REGISTERS, 0,regsSp)
    except:
        errCnt += 1
        tb = traceback.format_exc()
stopTs = time.time()
timeDiff = stopTs  - startTs
print "modbus-tk:\ttime to read %s x %s (x %s regs): %.3f [s] / %.3f [s/req]" % (len(slavesArr),iterSp, regsSp, timeDiff, timeDiff/iterSp)
if errCnt >0:
    print "   !modbus-tk:\terrCnt: %s; last tb: %s" % (errCnt, tb)
tkmc.close()

结果:

platform:
P8700 @2.53GHz
WinXP sp3 32bit
Python 2.7.1
FTDI FT232R series 1220-0
FTDI driver 2.08.26 (watch out for possible issues with 2.08.30 version on Windows)
pymodbus version 1.2.0
MinimalModbus version 0.4
modbus-tk version 0.4.2

读取100个64位寄存器:

无节能功能

timeout: 0.05 [s]
Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 64 regs): 9.135 [s] / 0.091 [s/req]
pymodbus:       time to read 1 x 100 (x 64 regs): 6.151 [s] / 0.062 [s/req]
modbus-tk:      time to read 1 x 100 (x 64 regs): 2.280 [s] / 0.023 [s/req]

timeout: 0.03 [s]
Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 64 regs): 7.292 [s] / 0.073 [s/req]
pymodbus:       time to read 1 x 100 (x 64 regs): 3.170 [s] / 0.032 [s/req]
modbus-tk:      time to read 1 x 100 (x 64 regs): 2.342 [s] / 0.023 [s/req]


timeout: 0.018 [s]
Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 64 regs): 4.481 - 7.198 [s] / 0.045 - 0.072 [s/req]
pymodbus:       time to read 1 x 100 (x 64 regs): 3.045 [s] / 0.030 [s/req]
modbus-tk:      time to read 1 x 100 (x 64 regs): 2.342 [s] / 0.023 [s/req]

最大节能

timeout: 0.05 [s]
Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 64 regs): 10.289 [s] / 0.103 [s/req]
pymodbus:       time to read 1 x 100 (x 64 regs):  6.074 [s] / 0.061 [s/req]
modbus-tk:      time to read 1 x 100 (x 64 regs):  2.358 [s] / 0.024 [s/req]

timeout: 0.03 [s]
Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 64 regs): 8.166 [s] / 0.082 [s/req]
pymodbus:       time to read 1 x 100 (x 64 regs): 4.138 [s] / 0.041 [s/req]
modbus-tk:      time to read 1 x 100 (x 64 regs): 2.327 [s] / 0.023 [s/req]

timeout: 0.018 [s]
Serial<id=0xd57330, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 64 regs): 7.776 [s] / 0.078 [s/req]
pymodbus:       time to read 1 x 100 (x 64 regs): 3.169 [s] / 0.032 [s/req]
modbus-tk:      time to read 1 x 100 (x 64 regs): 2.342 [s] / 0.023 [s/req]

读取100个10寄存器:

不支持省电功能

timeout: 0.05 [s]
Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 10 regs): 6.246 [s] / 0.062 [s/req]
pymodbus:       time to read 1 x 100 (x 10 regs): 6.199 [s] / 0.062 [s/req]
modbus-tk:      time to read 1 x 100 (x 10 regs): 1.577 [s] / 0.016 [s/req]

timeout: 0.03 [s]
Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.088 [s] / 0.031 [s/req]
pymodbus:       time to read 1 x 100 (x 10 regs): 3.143 [s] / 0.031 [s/req]
modbus-tk:      time to read 1 x 100 (x 10 regs): 1.533 [s] / 0.015 [s/req]

timeout: 0.018 [s]
Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.066 [s] / 0.031 [s/req]
pymodbus:       time to read 1 x 100 (x 10 regs): 3.006 [s] / 0.030 [s/req]
modbus-tk:      time to read 1 x 100 (x 10 regs): 1.533 [s] / 0.015 [s/req]

最大功耗节约

timeout: 0.05 [s]
Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 10 regs): 6.386 [s] / 0.064 [s/req]
pymodbus:       time to read 1 x 100 (x 10 regs): 5.934 [s] / 0.059 [s/req]
modbus-tk:      time to read 1 x 100 (x 10 regs): 1.499 [s] / 0.015 [s/req]

timeout: 0.03 [s]
Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.03, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.139 [s] / 0.031 [s/req]
pymodbus:       time to read 1 x 100 (x 10 regs): 3.170 [s] / 0.032 [s/req]
modbus-tk:      time to read 1 x 100 (x 10 regs): 1.562 [s] / 0.016 [s/req]

timeout: 0.018 [s]
Serial<id=0xd56350, open=False>(port='com22', baudrate=153600, bytesize=8, parity='N', stopbits=1, timeout=0.018, xonxoff=False, rtscts=False, dsrdtr=False)
mimalmodbus:    time to read 1 x 100 (x 10 regs): 3.123 [s] / 0.031 [s/req]
pymodbus:       time to read 1 x 100 (x 10 regs): 3.060 [s] / 0.031 [s/req]
modbus-tk:      time to read 1 x 100 (x 10 regs): 1.561 [s] / 0.016 [s/req]

真实应用:

Modbus-rpc桥的负载示例(~3%是由RPC服务器部分引起的)

  • 每秒同步读取5个64个寄存器,并同时进行异步访问,串口超时设置为0.018秒

  • modbus-tk:

    • 10个寄存器: {'currentCpuUsage': 20.6, 'requestsPerSec': 73.2} // 可以改进,请参见下面的EDIT部分
    • 64个寄存器:{'currentCpuUsage': 31.2, 'requestsPerSec': 41.91} // 可以改进,请参见下面的EDIT部分
  • pymodbus:

    • 10个寄存器: {'currentCpuUsage': 5.0, 'requestsPerSec': 36.88}
    • 64个寄存器: {'currentCpuUsage': 5.0, 'requestsPerSec': 34.29}

编辑: modbus-tk库可以很容易地改进以减少CPU使用率。在原始版本中,请求发送后,经过T3.5休眠,主机一次一个字节地组装响应。优化证明,大部分时间都花在串口访问上。通过尝试从串行缓冲区中读取预期的数据长度,可以改进此问题。根据pySerial文档,如果设置了超时时间,则应该是安全的(当响应丢失或太短时不会挂起):

read(size=1)
Parameters: size – Number of bytes to read.
Returns:    Bytes read from the port.
Read size bytes from the serial port. If a timeout is set it may return less characters as   
requested. With no timeout it will block until the requested number of bytes is read. 

在以下方式中修改 `modbus_rtu.py':

def _recv(self, expected_length=-1):
     """Receive the response from the slave"""
     response = ""
     read_bytes = "dummy"
     iterCnt = 0
     while read_bytes:
         if iterCnt == 0:
             read_bytes = self._serial.read(expected_length)  # reduces CPU load for longer frames; serial port timeout is used anyway 
         else:
             read_bytes = self._serial.read(1)
         response += read_bytes
         if len(response) >= expected_length >= 0:
             #if the expected number of byte is received consider that the response is done
             #improve performance by avoiding end-of-response detection by timeout
             break
         iterCnt += 1

在modbus-tk修改后,实际应用中的CPU负载明显下降,而性能损失不大(仍优于pymodbus):

更新的modbus-rpc桥负载示例(约3%由RPC服务器部分引起)

  • 每秒同步读取5个64个寄存器,并进行同时异步访问,串口超时设置为0.018秒

  • modbus-tk:

    • 10个寄存器:{'currentCpuUsage': 7.8, 'requestsPerSec': 66.81}
    • 64个寄存器:{'currentCpuUsage': 8.1, 'requestsPerSec': 37.61}
  • pymodbus:

    • 10个寄存器:{'currentCpuUsage': 5.0, 'requestsPerSec': 36.88}
    • 64个寄存器:{'currentCpuUsage': 5.0, 'requestsPerSec': 34.29}

19
我已经使用stackoverflow超过5年了,这可能是我在上面遇到的最好的回答。写得很好,解释了许多问题。我一直在尝试弄清楚为什么通过modbus进行慢速数据传输时,我可以通过115220波特率的纯串行读/写获得37k+的吞吐量。感谢您提供基准测试代码,我之前不知道modbus-tk。我已经使用pymodbus一段时间通过TCP,它运行得非常快而且很好用,但测试一些串行设备时速度很慢,但这个回答解决了我的一些问题,所以谢谢。 - xamox
感谢您的赞赏之词。我很高兴我的努力对他人有所帮助。顺便提一下,您提到的性能(每分钟37k次读写)看起来对于RTU标准来说是不现实的,因为对于波特率> 19000,它规定了1.75毫秒的T3.5,这几乎等于整个通信周期所需的时间(每分钟37k次读写≈617个周期/秒≈1.62毫秒/周期)。您是否使用Modbys二进制方式来实现这样的结果? - Mr. Girgitt
抱歉,我应该更具体一些。我使用的是pyserial,这就是我所说的直接串口。我只是在一台机器上写入时间戳,然后从另一台机器上读取。我认为它会更快,但并没有像以前运行得那么快。我测试了ASCII和RTU模式下的modbus,但没有看到太大的改进。我还通过pyserial进行了测试,以查看是否可能是瓶颈,因为我已经提到过在pymodbus中使用TCP可以运行得更快。我还测试了同步与异步服务器,只注意到异步有轻微的改进。 - xamox
在你的tk示例中 tkmc.execute(slaveId, tkCst.READ_HOLDING_REGISTERS, 0,regsSp),如何读取特定的保持寄存器或多个寄存器?以及如何打印响应? - mrid
@mrid 最后两个参数(... 0,regSp)告诉寄存器的范围:要读取的第一个寄存器的索引(0)和从第一个寄存器开始要读取的寄存器数量(regSp - 设置为10,因此调用将读取寄存器0-10)。有关如何处理响应的详细信息,请参阅单元测试:https://github.com/mushorg/modbus-tk/blob/master/tests/functest_modbus.py#L57 - 如您所见,execute方法返回一个元组,可以迭代等以打印其值。 - Mr. Girgitt
3
仅供参考,建议的改进已在modbus-tk中实施。感谢您的建议。 - luc

11
我刚刚发现了uModbus,对于像树莓派(或其他小型单板计算机)这样的部署来说,它是一个绝佳选择。它是一个简单、单一而又功能齐全的软件包,与pymodbus不同的是,它不会引入10多个依赖项。

uModbus看起来很好,代码结构清晰。它可以高效地读取串行接收缓冲区(期望字节数),如果您记得配置超时并自己处理串行端口的可用性(例如,在操作过程中重新连接USB串行适配器),则应该能够可靠地工作。它还可以解码Modbus异常代码。这就是我在没有进行长期测试的情况下所能告诉您的全部内容。 - Mr. Girgitt

7

这真的取决于您使用的应用程序以及您想要实现的内容。

pymodbus是一个非常强大的库。它能够正常工作,并提供了许多工具供您使用。但当您尝试使用它时,它可能会显得有些令人生畏。我个人发现使用它很困难。它提供了同时使用RTU和TCP/IP的功能,这非常好!

MinimalModbus是一个非常简单的库。最终,我选择使用它来解决我的问题,因为它恰好满足了我所需求的。它只支持RTU通信,并且据我所知,它的表现非常出色。我从未遇到过任何问题。

我从未研究过Modbus-tk,所以我不知道它处于什么位置。

但总的来说,这取决于您的应用程序。最终,我发现对我来说,Python并不是最好的选择。


-1

我使用Python 2.7.18和Python 3.8.10进行了一些测试。看起来pymodbus针对Python3进行了优化。

$ python2 test.py 
timeout: 0.018 [s]
umodbus:        time to read 1 x 1000 (x 16 regs): 53.261 [s] / 0.053 [s/req]
pymodbus:       time to read 1 x 1000 (x 16 regs): 65.648 [s] / 0.066 [s/req]
modbus-tk:      time to read 1 x 1000 (x 16 regs): 60.191 [s] / 0.060 [s/req]

$ python3 test.py 
timeout: 0.018 [s]
umodbus:        time to read 1 x 1000 (x 16 regs): 53.246 [s] / 0.053 [s/req]
pymodbus:       time to read 1 x 1000 (x 16 regs): 32.765 [s] / 0.033 [s/req]
modbus-tk:      time to read 1 x 1000 (x 16 regs): 60.352 [s] / 0.060 [s/req]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接