一种异种网络Modbus软件网关的实现
摘 要:针对Modbus网关大部分产品为硬件实现成本高,可扩展性差且功能单一的问题,提出了一种在基于软件实现Modbus网关的方法。该方法支持异种网络设备拓扑架构,支持多进程处理进一步提高Modbus协议的吞吐量。同时在嵌入式Linux和Arm硬件平台架构上对该方法的正确性进行了检测验证。
关键词:Modbus;软件网关;异种网络;吞吐量
中图分类号:TP273 文献标识码:A 文章编号:2095-1302(2015)01-00-04
0 引 言
Modbus协议作为一种在工业控制领域广泛使用的总线协议,其有着标准、开放,支持多种电气接口,以及帧格式简单、紧凑、通俗易懂等优点。据不完全统计,自Modbus协议1979年面世以来,截止2007年,其已被应用在了超过1 000万个设备节点中。
由于Modbus协议本身支持TCP和RTU两种链路连接方式。对于TCP,其基于以太网为物理连接链路。而对于RTU,一般基于RS 232或者RS 485作为物理链路连接。对于Modbus设备节点的组网,通常会基于同种物理链路,基于TCP方式的组网,可以使用以太网交换机来完成。而对于RTU方式的组网,RS 485通过双绞线直接串接在一起即可。
但由于实际应用场景中,通常会结合两种网络的优点。如以太网传输速度快,具有高吞吐率的特点,RS 485具有组网简单,传输距离长的特点。通常采用Modbus硬件网关来结合两种网络组成异种网络拓扑结构。但硬件网关具有接口数目固定,成本高,不便扩展等缺点。
本文针对这些实际应用和产品维护期间遇到的问题,设计实现了一种针对异种网络的,设置灵活,性能可靠,易扩展,低成本的软件网关实现方法。
1 总体架构设计
Modbus协议通常用在如油田、车间等有多设备需要进行组网且有复杂工况的环境下。多台上位机可以通过RJ45连接到以太网交换机,其通过1502端口发送标准的TCP数据包到以太网交换机。运行有Modbus网关的Modbus Server连接到以太网交换机,其接收Modbus TCP数据请求并分析目的地址后将该数据请求分发到对应的Modbus Server中。数据分发时,组件对应的RS 485数据包并通过485总线传递数据。具体结构如图1所示。
图1 Modbus异种网络组网拓扑图
一种在实际生产环境中经常会见到的用例是:在油田监控网络中,上位机运行于监控室监控各个油井抽油机的状态。而Modbus Server作为安装在每一个油井抽油机上的监控装置用向采集油井状态并向上位机汇报。运行有Modbus网关的Modbus Sever则可作为井场主监控装置起到一个数据转发的作用。由于油井之间的距离长度通常会超过100 m,因此采用485串行物理链路的方式会更便于油井采集器之间的组网。而各个油井设备的数据汇聚到了主采集器之后,其过大的数据量对于485串行链路而言负担较重,容易丢失数据。因此主采集器采用以太网物理链路与上位机连接。
1.1 网关结构
Modbus网关运行于Modbus Server中,可根据配置文件来配置为是否启动网关。启动了网关的Modbus Server本身和其它Modbus Server设备一样,也可以提供数据采集功能。
为了能接收多个上位机的数据请求,运行了Modbus网关的Modbus Server会启动一个支持多路输入的Socket服务器用于监听上位机的TCP数据请求。对于请求本机地址的数据,将直接返回相关数据。而对于其它Modbus Server地址的数据请求,将被网关中的数据分发器Dispatcher通过启动一个独立进程的方式分发到Sub-Modbus Server中。为保证数据多个数据请求之间不会在485总线网络中造成冲突,在数据分发器启动的多个数据处理进程和Sub-Modbus Servers之间,会存在一个总线锁。Modbus网关结构如图2所示。
图2 Modbus网关结构
1.2 目的地址传递
Modbus协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。特定总线或者网络上的Modbus协议映射能够在应用数据单元(ADU)上☢引入一些附加域,如地址域或差错校验域。Modbus通用数据帧如图三所示。
图3 Modbus通用数据帧
一个ADU最大长度为256 B。对于TCP通信链路,通常其ADU不包含差错校验部分,而会包含一个7 B的Modbus报文头(MBAP)。MBAP的组成为2 B的事务元标识符,2 B的协议标识符以及2 B的数据包长度和1 B的单元标识符。通常☒情况下,Modbus网关中在向串行链路设备转发数据时的设备地址就保存在单元标识符中。TCP通信链路下,ADU组成为:
TCP MODBUS ADU = 249 B+ MBAP (7 B) = 256 B
对于串型链路通信来说,其地址域为一个字节长度。而差错校验部分ฝ存储的数据通过CRC16算法所得,其数据长度为2 B。因此最大PDU长度为253 B。一个最大长度的ADU组成为:
RS 232 / RS 485 ADUฌ = 253 B + 服务器地址(1 B) + CRC (2 B) = 256 B。
1.3 数据包解析
Modbus定义了4种具有不同特征的数据模型,分别是:
(1)离散量输入,为只读的单个比特位;
(2)线圈变量,为可读写的单个比特位; (3)输入寄存器,为只读的2 B;
(4)输出寄存器,为可读写的2 B。
对于这4种数据,Modbus协议都允许单个选择65 536个数据项,但实际而言,数据的大小规格限制和事务处理的功能码是相关联的。在本实现中,取工业控制领域常用的8个功能码来作为数据网关中处理的数据请求,如下:
0x01 读线圈
0x02 读输入离散量
0x03 读多个输出寄存器
0x04 读多个输入寄存器
0x05 写单个线圈
0x06 写单个寄存器
0x0F 写多个线圈
0x10写多个寄存器
以读多个输出寄存器为例。读数据时,请求PDU中需指定输出寄存器的起始地址和读取数量。一个读取输出寄存器第108到110的3个寄存器的数据如表1所示。
表1 读输出寄存器
请求 响应
名称 数值(十
六进制) 名称 数值(十
六进制)
功能码 03 功能码 03
起始地址(高) 00 数据长度 06
起始地址(低) 6B 寄存器值(高)- 108 02
读取寄存器个(高) 00 寄存器值(低)- 108 2B
读取寄存器个(低) 03 寄存器值(高)- 109 00
寄存器值(低)- 109 00
寄存器值(高)- 110 00
寄存器值(低)- 110 64
在网关中对数据解析时,对于不同的功能码,应根据Modbus协议中的数据格式定义来对数据包进行解析。
2 模块功能详细设计
2.1 Socket服务器模块设计
软件网关与上位机之间通过TCP进行连接,网关将启动一个socket服务器监听来自上位机的Modbus客户端的请求。由于会存在多台上位机,socket服务器在设计中需考虑可同时接受多个TCP请求。而对于TCP请求,在一条链路已经建立以后,来自同一台上位机的再次请求应不需要再次进行链路的建立。
首先对于一个未建立过连接的TCP请求,需要为该请求创建必要的工作环境并保存环境。考虑到网关的处理能力,对于允许的最大连接数将通过配置文件进行读取。
// Clear the reference set of socket
FD_ZERO(refset);
// Add the server socket
FD_SET(server_socket, refset);
// Keep track oฏf the max file descriptor
fdmax = server_socket;
for (;;) {
rdset = refset;
select(fdmax+1, rdset, NULL, NULL, NULL);
// Run through the existing connections looking for data to be read
for (master_socket = 0; master_socket master_socket++) {
if (FD_ISSET(master_socket, rdset)) {
if (master_socket == server_socket) {
/* A client is asking a new connection */
memset(clientaddr, 0, sizeof(clientaddr));
newfd = accept(server_socket, (struct sockaddr *)clientaddr, addrlen);
if (newfd == -1) { perror(“Server accept() error”); }
else { FD_SET(newfd, refset);
if (newfd fdmax) {
fdmax = newfd; /* Keep track of the maximum */
} } }
}
}
}
而对于一个已经建立好的连接,则需要调用转发函数将其转发到对应的子设备中去。对于目的地址为网关设备本身的,需要根据上位机的请求,将网关设备本身采集到的数据返回给上位机或者完成对应的写数据操作。在对于网关设备本身的操作中,由于modbus数据通常为由一个独立的进程采集并存放到共享内存中,因此在操作共享内存时,需使用信号量来保证数据的正确性。
/* An already connected master has sent a new query */
modbus_set_socket(ctx, master_socket);
rc = modbus_receive(ctx, query);
if (rc 0) {if (query[header_length -1] != config-server_id) {