外設驅動庫開發筆記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

​歡迎關注:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM