Arduino UNO - PinChangeInterrupt "perde colpi"

Buongiorno a tutti e buona Pasqua!

Da giorni non vengo a capo ad un problema con questo codice.
Nel loop faccio contare inizialmente da 1 a 90, poi decrementando di uno ad ogni estrazione. Il ciclo gira continuamente, tramite PinChangeInterrupt lo faccio saltare alla routine che memorizza il punto in cui è stato fermato dalla pressione del pulsante; al ritorno dall'interrupt (uscita dalla funzione estrai() ) termina il ciclo, quindi legge il corrispondente numero estratto e decrementa di uno i dumeri da estrarre ancora.
dopo ogni estrazione vengono visualizzati via seriale il numero estratto, i numeri rimanenti e i numeri presenti ancora da estrarre.
Sempra funzionare tutto, ma ogni tanto "perde un colpo" e salta un numero. Perché?
Colpa del mio codice, scritto sbagliato? Limite del ATMega328P? Processore difettoso?
Io sto impazzendo!!! Qualcuno mi dia un lume! Grazie anticipatamente!!!

KMZ

//
// SuperEnalotto:
// estrae casualmente alla pressione del pulsante
// un numero tra 1 e 90 eliminandolo, fino ad
// estrarre tutti e 90 i numeri
//

#include "PinChangeInterrupt.h"
#define PULSANTE 7

volatile int i,  estratto,  numero, transit=0, biglie = 90, tabellone[91];

void setup() {
  Serial.begin(9600); // Setup comunicazione Seriale - può avere errori con l'interrupt abilitato
  pinMode(PULSANTE, INPUT);
  digitalWrite(PULSANTE, HIGH); // Abilita il pull-up interno
  delay(100);
  for (i = 1; i < 91; i++) {
    tabellone[i] = i; // riempie il tabellone in RAM
  }
  delay(100);
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(PULSANTE), estrai, FALLING); // Abilita l'interrupt sul PULSANTE
}

///////////////////////////////////////////////////////////

void loop() {
  while (biglie > 0) {
    for (numero = 1; numero < (biglie + 1); numero++) {
      __asm__ __volatile__ ("nop\n\t"); // se devi fare qualcosa, fai niente
    }
    if (transit != 0) {
      //disablePinChangeInterrupt(digitalPinToPinChangeInterrupt(PULSANTE));
      delay(100);
      estratto = tabellone[transit];
      tabellone[transit] = tabellone[biglie];
      tabellone[biglie] = 0;
      biglie--;
      transit = 0;
      if (estratto < 10) Serial.print("0");
      Serial.print(estratto);
      Serial.print(" estratto\t");
      if (biglie < 10) Serial.print("0");
      Serial.print(biglie);
      Serial.println(" biglie\n");

      for (i = 1; i < 91; i = i + 10) {
        Serial.print(tabellone[i]);
        Serial.print(" ");
        Serial.print(tabellone[i + 1]);
        Serial.print(" ");
        Serial.print(tabellone[i + 2]);
        Serial.print(" ");
        Serial.print(tabellone[i + 3]);
        Serial.print(" ");
        Serial.print(tabellone[i + 4]);
        Serial.print("\t");
        Serial.print(tabellone[i + 5]);
        Serial.print(" ");
        Serial.print(tabellone[i + 6]);
        Serial.print(" ");
        Serial.print(tabellone[i + 7]);
        Serial.print(" ");
        Serial.print(tabellone[i + 8]);
        Serial.print(" ");
        Serial.println(tabellone[i + 9]);
      }
      delay(100);
      enablePinChangeInterrupt(digitalPinToPinChangeInterrupt(PULSANTE)); // riattiva l'interrupt
    }
  }
  Serial.print("FINITI!!!");
  while(1);
}

void estrai() {
  disablePinChangeInterrupt(digitalPinToPinChangeInterrupt(PULSANTE)); // disattiva momentaneamente l'interrupt  
  transit = numero;
}

Ho provato ad aggiungere delay, a disabilitare e riabilitare temporaneamente l'interrupt (come farei in una routine in assembly), ma ogni tanto perde colpi!
Ma se il codice è sbagliato dovrebbe farlo sempre! è dovuto a rimbalzi del pulsante? Ma ci ho pure messo un condensatore in parallelo (220 nF), è forse il mio atmega andato?

NON è così che si fa il debouncing di un pulsante legato ad un interrupt ...

... ma con una rete R/C.

Guglielmo

ok, ma con 220 nF e con i 10÷50 kohm di pull-up interno al microcontrollore, come temporizzazione sono ancora meglio, nello schema usi un 22 nF con 22 kohm massimi, io dieci volte tanto, dovrebbe essere piuù che sufficiente credo.
Ma il codice è corretto?
È meglio disabilitare l'interrupt quando viene chiamato e riabilitarlo a fine funzione, o è inutile?
Perché secondo te ogni tanto sbaglia? Perché non sbaglia sempre? É solo un problema di rimbalzo secondo te? Io non ne vengo fuori!

p.s.: Ovviamente GRAZIE!

L'antirimbalzo va bene, ma perché usi l'interrupt?...

Per avere una totale casualità. L'interrupt interrompe il conteggio velocissimo.
Quello che non capisco è perché e dove sbaglia, e se sono io e quindi ll mio codice a sbagliare (è la prima volta che uso interrupt con Arduino, prima sono con PIC in assembly), o se è un limite o errore del micro!

Usa rnd(), seminando con millis() alla pressione del pulsante. Io provai così e funzionava bene, usando un classico modulo con display a 7 segmenti e MAX7219:

#include "LedControl.h"
#define DIN A0 
#define CS  A1 
#define CLK A2 
LedControl disp = LedControl(DIN,CLK,CS);
 
// Tono singolo:
void suono() {tone(13,100,random(10,50)); delay(random(5,150));}
// Multitono:
// void suono() {tone(13,random(100,200),random(10,50)); delay(random(5,150));}
 
bool premuto=false;
uint8_t numero=0;
uint8_t usciti[91];
uint8_t n=0;
bool gia_uscito=false;
 
void setup () 
{
Serial.begin(115200);
pinMode (9, INPUT_PULLUP); // Pulsante a Gnd con 100nF in parallelo.
disp.shutdown (0,false); // Wakeup MAX7219.
disp.setIntensity (0,4); // Da 0 a 15.
disp.clearDisplay (0);
usciti[0]=0;
disp.setRow  (0,7,0x0f); // t
disp.setChar (0,6,'0',false);
disp.setChar (0,5,'H',false); 
disp.setChar (0,4,'8',false); 
disp.setChar (0,3,'0',false); 
disp.setChar (0,2,'L',false); 
disp.setChar (0,1,'A',false); 
delay(1500);
disp.clearDisplay (0);
disp.setChar (0,3,48,false);
}
 
 
void loop () 
{
if(n==90)
  {
  Serial.print("FINITO");
  n=0;
  usciti[0]=0;
  }
 
if(premuto==false && !digitalRead(9)) // Se è stato premuto, prende il tempo.
  {
  premuto=true;
  }
if(premuto) suono();
if(premuto && digitalRead(9)) // Se è stato lasciato:
  {
  premuto=false;
  for (uint8_t x=0; x<20; x++)
    {
    suono();
    }
  n+=1;
  do
    {
    gia_uscito=false;
    numero=((micros()/4+analogRead(A0)+random(1,100))%90)+1; // Genera un numero casuale tra 1 e 90.
    for(uint8_t x=1; x<n; x++)
      {
      if(numero==usciti[x]) gia_uscito=true;
      }
    }
  while (gia_uscito);
  usciti[n]=numero;
  Serial.print(numero);
// posizione sui display: 76543210
  if(numero>9) disp.setChar (0,4,numero/10+48,false); 
  else disp.setChar (0,4,' ',false); 
  disp.setChar (0,3,(numero%10)+48,false); 
  Serial.print("  Usciti:"); Serial.println(n);
  while(digitalRead(9));
  }
}

