将 FreeRTOS-Plus-TCP 移植到不同的微控制器
简介
网络接口的实现,特别是以太网
MAC 驱动程序的实现,
对使用嵌入式 TCP/IP 堆栈时可实现的数据吞吐量至关重要。 为获得高吞吐量,
MAC 驱动程序必须有效利用 DMA 并
尽可能避免复制数据。 可以通过
FreeRTOS-Plus-TCP 对 UDP 数据包进行端到端零拷贝;另外,也可以通过高级接口
对 TCP 数据包进行零拷贝。 还有一些高级选项
可以在数据包被发送到嵌入式
TCP/IP 堆栈前对其进行过滤,而且还可以一次性地将接连收到的数据包
发送到嵌入式 TCP/IP 堆栈, 无需一个个单独发送。
但实际上,需要最大化吞吐量的应用程序并不多,
尤其是在小型 MCU 上,实现者可能会
选择牺牲吞吐量以提升简洁性。
本页面介绍了如何连接 FreeRTOS-Plus-TCP 和网络驱动程序,
并提供了一个简要示例,其中包含简单接口实现
和快速 (但更复杂)
接口实现。 大家有必要参考这些示例,
因为它们展示了数据传输后网络缓冲区被释放的过程。
数据来源。
随 FreeRTOS-Plus-TCP 一起提供的网络驱动程序移植层
位于 FreeRTOS-Plus-TCP/source/portable/NetworkInterface 目录中
(可在 FreeRTOS-Plus-TCP 下载包中找到)。 但请注意,这些驱动程序
是为测试嵌入式 TCP/IP 堆栈而创建的,并非
用于表示优化过的示例。
要点总结
网络接口移植层位于
IP 堆栈和嵌入式
以太网硬件驱动程序之间
本页面详细介绍了上述步骤的具体操作过程,并提供了两个
示例。 第一个示例演示了
如何实现简单(但更慢)驱动程序。 而
第二个示例
则演示了如何实现更复杂(也更快)的驱动程序。
大家有必要参考这些示例,
因为它们展示了数据传输后网络缓冲区被释放的过程。
。
网络缓冲区和网络缓冲区描述符
以太网(或其他网络)帧存储在网络缓冲器中。 网络缓冲区描述符
(
NetworkBufferDescriptor_t 类型的变量)用于描述网络
缓冲区,并在 TCP/IP 堆栈和网络
驱动程序之间传递网络缓冲区。
pucEthernetBuffer 指向网络缓冲区的起点。
xDataLength 保存缓冲区的大小(以字节为单位),不包括以太网 CRC 字节。
仅需访问以下两个 NetworkBufferDescriptor_t 结构体成员
:
-
uint8_t *pucEthernetBuffer;
pucEthernetBuffer 指向网络缓冲区的起点。
-
size_t xDataLength
xDataLength 保持 pucEthernetBuffer 所指向的网络缓冲区的大小。
此缓冲区大小以字节为单位,但其长度不包括
含有以太网帧 CRC 字段的字节。
pucGetNetworkBuffer()
仅用于获取网络缓冲区本身,且通常仅
在零拷贝驱动程序中使用。
pxGetNetworkBufferWithDescriptor()
用于同时获取网络缓冲区和网络缓冲区
描述符。
必须由移植层实现的函数
px${port_name}_FillInterfaceDescriptor()
(例如:pxSAM_FillInterfaceDescriptor)
px${port_name}_FillInterfaceDescriptor()
必须初始化“网络接口移植层”实例。下面是该实例的数据结构体。
typedef struct xNetworkInterface
{
const char * pcName;
void * pvArgument;
NetworkInterfaceInitialiseFunction_t pfInitialise;
NetworkInterfaceOutputFunction_t pfOutput;
GetPhyLinkStatusFunction_t pfGetPhyLinkStatus;
struct
{
uint32_t
bInterfaceUp : 1,
bCallDownEvent : 1;
} bits;
struct xNetworkEndPoint * pxEndPoint;
struct xNetworkInterface * pxNext;
} NetworkInterface_t;
- pcName 用于调试目的,通常是网络接口的唯一名称。
- pvArgument 是传递给访问函数的实参。
- pfInitialize 是初始化 MAC 驱动程序的回调函数。
- pfOutput 是一个回调函数,用于将从嵌入式 TCP/IP 堆栈中收到的数据发送到以太网 MAC 驱动程序以进行传输。
- pfGetPhyLinkStatus 为用户提供了获取接口状态的查询函数。
- bits 包含 bInterfaceUp 和 bCallDownEvent ,分别表示接口是否处于上行和下行状态
。
- pxEndPoint 是绑定到此接口的端点列表。
- pxNext 是链表结构体中的下一个接口实例。
pxFillInterfaceDescriptor()
启用
ipconfigIPv4_BACKWARD_COMPATIBLE 时,
函数
FreeRTOS_IPInit() 将调用
pxFillInterfaceDescriptor 以初始化接口描述符。
要实现此函数,只需调用
px${port_name}_FillInterfaceDescriptor() 即可,因为二者功能相同。
pfInitialise()
pfInitialise() 是“网络接口移植层”实例中的回调函数,
它必须让以太网 MAC 做好发送和接收数据的准备。大多数情况下,这只需调用
以太网 MAC 外设驱动程序中提供的初始化函数——这将确保
MAC 硬件已启用,且时钟已设置,并配置 MAC 外设的 DMA 描述符。
pfInitialise() 将网络接口作为参数。如果初始化成功,则返回 pdPASS;
如果初始化失败,则返回 pdFAIL 。
typedef BaseType_t ( * NetworkInterfaceInitialiseFunction_t ) ( struct xNetworkInterface * pxDescriptor );
pfInitialise() 函数原型
pfOutput()
每当网络缓冲区做好传输准备时,TCP/IP 堆栈就会调用 pfOutput()。
使用函数的
pxDescriptor 参数,将传输缓冲区的描述信息传入函数。
如果
xReleaseAfterSend 不等于
pdFALSE,
则在不再需要缓冲区和缓冲区的描述符时,驱动程序代码必须将它们释放(返回)给嵌入式 TCP/IP 堆栈。如果
xReleaseAfterSend 为
pdFALSE,则网络缓冲区和缓冲区的描述符
将由 TCP/IP 堆栈本身释放(在这种情况下,驱动程序不需要释放它们)。
请注意,此时 pfOutput() 返回的值将被忽略。嵌入式 TCP/IP 堆栈
不会为同一个网络缓冲区调用 pfOutput() 两次,即使第一次调用 pfOutput()
无法将网络缓冲区发送到网络上。
下面提供了基本示例和更高级的示例,FreeRTOS-Plus-TCP/source/portable/NetworkInterface 目录
(位于 FreeRTOS-Plus-TCP 下载文件中)包含可以参考的示例。但请注意,
下载文件中的示例可能未经过优化。
typedef BaseType_t ( * NetworkInterfaceOutputFunction_t ) ( struct xNetworkInterface * pxDescriptor,
NetworkBufferDescriptor_t * const pxNetworkBuffer,
BaseType_t xReleaseAfterSend );
pfOutput() 函数原型
vNetworkInterfaceAllocateRAMToBuffers()
仅当使用 BufferAllocation_1.c 时
BufferAllocation_1.c
使用预分配网络缓冲区,这些缓冲区通常
在编译时被静态分配。
必须分配的网络缓冲区数目由
ipconfigNUM_NETWORK_BUFFER_DESCRIPTORS
定义(在 FreeRTOSIPConfig.h 中)设置,并且每个缓冲区的大小必须为
( ipTOTAL_ETHERNET_FRAME_SIZE + ipBUFFER_PADDING )。 ipTOTAL_ETHERNET_FRAME_SIZE 是
根据
ipconfigNETWORK_MTU 的值自动计算出来的,
ipBUFFER_PADDING 则是根据
ipconfigBUFFER_PADDING 自动计算出来的。
联网硬件会对
分配的缓冲区提出严格的对齐要求,因此建议
在嵌入式以太网驱动程序中分配缓冲区——这样,缓冲区的对齐方式
总是能满足硬件的要求。
虽然嵌入式 TCP/IP 堆栈会分配网络缓冲区描述符,
但它对网络缓冲区本身的对齐方式一无所知。
因此,嵌入式以太网驱动程序还必须提供
名为 vNetworkInterfaceAllocateRAMToBuffers() 的函数,
为每个描述符分配一个静态声明的缓冲区。 请注意,缓冲区开头的 ipBUFFER_PADDING
字节留给嵌入式 TCP/IP
堆栈使用。 请参见下文示例。
void vNetworkInterfaceAllocateRAMToBuffers(
NetworkBufferDescriptor_t xDescriptors[ ipconfigNUM_NETWORK_BUFFERS ] );
The vNetworkInterfaceAllocateRAMToBuffers() 函数原型
#define BUFFER_SIZE ( ipTOTAL_ETHERNET_FRAME_SIZE + ipBUFFER_PADDING )
#define BUFFER_SIZE_ROUNDED_UP ( ( BUFFER_SIZE + 7 ) & ~0x07UL )
static uint8_t ucBuffers[ ipconfigNUM_NETWORK_BUFFERS ][ BUFFER_SIZE_ROUNDED_UP ];
void vNetworkInterfaceAllocateRAMToBuffers(
NetworkBufferDescriptor_t pxNetworkBuffers[ ipconfigNUM_NETWORK_BUFFERS ] )
{
BaseType_t x;
for( x = 0; x < ipconfigNUM_NETWORK_BUFFERS; x++ )
{
pxNetworkBuffers[ x ].pucEthernetBuffer = &( ucBuffers[ x ][ ipBUFFER_PADDING ] );
*( ( uint32_t * ) &ucBuffers[ x ][ 0 ] ) = ( uint32_t ) &( pxNetworkBuffers[ x ] );
}
}
vNetworkBufferInterfaceAllocateRAMToBuffers() 实现示例。
由 TCP/IP 堆栈提供给移植层使用的函数
移植层可使用以下函数:
接收 数据
以太网 MAC 驱动程序会将接收到的以太网帧放入缓冲区。
移植层必须:
-
确定是否需要将接收到的数据发送至嵌入式
TCP/IP 堆栈。 理想情况下,这将在接收中断中完成,
以尽早丢弃不必要的数据包
。
-
分配网络缓冲区描述符。
-
设置所分配的描述符的 xDataLength 和 pucEthernetBuffer
成员(请参见本页后文中的基础示例和零拷贝示例)
。
-
调用 xSendEventStructToIPTask() 以将网络缓冲区
描述符发送到嵌入式 TCP/IP 堆栈进行处理
(请参见本页后文的基础示例和零拷贝示例)
。
typedef struct IP_TASK_COMMANDS
{
eIPEvent_t eEventType;
void *pvData;
} IPStackEvent_t;
IPStackEvent_t 类型
BaseType_t xSendEventStructToIPTask( const IPStackEvent_t *pxEvent, TickType_t xTimeout )
xSendEventStructToIPTask() 函数原型
下文将提供基础示例和高级示例教程。 由
FreeRTOS-Plus-TCP 自带的网络驱动程序移植层(不一定经过
优化)可在 FreeRTOS-Plus-TCP/source/portable/NetworkInterface
目录中找到。
网络接口移植层示例
1. px${port_name}_FillInterfaceDescriptor() 作为接口移植层常用部分的示例。
每个网络接口都必须提供一个函数来初始化其实例,该函数称为
px${port_name}_FillInterfaceDescriptor()。
该函数将初始化实例结构体中的回调函数,
并通过调用
FreeRTOS_AddNetworkInterface() 将实例添加到链表中。
px${port_name}_FillInterfaceDescriptor() 的实现示例(以 SAM 驱动程序为例)
NetworkInterface_t * pxSAM_FillInterfaceDescriptor( BaseType_t xEMACIndex,
NetworkInterface_t * pxInterface )
{
pxInterface->pcName = "InterfaceName";
pxInterface->pvArgument = ( void * ) xEMACIndex;
pxInterface->pfInitialise = prvSAM_NetworkInterfaceInitialise;
pxInterface->pfOutput = prvSAM_NetworkInterfaceOutput;
pxInterface->pfGetPhyLinkStatus = prvSAM_GetPhyLinkStatus;
FreeRTOS_AddNetworkInterface( pxInterface );
return pxInterface;
}
pxpxFillInterfaceDescriptor()${port_name}_ 的实现示例(以 SAM 驱动程序为例)
#if ( ipconfigIPv4_BACKWARD_COMPATIBLE == 1 )
NetworkInterface_t * pxFillInterfaceDescriptor( BaseType_t xEMACIndex,
NetworkInterface_t * pxInterface )
{
return pxSAM_FillInterfaceDescriptor( xEMACIndex, pxInterface );
}
#endif
2. 基础网络接口移植层示例
简单的网络接口可通过复制以太网帧
(位于由以太网 MAC 驱动程序库分配的缓冲区和由移植层分配的缓冲区之间)
创建。
[更有效的零拷贝
替代方案将在
此简单示例之后提供。]
适用于基础移植层的 pfInitialise() 实现示例
static BaseType_t prvSAM_NetworkInterfaceInitialise( NetworkInterface_t * pxInterface )
{
BaseType_t xReturn;
if( InitialiseNetwork() == 0 )
{
xReturn = pdFAIL;
}
else
{
if( SetMACAddress() == 0 )
{
xReturn = pdFAIL;
}
else
{
xReturn = pdPASS;
}
}
return xReturn;
}
pfInitialise() 是针对特定硬件的,因此本示例只描述需要完成的操作,而不显示任何细节
适用于基础移植层的 pfOutput() 实现示例
static BaseType_t prvSAM_NetworkInterfaceOutput( NetworkInterface_t * pxInterface,
NetworkBufferDescriptor_t * const pxDescriptor,
BaseType_t xReleaseAfterSend )
{
SendData( pxDescriptor->pucBuffer, pxDescriptor->xDataLength );
iptraceNETWORK_INTERFACE_TRANSMIT();
if( xReleaseAfterSend != pdFALSE )
{
vReleaseNetworkBufferAndDescriptor( pxDescriptor );
}
return pdTRUE;
}
适用于简单(而非零拷贝)网络接口实现的 pfOutput() 实现示例
将接收到的数据传递到基础移植层中的 TCP/IP 示例
接收到来自以太网(或其他网络)驱动程序的数据包后,
移植层必须使用
NetworkBufferDescriptor_t
结构体描述此数据包,然后调用
xSendEventStructToIPTask()
将 NetworkBufferDescriptor_t 结构体发送至嵌入式 TCP/IP 堆栈。
注意 1: 如果要使用 BufferAllocation_2.c,
那么网络缓冲区
描述符和以太网缓冲区不能从
中断服务程序 (ISR) 内部分配。 在这种情况下,以太网 MAC
接收中断可以在任务中延迟处理接收到的数据
。 下文将展示这一情况
。
注意 2: 您可以通过各种高级技巧
最小化从移植层发送至嵌入式
TCP/IP 堆栈的数据量。 例如,可以调用 eConsiderFrameForProcessing()
来确定已接收的以太网帧是否需要
发送至嵌入式 TCP/IP 堆栈,以及接连收到的以太网帧
是否可以一次性发送至嵌入式 TCP/IP 堆栈
。 详情请参见
硬件和驱动程序特定设置
部分(位于 FreeRTOS-Plus-TCP 配置页面)。
注 3:切记通过调用 pxEndPoint 正确设置 pxEndPoint
FreeRTOS_MatchingEndPoint()
在向 TCP/IP 堆栈发送数据包和描述符之前。
static void prvEMACDeferredInterruptHandlerTask( void *pvParameters )
{
NetworkBufferDescriptor_t *pxBufferDescriptor;
size_t xBytesReceived;
IPStackEvent_t xRxEvent;
for( ;; )
{
ulTaskNotifyTake( pdFALSE, portMAX_DELAY );
xBytesReceived = ReceiveSize();
if( xBytesReceived > 0 )
{
pxBufferDescriptor = pxGetNetworkBufferWithDescriptor( xBytesReceived, 0 );
if( pxBufferDescriptor != NULL )
{
ReceiveData( pxBufferDescriptor->pucEthernetBuffer );
pxBufferDescriptor->xDataLength = xBytesReceived;
if( eConsiderFrameForProcessing( pxBufferDescriptor->pucEthernetBuffer )
== eProcessBuffer )
{
pxBufferDescriptor->pxEndPoint = FreeRTOS_MatchingEndpoint( pxMyInterface, pxBufferDescriptor->pucEthernetBuffer );
if( pxBufferDescriptor->pxEndPoint != NULL )
{
xRxEvent.eEventType = eNetworkRxEvent;
xRxEvent.pvData = ( void * ) pxBufferDescriptor;
if( xSendEventStructToIPTask( &xRxEvent, 0 ) == pdFALSE )
{
vReleaseNetworkBufferAndDescriptor( pxBufferDescriptor );
iptraceETHERNET_RX_EVENT_LOST();
}
else
{
iptraceNETWORK_INTERFACE_RECEIVE();
}
}
else
{
vReleaseNetworkBufferAndDescriptor( pxBufferDescriptor );
}
}
else
{
vReleaseNetworkBufferAndDescriptor( pxBufferDescriptor );
}
}
else
{
iptraceETHERNET_RX_EVENT_LOST();
}
}
}
}
简单的(并非高效的零拷贝)接收处理程序示例
3. 更高效的网络接口移植层示例
查看本节内容前,请先查阅
上节有关如何创建简单的网络接口移植层的内容。
简单的网络接口在
TCP/IP 堆栈使用、管理的缓冲区和以太网(或其他网络)
MAC 驱动程序使用、管理的缓冲区之间复制以太网帧。 在上述
缓冲区之间复制数据使得驱动程序实现变得简单,但效率低下。
零拷贝网络接口不在上述缓冲区之间复制数据,而是在
TCP/IP 堆栈和以太网 MAC 驱动程序之间将引用传递给缓冲区
。
零拷贝接口更复杂,并且很少能在不
编辑以太网 MAC 驱动程序的情况下被创建。
如要使用零拷贝进行传送,则必须
设置
ipconfigZERO_COPY_TX_DRIVER 为 1。
大多数以太网硬件都使用 DMA (直接内存访问)
在以太网硬件和预分配 RAM 缓冲区之间移动帧。 通常
可使用一组 DMA
描述符引用预分配内存缓冲区。 DMA 描述符通常是链式的——每个描述符
都指向链中的下一个描述符,链中最后一个描述符则指向
第一个描述符。
适用于零拷贝移植层的 pfInitialise() 实现示例
pfInitialise() 必须使用
pucGetNetworkBuffer()
来获取接收 DMA 描述符所指向的指针。 不必为
传输 DMA 描述符而分配任何缓冲区——
当有数据可发送时,可(通过引用)将其传入缓冲区
。
初始化 DMA Rx 描述符以指向由
pucGetNetworkBuffer() 分配的缓冲区。
初始化之后,DMA Tx 描述符不指向任何缓冲区
。
适用于零拷贝层的 pfOutput() 实现示例
pfOutput() 不会将传输中的帧复制到由
MAC 驱动程序管理的缓冲区中(它做不到,因为
DMA Tx 描述符不指向任何缓冲区) ,而是会
更新下一个 DMA Tx 描述符,使此描述符指向包含数据的缓冲区
。
注意: 以太网缓冲区内的数据被传输后,必须将此缓冲区释放
。 若有使用 BufferAllocation_2.c,
那
以太网缓冲区则无法从以太网传输端中断中释放,
因此,必须在下次使用同一 DMA 描述符时,通过 pfOutput()
函数释放它。 反正通常只使用一、两个描述符传输数据,
所以不会浪费太多的
RAM。
static BaseType_t prvSAM_NetworkInterfaceOutput(
NetworkInterface_t * pxInterface,
NetworkBufferDescriptor_t * const pxDescriptor,
BaseType_t xReleaseAfterSend )
{
DMADescriptor_t *pxDMATxDescriptor;
pxDMATxDescriptor = GetNextTxDescriptor();
if( pxDMATxDescriptor->pucEthernetBuffer != NULL )
{
vReleaseNetworkBuffer( pxDMATxDescriptor->pucEthernetBuffer );
}
pxDMATxDescriptor->pucEthernetBuffer = pxDescriptor->pucEthernetBuffer;
pxDMATxDescriptor->xDataLength = pxDescriptor->xDataLength;
SendData( pxDMATxDescriptor );
iptraceNETWORK_INTERFACE_TRANSMIT();
if( xReleaseAfterSend != pdFALSE )
{
pxDescriptor->pucEthernetBuffer = NULL;
vReleaseNetworkBufferAndDescriptor( pxDescriptor );
}
return pdTRUE;
}
xNetworkInterfaceOutput() 的零拷贝实现示例
使用零拷贝接收数据
如果使用零拷贝执行接收,则必须
设置
ipconfigZERO_COPY_RX_DRIVER 为 1。
接收 DMA 会将接收到的帧放入
接收 DMA 描述符指向的缓冲区。 此缓冲区通过调用
pucGetNetworkBuffer() 来分配,
以便可以从网络缓冲区描述符中引用它,
从而通过引用直接将其传递至 TCP/IP 堆栈。 然后
分配一个新的空网络缓冲区,并更新接收 DMA 描述符
以指向随时可接收下一个数据包的空缓冲区。
所有关于简单接收处理程序实现的说明事项
(包括旨在提高效率的高级功能)均适用于零
拷贝接收处理程序,此处不再赘述。
static void prvEMACDeferredInterruptHandlerTask( void *pvParameters )
{
NetworkBufferDescriptor_t *pxDescriptor;
size_t xBytesReceived;
DMADescriptor_t *pxDMARxDescriptor;
uint8_t *pucTemp;
IPStackEvent_t xRxEvent;
for( ;; )
{
ulTaskNotifyTake( pdFALSE, portMAX_DELAY );
pxDMARxDescriptor = GetNextRxDescriptor();
pxDescriptor = pxGetNetworkBufferWithDescriptor( ipTOTAL_ETHERNET_FRAME_SIZE, 0 );
pucTemp = pxDescriptor->pucEthernetBuffer;
pxDescriptor->pucEthernetBuffer = pxDMARxDescriptor->pucEthernetBuffer;
pxDescriptor->xDataLength = pxDMARxDescriptor->xDataLength;
pxDMARxDescriptor->puxEthernetBuffer = pucTemp;
*( ( NetworkBufferDescriptor_t ** )
( pxDescriptor->pucEthernetBuffer - ipBUFFER_PADDING ) ) = pxDescriptor;
*( ( NetworkBufferDescriptor_t ** )
( pxDMARxDescriptor->pucEthernetBuffer - ipBUFFER_PADDING ) ) = pxDMARxDescriptor;
if( eConsiderFrameForProcessing( pxDescriptor->pucEthernetBuffer )
== eProcessBuffer )
{
pxDescriptor->pxEndPoint = FreeRTOS_MatchingEndpoint( pxMyInterface, pxBufferDescriptor->pucEthernetBuffer );
if( pxDescriptor->pxEndPoint != NULL )
{
xRxEvent.eEventType = eNetworkRxEvent;
xRxEvent.pvData = ( void * ) pxDescriptor;
if( xSendEventStructToIPTask( &xRxEvent, 0 ) == pdFALSE )
{
vReleaseNetworkBufferAndDescriptor( pxDescriptor );
iptraceETHERNET_RX_EVENT_LOST();
}
else
{
iptraceNETWORK_INTERFACE_RECEIVE();
}
}
else
{
vReleaseNetworkBufferAndDescriptor( pxDescriptor );
}
}
else
{
vReleaseNetworkBufferAndDescriptor( pxDescriptor );
}
}
}
零拷贝接收处理程序函数示例
Copyright (C) Amazon Web Services, Inc. or its affiliates. All rights reserved.