Il Sistema Operativo eCos

David N. Welton
davidw@dedasys.com
2004-09-25

Introduzione ai sistemi embedded, e un esempio pratico di un'applicazione sviluppata con eCos.

Il mondo embedded

Il mondo del software embedded è estremamente interessante. Mentre i computer 'normali' hanno processori velocissimi, più memoria dei dischi fissi di 10 anni fa, e spazio HD da vendere, nell'ambiente delle applicazioni embedded è ancora molto importante conservare le risorse, e utilizzarle con attenzione. Inoltre, in molti casi nello stesso ambiente si parla anche di 'real time', ossia applicazioni che devono rispondere entro un certo tempo agli stimoli esterni. Per esempio i chip che controllano il motore di un'automobile non possono fermarsi a fare i calcoli - devono reagire continuamente a quello che sta succedendo in tempo reale.

A causa di questa combinazione di fattori, i sistemi operativi usati nell'ambiente embedded devono essere scritti con molta cura per rispondere ad esigenze particolari. Molto spesso girano su processori poco usati per i desktop e server come arm, mips, versioni speciali di powerpc, e altri ancora.

E cosa si può dire di Linux? Linux viene sempre più usato negli ambienti embedded dove le risorse non sono troppo limitate, e dove alcune sue feature più avanzate sono più importanti che avere un sistema di dimensioni molto ridotte. Se limitare le risorse è il fattore più importante, Linux non è sempre la scelta migliore. Si pensi che anche per un linux ridotto alle ossa, come ETLinux, ci vogliono 2 meg di ram, e 2 di storage.

Un altro fattore importante nel mercato embedded è che di solito le applicazioni sono sviluppate con un target in mente, e tutte le cose che non sono necessarie per fare il lavoro in questione sono un peso in più (e quindi memoria in più e un costo maggiore!). Al posto di un sistema 'generale', come, appunto, Linux o Windows, conviene un sistema mirato a una funzione particolare.

Per questo motivo, invece dei pochi OS che sono usati sui desktop e server, ce n'erano decine da usare per i progetti embedded, molti dei quali creati da zero per qualche progetto particolare. Ad aumentare questa tendenza a fare le cose 'in house' è il fatto che molti prodotti con un sistema operativo embedded a bordo sono destinati al mercato di massa, e quindi risentono dai diritti di riproduzione che andrebbero pagati per ogni unità venduta.

eCos

Che ruolo ha eCos in tutto questo? Qualche anno fa - nel 1998 per essere precisi - la Cygnus Solutions ha rilasciato un "real-time embedded operating system" con una licenza libera e un sistema di configurazione estremamente flessibile. Con questo approccio, Cygnus (comprata dalla Redhat nel frattempo) sperava di attirare l'interesse di aziende e sviluppatori che prima facevano sistemi 'in house', dandogli una soluzione libera che poteva essere facilmente modificata anche per rispondere alle esigenze più specifiche. Hanno avuto molto successo, e attualmente eCos è usato da diverse aziende e gira su molti processori e piattaforme.

Rimane un sistema operativo mirato esclusivamente all'uso embedded, e perciò non sono sempre presenti i servizi che ci si potrebbe aspettare di trovare, come shell, processi, un'interfaccia grafica pronta per l'uso, ed altre cose utili che si trovano in un sistema dedicato all'utente finale. eCos non è destinato a questo uso, se non in un appliance con un'interfaccia specifica e limitata come un telefonino o qualcosa di simile.

Se non è dotato di tutto ciò che troviamo già pronto in un sistema come Linux o FreeBSD, allora che vantaggi ha?

Prima di tutto, è possibile creare applicazioni estremamente piccole. Per esempio, il codice sviluppato per quest'articolo come dimostrazione di eCos sta in meno di 130K, e include 1) il sistema operativo 2) libjpeg 3) l'applicazione stessa. E tutto questo senza neanche aver fatto molto per ridurre le misure!

eCos è anche real-time, e permette un controllo molto preciso dei thread e interrupt handlers sul sistema. Il kernel Linux normale non ha questa abilità - è invece necessario usare soluzione come RTLinux o RTAI.

Inoltre, dal punto di vista di un aspirante kernel hacker, i sorgenti di eCos sono molto più facili da digerire rispetto a quelli di Linux o FreeBSD. È ancora un sistema semplice, che si limita ad alcune applicazioni, e quindi molto più comprensibile di Linux. Detto questo, comunque, non è assolutamente un "giocattolo" come Minix, è un OS usato commercialmente da molte aziende.

Un'applicazione con eCos

L'applicazione che andremo a costruire con eCos è molto semplice: ha il compito di bootare, leggere dal floppy una serie di immagini, e farle vedere una dopo l'altra sullo schermo del PC dopo una breve pausa. Con Linux, sarebbe estremamente semplice, ma il kernel di Linux è molto grande e ci sarebbe meno spazio rimasto per le JPEG. Invece, eCos, libjpeg, e il driver floppy stanno in meno di 130K, lasciando molto spazio per le immagini.

Per provare subito:

Scaricare da http://dedasys.com/freesoftware/
cp image.bin /dev/fd0
reboot dal floppy!

Non funziona su tutti i computer, perchè purtroppo alcune schede grafiche non sono compatibili con lo standard VBE usato per il display.

I Primi Passi

Per iniziare a sviluppare un'applicazione con eCos, bisogna crearne un'istanza - un 'tree' con la configurazione che vogliamo noi. Il tool che eCos fornisce per fare questo si chiama ecosconfig:

ecosconfig list - mostra tutte le configurazioni e packages disponibili

ecosconfig new - crea un nuovo file di configurazione, 'ecos.ecc'

Per esempio, per avere il nostro file di configurazione di base, facciamo:

ecosconfig new pc

in modo di avere una configurazione che corrisponde all'architettura del normale PC i386. È altamente consigliato fare questo passo in una directory vergine, perchè molti altri file e directory verranno creati.

Ora, con il file ecos.ecc in mano, possiamo modificarlo per le nostre esigenze. Ad esempio, per l'applicazione che stiamo creando non ha bisogno di una scheda ethernet, e tale support è invece incluso con la configurazione, e quindi togliamolo:

ecosconfig remove i82559_eth_driver
ecosconfig remove pc_etherpro

È anche possibile configurare tante opzioni, dalla misura dello stack, al numero di threads, come gestire gli interrupt e così via.

Quando siamo soddisfatti con la configurazione ottenuta, partiamo con

ecosconfig tree

il comando che dal 'repository' di eCos sul sistema tira fuori i file necessari per compilare la nostra copia di eCos. A questo punto possiamo eseguire "make" - una questione di qualche minuto - e abbiamo eCos pronto da linkare con la nostra applicazione.

Applicazione

Listato 1


while (cinfo.output_scanline < cinfo.output_height) {
	/* jpeg_read_scanlines expects an array of pointers to scanlines.
	 * Here the array is only one element long, but you could ask for
	 * more than one scanline at a time if that's more convenient.
	 */
	unsigned char *imgbuf;
	unsigned char *line_buf = NULL;

	jpeg_read_scanlines(&cinfo, buffer, 1);

	if (imgh > si->height) {
	    if (cinfo.output_scanline < (imgh - si->height) / 2) {
		continue;
	    }
	}

/* Facciamo il test sopra per assicurarci che l'immagine ci sta sullo
schermo, anche se e` piu` alta dello spazio disponibile. */

	line_buf = si->base_buf + (si->scanline * y) + (si->scanline * ystart);

	/* If the image is bigger than the screen, clip it. */
	if (imgw > si->width) {
	    imgcnt = (imgw/2 - si->width/2) * bpp;
	    imgcnt /= 2;
	} else {
	    imgcnt = 0;
	}

/* E anche qua dobbiamo tagliare se l'immagine e` piu` larga dello
schermo. */

	imgbuf = buffer[0];
	imgbuf += imgcnt;

	for (x = xstart; x < si->width; x++)
	{
	    unsigned char *buf = line_buf + (si->bpp * x);

 	    if (x < xstart + imgw) {
		*buf = imgbuf[imgcnt+2];
		*(buf + 1) = imgbuf[imgcnt + 1];
		*(buf + 2) = imgbuf[imgcnt];
		*(buf + 3) = 0;
		imgcnt += 3;
	    }
	}

/* Qua passiamo pixel per pixel e mettiamo li trasferiamo dal nostro
buffer alla memoria video.  Notate che bisogna anche cambiare l'ordine
dei vari pixel rossi, verdi, e blu. */

 	if (y >= imgh + ystart)
	    break;

	/* If we have run out of screen, we still have to finish
	 * hoovering up the data so that libjpeg is happy. */
	if (y >= si->height) {
	    while (cinfo.output_scanline < cinfo.output_height) {
		jpeg_read_scanlines(&cinfo, buffer, 1);
	    }
	    break;
	}

/* Come dice il commento, dobbiamo finire di leggere i dati nel jpeg,
anche se abbiamo finito lo spazio sullo schermo. */

	y++;
    }

Questo e` il loop che legge le righe dal JPEG residente in memoria, e le scrive allo schermo.

eCos non è un multi-process operating system. Ha i thread, ma non i process. Di conseguenza, non ha neanche l'idea di 'binaries' o eseguibili. Il programma è linkato staticamente con il sistema operativo - ed è quindi un file unico da mettere sul 'target' (per esempio un rom, flash, un floppy disk...). E quindi, quando iniziamo a scrivere un programma per eCos, partiamo sempre dal buon vecchio main().

Noterete che subito si va a prendere le informazioni sulla configurazione VBE della scheda grafica:

      
	info_block = (struct VBESVGAInfoBlock*) 0x000A0000;
	current_mode = (struct VBEModeInfoBlock*) 0x000A0200;
	...
	get_screen_info(&si);
      
    

eCos è 'operativo' in memoria - in questo modo abbiamo sistemato lo schermo, ora pensiamo al floppy.

Prima, dobbiamo inizializzare con cyg_io_lookup("/dev/fd", &fh), e poi proseguiamo subito alla lettura del disco:

floppy_raw_read(buf, &len, imagesize)

Ora, dobbiamo cambiare argomento: come utilizzare meglio quel poco spazio che c'è sul floppy? Come abbiamo spiegato in precedenza, non usiamo un filesystem sul floppy. Così non dobbiamo includere codice in grado di leggere il filesystem, e non complichiamo il processo di boot. Invece, mettiamo due cose sul floppy:

      0K ----------------
      Applicazione eCos
      130K --------------
      Archivio jpeg
      Fine Dischetto ----
    

L'applicazione eCos è semplicemente il programma, compilato, in formato 'binario' (non ELF, come i programmi Linux).

Invece, l'archivio jpeg merita una spiegazione. Per contenere più jpeg in meno spazio possibile, sul disco abbiamo scritto solo una tabella di offset, e poi tutti i jpeg:

Tutti i numeri sono unsigned int, little-endian.

      ------
      Numero di Immagini N
      ------
      Offset 1
      ------
      Offset 2
      ------
      ......
      ------
      Offset N-1
      ------
      Offset N
    

Avendo in mano il numero di jpeg, e i loro offset, possiamo calcolare dove stanno tutti sul disco, e caricarli in memoria, ed è esattamente quello che facciamo. Leggiamo, dunque, il jpeg dal disco, ma non è ancora pronto da mettere sullo schermo, perchè è ancora compresso. Per mettere l'immagine sullo schermo, passiamo il suo indirizzo e misura a "get_jpeg_image", assieme alle informazioni sullo schermo stesso.

get_jpeg_image si occupa quindi di decomprimere l'immagine, usando alcune chiamate da libjpeg, e poi scrive il risultato direttamente alla memoria dello schermo.

Una volta che tutte le immagini sono state caricate in memoria, il programma gira sempre in un loop, mostrandole sullo schermo.

Questo illustra bene un altro compromesso fra velocità e l'uso di memoria. Ossia, abbiamo tre possibili forme di storage per le immagini: il floppy, un jpeg in memoria e l'immagine decompressa in memoria. Potremmo, per esempio, evitare di tenere una referenza al jpeg in memoria una volta che è stato scritto al display, mantenendo libera più memoria. Sarebbe molto lento, perchè dovremmo leggere il floppy ogni volta per accedere alle immagini (tra l'altro, potrebbe non essere proprio sano per il floppy e drive essere sempre attivi).

Oppure, all'altro estremo, potremmo tenere tutte le immagini decompresse in memoria, in modo di poterle mandare più velocemente allo schermo. Molto veloce, ma consuma anche tanta memoria per ogni immagine, perchè la differenza fra un jpeg compresso e la stessa immagine sullo schermo è notevole - basta fare il calcolo di 800 x 600 x 4 bytes per pixel: 1920000 bytes, contro spesso meno di 100K per il jpeg equivalente.

Noi abbiamo quindi scelto una via di mezzo - tenere i jpeg in memoria, ma scrivere direttamente sulla memoria video senza poi tenere una copia dell'immagine decompressa. La nostra applicazione, dunque, non ha bisogno di più di 2 meg di memoria (teoricamente - non ho l'hardware per provare che è vero!).

Interrupt Service Routines

L'applicazione così com'è non è male... girano una serie di immagini sul monitor. Ma sarebbe carino poter interagire con il computer in qualche modo, magari per saltare un'immagine, o per aumentare o diminuire la velocità con cui cambia il display.

Per questo, dobbiamo permettere all'utente di dare un input al programma tramite la tastiera.

eCos ci permette di istallare un "routine" per rispondere agli interrupt dell'hardware. In realtà, eCos ha un modello sofisticato per gli interrupt. Parte da un livello di base, gli ISR (Interrupt Service Routine), la prima azione che fa il kernel quando riceve un interrupt, e che hanno priorità sulla maggior parte di quello che succede in eCos (i threads per esempio). È importante perciò passare meno tempo possibile negli ISR - confermare la ricezione dell'interrupt, fare un minimo di calcoli strettamente necessari, e poi segnalare al kernel che si può continuare con il prossimo passo: il DSR (Deferred Service Routine). Il DSR può fare il suo lavoro con più tranquilità, e molto importante, può anche comunicare con i threads con meccanismi di sincronizzazione del kernel come mutex, semaphore, flags e così via.

Il codice del nostro keyboard interrupt handler, in kbd.c, segue queste linee. Nell'ISR, non fa altro che disabilitare altri interrupt (tanto dobbiamo processarli uno alla volta), e leggere il keyboard port, mettendo il risultato in un variabile. A questo punto, il DSR entra in azione - a secondo il 'keyboard scan code' che abbiamo ricevuto, segnala questo tramite un flag (cyg_flag_setbits), e in fine, riabilita gli interrupt sul keyboard port.

Listato 2


static cyg_uint32 keyboard_isr(cyg_vector_t vector, cyg_addrword_t data)
{
    CYG_BYTE stat, code;
    CYG_BYTE c;

    cyg_interrupt_mask(vector);
    cyg_interrupt_acknowledge(vector);

    kbd_code = 0;
    HAL_READ_UINT8( KBSTATPORT, stat );

    /* Keyboard is not ready to be read. */
    if((stat & 0x01) == 0)
	kbd_code = 0;

    /* Read the keyboard scan code. */
    HAL_READ_UINT8( KBDATAPORT, kbd_code );

    return (CYG_ISR_HANDLED | CYG_ISR_CALL_DSR);
}

/* Il codice sopra non fa altro che leggere dal 'keyboard port' e mettere
le informazioni in un variabile globale.  Quando lasciamo l'ISR,
l'interrupt e` comunque ancora disabilitato. */

