【程序】STM32F107VC單片機驅動DP83848以太網PHY芯片,移植lwip 2.1.2協議棧,並加入網線熱插拔檢測的功能(HAL庫)

Keil5工程下載地址:https://pan.baidu.com/s/1Uf0eRFB35_-Sw_ovQf2Kwg(提取碼:694k)

開發板:

杜邦線傳輸高速數字信號容易出錯,所以在用麪包板搭建開發環境時,最好使用25MHz時鐘的MII接口。如果要用50MHz的RMII接口,那麼杜邦線必須要非常非常短,否則時鐘信號一旦失真,就無法收發數據!

如果DP83848的運行時鐘是由單片機的PA8 MCO引腳輸出的,那麼DP83848的復位引腳一定要接一個下拉電阻。當單片機沒有啓動的時候,這個下拉電阻會使DP83848處於復位狀態。因爲單片機沒有運行的時候,DP83848沒有時鐘信號,如果此時DP83848沒有處於復位狀態,將會對電路產生很大的影響!比如啓動時單片機的串口輸出會亂碼。

網口的燈不要接反了,黃燈接LED_ACT,綠燈接LED_LINK。插了網線後,正常情況下是黃燈閃爍,綠燈常亮。

程序裏面的USE_MII宏決定了是使用MII接口還是RMII接口。ETH_REMAP宏決定了是否重映射ETH引腳。

#define ETH_REMAP 1
#define USE_MII 0

DP83848的復位引腳接到PB15上(帶外部下拉電阻),中斷引腳接到PB14上(帶外部上拉電阻)。

【代碼講解】

程序裏面使用的lwip2.1.2除了下面幾個文件是修改過的以外,其餘的都是官網的原始文件:
修改的文件:ethernetif.c(修改前的原始文件位於contrib-2.1.0.zip)
添加的文件:arch/cc.h lwipopts.h
(lwip 2.0.3版本中的ethernetif.c文件位於lwip-2.0.3.zip壓縮包的src/netif文件夾下。而lwip 2.1.0~2.1.2版本中的ethernetif.c文件則被移動到了contrib-2.1.0.zip壓縮包的examples/ethernetif文件夾裏面了, 裏面有一些細微的修改)

系統時鐘的配置是在clock_init函數裏面完成的:

// 配置系統和總線時鐘
void clock_init(void)
{
  HAL_StatusTypeDef status;
  RCC_ClkInitTypeDef clk = {0};
  RCC_OscInitTypeDef osc = {0};

  osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  osc.HSEPredivValue = RCC_HSE_PREDIV_DIV5;
  osc.HSEState = RCC_HSE_ON;
  osc.PLL.PLLMUL = RCC_PLL_MUL9;
  osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  osc.PLL.PLLState = RCC_PLL_ON;
  osc.PLL2.HSEPrediv2Value = RCC_HSE_PREDIV2_DIV5;
  osc.PLL2.PLL2MUL = RCC_PLL2_MUL8;
  osc.PLL2.PLL2State = RCC_PLL2_ON;
  osc.Prediv1Source = RCC_PREDIV1_SOURCE_PLL2;
  status = HAL_RCC_OscConfig(&osc);
  assert_param(status == HAL_OK);
  
  // ADC時鐘不能超過14MHz, 所以需要6分頻, 72MHz經過分頻後是12MHz
  __HAL_RCC_ADC_CONFIG(RCC_ADCPCLK2_DIV6);
  
  // 配置AHB和APB2總線時鐘爲72MHz, APB1總線時鐘爲36MHz
  clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
  clk.APB1CLKDivider = RCC_HCLK_DIV2;
  clk.APB2CLKDivider = RCC_HCLK_DIV1;
  HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2);
}

該函數將系統時鐘配置爲72MHz:AHB和APB2時鐘爲72MHz,APB1時鐘爲36MHz。

初始化DP83848的函數是DP83848_Init,該函數是在netif_add添加網卡時調用的,調用關係如下:
main -> net_config -> netif_add(或netif_add_noaddr) -> ethernetif_init -> low_level_init -> DP83848_Init

其中,low_level_init函數的代碼如下:

/**
 * In this function, the hardware should be initialized.
 * Called from ethernetif_init().
 *
 * @param netif the already initialized lwip network interface structure
 *        for this ethernetif
 */
static void
low_level_init(struct netif *netif)
{
  //struct ethernetif *ethernetif = netif->state;
  int ret;

  /* set MAC hardware address length */
  netif->hwaddr_len = ETHARP_HWADDR_LEN;

  /* set MAC hardware address */
  ret = DP83848_Init();
  DP83848_GetMACAddress(netif->hwaddr);
  printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", netif->hwaddr[0], netif->hwaddr[1], netif->hwaddr[2], netif->hwaddr[3], netif->hwaddr[4], netif->hwaddr[5]);

  /* maximum transfer unit */
  netif->mtu = 1500;

  /* device capabilities */
  /* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */
  netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_MLD6; // 這裏MLD6是啓用IPv6多播
  if (ret == 0)
    netif->flags |= NETIF_FLAG_LINK_UP; // 只有插了網線, 才設置這個標誌

#if LWIP_IPV6 && LWIP_IPV6_MLD
  /*
   * For hardware/netifs that implement MAC filtering.
   * All-nodes link-local is handled by default, so we must let the hardware know
   * to allow multicast packets in.
   * Should set mld_mac_filter previously. */
  if (netif->mld_mac_filter != NULL) {
    ip6_addr_t ip6_allnodes_ll;
    ip6_addr_set_allnodes_linklocal(&ip6_allnodes_ll);
    netif->mld_mac_filter(netif, &ip6_allnodes_ll, NETIF_ADD_MAC_FILTER);
  }
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */

  /* Do whatever else is needed to initialize interface. */
}

在這個函數裏面,調用了DP83848_Init初始化網口。如果此時板子是插了網線的,那麼函數返回0,沒有插網線時返回-1。接着調用DP83848_GetMACAddress將網卡地址(在程序中由STM32單片機的器件ID生成)告訴lwip。接下來,如果插了網線(ret==0),則設置NETIF_FLAG_LINK_UP標誌,告訴lwip網卡現在有網,這個操作和netif_set_link_up是等價的。NETIF_FLAG_MLD6表示啓用IPv6多播功能,IPv6的運行依賴於多播,不打開多播的話IPv6是不能正常工作的。

接下來看看DP83848_Init函數:

int DP83848_Init(void)
{
  uint32_t uid;
  ETH_MACInitTypeDef macconf = {0};
  GPIO_InitTypeDef gpio;
  HAL_StatusTypeDef status;
  RCC_PLLI2SInitTypeDef plli2s;
  
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE();
  
  // PA2: MDIO, PA8: MCO
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_2 | GPIO_PIN_8;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &gpio);
  
  // PB11: TX_EN, PB12~13: TXD0~1
  gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13;
  HAL_GPIO_Init(GPIOB, &gpio);
  
  // PC1: MDC
  gpio.Pin = GPIO_PIN_1;
  HAL_GPIO_Init(GPIOC, &gpio);
  
#if ETH_REMAP
  __HAL_RCC_AFIO_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_AFIO_REMAP_ETH_ENABLE();
#endif
#if USE_MII
  // PB8: TXD3
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_8;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOB, &gpio);
  
  // PC2: TXD2
  gpio.Pin = GPIO_PIN_2;
  HAL_GPIO_Init(GPIOC, &gpio);
#else
  // 必須在打開ETH的三個時鐘前選擇好RMII接口
  __HAL_RCC_AFIO_CLK_ENABLE();
  __HAL_AFIO_ETH_RMII();
#endif
  
  // PB15: RESET_N (必須接外部下拉電阻, 否則單片機啓動時串口會亂碼)
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET); // 保證DP83848處於復位狀態
  gpio.Mode = GPIO_MODE_OUTPUT_PP;
  gpio.Pin = GPIO_PIN_15;
  gpio.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &gpio);
  
  // MCO給DP83848提供25MHz或50MHz的時鐘
  plli2s.HSEPrediv2Value = RCC_HSE_PREDIV2_DIV5;
  plli2s.PLLI2SMUL = RCC_PLLI2S_MUL10;
  HAL_RCCEx_EnablePLLI2S(&plli2s);
#if USE_MII
  HAL_RCC_MCOConfig(RCC_MCO, RCC_MCO1SOURCE_PLL3CLK_DIV2, RCC_MCODIV_1);
#else
  HAL_RCC_MCOConfig(RCC_MCO, RCC_MCO1SOURCE_PLL3CLK, RCC_MCODIV_1);
#endif
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); // 時鐘啓動後, 才能撤銷DP83848復位信號
  
  // 根據器件ID生成MAC地址
  uid = HAL_GetUIDw0() + HAL_GetUIDw1() + HAL_GetUIDw2();
  memcpy(dp83848_mac + 3, &uid, 3);
  
  // 初始化ETH
  __HAL_RCC_ETHMAC_CLK_ENABLE();
  __HAL_RCC_ETHMACRX_CLK_ENABLE();
  __HAL_RCC_ETHMACTX_CLK_ENABLE();
  
  heth.Instance = ETH;
  heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE; // 打開網絡模式自動協商
  heth.Init.ChecksumMode = ETH_CHECKSUM_BY_SOFTWARE;
  heth.Init.MACAddr = dp83848_mac;
#if USE_MII
  heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_MII;
#else
  heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;
#endif
  heth.Init.PhyAddress = DP83848_PHY_ADDRESS;
  heth.Init.RxMode = ETH_RXPOLLING_MODE;
  status = HAL_ETH_Init(&heth); // 一旦執行這個初始化函數, 所有的寄存器都會恢復默認值
  
  HAL_ETH_WritePHYRegister(&heth, PHY_MICR, PHY_MICR_INT_OE | PHY_MICR_INT_EN); // 打開中斷輸出
  HAL_ETH_WritePHYRegister(&heth, PHY_MISR, PHY_MISR_LINK_INT_EN); // 打開鏈路狀態中斷
  
  macconf.BroadcastFramesReception = ETH_BROADCASTFRAMESRECEPTION_ENABLE;
  macconf.MulticastFramesFilter = ETH_MULTICASTFRAMESFILTER_NONE; // 要使用IPv6, 必須要能接收多播幀
  macconf.PassControlFrames = ETH_PASSCONTROLFRAMES_BLOCKALL;
  HAL_ETH_ConfigMAC(&heth, &macconf);
  
  HAL_ETH_DMARxDescListInit(&heth, dp83848_rx, dp83848_rxbuf[0], ETH_RXBUFNB);
  HAL_ETH_DMATxDescListInit(&heth, dp83848_tx, dp83848_txbuf[0], ETH_TXBUFNB);
  
  if (status != HAL_OK)
  {
    printf("Failed to start ETH!\n"); // 一般是因爲網線沒有插
    return -1;
  }
  
  HAL_ETH_Start(&heth);
  return 0;
}

在這個函數中,首先配置好ETH的GPIO引腳,然後用HAL_RCC_MCOConfig函數配置MCO時鐘。時鐘啓用前,DP83848必須一直處於復位狀態(PB15=0),否則將導致串口亂碼。時鐘啓用後,才能撤銷DP83848的復位信號(PB15=1)。還有很重要的一點,就是RMII接口的選擇(__HAL_AFIO_ETH_RMII)必須在ETH時鐘啓用前(__HAL_RCC_ETHMAC_CLK_ENABLE)完成,否則將不能生效,導致後面數據包無法收發!基於這一點原因,後面的heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII實際上是沒有作用的,但是卻又不能不寫。

DP83848本身沒有MAC地址(網卡地址),所以需要我們自己生成一個MAC地址,同時告訴STM32 ETH和lwip協議棧。這裏的生成方式是,前三位固定爲00:80:E1,後三位由STM32的3個32位器件ID值(HAL_GetUIDw0~2)相加生成。特別注意的是,網卡的MAC地址必須爲單播地址(第一個字節爲偶數),絕對不可以設置一個多播MAC地址(第一個字節爲奇數),否則在很多路由器上不能正常通信

