Skip to the content.

Torna all’indice generazione tempi

SCHEDULAZIONE CON I PROTOTHREAD

Base teorica

I protothread sono un tipo di thread leggeri senza stack progettati per sistemi con vincoli di memoria severi come sottosistemi embedded o nodi di reti di sensori. I protothreads possono essere usati con o senza un RTOS.

I protothread forniscono un meccanismo di blocco dei task (compiti) in cima a un sistema ad eventi, senza l’overhead di uno stack per ogni thread. Lo scopo dei protothread è quello di implementare un flusso sequenziale di controllo senza utilizzare complesse macchine a stati finiti ed evitando l’overhead di un multi-threading completo, cioè quello dotato anche del modo preemptive. I protothread forniscono la sola modalità cooperativa e il rilascio anticipato delle risorse è realizzato senza l’utilizzo di interrupt che generino il cambio di contesto dei thread.

In altre parole, la gestione dei task è ad eventi all’interno di un singolo thread reale ma, tramite delle macro, un algoritmo si può programmare in maniera sequenziale all’interno di funzioni che girano su dei thread logici, cioè emulati. La concorrenza dei task è gestita da uno schedulatore che, invocato durante il periodo di sleep di un task, o tramite il comando yeld, assegna la risorsa CPU a quei task che, in quell’intervallo, ne fanno richiesta.

Confronto con le altre tecniche

Anche i processi e i thread sono flussi di esecuzione indipendenti che procedono in parallelo su una o più CPU, esiste però una differenza pratica notevole tra di essi:

Utilizzo in pratica

Ogni protothread realizza un flusso di esecuzione parallelo a quello degli altri thread, inoltre ognuno possiede un proprio loop() principale di esecuzione in cui realizzare le operazioni che tipicamente riguardano le tre fasi di lettura degli ingressi, calcolo dello stato del sistema e della sua risposta e la fase finale di scrittura della risposta sulle uscite. Il loop principale è definito sotto forma di ciclo infinito come ad esempio:

while(true) {
    // codice del protothread
    .........................
}

Le fasi di lavoro del loop possono essere schedulate (pianificate nel tempo) dal delay() non bloccante PT_SLEEP(pt) e dal rilascio spontaneo PT_YIELD(pt)che permettono la progettazione lineare di un algoritmo nel tempo.

Ogni protothread è definito da un descrittore che è una variabile di tipo struct, cioè il tipo record del C, che rappresenta il protothread. Il nome del descrittore è arbitrario a discrezione del programmatore. Il descrittore deve essere passato come argomento ad ogni comando (macro) della libreria protothread. Il descrittore può essere definito esattamente prima della definizione della funzione del protothread tramite la dichiarazione pt ptNome_descr;

Il flusso di esecuzione di un protothread è definito all’interno di una funzione e può essere avviato passando allo schedulatore il riferimento a questa funzione sotto la forma di parametro. In sostanza la funzione serve al programmatore per definire il protothread e allo schedulatore per poterlo richiamare. All’interno della funzione il protothread deve sempre cominciare con il comando PT_BEGIN(pt) e deve sempre terminare con il comando PT_END(pt).

In definitiva la dichiarazione e definizione di descrittore e funzione del protothread possono assumere la forma:

pt ptMioScopo;
int mioScopoThread(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop forever
  while(true) {
	// codice del protothread
	.........................
  }
  PT_END(pt);
}

Ogni protothread è inizializzato nel setup() tramite la funzione PT_INIT(&ptMioScopo), il passaggio del descrittore è per riferimento perchè questo deve poter essere modificato al momento della inizializzazione.

void setup() {
  PT_INIT(&ptMioScopo1);
  PT_INIT(&ptMioScopo2);
}

Ogni protothrad viene schedulato cioè, valutato periodicamente per stabilire se deve essere eseguito o meno, all’interno del loop() tramite il comando PT_SCHEDULE(mioScopoThread(&ptMioScopo)) che ha per argomento la funzione che definisce il protothread.

void loop() {
	PT_SCHEDULE(mioScopo1Thread(&ptMioScopo1));
	PT_SCHEDULE(mioScopo2Thread(&ptMioScopo2));
}

Per quanto riguarda la definizione di un protothread va ricordato che ll’interno del loop del protothread ogni ramo di esecuzione va reso non bloccante inserendo, la funzione PT_SLEEP(pt) (mai la normale delay()) se il flusso di esecuzione deve essere bloccato temporaneamente per un certo tempo fissato, oppure la funzione PT_YIELD(pt) se questo non deve essere bloccato. Ciò serve a richiamare lo schedulatore almeno una volta, qualunque direzione di esecuzione prenda il codice, in modo da cedere “spontaneamente” il controllo ad un altro prothread al termine del loop() del prothread corrente. La cessione del controllo dello schedulatore ad ogni ramo di esecuzione è necessario altrimenti gli altri protothread non verrebbero mai eseguiti (il sistema non è preemptive). Sia PT_YIELD(pt) che PT_SLEEP(pt) cedono il controllo della CPU allo schedulatore che lo assegna agli altri protothread che eventualmente in quel momento hanno scaduto il tempo di attesa di un loro delay.

Blink sequenziali interagenti

Di seguito è riportato un esempio di blink sequenziale in esecuzione su due thread separati su scheda Arduino Uno, con IDE Arduino e con la libreria protothread.h (https://gitlab.com/airbornemint/arduino-protothreads). I thread sono senza stack e non preemptive (solo collaborativi). La programmazione sequenziale del blink del led è emulata tramite una funzione delay() non bloccante PT_SLEEP(pt, 200) fornita dalla libreria protothreads.h.

/*
  Blink
  Turns an LED on for one second, then off for one second, repeatedly. Rewritten with Protothreads.
*/
/*
#define LC_INIT(lc)
struct pt { unsigned short lc; };
#define PT_THREAD(name_args)  char name_args
#define PT_BEGIN(pt)          switch(pt->lc) { case 0:
#define PT_WAIT_UNTIL(pt, c)  pt->lc = __LINE__; case __LINE__: \
                              if(!(c)) return 0
#define PT_END(pt)            } pt->lc = 0; return 2
#define PT_INIT(pt)   LC_INIT((pt)->lc)
#define PT_SLEEP(pt, delay) \
{ \
  do { \
    static unsigned long protothreads_sleep; \
    protothreads_sleep = millis(); \
    PT_WAIT_UNTIL(pt, millis() - protothreads_sleep > delay); \
  } while(false); \
}
#define PT_EXITED  2
#define PT_SCHEDULE(f) ((f) < PT_EXITED)
#define PT_YIELD(pt) PT_SLEEP(pt, 0)
//-----------------------------------------------------------------------------------------------------------
// se si usa questa libreria al posto delle macro sopra, togliere il commento iniziale all'include 
// e commentare le macro sopra
*/
#include "protothreads.h"

bool blink1_running = true;
bool blink2_running = true;
int led1 = 13;
int led2 = 12;
int count = 0;

pt ptBlink1;
int blinkThread1(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop forever
  while(true) {
	if (blink1_running == true) {
		digitalWrite(led1, HIGH);   // turn the LED on (HIGH is the voltage level)
		PT_SLEEP(pt, 500);
		digitalWrite(led1, LOW);    // turn the LED off by making the voltage LOW
		PT_SLEEP(pt, 500);
		count += 1;
		Serial.println(count);
	} else {
		digitalWrite(led1, LOW);    // turn the LED off by making the voltage LOW
		PT_YIELD(pt);
	}
  }
  PT_END(pt);
}

pt ptBlink2;
int blinkThread2(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop forever
  while(true) {
	if (blink2_running == true) {
		digitalWrite(led2, HIGH);   // turn the LED on (HIGH is the voltage level)
		PT_SLEEP(pt, 1000);
		digitalWrite(led2, LOW);    // turn the LED off by making the voltage LOW
		PT_SLEEP(pt, 1000);
	} else {
		digitalWrite(led2, LOW);    // turn the LED off by making the voltage LOW
		PT_YIELD(pt);
	}
  }
  PT_END(pt);
}

// the setup function runs once when you press reset or power the board
void setup() {
  Serial.begin(115200);
  PT_INIT(&ptBlink1);
  PT_INIT(&ptBlink2);
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  int count = 0;
}

// the loop function runs over and over again forever
void loop() {
	PT_SCHEDULE(blinkThread1(&ptBlink1));
	PT_SCHEDULE(blinkThread2(&ptBlink2));
  
	if(count >= 10){
		Serial.println("Ending threads...");
		blink1_running = false;
		blink2_running = false;
	}
}

Link simulazione online: https://wokwi.com/projects/348712126874911315

Osservazioni:

Pulsante responsivo + blink

Di seguito è riportato un esempio su scheda Arduino Uno, con IDE Arduino e con la libreria protothread.h di un blink sequenziale in esecuzione su un thread e di gestione del pulsante sul loop principale. I ritardi sleep agiscono sul thread secondario ma non bloccano la lettura dello stato del pulsante che rimane responsivo nell’accendere il secondo led durante entrambe le fasi del blink del primo led. (Link simulatore online: https://wokwi.com/projects/348713258463527506)

#include "protothreads.h"

bool blink1_running = true;
int led1 = 13;
int led2 = 12;
byte pulsante=2;

// definizione protothread del lampeggio
pt ptBlink1;
int blinkThread1(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop secondario protothread
  while(true) {
	digitalWrite(led1, HIGH);   // turn the LED on (HIGH is the voltage level)
	PT_SLEEP(pt, 500);			// delay non bloccanti
	digitalWrite(led1, LOW);    // turn the LED off by making the voltage LOW
	PT_SLEEP(pt, 500);			// delay non bloccanti
  }
  PT_END(pt);
}

// the setup function runs once when you press reset or power the board
void setup() {
  PT_INIT(&ptBlink1);
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
}

// the loop function runs over and over again forever
void loop() { // loop principale
	PT_SCHEDULE(blinkThread1(&ptBlink1)); // esecuzione schedulatore protothreads
    
	// gestione pulsante
	if (digitalRead(pulsante) == HIGH) {
		// turn LED on:
		digitalWrite(led2, HIGH);
	}else{
		// turn LED off:
		digitalWrite(led2, LOW);
	}
}

Lo schedulatore dei protothred può essere utilizzato pure su piattaforme di emulazione (ad es. Tinkercad) senza la necessità di includere librerie, basta inserire in cima al codice la definizione delle macro dello schedularore.

SOS + blink

Link simulazione su Arduino in Tinkercad: https://www.tinkercad.com/embed/2duYFwQzJgf?editbtn=1

Simulazione su Esp32 in Wowki: https://wokwi.com/projects/348709819084964435

/*
Realizzare il blink di un led insieme al lampeggio di un'altro led che codifica il messaggio morse dell'SOS.
*/
#define LC_INIT(lc)
struct pt { unsigned short lc; };
#define PT_THREAD(name_args)  char name_args
#define PT_BEGIN(pt)          switch(pt->lc) { case 0:
#define PT_WAIT_UNTIL(pt, c)  pt->lc = __LINE__; case __LINE__: \
                              if(!(c)) return 0
#define PT_END(pt)            } pt->lc = 0; return 2
#define PT_INIT(pt)   LC_INIT((pt)->lc)
#define PT_SLEEP(pt, delay) \
{ \
  do { \
    static unsigned long protothreads_sleep; \
    protothreads_sleep = millis(); \
    PT_WAIT_UNTIL(pt, millis() - protothreads_sleep > delay); \
  } while(false); \
}
#define PT_EXITED  2
#define PT_SCHEDULE(f) ((f) < PT_EXITED)
#define PT_YIELD(pt) PT_SLEEP(pt, 0)
//-----------------------------------------------------------------------------------------------------------
// se si usa questa libreria al posto delle macro sopra, togliere il commento iniziale all'include 
// e commentare le macro sopra
//#include "protothreads.h"
#define PDELAY  500
#define LDELAY  1500
#define BLINKDELAY  100
byte led1 = 12;
byte led2 = 13;

void setup()
{
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  PT_INIT(&ptSos);
  PT_INIT(&ptBlink);
}
 

// definizione protothread del pulsante
pt ptSos;
int SOSThread(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop del protothread
  while(true) {
	// 3 punti
	digitalWrite(led1, HIGH); // 1
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, HIGH); // 2
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, HIGH); // 3
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, PDELAY);
	// 1 linea
	digitalWrite(led1, HIGH); // 1 linea
	PT_SLEEP(pt, LDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, PDELAY);
	// 3 punti
	digitalWrite(led1, HIGH); // 1
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, HIGH); // 2
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, HIGH); // 3
	PT_SLEEP(pt, PDELAY);
	digitalWrite(led1, LOW);
	PT_SLEEP(pt, LDELAY);
  }
  PT_END(pt);
}

// definizione protothread del lampeggio
pt ptBlink;
int blinkThread(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop del protothread
  while(true) {
	digitalWrite(led2, HIGH);   	// turn the LED on (HIGH is the voltage level)
	PT_SLEEP(pt, BLINKDELAY);
	digitalWrite(led2, LOW);    	// turn the LED off by making the voltage LOW
	PT_SLEEP(pt, BLINKDELAY);
  }
  PT_END(pt);
}

// loop principale
void loop()
{
	PT_SCHEDULE(SOSThread(&ptSos)); 		// esecuzione schedulatore protothreads
	PT_SCHEDULE(blinkThread(&ptBlink)); 	// esecuzione schedulatore protothreads
}

Lampeggi multipli

LInk simulazione in Tinkercad: https://www.tinkercad.com/embed/gCkCBuwlY8E?editbtn=1

/*
Realizzare il blink di un led insieme al lampeggio di un'altro led che codifica il messaggio morse dell'SOS.
*/
#define LC_INIT(lc)
struct pt { unsigned short lc; };
#define PT_THREAD(name_args)  char name_args
#define PT_BEGIN(pt)          switch(pt->lc) { case 0:
#define PT_WAIT_UNTIL(pt, c)  pt->lc = __LINE__; case __LINE__: \
                              if(!(c)) return 0
#define PT_END(pt)            } pt->lc = 0; return 2
#define PT_INIT(pt)   LC_INIT((pt)->lc)
#define PT_SLEEP(pt, delay) \
{ \
  do { \
    static unsigned long protothreads_sleep; \
    protothreads_sleep = millis(); \
    PT_WAIT_UNTIL(pt, millis() - protothreads_sleep > delay); \
  } while(false); \
}
#define PT_EXITED  2
#define PT_SCHEDULE(f) ((f) < PT_EXITED)
//-----------------------------------------------------------------------------------------------------------
// se si usa questa libreria al posto delle macro sopra, togliere il commento iniziale all'include 
// e commentare le macro sopra
//#include "protothreads.h"
#define DELAY1  1000
#define DELAY2  500
#define DELAY3  300
byte led1 = 10;
byte led2 = 11;
byte led3 = 12;
byte led4 = 13;

void setup()
{
  pinMode(led1, OUTPUT);
  
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  pinMode(led4, OUTPUT);
  PT_INIT(&ptBlink2);
  PT_INIT(&ptBlink1);
}
 

// definizione protothread del pulsante
pt ptBlink1;
int blink1Thread(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop del protothread
  while(true) {//for(;;)
	// 3 punti
	digitalWrite(led1, HIGH);   	// turn the LED on (HIGH is the voltage level)
	PT_SLEEP(pt, DELAY1);
	digitalWrite(led1, LOW);    	// turn the LED off by making the voltage LOW
	PT_SLEEP(pt, DELAY1);
  }
  PT_END(pt);
}

// definizione protothread del lampeggio
pt ptBlink2;
int blink2Thread(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop del protothread
  while(true) {
	digitalWrite(led2, HIGH);   	// turn the LED on (HIGH is the voltage level)
	PT_SLEEP(pt, DELAY2);
	digitalWrite(led2, LOW);    	// turn the LED off by making the voltage LOW
	PT_SLEEP(pt, DELAY2);
  }
  PT_END(pt);
}

// definizione protothread del pulsante
pt ptBlink3;
int blink3Thread(struct pt* pt) {
  PT_BEGIN(pt);

  // Loop del protothread
  while(true) {//for(;;)
	// 3 punti
	digitalWrite(led3, HIGH);   	// turn the LED on (HIGH is the voltage level)
	PT_SLEEP(pt, DELAY3);
	digitalWrite(led3, LOW);    	// turn the LED off by making the voltage LOW
	PT_SLEEP(pt, DELAY3);
  }
  PT_END(pt);
}

// loop principale
void loop()
{
	PT_SCHEDULE(blink1Thread(&ptBlink1)); 		// esecuzione schedulatore protothreads
	PT_SCHEDULE(blink2Thread(&ptBlink2)); 	// esecuzione schedulatore protothreads
	PT_SCHEDULE(blink3Thread(&ptBlink3)); 	// esecuzione schedulatore protothreads
}

Sitografia:

Torna all’indice generazione tempi