Sketch controllo encoder con interrupt - ho bisogno di chiarimenti

Ciao, ho trovato questo Sketch sulla lettura di encoder utilizzando l'interrupt nel libro “Arduino Cookbook”.

/*
RotaryEncoderInterrupt sketch
*/
const int encoderPinA = 2;
const int encoderPinB = 4;
int Pos, oldPos;
volatile int encoderPos = 0; // variables changed within interrupts are volatile
void setup(){
  pinMode(encoderPinA, INPUT);
  pinMode(encoderPinB, INPUT);
  digitalWrite(encoderPinA, HIGH);
  digitalWrite(encoderPinB, HIGH);
  Serial.begin(9600);
  attachInterrupt(0, doEncoder, FALLING); // encoder pin on interrupt 0 (pin 2)
}
void loop(){
  uint8_t oldSREG = SREG;
  cli();
  Pos = encoderPos;
  SREG = oldSREG;
  if(Pos != oldPos){
    Serial.println(Pos,DEC);
    oldPos = Pos;
  }
  delay(1000);
}

void doEncoder(){
  if (digitalRead(encoderPinA) == digitalRead(encoderPinB))
  encoderPos++; // count up if both encoder pins are the same
  else
  encoderPos--; //count down if pins are different
}

L'ho provato su Arduino MEGA e ovviamente funziona, però non mi è del tutto chiaro. In particolare non ho capito lo scopo del comando

uint8_t oldSREG = SREG;

e della funzione

cli();

Ho bisogno di capire bene come funziona perché dovrei modificarlo per fare in modo che mi incrementi o decrementi (a seconda della rotazione) una variabile “i” ogni sette impulsi ricevuti.
Il delay è per forza necessario?

Infine ho un'altra domanda: dovendo controllare con due encoder due assi i rotazione indipendenti, è sempre possibile usare l'interrupt o sono costretto a usare due schede Arduino?
Grazie e Buona Pasqua a tutti
Ezio

Il codice nel libro è ha scopo didattico per questo hanno usato un modo per spegnere e riaccendere globalmente gli interrupt. Il bit per accendere e spegnere gli interrupt si trova nel registro SREG.

cli() in sostanza spegne o disabilita gli interrupt globalmente, la riga seguente:

Pos = encoderPos;

viene eseguita con gli interrupt disabilitati.

Continuando gli interrupt vengono riabilitati senza usare sei(), ma scrivendo il valore del registro salvato in oldSREG,
in SREG. In questo caso oldSREG mantiene lo stato di SREG nel momento in cui gli interrupt sono abilitati.

Il libro dovrebbe consigliarti di usare una macro con cui creare codice eseguito atomicamente, continua leggere magari lo spiega più avanti. Se hai ancora dubbi posta pure, comunque fai una ricerca nel forum perché l'argomento è stato più volte affrontato anche in profondità.

In tutte le architetture hardware delle CPU c'è il concetto di interrupt, che trasportato nel mondo reale equivale
a chiamare qualcuno impegnato in una conversazione al telefono, la sequenza di ciò che accade è la seguente:
Il tizio senta la richiesta di interruzione e dice al tipo che sta all'altro capo del telefono; scusa ho una richiesta di interruzione, dopo di che ti presta attenzione ed esegue la ISR e al termine torna a parlare con il tipo al telefono.

Se la telefonata è di importanza vitale, tizio disabilita gli interrupt in modo che non possa essere interrotto durante la conversazione.

PS: il libro non l'ho letto.
PS1: Per comprendere nel dettaglio gli interrupt devi leggere il datasheet del microcontroller.

Grazie per la risposta, ma ho ancora dei dubbi. La variabile uint_8 ho visto che corrisponde a unsigned int da 8 bit, però mi chiedo se era necessario dichiararla proprio in questo modo.
Poi, le variabili SREG e oldSREG non sono state dichiarate perchè dovrebbero essere delle variabili di registro. Dovendo lavorare con due assi di rotazione indipendenti che ruotano contemporaneamente, mi chiedo quante di queste variabili ho a disposizione?
Grazie

Grazie per la risposta, ma ho ancora dei dubbi. La variabile uint_8 ho visto che corrisponde a unsigned int da 8 bit, però mi chiedo se era necessario dichiararla proprio in questo modo.

mmm... no uint8_t è una typedef di unsigned char. Quindi si tratta di un tipo definito nell'header della librerie avr-libc.
Si poteva anche scrivere:

unsigned char oldSREG = SREG;

Documentati sulla keyword: typedef
uint8_t oldSREG; // si legge da destra verso sinistra nel seguente modo: oldSREG è una variabile di tipi uint8_t

Poi, le variabili SREG e oldSREG non sono state dichiarate perchè dovrebbero essere delle variabili di registro. Dovendo lavorare con due assi di rotazione indipendenti che ruotano contemporaneamente, mi chiedo quante di queste variabili ho a disposizione?

Sono considerazioni fatte sulla base di; "cosa capita male". Per quanto riguarda SREG in effetti nel codice di esempio non compare dichiarazione, infatti è una macro del preprocessore fornita dall'header specifico della libreria avr-libc, questa macro si risolve in un accesso diretto ad un indirizzo di memoria in cui c'è il registro hardware SREG in base al microcontroller scelto l'indirizzo di SREG può cambiare, grazie alla macro SREG il programmatore è svincolato dal dovere accedere ad un indirizzo di memoria che dipende dal microprocessore.

ciao.

Ok, grazie. Proverò a cercare ancora qualche informazione. Non è un argomento banale per i neofiti.

MauroTec:
Continuando gli interrupt vengono riabilitati senza usare sei(), ma scrivendo il valore del registro salvato in oldSREG,
in SREG. In questo caso oldSREG mantiene lo stato di SREG nel momento in cui gli interrupt sono abilitati.

Ho notato che anche in alcuni punti del core di Arduino si usa la tecnica di riattivare gli interrupt globali riscrivendo il valore precedente del registro SREG. Non capisco il vantaggio di fare in questo modo, oltretutto si va a consumare una variabile direi per nulla. Secondo me è più logico un sei().

@Leo
Leggevo ieri nel datasheet del 328p, pag 13 mi pare, che il registro di stato non é salvato in automatico durante l'esecuzione di una ISR ma é compito del software prevederlo. Questo é quello che si fa col cSREG e SREG.
Da quanto ho letto sei() non é necessario perché al rientro fa una ISR il registro globale degli interrupt é impostato attivo.

Anzi per essere precisi, durante la chiamata il registro è disabilitato e tutti gli interrupt sono spenti, in questo modo un altro interrupt non interrompe il corrente e vengono riabilitati in uscita.
Il valore che salvi in cSREG è quello con il registro interrupt disabilitato, riscrivendolo pari pari all'uscita lo lasci disabilitato e poi è il micro al rientro dalla ISR che li riabilita globalmente.
Quello che interessa di SREG sono gli altri parametri, non quello delle interrupt.

Ultimo datasheet --> http://www.atmel.com/Images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet.pdf

A pag. 14

When an interrupt occurs, the Global Interrupt Enable I-bit is cleared and all interrupts are disabled. The user software can write logic one to the I-bit to enable nested interrupts. All enabled interrupts can then interrupt the current
interrupt routine. The I-bit is automatically set when a Return from Interrupt instruction – RETI – is executed.

e ancora

When the AVR exits from an interrupt, it will always return to the main program and execute one more instruction
before any pending interrupt is served.

e infine

Note that the Status Register is not automatically stored when entering an interrupt routine, nor restored when returning from an interrupt routine. This must be handled by software.

Comunque da quanto si legge non vengono eseguite 2 ISR di seguito ma sempre intervallate da almeno una istruzione del flusso principale. Infatti con l'istruzione sei() prima di un qualsiasi interrupt viene prima eseguita l'istruzione successiva.

When using the SEI instruction to enable interrupts, the instruction following SEI will be executed before any pending interrupts, as shown in this example.

Il datasheet è una miniera d'oro.

@PaoloP

Ci pensa il C/C++ a fare tutto, quella doc è fondamentale per chi programma in asm, utile per chi programma in C.

Forse non è chiaro ma il mostrato è equivalente a quello di seguito:

void loop(){

  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      Pos = encoderPos;
  }
  if(Pos != oldPos) {
    Serial.println(Pos,DEC);
    oldPos = Pos;
  }
  delay(1000);
}

Per rendere atomica (cioè non interrompibile) una porzione di codice si usa quella macro oppure la soluzione mostrata nel libro, che didatticamente ha maggiore valore di usare la macro ATOMIC...

Ciao.

Scusa Paolo ma quelli che citi tu sono i comportamenti da osservare all'interno di una ISR. Io avevo chiesto il motivo perché invece viene fatto una riscrittura del vecchio valore del registro SREG per riattivare gli interrupt globali.
Ma se io disabilito gli interrupt globali, nessun interrupt viene chiamato, quindi nessuna istruzione RETI viene eseguita. Quindi nessuno mi tocca il bit SREG_I.

Il valore che salvi in cSREG è quello con il registro interrupt disabilitato, riscrivendolo pari pari all'uscita lo lasci disabilitato e poi è il micro al rientro dalla ISR che li riabilita globalmente.

Sì, ma come detto io stavo parlando di una condizione esterna ad una ISR. Cioè salvare lo stato del registro con interrupt globali abilitati, non disabilitati.
Difatti, la macro ISR istruisce il codice a mettere il salvataggio del registro

Meglio allora usare le macro predefinite, come ha suggerito Mauro, così siamo sicuri che il compilatore farà esattamente quel che si vuole.

http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html

Siete tutti molto preparati, ma ci capisco sempre meno... :~
C'è qualche testo o dispensa che possa aiutarmi comprendere quello che state dicendo?
Ho solo una conoscenza di base del C

Dal link che ho messo accedi a tutta la documentazione relativa alla gestione degli interrupt (e non solo) da parte del compilatore avr-gcc, quella è la tua bibbia.

@leo72
Non ho indagato di recente sul perché si usi salvare SREG, quello che al tempo lessi nella maillist di avr-libc e più o meno riassunto di seguito:

Serviva un modo per rendere atomica una porzione di codice, salvando SREG si risolve ma tutti gli RTOS usano delle macro per architettura per rendere atomica una porzione di codice e ci voleva qualcosa di semplice come START_ATOMIC(), END_ATOMIC(), l'obbiettivo alla fine la soluzione che ho mostrato è stata scelta.

Se apri il file atomic.h della librerie avr-libc trovi

#define ATOMIC_RESTORESTATE uint8_t sreg_save \
	__attribute__((__cleanup__(__iRestore))) = SREG

L'obbiettivo quindi non è disabilitare a prescindere gli interrupt ma è salvare lo stato, disabilitare gli interrupt eseguire codice protetto e poi rendere attuale lo stato salvato in precedenza.
Capisci che se hai poche righe di codice sai che gli interrupt sono abilitati e quindi chiami cli(), ma potrebbero anche essere disabilitati, ci potrebbe essere qualche flag che serve, e potresti anche avere 20 o più moduli c,
potresti usare RTOS che internamente necessitano di eseguire codice atomico.

@ziooetzi
In effetti siamo andati in profondità e non ci aspettavamo proprio che ci seguissi nel dettaglio.
Sei all'inizio dell'apprendimento ed è naturale ritrovarsi a usare codice pur non sapendo cosa accade
dietro le quinte. Di guide in rete ne trovi in grande quantità, il problema è che non sai cosa cercare e se ti dessi
dei riferimenti finiresti per studiare quello che al momento non ti serve.
Ti serve una infarinatura sull'architettura Hardware dei microcontroller o CPU, prestando più attenzione
al meccanismo degli interrupt che è il motivo della tua domanda. Ti servono i concetti chiave, senza i quali non riesci a capire l'esempio del tizio al telefono.

A questo livello di apprendimento puoi chiedere e documentarti sui concetti chiave, andare oltre non è proficuo.
Inoltre da quello che hai scritto in merito a "uint8_t" deduco che ti serve studiare un poco meglio il C/C++, anzi le basi di C devi farle tue, capisci che non puoi confondere uint8_t, int16_t, byte, bool ecc per variabili.

Quindi affronta il C dalle basi.

Quante parole chiave ha il C++?
non mi ricordo 37 forse.

Che differenza c'è tra C e C++?
Sono molto simili, il C++ è una evoluzione del C

