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.
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;
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!