Indice
Nel corso della vita da makers, una persona si potrebbe imbattere in progetti che potrebbero rivelarsi più grandi rispetto a quello che magari si potesse inizialmente pensare. In questo articolo, ho intenzione di dare qualche conoscenza base sul concetto delle architetture basate sui task, che permettono di semplificare la scrittura del codice. Aggiungo inoltre che per comprendere al meglio gli esempi che illustrerò sarà necessaria una infarinatura nella programmazione ad oggetti.
Che cosa sono i task?
I task sono delle unità di lavoro che devono essere svolte. I task possono essere suddivisi in modo ricorsivo in sub-task, fino ad arrivare a un punto che il sub-task non può essere più suddiviso. In questo caso avremo un task atomico.
Come si potrebbe rappresentare un task?
A questo punto dobbiamo fare una distinzione della fase di progettazione e della fase di implementazione. Nella fase di progettazione dobbiamo tenere in considerazione che il task è una entità dinamica e per progettarlo possiamo rappresentare il suo funzionamento tramite un diagramma a stati finiti. Nella fase di implementazione il task viene rappresentato da una classe per ogni task previsto.
Quali sono i vantaggi portati dai task?
I vantaggi a livello implementativo sono diversi: rendono il codice più comprensibile, aiutano a localizzare errori durante il debugging e hanno un maggiore supporto per la modificabilità e il riuso. Inoltre usando i task può avere una separazione netta dei compiti.
Come eseguire più task in una scheda?
Se abbiamo deciso di usare una architettura che prevede task, implica il fatto che avremo a che fare con almeno più di un task. Come possiamo far eseguire più task (soprattutto se eseguiti su una scheda Arduino uno)? Bhe, una possibile soluzione è realizzare un piccolo scheduler che faccia eseguire tutti i nostri task. In questo caso è stato utilizzato uno scheduler Round Robin cooperativo, ovvero ad ogni ciclo vengono eseguiti tutti i task uno dopo l’altro. La priorità in questo scheduler viene data sulla base dell’ordinamento dei task nella coda dello scheduler. Ovviamente anche lo scheduler viene rappresentato da una classe e la coda dei task viene implementata tramite un vettore.
Di seguito visualizzeremo il codice del file .ino e come viene impostato.
#include "Scheduler.h" #include "Task.h" #include "PrimoTask.h" #include "SecondoTask.h" Scheduler scheduler; void setup() { Serial.begin(9600); scheduler.init(50); //milliseconds //PrimoTask è una classe che modella l'implementazione del primo task Task* task0 = new PrimoTask(); task0->init(50); //il task0 viene messo prima nella coda dello scheduler, di conseguenza ha priorità più alta scheduler.addTask(task0); //SecondoTask è una classe che modella l'implementazione del secondo task creato Task* task1 = new SecondoTask(); task1->init(100); scheduler.addTask(task1); } void loop() { scheduler.schedule(); }
Notiamo che una volta scritto il codice nel file .ino, esso verrà modificato solo se si aggiungono nuovi task o se si rimuove uno già esistente. Questo è possibile grazie al fatto che il comportamento dello scheduler e dei task viene nascosto tramite l’incapsulamento della programmazione ad oggetti.
Di seguito è presentato un esempio di implementazione dello scheduler. Generalmente il codice del file .h e .cpp sono in 2 file separati ma per questioni di lunghezza l’ho messo tutto in un blocco.
/*inizio file Scheduler.h*/ #ifndef __SCHEDULER__ #define __SCHEDULER__ #include "Timer.h" #include "Task.h" #define NUM_TASKS 10 class Scheduler { public: void init(int basePeriod); bool addTask(Task* task); void schedule(); private: int basePeriod; int numTasks; Task* taskList[NUM_TASKS]; Timer timer; }; #endif /*Inizio file Scheduler.cpp*/ #include "Scheduler.h" /*inizializza lo scheduler, il periodo è il tempo ogni quanto deve controllare quale task deve far partire*/ void Scheduler::init(int basePeriod) { this->basePeriod = basePeriod; timer.setupPeriod(basePeriod); numTasks = 0; } /*Aggiunge task alla lista dei task da gestire*/ bool Scheduler::addTask(Task* task) { if (numTasks < NUM_TASKS) { taskList[numTasks] = task; numTasks++; return true; } return false; } /*Qui fa tutto !! 1 aspetta l'evetnto dal timer per partire 2 per ogni task nella coda controlla ogni quanto tempo deve essere eseguito ed esegue se quel tempo è passato, in caso contrario non lo esegue e passa al prossimo */ void Scheduler::schedule() { timer.waitForNextTick(); for (int i = 0; i < numTasks; i++) { if (taskList[i]->updateAndCheckTime(basePeriod)) { taskList[i]->tick(); } } }
Come possiamo vedere ogni task è stato implementato in una classe assestante, nell’esempio le classi sono PrimoTask e SecondoTask. Queste 2 classi derivano, a loro volta, da una classe astratta che si chiama Task. Questo è stato fatto per far esporre la stessa interfaccia da entrambe classi e per essere facilmente gestiti dalla classe scheduler usando la loro generalizzazione.
Come possiamo far comunicare 2 task?
Avendo più task si potrebbe avere la necessità che questi task debbano scambiarsi delle informazioni. Il metodo più semplice è quello di creare delle variabili globali, accessibili da tutti i task, che mantengono in memoria determinate informazioni. Questo metodo non lo potremmo utilizzare in presenza di uno scheduler preemptive poiché si potrebbero presentare delle corse critiche. Un ulteriore metodo per scambiarsi le informazioni tra task potrebbe essere implementando un modello a scambio di messaggi asincrono. Per avere una visione centralizzata delle variabili per il passaggio di valori tra task possiamo creare una classe per gestire tutto il contesto della applicazione con all’interno le variabili che ci servono senza perderle per l’immensa quantità di file che potremmo avere nel nostro progetto.
Questo articolo ha come lo scopo di dare una infarinatura “teorica” sulla programmazione basata su task. Ovviamente ci sarebbero diversi aspetti citati che si potrebbero approfondire. L’obbiettivo che mi sono posto con questo articolo è quello di dare al lettore uno strumento in più per gestire al meglio i progetti che necessitano di un software di elevata complessità.
E’ possibile avere un esempio di una classe contenuta in “PrimoTask.h” ?
è un po’ che non riprendo in mano questo codice quindi ho rifatto l’Interfaccia Task (Task.h) e la classe PrimoTask (PrimoTask.h). Non escludo che abbiano bisogno di sistemate, comunque indicativamente sono costituite cosi:
//Task.h
#ifndef __TASK__
#define __TASK__
class Task{
public:
virtual void tick() = 0
void init(int period) {
taskPeriod = period;
}
bool updateAndCheckTime(int basePeriod){
enlapsedTime += basePeriod;
return enlapsedTime > taskPeriod ? true : false;
}
protected:
int taskPeriod;
int enlapsedTime;
}
#endif
//—————————————-
//PrimoTask.h
#ifndef __FIRST_TASK__
#define __FIRST_TASK__
#include “Task.h”
class PrimoTask: public Task{
public:
void tick() {
//add task behaviour
}
};
#endif
bravi e complimenti. Io cerco uno schetck per arduino che si interfacci con lidar Hitachi e faccia le seguneti operazioni:
1) inizializza è azzera l’angolo di un motore passo passo;
2) invii un codice ad di avvio ad un sensore (seriale su porta TX1);
3) legga i dati seriali dal sensore (su porta RX1);
4) Registri il dato su SD;
5) invii il dato su porta bluetooh;
6) incrementi di uno steep il motore passo passo.
Ti ringrazio, buona serata!
Sto cercando di implementare un programma in Arduino che lavori con i Task, ma non riesco a capire come definire delle variabili globali visibili a tutti questi. Sarebbe possibile avere un esempio con del codice?
Grazie.
Aggiungo che le varie classi che rappresentano i Task sono implementati in file c++ diversi.
Concettualmente i task dovrebbero essere delle unità di lavoro autonome e separate, quindi l’ideale sarebbe evitare variabili globali condivise.
Nonostante ciò all’atto pratico tali variabili sono veramente comode. Un modo molto semplice e rapito per implementare questa funzionalità è quello di creare una nuova classe che rappresenti il contesto della applicazione e al suo interno inserirci tutte le variabili che dovrebbero essere globali.
Poi durante l’inizializzazione del task bisogna passare la medesima istanza di quella classe a tutti i task.
Avvertenze:
1) le variabili globali che possono essere modificate da più task potrebbe comportare a un funzionamento anomalo in determinate situazioni. Questa problematica è attenuata dal fatto che lo scheduler è cooperativo e non preemptive, ma comunque presente.
2) Tenere a mente che la memoria dei microcontrollori è molto limitata, quindi fare sempre molta attenzione a quando si alloca della memoria per tenere delle variabili