Come funzionano gli interrupt su Arduino Uno e schede simili?


11

Spiegare come funzionano gli interrupt su Arduino Uno e le relative schede utilizzando il processore ATmega328P. Schede come:

  • Uno
  • Mini
  • nano
  • Pro Mini
  • Lilypad

In particolare si prega di discutere:

  • Per cosa usare gli interrupt
  • Come scrivere una routine di servizio di interruzione (ISR)
  • Problemi di tempistica
  • Sezioni critiche
  • Accesso atomico ai dati

Nota: questa è una domanda di riferimento .

Risposte:


25

TL; DR:

Quando si scrive una routine di servizio di interruzione (ISR):

  • Tienilo breve
  • Non usare delay ()
  • Non eseguire stampe seriali
  • Variabili make condivisi con il codice principale volatili
  • Le variabili condivise con il codice principale potrebbero dover essere protette da "sezioni critiche" (vedi sotto)
  • Non tentare di attivare o disattivare gli interrupt

Cosa sono gli interrupt?

La maggior parte dei processori ha interruzioni. Gli interrupt ti consentono di rispondere ad eventi "esterni" mentre fai qualcos'altro. Ad esempio, se stai cucinando la cena puoi mettere le patate a cuocere per 20 minuti. Invece di fissare l'orologio per 20 minuti, potresti impostare un timer, quindi guardare la TV. Quando il timer suona "interrompi" la visione della TV per fare qualcosa con le patate.


Esempio di interrupt

const byte LED = 13;
const byte SWITCH = 2;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Questo esempio mostra come, anche se il loop principale non sta facendo nulla, è possibile accendere o spegnere il LED sul pin 13, se si preme l'interruttore sul pin D2.

Per provare questo, basta collegare un filo (o interruttore) tra D2 e ​​terra. Il pullup interno (abilitato nella configurazione) forza normalmente il pin HIGH. Una volta messo a terra, diventa BASSO. La modifica del pin viene rilevata da un interruzione CHANGE, che provoca la chiamata alla routine di servizio di interruzione (ISR).

In un esempio più complicato, il circuito principale potrebbe fare qualcosa di utile, come rilevare le letture della temperatura, e consentire al gestore di interruzione di rilevare un pulsante premuto.


Conversione di numeri pin in numeri di interruzione

Per semplificare la conversione di numeri vettoriali di interrupt in numeri di pin, è possibile chiamare la funzione digitalPinToInterrupt(), passando un numero di pin. Restituisce il numero di interruzione appropriato o NOT_AN_INTERRUPT(-1).

Ad esempio, su Uno, il pin D2 sulla scheda è l'interruzione 0 (INT0_vect dalla tabella seguente).

Quindi queste due linee hanno lo stesso effetto:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

Tuttavia, il secondo è più facile da leggere e più portatile per diversi tipi di Arduino.


Interrupt disponibili

Di seguito è riportato un elenco di interruzioni, in ordine di priorità, per Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

I nomi interni (che è possibile utilizzare per impostare i callback ISR) sono tra parentesi.

Avvertenza: se si scrive in modo errato il nome del vettore di interrupt, anche semplicemente sbagliando la maiuscola (cosa facile da fare), la routine di interrupt non verrà chiamata e non si verificherà un errore del compilatore.


Ragioni per usare gli interrupt

I motivi principali per cui potresti utilizzare gli interrupt sono:

  • Per rilevare la modifica dei pin (ad es. Encoder rotativi, pressione dei pulsanti)
  • Timer watchdog (es. Se non succede nulla dopo 8 secondi, interrompimi)
  • Interruzioni timer: utilizzate per confrontare / traboccare i timer
  • Trasferimento di dati SPI
  • Trasferimento di dati I2C
  • Trasferimento dati USART
  • Conversioni ADC (da analogico a digitale)
  • EEPROM pronta per l'uso
  • Memoria flash pronta

I "trasferimenti di dati" possono essere utilizzati per consentire a un programma di fare qualcos'altro mentre i dati vengono inviati o ricevuti sulla porta seriale, porta SPI o I2C.

Riattiva il processore

Per riattivare il processore è anche possibile utilizzare interrupt esterni, interruzioni di cambio pin e interruzione timer watchdog. Questo può essere molto utile, poiché in modalità di sospensione il processore può essere configurato per consumare molta meno energia (ad esempio circa 10 microampere). Un allarme di salita, discesa o basso livello può essere utilizzato per riattivare un gadget (ad es. Se si preme un pulsante su di esso), oppure un interrupt "timer watchdog" potrebbe svegliarlo periodicamente (ad es. Per controllare l'ora o temperatura).

Gli interrupt di cambio pin potrebbero essere usati per riattivare il processore se si preme un tasto su una tastiera o simili.

Il processore può anche essere risvegliato da un interruzione del timer (ad es. Un timer che raggiunge un certo valore o trabocca) e alcuni altri eventi, come un messaggio I2C in arrivo.


Abilitazione / disabilitazione degli interrupt

L'interrupt "reset" non può essere disabilitato. Tuttavia, gli altri interrupt possono essere temporaneamente disabilitati cancellando il flag di interrupt globale.

Abilita interrupt

È possibile abilitare gli interrupt con la chiamata di funzione "interrupt" o "sei" in questo modo:

interrupts ();  // or ...
sei ();         // set interrupts flag

Disabilita gli interrupt

Se è necessario disabilitare gli interrupt, è possibile "cancellare" il flag di interrupt globale in questo modo:

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

Entrambi i metodi hanno lo stesso effetto, usando interrupts/ noInterruptsè un po 'più facile ricordare in che modo si trovano.

L'impostazione predefinita in Arduino è l'attivazione degli interrupt. Non disabilitarli per lunghi periodi o cose come i timer non funzioneranno correttamente.

Perché disabilitare gli interrupt?

Potrebbero esserci pezzi di codice time-critical che non si desidera vengano interrotti, ad esempio da un interrupt del timer.

Inoltre, se i campi multi-byte vengono aggiornati da un ISR, potrebbe essere necessario disabilitare gli interrupt in modo da ottenere i dati "atomicamente". Altrimenti un ISR può essere aggiornato dall'ISR mentre stai leggendo l'altro.

Per esempio:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

La disattivazione temporanea degli interrupt assicura che isrCounter (un contatore impostato all'interno di un ISR) non cambi mentre stiamo ottenendo il suo valore.

Avvertenza: se non si è sicuri che gli interrupt siano già attivi o meno, è necessario salvare lo stato corrente e ripristinarlo in seguito. Ad esempio, il codice della funzione millis () fa questo:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

Notare che le righe indicate salvano l'attuale SREG (registro di stato) che include il flag di interrupt. Dopo aver ottenuto il valore del timer (che è lungo 4 byte), riportiamo il registro di stato com'era.


Suggerimenti

Nomi delle funzioni

Le funzioni cli/ seie il registro SREG sono specifici per i processori AVR. Se si utilizzano altri processori come quelli ARM, le funzioni potrebbero essere leggermente diverse.

Disabilitazione globale vs disabilitazione di un interrupt

Se lo si utilizza, cli()si disabilitano tutti gli interrupt (inclusi gli interrupt del timer, gli interrupt seriali, ecc.).

Tuttavia, se si desidera solo disabilitare un particolare interrupt, è necessario cancellare il flag di abilitazione dell'interrupt per quella particolare sorgente di interrupt. Ad esempio, per interrupt esterni, chiamare detachInterrupt().


Cos'è la priorità di interruzione?

Poiché vi sono 25 interrupt (diversi dal reset), è possibile che si verifichi più di un evento di interruzione alla volta, o almeno, prima che venga elaborato quello precedente. Inoltre, potrebbe verificarsi un evento di interruzione mentre gli interrupt sono disabilitati.

L'ordine di priorità è la sequenza in cui il processore verifica gli eventi di interruzione. Più in alto nell'elenco, maggiore è la priorità. Pertanto, ad esempio, una richiesta di interruzione esterna 0 (pin D2) verrebbe gestita prima della richiesta di interruzione esterna 1 (pin D3).


Possono verificarsi interruzioni mentre le interruzioni sono disabilitate?

Gli eventi di interruzione (ovvero la notifica dell'evento) possono verificarsi in qualsiasi momento e la maggior parte viene ricordata impostando un flag di "evento di interruzione" all'interno del processore. Se gli interrupt sono disabilitati, quell'interrupt verrà gestito quando saranno nuovamente abilitati, in ordine di priorità.


Come usi gli interrupt?

  • Scrivi un ISR (routine di servizio di interrupt). Questo viene chiamato quando si verifica l'interrupt.
  • Dite al processore quando volete che l'interrupt si attivi.

Scrivere un ISR

Le routine di servizio di interruzione sono funzioni senza argomenti. Alcune librerie Arduino sono progettate per chiamare le tue funzioni, quindi devi semplicemente fornire una funzione ordinaria (come negli esempi sopra), ad es.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

Tuttavia, se una libreria non ha già fornito un "hook" a un ISR, puoi crearne uno tuo, in questo modo:

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

In questo caso si utilizza la macro "ISR" e si fornisce il nome del vettore di interrupt rilevante (dalla tabella precedente). In questo caso l'ISR sta gestendo il completamento di un trasferimento SPI. (Nota, alcuni vecchi codici usano SIGNAL invece di ISR, tuttavia SIGNAL è deprecato).

Collegamento di un ISR a un interrupt

Per gli interrupt già gestiti dalle librerie, basta usare l'interfaccia documentata. Per esempio:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

In questo caso, la libreria I2C è progettata per gestire internamente i byte I2C in entrata, quindi chiamare la funzione fornita alla fine del flusso di dati in entrata. In questo caso, ricezioneEvent non è strettamente un ISR (ha un argomento) ma viene chiamato da un ISR integrato.

Un altro esempio è l'interrupt "pin esterno".

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

In questo caso la funzione attachInterrupt aggiunge l'interruttore di funzione Pressato a una tabella interna e configura inoltre i flag di interruzione appropriati nel processore.

Configurazione del processore per gestire un interrupt

Il passo successivo, una volta che hai un ISR, è dire al processore che vuoi che questa particolare condizione generi un interrupt.

Ad esempio, per External Interrupt 0 (l'interrupt D2) potresti fare qualcosa del genere:

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

Più leggibile sarebbe usare i nomi definiti, in questo modo:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

EICRA (registro di controllo di interrupt esterno A) verrebbe impostato in base a questa tabella dal foglio dati Atmega328. Ciò definisce il tipo esatto di interrupt desiderato:

  • 0: il livello basso di INT0 genera una richiesta di interruzione (interruzione BASSA).
  • 1: qualsiasi modifica logica su INT0 genera una richiesta di interruzione (CHANGE interrupt).
  • 2: il fronte di discesa di INT0 genera una richiesta di interruzione (interruzione CADUTA).
  • 3: il fronte di salita di INT0 genera una richiesta di interruzione (interruzione RISING).

EIMSK (External Mask Mask Register) abilita effettivamente l'interrupt.

Fortunatamente non è necessario ricordare quei numeri perché attachInterrupt lo fa per te. Tuttavia, questo è ciò che sta realmente accadendo e per altri interrupt potrebbe essere necessario impostare "manualmente" i flag di interrupt.


ISR di basso livello vs. ISR di biblioteca

Per semplificarti la vita, alcuni gestori di interrupt comuni si trovano effettivamente all'interno del codice della libreria (ad esempio INT0_vect e INT1_vect) e quindi viene fornita un'interfaccia più intuitiva (ad esempio attachInterrupt). Quello che effettivamente attachInterrupt è salvare l'indirizzo del gestore di interrupt desiderato in una variabile, quindi chiamarlo da INT0_vect / INT1_vect quando necessario. Imposta inoltre i flag di registro appropriati per chiamare il gestore quando richiesto.


Gli ISR ​​possono essere interrotti?

In breve, no, a meno che tu non voglia che lo siano.

Quando viene inserito un ISR, gli interrupt sono disabilitati . Naturalmente devono essere stati abilitati in primo luogo, altrimenti l'ISR non verrebbe inserito. Tuttavia, per evitare che un ISR stesso venga interrotto, il processore disattiva gli interrupt.

Quando esce un ISR, gli interrupt vengono nuovamente abilitati . Il compilatore genera anche codice all'interno di un ISR per salvare registri e flag di stato, in modo che qualunque cosa stiate facendo quando si è verificato l'interrupt non sarà influenzata.

Tuttavia, è possibile attivare gli interrupt all'interno di un ISR se assolutamente è necessario, ad es.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

Normalmente avresti bisogno di una ragione abbastanza buona per farlo, dato che un altro interruzione ora potrebbe comportare una chiamata ricorsiva a pinChange, con risultati molto probabilmente indesiderabili.


Quanto tempo ci vuole per eseguire un ISR?

Secondo il foglio dati, il tempo minimo necessario per eseguire l'interruzione è di 4 cicli di clock (per spingere il contatore del programma corrente nello stack) seguito dal codice che ora viene eseguito nella posizione del vettore di interruzione. Questo di solito contiene un salto nel punto in cui si trova realmente la routine di interrupt, ovvero altri 3 cicli. L'esame del codice prodotto dal compilatore mostra che l'esecuzione di un ISR con la dichiarazione "ISR" può richiedere circa 2,625 µs, più qualunque cosa faccia il codice stesso. L'importo esatto dipende da quanti registri devono essere salvati e ripristinati. La quantità minima sarebbe di 1,1875 µs.

Gli interrupt esterni (dove usi attachInterrupt) fanno un po 'di più e prendono circa 5,125 µs in totale (funzionando con un clock a 16 MHz).


Quanto tempo prima che il processore inizi a inserire un ISR?

Questo varia leggermente. Le cifre sopra citate sono quelle ideali in cui l'interrupt viene immediatamente elaborato. Alcuni fattori possono ritardare che:

  • Se il processore è in modalità di sospensione, vengono indicati i tempi di "sveglia", che potrebbero essere di alcuni millisecondi, mentre l'orologio viene riavviato per accelerare. Questa volta dipende dalle impostazioni dei fusibili e dalla profondità del sonno.

  • Se una routine di servizio di interrupt è già in esecuzione, non è possibile immettere ulteriori interrupt fino a quando non termina o abilita gli interrupt stessi. Questo è il motivo per cui dovresti tenere breve ogni routine del servizio di interruzione, poiché ogni microsecondo che trascorri in uno di questi, stai potenzialmente ritardando l'esecuzione di un altro.

  • Alcuni codici disattivano gli interrupt. Ad esempio, la chiamata a millis () disattiva brevemente gli interrupt. Pertanto, il tempo necessario per la manutenzione di un interrupt verrebbe prolungato della durata dell'interruzione.

  • Gli interrupt possono essere sottoposti a manutenzione solo alla fine di un'istruzione, quindi se una particolare istruzione richiede tre cicli di clock e si è appena avviata, l'interruzione verrà ritardata di almeno un paio di cicli di clock.

  • Un evento che riattiva gli interrupt (es. Ritorno da una routine di servizio di interrupt) garantisce l'esecuzione di almeno un'altra istruzione. Quindi, anche se un ISR termina e l'interruzione è in sospeso, deve ancora attendere un'altra istruzione prima che venga revisionata.

  • Poiché gli interrupt hanno una priorità, un interrupt con priorità più elevata potrebbe essere gestito prima dell'interrupt a cui si è interessati.


Considerazioni sulle prestazioni

Gli interrupt possono aumentare le prestazioni in molte situazioni perché puoi andare avanti con il "lavoro principale" del tuo programma senza dover costantemente testare per vedere se gli interruttori sono stati premuti. Detto questo, l'overhead della manutenzione di un interrupt, come discusso in precedenza, sarebbe in realtà più che fare un "circuito chiuso" nel polling di una singola porta di input. Riesci a malapena a rispondere a un evento entro, diciamo, un microsecondo. In tal caso, è possibile disabilitare gli interrupt (ad es. Timer) e semplicemente cercare il pin per cambiare.


Come vengono messi in coda gli interrupt?

Esistono due tipi di interrupt:

  • Alcuni impostano un flag e vengono gestiti in ordine di priorità, anche se l'evento che li ha causati si è interrotto. Ad esempio, un livello crescente, discendente o variabile interrompe il pin D2.

  • Altri vengono testati solo se stanno accadendo "adesso". Ad esempio, un interrupt di basso livello sul pin D2.

Quelli che impostano un flag potrebbero essere considerati in coda, poiché il flag di interruzione rimane impostato fino a quando non viene immessa la routine di interruzione, momento in cui il processore cancella il flag. Naturalmente, poiché esiste un solo flag, se si verifica nuovamente la stessa condizione di interruzione prima che il primo venga elaborato, non verrà revisionato due volte.

Qualcosa da tenere presente è che questi flag possono essere impostati prima di collegare il gestore di interrupt. Ad esempio, è possibile che un interrupt di livello crescente o decrescente sul pin D2 sia "contrassegnato", quindi non appena si effettua un collegamento, l'interruzione si attiva immediatamente, anche se l'evento si è verificato un'ora fa. Per evitare ciò, è possibile cancellare manualmente la bandiera. Per esempio:

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

Tuttavia, gli interrupt di "livello basso" vengono continuamente controllati, quindi se non si presta attenzione continueranno a sparare, anche dopo che l'interrupt è stato chiamato. Cioè, l'ISR uscirà e quindi l'interruzione si attiverà immediatamente di nuovo. Per evitare ciò, dovresti fare un distaccoInterrupt immediatamente dopo aver saputo che l'interrupt è stato attivato.


Suggerimenti per la scrittura di ISR

In breve, mantienili brevi! Durante l'esecuzione di un ISR, non è possibile elaborare altri interrupt. Quindi potresti facilmente perdere la pressione dei pulsanti o le comunicazioni seriali in entrata, se provi a fare troppo. In particolare, non si dovrebbe tentare di eseguire "stampe" di debug all'interno di un ISR. È probabile che il tempo impiegato per fare ciò causi più problemi di quanti ne risolvano.

Una cosa ragionevole da fare è impostare un flag a byte singolo e quindi testare quel flag nella funzione loop principale. Oppure, memorizzare un byte in arrivo da una porta seriale in un buffer. Gli interrupt incorporati del timer tengono traccia del tempo trascorso attivando ogni volta che il timer interno trabocca, e quindi puoi calcolare il tempo trascorso sapendo quante volte il timer ha traboccato.

Ricorda, all'interno di un ISR gli interrupt sono disabilitati. Sperando così che il tempo restituito dalle chiamate di funzione millis () cambierà, porterà alla delusione. È valido per ottenere il tempo in questo modo, basta essere consapevoli che il timer non sta aumentando. E se trascorri troppo tempo nell'ISR, il timer potrebbe perdere un evento di overflow, portando al tempo restituito da millis () che diventa errato.

Un test mostra che, su un processore Atmega328 a 16 MHz, una chiamata a micros () richiede 3,5625 µs. Una chiamata a millis () richiede 1,99375 µs. Registrare (salvare) il valore corrente del timer è una cosa ragionevole da fare in un ISR. Trovare i millisecondi trascorsi è più veloce dei microsecondi trascorsi (il conteggio dei millisecondi viene appena recuperato da una variabile). Tuttavia, il conteggio dei microsecondi si ottiene aggiungendo il valore corrente del timer Timer 0 (che continuerà ad aumentare) a un "conteggio di overflow del Timer 0" salvato.

Avvertenza: poiché gli interrupt sono disabilitati all'interno di un ISR e poiché l'ultima versione dell'IDE di Arduino utilizza gli interrupt per la lettura e la scrittura seriale e anche per incrementare il contatore utilizzato da "millis" e "delay", non si dovrebbe tentare di utilizzare quelle funzioni all'interno di un ISR. Per dirla in un altro modo:

  • Non tentare di ritardare, ad esempio: delay (100);
  • Puoi passare il tempo da una chiamata a millis, tuttavia non aumenterà, quindi non tentare di ritardare aspettando che aumenti.
  • Non eseguire stampe seriali (ad es. Serial.println ("ISR entered");)
  • Non provare a leggere in serie.

Interruzione cambio pin

Esistono due modi per rilevare eventi esterni sui pin. Il primo sono i pin speciali "interrupt esterno", D2 e ​​D3. Questi eventi di interruzione discreti generali, uno per pin. Puoi arrivare a quelli usando attachInterrupt per ogni pin. È possibile specificare una condizione di salita, discesa, modifica o di basso livello per l'interrupt.

Tuttavia, ci sono anche interruzioni "cambio pin" per tutti i pin (su Atmega328, non necessariamente tutti i pin su altri processori). Questi agiscono su gruppi di pin (da D0 a D7, da D8 a D13 e da A0 a A5). Hanno anche una priorità inferiore rispetto agli interrupt di evento esterno. Tuttavia sono un po 'più complicati da usare rispetto agli interrupt esterni perché sono raggruppati in batch. Quindi se l'interruzione si attiva devi capire esattamente quale pin ha causato l'interruzione nel tuo codice.

Codice di esempio:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  }

Per gestire un interrupt di modifica pin è necessario:

  • Specifica quale pin nel gruppo. Questa è la variabile PCMSKn (dove n è 0, 1 o 2 dalla tabella seguente). Puoi avere interruzioni su più di un pin.
  • Abilita il gruppo appropriato di interrupt (0, 1 o 2)
  • Fornire un gestore di interrupt come mostrato sopra

Tabella dei pin -> nomi di modifica dei pin / maschere

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Interrompere l'elaborazione del gestore

Il gestore di interrupt dovrebbe determinare quale pin ha causato l'interruzione se la maschera ne specifica più di uno (ad es. Se si desidera interruzioni su D8 / D9 / D10). Per fare ciò, è necessario memorizzare lo stato precedente di quel pin ed elaborare (eseguendo una lettura digitale o simile) se questo pin particolare fosse cambiato.


Probabilmente stai usando gli interrupt comunque ...

Un ambiente "normale" di Arduino utilizza già gli interrupt, anche se non ci provi personalmente. Le chiamate alle funzioni millis () e micros () fanno uso della funzione "overflow timer". Uno dei timer interni (timer 0) è impostato per interrompere circa 1000 volte al secondo e incrementare un contatore interno che diventa effettivamente il contatore millis (). C'è un po 'di più in questo, dato che viene effettuata la regolazione per la velocità esatta del clock.

Inoltre la libreria seriale hardware utilizza gli interrupt per gestire i dati seriali in entrata e in uscita. Questo è molto utile poiché il tuo programma può fare altre cose mentre gli interrupt vengono attivati ​​e riempire un buffer interno. Quindi quando si controlla Serial.available () è possibile scoprire cosa, se non altro, è stato inserito in quel buffer.


Esecuzione dell'istruzione successiva dopo aver abilitato gli interrupt

Dopo un po 'di discussioni e ricerche sul forum di Arduino, abbiamo chiarito esattamente cosa succede dopo aver abilitato gli interrupt. Ci sono tre modi principali in cui posso pensare che puoi abilitare gli interrupt, che in precedenza non erano abilitati:

  sei ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

In tutti i casi, il processore garantisce che le istruzioni successive dopo l'attivazione degli interrupt (se precedentemente disabilitate) verranno sempre eseguite, anche se è in corso un evento di interruzione. (Per "successivo" intendo il successivo nella sequenza del programma, non necessariamente quello che segue fisicamente. Ad esempio, un'istruzione RETI ritorna al punto in cui si è verificato l'interrupt e quindi esegue un'altra istruzione).

Questo ti permette di scrivere codice in questo modo:

sei ();
sleep_cpu ();

Se non fosse per questa garanzia, l'interrupt potrebbe verificarsi prima che il processore dormisse, e quindi potrebbe non essere mai attivato.


Interrupt vuoti

Se si desidera semplicemente un interrupt per riattivare il processore, ma non fare nulla in particolare, è possibile utilizzare la definizione EMPTY_INTERRUPT, ad es.

EMPTY_INTERRUPT (PCINT1_vect);

Questo genera semplicemente un'istruzione "reti" (return from interrupt). Dal momento che non tenta di salvare o ripristinare i registri, questo sarebbe il modo più veloce per ottenere un interrupt per svegliarlo.


Sezioni critiche (accesso alle variabili atomiche)

Esistono alcuni problemi sottili relativi alle variabili condivise tra le routine di servizio di interruzione (ISR) e il codice principale (ovvero, il codice non in un ISR).

Poiché un ISR può attivarsi in qualsiasi momento quando gli interrupt sono abilitati, è necessario essere cauti nell'accedere a tali variabili condivise, poiché potrebbero essere aggiornate nel momento stesso in cui si accede ad esse.

Primo ... quando usi le variabili "volatili"?

Una variabile dovrebbe essere contrassegnata come volatile solo se utilizzata sia all'interno di un ISR che all'esterno.

  • Le variabili utilizzate solo al di fuori di un ISR non dovrebbero essere volatili.
  • Le variabili utilizzate solo all'interno di un ISR non dovrebbero essere volatili.
  • Le variabili utilizzate sia all'interno che all'esterno di un ISR dovrebbero essere volatili.

per esempio.

volatile int counter;

Contrassegnare una variabile come volatile indica al compilatore di non "memorizzare" il contenuto della variabile in un registro del processore, ma di leggerlo sempre dalla memoria, quando necessario. Ciò può rallentare l'elaborazione, motivo per cui non si rende volatile ogni variabile, quando non è necessario.

Disattiva gli interrupt mentre accedi a una variabile volatile

Ad esempio, per confrontare un countcerto numero, disattivare gli interrupt durante il confronto nel caso in cui un IS countsia stato aggiornato dall'ISR e non dall'altro byte.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

Leggi la scheda tecnica!

Maggiori informazioni su interrupt, timer, ecc. Possono essere ottenute dalla scheda tecnica per il processore.

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


Ulteriori esempi

Considerazioni di spazio (limite dimensione post) impediscono al mio elenco di ulteriori esempi di codice. Per ulteriori esempi di codice vedi la mia pagina sugli interrupt .


Un riferimento molto utile, che è stata una risposta incredibilmente veloce.
Dat Han Bag,

Era una domanda di riferimento. Avevo preparato la risposta e sarebbe stata ancora più veloce se la risposta non fosse stata troppo lunga, quindi ho dovuto potarla di nuovo. Vedi il sito collegato per maggiori dettagli.
Nick Gammon

A proposito della "modalità sleep" è efficace far dormire Arduino per, diciamo, 500ms?
Dat Ha,

@Nick Gammon Immagino che accendere o spegnere (con automazione o meno) per la CPU possa essere definito come un interrupt non convenzionale - se lo si desidera. "Avevo preparato la risposta" - hai appena tirato fuori tutta la magia di quel momento che pensavo avesse.
Dat Han Bag,

1
Temo che non sia vero. Ho un esempio che utilizza gli interruttori di cambio pin per riattivare dalla modalità di spegnimento. Inoltre, come menziono nella mia pagina sugli interrupt, Atmel ha confermato che qualsiasi interrupt esterno riattiverà il processore (es. Aumento / discesa / modifica e basso).
Nick Gammon
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.