使用的MCU型號為STM32F429IGT6,PHY為LAN7820A
目標是通過MCU的ETH給LWIP提供輸入輸出從而實現基本的Ping應答
OK廢話不多說我們直接開始
下載源碼
- LWIP包源碼:lwip源碼
-在這里下載
- ST官方支持的ETH包:ST-ETH支持包
這里下載
創建工程
這里我使用我的STM32外擴RAM工程,若是手里無有外擴內存的板卡也可以直接使用點燈工程
加入ETH支持包
將剛剛下載的ETH支持包里
STM32F4x7_ETH_LwIP_V1.1.1\Libraries\STM32F4x7_ETH_Driver
目錄下有/inc /src 兩個文件夾,分別存放著ETH驅動的源文件和頭文件
把他們對應的加入源碼工程中
\Libraries\STM32F4xx_StdPeriph_Driver
的 /inc 和 /src中
然后將stm32f4x7_eth_conf_temp.h重命名為stm32f4x7_eth_conf.h
在keil工程中加入他們!
修改stm32f4x7_eth_conf.h
直接編譯會報錯,因為沒ETH使用的delay函數,這里直接不使用ETH的delay,直接注釋掉USE_Delay
修改stm32f4x7_eth.c
在這個文件的一開始會發現
搜索這里的宏定義是發現這些描述符和Buffer占用了大量的空間,描述符占用了320byte,因為后面用DMA搬運所以使用片內RAM,而這里的Buffer一共占用了大約38Kbyte,這非常大,所以一般放在外部RAM,這里我使用的片外SRAM是IS42S16400J 擁有8M內存,所以可以放在片外SRAM,所以這里先注釋掉,稍后使用malloc分配內存給它們,如果移植的板卡無片外擴展SRAM則不用管這里,直接放在內部RAM
然后注釋的后面添加指針
ETH_DMADESCTypeDef *DMARxDscrTab;
ETH_DMADESCTypeDef *DMATxDscrTab;
uint8_t *Tx_Buff;
uint8_t *Rx_Buff;
這里需要使用malloc.c和malloc.h
malloc.c
#include "malloc.h"
#include "stdio.h"// //內存池(4字節對齊)
#pragma pack(4)u8 mem1base[MEM1_MAX_SIZE];u8 mem2base[MEM2_MAX_SIZE] __attribute__((at(0xD0000000))); //外部SRAM內存池
#pragma pack()//內存管理表
u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; //內部SRAM內存池MAP
u16 mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((at(0xD0000000+MEM2_MAX_SIZE))); //外部SRAM內存池MAP
//內存管理參數
const u32 memtblsize[SRAMBANK]={MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE}; //內存表大小
const u32 memblksize[SRAMBANK]={MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE}; //內存分塊大小
const u32 memsize[SRAMBANK]={MEM1_MAX_SIZE,MEM2_MAX_SIZE}; //內存總大小//內存管理控制器
struct _m_mallco_dev mallco_dev=
{mymem_init, //內存初始化mem_perused, //內存使用率mem1base,mem2base, //內存池mem1mapbase,mem2mapbase,//內存管理狀態表0,0, //內存管理未就緒
};//復制內存
//*des:目的地址
//*src:源地址
//n:需要復制的內存長度(字節為單位)
void mymemcpy(void *des,void *src,u32 n)
{u8 *xdes = des;u8 *xsrc = src;while(n--) *xdes++ = *xsrc++;
}//設置內存
//*s:內存首地址
//c :要設置的值
//count:需要設置的內存大小(字節為單位)
void mymemset(void*s,u8 c,u32 count)
{u8 *xs = s;while(count--) *xs++=c;
}//內存管理初始化
//memx:所屬內存塊
void mymem_init(u8 memx)
{mymemset(mallco_dev.memmap[memx],0,memtblsize[memx]*2); //內存狀態表清零mymemset(mallco_dev.membase[memx], 0,memsize[memx]); //內存池所有數據清零 mallco_dev.memrdy[memx]=1; //內存管理初始化OK
}//獲取內存使用率
//memx:所屬內存塊
//返回值:使用率(0~100)
u8 mem_perused(u8 memx)
{ u32 used=0; u32 i; for(i=0;i<memtblsize[memx];i++) { if(mallco_dev.memmap[memx][i])used++; } return (used*100)/(memtblsize[memx]);
} //內存分配(內部調用)
//memx:所屬內存塊
//size:要分配的內存大小(字節)
//返回值:0XFFFFFFFF,代表錯誤;其他,內存偏移地址
u32 mymem_malloc(u8 memx,u32 size)
{ signed long offset=0; u16 nmemb; //需要的內存塊數 u16 cmemb=0;//連續空內存塊數u32 i; if(!mallco_dev.memrdy[memx])mallco_dev.init(memx);//未初始化,先執行初始化 if(size==0)return 0XFFFFFFFF;//不需要分配nmemb=size/memblksize[memx]; //獲取需要分配的連續內存塊數if(size%memblksize[memx])nmemb++; for(offset=memtblsize[memx]-1;offset>=0;offset--)//搜索整個內存控制區 { if(!mallco_dev.memmap[memx][offset])cmemb++;//連續空內存塊數增加else cmemb=0; //連續內存塊清零if(cmemb==nmemb) //找到了連續nmemb個空內存塊{for(i=0;i<nmemb;i++) //標注內存塊非空 { mallco_dev.memmap[memx][offset+i]=nmemb; } return (offset*memblksize[memx]);//返回偏移地址 }} return 0XFFFFFFFF;//未找到符合分配條件的內存塊
} //釋放內存(內部調用)
//memx:所屬內存塊
//offset:內存地址偏移
//返回值:0,釋放成功;1,釋放失敗;
u8 mymem_free(u8 memx,u32 offset)
{ int i; if(!mallco_dev.memrdy[memx])//未初始化,先執行初始化{mallco_dev.init(memx); return 1;//未初始化 } if(offset<memsize[memx])//偏移在內存池內. { int index=offset/memblksize[memx]; //偏移所在內存塊號碼 int nmemb=mallco_dev.memmap[memx][index]; //內存塊數量for(i=0;i<nmemb;i++) //內存塊清零{ mallco_dev.memmap[memx][index+i]=0; } return 0; }else return 2;//偏移超區了.
} //釋放內存(外部調用)
//memx:所屬內存塊
//ptr:內存首地址
void myfree(u8 memx,void *ptr)
{ u32 offset; printf("myfree\r\n"); if(ptr==NULL)return;//地址為0. offset=(u32)ptr-(u32)mallco_dev.membase[memx]; mymem_free(memx,offset);//釋放內存
} //分配內存(外部調用)
//memx:所屬內存塊
//size:內存大小(字節)
//返回值:分配到的內存首地址.
void *mymalloc(u8 memx,u32 size)
{ u32 offset; offset=mymem_malloc(memx,size); if(offset==0XFFFFFFFF)return NULL; else return (void*)((u32)mallco_dev.membase[memx]+offset);
} //重新分配內存(外部調用)
//memx:所屬內存塊
//*ptr:舊內存首地址
//size:要分配的內存大小(字節)
//返回值:新分配到的內存首地址.
void *myrealloc(u8 memx,void *ptr,u32 size)
{ u32 offset; offset=mymem_malloc(memx,size); if(offset==0XFFFFFFFF)return NULL; else { mymemcpy((void*)((u32)mallco_dev.membase[memx]+offset),ptr,size); //拷貝舊內存內容到新內存 myfree(memx,ptr); //釋放舊內存return (void*)((u32)mallco_dev.membase[memx]+offset); //返回新內存首地址}
}
malloc.h
#ifndef _MALLOC_H
#define _MALLOC_H
#include "stm32f4xx.h"#ifndef NULL
#define NULL 0
#endif//定義三個內存池
#define SRAMIN 0 //內部內存池
#define SRAMEX 1 //外部內存池#define SRAMBANK 2 //定義支持的SRAM塊數//mem1內存參數設定,mem1完全處于內部SRAM里面
#define MEM1_BLOCK_SIZE 32 //內存塊大小為32字節
#define MEM1_MAX_SIZE 30*1024 //最大管理內存 10k
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE //內存表大小//mem2內存參數設定,mem2處于外部SRAM里面
#define MEM2_BLOCK_SIZE 32 //內存塊大小為32字節
#define MEM2_MAX_SIZE 500*1024 //最大管理內存 500k
#define MEM2_ALLOC_TABLE_SIZE MEM2_MAX_SIZE/MEM2_BLOCK_SIZE //內存表大小//內存管理控制器
struct _m_mallco_dev
{void (*init)(u8); //初始化u8 (*perused)(u8); //內存使用率u8 *membase[SRAMBANK]; //內存池,管理SRAMBANK個區域的內存u16 *memmap[SRAMBANK]; //內存狀態表u8 memrdy[SRAMBANK]; //內存管理是否就緒
};
extern struct _m_mallco_dev mallco_dev; //在malloc.c里面定義void mymemset(void *s,u8 c,u32 count); //設置內存
void mymemcpy(void *des,void *src,u32 n);//復制內存
void mymem_init(u8 memx); //內存管理初始化函數(外/內部調用)
u32 mymem_malloc(u8 memx,u32 size); //內存分配(內部調用)
u8 mymem_free(u8 memx,u32 offset); //內存釋放(內部調用)
u8 mem_perused(u8 memx); //獲得內存使用率(外/內部調用)
//用戶調用函數
void myfree(u8 memx,void *ptr); //內存釋放(外部調用)
void *mymalloc(u8 memx,u32 size); //內存分配(外部調用)
void *myrealloc(u8 memx,void *ptr,u32 size);//重新分配內存(外部調用)
#endif
然后在main函數中使用
修改stm32f4x7_eth.h
在 #include “stm32f4x7_eth.h” 的最后添加 extern 使得外部文件可以使用
至此 ETH的DMA描述符,緩存,接收幀內存 都可以使用了
加入LWIP包
在工程源目錄中加入LWIP文件夾, 并且把lwip包的文件全部復制到源碼目錄的LWIP文件夾里
添加lwip源碼
在keil中創建相對應的Group并且在keil中加入這些路徑
- lwip/core
需要單獨加入ipv4的內容,不加ipv6的內容
lwip/netif 加入這些中的ethernet.c文件,注意只加ethernet.c
- lwip.api
加入這些
- lwip/arch
這個文件夾是單獨創建在User中的arch文件夾,這里存放著lwip與用戶的接口
在我的文件夾中的User/arch 文件夾中,直接復制過去
添加lwip頭文件路徑
在keil工程中加入頭文件路徑
添加lwip時鐘更新
在這里我使用的是我10ms的定時器驅動的一個任務調度器,沒軟件定時器的可以直接放入10ms定時器中.
把上圖的函數放入10ms任務中,其中lwip_localtime+=10表示的是10ms更新的時基。
添加以太網底層驅動
以太網初始化
初始化GPIO
初始化GPIO并且選擇RMII接口的SYSCFG
RCC->AHB1ENR |= RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOB|RCC_AHB1Periph_GPIOC|RCC_AHB1Periph_GPIOG;RCC->APB2ENR |=RCC_APB2Periph_SYSCFG;//使能SYSCFG時鐘SYSCFG->PMC=(uint32_t)(0x800000);//MAC和PHY之間使用RMII接口GPIOA->MODER|=(uint32_t)(0x8028); //PA1 PA2 PA7GPIOB->MODER|=(uint32_t)(0x800000); //PB11GPIOC->MODER|=(uint32_t)(0xA08); //PC1 PC4 PC5GPIOG->MODER|=(uint32_t)(0x28000000); //PG13 PG14GPIOA->AFR[0]|=(uint32_t)(0xB0000BB0);//PA1 PA2 PA7GPIOB->AFR[1]|=(uint32_t)(0xB000); //PB11GPIOC->AFR[0]|=(uint32_t)(0xBB00B0); //PC1 PC4 PC5GPIOG->AFR[1]|=(uint32_t)(0xBB00000); //PG13 PG14GPIOA->OSPEEDR|=(uint32_t)(0xC03C); //PA1 PA2 PA7GPIOB->OSPEEDR|=(uint32_t)(0xC00000); //PB11GPIOC->OSPEEDR|=(uint32_t)(0xF0C); //PC1 PC4 PC5GPIOG->OSPEEDR|=(uint32_t)(0x3C000000); //PG13 PG14
初始化以太網MAC_DMA
//初始化ETH MAC層及DMA配置
//返回值:ETH_ERROR,發送失敗(0)
// ETH_SUCCESS,發送成功(1)
u8 ETH_MAC_DMA_Config(void)
{u8 rval;ETH_InitTypeDef ETH_InitStructure; //使能以太網時鐘RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC | RCC_AHB1Periph_ETH_MAC_Tx |RCC_AHB1Periph_ETH_MAC_Rx, ENABLE);ETH_DeInit(); //AHB總線重啟以太網ETH_SoftwareReset(); //軟件重啟網絡while (ETH_GetSoftwareResetStatus() == SET);//等待軟件重啟網絡完成 ETH_StructInit(Ð_InitStructure); //初始化網絡為默認值 ///網絡MAC參數設置 ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Enable; //開啟網絡自適應功能ETH_InitStructure.ETH_LoopbackMode = ETH_LoopbackMode_Disable; //關閉反饋ETH_InitStructure.ETH_RetryTransmission = ETH_RetryTransmission_Disable; //關閉重傳功能ETH_InitStructure.ETH_AutomaticPadCRCStrip = ETH_AutomaticPadCRCStrip_Disable; //關閉自動去除PDA/CRC功能 ETH_InitStructure.ETH_ReceiveAll = ETH_ReceiveAll_Disable; //關閉接收所有的幀ETH_InitStructure.ETH_BroadcastFramesReception = ETH_BroadcastFramesReception_Enable;//允許接收所有廣播幀ETH_InitStructure.ETH_PromiscuousMode = ETH_PromiscuousMode_Disable; //關閉混合模式的地址過濾 ETH_InitStructure.ETH_MulticastFramesFilter = ETH_MulticastFramesFilter_Perfect;//對于組播地址使用完美地址過濾 ETH_InitStructure.ETH_UnicastFramesFilter = ETH_UnicastFramesFilter_Perfect; //對單播地址使用完美地址過濾 ETH_InitStructure.ETH_ChecksumOffload = ETH_ChecksumOffload_Enable; //開啟ipv4和TCP/UDP/ICMP的幀校驗和卸載 //當我們使用幀校驗和卸載功能的時候,一定要使能存儲轉發模式,存儲轉發模式中要保證整個幀存儲在FIFO中,//這樣MAC能插入/識別出幀校驗值,當真校驗正確的時候DMA就可以處理幀,否則就丟棄掉該幀ETH_InitStructure.ETH_DropTCPIPChecksumErrorFrame = ETH_DropTCPIPChecksumErrorFrame_Enable; //開啟丟棄TCP/IP錯誤幀ETH_InitStructure.ETH_ReceiveStoreForward = ETH_ReceiveStoreForward_Enable; //開啟接收數據的存儲轉發模式 ETH_InitStructure.ETH_TransmitStoreForward = ETH_TransmitStoreForward_Enable; //開啟發送數據的存儲轉發模式 ETH_InitStructure.ETH_ForwardErrorFrames = ETH_ForwardErrorFrames_Disable; //禁止轉發錯誤幀 ETH_InitStructure.ETH_ForwardUndersizedGoodFrames = ETH_ForwardUndersizedGoodFrames_Disable; //不轉發過小的好幀 ETH_InitStructure.ETH_SecondFrameOperate = ETH_SecondFrameOperate_Enable; //打開處理第二幀功能ETH_InitStructure.ETH_AddressAlignedBeats = ETH_AddressAlignedBeats_Enable; //開啟DMA傳輸的地址對齊功能ETH_InitStructure.ETH_FixedBurst = ETH_FixedBurst_Enable; //開啟固定突發功能 ETH_InitStructure.ETH_RxDMABurstLength = ETH_RxDMABurstLength_32Beat; //DMA發送的最大突發長度為32個節拍 ETH_InitStructure.ETH_TxDMABurstLength = ETH_TxDMABurstLength_32Beat; //DMA接收的最大突發長度為32個節拍ETH_InitStructure.ETH_DMAArbitration = ETH_DMAArbitration_RoundRobin_RxTx_1_1;rval=ETH_Init(Ð_InitStructure,LAN8720_PHY_ADDRESS); //配置ETHif(rval==ETH_SUCCESS)//配置成功{ETH_DMAITConfig(ETH_DMA_IT_NIS|ETH_DMA_IT_R,ENABLE); //使能以太網接收中斷 NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = ETH_IRQn; //以太網中斷NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //中斷寄存器組2最高優先級NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure);ETH_MACAddressConfig(ETH_MAC_Address0,lwipdev.mac);printf("ETH Init Sucess\r\n");}return rval;
}
- 這里 設置MAC地址 很重要,否則以太網無法接收自己的IP地址所對應的包!
ETH_MACAddressConfig(ETH_MAC_Address0,lwipdev.mac);
的是lwipdev.mac
這里的lwipdev.mac在lwip_comm.h中,在main函數中調用lwip_comm_init() 用來初始化lwip的底層設備和lwip內核,MAC地址在這個函數的lwip_comm_default_ip_set(&lwipdev); 中修改。
2. 這里一定要 開啟ETH的DMA中斷并且使能ETH_IRQn !
設置以太網DMA描述符 & DMA緩存的對應關系
rval=ETH_MAC_DMA_Config();if(rval==ETH_SUCCESS){printf("ETH init OK ");}else{printf("ETH init Failed ");}ETH_DMATxDescChainInit(DMATxDscrTab,Tx_Buff,ETH_TXBUFNB);//將接收描述符和接收緩存區關聯起來 串成鏈式結構 初始化了發送追蹤描述符ETH_DMARxDescChainInit(DMARxDscrTab,Rx_Buff,ETH_RXBUFNB);//將發送描述符和發送緩存區關聯起來 串成鏈表 初始化了接收追蹤描述符for(uint8_t i=0; i<ETH_TXBUFNB; i++){ETH_DMATxDescChecksumInsertionConfig(&DMATxDscrTab[i], ETH_DMATxDesc_ChecksumTCPUDPICMPFull);ETH_DMATxDescCRCCmd(&DMATxDscrTab[i],ENABLE);} ETH_Start();
后面
for(uint8_t i=0; i<ETH_TXBUFNB; i++)
{
ETH_DMATxDescChecksumInsertionConfig(&DMATxDscrTab[i],ETH_DMATxDesc_ChecksumTCPUDPICMPFull);
ETH_DMATxDescCRCCmd(&DMATxDscrTab[i],ENABLE);
}
設置了以太網TX發送描述緩存幀的和校驗,這步是我在前面測試的時候發現的問題若沒這段程序,以太網只可以發送ARP請求,TCP/UDP/ICMP等都發送出去的幀都是0和校驗,形成錯誤幀,所以一定要開啟TX緩存的和校驗!
另一個需要注意的是一定要開啟ETH!
ETH_Start();
在這里我貼出的這段程序,可以獲得PHY芯片和外部協商的結果,可以驗證設置的以太網是是否和外部PHY芯片通訊上。
//得到8720的速度模式
//返回值:
//001:10M半雙工
//101:10M全雙工
//010:100M半雙工
//110:100M全雙工
//其他:錯誤.
void LAN8720_Get_Speed(void)
{u8 speed;speed=((ETH_ReadPHYRegister(PHY_BCR,PHY_SR)&0x1C)>>2); //從LAN8720的31號寄存器中讀取網絡速度和雙工模式switch(speed){case 1: printf("10M半雙工\r\n"); break;case 5: printf("10M全雙工\r\n"); break;case 2: printf("100M半雙工\r\n"); break;case 6: printf("100M全雙工\r\n"); break;default: printf("ETH 初始化失敗 %d\r\n",speed); break;}
}
以太網接收中斷函數
在前初始化了以太網中斷,這里編寫以太網中斷函數
//以太網中斷服務函數
void ETH_IRQHandler(void){if(ETH_CheckFrameReceived()){ETH_flag=1;}ETH_DMAClearITPendingBit(ETH_DMA_IT_R); //清除DMA中斷標志位ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS); //清除DMA接收中斷標志位
}
LWIP初始化
在LWIP底層硬件初始化
在這個函數中加入ETH初始化
static void low_level_init(struct netif *netif)
設置LWIP底層輸入/ 輸出函數
底層輸入函數:
static struct pbuf * low_level_input(struct netif *netif)
底層輸出函數:
static err_t low_level_output(struct netif *netif, struct pbuf *p)
修改這兩個函數即可使用LWIP適配以太網(從這里看LWIP其實可以通過任何通訊方式運行,不只局限于ETH,只要是輸入的是以太網幀格式的數據就可以,但是需要修改的部分較多,需要重新定義DMA描述符、追蹤描述符、緩存的數據方向)。
LWIP輸入處理
在while(1)中檢測ETH中斷函數的flg并且做出反應
if(ETH_flag){ETH_flag = 0;ethernetif_input(&lwip_netif);//調用網卡接收函數}
有兩種ETH輸入數據分解的方法, ETH中斷是其中一種,但是實測發現了一個問題,當我把我的以太網線插入開發板<—>電腦 之間后,因為電腦一邊從WIFI中獲取數據一邊會從以太網中嘗試獲取數據,于是會發生ETH超級頻繁的進入ETH中斷導致ETH這次的任務還沒有處理完,下一次的任務標志位又被置為了導致出現了原子操作,也就是會出現數據量大的時候可能漏掉網絡請求的情況,如圖:
于是發現可以不使用ETH中斷,禁用掉ETH中斷,改用頻率更高的while(1)大循環中循環檢測,如圖
在這里每一次大循環都會檢測ETH輸入幀是否收到,大大提高了實時響應性,ping命令不會出現遺漏的現象。
測試程序
ETH是否正常接收可以查看debug,這里貼出兩個測試程序用來測試ETH是否可以正常發送
void ethernet_sendtest1(void){uint8_t frame_data[] ={/* 以太網幀格式 */0x50,0xFA,0x84,0x15,0x3C,0x3C, /* 遠端MAC */0x0,0x80,0xE1,0x0,0x0,0x0, /* 本地MAC */0x8,0x0, /* ip類型 */0x45,0x0,0x0,0x26/*l*/,0x0,0x0,0x0,0x0,0xFF,0x11,0x0,0x0, /* UDP報頭 */0xC0,0xA8,0x2,0x8, /* 本地IP */0xC0,0xA8,0x2,0xC2, /* 遠端IP */0x22,0xB0, /* 本地端口 */0x22,0xB1, /* 遠端端口 */0x0,0x12, /* UDP長度 */0x0,0x0, /* UDP校驗和 */0x68,0x65,0x6C,0x6C,0x6F,0x20,0x7A,0x6F,0x72,0x62 /* 數據 */};struct pbuf *p;/* 分配緩沖區空間 */p = pbuf_alloc(PBUF_TRANSPORT, 0x26 + 14, PBUF_POOL);if (p != NULL){/* 填充緩沖區數據 */pbuf_take(p, frame_data, 0x26 + 14);/* 把數據直接通過底層發送 */lwip_netif.linkoutput(&lwip_netif, p);/* 釋放緩沖區空間 */pbuf_free(p);}}void ethernet_sendtest2(void){uint8_t dstAddr[6] = {0x50,0xFA,0x84,0x15,0x3C,0x3C}; /* 遠端MAC */uint8_t frame_data[] ={/* UDP幀格式 */0x45,0x0,0x0,0x26/*l*/,0x0,0x0,0x0,0x0,0xFF,0x11,0x0,0x0, /* UDP報頭 */192,168,1,68, /* 本地IP */192,168,1,11, /* 遠端IP */0x22,0xB0, /* 本地端口 */0x22,0xB1, /* 遠端端口 */0x0,0x12, /* UDP長度 */0x0,0x0, /* UDP校驗和 */1,2,3,4,5,6,6,6,6,6 /* 數據 */};struct pbuf *p;/* 分配緩沖區空間 */p = pbuf_alloc(PBUF_TRANSPORT, 0x26, PBUF_POOL);if (p != NULL){/* 填充緩沖區數據 */pbuf_take(p, frame_data, 0x26);/* 把數據進行以太網封裝,再通過底層發送 */ethernet_output(&lwip_netif, p, (const struct eth_addr*)lwip_netif.hwaddr,(const struct eth_addr*)dstAddr, ETHTYPE_IP);/* 釋放緩沖區空間 */pbuf_free(p);}}
至此,ETH已經具備了運行LWIP并且可以PING了。
這里我修改IP地址和MAC,防止電腦內部MAC / ARP表影響測試結果
ping了好多次發現都可以,實驗成功
總結一下,這里有很重要但是也很有可能會疏漏的幾點:
- 設置為MCU設置MAC地址,有專門的一個函數用來為MCU設置自己ETH的MAC地址,否則無法接收到自己IP地址包。
- 配置DMA的描述符和緩存的關系,這也是有專門的函數用來初始化對應的關系,否則描述符無法和緩存對應起來,接收到的是亂碼或者直接進入硬件錯誤中斷
- 如果是外部SRAM的板子,ETH的TX/RX的30K緩存可以放到外部SRAM中,而占用300字節的設備描述符不可以放入外部SRAM,因為這些描述符是直接與內部ETH的DMA交互的,將這些描述符的內存指向外部RAM會導致讀取不到描述符出現直接無法讀取的現象。
- 開啟每個TX緩存的和校驗,有專門的函數,如果不設置TX緩存和校驗會導致TX發送的所有數據的和校驗都是0x00 在抓包軟件中出現的是錯誤。
- 如果使用片內RAM存儲緩存,可以調節ETH_TXBUFNB 或者 ETH_RXBUFNB 調節有多少個緩存區從而調節緩存區大小
- 經過我的實測,將RX/TX緩存BUFFER存放在外部SRAM中會有幾率ping失敗或者超時幾個,存放在內部RAM會一直可以使用,懷疑是SRAM的速度影響了DMA搜索地址傳輸的速度,緩存存放在內部RAM效率高,失誤率低,(是否有可能是cache的作用?)
處理以太網數據幀的三種方式對比
續:在寫完這篇博客之后我再次重新理解了以太網響應數據的方式,在以太網使用中斷檢測可用的幀數據&大循環處理 / 直接在大循環中檢測可用的幀&處理 / 以太網中斷接收到置為標志位&大循環中處理這三種方法我都嘗試了一下,
中斷檢測可使用的幀 & 大循環處理:
這里使用的是在中斷中
//以太網中斷服務函數
void ETH_IRQHandler(void){
if(ETH_CheckFrameReceived()){
ETH_flag=1;
}
ETH_DMAClearITPendingBit(ETH_DMA_IT_R); //清除DMA中斷標志位
ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS); //清除DMA接收中斷標志位
}
大循環中
if(ETH_flag==1){
ETH_flag=0;
if(ETH_CheckFrameReceived()){
ethernetif_input(&lwip_netif);//調用網卡接收函數
}
}
這樣實際測試發現因為在ETH_CheckFrameReceived( ) 這個函數中有這樣一段
/* check if last segment */
if(((DMARxDescToGet->Status & ETH_DMARxDesc_OWN) == (uint32_t)RESET) &&
((DMARxDescToGet->Status & ETH_DMARxDesc_LS) != (uint32_t)RESET))
{
DMA_RX_FRAME_infos->Seg_Count++;
if (DMA_RX_FRAME_infos->Seg_Count == 1)
{
DMA_RX_FRAME_infos->FS_Rx_Desc = DMARxDescToGet;
}
DMA_RX_FRAME_infos->LS_Rx_Desc = DMARxDescToGet;
return 1;
}
這里可以看到它會不斷更新是否是最新的緩存區域,如果不是,則++緩存到另一個,但是這樣當中斷正在檢測frame的時候又發生了中斷,會直接導致frame檢測混亂,count++了之后又++,正在處理上一個事件的時候又被后半部分指到了下一個事件的數據,所以 ETH_CheckFrameReceived( ) 這個函數不可重入!!,實際測發現這樣有概率成功ping通幾個包,大部分因為網絡頻繁進中斷導致了無法ping通,那么在中斷檢測幀在大循環處理這個方法PASS
直接在大循環里檢測 & 處理
例如:
if(ETH_CheckFrameReceived()){
ethernetif_input(&lwip_netif);//調用網卡接收函數
}
直接在while(1)中不斷檢測,這樣的好處是可以一直檢測,有任何幀被發現都會處理,缺點是這樣會占用大量MCU資源,當任務多了之后會發現檢測不是很及時,并且會拖累其他任務的響應,直接導致系統響應慢。
中斷置位標志位累加 & 大循環處理
這里我使用的是在中斷中給需要處理的事件++
//以太網中斷服務函數
void ETH_IRQHandler(void){
if(ETH_CheckFrameReceived()){
ETH_flag++;
}
ETH_DMAClearITPendingBit(ETH_DMA_IT_R); //清除DMA中斷標志位
ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS); //清除DMA接收中斷標志位
}
這樣相當于記錄了有多少個事件應該被處理
在大循環中處理
if(ETH_flag){
ETH_flag–;
ethernetif_input(&lwip_netif);//調用網卡接收函數
printf(“ETH_flag=%d\r\n”,ETH_flag);
}
這樣既不會發生frame檢測重入,又可以即使處理所有緩存中的事件!!
實測效果如下:
在debug界面,最多發生一次剩余緩存未處理,并且可以看到后面已經即使處理了
在ping響應中,往返小于1ms
之前方法2全部在while中處理的時候的時間是2-3ms:
所以方法3無論是響應速度或者是處理數據的數量來說都是比較合理的,如果又更好的方法歡迎私信我!
源碼獲取
文件鏈接:
通過網盤分享的文件:CSDN_ETH.rar
鏈接: https://pan.baidu.com/s/15UMS1rLIsaaPfxGsWYXK9Q?pwd=kg2m 提取碼: kg2m