Overview
The Echo server listens to the UDP port 7 on the Ethernet network and it
sends back the received packet to the sender: this is the RFC 862 Echo protocol.
Our application follows that RFC but it also maintains a list of the last 10 messages
that have been received. The list is then displayed on the STM32 display so that
we get a visual feedback of the received messages.
The Echo server uses the DHCP client to get and IPv4 address and the default gateway.
We will see how that DHCP client is integrated in the application.
The application has two tasks. The main task loops to manage the refresh of the STM32
display and also to perform some network housekeeping such as the DHCP client management and
ARP table management. The second task is responsible for waiting Ethernet packets, analyzing them to handle ARP, ICMP and UDP packets.
Through this article, you will see:
- How the STM32 board and network stack are initialized,
- How the board gets an IPv4 address using DHCP,
- How to implement the UDP echo server,
- How to build and test the echo server.
Initialization
STM32 Board Initialization
First of all, the STM32 board must be initialized. There is no random generator available in the
Ada Ravenscar profile and we need one for the DHCP protocol for the XID generation. The STM32
provides a hardware random generator that we are going to use. The Initialize_RNG must be
called once during the startup and before any network operation is called.
We will use the display to list the messages that we have received. The Display instance
must be initialized and the layer configured.
with HAL.Bitmap;
with STM32.RNG.Interrupts;
with STM32.Board;
...
STM32.RNG.Interrupts.Initialize_RNG;
STM32.Board.Display.Initialize;
STM32.Board.Display.Initialize_Layer (1, HAL.Bitmap.ARGB_1555);
Network stack initialization
The network stack will need some memory to receive and send network packets.
As described in Using the Ada Embedded Network STM32 Ethernet Driver, we allocate the memory by using the SDRAM.Reserve function and the
Add_Region procedure to configure the network buffers that will be available.
An instance of the STM32 Ethernet driver must be declared in a package. The instance must be
aliased because the network stack will need to get an access to it.
with Interfaces;
with Net.Buffers;
with Net.Interfaces.STM32;
with STM32.SDRAM;
...
NET_BUFFER_SIZE : constant Interfaces.Unsigned_32 := Net.Buffers.NET_ALLOC_SIZE * 256;
Ifnet : aliased Net.Interfaces.STM32.STM32_Ifnet;
The Ethernet driver is
initialized by calling the Initialize procedure. By doing so, the Ethernet receive and transmit
rings are configured and we are ready to receive and transmit packets. On its side the Ethernet driver will also reserve some memory by using the Reserve and Add_Region operations. The
buffers allocated will be used for the Ethernet receive ring.
Net.Buffers.Add_Region (STM32.SDRAM.Reserve (Amount => NET_BUFFER_SIZE), NET_BUFFER_SIZE);
Ifnet.Initialize;
The Ethernet driver configures the MII transceiver and enables interrupts for the receive and
transmit rings.
Getting the IPv4 address with DHCP
At this stage, the network stack is almost ready but it does not have any IPv4 address.
We are going to use the DHCP protocol to automatically get an IPv4 address, get the default
gateway and other network configuration such as the DNS server.
The DHCP client uses a UDP socket on port 68 to send and receive DHCP messages.
Such DHCP client is provided by
the Net.DHCP package and we need to declare an instance of it. The DHCP client is based on
the UDP socket support that we are going to use for the echo server. The DHCP client instance
must be declared aliased because the UDP socket layer need to get an access to it to propagate
the DHCP packets that are received.
with Net.DHCP;
...
Dhcp : aliased Net.DHCP.Client;
The DHCP client instance must be initialized and the Ethernet driver interface must be passed
as parameter to
correctly configure and bind the UDP socket. After the Initialize procedure is called, the DHCP
state machine is ready to enter into action. We don't have an IPv4 address after the procedure returns.
Dhcp.Initialize (Ifnet'Access);
The DHCP client is using an asynchronous implementation to maintain the client state according
to RFC 2131. For this it has two important operations that
are called by tasks in different contexts. First the Process procedure is responsible
for sending requests to the DHCP
server and to manage the timeouts used for the retransmissions, renewal and lease expiration.
The Process procedure sends the DHCPDISCOVER and DHCPREQUEST messages.
On the other hand, the Receive procedure is called by the network stack
to handle the DHCP packets sent by the DHCP server. The Receive procedure gets the
DHCPOFFER and DHCPACK messages.
Getting an IPv4 address with the DHCP protocol can take some time and must be repeated continuously
due to the DHCP lease expiration. This is why the DHCP client must not be stopped and should
continue forever.
Refer to the DHCP documentation to learn
more about this process.
UDP Echo Server
Logger protected type
The echo server will record the message that are received. The message is inserted in the list
by the receive task and it is read by the main task. We use the an Ada protected type to
protect the list from concurrent accesses.
Each message is represented by the Message record which has an identifier that is unique
and incremented each time a message is received. To avoid dynamic
memory allocation the list of message is fixed and is represented by the Message_List array.
The list itself is managed by the Logger protected type.
type Message is record
Id : Natural := 0;
Content : String (1 .. 80) := (others => ' ');
end record;
type Message_List is array (1 .. 10) of Message;
protected type Logger is
procedure Echo (Content : in Message);
function Get return Message_List;
private
Id : Natural := 0;
List : Message_List;
end Logger;
The Logger protected type provides the Echo procedure to insert a message to the list
and the Get function to retrieve the list of messages.
Server Declaration
The UDP Echo Server uses the UDP socket support provided by the Net.Sockets.UDP package.
The UDP package defines the Socket abstract type which represents the UDP endpoint.
The Socket type is abstract because it defines the Receive procedure that must be
implemented. The Receive procedure will be called by the network stack when a UDP packet
for the socket is received.
The declaration of our echo server is the following:
with Net.Buffers;
with Net.Sockets;
...
type Echo_Server is new Net.Sockets.UDP.Socket with record
Count : Natural := 0;
Messages : Logger;
end record;
It holds a counter of message as well as the messages in the Logger protected type.
The echo server must implement the Receive procedure:
overriding
procedure Receive (Endpoint : in out Echo_Server;
From : in Net.Sockets.Sockaddr_In;
Packet : in out Net.Buffers.Buffer_Type);
The network stack will call the Receive procedure each time a UDP packet for the socket is received.
The From parameter will contain the IPv4 address and UDP port of the client that sent the
UDP packet. The Packet parameter contains the received UDP packet.
Server Implementation
Implementing the server is very easy because we only have to implement the Receive procedure
(we will leave the Logger protected type implementation as an exercise to the reader).
First we use the Get_Data_Size function to get the size of our packet. The function is able
to return different sizes to take into account one or several protocol headers. We want to know
the size of our UDP packet, excluding the UDP header. We tell Get_Data_Size we want to get
the UDP_PACKET size. This size represents the size of the echo message sent by the client.
Msg : Message;
Size : constant Net.Uint16 := Packet.Get_Data_Size (Net.Buffers.UDP_PACKET);
Len : constant Natural
:= (if Size > Msg.Content'Length then Msg.Content'Length else Natural (Size));
Having the size we truncate it so that we get a string that fits in our message. We then use
the Get_String procedure to retrieve the echo message in a string. This procedure gets from
the packet a number of characters that corresponds to the string length passed as parameter.
Packet.Get_String (Msg.Content (1 .. Len));
The Buffer_Type provides other Get operations to extract data from the packet.
It maintains a position in the buffer that tells the Get operation the location to read
in the packet and each Get updates the position according to what was actually read.
There are also several Put operations intended to be used to write and build the packet
before sending it. We are not going to use them because the echo server has to return the
original packet as is. Instead, we have to tell what is the size of the packet that we are
going to send. This is done by the Set_Data_Size procedure:
Packet.Set_Data_Size (Size);
Here we want to give the orignal size so that we return the full packet.
Now we can use the Send procedure to send the packet back to the client.
We use the client IPv4 address and UDP port represented by From as the destination address.
The Send procedure returns a status that tells whether the packet was successfully sent or
queued.
Status : Net.Error_Code;
...
Endpoint.Send (To => From, Packet => Packet, Status => Status);
Server Initialization
Now that the Echo_Server type is implemented, we have to make a global instance of it
and bind it to the UDP port 7 that corresponds to the UDP echo protocol. The port number must
be defined in network byte order (as in Unix Socket API) and this is why it is converted using
the To_Network function. We don't know our IPv4 address and by using 0 we tell the UDP stack
to use the IPv4 address that is configured on the Ethernet interface.
Server : aliased Echo_Server;
...
Server.Bind (Ifnet'Access, (Port => Net.Headers.To_Network (7),
Addr => (others => 0)));
Main loop and receive task
As explained in the overview, we need several tasks to handle the display, network housekeeping
and reception of Ethernet packets. To make it simple the display, ARP table management and DHCP
client management will be handled by the main task. The reception of Ethernet packet will be
handled by a second task. It is possible to use a specific task for the ARP management and
another one for the DHCP but there is no real benefit in doing so for our simple echo server.
The main loop repeats calls to the ARP Timeout procedure and the DHCP Process procedure.
The Process procedure returns a delay that we are supposed to wait but we are not going to
use it for this example. The main loop simply looks as follows:
Dhcp_Timeout : Ada.Real_Time.Time_Span;
...
loop
Net.Protos.Arp.Timeout (Ifnet);
Dhcp.Process (Dhcp_Timeout);
...
delay until Ada.Real_Time.Clock + Ada.Real_Time.Milliseconds (500);
end loop;
The receive task was described in the previous article
Using the Ada Embedded Network STM32 Ethernet Driver. The task is declared
at package level as follows:
task Controller with
Storage_Size => (16 * 1024),
Priority => System.Default_Priority;
And the implementation loops to receive packets from the Ethernet driver and calls either
the ARP Receive procedure, the ICMP Receive procedure or the UDP Input procedure.
The complete implementation can be found in the receive.adb file.
Building and testing the server
To build the UDP echo server and have it run on the STM32 board is a three step process:
- First, you will use the arm-eabi-gnatmake command with the
echo GNAT project. After successful build, you will get the echo ELF binary image in obj/stm32f746disco/echo.
- Then, the ELF image must be converted to binary by extracting the ELF sections that must be put on
the flash. This is done by running the arm-eabi-objcopy command.
- Finaly, the binary image produced by arm-eabi-objcopy must be put on the flash using the st-util utility. You may have to press the reset button on the board so that the st-util is able to take control of the board; then release the reset button to let st-util the flas