1.裸机lwIP启动流程——旧版本
首先会先调用ethernet_mem_malloc这个函数来申请内存,两个是DMA描述符,还有两个就是缓冲区的数组,如下面的代码所示:
ETH_DMADescTypeDef *g_eth_dma_rx_dscr_tab; /* 以太网DMA接收描述符数据结构体指针 */
ETH_DMADescTypeDef *g_eth_dma_tx_dscr_tab; /* 以太网DMA发送描述符数据结构体指针 */
uint8_t *g_eth_rx_buf; /* 以太网底层驱动接收buffers指针 */
uint8_t *g_eth_tx_buf; /* 以太网底层驱动发送buffers指针 */
uint8_t ethernet_mem_malloc(void)
{
g_eth_dma_rx_dscr_tab = mymalloc(SRAMIN, ETH_RXBUFNB * sizeof(ETH_DMADescTypeDef)); /* 申请内存 */
g_eth_dma_tx_dscr_tab = mymalloc(SRAMIN, ETH_TXBUFNB * sizeof(ETH_DMADescTypeDef)); /* 申请内存 */
g_eth_rx_buf = mymalloc(SRAMIN, ETH_RX_BUF_SIZE * ETH_RXBUFNB); /* 申请内存 */
g_eth_tx_buf = mymalloc(SRAMIN, ETH_TX_BUF_SIZE * ETH_TXBUFNB); /* 申请内存 */
if (!(uint32_t)&g_eth_dma_rx_dscr_tab || !(uint32_t)&g_eth_dma_tx_dscr_tab || !(uint32_t)&g_eth_rx_buf || !(uint32_t)&g_eth_tx_buf)
{
ethernet_mem_free();
return 1; /* 申请失败 */
}
return 0; /* 申请成功 */
}
然后会调用ethernet_init()函数,实现以太网参数的初始化,配置传输速率、双工模式以及以太网芯片地址、MAC地址以及介质接口RMII地址,然后还对调用Msp函数对IO口进行初始化,最后硬件复位PHY芯片;
然后通过lwIP_init()进行lwIP的初始化,主要是完成内存、pbuf以及netif的初始化,还有通讯协议例如TCP和UDP的初始化(通过宏控制);
然后通过宏,判断是否DHCP动态分配IP地址,如果不是那就采取之前设置的静态的IP地址初始化;
我们需要添加虚拟网卡,才能发送和接受数据,所以要先设置虚拟网卡并添加进来,这一块是通过netif的函数进行操作的;通过设置完成后,需要通过etharp_output判断有没有对应的MAC地址,这一部分之后有讲解,涉及到ARP协议;
之后,通过low_level_output对数据进行传输,把网络层的数据包给到pbuf缓冲,pbuf在通过操作进入到Tx的缓冲区中,其中需要对1524字节的数据进行切分传输,代码如下:
low_level_output(struct netif *netif, struct pbuf *p)
{
err_t errval;
struct pbuf *q;
uint8_t *buffer = (uint8_t *)(g_eth_handler.TxDesc->Buffer1Addr);
__IO ETH_DMADescTypeDef *DmaTxDesc;
uint32_t framelength = 0;
uint32_t bufferoffset = 0;
uint32_t byteslefttocopy = 0;
uint32_t payloadoffset = 0;
DmaTxDesc = g_eth_handler.TxDesc;
bufferoffset = 0;
#if ETH_PAD_SIZE
pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
/* 从pbuf中拷贝要发送的数据 */
for(q = p;q != NULL;q = q->next)
{
/* 判断此发送描述符是否有效,即判断此发送描述符是否归以太网DMA所有 */
if((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
{
errval = ERR_USE;
goto error; /* 发送描述符无效,不可用 */
}
byteslefttocopy = q->len; /* 要发送的数据长度 */
payloadoffset = 0;
/* 将pbuf中要发送的数据写入到以太网发送描述符中,有时候我们要发送的数据可能大于一个以太网
描述符的Tx Buffer,因此我们需要分多次将数据拷贝到多个发送描述符中 */
while((byteslefttocopy + bufferoffset) > ETH_TX_BUF_SIZE )
{
/* 将数据拷贝到以太网发送描述符的Tx Buffer中 */
memcpy((uint8_t*)((uint8_t*)buffer + bufferoffset),(uint8_t*)((uint8_t*)q->payload + payloadoffset),(ETH_TX_BUF_SIZE - bufferoffset));
/* DmaTxDsc指向下一个发送描述符 */
DmaTxDesc = (ETH_DMADescTypeDef *)(DmaTxDesc->Buffer2NextDescAddr);
/* 检查新的发送描述符是否有效 */
if((DmaTxDesc->StatusÐ_DMATXDESC_OWN) != (uint32_t)RESET)
{
errval = ERR_USE;
goto error; /* 发送描述符无效,不可用 */
}
buffer = (uint8_t *)(DmaTxDesc->Buffer1Addr); /* 更新buffer地址,指向新的发送描述符的Tx Buffer */
byteslefttocopy = byteslefttocopy - (ETH_TX_BUF_SIZE - bufferoffset);
payloadoffset = payloadoffset + (ETH_TX_BUF_SIZE - bufferoffset);
framelength = framelength + (ETH_TX_BUF_SIZE - bufferoffset);
bufferoffset = 0;
}
/* 拷贝剩余的数据 */
memcpy( (uint8_t*)((uint8_t*)buffer + bufferoffset),(uint8_t*)((uint8_t*)q->payload+payloadoffset),byteslefttocopy );
bufferoffset = bufferoffset + byteslefttocopy;
framelength = framelength + byteslefttocopy;
}
/* 当所有要发送的数据都放进发送描述符的Tx Buffer以后就可发送此帧了 */
HAL_ETH_TransmitFrame(&g_eth_handler,framelength);
errval = ERR_OK;
error:
/* 发送缓冲区发生下溢,一旦发送缓冲区发生下溢TxDMA会进入挂起状态 */
if((g_eth_handler.Instance->DMASRÐ_DMASR_TUS) != (uint32_t)RESET)
{
/* 清除下溢标志 */
g_eth_handler.Instance->DMASR = ETH_DMASR_TUS;
/* 当发送帧中出现下溢错误的时候TxDMA会挂起,这时候需要向DMATPDR寄存器 */
/* 随便写入一个值来将其唤醒,此处我们写0 */
g_eth_handler.Instance->DMATPDR=0;
}
#if ETH_PAD_SIZE
pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif
return errval;
}
发送的output完成后,就会通过low_level_init(netif)完成初始化,配置MAC地址,允许的最大传输字节(1500),以及ARP协议使能(用来广播MAC地址),然后使能两个DMA描述符,最后HAL_ETH_Start()开启以太网。
当然除了发送,还有接收函数,其代码逻辑与output是非常类似的,通过内存池(512大小),最大1500的数据,所以申请3个512的内存池,然后for循环去把Rx的缓冲区转移到pbuf中,代码如下:
low_level_input(struct netif *netif)
{
struct pbuf *p, *q;
u16_t len;
uint8_t *buffer;
__IO ETH_DMADescTypeDef *dmarxdesc;
uint32_t bufferoffset = 0;
uint32_t payloadoffset = 0;
uint32_t byteslefttocopy = 0;
uint32_t i = 0;
if(HAL_ETH_GetReceivedFrame(&g_eth_handler) != HAL_OK) /* 判断是否接收到数据 */
return NULL;
len = g_eth_handler.RxFrameInfos.length; /* 获取接收到的以太网帧长度 */
#if ETH_PAD_SIZE
len += ETH_PAD_SIZE; /* allow room for Ethernet padding */
#endif
buffer = (uint8_t *)g_eth_handler.RxFrameInfos.buffer; /* 获取接收到的以太网帧的数据buffer */
p = pbuf_alloc(PBUF_RAW,len,PBUF_POOL); /* 申请pbuf */
if(p != NULL) /* pbuf申请成功 */
{
dmarxdesc = g_eth_handler.RxFrameInfos.FSRxDesc; /* 获取接收描述符链表中的第一个描述符 */
bufferoffset = 0;
for(q = p;q != NULL;q = q->next)
{
byteslefttocopy = q->len;
payloadoffset = 0;
/* 将接收描述符中Rx Buffer的数据拷贝到pbuf中 */
while((byteslefttocopy + bufferoffset) > ETH_RX_BUF_SIZE )
{
/* 将数据拷贝到pbuf中 */
memcpy((uint8_t*)((uint8_t*)q->payload+payloadoffset),(uint8_t*)((uint8_t*)buffer + bufferoffset),(ETH_RX_BUF_SIZE - bufferoffset));
/* dmarxdesc向下一个接收描述符 */
dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
/* 更新buffer地址,指向新的接收描述符的Rx Buffer */
buffer = (uint8_t *)(dmarxdesc->Buffer1Addr);
byteslefttocopy = byteslefttocopy - (ETH_RX_BUF_SIZE - bufferoffset);
payloadoffset = payloadoffset + (ETH_RX_BUF_SIZE - bufferoffset);
bufferoffset = 0;
}
/* 拷贝剩余的数据 */
memcpy((uint8_t*)((uint8_t*)q->payload + payloadoffset),(uint8_t*)((uint8_t*)buffer + bufferoffset),byteslefttocopy);
bufferoffset = bufferoffset + byteslefttocopy;
}
}
else
{
/* drop packet(); 丢包函数自行编写 */
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
MIB2_STATS_NETIF_INC(netif, ifindiscards);
}
/* 释放DMA描述符 */
dmarxdesc = g_eth_handler.RxFrameInfos.FSRxDesc;
for(i = 0;i < g_eth_handler.RxFrameInfos.SegCount; i ++)
{
dmarxdesc->Status |= ETH_DMARXDESC_OWN; /* 标记描述符归DMA所有 */
dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
}
g_eth_handler.RxFrameInfos.SegCount = 0; /* 清除段计数器 */
if((g_eth_handler.Instance->DMASR & ETH_DMASR_RBUS) != (uint32_t)RESET) /* 接收缓冲区不可用 */
{
/* 清除接收缓冲区不可用标志 */
g_eth_handler.Instance->DMASR = ETH_DMASR_RBUS;
/* 当接收缓冲区不可用的时候RxDMA会进去挂起状态,通过向DMARPDR写入任意一个值来唤醒Rx DMA */
g_eth_handler.Instance->DMARPDR = 0;
}
return p;
}
以上的两个数据传输的流程,可以总结成下图:
以上netif全部完成之后,就会开启网卡,完成init。
全部的协议启动流程,可以如下图展示的来概括:
2.裸机lwIP启动流程——新版本
总体流程是相似的,只不过一些小细节有所改动。
初始化的时候,PHY芯片的初始化要调用pcf8574_init(),是在pcf8574.c的文件中的。
之后的内容很类似,就是到了low_level_output()的源码有了区别,新版本是直接定义了一个数组,Tx的缓冲数据直接到定义的Txbuffer数组中,同样完成切片,但是操作起来简便了很多,如下所示:
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
uint32_t i = 0U;
struct pbuf *q = NULL;
err_t errval = ERR_OK;
ETH_BufferTypeDef Txbuffer[ETH_TX_DESC_CNT] = {0};
memset(Txbuffer, 0 , ETH_TX_DESC_CNT * sizeof(ETH_BufferTypeDef));
for (q = p; q != NULL; q = q->next)
{
if (i >= ETH_TX_DESC_CNT)
return ERR_IF;
Txbuffer[i].buffer = q->payload;
Txbuffer[i].len = q->len;
if (i > 0)
{
Txbuffer[i - 1].next = &Txbuffer[i];
}
if (q->next == NULL)
{
Txbuffer[i].next = NULL;
}
i++;
}
TxConfig.Length = p->tot_len;
TxConfig.TxBuffer = Txbuffer;
TxConfig.pData = p;
HAL_ETH_Transmit(&g_eth_handler, &TxConfig, ETH_DMA_TRANSMIT_TIMEOUT);
return errval;
}
同时,在v1.27的版本中,IP地址等都需要手动设置(之前的v1.26都是可以自动设置的)。这些配置都在ethernet_chip.c中进行配置。
总体的流程仍然是一样的,就是output和input的实现方法略有不同,其他就没什么区别了。
总结
这一讲其实就是带着看看源码,如果有底层的学习需求,最好是自己F12进去看函数是怎么实现的,这个笔记也只是我自己的当前的一些理解和记录,后期真的做项目用到,移植出现了问题就会去看这些底层实现。
当然,经过后面的课程学习之后应该会更有理解,现在就是大概了解了一下。