Il sito per chi ama smanettare con Arduino, Raspberry Pi & Co.
Arduino e la progettazione con i Task

Arduino e la progettazione con i Task

by

Arduino e Task: la progettazione basata sui task per semplificarci la gestione del codice del progetto

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à.

Spread the love
  • 5
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    5
    Shares

Ti è rimasta una domanda o un dubbio?

Iscriviti e chiedi nella community

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *