IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> lwip系列一之数据的收发 -> 正文阅读

[网络协议]lwip系列一之数据的收发

lwip系列一之数据的收发

lwip宏观的

经过一段时间的反复折磨,也看了许多资料,做一下学习总结,同时希望通过向他人表述来加深对内容的理解。驱动程序是参照野火的,但是我觉得这里面有点小小的疑问没有解决。

我不知道大家曾经是否有和我一样的疑问,学完计算机网络后,对计算机网络的各个层次的原理有所了解,但是有个疑问就是如何将这个协议起来,为了能更好的说明数据收发过程,我们暂时将这个协议当成一个黑盒子,观察其如何在计算机中起来。

参考下面的图

image-20220221223233960

将lwip看成是看成是黑盒子,他能接收数据,然后对其进行处理,怎么处理暂时不管,然后递交给应用端;

反之,应用端发送数据,经过协议处理后,递交给外设,然后发送出去。

所以说这个lwip可以认为是一个消息处理器,对接收的消息进行处理,对发送的消息进行处理。

所以说可以直接将这个协议栈打包成一个高优先级的线程,干什么呢,不断的去读取邮箱中的需要处理的数据(包含发送和接收)谁先来,谁先处理。这样就将协议“立”起来了。

实际中就有一个线程,叫做tcpip_thread,有个邮箱tcpip_mbox来存放等待处理的消息,这个线程在干什么事呢?

不断的尝试从邮箱中取数据,取到呢,就进行消息处理

取不到呢?判断一下有没有超时事件,

没有超时事件,那就一直阻塞,任务进入阻塞态,让其他任务运行

有超时事件,获取下次超时的时间,然后阻塞这段时间,然后进行超时检查。大致的逻辑是这样的。具体的代码细节,暂时不管,这里主要是方便我的记忆。

这样就在整体上能对lwip有个认识。

然后呢,关注数据的收发了,这里呢只描述上图左边,靠近底层的数据收发的实现过程。就是在那种软件、硬件交叉的地方。

数据的接收过程

我们先来看一张逻辑框图:

image-20220220155306248

对于接收过程:我们传输的电平信号通过网线的接口进入到外部PHY中,然后再被MAC所接收,当然这个MAC具有地址过滤的机制们也就是说能根据MAC地址能进行过滤,还会进行CRC校验进行帧的接收,这些是可配置的,配置完后,硬件会自动帮你做的,还会硬件帮你过滤掉以太网帧的前导符,帧首符,还有CRC校验字段。

MAC完成到它的功能后,就会将接收到的数据放入2k字节的接收FIFO中,然后呢,通过DMA将数据传送至物理内存,直白一点说就是传送至你定义数组啊之类的能存放数据的空间。

对于发送过程:就是上述的逆过程了。

数据包装过程

我们知道,tcp/ip是不同层次的,如果是层与层之间递交数据时,要发生数据的拷贝,那么整个lwip内核的运行效率就会十分低下。

所以说为了避免数据产生层层拷贝,引入了pbuf结构体,通过一个指向该结构体的指针来访问全部数据。

该结构体的原理图如下:

image-20220222110113487

此时我们大致来说说数据的流向:

数据被以太网外设接收后,再通过DMA传输至物理内存,这里的物理内存呢实际上就是数组(也称为缓存),也就是说数据经过DMA后,就到一个数组中,等着你来用。你不能直接占据这个空间来使用,因为这个空间还需要接收其他数据,所以数据来了后要马上把这个空间的数据搬出来,把这个空间释放出来以接收其他以太网数据,因为以太网一直不断的在接收数据,你不出来,后续数据不就没有空间可以放了吗。

所以说,一旦有数据之后呢,就要立刻去将数据清出来,放入上面提及的pbuf中。

为了实现上面这句话,在lwip中呢,创建了一个计数信号量和一个接收线程来实现任务同步的功能,为什么是计数信号量而不是二值信号量呢,二值不适合频繁发生中断的场合。

在以太网外设初始化时,初始化为接收完成中断,然后在接收中断完成的回调函数中释放信号量,信号量的释放导致接收线程的运行,接收线程干什么事呢?核心就是将缓存中的数据清出来,然后包装成pbuf,然后对pbuf再进行简单的包装变成消息,然后投递给前面提到的tcpip_mbox,然后线程tcpip_thread就运行起来了,就可以开始处理消息了。

