Spiegazione su vettore di struct detto anche Tabellina

Progetti Arduino
Rispondi
RoccoCostruzioni
Messaggi: 51
Iscritto il: mer 9 set 2020, 21:16

Spiegazione su vettore di struct detto anche Tabellina

Messaggio da RoccoCostruzioni »

Salve a tutti, la questione "Tabellina" è stata sollevata da un utente in un mio post che pare eviti il bloccaggio del loop con i delay e sinceramente vorrei approfondire la cosa, chi è pratico su questo modo di programmare?
Avatar utente
pgv
Messaggi: 484
Iscritto il: gio 17 set 2020, 13:16
Località: Ginevra

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da pgv »

Cambiamogli nome poverino, lui e' uno Scheduler Cooperativo... L'implementazione richiede di creare una Tabellina di task da eseguire.

Si tratta di un sistema operativo che puo' andare dal semplicissimo al relativamente complicato (i primi Macintosh, con le CPU serie 680x0, giravano un sistema operativo basato su questo principio) e in cui ci sono:

  1. una serie di "task" (compiti, operazioni) da svolgere, sia periodicamente (quindi ogni tot millisecondi), sia "una tantum". Un task e' caratterizzato da:
    una funzione che deve essere chiamata, opzionalmente uno o piu' parametri (per risparmiare sul numero delle funzioni, per esempio se dobbiamo attivare o disattivare una uscita dell'Arduino non scriveremo due funzioni per ogni pin ma solamente una accendiPin e una spengiPin e poi passeremo loro il numero di pin su cui debbono agire)
    • una funzione che deve essere chiamata, opzionalmente uno o piu' parametri (per risparmiare sul numero delle funzioni, per esempio se dobbiamo attivare o disattivare una uscita dell'Arduino non scriveremo due funzioni per ogni pin ma solamente una accendiPin e una spengiPin e poi passeremo loro il numero di pin su cui debbono agire);
    • una "fase", ossia il ritardo tra il momento in cui immettiamo la funzione nella nostra lista e l'istante in cui vogliamo sia eseguita. Questo ci permette, per esempio, di far eseguire una certa funzione esattamente n millisecondi dal momento in cui, per l'arrivo di un segnale esterno, andiamo a metterla nella lista di esecuzione. Serve anche a sincronizzare tra di loro varie funzioni, per esempio per accendere e spengere un LED con un certo periodo e un certo duty cycle (tempo acceso/periodo totale);
    • un "periodo", ossia il tempo che deve passare tra due esecuzioni della stessa funzione. Tipicamente ci si mettera' -1 se si vuole indicare che la funzione deve eseguire una volta sola;
    • opzionalmente, uno o piu' parametri da passare alla funzione da eseguire. Questo permette di "riciclare" le funzioni quando devono eseguire lo stesso compito ma, per esempio, su dei pin di uscita diversi;
  • una funzione che "gestisce il traffico", ossia determina a quale task tocca essere eseguito (o se non occorre eseguirne nessuno a questa mandata). Questa funzione deve essere eseguita ad intervalli regolari, che ci danno la granularita' temporale. Se possiamo chiamarla ogni millisecondo, potremo definire i tempi di esecuzione dei task con la precisione del millisecondo. Quando insegnavo agli studenti di Informatica del II anno di Universita', avevamo dei microcontrollori della famiglia 8051, e per la particolare lentezza eravamo limitati ad un minimo di 10 millisecondi. Con un Arduino a 16 MHz, un millisecondo dovrebbe andar benissimo per dei task sufficientemente brevi.
Possiamo immaginare la lista dei task come l'agenda di un medico, e la funzione di smistamento come la segretaria che controlla la sala d'attesa e fa entrare il paziente che ha l'appuntamento per una certa ora. E come nel caso del medico, se un paziente richiede piu' tempo di quello disponibile, il paziente successivo aspetta un pochino ma alla fine passa.

Il seguito alla prossima puntata, con esempi di codice...
Guido
Messaggi: 1443
Iscritto il: dom 18 mar 2018, 20:21

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da Guido »

Se ho ben capito facendo un esempio pratico io mi trovavo nella situazione di dover ricorrere a questa tecnica di programmazione quando realizzavo programmi con tante voci di menu' : basti pensare per es alle voci di menu' di un semplice Word processor quale il Write di windows. Lo stato di attivo/disattivo delle voci di menu' deve essere controllato istante dopo istante in una apposita funzione che deve essere chiamata ad intervalli regolari, qualsiasi cosa stia facendo il programma. Se per es. il word processor non ha alcun testo nella finestra tutte le voci dei menu' che attiverebbero funzioni per manipolare detto testo devono essere disattivate quali per es "Stampa" "salva" etc. Se c'e' il testo ma non c'e' alcun testo selezionato devono essere disattivate le voci "Copia", "Taglia" etc Sarebbe pazzesco controllare tutte queste cose se non sono tutte raggruppate in una unica funzione.
Avatar utente
pgv
Messaggi: 484
Iscritto il: gio 17 set 2020, 13:16
Località: Ginevra

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da pgv »

