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

STM32開發筆記54:STM32F4+DP83848乙太網通訊指南系列(八):收包流程

本章為系列指南的第八章,講述如何使用STM32F407晶片配合DP83848進行乙太網資料的收包流程,將監聽到的網路包資料通過UART傳給PC,同時輔以WireShark監聽對比驗證。

關於UART,也就是串列埠通訊的使用,這裡不做贅述,我們這裡預設兩個函式分別為UART6Init()和UART6Send(),實現的功能是串列埠6的初始化和傳送。

乙太網中斷

在《STM32F4+DP83848乙太網通訊指南第五章:MAC+DMA配置》中,我們已經添加了乙太網中斷,其思路就是想讓每次乙太網上有收到包都能觸發中斷,我們可以在中斷中將DMA中的資料包取出來進行分析,然後復位,讓晶片下一次繼續響應中斷。

配置中斷的程式碼非常簡單,跟其他任何中斷都一樣,這裡再複習一次:

void ETH_NVIC_Config(void) {
    NVIC_InitTypeDef   NVIC_InitStructure;

    /* Enable the Ethernet global Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = ETH_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

整個工程的優先順序組別選用的NVIC_PriorityGroup_4,有4位搶佔位,0位響應位,也就是可以分配16個可以互相巢狀的中斷等級。這裡乙太網中斷的主優先順序為1,相當於第二高,前面預留了個優先順序為0的,用來分配給系統計時器,畢竟不能因為乙太網資料的響應影響系統走時。

在UART6Init()串列埠初始化函式中,給串列埠的中斷等級是2,低於乙太網中斷,因為串列埠的波特率是9600,要遠遠低於乙太網速率,如果給串列埠的優先順序過高,會影響乙太網的使用。

配置了中斷後,我們還需要知道中斷的入口函式,這個函式名是固定死的,不能亂寫,我們去找找。

在《STM32F4+DP83848乙太網通訊指南第五章:MAC+DMA配置》我提到,中斷配置程式碼中的ETH_IRQn變數,我們可以在stm32f4xx.h檔案中找到定義,是在一個列舉結構中。那與之對應的中斷入口名稱該怎麼找呢,原來所有的中斷入口的定義,都用匯編入口的方式定義在啟動檔案中,這份啟動檔案我們之前一直沒有關注過,現在開啟看一看,在startup_stm32f40_41xxx.s中148行有其定義,截圖如下。

中斷中的資料處理

配置好了乙太網中斷,也知道了中斷入口函式的名稱,下面我們就來編寫乙太網中斷函式。開啟工程中的stm32f4xx_it.c檔案,一般每一個使用了中斷的STM32工程都會有這麼一個檔案,用來集中管理中斷入口。追加以下程式碼:

/**
  * @brief  This function handles ethernet DMA interrupt request.
  * @param  None
  * @retval None
  */
void ETH_IRQHandler(void)
{
    /* Handles all the received frames */
    /* check if any packet received */
      while(ETH_CheckFrameReceived()){ 
        /* process received ethernet packet */
        Pkt_Handle();
    }
    /* Clear the Eth DMA Rx IT pending bits */
    ETH_DMAClearITPendingBit(ETH_DMA_IT_R);
    ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS);
}

上面的程式碼配上英文的註釋也很好理解,乙太網中斷中首先檢查是否接受到乙太網的資料包,如果是,就呼叫Pkt_Handle()函式進行下一層的分析和處理,最後兩行Clear復位中斷標記,讓下一次中斷能夠產生。那麼問題就集中到Pkt_Handle()函式上來了。kt_Handle()函式是我自己命名的,這個函式的原型取自LWIP中的LWIP_Pkt_Handle(),我們先來觀察一下LWIP中的包分析函式怎麼寫的:在STM32F4x7_ETH_LwIP_V1.1.1/Project/Standalone/udp_echo_client/src/netconf.c中,有如下程式碼:

/**
* @brief  Called when a frame is received
* @param  None
* @retval None
*/
void LwIP_Pkt_Handle(void)
{
  /* Read a received packet from the Ethernet buffers and send it to the lwIP for handling */
  ethernetif_input(&gnetif);
}

可以看到LWIP繼續呼叫了下層的ethernetif_input(),繼續追蹤到我們之前提到的最底層檔案ethernetif.c,是不是有種似曾相識的感覺,這個檔案前幾章我們不止一次遇到過,分別為我們提供了low_level_init、low_level_output多個重要函式,我們現在又一次遇到它了,看上去它這次要為我們的乙太網監聽提供low_level_input了。

果不其然,在ethernetif_input函式中,我們看到了這個預料中的函式呼叫,截圖如下:

