1. 程式人生 > >STM32開發筆記55:STM32F4+DP83848乙太網通訊指南系列(九):自己寫一個ARP協議

STM32開發筆記55:STM32F4+DP83848乙太網通訊指南系列(九):自己寫一個ARP協議

本章為系列指南的第九章,終結篇,本章主要來分析一下完整的ARP協議,並在STM32F4中實現一個精簡的ARP協議響應流程。

ARP協議的本質是使區域網內的其他主機能夠知道我在哪兒,比如在區域網上有人衝著所有人喊了一句「IP為XXXX的傢伙,你在哪兒」,我一聽,XXXX不是我的IP嗎,我得回答他啊,於是我衝著所有人(也可以是單獨的這個人)喊一句「我在這兒呢,我的MAC是YYYY」,這樣區域網內所有使用者,包括交換機就知道IP為XXXX的傢伙MAC地址是YYYY,並且交換機知道了我連在它的第N個埠上,以後有人要通過交換機向IP為XXXX的我發信息,交換機就把資料包發到N port上去了。沒有上面這套流程,區域網上就沒人知道你是誰,你的IP多少,你的MAC多少,你連在交換機大佬的第幾個埠上,自然你也收不到任何非廣播包資料了。

ARP請求

如果我要向一個之前沒有任何聯絡的主機主動發一個數據包,一般情況下都會觸發一個ARP詢問,比如我們上一章《STM32F4+DP83848乙太網通訊指南第八章:收包流程》中,最後的實驗,我開啟CMD命令列,輸入了一個指令ping 192.168.1.201,這時候PC的底層裝置(一般是網絡卡)首先判斷一下自己的快取中有沒有192.168.1.201這個IP的快取資訊,如果沒有通訊記錄或者時間過長失效了,就得在網路上廣播一下,找找看當前有沒有誰在用192.168.1.201這個IP地址,包內容如下:

ff ff ff ff ff ff 00 0e c6 d4 1d d4 08 06 00 01
08 00 06 04 00 01 00 0e c6 d4 1d d4 c0 a8 01 c8
00 00 00 00 00 00 c0 a8 01 C9

WireShark分析以上報文的解析如下圖所示:

ARP響應

目標主機的MAC地址為FF FF FF FF FF FF時,交換機會將此包廣播到所有埠,這時候所有節點的網絡卡上都能收到這個廣播包,如果某一節點的IP地址為192.168.1.201則需要對此包進行響應,判斷依據為上述包的以下特徵:

第一行倒數第4,第3位元組:0806,代表的是ARP協議

第二行第6位元組:01,代表的是ARP請求

第三行最後4位元組:c0 a8 01 C9,代表的是192.168.1.1,跟自己暫存器中的值匹配

響應資料包為以下內容:

00 0e c6 d4 1d d4 00 11 0e 0b 03 8a 08 06 00 01
08 00 06 04 00 02 00 11 0e 0b 03 8a c0 a8 01 c9
00 0e c6 d4 1d d4 c0 a8 01 c8 00 00 00 00 00 00
00 00 00 00 00 00 00 00 20 20 20 20

上述中,以下資料是動態變化的:

第一行前6個位元組:00 0e c6 d4 1d d4,根據請求包中的第7-12六個位元組決定,表示目標MAC地址

第一行第7-12六個位元組:00 11 0e 0b 03 8a,自由設定的自己的MAC地址

第二行第7-12六個位元組:00 11 0e 0b 03 8a,自由設定的自己的MAC地址

第二行倒數4個位元組:c0 a8 01 c9,自己的IP地址,也就是請求包中所呼喚的IP地址,192.168.1.201

第三行前6個位元組:00 0e c6 d4 1d d4,根據請求包中的第7-12六個位元組決定,表示目標MAC地址

第三行第7-10四個位元組:c0 a8 01 c8,根據請求包第二行倒數四個位元組決定,表示目標IP地址

這些資訊都可以用WireShark分析出來,WireShark對每一個位元組的含義都有明確的解釋。

編碼實現

有了特徵匹配及填充規則,配合我們之前的STM32發包和收包DEMO,我們就可以程式設計實現ARP資料包的響應了。

#define LEN_ARP 42

/* 以下為業務邏輯需要用到的全域性變數 */
u8 IPAddr[4];

vu8  SystemStatus; /* 系統狀態,bit0:是否需要傳送被動響應資料包,bit1:是否打開了PNIO開關,bit2:是否需要傳送PNControl request */
vu16 sendLen;      /* 傳送包長度 */
u8 sendBuffer[1024];   /* 傳送包BUFFER */
u8 arp_answer[LEN_ARP]={
    0,0,0,0,0,0, /* ArpAskerMac */ 0,0,0,0,0,0, /* myMac */ 0x08, 0x06, 0x00, 0x01,
    0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0,0,0,0,0,0, /* myMac */ 0,0,0,0, /* IP address */
    0,0,0,0,0,0, /* ArpAskerMac */ 0,0,0,0/* ArpAskerIP */
};

