Switch Interrupt

3. Schakelaars

Schakelaars of switches zijn heb je in het eerste labo gezien als een opening in de stroomkring, die met een druk op de knop gesloten kan worden. De schakelaars in jullie ELSY kit zijn hiervan een klassiek voorbeeld, het werkingsprincipe kan je hieronder terugvinden. Dit type van schakelaar is een “Momentary” schakelaar, deze schakelt enkel wanneer ingedrukt. Let op: deze kunnen zowel “Normally Open (NO)” zijn, zoals in jullie kit, maar ook “Normally Closed (NC)”! Dit betekent dat ze geleiden, zolang de knop niet ingedrukt is! Dit is hoe het lampje in je koelkast werkt, of toch zou moeten werken.

Naast Momentary Normaal Geopende of Gesloten schakelaars, is er een categorie die minstens even belangrijk is: de “Latching” schakelaars. Hierbij breng je de schakelaar in een toestand, en deze blijft behouden tot je de schakelaar opnieuw gebruikt. Voorbeelden hierbij zijn bijna alle Aan-Uit schakelaars, gewone lichtschakelaars, voetschakelaar voor een staande lamp, … Sommige latching schakelaars hebben een gemeenschappelijk contact en daarnaast zowel een NO als een NC contact, deze kan je beide gebruiken of slechts 1 van beide functies.

Daarzijn zijn er nog erg veel specifieke schakelaars, een kleine bloemlezing:

  • Schuifschakelaar (Slideswitch), vaak gebruikt om kleine electro in en uit te schakelen.
  • DIP schakelaar: Gebuikt om opties of instelling aan te passen.
  • Draaischakelaars: Laten je meerdere paden selecteren (Multimeter)
  • Eindeloopschakelaars: Vergelijkbaar met de microswitch uit je kit, maar bedoeld voor een machine of robot.
  • Reedcontacten: Magnetische schakelaar, die schakelt onder invloed van een sterk magnetisch veld. (Laat je laptop in standby gaan wanneer deze gesloten is)
  • ….

Status van de knop uitlezen

  • Het doel is dat we met een druk op de knop (BTN1), de led (LED1) kunnen togglen.
  • Togglen = Veranderen van status, met 1 druk op de knop. Van AAN naar UIT of van UIT naar AAN.
  • Wanneer de knop wordt losgelaten mag er niets gebeuren.
  • Hieronder kan je voorbeeldcode als startpunt vinden. Kopieer deze naar een nieuwe sketch in je Arduino IDE.
  • We gaan tijdens dit labo nog veel met dezelfde code en schakeling werken, dus pas afbreken wanneer de gemeld wordt!
  • LET OP: Op verschillend regels in de code staat “…”, vul dit telkens aan met het toepasselijke statement. Pas voorlopig nog niets anders aan.
// 1) Import libraries
#include <SPI.h>
#include <TFT_eSPI.h> // Hardware-specific library

TFT_eSPI tft = TFT_eSPI(); // Constructor for the TFT library

// 2) Define constants
#define TFT_GREY 0x5AEB // New colour
#define BTN_PIN_1 ...
#define LED_PIN_1 ...
// 3) Init vareables
bool stateLed1 = true;
bool stateButton1;

void setup(void) {	// 4) The setup
  Serial.begin(115200);
  tft.init();
  tft.setRotation(1);   // setRotation: 1: Screen in landscape
  printTitle();

  pinMode(LED_PIN_1, ...);
  pinMode(BTN_PIN_1, ...);

  digitalWrite(..., ...); // apply the correct output for LED_PIN1
}

void loop() {	// 5) The main loop
  stateButton1 = !digitalRead(...);
  displayStateButton1(stateButton1);

  if (stateButton1) {
    btn1Handler();
  }

}

void printTitle() {
  // Print title
  tft.fillScreen(TFT_BLACK);   //Fill screen with random colour
  tft.setCursor(0, 0, 4);   //(cursor at 0,0; font 4, println autosets the cursor on the next line)
  tft.setTextColor(TFT_BLACK, TFT_YELLOW); // Textcolor, BackgroundColor; independent of the fillscreen
  tft.println("- Lab 2 -");    //Print on cursorpos 0,0
}

