Python Modbus-RTU 串口编程中结构数据收发的相关问题


目录

  • Python Modbus-RTU 串口编程中结构数据收发的相关问题
  • 一、引言
  • 二、问题
  • 三、解决方案


一、引言

    异步串口通信往往是以字符(字节)为单位进行的,但在很多情况下,需要用串口收发具有规定结构的一组数据(数据帧或数据包),例如 Modbus-RTU 的数据帧。为了防止丢失数据或粘包的现象发生,往往会采用以下几种措施:
   1. 使用定界符来区隔数据帧或数据包,这种方法对于ASCII字符的传输是比较有效的,但其问题是不支持完全透明的二进制数据传输。
   2. 在两个连续发送的数据帧或数据包之间设置保护时间间隔,保证收发的完整性,其缺点是会增加时间开销,降低收发能力。
   3. 采用请求 - 响应的方式进行数据收发,它适合在一个接口上有多个发送源的场合,Modbus-RTU 就采用这种方式。
   理论上,采用上述措施以后,可以在传输中保证数据结构的完整性。但是,在实现接口程序中我们发现,在这方面有些编程的具体问题还需要解决。

二、问题

    如前所述,Modbus-RTU 是用 请求 - 响应方式进行通信的(Modbus-TCP 也一样)。在一个 RS-485 串行接口上,有一个 Modbus 主站和一个或多个 Modbus 从站,在交互过程中,由主站发送命令,命令中带有从站标识,标识所指定的从站收到命令后发送响应。
    用 Python 编程实现 Modbus-RTU 主站通信模块的流程如下:
    1. 等待调用者从队列发送的请求;
    2. 收到请求后,向串行接口发送一个Modbus-RTU 命令帧;
    3. 等待从站的响应;
    4. 收到从站完整的响应帧以后,将其转发给调用者。
   下面是这段程序的实现(用 pyserial_asyncio 模块):

import serial_asyncio
......

class SerialModbusRTU
......

    async def write_to_serial(self):
        while self.running:
            # 从其他线程输入的请求在 self.north_input_queue 中,为防止异步协程被阻塞,
            # 只有其不为空时才进行后续操作
            while self.north_input_queue.empty():
                await asyncio.sleep(0.05)
			# 从队列中获取调用者的请求数据(Modbus-RTU 命令帧)
            frm = self.north_input_queue.get()
            try:
            	# 将命令帧发送给从站
                self.writer.write(frm)
                await self.writer.drain()
                # 等待从站的响应,设置超时时间为4秒
                data = await asyncio.wait_for(self.reader.read(1000), timeout=4)
                # 将响应收到的数据从另一个队列返回给调用者
                local_var.fw_south_input_queue.put(data)
                # 如果超时,返回超时报告(编码为1)
                except TimeoutError as e:
                    print(self.module_name + '.write_to_serial ', e)
                    data = make_error_report(self.module_code, 1)
                    local_var.fw_south_input_queue.put(data)
                # 发生其他错误,报告其他错误(编码为2)
                except Exception as e:
                    print(self.module_name + '.write_to_serial ', e)
                    data = make_error_report(self.module_code, 2)
                    local_var.fw_south_input_queue.put(data)
                    self.running = False
                    self.writer.close()

   这段程序开始运行时比较正常,没有发现什么问题。但在与一种新的传感设备连接时,却发现时常有接收到不完整帧的现象发生。经过硬件测试分析,发现这种传感设备在发送数据时,会有几个毫秒的停顿,这时,就会发生接收不完整的现象。

三、解决方案

   为了解决上述问题,最直接的方法就是对接收到的数据进行解析,如果没有完成,就继续等待,直至收到完整的数据帧。
   在分析中发现,在通信过程中这种停顿的时间很短,只有一两个毫秒,而且一次交互中返回的数据包也不是很长,一般只有十几到一百多个字节。根据这种情况,我们采用了一种简化方法,具体做法如下:
   完成第一次接收以后,再一次启动等待接收,调整接收的超时时间,使得既不漏掉停顿后发送的数据,也不至于等待时间过长而引起的通信效率下降。修改后的程序如下(见其中“增加的代码段”和“增加结束”两个注释之间的代码):

async def write_to_serial(self):
        while self.running:
            # 从其他线程输入的请求在 self.north_input_queue 中,为防止异步协程被阻塞,
            # 只有其不为空时才进行后续操作
            while self.north_input_queue.empty():
                await asyncio.sleep(0.05)
			# 从队列中获取调用者的请求数据(Modbus-RTU 命令帧)
            frm = self.north_input_queue.get()
            try:
            	# 将命令帧发送给从站
                self.writer.write(frm)
                await self.writer.drain()
                # 等待从站的响应,设置超时时间为4秒
                data = await asyncio.wait_for(self.reader.read(1000), timeout=4)
                
                # 增加的代码段
                try:
                	# 第二次接收,接收停顿后发送的部分数据,超时设为20ms,需要根据具体情况选择
					data = data + await asyncio.wait_for(self.reader.read(1000), timeout=0.02)
	=			except TimeoutError as e:
					# 第二次接收的超时处理,如果超时,表示没有停顿,接收完成,打印即可
					print(‘接收完成’)
                # 增加结束
                
                # 将响应收到的数据从另一个队列返回给调用者
                local_var.fw_south_input_queue.put(data)
            # 如果超时,返回超时报告(编码为1)
            except TimeoutError as e:
                print(self.module_name + '.write_to_serial ', e)
                data = make_error_report(self.module_code, 1)
                local_var.fw_south_input_queue.put(data)
            # 发生其他错误,报告其他错误(编码为2)
            except Exception as e:
                    print(self.module_name + '.write_to_serial ', e)
                    data = make_error_report(self.module_code, 2)
                    local_var.fw_south_input_queue.put(data)
                    self.running = False
                    self.writer.close()

   修改后,就能够确保接收到完整的Modbus数据帧了。如果修改后仍然会收到不完整的数据包,可以延长第二次读串口的等待时间(程序中是20ms)。
   相对于加入数据包解析方法,这种解决方案简单易行,降低了程序的复杂性。与此同时,由于不需要解析数据,所以它对数据格式的适应性也很好。
   这种方法也适用于引言中的第 2 种情况。
   应当注意的是:和解析的方式相比,使用这种方法的限制是需要两个数据包之间有保护时间,而且第二次读串口的等待时间要小于数据包之间的保护时间。