Esattamente. O ancora, un menu' al quale si possono aggiungere e dal quale si possono togliere opzioni. O come il famoso esercizio di muovere una mano da sinistra a destra e l'altra avanti e indietro con periodi diversi e non sottomultipli l'uno dell'altro. Difficilissimo...
Sto lavorando ad una realizzazione sw esemplificativa, ma non mi va di pubblicare sw non testato e negli ultimi giorni una libreria di Arduino mi ha reso la vita difficile (fino a che non ho modificato una funzione che faceva dei realloc() in 2 kbyte di RAM totale del mio Nano...
Scriviamo tutti sul diario: "NON SI GIOCA CON L'ALLOCAZIONE DINAMICA DELLA MEMORIA SU UN ARDUINO". Semplicemente perche' di memoria ce n'e' pochissima e la garbage collection latita.
Domani spero di presentare la seconda puntata con le basi construttive ed un esempio semplice semplice (ma che funziona per davvero), esempio che sarete invitati a copiare per giocherellarci a piacimento.
Avatar utente
pgv
Messaggi: 484
Iscritto il: gio 17 set 2020, 13:16
Località: Ginevra

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da pgv »

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:
  1. 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:
  1. 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);
  }
}
Guido
Messaggi: 1443
Iscritto il: dom 18 mar 2018, 20:21

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da Guido »

Aiuto !! Dacci un mese per digerire il tutto :D
Avatar utente
pgv
Messaggi: 484
Iscritto il: gio 17 set 2020, 13:16
Località: Ginevra

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da pgv »

Se ci sono domande, fatele pure. Sembra complicato ma in realta' e' molto piu' semplice di cosi'...
Avatar utente
pgv
Messaggi: 484
Iscritto il: gio 17 set 2020, 13:16
Località: Ginevra

Re: Spiegazione su vettore di struct detto anche Tabellina

Messaggio da pgv »

Per la seconda puntata, un compitino facile facile... Aggiungete un secondo LED che, lui, deve cominciare a lampeggiare dopo 100 chiamate dell'ISR, stare acceso 200 chiamate, e ripetere lo stesso ciclo ogni 347 chiamate. Sono stato accusato di malvagita' in passato dagli studenti, e confesso che non sono tutte invenzioni, perche' si da' il caso che 347 sia un numero primo, e quindi 347 (il periodo del secondo LED) e 1000 (il periodo del primo LED) essendo (per forza, dato che 1000 non e' un multiplo di 347) sono primi tra loro e i due LED ritornano in fase una volta ogni 347,000 chiamate dell'ISR (347 per 1000), ossia ogni 5 minuti e 39 secondi circa... Farlo a botte di delay() e' "difficilotto".

Posto anche il nuovo codice per la funzione setup() (che e' la sola che ha bisogno di cambiare):

Codice: Seleziona tutto

// Dichiariamo anche la seconda uscita gia' che ci siamo, dicono che porti fortuna
const uint8_t uscitaUno = 13;
const uint8_t uscitaDue = 12;

void setup() {
  int index;
  int risultato;
  
  // Dichiariamo i nostri pin, e azzeriamo le uscite
  pinMode(uscitaUno, OUTPUT);
  digitalWrite(uscitaUno, LOW);
  pinMode(uscitaDue, OUTPUT);
  digitalWrite(uscitaDue, LOW);

// qui c'e' un bel buco grosso, visto che non cambiava niente non ho copiato il codice esistente...

  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 1 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 1 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);
  // *** IL VECCHIO CODICE FINIVA QUI ***//
  // *** IL NUOVO CODICE COMINCIA QUI ***//
  // Task 3: accendere il LED 2 dopo 100 chiamate, e poi ogni 347 chiamate
  //         da notare che 347 e' un numero primo, e quindi i due LED saranno in fase ogni 347,000 chiamate!
  risultato = inserisciTask(accendiLed, uscitaDue, 100, 347);
  Serial.print("Inserito il task di accensione con indice ");
  Serial.println(risultato);
  // Task 4: spengere il LED 200 chiamate dopo, e poi ogni 347 chiamate
  risultato = inserisciTask(spengiLed, uscitaDue, 300, 347);
  Serial.print("Inserito il task di spengimento con indice ");
  Serial.println(risultato);
  // *** IL NUOVO CODICE FINISCE QUI ***//
}
Rispondi