前段时间折腾了好久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开发板基本都长一个样,其上基本也就这几个引脚
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上,这个口有下降沿中断功能。
忘记官方网站上怎么找的了,直接给出github链接,另,本文基于V3.1.1,最近新出的V4.0.0还没测试过:
https://github.com/Wiznet/ioLibrary_Driver
整个官方库的包的文件结构如下:
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
如果编译时报错说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和临界区的具体实现,于是需要用户主动把相关驱动函数注册给库使用。
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);
实际上,如果不需要定制缓冲区大小的话,直接把软重置当做初始化来用就行。
这是个毫无意义的功能,但还是蛮提一下。
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状态机的方法是读取状态寄存器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最初处于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);
如把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,这并不代表调用就失败了。
如把socket 2配置为udp模式,并绑定5555端口:
if(socket(2,Sn_MR_UDP,5555,SF_IO_NONBLOCK) == 2){
// 打开成功
}else{
// 打开失败
}
主要就是改了下协议。
SF_IO_NONBLOCK 作用同上。如果flags参数不需要的话直接传个0进去就行。
在成功把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);
在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){
// 对刚收到的数据进行处理
}
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收到的数据进行处理
}
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链接,或者更多情况下是因为发现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,调用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),但以上还属于轮询方式设计程序。
这其实是TCP/IP的知识点,不属于W5500使用上的问题。广播时根据需求构造目标IP地址
如果要全局广播,则发往255.255.255.255
如果要在192.168.2.0/24(即掩码为255.255.255.0)上广播,则发往192.168.2.255
具体代码就不写,和sendto那里的示例代码就差个ip地址具体的值。
发送数据给多播地址没什么可说的,就是设置为多播的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