【程序】STM32H743ZI单片机驱动DP83848以太网PHY芯片,移植lwip 2.1.3协议栈,并加入网线热插拔检测的功能

STM32H7的ETH HAL库封装得比较好,真正做到了完全用HAL API函数操作,不需要再用寄存器操作。
而STM32F1的ETH HAL库就没有完全封装,有些操作必须要用寄存器完成,而且还需要单独改动HAL库结构体里面的变量。
(关于STM32F1的ETH HAL库用法,请参阅:https://blog.csdn.net/ZLK1214/article/details/105457370

HAL库版本:STM32Cube_FW_H7_V1.9.0
Keil5工程下载地址:百度网盘 请输入提取码(提取码:p3xd)
开发板:

【电路连线】

晶振采用的是XTAL-3225封装的50MHz有源晶振。

引脚名称单片机I/O口
ETH_REF_CLKPA1
ETH_MDIOPA2
ETH_CRS_DVPA7
ETH_TX_ENPG11
ETH_TXD0PB12
ETH_TXD1PB13
ETH_MDCPC1
ETH_RXD0PC4
ETH_RXD1PC5
DP83848_RST(复位)PE4(普通I/O口)
DP83848_INT(中断)PE5(普通I/O口)

【代码讲解】

程序里面使用的lwip2.1.3除了下面几个文件是修改过的以外,其余的都是官网的原始文件:
修改的文件: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.3版本中的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};
  
  HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);
  while (__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY) == RESET);
  
  osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  osc.HSEState = RCC_HSE_ON;
  osc.PLL.PLLFRACN = 0;
  osc.PLL.PLLM = 5;
  osc.PLL.PLLN = 192;
  osc.PLL.PLLP = 2;
  osc.PLL.PLLQ = 2;
  osc.PLL.PLLR = 2;
  osc.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
  osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  osc.PLL.PLLState = RCC_PLL_ON;
  osc.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
  status = HAL_RCC_OscConfig(&osc);
  if (status != HAL_OK)
    abort();
  
  clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 | RCC_CLOCKTYPE_D1PCLK1 | RCC_CLOCKTYPE_D3PCLK1;
  clk.SYSCLKDivider = RCC_SYSCLK_DIV1;
  clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  clk.AHBCLKDivider = RCC_HCLK_DIV2;
  clk.APB1CLKDivider = RCC_APB1_DIV2;
  clk.APB2CLKDivider = RCC_APB2_DIV2;
  clk.APB3CLKDivider = RCC_APB3_DIV2;
  clk.APB4CLKDivider = RCC_APB4_DIV2;
  HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4);
}

该函数将系统时钟配置为480MHz:AHB时钟为240MHz,APB1~4时钟为120MHz。

初始化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;

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

  /* set MAC hardware address */
  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多播
  // 不设置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初始化网口,接着调用DP83848_GetMACAddress将网卡地址(在程序中由STM32单片机的器件ID生成)告诉lwip。netif->flags不设置NETIF_FLAG_LINK_UP标志,告诉lwip网卡现在没有网。NETIF_FLAG_MLD6表示启用IPv6多播功能,IPv6的运行依赖于多播,不打开多播的话IPv6是不能正常工作的。

接下来看看DP83848_Init函数:

void DP83848_Init(void)
{
  int i;
  uint32_t uid;
  ETH_MACFilterConfigTypeDef macfilter;
  GPIO_InitTypeDef gpio;
  
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOE_CLK_ENABLE();
  __HAL_RCC_GPIOG_CLK_ENABLE();
  
  // PA1: ETH_REF_CLK, PA2: ETH_MDIO, PA7: ETH_CRS_DV
  gpio.Alternate = GPIO_AF11_ETH;
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7;
  gpio.Pull = GPIO_NOPULL;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &gpio);
  
  // PB12~13: ETH_TXD0~1
  gpio.Pin = GPIO_PIN_12 | GPIO_PIN_13;
  HAL_GPIO_Init(GPIOB, &gpio);
  
  // PC1: ETH_MDC, PC4~5: ETH_RXD0~1
  gpio.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5;
  HAL_GPIO_Init(GPIOC, &gpio);
  
  // PE4: DP83848_RST
  gpio.Mode = GPIO_MODE_OUTPUT_PP;
  gpio.Pin = GPIO_PIN_4;
  HAL_GPIO_Init(GPIOE, &gpio);
  
  // PE5: DP83848_INT
  gpio.Mode = GPIO_MODE_INPUT;
  gpio.Pin = GPIO_PIN_5;
  HAL_GPIO_Init(GPIOE, &gpio);
  
  // PG11: ETH_TX_EN
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_11;
  HAL_GPIO_Init(GPIOG, &gpio);
  
  HAL_Delay(100); // 延长复位时间, 以免通电后检测不到中断
  HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET); // 撤销DP83848复位信号
  
  // 根据器件ID生成MAC地址
  uid = HAL_GetUIDw0() + HAL_GetUIDw1() + HAL_GetUIDw2();
  memcpy(dp83848_mac + 3, &uid, 3);
  
  // 初始化ETH
  __HAL_RCC_ETH1MAC_CLK_ENABLE();
  __HAL_RCC_ETH1RX_CLK_ENABLE();
  __HAL_RCC_ETH1TX_CLK_ENABLE();
  
  heth.Instance = ETH;
  heth.Init.MACAddr = dp83848_mac;
  heth.Init.MediaInterface = HAL_ETH_RMII_MODE;
  heth.Init.RxBuffLen = ETH_MAX_PACKET_SIZE;
  heth.Init.RxDesc = dp83848_rx;
  heth.Init.TxDesc = dp83848_tx;
  HAL_ETH_Init(&heth);
  
  HAL_ETH_WritePHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_MICR, PHY_MICR_INT_OE | PHY_MICR_INT_EN); // 打开中断输出
  HAL_ETH_WritePHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_MISR, PHY_MISR_LINK_INT_EN); // 打开链路状态中断
  
  HAL_ETH_GetMACFilterConfig(&heth, &macfilter);
  macfilter.BroadcastFilter = ENABLE; // 要使用IPv4, 必须要能接收广播帧
  macfilter.PassAllMulticast = ENABLE; // 要使用IPv6, 必须要能接收多播帧
  HAL_ETH_SetMACFilterConfig(&heth, &macfilter);
  
  for (i = 0; i < ETH_RX_DESC_CNT; i++)
    HAL_ETH_DescAssignMemory(&heth, i, dp83848_rxbuf[i], NULL);
}

在这个函数中,首先配置好ETH的GPIO引脚。电路板上DP83848默认是处于复位状态,PE4拉高后撤销复位,DP83848进入正常工作状态。
请注意要先延时,再撤销复位信号,以免开发板通电瞬间,DP83848复位时间太短,不能产生网线插拔中断信号,ETH以为没有插网线而不工作。

DP83848本身没有MAC地址(网卡地址),所以需要我们自己生成一个MAC地址,同时告诉STM32 ETH和lwip协议栈。这里的生成方式是,前三位固定为00:80:E1,后三位由STM32的3个32位器件ID值(HAL_GetUIDw0~2)相加生成。特别注意的是,网卡的MAC地址必须为单播地址(第一个字节为偶数),绝对不可以设置一个多播MAC地址(第一个字节为奇数),否则在很多路由器上不能正常通信!

接着调用HAL_ETH_Init函数初始化ETH,设置好MAC地址,选择MII或RMII接口,设置接收描述符和发送描述符。调用HAL_ETH_WritePHYRegister函数写DP83848的寄存器,打开中断输出,插拔网线时能产生外部中断。
HAL_ETH_SetMACFilterConfig函数配置MAC,开启多播和广播。必须要打开广播才能ping通IPv4地址,必须要打开多播才能ping通IPv6地址。
最后用HAL_ETH_DescAssignMemory函数将dp83848_rx接收描述符和dp83848_rxbuf接收缓冲区绑定,网卡收到的数据都是保存在这些缓冲区里面的。

到这里为止,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才能正常工作。

我们之前在ethernetif.c的low_level_init函数中去掉了netif->flags的NETIF_FLAG_LINK_UP选项,所以网卡默认状态是未连接状态。
netif_add添加网卡后,只要netif_set_up启用了网卡,就可以马上调用dhcp_start启动DHCP服务器,不需要等到连上网络后netif_set_link_up再启动DHCP。
此外,netif_set_link_up函数内部会调用dhcp_network_changed,进而调用dhcp_reboot,所以网络断开后再连接,DHCP会自动重新获取IP地址,不需要自己再去调用dhcp_start。
需要注意的是,dhcp_start只是启动DHCP服务器,函数返回时,IP地址还没有获取到。在裸机环境下,用dhcp_supplied_address判断是否获取到IP地址,应该在main函数的while(1)主循环里面进行。dhcp_start后马上用dhcp_supplied_address判断,判断结果肯定是没有获取到。

接下来看下main函数的主循环:

while (1)
{
  if (DP83848_GetITStatus())
  {
    HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_MISR, &status);
    printf("DP83848 interrupt occurred! status=%#x\n", status);
    if (status & PHY_LINK_INTERRUPT)
    {
      // 这里必须要多读几次
      HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_BSR, &value);
      HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_BSR, &value);
      
      if (value & PHY_LINKED_STATUS)
      {
        if (!netif_is_link_up(&netif_dp83848))
        {
          printf("Link is up!\n");
          DP83848_Start();
          netif_set_link_up(&netif_dp83848);
        }
      }
      else
      {
        printf("Link is down!\n");
        DP83848_Stop();
        netif_set_link_down(&netif_dp83848);
      }
    }
  }
  
  while (HAL_ETH_IsRxDataAvailable(&heth))
    ethernetif_input(&netif_dp83848);
  
  display_ip();
  
  sys_check_timeouts();
}

主循环中,if (DP83848_GetITStatus())判断是否产生了DP83848中断,如果有中断就处理中断。
HAL_ETH_IsRxDataAvailable函数判断是否收到了网卡数据,如果收到了网卡数据,则交给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()根据PE5是否为低电平来判断是否产生了中断,如果产生了就处理中断。读取PHY_MISR寄存器看看产生了哪些中断。如果板子上没有将DP83848的INT中断引脚接到单片机上,那么可以通过一直轮询PHY_MISR寄存器是否不为0,来判断是否产生了中断。当PHY_MISR寄存器的PHY_LINK_INTERRUPT位为1时,表明产生了网线插拔中断。读取PHY_BSR寄存器两次判断是插了网线还是拔了网线。必须要读取两次,读取一次是不行的。如果PHY_BSR寄存器的PHY_LINKED_STATUS位为1,则说明是插了网线,此时调用DP83848_Start打开ETH收发功能,然后netif_set_link_up告诉lwip现在网卡有网了。为了防止DP83848_Start函数重复调用,这里加了一个netif_is_link_up判断。

DP83848_Start函数的实现如下:

void DP83848_Start(void)
{
  uint32_t value;
  ETH_MACConfigTypeDef macconf;
  
  // 只要打开了自动协商功能, 插上网线后就能自动开始协商, 不需要软件写寄存器来触发
  do
  {
    HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_BSR, &value);
  } while ((value & PHY_AUTONEGO_COMPLETE) == 0);
  
  // 读取自动协商结果, 并写入STM32的ETH寄存器
  HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_SR, &value);
  HAL_ETH_GetMACConfig(&heth, &macconf);
  if (value & PHY_DUPLEX_STATUS)
    macconf.DuplexMode = ETH_FULLDUPLEX_MODE;
  else
    macconf.DuplexMode = ETH_HALFDUPLEX_MODE;
  if (value & PHY_SPEED_STATUS)
    macconf.Speed = ETH_SPEED_10M;
  else
    macconf.Speed = ETH_SPEED_100M;
  HAL_ETH_SetMACConfig(&heth, &macconf);
  HAL_ETH_Start(&heth);
  printf("ETH is started!\n");
}

插上网线后,DP83848会进行自动协商。这里要做的就是等待自动协商完毕,读取PHY_SR寄存器获取网络参数,然后将网络参数通过HAL_ETH_SetMACConfig告诉STM32,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;
  int i = 0;
  ETH_BufferTypeDef data[ETH_TX_DESC_CNT];
  ETH_TxPacketConfig config = {0};
  HAL_StatusTypeDef status;

  //initiate transfer();

#if ETH_PAD_SIZE
  pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
  
  if (p->tot_len > ETH_MAX_PACKET_SIZE || !netif_is_link_up(netif))
  {
    err = ERR_IF;
    goto end;
  }
  
  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);
    if (i >= ETH_TX_DESC_CNT)
    {
      err = ERR_IF;
      break;
    }
    data[i].buffer = q->payload;
    data[i].len = q->len;
    if (q->next == NULL)
      data[i].next = NULL;
    else
      data[i].next = &data[i + 1];
    i++;
  }

  //signal that packet should be sent();
  config.Attributes = ETH_TX_PACKETS_FEATURES_CSUM | ETH_TX_PACKETS_FEATURES_CRCPAD;
  config.ChecksumCtrl = ETH_CHECKSUM_DISABLE; // 由软件计算网络层和传输层的校验和
  config.CRCPadCtrl = ETH_CRC_PAD_INSERT; // 填充短帧, 并由硬件计算数据链路层校验和
  config.Length = p->tot_len;
  config.TxBuffer = data;
  status = HAL_ETH_Transmit(&heth, &config, HAL_MAX_DELAY);
  if (status != HAL_OK)
    err = ERR_IF;

  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:
  printf("[Send] len=%u", p->tot_len);
  if (err == ERR_OK)
    printf("\n");
  else
    printf(" (failed)\n");
#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)),就返回错误。struct pbuf是要发送的数据,是一个链表,用for循环转换为ETH_BufferTypeDef形式的链表,再调用HAL_ETH_Transmit函数将数据包发送出去,发送的长度为p->tot_len。

再看看数据包的接收:

最开始main函数的主循环中,检测到有数据包后,调用ethernetif_input函数,这个函数又跳转到了low_level_input里面去了。

static struct pbuf *
low_level_input(struct netif *netif)
{
  //struct ethernetif *ethernetif = netif->state;
  struct pbuf *p, *q;
  int i = 0;
  u32_t len; // 这里len必须定义为32位整形
  ETH_BufferTypeDef data = {0};

  /* Obtain the size of the packet and put it into the "len"
     variable. */
  HAL_ETH_GetRxDataBuffer(&heth, &data);
  HAL_ETH_GetRxDataLength(&heth, &len);
  printf("[Recv] len=%u\n", len);
  LWIP_ASSERT("buf.len == len", data.len == 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, data.buffer + 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);
  }
  
  // 确认收到数据
  HAL_ETH_BuildRxDescriptors(&heth);
  if (__HAL_ETH_DMA_GET_FLAG(&heth, ETH_DMA_RX_BUFFER_UNAVAILABLE_FLAG) != RESET)
  {
    printf("Receive buffer unavailable!\n");
    __HAL_ETH_DMA_CLEAR_FLAG(&heth, ETH_DMA_RX_BUFFER_UNAVAILABLE_FLAG);
  }

  return p;
}

此函数先调用HAL_ETH_GetRxDataBuffer函数获取数据包的内容,再调用HAL_ETH_GetRxDataLength函数获取数据包的大小。然后用pbuf_alloc分配pbuf内存,把收到的数据包的内容复制到pbuf内存里面,复制完了之后要释放接收缓冲区,调用HAL_ETH_BuildRxDescriptors函数将该数据包占用的缓冲区还给DMA。最后,判断ETH_DMA_RX_BUFFER_UNAVAILABLE_FLAG标志位,检查之前是否因缓冲区满了。在H7的HAL库中,接收缓冲区满了不需要做额外处理(在F1里面还需要做额外处理)。

【程序运行结果】

开机时如果插了网线,串口的输出如下:

STM32H743ZI DP83848
SystemCoreClock=480000000
MAC address: 00:80:E1:61:84:93
IPv6 link-local address: FE80::280:E1FF:FE61:8493
DP83848 interrupt occurred! status=0x2c20
Link is up!
ETH is started!
[Send] len=350
[Send] len=86
[Send] len=78
[Send] len=86
[Send] len=86
[Send] len=350
[Recv] len=52
[Send] len=70
[Recv] len=52
[Send] len=350
[Recv] len=60
[Recv] len=52
[Send] len=70
[Recv] len=142
[Send] len=86
[Recv] len=86
[Recv] len=60
[Send] len=78
[Recv] len=52
[Send] len=86
IPv6 address 1: 2409:8A62:321:6D60:280:E1FF:FE61:8493
DNS Server: FE80::1
[Recv] len=590
[Send] len=350
[Recv] len=590
[Send] len=42
[Send] len=42
[Send] len=42
[Recv] len=52
[Send] len=42
DHCP supplied address!
IP address: 192.168.1.107
Subnet mask: 255.255.255.0
Default gateway: 192.168.1.1
DNS Server: 192.168.1.1
[Send] len=42
[Send] len=42
[Recv] len=86
[Send] len=86
[Recv] len=60
[Recv] len=52
[Send] len=42
[Send] len=42
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Send] len=86
[Recv] len=78
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
[Recv] len=342
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52

从中可以看到ETH硬件会自动去除收到的数据包的填充部分,从而收到的数据包的长度len小于60字节。若想要像STM32F1那样不去除填充字节,可在DP83848_Start函数的HAL_ETH_GetMACConfig(&heth, &macconf)下方加一句macconf.AutomaticPadCRCStrip = DISABLE。

电脑可以ping通板子的IPv4地址和IPv6地址,以及NetBIOS计算机名:

电脑可以通过IPv4和IPv6地址,以及NetBIOS计算机名,访问板子上的网页服务器:

在路由器管理页面中也能看到STM32设备:

设备名STM32H743ZI_ETH是由ethernetif.c的ethernetif_init函数里面的netif->hostname变量指定的。

  • 4
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巨大八爪鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值