Modbus从机(服务器)通讯设计

摘要:本文在国家标准GB/T 19582-2008的框架下,讨论Modbus协议在串行链路RS485以及TCP/IP上的实现过程和注意事项。涉及到Modbus帧界定、lwip协议栈移植等关键内容,对于难度较大的读写多个线圈命令,本文给出了关键源代码。

1. 简介

从1979年开始,Modbus作为工业串行链路的事实标准,Modbus使成千上万的自动化设备能够通信。目前,对简单而精致的Modbus结构的支持仍在增长。互联网用户能够使用TCP/IP栈上的保留系统端口502访问Modbus。

Modbus通信可以使用串行链路和以太网上的TCP/IP这两种方式实现。本文使用两种物理层进行Modbus通讯,Modbus在串行链路上的实现是通过RS485接口,在TCP/IP上的实现是通过以太网接口。

  • RS485物理层由EIA/TIA-485标准规定,此外还需满足国标GB/T 19582.2-2008的第7部分的要求。
  • 以太网物理层由IEEE 802.3标准规定

本文不对Modbus协议层做过多描述,假设读者了解基本的Modbus协议,已经通读过GB/T 19582.1-2008、GB/T 19582.2-2008、GB/T 19582.3-2008文档。

2. Modbus串行链路

Modbus通信可以在串行链路上实现,从数据流的角度来看,其本质是在各种介质上的异步串行传输。Modbus也有自己的帧格式,串行链路的数据链路层的主要工作是将收到的数据打包成帧交给应用层处理并将应用层处理后的数据发送出去。

2.1帧格式

每帧数据均包括地址码、功能码、数据区和检验,如图2-1所示。最大帧数据长度为256字节,整个帧采用CRC16校验。CRC16描述详见国标GB/T 19582.2-2008的附录B。

2.2帧界定

帧与帧间隔时间至少为3.5个字符传输时间,我们根据这个时间来判定是否一帧结束。波特率为1200时,这个时间约为35ms;波特率越大,需要的定时器越精细,为了减轻CPU负担,当波特率大于19200bit/s,这个时间固定为1.75ms。

帧中的字符与字符之间的空闲间隔不得小于1.5个字符传输时间,如果超过该时间,整个帧丢掉。当波特率大于19200bit/s,同样为了减轻CPU负担,这个时间固定为750us。

为了获取一帧数据,软件需要维护两个定时值:帧与帧间隔定时以及一帧当中字符与字符间隔定时。可以使用一个硬件定时器来完成帧与帧间隔定时以及一帧当中字符与字符间隔定时的判定,硬件定时器的定时间隔与波特率有关,波特率越大,定时间隔应越小。以1200bps为例,1ms的定时间隔就足够了,但如果波特率在19200bps以上,就需要更细粒度的定时器,比如100us的定时器。

本文以1200bps为例,给出获取一帧数据的伪代码。

2.2.1 定义数据结构以及变量:

1./*Modbus帧接收结构体*/  
2.#define FARME_END_FALG 0x5A  
3.#define START_REC_TIMER 0x5A  
4.typedef struct{    
5.    uint16_t rec_end_flag;      //帧结束标志,为FARME_END_FALG时找到完整帧  
6.    uint16_t rec_len;           //帧长度,包括地址码和校验  
7.    uint32_t rec_timer_flag;    //启动定时器标志,为START_REC_TIMER启动定时器  
8.    uint32_t rec_timer;         //定时器计数值  
9.    uint8_t  *rec_data;         //指向接收缓冲区  
10.}modbus_rec_struct;

2.2.2 定义数据变量

modbus_rec_struct  modbus_rec; 

2.2.3初始化数据结构

使用前必须先对数据结构初始化,一般放在串口初始化中。

1./** 
2.* @brief    初始化modbus 接收数据结构体 
3.* @param    p_modbus_rec:指向Modbus接收数据结构变量 
4.* @param    rec_buf:指向接收缓冲区 
5.*/  
6.void init_modbus_data_str(modbus_rec_struct *p_modbus_rec,uint8_t * rec_buf)  
7.{  
8.    p_modbus_rec->rec_len=0;  
9.    p_modbus_rec->rec_end_flag=0;  
10.    p_modbus_rec->rec_timer_flag=0;  
11.    p_modbus_rec->rec_timer=0;  
12.    p_modbus_rec->rec_data=rec_buf;  
13.} 

2.2.4 串口接收中断

在串口接收中断中,一方面要进行数据接收,另一方面要判断一帧中字符与字符间隔不大于1.5个字符传输时间。

1./** 
2.* 串口中断服务函数 
3.*/  
4.void modbus_irq_handler(void)  
5.{  
6.    //其它状态判断  
7.      
8.    if(有数据需要接收)  
9.    {       
10.        modbus_rec.rec_timer_flag=START_REC_TIMER;  
11.        if(modbus_rec.rec_timer>1.5个字符超时时间)      //1.5个字符超时,丢弃帧  
12.        {  
13.            modbus_rec.rec_len=0;  
14.        }  
15.          
16.        if(接收缓存未越界)  
17.        {  
18.            modbus_rec.rec_data[...]=接收数据;  
19.        }  
20.        else  
21.        {  
22.            //其它处理  
23.        }  
24.        modbus_rec.rec_timer=0;  
25.    }  
26.}

2.2.5 定时器中断

一帧是否结束,是在定时器中判断。

1./** 
2.* 定时器处理函数,1毫秒调用一次 
3.* 在定时器中断中调用,判断帧是否结束.modbus要求帧结束的标志为总线上有3.5个字符传输时间空闲 
4.*/  
5.void modbus_handle_timer(void)  
6.{  
7.    if(modbus_rec.rec_timer_flag==START_REC_TIMER)  
8.    {  
9.        modbus_rec.rec_timer++;  
10.    }  
11.    else  
12.    {  
13.        modbus_rec.rec_timer=0;  
14.    }  
15.      
16.    if(modbus_rec.rec_timer>超时时间)        //找到一帧数据  
17.    {  
18.        modbus_rec.rec_timer_flag=0;  
19.        modbus_rec.rec_timer=0;  
20.        modbus_rec.rec_end_flag=FARME_END_FALG;  
21.          
22.        //其它处理  
23.    }  
24.} 

3. Modbus TCP

Modbus协议在TCP/IP上的实现是在TCP/IP协议层上的应用,它需要一个完整的TCP/IP协议栈做支撑。

Modbus帧由TCP层提供,不需要像串行链路那样自己判断一帧是否结束,所有数据传输由TCP/IP层处理,Modbus帧结构见图3-1所示,最大帧数据长度为260字节。与串行链路相比,Modbus TCP帧多了MBAP报文头,少了地址码和校验字段。         因为TCP本身就是被设计为安全交付型协议,所以校验部分就交给TCP层来处理。Modbus TCP通过IP地址以及端口号来唯一确定一个设备,此外在MBAP报文头中也包含一个协议标识符字段,在获取到数据帧后,需要判断这个字段值是否为0。

其中,MBAP报文头包括下列字段,见表2-1

表2-1 MBAP报文头的字段

字  段 长 度 描  述 客 户 机 服 务 器
事务处理标识符 2字节 Modbus请求/响应事务处理的识别 由客户机设置 服务器从接收的请求中重新复制
协议标识符 2字节 0=Modbus 由客户机设置 服务器从接收的请求中重新复制
长度 2字节 随后字节的数量(包括单元标识符) 由客户机设置 由服务器设置(响应)
单元标识符 1字节 串行链路或其他总线上连接的远程从站的识别 由客户机设置 服务器从接收的请求中重新复制

3.1 lwIP协议栈