void displayStateButton1(bool value) {
  // Print aantal
  tft.setCursor(0, 50, 4);   //(cursor at 0,0; font 4, println autosets the cursor on the next line)
  tft.setTextColor(TFT_GREEN, TFT_BLACK); // Green Text with black background
  // Test some print formatting functions
  tft.print("stateButton1 = "); tft.println(value);    // Print floating point number
}

void btn1Handler() {
  Serial.println("Button 1 was clicked");
  stateLed1 = !stateLed1; // toggle the state of led 1
  digitalWrite(LED_PIN_1, stateLed1); // apply the correct output for LED_PIN1
}

Als je alle “…” velden correct hebt ingevuld zal je schakeling werken zoals hieronder. Niet helemaal zoals gewenst dus!

GIF

Rising of Falling Edge? (1)

We lezen onze knop uit op een manier zodat:

  • stateButton1 = 0 → Knop is niet ingedrukt
  • stateButton1 = 1 → Knop is wel ingedrukt LET OP: Aangezien we gebruik maken van een INPUT_PULLUP hebben we dus wel het ingelezen signaal moeten inverteren om dit gedrag te bekomen. Als dit niet duidelijk is, dan is het nu het moment om je vinger op te steken om hier meer info over te vragen.

Uitgaande van bovenstaande info ziet het signaal van de knop er dus uit zoals onderstaande illustratie. We gebruiken deze illustratie om twee nieuwe begrippen in te voeren die heel vaak in de digitale elektronica voorkomen

  • rising edge (of positive edge)
  • falling edge (of negative edge)
  • Het is de bedoeling dat de functie btn1Handler() enkel wordt uitgevoerd wanneer de knop wordt ingedrukt (rising edge).
  • Implementeer dit in de code
  • TIP: Gebruik een hulp-variable (oldStateButton1) om deze rising edge te detecteren

Rising of Falling Edge? (2)

De schakeling werkt ondertussen al een stuk beter, maar soms kan je toch nog ongewenst gedrag observeren. Dit komt door het bouncen van je knop. Zoek indien nodig online op wat “knop dender” of switch bouncing inhoudt.

  • Implementeer debouncing door de btn1Handler() functie te vervangen met de onderstaande. Voeg de nodige extra variabele toe, en let hierbij op het Type.
void btn1Handler() {
  Serial.println("Button 1 was clicked");
  unsigned long switch_time = millis();
  if (switch_time - last_switch_time > 200) { // debounce time of 200 ms
    stateLed1 = !stateLed1; // toggle the state of led 1
    Serial.print("stateLed1 = "); Serial.println(stateLed1); // Print the new state of the LED
    digitalWrite(LED_PIN_1, stateLed1); // apply the correct output for LED_PIN1
    last_switch_time = switch_time; // update the last switch time
  } else {
    Serial.println("Button bounce detected - no action performed");
  }
}
  • Je schakeling moet nu perfect werken. Telkens wanneer de knop wordt ingedrukt zal de led togglen.
  • Je zou geen last meer mogen ondervinden van bouncing.
  • Eventuele haperingen kunnen verklaard worden door slecht contact van de knop op het breadboard.
  • Het nut van de debounce implementatie kan aangetoond worden in de serial monitor
  • Om de x-aantal clicks zal er wel eens bouncing optreden (zie hieronder).
  • Het belangrijkste is dat jullie begrijpen waarom debouncen nodig is, en dan het kan gebeuren op verschillende manieren, zowel hardwarematig als softwarematig. In dit voorbeeld hebben we gekozen voor een softwarematige debounce door gebruik te maken van een simpele delay of cooldown via de millis() functie.
ANS

ANS vraag 3a, 3b, 3c

Interrupts

OPGELET: Je moet nog niets afbreken. We bouwen verder op je bestaande schakeling en code!

LET OP: Behoud je huidige code volledig en vul deze aan:

  • Voeg code aan de loop toe zodat: LED2 knippert met een periode van 2 seconden door gebruik te maken van de delay() functie.

  • LET OP: De periode van een signaal is een veelgebruikt begrip binnen digitale elektronica, zoek online op, indien noodzakelijk.

Test je schakeling nadat je de code voor LED2 hebt geïmplementeerd. Wat gebeurt er wanneer je op de drukknop klikt? Wordt LED1 nog correct getoggled? Hoe is het mogelijk dat de knop nu niet meer goed wordt uitgelezen terwijl we niets aan de code van BTN1 hebben veranderd? Dit probleem kan opgelost worden door het verschil te kennen tussen polling en interrupts, en dit te implementeren.

Polling vs Interrupts

Onze knop wordt momenteel uitgelezen door middel van polling

  • In de loop lezen we continu de waarde van de button uit. Dit ging goed toen onze microcontroller weinig te doen had. Momenteel voert onze microcontroller echter twee delay() functies uit. Doordat de delay() functie een blocking functie is zal er gedurende de wachttijd niets gebeuren… dus ook niet het pollen van onze knop. We kunnen dit probleem onder meer oplossen door gebruik te maken van een interrupt.

We hebben de uitleg van learn.adafruit.com wat aangepast om het begrip interrupt te introduceren:

What is an interrupt?

Een interrupt is een signaal dat de processor vertelt om onmiddellijk te stoppen met waar hij mee bezig is en een taak met hoge prioriteit af te handelen. Die taak met hoge prioriteit wordt een “Interrupt Handler” genoemd. Een interrupt handler is vergelijkbaar met elke andere ‘void’ functie (= een functie die niets teruggeeft, wanneer aangeroepen). Als je er een schrijft en deze aan een interrupt koppelt, wordt de functie aangeroepen zodra dat interrupt-signaal wordt geactiveerd. Wanneer de interrupt handler klaar is, keert de processor terug naar de taak waar hij daarvoor mee bezig was.$

Where do they come from?

Interrupts kunnen vanuit verschillende bronnen worden gegenereerd: Interne interrupts: Timer: Afkomstig van de interne ESP-timers, Peripherals, … Externe interrupts: Veroorzaakt door een toestandsverandering op een van de specifieke externe interrupt-pinnen, Capsense, …

What are they good for?

Door interrupts te gebruiken, hoef je geen code in de loop() te schrijven die continu controleert op een belangrijke conditie (dit noemen we polling). Je hoeft je geen zorgen te maken over een trage reactie of gemiste knopdrukken door langdurige subroutines (functies) in je programma. De processor stopt “als bij toverslag” met waar hij mee bezig is zodra de interrupt optreedt en roept jouw interrupt handler aan. Je hoeft alleen maar de code te schrijven die reageert op de interrupt wanneer deze plaatsvindt.

**Een overzicht van het verschil tussen interrupt en polling**

Fun fact

PS/2 = keyboard triggers an Interrupt Request (IRQ) when a key is pressed and the CPU basically must address it immediately (consistent, lowest input latency possible makes this ideal for gaming). For mice an IRQ is triggered every 10ms.

USB = Operating System constantly polls keyboard/mice for input events and responds when an event is detected (variable latency, delayed processing of input may occur if system is overburdened, significantly increased driver complexity).

Een interrupt, hoe?

In de Arduino IDE gebruiken we de functie attachInterrupt() om een interrupt aan een bepaalde GPIO pin te koppelen:

attachInterrupt(GPIOPin, ISR, Mode);

Deze functie gebruikt drie parameters:

  • GPIOPin – Stelt de GPIO pin in als een interrupt pin, en laat de ESP weten deze pin in het oog te houden.
  • ISR – De Interupt Service Routine (ISR) is de naam van de functie die aangeoepen moet worden elke keer wanneer de interrupt getriggerd wordt.
  • Mode – Definieert hoe de interrupt getriggerd kan worden. Er zijn hierbij 5 mogelijkheden op de ESP32:
    • LOW - Triggert de interrupt wanneer de pin LAAG is (telkens opnieuw!)
    • HIGH - Triggert de interrupt wanneer de pin HOOG is (telkens opnieuw!)
    • CHANGE - Triggers interrupt whenever the pin changes value, from HIGH to LOW or LOW to HIGH
    • FALLING - Triggers interrupt when the pin goes from HIGH to LOW
    • RISING - Triggers interrupt when the pin goes from LOW to HIGH

Aan de IRS of interruptfunctie zelf stellen we ook nog wat eisen: Je kan uiteraard zelf de naam van je functie kiezen (ISR in het voorbeeldje hieronder), maar let er op dat de ISR een VOID functie is, en geen argumenten kan aanvaarden! Het keyword “IRAM_ATTR” laat aan de compiler weten dat de functie vaak aangeroepen zal worden en in RAM moet blijven zitten ipv op het (trage) externe flash geheugen!

void IRAM_ATTR ISR() {
  // Code to handle the interrupt
}