接着調用HAL_ETH_Init函數初始化ETH。這裏面heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE這句話很重要,這是開啓自動協商功能,自動協商網絡參數(全雙工/半雙工,10M或100M速率)。如果沒有打開這個,那麼可能會因爲網絡配置不正確而無法收發數據包!如果此時插了網線,那麼HAL_ETH_Init返回HAL_OK,如果沒有插網線則返回HAL_ERROR或HAL_TIMEOUT。

然後,調用HAL_ETH_WritePHYRegister函數寫DP83848的寄存器,打開中斷輸出,插拔網線時能產生外部中斷。
HAL_ETH_ConfigMAC函數配置MAC,開啓多播和廣播。必須要打開多播才能ping通IPv6地址,必須要打開廣播才能ping通IPv4地址。
HAL_ETH_DMARxDescListInit和HAL_ETH_DMATxDescListInit函數用於初始化接收和發送緩衝區,網卡收到的數據都是保存在這些緩衝區裏面的。

HAL_ETH_Start的作用是開啓發送和接收。這裏要注意的是,如果沒有插網線,HAL_ETH_Init的返回值不爲HAL_OK,就不需要打開發送和接收,函數直接返回-1,後面在初始化lwip時就不會設置NETIF_FLAG_LINK_UP標誌位。插了網線之後,再由DP83848_Restart函數調用HAL_ETH_Start打開收發功能。

到這裏爲止,lwip就初始化完成了。由net_config配置網卡的IPv4和IPv6地址。

static void net_config(int use_dhcp)
{
  ip4_addr_t ipaddr, netmask, gw;
  
  if (use_dhcp)
    netif_add_noaddr(&netif_dp83848, NULL, ethernetif_init, netif_input);
  else
  {
    IP4_ADDR(&ipaddr, 192, 168, 137, 20);
    IP4_ADDR(&netmask, 255, 255, 255, 0);
    IP4_ADDR(&gw, 192, 168, 137, 1);
    netif_add(&netif_dp83848, &ipaddr, &netmask, &gw, NULL, ethernetif_init, netif_input);
  }
  netif_set_default(&netif_dp83848);
  netif_set_up(&netif_dp83848);
  
  if (use_dhcp)
    dhcp_start(&netif_dp83848);
  
  netif_create_ip6_linklocal_address(&netif_dp83848, 1);
  printf("IPv6 link-local address: %s\n", ipaddr_ntoa(netif_ip_addr6(&netif_dp83848, 0)));
  netif_set_ip6_autoconfig_enabled((struct netif *)(uintptr_t)&netif_dp83848, 1);
}

當參數use_dhcp爲0時,爲固定IP地址、子網掩碼和網關,當use_dhcp爲1時,則通過DHCP獲取。這裏只需要執行dhcp_start就不用管了,插拔網線後,只要調用了netif_set_link_up/down函數,那麼lwip自己會調用dhcp_network_changed函數重新通過DHCP獲取地址。另外,判斷DHCP是否獲取到IP地址的函數是dhcp_supplied_address函數。
netif_create_ip6_linklocal_address函數創建網卡的IPv6本地鏈路地址(即fe80開頭的地址),netif_set_ip6_autoconfig_enabled是通過SLAAC協議獲取IPv6地址。這裏進行(struct netif *)(uintptr_t)強制類型轉換,僅僅是爲了避免Keil編譯器的警告,沒有實際作用。
這裏再強調一下,網卡必須要啓用廣播,IPv4才能正常工作。必須要啓用多播,IPv6才能正常工作。

接下來看下main函數的主循環:

while (1)
{
  if (DP83848_GetITStatus())
  {
    HAL_ETH_ReadPHYRegister(&heth, PHY_MISR, &status);
    printf("DP83848 interrupt occurred! status=%#x\n", status);
    if (status & PHY_LINK_INTERRUPT)
    {
      // 這裏必須要多讀幾次
      HAL_ETH_ReadPHYRegister(&heth, PHY_BSR, &value);
      HAL_ETH_ReadPHYRegister(&heth, PHY_BSR, &value);
      
      if (value & PHY_LINKED_STATUS)
      {
        if (!netif_is_link_up(&netif_dp83848))
        {
          printf("Link is up!\n");
          DP83848_Restart();
          netif_set_link_up(&netif_dp83848);
        }
      }
      else
      {
        printf("Link is down!\n");
        DP83848_Stop();
        netif_set_link_down(&netif_dp83848);
      }
    }
  }
  
  if (__HAL_ETH_DMA_GET_FLAG(&heth, ETH_DMA_FLAG_R) != RESET)
  {
    __HAL_ETH_DMA_CLEAR_FLAG(&heth, ETH_DMA_FLAG_R);
    while (HAL_ETH_GetReceivedFrame(&heth) == HAL_OK)
      ethernetif_input(&netif_dp83848);
  }
  
  display_ip();
  
  sys_check_timeouts();
}

主循環中,if (DP83848_GetITStatus())判斷是否產生了DP83848中斷,如果有中斷就處理中斷。
if (__HAL_ETH_DMA_GET_FLAG(&heth, ETH_DMA_FLAG_R) != RESET)判斷是否收到了網卡數據,如果收到了網卡數據,則交給lwip處理。
display_ip是我們自己實現的IP地址顯示函數,當DHCP獲取到IPv4地址後,以及SLAAC獲取到IPv6地址後,將地址通過串口打印出來。顯示IP地址的代碼如下:

// 顯示DHCP分配的IP地址
static void display_ip(void)
{
  const ip_addr_t *addr;
  static uint8_t ip_displayed = 0;
  static uint8_t ip6_displayed = 0;
  int i, dns = 0;
  
  if (dhcp_supplied_address(&netif_dp83848))
  {
    if (ip_displayed == 0)
    {
      ip_displayed = 1;
      
      printf("DHCP supplied address!\n");
      printf("IP address: %s\n", ipaddr_ntoa(&netif_dp83848.ip_addr));
      printf("Subnet mask: %s\n", ipaddr_ntoa(&netif_dp83848.netmask));
      printf("Default gateway: %s\n", ipaddr_ntoa(&netif_dp83848.gw));
      dns = 1;
    }
  }
  else
    ip_displayed = 0;
  
  for (i = 1; i < LWIP_IPV6_NUM_ADDRESSES; i++) // 0號地址是本地鏈路地址, 不需要顯示
  {
    if (ip6_addr_isvalid(netif_ip6_addr_state(&netif_dp83848, i)))
    {
      if ((ip6_displayed & _BV(i)) == 0)
      {
        ip6_displayed |= _BV(i);
        printf("IPv6 address %d: %s\n", i, ipaddr_ntoa(netif_ip_addr6(&netif_dp83848, i)));
        dns = 1;
      }
    }
    else
      ip6_displayed &= ~_BV(i);
  }
  
  // 顯示DNS服務器地址
  // 在lwip中, IPv4 DHCP和IPv6 SLAAC獲取到的DNS地址會互相覆蓋
  if (dns)
  {
    addr = dns_getserver(0);
    if (ip_addr_isany(addr))
      return;
    printf("DNS Server: %s", ipaddr_ntoa(addr));
    
    addr = dns_getserver(1);
    if (!ip_addr_isany(addr))
      printf(" %s", ipaddr_ntoa(addr));
    
    printf("\n");
  }
}


sys_check_timeouts函數是lwip內部的定時處理,必須在主循環中調用這個函數。

DP83848_GetITStatus()根據PB14是否爲低電平來判斷是否產生了中斷,如果產生了就處理中斷。讀取PHY_MISR寄存器看看產生了哪些中斷。如果板子上沒有將DP83848的INT中斷引腳接到單片機上,那麼可以通過一直輪詢PHY_MISR寄存器是否不爲0,來判斷是否產生了中斷。當PHY_MISR寄存器的PHY_LINK_INTERRUPT位爲1時,表明產生了網線插拔中斷。讀取PHY_BSR寄存器兩次判斷是插了網線還是拔了網線。必須要讀取兩次,讀取一次是不行的。如果PHY_BSR寄存器的PHY_LINKED_STATUS位爲1,則說明是插了網線,此時調用DP83848_Restart打開ETH收發功能,然後netif_set_link_up告訴lwip現在網卡有網了。爲了防止DP83848_Restart函數重複調用,這裏加了一個netif_is_link_up判斷。

DP83848_Restart函數的實現如下:

void DP83848_Restart(void)
{
  uint32_t value;
  
  // 只要打開了自動協商功能, 插上網線後就能自動開始協商, 不需要軟件寫寄存器來觸發
  do
  {
    HAL_ETH_ReadPHYRegister(&heth, PHY_BSR, &value);
  } while ((value & PHY_AUTONEGO_COMPLETE) == 0);
  
  // 讀取自動協商結果, 並寫入STM32的ETH寄存器
  HAL_ETH_ReadPHYRegister(&heth, PHY_SR, &value);
  if (value & PHY_DUPLEX_STATUS)
    heth.Init.DuplexMode = ETH_MODE_FULLDUPLEX;
  else
    heth.Init.DuplexMode = ETH_MODE_HALFDUPLEX;
  if (value & PHY_SPEED_STATUS)
    heth.Init.Speed = ETH_SPEED_10M;
  else
    heth.Init.Speed = ETH_SPEED_100M;
  HAL_ETH_ConfigMAC(&heth, NULL);
  HAL_ETH_Start(&heth);
  printf("ETH is restarted!\n");
}

這裏同樣要進行自動協商。由於我們最開始調用HAL_ETH_Init打開了自動協商功能,所以這裏就不需要再打開了,插上網線後DP83848就會自己開始協商,我們只需要等待自動協商完畢,讀取PHY_SR寄存器獲取網絡參數,然後HAL_ETH_ConfigMAC配置一下STM32 ETH的網絡參數(第二個參數必須爲NULL纔行),HAL_ETH_Start使能收發就行了。

網線拔掉時,只需要調用HAL_ETH_Stop函數,然後調用netif_set_link_down函數告訴lwip現在網卡沒網了就行。

void DP83848_Stop(void)
{
  HAL_ETH_Stop(&heth);
  printf("ETH is stopped!\n");
}

最後,我們來看一下數據包的收發。底層處理數據包發送的函數是low_level_output函數,處理數據包接收的函數是low_level_input函數,這是移植lwip時最重要的兩個函數。

首先看數據包的發送:

static err_t
low_level_output(struct netif *netif, struct pbuf *p)
{
  err_t err = ERR_OK;
  //struct ethernetif *ethernetif = netif->state;
  struct pbuf *q;
  uint32_t i = 0;

  //initiate transfer();

#if ETH_PAD_SIZE
  pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
  
  printf("[Send] len=%u", p->tot_len);
  if (p->tot_len > ETH_TX_BUF_SIZE || !netif_is_link_up(netif))
  {
    printf(" (failed)\n");
    err = ERR_IF;
    goto end;
  }
  else
    printf("\n");
  while (heth.TxDesc->Status & ETH_DMATXDESC_OWN);
  
  for (q = p; q != NULL; q = q->next) {
    /* Send the data from the pbuf to the interface, one pbuf at a
       time. The size of the data in each pbuf is kept in the ->len
       variable. */
    //send data from(q->payload, q->len);
    memcpy((uint8_t *)heth.TxDesc->Buffer1Addr + i, q->payload, q->len);
    i += q->len;
  }

  //signal that packet should be sent();
  HAL_ETH_TransmitFrame(&heth, p->tot_len);

  MIB2_STATS_NETIF_ADD(netif, ifoutoctets, p->tot_len);
  if (((u8_t *)p->payload)[0] & 1) {
    /* broadcast or multicast packet*/
    MIB2_STATS_NETIF_INC(netif, ifoutnucastpkts);
  } else {
    /* unicast packet */
    MIB2_STATS_NETIF_INC(netif, ifoutucastpkts);
  }
  /* increase ifoutdiscards or ifouterrors on error */

end:
#if ETH_PAD_SIZE
  pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif

  if (err == ERR_OK)
    LINK_STATS_INC(link.xmit);
  else
    MIB2_STATS_NETIF_INC(netif, ifouterrors);

  return err;
}