static void keyboard_dsr(cyg_vector_t vector, cyg_ucount32 count,
		    cyg_addrword_t data)
{
    switch (kbd_code) {
    case 0x39: /* space key */
	cyg_flag_setbits(&kbd_flag, SPACE_FLAG);
	break;
    case 0x21: /* 'f' */
	cyg_flag_setbits(&kbd_flag, FASTER_FLAG);
	break;
    case 0x1f: /* 's' */
	cyg_flag_setbits(&kbd_flag, SLOWER_FLAG);
	break;
    default:
	break;
    }

    cyg_interrupt_unmask(vector);
}

/* E qua "mandiamo" il flag, dicendo al resto del sistema che tipo di
evento abbiamo ricevuto.  Solo alla fine del DSR riabilitiamo
l'interrupt. */

Gli "interrupt handler" ricevano i segnali generati dalla tastiera, e di conseguenza aggiornano dei "flag" che poi vengono letti dal programma che aggisce di conseguenza.

La funzione cyg_flag_timed_wait "riceve il flag nel while loop che gira in continuazione, e a seconda del tasto premuto, compie azioni diverse. Per ora, queste si limitano a: 'spazio' - passa all'immagine successiva, 'f' per diminuire il delay fra immagini, e 's' per aumentarlo, per procedere più lentamente.

Siccome non vogliamo interferrenza dalla parte dell'utente mentre carichiamo le immagini dal disco, tutto questo è abilitato, con setup_kbd(), solo dopo la lettura del floppy è completa.

Conclusioni

Il bello di eCos, come potete vedere dal codice, è che abbiamo incluso solo le cose che ci servono, e poco altro. È anche un sistema piccolo, semplice e ben documentato, un altro vantaggio per chi vuole prenderlo in mano per farci qualcosa di interessante. La possibilità di includere un sistema operativo e una specie di 'image viewer' assieme a più di un meg di jpeg tutto su un dischetto è anche una prova che eCos usa poche risorse.

Copyright © 2000-2015 David N. Welton