Modbus TCP需要一个完整的TCP/IP协议栈做支撑,目前公司使用TCP/IP协议栈多为lwIP协议。该协议栈专为嵌入式系统而设计,可在资源匮乏的微控制器上实现完整的可裁剪的TCP/IP协议。下面简单介绍一下lwIP移植。

3.1.1编写与编译器相关的头文件

头文件cc.h主要完成协议栈内部使用的数据类型定义,用户需要根据自己使用的编译器和处理器特性来定义好这些数据类型长度;除此之外,cc.h文件还要完成临界代码保护、协议栈调试信息输出相关宏、大小端定义等。

3.1.2编写与硬件的接口函数

lwIP协议栈已经在ethernetif.c文件中给出了硬件接口函数的原型,一共5个函数的框架,包括函数名、函数参数、函数内容等,用户需要完成这5个函数的编写。当然,你也可以按照自己的需求来编写底层硬件接口函数,不必和ethernetif.h文件中给出的函数相同。大多数应用是以这5个函数为基础的,所以我们这里来讨论一下这五个函数。

3.1.2.1 网卡初始化

1. static void low_level_init(struct netif *netif)

主要完成对底层硬件MAC和PHY芯片的初始化工作,此外还需要设置协议栈网络接口管理结构netif中与网卡属性相关的字段,比如网卡MAC地址长度等。

3.1.2.2 数据包发送

1. static err_t low_level_output(struct netif *netif,struct pbuf *p)

将内核数据结构pbuf描述的数据包发送出去。

3.1.2.3 数据包接收

1. static struct pbuf * low_level_input(struct netif *netif)

要将网卡接收的数据封装到内核能识别的pbuf形式。

3.1.2.4 接收数据初步解析

1. static void ethernetif_input(struct netif * netif)

调用数据包接收函数low_level_input从网卡处读取一个数据包,然后解析该数据包类型(ARP或IP包),最后将该数据包递交给相应的上层。对于一般无操作系统应用,该函数可以直接使用。

3.1.2.5 底层初始化

1. err_t ethernetif_init(struct netif * netif)

由协议栈自动调用该函数,主要完成netif结构中某些初始化,并最终调用low_level_init完成对网卡的初始化。对于一般应用,该函数可以直接使用。

3.2 lwIP 应用关键事项

对于Modbus TCP应用,使用lwIP协议栈还需要一些额外注意的点:

  • 禁止Nagle算法,以便允许lwIP发送小数据包。如果不禁用,lwIP的默认机制会尽量等到更多的数据,再一起发送,这样不利于控制的实时性。lwIP提供了禁用Nagle算法的函数。
  • 使能并更改保活机制。客户端和服务器建立连接后,如果二者长时间进行通讯,服务器会发送探测包,来检测客户端是否还在线,如果这时客户端没有响应服务器的探测包,服务器端会释放相应的资源,以便接受其它客户端连接。但默认情况下,lwIP并没有开启这个功能,需要手动开启。

4. Modbus应用层

无论是Modbus串行链路还是Modbus TCP接收到的一帧数据,都要将协议数据单元(PDU,即图2-1和图3-1的功能码及数据区域部分)从一帧中剥离出来提交给Modbus应用层处理,应用层根据功能码来执行相应的操作。Modbus有着众多功能码,其中某些功能码还具有子码,可以根据具体应用来实现这些功能码的全部或一部分,一般数据访问功能码都是需要实现的。

关于功能码的描述可以参考GB/T 19582.1-2008,下面介绍几个实现起来稍繁琐的功能码,并给出关键源代码。

4.1读线圈

主机或客户端使用该功能码从远程设备中读取1~2000个连续的线圈状态,请求帧中会指定第一个线圈地址和线圈数目。从机或服务器需要将每位一个线圈进行打包,第一个数据字节的LSB包含包含询问中所寻址的输出。其它线圈依次类推,一直到这个字节的高位为止,并在后续字节中按照从低位到高位的顺序排列。如果返回的输出数量不是8的倍数,将用零填充最后数据字节中的剩余位。

