LwIP: una libreria TCP/IP per sistemi embedded

LwIp è un’implementazione della suite TCP/IP per architetture basate su microcontrollori ed è stata sviluppata, inizialmente, da Adam Dunkels al SICS (Swedish Institute of Computer Science): oggi è diventata una buona soluzione per le applicazioni embedded.

L’obiettivo di Dunkels è di utilizzare lo stack LwIP in applicazioni con ridotte capacità di memoria e, a maggior ragione, questo è fra i  protocolli open source, che offre un miglior rapporto fra completezza e prestazioni; inoltre, essendo pensato per l’utilizzo su microcontrollori, garantisce la gestione dell’elevato flusso di dati proprio dei protocolli internet con il minor dispendio possibile di risorse di memoria. La pila LwIP richiede poche decine di kilobytes di ram e circa 40 Kb di memoria di tipo rom. Esistono diverse implementazioni per le più svariate architetture hardware, da microprocessori a 8 bit fino ad arrivare alle soluzioni a 32 bit, con piattaforme come AVR, H8S/300, 8051, Z80, ARM, M16c, e CPU basate su x86.

ATMEL

La soluzione LwIP è utilizzata nelle proposte ATMEL, a questo riguardo può essere interessante consultare l’application note “AVR32753: AVR32 UC3 How to connect to an SSL-server”. La figura 1 mostra l’applicazione, mentre la figura 2 pone in evidenza le relazioni tra le varie componenti software.

Figura 1: SSL application.

Figura 1: SSL application.

 

Figura 2: Software Components

Figura 2: Software Components

In questa applicazione LwIP è utilizzata con FreeRTOS, ma ciò non vuol dire che la pila può essere utilizzata soltanto in presenza di un sistema operativo. Le tabelle 2 e 3 mostrano le richieste di memoria per differenti ambiti applicativi, questi dati sono puramente indicativi. Il codice LwIP è stato compilato per un architettura 32-bit Intel x86 e 8-bit Atmel AVR utilizzando  il compilatore GCC nella versione 2.95.3 e 3.3 con le opzioni di ottimizzazioni selezionate. Le tabelle indicate mostrano i vari risultati: entrambe le applicazioni utilizzano ARP e SLIP e lo stack LwIP include UDP. La tabella 4, invece, pone in evidenza le richieste di memoria necessaria per implementare SSL secondo quanto riporta l’application note precedente. Per dovere di cronaca occorre anche dire che esiste un altro stack che è utilizzato in ambito embedded, mIP,sempre sviluppato da Dunkels. mIP richiede capacità di memoria ancora più ridotte a scapito delle funzionalità offerte ed è utilizzato anche senza un RTOS. La tabella 1 mostra le differenze tra questi due stack.

TABELLA 1 - CONFRONTO TRA MIP E LWIP

TABELLA 1: Confronto tra MIP e LWIP

 

TABELLA 2: Occupazione di memoria per LwIP (X86)

TABELLA 2: Occupazione di memoria per LwIP (X86)

 

TABELLA 3: Occupazione di memoria per LwIP (AVR)

TABELLA 3: Occupazione di memoria per LwIP (AVR)

 

TABELLA 4: Occupazione di memoria per SSL

TABELLA 4: Occupazione di memoria per SSL

Utilizzare LwIp con o senza RTOS

Un altro aspetto importante, non di secondaria importanza, è la possibilità di utilizzare lo stack LwIP anche senza un RTOS, cioè un sistema privo di meccanismi di tipo multi-threading. Il  core di LwIP comprende un insieme di protocolli: IP, ICMP, UDP e TCP con una serie di funzioni di supporto, come un memory e buffer management. Questa è la sola parte indispensabile, quando si utilizza un sistema basato su single-thread (un ambiente non RTOS). Il core di LwIP può essere visto, semplicemente, come una libreria software con una serie d’interfacce:

» ip_input(pbuf,netif). In questo caso un pacchetto IP, con la relativa comunicazione con l’interfaccia di rete, è gestito come argomento della funzione.

» tcp_tmr().  Questa interfaccia deve essere chiamata ogni 100 ms e gestisce tutta la cosiddetta timer processing del TCP, come una ri-trasmissione.

Il listato 1 mostra un semplice ciclo per un single-thread. Viceversa, utilizzare LwIP in un ambiente multi-thread è abbastanza semplice.

while(1) {
   if(poll_driver(netif) == PACKET_READY) {
        pbuf = get_packet(netif);
        ip_input(pbuf, netif);
            }
if(clock() - last_time == 100 * CLOCK_MS) {
         tcp_tmr();
         last_time = clock();
         }
}
Listato 1 - Single-thread

