Suddividi e gestisci: come FreeRTOS rende semplice la creazione di applicazioni a tempo reale per microcontrollori (Lezione 2)
Se si scrive un codice senza utilizzare un sistema operativo come FreeRTOS, l’elaborazione delle istruzioni avverrà in modo sequenziale, ovvero una istruzione dopo l’altra, come indicato nella figura di seguito.
Ciò significa che tutti i task del programma verranno eseguite un dopo l’altro, senza alcuna forma di parallelismo o multitasking.
Ciò può essere sufficiente per alcuni casi d’uso semplice, come l’accensione di un LED, ma per sistemi più complessi, questo approccio può diventare limitante.
Inoltre, se una parte del codice è bloccante, ad esempio in attesa di un input o in un ciclo infinito, il resto del codice non verrà eseguito fino a quando quella parte non terminerà.
Ciò può causare problemi di latenza, se queste condizioni non sono state gestite dallo sviluppatore, e rende difficile gestire più funzioni contemporaneamente.
Invece, l’uso di un sistema operativo come FreeRTOS consente di creare diverse attività o task che possono essere eseguite in parallelo, utilizzando la tecnologia dello schedulatore.
Si ha una maggiore flessibilità e scalabilità del codice, per gestire più funzioni contemporaneamente e situazioni in cui una parte del codice è bloccante senza influire sul funzionamento globale del sistema.
FreeRTOS offre una funzionalità denominata “task” per suddividere il codice di un’applicazione in diverse parti che possono essere eseguite indipendentemente l’una dall’altra.
La suddivisione di un problema complesso rende più semplice la sua risoluzione, inoltre, se il microcontrollore ha più di un core (come alcune versioni del microcontrollore ESP32) è possibile far eseguire, al microcontrollore, due istruzioni contemporanemente.
FreeRTOS fornisce una serie di strumenti per la gestione risorse del sistema, come la gestione della memoria, lo scambio di dati tra i task e ecc.
Un microcontrollore con un solo core può eseguire una sola attività alla volta, lo scheduler di FreeRTOS utilizza il timer hardware interno al microcontrollore per gestire tutte le attività presenti nella coda dei task in attesa.
Quando il timer raggiunge il timeout, lo scheduler salva lo stato del task corrente e passa ad un altro task.
In seguito, quando il task viene ripreso, lo scheduler carica i registri di stato precedentemente salvati per riportare l’attività allo stesso stato in cui era stata interrotta.
Lo scheduler di FreeRTOS seleziona il task successiva da eseguire in base alla priorità assegnata ad ogni task.
Il processo di alternanza dei task viene ripetuto continuamente, consentendo ad ogni task di utilizzare una parte del tempo di elaborazione del sistema.
Ciò consente di ottimizzare l’utilizzo delle risorse del sistema, garantendo che le attività più importanti vengano eseguite in modo tempestivo e che il sistema possa rispondere in modo efficace alle esigenze dell’utente o dell’applicazione.
L’intervallo di tempo di esecuzione di un task è solitamente abbastanza breve, a volte anche dell’ordine di microsecondi.
Ciò è noto come elaborazione simultanea o elaborazione concorrente.
In questo modo, l’utilizzo delle risorse del sistema è ottimizzato e i task vengono eseguiti in modo tempestivo, garantendo che il sistema possa rispondere in modo efficace alle esigenze dell’utente.
Il timer hardware svolge un ruolo cruciale nel processo di schedulazione delle attività.
Il timer hardware genera un segnale di interruzione allo scadere del suo conteggio, questo segnale di interruzione viene utilizzato dallo scheduler per interrompere l’esecuzione del task corrente e passare ad un altro task.
In questo modo, il timer hardware impedisce che una solo task monopolizzi la CPU e si ha la garanzia che tutti task abbiano la possibilità di utilizzare una parte del tempo di elaborazione del sistema.
Anche se un task è bloccata in un ciclo infinito, il timer hardware garantirà che gli altri task possano continuare ad essere eseguite in modo tempestivo.
FreeRTOS è un sistema operativo real-time per microcontrollori, non è nativo per l’ambiente di sviluppo di Arduino, tuttavia esistono delle librerie che permettono di utilizzarlo.
In generale è necessario scaricare le librerie FreeRTOS per Arduino dal sito web ufficiale o da un repository esterno e importarle nell’ambiente di sviluppo per poterle utilizzare nei propri sketch.
FreeRTOS fornisce una serie di API per la creazione e la gestione dei task, come la loro creazione, la gestione della priorità, la sospensione e la loro ripresa .
Inoltre, fornisce anche una serie di strutture dati e funzioni per la gestione della sincronizzazione tra task, come i semafori, i mutex e i timer ( li descriveremo in un altro post).
Queste API consentono agli sviluppatori di scrivere codice multi-tasking efficiente per microcontrollori.
L’IDE di Arduino include diverse librerie per la gestione di periferiche come WiFi, Ethernet, TCP/IP e Bluetooth.
Queste librerie sono progettate per semplificare l’utilizzo di queste periferiche, fornendo funzioni di alto livello per la loro configurazione e il controllo.
Ciò consente agli sviluppatori di concentrarsi sulla logica dell’applicazione piuttosto che sui dettagli a basso livello della comunicazione con le periferiche.
Tuttavia, è importante notare che queste librerie possono non essere sempre disponibili per tutti i tipi di moduli e dispositivi, pertanto è sempre consigliabile verificare la compatibilità prima di utilizzarle
Creare un task in FreeRTOS: un esempio semplice per comprendere il funzionamento
La creazione di un task può essere facilmente compresa attraverso l’esempio di un semplice codice.
Il codice seguente crea tre task che fa lampeggiare tre LED.
Il codice riportato di seguito fornisce una dimostrazione semplice ma efficace di come vengono creati i task in FreeRTOS utilizzando l’ambiente di sviluppo di Arduino.
Il codice mostra come utilizzare la funzione xTaskCreate() per creare un nuovo task, passando il puntatore alla funzione che rappresenta il task e alcune informazioni come la dimensione dello stack del task e la priorità.
In generale, questo esempio fornisce una buona base per comprendere come creare un task in FreeRTOS e come utilizzare le funzioni per gestirlo.
#define LED1 16 // GPIO 16 #define LED2 17 // GPIO 17 #define LED3 18 // GPIO 18 struct s_led { byte gpio; // LED numero del GPIO byte state; // stao del LED unsigned napms; // Delay (ms) TaskHandle_t taskh; // Task handle }; static s_led leds[3] = { { LED1, 0, 500, 0 }, { LED2, 0, 200, 0 }, { LED3, 0, 750, 0 } }; static void led_task_func(void *argp) { s_led *ledp = (s_led*)argp; unsigned stack_hwm = 0, temp; delay(1000); for (;;) { digitalWrite(ledp->gpio,ledp->state ^= 1); temp = uxTaskGetStackHighWaterMark(nullptr); if ( !stack_hwm || temp < stack_hwm ) { stack_hwm = temp; printf("Task per GPIO %d lo stack hwm %u\n", ledp->gpio,stack_hwm); } delay(ledp->napms); } } void setup() { int app_cpu = 0; // numero della CPU delay(500); // Pausa per il setup della seriale app_cpu = xPortGetCoreID(); printf("app_cpu is %d (%s core)\n", app_cpu, app_cpu > 0 ? "Dual" : "Single"); printf("LEDs su gpios: "); for ( int x=0; x<3; ++x ) { s_led& led = leds[x]; pinMode(led.gpio,OUTPUT); digitalWrite(led.gpio,LOW); xTaskCreatePinnedToCore( led_task_func, "led_task", 2048, &led, 1, &led.taskh, app_cpu ); printf("%d ",led.gpio); } putchar('\n'); } void loop() { delay(1000); }
Descrizione del codice
struct s_led { byte gpio; // LED numero del GPIO byte state; // stao del LED unsigned napms; // Delay (ms) TaskHandle_t taskh; // Task handle }; static s_led leds[3] = { { LED1, 0, 500, 0 }, { LED2, 0, 200, 0 }, { LED3, 0, 750, 0 } };
Il codice utilizza una struct chiamata “s_led” per rappresentare ciascun LED e un array di queste struct rappresentare tutti le variabili di configurazione dei tre LED.
La struct contiene informazioni sul pin GPIO del LED, lo stato attuale del LED, il ritardo (napms) da utilizzare per il blink e l’handle del task.
static void led_task_func(void *argp) { s_led *ledp = (s_led*)argp; unsigned stack_hwm = 0, temp; delay(1000); for (;;) { digitalWrite(ledp->gpio,ledp->state ^= 1); temp = uxTaskGetStackHighWaterMark(nullptr); if ( !stack_hwm || temp < stack_hwm ) { stack_hwm = temp; printf("Task per GPIO %d lo stack hwm %u\n", ledp->gpio,stack_hwm); } delay(ledp->napms); } }
La funzione “led_task_func” gestisce il comportamento del task per ciascun LED.
La riga di codice “digitalWrite(ledp->gpio,ledp->state ^= 1);” imposta lo stato del LED specificato dalla struct s_led come l’opposto dello stato attuale.
La funzione digitalWrite() prende come primo parametro il numero del pin GPIO del LED e come secondo parametro lo stato del LED (HIGH per acceso e LOW per spento).
La notazione ledp->state ^= 1 utilizza l’operatore di XOR con 1, che inverte lo stato corrente del LED.
In questo caso, se lo stato del LED è attualmente spento (0), verrà impostato come acceso (1) e viceversa.
La riga di codice “temp = uxTaskGetStackHighWaterMark(nullptr);” utilizza la funzione uxTaskGetStackHighWaterMark() per ottenere la dimensione massima utilizzata dello stack del task corrente.
La funzione prende un parametro che è un puntatore alla variabile TaskHandle_t del task di cui si desidera conoscere la dimensione massima utilizzata dello stack.
In questo caso, il parametro è impostato su nullptr, il che indica che la funzione deve restituire la dimensione massima utilizzata dello stack del task corrente.
Il valore restituito viene assegnato alla variabile “temp” e usato per conoscere la dimensione massima utilizzata dello stack del task.
La riga di codice “if ( !stack_hwm || temp < stack_hwm ) “ verifica se la dimensione massima utilizzata dello stack del task (contenuta nella variabile temp) è minore della dimensione massima utilizzata dello stack del task precedente (contenuta nella variabile stack_hwm). Se la condizione è vera, la dimensione massima utilizzata dello stack del task corrente (temp) viene assegnata alla variabile stack_hwm.
Questo consente di tenere traccia della dimensione massima utilizzata dello stack per il task corrente e confrontarla con quella del task precedente.
“printf(“Task per GPIO %d lo stack hwm %u\n”,ledp->gpio,stack_hwm);” stampa un messaggio che indica la dimensione massima utilizzata dello stack del task e il numero di pin del LED associato al task, in modo che l’utente possa vedere quali task hanno utilizzato una maggiore quantità di memoria.
In generale, questo controllo della dimensione massima utilizzata dello stack consente all’utente di monitorare la memoria utilizzata dai task e di individuare eventuali problemi di utilizzo della memoria in modo tempestivo.
“delay(ledp->napms);” utilizza la funzione delay() per sospendere l’esecuzione del task corrente per un periodo di tempo specifico.
Il parametro della funzione è il valore contenuto nella struct s_led (ledp->napms) rappresenta il ritardo (napms) da utilizzare per il blink.
Il valore di questa variabile è impostato durante la creazione dell’array di struct leds all’inizio del codice.
Questo consente di fare lampeggiare i led con tempi diversi, creando un effetto di blink differente per ogni led, essendo i tempi di blink differenti.
printf("LEDs su gpios: "); for ( int x=0; x<3; ++x ) { s_led& led = leds[x]; pinMode(led.gpio,OUTPUT); digitalWrite(led.gpio,LOW); xTaskCreatePinnedToCore( led_task_func, "led_task", 2048, &led, 1, &led.taskh, app_cpu ); printf("%d ",led.gpio); } putchar('\n');
“for ( int x=0; x<3; ++x )” inizia un ciclo for che iterare 3 volte, una volta per ogni elemento dell’array di struct leds.
“s_led& led = leds[x];” crea un riferimento ad un elemento dell’array di struct, denominato led, che consente di accedere alle informazioni della struct per un determinato LED.
“pinMode(led.gpio,OUTPUT);” configura il pin del LED specificato dalla struct come una uscita utilizzando la funzione pinMode().
“digitalWrite(led.gpio,LOW);” imposta lo stato del LED specificato dalla struct su spento utilizzando la funzione “digitalWrite()”.
Questo ciclo for configura tutti i pin dei LED come uscite e impostare tutti i LED sullo stato spento all’inizio del programma.
Ciò garantisce che tutti i LED siano inizialmente spenti quando i task vengono creati e avviati.
La funzione “led_task_func” è chiamata da “xTaskCreatePinnedToCore()” che crea il task, passando l’indirizzo della struct del LED corrispondente come argomento.
Il codice d’esempio nel post in questione è stato preso da GitHub, una piattaforma di sviluppo software che consente agli utenti di archiviare, condividere e collaborare su progetti di codice sorgente.
Se hai domande o curiosità riguardo al codice o al post, non esitare a contattarci tramite il modulo di contatto sottostante. Saremo felici di aiutarti!
Buon giorno, ho riscoperto da 4/5 mesi arduino e (rubando ) i comandi da tutorial vari sono riuscito a compilare un programmino niente male(qualche difettino rimane)
Ora leggendo qui e là mi si prospetta questo Freertos, questa piattaforma dovrebbe eliminare i (difetti) del programma da me compilato.
Domanda dove o come posso recuperare i comandi x compilare uno stak x gestire con 3 comandi una striscia di neopixels( con effetti,lampeggio/ led che si rincorrono) su arduino nano every .
Grazie del tempo che mi dedicherete
Ciao Fulvio,
FreeRTOS è un sistema operativo progettato per dispositivi embedded.
Non risolve direttamente i problemi nel tuo codice, ma ti fornisce un ambiente per progettare e gestire sistemi in tempo reale.