如果从机(客户端)设备的每一个线圈都占用1个字节存储,那么要应答读线圈是很简单的,这么做的好处是简化数据处理逻辑,提高响应速度;坏处是占用的RAM会增多。但如果线圈数量比较少,而微控制器的RAM又足够多的话,非常推荐这么处理。

1./** 
2.* @brief    将要返回的线圈状态打包,在设备中每个线圈占1位 
3.* @param    coil_num:线圈数量 
4.* @param    src_data:指向第一个线圈所在的字节地址,按照字节读取 
5.* @param    send_buf:指向打包后的线圈存放地址,按照字节存放 
6.*/  
7.void pack_coils(uint16_t coil_num,uint8_t *src_data,uint8_t *send_buf)  
8.{  
9.    uint32_t tmp_data =0;  
10.    uint32_t data_addr=0;  
11.    uint32_t send_data_offset=0;  
12.    uint32_t i;  
13.    while(coil_num)  
14.    {  
15.        if(coil_num>=8)  
16.        {  
17.            for(i=0;i<8;i++)  
18.            {  
19.                if(src_data[data_addr++])  
20.                {  
21.                    tmp_data += 1<<i ;  
22.                }                    
23.            }   
24.            send_buf [send_data_offset++]=tmp_data;  
25.            tmp_data =0;  
26.            coil_num -= 8;                  
27.        }  
28.        else  
29.        {  
30.            for(i=0;i<coil_num ;i++)  
31.            {  
32.                if(src_data[data_addr++])  
33.                {  
34.                    tmp_data += 1<<i ;  
35.                }   
36.            }  
37.            send_buf [send_data_offset++]=tmp_data;  
38.            coil_num =0;                 
39.        }                       
40.    }  
}

有些时候线圈数量非常多或者微控制器RAM资源紧张的情况下,一个线圈占用一个位存储空间,微控制器内的RAM通常是最小按照字节访问的,这样一字节存储空间可以放8个线圈。如果按照这种位存储,打包程序会变得繁琐,要考虑各各种情况,比如起始地址是否从整字节开始,线圈数量是否大于8个等等。下面给按位存储情况下的打包示例程序。

1./** 
2.* @brief    将要返回的线圈状态打包,在设备中每个线圈占1位 
3.* @param    start_addr:线圈起始地址 
4.* @param    coil_num:线圈数量 
5.* @param    src_data:指向第一个线圈所在的字节地址,按照字节读取 
6.* @param    send_buf:指向打包后的线圈存放地址,按照字节存放 
7.*/  
8.void pack_coils(uint16_t start_addr,uint16_t coil_num,uint8_t *src_data,
9.                  uint8_t *send_buf)  
10.{  
11.    uint32_t data_addr=0;  
12.    uint32_t send_data_offset=0;  
13.      
14.    if(start_addr%8==0)         //从整字节开始  
15.    {             
16.        while(coil_num)  
17.        {  
18.            if(coil_num>=8)  
19.            {  
20.                send_buf[send_data_offset++]=src_data[data_addr++];  
21.                coil_num-=8;  
22.            }  
23.            else  
24.            {  
25.                send_buf[send_data_offset]=src_data[data_addr];  
26.                send_buf[send_data_offset]&=((1<<coil_num)-1);  
27.                coil_num=0;  
28.            }  
29.        }  
30.    }  
31.    else                        //非整字节开始  
32.    {  
33.       uint32_t bit_nonint,bit_remainder,read_data,tmp_data;  
34.          
35.        bit_remainder =start_addr%8;            //先处理非整字节的位  
36.        bit_nonint=8-bit_remainder;  
37.        if(bit_nonint<coil_num)  
38.        {                  
39.            read_data=src_data[data_addr++];  
40.            tmp_data = read_data>>bit_remainder;  
41.            coil_num-=bit_nonint ;  
42.            while(coil_num)                               
43.            {  
44.                if(coil_num>8)  
45.                {  
46.                    read_data=src_data[data_addr++];  
47.                    tmp_data+=read_data<<bit_nonint;  
48.                    send_buf[send_data_offset++]=tmp_data;   //够1字节则存盘  
49.                    tmp_data=read_data>>bit_remainder;  
50.                    coil_num-=8;  
51.                }  
52.                else  
53.                {  
54.                    read_data=src_data[data_addr++];  
55.                    if(coil_num>bit_remainder)  
56.                    {                             
57.                        tmp_data+=read_data<<bit_nonint;  
58.                        send_buf[send_data_offset++]=tmp_data;     
59.                        tmp_data=read_data>>bit_remainder;  
60.                        tmp_data &=((1UL<<(coil_num-bit_remainder))-1);  
61.                        send_buf[send_data_offset]=tmp_data;                   
62.                    }  
63.                    else  
64.                    {  
65.                        tmp_data+=(read_data & ((1UL<<coil_num)-1))<<bit_nonint ;  
66.                        send_buf[send_data_offset++]=tmp_data;  
67.                    }  
68.                    coil_num=0;  
69.                }  
70.            }  
71.        }  
72.        else          
73.        {  
74.            read_data=src_data[data_addr];  
75.            tmp_data = read_data>>bit_remainder;  
76.            tmp_data &=((1UL<<coil_num)-1);                
77.            send_buf[send_data_offset]=tmp_data;  
78.        }             
79.    }  
80.}

4.2 读离散量和写多个线圈

读离散量和写多个线圈这两个功能码的实现所遇到的问题跟读线圈类似,从机(服务器)设备都可以用不同的方法存储离散量和线圈,可以选择按字节存储,也可以选择按位存储,两种方法各有优点。如果设备条件允许,建议按照字节存储。由于实现方法类似于读线圈,所以不再给出打包(解包)源码。

5. 总结

本文介绍Modbus 串行链路和Modbus TCP的从机(服务器)设计的关键点,虽然有众多的文献、论文涉及到该内容,但本文不局限于文字,还以源码或伪代码的形式给出参考例程,可以帮助开发者更快的理解Modbus协议,缩短开发周期。

时间: 2024-10-19 04:43:44

Modbus从机(服务器)通讯设计的相关文章

一种高性能网络游戏服务器架构设计

网络游戏的结构分为客户端与服务器端,客户端采用2D绘制引擎或者3D绘制引擎绘制游戏世界的实时画面,服务器端则负责响应所有客户端的连接请求和游戏逻辑处理,并控制所有客户端的游戏画面绘制.客户端与服务器通过网络数据包交互完成每一步游戏逻辑,由于游戏逻辑是由服务器负责处理的,要保证面对海量用户登录时,游戏具有良好的流畅性和用户体验,优秀的服务器架构起到了关键的作用. 1  服务器架构设计 1.1  服务器架构分类 服务器组的架构一般分为两种:第一种是带网关服务器的服务器架构:第二种是不带网关服务器的服

大数据时代,计算模式从客户机/服务器到节点的转变

在数据库时代,计算机在分布体系中的角色有明确划分,不是客户机就是服务器,通常是一台服 务器连着多台客户机,服务器承担存储和计算的工作,客户机负责显示服务器的处理结果.高性能的计算机,比如小型机会被做为服务器,低端的计算机,如个人计 算机成为客户机.这就是以前经常说的Client/Server(客户机/服务器)结构. 到了大数据时代,这种角色已经悄然发生了变化.客户机/服务器的概念已经模糊化,被"节点"的概念取代.而这种变化的原因,归根结底,还是数据处 理需求发生了本质变化,迫使数据处理

关于大型web服务器的设计思路