我這裡把路徑為STM32F4x7_ETH_LwIP_V1.1.1/Utilities/Third_Party/lwip-1.4.1/port/STM32F4x7/Standalone/ethernetif.c的原版low_level_input()的所有程式碼都貼出來,感興趣的朋友可以仔細研讀:

/**
 * Should allocate a pbuf and transfer the bytes of the incoming
 * packet from the interface into the pbuf.
 *
 * @param netif the lwip network interface structure for this ethernetif
 * @return a pbuf filled with the received packet (including MAC header)
 *         NULL on memory error
 */
static struct pbuf * low_level_input(struct netif *netif)
{
  struct pbuf *p, *q;
  u16_t len;
  int l =0;
  FrameTypeDef frame;
  u8 *buffer;
  uint32_t i=0;
  __IO ETH_DMADESCTypeDef *DMARxNextDesc;


  p = NULL;

  /* get received frame */
  frame = ETH_Get_Received_Frame();

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

  /* We allocate a pbuf chain of pbufs from the Lwip buffer pool */
  p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);

  /* copy received frame to pbuf chain */
  if (p != NULL)
  {
    for (q = p; q != NULL; q = q->next)
    {
      memcpy((u8_t*)q->payload, (u8_t*)&buffer[l], q->len);
      l = l + q->len;
    }    
  }

  /* Release descriptors to DMA */
  /* 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;
  }
  return p;
}

同樣的,配合註釋應該也容易理解。22行的frame變數取到了乙太網資料包,ETH_Get_Received_Frame函式,不用LWIP也是有的,在之前我們提到過的stm32f4x7_eth.c檔案中,len和buffer兩個變數一個是包長度,一個是包內容頭指標。下面/* copy received frame to pbuf chain */那一段是用連結串列遍歷的方式,將乙太網包資料放入LWIP處理資料的pbuf連結串列中,方便LWIP上層邏輯獲取資料,這裡我們不使用LWIP,這一段可忽略。接下來所有的操作都是針對DMA進行的,將DMA復位,因此我們需要保留,否則會一直產生重複的中斷。

通過以上的分析,我們可以輕鬆寫出自己的Pkg_Handle()函數了:

void Pkt_Handle(void) {
    FrameTypeDef frame;

    /* 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;

    printf("0011%d0022\n", receiveLen);    //將每一個的包長度發往串列埠

    if(receiveBuffer[41] == 201){        //如果第42位元組是十進位制201,則將整個包內容發往串列埠
        for (i = 0; i < receiveLen; i++) {
            printf("%c", receiveBuffer[i]);
        }
    }

    /* 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;
    }
}

驗證和總結

上述函式,配合ETH_IRQHandler中斷中的呼叫,完成了乙太網的收包,並且將接受的包的長度使用0011%d0022通過printf函式通過UART發往了PC端,因為如果將整個包內容發往PC的話,串列埠資料會非常多。同時如果為了驗證buffer中的內容能正確獲取,我們寫了一個if判斷,判斷如果資料包中的第42個位元組為201,則將包內容轉發到串列埠中去。

我們用JLink燒錄進STM32F4,將PC的有線網絡卡與STM32直接,開啟WireShark和串列埠通訊助手進行觀察和驗證,截圖如下。

紅色框框部分是關注重點,我們在CMD命令視窗ping 192.168.1.201,可以觸發一個ARP包,這個包中的第42個位元組就是201,因此可以觸發STM32中的if判斷,將包內容通過串列埠轉發給PC,而其他普通包,STM32則使用0011%d0022的格式將包長度發給了PC,整個實驗順利完成。

總結一下,本章我們依舊分析到了ethernetif.c檔案,這次是觀察的它的low_level_input()函式,藉助這個函式,我們編寫了我們自己的處理包的邏輯函式Pkg_Handle(),並通過乙太網中斷入口函式ETH_IRQHandler呼叫它,最後我們成功的使用WireShark配合串列埠進行了收包的驗證。

我們可以發現這個系列教程的後半段幾乎都在不停地圍繞LWIP庫中的ethernetif.c檔案進行分析,到目前為止,它的幾個重要底層函式low_level_init、low_level_output、low_level_input已經分別為我們的乙太網初始化、乙太網發包、乙太網收包等程式碼提供了重要的核心邏輯。

下一章,是我們這個系列的最後一章,我們將在STM32F4上,利用之前實驗過的各個功能,自己構建一個能響應ARP協議的程式。有了ARP協議的處理過程,我們就能在此基礎上擴充套件更多其他的協議,即使遇到工業乙太網跑在鏈路層的各個協議,我們也能撿其重點,按照自己的意願,隨心所欲的搭建了。