Come promesso, ecco l'esempio "semplie". Si tratta dell'ormai famoso programma "Blink" ma gestito tramite uno scheduler. Il codice completo segue alla fine del post, per ora spiego un pezzetto alla volta.
Cominciamo con le strutture dati di cui ci serviremo per tenere conto dei task da eseguire. Abbiamo bisogno di sapere:
- che cosa eseguire (bisogna specificare una funzione per farla eseguire...);
- un parametro, di tipo int, da passare alla funzione, in modo da non dover scrivere funzioni diverse per accendere o spengere LED diversi. A seconda delle specificita' del progetto e' possibile aggiungere altri parametri, o magari eliminare questo;
- il ritardo iniziale dopo il quale deve essere eseguita la funzione, espresso in "numero di interrupt di fine conteggio del Timer 0" (siccome questo interrupt viene utilizzato da Arduino per calcolare il tempo trascorso come riportato dalla funzione millis(), l'interrupt e' gia' programmato e la sua frequenza e' di 250 kHz diviso 256, per la precisione, quindi il tempo tra due interrupt e' di 1.024 millisecondi);
- il perdiodi dopo il quale la funzione deve essere eseguita di nuovo (stesse unita' temporali), oppure -1 se la funzione deve essere eseguita una volta sola (per esempio a seguito dell'arrivo di un segnale in ingresso);
Il codice che definisce la struttura dati per un task di questo genere segue:
Codice: Seleziona tutto
// Cominciamo col definire la struttura (un insieme di campi non necessariamente omogenei ma associati) di cui ci
// serviremo per definire un Task, ossia una funzione da eseguire a intervalli regolari o no.
typedef struct {
void(*funzione)(int); // Puntatore alla funzione da chiamare, void funzione(int parametro)
int parametro; // Per ora, unico parametro (costante!) per la funzione, tra -32768 e +32767
int traQuanto; // Numero di microintervalli che devono ancora passare prima della chiamata, max 32767
int periodo; // Numero di microintervalli tra due chiamate consecutive, -1 per one-shot, max 32768
} Task;
La sola parte un po' "magica" e' la
void(*funzione)(int); ma per ora fidatevi di me, vuol dire che questo campo prende come valore l'indirizzo di una funzione che prende un solo argomento di tipo int e restituisce void (ossia non restituisce niente).
Per poter fare qualcosa di utile abbiamo bisogno di poter definire un certo numero di questi Task, per cui definiremo anche la struttura seguente, che contiene essenzialmente il numero massimo di Task disponibili, e un vettore di descrittori di Task (quello che abbiamo appena visto).
Nel codice che segue, inizialmente definiamo una costante magica
MAXTASK che utilizziamo per dimensionare il vettore di Task. Dopo aver definito un nuovo tipo di struttura
Agenda finalmente dichiariamo una variabile
laMiaAgenda che ha come tipo proprio
Agenda.
La dichiariamo
volatile per avvertire il compilatore che il suo contenuto puo' cambiare anche se non a causa delle istruzioni che sta compilando (lo puo' cambiare la routine che risponde all'interrupt) e che quindi non DEVE ottimizzare in alcun modo gli accessi, ma andare a rileggere ogni volta i dati contenuti (il compilatore e' "furbo" e se ha il contenuto i una variabile gia' caricato in uno dei registri del processore puo' ottimizzare l'accesso e non andare a rileggere quel valore, con
volatile lo avvertiamo che non si puo' mai fidare).
Codice: Seleziona tutto
// Ora definiamo una struttura piu' grande, che contiene le informazioni di cui abbiamo bisogno per far funzionare
// tutto il sistema, piu' abbastanza strutture per alloggiare il numero di task di cui riteniamo di aver bisogno
#define MAXTASK 10 // Cambiate quetso per estendere o ridurre la tabella di task
typedef struct {
int massimoTask = MAXTASK; // Massimo numero di task ammissibili (poi finisce lo spazio utile!)
Task tabellina[MAXTASK];
} Agenda;
volatile Agenda laMiaAgenda; // La definiamo volatile non perche' fa le uova e svolazza ma perche' puo' cambiare
// senza che il compilatore lo sappia. Questo dice al compilatore di NON azzardarsi
// ad ottimizzare gli accessi a questa variabile ma di accedere SEMPRE al valore in
// memoria (anche se dovesse essere caricato in un registro del processore).
Ora ci serve un modo di aggiungere Task alla nostra Agenda, ed e' quello che fa la prossima funzione che vedremo. Siccome al medico, al prete ed al compilatore NON si deve mai mentire, dovremo passare a questa funzione gli argomenti di cui ha bisogno in maniera corretta, altrimenti rischiamo che il programma non compili, o peggio ancora che compili ma non faccia quel che vogliamo...
Gli argomenti sono ovviamente i valori con cui riempire i vari campi della nostra strutture Task, e precisamente:
- un puntatore ad una funzione che deve essere definita come void <nomeDelleFunzione>(int argomento) { ... }. Potete chiamarla come vi pare, perfino Beppe se non avete gia' usato questo nome, ma deve essere di tipo void e deve prendere esattamente un argomento di tipo int. Nel seguito vedremo un paio di esempi;
- un parametro di tipo int da passare come argomento alla nostra funzione (per esempio il numero del LED su cui agire, ma non solo);
- un parametro di tipo int che contiene il ritardo con cui vogliamo sia eseguita la prima volta la nostra funzione. Senza questa possibilita' non potremmo sincronizzare i nostri Task tra di loro e, per esempio, accendere un LED ogni secondo a partire da "adesso" e spengere lo stesso LED sempre ogni secondo ma a partire da "adesso piu' mezzo secondo";
- un parametro di tipo int che contiene il periodo con cui deve essere eseguita la nostra funzione, o -1 se vogliamo che sia eseguita una volta sola (pensate ad una porta che si apre premendo un pulsante e che deve essere richiusa un minuto dopo l'apertura);
Per renderci la vita piu' difficile, le alterazioni all'Agenda dobbiamo farle disabilitando momentaneamente le interruzioni, per evitare che l'arrivo di un interrupt mentre stiamo ancora inserendo il nuovo Task ci colga alla sprovvista...
Ecco il codice:
Codice: Seleziona tutto
// Questa funzione serve per inserire un task nella lista di attesa.
// Prende come argomenti:
// void (*funzione)(int) = un puntatore ad una funzione che restituisce void e prende un argomento di tipo int
// quindi deve essere dichiarata come: void funzione(int parametro) { ... } (nomi a piacere...)
// int parametro = un parametro intero, che sara' passato alla funzione al momento della chiamata
// int traQuanto = il numero di chiamate alla ISR che devono passare prima dell'esecuzione iniziale della
// funzione. Un valore di 1 causa la prima esecuzione "quasi immediatamente"
// int periodo = il numero di chiamate alla ISR dopo le quali la funzione viene rieseguita (se e' ciclica).
// un valore di -1 (o comunque negativo) significa che la funzione e' eseguita una volta sola
// Il valore int restituito contiene la posizione nella nostra tabellina in cui e' stato inserito il Task, o
// -1 se la tabellina e' gia' piena.
int inserisciTask(void (*funzione)(int), int parametro, int traQuanto, int periodo) {
int index;
for (index = 0; index < laMiaAgenda.massimoTask; index++) {
if (laMiaAgenda.tabellina[index].funzione == NULL) {
cli(); // E' cosa delicata, non possiamo permetterci che la ISR ci muva la tabellina sotto il naso
laMiaAgenda.tabellina[index].funzione = funzione;
laMiaAgenda.tabellina[index].parametro = parametro;
laMiaAgenda.tabellina[index].traQuanto = traQuanto;
laMiaAgenda.tabellina[index].periodo = periodo;
// Riabilitiamo subito gli interrupt, non appena finito!
sei();
return index;
}
}
// Se siamo arrivati fin qui vuol dire che non c'era posto in tabellina per un nuovo task
return -1;
}
Ora viene un pezzo di codice rognosetto, ossia la routine di risposta all'interrupt del Timer 0. Bisogna scorrere la nostra Agenda e vedere, per ogni elemento del vettore di task, se il Task in questione esiste (e allora il puntatore alla funzione non sara' NULL) oppure no. Se esiste, diminuiamo di uno il numero di interrupt che deve aspettare prima di essere eseguito, e se arriviamo a zero lo eseguiamo! Dopo averlo eseguito controlliamo se e' un Task ciclico, e allora ricarichiamo il contatore del numero di interrupt da aspettare con il valore di
periodo o se era un task monouso, e allora ci riprendiamo lo slot sovrascrivendo il puntatore alla sua funzione col valore NULL (e cosi' potremo inserire un nuovo Task al suo posto, se ci scappa).
Codice: Seleziona tutto
// Qui vive la "gestione" del sistema, il codice che controlla a che task tocca di eseguire
SIGNAL(TIMER0_COMPA_vect)
{
int index;
// Esaminiamo la tabellina di task
for (index = 0; index < laMiaAgenda.massimoTask; index++) {
if (laMiaAgenda.tabellina[index].funzione != NULL) { // C'e' una funzione, non e' una locazione vuota
laMiaAgenda.tabellina[index].traQuanto--; // Sottraiamo 1 al numero di intervalli di attesa di ciascun task
// poi verifichiamo se il task e' "maturo" per l'esecuzione (ha aspettato abbastanza a lungo)
if (laMiaAgenda.tabellina[index].traQuanto <= 0) {
laMiaAgenda.tabellina[index].funzione(laMiaAgenda.tabellina[index].parametro);
// ora vediamo se si tratta di un task periodico da rieseguire in seguito o una tantum
if (laMiaAgenda.tabellina[index].periodo > 0) { // Periodico
laMiaAgenda.tabellina[index].traQuanto = laMiaAgenda.tabellina[index].periodo;
} else { // No, era un task una tantum, allora lo "cancelliamo" dalla lista scrivendo NULL come
// indirizzo della funzione da eseguire
laMiaAgenda.tabellina[index].funzione = NULL;
}
}
}
}
}
Puff! Che faticata! Ma siamo quasi arrivati in fondo, e il codice che abbiamo visto fin qui e' quello che restera' "fisso" indipendenytemente dalla applicazione specifica per cui ce ne serviremo, mentre quanto segue e' piu' una "personalizzazione" in vista della specifica applicazione, che altro non e' se non la famosa "Blink" che lampeggia un LED ogni secondo. Ci servono due funzioni da usare nei nostri task, una per accendere il LED e una per spengerlo, ed eccole. Notate che sono definite come
void <il nome che mi pare>(in argomento) perche' altrimenti risultano indigeste alla nostra Agenda.
Codice: Seleziona tutto
// Una funzione per accendere un LED
void accendiLed(int qualeLed) {
digitalWrite(qualeLed, HIGH);
}
// E l'equivalente funzione per spengere un LED
void spengiLed(int qualeLed) {
digitalWrite(qualeLed, LOW);
}
Finalmente la funzione setup(), nella quale "costruiamo" la nostra Agenda, iscrivendo prima uno e poi l'altro dei Task. L'ordine di inserzione non e' importante, quello che decide la sequenza e il ritardo sono i due parametri
traQuanto e
periodo. In particolare diamo disposizione che il LED sia acceso ogni 1000 interrupt (potremmo fare 1024 se fossimo pignolissimi e volessimo un secodno esatto) e spento ugualmente ogni 1000, ma che la prima accensione avvenga subito, e il primo spengimento mezzo secondo dopo. Questo stabilisce il ritardo tra le due chiamate, che si conserva "ad infinitum". Nella setup() "agganciamo" anche ilnostro "gestore" all'interrupt del Timer0:
Codice: Seleziona tutto
const uint8_t uscitaUno = 13;
void setup() {
int index;
int risultato;
// Dichiariamo i nostri pin, e azzeriamo le uscite
pinMode(uscitaUno, OUTPUT);
digitalWrite(uscitaUno, LOW);
// Azzeriamo tutta la tabellina di funzioni gia' che ci siamo
for (index = 0; index < laMiaAgenda.massimoTask; index++) {
laMiaAgenda.tabellina[index].funzione = NULL;
laMiaAgenda.tabellina[index].parametro = 0;
laMiaAgenda.tabellina[index].traQuanto = 0;
laMiaAgenda.tabellina[index].periodo = -1;
}
// Timer0 e' gia' usato per la funzione millis()
// ma noi ci agganciamo da qualche parte nel mezzo
// e chiamiamo la nostra funzione
TIMSK0 |= _BV(OCIE0A); // Questa e' la maschera degli Interrupt del Timer 0, e questa strana istruzione
// _BV() e' una macro offertaci da Arduino che vuol dire "Bit Value", quindi _BV(2) = 4
// (il valore del Bit 2). E' definita come uno shift a sinistra di N posizioni:
// #define _BV(bit) (1 << (bit))
// Facciamo l'OR bita a bit del contenuto del registro con _BV(OCIE0A) per mettere
// A "1" il bit che abilita le interruzioni quando il Timer 0 raggiunge il valore di sopra
// E predisponiamo la porta seriale per vedere che cosa succede
Serial.begin(9600);
// Ora aggiungiamo due task per eseguire il famoso "Blink!"
// La temporizzazione si fa intermini di chiamate alla ISR, quindi circa ogni millisecondo.
// Task 1: accendere il LED appena possibile, e poi ogni 1000 chiamate (circa 1000 ms)
risultato = inserisciTask(accendiLed, uscitaUno, 1, 1000);
Serial.print("Inserito il task di accensione con indice ");
Serial.println(risultato);
// Task 2: spengere il LED 500 chiamate dopo, e poi ogni 1000 chiamate (circa 1000 ms)
risultato = inserisciTask(spengiLed, uscitaUno, 501, 1000);
Serial.print("Inserito il task di spengimento con indice ");
Serial.println(risultato);
}
Ed ora la parte piu' facile, la loop(), che non fa proprio niente! Per evitare che si annoi, ci ho messo una stampa su Serial che ci ricorda che non sta facendo niente:
Codice: Seleziona tutto
void loop() {
while(true) {
Serial.println("Guarda mamma! Senza mani!");
delay(10000);
}
}
Infine, come promesso, tutto il codice in un solo blocco:
Codice: Seleziona tutto
// Cominciamo col definire la struttura (un insieme di campi non necessariamente omogenei ma associati) di cui ci
// serviremo per definire un Task, ossia una funzione da eseguire a intervalli regolari o no.
typedef struct {
void(*funzione)(int); // Puntatore alla funzione da chiamare, void funzione(int parametro)
int parametro; // Per ora, unico parametro (costante!) per la funzione, tra -32768 e +32767
int traQuanto; // Numero di microintervalli che devono ancora passare prima della chiamata, max 32767
int periodo; // Numero di microintervalli tra due chiamate consecutive, -1 per one-shot, max 32768
} Task;
// Ora definiamo una struttura piu' grande, che contiene le informazioni di cui abbiamo bisogno per far funzionare
// tutto il sistema, piu' abbastanza strutture per alloggiare il numero di task di cui riteniamo di aver bisogno
#define MAXTASK 10 // Cambiate quetso per estendere o ridurre la tabella di task
typedef struct {
int massimoTask = MAXTASK; // Massimo numero di task ammissibili (poi finisce lo spazio utile!)
Task tabellina[MAXTASK];
} Agenda;
volatile Agenda laMiaAgenda; // La definiamo volatile non perche' fa le uova e svolazza ma perche' puo' cambiare
// senza che il compilatore lo sappia. Questo dice al compilatore di NON azzardarsi
// ad ottimizzare gli accessi a questa variabile ma di accedere SEMPRE al valore in
// memoria (anche se dovesse essere caricato in un registro del processore).
// Questa funzione serve per inserire un task nella lista di attesa.
// Prende come argomenti:
// void (*funzione)(int) = un puntatore ad una funzione che restituisce void e prende un argomento di tipo int
// quindi deve essere dichiarata come: void funzione(int parametro) { ... } (nomi a piacere...)
// int parametro = un parametro intero, che sara' passato alla funzione al momento della chiamata
// int traQuanto = il numero di chiamate alla ISR che devono passare prima dell'esecuzione iniziale della
// funzione. Un valore di 1 causa la prima esecuzione "quasi immediatamente"
// int periodo = il numero di chiamate alla ISR dopo le quali la funzione viene rieseguita (se e' ciclica).
// un valore di -1 (o comunque negativo) significa che la funzione e' eseguita una volta sola
// Il valore int restituito contiene la posizione nella nostra tabellina in cui e' stato inserito il Task, o
// -1 se la tabellina e' gia' piena.
int inserisciTask(void (*funzione)(int), int parametro, int traQuanto, int periodo) {
int index;
for (index = 0; index < laMiaAgenda.massimoTask; index++) {
if (laMiaAgenda.tabellina[index].funzione == NULL) {
cli(); // E' cosa delicata, non possiamo permetterci che la ISR ci muva la tabellina sotto il naso
laMiaAgenda.tabellina[index].funzione = funzione;
laMiaAgenda.tabellina[index].parametro = parametro;
laMiaAgenda.tabellina[index].traQuanto = traQuanto;
laMiaAgenda.tabellina[index].periodo = periodo;
// Riabilitiamo subito gli interrupt, non appena finito!
sei();
return index;
}
}
// Se siamo arrivati fin qui vuol dire che non c'era posto in tabellina per un nuovo task
return -1;
}
// Una funzione per accendere un LED
void accendiLed(int qualeLed) {
digitalWrite(qualeLed, HIGH);
}
// E l'equivalente funzione per spengere un LED
void spengiLed(int qualeLed) {
digitalWrite(qualeLed, LOW);
}
const uint8_t uscitaUno = 13;
void setup() {
int index;
int risultato;
// Dichiariamo i nostri pin, e azzeriamo le uscite
pinMode(uscitaUno, OUTPUT);
digitalWrite(uscitaUno, LOW);
// Azzeriamo tutta la tabellina di funzioni gia' che ci siamo
for (index = 0; index < laMiaAgenda.massimoTask; index++) {
laMiaAgenda.tabellina[index].funzione = NULL;
laMiaAgenda.tabellina[index].parametro = 0;
laMiaAgenda.tabellina[index].traQuanto = 0;
laMiaAgenda.tabellina[index].periodo = -1;
}
// Timer0 e' gia' usato per la funzione millis()
// ma noi ci agganciamo da qualche parte nel mezzo
// e chiamiamo la nostra funzione
TIMSK0 |= _BV(OCIE0A); // Questa e' la maschera degli Interrupt del Timer 0, e questa strana istruzione
// _BV() e' una macro offertaci da Arduino che vuol dire "Bit Value", quindi _BV(2) = 4
// (il valore del Bit 2). E' definita come uno shift a sinistra di N posizioni:
// #define _BV(bit) (1 << (bit))
// Facciamo l'OR bita a bit del contenuto del registro con _BV(OCIE0A) per mettere
// A "1" il bit che abilita le interruzioni quando il Timer 0 raggiunge il valore di sopra
// E predisponiamo la porta seriale per vedere che cosa succede
Serial.begin(9600);
// Ora aggiungiamo due task per eseguire il famoso "Blink!"
// La temporizzazione si fa intermini di chiamate alla ISR, quindi circa ogni millisecondo.
// Task 1: accendere il LED appena possibile, e poi ogni 1000 chiamate (circa 1000 ms)
risultato = inserisciTask(accendiLed, uscitaUno, 1, 1000);
Serial.print("Inserito il task di accensione con indice ");
Serial.println(risultato);
// Task 2: spengere il LED 500 chiamate dopo, e poi ogni 1000 chiamate (circa 1000 ms)
risultato = inserisciTask(spengiLed, uscitaUno, 501, 1000);
Serial.print("Inserito il task di spengimento con indice ");
Serial.println(risultato);
}
// Qui vive la "gestione" del sistema, il codice che controlla a che task tocca di eseguire
SIGNAL(TIMER0_COMPA_vect)
{
int index;
// Esaminiamo la tabellina di task
for (index = 0; index < laMiaAgenda.massimoTask; index++) {
if (laMiaAgenda.tabellina[index].funzione != NULL) { // C'e' una funzione, non e' una locazione vuota
laMiaAgenda.tabellina[index].traQuanto--; // Sottraiamo 1 al numero di intervalli di attesa di ciascun task
// poi verifichiamo se il task e' "maturo" per l'esecuzione (ha aspettato abbastanza a lungo)
if (laMiaAgenda.tabellina[index].traQuanto <= 0) {
laMiaAgenda.tabellina[index].funzione(laMiaAgenda.tabellina[index].parametro);
// ora vediamo se si tratta di un task periodico da rieseguire in seguito o una tantum
if (laMiaAgenda.tabellina[index].periodo > 0) { // Periodico
laMiaAgenda.tabellina[index].traQuanto = laMiaAgenda.tabellina[index].periodo;
} else { // No, era un task una tantum, allora lo "cancelliamo" dalla lista scrivendo NULL come
// indirizzo della funzione da eseguire
laMiaAgenda.tabellina[index].funzione = NULL;
}
}
}
}
}
void loop() {
while(true) {
Serial.println("Guarda mamma! Senza mani!");
delay(10000);
}
}