W5500简单使用及官方IO库 快速入门

简介

前段时间折腾了好久W5500模块。已经通过其实现了基于TCP/IP的bootloader。基于轮询的方式下能保证稳定可靠地调用协议栈,下一篇中已放出驱动模块及示例代码;基于中断的基本能用,但还没信心稳定可靠,还在继续折腾。

这个网络芯片通过硬件实现了TCP/IP协议栈,10/100M以太网数据链路层(MAC)及物理层(PHY);支持TCP、UDP、IPv4、ICMP、ARP、IGMP以及PPPoE。内嵌32K字节缓存。MCU通过SPI与其通讯来配置网络及进行网络通信,SPI速率达80MHz。

其上提供多达8个独立的socket(套接字),编号0-7,这个socket和平常所说的socket稍微有点差别。对于UDP,设置对应UDP后的操作和普通的socket差不多;但对于TCP,1个socket只能对应一条TCP链接,也就是说,比如你在一个端口上打开了监听TCP的如5000端口,然后使用两个TCP客户端去连接,结果是只有先连入的那个TCP链接能成功。为了在一个TCP端口上同时服务多个TCP链接,需要在多个socket上同时监听那个端口。

另稍微提一下,与W5500通讯的基本思想就是读写W5500的寄存器,来控制W5500的各种功能或读写数据,就好像我们在单片机上通过设置各个寄存器的值来操作各个模块,但因为它不是直接接在MCU的地址总线上的,所以要通过特定格式的SPI帧来间接实现操作寄存器。每次读写寄存器的时候先按固定格式指定起始地址,然后按序依次从MISO读取或从MOSI写入数据。同时还可能随带一些其他操作,比如可变数据长度模式下还需要在每次开始发送前拉低片选信号,发送结束后再拉高。(与可变数据长度模式对应的就是固定数据长度模式,这种模式下不需要频繁控制片选信号,但是在大量读写时就很蛋疼。现在版本的官方库中只支持可变数据长度模式,所以就不需要纠结这个事情了)

当然,这些细节操作官方的IO库已经都帮我们隐藏了,基本我们只需要调用库提供的api就行。

硬件连接

这里大概讲下主要的几根线怎么连。这里假设你用的是网上现成的开发板,要是你是想自己画电路板请点右上角的×。就是差不多长这个样的(没有收广告费,所以相关logo去掉了hhh)↓
W5500简单使用及官方IO库 快速入门_第1张图片

嗯,反正网上买现成的W5500开发板基本都长一个样,其上基本也就这几个引脚

pin 功能
3.3V/5V 电源输入
GND 电源地
INT 中断引脚
RST 硬件复位引脚
MISO SPI主机输入从机输出引脚
MOSI SPI主机输出从机输入引脚
SCS/CS SPI SLAVE选择引脚
SCLK/SCK SPI时钟引脚

电源的输入和地不用多说,就是正常连接,注意和主芯片共地,不然SPI通讯可能会出问题。

最下面四个就是很常规的SPI通讯用到的线路,根据自己的芯片引脚连接好。

INT是W5500用于输出中断信号的,W5500输出中断时会拉低这个引脚,另外,如果同时有多个中断的话,W5500的机制是会一个个的输出中断,比如同时有两个中断的话,会先拉低一小会,然后拉高,然后一会后再拉低。 如果要用中断机制的话,把这个线接到MCU有下拉中断的引脚上,然后还需要对W5500内部寄存器进行一定的设置,如果不用的话可以忽略这个引脚。

RST是W5500的硬件复位引脚,通过拉低其一会然后再拉高可以强制W5500复位。另外还有软复位,所以其实可以不用这个引脚,不用的话就直接一直拉高就好了。根据需要进行连接。

具体来说,我自己连MC9S12XEP100的话:
MISO->S4 、MOSI->S5 、SCLK->S6 、SCS->S7 这是对应SPI0的引脚
INT则比如连到H0上,这个口有下降沿中断功能。

官方IO库

下载地址及文件结构:

