Writing a simple UDP server in Ada for a STM32F746 ARM controller is now easy with the use of the Ada Embedded Network stack. The article describes through a simple UDP echo server the different steps for the implementation of an UDP server.
Simple UDP Echo Server on STM32F746
By Stephane Carrez2016-12-04 23:01:00
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 thest-util
utility. You may have to press the reset button on the board so that thest-util
is able to take control of the board; then release the reset button to letst-util
the flas
Tags
- Facelet
- NetBSD
- framework
- Mysql
- generator
- files
- application
- gcc
- ReadyNAS
- Security
- binutils
- ELF
- JSF
- Java
- bacula
- Tutorial
- Apache
- COFF
- collaboration
- planning
- project
- upgrade
- AWA
- C
- EL
- J2EE
- UML
- php
- symfony
- Ethernet
- Ada
- FreeBSD
- Go
- KVM
- MDE
- Proxy
- STM32
- Servlet
- backup
- lvm
- multiprocessing
- web
- Bean
- Jenkins
- release
- OAuth
- ProjectBar
- REST
- Rewrite
- Sqlite
- Storage
- USB
- Ubuntu
- bison
- cache
- crash
- Linux
- firefox
- performance
- interview
Add a comment
To add a comment, you must be connected. Login