通讯接口应用笔记3:使用W5500实现Modbus TCP服务器

  前面我们设计实现了W5500的驱动程序,也讲解了驱动的使用方式。在最近一次的项目应用中,正好有一个使用W5500实现TCP通讯的需求,所以我们就使用该驱动程序轻松实现。这一篇中我们就来说一说基于我们W5500通讯驱动程序实现TCP通讯的过程。

1、应用需求

  在本次应用中,要求实现一个基于W5500的Modbus TCP服务器。这个需求的描述虽然只有一句话,但是这个需求的内容可不简单。我们首先来分析一下这个需求的具体内容。

  为了实现基于W5500的Modbus TCP服务器,我们必先须基于W5500实现一个TCP服务器。W5500本身是带硬件协议栈的,但却并不带TCP服务器。不过在我们前面的关于外设驱动库的系列文章中已经封装了W5500的驱动,其中就带有一个TCP服务器,我们可以直接采用就可以了。

  其次我们要在TCP服务器的基础上实现Modbus TCP协议。关于Modbus协议栈,我们以前的文章就讲述过Modbus通讯协议栈的开发问题。而且我们已经将我们开发的Modbus通讯协议栈开源。其中已经封装了Modbus TCP服务器对象,所以我们直接采用这一Modbus通讯协议栈就可以了。

  有了驱动和协议栈,我们还需要考虑应用层面的具体问题,而且也只需要考虑应用层面的具体问题。这里就看出我们前面封装外设驱动和Modbus通讯协议栈的价值所在了。关于应用层面的问题我们主要需要重点考虑几个问题:

  第一,数据的存储类型及地址范围。我们知道Modbus协议常见的数据类型有4种。我们需要考虑在系统中需要使用到的类型及地址,这将决定Modbus协议数据处理回调函数的实现。

  第二,网络配置问题,我们需要通过网络访问这台下位机就需要要为其配置网络。这存在静态配置,动态配置和系统自动分配的问题。作为服务器,我们一般不会希望让系统自动分配。所以我们需要考虑的是如何方便使用者为其分配地址的问题。

  第三,并发访问的问题。挂载在网络上的服务器肯定面临多个客户端来访问的问题。W5500可以实现8个Socket,而Modbus TCP通用的默认端口号是502,当然也可以使用其它端口,只要不冲突就好。所以我们可以考虑使用不同的Socket和不同的端口号来实现并发访问。

2、功能设计

  我们分析了基于W5500实现Modbus TCP服务器的需求。我们现在从硬件和软件两个方面来分析器功能的实现。

2.1、硬件功能设计

  我们知道W5500带有硬件协议栈,集成有以太网控制器和物理层,所以对外我们只需要实现以太网变压器和硬件接口就好了。但与控制器部分的连接则采用SPI接口,除此之外还需要提供中断输入和模式设定的相关接口。在这里我们设计器硬件连接如下:

  在上图中,我们将中断输入引入到MCU的GPIO端口,而模式设定PMODE0、PMODE1、PMODE2均通过电阻上拉到电源。对于W5500来说PMODE0、PMODE1、PMODE2均为高电平表示开启全部功能,所以我们直接拉高而不是引入到MCU引脚来控制。

2.2、软件功能设计

  从需求来说,软件的功能非常简单,就是实现一个Modbus TCP服务器。但实际上,如我们前面所描述的那样,软件需要考虑的问题还是比较多的。从功能实现上主要有3个方面需要考虑:

  第一,实现TCP服务器,这个服务器用于在系统中轮询处理,从W5500获取数据和发送数据给W5500都需要通过这部分来实现。

  第二,TCP服务器得到数据后,我们需要解析数据,并根据解析的上位数据决定进一步的动作,还需要生成返回信息。这部分对应功能就是Modbus TCP服务器的实现。

  第三,根据Modbus TCP服务器解析出的Modbus消息,需要决定下一步的动作,这个具体动作根据功能码的不同可能有不同需求,所以我们需要根据具体的要求实现不同功能码的动作。

  根据上述的设计,我们可以简单的将需要实现的软件功能图示如下:

  上图中,因为W5500的TCP服务器以及Modbus TCP协议栈的相关函数我们都做了封装,所以它们之间的调用都将以回调函数的方式实现。除了上述的软件实现外,还需要注意必要的初始化配置。

3、应用实现

  根据我们前面的设计,接下来我们考虑一下这一需求的具体实现过程。我们将这一过程分为4个部分来分别描述。

3.1、系统的初始化

  在实现具体的功能之前,我们需要对硬件以及软件环境做必要的初始化配置。具体到这里就是对W5500作必要的软硬件配置,包括接口、网络以及回调函数等。具体实例代码如下:

/* 以太网通讯配置 */
void McEthernetConfiguration(void)
{uint8_t mac[6]={0x01, 0x08, 0xdc,0x00, 0xab, 0xcd}; //本地Mac地址uint8_t ip[4]={192, 168, 1, 190};          //本地IP地址uint8_t sn[4]={255,255,255,0};           //子网掩码uint8_t gw[4]={192, 168, 1, 1};           //网关地址uint8_t dns[4]={0,0,0,0};              //DNS服务器地址/* 以太网使用GPIO初始化 */GPIO_Init_Configuration();/* SPI1端口初始化 */SPI1_Init_Configuration();/*W5500对象初始化函数*/W5500Initialization(&w5500,      //W5500对象mac,        //本地Mac地址ip,        //本地IP地址sn,        //子网掩码gw,        //网关地址dns,        //DNS服务器地址NETINFO_STATIC,  //DHCP类型EnterCritical,   //进入临界区ExitCritical,   //退出临界区EnableChipSelect, //片选使能DisableChipSelect, //片选失能ReadByteBySPI,   //SPI读字节WriteByteBySPI,  //SPI写字节W5500DataParsing, //报文解析函数NULL        //数据请求函数);
}

  在这个实例中,我们对网络部分采用的是静态配置,就是说网络参数是固定不变的,而且我们的测试环境只限于局域网内。

3.2、数据处理函数

  数据处理函数是最灵活的,因为每个项目及每个人对数据处理的要求都是不一样的,只要能符合应用要求就没问题。需要说一下的是,这部分是Modbus协议栈对处理数据的要求,想要详细了解的话,可以看我们以前关于Modbus协议站的文章。对于这个实例,数据处理函数如下:

/*获取想要读取的Coil量的值*/
void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList)
{uint16_t start;uint16_t count;/*先判断地址是否处于合法范围*/start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);for(int i=0;i<count;i++){statusList[i]=dPara.coil[start+i];}
}/*获取想要读取的保持寄存器的值*/
void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{uint16_t start;uint16_t count;/*先判断地址是否处于合法范围*/start=(startAddress>HoldingRegisterStartAddress)?((startAddress<=HoldingRegisterEndAddress)?startAddress:HoldingRegisterEndAddress):HoldingRegisterStartAddress;count=((start+quantity-1)<=HoldingRegisterEndAddress)?quantity:(HoldingRegisterEndAddress-start);for(int i=0;i<count;i++){registerValue[i]=aPara.holdingRegister[start+i];}
}/*设置单个线圈的值*/
void SetSingleCoil(uint16_t coilAddress,bool coilValue)
{/*先判断地址是否处于合法范围*/if(coilAddress<=12){dPara.coil[coilAddress]=coilValue;}
}/*设置多个线圈的值*/
void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue)
{uint16_t endAddress=startAddress+quantity-1;if((startAddress<=12)&&(endAddress<=12)){for(int i=0;i<quantity;i++){dPara.coil[i+startAddress]=statusValue[i];}}
}/*设置单个寄存器的值*/
void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue)
{bool noError=(bool)(((50<=registerAddress)&&(registerAddress<=59))||((73<=registerAddress)&&(registerAddress<=74))||((90<=registerAddress)&&(registerAddress<=91)));if(noError){aPara.holdingRegister[registerAddress]=registerValue;}}/*设置多个寄存器的值*/
void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{uint16_t endAddress=startAddress+quantity-1;bool noError=(bool)(((18<=startAddress)&&(startAddress<=28)&&(18<=endAddress)&&(endAddress<=28))||((50<=startAddress)&&(startAddress<=59)&&(50<=endAddress)&&(endAddress<=59))||((73<=startAddress)&&(startAddress<=74)&&(73<=endAddress)&&(endAddress<=74))||((90<=startAddress)&&(startAddress<=91)&&(90<=endAddress)&&(endAddress<=91)));if(noError){for(int i=0;i<quantity;i++){aPara.holdingRegister[startAddress+i]=registerValue[i];}}}

3.2、数据解析函数

  大家可能在前面的初始化函数中发现有一个名为W5500DataParsing的数据解析函数。这个函数是W5500驱动中,TCP服务器的要求,实现对数据的解析。因为具体的应用层协议解析多不胜数,所以设计成了回调函数,其函数原型如下:

/*解析接收到的数据*/
typedef uint16_t (*W5500DataParsingType)(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer);

  对于我们来说,我们需要根据具体的应用层协议来实现这一函数。不过我们采用的Modbus TCP协议,在我们的Modbus协议栈中已经实现了解析函数,所以我们调用如下:

/*报文解析函数*/
static uint16_t W5500DataParsing(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer)
{/*解析接收到的信息,返回响应命令的长度*/return ParsingClientAccessCommand(rxBuffer,txBuffer);
}

3.3、TCP服务器

  我们在前面已经说过了,需要对服务器进行轮询。所以我们需要在一个进程中轮询访问W5500的TCP服务器。同样我们也要考虑多客户端同时访问的问题,我们将轮询函数实现如下:

/* 以太网通讯处理 */
void McEthernetProcess(void)
{/*TCP服务器数据通讯*/W5500TCPServer(&w5500,Socket0,502);W5500TCPServer(&w5500,Socket1,503);W5500TCPServer(&w5500,Socket2,504);W5500TCPServer(&w5500,Socket3,505);W5500TCPServer(&w5500,Socket4,506);W5500TCPServer(&w5500,Socket5,507);W5500TCPServer(&w5500,Socket6,508);W5500TCPServer(&w5500,Socket7,509);
}

  事实上使用同一个Socket和不同的端口也是可以实现多客户端访问的,但既然有8个Socket,用起来自然更好一点。

4、应用验证

  我们已经根据需求实现了一个Modbus TCP服务器,究竟效果如何呢?我们还需要测试一下,以确认设计的正确性。

4.1、通讯测试

  我们将目标板连接到局域网中,使用著名的Modbus Poll软件来测试一下我们设计的程序是否符合要求。

  我们首先在一台机器上连接端口为504的Modbus TCP服务器,连接正常且数据获取也完全正确。具体如下图所示:


  同时,我们采用局域网内的另一台机器连接端口为502的Modbus TCP服务器,连接正常且数据获取也完全正确。具体如下图所示:

  经过上述测试,我们可以确定我们实现的Modbus TCP服务器是可行的,而且在多客户端并行访问下也可以正确工作。

4.2、小结

  这一篇中,我们实现了可以支持多客户端访问的Modbus TCP服务器,经测试运行也符合设计预期。这里我们将需要考虑的几个问题总结如下:

  关于初始化配置的问题,在这个例子中,我们对网络的配置是直接在软件上固定死的,这样做虽然简单直接但并不是一个好的选择。更好的办法是可以让使用者自己配置,方法有多种,可以根据自己的实际情况,在软件上进一步的考虑。

  关于数据处理的问题,具体的数据处理与实际的应用需求有关,也与应用层协议的要求有关,这个例子中实现的Modbus的数据处理函数并不是唯一的,但可参考其思路。

  关于数据解析的问题,在本例中实现的是Modbus TCP服务器的解析函数。对于不同的应用协议需要编写不同的解析函数,这部分是灵活性最大的,支持所有可运行于TCP应用层的通讯协议。

  关于多客户端访问的问题,W5500可以实现8个Socket,而Modbus TCP默认端口号是502,当然也可以使用其它端口。所以我们可以考虑使用不同的Socket和不同的端口号来实现并发访问。事实上,经过我们测试使用同一个Socket和不同的端口也是可以实现多客户端访问的,有兴趣的同仁可以试试。

欢迎关注:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499323.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电机速度曲线规划2:S形速度曲线设计与实现

电机驱动是很常见的应用&#xff0c;在很多系统中我们都会碰到需要改变电机的速度以实现相应的控制功能&#xff0c;这就涉及到电机速度曲线规划的问题。在这篇中我们就来简单讨论一下电机的S型曲线规划的问题。 1、基本原理 S型速度曲线控制算法是工业控制领域另一种常用的加…

USB应用开发笔记之一:STM32上实现USB主机读写U盘

在项目应用中&#xff0c;经常会有对外交换数据的需求。USB接口读写U盘无疑是一种颇为方便的选择。在这一篇中&#xff0c;我们就来讨论如何在STM32上实现USB主机读写U盘文件的方法。 1、应用概述 在我们的产品上有这样一个需求&#xff0c;希望通过大容量的U盘存取数据。我们…

Modbus协议栈综合实例设计

自我们开源了我们的Modbus协议栈之后&#xff0c;就一直有朋友来信说希望提供示例。这次我们整理了几个例子以供参考。 1、应用实例规划 在这次的实例中&#xff0c;我们使用的目标板拥有一个以太网接口、一个RS232串行接口和一个RS485串行接口&#xff0c;所以我们规划实现&a…

ThreadX应用开发笔记之二:移植ThreadX到STM32H7平台

前面我们将ThreadX成功移植到了STM32F4平台&#xff0c;但这只是我们的部分应用。我们希望将ThreadX的优势发挥到我们的更多应用中&#xff0c;所以在这一篇中我们就来实现将ThreadX移植到STM32H7平台中。 1、前期准备 在开始将ThreadX移植到STM32H7平台之前&#xff0c;我们需…

外设驱动库开发笔记31:S-Modlue远红外气体传感器驱动

在气体分析类产品中&#xff0c;我们经常会用到远红外气体传感器。我们就在碳氢类气体成分分析中使用了S-Modlue远红外气体传感器。接下来&#xff0c;我们将讨论S-Modlue远红外气体传感器驱动的设计与实现。 1、功能概述 S-MODULE EVO 使用非分散红外检测技术NDIR&#xff0c…

外设驱动库开发笔记32:HLPM025K3 PM2.5传感器驱动

现在人们对大气环境及室内环境都比较关注。PM2.5在生活中也是常见的词汇。在有些产品中就要求检测PM2.5的数值。检测PM2.5的手段多种多样&#xff0c;在要求不高时我们通常可以采用激光模块。在这一篇中&#xff0c;我们将讨论HLPM025K3 PM2.5传感器驱动的设计与实现。 1、功能…

外设驱动库开发笔记33:LCD1602液晶显示屏驱动

LCD1602是一种工业字符型液晶&#xff0c;能够同时显示16x02即32个字符。LCD1602液晶显示的原理是利用液晶的物理特性&#xff0c;通过电压对其显示区域进行控制&#xff0c;即可以显示出图形。在这一章我们就来讨论LCD1602液晶显示屏驱动的设计与实现。 1、功能概述 LCD1602液…

滤波器开发之四:基于算术平均的中值滤波器

在信号采集系统中&#xff0c;除了我们感兴趣的数据外&#xff0c;难免会有一些来自于环境的干扰信号。但我们总希望我们得到的数据是纯净而真实的&#xff0c;为了达到这个目标&#xff0c;我们不得不想办法去除这些干扰信号&#xff0c;于是滤波器就成为我们必不可少的帮手。…

LwIP应用开发笔记之十一:LwIP带操作系统UDP服务器

我们已经实现了在FreeRTOS系统上的LwIP的移植工作&#xff0c;但只是简单的在系统平台上跑了起来。我们还希望能做更多的事情&#xff0c;这一节我们就在FreeRTOS系统上实现基于LwIP的UDP服务器。 1、UDP协议简述 UDP协议全称是用户数据报协议&#xff0c;在网络中它与TCP协议…

滤波器开发之五:基于算术平均的限幅滤波器

通过AD采集数据时&#xff0c;我们总是希望采集到的数据是纯净而真实的&#xff0c;而实际上环境中存在太多的干扰信号&#xff0c;为了让我们得到的数据尽可能地接近实际值&#xff0c;我们需要降低这些干扰信号的影响。所以软件实现的数字滤波器应运而生&#xff0c;这一篇我…

外设驱动库开发笔记34:OLED显示屏驱动

现在OLED显示屏在嵌入式系统中应用的越来越多。对于一些显示信息不太复杂&#xff0c;以显示信息为主的需求&#xff0c;我们一般会选择OLED显示屏。在这一篇中&#xff0c;我们将讨论OLED显示屏驱动的设计与实现。 1、功能概述 从使用的情况来说&#xff0c;较为常用的是0.96…

外设驱动库开发笔记35:迪文触摸屏驱动

有些时候嵌入式系统也需要显示更为复杂的图形&#xff0c;需要更丰富的数据展示。为此&#xff0c;我们需要更大&#xff0c;色彩更丰富&#xff0c;带触屏的显示屏&#xff0c;当然性价比更高就最好了。在我们的项目中遇到此类需求&#xff0c;我们有时会选择DWIN触摸屏。在本…

快速实现一个室内空气质量检测仪

冬天我们大多会关闭门窗&#xff0c;而依靠暖通空调设备来维持室内温度。而在保证居室温度的同时&#xff0c;我们也希望保持居室内大气环境的健康度。鉴于此&#xff0c;我们设计了一个简单的室内空气质量检测器。 1、系统概述 我们依靠暖通空调设备来维持室内温度、湿度和通…

外设驱动库开发笔记36:NTC负温度系数热电阻测温驱动

在嵌入式产品中&#xff0c;温度检测非常常见。在成本比较敏感而精度要求较低时&#xff0c;NTC电阻是个不错的选择。在这一篇中&#xff0c;我们将讨论如何和设计并实现一个通用的NTC驱动&#xff0c;以便在后续的项目中更方便的复用。 1、功能概述 NTC是指随温度上升电阻呈指…

外设驱动库开发笔记37:S1336-5BQ光敏二极管作为光度计驱动

光敏二极管能够实现很多应用&#xff0c;用于光度检测即是其一。我们在一些产品中就曾使用S1336-5BQ光敏二极管进行光度值检测。所以在本篇中&#xff0c;我们将讨论如何设计并实现S1336-5BQ光敏二极管用于光度检测的驱动。 1、功能概述 根据相关的资料&#xff0c;光电二极管…

PID控制器改进笔记之六:改进PID控制器之参数设定

前面我们发布了一系列PID控制器相关的文章&#xff0c;包括经典PID控制器以及参数自适应的PID控制器。这一系列PID控制器虽说实现了主要功能&#xff0c;也在实际使用中取得了良好效果&#xff0c;但还有很多的细节部分可以改进以提高性能和灵活性。这篇中我们来讨论改进PID控制…

软件设计开发笔记1:基于状态机的程序设计

在编码实现的过程中&#xff0c;我们会经常使用到条件判断结构&#xff0c;而且使用起来很方便。但是在需要转移的状态比较多&#xff0c;或是条件比较复杂时&#xff0c;我们就可能需要很长的条件判断结构来处理。不过&#xff0c;过于复杂的条件判断结构会给代码的编写和维护…

外设驱动库开发笔记38:RTD热电阻测温驱动

我们已经讨论过多种温度检测方式&#xff0c;但我们尚未关注热电阻温度检测&#xff0c;但热电阻测温在工业环境中是非常常见的。尽管有很多集成的数字式的热电阻接口元器件&#xff0c;但这些器件不但成本较高&#xff0c;灵活性也大打折扣。所以我们有时会使用更简单灵活的电…

外设驱动库开发笔记39:按键操作驱动

按键在我们的项目中是经常使用到的组件。一般来说&#xff0c;我们都是在用到按键时直接针对编码&#xff0c;但这样每次都做很多重复性的工作。所以在这里我们考虑做一般性抽象得到一个可应用于按键操作的通用性驱动程序。 1、功能概述 按键操作在我们的产品种经常用到&#…

外设驱动库开发笔记40:AT25xxx外部存储器驱动

我们在前面开发过AT24CXX系列EEPROM存储器&#xff0c;它使用的是I2C接口。不过有时候我们也会使用SPI接口的EEPROM存储器。在这一篇我们将来讨论AT25XXX系列EEPROM存储器的驱动设计、实现及使用。 1、功能概述 AT25XXX系列EEPROM存储器采用SPI接口&#xff0c;因其操作简单且…