忘记官方网站上怎么找的了,直接给出github链接,另,本文基于V3.1.1,最近新出的V4.0.0还没测试过:
https://github.com/Wiznet/ioLibrary_Driver
整个官方库的包的文件结构如下:
W5500简单使用及官方IO库 快速入门_第2张图片

application/loopback是一个回环测试的示例程序。
Internet里则是上层多种协议的驱动程序。

库中最重要的就是Ethernet里头的几个文件。
wizchip_conf.h/c 里有用户用于注册函数的几个api以及配置W5X00的一些驱动函数。
W5X00.h/c 里则是几个基础IO函数以及芯片上寄存器的相关定义和读写函数。
socket.h/c 里则提供了类berkeley socket api的接口函数,直接用于驱动TCP/IP功能。其中大量调用另外两个文件中提供的驱动函数,可以将其视作更高层的抽象。当然,抽象是要付出代价的,如果希望省点代码的话,可以直接调用真正的底层函数。

我们可以看到这个库其实是兼容W5100、W5200、W5300、W5500的,但由于我只试过W5500,所以后面说的只针对W5500。

严谨来说,使用之前应该先打开wizchip_conf.h把以下宏设置为5500,但由于这是默认选项,所以其实不用管。

#ifndef _WIZCHIP_
#define _WIZCHIP_                      5500   // 5100, 5200, 5300, 5500
#endif

stdint.h

如果编译时报错说include不到stdint.h,可以自己加入这个头文件。

#ifndef _STDINT_H
#define _STDINT_H


typedef   signed char   int8_t;
typedef unsigned char  uint8_t;
typedef   signed short  int16_t;
typedef unsigned short uint16_t;
typedef   signed long   int32_t;
typedef unsigned long  uint32_t;

#endif

下面按使用步骤介绍主要的一些函数。

SPI函数及临界区函数注册

为了兼容性,库无法假设SPI和临界区的具体实现,于是需要用户主动把相关驱动函数注册给库使用。

wizchip_conf中提供了如下函数以注册驱动,:

// 注册进入\离开临界区函数
// cris_en 进入
// cris_ex 离开
void reg_wizchip_cris_cbfunc(void(*cris_en)(void), void(*cris_ex)(void));
// 注册spi片选函数
// cs_sel   选定
// cs_desel 取消选定
void reg_wizchip_cs_cbfunc(void(*cs_sel)(void), void(*cs_desel)(void));
// 注册spi读写单字节函数
// spi_rb   从spi读一个字节
// spi_wb   往spi写一个字节
void reg_wizchip_spi_cbfunc(uint8_t (*spi_rb)(void), void (*spi_wb)(uint8_t wb));
// 注册spi大量读写函数
// spi_rb   从spi读取len个字节到pBuf
// spi_wb   从pBuf往spi写len个字节
void reg_wizchip_spiburst_cbfunc(void (*spi_rb)(uint8_t* pBuf, uint16_t len), void (*spi_wb)(uint8_t* pBuf, uint16_t len));

注:这些函数都有默认的空函数,所以不需要用到某个功能的时候直接不注册就行。
注:其实还有一个注册总线接口的,但是由于W5500只有SPI接口,所以这个函数就直接忽略吧。

临界区函数会在内部基础IO函数的开始和结束时被调用,它保证单次读写W5500寄存器的原子性,当然,实际上一个简单的功能可能都要读写好几个寄存器,如果你需要保证更大范围的互斥操作,那还得自己实现些其他机制。
如果是单线程实现所有与W5500的通讯的话,那其实注不注册这个函数也无所谓。

片选函数这是在进入临界区之后立刻被使用的函数,它用于通知W5500通讯的开始和结束。所以一定要正确注册这两个函数,在选定时拉低片选信号,取消选定时拉高。

SPI读写函数则就没什么好说的了,基础IO嘛,要不人家库怎么知道去哪里收发数据。

注册示例(具体根据自己实际情况):

