1. 程式人生 > >W5500簡單使用及官方IO庫 快速入門

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開發板基本都長一個樣,其上基本也就這幾個引腳

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
整個官方庫的包的檔案結構如下:

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);