發送前先做判斷,如果發送的數據量大於緩衝區的大小(p->tot_len > ETH_TX_BUF_SIZE),或者網口沒網(!netif_is_link_up(netif)),就返回錯誤。然後用while循環等待緩衝區可用(while (heth.TxDesc->Status & ETH_DMATXDESC_OWN)),再用for循環將pbuf裏面的數據拷貝到發送緩衝區heth.TxDesc->Buffer1Addr中。最後調用HAL_ETH_TransmitFrame函數將數據包發送出去,發送的長度爲p->tot_len。

再看看數據包的接收:

最開始main函數的主循環中,HAL_ETH_GetReceivedFrame接收了一個數據包後,調用ethernetif_input函數,這個函數又跳轉到了low_level_input裏面去了。

static struct pbuf *
low_level_input(struct netif *netif)
{
  //struct ethernetif *ethernetif = netif->state;
  struct pbuf *p, *q;
  u16_t len;
  uint32_t i = 0;
  ETH_DMADescTypeDef *desc;

  /* Obtain the size of the packet and put it into the "len"
     variable. */
  len = heth.RxFrameInfos.length;
  printf("[Recv] len=%u\n", len);

#if ETH_PAD_SIZE
  len += ETH_PAD_SIZE; /* allow room for Ethernet padding */
#endif

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

  if (p != NULL) {

#if ETH_PAD_SIZE
    pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif

    /* We iterate over the pbuf chain until we have read the entire
     * packet into the pbuf. */
    for (q = p; q != NULL; q = q->next) {
      /* Read enough bytes to fill this pbuf in the chain. The
       * available data in the pbuf is given by the q->len
       * variable.
       * This does not necessarily have to be a memcpy, you can also preallocate
       * pbufs for a DMA-enabled MAC and after receiving truncate it to the
       * actually received size. In this case, ensure the tot_len member of the
       * pbuf is the sum of the chained pbuf len members.
       */
      //read data into(q->payload, q->len);
      memcpy(q->payload, (uint8_t *)heth.RxFrameInfos.FSRxDesc->Buffer1Addr + i, q->len);
      i += q->len;
    }
    //acknowledge that packet has been read();

    MIB2_STATS_NETIF_ADD(netif, ifinoctets, p->tot_len);
    if (((u8_t *)p->payload)[0] & 1) {
      /* broadcast or multicast packet*/
      MIB2_STATS_NETIF_INC(netif, ifinnucastpkts);
    } else {
      /* unicast packet*/
      MIB2_STATS_NETIF_INC(netif, ifinucastpkts);
    }
#if ETH_PAD_SIZE
    pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif

    LINK_STATS_INC(link.recv);
  } else {
    //drop packet();
    LINK_STATS_INC(link.memerr);
    LINK_STATS_INC(link.drop);
    MIB2_STATS_NETIF_INC(netif, ifindiscards);
  }
  
  // 確認收到數據
  desc = heth.RxFrameInfos.FSRxDesc;
  for (i = 0; i < heth.RxFrameInfos.SegCount; i++)
  {
    desc->Status |= ETH_DMARXDESC_OWN;
    desc = (ETH_DMADescTypeDef *)(desc->Buffer2NextDescAddr);
  }
  heth.RxFrameInfos.SegCount = 0;
  
  if (__HAL_ETH_DMA_GET_FLAG(&heth, ETH_DMA_FLAG_RBU) != RESET)
  {
    printf("Receive buffer unavailable!\n");
    __HAL_ETH_DMA_CLEAR_FLAG(&heth, ETH_DMA_FLAG_RBU);
    WRITE_REG(heth.Instance->DMARPDR, 0);
  }

  return p;
}

此函數將接收緩衝區heth.RxFrameInfos.FSRxDesc->Buffer1Addr中的數據複製到pbuf裏面去,複製完了之後要釋放接收緩衝區,將該數據包占用的緩衝區還給DMA(置位ETH_DMARXDESC_OWN)。而且還必須將heth.RxFrameInfos.SegCount清零,這一步很關鍵,否則接收後續數據包會出問題。最後,判斷ETH_DMA_FLAG_RBU標誌位,檢查之前是否因緩衝區滿了導致了接收停止,如果接收已停止,則對DMARPDR寄存器寫0,恢復接收。這個操作沒有封裝成HAL庫函數,所以必須直接寫寄存器完成。

【程序運行結果】

開機時如果插了網線,串口的輸出如下:

STM32F107VC DP83848
SystemCoreClock=72000000
MAC address: 00:80:E1:CD:9E:1A
[Send] len=350
IPv6 link-local address: FE80::280:E1FF:FECD:9E1A
DP83848 interrupt occurred! status=0x2c20
[Send] len=86
[Send] len=78
[Send] len=62
[Recv] len=590
[Send] len=350
[Send] len=86
[Recv] len=60
[Recv] len=590
[Send] len=42
[Send] len=42
[Send] len=86
[Send] len=42
[Recv] len=60
[Recv] len=60
[Recv] len=60
[Recv] len=60
[Recv] len=90
[Recv] len=142
[Send] len=86
[Recv] len=142
[Recv] len=86
[Recv] len=90
[Send] len=42
DHCP supplied address!
IP address: 192.168.1.3
Subnet mask: 255.255.255.0
Default gateway: 192.168.1.1
DNS Server: FE80::1
[Recv] len=90
[Send] len=42
[Recv] len=60
[Send] len=70
[Send] len=42
[Send] len=78
[Send] len=42
IPv6 address 1: 240E:398:394:7C60:280:E1FF:FECD:9E1A
DNS Server: FE80::1
[Send] len=86
[Recv] len=90
[Recv] len=60
[Send] len=42

能夠獲取到IPv4和IPv6地址,以及DNS服務器的地址。
拔掉網線再重新插入,能產生網線插拔中斷,然後DHCP也能重新獲取IP地址:

[Recv] len=60
[Recv] len=60
[Recv] len=60
DP83848 interrupt occurred! status=0x2820
Link is down!
ETH is stopped!
DP83848 interrupt occurred! status=0x2c20
Link is up!
ETH is restarted!
[Send] len=350
[Send] len=42
[Recv] len=590
DHCP supplied address!
IP address: 192.168.1.3
Subnet mask: 255.255.255.0
Default gateway: 192.168.1.1
DNS Server: 192.168.1.1
[Recv] len=60

如果開機的時候沒有插網線,則ETH初始化失敗,之後重新插了網線之後,也能正常通信,並獲取到IP地址:

STM32F107VC DP83848
SystemCoreClock=72000000
Failed to start ETH!
MAC address: 00:80:E1:CD:9E:1A
IPv6 link-local address: FE80::280:E1FF:FECD:9E1A
DP83848 interrupt occurred! status=0x2c20
Link is up!
ETH is restarted!
[Send] len=350
[Recv] len=590
[Send] len=350
[Recv] len=60
[Recv] len=590
[Send] len=42
[Send] len=42
[Recv] len=60
[Recv] len=60
[Send] len=42
[Send] len=86
[Send] len=78
[Send] len=62
[Send] len=42
[Send] len=86
DHCP supplied address!
IP address: 192.168.1.3
Subnet mask: 255.255.255.0
Default gateway: 192.168.1.1
DNS Server: 192.168.1.1
[Recv] len=60
[Recv] len=60
[Recv] len=60
[Recv] len=60
[Recv] len=90
[Send] len=42
[Recv] len=60
[Send] len=70
[Recv] len=142
[Send] len=78
[Recv] len=86
[Recv] len=142
[Recv] len=60
[Send] len=42
[Send] len=78
[Send] len=86
[Recv] len=60
[Send] len=42
[Send] len=86
IPv6 address 1: 240E:398:394:7C60:280:E1FF:FECD:9E1A
DNS Server: FE80::1
[Recv] len=102
[Recv] len=122
[Recv] len=102
[Recv] len=122
[Send] len=42

電腦可以ping通板子的IPv4地址和IPv6地址,以及NetBIOS計算機名:

電腦可以通過IPv4和IPv6地址,以及NetBIOS計算機名,訪問板子上的網頁服務器:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章