#include "SPI.h"
……
uint8_t spi_readbyte(void)        {return SPI_ExchangeChar(SPI0,0xaa);}
void 	spi_writebyte(uint8_t wb) {SPI_ExchangeChar(SPI0,wb);}
void 	spi_readburst(uint8_t* pBuf, uint16_t len) 	{
  for(;len > 0;len--, pBuf++){
    *pBuf = SPI_ExchangeChar(SPI0,0xaa);
  }
}
void spi_writeburst(uint8_t* pBuf, uint16_t len) {
  for(;len > 0;len--, pBuf++){
    SPI_ExchangeChar(SPI0,*pBuf);
  }
}
void 	cs_select(void)            {PTS_PTS7 = 0;}
void 	cs_deselect(void)          {PTS_PTS7 = 1;}

int main(){
  ……
  // 设置S7为输出引脚(用于片选)
  DDRS_DDRS7 = 1;
  // 初始化SPI0
  SPI_Init(SPI0);
  SPI_Enable(SPI0);
  // 注册驱动函数
  reg_wizchip_spi_cbfunc(spi_readbyte,spi_writebyte);
  reg_wizchip_spiburst_cbfunc(spi_readburst,spi_writeburst);
  reg_wizchip_cs_cbfunc(cs_select,cs_deselect);
  //reg_wizchip_cris_cbfunc(cris_en, cris_ex);
  ……
}

初始化和重置

初始化W5500使用(socket.h中)

uint8_t ar[16] = {2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2}; // 全部收发缓冲区设为2KB(默认)
ctlwizchip(CW_INIT_WIZCHIP,ar);

返回值0为成功,-1为失败,之后使用到这个函数时不再说明。
其会软重置W5500,然后根据参数重置每个端口的收发缓冲区大小。

第二个参数的类型为uint8_t [2][8],分别指定8个端口上收和发缓冲区的大小(KB),收和发分别加起来不能超过16K。默认每个端口的收发端口分别为2K。

对应(wizchip_conf.h中)

int8_t wizchip_init(uint8_t* txsize, uint8_t* rxsize)

txsize和rxsize都为uint8_t[8]

软重置W5500使用(socket.h中)

ctlwizchip(CW_RESET_WIZCHIP,(void *)0);

相当于直接调用(wizchip_conf.h中)

void wizchip_sw_reset(void);

实际上,如果不需要定制缓冲区大小的话,直接把软重置当做初始化来用就行。

获取芯片 ID

这是个毫无意义的功能,但还是蛮提一下。

char id[6];
ctlwizchip(CW_GET_ID,(void *)id);

id里是固定的 “W5500”。

设置和读取网络配置

(socket.h中)

ctlnetwork(CN_SET_NETINFO,(void *)&conf);
ctlnetwork(CN_GET_NETINFO,(void *)&conf);

分别是设置和获取网络配置,其参数为指向下面结构的指针

typedef struct wiz_NetInfo_t
{
   uint8_t mac[6];  ///< Source Mac Address
   uint8_t ip[4];   ///< Source IP Address
   uint8_t sn[4];   ///< Subnet Mask 
   uint8_t gw[4];   ///< Gateway IP Address
   uint8_t dns[4];  ///< DNS server IP Address
   dhcp_mode dhcp;  ///< 1 - Static, 2 - DHCP
}wiz_NetInfo;

分别相当于直接调用(wizchip_conf.h中)

void wizchip_setnetinfo(wiz_NetInfo* pnetinfo);
void wizchip_getnetinfo(wiz_NetInfo* pnetinfo);

ctlnetwork的返回值并不能判断是否设置成功,所以一般在设置之后我们会立刻回读配置以确定正确设置,如二者一致,则说明正确设置,否则就得检查是不是通讯哪里出问题了。

……
static wiz_NetInfo NetConf = {
  {0x0c,0x29,0xab,0x7c,0x04,0x02},  // mac地址
  {192,168,1,133},                  // 本地IP地址
  {255,255,255,0},                  // 子网掩码
  {192,168,1,1},                    // 网关地址
  {0,0,0,0},                        // DNS服务器地址
  NETINFO_STATIC                    // 使用静态IP
};