大型网站,比如门户网站,在海量用户访问.高并发请求方面,基本的解决方案是以下几点:  1.高性能的数据库(oracle/db2/mysql...)  2.高性能的Web容器(weblogic/apache...)  3.高效率的编程语言(java/C#)  4.使用高性能的服务器(小型机.PC服务器)  5.集群分布式运行(比如上百台小型机器在线运行) 但是在在线用户上百万,日点击量超亿,而数据达几十T,甚至日数据量就达到T级别这种情况下还是难以解决大型网站面临的高负载和高并发问题.   本人也

服务器的设计与实现(三)——FTP服务器之设计与实现

在实现了Http服务器之后,本人打算再实现一个Ftp服务器.由于Ftp协议与Http一样都位于应用层,所以实现原理也类似.在这里把实现的原理和源码分享给大家. 首先需要明确的是Ftp协议中涉及命令端口和数据端口,即每个客户端通过命令端口向服务器发送命令(切换目录.删除文件等),通过数据端口从服务器接收数据(目录列表.下载上传文件等).这就要求对每个连接都必须同时维护两个端口,如果使用类似于上一篇文章中的多路IO就会复杂很多,因此本文采用了类似Apache的多进程机制,即对每个连接创建一个单独的进

多机串口通讯

★使用器件 使用了3块80c51的单片机,其中U1为主机控制其他两个从机U2,U3.每个单片机上都有一个数码管用来显示数据.主机上有两个按键KEY_1,KEY_2,分别用来控制不同的从机. ★实现目标 主要实现的目标就是通过写多机通讯来了解他们其中的协议,以及简单协议的写法!本程序主要达到了一下效果,主机可以通过发送命令来控制从机:发送数据给从机.接收从机的数据.然后将从机或者主机显示的数据显示在数码管上. ★协议要求 1.地址:主机的地址设置为0x01,从机1(U3)的地址为0x03,从机2(

简单的客户机服务器投射模拟

下面模拟了,简单的客户机服务器投射模拟的过程.客户机像服务器发送数据,服务器接受到数据后,发送回给客户机.再由客户机打印出来. 需要的函数: 网络方面 服务器 socket(AF_INET,SOCK_STREAM,0); AF_INET表示IPV4,SOCK_STREAM表示基于字节流的,0表示协议由前面两个参数组合而成.返回描述符 bind(sockdf,(struct sockaddr*)servaddr,sizeof(servaddr)); 用于把描述符与本地协议地址联系起来. liste

升讯威微信营销系统开发实践:(3)中控服务器的设计 .Net 还是 Java?

.Net 还是 Java?  :) 最近园子里又出现了.Net 和 Java 的口水贴,如果你觉得本文的内容根本就是 a piece of cake,不值一提,轻轻松松就能码出可靠健壮的实现,或许还可以讨论下.Net 和 Java 的问题,否则我想你还是歇歇吧,对你来说都是一样的,用好一样就行了,否则 .Net 1.1 都能完爆你. .Net的应用领域没有有些人想的那么窄,只能说你眼界实在是太浅了..Net在国外是否受待见?自己去国外招聘网站看一看就是了,又没被墙的,呵呵. 技术不过关不要赖平台

Android模拟器访问本机服务器

Android模拟器访问本机服务器,用127.0.0.1访问不到,因为127.0.0.1已经被映射到模拟器了. 可以用以下两种方式访问 1. 用 10.0.2.2 2. 直接用 本机的IP地址,如:192.168.x.x .

网站经常被投诉 那么问题来了 抗投诉不封机服务器哪里有?

华普在线慧霞QQ2850693176,外贸用户网站经常因为投诉被关?网站域名因为投诉转来转去?网站打开速度慢?经常打不开?后台无法安排订单?如果以上问题是被投诉造成的,美国洛杉矶ps机房绝对是首选!抗投诉免攻击服务器  抗投诉不封机服务器  抗投诉服务器租用  瑞典仿牌机房 巴拿马抗投诉服务器租用 大家在选择抗投诉服务器时最担心的问题是什么?我相信是遇到投诉问题机房那边该做如何处理? 我司美国ps机房,位于美国洛杉矶,是访问大陆速度最好的机房之一,如果是针对欧美用户的话,ps机房更是首选!速度快