Grundlagen
Mehrere Pin Change Interrupts verwenden
- Details
- Hauptkategorie: Arduino
- Zuletzt aktualisiert am Donnerstag, 14. Juni 2012 14:08
Die Erfassung mehrerer Impulse kann durch die Verwendung von Pin Change Interrupts realisiert werden. Ein Polling (Abfragen der Ports in Endlosschleifen) ist nicht zielführend, weil dabei kurze Impulse übersehen werden können. Da der Arduino nur zwei externe Interrupts unterstützt, muss man auf die im Mega168/328 implementierten Pin Change Interrupts (PCI) zurückgreifen.
Diese PCI sind von der Arduino-IDE nicht unterstützt. Hier müssen CPU-Register direkt manipuliert werden. Vorteilhaft ist, dass die IDE die Register als reservierte Namen kennt. Dadurch sind Zuweisungen und Abfragen sehr einfach möglich.
Im folgenden Demosketch werden diese vier Interrupts verwendet:
- Interrupt 1: Digital Pin 11
- Interrupt 2: Digital Pin 12
- Interrupt 3: Analog Input 1
- Interrupt 4: Analog Input 2
Die analogen Eingänge können alternativ auch als Digitaleingänge betrieben werden. Eine interessante Option, wenn man zu wenig digitale Pins hat.
Im Sketch wurde ausserdem eine Software-Entprellung der Eingänge implementiert. Man kann also "prellende" mechanische Kontakte verwenden - vorteilhafter sind natürlich elektronisch gewonnene Impulse (Open-Collector-Ausgänge, Hallgeber, Lichtschranken etc.). Die Entprellung erfordert Zeit. Speist man den Arduino mit entprellten Impulsen, kann die Frequenz höher sein.
Interrupt-Tabelle
Es stehen bis zu 20 Interrupts zur Verfügung:
| Arduino-Pin | Pin# | Port# | Port Cmd | PCIE | PCMASK | INT | IRQ Service | Bemerkung |
| Hinweis 1 | Hinweis 2 |
Hinweis 3 | Hinweis 4 | Hinweis 5 | Hinweis 6 | Hinweis 7 | Hinweis 8 | --- |
| Digital Pin 0 | 0 | D | PIND | PCIE2 | PCMASK2 | PCINT16 | PCINT2_vect | Seriell Rx |
| Digital Pin 1 | 1 | D | PIND | PCIE2 | PCMASK2 | PCINT17 | PCINT2_vect | Seriell Tx |
| Digital Pin 2 | 2 | D | PIND | PCIE2 | PCMASK2 | PCINT18 | PCINT2_vect | ext. INT0 |
| Digital Pin 3 | 3 | D | PIND | PCIE2 | PCMASK2 | PCINT19 | PCINT2_vect | ext. INT1 |
| Digital Pin 4 | 4 | D | PIND | PCIE2 | PCMASK2 | PCINT20 | PCINT2_vect | DFRobots Shield |
| Digital Pin 5 | 5 | D | PIND | PCIE2 | PCMASK2 | PCINT21 | PCINT2_vect | DFRobots Shield |
| Digital Pin 6 | 6 | D | PIND | PCIE2 | PCMASK2 | PCINT22 | PCINT2_vect | DFRobots Shield |
| Digital Pin 7 | 7 | D | PIND | PCIE2 | PCMASK2 | PCINT23 | PCINT2_vect | DFRobots Shield |
| Digital Pin 8 | 8 | B | PINB | PCIE0 | PCMASK0 | PCINT0 | PCINT0_vect | DFRobots Shield |
| Digital Pin 9 | 9 | B | PINB | PCIE0 | PCMASK0 | PCINT1 | PCINT0_vect | DFRobots Shield |
| Digital Pin 10 | 10 | B | PINB | PCIE0 | PCMASK0 | PCINT2 | PCINT0_vect | DFRobots Shield |
| Digital Pin 11 | 11 | B | PINB | PCIE0 | PCMASK0 | PCINT3 | PCINT0_vect | verfügbar |
| Digital Pin12 | 12 | B | PINB | PCIE0 | PCMASK0 | PCINT4 | PCINT0_vect | verfügbar |
| Digital Pin13 | 13 | B | PINB | PCIE0 | PCMASK0 | PCINT5 | PCINT0_vect | interne LED |
| Analog Input 0 | 14 | C | PINC | PCIE1 | PCMASK1 | PCINT8 | PCINT1_vect | DFRobots Shield |
| Analog Input 1 | 15 | C | PINC | PCIE1 | PCMASK1 | PCINT9 | PCINT1_vect | verfügbar |
| Analog Input 2 | 16 | C | PINC | PCIE1 | PCMASK1 | PCINT10 | PCINT1_vect | verfügbar |
| Analog Input 3 | 17 | C | PINC | PCIE1 | PCMASK1 | PCINT11 | PCINT1_vect | verfügbar |
| Analog Input 4 | 18 | C | PINC | PCIE1 | PCMASK1 | PCINT12 | PCINT1_vect | I2C-Bus SDA |
| Analog Input 5 | 19 | C | PINC | PCIE1 | PCMASK1 | PCINT13 | PCINT1_vect | I2C-Bus SCL |
Die verschiedenen Spalten der Tabelle werden bei der Programmierung benötigt und sind weiter unten erklärt.
Hardware
![]() |
Verwendet wird ein Arduino mit aufgestecktem Protoshield zur einfacheren Verdrahtung der Taster. Alternativ kann man ein normales Breadboard verwenden oder die Taster an Kabel löten. Jeder Taster hat einen Pin an Ground, während der andere zum Eingang des Arduino führt. Pullup-Widerstände werden nicht benötigt, weil die internen Widerstände eingeschaltet werden. |
Software
Für diesen Sketch wird keine Library benötigt!
Um die Auswirkungen der Interrupts zu sehen, werden mit jedem (entprellten) Interrupt entsprechende Countervariablen hochgezählt. In der Main Loop werden die Counterstände über die serielle Schnittstelle an den seriellen Monitor der IDE geschickt. In nachfolgendem Bild sieht man, wie die Zähler Counter1...counter4 der Interrupts 1..4 durch Drücken der entsprechenden Taster hochzählen:
![]() |
Sketch
Hier der vollständige Sketch. Die Erklärung der Programmteile erfolgt weiter unten.
/***************************************************************************** * Sketch: PC_IRQ_MULTI.pde * Author: A. Kriwanek: http://www.kriwanek.de/arduino/komponenten.html * Version: 1.0 26.06.2011/20:20 * * This sketch is waiting for a pin change interrupts on Arduino pins. If an interrupt * occurs, the interrupt service routine is called. The routine increments a counter. * The main program sends the counter value over the serial port. The interrupt * service routine debounces the input signal (e.g. for a switch contact). * * This sketch uses 4 pin change interrupts. * * This sketch is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * *****************************************************************************/ //----------------------------------------------------------------------------- // Define values for the interrupt counter: volatile int counter1 = 0; // Counter incremented by pin change interrupt volatile int counter2 = 0; // Counter incremented by pin change interrupt volatile int counter3 = 0; // Counter incremented by pin change interrupt volatile int counter4 = 0; // Counter incremented by pin change interrupt volatile int bounceTime = 20; // Switch bouncing time in milliseconds volatile unsigned long IRQ1PrevTime; // Last time in milliseconds IRQ1 arrived volatile unsigned long IRQ2PrevTime; // Last time in milliseconds IRQ2 arrived volatile unsigned long IRQ3PrevTime; // Last time in milliseconds IRQ3 arrived volatile unsigned long IRQ4PrevTime; // Last time in milliseconds IRQ4 arrived volatile int IRQ1PrevVal = 0; // Contact level last IRQ1 volatile int IRQ2PrevVal = 0; // Contact level last IRQ2 volatile int IRQ3PrevVal = 0; // Contact level last IRQ3 volatile int IRQ4PrevVal = 0; // Contact level last IRQ4 volatile int irqFlag = 0; // 1=display counters after IRQ; 0=do nothing // Setup and Main: void setup(){ Serial.begin(9600); // Initialize serial interface with 9600 Baud Serial.write("Waiting for an interrupt...\n"); // INT1 --> Make Arduino Pin 11 (PCINT3/Port B.3) an input and set pull up resistor: pinMode(11, INPUT); digitalWrite(11, HIGH); // INT2 --> Make Arduino Pin 12 (PCINT4/Port B.4) an input and set pull up resistor: pinMode(12, INPUT); digitalWrite(12, HIGH); // INT3 --> Make Arduino Analog input 1 (PCINT9/Port C.1) an input and set pull up resistor: pinMode(15, INPUT); digitalWrite(15, HIGH); // INT4 --> Make Arduino Analog input 2 (PCINT10/Port C.2) an input and set pull up resistor: pinMode(16, INPUT); digitalWrite(16, HIGH); // This is ATMEGA368 specific, see page 75 of long datasheet // PCICR: Pin Change Interrupt Control Register - enables interrupt vectors // Bit 2 = enable PC vector 2 (PCINT23..16) // Bit 1 = enable PC vector 1 (PCINT14..8) // Bit 0 = enable PC vector 0 (PCINT7..0) PCICR |= (1 << PCIE0); // Set port bit in CICR for INT1 and INT2 PCICR |= (1 << PCIE1); // Set port bit in CICR for INT3 and INT4 // Pin change mask registers decide which pins are enabled as triggers: PCMSK0 |= (1<<PCINT3); // Set pin interrupt for INT1 PCMSK0 |= (1<<PCINT4); // Set pin interrupt for INT2 PCMSK1 |= (1<<PCINT9); // Set pin interrupt for INT3 PCMSK1 |= (1<<PCINT10); // Set pin interrupt for INT4 IRQ1PrevTime=millis(); // Hold actual time IRQ2PrevTime=millis(); // Hold actual time IRQ3PrevTime=millis(); // Hold actual time IRQ4PrevTime=millis(); // Hold actual time interrupts(); // Enable interrupts } void loop() { // Place your main loop commands here (e.g. output to LCD) if (irqFlag==1) // Flag was set by IRQ routine { Serial.write("IRQ rising edge, Counter1 = "); Serial.print(counter1); Serial.write(", Counter2 = "); Serial.print(counter2); Serial.write(", Counter3 = "); Serial.print(counter3); Serial.write(", Counter4 = "); Serial.println(counter4); irqFlag=0; // Reset IRQ flag } } //----------------------------------------------------------------------------- // Subs and Functions: ISR(PCINT0_vect) { // You have to write your own interrupt handler. Don't change the name! // This code will be called anytime when PCINT23 switches high to low, // or low to high. This is for INT1 and INT2 (both Port B) byte PVal; // Port value (8 Bits) byte IRQ1ActVal; // Actual IRQ1 value byte IRQ2ActVal; // Actual IRQ2 value long unsigned IRQ1ActTime; long unsigned IRQ2ActTime; PVal = PINB; // Read port D (8 bit) IRQ1ActVal = PVal & (1<<PCINT3); // Mask out all except IRQ1 IRQ1ActVal = IRQ1ActVal >> PCINT3; // shift to right for bit0 position IRQ2ActVal = PVal & (1<<PCINT4); // Mask out all except IRQ2 IRQ2ActVal = IRQ2ActVal >> PCINT4; // shift to right for bit0 position IRQ1ActTime=millis(); // Read actual millis time if(IRQ1ActTime - IRQ1PrevTime > bounceTime) // No bouncing anymore: { // No contact bouncing anymore: if(IRQ1PrevVal==0 && IRQ1ActVal==1) // Transition 0-->1 { // Place your command for rising signal here... counter1++; if(counter1>255) counter1 = 0; IRQ1PrevTime=IRQ1ActTime; IRQ1PrevVal=IRQ1ActVal; irqFlag=1; } if(IRQ1PrevVal==1 && IRQ1ActVal==0) // Transition 1-->0 { // Place your command for falling signal here... IRQ1PrevVal=IRQ1ActVal; } } IRQ2ActTime=millis(); // Read actual millis time if(IRQ2ActTime - IRQ2PrevTime > bounceTime) // No bouncing anymore: { // No contact bouncing anymore: if(IRQ2PrevVal==0 && IRQ2ActVal==1) // Transition 0-->1 { // Place your command for rising signal here... counter2++; if(counter2>255) counter2 = 0; IRQ2PrevTime=IRQ2ActTime; IRQ2PrevVal=IRQ2ActVal; irqFlag=1; } if(IRQ2PrevVal==1 && IRQ2ActVal==0) // Transition 1-->0 { // Place your command for falling signal here... IRQ2PrevVal=IRQ2ActVal; } } } ISR(PCINT1_vect) { // You have to write your own interrupt handler. Don't change the name! // This code will be called anytime when PCINT23 switches high to low, // or low to high. This is for INT3 and INT4 (both Port C) byte PVal; // Port value (8 Bits) byte IRQ3ActVal; // Actual IRQ3 value byte IRQ4ActVal; // Actual IRQ4 value long unsigned IRQ3ActTime; long unsigned IRQ4ActTime; PVal = PINC; // Read port C (8 bit) IRQ3ActVal = PVal & (1<<PCINT9); // Mask out all except IRQ3 IRQ3ActVal = IRQ3ActVal >> PCINT9; // shift to right for bit0 position IRQ4ActVal = PVal & (1<<PCINT10); // Mask out all except IRQ4 IRQ4ActVal = IRQ4ActVal >> PCINT10; // shift to right for bit0 position IRQ3ActTime=millis(); // Read actual millis time if(IRQ3ActTime - IRQ3PrevTime > bounceTime) // No bouncing anymore: { // No contact bouncing anymore: if(IRQ3PrevVal==0 && IRQ3ActVal==1) // Transition 0-->1 { // Place your command for rising signal here... counter3++; if(counter3>255) counter3 = 0; IRQ3PrevTime=IRQ3ActTime; IRQ3PrevVal=IRQ3ActVal; irqFlag=1; } if(IRQ3PrevVal==1 && IRQ3ActVal==0) // Transition 1-->0 { // Place your command for falling signal here... IRQ3PrevVal=IRQ3ActVal; } } IRQ4ActTime=millis(); // Read actual millis time if(IRQ4ActTime - IRQ4PrevTime > bounceTime) // No bouncing anymore: { // No contact bouncing anymore: if(IRQ4PrevVal==0 && IRQ4ActVal==1) // Transition 0-->1 { // Place your command for rising signal here... counter4++; if(counter4>255) counter4 = 0; IRQ4PrevTime=IRQ4ActTime; IRQ4PrevVal=IRQ4ActVal; irqFlag=1; } if(IRQ4PrevVal==1 && IRQ4ActVal==0) // Transition 1-->0 { // Place your command for falling signal here... IRQ4PrevVal=IRQ4ActVal; } } }
Erklärung zu den Konfigurationsmöglichkeiten
Zuerst sind die benötigten Arduino Pins festzulegen. Spalte Hinweis 1 stellt die auf dem Arduino-Board aufgedruckten Namen dar. Im Setup werden die Interrupt-Pins auf Eingang geschaltet und die internen Pullup-Widerstände an den Eingang gelegt. Die anzugebenden Pin-Nummern sind der Spalte Hinweis 2 zu entnehmen und im Setup anzugeben:
// INT1 --> Make Arduino Pin 11 (PCINT3/Port B.3) an input and set pull up resistor:
pinMode(11, INPUT);
digitalWrite(11, HIGH);
// INT2 --> Make Arduino Pin 12 (PCINT4/Port B.4) an input and set pull up resistor:
pinMode(12, INPUT);
digitalWrite(12, HIGH);
// INT3 --> Make Arduino Analog input 1 (PCINT9/Port C.1) an input and set pull up resistor:
pinMode(15, INPUT);
digitalWrite(15, HIGH);
// INT4 --> Make Arduino Analog input 2 (PCINT10/Port C.2) an input and set pull up resistor:
pinMode(16, INPUT);
digitalWrite(16, HIGH);
In diesem Programmabschnitt werden die Interrupts festgelegt. Bei Pin Change Interrupts gibt es nur einen Interrupt pro Port (siehe Spalte Hinweis 3), nicht für jedes Bit einzeln. Nach Auswahl der zu verwendenden Arduino-Pins wird in der Tabelle unter Hinweis 5 nachgesehen, wie viele verschiedene Ports für die gewählten Pins benötigt werden (PCIE0/PCIE1/PCIE2). In unserem Beispiel liegen die vier Interrupts in den Ports B (=PCIE0) und Port C (=PCIE1) Diese Bitwerte werden auf das Register PCICR "verodert". Sollten bei Eurer Konfiguration andere Ports betroffen sein, dann einfach die Zeilen anpassen oder eine dritte hinzufügen.
PCICR |= (1 << PCIE0); // Set port bit in CICR for INT1 and INT2
PCICR |= (1 << PCIE1); // Set port bit in CICR for INT3 and INT4
Die CPU benötigt noch Informationen, welche Bits des Ports einen Interrupt erzeugen dürfen. Dies geschieht über die Variable PCMASKx. Abhängig von den gewählten Pins in der Tabelle findet Ihr in der Spalte Hinweis 6 die zugehörigen Maskenregister PCMASKx. Auf diese Register werden die Interruptvariablen aus Spalte Hinweis 7 der Tabelle "verodert". In unserem Beispiel fallen PCINT3 und PCINT4 ins Register PCMASK0, während die Analogeingänge mit PCINT9 und PCINT10 ins Register PCMASK1 "verodert" werden. Verwendet man weitere Interrupts, dann einfach die Definitionen erweitern:
// Pin change mask registers decide which pins are enabled as triggers:
PCMSK0 |= (1<<PCINT3); // Set pin interrupt for INT1
PCMSK0 |= (1<<PCINT4); // Set pin interrupt for INT2
PCMSK1 |= (1<<PCINT9); // Set pin interrupt for INT3
PCMSK1 |= (1<<PCINT10); // Set pin interrupt for INT4
IRQ1PrevTime=millis(); // Hold actual time
IRQ2PrevTime=millis(); // Hold actual time
IRQ3PrevTime=millis(); // Hold actual time
IRQ4PrevTime=millis(); // Hold actual time
interrupts(); // Enable interrupts
Mit interrupts() wird das Interruptsystem scharf geschaltet.
In diesem Sketch werden die Interruptgruppen PCIE0 und PCIE1 verwendet. Korrespondierend dazu gibt es genau festgelegte Namen für die Interrupt-Servie-Routinen, die nicht geändert werden dürfen. Diese befinden sich in der Spalte Hinweis 8. Sollte auch noch PCIE2 benötigt werden, benötigt man eine weitere Service Routine mit dem Namen PCINT2_vect. In diesem Sketch werden ISR(PCINT0_vect) und ISR(PCINT1_vect) benötigt.
Die Interrupt Service Routinen müssen jeweils noch auswerten, welches erlaubte Bit den Interrupt hervorgerufen hat. Wenn man steigende und fallende Flanken unterscheiden möchte, muss dies ebenfalls in der Routine erledigt werden. Generell gilt, dass man in den Interrupt Service Routinen möglichst wenig Code unterbringen sollte, um eine schnelle Auführung zu ermöglichen. Im Sketch sind die Nutzbefehle lediglich
counter2++;
if(counter2>255) counter2 = 0;
Wie kann man die Impulse per Software entprellen? Ein Prellimpuls ist ein schneller, mehrfacher Wechsel von 0->1 und 1->0, der eine gewisse Zeit andauert (meist 2-20ms, je nach Kontakt). In der Service Routine wird bei einem gültigen Pegelwechsel eines Signals ein Timer gesetzt, der bei jedem IRQ-Aufruf geprüft wird. Ist die Zeit seit dem letzten Aufruf kleiner als die Prellzeit, dann wird der IRQ nicht weiter bearbeitet. Ist er größer als die Prellzeit, dann wird der Pegelwechsel als gültig angenommen. In der IRQ Service Routine wird die aktuelle Zeit über millis() in der Variablen IRQ1ActTime festgehalten und mit der Zeit des letzten Interrupts (IRQ1PrevTime) mit gültigem Pegelwechsel verglichen. Ist die Differenz größer als bounceTime, wird die Flankenerkennung gestartet.
IRQ1ActTime=millis(); // Read actual millis time
if(IRQ1ActTime - IRQ1PrevTime > bounceTime) // No bouncing anymore:
{
Diese prüft ab, ob ein Wechsel von 0-->1 erfolgt ist:
if(IRQ1PrevVal==0 && IRQ1ActVal==1) // Transition 0-->1
{
In einem zweiten Schritt wird der Wechsel von 1-->0 geprüft
if(IRQ1PrevVal==1 && IRQ1ActVal==0) // Transition 1-->0
{
Die Zeitvariablen müssen als "volatile" definiert werden, damit sowohl das Hauptprogramm als auch die Service Routine darauf zugreifen können.
Der Sketch befindet sich im Downloadbereich.