void configNet(){
  wiz_NetInfo conf;
  // 配置网络地址
  ctlnetwork(CN_SET_NETINFO,(void *)&NetConf);
  // 回读
  ctlnetwork(CN_GET_NETINFO,(void *)&conf);
  if(memcmp(&conf,&NetConf,sizeof(wiz_NetInfo)) == 0){
    // 配置成功
  }else{
    // 配置失败
  }
}

需要注意的是,虽然wiz_NetInfo结构中有DNS呀、网关呀什么的可选,但实际上这些功能都需要外挂其他程序才能实现,否则没有任何意义。想要直接能用的话就把它当静态IP老老实实设置mac地址、本地IP地址,子网掩码、网关地址。

配置超时

如发起tcp链接、发送数据等的时候,如果一段时间没有答复(即超过超时时间),W5500会重发,直到超过重试次数还没有得到答复,就会触发超时中断。
如果需要配置超时行为:

wiz_NetTimeout to;
to.retry_cnt = 8;   // 重试次数,默认8次
to.time_100us = 2000; // 超时时间,默认2000*100us = 200ms
wizchip_settimeout(&to);

等价于调用socket.h中的:

ctlnetwork(CN_SET_TIMEOUT,(void *)&to);

当然,也都有对应的get方法,就是把set改为get。

读取socket状态机

控制各个socket的基本方法就是读取对应socket的当前状态,然后据此进行各种处理,比如打开socket、开启监听、断连等。读取socket状态机的方法是读取状态寄存器SR,即以下函数

getSn_SR(sn);

其返回值见下表,为了方便理解,描述中直接改为了io库中的函数:

状态 描述
SOCK_CLOSED socket处于关闭状态,资源被释放。disconnect或close命令生效后,或者超时后,无视之前状态变为这个状态
SOCK_INIT socket以TCP模式打开,然后才可以调用connect或listen。通过正确地调用socket函数以转变为这个状态
SOCK_LISTEN socket正以TCP服务器模式运作,并正在等待(监听)连接请求
SOCK_SYNSENT socket发送了一个连接请求包(SYN包),这是从SOCK_INIT使用connect命令后的中间状态,如果随后收到了“接受连接”(SYN/ACK包),则会转为SOCK_ESTABLISHED;否则在超时后会转为SOCK_CLOSED,同时会设置超时中断标志位
SOCK_SYNRECV socket接收到了“请求连接”(SYN包),如果随后发送答复(SYN/ACK包)成功,则会转为SOCK_ESTABLISHED;否则在超时后会转为SOCK_CLOSED,同时会设置超时中断标志位。
SOCK_ESTABLISHED socket tcp连接已建立,即在SOCK_LISTEN状态下收到了tcp客户端发来的SYN包并答复成功,或使用connect命令成功后会转变为的状态。
SOCK_FIN_WAIT
SOCK_CLOSING
SOCK_TIME_WAIT
表明socket正在关闭。它们是tcp链接主动或被动关闭的中间状态。
SOCK_CLOSE_WAIT 表明socket正在关闭。这个状态说明socket收到了tcp链接的另一方发来的“断连请求”(FIN包)。这是半关闭状态,可以继续发送数据。发送完后应该调用disconnect或者close来完全关闭。
SOCK_LAST_ACK 表明socket正在被动关闭状态下。这个状态说明socket正在等待对“断连请求”(FIN包)的答复(FIN/ACK包)。当成功收到答复或者超时后会变为SOCK_CLOSED状态。
SOCK_UDP socket正以UDP模式运作。通过正确地调用socket函数以转变为这个状态
SOCK_IPRAW IP raw模式。本文不涉及这方面内容。
SOCK_MACRAW MACRAW模式。本文不涉及这方面内容。

所以你看到大部分程序都是长这个样子的:

  switch(getSn_SR(sn)){         // 获取socket的状态
    case SOCK_CLOSE_WAIT:       // Socket处于等待关闭状态
      disconnect(sn);
      ……
    break;
    case SOCK_CLOSED:           // Socket关闭状态
	  ……
	break;
    case SOCK_INIT:            	// Socket处于初始化完成(打开)状态
      ……
    break;
    ……
  }

这就是基于socket sn的状态机驱动程序。

另,如果要使用socket.h中的函数的话,上面等价于:

uint8_t sr;
while(1){
  getsockopt(sn,SO_STATUS,(void *)&sr);
  switch(sr){
    ...
  }
}

打开socket

注:打开socket前先配置好网络参数。
socket最初处于SOCK_CLOSED状态,这个时候是无法进行通讯的。
为了进行通讯,需要先把socket打开并配置为某一协议,方法为调用socket.h中的socket函数:

// 描述:  按照传递的参数初始化并打开socket sn。
// 参数:  sn           socket号(0-7)
//        protocol     指定要运行的协议类型(Sn_MR_XXX)
//        port         绑定的端口号,如果为0则自动分配
//        flag         socket flags,见SF_XXXXXXX
// 返回:  sn                 如果成功
//        SOCKERR_SOCKNUM    如果socket号无效
//        SOCKERR_SOCKMODE   不支持的socket模式
//        SOCKERR_SOCKFLAG   无效的socket flags.
int8_t  socket(uint8_t sn, uint8_t protocol, uint16_t port, uint8_t flag);

TCP socket

如把socket 1配置为tcp模式,并绑定5555端口:

if(socket(1,Sn_MR_TCP,5555,SF_TCP_NODELAY | SF_IO_NONBLOCK) == 1){
  // 打开成功
}else{
  // 打开失败
}

SF_TCP_NODELAY 指定socket在收到对方的数据包后应该没有延时尽快答复ACK包,否则需要超时时间做延时。
SF_IO_NONBLOCK 用于控制socket.h中函数的行为,如启用这一选项,对这一socket调用socket.h中大部分函数不会阻塞等待调用结果,而是会在确认发出指令后尽快返回。要注意的是,启用后,大部分函数的返回值会为SOCK_BUSY,这并不代表调用就失败了。

UDP socket

如把socket 2配置为udp模式,并绑定5555端口:

if(socket(2,Sn_MR_UDP,5555,SF_IO_NONBLOCK) == 2){
  // 打开成功
}else{
  // 打开失败
}

主要就是改了下协议。
SF_IO_NONBLOCK 作用同上。如果flags参数不需要的话直接传个0进去就行。

TCP 打开监听和发起连接

在成功把socket配置为tcp模式后,通过调用listen来打开监听,即作为tcp服务器:

// 描述:  监听来自客户端的连接请求
// 参数:  sn           socket号(0-7)
// 返回:  SOCK_OK             如果成功
//        SOCKERR_SOCKINIT    如果还未初始化socket
//        SOCKERR_SOCKCLOSED  如果socket意外关闭
int8_t  listen(uint8_t sn);

在成功把socket配置为tcp模式后,通过调用connect来发起tcp链接:

// 描述:  尝试连接一个tcp服务器
// 参数:  sn           socket号(0-7)
//        addr         目标IP地址(uint_8[4])
//        port         目标端口号
// 返回:  SOCK_OK             如果成功
//        SOCKERR_SOCKNUM     无效的socket号
//        SOCKERR_SOCKMODE    socket模式无效
//        SOCKERR_SOCKINIT    如果还未初始化socket
//        SOCKERR_IPINVALID   IP地址错误
//        SOCKERR_PORTZERO    port参数为0
//        SOCKERR_TIMEOUT     连接超时
//        SOCK_BUSY           非阻塞模式下立即返回此值
// 注意:1. 仅在tcp模式下有效
//      2. 在阻塞io(默认)模式下,直到确认连接完成才会返回
//      3. 在非阻塞io(即指定了SF_IO_NONBLOCK)模式下,会立即返回SOCK_BUSY
int8_t  connect(uint8_t sn, uint8_t * addr, uint16_t port);

如想让已经初始化为tcp模式的socket 1连接到192.168.1.40:8080:

uint8_t ipAddr[4] = {192,168,1,40};
connect(1,ipAddr,8080);

发送和接收数据

TCP

在socket上的tcp链接成功建立后,可以调用send函数来发送数据。

// 描述:  发送数据给TCP socket上连接的对象
// 参数:  sn           socket号(0-7)
//        buf          指向要发送的数据的缓冲区
//        len          缓冲区中数据的字节长度
// 返回:  发送的字节长度       如果成功
//        SOCKERR_SOCKSTATUS  无效的socket状态
//        SOCKERR_TIMEOUT     发送超时
//        SOCKERR_SOCKMODE    socket模式无效
//        SOCKERR_SOCKNUM     无效的socket号
//        SOCKERR_DATALEN     len为0
//        SOCK_BUSY           socket正忙
// 注意:1. 仅在tcp服务器或客户端模式下有效,且无法发送大于socket发送缓冲区大小的数据
//      2. 在阻塞io(默认)模式下,直到数据发送完成才会返回 — socket发送缓冲区大小比数据大
//      3. 在非阻塞io(即指定了SF_IO_NONBLOCK)模式下,当socket缓冲区不够用,会立即返回SOCK_BUSY
int32_t send(uint8_t sn, uint8_t * buf, uint16_t len);

如往已经建立了tcp链接的socket 1上发送hello world:

char buf[] = "hello world!";
send(1,buf,strlen(buf));

在socket上的tcp链接成功建立后,可以调用recv函数来获得收到的数据。

// 描述:  接收TCP socket上连接的对象发来的数据
// 参数:  sn           socket号(0-7)
//        buf          指向要接收数据的缓冲区
//        len          缓冲区的最大长度
// 返回:  接收的字节长度       如果成功
//        SOCKERR_SOCKSTATUS  无效的socket状态
//        SOCKERR_SOCKMODE    socket模式无效
//        SOCKERR_SOCKNUM     无效的socket号
//        SOCKERR_DATALEN     len为0
//        SOCK_BUSY           socket正忙
// 注意:1. 仅在tcp服务器或客户端模式下有效,且无接收大于socket接收缓冲区大小的数据
//      2. 在阻塞io(默认)模式下,如果暂时没有数据,就会不停阻塞等待直到收到任意数据或者链接断开。
//      3. 在非阻塞io(即指定了SF_IO_NONBLOCK)模式下,如果暂时没有数据可接收,会立刻返回SOCK_BUSY
int32_t recv(uint8_t sn, uint8_t * buf, uint16_t len);

注意,如果缓冲区大小比socket接收缓冲区小的话并不能保证一次调用就能接收完所有数据。所以常会循环接收并处理,直到返回值小于等于0。

int32_t len;
uint8_t buf[BUF_SIZE];
while((len = recv(1,buf,BUF_SIZE)) > 0){
  // 对刚收到的数据进行处理
}

UDP

socket初始化为udp模式后,发送数据需要使用sendto函数。

// 描述:  发送UDP或MACRAW数据报给参数指定的IP地址
// 参数:  sn           socket号(0-7)
//        buf          指向要发送的数据的缓冲区
//        len          缓冲区中数据的字节长度
//        addr         目标IP地址,uint8_t[4]
//        port         目标端口号
// 返回:  发送的字节长度       如果成功
//        SOCKERR_SOCKNUM     无效的socket号
//        SOCKERR_SOCKMODE    socket模式无效
//        SOCKERR_SOCKSTATUS  无效的socket状态
//        SOCKERR_DATALEN     len为0
//        SOCKERR_IPINVALID   错误的IP地址
//        SOCKERR_PORTZERO    port为0
//        SOCKERR_SOCKCLOSED  socket意外关闭
//        SOCKERR_TIMEOUT     发送超时
//        SOCK_BUSY           socket正忙
// 注意:1. 仅在tcp服务器或客户端模式下有效,且无法发送大于socket发送缓冲区大小的数据
//      2. 在阻塞io(默认)模式下,直到数据发送完成才会返回 — socket发送缓冲区大小比数据大
//      3. 在非阻塞io(即指定了SF_IO_NONBLOCK)模式下,当socket缓冲区不够用,会立即返回SOCK_BUSY
int32_t sendto(uint8_t sn, uint8_t * buf, uint16_t len, uint8_t * addr, uint16_t port);

