1. 程式人生 > >STM32開發筆記53:STM32F4+DP83848乙太網通訊指南系列(七):發包流程

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對乙太網上資料的監聽。