十二、串口服务器:RTU转TCP
12.1 典型应用场景
问题:工厂有200个Modbus RTU设备(8条RS485总线,每条25个设备),新MES系统只支持Modbus TCP。
解决方案:使用串口服务器(Serial Device Server)或多端口网关。
12.2 架构图
MES/SCADA (TCP Client)
│
├── 网关1 (IP: 192.168.1.10) ── RS485总线1 ── RTU设备(地址1-25)
│
├── 网关2 (IP: 192.168.1.11) ── RS485总线2 ── RTU设备(地址1-25)
│
├── 网关3 (IP: 192.168.1.12) ── RS485总线3 ── RTU设备(地址1-25)
│
└── 网关...
12.3 常见方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 单端口串口服务器 | 简单、便宜 | 每个总线一个IP | 少量RTU设备(<32) |
| 多端口网关 | 节省IP、管理方便 | 贵、配置复杂 | 大量RTU设备 |
| 软件网关(如MBpoll) | 灵活、可定制 | 需PC、稳定性一般 | 测试开发 |
| 嵌入式网关(Arduino/Pi) | 低成本、可控 | 需开发、维护 | 定制化需求 |
12.4 串口服务器配置要点
12.4.1 常见参数
工作模式: TCP Server
监听端口: 502
串口参数: 9600/8/N/1
打包时间: 10ms (或4ms)
打包字节: 200
连接模式: 允许最多5个TCP客户端
12.4.2 地址映射策略
| 策略 | 说明 | 示例 |
|---|---|---|
| 每个网关一个IP | 通过IP区分总线 | 网关1:192.168.1.10, 网关2:192.168.1.11 |
| 每个网关一个端口 | 通过端口区分总线 | 网关:502→总线1, 503→总线2 |
| 单元ID路由 | 通过MBAP单元ID区分 | 单元ID1-25→总线1, 26-50→总线2 |
12.5 实际部署坑点与解决
12.5.1 超时设置问题
问题:网关默认超时200ms,但RTU长距离19200bps下读32个寄存器需280ms。
解决:网关超时调整至500ms。
bash
# Moxa NPort配置示例
set timeout=500 # 毫秒
12.5.2 广播风暴
问题:MES误发功能码43(设备识别)扫描所有设备,导致总线阻塞。
解决:网关过滤未实现的功能码。
python
# 网关转发逻辑
def forward_filter(pdu):
func = pdu[0]
# 只转发常用功能码
if func in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10]:
return True
else:
# 返回异常响应(非法功能)
send_exception(pdu, 0x01)
return False
12.5.3 连接数限制
问题:多个MES客户端同时访问,但网关只允许1个TCP连接。
解决:使用支持多连接的网关(如Moxa NPort 5150支持5个连接),或前端加负载均衡。
12.6 Python软件网关示例
python
python
import socket
import serial
import threading
class ModbusRTUtoTCPGateway:
def __init__(self, serial_port, baudrate=9600, tcp_port=502):
self.serial = serial.Serial(serial_port, baudrate, timeout=0.1)
self.tcp_port = tcp_port
def start(self):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', self.tcp_port))
server.listen(5)
print(f"Gateway listening on port {self.tcp_port}")
while True:
client, addr = server.accept()
print(f"TCP client connected: {addr}")
threading.Thread(target=self.handle_client, args=(client,)).start()
def handle_client(self, client):
try:
while True:
# 接收TCP请求
mbap = client.recv(7)
if len(mbap) < 7:
break
trans_id, proto_id, length, unit_id = struct.unpack('>HHHB', mbap)
pdu = client.recv(length - 1)
# 转换为RTU帧
rtu_frame = bytes([unit_id]) + pdu
crc = modbus_crc(rtu_frame)
rtu_frame += crc.to_bytes(2, 'little')
# 发送到串口
self.serial.write(rtu_frame)
# 等待RTU响应(3.5字符间隔)
time.sleep(0.004) # 4ms
response = self.serial.read(256)
# 解析RTU响应
if len(response) < 5:
continue
# 验证CRC,提取数据
recv_crc = (response[-2] << 8) | response[-1]
calc_crc = modbus_crc(response[:-2])
if recv_crc != calc_crc:
continue
# 转换为TCP响应
resp_unit_id = response[0]
resp_pdu = response[1:-2]
resp_mbap = struct.pack('>HHHB', trans_id, 0, len(resp_pdu)+1, resp_unit_id)
# 发回TCP客户端
client.send(resp_mbap + resp_pdu)
except Exception as e:
print(f"Error: {e}")
finally:
client.close()
# 启动网关
gateway = ModbusRTUtoTCPGateway('/dev/ttyUSB0', 9600, 502)
gateway.start()