如使用初始化为udp的socket 2发送数据给192.168.1.40:3333:

char buf[] = "data!";
uint8_t ipAddr[4] = {192,168,1,40};
sendto(2,buf,strlen(buf),ipAddr,3333);

而对应的,接收时需要使用recvfrom函数。

// 描述:  接收UDP或MACRAW数据报
// 参数:  sn           socket号(0-7)
//        buf          指向要接收数据的缓冲区
//        len          缓冲区的最大长度,当大于数据包大小时,接收数据包大小的数据;小于时,接收len大小的数据。
//        addr         用于返回发送者ip地址,仅在对每个收的包第一次调用recv时有效。
//        port         用于返回发送者的端口号,仅在对每个收的包第一次调用recv时有效。
// 返回:  实际接收的字节长度    如果成功
//        SOCKERR_DATALEN     len为0
//        SOCKERR_SOCKMODE    socket模式无效
//        SOCKERR_SOCKNUM     无效的socket号
//        SOCK_BUSY           socket正忙
// 注意:1. 在阻塞io(默认)模式下,如果暂时没有数据,就会不停阻塞等待直到收到任意数据。
//      2. 在非阻塞io(即指定了SF_IO_NONBLOCK)模式下,如果暂时没有数据可接收,会立刻返回SOCK_BUSY
int32_t recvfrom(uint8_t sn, uint8_t * buf, uint16_t len, uint8_t * addr, uint16_t *port);

对于UDP这类无连接的协议,W5500并不会自动帮忙提取ip地址,而是直接把整个包交给用户去处理,所以发送方的ip地址实际上是软件的方式从每个数据报的报头处提取出来的。为了实现这个功能,io库内部有标志位来记录当前是不是一个新的包,如是,则提取地址并返回。这种解决方案也直接导致了每个包只有第一次调用recvfrom时能得到ip地址信息。

虽然库的英文注释中有说通过读取PACKINFO的方法来判断addr和port是否有效,但是通过阅读源码,好像这个方法并没有用。好在如果不是第一个包,函数内部并不会动addr和port指向的值。所以下面方法接收时能保证ip地址每次循环都是正确的。

int32_t len;
uint8_t addr[4];
uint8_t port;
uint8_t buf[BUF_SIZE];
while((len = recvfrom(1,buf,BUF_SIZE,addr,&port)) > 0){
  // 对从addr:port收到的数据进行处理
}

获取链接对象的IP地址

UDP下,recvfrom中直接能获得对方的ip地址。但是对于TCP链接,主要是TCP服务器,如果需要知道链接对象的ip地址的话。

uint8_t ip[4];
uint16_t port;
// 获得连接对象的ip
getSn_DIPR(sn,ip);
// 获得连接对象的端口号
port = getSn_DPORT(sn);

等价于调用socket.h中的:

uint8_t ip[4];
uint16_t port;
// 获得连接对象的ip
getsockopt(sn,SO_DESTIP,(void *)ip);
// 获得连接对象的端口号
getsockopt(sn,SO_DESTPORT,(void *)&port);

自动心跳包检测

对于TCP链接,如果意外断连,比如网线被拔了呀之类的,W5500很可能并没法知道实际已经没有链接了,然后就认为自己还连着,一直等待着数据,然后就很爆炸。
为此,需要加入心跳检测来保证确实通讯还正常。
为了开启自动心跳检测,在socket初始化为TCP模式后:

setSn_KPALVTR(sn,2);    // 设置心跳包自动发送间隔,单位时间为5s,所以这里设置为10s。为0则不启用。

以上代码等价于:

uint8_t t = 2;
setsockopt(sn, SO_KEEPALIVEAUTO, (void*)&t);

需要注意的是,KeepAlive包会在socket状态变为SOCK_ESTABLISHED且与对方至少进行过一次收或发的通讯后进行传输。所以建立链接后建议立即随便发点什么。

