Über den Beitrag
In dieser Fortsetzung meines letzten Beitrages werde ich Pin Change Interrupts behandeln. AVR Mikrocontroller und darauf basierende Arduinos sind relativ spärlich mit jenen externen Interrupts ausgestattet, die ihr über attachInterrupt()
einrichten könnt. Die gute Nachricht ist jedoch, dass euch an allen I/O Pins stattdessen die Pin Change Interrupts zur Verfügung stehen. Sie sind nicht ganz so komfortabel und außerdem ein wenig schwieriger zu konfigurieren, da man sich mit den Registern der zugrundeliegenden Mikrocontroller auseinandersetzen muss. Aber keine Sorge, Raketenwissenschaft ist das auch nicht.
Ich werde den Umgang mit Pin Change Interrupts ausführlich am ATmega328P (Arduino UNO, Nano, Pro Mini) erklären und dann am Beispiel des ATtiny85 zeigen, dass die Systematik dahinter bei allen AVR-Mikrocontrollern sehr ähnlich ist.
Folgendes kommt auf euch zu:
- Ein Blick in den ATmega328P
- Praktische Beispiele mit dem ATmega328P
- Pin Change Interrupts auf anderen AVR Mikrocontrollern
- Pin Change Interrupts auf MiniEVB Boards
Ein Blick in den ATmega328P
Ports, Pins und PCINT
Die I/O Pins des ATmega328P sind in den drei Ports PORTB, PORTC und PORTD organisiert, die jeweils bis zu 8 Pins umfassen. Die Pins heißen entsprechend: PB[7:0], PC[6:0], PD[7:0]. Dabei bedeutet die Schreibweise „PB[7:0]“: PB7, PB6, PB5 … PB1, PB0. Jeder Pin hat eine eindeutige Pin Change Interrupt Nummer, nämlich PCINT[23:0].