Quali sono le parole chiave?
auto, bool, char, if, while, for, switch ecc

Che tipo di linguaggio è?
Compilato, strutturato, procedurale, orientato agli oggetti (C++ solo)

Ciao.

Io generalmente agisco sui singoli bit dei registri, non manipolo quasi mai il registro per intero.
Difatti io disattivo gli interrupt così:

SREG &= ~(1<<SREG_I);

e li riattivo così:

SREG |= (1<<SREG_I);

In questo modo tocco solo il bit degli interrupt globali. Che poi è identico a fare cli() e sei().

Ora, c'è da fare un piccolo ragionamento.
SE, e dico SE, il bit SREG_I degli interrupt globali NON è settato, con il metodo del cli() e sei() io riattivi gli interrupt anche se questi erano disattivati. E forse è questo il motivo per cui viene usato il giochino del rimettere il valore di SREG.
PERO', stiamo lavorando su Arduino, ed il core di Arduino attiva gli interrupt globali SEMPRE perché da essi dipendono componenti fondamentali per il suo funzionamento. Come la seriale. O come delay(). Quindi è un problema che si pone il giusto, se ragioniamo di Arduino. Se invece parliamo di un altro ambiente, allora le cose possono cambiare.

PERO', stiamo lavorando su Arduino, ed il core di Arduino attiva gli interrupt globali SEMPRE perché da essi dipendono componenti fondamentali per il suo funzionamento. Come la seriale. O come delay(). Quindi è un problema che si pone il giusto, se ragioniamo di Arduino. Se invece parliamo di un altro ambiente, allora le cose possono cambiare.

mmmm...si e no, più che si, in ogni caso non ti dimenticare di questa cosa, potrebbe esserti di aiuto.

Anche per arduino ci sono RTOS e lo switch context con gli interrupt di abilitati non si fa.
Il C è statico, tuttavia non è detto che non si possa creare codice che si comporta dinamicamente e qui conviene
salvare SREG o usare la macro. Pensa ad un array di puntatori a funzione, ogni funzione può modificare i puntatori, ne viene fuori che in base alla evoluzione dinamica del codice si chiamerà una funzione anziché un'altra e chi mi dice che la funzione che mi ha chiamato ha disabilitato gli interrupt a fatto qualcosa e poi mi ha chiamato e io che sono la funzione chiamata ho porzioni di codice che devo eseguire atomicamente e non posso disabilitare e riabilitare gli interrupt devo salvare lo stato, altrimenti alla funzione che mi ha chiamato gli restituisco il controllo con gli interrupt abilitati. ps: ho preso le sembianze di una funzione, terrificante.

Forse messa in questi termini ti fa più simpatia salvare lo stato:
Onde evitare che il programma costato mal di testa si comporti correttamente ora si ora no, ti metti al sicuro e salvi sempre lo stato, cioè la prendi come buona e saggia abitudine, in questo modo quando ti ritroverai potenzialmente nelle condizioni di introdurre side effect, la ATOMIC.... ti salva.

Ciao.

Senz'altro. Difatti ho chiuso il ragionamento proprio dicendo che lavorando su Arduino la cosa è indifferente perché so a priori lo stato del bit SREG_I, è 1 non si scappa.
Se invece avessi un RTOS o simil-tale allora la cosa cambia perché appunto non so cosa sta succedendo non tanto nei task ma nello scheduler, che usa appunto un interrupt come sys-tick. Se malauguratamente vado a riattivare gli interrupt globali quando in realtà dovrebbero essere disattivati, lo scheduler potrebbe ripartire e fare il cambio di un task quando in realtà non doveva farlo.

Vi state concentrando su un bit del registro quando gli altri sono più importanti.
E' giusto il discorso di Mauro.
Il datasheet del 328P inoltre dice che in caso di chiamata il registro di stato (tutto, non solo il bit I) DEVE essere salvato.
Questo perché all'interno dell'interrupt potrebbero essere fatti dei calcoli che cambiano la configurazione del registro e tornando da un'interrupt col registro cambiato il flusso di operazioni successive può dare risultati imprevedibili.
Copiare e incollare l'SREG non serve per riabilitare gli interrupt globali ma per salvare lo stato precedente alla chiamata all'interrupt e per me è più importante.
Parlo naturalmente per il solo Arduino, riferendomi al datasheet del 328P.

Inoltre secondo me usare il comando

SREG &= ~(1<<SREG_I);

al posto di

cli();

esegue l'impostazione di SREG più lentamente.
Cli() viene tradotto in una unica istruzione asm. bisognerebbe vedere in quante istruzioni è tradotto l'assegnamento al registro.

Come fa notare PaoloP ci possono essere altri motivi che ci portano a salvare SREG. La domanda è:
Siamo davvero così infallibili da ricordarci cosa abbiamo scritto nella funzione x tre mesi fa, io no.
Io l'ho scritta non la guardo da tre mesi e ora mi viene in mente che posso usarla nel codice di esempio.

void loop(){
  uint8_t oldSREG = SREG;
  cli();
  fuctionVeryOld();
  Pos = encoderPos;      // ops doveva essere eseguita atomicamente. 
  SREG = oldSREG;
  if(Pos != oldPos){
    Serial.println(Pos,DEC);
    oldPos = Pos;
  }
  delay(1000);
}

Se functionVeryOld disabilita con cli() ed esegue codice e poi prima di uscire riabilita gli interrupt con sei()
si verifica il problema. Mi ritrovo ad eseguire codice con gli iterrupt abilitati quando e chiaro che io li ho disabilitati, chi li riabilita?

Qui è semplice capire che fuctionVeryOld() potrebbe essere la causa, e allora vado a vedere il codice sorgente,
nel caso io non l'avessi il codice sorgente quella funzione è da considerare buggata.

Siamo davvero in grado si seguire l'evoluzione del codice a mente e ricordarci tutto?
Io non me la sento di rischiare e allora uso la macro, che salva SREG e lascia l'ambiente nello stesso modo in cui
lo ha ricevuto.

Altro motivo per usare la macro:
Salvo SREG, cli() e mi dimentico di ripristinare lo stato di SREG.
Con la macro la dimenticanza non può proprio verificarsi e al massimo mi scordo a chiudere } e
il compilatore me lo segnala.

Quindi io raccomando di usare la macro anche in ambiente arduino. Come detto nel libro l'esempio è
didattico e ci sta mostrare i passaggi.

Ciao.

PaoloP:
Vi state concentrando su un bit del registro quando gli altri sono più importanti.

E quali, scusa? Il flag negativo? O quello di riporto? O lo zero? Naaa.. :wink:
Di importante per la gestione degli interrupt nel registro di stato c'è solo il bit SREG_I, fidati. Tutto il resto serve alla CPU durante l'esecuzione dei calcoli.

leo72:
Di importante per la gestione degli interrupt nel registro di stato c'è solo il bit SREG_I, fidati. Tutto il resto serve alla CPU durante l'esecuzione dei calcoli.

E dici niente...
Tu esegui un calcolo in cui parte del risultato è nel registro di stato.
Esegui un interrupt. Il programma principale viene interrotto, viene eseguita una ISR che esegue calcoli che cambia alcuni bit del registro di stato, torni al programma principale con una configurazione diversa del registro di stato.
Il tuo programma prosegue, ma puoi essere sicuro che l'esecuzione e i risultati siano corretti?

Se invece salvi e ripristini il registro alla fine della ISR sai con certezza che il programma prosegue con la stessa configurazione lasciata quando ha dovuto eseguire la ISR.

Paolo, tu continui a parlare di un problema quando io ho iniziato a discutere di un altro :wink: :wink:
Io stavo parlando di istruzioni cli() e sei() usate in un programma, tu invece continui a fare l'esempio di una ISR :wink:
Nella ISR ci pensa il codice a salvare lo stato del registro ed a ripristinarlo in uscita. Cli() e sei() sono invece usate per rendere un blocco di codice temporaneamente atomico, e non mi interessa dello stato di quei flag perché non sto andando ad eseguire un task in un ambiente multitasking, sto solo armeggiando con qualcosa che non deve essere interrotta da un interrupt per qualche microsecondo (es. configurare un timer). :wink:

EDIT:
si fa solo per parlare, non voglio che tu pensi che sto criticando tutto quel che dici, mi piace solo scambiare opinioni per capire i pro ed i contro. :wink: :wink: