FreeRTOS: un sistema operativo real-time anche per microcontrollori AVR

FreeRTOS per AVR

FreeRTOS è un sistema operativo real-time (RTOS) che permette la commutazione tra vari task assicurandone la corretta esecuzione in base alla priorità del task stesso. Questo meccanismo dipende fortemente dall’architettura del microcontrollore. Ecco come implementare un RTOS, nello specifico FreeRTOS, su un microcontrollore AVR.

Le applicazioni che utilizzano un sistema operativo real-time (RTOS) sono costituite da un insieme di processi (task) ed è compito  del sistema operativo eseguire i vari task ad intervalli di tempo ben definiti. Il kernel del sistema operativo prepara i vari processi per l’esecuzione o la sospensione ed un processo può anche autosospendersi (ad esempio nel caso in cui sia in attesa che una determinata risorsa divenga disponibile).

L’UNITÀ DI TEMPO

Come sistema operativo di riferimento in questo articolo verrà considerato il FreeRTOS (www.freertos.org) i cui sorgenti sono disponibili come open source. FreeRTOS  misura il tempo utilizzando una variabile tick di conteggio che viene incrementata ad intervalli temporali ben precisi grazie ad un interrupt handler scatenato da un timer (tick interrupt). Prima dell’autosospensione, un processo comunica al sistema operativo il tempo per il quale rimarrà in sospensione e sarà cura del kernel confrontare  di volta in volta il raggiungimento  del timeout specificato. Ecco come avviene il meccanismo di gestione dei processi:

  1. l’ISR del timer incrementa il tick;
  2. se l’incremento sblocca un task allora viene controllato che quel task sia pronto per l’esecuzione e che la sua priorità sia maggiore di quella del processo appena interrotto;
  3. in caso affermativo viene fatto un cambio di contesto e all’uscita dell’ISR il nuovo  task passa in esecuzione.

Per implementare questa tecnica su un AVR viene utilizzato il TIMER1 per la generazione dei tick. La durata di un tick viene determinata sia dalla frequenza di aggiornamento del timer (eventualmente modificata dal prescaler) sia dal valore finale impostato per il timer. Quando il timer raggiunge il valore finale, l’ISR si preoccuperà anche di azzerare il timer. Nel listato 1 è rappresentato il codice sorgente in C per l’impostazione del timer1 (quindi per la determinazione della durata di un tick).

#define portCLEAR_COUNTER_ON_MATCH             ( 0x08 )
#define portPRESCALE_256                       ( 0x04 )
#define portCLOCK_PRESCALER                    ( 256 )
#define portCOMPARE_MATCH_A_INTERRUPT_ENABLE   ( 0x10 )

static void prvSetupTimerInterrupt ( void )
{
unsigned portLONG ulCompareMatch;
unsigned portCHAR ucHighByte, ucLowByte;
ulCompareMatch = portCPU_CLOCK_HZ / portTICK_RATE_HZ;
ulCompareMatch /= portCLOCK_PRESCALER;
ucLowByte = ulCompareMatch & 0xff;
ulCompareMatch >>= 8;
ucHighByte = ulCompareMatch & 0xff;
outb( OCR1AH, ucHighByte );
outb( OCR1AL, ucLowByte );
ucLowByte = portCLEAR_COUNTER_ON_MATCH | portPRESCALE_256;
outb( TCCR1B, ucLowByte );
ucLowByte = inb( TIMSK );
ucLowByte |= portCOMPARE_MATCH_A_INTERRUPT_ENABLE;
outb( TIMSK, ucLowByte );
}

Listato 1

IL CONTESTO

Un processo (o task) è un frammento  di codice sequenziale la cui esecuzione può essere sospesa o ripresa dal sistema operativo sia in maniera schedulata, sia in maniera asincrona. Viene definito contesto per un processo, l’insieme di tutti i registri in RAM e in ROM utilizzati dal processo. In altre parole il contesto può essere considerato come l’ambiente di lavoro di un processo, le sue variabili, i suoi puntatori,  il  suo stack e così via. Quando un task entra in sospensione viene salvato il suo contesto, quindi “congelata”  la situazione al momento della sospensione. Il tutto viene poi ripristinato alla successiva esecuzione. Il salvataggio del contesto per un determinato task ed il ripristino del contesto per il successivo processo in esecuzione è noto  come cambiamento di contesto (context switching). Su un microcontrollore AVR il contesto è costituito da (figura 1):

  • 32 registri general purpose;
  • Registro di stato (status register);
  • Program counter;
  • I due puntatori allo stack.
Figura 1. Il contesto per un processo sul microcontrollore AVR

Figura 1. Il contesto per un processo sul microcontrollore AVR

Nel listato 2 è riportata  la sintassi per la scrittura dell’ISR, la routine che andrà in esecuzione a seguito dell’interrupt  scatenata dal timer1 precedentemente impostato.

void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal ) );
void SIG_OUTPUT_COMPARE1A( void )
{
    /* ISR C code for RTOS tick. */
    vPortYieldFromTick();
}

Listato 2

La direttiva      attribute ((signal)) informa il compilatore che la funzione è in effetti una ISR il che comporta due importanti differenze nel codice generato dal compilatore:

  1. l’attributo signal assicura che ciascun registro dell’AVR modificato durante l’esecuzione dell’ISR sia ripristinato al valore originale all’uscita dell’ISR;
  2. l’attributo  signal forza inoltre il compilatore ad utilizzare una istruzione di  ritorno  da interruzione (RETI) anziché  una semplice RET (ritorno da funzione). Questo permette di abilitare nuovamente le interruzioni all’uscita dell’ISR.

Il listato 3 mostra il codice assembler generato dalla compilazione della funzione del listato 2. Si noti che parte del salvataggio del contesto avviene in maniera automatica (vengono salvati solo i registri general purpose utilizzati dalla ISR).

;void SIG_OUTPUT_COMPARE1A( void )
;{
    ; ---------------------------------------
    ; CODE GENERATED BY THE COMPILER TO SAVE
    ; THE REGISTERS THAT GET ALTERED BY THE
    ; APPLICATION CODE DURING THE ISR.
    PUSH R1
    PUSH R0
    IN R0,0x3F
    PUSH R0
    CLR R1
    PUSH R18
    PUSH R19
    PUSH R20
    PUSH R21
    PUSH R22
    PUSH R23
    PUSH R24
    PUSH R25
    PUSH R26
    PUSH R27
    PUSH R30
    PUSH R31
    ; ---------------------------------------
    ; CODE GENERATED BY THE COMPILER FROM THE
    ; APPLICATION C CODE.
    ;vTaskIncrementTick();
    CALL 0x0000029B ;Call subroutine
;}
    ; ---------------------------------------
    ; CODE GENERATED BY THE COMPILER TO
    ; RESTORE THE REGISTERS PREVIOUSLY
    ; SAVED.
    POP R31
    POP R30
    POP R27
    POP R26
    POP R25
    POP R24
    POP R23
    POP R22
    POP R21
    POP R20
    POP R19
    POP R18
    POP R0
    OUT 0x3F,R0
    POP R0
    POP R1
    RETI
    ; ---------------------------------------

Listato 3

Come già accennato però, il cambiamento del contesto prevede il salvataggio dell’intero  contesto ma provvedendo manualmente a salvare i 32 registri general purpose si avrebbero inevitabilmente  salvataggi duplicati. Per evitare questo, il listato 2 può essere modificato come indicato nel listato 4, utilizzando cioè l’attributo naked.

void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) );
void SIG_OUTPUT_COMPARE1A( void )
{
    /* ISR C code for RTOS tick. */
    vPortYieldFromTick();
}

Listato 4

Compilando il listato 4 si otterrà il codice del listato 5. Si noti che in questo caso non  viene generato alcun codice di ingresso e di uscita dalla funzione. Il salvataggio ed il ripristino del contesto dovrà quindi essere fatto manualmente da opportune macro come indicato nel listato 6. Si noti, nel listato 6, che anche l’istruzione RETI  di ritorno  da interrupt  deve essere aggiunta manualmente.

;void SIG_OUTPUT_COMPARE1A( void )
;{
    ; ---------------------------------------
    ; NO COMPILER GENERATED CODE HERE TO SAVE
    ; THE REGISTERS THAT GET ALTERED BY THE
    ; ISR.
    ; ---------------------------------------
    ; CODE GENERATED BY THE COMPILER FROM THE
    ; APPLICATION C CODE.
    ;vTaskIncrementTick();
    CALL 0x0000029B ;Call subroutine
    ; ---------------------------------------
    ; NO COMPILER GENERATED CODE HERE TO RESTORE
    ; THE REGISTERS OR RETURN FROM THE ISR.
    ; ---------------------------------------
;}

Listato 5
void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) );
void SIG_OUTPUT_COMPARE1A( void )
{
    /* Salvataggio del contesto. */
    portSAVE_CONTEXT();
    /* ISR C code for RTOS tick. */
    vPortYieldFromTick();
    /* Ripristino del contesto. */
    portRESTORE_CONTEXT();
    /* The return from interrupt call must also
    be explicitly added. */
    asm volatile ( "reti" );
}

Listato 6

Salvataggio del contesto

Il salvataggio del contesto viene fatto semplicemente salvando i vari registri nello stack (ciascun processo ha il proprio stack).
Il modo più semplice per fare questa operazione è utilizzare direttamente istruzioni assembler  PUSH, per cui la portSAVE_CONTEXT() utilizzata nel listato 6 può essere realizzata come indicato nel listato 7.

#define portSAVE_CONTEXT()            \
asm volatile (                              \
  "push r0                        \n\t" \
  "in r0, __SREG__                \n\t" \
  "cli                            \n\t" \
  "push r0                        \n\t" \
  "push r1                        \n\t" \
  "clr r1                         \n\t" \
  "push r2                        \n\t" \
  "push r3                        \n\t" \
  "push r4                        \n\t" \
  "push r5                        \n\t" \
:
:
:
  "push r30                      \n\t" \
  "push r31                      \n\t" \
  "lds r26, pxCurrentTCB         \n\t" \
  "lds r27, pxCurrentTCB + 1     \n\t" \
  "in r0, __SP_L__               \n\t" \
  "st x+, r0                     \n\t" \
  "in r0, __SP_H__               \n\t" \
  "st x+, r0                     \n\t" \
);

Listato 7

Ripristino del contesto

Il ripristino del contesto avviene in modo del tutto simmetrico  al  salvataggio. Il listato 8 riporta la macro portRESTORE_CONTEXT() per il ripristino del contesto.

#define portRESTORE_CONTEXT()            \
asm volatile (
  "lds r26, pxCurrentTCB            \n\t" \
  "lds r27, pxCurrentTCB + 1        \n\t" \
  "ld r28, x+                       \n\t" \
  "out __SP_L__, r28                \n\t" \
  "ld r29, x+                       \n\t" \
  "out __SP_H__, r29                \n\t" \
  "pop r31                          \n\t" \
  "pop r30                          \n\t" \
:
:
:
  "pop r1                           \n\t" \
  "pop r0                           \n\t" \
  "out __SREG__, r0                 \n\t" \
  "pop r0                           \n\t" \
);

Listato 8

CAMBIAMENTO DI CONTESTO: UN ESEMPIO

Come esempio di cambiamento di contesto verrà mostrato passo per passo come il sistema operativo sospende l’esecuzione di un task (TaskA) per avviare un secondo task (TaskB). Ecco il meccanismo descritto in dettaglio:

    1. Il TaskA è in esecuzione (il suo contesto è quello riportato in figura 2). Si supponga che l’interruzione del timer 1 avvenga quando TaskA sta per eseguire l’istruzione LDI R0,0 (situazione di figura2).

      Figura 2. Il contesto del TaskA in esecuzione

      Figura 2. Il contesto del TaskA in esecuzione

    2. All’occorrenza  dell’interruzione   viene  salvato  il program conuter del TaskA (PC(A)) nello stack del TaskA (figura 3), quindi viene eseguita la ISR.

      Figura 3. Salvataggio del program counter di TaskA nello stack

      Figura 3. Salvataggio del program counter di TaskA nello stack

    3. La routine ISR va a controllare se il nuovo valore tick risveglia qualche processo (si supponga che si debba risvegliare il processo TaskB e che questo abbia priorità  maggiore rispetto a TaskA) quindi provvede al salvataggio del contesto per TaskA. Ovviamente il contesto di TaskA viene salvato nel proprio stack (figura  4).  Il kernel conserva una copia dei puntatori allo stack di ogni processo.

      Figura 4. Salvataggio del contesto di TaskA nel proprio stack

      Figura 4. Salvataggio del contesto di TaskA nel proprio stack

    4. Poiché il  TaskB deve  andare  in  esecuzione, è necessario ripristinarne  il  proprio  contesto.  Per prima cosa viene recuperato il puntatore allo stack grazie alle copie conservate dal kernel (figura 5) quindi  vengono  estratti tutti  i  registri salvati al momento  della sospensione del TaskB. A questo punto  il contesto del TaskB  è ripristinato.  Resta solo da estrarre il Program Counter (PC(B)) il cui valore è ancora nello stack (figura 6).
      Figura 5. Recupero del puntatore allo stack del TaskB

      Figura 5. Recupero del puntatore allo stack del TaskB

      Figura 6. Ripristino del contesto per TaskB

      Figura 6. Ripristino del contesto per TaskB

    5. Il valore del program counter del TaskB viene ripristinato automaticamente all’esecuzione dell’istruzione RETI in uscita dalla routine ISR. A questo punto il TaskB risulta in esecuzione dal punto in cui era stato interrotto al momento della sua sospensione (figura 7).
Figura 7. TaskB in esecuzione al ritorno dall’interrupt

Figura 7. TaskB in esecuzione al ritorno dall’interrupt

 

5 Commenti

  1. Maurizio Di Paolo Emilio Maurizio 6 gennaio 2016
  2. Daniele Facchin 7 gennaio 2016
  3. Emanuele Paiano Emanuele Paiano 12 gennaio 2016
  4. claudio.verilli 1 novembre 2018

Scrivi un commento

EOS-Academy

Ricevi GRATIS le pillole di Elettronica

Ricevi via EMAIL 10 articoli tecnici di approfondimento sulle ultime tecnologie