目录
什么是以太网DMA描述符
TX DMA描述符成员变量简介
RX DMA描述符成员变量简介
以太网DMA描述符结构
如何追踪描述符
如何创建Tx/Rx描述符
以太网发送和接收数据流程
总结
在移植LwIP之前有必要了解一下以太网DMA描述符的相关知识,ST以太网模块中的接收/发送FIFO和内存之间的以太网数据包传输是以太网DMA描述符完成的。它们一共有两个描述符列表:一个用于接收,一个用于发送,这两个列表的基地址分别写入DMARDLAR寄存器和DMATDLAR寄存器。关于DMA的相关详细介绍可以参考之前写的一篇博客。
【STM32】DMA原理,配置步骤超详细,一文搞懂DMA_~Old的博客-CSDN博客_dma配置
什么是以太网DMA描述符
发送:不需要CPU参与下,把描述符指向的缓冲区数据传输到TX FIFO当中
接收:不需要CPU参与下,把RX FIFO中的数据传输到描述符指向的缓冲区当中。
其中,ST把描述符分为两种:
对应于在HAL库中的头文件stm32f4xx_hal_eth.h中的结构体ETH_DMAInitTypeDef
typedef struct { __IO uint32_t Status; /*!<状态 */ uint32_t ControlBufferSize; /*!< 缓冲区1和2的大小 */ uint32_t Buffer1Addr; /*!< 缓冲区1的地址 */ uint32_t Buffer2NextDescAddr; /*!< 缓冲区2的地址/保存下一个描述符的地址 */ /*!< Enhanced ETHERNET DMA PTP Descriptors 增强描述符相关的内容,比如时间戳和IPV4校验和 */ uint32_t ExtendedStatus; /*!<增强描述符状态 */ uint32_t Reserved1; /*!<保留*/ uint32_t TimeStampLow; /*!< 时间戳低位 */ uint32_t TimeStampHigh; /*!< 时间戳高位 */ } ETH_DMADescTypeDef;
关于TX DMA描述符成员变量的介绍可以参考ST官方的中文参考手册以太网章节:
TX DMA描述符成员变量简介
TDES0[31]:置0:CPU可将数据拷贝到描述符中,拷贝完成之后把该位置1,告诉DMA可以发送数据
TDES0[20]:置1,描述符中的第二个地址是下一个描述符地址,ST默认是把该位置1了
TDES1[28:16]:如果TDES0[20]位置1,则该字段无效
TDES3[31:0]:取决于TDES0[20]的值,为1,则指向下一个描述符地址
RX DMA描述符成员变量简介
RDES0[31]:置1,MAC将数据从RX FIFO 传输到RX描述符中,拷贝完成之后该位置0,告诉CPU可以接收数据
RDES0[14]:描述符中的第二个地址是下一个描述符地址,ST以太网驱动库是默认把该位置1
RSES1[28;16] ;如果RDES0[14]位置1,则该字段无效
RDES3[31:0]:取决于RDES0[14]的值,为1,则指向下一个描述符地址
以太网DMA描述符结构
有两种描述符结构,一种是环形结构,一种是链接结构。在ST提供的以太网驱动库stm32f4xx_hal_eth.c中初始化发送和接收描述符列表就使用的是链接结构,DMA描述符连接结构的具体描述如下图。
上面第一幅图是对于HAL版本V1.26而言的,下面的是HAL版本V1.27而言。
上面的DMA描述符链接结构可以由一下的函数来实现,如下表所示:
HAL版本 | 函数 |
F4_V1.26.0 | HAL_ETH_DMATxDescListInit() HAL_ETH_DMARxDescListInit() |
F4_V1.27.0/F7_V1.17/H7_V1.10 | ETH_DMATxDescListInit() ETH_DMARxDescListInit() |
这些函数应该注意以下几点:
- 跨度性:一个以太网数据包可以跨越一个或者多个DMA描述符
- 唯一性:一个DMA描述符只能用于一个以太网数据包
- 循环性:DMA描述符列表中的最后一个描述符指向第一个,形成循环链式结构。
什么是环形单向链表
将一系列节点链接成链,并且单链表的最后一个节点指向链表的第一个节点,构成环状的链表
如何追踪描述符
为了追踪Tx/Rx的DMA描述符,在下图结构体中定义了两个非常重要的指针变量,如下代码所示:
#if (USE_HAL_ETH_REGISTER_CALLBACKS == 1) typedef struct __ETH_HandleTypeDef #else typedef struct #endif /* USE_HAL_ETH_REGISTER_CALLBACKS */ { ETH_TypeDef *Instance; /*!< Register base address */ ETH_InitTypeDef Init; /*!< Ethernet Init Configuration */ uint32_t LinkStatus; /*!< Ethernet link status */ ETH_DMADescTypeDef *RxDesc; /*!< Rx descriptor to Get */ ETH_DMADescTypeDef *TxDesc; /*!< Tx descriptor to Set */ ETH_DMARxFrameInfos RxFrameInfos; /*!< last Rx frame infos */ __IO HAL_ETH_StateTypeDef State; /*!< ETH communication state */ HAL_LockTypeDef Lock; /*!< ETH Lock */ #if (USE_HAL_ETH_REGISTER_CALLBACKS == 1) void (* TxCpltCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Tx Complete Callback */ void (* RxCpltCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Rx Complete Callback */ void (* DMAErrorCallback) ( struct __ETH_HandleTypeDef * heth); /*!< DMA Error Callback */ void (* MspInitCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Msp Init callback */ void (* MspDeInitCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Msp DeInit callback */ #endif /* USE_HAL_ETH_REGISTER_CALLBACKS */ } ETH_HandleTypeDef;
其中的RxDesc和TxDesc两个指针变量指向ETH_DMADescTypeDef结构体,在使用过程中它们两个分别指向下一个要发送或者接收的描述符,如下图所示那样:
如何创建Tx/Rx描述符
为发送/接收描述符和缓冲区申请内存
这里观看正点原子视频,其实发送/接收描述符本质上也是一个内存块,和缓冲区本质上都是内存。
方法1:使用算法实现的内存申请和释放函数来申请内存
/** * @breif 为ETH底层驱动申请内存 * @param 无 * @retval 0,正常 * 1,失败 */ 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; /* 申请成功 */ }
方法2:ST官方给出的例程使用数组来实现内存申请
/* Private variables ---------------------------------------------------------*/ #if defined ( __ICCARM__ ) /*!< IAR Compiler */ #pragma data_alignment=4 #endif __ALIGN_BEGIN ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] __ALIGN_END;/* Ethernet Rx MA Descriptor */ #if defined ( __ICCARM__ ) /*!< IAR Compiler */ #pragma data_alignment=4 #endif __ALIGN_BEGIN ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB] __ALIGN_END;/* Ethernet Tx DMA Descriptor */ #if defined ( __ICCARM__ ) /*!< IAR Compiler */ #pragma data_alignment=4 #endif __ALIGN_BEGIN uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __ALIGN_END; /* Ethernet Receive Buffer */ #if defined ( __ICCARM__ ) /*!< IAR Compiler */ #pragma data_alignment=4 #endif __ALIGN_BEGIN uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __ALIGN_END; /* Ethernet Transmit Buffer */
其中
/* 初始化发送描述符 */ HAL_ETH_DMATxDescListInit(&heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB); /* 初始化接收描述符 */ HAL_ETH_DMARxDescListInit(&heth, DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);
HAL_StatusTypeDef HAL_ETH_DMATxDescListInit(ETH_HandleTypeDef *heth, ETH_DMADescTypeDef *DMATxDescTab, uint8_t *TxBuff, uint32_t TxBuffCount)
{
uint32_t i = 0U;
ETH_DMADescTypeDef *dmatxdesc;
/* Process Locked */
__HAL_LOCK(heth);
/*设置状态等于BUSY */
heth->State = HAL_ETH_STATE_BUSY;
/* 指向第一个描述符 */
heth->TxDesc = DMATxDescTab;
/* 通过for循环来添加每一个描述符 */
for(i=0U; i < TxBuffCount; i++)
{
/* 获取Tx Desc列表的第i个成员的指针 */
dmatxdesc = DMATxDescTab + i;
/*TDES0[20]置1:设置第二个地址链接位 */
dmatxdesc->Status = ETH_DMATXDESC_TCH;
/* 给描述符的buffer赋值地址 */
dmatxdesc->Buffer1Addr = (uint32_t)(&TxBuff[i*ETH_TX_BUF_SIZE]);
if ((heth->Init).ChecksumMode == ETH_CHECKSUM_BY_HARDWARE)
{
/* Set the DMA Tx descriptors checksum insertion */
dmatxdesc->Status |= ETH_DMATXDESC_CHECKSUMTCPUDPICMPFULL;
}
/* 如果小于最大值 */
if(i < (TxBuffCount-1U))
{
/* 描述符的next就指向下一个,否则就指向第一个描述符 */
dmatxdesc->Buffer2NextDescAddr = (uint32_t)(DMATxDescTab+i+1U);
}
else
{
/* 最后一个描述符,设置下一个描述符地址寄存器就等于第一个描述符基地址 */
dmatxdesc->Buffer2NextDescAddr = (uint32_t) DMATxDescTab;
}
}
/* 把描述符地址赋值给DMA的寄存器 */
(heth->Instance)->DMATDLAR = (uint32_t) DMATxDescTab;
/* 设置状态等于Ready */
heth->State= HAL_ETH_STATE_READY;
/* Process Unlocked */
__HAL_UNLOCK(heth);
/* Return function status */
return HAL_OK;
}
以太网发送和接收数据流程
发送:通过网络层下发数据包之后,通过内存拷贝函数,把pbuf数据包拷贝到Tx描述符管理的缓冲区中,缓冲区就保存了要发送的数据包,之后通过以太网的DMA实现存储器到存储器的传输,转移到TX FIFO中,转发到MAC内核,这时候通过介质独立接口发往到PHY设备中,转成电信号/光信号发送出去。
接收:PHY接收到电信号/光信号经过内部的解调和AD的转换,利用介质独立接口发往到MAC内核,再转发到RX FIFO中,通过DMA的存储器到存储器传输到Rx描述符管理的缓冲区中,再拷贝到Rx对应的pbuf中,再往上层网络层发送数据包。
总结
1、TxDesc用来追踪DMA描述符
2、DMA描述符是用来管理缓冲区的
3、缓存区是用来保存数据包的