STM32的以太网外设+PHY(LAN8720)使用详解(6):以太网数据接收及发送

0 工具准备

1.野火 stm32f407霸天虎开发板
2.LAN8720数据手册
3.STM32F4xx中文参考手册

1 以太网数据接收及发送

1.1 以太网数据接收(轮询)

1.1.1 检查是否接收到一帧完整报文

使用轮询的方式接收以太网数据是一种简单但是效率低下的方法,为了保证及时处理以太网数据我们需要在主循环内高频轮询是否接收到了以太网数据。轮询的函数为ETH_CheckFrameReceived,内容如下:

uint32_t ETH_CheckFrameReceived(void)
{
  /* check if last segment */
  if(((DMARxDescToGet->Status & ETH_DMARxDesc_OWN) == (uint32_t)RESET) &&
     ((DMARxDescToGet->Status & ETH_DMARxDesc_LS) != (uint32_t)RESET)) 
  {
    DMA_RX_FRAME_infos->Seg_Count++;
    if (DMA_RX_FRAME_infos->Seg_Count == 1)
    {
      DMA_RX_FRAME_infos->FS_Rx_Desc = DMARxDescToGet;
    }
    DMA_RX_FRAME_infos->LS_Rx_Desc = DMARxDescToGet;
    return 1;
  }

  /* check if first segment */
  else if(((DMARxDescToGet->Status & ETH_DMARxDesc_OWN) == (uint32_t)RESET) &&
          ((DMARxDescToGet->Status & ETH_DMARxDesc_FS) != (uint32_t)RESET)&&
            ((DMARxDescToGet->Status & ETH_DMARxDesc_LS) == (uint32_t)RESET))
  {
    DMA_RX_FRAME_infos->FS_Rx_Desc = DMARxDescToGet;
    DMA_RX_FRAME_infos->LS_Rx_Desc = NULL;
    DMA_RX_FRAME_infos->Seg_Count = 1;   
    DMARxDescToGet = (ETH_DMADESCTypeDef*) (DMARxDescToGet->Buffer2NextDescAddr);
  }

  /* check if intermediate segment */ 
  else if(((DMARxDescToGet->Status & ETH_DMARxDesc_OWN) == (uint32_t)RESET) &&
          ((DMARxDescToGet->Status & ETH_DMARxDesc_FS) == (uint32_t)RESET)&&
            ((DMARxDescToGet->Status & ETH_DMARxDesc_LS) == (uint32_t)RESET))
  {
    (DMA_RX_FRAME_infos->Seg_Count) ++;
    DMARxDescToGet = (ETH_DMADESCTypeDef*) (DMARxDescToGet->Buffer2NextDescAddr);
  } 
  return 0;
}

当以太网帧大于我们设置的DMA描述符buffer大小时,以太网帧将会被分成若干段被存储在不同的DMA描述符中,DMA描述符使用接收描述符字0来表示当前DMA描述符是第一个描述符或最后一个描述符或中间描述符:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当DMA描述符是首个描述符时将段计数置为1,保存首个描述符到FS_Rx_Desc同时将Rx描述符指向下一个DMA描述符;当DMA描述符是中间描述符时将段计数+1,同时将Rx描述符指向下一个DMA描述符;当DMA描述符是最后一个描述符时将段计数+1,保存最后一个描述符到LS_Rx_Desc(如果段计数为1也就是一个完整的以太网帧被保存在一个DMA描述符内,保存最后一个描述符到FS_Rx_Desc)同时返回1表明接收到了一帧完整以太网数据。

1.1.2 读取一帧完整报文

在我们检查到接收了一帧完整报文后,就可以调用low_level_input函数读取该帧报文。

FrameTypeDef low_level_input(void)
{
    struct pbuf *p, *q;
    uint32_t len;
    FrameTypeDef frame;
    u8 *buffer;
    __IO ETH_DMADESCTypeDef *DMARxDesc;
    uint32_t bufferoffset = 0;
    uint32_t payloadoffset = 0;
    uint32_t byteslefttocopy = 0;
    uint32_t i = 0;

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

    /* Release descriptors to DMA 将描述符释放到DMA */
    DMARxDesc = 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++)
    {
        DMARxDesc->Status = ETH_DMARxDesc_OWN;
        DMARxDesc = (ETH_DMADESCTypeDef *)(DMARxDesc->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 恢复DMA接收 */
        ETH->DMARPDR = 0;
    }
    return frame;
}

该函数操作流程如下:
(1)获取报文长度
调用ETH_Get_Received_Frame函数会返回以太网帧最后一个描述符存储的报文长度和buffer地址。我们可以将DMA描述符buffer数据拷贝到协议栈buffer中。
(2)释放DMA控制权给DMA
在我们拷贝完了DMA描述符的buffer数据后需要释放DMA控制权,相关语句如下:

for (i = 0; i < DMA_RX_FRAME_infos->Seg_Count; i++)
    {
        DMARxDesc->Status = ETH_DMARxDesc_OWN;
        DMARxDesc = (ETH_DMADESCTypeDef *)(DMARxDesc->Buffer2NextDescAddr);
    }
    DMA_RX_FRAME_infos->Seg_Count = 0;

上述语句将首个DMA描述符到最后一个DMA描述符的控制权交给DMA,最后清空段计数。
(3)检查DMA状态寄存器
涉及到寄存器如下:
在这里插入图片描述
相关bit描述:
在这里插入图片描述
这里检查位7是否为1,如果为1则设置DMASR寄存器的值为0x00000080,然后恢复DMA接收。相关语句如下:

if ((ETH->DMASR & ETH_DMASR_RBUS) != (u32)RESET)
    {
        /* Clear RBUS ETHERNET DMA flag */
        ETH->DMASR = ETH_DMASR_RBUS;
        /* Resume DMA reception 恢复DMA接收 */
        ETH->DMARPDR = 0;
    }

DMARPDR寄存器描述如下:
在这里插入图片描述

1.2 以太网数据接收(中断)

在主循环或者线程内使用轮询的方式判断是否接收到以太网报文效率比较低下,而且容易出现未及时处理接收报文导致溢出的问题。因此,建议使能ETH接收中断,在中断内释放信号量然后处理以太网数据。
打开ETH接收中断语句如下:

NVIC_InitStructure.NVIC_IRQChannel = ETH_IRQn;  // 以太网中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0X00; // 中断寄存器组2最高优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0X00;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

ETH_DMAClearITPendingBit(ETH_DMA_IT_R);
ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS);
if (EthStatus == ETH_SUCCESS)
{
    /* 使能接收中断 */
    ETH_DMAITConfig(ETH_DMA_IT_NIS | ETH_DMA_IT_R, ENABLE);
}

使能接收中断涉及的寄存器如下:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
接收中断服务函数如下:

void ETH_IRQHandler(void)
{
    int i;
    FrameTypeDef frame;
    while(ETH_CheckFrameReceived() != 0) // 检测是否收到数据包
    {
        frame = low_level_input();
        printf("Len : %d\r\n", frame.length);
        for (i = 0; i < frame.length; i++)
        {
            printf("%02X ", ((u8 *)frame.buffer)[i]);
        }
        printf("\r\n");
    }
    ETH_DMAClearITPendingBit(ETH_DMA_IT_R);
    ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS);
} 

(1)特别要注意我们这里使用的是while而不是if,因为每次触发接收中断可能接收到了多个报文,我们应该尽快将报文全部取出,避免DMA描述符占用标志一直是CPU。
(2)上面的中断服务函数只是用于演示,我们直接在中断内打印接收到的报文。正常操作是释放信号量或者将接收标志置位,通知RTOS的接收线程或者裸机下的主循环内的回调函数处理。

1.3 以太网数据发送

uint8_t low_level_output(uint8_t *sendBuffer, uint16_t len)
{
    uint8_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;

    memcpy((u8_t *)buffer, (u8_t *)sendBuffer, len);

    /* Prepare transmit descriptors to give to DMA 准备发送描述符给DMA使用 */
    ETH_Prepare_Transmit_Descriptors(len);

    errval = 0;

error:
    errval = -1;
    /* 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;
}

相比起接收,以太网数据发送则显得比较简单,因为DMA描述符的主动操作方在CPU这一侧。上述函数的操作如下:
(1)将待发送数据拷贝到当前跟踪的发送DMA描述符
(2)将跟踪的发送DMA描述符控制权交给DMA,设置DMA描述符相关状态:

uint32_t ETH_Prepare_Transmit_Descriptors(u16 FrameLength)
{   
  uint32_t buf_count =0, size=0,i=0;
  __IO ETH_DMADESCTypeDef *DMATxDesc;

  /* Check if the descriptor is owned by the ETHERNET DMA (when set) or CPU (when reset) */
  if((DMATxDescToSet->Status & ETH_DMATxDesc_OWN) != (u32)RESET)
  {  
    /* Return ERROR: OWN bit set */
    return ETH_ERROR;
  }

  DMATxDesc = DMATxDescToSet;
  
  if (FrameLength > ETH_TX_BUF_SIZE)
  {
    buf_count = FrameLength/ETH_TX_BUF_SIZE;
    if (FrameLength%ETH_TX_BUF_SIZE) buf_count++;
  }
  else buf_count =1;

  if (buf_count ==1)
  {
    /*set LAST and FIRST segment */
    DMATxDesc->Status |=ETH_DMATxDesc_FS|ETH_DMATxDesc_LS;
    /* Set frame size */
    DMATxDesc->ControlBufferSize = (FrameLength & ETH_DMATxDesc_TBS1);
    /* Set Own bit of the Tx descriptor Status: gives the buffer back to ETHERNET DMA */
    DMATxDesc->Status |= ETH_DMATxDesc_OWN;
    DMATxDesc= (ETH_DMADESCTypeDef *)(DMATxDesc->Buffer2NextDescAddr);
  }
  else
  {
    for (i=0; i< buf_count; i++)
    {
      /* Clear FIRST and LAST segment bits */
      DMATxDesc->Status &= ~(ETH_DMATxDesc_FS | ETH_DMATxDesc_LS);
      
      if (i==0) 
      {
        /* Setting the first segment bit */
        DMATxDesc->Status |= ETH_DMATxDesc_FS;  
      }

      /* Program size */
      DMATxDesc->ControlBufferSize = (ETH_TX_BUF_SIZE & ETH_DMATxDesc_TBS1);
      
      if (i== (buf_count-1))
      {
        /* Setting the last segment bit */
        DMATxDesc->Status |= ETH_DMATxDesc_LS;
        size = FrameLength - (buf_count-1)*ETH_TX_BUF_SIZE;
        DMATxDesc->ControlBufferSize = (size & ETH_DMATxDesc_TBS1);
      }

      /* Set Own bit of the Tx descriptor Status: gives the buffer back to ETHERNET DMA */
      DMATxDesc->Status |= ETH_DMATxDesc_OWN;

      DMATxDesc = (ETH_DMADESCTypeDef *)(DMATxDesc->Buffer2NextDescAddr);
    }
  }
  
  DMATxDescToSet = DMATxDesc;

  /* When Tx Buffer unavailable flag is set: clear it and resume transmission */
  if ((ETH->DMASR & ETH_DMASR_TBUS) != (u32)RESET)
  {
    /* Clear TBUS ETHERNET DMA flag */
    ETH->DMASR = ETH_DMASR_TBUS;
    /* Resume DMA transmission*/
    ETH->DMATPDR = 0;
  }

  /* Return SUCCESS */
  return ETH_SUCCESS;   
}

这个函数篇幅有点长,这里举例说一下当我们发送的以太网报文可以被一个发送DMA描述符容纳时的操作:
(2.1)设置发送DMA描述符的LS和FS位为1,也就是一个发送DMA描述符对应一个以太网报文:

DMATxDesc->Status |=ETH_DMATxDesc_FS|ETH_DMATxDesc_LS;

在这里插入图片描述
在这里插入图片描述
(2.2)设置报文长度:

DMATxDesc->ControlBufferSize = (FrameLength & ETH_DMATxDesc_TBS1);

在这里插入图片描述
(2.3)设置发送DMA描述符控制权为DMA

DMATxDesc->Status |= ETH_DMATxDesc_OWN)

(2.4)将跟踪发送DMA描述符指向下一个发送DMA描述符
(3)当Tx Buffer不可用标志被设置时,清除该标志并恢复传输
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里的DMA就相当于头指针,而CPU则相当于尾指针。

2 总结

(1)以太网数据接收可以使用轮询和中断2种方式,建议使用中断方式在中断内释放信号量通知以太网报文接收线程进行处理
(2)发送DMA描述符运作方式类似于环形buffer,CPU是尾指针,DMA是头指针;接收DMA描述符运作方式类似于环形buffer,CPU是头指针,DMA是尾指针
(3)在接收以太网数据时一定要及时取出DMA描述符中的数据,将控制权交还给DMA,避免报文阻塞
(4)在发送以太网数据时一定要及时清除Tx Buffer标志并恢复发送