?
一、環境介紹
代碼開發工具: Keil5
TCP/IP協議棧: LWIP
網卡: DM9000
本篇文章主要講解如何在STM32F103工程里添加移植LWIP協議,最終完成TCP服務器、TCP客戶端的通信測試。 網卡采用的是DM9000,工程代碼中,采用STM32的FSMC接口來驅動DM900網卡,DM9000是并口網卡,引腳多,但是速度快,也可以采用其他網卡,SPI協議的、UART協議的等。 比如:ENC28J60。 因為主要是講LWIP協議棧的移植,所以網卡相關的代碼就沒有細說(需要準備一個網卡可以正常通信的工程,再移植)。
下面進行工程的移植代碼比較多,需要下載對應版本的LWIP源碼,STM32本身的DM9000網卡代碼也比較多,文章里會講解移植的詳細的過程,代碼貼出了核心部分。
為了方便大家下載體驗,下面給了個下載鏈接,里面包含了本次文章移植的工程、LWIP源碼,移植文檔等,方便直接體驗。地址在這里:https://download.csdn.net/download/xiaolong1126626497/19907087
資料包里的內容如下:
?
?
?
二、D9000網卡
2.1 DM9000簡介
DM9000 是一款完全集成的、性價比高、引腳數少、帶有通用處理器接口的單芯片快速以太網控制器。 自帶一個 10/100M PHY 和 4K 雙字的 SRAM ,DM9000A 為適應各種處理器提供了8位、16 位數據接口訪問內部存儲器,DM9000擁有自動協商功能,DM9000特性如下:
1、集成自適應10/100M收發器。
2、內置16k字節的SRAM。
3、支持硬件幀校驗。
4、兼容3.3V和5.0V輸入輸出電壓。
DM9000 有多種型號,有 100 引腳和 48 引腳的, 開發板選擇的是 48 引腳的 DM9000,型號為 DM9000CEP。
2.2 DM9000 中斷引腳電平設置
DM9000的34(INT)引腳為中斷輸出引腳,默認情況下該引腳高電平有效??梢酝ㄟ^設置DM9000 的 20(EECK)引腳來改變 INT 的有效電平,當 EECK 拉高以后, INT 低電平有效,否則的話 INT 是高電平有效的。開發板上 R66 電阻為 EECK 的上拉電阻,因此開發板上 DM9000 的 INT 引腳是低電平有效的。
?
2.3 DM9000 數據位寬設置
前面我們提了一下 DM9000 支持 8 位和 16 位兩種數據位寬,可以通過 DM9000 的 21(EECS)引腳設置其數據位寬,當 EECS 上拉的時候 DM9000 選擇 8 位數據位寬,否則的話選擇 16 位數據位寬。開發板上的 R65 電阻為 EECS 的上拉電阻,但是此電阻并未焊接! DM9000 芯片的數據位寬為 16 位。
?
2.4 DM9000寄存器表
寄存器 |
描述 |
寄存器地址 |
默認值 |
|
NCR |
網絡控制寄存器。 |
00H |
00H |
|
NSR |
網絡狀態寄存器。 |
01H |
00H |
|
TCR |
發送控制寄存器。 |
02H |
00H |
|
TSR I |
發送狀態寄存器 I。 |
03H |
00H |
|
TSR II |
發送狀態寄存器 II。 |
04H |
00H |
|
RCR |
接收控制寄存器。 |
05H |
00H |
|
RSR |
接收狀態寄存器。 |
06H |
00H |
|
ROCR |
接收溢出計數寄存器。 |
07H |
00H |
|
BPTR |
背壓門限寄存器。 |
08H |
37H |
|
FCTR |
溢出控制門限寄存器。 |
09H |
38H |
|
FCR |
TX/RX 流量控制寄存器。 |
0AH |
00H |
|
EPCR |
EEPROM/PHY 控制寄存器。 |
0BH |
00H |
|
EPAR |
EEPROM/PHY 地址寄存器。 |
0CH |
40H |
|
EPDRL |
EEPROM/PHY 數據寄存器低位。 |
0DH |
XXH |
|
EPDRH |
EEPROM/PHY 數據寄存器高位。 |
0EH |
XXH |
|
WCR |
喚醒控制寄存器。 |
0FH |
00H |
|
PAR |
物理地址寄存器。 |
10H~15H |
由 EEPROM 決定 |
|
MAR |
廣播地址寄存器。 |
16H~1DH |
XXH |
|
GPCR |
通用目的控制寄存器(8bit 模式)。 |
1EH |
01H |
|
GPR |
通用目的寄存器。 |
1FH |
XXH |
|
TRPAL |
TX SRAM 讀指針地址低字節。 |
22H |
00H |
|
TRPAH |
TX SRAM 讀指針地址高字節。 |
23H |
00H |
|
RWPAL |
RX SRAM 寫指針地址低字節。 |
24H |
00H |
|
RWRAH |
RX SRAM 寫指針地址高字節。 |
25H |
0CH |
|
VID |
廠家 ID。 |
28H~29H |
0A46H |
|
PID |
產品 ID。 |
2AH~2BH |
9000H |
|
CHIPR |
芯片版本。 |
2CH |
18H |
|
TCR2 |
發送控制寄存器 2。 |
2DH |
00H |
|
OCR |
操作控制寄存器。 |
2EH |
00H |
|
SMCR |
特殊模式控制寄存器。 |
2FH |
00H |
|
ETXCSR |
即將發送控制/狀態寄存器。 |
30H |
00H |
|
TCSCR |
發送校驗和控制寄存器。 |
31H |
00H |
|
RCSCSR |
接收校驗和控制狀態寄存器。 |
32H |
00H |
|
MRCMDX |
內存數據預取讀命令寄存器(地址不加 1)。 |
F0H |
XXH |
|
MRCMDX1 |
內存數據讀命令寄存器(地址不加 1)。 |
F1H |
XXH |
|
MRCMD |
內存數據讀命令寄存器(地址加 1)。 |
F2H |
XXH |
|
MRRL |
內存數據讀地址寄存器低字節。 |
F4H |
00H |
|
MRRH |
內存數據讀地址寄存器高字節。 |
F5H |
00H |
|
MWCMDX |
內存數據寫命令寄存器(地址不加 1) |
F6H |
XXH |
|
MWCMD |
內存數據寫命令寄存器(地址加 1)。 |
F8H |
XXH |
|
MWRL |
內存數據寫地址寄存器低字節。 |
FAH |
00H |
|
MWRH |
內存數據寫地址寄存器高字節。 |
FBH |
00H |
|
TXPLL |
TX 數據包長度低字節寄存器。 |
FCH |
XXH |
|
TXPLH |
TX 數據包長度高字節寄存器。 |
FDH |
XXH |
|
ISR |
中斷狀態寄存器。 |
FEH |
00H |
|
IMR |
中斷屏蔽寄存器。 |
FFH |
00H |
|
2.5 DM9000常用寄存器介紹
NCR、 NSR、 TCR、 RCR、 FCTR、 BPTR、 TCR2、 ISR、 IMR。
NCR(網絡控制寄存器)寄存器
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
RESERVED |
WAKEEN |
RESERVED |
FCOL |
FDX |
LBK |
RST |
FCOL:強制沖突模式,用于檢測。
FDX:內部 PHY 全雙工模式。
LBK:回環模式(LoopBack)
00 正常;
01 MAC 內部回環;
10 內部 PHY100M 模式數字回環;
11 保留;
RST:置 1 軟件復位, 10us 后自動清零。
NSR 寄存器(網絡狀態寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名 稱 |
SPEED |
LINKST |
WAKEST |
RESERVED |
TX2END |
TX1END |
RXOV |
RESERVED |
SPEED:網絡速度,在使用內部 PHY 情況下,0 表示 100Mbps,1 表示 100Mbps,當 LINKST=0時,此位無意義。
LINKST:連接狀態, 0 為連接失敗, 1 位已連接。
TX2END: TX(發送)數據包 2 完成標志,讀取或寫 1 將清零該位。
TX1END: TX(發送)數據包 1 完成標志,讀取或寫 1 將清零該位。
RXOV: RX(接收)FIFO 溢出標志。
TCR 寄存器(發送控制寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名 稱 |
RESERVED |
TJDIS |
EXCECM |
PAD_DIS2 |
CRC_DIS2 |
PAD_DIS1 |
CRC_DIS1 |
TXREQ |
TJDIS: Jabber 傳輸禁止。
1,禁止 Jabber 傳輸定時器(2048 字節)。
0,使能。
EXCECM:嚴重沖突模式控制
0,當沖突計數多于 15 則終止本次數據包。
1,始終嘗試發送本次數據包。
PAD_DIS2:禁止為數據包 II 添加填充。
CRC_DIS2:禁止為數據包 II 添加 CRC 校驗。
PAD_DIS1:禁止為數據包 I 添加填充。
CRC_DIS1:禁止為數據包 I 添加 CRC 校驗。
TXREQ: TX(發送)請求,發送完成后自動清零該位
RCR 寄存器(發送控制寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
RESERVED |
WTDIS |
DIS_LONG |
DIS_CRC |
ALL |
RUNT |
PRMSC |
RXEN |
WTDIS:看門狗定時器(2048 字節)禁止。
1,進制
0,使能
DIS_LONG:丟棄長數據包, 1,丟棄數據包長度超過 1522 字節的數據包。
DIS_CRC:丟棄 CRC 校驗錯誤數據包。
ALL:允許廣播。
RUNT:允許小于最小長度的數據包。
PRMSC:各種模式。
RXEN:接收使能。
FCTR 寄存器(流控制閾值寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
HWOT |
HWOT |
HWOT |
HWOT |
LWOT |
LWOT |
LWOT |
LWOT |
HWOT:RX FIFO 緩存高位溢出門限
當 RX SRAM 空閑空間小于該門限值時則發送一個暫停時間為 FFFFH 的暫停包,若該值為 0,則無接收控件。 1=1k 字節,默認值為 3H,即 3K 字節空閑空間,不要超過 S RAM 大小。
LWOT:RX FIFO 緩存低位溢出門限當 RX SRAM 空閑空間大于該門限值時則發送一個暫停時間為 0000H 的暫停包。
當溢出門限最高值的暫停包發送之后,溢出門限最低值的暫停包才有效,默認值為 8K,不要超過 SRAM 大小。
BPTR 寄存器(背壓閾值寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
BPHW |
JPT |
BPHW:背壓閾值最高值當接收 SRAM 空閑空間低于該閾值,則 MAC 將產生一個擁擠狀態, 1=1k 字節。默認值為 3H,即 3K 字節空閑空間,不要超過 SRAM 大小。
JPT:擁擠狀態時間,模式為 200us, JPT 值與其對應的擁擠狀態時間表
JPT 值 |
擁擠狀態時間(us) |
JPT 值 |
擁擠狀態時間(us) |
0000 |
5 |
1000 |
250 |
0001 |
10 |
1001 |
300 |
0010 |
15 |
1010 |
350 |
0011 |
25 |
1011 |
400 |
0100 |
50 |
1100 |
450 |
0101 |
100 |
1101 |
500 |
0110 |
150 |
1110 |
550 |
0111 |
200 |
1111 |
600 |
TCR2 寄存器(發送控制寄存器 2)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
RLCP |
DTU |
ONEPM |
IFGS |
LED: LED 模式
1,設置 LED 引腳為模式 1
0,設置 LED 引腳為模式 0 或根據 EEPROM 的設定。
RLCP:重試沖突延時數據包, 1 重新發送有沖突延遲的數據包。
DTU: 1 禁止重新發送“underruned”數據包。
ONEPM:單包模式。
1,發送完成前發送一個數據包的命令能被執行。
0,發送完成前發送最多兩個數據包的命令能被執行。
IFGS:幀間間隔設置。
0XXX 為 96bit, 1000 為 64bit, 1001 為 72bit
1010 為 80bit, 1011 為 88bit, 1100 為 96bit
1101 為 104bit, 1110 位 112bit, 1111 為 120bit
ISR 寄存器(中斷狀態寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
IOMODE |
RESERVED |
LNKCHG |
UDRUN |
ROO |
PT |
PR |
IOMODE: 0,16 位模式; 1,8 位模式。
LNKCHG:連接狀態改變。
UDRUN:發送“Underrun”
ROO:接收溢出計數器溢出
ROS:接收溢出。
PT:數據包發送。
PR:數據包接收。
IMR 寄存器(中斷狀態寄存器)
BIT |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
名稱 |
PAR |
RESERVED |
LNKCHGI |
UDRUNI |
ROOI |
ROSI |
PTI |
PRI |
PAR:使能 SRAM 的讀/寫指針在指針地址超過 SRAM 的大小時自動跳回起始位置。需要驅動程序設置該位,若設置該位, REG_F5 將自動置為 0XH。
LNKCHGI:使能連接狀態改變中斷。
UDRUNI:使能發送“Underrun”中斷。
ROOI:使能接收溢出計數器溢出中斷。
ROI:使能接收溢出中斷。
PTI:使能數據包發送中斷。
PRI:使能數據包接收中斷。
2.6 DM9000 直接內存訪問控制(DMAC)
DM9000 直接內存訪問控制(DMAC)
DM9000 支持 DMA 方式簡化對內部存儲器的訪問。在我們編程寫好內部存儲器地址后,就可以用一個讀/寫命令偽指令把當前數據加載到內部數據緩沖區,這樣,內部存儲器指定位置就可以被讀/寫命令寄存器訪問。存儲器地址將會自動增加,增加的大小與當前總線操作模式相同(比如:8-bit、 16-bit 或 32-bit),接著下一個地址數據將會自動加載到內部數據緩沖區。要注意的是在連續突發式第一次訪問的數據應該被忽略,因為,這個數據是最后一次讀寫命令的內容。內部存儲器空間大小 16K 字節。前 3K 字節單元用作發送包的緩沖區,其他 13K 字節用作接收包的緩沖區。所以在寫存儲器操作時,如果地址越界(即超出 3K 空間),在 IMR 寄存器 bit7 置位的情況下,地址指針將會返回到存儲器 0 地址處。同樣,在讀存儲器操作時,如果地址越界(即超出 16K 空間),在 IMR 寄存器 bit7 置位的情況下,地址指針將會返回到存儲器 0x0C00 地址處。
DM9000 數據包發送
DM9000 有兩個發送數據包: index1 和 index2,同時存儲在 TX SRAM 中。發送控制寄存器(02h)控制循環冗余校驗碼(CRC)和填充(pads)的插入,其狀態分別記錄在發送狀態寄存器I(03H)和發送狀態寄存器 II(04H)中。發送器的起始地址為 0x00H,在軟件或硬件復位后,默認的數據發送包為 index1。首先,使用 DMA 端口將數據寫 TX SRAM 中,然后,在發送數據包長度寄存器中把數據字節數寫入字節計數寄存器。置位發送控制寄存器的 bit0 位,則 DM9000 開始發送 index1 數據包。在 index1數據包發送結束之前,數據發送包 index2 被移入 TX SRAM 中。在 index1 數據包發送結束后,將 index2 數據字節數寫入字節計數寄存器中,然后,置位發送控制寄存器的 bit0 位,則 index2數據包開始發送。以此類推,后面的數據包都以此方式進行發送。
DM9000 數據包接收
RX SRAM 是一個環形數據結構。在軟件或硬件復位后, RX SRAM 的起始地址為 0X0C00。每個接收數據包都包含有 CRC 校驗域,數據域,以及緊跟其后的 4 字節包頭域。 4 字節包頭格式為: 01h、狀態、 BYTE_COUNT 低、 BYTE_COUNT 高。請注意:每個接收包的起始地址處在適當的地址邊界,這取決于當前總線操作模式(8bit 或者 16bit)
2.7 DM9000原理圖介紹
?
各信號線描述如下:
PWRST: DM9000 復位信號。
CS: DM9000 的片選信號。
WR(IOW): 處理器寫命令。
RD(IOR): 處理器讀命令。
CMD: 命令/數據標志, 0,讀寫命令; 1,讀寫數據。
SD0~SD15: 16 位雙向數據線。
信號線對應的GPIO口對應關系
引腳名稱 |
GPIO口 |
功能說明 |
PWRST-->DM9000_RST |
PD7 |
復位信號 |
CS-->FSMC_NE2 |
PG9 |
片選信號 |
WR(IOW)-->FSMC_NWE |
PD5 |
處理器寫命令 |
RD(IOR) --->FSMC_NOE |
PD4 |
處理器讀命令 |
CMD—>FSMC-A7 |
PF13 |
命令/數據標志, 0,讀寫命令; 1,讀寫數據 |
INT--->DM9000_INT |
PG6 |
中斷引腳 |
FSMC_D0 |
PD14 |
數據線0 |
FSMC_D1 |
PD15 |
數據線1 |
FSMC_D2 |
PD0 |
數據線2 |
FSMC_D3 |
PD1 |
數據線3 |
FSMC_D4 |
PE7 |
數據線4 |
FSMC_D5 |
PE8 |
數據線5 |
FSMC_D6 |
PE9 |
數據線6 |
FSMC_D7 |
PE10 |
數據線7 |
FSMC_D8 |
PE11 |
數據線8 |
FSMC_D9 |
PE12 |
數據線9 |
FSMC_D10 |
PE13 |
數據線10 |
FSMC_D11 |
PE14 |
數據線11 |
FSMC_D12 |
PE15 |
數據線12 |
FSMC_D13 |
PD8 |
數據線13 |
FSMC_D14 |
PD9 |
數據線14 |
FSMC_D15 |
PD10 |
數據線15 |
FSMC接口框圖
?
DM9000網卡接在FSMC的第2塊上,數據線地址: 0x64000000
PA7地址線作為命令與數據線切換引腳。
外接16位寬度存儲器:HADDR[25:1] ? FSMC_A[24:0]
外接8位寬度存儲器: HADDR[25:0] ? FSMC_A[25:0]
0x64000000基地址:01100100000000000000000000000000 0x64000000寫數據:01100100000000000000000000000000 0x64000100寫命令:01100100000000000000000100000000 |
2.8 DM9000時序圖介紹
IOR和IOW是DM9000的讀寫選擇引腳,低電平有效,即低電平時進行讀(IOR)寫(IOW)操作;AEN是芯片選通引腳,低電平有效,該引腳為低時才能進行讀寫操作;CMD的命令/數據切換引腳,低電平時讀寫命令操作,高電平時讀寫數據操作。
讀時序:
?
寫時序:
?
三、LWIP(TCP/IP)網絡協議棧介紹
根據以太網幀頭攜帶的上層協議類型值傳遞數據。
以太網幀格式定義:
目的MAC地址 源MAC地址 類型/長度 數據 校驗
6字節 6字節 2字節 46-1500字節 4字節
ip:0x0800
ARP:0x0806
最大幀長1518字節 最小字節64字節
3.1 LWIP介紹
lwip是瑞典計算機科學院網絡嵌入式系統小組(SICS)的Adam Dunkels(亞當·鄧克爾) 開發的一個小型開源的TCP/IP協議棧。實現的重點是在保持TCP協議主要功能的基礎上減少對RAM 的占用。
LwIP是Light Weight (輕型)IP協議,有無操作系統的支持都可以運行。LwIP實現的重點是在保持TCP協議主要功能的基礎上減少對RAM 的占用,它只需十幾KB的RAM和40K左右的ROM就可以運行,這使LwIP協議棧適合在低端的嵌入式系統中使用。lwip提供三種API:1)RAW API 2)(NETCONN)lwip API 3)BSD API。
RAW 編程接口使得程序效率高,但是需要對 LWIP 有深入的了解,而且不適合大數據量等場合。 NETCONN 編程接口,使用 NETCONN API 時需要有操作系統的支持。
RAW API把協議棧和應用程序放到一個進程里邊,該接口基于函數回調技術,使用該接口的應用程序可以不用進行連續操作。不過,這會使應用程序編寫難度加大且代 碼不易被理解。為了接收數據,應用程序會向協議棧注冊一個回調函數。該回調函數與特定的連接相關聯,當該關聯的連接到達一個信息包,該回調函數就會被協議 棧調用。這既有優點也有缺點。優點是既然應用程序和TCP/IP協議棧駐留在同一個進程中,那么發送和接收數據就不再產生進程切換。主要缺點是應用程序不 能使自己陷入長期的連續運算中,這樣會導致通訊性能下降,原因是TCP/IP處理與連續運算是不能并行發生的。這個缺點可以通過把應用程序分為兩部分來克 服,一部分處理通訊,一部分處理運算。
lwip API把接收與處理放在一個線程里面。這樣只要處理流程稍微被延遲,接收就會被阻塞,直接造成頻繁丟包、響應不及時等嚴重問題。因此,接收與協議處理必須 分開。LwIP的作者顯然已經考慮到了這一點,他為我們提供了 tcpip_input() 函數來處理這個問題, 雖然他并沒有在 rawapi 一文中說明。 講到這里,讀者應該知道tcpip_input()函數投遞的消息從哪里來的答案了吧,沒錯,它們來自于由底層網絡驅動組成的接收線程。我們在編寫網絡驅動時, 其接收部分以任務的形式創建。 數據包到達后, 去掉以太網包頭得到IP包, 然后直接調用tcpip_input()函數將其 投遞到mbox郵箱。投遞結束,接收任務繼續下一個數據包的接收,而被投遞得IP包將由TCPIP線程繼續處理。這樣,即使某個IP包的處理時間過長也不 會造成頻繁丟包現象的發生。這就是lwip API。
BSD API提供了基于open-read-write-close模型的UNIX標準API,它的最大特點是使應用程序移植到其它系統時比較容易,但用在嵌入式系統中效率比較低,占用資源多。這對于我們的嵌入式應用有時是不能容忍的
lwIP協議棧主要關注的是怎么樣減少內存的使用和代碼的大小,這樣就可以讓lwIP適用于資源有限的小型平臺例如嵌入式系統。為了簡化處理過程和內存要求,lwIP對API進行了裁減,可以不需要復制一些數據。
其主要特性如下:
(1)支持多網絡接口下的IP轉發;
(2)支持ICMP協議;
(3)包括實驗性擴展的UDP(用戶數據報協議);
(4)包括阻塞控制、RTT 估算、快速恢復和快速轉發的TCP(傳輸控制協議);
(5)提供專門的內部回調接口(Raw API),用于提高應用程序性能;
(6)可選擇的Berkeley接口API (在多線程情況下使用) 。
(7)在最新的版本中支持ppp
(8) 新版本中增加了的IP fragment(IP分片)的支持.
(9) 支持DHCP協議,動態分配ip地址.
3.2 幾種開源TCPIP協議概述
1、BSD TCP/IP協議棧
BSD棧歷史上是商業棧的起點,大多數專業TCP/IP棧(VxWorks內嵌的TCP/IP棧)是BSD棧派生的。這是因為BSD棧在BSD許可協議下提供了這些專業棧的雛形,BSD許用證允許BSD棧以修改或未修改的形式結合這些專業棧的代碼而無須向創建者付版稅。同時,BSD也是許多TCP/IP協議中的創新(如廣域網中餓擁塞控制和避免)的點。
2、uC/IP
uC/IP是由Guy Lancaster編寫的一套基于uC/OS且開放源碼的TCP/IP協議棧,亦可移植到操作系統,是一套完全免費的、可供研究的TCP/IP協議棧,uC/IP大部分源碼是從公開源碼BSD發布站點和KA9Q(一個基于DOS單任務環境運行的TCP/IP協議棧)移植過來。uC/IP具有如下一些特點:帶身份驗證和報頭壓縮支持的PPP協議,優化的單一請求/回復交互過程,支持IP/TCP/UDP協議,可實現的網絡功能較為強大,并可裁減。UCIP協議棧被為一個帶最小化用戶接口及可應用串行鏈路網絡模塊。根據采用CPU、編譯器和系統所需實現協議的多少,協議棧需要的代碼容量空間在30-60KB之間。http://ucip.sourceforge.net
3、LwIP
LwIP是瑞士計算機科學院(Swedish Institute of Computer Science)的Adam Dunkels等開發的一套用于嵌入式系統的開放源代碼TCP/IP協議棧。LwIP的含義是Light Weight(輕型)IP協議,相對于uip。LwIP可以移植到操作系統上,也可以在無操作系統的情況下獨立運行。LwIP TCP/IP實現的重點是在保持TCP協議主要功能的基礎上減少對RAM的占用,一般它只需要幾十K的RAM和40K左右的ROM就可以運行,這使LwIP協議棧適合在低端嵌入式系統中使用。LwIP的特性如下:支持多網絡接口下的IP轉發,支持ICMP協議 ,包括實驗性擴展的的UDP(用戶數據報協議),包括阻塞控制,RTT估算和快速恢復和快速轉發的TCP(傳輸控制協議),提供專門的內部回調接口(Raw API)用于提高應用程序性能,并提供了可選擇的Berkeley接口API。Svensk forskning f?r h?llbar tillv?xt| RISE或lwIP - A Lightweight TCP/IP stack - Summary [Savannah]
4、uIP
uIP是專門為8位和16位控制器設計的一個非常小的TCP/IP棧。完全用C編寫,因此可移植到各種不同的結構和操作系統上,一個編譯過的棧可以在幾KB ROM或幾百字節RAM中運行。uIP中還包括一個HTTP服務器作為服務內容。許可:BSD許用證Svensk forskning f?r h?llbar tillv?xt| RISE
uIP是一個完全由C語言編寫的開源軟件, 它的文檔和源代碼可用于商業和非商業用途, 它已經移植到了大部分的8位微控制器, 而且已在很多的嵌入式產品和項目中使用.
5、TinyTcp
TinyTcp 棧是TCP/IP的一個非常小和簡單的實現,它包括一個FTP客戶。TinyTcp是為了燒入ROM設計的并且現在開始對大端結構似乎是有用的(初始目標是68000芯片)。TinyTcp也包括一個簡單的以太網驅動器用于3COM多總線卡http://ftp.ecs.soton.ac.uk/pub/elks/utils/tiny-tcp.txt
選擇一個開源協議??梢詮乃膫€方面來考慮:
是否提供易用的底層硬件API,即與硬件平臺的無關性;
協議棧需要調用的系統函數接口是否容易構造,另一個對于應用支持程度。
最關鍵的是占用的系統資源是否在可接受范圍內,有裁減優化的空間否? 其中,
BSD ??赏暾麑崿FTCP/IP協議,但代碼龐大,70KB-150KB之間,裁減優化有難度,
uIP和TinyTcp代碼容量小巧,實現功能精簡,限制了在一些較高要求場合下的應用,如可靠性與大容量數據傳輸。
LwIP和uC/IP是同量級別的兩個開源協議棧,兩者代碼容量和實現功能相似,LwIP沒有操作系統針對性,它將協議棧與平臺相關的代碼抽象出來,用戶如果要移植到自己的系統,需要完成該部分代碼的封裝,并為網絡應用支持提供了API接口的可選性。
uC/IP協議最初是針對uC/OS設計,為方便用戶移植實現,同樣也抽象了協議棧與平臺相關代碼,但是協議棧所需調用的系統函數大多參照uC/OS內核函數原型設計,并提供了協議棧的函數,方便用戶參考,其不足在于該協議棧對網絡應用支持不足。
根據以上分析,從應用和開發的角度看,似乎LWIP更得到了網上很多朋友使用的青睞;uC/IP在文檔支持與軟件升級管理上有很多不足,但是它最初是針對UC/OS而設計,如果選用UC/OS作為軟件基礎的話,在系統函數構造方面有優勢。當然你選擇其他操作系統的話,可參照OS_NULL文件夾下的文件修改。 以上的這些開源協議棧也并非免費,拿來就可以用,據我所知,UC/OS的母公司推出UC/OS-TCP/IP花了6人*2年的工作量,國內某公司使用LWIP作為移植的參照,花了4-5人*2年的工作量來測試與優化協議,使用商用TCP/IP棧的高費用就不足為奇了。 作為廣大的愛好者學習而言,如果只是跑跑原型,實驗一下效果,以上的幾種開源協議棧都提供了測試的例子,應該是不錯的選擇。
終上所述:LWIP可優先考慮,參考的資料較多
四、LWIP協議棧移植
4.1 LWIP源碼下載
源碼下載地址: http://ftp.yzu.edu.tw/nongnu/lwip/
?
下載LWIP1.4.1版本、并下載contrib-1.4.1版本。
?
4.2 將LWIP源碼加入到工程目錄
?
?
?
?
?
?
?
?
?
?
?
?
4.3 配置lwipopts.h文件
?
?
?
?
4.4 修改ethernetif.c文件
ethernetif.c文件默認是不編譯的,該文件是網卡底層接口的模板文件,需要根據修改網卡發送接口和接收接口。
?
?
?
?
4.5 修改sys_arch.c文件
修改sys_arch.c只是留下sys_now()函數,其他代碼全部刪除掉。刪除windows.h頭文件。
sys_now()函數用于返回一個32位的系統時鐘,單位是ms。沒有操作系統的情況下,使用定時器提供時間即可。
?
4.6 新建lwip_config.c文件
在LWIP/app目錄下新建一個lwip_config.c/lwip_config.h文件。用于編寫動態IP地址分配處理代碼,和LWIP事物輪詢、初始化代碼。
編寫一個LWIP初始化配置函數,向LWIP協議棧添加一個新的網卡設備
/*
函數功能: LWIP協議棧初始化
*/
void lwip_config_init(void)
{
ip_addr_t ipaddr; //IP地址
ip_addr_t netmask; //子網掩碼
ip_addr_t gw; //網關
//全部初始化為0 -因為使用了動態IP地址分配
ipaddr.addr=0;
netmask.addr=0;
gw.addr=0;
/*1. 初始化LWIP內核*/
lwip_init();
/*2. 向網卡列表中添加一個網絡設備*/
netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,eernetif_init,eernet_input);
/*3. 開啟DHCP服務 */
dhcp_start(&lwip_netif);
/*4. 設置netif為默認網口*/
netif_set_default(&lwip_netif);
/*5. 打開netif網口*/
netif_set_up(&lwip_netif);
}
編寫LWIP事物輪詢函數與DHCP處理函數
u32 TCPTimer=0; //TCP查詢計時器
u32 ARPTimer=0; //ARP查詢計時器
u32 DHCPfineTimer=0; //DHCP精細處理計時器
u32 DHCPcoarseTimer=0; //DHCP粗糙處理計時器
u32 DHCP_State=1; //保存DHCP狀態 1表示沒有分配成功 0表示分配成功
/*
函數功能: LWIP輪詢任務
*/
void lwip_periodic_handle()
{
//每250ms調用一次tcp_tmr()函數
if(TCPTimer >= TCP_TMR_INTERVAL)
{
TCPTimer=0;
tcp_tmr(); //處理TCP協議請求
}
//ARP每5s周期性調用一次
if(ARPTimer >= ARP_TMR_INTERVAL)
{
ARPTimer=0;
etharp_tmr();
}
//每500ms調用一次dhcp_fine_tmr()
if(DHCPfineTimer >= DHCP_FINE_TIMER_MSECS)
{
DHCPfineTimer=0;
dhcp_fine_tmr(); //動態IP地址分配的事物處理
if(DHCP_State)lwip_dhcp_process_handle(); //DHCP處理
}
//每60s執行一次DHCP粗糙處理
if(DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS)
{
DHCPcoarseTimer=0;
dhcp_coarse_tmr();
}
}
//lwip控制結構體
typedef struct
{
u8 remoteip[4]; //服務器主機IP地址
u8 ip[4]; //本機IP地址
u8 netmask[4]; //子網掩碼
u8 gateway[4]; //默認網關的IP地址
}__lwip_dev;
extern __lwip_dev lwipdev; //lwip信息結構體
__lwip_dev lwipdev; //lwip信息結構體
/*
函數功能: DHCP處理任務
*/
void lwip_dhcp_process_handle(void)
{
u32 ip=0,netmask=0,gw=0;
ip=lwip_netif.ip_addr.addr; //讀取新IP地址
netmask=lwip_netif.netmask.addr; //讀取子網掩碼
gw=lwip_netif.gw.addr; //讀取默認網關
if(ip!=0) //正確獲取到IP地址的時候
{
DHCP_State=0; //表示分配成功
//解析出通過DHCP獲取到的IP地址
lwipdev.ip[3]=(uint8_t)(ip>>24);
lwipdev.ip[2]=(uint8_t)(ip>>16);
lwipdev.ip[1]=(uint8_t)(ip>>8);
lwipdev.ip[0]=(uint8_t)(ip);
printf("動態分配
IP:..............%d.%d.%d.%d\r\n",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
//解析通過DHCP獲取到的子網掩碼地址
lwipdev.netmask[3]=(uint8_t)(netmask>>24);
lwipdev.netmask[2]=(uint8_t)(netmask>>16);
lwipdev.netmask[1]=(uint8_t)(netmask>>8);
lwipdev.netmask[0]=(uint8_t)(netmask);
printf("子網掩
碼............%d.%d.%d.%d\r\n",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);
//解析出通過DHCP獲取到的默認網關
lwipdev.gateway[3]=(uint8_t)(gw>>24);
lwipdev.gateway[2]=(uint8_t)(gw>>16);
lwipdev.gateway[1]=(uint8_t)(gw>>8);
lwipdev.gateway[0]=(uint8_t)(gw);
printf("網
關.........%d.%d.%d.%d\r\n",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);
}
}
4.7 配置一個定時器提供時間基準
?
4.8 初始化lwip動態獲取IP地址
?
4.9 LWIP內存配置選擇
LWIP可以選擇使用系統庫自帶的函數malloc/free進行管理空間,也可以使用lwip自己的內存管理函數進行管理,源碼默認就是使用lwip自己的內存管理方法,就是在初始化內存的時候定義一個數組,數組的大小在lwipopts.h文件MEM_SIZE宏定義的。
?
?
?
?
五、LWIP函數使用(RAW編程接口)
5.1 LWIP初始化配置
?
ip_addr_t ipaddr; //IP地址
ip_addr_t netmask; //子網掩碼
ip_addr_t gw; //網關
//全部初始化為0 -因為使用了動態IP地址分配
ipaddr.addr=0;
netmask.addr=0;
gw.addr=0;
/*1. 初始化LWIP內核*/
lwip_init();
/*2. 向網卡列表中添加一個網絡設備*/
netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,eernetif_init,eernet_input);
/*3. 開啟DHCP服務 */
dhcp_start(&lwip_netif);
/*4. 設置netif為默認網口*/
netif_set_default(&lwip_netif);
/*5. 打開netif網口*/
netif_set_up(&lwip_netif);
5.2 LWIP輪詢函數處理
LWIP輪詢期間:
1. 推薦每250ms周期性調用一次tcp_tmr()函數,處理TCP協議請求。
超時時間LWIP使用TCP_TMR_INTERVAL宏進行了定義。
2. 推薦每5s周期性調用一次etharp_tmr()函數,清除ARP表中過期的數據。
超時時間LWIP使用ARP_TMR_INTERVAL宏進行了定義。
3. (如果開啟了動態IP分配功能)推薦每500ms周期性調用一次dhcp_fine_tmr()函數,處理DHCP動態IP地址分配請求。 如果IP地址獲取成功,將會放在初始化時注冊的網絡設備結構體里(struct netif)。
超時時間LWIP使用DHCP_FINE_TIMER_MSECS宏進行了定義。
4. (如果開啟了動態IP分配功能)推薦每60s調用一次dhcp_coarse_tmr()函數,用于檢查DHCP租約時間,并進行重新綁定。
超時時間LWIP使用DHCP_COARSE_TIMER_MSECS宏進行了定義。
5. 在LWIP運行期間,當網卡收到數據時,還需要調用ethernetif_input函數讀取網卡數據。
在函數ethernetif_input()主要完成兩個工作
1、調用low_level_input(); 讀取網卡實際數據。
2、調用netif->input();
所以,為了能夠實時的讀取數據,需要最快的速度輪詢調用ethernetif_input函數。
5.3 LWIP編程RAW接口函數
tcp_new() 創建一個 TCP 的 PCB 控制塊
tcp_bind() 為 TCP 的 PCB 控制塊綁定一個本地 IP 地址和端口號
tcp_listen() 開始 TCP 的 PCB 監聽
tcp_accept() 控制塊 accept字段注冊的回調函數,偵聽到連接時被調用
tcp_accepted() 通知 LWIP 協議棧一個 TCP 連接被接受了
tcp_conect() 連接遠端主機
tcp_write() 構造一個報文并放到控制塊的發送緩沖隊列中
tcp_sent() 控制塊 sent 字段注冊的回調函數,數據發送成功后被回調
tcp_output() 將發送緩沖隊列中的數據發送出去
tcp_recv()控制塊 recv 字段注冊的回調函數,當接收到新數據時被調用
tcp_recved()當程序處理完數據后一定要調用這個函數,通知內核更新接收窗口
tcp_poll() 控制塊 poll 字段注冊的回調函數,該函數周期性調用
tcp_close() 關閉一個 TCP 連接
tcp_err() 控制塊 err 字段注冊的回調函數,遇到錯誤時被調用
tcp_abort() 中斷 TCP 連接
5.4 創建TCP服務器示例
下面演示了TCP服務器創建步驟,測試服務器是否正常。
u8 TCP_Create(u16_t port)
{
struct tcp_pcb *pcb=NULL;
pcb=tcp_new(); //創建套接字
if(pcb==NULL)return 1;
if(tcp_bind(pcb,IP_ADDR_ANY,port)!=ERR_OK)return 2; //綁定端口號
pcb=tcp_listen(pcb); //開始監聽
tcp_accept(pcb,TCP_accept);//等待連接
return 0;
}
err_t TCP_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
u8 addr[4];
//tcp_setprio(newpcb, TCP_PRIO_MIN); 設置優先級
printf("有新的客戶端連接!\n");
addr[3]=(newpcb->remote_ip.addr>>24)&0xFF;
addr[2]=(newpcb->remote_ip.addr>>16)&0xFF;
addr[1]=(newpcb->remote_ip.addr>>8)&0xFF;
addr[0]=(newpcb->remote_ip.addr>>0)&0xFF;
printf("ip地址:%d.%d.%d.%d\n",addr[0],addr[1],addr[2],addr[3]);
printf("端口號:%d\n",newpcb->remote_port);
printf("當前隊列剩余字節:%d\n",tcp_sndbuf(newpcb));
tcp_write(newpcb,"1234567890",10,1); //將要發送的數據提交到發送隊列(不會立即發送)
tcp_output(newpcb); //提示系統現在,發送數據
tcp_sent(newpcb,TCP_sent); //發送成功的回調函數
tcp_recv(newpcb,TCP_recv);
return ERR_OK;
}
err_t TCP_sent(void *arg, struct tcp_pcb *tpcb,u16_t len)
{
printf("成功發送:%d字節\n",len);
//tcp_close(tpcb); //關閉客戶端連接
return ERR_OK;
}
u8 rx_buff[1024];
err_t TCP_recv(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err)
{
u32 rx_cnt=0;
struct pbuf *q;
memset(rx_buff,0,sizeof(rx_buff));
if(p==NULL)
{
printf("客戶端已經斷開連接!\n");
}
else
{
for(q=p;q!=NULL;q=q->next)
{
memcpy(rx_buff+rx_cnt,q->payload,q->len);
rx_cnt+=q->len;
}
pbuf_free(p); //釋放PUFF
printf("成功接收:%d字節\n",rx_cnt);
printf("收到的數據=%s\n",rx_buff);
}
return ERR_OK;
}
5.5 創建TCP客戶端示例
u8 TCP_Create(u16_t port)
{
struct tcp_pcb *pcb=NULL;
pcb=tcp_new(); //創建套接字
ip_addr_t ipaddr;
if(pcb==NULL)return 1;
IP4_ADDR(&ipaddr,192,168,31,54); //在ip_addr.h里定義
tcp_connect(pcb,&ipaddr,port,TCP_connected);
return 0;
}
err_t TCP_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
{
u8 addr[4];
//tcp_setprio(newpcb, TCP_PRIO_MIN); 設置優先級
printf("服務器連接成功!\n");
addr[3]=(tpcb->remote_ip.addr>>24)&0xFF;
addr[2]=(tpcb->remote_ip.addr>>16)&0xFF;
addr[1]=(tpcb->remote_ip.addr>>8)&0xFF;
addr[0]=(tpcb->remote_ip.addr>>0)&0xFF;
printf("服務器ip地址:%d.%d.%d.%d\n",addr[0],addr[1],addr[2],addr[3]);
printf("服務器端口號:%d\n",tpcb->remote_port);
printf("當前隊列剩余字節:%d\n",tcp_sndbuf(tpcb));
tcp_write(tpcb,"1234567890",10,1); //將要發送的數據提交到發送隊列(不會立即發送)
tcp_output(tpcb); //提示系統現在,發送數據
tcp_sent(tpcb,TCP_sent); //發送成功的回調函數
tcp_recv(tpcb,TCP_recv);
return ERR_OK;
}
err_t TCP_sent(void *arg, struct tcp_pcb *tpcb,u16_t len)
{
printf("成功發送:%d字節\n",len);
//tcp_close(tpcb); //關閉客戶端連接
return ERR_OK;
}
u8 rx_buff[1024];
err_t TCP_recv(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err)
{
u32 rx_cnt=0;
struct pbuf *q;
memset(rx_buff,0,sizeof(rx_buff));
if(p==NULL)
{
printf("服務器已經斷開連接!\n");
}
else
{
for(q=p;q!=NULL;q=q->next)
{
memcpy(rx_buff+rx_cnt,q->payload,q->len);
rx_cnt+=q->len;
}
printf("成功接收:%d字節\n",rx_cnt);
printf("收到的數據=%s\n",rx_buff);
}
return ERR_OK;
}
-
網卡
+關注
關注
4文章
311瀏覽量
27384 -
STM32
+關注
關注
2270文章
10900瀏覽量
356006 -
LwIP
+關注
關注
2文章
86瀏覽量
27172 -
FSMC
+關注
關注
0文章
55瀏覽量
38151 -
keil5
+關注
關注
6文章
44瀏覽量
20672
發布評論請先 登錄
相關推薦
評論