工作笔记-----基于FreeRTOS的lwIP网络任接收过程,从MAC至协议栈
基于FreeRTOS的lwIP网络任接收过程,从MAC至协议栈
@@ Author:明月清了个风
@@ Date: 2025/9/15
@@ PS:在之前已经写过一篇有关lwip的文章了,其中也涉及到了部分lwip的初始化过程(链接在这)。本文主要针对基于FreeRTOS的lwIP(版本2.1.2)网络接收任务中涉及的关键流程和函数进行了更进一步的梳理,包含了lwip初始化过程,MAC与phy的初始化过程,lwip网络接收任务ethernetif_input的流程以及最后如何传递至lwip的内核协议栈处理,如有错误和问题,可以在评论指出,会加以修改.
一. low_level_init()函数
在之前的lwip网络初始化过程中,有一个比较关键的函数low_level_init
,该函数中完成了网络任务的硬件层初始化,以下为该函数的简化执行内容,忽略了部分宏定义。
low_level_init()|--->初始化ETH_HandleTypeDef heth|--->HAL_ETH_Init()|--->HAL_ETH_MspInit()---硬件GPIO初始化,网络中断ETH_IRQn|--->选择RMII还是MII接口|--->软件复位MAC并设置SMI时钟|--->初始化PHY|--->ETH_MACDMAConfig()---设置MAC和DMA|--->HAL_ETH_DMATxDescListInit()---以太网DMA发送描述符初始化|--->HAL_ETH_DMARxDescListInit()---以太网DMA接收描述符初始化|--->lwip的虚拟网卡netif的相关初始化--(LWIP_ARP宏开启)|--->创建s_xSemaphore互斥量,创建ethernetif_input接收任务|--->HAL_ETH_Start()
接着一步步来看
-
初始化ETH_HandleTypeDef heth
主要是对一些
heth.init
结构体成员的赋值,这些值在后面的过程中都会被用到,包含以下几项typedef struct {uint32_t AutoNegotiation; /* 是否使能PHY芯片的自动协商 */ uint32_t Speed; /* 网络通信速度 */ uint32_t DuplexMode; /* 通讯模式 */ uint16_t PhyAddress; /* phy芯片地址 */ uint8_t *MACAddr; /* MAC地址 */ uint32_t RxMode; /* 网络数据接收模式 */ uint32_t ChecksumMode; /* 校验和模式 */ uint32_t MediaInterface ; /* 网络介质接口选择 */ } ETH_InitTypeDef;
📖AutoNegotiation:这里首先要引入两个概念,MCU的Eth外设也就是MAC属于网络模型中的数据链路层,外部phy芯片,如LAN8742A这种属于物理层。当我们设置eth时就是在操作MAC,而phy属于外部芯片,需要通过SMI站管理接口进行设置,下图时stm32f745的以太网外设框图,图中蓝色框出的属于stm32的内部外设,绿色框出的是外部phy芯片,SMI站管理接口是图中红色框出的MDC和MDIO两根线。stm32通过SMI对phy芯片的寄存器进行读写以达到配置的目的。
我们已经知道了STM32如何配置外部的phy芯片,那么上面的自动协商其实是要开启phy芯片的一个功能,也就是让phy芯片自动检测MCU支持的网络通信速度以及双工通信模式,并且选择双方都能适配的最佳选择,如果不启用该功能,需要将两边都强制设置相同的速度及模式才可以进行通讯.
📖关于phy芯片地址和MAC地址不必多说,之前调试网络时一直不同就是phy芯片地址配置错误,对于MAC地址后面会再讲他的作用.
📖网络数据接收模式共有两种:一种是中断接收,一种是轮询接收.两者是实时性的差别.
📖校验和模式分两种,一种是软件校验,一种是硬件校验
📖网络介质接口也是两种,分别是RMII和MII
-
第二步是函数
HAL_ETH_Init()
中的初始化过程,传入的参数就是上面初始化过的heth
以太网句柄首先对上面设置的参数进行合法性判断,然后将其传入
HAL_ETH_MspInit(heth)
函数进行ETH硬件初始化,也就是ETH外设用到的GPIO的配置以及以太网中断.然后通过下面两行代码设置了介质接口,用到的参数就是第一步里设置的结构体成员,注意这里设置的寄存器并不在以太网外设一章中,在SYSCFG寄存器里。
SYSCFG->PMC &= ~(SYSCFG_PMC_MII_RMII_SEL); /* 清除选项 */SYSCFG->PMC |= (uint32_t)heth->Init.MediaInterface; /* 设置选项 */
-
然后是第三部软件复位MAC并初始化SMI时钟线
在初始化MAC之前进行了以太网的软件复位,操作的是以太网DMA总线模式寄存器
ETH_DMABMR->SR
,代码及该位介绍如下,也就是在设置MAC子系统的功能前需要等待硬件将该位清0(heth->Instance)->DMABMR |= ETH_DMABMR_SR; /* 将该位置1 */
然后,对以太网MAC MII地址寄存器
ETH_MACMIIAR->CR
字段进行了设置,根据HCLK时钟值设置了该字段,该字段的描述如图:注意这里的MDC就是上面提到的MCU管理外部PHY芯片使用到的SMI站管理接口的MDC线,也就是与phy芯片通信时使用的时钟信号,原文该部分内容如下
初始化了SMI的时钟线后就可以通过SMI对phy芯片进行配置了。
-
初始化PHY
首先调用了
HAL_ETH_WritePHYRegister(heth, PHY_BCR, PHY_RESET)
,也就是向PHY芯片的PHY_BCR
寄存器中写入PHY_RESET
,该函数会对ETH_MACMIIAR
寄存器中除了上文中提到的为SMI的MDC时钟设置的CR字段外的所有字段,也就是下图中红色框中的字段,其中,MR
字段就是phy芯片中的目标地址PHY_BCR
要写入的值,也就是
PHY_RESET
会存入ETH_MACMIIDR
寄存器进行传递,将待写入phy芯片的值写入ETH_MACMIIDR
后会硬件触发(应该是硬件触发吧😆,原文没有明确说明,原文如下:)当应用程序将 MII 写入位和繁忙位(在以太网 MAC MII 地址寄存器 (ETH_MACMIIAR) 中)
置 1 时,SMI 将通过传输 PHY 地址、PHY 中的寄存器地址以及写入数据(在以太网 MAC
MII 数据寄存器 (ETH_MACMIIDR) 中)来触发对 PHY 寄存器进行写操作。事务进行期间,
应用程序不应更改 MII 地址寄存器的内容或 MII 数据寄存器。在此期间对 MII 地址寄存器或
MII 数据寄存器执行的写操作将会忽略(繁忙位处于高电平状态),事务将无错完成。写操
作完成后,SMI 将通过复位繁忙位进行指示。所谓的繁忙位指示就是
ETH_MACMIIAR->MB
字段,直到MAC内核将其硬件清零后才能重新进行上述的写入操作。在上面对phy芯片进行复位之后就会判断之前设置的
heth->init.AutoNegotiation
,也就是是否开启了自动协商-
若开启了自动协商,首先在一个
do {}while
中读取PHY的PHY_BSR
寄存器,判断PHY芯片的连接建立状态,连接成功后在PHY芯片的控制寄存器PHY_BCR
中写入对应的值开启自动协商,然后循环等待自动协商完成,协商结果存入PHY_SR
寄存器,该过程同样是读取PHY_BSR
寄存器;之后就可以通过读取PHY_SR
寄存器判断双工模式和通讯速度。 -
若不开启自动协商,那么会直接将第一步中设置的
heth->init
中的DuplexMode
和Speed
写入PHY_BCR
,让PHY芯片进行配置。
至此,PHY芯片的内容初始化完成
-
-
设置MAC和DMA
接下来调用
ETH_MACDMAConfig(heth, err)
设置MAC和DMA,这个函数非常长有300多行,主要是对于MAC内核功能和DMA的设置,都是通过对寄存器的配置进行的 -
初始化接收和发送的DMA描述符
下面会讲到
-
lwip中的netif虚拟网卡相关成员初始化
这里初始化了硬件的MAC地址,MTU(maximum transfer unit最大传输单元)以及netif的flags成员。
需要注意的是,使用TCP连接依赖与ARP,TCP的发送依靠ip地址进行,而要传输至具体设备位置依赖于MAC地址,ARP的工作就是将IP地址与MAC地址之间建立映射,当然,这是基于局域网的。
-
创建
s_xSemaphore
信号量,创建ethernetif_input
任务这个后面细讲。
-
最后设置了PHY芯片的链路断开中断
二. ethernetif_input
接收任务
接下来看上面创建的内部网络接收任务ethernetif_input
,其代码如下,非常短
void ethernetif_input(void const * argument)
{struct pbuf *p;struct netif *netif = (struct netif *) argument;for( ;; ){if (osSemaphoreWait(s_xSemaphore, TIME_WAITING_FOR_INPUT) == osOK){do{LOCK_TCPIP_CORE();p = low_level_input( netif );if (p != NULL){if (netif->input( p, netif) != ERR_OK ){pbuf_free(p);}}UNLOCK_TCPIP_CORE();} while(p!=NULL);}}
}
可以看到,该任务会阻塞等待互斥量s_xSemaphore
,该互斥量由函数HAL_ETH_RxCpltCallback(heth)
释放,其在以太网中断函数中被调用,响应ETH_DMA_FLAG_R
中断标志位,该含义如下:
需要注意的是,这里的接收已完成指的是已经有完整的以太网帧被放入了low_level_init()
函数中创建的DMA描述符所拥有的缓冲中,虽然触发了中断,但是并不会中止接收的过程。
在中断释放了互斥量后,ethernetif_input
开始处理过程,这是一个do{}while
循环,不停的从DMA缓冲中拿出数据,通过lwip内部的pbuf结构进行存储,这个过程的第一个函数是 p = low_level_input( netif );
low_level_input()
-
该函数首先调用了
HAL_ETH_GetReceivedFrame_IT()
获取DMA描述符接收到的数据帧。函数主要执行内容是一个while循环。先来看这个循环的判断条件
while (((heth->RxDesc->Status & ETH_DMARXDESC_OWN) == (uint32_t)RESET) && (descriptorscancounter < ETH_RXBUFNB)) {}
第一个条件
((heth->RxDesc->Status & ETH_DMARXDESC_OWN) == (uint32_t)RESET)
的意思是以太网句柄的DMA接收描述符成员RxDesc
的状态表示为0,可以看到ETH_DMARXDESC_OWN
意思是DMA描述符是否为DMA所拥有,来看一下他是什么时候被初始化的。我们看到
low_level_init()
函数的第6步初始化接收和发送的DMA描述符,因为接收和发送的初始化过程是基本一致的,因此我们这里就看接收就行了,也就是这个函数HAL_ETH_DMARxDescListInit()
,根据名字就可以知道DMA接收描述符是一个List,这在freeRTOS中对应着链表。传入的四个参数分别为一直用到的以太网句柄heth
,DMA接收描述符数组DMATxDscrTab
,接收缓冲区首地址&Rx_Buff[0][0]
,DMA描述符数组包含的描述符数量ETH_RXBUFNB
。这里的描述符数量ETH_RXBUFNB
不仅是描述符数组的大小,同时也是接收缓冲区的一维大小,而缓冲区第二维的大小为ETH_RX_BUF_SIZE
,在stm32f7xx_hal_eth.h
中定义为1524,也就是每个描述符都对应了一个1524大小的数据缓冲区然后来看DMA接收描述符是如何被初始化的,首先通过
heth->RxDesc = DMARxDescTab
将描述符数组首地址赋值给了heth->RxDesc
,然后通过for(i=0; i < RxBuffCount; i++)
遍历描述符数组,这里的RxBuffCount
就是传入的参数ETH_RXBUFNB
。在循环中,对于每一个以太网描述符都会进行下面的操作
DMARxDesc->Status = ETH_DMARXDESC_OWN; DMARxDesc->ControlBufferSize = ETH_DMARXDESC_RCH | ETH_RX_BUF_SIZE; DMARxDesc->Buffer1Addr = (uint32_t)(&RxBuff[i*ETH_RX_BUF_SIZE]); if((heth->Init).RxMode == ETH_RXINTERRUPT_MODE) {/* Enable Ethernet DMA Rx Descriptor interrupt */DMARxDesc->ControlBufferSize &= ~ETH_DMARXDESC_DIC; }
第一句就是将其status标记为
ETH_DMARXDESC_OWN
,这意味着该描述符被硬件掌握,也就是DMA以及以太网外设,当以太网外设接收到数据时,会判断一个描述符是否属于自己,只有属于自己的,才会将数据通过DMA写入该描述符对应的缓冲区.第二句设置了一个标志位以及缓冲区的长度(就是上面1524的那个宏),这里的这个标志位
ETH_DMARXDESC_RCH
指示了DMA描述符是通过链表进行连接的,下面可以看到链表的构造过程第三句就是将每一个缓冲区对应到每一个DMA描述符的
Buffer1Addr
成员.第四句根据是否开启了以太网中断模式,如果是中断模式,那么就会将DMA描述符的
ETH_DMARXDESC_DIC
清除,该位若置1表示关闭接收完成中断,注意这里的接收完成是对于每一个描述符适用的,也就是每一个描述符接收完成都会触发中断.最后循环体中建立了DMA描述符的链式结构,将每一个DMA描述符的
Buffer2NextDescAddr
成员指向下一个DMA描述符的地址,并且将最后一个描述符指向第一个描述符if(i < (RxBuffCount-1)){DMARxDesc->Buffer2NextDescAddr = (uint32_t)(DMARxDescTab+i+1); }else{DMARxDesc->Buffer2NextDescAddr = (uint32_t)(DMARxDescTab); }
最后将描述符列表首地址写入以太网 DMA 接收描述符列表地址寄存器 (ETH_DMARDLAR)
(heth->Instance)->DMARDLAR = (uint32_t) DMARxDescTab;
上面所述的DMA描述符过程完全符合参考手册中的描述,如下图:
我们重新回到
HAL_ETH_GetReceivedFrame_IT()
函数中,根据上面的初始化过程,我们知道了该函数的while
判断条件是对应的以太网DMA接收描述符是否为硬件所拥有,也就是是否存储了接受数据帧,如果包含了数据则会继续进行下面的循环体,下面是一个条件判断,先不看每个判断里执行的内容,而是看条件判断if((heth->RxDesc->Status & (ETH_DMARXDESC_FS | ETH_DMARXDESC_LS)) == (uint32_t)ETH_DMARXDESC_FS) {} else if ((heth->RxDesc->Status & (ETH_DMARXDESC_LS | ETH_DMARXDESC_FS)) == (uint32_t)RESET) {} else {}
根据手册,判断内容如下:
也就是由于一帧可能会被分到多个以太网描述符对应的缓冲区中,如果需要还原则需要知道顺序和包含该数据帧的以太网描述符数量.
如果是帧的第一个缓冲区,则会进行以下操作:将当前DMA描述符地址存入
heth->RxFrameInfos.FSRxDesc
,根据名称就可以知道这是存首个描述符的,然后将heth->RxFrameInfos.SegCount
计数值设为1,最后将heth->RxDesc
指向下一个DMA接受描述符heth->RxFrameInfos.FSRxDesc = heth->RxDesc; heth->RxFrameInfos.SegCount = 1; /* Point to next descriptor */ heth->RxDesc = (ETH_DMADescTypeDef*) (heth->RxDesc->Buffer2NextDescAddr);
如果是不是第一个也不是最后一个,对于中间的包含该数据帧的DMA接受描述符,执行操作如下,只会将
heth->RxFrameInfos.SegCount
计数值加1,并且将heth->RxDesc
指向下一个DMA接受描述符,由此我们也可以知道heth->RxDesc
指向的DMA描述符是下一次接受时会被使用的描述符,而不总是第一个./* Increment segment count */(heth->RxFrameInfos.SegCount)++;/* Point to next descriptor */heth->RxDesc = (ETH_DMADescTypeDef*)(heth->RxDesc->Buffer2NextDescAddr);
如果是最后一个包含该数据帧的DMA接受描述符,执行操作如下:
heth->RxFrameInfos.LSRxDesc = heth->RxDesc; /* 记录最后一个描述符地址 */ (heth->RxFrameInfos.SegCount)++; /* 当前数据帧包含描述符数量+1 */ if ((heth->RxFrameInfos.SegCount) == 1) /* 首尾是同一个的情况 */ {heth->RxFrameInfos.FSRxDesc = heth->RxDesc; } heth->RxFrameInfos.length = (((heth->RxDesc)->Status & ETH_DMARXDESC_FL) >> ETH_DMARXDESC_FRAMELENGTHSHIFT) - 4; /* 提取帧长度,并减去4byte的CRC */ heth->RxFrameInfos.buffer =((heth->RxFrameInfos).FSRxDesc)->Buffer1Addr; heth->RxDesc = (ETH_DMADescTypeDef*) (heth->RxDesc->Buffer2NextDescAddr); /* 更新下一个可用的描述符 */
需要注意的是上面的DMA描述符中包含的数据帧长度是多了4byte的CRC校验的,这个在手册里有说明,如下
至此,接受过程调用的第一个函数内容讲解完毕。
-
在上面从DMA接受描述符中提取的信息全部记录到了以太网句柄的接受数据帧信息成员中,即
heth.RxFrameInfos
,通过他可以获取到接受数据帧的长度以及存储该数据帧的首个DMA描述符缓冲区len = heth.RxFrameInfos.length; buffer = (uint8_t *)heth.RxFrameInfos.buffer;
如果有数据,那么
len > 0
,进而会为其分配lwip用于存储数据的数据结构pbuf
p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
分配函数
pbuf_alloc
的三个参数分别为:类型为枚举量pbuf_layer
的PBUF_RAW
,pbuf需保存的数据长度len
和分配的pbuf类型PBUF_POOL
。第二个变量不难理解,就是上面从DMA描述符里提取出来的长度,这里解释第一个和第三个变量含义✒️ 第一个参数指定了分配的pbuf是用在协议栈的哪一层中的,根据不同的协议层需要添加不同的首部,比如应用层的TCP,网络层的IP,链路层的MAC都有对应的
pbuf_layer
类型,这些其实是一个偏移量,也就是会在后续的pbuf分配过程中给pbuf多预留出添加对应首部的空间。因此,在pbuf_alloc
中,这个参数被赋值给了一个叫做offset
的变量,他的核心作用就是预留头部空间,避免数据拷贝,试想如果没有预留这部分空间,那么后续添加的过程就需要拷贝len
长度的数据了。✒️第三个参数指定了分配的pbuf类型,这个值会直接决定pbuf的分配过程:如何分配和分配在哪里。这部分内容非常多,这里就不细讲了。只需要知道传入参数
PBUF_POOL
最终得到会是一个pbuf链就行了。了解了参数后,我们来看具体是如何分配的,
pbuf_alloc()
函数体中根据第三个参数进行switch
,因此直接看case PBUF_POOL
的处理过程,源码如下,由于关于pbuf的内容也非常多,这里就不展开了,添加了详细的注释:case PBUF_POOL: {struct pbuf *q, *last;u16_t rem_len; /* 剩余数据长度 */p = NULL; /* 创建的pbuf指针,记录的是首个分配的pbuf */last = NULL; /* 记录最后一个pbuf指针 */rem_len = length;do { /* 只要剩余的数据长度>0就会一直分配pbuf来存 */u16_t qlen;q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL); /* 从内存池中分配pbuf */if (q == NULL) { /* 分配失败,内存池内存不够了 */PBUF_POOL_IS_EMPTY(); if (p) { /* 如果前面以及分配成功过了,那么需要释放,也就是数据要么全部存下来,要不就不存 */pbuf_free(p); /* 释放已经分配的pbuf,这是一个链表,会挨个全部释放 */}/* bail out unsuccessfully */return NULL;}qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset))); /* 计算本次分配pbuf包含的数据长度 */pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)), /* 初始化本次分配的pbuf */rem_len, qlen, type, 0);if (p == NULL) { /* 如果是第一次分配,记录第一个pbuf */p = q;} else {last->next = q; /* 不是第一个,就链式存储 */}last = q; /* 每次都更新最后一个pbuf,最后结束的时候就是最后一个 */rem_len = (u16_t)(rem_len - qlen); /* 减去本次分配的pbuf包含的数据长度 */offset = 0; /* 这个就是pbuf_layer指定的首部预留量,只有第一个pbuf需要 */} while (rem_len > 0);break;}
-
分配了最够存储DMA描述符中数据的pbuf后,回到
low_level_input()
开始进行数据的搬运,这个过程可以看作是两个链式数组的数据搬运过程,该过程示意如下图:每次循环时都会判断当前
pbuf->payload
是否能够容纳当前的dmarxdesc->Buffer1Addr
,这就是while
判断语句(byteslefttocopy + bufferoffset) > ETH_RX_BUF_SIZE
作用,因为ETH_RX_BUF_SIZE
是一个DMA描述符缓冲区容纳的数据大小,而当前DMA描述符缓冲区未被搬运完的数据通过bufferoffset
变量存储,也就是说当前DMA描述符缓冲区中未被搬运的数据大小为ETH_RX_BUF_SIZE -bufferoffset
,而一个pbuf->payload
能够容纳的数据大小为pbuf->len
,也就是图中的byteslefttocopy
变量。若
while
判断成功,意味着当前dma描述符缓冲区的数据可以全部搬运至pbuf->payload
,进行搬运并更新两个偏移量,并取下一个dma描述符缓冲区;若while
判断失败,表示当前pbuf->payload
只能容纳部分当前dma描述符缓冲区的数据,进行搬运并取下一个pbuf->payload
-
搬运数据至pbuf后,原来存在dma描述符缓冲区的数据就没有用了,因此需要释放这些缓冲区,也就是将dma描述符还给硬件用于继续接受新的数据,这里就是还原DMA描述符的OWN标记,也就是上面说过的
ETH_DMARXDESC_OWN
,这意味着并没有清空DMA描述符的缓冲区而是标记了DMA可用;下一个需要还原的就是以太网句柄的heth->RxFrameInfos.SegCount
成员,该值表示了接受到的数据帧被几个DMA描述符包含。 -
最后将DMA接收标志缓冲区不可用标志清零,也就是使能DMA的接受过程
接收回调函数netif->input(p, netif)
在ethernetif_input
任务中,通过low_level_input()
函数从硬件层的DMA描述符中拷贝了数据至pbuf
中后,将存储了接收数据的pbuf链返回至任务中,并将其作为参数传入netif->input()
函数中,这个函数是在lwip的网络接口(在lwip中被称为“network interface”)数据结构体·netif
的初始化过程中被赋值的,也就是在neiif_add()
函数中,由于我们使用的TCP协议,因此在初始化的时候传入的是tcpip_input()
函数,因此这里执行的就是tcp_input(p, netif)
这个函数非常短,源码如下:
err_t
tcpip_input(struct pbuf *p, struct netif *inp)
{
#if LWIP_ETHERNETif (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);
}
这个函数最终会调用到,由于开启了LWIP_ETHERNET
宏,因此实际上执行tcpip_inpkt(p, inp, ethernet_input);
,这里传入的ethernet_input
同样是一个函数指针,在tcpip_inpkt()
函数中被执行
📘tcpip_inpkt()
函数
在该函数中有一个宏判断LWIP_TCPIP_CORE_LOCKING_INPUT
,这个宏的注释原文如下:
LWIP_TCPIP_CORE_LOCKING_INPUT: when LWIP_TCPIP_CORE_LOCKING is enabled, this lets tcpip_input() grab the mutex for input packets as well, instead of allocating a message and passing it to tcpip_thread.
意思是当这个宏启用时,会让tcpip_input()
函数执行时获得输入数据包的锁,而不是分配一个消息传递至tcpip_thread(这个任务是在MX_LWIP_Init()
函数中最开始创建的,通过函数tcpip_init()
函数).这个锁是什么?来看如果该宏为1时执行的内容
#if LWIP_TCPIP_CORE_LOCKING_INPUTerr_t ret;LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_inpkt: PACKET %p/%p\n", (void *)p, (void *)inp));LOCK_TCPIP_CORE(); /* 这里进去就是sys_mutex_lock(&lock_tcpip_core) */ret = input_fn(p, inp);UNLOCK_TCPIP_CORE();return ret;
可以看到,这个锁是lock_tcpip_core
,上锁后执行传入的函数input_fn = ethernet_input
对传入的pbuf链进行处理.
如果该宏不为1则会执行下面的代码,也就是将传入的pbuf及其包含的数据包装成一个tcp消息,投入tcpip_mbox
这是一个全局的静态变量(该变量同样在tcpip_init()
函数中被创建),类型为sys_mbox_t
,其实就是一个消息队列QueueHandle_t
.
struct tcpip_msg *msg;
LWIP_ASSERT("Invalid mbox", sys_mbox_valid_val(tcpip_mbox));
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;
if (sys_mbox_trypost(&tcpip_mbox, msg) != ERR_OK) {memp_free(MEMP_TCPIP_MSG_INPKT, msg);return ERR_MEM;
}
return ERR_OK;
那到这就很明显了,结合上面LWIP_TCPIP_CORE_LOCKING_INPUT
宏的注释,如果该宏未开启,就会将接收到的数据帧投入一个消息队列中,并且肯定tcpip_thread
是在阻塞接收该消息队列.
三.tcpip_thread
任务
简单总结以下上面的内容,在lwip的初始化过程中,会创建一个ethernetif_input
函数从底层硬件接收消息,并将硬件层(MAC的DMA)的消息拷贝至lwip的pbuf后在lwip中进行传递,并最后根据数据类型构建了消息传入了tcpip_thread
的消息队列,使其运行.
根据网络的分层模型可以知道,接下来就是tcpip_thread
中网络协议栈的处理.
上面构建消息时指定了消息的类型TCPIP_MSG_INPKT
,tcpip_thread
中调用了函数tcpip_thread_handle_msg()
根据该类型进行消息的处理,这里只看该类型中对应的处理.
case TCPIP_MSG_INPKT:if (msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif) != ERR_OK) {pbuf_free(msg->msg.inp.p);
}
memp_free(MEMP_TCPIP_MSG_INPKT, msg);
break;
可以看到会执行上面构建消息中传入的input_fn
,那其实也就是ethernet_input
函数,
ethernet_input
函数
在这个函数中,会进行网络协议栈的处理,首先就是取出数据中包含的以太网帧头,帧头包含6个byte的目的地址,6个byte的源地址以及2个byte 的帧类型
/* points to packet payload, which starts with an Ethernet header */ethhdr = (struct eth_hdr *)p->payload;LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE,("ethernet_input: dest:%"X8_F":%"X8_F":%"X8_F":%"X8_F":%"X8_F":%"X8_F", src:%"X8_F":%"X8_F":%"X8_F":%"X8_F":%"X8_F":%"X8_F", type:%"X16_F"\n",(unsigned char)ethhdr->dest.addr[0], (unsigned char)ethhdr->dest.addr[1], (unsigned char)ethhdr->dest.addr[2],(unsigned char)ethhdr->dest.addr[3], (unsigned char)ethhdr->dest.addr[4], (unsigned char)ethhdr->dest.addr[5],(unsigned char)ethhdr->src.addr[0], (unsigned char)ethhdr->src.addr[1], (unsigned char)ethhdr->src.addr[2],(unsigned char)ethhdr->src.addr[3], (unsigned char)ethhdr->src.addr[4], (unsigned char)ethhdr->src.addr[5],lwip_htons(ethhdr->type)));type = ethhdr->type;
然后会根据目标地址判断数据类型是组播/广播,这就是更细节的协议栈内容了,这里不讲.
下面会根据上面得到的type
来对消息进行处理,根据不同的type
将消息放入不同的模块进行处理,比如IP模块,ARP模块,PPPOE模块等等,每个模块都有自己的处理函数.
四.总结
至此,已完整讲解了lwip网络接收过程中将数据从MAC的DMA接受至lwip内核协议栈进行处理的过程以及中间涉及的关键函数,由于后续的网络协议栈处理情况复杂, 因此不在此继续深入,如果有问题可以提出,后面会加.🌙
2],
(unsigned char)ethhdr->src.addr[3], (unsigned char)ethhdr->src.addr[4], (unsigned char)ethhdr->src.addr[5],
lwip_htons(ethhdr->type)));
type = ethhdr->type;
然后会根据目标地址判断数据类型是组播/广播,这就是更细节的协议栈内容了,这里不讲.下面会根据上面得到的`type`来对消息进行处理,根据不同的`type`将消息放入不同的模块进行处理,比如IP模块,ARP模块,PPPOE模块等等,每个模块都有自己的处理函数.## 四.总结至此,已完整讲解了lwip网络接收过程中将数据从MAC的DMA接受至lwip内核协议栈进行处理的过程以及中间涉及的关键函数,由于后续的网络协议栈处理情况复杂, 因此不在此继续深入,如果有问题可以提出,后面会加.:crescent_moon: