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