int main() {
    SystemStatus = SS_NOTHING;
    sendLen = 0;    

    /* 預設呼叫SystemInit,系統時鐘168MHz */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);        //4位搶佔,0位響應

    DelayInit(168);        //初始化168MHz

    NVIC_SetPriority (SysTick_IRQn, 0);        //系統滴答定時器優先順序最高  

    DP83848Init(MyMacAddr);        //初始化DP83848

    while(1) {
        if (0x01 == (SystemStatus & 0x01)){
            /* 如果需要傳送被動網絡卡響應 */
            DP83848Send(sendBuffer, sendLen);            
            sendLen = 0;
            SystemStatus = SystemStatus & 0xFE; /* bit0置為0 */
        }
    }
}

void Pkt_Handle(void) {
    FrameTypeDef frame;
    __IO ETH_DMADESCTypeDef *DMARxNextDesc;

    /* get received frame */
    frame = ETH_Get_Received_Frame();
    /* Obtain the size of the packet and put it into the "len" variable. */
    receiveLen = frame.length;
    receiveBuffer = (u8 *)frame.buffer;

    if (Match(receiveBuffer, ArpProtocol, 12, 2) && Match(receiveBuffer, IPAddr, 38, 4)){
        /* ARP */
        //printf("ARP test received\n");
        memcpy(sendBuffer, arp_answer, LEN_ARP);

        for (int i = 0; i < 6; i++) {
            /* ArpAskerMac */
            sendBuffer[i] = receiveBuffer[6 + i];    
            sendBuffer[32 + i] = receiveBuffer[6 + i];
        }
        for (int i = 0; i < 4; i++) {
            /* ArpAskerIP */
            sendBuffer[38 + i] = receiveBuffer[28 + i];    
        }

        sendLen = LEN_ARP;
        SystemStatus = (SystemStatus | 0x01); /* 開啟網絡卡被動資料傳送開關 */
    }

    /* Check if frame with multiple DMA buffer segments */
    if (DMA_RX_FRAME_infos->Seg_Count > 1) {
        DMARxNextDesc = DMA_RX_FRAME_infos->FS_Rx_Desc;
    } else {
        DMARxNextDesc = frame.descriptor;
    }

    /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
    for (i = 0; i < DMA_RX_FRAME_infos->Seg_Count; i++) {
        DMARxNextDesc->Status = ETH_DMARxDesc_OWN;
        DMARxNextDesc = (ETH_DMADESCTypeDef *)(DMARxNextDesc->Buffer2NextDescAddr);
    }

    /* Clear Segment_Count */
    DMA_RX_FRAME_infos->Seg_Count = 0;

    /* When Rx Buffer unavailable flag is set: clear it and resume reception */
    if ((ETH->DMASR & ETH_DMASR_RBUS) != (u32)RESET) {
        /* Clear RBUS ETHERNET DMA flag */
        ETH->DMASR = ETH_DMASR_RBUS;
        /* Resume DMA reception */
        ETH->DMARPDR = 0;
    }
}

編碼的總體思想就是中斷中快進快出,用變數來標記狀態,主迴圈中不斷判斷狀態並復位狀態。

使用這種方式,可以繼續擴充套件ICMP協議,實現PING指令,以及更多按需裁剪的TCP/IP協議,當然擴充套件的協議越多,這種純粹的if判斷也會越多,以後會細分各個層,在層次中繼續if判斷,當到了頂層應用層之後,就只管TCP/UDP協議了,內容一般都是應用協議自己規劃的比如HTTP,FTP這些協議,只需要把上層協議的資料結構填充到下層協議的內容區域就行了。當遇到資料包比較龐大的時候,還需要分包傳輸,暫時我們沒有用到,LWIP中有相應的實現,有需求的話可以去研究。此外,我們實驗中的程式碼都是基於陣列進行資料裝箱拆箱操作的,而網路包的協議是按層次來的,使用陣列不斷的進行資料的填充是非常消耗效能的,因此在遇到資料量比較大,需要實現的協議比較多時,仍然建議按照LWIP的思路使用收尾相接的鏈式結構進行拆裝箱。

系列總結

到此我們這個系列就算告一段落了,後面的工業乙太網的協議的分析和實現我不方便做公開的教程,也是自己一步一步摸索過來的,目前自己在設計PCB板,後期這個工程需要制板貼片並做成最終的工業產品。

我們來回顧總結一下這個系列教程,首先我們對STM32F407的時鐘、中斷等相關知識做了一個梳理,接著我們認識了STM32中的MAC,以及跟MAC搭配的PHY,同時還對DMA技術做了一個粗略的瞭解。試著初始化相關的GPIO,使能了MAC,DMA,PHY,這樣一個DP83848Init()函式就搞定了;再後來我們又完成DP83848Send()函式,能夠發包了,接著配置了乙太網中斷,能夠在Pkt_Handle()函式中進行乙太網收包了,以上函式的編寫,我們都是參考的ethernetif.c檔案,它的幾個底層函式low_level_init、low_level_output、low_level_input給我們提供了重要的線索。最後對TCP/IP協議棧中的ARP協議進行了分析,並運用之前的全部知識,進行了ARP響應的編碼實現,並對以後其他協議的擴充套件實現提出了思路和優化建議。

好了,期待今後有更多的系列教程跟大家分享。再見!