Je kan dus geen informatie meegeven aan deze functie. Je kan echter wel werken met “Globale Variabelen” om gegevens uit de ISR te halen. Let er hierbij op dat je niet weet welke toestand de variabelen hebben wanneer de ISR opgeroepen wordt! Je doet er ook goed aan om deze variabelen als “Volatile” te markeren wanneer je ze declareert!

volatile int time = 0;
IRS functies zijn per definitie eenvoudig en snel, dus GEEN: print statements, berekeningen met kommagetallen, lange berekeningen, … Als je dit vergeet, dan zal je ESP waarschijnlijk crashen, met een cryptisch BSOD bericht tot gevolg.

Schema

Een interrupt implementeren

Voeg een interrupt toe aan BTN1 TIP: Gebruik btnHandler1() als ISR pinMode(BTN_PIN_1, INPUT_PULLUP); attachInterrupt(BTN_PIN_1, btn1Handler, …);

LET OP: Het is vanzelfsprekend dat je nu de digitalRead() functie verwijdert uit de loop. Wanneer je voor een interrupt implementatie kiest moet je namelijk niet meer pollen Verder zijn onderstaande variabelen ook niet meer nodig. Aan de attachInterrupt() functie kunnen we namelijk meegeven of we moeten reageren op een rising- of falling edge. bool stateButton1; bool oldStateButton1; Dat maakt dat je in de loop nu enkel nog LED_PIN_2 aanstuurt:

void loop() {
    digitalWrite(LED_PIN_2, HIGH);
    delay(1000);
    digitalWrite(LED_PIN_2, LOW);
    delay(1000);
}

ANS

ANS vraag 3d

Een belangrijke opmerking: We gebruikten nu btnHandler1() als ISR. Echter, het is vaak aangeraden om de ISR zo kort mogelijk te houden, zodat het programma zo snel mogelijk terug in de main loop terecht komt. Vaak wordt ervoor gekozen om in de ISR enkel een ‘flag’ te setten die de main loop zal tegenkomen en afhandelen. Hieronder zie je een voorbeeldje waarin de flag ‘onderneemActie’ wordt geset in de ISR. Gebruik dit voorbeeldje als inspiratie om de ISR in jullie huidige code zo kort mogelijk te houden.

volatile bool onderneemActie;

void setup() {
    onderneemActie = false;
    attachInterrupt(btn_pin, interruptFunctie, FALLING);
}

void loop() {
    // overige code
    if (onderneemActie) {
        voerActieUit();
    }
}

void voerActieUit() {
    // onderneem actie
    onderneemActie = false;
}

void interruptFunctie() {
    onderneemActie = true;
}

Na het implementeren van de flag zal je opmerken dat de led niet meer onmiddellijk toggled na het indrukken van de knop. We moeten dus besluiten dat het in deze situatie alsnog interessant is om de LED meteen te toggelen in de ISR. Toch is het belangrijk dat jullie onthouden om de ISR zo kort mogelijk te houden, en dat flags hierbij kunnen helpen.

Let op: Je moet deze opgave niet laten zien, maar ze maken is wel ERG BELANGRIJK, de kennis die je hier opdoet heb je zeker nodig in deel 4 (en je PES project!)!

Optioneel: De korte code hieronder laat je goed zien wat er gebeurt wanneer je een lange ISR zou bouwen. Plak de code hieronder in je Arduino IDE en kijk naar het resultaat wanneer je GPIO-0 inklikt (Knopje naast de USB-C connector).

#define buttonPin 0

// Interrupt Service Routine (ISR)
void ARDUINO_ISR_ATTR buttonISR() {
  Serial.print("Dit is een lang printstatement vanuit de interrupt."
  "\nEigenlijk hoort dit niet, lange ISR-routines zorgen voor instabiliteit, "
  "en je loopt een grote kans op een Kernel Panic, met een reboot tot gevolg.\n");
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(buttonPin, buttonISR, RISING);
  Serial.println("\n\n\Press the button on GPIO-0 (Naast de USB connector).");
  Serial.println("Dit zal een Kernel Panic triggeren en de ESP heropstarten.\n\n");
}

void loop() {
  //Do nothing
  delay(100);
}

Dit concludeert het “Digitale Sensor”-luik van dit labo, nu kan je verdergaan met de analoge sensoren. Je mag de huidige schakeling afbreken, en een nieuw codevenster openen in de Arduino IDE (CTRL+N of Cmd+N).