The Ada Embedded Network is a small IPv4 network stack intended to run on STM32F746 or equivalent devices. This network stack is implemented in Ada 2012 and its architecture has been inspired by the BSD network architecture described in the book "TCP/IP Illustrated, Volume 2, The Implementation" by Gary R. Wright and W. Richard Stevens.
This article discusses the Ethernet Driver design and implementation. The IP protocol layer part will be explained in a next article.
In any network stack, the buffer management is key to obtain good performance. Let's see how it is modeled.
Net.Buffers
The Net.Buffers
package provides support for network buffer management. A network buffer can hold a single packet frame so that it is limited to 1500 bytes of payload with 14 or 16 bytes for the Ethernet header. The network buffers are allocated by the Ethernet driver during the initialization to setup the Ethernet receive queue. The allocation of network buffers for the transmission is under the responsibility of the application.
Before receiving a packet, the application also has to allocate a network buffer. Upon successful reception of a packet by the Receive
procedure, the allocated network buffer will be given to the Ethernet receive queue and the application will get back the received buffer. There is no memory copy.
The package defines two important types: Buffer_Type
and Buffer_List
. These two types are limited types to forbid copies and force a strict design to applications. The Buffer_Type
describes the packet frame and it provides various operations to access the buffer. The Buffer_List
defines a list of buffers.
The network buffers are kept within a single linked list managed by a protected object. Because interrupt handlers can release a buffer, that protected object has the priority System.Max_Interrupt_Priority
. The protected operations are very basic and are in O(1) complexity so that their execution is bounded in time whatever the arguments.
Before anything, the network buffers have to be allocated. The application can do this by reserving some memory region (using STM32.SDRAM.Reserve
) and adding the region with the Add_Region
procedure. The region must be a multiple of NET_ALLOC_SIZE
constant. To allocate 32 buffers, you can do the following:
NET_BUFFER_SIZE : constant Interfaces.Unsigned_32 := Net.Buffers.NET_ALLOC_SIZE * 32;
...
Net.Buffers.Add_Region (STM32.SDRAM.Reserve (Amount => NET_BUFFER_SIZE), NET_BUFFER_SIZE);
An application will allocate a buffer by using the Allocate
operation and this is as easy as:
Packet : Net.Buffers.Buffer_Type;
...
Net.Buffers.Allocate (Packet);
What happens if there is no available buffer? No exception is raised because the networks stack is intended to be used in embedded systems where exceptions are not available. You have to check if the allocation succeeded by using the Is_Null
function:
if Packet.Is_Null then
null; -- Oops
end if;
Net.Interfaces
The Net.Interfaces
package represents the low level network driver that is capable of sending and receiving packets. The package defines the Ifnet_Type
abstract type which defines the three important operations:
Initialize
to configure and setup the network interface,Send
to send a packet on the network.Receive
to wait for a packet and get it from the network.
STM32 Ethernet Driver
The STM32 Ethernet driver implements the three important operations required by the Ifnet_Type
abstraction. The Initialize
procedure performs the STM32 Ethernet initialization, configures the receive and transmit rings and setup to accept interrupts. This operation must be called prior to any other.
Sending a packet
The STM32 Ethernet driver has a transmit queue to manage the Ethernet hardware transmit ring and send packets over the network. The transmit queue is a protected object so that concurrent accesses between application task and the Ethernet interrupt are safe. To transmit a packet, the driver adds the packet to the next available transmit descriptor. The packet buffer ownership is transferred to the transmit ring so that there is no memory copy. Once the packet is queued, the application has lost the buffer ownership. The buffer being owned by the DMA, it will be released by the transmit interrupt, as soon as the packet is sent (3).
When the transmit queue is full, the application is blocked until a transmit descriptor becomes available.
Receiving a packet
The SMT32 Ethernet driver has a receive queue which is a second protected object, separate from the transmit queue. The receive queue is used by the Ethernet hardware to control the Ethernet receive ring and by the application to pick received packets. Each receive descriptor is assigned a packet buffer that is owned by default to the DMA. When a packet is available and the application calls the Wait_Packet
operation, the packet buffer ownership is transferred to the application to avoid any memory copy. To avoid having a ring descriptor loosing its buffer, the application gives a new buffer that is used for the ring descriptor. This is why the application has first to allocate the buffer (1), call the Receive
operation (2) to get back the packet in a new buffer and finally release the buffer when it has done with it (3).
Receive loop example
Below is an example of a task that loops to receive Ethernet packets and process them. This is the main receiver task used by the EtherScope monitoring tool.
The Ifnet driver initialization is done in the main EtherScope task. We must not use the driver before it is full initialized. This is why the task starts to loop for the Ifnet driver to be ready.
task body Controller is
use type Ada.Real_Time.Time;
Packet : Net.Buffers.Buffer_Type;
begin
while not Ifnet.Is_Ready loop
delay until Ada.Real_Time.Clock + Ada.Real_Time.Seconds (1);
end loop;
Net.Buffers.Allocate (Packet);
loop
Ifnet.Receive (Packet);
EtherScope.Analyzer.Base.Analyze (Packet);
end loop;
end Controller;
Then, we allocate a packet buffer and enter in the main loop to continuously receive a packet and do some processing. The careful reader will note that there is no buffer release. We don't need that because the Receive
driver operation will pick our buffer for its ring and it will give us a buffer that holds the received packet. We will give him back that buffer at the next loop. In this application, the number of buffers needed by the buffer pool is the size of the Ethernet Rx ring plus one.
The complete source is available in etherscope-receiver.adb.
Using this design and implementation, the EtherScope application has shown that it can sustain more than 95Mb of traffic for analysis. Quite nice for 216 Mhz ARM Cortex-M7!
Add a comment
To add a comment, you must be connected. Login