Codice Overlay con il Parallax Propeller e GCC

Scritto il 7 aprile 2016 da

I programmi che utilizzano codice overlay erano molto popolari nell’era dei primi home computer quando la memoria a disposizione non era molta e la possibilità di caricare porzioni di codice solo quando necessario permetteva di realizzare programmi molto più grandi della memoria disponibile.

Con i microcontrollori siamo in una situazione simile con programmi a volte molto complessi e una memoria piuttosto limitata, basti pensare ad esempio alle librerie per la gestione del file system di una SD card o per l’accesso ad internet che posso esaurire o limitare fortemente la memoria a disposizione per il programma vero e proprio, specialmente se devono essere utilizzate contemporaneamente.

Il Parallax Propeller è un microcontrollore dotato di 32K di memoria RAM interna e utilizza una EEPROM esterna per caricare il programma da eseguire al momento dell’accensione. Poichè solo i primi 32K della EEPROM vengono utilizzati per il programma è possibile dedicare l’eccedenza delle memorie più ampie alla memorizzazione dei dati. Fortunatamente con il compilatore GCC e gli strumenti standard messi a disposizione è possibile anche memorizzare porzioni di programma da caricare nella ram principale quando necessario.

Il programma

Realizziamo a titolo di esempio un piccolo programma che scriva sulla porta seriale due frasi tramite due funzioni memorizzate in due sorgenti distinti che andremo poi a gestire tramite overlay.

I due file sorgenti potrebbero essere come i seguenti:

overlay0.c:
	void overlay_function1()
	{
	    uart_print("Written by overlay_function1 !\r\n");
	}

overlay1.c:
	void overlay_function2()
	{
	    uart_print("Written by overlay_function2 !\r\n");
	}

Il programma potrebbe semplicemente inizializzare la porta seriale del microcontrollore, scrivere un’intestazione per segnalare che sta funzionano e chiamare le due funzioni di cui sopra. Aggiungiamo anche i sorgenti per gestire la EEPROM tramite I2C sui pin standard.

Durante l’esposizione farò riferimento al sorgente senza riportarlo completamente per non allungare inutilmente l’articolo. Il pacchetto allegato contiene i file di esempio completi utilizzabili come riferimento.

Lo script per il linker

Per utilizzare gli overlay dobbiamo istruire il linker su quali file oggetto rappresentano il codice e dove inserirli nella memoria del microcontrollore. Per fare ciò dobbiamo utilizzare uno script personalizzato.

Partiamo dallo script standard che trovate nella directory propeller-elf/lib/ldscripts con il nome propeller.x. Copiate il file nella vostra directory di lavoro e rinominatelo in propeller_ovl.ld. Aprite un editor di testo e caricate il file.

In testa allo script troviamo la sezione MEMORY che definisce le aree di memoria in cui piazzare il codice e i dati. Dobbiamo definire una nuova zona in cui piazzare il codice overlay, aggiungiamo quindi una riga come quella evidenziata subito sotto la riga che inizia con hub:

MEMORY
{
  hub     : ORIGIN = 0, LENGTH = 32K
  ovl     : ORIGIN = 28K, LENGTH = 4K
  cog     : ORIGIN = 0, LENGTH = 1984 /* 496*4 */

Questa riga definisce un’area di memoria chiamata ovl che inizia alla locazione 28762 (28K) alla fine della ram interna di ampiezza pari a 4096 (4K) bytes. Tutti i file oggetto definiti come overlay verranno caricati in questa zona.

Poichè la zona si trova nella memoria ram del microcontrollore dobbiamo di conseguenza ridurre la memoria disponibile per il normale codice, quindi andiamo a modificare la riga hub in modo che l’ampiezza sia di 32K-4K = 28K. Le prime righe dello script saranno quindi come queste:

MEMORY
{
  hub     : ORIGIN = 0, LENGTH = 28K
  ovl     : ORIGIN = 28K, LENGTH = 4K
  cog     : ORIGIN = 0, LENGTH = 1984 /* 496*4 */

A questo punto dobbiamo dire al linker quali sono i file oggetto che costituiscono il codice overlay. Spostiamoci quindi verso il basso fino ad arrivare alla sezione SECTIONS. Le prime righe di questa sezione nello script standard dovrebbero essere simili alle seguenti:

SECTIONS
{
  /* if we are not relocating (-r flag given) then discard the boot and bootpasm sections; otherwise keep them */
  /* the initial spin boot code, if any */
   .boot : { KEEP(*(.boot)) } >hub
   .bootpasm : { KEEP(*(.bootpasm)) } >bootpasm AT>hub

Andremo quindi ad aggiungere una sezione OVERLAY subito dopo la parentesi graffa in modo che sia letta prima di ogni altra istruzione:

SECTIONS
{
  /* overlays */
  OVERLAY : NOCROSSREFS
  {
    .ovly0  { overlay0.o(.text .data) }
    .ovly1  { overlay1.o(.text .data) }
  } >ovl AT>drivers

  /* if we are not relocating (-r flag given) then discard the boot and bootpasm sections; otherwise keep them */
  /* the initial spin boot code, if any */
   .boot : { KEEP(*(.boot)) } >hub
   .bootpasm : { KEEP(*(.bootpasm)) } >bootpasm AT>hub

La riga OVERLAY : NOCROSSREFS definisce l’inizio della sezione informando il linker che stiamo appunto definendo del codice overlay e che ogni overlay non può chiamare il codice contenuto in un altro overlay. Nelle righe tra le parentesi graffe andremo ad inserire i file oggetto contenenti il codice overlay, uno per ogni riga. Nell’esempio qui sopra abbiamo due file oggetto overlay0.o e overlay1.o.

L’ultima riga con la parentesi graffa chiusa definisce la zona di memoria in cui il codice verrà eseguito, la zona ovl definita all’inizio dello script, e dove fisicamente ogni oggetto verrà memorizzato, la zona drivers che corrisponde all’area di memoria della EEPROM oltre i 32K.

A questo punto lo script potrebbe essere già utilizzabile in quanto il linker produrrà automaticamente i simboli necessari per caricare in memoria gli oggetti, tuttavia per semplificarci ulteriormente le cose, andiamo ad aggiungere una tabella che possiamo utilizzare per caricare il codice tramite un semplice indice.

Scendiamo ancora un po’ nello script fino ad arrivare alla sezione .data e modifichiamola nel modo seguente:

  .data	  :
  {
    . = ALIGN(4);
    __ovly_table = .; 
        LONG(ABSOLUTE(ADDR(.ovly0))); LONG(SIZEOF(.ovly0)); LONG(LOADADDR(.ovly0));
        LONG(ABSOLUTE(ADDR(.ovly1))); LONG(SIZEOF(.ovly1)); LONG(LOADADDR(.ovly1));
  }  >hub AT>hub

Le righe aggiunte definiscono una tabella di 2×3 long chiamata _ovly_table che possiamo utilizzare direttamente in C tramite una definizione come la seguente:

extern uint32_t _ovly_table[2][3];

Il primo elemento di ogni riga contiene l’indirizzo della ram interna in cui caricare l’overlay, il secondo elemento contiene la dimensione in bytes del codice e il terzo elemento l’indirizzo di memoria in cui è stato memorizzato nella EEPROM.

L’ultimo passo da compiere è ridefinire il puntatore allo stack in quanto l’impostazione standard lo colloca all’indirizzo 0x8000 alla fine della ram interna, nella stessa zona che verrà occupata dal codice overlay (ricordiamo che lo stack si espande verso il basso).

L’ultima riga dello script contiene la definizione del puntatore dello stack, andremo quindi a modificarla indicando l’indirizzo dell’inizio della zona overlay, 28*1024=28672 (7000 in esadecimale):

  /* default initial stack pointer */
  PROVIDE(__stack_end = 0x7000) ;
}

In questo modo non ci saranno interferenze tra il codice overlay e lo stack.

Lo script è adesso completo e possiamo utilizzarlo con il linker aggiungendo la direttiva -T sulla linea di comando. In un makefile tipico avremo una riga simile a questa:

CFLAGS := -Os -Wall
CXXFLAGS := -Os -Wall
LDFLAGS := -s -T propeller_ovl.ld -Wl,-Map=$(NAME).map -fno-exceptions

La direttiva -T dice al linker di utilizzare il file propeller_ovl.ld come script personalizzato.

Caricare gli overlay

Ora abbiamo un programma che si compila correttamente ma quando viene eseguito non mostra l’output che ci aspettiamo in quanto il codice relativo alle due funzioni di esempio non è stato ancora caricato nella memoria interna. Per caricare il codice dalla EEPROM, aggiungiamo la definizione dell’array contenente i dati degli overlay generati dal linker e una funzione che carica il codice vero e proprio:

extern uint32_t _ovly_table[2][3];

void eeprom_load_overlay(int n)
{
    uart_print("--- loading overlay ");
    uart_print_dec(n);
    uart_print(" from ");
    uart_print_number(_ovly_table[n][2], 16, 0);
    uart_print(" to ");
    uart_print_number(_ovly_table[n][0], 16, 0);
    uart_print(" size ");
    uart_print_dec(_ovly_table[n][1]);
    uart_print("\r\n");

    eeprom_read(HIGH_EEPROM_OFFSET(_ovly_table[n][2]), (uint8_t *)_ovly_table[n][0], _ovly_table[n][1]);
}

Nella funzione main del programma aggiungiamo poi il caricamento dell’overlay subito prima di chiamare la funzione relativa:

    uart_print("\r\n*** OVERLAY DEMO ***\r\n\r\n");

    eeprom_load_overlay(0);
    overlay_function1();

    eeprom_load_overlay(1);
    overlay_function2();

Upload del programma

Per fare l’upload del programma è necessario utilizzare il programma propeller-load fornito insieme al compilatore gcc. Il programma non necessita di alcuna istruzione particolare in quanto è in grado di accorgersi che una porzione del programma deve essere caricata nella parte superiore della EEPROM grazie all’utilizzo della zona drivers della mappa della memoria. Assicuratevi di aver installato una EEPROM da almeno 64K:

marco@bridge:~/parallax/overlay$ propeller-load -t -p /dev/ttyACM0 -r demo.elf 
Propeller Version 1 on /dev/ttyACM0
Loading the serial helper to hub memory
10392 bytes sent                  
Verifying RAM ... OK
Loading cache driver 'eeprom_cache.dat'
1540 bytes sent                  
Writing cog images to eeprom
104 bytes sent                  
Loading demo.elf to hub memory
4792 bytes sent                  
Verifying RAM ... OK
[ Entering terminal mode. Type ESC or Control-C to exit. ]

*** OVERLAY DEMO ***

--- loading overlay 0 from C0000000 to 7000 size 52
Written by overlay_function1 !
--- loading overlay 1 from C0000034 to 7000 size 52
Written by overlay_function2 !

L’output di esempio mostra i dati degli overlay caricati dalla EEPROM.

Conclusioni

Utilizzare il codice overlay nei programmi apre a molte possibilità. Nell’esempio allegato abbiamo utilizzato semplici funzioni ma nulla vieta di avere overlay molto più complessi, occorre solamente tenere conto delle limitazioni che abbiamo ora. La memoria utilizzata dall’overlay non può essere utilizzata dal programma principale quindi overlay complessi che occupano diversi K-bytes automaticamente riducono lo spazio a disposizione. Un overlay non può chiamare il codice contenuto in un altro overlay, questo perché sarebbe necessario implementare un sistema di tracciamento delle chiamate alle funzioni in grado di caricare il codice giusto, aumentando molto la complessità. Il caricamento del codice richiede sempre un certo tempo quindi è consigliabile ragguppare le funzioni in modo da minimizzare i caricamenti. Ma anche con qualche limitazione adesso abbiamo la possibilità di avere programmi più grandi di 32K di memoria.

Links