到这里为止,我们涉及到关键的二个线程,一个邮箱,一个计数信号量。

一个tcp/ip处理线程tcpip_thread:不断地尝试去读邮箱的消息,然后进行消息处理

一个先进先出的邮箱tcpip_mbox:不管是接收的还是需要发送的,最终都会在邮箱中排队,等待处理

一个计数信号量s_xSemaphore:用于在接收完成时,触发中断,在中断回调中释放信号量,触发接收线程的运行

一个接收线程ethernetif_input:核心是将缓存的数据清出来,包装成pbuf,再包装成消息,发到邮箱tcpip_mbox

上述呢,基本上将整个lwip的运行,以及数据的流向大致有个印象。当然要通过一篇文章将所有方方面面讲清楚是不可能的,最终要搞清楚什么的,必须啃代码,这里只是整体有个印象,能将整个过程联通起来。

再细节一点

前面提到数据的流向个过程是

缓存——>pbuf——>消息。前面认为缓存是一个数组,确实是一个数组,每个缓存呢对应有一个描述符来管理,这个描述符是个结构体,按道理说是软件来管理的,但是实际上确是软件定义了这个结构体,但是里面一些状态字段的更新之类的却是硬件帮你做的,搞得有点像寄存器,这是我个人就觉得是最抽象的地方,你说你软件搞得,看看程序就行,硬件搞得,看看原理就行,对吧,这里呢,这个描述符搞得又与软件相关,又与硬件相关。先看看几个定义:


 ETH_DMADescTypeDef  DMARxDscrTab[ETH_RXBUFNB] ;/*接收描述符 */


 ETH_DMADescTypeDef  DMATxDscrTab[ETH_TXBUFNB] ;/* 发送描述符 */


 uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] ; /* 接收缓冲区 */


 uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] ; /* 发送缓冲区 */

从上面可以看到,描述符是就是特定类型的结构,当然结构体内部成员字段代表什么含义就去看参考手册,这里不说,定义的是一个结构体数组,每个描述符对应一个缓存,缓存就是数组,来源于下面的Rx_Buff缓冲区(本质上就是一个二维数组)

这里区别一下缓存与缓冲区的关系,缓存是缓冲区的一部分,像野火的驱动设计中就设计缓存为1/8的缓冲区大小。缓存就是缓冲区的一个子集。

我们接收到的数据呢,就存放在缓存里,我们发送数据呢,就将数据放入发送的缓冲区中,并将发送描述符第一个成员字段的OWN置位,就把数据发送出去了。

我们通过下面一张图来描述缓存,描述符的关系。

image-20220219223116744

写过一点程序的就知道,光定义一个结构体,内部是空的,所以要建立上图中的这个样子,需要在初始化时调用一个函数

HAL_ETH_DMATxDescListInit(&heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);来建立起上图的关系。这个函数会在后续进行注释,可以看看其是怎么样的过程。

好了,到此,基本上呢原理性的东西基本上说的差不多了。下面能就对关键的代码语句进行说明与注释,当然有些东西在一篇文章中没法说的特别详细,大致看看,同时加深自己的印象。

数据接收过程关键性代码阅读

1)前面提到过,在接收中断中的服务函数中接收完成回调函数中释放信号量:来触发接收线程的运行,代码如下:

extern xSemaphoreHandle s_xSemaphore;
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
  LED2_TOGGLE;
  portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
  xSemaphoreGiveFromISR( s_xSemaphore, &xHigherPriorityTaskWoken );//释放信号量
  portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

2)释放信号量后,线程ethernetif_input运行,这个线程的代码如下:核心逻辑还是如文章上面所说,具体涉及的重要函数的注释放在文章后面,感兴趣的可以看看

void ethernetif_input(void *pParams) 
{
	struct netif *netif;
	struct pbuf *p = NULL;
	netif = (struct netif*) pParams;
	while(1) 
  {
    if(xSemaphoreTake( s_xSemaphore, portMAX_DELAY ) == pdTRUE)//获取信号量
    {
      taskENTER_CRITICAL();
TRY_GET_NEXT_FRAGMENT:
      p = low_level_input(netif);//将缓存数据取出,并打包成pbuf
      taskEXIT_CRITICAL();
      if(p != NULL)
      {
        taskENTER_CRITICAL();
        if (netif->input(p, netif) != ERR_OK)//将pbuf打包成消息,并发送至邮箱中这个函数是
            //tcpip_input(struct pbuf *p, struct netif *inp)
        {
          LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
          pbuf_free(p);
          p = NULL;
        }
        else
        {
          xSemaphoreTake( s_xSemaphore, 0);
          goto TRY_GET_NEXT_FRAGMENT;
        }
        taskEXIT_CRITICAL();
      }
    }
  }
}

函数low_level_input的注释

static struct pbuf * low_level_input(struct netif *netif)
{
  struct pbuf *p = NULL;
  struct pbuf *q = NULL;
  uint16_t len = 0;
  uint8_t *buffer;
  __IO ETH_DMADescTypeDef *dmarxdesc;//以太网接收描述符
  uint32_t bufferoffset = 0;
  uint32_t payloadoffset = 0;
  uint32_t byteslefttocopy = 0;
  uint32_t i=0;
  
  
  /* get received frame ,该函数返回HAL_OK,说明数据已经DMA至数据存储区了,但是
  这个是野火的驱动程序写法,这里有个疑问,因为在野火的驱动程序中,因为他的描述符对应的缓存空间为1524,能够放下最大的以太网数   据,所以说,这个low_level_input这样写是没问题,但是如果说你分配的每个缓存的空间不一定能容纳最大以太网帧,那么我觉得这么   就非常有问题
  */
  if (HAL_ETH_GetReceivedFrame(&heth) != HAL_OK)
  {
//    PRINT_ERR("receive frame faild\n");
    return NULL;
  }
  /* Obtain the size of the packet and put it into the "len" variable. */
  len = heth.RxFrameInfos.length;//获取帧长
  buffer = (uint8_t *)heth.RxFrameInfos.buffer;//获取帧信息的接收缓冲区的地址
  
  PRINT_INFO("receive frame len : %d\n", len);
  
  if (len > 0)
  {
    /* We allocate a pbuf chain of pbufs from the Lwip buffer pool */
    p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);//分配的是帧长的空间,类型是pbuf,(可能有多个)
  }
  
  if (p != NULL)
  {
    dmarxdesc = heth.RxFrameInfos.FSRxDesc;//获取帧的第一个描述符,野火中一个描述符能能缓存一个帧
    bufferoffset = 0;//缓存中的的数据偏移
    for(q = p; q != NULL; q = q->next)//遍历pbuf
    {
      byteslefttocopy = q->len;//总共剩下的需要拷贝的字节数,初始为总的帧长
      payloadoffset = 0;//数据偏移,指的是拷贝到哪了
      
      /* Check if the length of bytes to copy in current pbuf is bigger than Rx buffer size*/
      while( (byteslefttocopy + bufferoffset) > ETH_RX_BUF_SIZE )//如果帧长大于缓存大小(野火中的驱动不会大于)
      {
        /* Copy data to pbuf */
        memcpy( (uint8_t*)((uint8_t*)q->payload + payloadoffset), (uint8_t*)((uint8_t*)buffer + bufferoffset), (ETH_RX_BUF_SIZE - bufferoffset));//从接收缓冲区拷贝数据(缓存大小)到pbuf的数据区域,
        
        /* Point to next descriptor */
        dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);//指向下一个描述符
        buffer = (uint8_t *)(dmarxdesc->Buffer1Addr);//同样获取缓冲区地址
        
        byteslefttocopy = byteslefttocopy - (ETH_RX_BUF_SIZE - bufferoffset);//剩余需拷贝字节数
        payloadoffset = payloadoffset + (ETH_RX_BUF_SIZE - bufferoffset);//拷贝到这了
        bufferoffset = 0;
      }//直到最后一个描述符退出
      /* Copy remaining data in pbuf */
      memcpy( (uint8_t*)((uint8_t*)q->payload + payloadoffset), (uint8_t*)((uint8_t*)buffer + bufferoffset), byteslefttocopy);//拷贝剩下的字节
      bufferoffset = bufferoffset + byteslefttocopy;
    }//到此拷贝进pbuf中
  }  
  
    /* Release descriptors to DMA */
    /* Point to first descriptor */
    dmarxdesc = heth.RxFrameInfos.FSRxDesc;//指向帧信息的第一个描述符
    /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
    for (i=0; i< heth.RxFrameInfos.SegCount; i++)//遍历帧信息的每个描述符(野火中一个帧帧信息描述符只有一个)
    {  
      dmarxdesc->Status |= ETH_DMARXDESC_OWN;//置位own,代表的是DMA拥有,只有复位时CPU才能去读取描述符中的缓存数据
      dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
    }
    
    /* Clear Segment_Count */
    heth.RxFrameInfos.SegCount =0;  //释放过程的清0 操作
  
  /* When Rx Buffer unavailable flag is set: clear it and resume reception
  寄存器标志位清0 的操作
  */
  if ((heth.Instance->DMASR & ETH_DMASR_RBUS) != (uint32_t)RESET)  
  {
    /* Clear RBUS ETHERNET DMA flag */
    heth.Instance->DMASR = ETH_DMASR_RBUS;
    /* Resume DMA reception */
    heth.Instance->DMARPDR = 0;
  }
  return p;//最后返回PBUF
}

