第39章 ETH—Lwip以太網通信
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載:www.firebbs.cn
野火視頻教程優酷觀看網址:http://i.youku.com/firege
互聯網技術對人類社會的影響不言而喻。當今大部分電子設備都能以不同的方式接入互聯網(Internet),在家庭中PC常見的互聯網接入方式是使用路由器(Router)組建小型局域網(LAN),利用互聯網專線或者調制調解器(modem)經過電話線網絡,連接到互聯網服務提供商(ISP),由互聯網服務提供商把用戶的局域網接入互聯網。而企業或學校的局域網規模較大,常使用交換機組成局域網,經過路由以不同的方式接入到互聯網中。
39.1 互聯網模型
通信至少是兩個設備的事,需要相互兼容的硬件和軟件支持,我們稱之為通信協議。以太網通信在結構比較復雜,國際標准組織將整個以太網通信結構制定了OSI模型,總共分層七個層,分別為應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層以及物理層,每個層功能不同,通信中各司其職,整個模型包括硬件和軟件定義。OSI模型是理想分層,一般的網絡系統只是涉及其中幾層。
TCP/IP是互聯網最基本的協議,是互聯網通信使用的網絡協議,由網絡層的IP協議和傳輸層的TCP協議組成。TCP/IP只有四個分層,分別為應用層、傳輸層、網絡層以及網絡訪問層。雖然TCP/IP分層少了,但與OSI模型是不沖突的,它把OSI模型一些層次整合一起的,本質上可以實現相同功能。
實際上,還有一個TCP/IP混合模型,分為五個層,參考圖 391,它實際與TCP/IP四層模型是相通的,只是把網絡訪問層拆成數據鏈路層和物理層。這種分層方法對我們學習理解更容易。
圖 391 TCP/IP混合參考模型
設計網絡時,為了降低網絡設計的復雜性,對組成網絡的硬件、軟件進行封裝、分層,這些分層即構成了網絡體系模型。在兩個設備相同層之間的對話、通信約定,構成了層級協議。設備中使用的所有協議加起來統稱協議棧。在這個網絡模型中,每一層完成不同的任務,都提供接口供上一層訪問。而在每層的內部,可以使用不同的方式來實現接口,因而內部的改變不會影響其它層。
在TCP/IP混合參考模型中,數據鏈路層又被分為LLC層(邏輯鏈路層)和MAC層(媒體介質訪問層)。目前,對於普通的接入網絡終端的設備, LLC層和MAC層是軟、硬件的分界線。如PC的網卡主要負責實現參考模型中的MAC子層和物理層,在PC的軟件系統中則有一套龐大程序實現了LLC層及以上的所有網絡層次的協議。
由硬件實現的物理層和MAC子層在不同的網絡形式有很大的區別,如以太網和Wi-Fi,這是由物理傳輸方式決定的。但由軟件實現的其它網絡層次通常不會有太大區別,在PC上也許能實現完整的功能,一般支持所有協議,而在嵌入式領域則按需要進行裁剪。
39.2 以太網
以太網(Ethernet)是互聯網技術的一種,由於它是在組網技術中占的比例最高,很多人直接把以太網理解為互聯網。
以太網是指遵守IEEE 802.3標准組成的局域網,由IEEE 802.3標准規定的主要是位於參考模型的物理層(PHY)和數據鏈路層中的介質訪問控制子層(MAC)。在家庭、企業和學校所組建的PC局域網形式一般也是以太網,其標志是使用水晶頭網線來連接(當然還有其它形式)。IEEE還有其它局域網標准,如IEEE 802.11是無線局域網,俗稱Wi-Fi。IEEE 802.15是個人域網,即藍牙技術,其中的802.15.4標准則是ZigBee技術。
現階段,工業控制、環境監測、智能家居的嵌入式設備產生了接入互聯網的需求,利用以太網技術,嵌入式設備可以非常容易地接入到現有的計算機網絡中。
39.2.1 PHY層
在物理層,由IEEE 802.3標准規定了以太網使用的傳輸介質、傳輸速度、數據編碼方式和沖突檢測機制,物理層一般是通過一個PHY芯片實現其功能的。
1. 傳輸介質
傳輸介質包括同軸電纜、雙絞線(水晶頭網線是一種雙絞線)、光纖。根據不同的傳輸速度和距離要求,基於這三類介質的信號線又衍生出很多不同的種類。最常用的是"五類線"適用於100BASE-T和10BASE-T的網絡,它們的網絡速率分別為100Mbps和10Mbps。
2. 編碼
為了讓接收方在沒有外部時鍾參考的情況也能確定每一位的起始、結束和中間位置,在傳輸信號時不直接采用二進制編碼。在10BASE-T的傳輸方式中采用曼徹斯特編碼,在100BASE-T中則采用4B/5B編碼。
曼徹斯特編碼把每一個二進制位的周期分為兩個間隔,在表示"1"時,以前半個周期為高電平,后半個周期為低電平。表示"0"時則相反,見圖 392
圖 392 曼徹斯特編碼
采用曼徹斯特碼在每個位周期都有電壓變化,便於同步。但這樣的編碼方式效率太低,只有50%。
在100BASE-T 采用的4B/5B編碼是把待發送數據位流的每4位分為一組,以特定的5位編碼來表示,這些特定的5位編碼能使數據流有足夠多的跳變,達到同步的目的,而且效率也從曼徹斯特編碼的50%提高到了80%。
3. CSMA/CD沖突檢測
早期的以太網大多是多個節點連接到同一條網絡總線上(總線型網絡),存在信道競爭問題,因而每個連接到以太網上的節點都必須具備沖突檢測功能。以太網具備CSMA/CD沖突檢測機制,如果多個節點同時利用同一條總線發送數據,則會產生沖突,總線上的節點可通過接收到的信號與原始發送的信號的比較檢測是否存在沖突,若存在沖突則停止發送數據,隨機等待一段時間再重傳。
現在大多數局域網組建的時候很少采用總線型網絡,大多是一個設備接入到一個獨立的路由或交換機接口,組成星型網絡,不會產生沖突。但為了兼容,新出的產品還是帶有沖突檢測機制。
39.2.2 MAC子層
1. MAC的功能
MAC子層是屬於數據鏈路層的下半部分,它主要負責與物理層進行數據交接,如是否可以發送數據,發送的數據是否正確,對數據流進行控制等。它自動對來自上層的數據包加上一些控制信號,交給物理層。接收方得到正常數據時,自動去除MAC控制信號,把該數據包交給上層。
2. MAC數據包
IEEE對以太網上傳輸的數據包格式也進行了統一規定,見圖 393。該數據包被稱為MAC數據包。
圖 393 MAC數據包格式
MAC數據包由前導字段、幀起始定界符、目標地址、源地址、數據包類型、數據域、填充域、校驗和域組成。
前導字段,也稱報頭,這是一段方波,用於使收發節點的時鍾同步。內容為連續7個字節的0x55。字段和幀起始定界符在MAC收到數據包后會自動過濾掉。
幀起始定界符(SFD):用於區分前導段與數據段的,內容為0xD5。
MAC地址: MAC地址由48位數字組成,它是網卡的物理地址,在以太網傳輸的最底層,就是根據MAC地址來收發數據的。部分MAC地址用於廣播和多播,在同一個網絡里不能有兩個相同的MAC地址。PC的網卡在出廠時已經設置好了MAC地址,但也可以通過一些軟件來進行修改,在嵌入式的以太網控制器中可由程序進行配置。數據包中的DA是目標地址,SA是源地址。
數據包類型:本區域可以用來描述本MAC數據包是屬於TCP/IP協議層的IP包、ARP包還是SNMP包,也可以用來描述本MAC數據包數據段的長度。如果該值被設置大於0x0600,不用於長度描述,而是用於類型描述功能,表示與以太網幀相關的MAC客戶端協議的種類。
數據段:數據段是MAC包的核心內容,它包含的數據來自MAC的上層。其長度可以從0~1500字節間變化。
填充域:由於協議要求整個MAC數據包的長度至少為64字節(接收到的數據包如果少於64字節會被認為發生沖突,數據包被自動丟棄),當數據段的字節少於46字節時,在填充域會自動填上無效數據,以使數據包符合長度要求。
校驗和域:MAC數據包的尾部是校驗和域,它保存了CRC校驗序列,用於檢錯。
以上是標准的MAC數據包,IEEE 802.3同時還規定了擴展的MAC數據包,它是在標准的MAC數據包的SA和數據包類型之間添加4個字節的QTag前綴字段,用於獲取標志的MAC幀。前2個字節固定為0x8100,用於識別QTag前綴的存在;后兩個字節內容分別為3個位的用戶優先級、1個位的標准格式指示符(CFI)和一個12位的VLAN標識符。
39.3 TCP/IP協議棧
標准TCP/IP協議是用於計算機通信的一組協議,通常稱為TCP/IP協議棧,通俗講就是符合以太網通信要求的代碼集合,一般要求它可以實現圖 391中每個層對應的協議,比如應用層的HTTP、FTP、DNS、SMTP協議,傳輸層的TCP、UDP協議、網絡層的IP、ICMP協議等等。關於TCP/IP協議詳細內容推薦閱讀《TCP-IP詳解》和《用TCP/IP進行網際互連》理解。
Windows操作系統、UNIX類操作系統都有自己的一套方法來實現TCP/IP通信協議,它們都提供非常完整的TCP/IP協議。對於一般的嵌入式設備,受制於硬件條件沒辦法支持使用在Window或UNIX類操作系統的運行的TCP/IP協議棧,一般只能使用簡化版本的TCP/IP協議棧,目前開源的適合嵌入式的有uIP、TinyTCP、uC/TCP-IP、LwIP等等。其中LwIP是目前在嵌入式網絡領域被討論和使用廣泛的協議棧。本章內容其中一個目的就是移植LwIP到開發板上運行。
39.3.1 為什么需要協議棧
物理層主要定義物理介質性質,MAC子層負責與物理層進行數據交接,這兩部分是與硬件緊密聯系的,就嵌入式控制芯片來說,很多都內部集成了MAC控制器,完成MAC子層功能,所以依靠這部分功能是可以實現兩個設備數據交換,而時間傳輸的數據就是MAC數據包,發送端封裝好數據包,接收端則解封數據包得到可用數據,這樣的一個模型與使用USART控制器實現數據傳輸是非常類似的。但如果將以太網運用在如此基礎的功能上,完全是大材小用,因為以太網具有傳輸速度快、可傳輸距離遠、支持星型拓撲設備連接等等強大功能。功能強大的東西一般都會用高級的應用,這也是設計者的初衷。
使用以太網接口的目的就是為了方便與其它設備互聯,如果所有設備都約定使用一種互聯方式,在軟件上加一些層次來封裝,這樣不同系統、不同的設備通訊就變得相對容易了。而且只要新加入的設備也使用同一種方式,就可以直接與之前存在於網絡上的其它設備通訊。這就是為什么產生了在MAC之上的其它層次的網絡協議及為什么要使用協議棧的原因。又由於在各種協議棧中TCP/IP協議棧得到了最廣泛使用,所有接入互聯網的設備都遵守TCP/IP協議。所以,想方便地與其它設備互聯通信,需要提供對TCP/IP協議的支持。
39.3.2 各網絡層的功能
用以太網和Wi-Fi作例子,它們的MAC子層和物理層有較大的區別,但在MAC之上的LLC層、網絡層、傳輸層和應用層的協議,是基本上同的,這幾層協議由軟件實現,並對各層進行封裝。根據TCP/IP協議,各層的要實現的功能如下:
LLC層:處理傳輸錯誤;調節數據流,協調收發數據雙方速度,防止發送方發送得太快而接收方丟失數據。主要使用數據鏈路協議。
網絡層:本層也被稱為IP層。LLC層負責把數據從線的一端傳輸到另一端,但很多時候不同的設備位於不同的網絡中(並不是簡單的網線的兩頭)。此時就需要網絡層來解決子網路由拓撲問題、路徑選擇問題。在這一層主要有IP協議、ICMP協議。
傳輸層:由網絡層處理好了網絡傳輸的路徑問題后,端到端的路徑就建立起來了。傳輸層就負責處理端到端的通訊。在這一層中主要有TCP、UDP協議
應用層:經過前面三層的處理,通訊完全建立。應用層可以通過調用傳輸層的接口來編寫特定的應用程序。而TCP/IP協議一般也會包含一些簡單的應用程序如Telnet遠程登錄、FTP文件傳輸、SMTP郵件傳輸協議。
實際上,在發送數據時,經過網絡協議棧的每一層,都會給來自上層的數據添加上一個數據包的頭,再傳遞給下一層。在接收方收到數據時,一層層地把所在層的數據包的頭去掉,向上層遞交數據,參考圖 394。
圖 394 數據經過每一層的封裝和還原
39.4 以太網外設(ETH)
STM32F42x系列控制器內部集成了一個以太網外設,它實際是一個通過DMA控制器進行介質訪問控制(MAC),它的功能就是實現MAC層的任務。借助以太網外設,STM32F42x控制器可以通過ETH外設按照IEEE 802.3-2002標准發送和接收MAC數據包。ETH內部自帶專用的DMA控制器用於MAC,ETH支持兩個工業標准接口介質獨立接口(MII)和簡化介質獨立接口(RMII)用於與外部PHY芯片連接。MII和RMII接口用於MAC數據包傳輸,ETH還集成了站管理接口(SMI)接口專門用於與外部PHY通信,用於訪問PHY芯片寄存器。
物理層定義了以太網使用的傳輸介質、傳輸速度、數據編碼方式和沖突檢測機制,PHY芯片是物理層功能實現的實體,生活中常用水晶頭網線+水晶頭插座+PHY組合構成了物理層。
ETH有專用的DMA控制器,它通過AHB主從接口與內核和存儲器相連,AHB主接口用於控制數據傳輸,而AHB從接口用於訪問"控制與狀態寄存器"(CSR)空間。在進行數據發送是,先將數據有存儲器以DMA傳輸到發送TX FIFO進行緩沖,然后由MAC內核發送;接收數據時,RX FIFO先接收以太網數據幀,再由DMA傳輸至存儲器。ETH系統功能框圖見圖 395。
圖 395 ETH功能框圖
39.4.1 SMI接口
SMI是MAC內核訪問PHY寄存器標志接口,它由兩根線組成,數據線MDIO和時鍾線MDC。SMI支持訪問32個PHY,這在設備需要多個網口時非常有用,不過一般設備都只使用一個PHY。PHY芯片內部一般都有32個16位的寄存器,用於配置PHY芯片屬性、工作環境、狀態指示等等,當然很多PHY芯片並沒有使用到所有寄存器位。MAC內核就是通過SMI向PHY的寄存器寫入數據或從PHY寄存器讀取PHY狀態,一次只能對一個PHY的其中一個寄存器進行訪問。SMI最大通信頻率為2.5MHz,通過控制以太網MAC MII地址寄存器 (ETH_MACMIIAR)的CR位可選擇時鍾頻率。
1. SMI幀格式
SMI是通過數據幀方式與PHY通信的,幀格式如表 391,數據位傳輸順序從左到右。
表 391 SMI幀格式
管理幀字段 |
||||||||
報頭(32bit) |
起始 |
操作 |
PADDR |
RADDR |
TA |
數據(16bit) |
空閑 |
|
讀取 |
111…111 |
01 |
10 |
ppppp |
rrrrr |
Z0 |
ddd…ddd |
Z |
寫入 |
111…111 |
01 |
01 |
ppppp |
rrrrr |
10 |
ddd…ddd |
Z |
PADDR用於指定PHY地址,每個PHY都有一個地址,一般由PHY硬件設計決定,所以是固定不變的。RADDR用於指定PHY寄存器地址。TA為狀態轉換域,若為讀操作,MAC輸出兩個位高阻態,而PHY芯片則在第一位時輸出高阻態,第二位時輸出"0"。若為寫操作,MAC輸出"10",PHY芯片則輸出高阻態。數據段有16位,對應PHY寄存器每個位,先發送或接收到的位對應以太網 MAC MII 數據寄存器(ETH_MACMIIDR)寄存器的位15。
2. SMI讀寫操作
當以太網MAC MII地址寄存器 (ETH_MACMIIAR)的寫入位和繁忙位被置1時,SMI將向指定的PHY芯片指定寄存器寫入ETH_MACMIIDR中的數據。寫操作時序見圖 396。
圖 396 SMI寫操作
當以太網MAC MII地址寄存器 (ETH_MACMIIAR)的寫入位為0並且繁忙位被置1時,SMI將從向指定的PHY芯片指定寄存器讀取數據到ETH_MACMIIDR內。讀操作時序見圖 397。
圖 397 SMI讀操作
39.4.2 MII和RMII接口
介質獨立接口(MII)用於理解MAC控制器和PHY芯片,提供數據傳輸路徑。RMII接口是MII接口的簡化版本,MII需要16根通信線,RMII只需7根通信,在功能上是相同的。圖 398為MII接口連接示意圖,圖 399為RMII接口連接示意圖。
圖 398 MII接口連接
圖 399 RMII接口連接
TX_CLK:數據發送時鍾線。標稱速率為10Mbit/s時為2.5MHz;速率為100Mbit/s時為25MHz。RMII接口沒有該線。
RX_CLK:數據接收時鍾線。標稱速率為10Mbit/s時為2.5MHz;速率為100Mbit/s時為25MHz。RMII接口沒有該線。
TX_EN:數據發送使能。在整個數據發送過程保存有效電平。
TXD[3:0]或TXD[1:0]:數據發送數據線。對於MII有4位,RMII只有2位。只有在TX_EN處於有效電平數據線才有效。
CRS:載波偵聽信號,由PHY芯片負責驅動,當發送或接收介質處於非空閑狀態時使能該信號。在全雙工模式該信號線無效。
COL:沖突檢測信號,由PHY芯片負責驅動,檢測到介質上存在沖突后該線被使能,並且保持至沖突解除。在全雙工模式該信號線無效。
RXD[3:0]或RXD[1:0]:數據接收數據線,由PHY芯片負責驅動。對於MII有4位,RMII只有2位。在MII模式,當RX_DV禁止、RX_ER使能時,特定的RXD[3:0]值用於傳輸來自PHY的特定信息。
RX_DV:接收數據有效信號,功能類似TX_EN,只不過用於數據接收,由PHY芯片負責驅動。對於RMII接口,是把CRS和RX_DV整合成CRS_DV信號線,當介質處於不同狀態時會自切換該信號狀態。
RX_ER:接收錯誤信號線,由PHY驅動,向MAC控制器報告在幀某處檢測到錯誤。
REF_CLK:僅用於RMII接口,由外部時鍾源提供50MHz參考時鍾。
因為要達到100Mbit/s傳輸速度,MII和RMII數據線數量不同,使用MII和RMII在時鍾線的設計是完全不同的。對於MII接口,一般是外部為PHY提供25MHz時鍾源,再由PHY提供TX_CLK和RX_CLK時鍾。對於RMII接口,一般需要外部直接提供50MHz時鍾源,同時接入MAC和PHY。
開發板板載的PHY芯片型號為LAN8720A,該芯片只支持RMII接口,電路設計時參考圖 399。
ETH相關硬件在STM32F42x控制器分布參考表 392。
表 392 ETH復用引腳
ETH(AF11) |
GPIO |
|
MII |
MII_TX_CLK |
PC3 |
MII_TXD0 |
PB12/PG13 |
|
MII_TXD1 |
PB13/PG14 |
|
MII_TXD2 |
PC2 |
|
MII_TXD3 |
PB8/PE2 |
|
MII_TX_EN |
PB11/PG11 |
|
MII_RX_CLK |
PA1 |
|
MII_RXD0 |
PC4 |
|
MII_RXD1 |
PC5 |
|
MII_RXD2 |
PB0/PH6 |
|
MII_RXD3 |
PB1/PH7 |
|
MII_RX_ER |
PB10/PI10 |
|
MII_RX_DV |
PA7 |
|
MII_CRS |
PA0/PH2 |
|
MII_COL |
PA3/PH3 |
|
RMII |
RMII_TXD0 |
PB12/PG13 |
RMII_TXD1 |
PB13/PG14 |
|
RMII_TX_EN |
PG11 |
|
RMII_RXD0 |
PC4 |
|
RMII_RXD1 |
PC5 |
|
RMII_CRS_DV |
PA7 |
|
RMII_REF_CLK |
PA1 |
|
SMI |
MDIO |
PA2 |
MDC |
PC1 |
|
其他 |
PPS_OUT |
PB5/PG8 |
其中,PPS_OUT是IEEE 1588定義的一個時鍾同步機制。
39.4.3 MAC數據包發送和接收
ETH外設負責MAC數據包發送和接收。利用DMA從系統寄存器得到數據包數據內容,ETH外設自動填充完成MAC數據包封裝,然后通過PHY發送出去。在檢測到有MAC數據包需要接收時,ETH外設控制數據接收,並解封MAC數據包得到解封后數據通過DMA傳輸到系統寄存器內。
1. MAC數據包發送
MAC數據幀發送全部由DMA控制,從系統存儲器讀取的以太網幀由DMA推入FIFO,然后將幀彈出並傳輸到MAC內核。幀傳輸結束后,從MAC內核獲取發送狀態並傳回DMA。在檢測到SOF(Start Of Frame)時,MAC接收數據並開始MII發送。在EOF(End Of Frame)傳輸到MAC內核后,內核將完成正常的發送,然后將發送狀態返回給DMA。如果在發送過程中發送常規沖突,MAC內核將使發送狀態有效,然后接受並丟棄所有后續數據,直至收到下一SOF。檢測到來自MAC的重試請求時,應從SOF重新發送同一幀。如果發送期間未連續提供數據,MAC將發出下溢狀態。在幀的正常傳輸期間,如果MAC在未獲得前一幀的EOF的情況下接收到SOF,則將忽略該SOF並將新的幀視為前一幀的延續。
MAC控制MAC數據包的發送操作,它會自動生成前導字段和SFD以及發送幀狀態返回給DMA,在半雙工模式下自動生成阻塞信號,控制jabber(MAC看門狗)定時器用於在傳輸字節超過2048字節時切斷數據包發送。在半雙工模式下,MAC使用延遲機制進行流量控制,程序通過將ETH_MACFCR寄存器的BPA位置1來請求流量控制。MAC包含符合IEEE 1588的時間戳快照邏輯。MAC數據包發送時序參考圖 3910。
圖 3910 MAC數據包發送時序(無沖突)
2. MAC數據包接收
MAC接收到的數據包填充RX FIFO,達到FIFO設定閾值后請求DMA傳輸。在默認直通模式下,當FIFO接收到64個字節(使用ETH_DMAOMR寄存器中的RTC位配置)或完整的數據包時,數據將彈出,其可用性將通知給DMA。DMA向AHB接口發起傳輸后,數據傳輸將從FIFO持續進行,直到傳輸完整個數據包。完成EOF幀的傳輸后,狀態字將彈出並發送到DMA控制器。在Rx FIFO存儲轉發模式(通過ETH_DMAOMR寄存器中的RSF位配置)下,僅在幀完全寫入Rx FIFO后才可讀出幀。
當MAC在MII上檢測到SFD時,將啟動接收操作。MAC內核將去除報頭和SFD,然后再繼續處理幀。檢查報頭字段以進行過濾,FCS字段用於驗證幀的CRC如果幀未通過地址濾波器,則在內核中丟棄該幀。MAC數據包接收時序參考圖 3911。
圖 3911 MAC數據包接收時序(無錯誤)
39.4.4 MAC過濾
MAC過濾功能可以選擇性的過濾設定目標地址或源地址的MAC幀。它將檢查所有接收到的數據幀的目標地址和源地址,根據過濾選擇設定情況,檢測后報告過濾狀態。針對目標地址過濾可以有三種,分別是單播、多播和廣播目標地址過濾;針對源地址過濾就只有單播源地址過濾。
單播目標地址過濾是將接收的相應DA字段與預設的以太網MAC地址寄存器內容比較,最高可預設4個過濾MAC地址。多播目標地址過濾是根據幀過濾寄存器中的HM位執行對多播地址的過濾,是對MAC地址寄存器進行比較來實現的。單播和多播目標地址過濾都還支持Hash過濾模式。廣播目標地址過濾通過將幀過濾寄存器的BFD位置1使能,這使得MAC丟棄所有廣播幀。
單播源地址過濾是將接收的SA字段與SA寄存器內容進行比較過濾。
MAC過濾還具備反向過濾操作功能,即讓過濾結構求補集。
39.5 PHY:LAN8720A
LAN8720A是SMSC公司(已被Microchip公司收購)設計的一個體積小、功耗低、全能型10/100Mbps的以太網物理層收發器。它是針對消費類電子和企業應用而設計的。LAN8720A總共只有24Pin,僅支持RMII接口。由它組成的網絡結構見圖 3912。
圖 3912 由LAN8720A組成的網絡系統結構
LAN8720A通過RMII與MAC連接。RJ45是網絡插座,在與LAN8720A連接之間還需要一個變壓器,所以一般使用帶電壓轉換和LED指示燈的HY911105A型號的插座。一般來說,必須為使用RMII接口的PHY提供50MHz的時鍾源輸入到REF_CLK引腳,不過LAN8720A內部集成PLL,可以將25MHz的時鍾源陪頻到50MHz並在指定引腳輸出該時鍾,所以我們可以直接使其與REF_CLK連接達到提供50MHz時鍾效果。
LAN8720A內部系統結構見圖 3913。
圖 3913 LAN8720A內部系統結構
LAN8720A有各個不同功能模塊組成,最重要的要數接收控制器和發送控制器,其它的基本上都是與外部引腳掛鈎,實現信號傳輸。部分引腳是具有雙重功能的,比如PHYAD0與RXER引腳是共用的,在系統上電后LAN8720A會馬上讀取這部分共用引腳的電平,以確定系統的狀態並保存在相關寄存器內,之后則自動轉入作為另一功能引腳。
PHYAD[0]引腳用於配置SMI通信的LAN8720A地址,在芯片內部該引腳已經自帶下拉電阻,默認認為0(即使外部懸空不接),在系統上電時會檢測該引腳獲取得到LAN8720A的地址為0或者1,並保存在特殊模式寄存器(R18)的PHYAD位中,該寄存器的PHYAD有5個位,在需要超過2個LAN8720A時可以通過軟件設置不同SMI通信地址。PHYAD[0]是與RXER引腳共用。
MODE[2:0]引腳用於選擇LAN8720A網絡通信速率和工作模式,可選10Mbps或100Mbps通信速度,半雙工或全雙工工作模式,另外LAN8720A支持HP Auto-MDIX自動翻轉功能,即可自動識別直連或交叉網線並自適應。一般將MODE引腳都設置為1,可以讓LAN8720A啟動自適應功能,它會自動尋找最優工作方式。MODE[0]與RXD0引腳共用、MODE[1]與RXD1引腳共用、MODE[2]與CRS_DV引腳共用。
nINT/REFCLKO引腳用於RMII接口中REF_CLK信號線,當nINTSEL引腳為低電平是,它也可以被設置成50MHz時鍾輸出,這樣可以直接與STM32F42x的REF_CLK引腳連接為其提供50MHz時鍾源,這種模式要求為XTAL1與XTAL2之間或為XTAL1/CLKIN提供25MHz時鍾,由LAN8720A內部PLL電路陪頻得到50MHz時鍾,此時nIN/REFCLKO引腳的中斷功能不可用,用於50MHz時鍾輸出。當nINTSEL引腳為高電平時,LAN8720A被設置為時鍾輸入,即外部時鍾源直接提供50MHz時鍾接入STM32F42x的REF_CLK引腳和LAN8720A的XTAL1/CLKIN引腳,此時nINT/REFCLKO可用於中斷功能。nINTSEL與LED2引腳共用,一般使用下拉
REGOFF引腳用於配置內部+1.2V電壓源,LAN8720A內部需要+1.2V電壓,可以通過VDDCR引腳輸入+1.2V電壓提供,也可以直接利用LAN8720A內部+1.2V穩壓器提供。當REGOFF引腳為低電平時選擇內部+1.2V穩壓器。REGOFF與LED1引腳共用。
SMI支持尋址32個寄存器,LAN8720A只用到其中14個,參考表 393。
表 393 LAN8720A寄存器列表
序號 |
寄存器名稱 |
分組 |
0 |
Basic Control Register |
Basic |
1 |
Basic Status Register |
Basic |
2 |
PHY Identifier 1 |
Extended |
3 |
PHY Identifier 2 |
Extended |
4 |
Auto-Negotiation Advertisement Register |
Extended |
5 |
Auto-Negotiation Link Partner Ability Register |
Extended |
6 |
Auto-Negotiation Expansion Register |
Extended |
17 |
Mode Control/Status Register |
Vendor-specific |
18 |
Special Modes |
Vendor-specific |
26 |
Symbol Error Counter Register |
Vendor-specific |
27 |
Control / Status Indication Register |
Vendor-specific |
29 |
Interrupt Source Register |
Vendor-specific |
30 |
Interrupt Mask Register |
Vendor-specific |
31 |
PHY Special Control/Status Register |
Vendor-specific |
序號與SMI數據幀中的RADDR是對應的,這在編寫驅動時非常重要,本文將它們標記為R0~R31。寄存器可規划為三個組:Basic、Extended和Vendor-specific。Basic是IEEE 802.3要求的,R0是基本控制寄存器,其位15為Soft Reset位,向該位寫1啟動LAN8720A軟件復位,還包括速度、自適應、低功耗等等功能設置。R1是基本狀態寄存器。Extended是擴展寄存器,包括LAN8720A的ID號、制造商、版本號等等信息。Vendor-specific是供應商自定義寄存器,R31是特殊控制/狀態寄存器,指示速度類型和自適應功能。
39.6 LwIP:輕型TCP/IP協議棧
LwIP是Light Weight Internet Protocol 的縮寫,是由瑞士計算機科學院Adam Dunkels等開發的適用於嵌入式領域的開源輕量級TCP/IP協議棧。它可以移植到含有操作系統的平台中,也可以在無操作系統的平台下運行。由於它開源、占用的RAM和ROM比較少、支持較為完整的TCP/IP協議、且十分便於裁剪、調試,被廣泛應用在中低端的32位控制器平台。可以訪問網站:http://savannah.nongnu.org/projects/lwip/ 獲取更多LwIP信息。
目前,LwIP最新更新到1.4.1版本,我們在上述網站可找到相應的LwIP源碼下載通道。我們下載兩個壓縮包:lwip-1.4.1.zip和contrib-1.4.1.zip,lwip-1.4.1.zip包括了LwIP的實現代碼,contrib-1.4.1.zip包含了不同平台移植LwIP的驅動代碼和使用LwIP實現的一些應用實例測試。
但是,遺憾的是contrib-1.4.1.zip並沒有為STM32平台提供實例,這對於初學者想要移植LwIP來說難度還是非常大的。ST公司也是認識到LwIP在嵌入式領域的重要性,所以他們針對LwIP應用開發了測試平台,其中有一個是在STM32F4x7系列控制器運行的(文件編號為:STSW-STM32070),雖然我們的開發板平台是STM32F429控制器,但經測試發現關於ETH驅動部分以及LwIP接口函數部分是可以通用的。為減少移植工作量,我們選擇使用ST官方例程相關文件,特別是ETH底層驅動部分函數,這樣我們也可以花更多精力在理解代碼實現方法上。
本章的一個重點內容就是介紹LwIP移植至我們的開發平台,詳細的操作步驟參考下文介紹。
39.7 ETH初始化結構體詳解
一般情況下,標准庫都會為外設建立一個外設對應的文件存放外設相關庫函數的實現,比如stm32f4xx_adc.c、stm32f4xx_can.c等等,然而標准庫並沒有為ETH外設建立相關的文件,這樣我們根本沒有標准庫函數可以使用,究其原因是ETH驅動函數與PHY芯片連續較為緊密,很難使用一套通用的代碼實現兼容。難道要我們自己寫寄存器實現?實際情況還沒有這么糟糕,正如上文所說的ST官方有提供LwIP方面的測試平台,特別是基於STM32F4x7控制器的測試平台是非常合適我們參考的。我們在解壓stsw-stm32070.rar壓縮包之后,在其文件目錄(…\STM32F4x7_ETH_LwIP_V1.1.1\Libraries\STM32F4x7_ETH_Driver\)下可找到stm32f4x7_eth.c、stm32f4x7_eth.h和stm32f4x7_eth_conf_template.h三個文件,其中的stm32f4x7_eth.c和stm32f4x7_eth.h就是類似stm32f4xx_adc.c是關於ETH外設的驅動,我們在以太網通信實現實驗中會使用到這三個文件,stm32f4x7_eth.c和stm32f4x7_eth.h兩個文件內容不用修改(不過修改了文件名稱)。
stm32f4x7_eth.h有定義了一個ETH外設初始化結構體ETH_InitTypeDef,理解結構體成員可以幫助我們使用ETH功能。初始化結構體成員用於設置ETH工作環境參數,並由ETH相應初始化配置函數或功能函數調用,這些設定參數將會設置ETH相應的寄存器,達到配置ETH工作環境的目的。
代碼清單 391 ETH_InitTypeDef
1 typedef struct {
2 /**
3 * @brief / * MAC
4 */
5 uint32_t ETH_AutoNegotiation; // 自適應功能
6 uint32_t ETH_Watchdog; // 以太網看門狗
7 uint32_t ETH_Jabber; // jabber定時器功能
8 uint32_t ETH_InterFrameGap; // 發送幀間間隙
9 uint32_t ETH_CarrierSense; // 載波偵聽
10 uint32_t ETH_Speed; // 以太網速度
11 uint32_t ETH_ReceiveOwn; // 接收自身
12 uint32_t ETH_LoopbackMode; // 回送模式
13 uint32_t ETH_Mode; // 模式
14 uint32_t ETH_ChecksumOffload; // 校驗和減荷
15 uint32_t ETH_RetryTransmission; // 傳輸重試
16 uint32_t ETH_AutomaticPadCRCStrip; // 自動去除PAD和FCS字段
17 uint32_t ETH_BackOffLimit; // 后退限制
18 uint32_t ETH_DeferralCheck; // 檢查延遲
19 uint32_t ETH_ReceiveAll; // 接收所有MAC幀
20 uint32_t ETH_SourceAddrFilter; // 源地址過濾
21 uint32_t ETH_PassControlFrames; // 傳送控制幀
22 uint32_t ETH_BroadcastFramesReception; // 廣播幀接收
23 uint32_t ETH_DestinationAddrFilter; // 目標地址過濾
24 uint32_t ETH_PromiscuousMode; // 混合模式
25 uint32_t ETH_MulticastFramesFilter; // 多播源地址過濾
26 uint32_t ETH_UnicastFramesFilter; // 單播源地址過濾
27 uint32_t ETH_HashTableHigh; // 散列表高位
28 uint32_t ETH_HashTableLow; // 散列表低位
29 uint32_t ETH_PauseTime; // 暫停時間
30 uint32_t ETH_ZeroQuantaPause; // 零時間片暫停
31 uint32_t ETH_PauseLowThreshold; // 暫停閾值下限
32 uint32_t ETH_UnicastPauseFrameDetect; // 單播暫停幀檢測
33 uint32_t ETH_ReceiveFlowControl; // 接收流控制
34 uint32_t ETH_TransmitFlowControl; // 發送流控制
35 uint32_t ETH_VLANTagComparison; // VLAN標記比較
36 uint32_t ETH_VLANTagIdentifier; // VLAN標記標識符
37 /**
38 * @brief / * DMA
39 */
40 uint32_t ETH_DropTCPIPChecksumErrorFrame; // 丟棄TCP/IP校驗錯誤幀
41 uint32_t ETH_ReceiveStoreForward; // 接收存儲並轉發
42 uint32_t ETH_FlushReceivedFrame; // 刷新接收幀
43 uint32_t ETH_TransmitStoreForward; // 發送存儲並並轉發
44 uint32_t ETH_TransmitThresholdControl; // 發送閾值控制
45 uint32_t ETH_ForwardErrorFrames; // 轉發錯誤幀
46 uint32_t ETH_ForwardUndersizedGoodFrames; // 轉發過小的好幀
47 uint32_t ETH_ReceiveThresholdControl; // 接收閾值控制
48 uint32_t ETH_SecondFrameOperate; // 處理第二個幀
49 uint32_t ETH_AddressAlignedBeats; // 地址對齊節拍
50 uint32_t ETH_FixedBurst; // 固定突發
51 uint32_t ETH_RxDMABurstLength; // DMA突發接收長度
52 uint32_t ETH_TxDMABurstLength; // DMA突發發送長度
53 uint32_t ETH_DescriptorSkipLength; // 描述符跳過長度
54 uint32_t ETH_DMAArbitration; // DMA仲裁
55 } ETH_InitTypeDef;
ETH_AutoNegotiation:自適應功能選擇,可選使能或禁止,一般選擇使能自適應功能,系統會自動尋找最優工作方式,包括選擇10Mbps或者100Mbps的以太網速度以及全雙工模式或半雙工模式。
ETH_Watchdog:以太網看門狗功能選擇,可選使能或禁止,它設定以太網MAC配置寄存器(ETH_MACCR)的WD位的值。如果設置為1,使能看門狗,在接收MAC幀超過2048字節時自動切斷后面數據,一般選擇使能看門狗。如果設置為0,禁用看門狗,最長可接收16384字節的幀。
ETH_Jabber:jabber定時器功能選擇,可選使能或禁止,與看門狗功能類似,只是看門狗用於接收MAC幀,jabber定時器用於發送MAC幀,它設定ETH_MACCR寄存器的JD位的值。如果設置為1,使能jabber定時器,在發送MAC幀超過2048字節時自動切斷后面數據,一般選擇使能jabber定時器。
ETH_InterFrameGap:控制發送幀間的最小間隙,可選96bit時間、88bit時間、…、40bit時間,他設定ETH_MACCR寄存器的IFG[2:0]位的值,一般設置96bit時間。
ETH_CarrierSense:載波偵聽功能選擇,可選使能或禁止,它設定ETH_MACCR寄存器的CSD位的值。當被設置為低電平時,MAC發送器會生成載波偵聽錯誤,一般使能載波偵聽功能。
ETH_Speed:以太網速度選擇,可選10Mbps或100Mbit/s,它設定ETH_MACCR寄存器的FES位的值,一般設置100Mbit/s,但在使能自適應功能之后該位設置無效。
ETH_ReceiveOwn:接收自身幀功能選擇,可選使能或禁止,它設定ETH_MACCR寄存器的ROD位的值,當設置為0時,MAC接收發送時PHY提供的所有MAC包,如果設置為1,MAC禁止在半雙工模式下接收幀。一般使能接收。
ETH_LoopbackMode:回送模式選擇,可選使能或禁止,它設定ETH_MACCR寄存器的LM位的值,當設置為1時,使能MAC在MII回送模式下工作。
ETH_Mode:以太網工作模式選擇,可選全雙工模式或半雙工模式,它設定ETH_MACCR寄存器DM位的值。一般選擇全雙工模式,在使能了自適應功能后該成員設置無效。
ETH_ChecksumOffload:IPv4校驗和減荷功能選擇,可選使能或禁止,它設定ETH_MACCR寄存器IPCO位的值,當該位被置1時使能接收的幀有效載荷的TCP/UDP/ICMP標頭的IPv4校驗和檢查。一般選擇禁用,此時PCE和IP HCE狀態位總是為0。
ETH_RetryTransmission:傳輸重試功能,可選使能或禁止,它設定ETH_MACCR寄存器RD位的值,當被設置為1時,MAC僅嘗試發送一次,設置為0時,MAC會嘗試根據BL的設置進行重試。一般選擇使能重試。
ETH_AutomaticPadCRCStrip:自動去除PAD和FCS字段功能,可選使能或禁用,它設定ETH_MACCR寄存器APCS位的值。當設置為1時,MAC在長度字段值小於或等於1500自己是去除傳入幀上的PAD和FCS字段。一般禁止自動去除PAD和FCS字段功能。
ETH_BackOffLimit:后退限制,在發送沖突后重新安排發送的延遲時間,可選10、8、4、1,它設定ETH_MACCR寄存器BL位的值。一般設置為10。
ETH_DeferralCheck:檢查延遲,可選使能或禁止,它設定ETH_MACCR寄存器DC位的值,當設置為0時,禁止延遲檢查功能,MAC發送延遲,直到CRS信號變成無效信號。
ETH_ReceiveAll:接收所有MAC幀,可選使能或禁用,它設定以太網MAC幀過濾寄存器(ETH_MACFFR)RA位的值。當設置為1時,MAC接收器將所有接收的幀傳送到應用程序,不過濾地址。當設置為0是,MAC接收會自動過濾不與SA/DA匹配的幀。一般選擇不接收所有。
ETH_SourceAddrFilter:源地址過濾,可選源地址過濾、源地址反向過濾或禁用源地址過濾,它設定ETH_MACFFR寄存器SAF位和SAIF位的值。一般選擇禁用源地址過濾。
ETH_PassControlFrames:傳送控制幀,控制所有控制幀的轉發,可選阻止所有控制幀到達應用程序、轉發所有控制幀、轉發通過地址過濾的控制幀,它設定ETH_MACFFR寄存器PCF位的值。一般選擇禁止轉發控制幀。
ETH_BroadcastFramesReception:廣播幀接收,可選使能或禁止,它設定ETH_MACFFR寄存器BFD位的值。當設置為0時,使能廣播幀接收,一般設置接收廣播幀。
ETH_DestinationAddrFilter:目標地址過濾功能選擇,可選正常過濾或目標地址反向過濾,它設定ETH_MACFFR寄存器DAIF位的值。一般設置為正常過濾。
ETH_PromiscuousMode:混合模式,可選使能或禁用,它設定ETH_MACFFR寄存器PM位的值。當設置為1時,不論目標或源地址,地址過濾器都傳送所有傳入的幀。一般禁用混合模式。
ETH_MulticastFramesFilter:多播源地址過濾,可選完美散列表過濾、散列表過濾、完美過濾或禁用過濾,它設定ETH_MACFFR寄存器HPF位、PAM位和HM位的值。一般選擇完美過濾。
ETH_UnicastFramesFilter:單播源地址過濾,可選完美散列表過濾、散列表過濾或完美過濾,它設定ETH_MACFFR寄存器HPF位和HU位的值。一般選擇完美過濾。
ETH_HashTableHigh:散列表高位,和ETH_HashTableLow組成64位散列表用於組地址過濾,它設定以太網MAC散列表高位寄存器(ETH_MACHTHR)的值。
ETH_HashTableLow:散列表低位,和ETH_ HashTableHigh組成64位散列表用於組地址過濾,它設定以太網MAC散列表低位寄存器(ETH_MACHTLR)的值。
ETH_PauseTime:暫停時間,保留發送控制幀中暫停時間字段要使用的值,可設置0至65535,它設定以太網MAC流控制寄存器(ETH_MACFCR)PT位的值。
ETH_ZeroQuantaPause:零時間片暫停,可選使用或禁止,它設定ETH_MACFCR寄存器ZQPD位的值。當設置為1時,當來自FIFO層的流控制信號去斷言后,此位會禁止自動生成零時間片暫停控制幀。一般選擇禁止。
ETH_PauseLowThreshold:暫停閾值下限,配置暫停定時器的閾值,達到該值值時,會自動程序傳輸暫停幀,可選暫停時間減去4個間隙、28個間隙、144個間隙或256個間隙,它設定ETH_MACFCR寄存器PLT位的值。一般選擇暫停時間減去4個間隙。
ETH_UnicastPauseFrameDetect:單播暫停幀檢測,可選使能或禁止,它設定ETH_MACFCR寄存器UPFD位的值。當設置為1時,MAC除了檢測具有唯一多播地址的暫停幀外,還會檢測具有ETH_MACA0HR和ETH_MACA0LR寄存器所指定的站單播地址的暫停幀。一般設置為禁止。
ETH_ReceiveFlowControl:接收流控制,可選使能或禁止,它設定ETH_MACFCR寄存器RFCE位的值。當設定為1時,MAC對接收到的暫停幀進行解碼,並禁止其在指定時間(暫停時間)內發送;當設置為0時,將禁止暫停幀的解碼功能,一般設置為禁止。
ETH_TransmitFlowControl:發送流控制,可選使能或禁止,它設定ETH_MACFCR寄存器TFCE位的值。在全雙工模式下,當設置為1時,MAC將使能流控制操作來發送暫停幀;為0時,將禁止MAC中的流控制操作,MAC不會傳送任何暫停幀。在半雙工模式下,當設置為1時,MAC將使能背壓操作;為0時,將禁止背壓功能。
ETH_VLANTagComparison:VLAN標記比較,可選12位或16位,它設定以太網MAC VLAN標記寄存器(ETH_MACVLANTR)VLANTC位的值。當設置為1時,使用12位VLAN標識符而不是完整的16位VLAN標記進行比較和過濾;為0時,使用全部16位進行比較,一般選擇16位。
ETH_VLANTagIdentifier:VLAN標記標識符,包含用於標識VLAN幀的802.1Q VLAN標記,並與正在接收的VLAN幀的第十五和第十六字節進行比較。位[15:13]是用戶優先級,位[12]是標准格式指示符(CFI),位[11:0]是VLAN標記的VLAN標識符(VID)字段。VLANTC位置1時,僅使用VID(位[11:0])進行比較。
ETH_DropTCPIPChecksumErrorFrame:丟棄TCP/IP校驗錯誤幀,可選使能或禁止,它設定以太網DMA工作模式寄存器(ETH_DMAOMR)DTCEFD位的值,當設置為 1時,如果幀中僅存在由接收校驗和減荷引擎檢測出來的錯誤,則內核不會丟棄它;為0時,如果FEF為進行了復位,則會丟棄所有錯誤幀。
ETH_ReceiveStoreForward:接收存儲並轉發,可選使能或禁止,它設定以太網DMA工作模式寄存器(ETH_DMAOMR)RSF位的值,當設置為1時,向RX FIFO寫入完整幀后可以從中讀取一幀,同時忽略接收閾值控制(RTC)位;當設置為0時,RX FIFO在直通模式下工作,取決於RTC位的閾值。一般選擇使能。
ETH_FlushReceivedFrame:刷新接收幀,可選使能或禁止,它設定ETH_DMAOMR寄存器FTF位的值,當設置為1時,發送FIFO控制器邏輯會恢復到缺省值,TX FIFO中的所有數據均會丟失/刷新,刷新結束后改為自動清零。
ETH_TransmitStoreForward:發送存儲並並轉發,可選使能或禁止,它設定ETH_DMAOMR寄存器TSF位的值,當設置為1時,如果TX FIFO有一個完整的幀則發送會啟動,會忽略TTC值;為0時,TTC值才會有效。一般選擇使能。
ETH_TransmitThresholdControl:發送閾值控制,有多個閾值可選,它設定ETH_DMAOMR寄存器TTC位的值,當TX FIFO中幀大小大於該閾值時發送會自動,對於小於閾值的全幀也會發送。
ETH_ForwardErrorFrames:轉發錯誤幀,可選使能或禁止,它設定ETH_DMAOMR寄存器FEF位的值,當設置為1時,除了段錯誤幀之外所有幀都會轉發到DMA;為0時,RX FIFO會丟棄滴啊有錯誤狀態的幀。一般選擇禁止。
ETH_ForwardUndersizedGoodFrames:轉發過小的好幀,可選使能或禁止,它設定ETH_DMAOMR寄存器FUGF位的值,當設置為1時,RX FIFO會轉發包括PAD和FCS字段的過小幀;為0時,會丟棄小於64字節的幀,除非接收閾值被設置為更低。
ETH_ReceiveThresholdControl:接收閾值控制,當RX FIFO中的幀大小大於閾值時啟動DMA傳輸請求,可選64字節、32字節、96字節或128字節,它設定ETH_DMAOMR寄存器RTC位的值。
ETH_SecondFrameOperate:處理第二個幀,可選使能或禁止,它設定ETH_DMAOMR寄存器OSF位的值,當設置為1時會命令DMA處理第二個發送數據幀。
ETH_AddressAlignedBeats:地址對齊節拍,可選使能或禁止,它設定以太網DMA總線模式寄存器(ETH_DMABMR)AAB位的值,當設置為1並且固定突發位(FB)也為1時,AHB接口會生成與起始地址LS位對齊的所有突發;如果FB位為0,則第一個突發不對齊,但后續的突發與地址對齊。一般選擇使能。
ETH_FixedBurst:固定突發,控制AHB主接口是否執行固定突發傳輸,可選使能或禁止,它設定ETH_DMABMR寄存器FB位的值,當設置為1時,AHB在正常突發傳輸開始期間使用SINGLE、INCR4、INCR8或INCR16;為0時,AHB使用SINGLE和INCR突發傳輸操作。
ETH_RxDMABurstLength:DMA突發接收長度,有多個值可選,一般選擇32Beat,可實現32*32bits突發長度,它設定ETH_DMABMR寄存器FPM位和RDP位的值。
ETH_TxDMABurstLength:DMA突發發送長度,有多個值可選,一般選擇32Beat,可實現32*32bits突發長度,它設定ETH_DMABMR寄存器FPM位和PBL位的值。
ETH_DescriptorSkipLength:描述符跳過長度,指定兩個未鏈接描述符之間跳過的字數,地址從當前描述符結束處開始跳到下一個描述符起始處,可選0~7,它設定ETH_DMABMR寄存器DSL位的值。
ETH_DMAArbitration:DMA仲裁,控制RX和TX優先級,可選RX TX優先級比為1:1、2:1、3:1、4:1或者RX優先於TX,它設定ETH_DMABMR寄存器PM位和DA位的值,當設置為1時,RX優先於TX;為0時,循環調度,RX TX優先級比由PM位給出。
39.8 以太網通信實驗:無操作系統LwIP移植
LwIP可以在帶操作系統上運行,亦可在無操作系統上運行,這一實驗我們講解在無操作系統的移植步驟,並實現簡單的傳輸代碼,后續章節會講解在帶操作系統移植過程,一般都是在無操作系統基礎上修改而來的。
39.8.1 硬件設計
在講解移植步驟之前,有必須先介紹我們的實驗硬件設計,主要是LAN8720A通過RMII和SMI接口與STM32F42x控制器連接,見圖 3914。
圖 3914 PHY硬件設計
電路設計時,將NINTSEL引腳通過下拉電阻拉低,設置NINT/FEFCLKO為輸出50MHz時鍾,當然前提是在XTAL1和XTAL2接入了25MHz的時鍾源。另外也把REGOFF引腳通過下拉電阻拉低,使能使用內部+1.2V穩壓器。
39.8.2 移植步驟
之前已經介紹了LwIP源代碼(lwip-1.4.1.zip)和ST官方LwIP測試平台資料(stsw-stm32070.zip)下載,我們移植步驟是基於這兩份資料進行的。
無操作系統移植LwIP需要的文件參考圖 3915,圖中只顯示了*.c文件,還需要用到對應的*.h文件。
圖 3915 LwIP移植實驗文件結構
接下來,我們就根據圖中文件結構詳解移植過程。實驗例程有需要用到系統滴答定時器systick、調試串口USART、獨立按鍵KEY、LED燈功能,對這些功能實現不做具體介紹,可以參考相關章節理解。
第一步:相關文件拷貝
首先,解壓lwip-1.4.1.zip和stsw-stm32070.zip兩個壓縮包,把整個lwip-1.4.1文件夾拷貝到USER文件夾下,特別說明,在整個移植過程中,不會對lwip-1.4.1.zip文件下的文件內容進行修改。然后,在stsw-stm32070文件夾找到port文件夾(路徑:… \Utilities\Third_Party\lwip-1.4.1\port),把整個port文件夾拷貝lwip-1.4.1文件夾中,在port文件夾下的STM32F4x7文件中把arch和Standalone兩個文件夾直接剪切到port文件夾中,即此時port文件夾有三個STM32F4x7、arch和Standalone文件夾,最后把STM32F4x7文件夾刪除,最終的文件結構見圖 3916,arch存放與開發平台相關頭文件,Standalone文件夾是無操作系統移植時ETH外設與LwIP連接的底層驅動函數。
圖 3916 LwIP相關文件拷貝
lwip-1.4.1文件夾下的doc文件夾存放LwIP版權、移植、使用等等說明文件,移植之前有必須認真瀏覽一遍;src文件夾存放LwIP的實現代碼,也是我們工程代碼真正需要的文件;test文件夾存放LwIP部分功能測試例程;另外,還有一些無后綴名的文件,都是一些說明性文件,可用記事本直接打開瀏覽。port文件夾存放LwIP與STM32平台連接的相關文件,正如上面所說contrib-1.4.1.zip包含了不同平台移植代碼,不過遺憾地是沒有STM32平台的,所以我們需要從ST官方提供的測試平台找到這部分連接代碼,也就是port文件夾的內容。
接下來,在Bsp文件下新建一個ETH文件夾,用於存放與ETH相關驅動文件,包括兩個部分文件,其中一個是ETH外設驅動文件,在stsw-stm32070文件夾中找到stm32f4x7_eth.h和stm32f4x7_eth.c兩個文件(路徑:…\Libraries\STM32F4x7_ETH_Driver\),將這兩個文件拷貝到ETH文件夾中,對應改名為stm32f429_eth.h和stm32f429_eth.c,這兩個文件是ETH驅動文件,類似標准庫中外設驅動代碼實現文件,在移植過程中我們幾乎不過文件的內容。這部分函數由port文件夾相關代碼調用。另外一部分是相關GPIO初始化、ETH外設初始化、PHY狀態獲取等等函數的實現,在stsw-stm32070文件夾中找到stm32f4x7_eth_bsp.c、stm32f4x7_eth_bsp.h和stm32f4x7_eth_conf.h三個文件(路徑:…\Project\Standalone\tcp_echo_client\),將這三個文件拷貝到ETH文件夾中,對應改名為stm32f429_phy.c、stm32f429_phy.h和stm32f429_eth_conf.h。因為,ST官方LwIP測試平台使用的PHY型號不是使用LAN8720A,所以這三個文件需要我們進行修改。
最后,是LwIP測試代碼實現,為測試LwIP移植是否成功和檢查LwIP功能,我們編寫TCP通信實現代碼,設置開發板為TCP從機,電腦端為TCP主機。在stsw-stm32070文件夾中找到netconf.c、tcp_echoclient.c、lwipopts.h、netconf.h和tcp_echoclient.h五個文件(路徑:…\Project\Standalone\tcp_echo_client\),直接拷貝到App文件夾(自己新建)中,netconf.c文件代碼實現LwIP初始化函數、周期調用函數、DHCP功能函數等等,tcp_echoclient.c文件實現TCP通信參數代碼,lwipopts.h包含LwIP功能選項。
第二部:為工程添加文件
第一步已經把相關的文件拷貝到對應的文件夾中,接下來就可以把需要用到的文件添加到工程中。圖 3915已經指示出來工程需要用到的*.c文件,所以最終工程文件結構見圖 3917,圖中api、ipv4和core都包含了對應文件夾下的所有*.c文件。
圖 3917 工程文件結構
接下來,還需要在工程選擇中添加相關頭文件路徑,參考圖 3918。
圖 3918 添加相關頭文件路徑
第三步:文件修改
ethernetif.c文件是無操作系統時網絡接口函數,該文件在移植是只需修改相關頭文件名,函數實現部分無需修改。該文件主要有三個部分函數,一個是low_level_init,用於初始化MAC相關工作環境、初始化DMA描述符鏈表,並使能MAC和DMA;一個是low_level_output,它是最底層發送一幀數據函數;最后一個是low_level_input,它是最底層接收一幀數據函數。
stm32f429_eth.c和stm32f429_eth.h兩個文件用於ETH驅動函數實現,它是通過直接操作寄存器方式實現,這兩個文件我們無需修改。stm32f429_eth_conf.h文件包含了一些功能選項的宏定義,我們對部分內容進行了修改。
代碼清單 392 stm32f429_eth_conf.h文件宏定義
1 #ifdef USE_Delay
2 #include "Bsp/systick/bsp_SysTick.h"
3 #define _eth_delay_ Delay_10ms
4 #else
5 #define _eth_delay_ ETH_Delay
6 #endif
7
8 #ifdef USE_Delay
9 /* LAN8742A Reset delay */
10 #define LAN8742A_RESET_DELAY ((uint32_t)0x00000005)
11 #else
12 /* LAN8742A Reset delay */
13 #define LAN8742A_RESET_DELAY ((uint32_t)0x00FFFFFF)
14 #endif
15
16 /* The LAN8742A PHY status register */
17 /* PHY status register Offset */
18 #define PHY_SR ((uint16_t)0x001F)
19 /* PHY Speed mask 1:10Mb/s 0:100Mb/s*/
20 #define PHY_SPEED_STATUS ((uint16_t)0x0004)
21 /* PHY Duplex mask 1:Full duplex 0:Half duplex*/
22 #define PHY_DUPLEX_STATUS ((uint16_t)0x0010)
通過宏定義USE_Delay可選是否使用自定義的延時函數,Delay_10ms函數是通過系統滴答定時器實現的延時函數,ETH_Delay函數是ETH驅動自帶的簡單循環延時函數,延時函數實現方法不同,對形參要求不同。因為ST官方例程是基於DP83848型號的PHY,而開發板的PHY型號是LAN8720A。LAN8720A復位時需要一段延時時間,這里需要定義延時時間長度,大約50ms。驅動代碼中需要獲取PHY的速度和工作模式,LAN8720A的R31是特殊控制/狀態寄存器,包括指示以太網速度和工作模式的狀態位。
stm32f42x_phy.c和stm32f42x_phy.h兩個文件是ETH外設相關的底層配置,包括RMII接口GPIO初始化、SMI接口GPIO初始化、MAC控制器工作環境配置,還有一些PHY的狀態獲取和控制修改函數。ST官方例程文件包含了中斷引腳的相關配置,主要用於指示接收到以太網幀,我們這里不需要使用,采用無限輪詢方法檢測接收狀態。stm32f42x_phy.h文件存放相關宏定義,包含RMII和SMI引腳信息等宏定義,其中要特別說明的有一個宏,定義了PHY地址:ETHERNET_PHY_ADDRESS,這里根據硬件設計設置為0x00,這在SMI通信是非常重要的。
代碼清單 393 ETH_GPIO_Config函數
1 void ETH_GPIO_Config(void)
2 {
3 GPIO_InitTypeDef GPIO_InitStructure;
4
5 /* Enable GPIOs clocks */
6 RCC_AHB1PeriphClockCmd(ETH_MDIO_GPIO_CLK | ETH_MDC_GPIO_CLK |
7 ETH_RMII_REF_CLK_GPIO_CLK|ETH_RMII_CRS_DV_GPIO_CLK|
8 ETH_RMII_RXD0_GPIO_CLK | ETH_RMII_RXD1_GPIO_CLK |
9 ETH_RMII_TX_EN_GPIO_CLK | ETH_RMII_TXD0_GPIO_CLK |
10 ETH_RMII_TXD1_GPIO_CLK | ETH_NRST_GPIO_CLK, ENABLE);
11
12 /* Enable SYSCFG clock */
13 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
14
15 /* MII/RMII Media interface selection ------------------------------*/
16 #ifdef MII_MODE /* Mode MII with STM324xx-EVAL */
17 #ifdef PHY_CLOCK_MCO
18 /* Output HSE clock (25MHz) on MCO pin (PA8) to clock the PHY */
19 RCC_MCO1Config(RCC_MCO1Source_HSE, RCC_MCO1Div_1);
20 #endif /* PHY_CLOCK_MCO */
21
22 SYSCFG_ETH_MediaInterfaceConfig(SYSCFG_ETH_MediaInterface_MII);
23 #elif defined RMII_MODE /* Mode RMII with STM324xx-EVAL */
24
25 SYSCFG_ETH_MediaInterfaceConfig(SYSCFG_ETH_MediaInterface_RMII);
26 #endif
27
28 /* Ethernet pins configuration *************************************/
29 /*
30 ETH_MDIO -------------------------> PA2
31 ETH_MDC --------------------------> PC1
32 ETH_MII_RX_CLK/ETH_RMII_REF_CLK---> PA1
33 ETH_MII_RX_DV/ETH_RMII_CRS_DV ----> PA7
34 ETH_MII_RXD0/ETH_RMII_RXD0 -------> PC4
35 ETH_MII_RXD1/ETH_RMII_RXD1 -------> PC5
36 ETH_MII_TX_EN/ETH_RMII_TX_EN -----> PB11
37 ETH_MII_TXD0/ETH_RMII_TXD0 -------> PG13
38 ETH_MII_TXD1/ETH_RMII_TXD1 -------> PG14
39 ETH_NRST -------------------------> PI1
40 */
41 GPIO_InitStructure.GPIO_Pin = ETH_NRST_PIN;
42 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
43 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
44 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
45 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;
46 GPIO_Init(ETH_NRST_PORT, &GPIO_InitStructure);
47
48 ETH_NRST_PIN_LOW();
49 _eth_delay_(LAN8742A_RESET_DELAY);
50 ETH_NRST_PIN_HIGH();
51 _eth_delay_(LAN8742A_RESET_DELAY);
52
53 /* Configure ETH_MDIO */
54 GPIO_InitStructure.GPIO_Pin = ETH_MDIO_PIN;
55 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
56 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
57 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
58 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
59 GPIO_Init(ETH_MDIO_PORT, &GPIO_InitStructure);
60 GPIO_PinAFConfig(ETH_MDIO_PORT, ETH_MDIO_SOURCE, ETH_MDIO_AF);
61
62 /* Configure ETH_MDC */
63 GPIO_InitStructure.GPIO_Pin = ETH_MDC_PIN;
64 GPIO_Init(ETH_MDC_PORT, &GPIO_InitStructure);
65 GPIO_PinAFConfig(ETH_MDC_PORT, ETH_MDC_SOURCE, ETH_MDC_AF);
66
67 /**************************************/
68 /** 省略部分引腳初始化 ***/
69 /**************************************/
70
71 /* Configure ETH_RMII_TXD1 */
72 GPIO_InitStructure.GPIO_Pin = ETH_RMII_TXD1_PIN;
73 GPIO_Init(ETH_RMII_TXD1_PORT, &GPIO_InitStructure);
74 GPIO_PinAFConfig(ETH_RMII_TXD1_PORT, ETH_RMII_TXD1_SOURCE,
75 ETH_RMII_TXD1_AF);
76 }
STM32f42x控制器支持MII和RMII接口,通過程序控制使用RMII接口,同時需要使能SYSYCFG時鍾,函數后部分就是接口GPIO初始化實現,這里我們還連接了LAN8720A的復位引腳,通過拉低一段時間讓芯片硬件復位。
代碼清單 394 ETH_MACDMA_Config函數
1 static void ETH_MACDMA_Config(void)
2 {
3 /* Enable ETHERNET clock */
4 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC |
5 RCC_AHB1Periph_ETH_MAC_Tx|RCC_AHB1Periph_ETH_MAC_Rx,ENABLE);
6
7 /* Reset ETHERNET on AHB Bus */
8 ETH_DeInit();
9 /* Software reset */
10 ETH_SoftwareReset();
11 /* Wait for software reset */
12 while (ETH_GetSoftwareResetStatus() == SET);
13
14 /* ETHERNET Configuration ------------------------------*/
15 /* 缺省配置ETH_InitStructure */
16 ETH_StructInit(Ð_InitStructure);
17
18 /* Fill ETH_InitStructure parametrs */
19 /*-------------------- MAC ----------------------------*/
20 /* 開啟網絡自適應功能,速度和工作模式無需配置 */
21 ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Enable;
22 // ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Disable;
23 // ETH_InitStructure.ETH_Speed = ETH_Speed_10M;
24 // ETH_InitStructure.ETH_Mode = ETH_Mode_FullDuplex;
25 /* 關閉反饋 */
26 ETH_InitStructure.ETH_LoopbackMode = ETH_LoopbackMode_Disable;
27 /* 關閉重傳功能 */
28 ETH_InitStructure.ETH_RetryTransmission=ETH_RetryTransmission_Disable;
29 /* 關閉自動去除PDA/CRC功能 */
30 ETH_InitStructure.ETH_AutomaticPadCRCStrip =
31 ETH_AutomaticPadCRCStrip_Disable;
32 /* 關閉接收所有的幀 */
33 ETH_InitStructure.ETH_ReceiveAll = ETH_ReceiveAll_Disable;
34 /* 允許接收所有廣播幀 */
35 ETH_InitStructure.ETH_BroadcastFramesReception =
36 ETH_BroadcastFramesReception_Enable;
37 /* 關閉混合模式的地址過濾 */
38 ETH_InitStructure.ETH_PromiscuousMode = ETH_PromiscuousMode_Disable;
39 /* 對於組播地址使用完美地址過濾 */
40 ETH_InitStructure.ETH_MulticastFramesFilter =
41 ETH_MulticastFramesFilter_Perfect;
42 /* 對單播地址使用完美地址過濾 */
43 ETH_InitStructure.ETH_UnicastFramesFilter =
44 ETH_UnicastFramesFilter_Perfect;
45 #ifdef CHECKSUM_BY_HARDWARE
46 /* 開啟ipv4和TCP/UDP/ICMP的幀校驗和卸載 */
47 ETH_InitStructure.ETH_ChecksumOffload = ETH_ChecksumOffload_Enable;
48 #endif
49
50 /*------------------------ DMA -------------------------------*/
51 /*當我們使用幀校驗和卸載功能的時候,一定要使能存儲轉發模式,存儲
52 轉發模式中要保證整個幀存儲在FIFO中, 這樣MAC能插入/識別出幀校驗
53 值,當真校驗正確的時候DMA就可以處理幀,否則就丟棄掉該幀*/
54 /* 開啟丟棄TCP/IP錯誤幀 */
55 ETH_InitStructure.ETH_DropTCPIPChecksumErrorFrame =
56 ETH_DropTCPIPChecksumErrorFrame_Enable;
57 /* 開啟接收數據的存儲轉發模式 */
58 ETH_InitStructure.ETH_ReceiveStoreForward =
59 ETH_ReceiveStoreForward_Enable;
60 /* 開啟發送數據的存儲轉發模式 */
61 ETH_InitStructure.ETH_TransmitStoreForward =
62 ETH_TransmitStoreForward_Enable;
63
64 /* 禁止轉發錯誤幀 */
65 ETH_InitStructure.ETH_ForwardErrorFrames =
66 ETH_ForwardErrorFrames_Disable;
67 /* 不轉發過小的好幀 */
68 ETH_InitStructure.ETH_ForwardUndersizedGoodFrames =
69 ETH_ForwardUndersizedGoodFrames_Disable;
70 /* 打開處理第二幀功能 */
71 ETH_InitStructure.ETH_SecondFrameOperate =
72 ETH_SecondFrameOperate_Enable;
73 /* 開啟DMA傳輸的地址對齊功能 */
74 ETH_InitStructure.ETH_AddressAlignedBeats =
75 ETH_AddressAlignedBeats_Enable;
76 /* 開啟固定突發功能 */
77 ETH_InitStructure.ETH_FixedBurst = ETH_FixedBurst_Enable;
78 /* DMA發送的最大突發長度為32個節拍 */
79 ETH_InitStructure.ETH_RxDMABurstLength = ETH_RxDMABurstLength_32Beat;
80 /*DMA接收的最大突發長度為32個節拍 */
81 ETH_InitStructure.ETH_TxDMABurstLength = ETH_TxDMABurstLength_32Beat;
82 ETH_InitStructure.ETH_DMAArbitration =
83 ETH_DMAArbitration_RoundRobin_RxTx_2_1;
84
85 /* 配置ETH */
86 EthStatus = ETH_Init(Ð_InitStructure, ETHERNET_PHY_ADDRESS);
87 }
首先是使能ETH時鍾,復位ETH配置。ETH_StructInit函數用於初始化ETH_InitTypeDef結構體變量,會給每個成員賦予缺省值。接下來就是根據需要配置ETH_InitTypeDef結構體變量,關於結構體各個成員意義已在"ETH初始化結構體詳解"作了分析。最后調用ETH_Init函數完成配置,ETH_Init函數有兩個形參,一個是ETH_InitTypeDef結構體變量指針,第二個是PHY地址,函數還有一個返回值,用於指示初始化配置是否成功。
代碼清單 395 ETH_BSP_Config函數
1 #define GET_PHY_LINK_STATUS()
2 (ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS,PHY_BSR)&0x00000004)
3
4 void ETH_BSP_Config(void)
5 {
6 /* Configure the GPIO ports for ethernet pins */
7 ETH_GPIO_Config();
8
9 /* Configure the Ethernet MAC/DMA */
10 ETH_MACDMA_Config();
11
12 /* Get Ethernet link status*/
13 if (GET_PHY_LINK_STATUS()) {
14 EthStatus |= ETH_LINK_FLAG;
15 }
16 }
GET_PHY_LINK_STATUS()是定義獲取PHY鏈路狀態的宏,如果PHY連接正常那么整個宏定義為1,如果不正常則為0,它是通過ETH_ReadPHYRegister函數讀取PHY的基本狀態寄存器(PHY_BSR)並檢測其Link Status位得到的。
ETH_BSP_Config函數分別調用ETH_GPIO_Config和ETH_MACDMA_Config函數完成ETH初始化配置,最后調用GET_PHY_LINK_STATUS()來判斷PHY狀態,並保存在EthStatus變量中。ETH_BSP_Config函數一般在main函數中優先LwIP_Init函數調用。
代碼清單 396 ETH_CheckLinkStatus函數
1 void ETH_CheckLinkStatus(uint16_t PHYAddress)
2 {
3 static uint8_t status = 0;
4 uint32_t t = GET_PHY_LINK_STATUS();
5
6 /* If we have link and previous check was not yet */
7 if (t && !status) {
8 /* Set link up */
9 netif_set_link_up(&gnetif);
10
11 status = 1;
12 }
13 /* If we don't have link and it was on previous check */
14 if (!t && status) {
15 EthLinkStatus = 1;
16 /* Set link down */
17 netif_set_link_down(&gnetif);
18
19 status = 0;
20 }
21 }
ETH_CheckLinkStatus函數用於獲取PHY狀態,實際上也是通過宏定義GET_PHY_LINK_STATUS()獲取得到的,函數還根據PHY狀態通知LwIP當前鏈路狀態,gnetif是一個netif結構體類型變量,LwIP定義了netif結構體類型,用於指示某一網卡相關信息,LwIP是支持多個網卡設備,使用時需要為每個網卡設備定義一個netif類型變量。無操作系統時ETH_CheckLinkStatus函數被無限循環調用。
代碼清單 397 ETH_link_callback函數
1 void ETH_link_callback(struct netif *netif)
2 {
3 __IO uint32_t timeout = 0;
4 uint32_t tmpreg;
5 uint16_t RegValue;
6 struct ip_addr ipaddr;
7 struct ip_addr netmask;
8 struct ip_addr gw;
9
10 if (netif_is_link_up(netif)) {
11 /* Restart the auto-negotiation */
12 if (ETH_InitStructure.ETH_AutoNegotiation !=
13 ETH_AutoNegotiation_Disable) {
14 /* Reset Timeout counter */
15 timeout = 0;
16 /* Enable auto-negotiation */
17 ETH_WritePHYRegister(ETHERNET_PHY_ADDRESS, PHY_BCR,
18 PHY_AutoNegotiation);
19 /* Wait until the auto-negotiation will be completed */
20 do {
21 timeout++;
22 } while (!(ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS, PHY_BSR)
23 &PHY_AutoNego_Complete)&&(timeout<(uint32_t)PHY_READ_TO));
24
25 /* Reset Timeout counter */
26 timeout = 0;
27 /* Read the result of the auto-negotiation */
28 RegValue = ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS, PHY_SR);
29
30 if ((RegValue & PHY_DUPLEX_STATUS) != (uint16_t)RESET) {
31 ETH_InitStructure.ETH_Mode = ETH_Mode_FullDuplex;
32 } else {
33 ETH_InitStructure.ETH_Mode = ETH_Mode_HalfDuplex;
34 }
35 if (RegValue & PHY_SPEED_STATUS) {
36 /* Set Ethernet speed to 10M following the auto-negotiation */
37 ETH_InitStructure.ETH_Speed = ETH_Speed_10M;
38 } else {
39 /* Set Ethernet speed to 100M following the auto-negotiation */
40 ETH_InitStructure.ETH_Speed = ETH_Speed_100M;
41 }
42
43 /*------------ ETHERNET MACCR Re-Configuration -------------*/
44 /* Get the ETHERNET MACCR value */
45 tmpreg = ETH->MACCR;
46
47 /* Set the FES bit according to ETH_Speed value */
48 /* Set the DM bit according to ETH_Mode value */
49 tmpreg |= (uint32_t)(ETH_InitStructure.ETH_Speed |
50 ETH_InitStructure.ETH_Mode);
51
52 /* Write to ETHERNET MACCR */
53 ETH->MACCR = (uint32_t)tmpreg;
54
55 _eth_delay_(ETH_REG_WRITE_DELAY);
56 tmpreg = ETH->MACCR;
57 ETH->MACCR = tmpreg;
58 }
59
60 /* Restart MAC interface */
61 ETH_Start();
62
63 #ifdef USE_DHCP
64 ipaddr.addr = 0;
65 netmask.addr = 0;
66 gw.addr = 0;
67 DHCP_state = DHCP_START;
68 #else
69 IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
70 IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1 ,
71 NETMASK_ADDR2, NETMASK_ADDR3);
72 IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
73 #endif /* USE_DHCP */
74
75 netif_set_addr(&gnetif, &ipaddr , &netmask, &gw);
76
77 /* When the netif is fully configured this function must be called.*/
78 netif_set_up(&gnetif);
79
80 EthLinkStatus = 0;
81 } else {
82 ETH_Stop();
83 #ifdef USE_DHCP
84 DHCP_state = DHCP_LINK_DOWN;
85 dhcp_stop(netif);
86 #endif /* USE_DHCP */
87
88 /* When the netif link is down this function must be called.*/
89 netif_set_down(&gnetif);
90 }
91 }
ETH_link_callback函數被LwIP調用,當鏈路狀態發送改變時該函數就被調用,用於狀態改變后處理相關事務。首先調用netif_is_link_up函數判斷新狀態是否是鏈路啟動狀態,如果是啟動狀態就進入if語句,接下來會判斷ETH是否被設置為自適應模式,如果不是自適應模式需要使用ETH_WritePHYRegister函數使能PHY工作為自適應模式,然后ETH_ReadPHYRegister函數讀取PHY相關寄存器,獲取PHY當前支持的以太網速度和工作模式,並保存到ETH_InitStructure結構體變量中。ETH_Start函數用於使能ETH外設,之后就是配置ETH的IP地址、子網掩碼、網關,如果是定義了DHCP (動態主機配置協議)功能則啟動DHCP。最后就是調用netif_set_up函數在LwIP層次配置啟動ETH功能。
如果檢測到是鏈路關閉狀態,調用ETH_Stop函數關閉ETH,如果定義了DHCP功能則需關閉DHCP,最后調用netif_set_down函數在LwIP層次關閉ETH功能。
以上對文件修改部分更多涉及到ETH硬件底層驅動,一些是PHY芯片驅動函數、一些是ETH外設與LwIP連接函數。接下來要講解的文件代碼更多是與LwIP應用相關的。
netconf.c和netconf.h文件用於存放LwIP配置相關代碼。netcon.h定義了相關宏。
代碼清單 398 LwIP配置相關宏定義
1 /* DHCP狀態 */
2 #define DHCP_START 1
3 #define DHCP_WAIT_ADDRESS 2
4 #define DHCP_ADDRESS_ASSIGNED 3
5 #define DHCP_TIMEOUT 4
6 #define DHCP_LINK_DOWN 5
7
8 //#define USE_DHCP /* enable DHCP, if disabled static address is used */
9
10 /* 調試信息輸出 */
11 #define SERIAL_DEBUG
12 /* 遠端IP地址和端口 */
13 #define DEST_IP_ADDR0 192
14 #define DEST_IP_ADDR1 168
15 #define DEST_IP_ADDR2 1
16 #define DEST_IP_ADDR3 105
17 #define DEST_PORT 6000
18
19 /* MAC地址:網卡地址 */
20 #define MAC_ADDR0 2
21 #define MAC_ADDR1 0
22 #define MAC_ADDR2 0
23 #define MAC_ADDR3 0
24 #define MAC_ADDR4 0
25 #define MAC_ADDR5 0
26
27 /*靜態IP地址 */
28 #define IP_ADDR0 192
29 #define IP_ADDR1 168
30 #define IP_ADDR2 1
31 #define IP_ADDR3 122
32
33 /* 子網掩碼 */
34 #define NETMASK_ADDR0 255
35 #define NETMASK_ADDR1 255
36 #define NETMASK_ADDR2 255
37 #define NETMASK_ADDR3 0
38
39 /* 網關 */
40 #define GW_ADDR0 192
41 #define GW_ADDR1 168
42 #define GW_ADDR2 1
43 #define GW_ADDR3 1
44
45 /* 檢測PHY鏈路狀態的實際間隔(單位:ms) */
46 #ifndef LINK_TIMER_INTERVAL
47 #define LINK_TIMER_INTERVAL 1000
48 #endif
49
50 /* MII and RMII mode selection ***********/
51 #define RMII_MODE
52 //#define MII_MODE
53
54 /* 在MII模式時,使能MCO引腳輸出25MHz脈沖 */
55 #ifdef MII_MODE
56 #define PHY_CLOCK_MCO
57 #endif
USE_DHCP宏用於定義是否使用DHCP功能,如果不定義該宏,直接使用靜態的IP地址,如果定義該宏,則使用DHCP功能,獲取動態的IP地址,這里有個需要注意的地方,電腦是沒辦法提供DHCP服務功能的,路由器才有DHCP服務功能,使用當開發板直連電腦時不能定義該宏。
SERIAL_DEBUG宏是定義是否使能串口定義相關調試信息功能,一般選擇使能,所以在main函數中需要添加串口初始化函數。
接下來,定義了遠端IP和端口、MAC地址、靜態IP地址、子網掩碼、網關相關宏,可以根據實際情況修改。
LAN8720A僅支持RMII接口,根據硬件設計這里定義使用RMII_MODE。
代碼清單 399 LwIP_Init函數
1 void LwIP_Init(void)
2 {
3 struct ip_addr ipaddr;
4 struct ip_addr netmask;
5 struct ip_addr gw;
6
7 /* Initializes the dynamic memory heap defined by MEM_SIZE.*/
8 mem_init();
9 /* Initializes the memory pools defined by MEMP_NUM_x.*/
10 memp_init();
11
12 #ifdef USE_DHCP
13 ipaddr.addr = 0;
14 netmask.addr = 0;
15 gw.addr = 0;
16 #else
17 IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
18 IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1 ,
19 NETMASK_ADDR2,NETMASK_ADDR3);
20 IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
21 #endif
22 /* 添加以太網設備 */
23 netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL,
24 ðernetif_init, ðernet_input);
25
26 /* 設置以太網設備為默認網卡 */
27 netif_set_default(&gnetif);
28
29 if (EthStatus == (ETH_INIT_FLAG | ETH_LINK_FLAG)) {
30 gnetif.flags |= NETIF_FLAG_LINK_UP;
31 /* 配置完成網卡后啟動網卡*/
32 netif_set_up(&gnetif);
33 #ifdef USE_DHCP
34 DHCP_state = DHCP_START;
35 #else
36 #ifdef SERIAL_DEBUG
37 printf("\n Static IP address \n");
38 printf("IP: %d.%d.%d.%d\n",IP_ADDR0,IP_ADDR1,IP_ADDR2,IP_ADDR3);
39 printf("NETMASK: %d.%d.%d.%d\n",NETMASK_ADDR0,NETMASK_ADDR1,
40 NETMASK_ADDR2,NETMASK_ADDR3);
41 printf("Gateway:%d.%d.%d.%d\n",GW_ADDR0,GW_ADDR1,GW_ADDR2,GW_ADDR3);
42 #endif /* SERIAL_DEBUG */
43 #endif /* USE_DHCP */
44 } else {
45 /* 當網絡鏈路關閉時關閉網卡設備 */
46 netif_set_down(&gnetif);
47 #ifdef USE_DHCP
48 DHCP_state = DHCP_LINK_DOWN;
49 #endif /* USE_DHCP */
50 #ifdef SERIAL_DEBUG
51 printf("\n Network Cable is \n");
52 printf(" not connected \n");
53 #endif /* SERIAL_DEBUG */
54 }
55 /* 設置鏈路回調函數,用於獲取鏈路狀態 */
56 netif_set_link_callback(&gnetif, ETH_link_callback);
57 }
LwIP_Init函數用於初始化LwIP協議棧,一般在main函數中調用。首先是內存相關初始化,mem_init函數是動態內存堆初始化,memp_init函數是存儲池初始化,LwIP是實現內存的高效利用,內部需要不同形式的內存管理模式。
接下來為ipaddr、netmask和gw結構體變量賦值,設置本地IP地址、子網掩碼和網關,如果使用DHCP功能直接賦值為0即可。netif_add是以太網設備添加函數,即向LwIP協議棧申請添加一個網卡設備,函數有7個形參,第一個為netif結構體類型變量指針,這里賦值為gnetif地址,該網卡設備屬性就存放在gnetif變量中;第二個為ip_addr結構體類型變量指針,用於設置網卡IP地址;第三個ip_addr結構體類型變量指針,用於設置子網掩碼;第四個為ip_addr結構體類型變量指針,用於設置網關;第五個為void變量,用戶自定義字段,一般不用直接賦值NULL;第六個為netif_init_fn類型函數指針,用於指向網卡設備初始化函數,這里賦值為指向ethernetif_init函數,該函數在ethernetif.c文件定義,初始化LwIP與ETH外設連接函數;最后一個參數為netif_input_fn類型函數指針,用於指向以太網幀接收函數,這里賦值為指向ethernet_input函數,該函數定義在etharp.c文件中。
netif_set_default函數用於設置指定網卡為默認的網絡通信設備。
在無硬件連接錯誤時,調用ETH_BSP_Config(優先LwIP_Init函數被調用)時會將EthStatus變量對應的ETH_LINK_FLAG位使能,所以在LwIP_INIT函數中會執行if判斷語句代碼,置位網卡設備標志位以及運行netif_set_up函數啟動網卡設備。否則執行netif_set_down函數停止網卡設備。
最后,根據需要調用netif_set_link_callback函數實在當鏈路狀態發生改變時需要調用的回調函數配置。
代碼清單 3910 LwIP_Pkt_Handle函數
1 void LwIP_Pkt_Handle(void)
2 {
3 /* 從以太網存儲器讀取一個以太網幀並將其發送給LwIP */
4 ethernetif_input(&gnetif);
5 }
LwIP_Pkt_Handle函數用於從以太網存儲器讀取一個以太網幀並將其發送給LwIP,它在接收到以太網幀時被調用,它是直接調用ethernetif_input函數實現的,該函數定義在ethernetif.c文件中。
代碼清單 3911 LwIP_Periodic_Handle函數
1 void LwIP_Periodic_Handle(__IO uint32_t localtime)
2 {
3 #if LWIP_TCP
4 /* TCP periodic process every 250 ms */
5 if (localtime - TCPTimer >= TCP_TMR_INTERVAL) {
6 TCPTimer = localtime;
7 tcp_tmr();
8 }
9 #endif
10
11 /* ARP periodic process every 5s */
12 if ((localtime - ARPTimer) >= ARP_TMR_INTERVAL) {
13 ARPTimer = localtime;
14 etharp_tmr();
15 }
16
17 /* Check link status periodically */
18 if ((localtime - LinkTimer) >= LINK_TIMER_INTERVAL) {
19 ETH_CheckLinkStatus(ETHERNET_PHY_ADDRESS);
20 LinkTimer=localtime;
21 }
22
23 #ifdef USE_DHCP
24 /* Fine DHCP periodic process every 500ms */
25 if (localtime - DHCPfineTimer >= DHCP_FINE_TIMER_MSECS) {
26 DHCPfineTimer = localtime;
27 dhcp_fine_tmr();
28 if ((DHCP_state != DHCP_ADDRESS_ASSIGNED) &&
29 (DHCP_state != DHCP_TIMEOUT) &&
30 (DHCP_state != DHCP_LINK_DOWN)) {
31 #ifdef SERIAL_DEBUG
32 LED1_TOGGLE;
33 printf("\nFine DHCP periodic process every 500ms\n");
34 #endif /* SERIAL_DEBUG */
35
36 /* process DHCP state machine */
37 LwIP_DHCP_Process_Handle();
38 }
39 }
40
41 /* DHCP Coarse periodic process every 60s */
42 if (localtime - DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS) {
43 DHCPcoarseTimer = localtime;
44 dhcp_coarse_tmr();
45 }
46
47 #endif
48 }
LwIP_Periodic_Handle函數是一個必須被無限循環調用的LwIP支持函數,一般在main函數的無限循環中調用,主要功能是為LwIP各個模塊提供時間並查詢鏈路狀態,該函數有一個形參,用於指示當前時間,單位為ms。
對於TCP功能,每250ms執行一次tcp_tmr函數;對於ARP(地址解析協議),每5s執行一次etharp_tmr函數;對於鏈路狀態檢測,每1s執行一次ETH_CheckLinkStatus函數;對於DHCP功能,每500ms執行一次dhcp_fine_tmr函數,如果DHCP處於DHCP_START或DHCP_WAIT_ADDRESS狀態就執行LwIP_DHCP_Process_Handle函數,對於DHCP功能,還有每60s執行一次dhcp_coarse_tmr函數。
代碼清單 3912 LwIP_DHCP_Process_Handle函數
1 void LwIP_DHCP_Process_Handle(void)
2 {
3 struct ip_addr ipaddr;
4 struct ip_addr netmask;
5 struct ip_addr gw;
6
7 switch (DHCP_state) {
8 case DHCP_START: {
9 DHCP_state = DHCP_WAIT_ADDRESS;
10 dhcp_start(&gnetif);
11 /* IP address should be set to 0
12 every time we want to assign a new DHCP address */
13 IPaddress = 0;
14 #ifdef SERIAL_DEBUG
15 printf("\n Looking for \n");
16 printf(" DHCP server \n");
17 printf(" please wait... \n");
18 #endif /* SERIAL_DEBUG */
19 }
20 break;
21
22 case DHCP_WAIT_ADDRESS: {
23 /* Read the new IP address */
24 IPaddress = gnetif.ip_addr.addr;
25
26 if (IPaddress!=0) {
27 DHCP_state = DHCP_ADDRESS_ASSIGNED;
28 /* Stop DHCP */
29 dhcp_stop(&gnetif);
30 #ifdef SERIAL_DEBUG
31 printf("\n IP address assigned \n");
32 printf(" by a DHCP server \n");
33 printf("IP: %d.%d.%d.%d\n",(uint8_t)(IPaddress),
34 (uint8_t)(IPaddress >> 8),(uint8_t)(IPaddress >> 16),
35 (uint8_t)(IPaddress >> 24));
36 printf("NETMASK: %d.%d.%d.%d\n",NETMASK_ADDR0,NETMASK_ADDR1,
37 NETMASK_ADDR2,NETMASK_ADDR3);
38 printf("Gateway: %d.%d.%d.%d\n",GW_ADDR0,GW_ADDR1,
39 GW_ADDR2,GW_ADDR3);
40 LED1_ON;
41 #endif /* SERIAL_DEBUG */
42 } else {
43 /* DHCP timeout */
44 if (gnetif.dhcp->tries > MAX_DHCP_TRIES) {
45 DHCP_state = DHCP_TIMEOUT;
46 /* Stop DHCP */
47 dhcp_stop(&gnetif);
48 /* Static address used */
49 IP4_ADDR(&ipaddr, IP_ADDR0 ,IP_ADDR1 , IP_ADDR2 , IP_ADDR3 );
50 IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1,
51 NETMASK_ADDR2, NETMASK_ADDR3);
52 IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
53 netif_set_addr(&gnetif, &ipaddr , &netmask, &gw);
54 #ifdef SERIAL_DEBUG
55 printf("\n DHCP timeout \n");
56 printf(" Static IP address \n");
57 printf("IP: %d.%d.%d.%d\n",IP_ADDR0,IP_ADDR1,
58 IP_ADDR2,IP_ADDR3);
59 printf("NETMASK: %d.%d.%d.%d\n",NETMASK_ADDR0,NETMASK_ADDR1,
60 NETMASK_ADDR2,NETMASK_ADDR3);
61 printf("Gateway: %d.%d.%d.%d\n",GW_ADDR0,GW_ADDR1,
62 GW_ADDR2,GW_ADDR3);
63 LED1_ON;
64 #endif /* SERIAL_DEBUG */
65 }
66 }
67 }
68 break;
69 default:
70 break;
71 }
72 }
LwIP_DHCP_Process_Handle函數用於執行DHCP功能,當DHCP狀態為DHCP_START時,執行dhcp_start函數啟動DHCP功能,LwIP會向DHCP服務器申請分配IP請求,並進入等待分配狀態。當DHCP狀態為DHCP_WAIT_ADDRESS時,先判斷IP地址是否為0,如果不為0說明已經有IP地址,DHCP功能已經完成可以停止它;如果IP地址總是為0,就需要判斷是否超過最大等待時間,並提示出錯。
lwipopts.h文件存放一些宏定義,用於剪切LwIP功能,比如有無操作系統、內存空間分配、存儲池分配、TCP功能、DHCP功能、UDP功能選擇等等。這里使用與ST官方例程相同配置即可。
LwIP為使用者提供了兩種應用程序接口(API函數)來實現TCP/IP協議棧,一種是低水平、基於回調函數的API,稱為RAW API,另外一種是高水平、連續的API,稱為sequential API,sequential API又有兩種函數結構,一種是Netconn,一種是Socket,它與在電腦端使用的BSD標准的Socket API結構和原理是非常相似的。
接下來內容我們使用RAW API實現一個簡單的TCP通信測試,ST官方有提供相關的例程,我們對其內容稍作調整。代碼內容存放在tcp_echoclient.c文件中。TCP在各個層次處理過程見圖 3919。
圖 3919 TCP處理過程
網絡接口層的netif->output和netif->input是在ethernetif.c文件中實現的,網絡層和傳輸層有LwIP協議棧實現,應用層代碼就是用戶使用LwIP函數實現網絡功能。
代碼清單 3913 tcp_echoclient_connect函數
1 void tcp_echoclient_connect(void)
2 {
3 struct ip_addr DestIPaddr;
4
5 /* create new tcp pcb */
6 echoclient_pcb = tcp_new();
7
8 if (echoclient_pcb != NULL) {
9 IP4_ADDR( &DestIPaddr, DEST_IP_ADDR0, DEST_IP_ADDR1,
10 DEST_IP_ADDR2, DEST_IP_ADDR3 );
11
12 /* connect to destination address/port */
13 tcp_connect(echoclient_pcb,&DestIPaddr,
14 DEST_PORT,tcp_echoclient_connected);
15 } else {
16 /* deallocate the pcb */
17 memp_free(MEMP_TCP_PCB, echoclient_pcb);
18 #ifdef SERIAL_DEBUG
19 printf("\n\r can not create tcp pcb");
20 #endif
21 }
22 }
tcp_echoclient_connect函數用於創建TCP從設備並啟動與TCP服務器連接。tcp_new函數創建一個新TCP協議控制塊,主要是必要的內存申請,返回一個未初始化的TCP協議控制塊指針。如果返回值不了0就可以使用tcp_connect函數連接到TCP服務器,tcp_connect函數用於TCP從設備連接至指定IP地址和端口的TCP服務器,它有四個形參,第一個為TCP協議控制塊指針,第二個為服務器IP地址,第三個為服務器端口,第四個為函數指針,當連接正常建立時或連接錯誤時函數被調用,這里賦值tcp_echoclient_connected函數名。如果tcp_new返回值為0說明創建TCP協議控制塊失敗,調用memp_free函數釋放相關內容。
代碼清單 3914 tcp_echoclient_disconnect函數
1 struct echoclient {
2 enum echoclient_states state; /* connection status */
3 struct tcp_pcb *pcb; /* pointer on the current tcp_pcb */
4 struct pbuf *p_tx; /* pointer on pbuf to be transmitted */
5 };
6
7 void tcp_echoclient_disconnect(void)
8 {
9 /* close connection */
10 tcp_echoclient_connection_close(echoclient_pcb,echoclient_es);
11 #ifdef SERIAL_DEBUG
12 printf("\n\r close TCP connection");
13 #endif
14 }
echoclient是自定義的一個結構體類型,包含了TCP從設備的狀態、TCP協議控制塊指針和發送數據指針。tcp_echoclient_disconnect函數用於斷開TCP連接,通過調用tcp_echoclient_connection_close函數實現,它有兩個形參,一個是TCP協議控制塊,一個是echoclient類型指針。
代碼清單 3915 tcp_echoclient_connected函數
1 static err_t tcp_echoclient_connected(void *arg, struct tcp_pcb *tpcb,
2 err_t err)
3 {
4 struct echoclient *es = NULL;
5
6 if (err == ERR_OK) {
7 /* allocate structure es to maintain tcp connection informations */
8 es = (struct echoclient *)mem_malloc(sizeof(struct echoclient));
9 echoclient_es=es;
10 if (es != NULL) {
11 es->state = ES_CONNECTED;
12 es->pcb = tpcb;
13 sprintf((char*)data, "sending tcp client message %d",
14 message_count);
15 /* allocate pbuf */
16 es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data),
17 PBUF_POOL);
18 if (es->p_tx) {
19 /* copy data to pbuf */
20 pbuf_take(es->p_tx, (char*)data, strlen((char*)data));
21 /* pass newly allocated es structure as argument to tpcb */
22 tcp_arg(tpcb, es);
23 /* initialize LwIP tcp_recv callback function */
24 tcp_recv(tpcb, tcp_echoclient_recv);
25 /* initialize LwIP tcp_sent callback function */
26 tcp_sent(tpcb, tcp_echoclient_sent);
27 /* initialize LwIP tcp_poll callback function */
28 tcp_poll(tpcb, tcp_echoclient_poll, 1);
29 /* send data */
30 tcp_echoclient_send(tpcb,es);
31 return ERR_OK;
32 }
33 } else {
34 /* close connection */
35 tcp_echoclient_connection_close(tpcb, es);
36 /* return memory allocation error */
37 return ERR_MEM;
38 }
39 } else {
40 /* close connection */
41 tcp_echoclient_connection_close(tpcb, es);
42 }
43 return err;
44 }
tcp_echoclient_connected函數作為tcp_connect函數設置的回調函數,在TCP建立連接時被調用,這里實現的功能是向TCP服務器發送一段數據。使用mem_malloc函數申請內存空間存放echoclient結構體類型數據,並賦值給es指針變量。如果內存申請失敗調用tcp_echoclient_connection_close函數關閉TCP連接;確保內存申請成功后為es成員賦值,p_tx成員是發送數據指針,這里使用pbuf_alloc函數向內存池申請存放發送數據的存儲空間,即數據發送緩沖區。確保發送數據存儲空間申請成功后使用pbuf_take函數將待發送數據data拷貝到數據發送存儲器。tcp_arg函數用於設置用戶自定義參數,使得該參數可在相關回調函數被重新使用。tcp_recv、tcp_sent和tcp_poll函數分別設置TCP協議控制塊對應的接收、發送和輪詢回調函數。最后調用tcp_echoclient_send函數發送數據。
代碼清單 3916 tcp_echoclient_recv函數
1 static err_t tcp_echoclient_recv(void *arg, struct tcp_pcb *tpcb,
2 struct pbuf *p, err_t err)
3 {
4 char *recdata=0;
5 struct echoclient *es;
6 err_t ret_err;
7
8 LWIP_ASSERT("arg != NULL",arg != NULL);
9 es = (struct echoclient *)arg;
10 /* if we receive an empty tcp frame from server => close connection */
11 if (p == NULL) {
12 /* remote host closed connection */
13 es->state = ES_CLOSING;
14 if (es->p_tx == NULL) {
15 /* we're done sending, close connection */
16 tcp_echoclient_connection_close(tpcb, es);
17 } else {
18 /* send remaining data*/
19 tcp_echoclient_send(tpcb, es);
20 }
21 ret_err = ERR_OK;
22 }
23 /* else : a non empty frame was received from echo server
24 but for some reason err != ERR_OK */
25 else if (err != ERR_OK) {
26 /* free received pbuf*/
27 pbuf_free(p);
28 ret_err = err;
29 } else if (es->state == ES_CONNECTED) {
30 /* increment message count */
31 message_count++;
32 /* Acknowledge data reception */
33 tcp_recved(tpcb, p->tot_len);
34 #ifdef SERIAL_DEBUG
35 recdata=(char *)malloc(p->len*sizeof(char));
36 if (recdata!=NULL) {
37 memcpy(recdata,p->payload,p->len);
38 printf("upd_rec<<%s",recdata);
39 }
40 free(recdata);
41 #endif
42 /* free received pbuf*/
43 pbuf_free(p);
44 ret_err = ERR_OK;
45 }
46 /* data received when connection already closed */
47 else {
48 /* Acknowledge data reception */
49 tcp_recved(tpcb, p->tot_len);
50
51 /* free pbuf and do nothing */
52 pbuf_free(p);
53 ret_err = ERR_OK;
54 }
55 return ret_err;
56 }
tcp_echoclient_recv函數是TCP接收回調函數,TCP從設備接收到數據時該函數就被運行一次,我們可以提取數據幀內容。函數先檢測是否為空幀,如果為空幀則關閉TCP連接,然后檢測是否發生傳輸錯誤,如果發送錯誤執行pbuf_free函數釋放內存。檢查無錯誤就可以調用tcp_recved函數接收數據,這樣就可以提取接收到信息。最后調用pbuf_free函數釋放相關內存。
代碼清單 3917 tcp_echoclient_send函數
1 static void tcp_echoclient_send(struct tcp_pcb *tpcb, struct echoclient * es)
2 {
3 struct pbuf *ptr;
4 err_t wr_err = ERR_OK;
5
6 while ((wr_err == ERR_OK) &&
7 (es->p_tx != NULL) &&
8 (es->p_tx->len <= tcp_sndbuf(tpcb))) {
9
10 /* get pointer on pbuf from es structure */
11 ptr = es->p_tx;
12
13 /* enqueue data for transmission */
14 wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);
15
16 if (wr_err == ERR_OK) {
17 /* continue with next pbuf in chain (if any) */
18 es->p_tx = ptr->next;
19
20 if (es->p_tx != NULL) {
21 /* increment reference count for es->p */
22 pbuf_ref(es->p_tx);
23 }
24
25 /* free pbuf: will free pbufs up to es->p
26 (because es->p has a reference count > 0) */
27 pbuf_free(ptr);
28 } else if (wr_err == ERR_MEM) {
29 /* we are low on memory, try later, defer to poll */
30 es->p_tx = ptr;
31 } else {
32 /* other problem ?? */
33 }
34 }
35 }
tcp_echoclient_send函數用於TCP數據發送,它有兩個形參,一個是TCP協議控制塊結構體指針,一個是echoclient結構體指針。在判斷待發送數據存在並不超過最大可用發送隊列數據數后,執行tcp_write函數將待發送數據寫入發送隊列,由協議內核決定發送時機。
代碼清單 3918 tcp_echoclient_poll函數
1 static err_t tcp_echoclient_poll(void *arg, struct tcp_pcb *tpcb)
2 {
3 err_t ret_err;
4 struct echoclient *es;
5
6 es = (struct echoclient*)arg;
7 if (es != NULL) {
8 if (es->p_tx != NULL) {
9 /* there is a remaining pbuf (chain) , try to send data */
10 tcp_echoclient_send(tpcb, es);
11 } else {
12 /* no remaining pbuf (chain) */
13 if (es->state == ES_CLOSING) {
14 /* close tcp connection */
15 tcp_echoclient_connection_close(tpcb, es);
16 }
17 }
18 ret_err = ERR_OK;
19 } else {
20 /* nothing to be done */
21 tcp_abort(tpcb);
22 ret_err = ERR_ABRT;
23 }
24 return ret_err;
25 }
tcp_echoclient_poll函數是由tcp_poll函數指定的回調函數,它每500ms執行一次,函數檢測是否有待發送數據,如果有就執行tcp_echoclient_send函數發送數據。
代碼清單 3919 tcp_echoclient_sent函數
1 static err_t tcp_echoclient_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
2 {
3 struct echoclient *es;
4
5 LWIP_UNUSED_ARG(len);
6
7 es = (struct echoclient *)arg;
8
9 if (es->p_tx != NULL) {
10 /* still got pbufs to send */
11 tcp_echoclient_send(tpcb, es);
12 }
13
14 return ERR_OK;
15 }
tcp_echoclient_sent函數是有tcp_sent函數指定的回調函數,當接收到遠端設備發送應答信號時被調用,它實際是通過調用tcp_echoclient_send函數發送數據實現的。
代碼清單 3920 tcp_echoclient_connection_close函數
1 static void tcp_echoclient_connection_close(struct tcp_pcb *tpcb,
2 struct echoclient * es )
3 {
4 /* remove callbacks */
5 tcp_recv(tpcb, NULL);
6 tcp_sent(tpcb, NULL);
7 tcp_poll(tpcb, NULL,0);
8
9 if (es != NULL) {
10 mem_free(es);
11 }
12 /* close tcp connection */
13 tcp_close(tpcb);
14 }
tcp_echoclient_connection_close函數用於關閉TCP連接,將相關的回調函數解除,釋放es變量內存,最后調用tcp_close函數關閉TCP連接,釋放TCP協議控制塊內存。
代碼清單 3921 定時器初始化配置及中斷服務函數
1 /* 初始化配置TIM3,使能每10ms發生一次中斷 */
2 static void TIM3_Config(uint16_t period,uint16_t prescaler)
3 {
4 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
5 NVIC_InitTypeDef NVIC_InitStructure;
6
7 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能TIM3時鍾
8
9 TIM_TimeBaseInitStructure.TIM_Prescaler=prescaler; //定時器分頻
10 TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
11 TIM_TimeBaseInitStructure.TIM_Period=period; //自動重裝載值
12 TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
13
14 TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
15
16 TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允許定時器3更新中斷
17 TIM_Cmd(TIM3,ENABLE); //使能定時器3
18
19 NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定時器3中斷
20 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01;
21 NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03;
22 NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
23 NVIC_Init(&NVIC_InitStructure);
24 }
25
26 /* TIM3中斷服務函數 */
27 void TIM3_IRQHandler(void)
28 {
29 if (TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) { //溢出中斷
30 LocalTime+=10;//10ms增量
31 }
32 TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中斷標志位
33 }
LwIP_Periodic_Handle函數執行LwIP需要周期性執行函數,該所以我們需要為該函數提高一個時間基准,這里使用TIM3產生這個基准,初始化配置TIM3每10ms中斷一次,在其中斷服務函數中遞增LocalTime變量值。
代碼清單 3922 main函數
1 int main(void)
2 {
3 uint8_t flag=0;
4 /* 初始化LED */
5 LED_GPIO_Config();
6
7 /* 初始化按鍵 */
8 Key_GPIO_Config();
9
10 /* 初始化調試串口,一般為串口1 */
11 Debug_USART_Config();
12
13 /* 初始化系統滴答定時器 */
14 SysTick_Init();
15
16 TIM3_Config(999,899);//10ms定時器
17 printf("以太網通信實現例程\n");
18
19 /* Configure ethernet (GPIOs, clocks, MAC, DMA) */
20 ETH_BSP_Config();
21 printf("PHY初始化結束\n");
22
23 /* Initilaize the LwIP stack */
24 LwIP_Init();
25
26 printf(" KEY1: 啟動TCP連接\n");
27 printf(" KEY2: 斷開TCP連接\n");
28
29 /* IP地址和端口可在netconf.h文件修改,或者使用DHCP服務自動獲取IP
30 (需要路由器支持)*/
31 printf("本地IP和端口: %d.%d.%d.%d\n",IP_ADDR0,IP_ADDR1,
32 IP_ADDR2,IP_ADDR3);
33 printf("遠端IP和端口: %d.%d.%d.%d:%d\n",DEST_IP_ADDR0, DEST_IP_ADDR1,
34 DEST_IP_ADDR2, DEST_IP_ADDR3,DEST_PORT);
35
36 while (1) {
37 if ((Key_Scan(KEY1_GPIO_PORT,KEY1_PIN)==KEY_ON) && (flag==0)) {
38 LED2_ON;
39 if (EthLinkStatus == 0) {
40 printf("connect to tcp server\n");
41 /*connect to tcp server */
42 tcp_echoclient_connect();
43 flag=1;
44 }
45 }
46 if ((Key_Scan(KEY2_GPIO_PORT,KEY2_PIN)==KEY_ON) && flag) {
47 LED2_OFF;
48 tcp_echoclient_disconnect();
49 flag=0;
50 }
51 /* check if any packet received */
52 if (ETH_CheckFrameReceived()) {
53 /* process received ethernet packet */
54 LwIP_Pkt_Handle();
55 }
56 /* handle periodic timers for LwIP */
57 LwIP_Periodic_Handle(LocalTime);
58 }
59 }
首先是初始化LED指示燈、按鍵、調試串口、系統滴答定時器,TIM3_Config函數配置10ms定時並啟動定時器,ETH_BSP_Config函數初始化ETH相關GPIO、配置MAC和DMA並獲取PHY狀態,LwIP_Init函數初始化LwIP協議棧。進入無限循環函數,不斷檢測按鍵狀態,如果KEY1被按下則調用tcp_echoclient_connect函數啟動TCP連接,如果KEY2被按下則調用tcp_echoclient_disconnect關閉TCP連接。ETH_CheckFrameReceived函數用於檢測是否接收到數據幀,如果接收到數據幀則調用LwIP_Pkt_Handle函數將數據幀從緩沖區傳入LwIP。LwIP_Periodic_Handle函執行必須被周期調用的函數。
下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板"USB TO UART"接口跟電腦,在電腦端打開串口調試助手並配置好相關參數;使用網線連接開發板網口跟路由器,這里要求電腦連接在同一個路由器上,之所以使用路由器是這樣連接方便,電腦端無需更多操作步驟,並且路由器可以提供DHCP服務器功能,而電腦不行的,最后在電腦端打開網絡調試助手軟件,並設置相關參數,見圖 3920,調試助手的設置與netconf.h文件中相關宏定義是對應的,不同電腦設置情況可能不同。把編譯好的程序下載到開發板。
圖 3920 調試助手設置界面
在系統硬件初始化時串口調試助手會打印相關提示信息,等待初始化完成后可打開電腦端CMD窗口,輸入ping命令測試開發板鏈路,圖 3921為鏈路正常情況,如果出現ping不同情況,檢查網線連接。
圖 3921 ping窗口
ping狀態正常后,可按下開發板KEY1按鍵,使能開發板連接電腦端的TCP服務器,之后就可以進行數據傳輸,需要接收傳輸時可以按下開發板KEY2按鍵,實際操作調試助手界面見圖 3922。
圖 3922 調試助手接發通信效果
39.9 基於uCOS-III移植LwIP實驗
上面的實驗是無操作系統移植LwIP,LwIP也確實是支持無操作系統移植運行,這對於芯片資源緊張、不合適運行操作系統環境還是有很大用處的。不過在很多應用中會采用操作系統上運行LwIP,這有利於提高整體性能。這個實驗我們主要講解移植操作步驟,過程中直接使用上個實驗LwIP底層驅動,除非有需要修改地方才指出,同時這里假設已有移植好的uCOS-III工程可參考使用,關於uCOS-III移植部分可參考我們相關文檔,這里主要介紹LwIP使用uCOS-III信號量、消息隊列、定時器函數等等函數接口。
這個實驗最終實現在uCOS-III操作系統基礎上移植LwIP,使能DHCP功能,在動態獲取IP之后即可ping通。運行uCOS-III操作系統之后一般會使用Netconn或Socket方法使用LwIP,關於這兩個的應用方法限於篇幅問題這里不做深入探究。
UCOS-III和LwIP都是屬於軟件編程層次,所以硬件設計部分並不需要做更改,直接使用上個實驗的硬件設計即可。
接下來開始介紹移植步驟,為簡化移植步驟,我們的思路是直接使用uCOS-III例程,在其基礎上移植LwIP部分。
第一步:文件拷貝
拷貝整個uCOS-III工程,修改文件夾名稱為"ETH—基於uCOS-III的LwIP移植",作為我們這個實驗工程基礎,我們在此基礎上添加功能。拷貝上個實驗工程中的lwip-1.4.1整個文件夾到USER文件夾(路徑:…\ETH—基於uCOS-III的LwIP移植\USER)中。
LwIP源碼部分,即src文件夾,內容是不用修改的,只有port文件夾內容需要修改。在stsw-stm32070文件夾找到FreeRTOS文件夾(路徑:… \Utilities\Third_Party\lwip-1.4.1\port \STM32F4x7\FreeRTOS),該文件夾內容是LwIP與FreeRTOS操作系統連接的相關接口函數,雖然我們選擇使用uCOS-III操作系統,當還是有很多可以借鑒的地方,移植過程我們采用修改這些文件方法實現而不是完全自己新建文件,把FreeRTOS整個文件夾拷貝到port文件夾(路徑:…\ETH—基於uCOS-III的LwIP移植\USER\lwip-1.4.1\port)內,並改名為UCOS305,此時port文件夾內有三個文件夾,分別為:arch、Standalone、UCOS305,其中Standalone在本實驗是不被使用的。
把上個實驗工程中的App文件夾拷貝到本實驗相同位置,其中tcp_echoclient.c和tcp_echoclient.h文件不是本實驗需要的,將其刪除。netconf.c、netconf.h和lwipopts.h三個文件是必需的,但因為如果在本實驗直接使用lwipopts.h文件需要修改較多地方,我們先將該文件刪除,然后在stsw-stm32070文件夾找到httpserver_socket文件夾。(路徑:… \Utilities\Third_Party\lwip-1.4.1\port \STM32F4x7\FreeRTOS\httpserver_socket),在該文件夾下inc文件夾中的lwipopts.h文件是更方便我們移植的文件,我們拷貝它到App文件夾中。
最后,把上個實驗工程中的ETH文件夾拷貝到本實驗相同位置,這個文件夾內容都是必需的,但我們不用進行修改。
第二步:為工程添加文件
與上個工程相比,LwIP部分文件只有port文件夾文件有所修改,其他使用與上個實驗相同文件結構皆可,最終工程文件結構參考圖 3923。
圖 3923 工程文件結構
添加完源文件后,還需要在工程選項中設置添加頭文件路徑,參考圖 3924。
圖 3924 添加頭文件路徑
第三步:文件修改
ETH文件夾內文件,stm32f429_eth.c、stm32f429_eth.h、stm32f429_phy.c和stm32f429_phy.h四個文件是ETH外部和PHY相關驅動,本實驗並無需修改硬件,所以這四個文件內容不用修改,stm32f429_eth_conf.h文件是與ETH外設相關硬件宏定義,因為本實驗使用操作系統,對延時函數定義與上個實驗工程有所不同,需要稍作修改。
代碼清單 3923 延時函數定義
1 #ifdef USE_Delay
2 #include "Bsp/bsp.h"
3 #define _eth_delay_ Delay_10ms
4 #else
5 #define _eth_delay_
6 #endif
這里使用在bsp.h文件中定義的Delay_10ms延時函數。
sys_arch.h和sys_arch.c兩個文件是LwIP與uCOS-III連接的實現代碼。sys_arch.h存放相關宏定義和類型定義。
代碼清單 3924 宏定義
1 #define LWIP_STK_SIZE 512
2 #define LWIP_TASK_MAX 8
3
4 #define LWIP_TSK_PRIO 3
5 #define LWIP_TASK_START_PRIO LWIP_TSK_PRIO
6 #define LWIP_TASK_END_PRIO LWIP_TSK_PRIO +LWIP_TASK_MAX
7
8 #define MAX_QUEUES 10 // 消息郵箱的數量
9 #define MAX_QUEUE_ENTRIES 20 // 每個郵箱的大小
10
11 #define SYS_MBOX_NULL (void *)0
12 #define SYS_SEM_NULL (void *)0
13
14 #define sys_arch_mbox_tryfetch(mbox,msg) sys_arch_mbox_fetch(mbox,msg,1)
宏LWIP_STK_SIZE定義LwIP任務棧空間大小,實際空間是4*LWIP_STK_SIZE個字節。宏LWIP_TASK_MAX定義預留給LwIP使用的最大任務數量。LWIP_TSK_PRIO、LWIP_TASK_START_PRIO和LWIP_TASK_END_PRIO三個宏指定LwIP任務的優先級范圍。宏MAX_QUEUES定義LwIP可以使用的最大郵箱數量,宏MAX_QUEUE_ENTRIES定義每個郵箱的大小。宏SYS_MBOX_NULL和SYS_SEM_NULL分別定義郵箱和信號量NULL對於的值。sys_arch_mbox_tryfetch函數是嘗試獲取郵箱內容,這里直接調用sys_arch_mbox_fetch函數實現。
代碼清單 3925 類型定義
1 typedef OS_SEM sys_sem_t; // type of semiphores
2 typedef OS_MUTEX sys_mutex_t; // type of mutex
3 typedef OS_Q sys_mbox_t; // type of mailboxes
4 typedef CPU_INT08U sys_thread_t; // type of id of the new thread
5
6 typedef CPU_INT08U sys_prot_t;
不同操作系統有不同名稱定義信號量、復合信號、郵箱、任務ID等等,這里使用uCOS-III操作系統需要使用對應的名稱。
實際上,除了需要定於與操作系統對應的名稱之外,還需要定於與編譯器相關的名稱,在sys_arch.h文件中有引用了cc.h頭文件,因為我們使用Windows操作系統的Keil開發工具,需要對cc.h文件進行必須修改。
代碼清單 3926 編譯器相關類型定於和宏定義
1 typedef u32_t mem_ptr_t;
2 //typedef int sys_prot_t;
3
4
5 //#define U16_F "hu"
6 //#define S16_F "d"
7 //#define X16_F "hx"
8 //#define U32_F "u"
9 //#define S32_F "d"
10 //#define X32_F "x"
11 //#define SZT_F "uz"
12
13 #define U16_F "4d"
14 #define S16_F "4d"
15 #define X16_F "4x"
16 #define U32_F "8ld"
17 #define S32_F "8ld"
18 #define X32_F "8lx"
sys_prot_t類型已在sys_arch.h文件中定於,在cc.h文件必須注釋掉不被使用。U16_F、S16_F、X16_F等等一系列名稱用於LwIP的調試函數,這一系列宏定於用於調試信息輸出格式化。
代碼清單 3927 調試信息輸出定於
1 #define LWIP_PLATFORM_DIAG(x) {printf x;}
2
3 #define LWIP_PLATFORM_ASSERT(x) do { printf("Assertion \"%s\" failed at \
4 line %d in %s\n",x, __LINE__, __FILE__);} while(0)
5
6 #define LWIP_ERROR(message, expression, handler) do { if (!(expression)) { \
7 printf("Assertion \"%s\" failed at line %d in %s\n", message, \
8 __LINE__, __FILE__); fflush(NULL);handler;} } while(0)
LwIP實現代碼已經添加了調試信息功能,我們只需要定於信息輸出途徑即可,這里直接使用printf函數,將調試信息打印到串口調試助手。
sys_arch.c文件存放uCOS-III與LwIP連接函數,LwIP為實現在操作系統上運行,預留了相關接口函數,不同操作系統使用不同方法實現要求的功能。該文件存放在UCOS305文件夾內。
代碼清單 3928 sys_now函數
1 u32_t sys_now()
2 {
3 OS_TICK os_tick_ctr;
4 CPU_SR_ALLOC();
5
6 CPU_CRITICAL_ENTER();
7 os_tick_ctr = OSTickCtr;
8 CPU_CRITICAL_EXIT();
9
10 return os_tick_ctr;
11 }
sys_now函數用於為LwIP提供系統時鍾,這里直接的讀取OSTickCtr變量值。CPU_CRITICAL_ENTER和CPU_CRITICAL_EXIT分別是關閉總中斷和開啟總中斷。
LwIP的郵箱用於緩存和傳遞數據包。
代碼清單 3929 郵箱創建與刪除
1 err_t sys_mbox_new(sys_mbox_t *mbox, int size)
2 {
3 OS_ERR ucErr;
4
5 OSQCreate(mbox,"LWIP quiue", size, &ucErr);
6 LWIP_ASSERT( "OSQCreate ", ucErr == OS_ERR_NONE );
7
8 if ( ucErr == OS_ERR_NONE) {
9 return 0;
10 }
11 return -1;
12 }
13
14 void sys_mbox_free(sys_mbox_t *mbox)
15 {
16 OS_ERR ucErr;
17 LWIP_ASSERT( "sys_mbox_free ", mbox != SYS_MBOX_NULL );
18
19 OSQFlush(mbox,& ucErr);
20
21 OSQDel(mbox, OS_OPT_DEL_ALWAYS, &ucErr);
22 LWIP_ASSERT( "OSQDel ", ucErr == OS_ERR_NONE );
23 }
sys_mbox_new函數要求實現的功能是創建一個郵箱,這里使用OSQCreate函數創建一個隊列。sys_mbox_free函數要求實現的功能是釋放一個郵箱,如果郵箱存在內容,會發生錯誤,這里先使用OSQFlush函數清除隊列內容,然后再使用OSQDel函數刪除隊列。LWIP_ASSERT函數是由LwIP定義的斷言,用於調試錯誤。
代碼清單 3930 郵箱發送和獲取
1 void sys_mbox_post(sys_mbox_t *mbox, void *data)
2 {
3 OS_ERR ucErr;
4 CPU_INT08U i=0;
5 if ( data == NULL ) data = (void*)&pvNullPointer;
6 /* try 10 times */
7 while (i<10) {
8 OSQPost(mbox, data,0,OS_OPT_POST_ALL,&ucErr);
9 if (ucErr == OS_ERR_NONE)
10 break;
11 i++;
12 OSTimeDly(5,OS_OPT_TIME_DLY,&ucErr);
13 }
14 LWIP_ASSERT( "sys_mbox_post error!\n", i !=10 );
15 }
16
17 err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
18 {
19 OS_ERR ucErr;
20 if (msg == NULL ) msg = (void*)&pvNullPointer;
21 OSQPost(mbox, msg,0,OS_OPT_POST_ALL,&ucErr);
22 if (ucErr != OS_ERR_NONE) {
23 return ERR_MEM;
24 }
25 return ERR_OK;
26 }
27
28 u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout)
29 {
30 OS_ERR ucErr;
31 OS_MSG_SIZE msg_size;
32 CPU_TS ucos_timeout;
33 CPU_TS in_timeout = timeout/LWIP_ARCH_TICK_PER_MS;
34 if (timeout && in_timeout == 0)
35 in_timeout = 1;
36 *msg = OSQPend (mbox,in_timeout,OS_OPT_PEND_BLOCKING,&msg_size,
37 &ucos_timeout,&ucErr);
38
39 if ( ucErr == OS_ERR_TIMEOUT )
40 ucos_timeout = SYS_ARCH_TIMEOUT;
41 return ucos_timeout;
42 }
sys_mbox_post函數要求實現的功能是發送一個郵箱,這里主要調用OSQPost函數實現隊列發送,為保證發送成功,最多嘗試10次隊列發送。sys_mbox_trypost函數是嘗試發送一個郵箱,這里我們直接使用OSQPost函數發送一次信號量,而不像sys_mbox_post函數在發送失敗時可能嘗試發送多次。sys_arch_mbox_fetch函數用於獲取郵箱內容,並指定等待超時時間,這里主要通過調用OSQPend函數實現隊列獲取。
代碼清單 3931 郵箱可用性檢查和不可用設置
1 int sys_mbox_valid(sys_mbox_t *mbox)
2 {
3 if (mbox->NamePtr)
4 return (strcmp(mbox->NamePtr,"?Q"))? 1:0;
5 else
6 return 0;
7 }
8
9 void sys_mbox_set_invalid(sys_mbox_t *mbox)
10 {
11 if (sys_mbox_valid(mbox))
12 sys_mbox_free(mbox);
13 }
sys_mbox_valid函數要求實現的功能是檢查指定的郵箱是否可用,對於uCOSIII,直接調用strcmp函數檢查隊列名稱是否存在"Q"字段,如果存在說明該郵箱可用,否則不可用。sys_mbox_set_invalid函數要求實現的功能是將指定的郵箱設置為不可用(無效),這里先調用sys_mbox_valid函數判斷郵箱是可用的,如果本身不可用就無需操作,確定郵箱可用后調用sys_mbox_free函數刪除郵箱。
LwIP的信號量用於進程間的通信。
代碼清單 3932 新建信號量
1 err_t sys_sem_new(sys_sem_t *sem, u8_t count)
2 {
3 OS_ERR ucErr;
4 OSSemCreate (sem,"LWIP Sem",count,&ucErr);
5 if (ucErr != OS_ERR_NONE ) {
6 LWIP_ASSERT("OSSemCreate ",ucErr == OS_ERR_NONE );
7 return -1;
8 }
9 return 0;
10 }
sys_sem_new函數要求實現的功能是新建一個信號量,這里直接調用OSSemCreate函數新建一個信號量,count參數用於指定信號量初始值。
代碼清單 3933 信號量相關函數
1 u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout)
2 {
3 OS_ERR ucErr;
4 CPU_TS ucos_timeout;
5 CPU_TS in_timeout = timeout/LWIP_ARCH_TICK_PER_MS;
6 if (timeout && in_timeout == 0)
7 in_timeout = 1;
8 OSSemPend (sem,in_timeout,OS_OPT_PEND_BLOCKING,&ucos_timeout,&ucErr);
9 /* only when timeout! */
10 if (ucErr == OS_ERR_TIMEOUT)
11 ucos_timeout = SYS_ARCH_TIMEOUT;
12 return ucos_timeout;
13 }
14
15 void sys_sem_signal(sys_sem_t *sem)
16 {
17 OS_ERR ucErr;
18 OSSemPost(sem,OS_OPT_POST_ALL,&ucErr);
19 LWIP_ASSERT("OSSemPost ",ucErr == OS_ERR_NONE );
20 }
21
22 void sys_sem_free(sys_sem_t *sem)
23 {
24 OS_ERR ucErr;
25 OSSemDel(sem, OS_OPT_DEL_ALWAYS, &ucErr );
26 LWIP_ASSERT( "OSSemDel ", ucErr == OS_ERR_NONE );
27 }
28
29 int sys_sem_valid(sys_sem_t *sem)
30 {
31 if (sem->NamePtr)
32 return (strcmp(sem->NamePtr,"?SEM"))? 1:0;
33 else
34 return 0;
35 }
36
37 void sys_sem_set_invalid(sys_sem_t *sem)
38 {
39 if (sys_sem_valid(sem))
40 sys_sem_free(sem);
41 }
sys_arch_sem_wait函數要求實現的功能是等待獲取一個信號量,並具有超時等待檢查功能,這里直接調用OSSemPend函數實現信號量獲取。sys_sem_signal函數要求實現的功能是發送一個信號量,這里直接調用OSSemPost函數發送一個信號量。sys_sem_free函數要求實現的功能是釋放一個信號量,這里直接調用OSSemDel函數刪除信號量。sys_sem_valid函數要求實現的功能是檢查指定的信號量是否可用,這里調用strcmp函數判斷信號量名稱中是否存在"SEM"字段,如果存在說明是信號量,否則不是信號量。sys_sem_set_invalid函數要求實現的功能是使指定的信號量不可用,這里先調用sys_sem_valid確保信號量可用,再調用sys_sem_free函數釋放該信號量。
代碼清單 3934 系統初始化
1 void sys_init(void)
2 {
3 OS_ERR ucErr;
4 memset(LwIP_task_priority_stask,0,sizeof(LwIP_task_priority_stask));
5 /* init mem used by sys_mbox_t, use ucosIII functions */
6 OSMemCreate(&StackMem,"LWIP TASK STK",(void*)LwIP_Task_Stk,
7 LWIP_TASK_MAX,LWIP_STK_SIZE*sizeof(CPU_STK),&ucErr);
8 LWIP_ASSERT( "sys_init: failed OSMemCreate STK", ucErr == OS_ERR_NONE );
9 }
sys_init函數在系統啟動時被調用,可以用於初始化工作環境,這里先調用memset函數將LwIP_task_priority_stask數組內容清空,該數值用於存放任務優先級,接下來調用OSMemCreate函數初始化申請內存空間,用於LwIP任務棧空間。
代碼清單 3935 任務創建
1 sys_thread_t sys_thread_new(const char *name, lwip_thread_fn thread ,
2 void *arg, int stacksize, int prio)
3 {
4 CPU_INT08U ubPrio = LWIP_TASK_START_PRIO;
5 OS_ERR ucErr;
6 int i;
7 int tsk_prio;
8 CPU_STK * task_stk;
9 if (prio) {
10 ubPrio +=(prio-1);
11 for (i=0; i<LWIP_TASK_MAX; ++i)
12 if (LwIP_task_priority_stask[i] == ubPrio)
13 break;
14 if (i == LWIP_TASK_MAX) {
15 for (i=0; i<LWIP_TASK_MAX; ++i)
16 if (LwIP_task_priority_stask[i]==0) {
17 LwIP_task_priority_stask[i] = ubPrio;
18 break;
19 }
20 if (i == LWIP_TASK_MAX) {
21 LWIP_ASSERT("sys_thread_new: there is no space for priority",0);
22 return (-1);
23 }
24 } else
25 prio = 0;
26 }
27 /* Search for a suitable priority */
28 if (!prio) {
29 ubPrio = LWIP_TASK_START_PRIO;
30 while (ubPrio < (LWIP_TASK_START_PRIO+LWIP_TASK_MAX)) {
31 for (i=0; i<LWIP_TASK_MAX; ++i)
32 if (LwIP_task_priority_stask[i] == ubPrio) {
33 ++ubPrio;
34 break;
35 }
36 if (i == LWIP_TASK_MAX)
37 break;
38 }
39 if (ubPrio < (LWIP_TASK_START_PRIO+LWIP_TASK_MAX))
40 for (i=0; i<LWIP_TASK_MAX; ++i)
41 if (LwIP_task_priority_stask[i]==0) {
42 LwIP_task_priority_stask[i] = ubPrio;
43 break;
44 }
45 if(ubPrio>=(LWIP_TASK_START_PRIO+LWIP_TASK_MAX)||i==LWIP_TASK_MAX){
46 LWIP_ASSERT( "sys_thread_new: there is no free priority", 0 );
47 return (-1);
48 }
49 }
50 if (stacksize > LWIP_STK_SIZE || !stacksize)
51 stacksize = LWIP_STK_SIZE;
52 /* get Stack from pool */
53 task_stk = OSMemGet( &StackMem, &ucErr );
54 if (ucErr != OS_ERR_NONE) {
55 LWIP_ASSERT( "sys_thread_new: impossible to get a stack", 0 );
56 return (-1);
57 }
58 tsk_prio = ubPrio-LWIP_TASK_START_PRIO;
59 OSTaskCreate(&LwIP_task_TCB[tsk_prio],
60 (CPU_CHAR *)name,
61 (OS_TASK_PTR)thread,
62 (void *)0,
63 (OS_PRIO )ubPrio,
64 (CPU_STK *)&task_stk[0],
65 (CPU_STK_SIZE)stacksize/10,
66 (CPU_STK_SIZE)stacksize,
67 (OS_MSG_QTY )0,
68 (OS_TICK )0,
69 (void *)0,
70 (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
71 (OS_ERR *)&ucErr);
72
73 return ubPrio;
74 }
sys_thread_new函數要求實現的功能是新建一個任務,函數有五個形參,分別指定任務名稱、任何函數、任務自定義參數、任務棧空間大小、任務優先級。對於LwIP,系統有限制其最多可用任務數,對於優先級也指定一定的范圍,sys_thread_new函數先對優先級參數進行處理,獲取合適的優先級。OSMemGet函數用於從分配給LwIP任務棧使用的內存空間中申請一塊空間用於本任務。OSTaskCreate函數用於創建一個任務。
代碼清單 3936 臨界區域保護
1 sys_prot_t sys_arch_protect(void)
2 {
3 CPU_SR_ALLOC();
4
5 CPU_CRITICAL_ENTER();
6 return 1;
7 }
8
9 void sys_arch_unprotect(sys_prot_t pval)
10 {
11 CPU_SR_ALLOC();
12
13 LWIP_UNUSED_ARG(pval);
14 CPU_CRITICAL_EXIT();
15 }
sys_arch_protecth函數要求實現的功能是完成臨界區域保護並保存當前內容,這里調用CPU_CRITICAL_ENTER函數進入臨界區域保護。sys_arch_unprotect函數要求實現的功能是恢復受保護區域的先前狀態,與sys_arch_protecth函數配套使用,這里直接調用CPU_CRITICAL_EXIT函數完成退出臨界區域保護。
ethernetif.c文件存放LwIP與ETH外設連接函數(網絡接口函數),屬於最底層驅動函數,與上個實驗無操作系統移植的文件內容有所不同,這里在函數內部會調用相關系統操作函數。
代碼清單 3937 low_level_init函數
1 static void low_level_init(struct netif *netif)
2 {
3 uint32_t i;
4
5 /* set netif MAC hardware address length */
6 netif->hwaddr_len = ETHARP_HWADDR_LEN;
7 /* set netif MAC hardware address */
8 netif->hwaddr[0] = MAC_ADDR0;
9 netif->hwaddr[1] = MAC_ADDR1;
10 netif->hwaddr[2] = MAC_ADDR2;
11 netif->hwaddr[3] = MAC_ADDR3;
12 netif->hwaddr[4] = MAC_ADDR4;
13 netif->hwaddr[5] = MAC_ADDR5;
14 /* set netif maximum transfer unit */
15 netif->mtu = 1500;
16 /* Accept broadcast address and ARP traffic */
17 netif->flags=NETIF_FLAG_BROADCAST|NETIF_FLAG_ETHARP|NETIF_FLAG_LINK_UP;
18 s_pxNetIf =netif;
19
20 /* initialize MAC address in ethernet MAC */
21 ETH_MACAddressConfig(ETH_MAC_Address0, netif->hwaddr);
22 /* Initialize Tx Descriptors list: Chain Mode */
23 ETH_DMATxDescChainInit(DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
24 /* Initialize Rx Descriptors list: Chain Mode */
25 ETH_DMARxDescChainInit(DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);
26
27 /* Enable Ethernet Rx interrrupt */
28 for (i=0; i<ETH_RXBUFNB; i++) {
29 ETH_DMARxDescReceiveITConfig(&DMARxDscrTab[i], ENABLE);
30 }
31
32 #ifdef CHECKSUM_BY_HARDWARE
33 /* Enable the checksum insertion for the Tx frames */
34 {
35 for (i=0; i<ETH_TXBUFNB; i++) {
36 ETH_DMATxDescChecksumInsertionConfig(&DMATxDscrTab[i],
37 ETH_DMATxDesc_ChecksumTCPUDPICMPFull);
38 }
39 }
40 #endif
41
42 /* create the task that handles the ETH_MAC */
43 sys_thread_new((const char*)"Eth_if",ethernetif_input,netif,
44 netifINTERFACE_TASK_STACK_SIZE,netifINTERFACE_TASK_PRIORITY);
45 /* Enable MAC and DMA transmission and reception */
46 ETH_Start();
47 }
low_level_init函數是網絡接口初始化函數,在ethernetif_init函數被調用,在系統啟動時被運行一次,用於初始化與網絡接口相關硬件。函數先是給netif結構體成員賦值配置網卡參數,調用ETH_MACAddressConfig函數綁定網卡MAC地址,ETH_DMATxDescChainInit和ETH_DMARxDescChainInit初始化網絡數據幀發送和接收描述符,設置為鏈模式。調用ETH_DMARxDescReceiveITConfig函數使能DMA數據接收相關中斷。通過定義宏CHECKSUM_BY_HARDWARE,可以使能發送數據硬件校驗和,這個需要硬件支持,STM32F42x控制器是支持的。調用sys_thread_new函數創建一個任務,設置任務函數是ethernetif_input,該函數用於講接收到數據包轉入到LwIP內部緩存區,這里還傳遞了netif結構體變量。最后,調用ETH_Start函數使能ETH。
low_level_output和low_level_input兩個函數內容與上個實驗工程同名函數幾乎相同,這里不再講解。
代碼清單 3938 ethernetif_input函數
1 void ethernetif_input( void * pvParameters )
2 {
3 struct pbuf *p;
4 OS_ERR os_err;
5 err_t err;
6
7 /* move received packet into a new pbuf */
8 while (1) {
9 SYS_ARCH_DECL_PROTECT(sr);
10
11 SYS_ARCH_PROTECT(sr);
12 p = low_level_input(s_pxNetIf);
13 SYS_ARCH_UNPROTECT(sr);
14 if (p == NULL)
15 continue;
16 err = s_pxNetIf->input(p, s_pxNetIf);
17 if (err != ERR_OK) {
18 LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
19 pbuf_free(p);
20 p = NULL;
21 }
22 /*sleep 5 ms*/
23 OSTimeDlyHMSM(0, 0, 0, 5, OS_OPT_TIME_DLY, (OS_ERR *)&os_err);
24 }
25 }
ethernetif_input函數作為sys_thread_new指定的任務函數,用於接收網絡數據包並將其轉入到LwIP內核。SYS_ARCH_DECL_PROTECT、SYS_ARCH_PROTECT和SYS_ARCH_UNPROTECT三個函數是與臨界區域保護相關代碼,可用在lwipopts.h文件中的SYS_LIGHTWEIGHT_PROT配置相關功能。調用low_level_input函數獲取接收到的數據包,如果判斷沒有接收到數據包則不執行本來循環后面內容。在判斷接收到數據包后,將數據包內容轉入LwIP內核。
相對與上個實驗工程,那個時候我們需要在main函數中的無限循環中調用數據包接收查詢,現在在這里我們創建一個這個數據包接收任務,讓它執行數據包接收並將數據轉入到LwIP內核。
代碼清單 3939 ethernetif_init函數
1 err_t ethernetif_init(struct netif *netif)
2 {
3 LWIP_ASSERT("netif != NULL", (netif != NULL));
4
5 #if LWIP_NETIF_HOSTNAME
6 /* Initialize interface hostname */
7 netif->hostname = "lwip";
8 #endif /* LWIP_NETIF_HOSTNAME */
9
10 netif->name[0] = IFNAME0;
11 netif->name[1] = IFNAME1;
12
13 netif->output = etharp_output;
14 netif->linkoutput = low_level_output;
15
16
17 /* initialize the hardware */
18 low_level_init(netif);
19
20 etharp_init();
21 sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);
22
23 return ERR_OK;
24 }
25
ethernetif_init函數用於初始化網卡,在系統啟動時必須被運行一次,該函數在LwIP_Init函數中被調用。函數首先為netif結構體成員賦值,然后調用low_level_init函數完成ETH外設初始化,etharp_init函數完成ARP協議初始化,sys_timeout函數啟動ARP超時並注冊一個ARP超時回調函數。
netconf.c和netconf.h用於存放LwIP配置相關代碼。netconf.h是相關的宏定義,具體參考代碼清單 398,不過本實驗定義了USE_DHCP宏,使能DHCP功能。不同於上個實驗,現在netconf.c文件只要求實現兩個函數,LwIP_Init和LwIP_DHCP_task函數,把其他函數刪除。其中LwIP_Init函數與上個實驗工程使用相同配置即可,參考代碼清單 399。
代碼清單 3940 LwIP_DHCP_task函數
1 #ifdef USE_DHCP
2 void LwIP_DHCP_task(void * pvParameters)
3 {
4 struct ip_addr ipaddr;
5 struct ip_addr netmask;
6 struct ip_addr gw;
7 OS_ERR os_err;
8
9 while (1) {
10 switch (DHCP_state) {
11 /*************************************************/
12 /* 與上個實驗工程代碼相同,參考代碼清單 3912 */
13 /*************************************************/
14 }
15 OSTimeDlyHMSM( 0u, 0u, 0u, 250u,OS_OPT_TIME_HMSM_STRICT,&os_err);
16 }
17 }
18 #endif
在netconf.h文件中定義了USE_DHCP宏,即開啟了DHCP功能,LwIP_DHCP_task函數才有效。在上個實驗工程中,我們使用LwIP_DHCP_Process_Handle函數(參考代碼清單 3912)完成DHCP功能實現,該函數是被周期調用執行的。現在,既然我們使用了操作系統,就可以直接創建一個任務執行DHCP功能,LwIP_DHCP_task函數就是DHCP任務函數,函數需要實現的內容與代碼清單 3912相同,在函數最后調用OSTimeDlyHMSM函數延時250ms。
lwipopts.h文件存放一些宏定義,用於剪切LwIP功能,該文件拷貝自ST官方帶操作系統的工程文件,方便我們移植,該文件同時使能了Netconn和Socket編程支持。這里我們還需要對該文件一個宏定義進行修改,直接把TCPIP_THREAD_PRIO宏定義為6,該宏定義了TCPIP任務的優先級。
至此,有關LwIP函數文件修改已經全部完成,接下來還需要實現就是調用相關初始化函數完成LwIP初始化,然后就可以直接使用LwIP函數完成用戶任務。
首先是ETH外設硬件相關初始化函數調用,bsp.c文件中BSP_Init函數用於放置系統啟動時各模塊硬件初始化函數。
代碼清單 3941 BSP_Init函數
1 void BSP_Init (void)
2 {
3 /* 設置NVIC優先級分組為Group2:0-3搶占式優先級,0-3的響應式優先級 */
4 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
5
6 /* 初始化LED */
7 LED_GPIO_Config();
8
9 Key_GPIO_Config();
10 /* 初始化調試串口,一般為串口1 */
11 Debug_USART_Config();
12
13 printf("基於uCOS-III的LwIP網絡通信測試\n");
14
15 BSP_Tick_Init();
16
17 /* Configure ethernet (GPIOs, clocks, MAC, DMA) */
18 ETH_BSP_Config();
19 printf("LAN8720A BSP INIT AND COMFIGURE SUCCESS\n");
20 }
在BSP_Init函數最后調用ETH_BSP_Config函數完成ETH外設相關硬件初始化,包含了RMII和SMI相關GPIO初始化,ETH外設時鍾使能、MAC和DMA配置並獲取PHY的狀態。
代碼清單 3942 開始任務函數
1 static void AppTaskStart (void *p_arg)
2 {
3 OS_ERR err;
4 (void)p_arg;
5
6 BSP_Init(); /* Initialize BSP functions*/
7 CPU_Init(); /* Initialize the uC/CPU services*/
8
9 #if OS_CFG_STAT_TASK_EN > 0u
10 OSStatTaskCPUUsageInit(&err);/*Compute CPU capacity with no task running*/
11 #endif
12
13 #ifdef CPU_CFG_INT_DIS_MEAS_EN
14 CPU_IntDisMeasMaxCurReset();
15 #endif
16
17 #if (APP_CFG_SERIAL_EN == DEF_ENABLED)
18 APP_TRACE_DBG(("Creating Application kernel objects\n\r"));
19 #endif
20 AppObjCreate();
21 /* Create Applicaiton kernel objects */
22 #if (APP_CFG_SERIAL_EN == DEF_ENABLED)
23 APP_TRACE_DBG(("Creating Application Tasks\n\r"));
24 #endif
25 AppTaskCreate(); /* Create Application tasks*/
26
27 /* Initilaize the LwIP stack */
28 LwIP_Init();
29
30 #ifdef USE_DHCP
31 /* Start DHCPClient */
32 OSTaskCreate(&AppTaskDHCPTCB,"DHCP",
33 LwIP_DHCP_task,
34 &gnetif,
35 APP_CFG_TASK_DHCP_PRIO,
36 &AppTaskDHCPStk[0],
37 AppTaskDHCPStk[APP_CFG_TASK_DHCP_STK_SIZE / 10u],
38 APP_CFG_TASK_DHCP_STK_SIZE,
39 0u,
40 0u,
41 0,
42 (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
43 &err);
44 #endif //#ifdef USE_DHCP
45
46 while (DEF_TRUE) {
47 OSTimeDlyHMSM(0u, 0u, 1u, 0u,
48 OS_OPT_TIME_HMSM_STRICT,
49 &err);
50 }
51 }
AppTaskStart函數是系統運行的啟動任務函數,先執行BSP_Init函數完成各個模塊硬件初始化,接下來幾個函數用於uCOS-III初始化,接下來調用LwIP_Init函數完成LwIP協議棧初始化。如果使能了DHCP功能,就創建DHCP任務,指定LwIP_DHCP_task函數為DHCP任務函數。
這個實驗只是簡單實現LwIP在uCOS-III操作系統基礎上移植,並沒有過多實現應用層方面代碼,最后通過開發板是否ping通檢驗。
下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板"USB TO UART"接口跟電腦,在電腦端打開串口調試助手並配置好相關參數;使用網線連接開發板網口跟路由器,這里要求電腦連接在同一個路由器上,這里要求使用路由器,可以提供DHCP服務器功能,而電腦不行的。編譯工程文件下載到開發板上。在串口調試助手可以看到相關信息,參考圖 3925,可以看到在使能DHCP功能之后,開發板動態獲取IP地址為:192.168.1.124,這與我們在netconf.h文件中設置的靜態地址是不同的(當然存在剛好相同概率)。
圖 3925 串口調試助手窗口
串口調試助手顯示如圖 3925信息是代碼移植成功的最基本保證。如果沒有代碼沒有移植成功或者網線沒有接好,是無法通過DHCP獲取動態IP的。保證移植成功之后,為進一步驗證程序,我們可以在電腦端ping開發板網絡。打開電腦端的DOC命令輸入窗口,輸入"ping 192.168.1.124",就可以測試網絡鏈路,鏈路正常時DOC窗口截圖如圖 3926。
圖 3926 ping命令窗口
39.10 每課一題
1. 列舉幾處有無操作系統移植LwIP的不同點。簡述兩種情況下,對數據包接收檢測方法的不同。
2. 無操作系統移植LwIP實驗使用靜態IP地址,實驗中我們使用了路由器中間連接,實際上是可以將開發板直連電腦的,請查閱資料實現。