STM32開發筆記53:STM32F4+DP83848乙太網通訊指南系列(七):發包流程
本章為系列指南的第七章,講述如何在之前的基礎上,編寫程式在STM32上傳送一個網路包,並使用WireShark進行驗證。
先回顧一下之前的章節我們做好的準備工作,在《STM32F4+DP83848乙太網通訊指南第五章:MAC+DMA配置》結束時我們封裝了一個DP83848的初始化函式,該函式完成了PHY的配置,MAC層的配置,DMA的配置,並且啟用了乙太網中斷,函式命名為DP83848Init(),那麼今天,我們要做的主要任務就是編寫一個類似的DP83848Send(u8* data, u16 length)函式。
可以在本章的一開始跟大家劇透一個好訊息,有了《STM32F4+DP83848乙太網通訊指南第四章:PHY配置》 和 《STM32F4+DP83848乙太網通訊指南第五章:MAC+DMA配置》 的基礎,我們本章最終實現的DP83848Send(u8* data, u16 length)函式,只有兩行程式碼,非常非常簡單。這兩行程式碼我暫時先不貼出來,我們來順著原來的思路,根據相關文件和官方示例程式碼,順藤摸瓜,一步一步深入瞭解乙太網發包的流程,最終理解體系結構後,也就水到渠成能夠寫出來了。
在 《STM32F4+DP83848乙太網通訊指南第五章:MAC+DMA配置》 最後一部分提到在LWIP官方樣例中,路徑為STM32F4x7_ETH_LwIP_V1.1.1\Utilities\Third_Party\lwip-1.4.1\port\STM32F4x7\Standalone\ethernetif.c的檔案中,第76行有個low_level_init()函式,該函式呼叫ETH庫函式對MAC底層及DMA進行了初始化。同樣的,這份檔案的138行,有個名為low_level_output(struct netif *netif, struct pbuf *p)的函式,疑似是向外輸出網路包的函式,下面就對這部分程式碼進行分析,並試著用其中的核心邏輯進行測試。
因為ethernetif.c這份程式碼本身隸屬於LWIP,而我們是不使用LWIP的,所以這份程式碼只能儘量去看懂和借鑑,想要原封不動地使用是不可以的。
我們先完整地貼出這個函式:
/** * This function should do the actual transmission of the packet. The packet is * contained in the pbuf that is passed to the function. This pbuf * might be chained. * * @param netif the lwip network interface structure for this ethernetif * @param p the MAC packet to send (e.g. IP packet including MAC addresses and type) * @return ERR_OK if the packet could be sent * an err_t value if the packet couldn't be sent * * @note Returning ERR_MEM here if a DMA queue of your MAC is full can lead to * strange results. You might consider waiting for space in the DMA queue * to become availale since the stack doesn't retry to send a packet * dropped because of memory failure (except for the TCP timers). */ static err_t low_level_output(struct netif *netif, struct pbuf *p) { err_t errval; struct pbuf *q; u8 *buffer = (u8 *)(DMATxDescToSet->Buffer1Addr); __IO ETH_DMADESCTypeDef *DmaTxDesc; uint16_t framelength = 0; uint32_t bufferoffset = 0; uint32_t byteslefttocopy = 0; uint32_t payloadoffset = 0; DmaTxDesc = DMATxDescToSet; bufferoffset = 0; /* copy frame from pbufs to driver buffers */ for(q = p; q != NULL; q = q->next) { /* Is this buffer available? If not, goto error */ if((DmaTxDesc->Status & ETH_DMATxDesc_OWN) != (u32)RESET) { errval = ERR_BUF; goto error; } /* Get bytes in current lwIP buffer */ byteslefttocopy = q->len; payloadoffset = 0; /* Check if the length of data to copy is bigger than Tx buffer size*/ while( (byteslefttocopy + bufferoffset) > ETH_TX_BUF_SIZE ) { /* Copy data to Tx buffer*/ memcpy( (u8_t *)((u8_t *)buffer + bufferoffset), (u8_t *)((u8_t *)q->payload + payloadoffset), (ETH_TX_BUF_SIZE - bufferoffset) ); /* Point to next descriptor */ DmaTxDesc = (ETH_DMADESCTypeDef *)(DmaTxDesc->Buffer2NextDescAddr); /* Check if the buffer is available */ if((DmaTxDesc->Status & ETH_DMATxDesc_OWN) != (u32)RESET) { errval = ERR_USE; goto error; } buffer = (u8 *)(DmaTxDesc->Buffer1Addr); byteslefttocopy = byteslefttocopy - (ETH_TX_BUF_SIZE - bufferoffset); payloadoffset = payloadoffset + (ETH_TX_BUF_SIZE - bufferoffset); framelength = framelength + (ETH_TX_BUF_SIZE - bufferoffset); bufferoffset = 0; } /* Copy the remaining bytes */ memcpy( (u8_t *)((u8_t *)buffer + bufferoffset), (u8_t *)((u8_t *)q->payload + payloadoffset), byteslefttocopy ); bufferoffset = bufferoffset + byteslefttocopy; framelength = framelength + byteslefttocopy; } /* Note: padding and CRC for transmitted frame are automatically inserted by DMA */ /* Prepare transmit descriptors to give to DMA*/ ETH_Prepare_Transmit_Descriptors(framelength); errval = ERR_OK; error: /* When Transmit Underflow flag is set, clear it and issue a Transmit Poll Demand to resume transmission */ if ((ETH->DMASR & ETH_DMASR_TUS) != (uint32_t)RESET) { /* Clear TUS ETHERNET DMA flag */ ETH->DMASR = ETH_DMASR_TUS; /* Resume DMA transmission*/ ETH->DMATPDR = 0; } return errval; }
這個函式的官方註釋描述的就是用來向外傳送乙太網包的,函式中說要發的包在第二個引數,型別為pbuf結構體指標的引數p中,並且說了p可能是個連結串列,我們看到函式的兩個入參都是結構體引數,這兩個結構體的定義我們不需要管,是LWIP自己封裝的一個結構體。我們去尋跡引數p的用法,在程式碼片段的30行,使用q變數和for迴圈遍歷p,因此我們能夠確定p就是個頭尾相接的pbuf連結串列。繼續觀察遍歷體中的操作邏輯,我們看到整個for迴圈的主要目的就是在嘗試將q->payload中的byte,利用函式memcopy()向buffer變數中堆,並且做了一些長度的校驗,我們繼而去觀察一下buffer變數的定義,第19行的u8 *buffer = (u8 *)(DMATxDescToSet->Buffer1Addr);是一個比較重要的線索,由此我們可以抽絲剝繭出整體的邏輯,應該就是將首尾相接的p遍歷出來,取其中每個元素的payload區域,向DMATxDescToSet->Buffer1Addr中壓。最後,第73行的ETH_Prepare_Transmit_Descriptors(framelength);呼叫了ETH庫中的函式,實現了最終的結局,將網路包發出去,入參的framelength應該就是需要發出去的包長度,包內容應該就是通過DMA技術,將記憶體中的DMATxDescToSet->Buffer1Addr發出去了。
有了以上針對low_level_output()函式的分析,我們來做實驗印證一下,因為我們從零開始構建的專案沒有LWIP,也沒有ethernetif.c,更沒有low_level_output()函式,因此,函式內部的邏輯都需要我們自己手動實現,慢著,不要一看到「手動實現」就頭疼,你以為手動實現就很複雜嗎?不,LWIP把事情搞複雜了,又是pbuf又是連結串列的,還有長度判斷導致的Buffer2NextDescAddr切換(詳見第43-62行一整段,不過不重要),如果我們手動寫這段邏輯,放棄一些異常處理,再放棄那些跟LWIP強相關的結構體,我們整個發包函式只要兩行就行:
void DP83848Send(u8* data, u16 length){
memcpy((u8 *)DMATxDescToSet->Buffer1Addr, data, length);
/* Prepare transmit descriptors to give to DMA*/
ETH_Prepare_Transmit_Descriptors(length);
}
這裡附帶說明一下,並不是LWIP原版程式碼又臭又長,LWIP要做一個TCP/IP全棧協議,還要考慮包長度溢位的眾多問題,我們精簡版的協議很多不需要考慮,因此可以放棄很多繁瑣的操作。
有了上述DP83848Send()函式,下面來做個小程式試驗一下:
int main() {
u8 MyMacAddr[6] = {0x08, 0x00, 0x06, 0x00, 0x00, 0x09};
/* 下面是一段60byte大小的ARP報文,手動構建的 */
u8 mydata[60] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0xa8,
0x02, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8,
0x02, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
u32 clock;
/* 預設呼叫SystemInit,系統時鐘168MHz */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //4位搶佔,0位響應
DP83848Init(MyMacAddr);
while(1){
DP83848Send(mydata, 60);
clock = 42000000; //1s延時,while中每個步進需要4個週期
while(clock--);
}
}
使用Keil編譯,用JLink下載到STM32F407中,給開發板接上網線,用WireShark就可以在網口中觀察到STM32每隔1秒鐘向外傳送ARP報文了,雖然這段報文幾乎沒有任何意義。
我使用WireShark截圖如下:
總結一下,這一章我們完成了一個DP83848Send()發包函式,這個函式可以接受一個位元組buffer,一個位元組buffer的長度,將這個buffer通過乙太網傳送出去,buffer內部的內容全部需要我們手工構建。DP83848Send()函式的設計思路來自於分析LWIP官網示例,主要是ethernetif.c中的程式碼。下一章我們同樣根據這份程式碼,分析收包邏輯,實現STM32對乙太網上資料的監聽。