函数tcpip_input注释

err_t
tcpip_input(struct pbuf *p, struct netif *inp)
{
#if LWIP_ETHERNET
  if (inp->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
    return tcpip_inpkt(p, inp, ethernet_input);
  } else
#endif /* LWIP_ETHERNET */
    return tcpip_inpkt(p, inp, ip_input);
}

函数tcpip_inpkt注释

该函数是将数据打包成消息,并将消息发送至邮箱中

err_t
tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn)
{

  struct tcpip_msg *msg;

  msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);
  if (msg == NULL) 
  {
    return ERR_MEM;
  }
  msg->type = TCPIP_MSG_INPKT;
  msg->msg.inp.p = p;
  msg->msg.inp.netif = inp;
  msg->msg.inp.input_fn = input_fn;//消息处理函数ethernet_input
  if (sys_mbox_trypost(&tcpip_mbox, msg) != ERR_OK) //向邮箱投递消息
  {
    memp_free(MEMP_TCPIP_MSG_INPKT, msg);
    return ERR_MEM;
  }
  return ERR_OK;
}

数据发送过程关键性代码阅读

数据发送最终就是将数据放到发送缓存中,然后就可以发送出去,与接收不同的是,数据接收对应有个接收线程,发送没有发送线程,数据要通过以太网发送出去,最终都要调用函数ethernet_output

函数ethernet_output注释

/*
这个函数是


*/
/*
参数分别是1网卡
2,pbuf
3,源以太网地址
4,目的以太网地址
5,以太网帧类型

*/
err_t
ethernet_output(struct netif * netif, struct pbuf * p,
                const struct eth_addr * src, const struct eth_addr * dst,
                u16_t eth_type) {
  struct eth_hdr *ethhdr;
  u16_t eth_type_be = lwip_htons(eth_type);
  {
    if (pbuf_add_header(p, SIZEOF_ETH_HDR) != 0) 
        //这个函数就是改变pbuf的payload()本身预留有空间,len,tot_len的值,
    {
      goto pbuf_header_failed;
    }
  }


  ethhdr = (struct eth_hdr *)p->payload;//指向以太网帧头部
  ethhdr->type = eth_type_be;//填充类型
  SMEMCPY(&ethhdr->dest, dst, ETH_HWADDR_LEN);//复制MAC地址 
  SMEMCPY(&ethhdr->src,  src, ETH_HWADDR_LEN);

  /* send the packet */
  return netif->linkoutput(netif, p);//这个网卡的linkoutput本质上是low_level_output

pbuf_header_failed:

  return ERR_BUF;
}
/*
这个函数也是很复杂,主要就是通过将pbuf的数据发送出去,那么就需要将pbuf中的数据通过以太网发送描述符放到缓存中去
然后建立起对应的描述符,最后核心就是将OWN置位,这样DMA就可以开始其工作,将数据发送至发送FIFO中,再通过MAC发送出去。

*/
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
	static sys_sem_t ousem = NULL;
	if(ousem == NULL)
  {
    sys_sem_new(&ousem,0);//创建一个二值信号量
    sys_sem_signal(&ousem);//先释放一个信号量,获取时就不会堵塞
  }
  err_t errval;
  struct pbuf *q;

  uint8_t *buffer = (uint8_t *)(heth.TxDesc->Buffer1Addr);//缓存地址为发送描述符的地址
  __IO ETH_DMADescTypeDef *DmaTxDesc;
  uint32_t framelength = 0;//帧长
  uint32_t bufferoffset = 0;
  uint32_t byteslefttocopy = 0;//剩余的需要拷贝的字节数
  uint32_t payloadoffset = 0;
  DmaTxDesc = heth.TxDesc;//指向第一个发送描述符
  bufferoffset = 0;
  /* Check if the descriptor is owned by the ETHERNET DMA (when set) or CPU (when reset) */
  /* Is this buffer available? If not, goto error */
  if((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)//如果是OWN置位,说明DMA拥有出错
  {
    errval = ERR_USE;
    goto error;
  }
  sys_sem_wait(&ousem);//核心还是获取信号量,(一直等待),为什么这里这么写不清楚。
  
  /* copy frame from pbufs to driver buffers */
  for(q = p; q != NULL; q = q->next)//遍历pbuf
    {
      /* Get bytes in current lwIP buffer */
      byteslefttocopy = q->len;
      payloadoffset = 0;
    
      /* Check if the length of data to copy is bigger than Tx buffer size*/
      while( (byteslefttocopy + bufferoffset) > ETH_TX_BUF_SIZE )//当帧长大于缓存(野火的不会)
      {
        /* Copy data to Tx buffer*/
        memcpy( (uint8_t*)((uint8_t*)buffer + bufferoffset), (uint8_t*)((uint8_t*)q->payload + payloadoffset), (ETH_TX_BUF_SIZE - bufferoffset) );//拷贝数据到缓存中从pbuf中
      
        /* Point to next descriptor */
        DmaTxDesc = (ETH_DMADescTypeDef *)(DmaTxDesc->Buffer2NextDescAddr);//指向下一个描述符
      
        /* Check if the buffer is available ,*/
        if((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
        {
          errval = ERR_USE;
          goto error;
        }
      
        buffer = (uint8_t *)(DmaTxDesc->Buffer1Addr);//下个描述符的缓存地址
      
        byteslefttocopy = byteslefttocopy - (ETH_TX_BUF_SIZE - bufferoffset);//计算剩余字节数
        payloadoffset = payloadoffset + (ETH_TX_BUF_SIZE - bufferoffset);//已拷贝字节数
        framelength = framelength + (ETH_TX_BUF_SIZE - bufferoffset);//计算帧长
        bufferoffset = 0;
      }
    
      /* Copy the remaining bytes 拷贝剩下的*/
      memcpy( (uint8_t*)((uint8_t*)buffer + bufferoffset), (uint8_t*)((uint8_t*)q->payload + payloadoffset), byteslefttocopy );
      bufferoffset = bufferoffset + byteslefttocopy;//最后一个描述符对应的字节数
      framelength = framelength + byteslefttocopy;
    }
  
  /* Prepare transmit descriptors to give to DMA
  前面已经将数据放至缓存中,并且描述符已经建立好对应关系了
  
  */ 
  HAL_ETH_TransmitFrame(&heth, framelength);//核心就是置位OWN,然后DMA就可以开始工作了。
  
  errval = ERR_OK;

error:
  
  /* When Transmit Underflow flag is set, clear it and issue a Transmit Poll Demand to resume transmission */
  if ((heth.Instance->DMASR & ETH_DMASR_TUS) != (uint32_t)RESET)
  {
    /* Clear TUS ETHERNET DMA flag */
    heth.Instance->DMASR = ETH_DMASR_TUS;

    /* Resume DMA transmission*/
    heth.Instance->DMATPDR = 0;
  }
  
  sys_sem_signal(&ousem);//这个还是释放信号量,暂时不知道为什么
  
  return errval;
}

线程tcpip_thread删了一小部分

static void
tcpip_thread(void *arg)
{
  struct tcpip_msg *msg;

  while (1) 
  {                          /* MAIN Loop */

    TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);//取消息,并超时检查
    if (msg == NULL) 
    {
      continue;
    }
    tcpip_thread_handle_msg(msg);
  }
}

该线程核心就是不断尝试取邮箱中消息,然后进行消息处理。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-02-26 12:07:35  更:2022-02-26 12:10:41 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 7:42:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码