In un ambiente del genere, dove le applicazioni sono in esecuzione su thread concorrenti, la pila LwIP, che implementa la suite TCP/IP, è posta in un singolo thread e le comunicazioni sono gestite attraverso delle API. La parte di inter-thread communications è  realizzata in  due  file  api_lib.c  e api_msg.c. Un terzo file, tcpip.c, si occupa della gestione dei pacchetti e degli eventi temporali associati. Quando si utilizza un ambiente multi-thread, i pacchetti sono gestiti dalla funzione tcpip_input() e questa utilizza gli stessi argomenti della funzione ip_input(). La funzione tcpip_input() non gestisce immediatamente  il pacchetto, ma lo inserisce in una coda che solo in seguito sarà presa in carico dal thread TCP/IP. Per il testing e la calibrazione del protocollo TCP/IP lwIP si può essere benissimo utilizzare il software freeware Ethereal, che consente di monitorare il traffico su reti ethernet, di generare statistiche sull’efficienza della stessa ed è inoltre un ottimo strumento per il testing dei protocolli over ethernet.

Nintendo DS

Esistono diversi porting di LwIP. Uno dei lavori più interessanti è sicuramente per la piattaforma Nintendo DS. Il codice sorgente, che può essere liberamente scaricato, contiene, oltre alla documentazione, anche informazioni aggiuntive sull’uso dello stack. Il  lavoro che è stato fatto su questa piattaforma non utilizza meccanismi semaforici per proteggere la pila: lo stack non è protetto e non è rientrante. Tutte le funzioni dell’interfaccia della pila devono essere chiamate direttamente dallo stesso thread in esecuzione. Per questa ragione se le API LwIP sono chiamate all’interno di procedure ISR, allora quasi sicuramente si ottengono problemi in fase di esecuzione. Inoltre, se si sta utilizzando devkitARM r18 (con l’ambiente di lavoro arm-elf-gcc nel la versione 4.1.0), la compilazione dei vari header file potrebbero generare molti warning legati all’attributo packed. È possibile evitare questo ricorrendo all’opzione -Wno-attributes durante il processo di compilazione. La pila LwIP è configurabile senza particolari problemi o accorgimenti; infatti, attraverso  il file lwipopts.h si selezionano le varie possibilità offerte.

Il layer ip

LwIP ha una architettura che rispecchia quello della pila TCP/IP, ciò vuol dire che ogni layer della pila è costituito da una serie di moduli implementati in LwIP. La pila LwIP implementa solo le principali funzionalità di IP: può inviare, ricevere e inoltrare pacchetti, ma non può inviare o ricevere pacchetti frammentati né gestire pacchetti con i parametri impostati nel campo option nell’header. Vediamo il comportamento di LwIP a seconda che il pacchetto è in ingresso o che debba essere trasmesso verso un’altra pila. Per i pacchetti IP in ingresso, l’elaborazione inizia quando un driver di periferica richiama la funzione ip_input. La prima cosa che occorre fare è quella di controllare la versione del protocollo IP e la lunghezza dell’header: queste informazioni si trovano nell’intestazione del pacchetto; subito dopo è calcolato il checksum associato. Per i  pacchetti in ingresso, la pila LwIP non riceve nessun frammento IP: questo perché il proxy riassembla ogni pacchetto frammentato. Solo successivamente, lo strato LwIP controlla l’indirizzo di destinazione, confrontandolo con gli indirizzi IP delle interfacce di rete presenti per determinare se il pacchetto è destinato all’host o meno. Le interfacce di rete sono ordinate in una lista, per la ricerca di elementi al suo interno si adotta un algoritmo lineare, dato che ci si aspetta che il  numero di interfacce di rete sia piccolo. Per questo motivo, si è scelto di non implementare una strategia di ricerca più complessa. Viceversa, se il pacchetto in ingresso è destinato all’host, si analizza il campo protocol dell’header per decidere a quale protocollo di livello superiore va trasmesso. Un pacchetto in uscita è gestito dalla funzione ip_output, che richiama la funzione ip_route per trovare l’interfaccia di rete appropriata per la trasmissione del pacchetto nella rete. Quando l’interfaccia è stata determinata, il pacchetto è passato alla funzione ip_output_if che riceve come argomento l’interfaccia di rete individuata. Qui verranno compilati tutti i campi dell’header e verrà calcolato il checksum. Gli indirizzi sorgente e destinazione del pacchetto IP sono passati come argomento alla funzione ip_output_if, anche se l’indirizzo sorgente può essere omesso. La funzione ip_route trova l’interfaccia di rete appropriata ricercandola linearmente nella lista. Durante la ricerca, all’indirizzo di destinazione del pacchetto viene applicata la maschera di rete associata all’interfaccia di rete in analisi: se la rete di destinazione è uguale alla rete dell’interfaccia, questa viene scelta come interfaccia di uscita; altrimenti, se non viene trovata nessuna corrispondenza, viene presa l’interfaccia di default. Quest’ultima viene configurata all’avvio, ma può essere modificata anche a run-time dall’amministratore di rete. Se l’indirizzo di rete dell’interfaccia di default non corrisponde all’indirizzCo della rete di destinazione, il campo gw nella struttura netif viene scelto come indirizzo IP di destinazione del frame nel link layer. Dato che i protocolli dello strato di trasporto necessitano dell’indirizzo IP di destinazione per calcolare il checksum di livello di trasporto, l’interfaccia di rete di uscita deve, in alcuni casi, essere determinata prima che il pacchetto venga passato allo strato IP. Ciò viene fatto lasciando allo strato di trasporto il compito di chiamare la funzione ip_route; in questo caso, poiché l’interfaccia di rete è già stata determinata nel momento in cui il segmento raggiunge lo strato IP, non è necessario effettuare nuovamente la ricerca. Questi protocolli, quindi, chiamano direttamente la funzione ip_output_if, visto che, ad essa, può essere passata un’interfaccia di rete come parametro. Ora, se nessuna delle interfacce di rete ha lo stesso indirizzo IP contenuto nel campo destination address del pacchetto in ingresso, questo deve essere inoltrato: tale operazione viene eseguita dalla funzione ip_forward. Qui, il  campo TTL è decrementato e, se raggiunge zero, è inviato al mittente del pacchetto un messaggio ICMP di errore e il pacchetto ricevuto è scartato. Dopo il decremento del campo TTL è necessario ricalcolare il checksum, poiché l’header IP è cambiato. Successivamente,  il pacchetto viene inoltrato all’interfaccia di rete.

Un piccolo esempio

In questa parte vi presentiamo un esempio, un web server, utilizzando le API LwIP. Questa applicazione è costituita da un singolo processo che accetta connessioni da un network, risponde alle richieste secondo il protocollo http e chiude successivamente la connessione. Nel la funzione main() ci sono due funzioni che si preoccupano di inizializzare e definire il setup della connessione, processarla e gestire un sottoinsieme del protocollo http/1.0. Il web server è scritto in C, come potete vedere nel listato 2.

/* A simple HTTP/1.0 server using the minimal API. */
#include “api.h”
/* This is the data for the actual web page.
Most compilers would place this in ROM. */
const static char indexdata[] =
“<html> \
<head><title>A test page</title></head> \
<body> \
This is a small test page. \
</body> \
</html>”;
const static char http_html_hdr[] =
“Content-type: text/html\r\n\r\n”;
/* This function processes an incomming connection. */
static void
process_connection(struct netconn *conn)
{
         struct netbuf *inbuf;
         char *rq;
         int len;
/* Read data from the connection into the netbuf inbuf.
We assume that the full request is in the netbuf. */
inbuf = netconn_recv(conn);
/* Get the pointer to the data in the first netbuf fragment
which we hope contains the request. */
netbuf_data(inbuf, &rq, &len);
/* Check if the request was an HTTP “GET /\r\n”. */
if(rq[0] == ‘G’ && rq[1] == ‘E’ &&
rq[2] == ‘T’ && rq[3] == ‘ ‘ &&
rq[4] == ‘/‘ && rq[5] == ‘\r’ &&
rq[6] == ‘\n’) {
/* Send the header. */
netconn_write(conn, http_html_hdr, sizeof(http_html_hdr),
NETCONN_NOCOPY);
/* Send the actual web page. */
netconn_write(conn, indexdata, sizeof(indexdata),
NETCONN_NOCOPY);
/* Close the connection. */
netconn_close(conn);
                }
}
/* The main() function. */
Int main()
{
struct netconn *conn, *newconn;
/* Create a new TCP connection handle. */
conn = netconn_new(NETCONN_TCP);
/* Bind the connection to port 80 on any local IP address. */
netconn_bind(conn, NULL, 80);
/* Put the connection into LISTEN state. */
netconn_listen(conn);
/* Loop forever. */
while(1) {
/* Accept a new connection. */
newconn = netconn_accept(conn);
/* Process the incomming connection. */
process_connection(newconn);
/* Deallocate connection handle. */
netconn_delete(newconn);
}
return 0;
}
Listato 2 - Semplice Web Server

Questo semplice web server risponde solo alle richieste HTTP GET.

 

Scarica subito una copia gratis

3 Commenti

  1. Avatar photo Riccardo Ventrella 17 Ottobre 2018
    • Avatar photo Francesco Marchianò 14 Gennaio 2019
      • Avatar photo Riccardo Ventrella 15 Gennaio 2019

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend