11. Interrupts στην πλατφόρμα Arduino

Τα Interrupts (διακοπές) είναι ένας από τους σημαντικότερους μηχανισμούς των μικροελεγκτών Arduino, επειδή επιτρέπουν στο πρόγραμμα να αντιδρά σε ένα εξωτερικό ή εσωτερικό γεγονός, χωρίς να χρειάζεται να ελέγχει συνεχώς την κατάσταση του μέσα στο loop( ).

Παράδειγμα: Αν θέλουμε να ανιχνεύσουμε αν πατήθηκε ένα κουμπί, μια απλή προσέγγιση είναι:

void setup() {
    pinMode(2, INPUT_PULLUP);
    pinMode(13, OUTPUT);
}
void loop(){
    if(digitalRead(2) == LOW)
       //Το κουμπί πατήθηκε
       digitalWrite(13, HIGH);
    else
       digitalWrite(13, LOW);
}

Εδώ ο μικροελεγκτής πρέπει να φτάσει στην εντολή digitalRead( ) για να διαπιστώσει την αλλαγή. Με ένα Interrupt αντίθετα μπορούμε να πούμε: Όταν συμβεί αυτό το γεγονός, σταμάτα προσωρινά ότι κάνεις και εκτέλεσε αυτή την συγκεκριμένη συνάρτηση. Ένας παρόμοιος κώδικας που χρησιμοποιεί Interrupts είναι ο ακόλουθος:

volatile bool buttonPressed = false;
void setup() {
    pinMode(2, INPUT_PULLUP);
    pinMode(13, OUTPUT);
    attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING);
}
void loop() {
    if (buttonPressed) {
    digitalWrite(13, HIGH);
    delay(2000);
    buttonPressed = false;
}
}
void buttonISR() {
     buttonPressed = true;
}

Αν κοιτάξουμε προσεκτικά, θα δούμε την διαφορά της λειτουργίας στους δυο αυτούς κώδικες: στον δεύτερο το LED ανάβει για 2 δευτερόλεπτα από την στιγμή που πατήσουμε το button, ενώ στον πρώτο το LED ανάβει όσο είναι πατημένο το button.

Η συνάρτηση pinMode(2, INPUT_PULLUP); ενεργοποιεί την εσωτερική pull-up αντίσταση. Έτσι όταν το button είναι ανοικτό έχουμε HIGH στην είσοδο του pin και όταν είναι πατημένο έχουμε LOW.

Με το mode ορισμένο σαν FALLING αυτό σημαίνει ότι όταν το button πατηθεί, από τη μετάβαση από HIGH σε LOW στην είσοδο του pin ενεργοποιείται η διακοπή και αμέσως εκτελείται η συνάρτηση διακοπής buttonISR( ).

Η attachInterrupt(digitalPinToInterrupt(pin), ISRfunction, mode); είναι η σημαντικότερη συνάρτηση για interrupts στο Arduino και έχει τρία βασικά στοιχεία:

Το interrupt number: digitalPinToInterrupt(2) όπου εδώ το pin είναι το 2. Μετατρέπει τον αριθμό του Arduino pin στον αντίστοιχο αριθμό interrupt. Είναι προτιμότερο να χρησιμοποιείται αυτή η μορφή αντί να γράφουμε απευθείας αριθμούς interrupt, επειδή οι αντιστοιχίες διαφέρουν μεταξύ πλακετών.

Η ISRfunction: είναι η συνάρτηση που θα εκτελεστεί όταν ενεργοποιηθεί το interrupt. Για παράδειγμα:

void ISRfunction( ){
// Κώδικας interrupt
}

Μέσα στο όρισμα της συνάρτησης attachInterrupt( ), δεν γράφουμε παρενθέσεις στο όνομα της συνάρτησης που τρέχει κατά την εκτέλεση της διακοπής.

To mode: Το τρίτο όρισμα καθορίζει πότε θα ενεργοποιείται το interrupt. Οι βασικές επιλογές είναι:
LOW CHANGE RISING FALLING HIGH

LOW – χαμηλή στάθμη: Η διακοπή ενεργοποιείται όσο το σήμα βρίσκεται σε χαμηλή λογική στάθμη, σύμφωνα με την υλοποίηση της συγκεκριμένης πλακέτας.
RISING – ανερχόμενη ακμή: Ενεργοποιείται όταν το σήμα αλλάζει από LOW (0 V) σε HIGH (π.χ. 5 V ή 3,3 V, ανάλογα με την πλακέτα).
FALLING – κατερχόμενη ακμή: Ενεργοποιείται όταν το σήμα αλλάζει από HIGH σε LOW.
CHANGE – ανερχόμενη ή κατερχόμενη ακμή: Ενεργοποιείται όταν το σήμα αλλάζει από LOW σε HIGH ή από HIGH σε LOW.
HIGH – υψηλή στάθμη: Η διακοπή ενεργοποιείται όσο το σήμα βρίσκεται σε υψηλή λογική στάθμη, σύμφωνα με την υλοποίηση της συγκεκριμένης πλακέτας.

Η υποστήριξη ορισμένων modes εξαρτάται από την πλατφόρμα. Δεν πρέπει να θεωρούμε ότι όλα τα Arduino υποστηρίζουν ακριβώς τα ίδια modes. Για κάθε διαφορετική πλακέτα πρέπει να ελέγχουμε τις δυνατότητες του συγκεκριμένου μικροελεγκτή. Για παράδειγμα, στο κλασικό Arduino Uno με ATmega328P, οι εξωτερικές διακοπές υποστηρίζουν LOW, CHANGE, RISING και FALLING, αλλά όχι HIGH ως mode του attachInterrupt().

Η συνάρτηση: digitalPinToInterrupt(pin) μετατρέπει ένα Arduino digital pin στον αντίστοιχο αριθμό hardware interrupt. Για παράδειγμα έχουμε:
int interruptNumber = digitalPinToInterrupt(2);

Στην πράξη τη χρησιμοποιούμε απευθείας:
attachInterrupt(digitalPinToInterrupt(2), myISR, RISING);

Η αντίστοιχη εντολή για την απενεργοποίηση ενός interrupt είναι:
detachInterrupt(digitalPinToInterrupt(pin);

όπου pin ο ακροδέκτης που προκαλεί την διακοπή. Για παράδειγμα:

void setup( ) {
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING);
}

Αργότερα μπορούμε να απενεργοποιήσουμε το interrupt:
detachInterrupt(digitalPinToInterrupt(2));

Έτσι η ISR δεν θα εκτελείται πλέον για το συγκεκριμένο interrupt.

Ο όρος volatile είναι ένα από τα σημαντικότερα σημεία των interrupts. Στο προηγούμενο παράδειγμα γράψαμε:

volatile bool flag = false;
void setup(){
     attachInterrupt(digitalPinToInterrupt(2), myISR, RISING);
}
void loop(){
     if(flag){
     // . . .
     }
}
void myISR(){
     flag = true;
}


Η flag αλλάζει μέσα στην ISR, επομένως πρέπει να δηλωθεί volatile. Γενικός κανόνας: Μεταβλητές που αλλάζουν μέσα σε ISR και χρησιμοποιούνται επίσης στην συνάρτηση loop() κατά κανόνα θα πρέπει να δηλώνονται volatile.

Η ISR πρέπει να είναι πολύ σύντομη. Για παράδειγμα, είναι κακή τακτική να γράφουμε:

void buttonISR( ) {
    delay(1000);
    Serial.println(“Button!”);
}

Η ISR δεν είναι κανονική συνάρτηση της loop( ) όπου κατά την διάρκεια της, η κανονική εκτέλεση του προγράμματος να διακόπτεται. Σε πολλές Arduino αρχιτεκτονικές άλλα interrupts μπορεί επίσης να μην εξυπηρετούνται ταυτόχρονα. Η καλύτερη πρακτική είναι:

volatile bool event = false;
void buttonISR( ){
    event = true;
}
void setup( ){
     attachInterrupt(digitalPinToInterrupt(2), myISR, RISING);
}
void loop( ){
     if(event){
     event = false;
     Serial.println(“Button!”);
     }
}

Γενικά πρέπει να αποφεύγουμε την συνάρτηση καθυστέρησης delay() και χρονοβόρες εργασίες. Επίσης χρειάζεται προσοχή με την συνάρτηση Serial.print( ), δυναμική μνήμη, μεγάλους υπολογισμούς και λειτουργίες που εξαρτώνται από άλλα interrupts. Για παράδειγμα, αντί:

void ISR( ){
Serial.println(“Interrupt!”);
}

καλύτερα:

volatile bool interruptOccurred = false;
void ISR( ){
   interruptOccurred = true;
}
void loop( ){
    if(interruptOccurred){
       interruptOccurred = false;
       Serial.println(“Interrupt!”);
    }
}

Υπάρχει ένα πιο προχωριμένο πρόβλημα. Ας υποθέσουμε ότι έχουμε:

volatile unsigned long counter = 0;
void ISR( ){
counter++;
}

Στην συνάρτηση loop() μπορεί να έχουμε την εντολή, η οποία μπορεί να διαβάζει ταυτόχρονα: Serial.println(counter);

Σε ορισμένους 8-bit μικροελεγκτές, μια μεταβλητή unsigned long αποτελείται από περισσότερα bytes. Άρα η ανάγνωση της μπορεί να χρειάζεται περισσότερους CPU κύκλους. Εδώ μπορούμε να χρησιμοποιήσουμε προστατευμένη πρόσβαση. Αυτό μπορεί να γίνει με τις ακόλουθες μακροεντολές:

noInterrupts( )
unsigned long value = counter;
interrupts( );
Serial.println(value);

Η εντολή noInterrupts( ); απενεργοποιεί προσωρινά τα interrupts. Χρησιμοποιείται όταν πρέπει να εκτελέσουμε ένα πολύ μικρό κρίσιμο τμήμα κώδικα.

Η εντολή interrupts( ); ενεργοποιεί ξανά τα interrupts. Θα πρέπει να προσέξουμε ότι δεν πρέπει να αφήνουμε τα interrupts απενεργοποιημένα για μεγάλο χρονικό διάστημα.

Μπορούμε να χρησιμοποιήσουμε interrupts για να μετρήσουμε πόσοι παλμοί εμφανίζονται σε συγκεκριμένο χρονικό διάστημα.

volatile unsigned long pulses = 0;
void pulseISR() {
     pulses++;
}
void setup() {
     Serial.begin(9600);
     pinMode(2, INPUT);
     attachInterrupt(digitalPinToInterrupt(2), pulseISR, RISING);
}
void loop() {
     static unsigned long previousTime = 0;
     if (millis() - previousTime >= 1000) {
         previousTime += 1000;
         noInterrupts();
         unsigned long count = pulses;
         pulses = 0;
         interrupts();
         Serial.print("Pulses/sec: ");
         Serial.println(count);
     }
}

Αυτό είναι ένα σημείο που προκαλεί συχνά συγχυνση. Δεν υποστηρίζουν όλα τα Arduino boards τα ίδια pins για external interrupts. Για παράδειγμα, στις κλασσικές πλακέτες με ATmega328P η διαθεσιμότητα είναι διαφορετική από αυτή άλλων Arduino οικογενειών. Επομένως δεν πρέπει να απομνημονεύουμε απλώς ότι “το pin 2 είναι interrupt” αλλά θα πρέπει να συμβουλευόμαστε το documentation της συγκεκριμένης πλακέτας.