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+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+TCP are located in the FreeRTOS-Plus-TCP/portable/NetworkInterface directory of the FreeRTOS+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.
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.
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:
pucEthernetBuffer points to the start of the network buffer.
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.
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+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
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
/* First statically allocate the buffers, ensuring an additional ipBUFFER_PADDING
bytes are allocated to each buffer. This example makes no effort to align
the start of the buffers, but most hardware will have an alignment requirement.
If an alignment is required then the size of each buffer must be adjusted to
ensure it also ends on an alignment boundary. Below shows an example assuming
the buffers must also end on an 8-byte boundary. */
#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 ];
/* Next provide the vNetworkInterfaceAllocateRAMToBuffers() function, which
simply fills in the pucEthernetBuffer member of each descriptor. */
void vNetworkInterfaceAllocateRAMToBuffers(
NetworkBufferDescriptor_t pxNetworkBuffers[ ipconfigNUM_NETWORK_BUFFERS ] )
{
BaseType_t x;
for( x = 0; x < ipconfigNUM_NETWORK_BUFFERS; x++ )
{
/* pucEthernetBuffer is set to point ipBUFFER_PADDING bytes in from the
beginning of the allocated buffer. */
pxNetworkBuffers[ x ].pucEthernetBuffer = &( ucBuffers[ x ][ ipBUFFER_PADDING ] );
/* The following line is also required, but will not be required in
future versions. */
*( ( uint32_t * ) &ucBuffers[ x ][ 0 ] ) = ( uint32_t ) &( pxNetworkBuffers[ x ] );
}
}
An example implementation of vNetworkBufferInterfaceAllocateRAMToBuffers().
The Ethernet MAC driver will place received Ethernet frames into a buffer. The port layer has to:
/* The timeout is specified in RTOS ticks. Returns pdTRUE if the message was
sent successfully, otherwise return pdFALSE. */
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+TCP (which are not necessarily
optimised) can be found in the FreeRTOS-Plus-TCP/portable/NetworkInterface
directory.
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+TCP configuration page for more information.
/* The deferred interrupt handler is a standard RTOS task. FreeRTOS's centralised deferred interrupt handling capabilities can also be used. */ static void prvEMACDeferredInterruptHandlerTask( void *pvParameters ) { NetworkBufferDescriptor_t *pxBufferDescriptor; size_t xBytesReceived; /* Used to indicate that xSendEventStructToIPTask() is being called because of an Ethernet receive event. */ IPStackEvent_t xRxEvent; for( ;; ) { /* Wait for the Ethernet MAC interrupt to indicate that another packet has been received. The task notification is used in a similar way to a counting semaphore to count Rx events, but is a lot more efficient than a semaphore. */ ulTaskNotifyTake( pdFALSE, portMAX_DELAY ); /* See how much data was received. Here it is assumed ReceiveSize() is a peripheral driver function that returns the number of bytes in the received Ethernet frame. */ xBytesReceived = ReceiveSize(); if( xBytesReceived > 0 ) { /* Allocate a network buffer descriptor that points to a buffer large enough to hold the received frame. As this is the simple rather than efficient example the received data will just be copied into this buffer. */ pxBufferDescriptor = pxGetNetworkBufferWithDescriptor( xBytesReceived, 0 ); if( pxBufferDescriptor != NULL ) { /* pxBufferDescriptor->pucEthernetBuffer now points to an Ethernet buffer large enough to hold the received data. Copy the received data into pcNetworkBuffer->pucEthernetBuffer. Here it is assumed ReceiveData() is a peripheral driver function that copies the received data into a buffer passed in as the function's parameter. Remember! While is is a simple robust technique - it is not efficient. An example that uses a zero copy technique is provided further down this page. */ ReceiveData( pxBufferDescriptor->pucEthernetBuffer ); pxBufferDescriptor->xDataLength = xBytesReceived; /* See if the data contained in the received Ethernet frame needs to be processed. NOTE! It is preferable to do this in the interrupt service routine itself, which would remove the need to unblock this task for packets that don't need processing. */ if( eConsiderFrameForProcessing( pxBufferDescriptor->pucEthernetBuffer ) == eProcessBuffer ) { /* The event about to be sent to the TCP/IP is an Rx event. */ xRxEvent.eEventType = eNetworkRxEvent; /* pvData is used to point to the network buffer descriptor that now references the received data. */ xRxEvent.pvData = ( void * ) pxBufferDescriptor; /* Send the data to the TCP/IP stack. */ if( xSendEventStructToIPTask( &xRxEvent, 0 ) == pdFALSE ) { /* The buffer could not be sent to the IP task so the buffer must be released. */ vReleaseNetworkBufferAndDescriptor( pxBufferDescriptor ); /* Make a call to the standard trace macro to log the occurrence. */ iptraceETHERNET_RX_EVENT_LOST(); } else { /* The message was successfully sent to the TCP/IP stack. Call the standard trace macro to log the occurrence. */ iptraceNETWORK_INTERFACE_RECEIVE(); } } else { /* The Ethernet frame can be dropped, but the Ethernet buffer must be released. */ vReleaseNetworkBufferAndDescriptor( pxBufferDescriptor ); } } else { /* The event was lost because a network buffer was not available. Call the standard trace macro to log the occurrence. */ iptraceETHERNET_RX_EVENT_LOST(); } } } } An example of a simple (rather than more efficient zero copy) receive handler
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.
Chained DMA descriptors
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.
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; /* This example assumes GetNextTxDescriptor() is an Ethernet MAC driver library function that returns a pointer to a DMA descriptor of type DMADescriptor_t. */ pxDMATxDescriptor = GetNextTxDescriptor(); /* Further, this example assumes the DMADescriptor_t type has a member called pucEthernetBuffer that points to the buffer the DMA will transmit, and a member called xDataLength that holds the length of the data the DMA will transmit. If BufferAllocation_2.c is being used then the DMA descriptor may still be pointing to the buffer it last transmitted. If this is the case then the old buffer must be released (returned to the TCP/IP stack) before descriptor is updated to point to the new data waiting to be transmitted. */ if( pxDMATxDescriptor->pucEthernetBuffer != NULL ) { /* Note this is releasing just an Ethernet buffer, not a network buffer descriptor as the descriptor has already been released. */ vReleaseNetworkBuffer( pxDMATxDescriptor->pucEthernetBuffer ); } /* Configure the DMA descriptor to send the data referenced by the network buffer descriptor. This example assumes SendData() is an Ethernet peripheral driver function. */ pxDMATxDescriptor->pucEthernetBuffer = pxDescriptor->pucEthernetBuffer; pxDMATxDescriptor->xDataLength = pxDescriptor->xDataLength; SendData( pxDMATxDescriptor ); /* Call the standard trace macro to log the send event. */ iptraceNETWORK_INTERFACE_TRANSMIT(); /* The network buffer descriptor must now be returned to the TCP/IP stack, but the Ethernet buffer referenced by the network buffer descriptor is still in use by the DMA. Remove the reference to the Ethernet buffer from the network buffer descriptor so releasing the network buffer descriptor does not result in the Ethernet buffer also being released. xReleaseAfterSend() should never equal pdFALSE when ipconfigZERO_COPY_TX_DRIVER is set to 1 (as it should be if data is transmitted using a zero copy driver.*/ if( xReleaseAfterSend != pdFALSE ) { pxDescriptor->pucEthernetBuffer = NULL; vReleaseNetworkBufferAndDescriptor( pxDescriptor ); } return pdTRUE; } An example zero copy implementation of xNetworkInterfaceOutput()
The receive DMA will place received frames into the buffer pointed to by the 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.
/* The deferred interrupt handler is a standard RTOS task. FreeRTOS's centralised deferred interrupt handling capabilities can also be used - however for additional speed use BufferAllocation_1.c to perform the entire operation in the interrupt handler. */ static void prvEMACDeferredInterruptHandlerTask( void *pvParameters ) { NetworkBufferDescriptor_t *pxDescriptor; size_t xBytesReceived; DMADescriptor_t *pxDMARxDescriptor; uint8_t *pucTemp; /* Used to indicate that xSendEventStructToIPTask() is being called because of an Ethernet receive event. */ IPStackEvent_t xRxEvent; for( ;; ) { /* Wait for the Ethernet MAC interrupt to indicate that another packet has been received. The task notification is used in a similar way to a counting semaphore to count Rx events, but is a lot more efficient than a semaphore. */ ulTaskNotifyTake( pdFALSE, portMAX_DELAY ); /* This example assumes GetNextRxDescriptor() is an Ethernet MAC driver library function that returns a pointer to the DMA descriptor (of type DMADescriptor_t again) that references the Ethernet buffer containing the received data. */ pxDMARxDescriptor = GetNextRxDescriptor(); /* Allocate a new network buffer descriptor that references an Ethernet frame large enough to hold the maximum network packet size (as defined in the FreeRTOSIPConfig.h header file). */ pxDescriptor = pxGetNetworkBufferWithDescriptor( ipTOTAL_ETHERNET_FRAME_SIZE, 0 ); /* Copy the pointer to the newly allocated Ethernet frame to a temporary variable. */ pucTemp = pxDescriptor->pucEthernetBuffer; /* This example assumes that the DMADescriptor_t type has a member called pucEthernetBuffer that points to the Ethernet buffer containing the received data, and a member called xDataLength that holds the length of the received data. Update the newly allocated network buffer descriptor to point to the Ethernet buffer that contains the received data. */ pxDescriptor->pucEthernetBuffer = pxDMARxDescriptor->pucEthernetBuffer; pxDescriptor->xDataLength = pxDMARxDescriptor->xDataLength; /* Update the Ethernet Rx DMA descriptor to point to the newly allocated Ethernet buffer. */ pxDMARxDescriptor->puxEthernetBuffer = pucTemp; /* A pointer to the descriptor is stored at the front of the buffer, so swap these too. */ *( ( NetworkBufferDescriptor_t ** ) ( pxDescriptor->pucEthernetBuffer - ipBUFFER_PADDING ) ) = pxDescriptor; *( ( NetworkBufferDescriptor_t ** ) ( pxDMARxDescriptor->pucEthernetBuffer - ipBUFFER_PADDING ) ) = pxDMARxDescriptor; /* * The network buffer descriptor now points to the Ethernet buffer that * contains the received data, and the Ethernet DMA descriptor now points * to a newly allocated (and empty) Ethernet buffer ready to receive more * data. No data was copied. Only pointers to data were swapped. * * THE REST OF THE RECEIVE HANDLER FUNCTION FOLLOWS THE EXAMPLE PROVIDED * FOR THE SIMPLE ETHERNET INTERFACE IMPLEMENTATION, whereby the network * buffer descriptor is sent to the TCP/IP on the network event queue. */ /* See if the data contained in the received Ethernet frame needs to be processed. NOTE! It might be possible to do this in the interrupt service routine itself, which would remove the need to unblock this task for packets that don't need processing. */ if( eConsiderFrameForProcessing( pxDescriptor->pucEthernetBuffer ) == eProcessBuffer ) { /* The event about to be sent to the TCP/IP is an Rx event. */ xRxEvent.eEventType = eNetworkRxEvent; /* pvData is used to point to the network buffer descriptor that references the received data. */ xRxEvent.pvData = ( void * ) pxDescriptor; /* Send the data to the TCP/IP stack. */ if( xSendEventStructToIPTask( &xRxEvent, 0 ) == pdFALSE ) { /* The buffer could not be sent to the IP task so the buffer must be released. */ vReleaseNetworkBufferAndDescriptor( pxDescriptor ); /* Make a call to the standard trace macro to log the occurrence. */ iptraceETHERNET_RX_EVENT_LOST(); } else { /* The message was successfully sent to the TCP/IP stack. Call the standard trace macro to log the occurrence. */ iptraceNETWORK_INTERFACE_RECEIVE(); } } else { /* The Ethernet frame can be dropped, but the Ethernet buffer must be released. */ vReleaseNetworkBufferAndDescriptor( pxDescriptor ); } } } An example of a zero copy receive handler function