外设驱动库开发笔记28:W5500以太网控制器
以太网通讯是一种被广泛使用的数据通讯方式。在嵌入式应用中也经常使用,但协议栈的实现并不是一件容易的事。不过有些以太网控制器就带有协议栈,如W5500。在本篇中我们将讨论如何设计并实现W5500以太网控制器的驱动。
1、功能概述
W5500是WIZnet开发的单芯片全硬件TCP/IP协议栈,能够方便的实现网络连接应用。
1.1、硬件描述
W5500作为一款全硬件TCP/IP嵌入式以太网控制器,为嵌入式系统提供了更加简易的互联网连接方案。W5500 集成了 TCP/IP 协议栈,10/100M 以太网数据链路层(MAC)及物理层(PHY),使得用户使用单芯片就能够在他们的应用中拓展网络连接。 其引脚排布及分装如下:
W5500全硬件 TCP/IP 协议栈支持 TCP,UDP,IPv4,ICMP,ARP,IGMP 以及 PPPoE 协议。W5500 内嵌 32K 字节片上缓存以供以太网包处理。使用W5500,只需要一些简单的Socket 编程就能实现以太网应用。用户可以同时使用8个硬件Socket 独立通讯。
W5500提供了SPI(外设串行接口)从而能够更加容易与外设MCU整合。而且,W5500的使用了新的高效SPI协议支持80MHz速率,从而能够更好的实现高速网络通讯。为了减少系统能耗,W5500提供了网络唤醒模式(WOL)及掉电模式供客户选择使用。
1.2、通讯接口
W5500提供了SPI(串行外部接口)作为外设主机接口,有SCSn,SCLK,MOSI, MISO共4路信号,且作为SPI从机工作。W5500与MCU的连接方式如下图所示。根据SCSn是否受主机控制,将其工作模式分为可变数据长度模式和固定数据长度模式。在可变数据长度模式中,W5500可以与其他SPI设备共用SPI接口。在固定数据长度模式,SPI将指定给W5500,不能与其他SPI设备共享。
SPI协议定义了四种工作模式(模式 0,1,2,3)。每种模式的区别是根据SCLK的极性及相位不同定义的。SPI 的模式 0 和模式 3 唯一不同的就是在非活动状态下,SCLK 信号的极性。SPI的模式0和3,数据都是在SCLK的上升沿锁存,在下降沿输出。W5500支持SPI模式0及模式3。MOSI和MISO信号无论是接收或发送,均遵从从最高标志位(MSB)到最低标志位(LSB)的传输序列。
1.3、内部寄存器
W5500的SPI数据帧包括了16位地址段的偏移地址,8位控制段和N字节数据段。如图下图所示:
地址段为W5500的寄存器或TX/RX缓存区指定了16位的偏移地址。 这16 位偏移地址的值来自从最高标志位到最低标志位的顺序传输。
控制段指定了地址段设定的偏移区域归属,读/写访问模式及SPI工作模式。8位控制段可以通过修改区域选择位(BSB[4:0]),读/写访问模式位(RWB)以及SPI工作模式位(OM[1:0])来重新定义。区域选择位选择了归属于偏移地址的区域。
SPI数据帧的数据段通过偏移地址自增(每传输1字节偏移地址加1),支持连续数据读/写。
W5500有1个通用寄存器,8个Socket寄存器区,以及对应每个Socket的收发缓存区。每个区域均通过SPI数据帧的区域选择位(BSB[4:0])来选取。每一个Socket的发送缓存区都在一个16KB的物理发送内存中,初始化分配为2KB。每一个Socket的接收缓存区都在一个16KB 的物理接收内存中,初始化分配为 2KB。无论给每个Socket 分配多大的收/发缓存,都必须在 16 位的偏移地址范围内(从 0x0000 到 0xFFFF)。
通用寄存器区配置了W5500的IP地址、MAC地址等基本信息。该区域可以通过SPI数据帧的区域选择位(BSB[4:0])选定。
W5500支持8个Socket作为通讯信道。每一个Socket通过Socket n寄存器区控制(0≤n≤7)。Socket n寄存器可以通过SPI数据帧中的区域选择寄存器(BSB[4:0])来选定对应的寄存器n。
2、驱动设计与实现
我们已经对W5500以太网控制器的引脚封装、接口方式、协议栈的操作流程以及基本操作库有了比较详细的了解。接下来我们将设计并实现W5500以太网控制器的驱动程序。
2.1、对象定义
在使用一个对象之前我们需要获得一个对象。同样的我们想要W5500以太网控制器就需要先定义W5500以太网控制器的对象。
2.1.1、对象的抽象
我们要得到W5500以太网控制器对象,需要先分析其基本特性。一般来说,一个对象至少包含两方面的特性:属性与操作。接下来我们就来从这两个方面思考一下W5500以太网控制器的对象。
先来考虑属性,作为属性肯定是用于标识或记录对象特征的东西。我们来考虑W5500以太网控制器对象属性。作为以太网控制器,W5500对象显然需要有网络配置参数作为它的属性,包括IP地址和MAC地址等。所以我们将网络参数定义为对象的属性。在这里我们以结构体的方式来定义网络参数。
接着我们还需要考虑W5500以太网控制器对象的操作问题。其实我们对W5500的操作就是对SPI接口的操作,这里我们因为使用了厂家的基础库,所以以函数注册回调函数的方式传递了操作函数。我们不需要再将对SPI端口作为对象的操作,而是将他们以函数指针的方式在初始化函数中传入。那么我们对对象的操作就是读取和写入信息的操作,而具体的数据处理总是依赖于具体应用,所以我们将其作为对象的操作。
根据上述我们对W5500以太网控制器的分析,我们可以定义W5500以太网控制器的对象类型如下:
1 /* 定义W5500对象类型 */ 2 typedef struct W5500Object { 3 wiz_NetInfo gWIZNETINFO; 4 uint16_t (*DataParsing)(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer);//接收消息解析及返回消息生成,返回值为返回消息的字节长度 5 uint16_t (*RequestData)(uint8_t *rqBuffer); //得到请求命令,一般用于客户端发起访问 6 }W5500ObjectType;
2.1.2、对象初始化
我们知道,一个对象仅作声明是不能使用的,我们需要先对其进行初始化,所以这里我们来考虑W5500以太网控制器对象的初始化函数。一般来说,初始化函数需要处理几个方面的问题。一是检查输入参数是否合理;二是为对象的属性赋初值;三是对对象作必要的初始化配置。据此我们设计W5500以太网控制器对象的初始化函数如下:
1 /*W5500对象初始化*/ 2 void W5500Initialization(W5500ObjectType *w5500, 3 uint8_t mac[6], //本地Mac地址 4 uint8_t ip[4], //本地IP地址 5 uint8_t sn[4], //子网掩码 6 uint8_t gw[4], //网关地址 7 uint8_t dns[4], //DNS服务器地址 8 dhcp_mode dhcp, //DHCP类型 9 W5500CSCrisType cris_en, 10 W5500CSCrisType cris_ex, 11 W5500CSCrisType cs_sel, 12 W5500CSCrisType cs_desel, 13 W5500SPIReadByteTYpe spi_rb, 14 W5500SPIWriteByteTYpe spi_wb, 15 W5500DataParsingType dataParse, 16 W5500RequestDataType requst 17 ) 18 { 19 if((w5500==NULL)||(cris_en==NULL)||(cris_ex==NULL)||(cs_sel==NULL)||(cs_desel==NULL)||(spi_rb==NULL)||(spi_wb==NULL)) 20 { 21 return; 22 } 23 24 for(int i=0;i<6;i++) 25 { 26 w5500->gWIZNETINFO.mac[i]=mac[i]; 27 } 28 29 for(int i=0;i<4;i++) 30 { 31 w5500->gWIZNETINFO.ip[i]=ip[i]; 32 w5500->gWIZNETINFO.sn[i]=sn[i]; 33 w5500->gWIZNETINFO.gw[i]=gw[i]; 34 w5500->gWIZNETINFO.dns[i]=dns[i]; 35 } 36 37 w5500->gWIZNETINFO.dhcp=dhcp; 38 39 /*注册TCP通讯相关的回调函数*/ 40 RegisterFunction(cris_en,cris_ex,cs_sel,cs_desel,spi_rb,spi_wb); 41 42 /*初始化芯片参数*/ 43 ChipParametersConfiguration(); 44 45 /*初始化网络通讯参数*/ 46 NetworkParameterConfiguration(w5500->gWIZNETINFO); 47 48 if(dataParse!=NULL) 49 { 50 w5500->DataParsing=dataParse; 51 } 52 else 53 { 54 w5500->DataParsing=LoopBackDataHandle; 55 } 56 57 if(requst!=NULL) 58 { 59 w5500->RequestData=requst; 60 } 61 else 62 { 63 w5500->RequestData=DefaultRequest; 64 } 65 }
2.2、对象操作
我们已经完成了W5500以太网控制器对象类型的定义和对象初始化函数的设计。但我们的主要目标是获取对象的信息,接下来我们还要实现面向W5500以太网控制器的各类操作。
W5500以太网控制器有哪些操作呢?作为通讯接口,最主要的就是数据的发送于接收。这些函数我们当然可以实现它,不过在厂商提供的基础库中已经提供了这些函数,我们直接实用就好了,这里就不再列出了。
3、驱动的使用
我们已经设计了W5500以太网控制器的驱动,接下来我们设计一个简单的应用验证这一驱动。
3.1、声明并初始化对象
使用基于对象的操作我们需要先得到这个对象,所以我们先要使用前面定义的W5500以太网控制器对象类型声明一个W5500以太网控制器对象变量,具体操作格式如下:
W5500ObjectType w5500;
声明了这个对象变量并不能立即使用,我们还需要使用驱动中定义的初始化函数对这个变量进行初始化。这个初始化函数所需要的输入参数如下:
W5500ObjectType *w5500,
uint8_t mac[6], //本地Mac地址
uint8_t ip[4], //本地IP地址
uint8_t sn[4], //子网掩码
uint8_t gw[4], //网关地址
uint8_t dns[4], //DNS服务器地址
dhcp_mode dhcp, //DHCP类型
W5500CSCrisType cris_en,
W5500CSCrisType cris_ex,
W5500CSCrisType cs_sel,
W5500CSCrisType cs_desel,
W5500SPIReadByteTYpe spi_rb,
W5500SPIWriteByteTYpe spi_wb,
W5500DataParsingType dataParse,
W5500RequestDataType requst
对于这些参数,对象变量我们已经定义了。而IP地址这些参数我们只需要睡着时输入就可以了。主要的是我们需要定义几个函数,并将函数指针作为参数。这几个函数的类型如下:
1 /*解析接收到的数据*/ 2 typedef uint16_t (*W5500DataParsingType)(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer); 3 4 /*得到请求命令,一般用于客户端发起访问*/ 5 typedef uint16_t (*W5500RequestDataType)(uint8_t *rqBuffer); 6 7 /*定义片选及临界区操作函数类型*/ 8 typedef void (*W5500CSCrisType)(void); 9 10 /*定义SPI读一个字节函数类型*/ 11 typedef uint8_t (*W5500SPIReadByteTYpe)(void); 12 13 /*定义SPI写一个字节函数类型*/ 14 typedef void (*W5500SPIWriteByteTYpe)(uint8_t wb);
对于这几个函数我们根据样式定义就可以了,具体的操作可能与使用的硬件平台有关系。片选操作函数用于多设备需要软件操作时,如采用硬件片选可以传入NULL即可。具体函数定义如下:
1 /*写1字节数据到SPI总线*/ 2 static void SPI_WriteByte(uint8_t TxData) 3 { 4 HAL_SPI_Transmit(&w5500hspi,&TxData,1,1000); 5 } 6 7 /*从SPI总线读取1字节数据*/ 8 static uint8_t SPI_ReadByte(void) 9 { 10 uint8_t rxData; 11 HAL_SPI_Receive(&w5500hspi,&rxData,1,1000); 12 return rxData;//返回接收的数据 13 } 14 15 /*进入临界区*/ 16 static void SPI_CrisEnter(void) 17 { 18 __set_PRIMASK(1); 19 } 20 21 /*退出临界区*/ 22 static void SPI_CrisExit(void) 23 { 24 __set_PRIMASK(0); 25 } 26 27 /*片选信号输出低电平*/ 28 static void SPI_CS_Select(void) 29 { 30 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); 31 } 32 33 /*片选信号输出高电平*/ 34 static void SPI_CS_Deselect(void) 35 { 36 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); 37 } 38 39 /*数据回环处理*/ 40 static uint16_t LoopBackDataHandle(uint8_t *rxBuffer,uint16_t rxSize,uint8_t *txBuffer) 41 { 42 uint16_t txSize = 0; 43 44 txSize=(uint16_t)rxSize; 45 46 for(int i=0;i<txSize;i++) 47 { 48 txBuffer[i]=rxBuffer[i]; 49 } 50 51 return txSize; 52 } 53 54 /*默认测试请求*/ 55 static uint16_t DefaultRequest(uint8_t *rqBuffer) 56 { 57 uint16_t rSize=0; 58 59 char requstString[]="This is a new client connection.\r\n"; 60 61 rSize=strlen(requstString); 62 63 for(int i=0;i<rSize;i++) 64 { 65 rqBuffer[i]=requstString[i]; 66 } 67 68 return rSize; 69 }
对于延时函数我们可以采用各种方法实现。我们采用的STM32平台和HAL库则可以直接使用HAL_Delay()函数。于是我们可以调用初始化函数如下:
1 /* W5500初始化配置 */ 2 void W5500Configuration(void) 3 { 4 uint8_t mac[6]= {0x01, 0x08, 0xdc,0x00, 0xab, 0xcd}; //本地Mac地址 5 uint8_t ip[4]= {192, 168, 1, 190}; //本地IP地址 6 uint8_t sn[4]= {255,255,255,0}; //子网掩码 7 uint8_t gw[4]= {192, 168, 1, 1}; //网关地址 8 uint8_t dns[4]= {0,0,0,0}; //DNS服务器地址 9 10 W5500_SPI_Configuration(); 11 W5500Initialization(&w5500,mac,ip,sn,gw,dns,NETINFO_STATIC,SPI_CrisEnter,SPI_CrisExit,SPI_CS_Select,SPI_CS_Deselect,SPI_ReadByte,SPI_WriteByte,NULL,NULL); 12 }
3.2、基于对象进行操作
我们定义了对象变量并使用初始化函数给其作了初始化。接着我们就来考虑操作这一对象获取我们想要的数据。我们在驱动中已经将获取数据并转换为转换值的比例值,接下来我们使用这一驱动开发我们的应用实例。我们实现以个TCP回环服务器。具体调用如下:
W5500TCPServer(&w5500,Socket0,502);
TCP服务器设计如下:
1 /*TCP服务器数据通讯*/ 2 int32_t W5500TCPServer(W5500ObjectType *w5500,W5500SocketType sn,uint16_t lPort) 3 { 4 int32_t ret; 5 6 switch(getSn_SR(sn)) 7 { 8 case SOCK_ESTABLISHED: 9 { 10 if(getSn_IR(sn) & Sn_IR_CON) 11 { 12 setSn_IR(sn,Sn_IR_CON); 13 } 14 uint16_t size=0; 15 if((size = getSn_RX_RSR(sn)) > 0) 16 { 17 if(size > DATA_BUFFER_SIZE) 18 { 19 size = DATA_BUFFER_SIZE; 20 } 21 22 uint8_t rxBuffer[DATA_BUFFER_SIZE]; 23 ret = recv(sn,rxBuffer,size); 24 if(ret <= 0) 25 { 26 return ret; 27 } 28 29 //添加数据解析及响应的函数 30 uint8_t txBuffer[DATA_BUFFER_SIZE]; 31 uint16_t length=w5500->DataParsing(rxBuffer,ret,txBuffer); 32 33 uint16_t sentsize=0; 34 while(length != sentsize) 35 { 36 ret = send(sn,txBuffer+sentsize,length-sentsize); 37 if(ret < 0) 38 { 39 close(sn); 40 return ret; 41 } 42 sentsize += ret; // 不用管SOCKERR_BUSY, 因为它是零. 43 } 44 } 45 break; 46 } 47 case SOCK_CLOSE_WAIT: 48 { 49 if((ret=disconnect(sn)) != SOCK_OK) 50 { 51 return ret; 52 } 53 break; 54 } 55 case SOCK_INIT: 56 { 57 if( (ret = listen(sn)) != SOCK_OK) 58 { 59 return ret; 60 } 61 break; 62 } 63 case SOCK_CLOSED: 64 { 65 if((ret=socket(sn,Sn_MR_TCP,lPort,0x00)) != sn) 66 { 67 return ret; 68 } 69 break; 70 } 71 default: 72 { 73 break; 74 } 75 } 76 return 1; 77 }
4、应用总结
这一篇中我们设计并实现了W5500以太网控制器的驱动程序,而且也设计了一个简单的应用来验证它。我们也在多个实际项目中使用W5500及驱动程序,并在此基础上实现过如Modbus TCP等数据传输协议,在实际使用中效果良好。
需要说明的是我们并没有从最底层开始实现驱动程序。当然,我们完全可以同过操作寄存器实现最基础的驱动开发,但在本篇中没有这么做是因为已有的驱动底层已经很完备了,不需要重复劳动。另一方面,我们希望再次基础上做更高层次的封装,以便与使用驱动的人能够专注于具体的应用逻辑,所以我们封装了如TCP服务器及TCP客户端等,使用者则可以专注于应用协议本身。
本篇中只是验证了TCP服务器,但在使用驱动时,如果向实现如HTTP服务器只需要修改对象的DataParsing操作就可以了。
源码下载:https://github.com/foxclever/ExPeriphDriver
欢迎关注:
如果阅读这篇文章让您略有所得,还请点击下方的【好文要顶】按钮。
当然,如果您想及时了解我的博客更新,不妨点击下方的【关注我】按钮。
如果您希望更方便且及时的阅读相关文章,也可以扫描上方二维码关注我的微信公众号【木南创智】