Pin Change Interrupts am ATmega328P – die relevanten Register
Für jeden Port steht nur ein Pin Change Interrupt zur Verfügung, der jedoch an jedem einzelnen Pin des Ports ausgelöst werden kann, sofern der Pin dafür eingerichtet worden ist. Um einen Pin für Change Interrupts zu aktivieren, setzt ihr das zugehörige Bit im zuständigen Pin Change Mask Register PCMSK[2:0]. Hier eine Übersicht:
![Pin Change Interrupt Mask Register PCMSK[0:2]](https://wolles-elektronikkiste.de/wp-content/uploads/2023/09/pcmsk_registers-1-1024x244.png)
Überdies müsst ihr noch den Interrupt selbst aktivieren. Das macht ihr, indem ihr das zuständige Pin Change Enable Bit PCIE[2:0] im Pin Change Interrupt Control Register PCICR setzt.

PCIE0 ist für PORTB zuständig, PCIE1 für PORTC und PCIE2 für PORTD.
Im Falle eines Interrupts wird das entsprechende Flag PCIF[2:0] im Pin Change Interrupt Flag Register PCIFR gesetzt:

Praktische Beispiele mit dem ATmega328P
Ein einzelner Pin Change Interruptpin
Im ersten Beispiel richten wir nur einen einzigen Interruptpin ein. Dafür nehmen wir Arduino Pin 6 (PD6 / PCINT22), dessen Level auf Tasterdruck von HIGH nach LOW wechseln soll. Dazu schalten wir den internen Pull-Up Widerstand zu. Als Folge des Tasterdrucks soll die Board LED für eine halbe Sekunde leuchten.

Hier der Beispielsketch dazu:
const int ledPin = 13; // = LED_BUILTIN const int interruptPin = 6; volatile bool event = false; ISR (PCINT2_vect){ // PCINT2_vect: interrupt vector for PORTD event = true; } void setup() { pinMode(interruptPin, INPUT_PULLUP); PCICR = (1<<PCIE2); // enable PCINT[23:16] interrupts PCMSK2 = (1<<PCINT22); // D6 = PCINT22 } void loop() { if(event){ digitalWrite(ledPin, HIGH); delay(500); digitalWrite(ledPin, LOW); event = false; } }
Erläuterungen zum Sketch:
- Mit
PCICR = (1<<PCIE2)
werden Pin Change Interrupts an PORTD erlaubt. PCMSK2 = (1<<PCINT22)
aktiviert den Pin Change Interrupt an PD6 (= Arduino Pin 6).- Der ISR übergeben wir den zuständigen Interruptvektor
PCINT2_vect
. Wie wohl die meisten schon richtig vermuten, wärePCINT1_vect
der zuständige Vektor für PORTC undPCINT0_vect
der Vektor für PORTB.
In diesem ersten, minimalistischen Beispiel wird der Pin Change Interrupt sowohl bei Wechseln von HIGH nach LOW als auch von LOW nach HIGH berücksichtigt. Haltet ihr den Taster länger als 500 Millisekunden gedrückt, wird die LED beim Loslassen noch einmal aufleuchten.
Das Tasterprellen ist in dieser Konstellation unauffällig. Da ich auf Tasterprellen und Debouncing schon im letzten Beitrag intensiv eingegangen bin, werde ich das hier nicht noch einmal wiederholen.
Und wer noch Nachholbedarf beim Thema Binäroperationen (z.B. 1<<PCIE2
)hat, der könnte sich diesen Beitrag anschauen.
Mehrere Pin Change Interruptpins an verschiedenen Ports
Im nächsten Schritt werden wir den Zustand von drei Tastern überwachen. Diesmal geben wir auf dem seriellen Monitor aus, wenn ein Interrupt ausgelöst wurde und an welchem Pin das passiert ist. Dazu verwenden wir drei Pins aus verschiedenen Ports:
- PB1 = Arduino Pin 9 = PCINT1
- PC2 = Arduino Pin A2 = PCINT10
- PD5 = Arduino Pin 5 = PCINT21
Hier die Schaltung:

Und hier der zugehörige Sketch:
const int interruptPin_1 = 9; //PB1 const int interruptPin_2 = A2; //PC2 const int interruptPin_3 = 5; //PD5 volatile bool event_1 = false; volatile bool event_2 = false; volatile bool event_3 = false; ISR (PCINT0_vect){ event_1 = true; } ISR (PCINT1_vect){ event_2 = true; } ISR (PCINT2_vect){ event_3 = true; } void setup() { Serial.begin(115200); pinMode(interruptPin_1, INPUT_PULLUP); pinMode(interruptPin_2, INPUT_PULLUP); pinMode(interruptPin_3, INPUT_PULLUP); PCICR = (1<<PCIE2) | (1<<PCIE1) | (1<<PCIE0); PCMSK0 = (1<<PCINT1); PCMSK1 = (1<<PCINT10); PCMSK2 = (1<<PCINT21); } void loop() { if(event_1){ Serial.println("Interrupt at pin 9"); delay(300); event_1 = false; } if(event_2){ Serial.println("Interrupt at pin A2"); delay(300); event_2 = false; } if(event_3){ Serial.println("Interrupt at pin 5"); delay(300); event_3 = false; } }
Erläuterung zu three_pcint_interrupts_at_three_ports.ino
- Mit
PCICR = (1<<PCIE2) | (1<<PCIE1) | (1<<PCIE0);
werden Interrupts an PORTB, PORTC und PORTD erlaubt. - Die drei Pins werden über ihr zuständiges PCMSK[2:0] Register als Interruptpins eingerichtet.
- Für jeden Pin Change Interrupt Pin ist eine eigene ISR zuständig.
- Die Wartezeiten von 300 Millisekunden kaschieren das Tasterprellen und den erneuten Interrupt beim Loslassen des Tasters.
Mehrere Pin Change Interrupt Pins an einem Port
Im nächsten Beispiel verwenden wir Pin Change Interrupts für drei Taster an den Pins 5, 6 und 7, also PCINT21, PCINT22 und PCINT23. Hier müssen wir jetzt berücksichtigen, dass alle drei Pins zu PORTD gehören und uns deswegen nur ein Interrupt zur Verfügung steht. Aber da wir ja die Level der Pins im Grundzustand kennen, überprüfen wir in der ISR einfach mittels digitalRead()
welcher Pin abweicht und damit „der Schuldige“ ist.
Um ein wenig Abwechslung hineinzubringen, ziehen wir Pin 5 und Pin 7 mit Pull-Down Widerständen auf LOW. Pin 6 verwendet den internen Pull-Up Widerstand.

Bei der Schaltung ist zu beachten, dass die Anschlüsse, die sich an den Tastern links oben und links unten bzw. rechts oben und rechts unten befinden, permanent verbunden sind.
Hier der Sketch:
const int interruptPin_1 = 5; const int interruptPin_2 = 6; const int interruptPin_3 = 7; volatile bool event_1 = false; volatile bool event_2 = false; volatile bool event_3 = false; ISR (PCINT2_vect){ if(digitalRead(interruptPin_1)){ event_1 = true; } else if(!digitalRead(interruptPin_2)){ event_2 = true; } else if(digitalRead(interruptPin_3)){ event_3 = true; } } void setup() { Serial.begin(115200); pinMode(interruptPin_1, INPUT); pinMode(interruptPin_2, INPUT_PULLUP); pinMode(interruptPin_3, INPUT); PCICR = (1<<PCIE2); PCMSK2 = (1<<PCINT23)|(1<<PCINT22)|(1<<PCINT21); } void loop() { if(event_1){ Serial.println("Interrupt at pin 5"); delay(300); event_1 = false; } if(event_2){ Serial.println("Interrupt at pin 6"); delay(300); event_2 = false; } if(event_3){ Serial.println("Interrupt at pin 7"); delay(300); event_3 = false; } }
Durch die Prüfung mittels digitalRead()
werden die Interrupts beim Loslassen der Taster ignoriert (es sei denn, der Taster prellt beim Loslassen). Das kann erwünscht sein – oder auch nicht. Im übernächsten Beispiel komme ich darauf zurück, wie ihr Pin Changes in beide Richtungen an einem Port detektieren könnt.
Einen ganzen Port verwenden
Jetzt werden wir (fast) den ganzen PORTD verwenden. Dieser umfasst die Arduino Pins 0 bis 7, wobei die Pins 0 und 1 für Pin Change Interrupts nicht gut geeignet sind, da es sich um RX und TX handelt. Wir beschränken uns also auf die Pins 2 bis 7 bzw. PD[7:2].
Dazu kommt die folgende Schaltung zum Einsatz:
Ich habe die Pins 3, 5 und 7 mit Pull-Down Widerständen versehen, die Pins 2, 4 und 6 werden mit ihren internen Pull-Up Widerständen auf HIGH gezogen. Wenn ihr lediglich Taster auswerten wollt, dann wäre es wesentlich einfacher, alle Pins auf dasselbe Level zu ziehen. Die hier verwendeten Taster sollen jedoch lediglich stellvertretend für irgendwelche Interruptauslöser stehen, und das könnte eine Mischung aus LOW-aktiven und HIGH-aktiven Vertretern sein.
Bei sechs Pins würde es etwas unhandlich, alle Taster- bzw. Pinzustände im Falle eines Interrupts mit digitalRead()
abzufragen und individuelle Flags event_[1:6]
zu verwenden. Das kann man smarter lösen – so zum Beispiel:
int interruptPin[6] = {2,3,4,5,6,7}; volatile bool event = false; volatile byte changedPin = 0; volatile unsigned long lastInterrupt = 0; const byte interruptPinMask = 0b11111100; const byte intPinPolarityMask = 0b01010100; ISR (PCINT2_vect){ if(millis()-lastInterrupt > 300){ byte interruptPinsStatus = PIND & interruptPinMask; interruptPinsStatus ^= intPinPolarityMask; if(interruptPinsStatus){ changedPin = log(interruptPinsStatus)/log(2); // short, but slow! Alternative below: // changedPin = 0; // while(!(interruptPinsStatus & 1)){ // changedPin++; // interruptPinsStatus = (interruptPinsStatus >> 1); // } event = true; } lastInterrupt = millis(); } } void setup() { Serial.begin(115200); pinMode(interruptPin[0], INPUT_PULLUP); pinMode(interruptPin[1], INPUT); pinMode(interruptPin[2], INPUT_PULLUP); pinMode(interruptPin[3], INPUT); pinMode(interruptPin[4], INPUT_PULLUP); pinMode(interruptPin[5], INPUT); PCICR = (1<<PCIE2); PCMSK2 = interruptPinMask; } void loop() { if(event){ Serial.print("Interrupt at pin "); Serial.println(changedPin); event = false; } }
Erläuterungen zu pcint_interrupt_generalized.ino
- Die Interruptpins werden als Array angelegt.
- Die
interruptPinMask
gibt an, welche PORTD Pins als Interruptpin dienen („1“) und welche ignoriert werden („0“). intPinPolarityMask
enthält die Information, welches Level die PORTD Pins im Grundzustand haben (Taster nicht gedrückt).- Im Falle eines Interrupts wird der Level der Pins mit PIND ermittelt. Das erspart die Abfragen mittels
digitalRead()
.- Durch
PIND & interruptPinMask
ignorieren wir den Zustand der Pins 0 und 1. - Mittels
interruptPinsStatus ^= intPinPolarityMask
finden wir heraus, welcher Pin vom Grundzustand abweicht. Der logische Operator „^“ (XOR, exklusives Oder) liefert eine 1, wenn genau einer der Operanden 1 ist.- Wurde der Interrupt beispielsweise an PD4 (4) ausgelöst, hätte
interruptPinsStatus
nach den Bitoperationen den Wert 0b00010000.
- Wurde der Interrupt beispielsweise an PD4 (4) ausgelöst, hätte
- Durch
- Die Bedingung
if(interruptPinsStatus){...
fängt Fälle ab, in denen kein auslösender Interruptpin ermittelt werden kann.
Im Falle von PORTD kommt uns entgegen, dass PDx dem Arduino Pin x entspricht. Um x (changedPin
) zu ermitteln, müssen wir die Stelle finden, an der die 1 in interruptPinsStatus
steht. Dafür gibt es verschiedene Möglichkeiten:
- x = log2(interruptPinsMask). Da aber kein Logarithmus zur Basis 2 als Funktion verfügbar ist, greifen wir auf die Logarithmenregeln zurück: log2(x) = log10(x)/log10(2). Das ist schöner, kurzer Code, allerdings ist das Errechnen eines Logarithmus recht aufwendig und entsprechend langsam. Ich habe gute hundert Mikrosekunden für diese Berechnung ermittelt.
- Ihr verschiebt
interruptPinsStatus
so oft nach rechts, bis dessen Wert 1 ist (siehe auskommentierter Code). - Ihr teilt
interruptPinsStatus
so oft durch 2, bis der Rest 1 ist.
Ansonsten kann man natürlich auch switch...case
Strukturen einsetzen.
HIGH-LOW und LOW-HIGH Wechsel verfolgen
Im letzten und vorletzten Sketch haben wir nur Level-Wechsel in eine Richtung, also Abweichungen vom Grundzustand, berücksichtigt. Um Wechsel in beide Richtungen auszuwerten, führen wir die Variable lastInterruptsPinStatus
ein, in der wir den Status von PIND (nach Bereinigung um die nicht berücksichtigten Pins 0 und 1) zum jeweiligen Zeitpunkt des letzten Interrupts speichern. Bei einem erneuten Interrupt vergleichen wir den neuen Status (currentInterruptPinsStatus
) mit dem vorherigen (anstelle intPinPolarityMask
).
Ob ein HIGH-LOW oder LOW-HIGH Wechsel stattgefunden hat, erkennen wir daran, ob der neue Statuswert kleiner oder größer ist.
Hier erst einmal der Sketch:
int interruptPin[6] = {2,3,4,5,6,7}; volatile bool event = false; volatile byte changedPin = 0; const byte interruptPinMask = 0b11111100; volatile byte lastInterruptPinsStatus = 0b01010100; volatile bool risingEdge = false; ISR (PCINT2_vect){ byte currentInterruptPinsStatus = PIND & interruptPinMask; if(currentInterruptPinsStatus > lastInterruptPinsStatus){ risingEdge = true; } else{ risingEdge = false; } byte statusCopy = currentInterruptPinsStatus; currentInterruptPinsStatus ^= lastInterruptPinsStatus; lastInterruptPinsStatus = statusCopy; if(currentInterruptPinsStatus){ changedPin = 0; while(!(currentInterruptPinsStatus & 1)){ changedPin++; currentInterruptPinsStatus = (currentInterruptPinsStatus >> 1); } event = true; } } void setup() { Serial.begin(115200); pinMode(interruptPin[0], INPUT_PULLUP); pinMode(interruptPin[1], INPUT); pinMode(interruptPin[2], INPUT_PULLUP); pinMode(interruptPin[3], INPUT); pinMode(interruptPin[4], INPUT_PULLUP); pinMode(interruptPin[5], INPUT); PCICR = (1<<PCIE2); PCMSK2 = interruptPinMask; } void loop() { if(event){ Serial.print("Interrupt at pin "); Serial.println(changedPin); Serial.print("Pin went: "); if(risingEdge){ Serial.println("HIGH"); } else{ Serial.println("LOW"); } event = false; } }
Wenn ihr den Sketch ausprobiert, werdet ihr wahrscheinlich feststellen, dass ich das Debouncing entfernt habe. Es verträgt sich nicht gut mit diesem Konzept, da Statusänderungen, die durch Tasterprellen ausgelöst werden, unter den Tisch fallen. Andererseits ist der Sketch in dieser Form auch nicht gut mit prellenden Tastern verträglich, da inmitten der Ausgabe auf dem seriellen Monitor erneute Interrupts auftreten können (Serial.print()
ist ziemlich langsam). Zuverlässig arbeitet der Sketch hingegen mit Hardware-entprellten Tastern (siehe letzter Beitrag) oder anderen Interruptquellen, die nicht zu schnell hintereinander auslösen.
Pin Change Interrupts auf anderen AVR Mikrocontrollern
Dadurch, dass wir direkt auf die Register des Mikrocontrollers zugreifen, ist der Code für Pin Change Interrupts für diesen spezifisch. Die Übertragung auf andere AVR-Mikrocontroller ist aber nicht besonders schwierig. Nehmt euch das Datenblatt vor und schaut im Kapitel Externe Interrupts, Unterpunkt: Pin Change Interrupts.
Beispiel: ATtiny85
Wir spielen das einmal am ATtiny85, der z. B. auf dem Digispark Board verwendet wird, durch. Er besitzt nur einen einzigen Pin Change Interrupt, den ihr über das Pin Change Enable Bit PCIE im Global Interrupt Mask Register GIMSK „einschaltet“:

Die Pins aktiviert ihr im Pin Change Mask Register PCMSK.

So könnte dann ein Sketch aussehen:
const int ledPin = 4; const int interruptPin = 3; volatile bool event = false; ISR (PCINT0_vect){ event = true; } void setup() { pinMode(ledPin, OUTPUT); pinMode(interruptPin, INPUT_PULLUP); GIMSK |= (1<<PCIE); // General Interrupt Mask Register PCMSK = (1<<PCINT3); // 3 = PCINT3 } void loop() { if(event){ digitalWrite(ledPin, HIGH); delay(500); digitalWrite(ledPin, LOW); event = false; } }
Pin Change Interrupts auf MiniEVB Boards
Die auf dem LGT8F328P basierenden MiniEVB Boards weisen eine hohe Kompatibilität mit dem ATmega328P auf. Besonders einfach ist die Portierung von Sketchen für ATmega328P-basierte Arduinos auf das LQFP32-MiniEVB Board. Ihr könnt die Sketche 1:1 übernehmen. Bei den LQFP48-basierten MiniEVB Boards kommen lediglich noch einige Pins hinzu, wohingegen bei den SSOP20-basierten Vertretern einige Pins nicht zur Verfügung stehen.
Ausblick
Im nächsten Beitrag behandele ich Timer Interrupts. Im Gegensatz zu den Pin Change Interrupts, die eine Besonderheit der AVR Mikrocontroller (und der LGT8F328P-Familie) darstellen, sind die Timer Interrupts wieder für alle Mikrocontroller relevant.
Hallo Wolfgang,
vielen Dank auch von mir für diese Einführung in das Thema. Auch wenn trotz mehrmaligem Lesens manches für mich wohl fremd bleiben wird, war das zusammen mit dem ersten Teil eine sehr gute Ergänzung um die Funktion von exisitierenden Beispielen der Interruptprogrammierfehlt, wäre daung um den Attiny85 dann auch zu verstehen und entsprechend anzupassen.
Was mir jetzt noch fehlt, wäre etwas Vergleichbares für die neue 0/1/2 Serie inklusive Attiny412. Erwähnt hast du den ja kurz, aber mit keinem Wort angedeutet, dass da alles (oder sind es nur die Registernamen?) anders ist. So zumindest mein Eindruck. Die Tatsache, dass zu dem Thema für diese Typen aktuell auch noch viel weniger Beispiele im Netz gibt, erschwert es mir gerade zusätzlich.
Aber falls du da möglicherweise Tipps oder (für Laien verständliche) Literatur auf Lager hast, fände ich das super. Das Datenblatt und AN1982 ist für mich schon recht schwere Kost.
vg,
Dieter
Hallo Dieter,
wenn du wissen möchtest, wie die neueren ATtinys der tinyAVR 0/1/2 Serie auf Registerebene programmiert werden, dann schaue dir vielleicht mal diesen Abschnitt in meinem Beitrag über den Arduino Nano Every (ATmega4808/4809) an:
https://wolles-elektronikkiste.de/arduino-nano-every-ein-deep-dive#reg_programming_in_c
Und vielleicht dieses Dokument zur Vertiefung:
https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ApplicationNotes/ApplicationNotes/AVR1000b-Getting-Started-Writing-C-Code-for-AVR-DS90003262B.pdf
Wahrscheinlich wirst du fragen, was der ATmega4808/4809 mit den Attinys zu tun hat. Dazu kannst du mal in die Datenblätter schauen:
https://ww1.microchip.com/downloads/en/DeviceDoc/ATmega4808-4809-Data-Sheet-DS40002173A.pdf
https://ww1.microchip.com/downloads/en/DeviceDoc/40001911A.pdf
Wenn du dort beispielsweise in das Kapitel 16.4 (Register Summary PORTx) oder in 20.4. (Register Summary Timer/Counter A) wirst du sehen, dass der Aufbau weitgehend identisch ist (und damit auch die Interruptprogrammierung, die ich auch in dem Arduino Nano Every Artikel beschreibe). Die neue Art der Programmierung ist etwas gewöhnungsbedürftig, aber sehr logisch.
Ich muss mal schauen, ob ich den Teil aus dem Arduino Nano Every Artikel noch einmal unter dem Gesichtspunkt der ATtinys wiederhole. Es ist allerdings 90% Copy/Paste. Das hält mich noch davon ab.
VG, Wolfgang
Danke für den Hinweis. Über den Zwischenweg auf der Suche nach Information zu „PINxCTRL “ war ich tatsächlich genau dort jetzt auch schon gelandet. Und hatte schnell festgestellt, dass es sich um die gleiche Familie handelt.
Die kleinen AVRs eignen sich vom Pinout übrigens ausgezeichnet als Ersatz für ungelabelte 8-pin Steuerprozessoren auf diversen Chinaboards auch im Batteriebetrieb. Die Originale sind zwar sicherlich noch um ein vielfaches günstiger, aber vermutlich nicht so gut dokumentiert.