Ti ringrazio dell'esempio che terrò come riserva.
Ma ora mi preme capire perché usando l'interrupt ogni tanto si inchioda!
Se mi capitasse di dover usare di nuovo l'interrupt succederebbe nuovamente, e devo capire perché: è il micro o io sbaglio codice o gestione dell'interrupt?

Che vuol dire "salta un numero"?

Allora ti spiego il funzionamento.
Inizialmente ci sono 90 biglie da estrarre, numerate dall'1 al 90. Di fatto un array intero di 90 elementi, ciascuno con all'interno il proprio numero, ossia tabellone[1] conterrà "1", tabellone[90] conterrà "90". E inizialmente il numero delle biglie è 90.
Nel loop inizialmente un ciclo for conta velocemente da 1 a 90, la pressione del pulsante fa interrompere momentaneamente il conteggio (mettiamo che sia arrivato a 23) e salta a estrai(), dove memorizza in transit il numero dove era arrivato, ossia 23.
Al ritorno dall'interrupt termina il conteggio.
All'uscita dal for, trovando transit diversa da 0 e contenente 23, esegue la visualizzazione e gestione del numero estratto, ossia:

  • legge il contenuto dell'array tabellone[23] (che all'inizio contiene appunto 23) e lo memorizza in estratto;
  • copia il contenuto dell'ultima biglia del tabellone (all'inizio la 90esima), in tal caso tabellone[90] (la cella puntata dal numero di biglie rimaste nel momento prima dell'estrazione) e lo sposta all'interno della cella estratta (in tal caso la 23esima), quindi scrive uno 0 all'interno della cella 90 e decrementa di uno il numero di biglie.
  • quindi a questo punto è stato estratto il numero 23, le biglie decrementate di uno e la biglia 90 prende il posto della biglia 23 appena estratta
  • ora il conteggio va da 1 a 89, e se venisse permato dal pulsante nuovamente sulla posizione 23, il numero estratto sarebbe il 90, e il numero 89, ultimo numero rimasto dell'array, finirebbe al posto 23esimo, ossia l posto del 90 estratto.

Per semplificare il debug e verificare che tutto proceda come sopra, uso la seriale per verificare l'output (fuori dall'interrupt).
Ad ogni estrazione mostro il numero estratto, le biglie rimaste e il contenuto di tabellone[], che al posto dei numeri estratti si riempie di 0. Se tutto funziona, alla fine devo avere 90 zeri sul tabellone e tutti e 90 i numeri estratti, invece qualche volta "salta un numero e si becca uno zero inspiegabilmente. La cosa non è sistematica e non è ripetitiva, lo fa quasi sempre ma in momenti diversi. Mediamente perde da uno a tre numeri su 90. Un paio di volte li ha azzeccati tutti senza errori.

Mah nel codice vedo varie cose che, diciamo, "non mi convincono". :wink:
Intanto l'uso dell'interrupt, come diceva anche @Datman: perché lo hai ritenuto necessario? Un ciclo che "conta" da 1 a 90 è velocissimo comunque, (considera che il loop gira migliaia di volte al secondo), secondo me non ne hai bisogno. Se ti serviva per "leggere" il valore corrente alla pressione del pulsante ed uscire dal for() sappi che esiste il comando "break"... In ogni caso basta e avanza anche la sola random().

Poi, proprio perché il loop() funziona così, in genere vedo male aggiungere dentro a questo dei cicli while: il loop() è già un ciclo, basta usare quello! Bisogna togliersi dalla mente il "normale" flusso del codice su altre piattaforme, qui a parte setup() è un loop continuo!
Poi perché tabellone[91] se deve contenere 90 numeri? Ok, è per "comodità" perché così l'indice coincide col valore, ma in fondo decrementare l'indice di 1 non è complicato... Tra l'altro tu poi nei cicli lo incrementi di 1 ad esempio "for (numero = 1; numero < (biglie + 1); numero++)" quindi comunque "qualcosa" in più la fai sempre... Tra l'altro basterebbe anche usare "<=": "for (numero = 1; numero <= biglie; numero++)"

infine la seriale in genere preferisco impostarla a velocità maggiore (115200), per limitare il più possibile eventuali latenze della seriale.

Insomma, mi pare che il tuo codice si possa semplificare molto (leggi: "diventa più facile da gestire e fare debug...) facendo una cosa di questo tipo (il codice non l'ho provato, vedi tu..):

//
// SuperEnalotto:
// estrae casualmente alla pressione del pulsante
// un numero tra 1 e 90 eliminandolo, fino ad
// estrarre tutti e 90 i numeri
//

#define PULSANTE 7

byte estratto, biglie = 90, tabellone[91];

void setup() {
  Serial.begin(115200);
  pinMode(PULSANTE, INPUT_PULLUP);
  for (byte i = 1; i <= 90; i++) {
    tabellone[i] = i; // riempie il tabellone in RAM
  }
  RealSeed(A0);
  Serial.println("SuperEnalotto");
}

///////////////////////////////////////////////////////////

void loop() {
  // Fine gioco?
  if (biglie == 0) {
    Serial.println("FINITI!!!");
    while(1);
  }
  // Il pulsante è premuto?
  if (digitalRead(PULSANTE) == LOW) {
    byte numero = random(1, biglie);
    delay(50); // questo debounce non serve se ce l'hai esterno...
    estratto = tabellone[numero];
    tabellone[numero] = tabellone[biglie];
    tabellone[biglie--] = 0;
    stampa(estratto);
    Serial.print(" estratto\t");
    stampa(biglie);
    Serial.println(" biglie\n");
    // Stampa il tabellone
    for (byte i = 1; i <= 90; i += 10) {
      for (byte j = 0; j <= 9; ++j) {
        stampa(tabellone[i+j]);
        if (j < 9) Serial.print((j!=4) ? " " : "\t");
      }
      Serial.println();
    }
    Serial.println();
  }
}

void stampa(byte num) {
  if (num < 10) Serial.print("0");
  Serial.print(num);
}

// Genera un "seed" migliore del solo analogRead(), che darebbe
// solamente 1024 possibili seed...
void RealSeed(byte pin) {
  unsigned long seed = analogRead(pin);
  randomSeed(seed);
  delay(random(50));
  seed += analogRead(pin)*1024UL;
  delay(random(50));
  seed += analogRead(pin)*1024UL*1024UL;
  randomSeed(seed);
}  

Poi io al posto tuo avrei preferito avere sulla seriale un tabellone più "reale" ossia con tutti e soli i numeri estratti, come nel vero tabellone, ma per ora l'ho lasciato come lo hai generato tu...

PS: nota alcune modifiche "non essenziali" che ti ho aggiunto ma che è utile conoscere. Vedi anche la funzione "RealSeed" che ho usato varie volte in passato per generare un seed casuale migliore di quello ottenibile dalla sola singola lettura di analogRead, che restituisce al massimo 1024 seed (ossia sequenze di numeri pseudocasuali) differenti.

Ti ringrazio per la soluzione proposta, tuttavia rimane una sequenza pseudorandom, seppure molto più ampia.
Ma la mia domanda non era come migliorare il codice, ma perché ogni tanto sbaglia!
Colpa della seriale troppo lenta? Ora provo settarla a 115200.

Mi sfugge quale sia il problema se sia "pseudorandom" o "quasi-random" (lo sono in fondo i pochi millisecondi usati per ciclare nella tua soluzione), non stiamo parlando di cifratura di documenti classificati epr cui hai bisogno di qualcosa di sofisticato. Il seed che genero è di fatto su un range molto ampio, poi segue la squenza pseudorandom dell'algoruitmo interno, non vedo il problema.

Difficile da dire, anche perché non mi è ancora chiaro cosa esattamente intendi con "sbaglia" e "salta un numero", oltre a definire "ogni tanto". Dici che "si becca uno zero", intendi che estrae uno zero? Puoi spiegare meglio, magari postando anche l'output seriale di quando "sbaglia" (racchiudi anche questo tra tag "code" ovviamente)?

E comunque sospetto che sia legato alla gestione delle variabili tramite interrupt: puoi provare anche il mio codice per favore e dirmi se con questo "sbaglia"?

EDIT: nel mio codice aggiungi alla fine queste istruzioni, che altrimenti estrae sempre mentre tieni premuto il tasto:

...
  // Il pulsante è premuto?
  if (digitalRead(PULSANTE) == LOW) {
    byte numero = random(1, biglie);
    delay(50); // questo debounce non serve se ce l'hai esterno...
    // Attende il rilascio del pulsante
    while (digitalRead(PULSANTE) == LOW)  delay(50);
    estratto = tabellone[numero];
...

Avevo interpretato che segna un numero come estratto senza che sia stato estratto veramente.
Comunque, se c'è un pulsante che viene premuto in un momento qualsiasi, credo che non ci sia casualità migliore di un numero opportunamente derivato da micros().

L'output è un po' lungo, lo accorcio dove sbaglia.

Qui commette l'errore e pesca uno 0, alla 72° estrazione, come se il contatore delle biglie non fosse stato aggiornato, o come se l'interrupt avesse pescato nel ciclo quando questo "sforava", puntando di fatto ad uno 0. Ed ora che ci penso credo sia questo l'errore! Che ne dite? Può essere? Spiegherebbe perché non capita sempre.

46 estratto 73 biglie

1 77 3 4 5 6 7 8 86 10
75 12 84 14 15 16 78 18 19 88
76 22 23 24 82 26 27 28 29 30
31 32 87 85 35 36 37 38 39 40
41 42 43 44 45 80 83 89 49 50
51 52 53 54 79 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
03 estratto 72 biglie

1 77 73 4 5 6 7 8 86 10
75 12 84 14 15 16 78 18 19 88
76 22 23 24 82 26 27 28 29 30
31 32 87 85 35 36 37 38 39 40
41 42 43 44 45 80 83 89 49 50
51 52 53 54 79 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
00 estratto 71 biglie

1 77 73 4 5 6 7 8 86 10
75 12 84 14 15 16 78 18 19 88
76 22 23 24 82 26 27 28 29 30
31 32 87 85 35 36 37 38 39 40
41 42 43 44 45 80 83 89 49 50
51 52 53 54 79 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 0 72 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
22 estratto 70 biglie

46 estratto 01 biglie

66 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
66 estratto 00 biglie

0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
FINITI!!!
EUREKA, FUNZIONA!!!
ERA QUELLO! Se l'interrupt cadeva quando il ciclo sforava, pescava oltre, ossia uno 0!

Grazie comunque a tutti per la preziosa collaborazione di cui farò tesoro!

E ancora buona Pasqua!

KMZ

Per questo ti dicevo di inserirlo nel tag "code". Per favore, modifica questo post #16, seleziona l'output e premi il tastino "<CODE/>" nella tua barra dell'editor in alto.

Appunto, infatti avevo scritto questo come "stimolo" per te:

E comunque sospetto che sia legato alla gestione delle variabili tramite interrupt

Però ripeto, è inutile secondo me impelagarsi con gli interrupt quando ci sono soluzioni più semplici. Prova a rivedere il mio codice, troverai non solo che è più corto e semplice da leggere, ma anche alcuni spunti che penso (spero) ti potranno essere utili anche in seguito...

Apposta ti ho chiesto perché usi un interrupt: un interrupt può andare a inserirsi in punti imprevedibili del programma e fare cose imprevedibili! Senza, invece, se non è indispensabile, il programma segue sicuramente il flusso previsto e, in caso di malfunzionamenti, basta studiarlo bene per trovare l'errore.