当然,也可以手动发送心跳包(没开启自动发送才行):

setsockopt(sn, SO_KEEPALIVESEND, (void *)0);

断连TCP链接

如需要主动断开TCP链接,或者更多情况下是因为发现socket的状态为SOCK_CLOSE_WAIT半关闭状态,而需要断开连接。
则调用disconnect函数:

// 描述:  断开一个连接着的socket
// 参数:  sn           socket号(0-7)
// 返回:  SOCK_OK             如果成功
//        SOCKERR_SOCKNUM     无效的socket号
//        SOCKERR_SOCKMODE    socket模式无效
//        SOCKERR_TIMEOUT     发生超时
//        SOCK_BUSY           socket正忙
// 注意:1. 仅在tcp服务器或客户端模式下有效
//      2. 在阻塞io(默认)模式下,直到断开完成才会返回
//      3. 在非阻塞io(即指定了SF_IO_NONBLOCK)模式下,会立即返回SOCK_BUSY
int8_t  disconnect(uint8_t sn);

关闭socket

如需要关闭socket,调用close函数:

// 描述:  关闭一个socket
// 参数:  sn           socket号(0-7)
// 返回:  SOCK_OK             如果成功
//        SOCKERR_SOCKNUM     无效的socket号
int8_t  close(uint8_t sn);

其实调用socket的时候内部会先调用一次close函数,所以其实没必要先调用close来关闭socket。

中断

W5500提供了中断机制来提高MCU响应的实时性。
使用中断机制来设计与W5500通讯程序属于较高级内容,这里不展开。但是要注意的是,如果使用中断且用了官方的IO库,要避开SENDOK和TIMEOUT中断,这两个中断被IO库内部使用,如果自己清零的话会导致库的运行不正常。
另外,中断标志位一个常用的技巧是用来判断接收到了数据以及新建立了tcp链接:

  uint8_t ir;
  ir = getSn_IR(sn);
  if(ir & Sn_IR_CON){
    setSn_IR(sn, Sn_IR_CON);  // 清零标志位
    // socket sn建立了新tcp链接
  }
  if(ir & Sn_IR_RECV){
    setSn_IR(sn, Sn_IR_RECV);  // 清零标志位
    // socket sn收到了新数据,此时应该调用对应接收函数来接收数据
  }

注意,虽然轮询的这个寄存器叫中断寄存器(IR),但以上还属于轮询方式设计程序。

杂项

UDP广播

这其实是TCP/IP的知识点,不属于W5500使用上的问题。广播时根据需求构造目标IP地址
如果要全局广播,则发往255.255.255.255
如果要在192.168.2.0/24(即掩码为255.255.255.0)上广播,则发往192.168.2.255
具体代码就不写,和sendto那里的示例代码就差个ip地址具体的值。

UDP多播

发送数据给多播地址没什么可说的,就是设置为多播的ip地址就行。下面讲下加入多播地址。
加入多播地址需要在打开socket为UDP前设置DIPR和DPORT为对应的组播地址,然后打开socket时使能多播。
与WIN7通讯时发现要使用IGMP版本2才能成功通讯(即flag中启用SF_IGMP_VER2 )
加入组播地址后,socket就无法发送普通UDP包了,只能发送组播包。
要想在同个UDP端口发送普通UDP包必须重新开个socket。
示例代码:

// 加入组播地址为224.0.0.251:6000
uint8_t ip[4] = {224,0,0,251};
setSn_DIPR(sn,dAddr.ip);
setSn_DPORT(sn,6000);
socket(sn,Sn_MR_UDP,6000,SF_IO_NONBLOCK | SF_IGMP_VER2 | SF_MULTI_ENABLE);

后记

本文基于自己的研究学习总结了W5500简单的使用方法和官方IO库的常用操作,能力有限,难免有所疏漏,敬请指出。

参考文献

[1] WIZnet Co., Ltd. W5500数据手册 v1.3

你可能感兴趣的:(嵌入式开发)