Porting FreeRTOS-Plus-TCP to a Different Microcontroller
Introduction
The implementation of the network interface, and in particular the Ethernet
MAC driver, are crucial to the data throughput that can be achieved when
using the embedded TCP/IP stack. For high throughput the
MAC driver must make efficient use of the DMA and avoid copying
data where possible. End to end zero copy is possible with
FreeRTOS-Plus-TCP for UDP packets, and an advanced interface exists that also
allows zero copy for TCP packets. There are also advanced options available
that allow packets to be filtered before they are even sent to the embedded
TCP/IP stack, and packets that are received in quick successions can be
sent to the embedded TCP/IP stack in one go rather than individually.
However, few applications actually require throughput to be maximised,
especially on small MCUs, and the implementer may instead
opt to sacrifice throughput in favour or simplicity.
This page describes how to interface FreeRTOS-Plus-TCP with a network driver,
and provides an outline example of both a simple
and a faster (but more complex)
interface. It is very important to refer to these examples as
they demonstrate how network buffers are freed after data has been
transmitted.
The network driver port layers that ship with FreeRTOS-Plus-TCP are located in the
FreeRTOS-Plus-TCP/portable/NetworkInterface directory of the
FreeRTOS-Plus-TCP download. Note however that these drivers have been created
in order to allow testing of the embedded TCP/IP stack, and are not
intended to represent optimised examples.
Summary Bullet Points
The network interface port layer sits
between the IP stack and the embedded
Ethernet hardware drivers
-
Each MCU to which FreeRTOS-Plus-TCP is ported requires an Ethernet
MAC driver. It is assumed this already exists and is known to
work.
-
FreeRTOS-Plus-TCP is ported to new hardware by providing a 'network
interface port layer' that provides the interface between the embedded
TCP/IP stack and the Ethernet MAC driver. See the image on the
right.
-
The network interface port layer must provide a function
called xNetworkInterfaceInitialise() that
initialises the MAC driver.
-
The network interface port layer must provide a function
called xNetworkInterfaceOutput()
that sends data received from the embedded TCP/IP stack to the
Ethernet MAC driver for transmission.
-
The network interface port layer must send packets received
from the Ethernet MAC driver to the TCP/IP stack by calling
xSendEventStructToIPTask().
-
Only if
BufferAllocation_1.c is used for buffer allocation, the network
interface port layer must statically allocate network buffers and
provide a function called
vNetworkInterfaceAllocateRAMToBuffers()
to assign the statically allocated network buffers to network
buffer descriptors.
-
Network buffers (the buffer in which the actual data is stored)
are referenced using
NetworkBufferDescriptor_t
structures.
-
The embedded TCP/IP stack provides a set of
porting utility functions
to allow the port layer to perform actions such as obtaining and
freeing network buffers.
This page provides more information on each of these steps, and provides two
examples. The first example demonstrates
how to implement a simple (but slower) driver. The
second example
demonstrates how to implement a more sophisticated (and faster) driver.
It is very important to refer to these examples as
they demonstrate how network buffers are freed after data has been
transmitted.
Ethernet (or other network) frames are stored in network buffers. A network buffer descriptor
(a variable of type
NetworkBufferDescriptor_t) is used to describe a network
buffer, and pass network buffers between the TCP/IP stack and the network
drivers.
pucEthernetBuffer points to the start of the network buffer.
xDataLength holds the size of the buffer in bytes, excluding the Ethernet CRC bytes.
Only the following two members of the NetworkBufferDescriptor_t structure should
be accessed:
-
uint8_t *pucEthernetBuffer;
pucEthernetBuffer points to the start of the network buffer.
-
size_t xDataLength
xDataLength holds the size of the network buffer pointed to by pucEthernetBuffer.
The size is specified in bytes but the length excludes the bytes that
hold the Ethernet frame's CRC byte.
pucGetNetworkBuffer()
is used to obtain just the network buffer itself, and is normally only
used in zero copy drivers.
pxGetNetworkBufferWithDescriptor()
is used to obtain both a network buffer and a network buffer
descriptor at the same time.
Function That Must be Implemented by the Port Layer
xNetworkInterfaceInitialise() must prepare the Ethernet MAC
to send and receive data. In most cases this will just involve calling
whichever initialise function is provided with the Ethernet MAC peripheral
drivers - which will in turn ensure the MAC hardware is enabled and clocked,
as well as configure the MAC peripheral's DMA descriptors.
xNetworkInterfaceInitialise() does not take any parameters,
returns pdPASS if the initialisation was successful, and returns
pdFAIL if the initialisation fails.
BaseType_t xNetworkInterfaceInitialise( void );
The xNetworkInterfaceInitialise() function prototype
The TCP/IP stack calls xNetworkInterfaceOutput() whenever a network
buffer is ready to be transmitted.
The buffer to transmit is described by the descriptor passed into the
function using the function's pxDescriptor parameter. If xReleaseAfterSend
does not equal pdFALSE then both the buffer and the buffer's descriptor
must be released (returned) back to the embedded TCP/IP stack by the driver
code when they are no longer required. If xReleaseAfterSend is pdFALSE
then both the network buffer and the buffer's descriptor will be released
by the TCP/IP stack itself (in which case the driver does not need to
release them).
Note that, at the time of writing, the value returned from
xNetworkInterfaceOutput() is ignored. The embedded TCP/IP
stack will NOT call xNetworkInterfaceOutput() for the same network buffer
twice, even if the first call to xNetworkInterfaceOutput() could not send
the network buffer onto the network.
Basic and more advanced examples are provided below, and the
FreeRTOS-Plus-TCP/portable/NetworkInterface directory of the
FreeRTOS-Plus-TCP download contained examples that can be referenced. Note
however that the examples in the download may not be optimised.
BaseType_t xNetworkInterfaceOutput( NetworkBufferDescriptor_t * const pxDescriptor,
BaseType_t xReleaseAfterSend );
The xNetworkInterfaceOutput() function prototype
BufferAllocation_1.c
uses pre-allocated network buffers that are normally
statically allocated at compile time.
The number of network buffers that must
be allocated is set by the
ipconfigNUM_NETWORK_BUFFER_DESCRIPTORS
definition in FreeRTOSIPConfig.h, and the size of each buffer must be
( ipTOTAL_ETHERNET_FRAME_SIZE + ipBUFFER_PADDING ). ipTOTAL_ETHERNET_FRAME_SIZE is
calculated automatically from the value of
ipconfigNETWORK_MTU,
and ipBUFFER_PADDING is calculated automatically from
ipconfigBUFFER_PADDING.
Networking hardware can impose strict alignment requirements on the
allocated buffers, so it is recommended that the buffers are allocated
in the embedded Ethernet driver itself - that way the buffer's alignment
can always be made to match the hardware's requirements.
The embedded TCP/IP stack allocates the network buffer descriptors, but
does not know anything about the alignment of the network buffers themselves.
Therefore the embedded Ethernet driver must also provide a function called
vNetworkInterfaceAllocateRAMToBuffers() that allocates a statically
declared buffer to each descriptor. Note that ipBUFFER_PADDING bytes
at the beginning of the buffer are left for use by the embedded TCP/IP
stack itself. See the example below.
void vNetworkInterfaceAllocateRAMToBuffers(
NetworkBufferDescriptor_t xDescriptors[ ipconfigNUM_NETWORK_BUFFERS ] );
The vNetworkInterfaceAllocateRAMToBuffers() function prototype
#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 ] );
}
}
An example implementation of vNetworkBufferInterfaceAllocateRAMToBuffers().
The port layer can use the following function:
-
pxGetNetworkBufferWithDescriptor() -
Obtains both a network buffer and a descriptor that describes the
network buffer. This function can also be used to obtain just a
network buffer descriptor - which can be useful when implementing
zero copy drivers.
-
vReleaseNetworkBufferAndDescriptor() -
Releases (returns to the embedded TCP/IP stack) a network buffer descriptor, and
the network buffer referenced by the descriptor (if any).
-
pxNetworkBufferGetFromISR() -
[not available when
BufferAllocation_2.c is used]
-
vNetworkBufferReleaseFromISR() -
[not available when BufferAllocation_2.c is used]
-
eConsiderFrameForProcessing() -
Used to determine if data received from the network needs to be
passed to the embedded TCP/IP stack. Ideally this function would
be called from the network interrupt to allow received packets to
be discarded at the earliest possible opportunity.
-
xSendEventStructToIPTask() -
xSendEventStructToIPTask() is a function used by the embedded TCP/IP
stack itself to send various events to the RTOS task that is running
the embedded TCP/IP stack. The port layer uses the function with
eNetworkRxEvent events to pass received data into the stack for processing.
-
pucGetNetworkBuffer() -
Obtains just a network buffer, without a network buffer descriptor.
This function is normally only used in zero copy interfaces to
allocate buffers to DMA descriptors.
-
vReleaseNetworkBuffer() -
Releases (returns to the embedded TCP/IP stack) a network buffer
by itself - without a network buffer descriptor. This function
is normally only used in zero copy interfaces where network buffers
were allocated to DMA descriptors.
The Ethernet MAC driver will place received Ethernet frames into a buffer.
The port layer has to:
-
Determine if the received data needs to be sent to the embedded
TCP/IP stack. Ideally this would be done in the receive interrupt
itself to allow unnecessary packets to be dropped at the earliest
possible time.
-
Allocate a network buffer descriptor.
-
Set the xDataLength and pucEthernetBuffer members of the allocated
descriptor accordingly (see both the basic and zero copy examples
later on this page).
-
Call xSendEventStructToIPTask() to send the network buffer
descriptor into the embedded TCP/IP stack for processing
(see both the basic and zero copy examples later on this
page).
typedef struct IP_TASK_COMMANDS
{
eIPEvent_t eEventType;
void *pvData;
} IPStackEvent_t;
The IPStackEvent_t type
BaseType_t xSendEventStructToIPTask( const IPStackEvent_t *pxEvent, TickType_t xTimeout )
The xSendEventStructToIPTask() function prototype
Basic and more advanced examples are provided below. The network driver
port layers that ship with FreeRTOS-Plus-TCP (which are not necessarily
optimised) can be found in the FreeRTOS-Plus-TCP/portable/NetworkInterface
directory.
Network Interface Port Layer Examples
Simple network interfaces can be created by copying Ethernet frames
between buffers allocated by the Ethernet MAC driver libraries and buffers
allocated by the port layer.
[A more efficient zero copy
alternative is provided
after the simple example.]
Example implementation of xNetworkInterfaceInitialise() for a basic port layer
BaseType_t xNetworkInterfaceInitialise( void )
{
BaseType_t xReturn;
if( InitialiseNetwork() == 0 )
{
xReturn = pdFAIL;
}
else
{
xReturn = pdPASS;
}
return xReturn;
}
xNetworkInterfaceInitialise() is hardware specific, therefore
this example describes what needs to be done without showing any detail
Example implementation of xNetworkInterfaceOutput() for a basic port layer
BaseType_t xNetworkInterfaceOutput( NetworkBufferDescriptor_t * const pxDescriptor,
BaseType_t xReleaseAfterSend )
{
SendData( pxDescriptor->pucBuffer, pxDescriptor->xDataLength );
iptraceNETWORK_INTERFACE_TRANSMIT();
if( xReleaseAfterSend != pdFALSE )
{
vReleaseNetworkBufferAndDescriptor( pxDescriptor );
}
return pdTRUE;
}
Example implementation of xNetworkInterfaceOutput() suitable for a
simple (rather than zero copy) network interface implementation
Example of passing received data to the TCP/IP in a basic port layer
When a packet is received from the Ethernet (or other network) driver
the port layer must use a
NetworkBufferDescriptor_t
structure to describe the packet, then call
xSendEventStructToIPTask()
to send the NetworkBufferDescriptor_t structure to the embedded TCP/IP stack.
NOTE 1: If BufferAllocation_2.c
is used then network buffer
descriptors and Ethernet buffers cannot be allocated from inside an
interrupt service routine (ISR). In this case the Ethernet MAC receive
interrupt can defer the receive
processing to a task. This is demonstrated
below.
NOTE 2: There are numerous advanced techniques that can be employed
to minimise the amount of data sent from the port layer into the embedded
TCP/IP stack. For example, eConsiderFrameForProcessing()
can be called to determine if the received Ethernet frame needs to be
sent to the embedded TCP/IP stack at all, and Ethernet frames that are
received in quick succession can be sent to the embedded TCP/IP stack in
one go. See the
Hardware and Driver Specific Settings
section of the FreeRTOS-Plus-TCP configuration page for more information.
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 )
{
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
{
iptraceETHERNET_RX_EVENT_LOST();
}
}
}
}
An example of a simple (rather than more efficient zero copy) receive handler
It is intended that this section is read after the section that describes
how to create a simple network interface port layer.
Simple network interfaces copy Ethernet frames between buffers used and
managed by the TCP/IP stack and buffers used and managed by the Ethernet
(or other network) MAC drivers. Copying data between
buffers makes the driver's implementation simple, but is inefficient.
Zero copy network interfaces do not copy data between buffers, but instead
pass references to buffers between the TCP/IP stack and the Ethernet MAC
drivers.
Zero copy interfaces are more complex, and can rarely be created without
editing the Ethernet MAC drivers themselves.
If transmission is performed using zero copy then it is necessary to
set
ipconfigZERO_COPY_TX_DRIVER to 1.
Most Ethernet hardware will use DMA (Direct Memory Access) to move frames
between the Ethernet hardware and pre-allocated RAM buffers. Normally
the pre-allocated memory buffers are referenced using a set of DMA
descriptors. DMA descriptors are normally chained - each descriptor
points to the next in the chain, with the last in the chain pointing
back to the first.
Example implementation of xNetworkInterfaceInitialise() for a zero copy port layer
xNetworkInterfaceInitialise() must use
pucGetNetworkBuffer()
to obtain the pointers to which the receive DMA descriptors point. It is
not necessary to allocate any buffers for the transmit DMA descriptors -
the buffers will be passed in (by reference) as data becomes available
to send.
The DMA Rx descriptors are initialised to point to buffers
that were allocated by pucGetNetworkBuffer().
The DMA Tx descriptors do not point to any buffers after
they have been initialised.
Example implementation of xNetworkInterfaceOutput() for a zero copy layer
xNetworkInterfaceOutput() does not copy the frame being transmitted
to a buffer that is being managed by the MAC driver (it can't because
the DMA's Tx descriptors are not pointing to any buffers) but instead
updates the next DMA Tx descriptor so the descriptor points to the buffer
that already contains the data.
NOTE: The Ethernet buffer must be released after the data it
contains has been transmitted. If BufferAllocation_2.c
is used the
Ethernet buffer cannot be released from the Ethernet Transmit End interrupt,
so must be released by the xNetworkInterfaceOutput() function the next
time the same DMA descriptor is used. Often only one or two descriptors
are used for transmitting data anyway, so this does not waste too much
RAM.
BaseType_t xNetworkInterfaceOutput( 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;
}
An example zero copy implementation of xNetworkInterfaceOutput()
Receiving Data Using Zero-Copy
If reception is performed using zero copy then it is necessary to
set
ipconfigZERO_COPY_RX_DRIVER to 1.
The receive DMA will place received frames into the buffer pointed to
by the receive DMA descriptor. The buffer was allocated using a call
to pucGetNetworkBuffer(),
which allows it to be referenced from a network buffer descriptor, and
therefore passed by reference directly into the TCP/IP stack. A new empty
network buffer is then allocated, and the receive DMA descriptor is updated
to point to the empty buffer ready to receive the next packet.
All the notes regarding the implementation of the simple receive handler
(including advanced features to improve efficiency) apply to the zero
copy receive handler and are not repeated here.
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 )
{
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 );
}
}
}
An example of a zero copy receive handler function
Copyright (C) Amazon Web Services, Inc. or its affiliates. All rights reserved.