Inhaltsverzeichnis
Interrupts
Interrupts sind ein wichtiger Bestandteil jeder Controller Programmierung. Leider findet man im Netz zuviele Informationen darüber, (darunter leider auch viele Wiedersprüchliche), auf den Arduino Seiten sowie in der Sprache wird das Thema aber nur sehr Stiefmütterlich behandelt.
Was ist ein Interrupt?
Also ein Interrupt ist ein Unterbrechung des „normalen“ Programms. Wird ein Interrupt (INT) ausgelöst, schaut der Controller nach, ob der Interrupt behandelt werden muss, und wo das Programm steht. Dann schaltet er alle weiteren Interrupts aus, sichert er den augenblicklichen Stand wichtiger Register und des Adresszählers und springt dann zu der Behandlungsroutine. Ist die Fertig, wird der ursprüngliche Zustand wiederhergestellt, auch das Adressregister, es werden die Interrupts wieder frei geschaltet und dann geht's an der ursprünglichen Stelle einfach weiter.
Viele Quellen können Interrupts auslösen, z.B. die Timer, serielle Schnittstelle, A/D Wandler, WatchDog und sogar jeder Pin des Controllers.
Für uns gibt es 2 wichtige Interruptquellen.
- External Interrupt Request EIR genannt.
Das ist der Interrupt, der per attachInterrupt beim Arduino auf verschiedene Pins gelegt werden kann.
Beim Uno (also bei den 328 basierten Arduinos) sind das die Pin 2 und 3. Wobei Pin 2 den Interrupt 0 und Pin 3 den Interrupt 1 auslöst. Ausgelöst kann bei LOW am Pin, oder bei fallender Flanke, oder steigender Flanke, oder bei jedem Pegelwechsel.
Bei Uno gibt's dann insgesamt 4 EIR (Pin3, 2, 0, 1 Reihenfolge = Nummer). Beim Mega sinds derer 6 ( 2, 3, 21, 20, 19, 18)
WICHTIG: Für jeden Pin gibt es eine eigene Interruptroutine.
- PinChangeInterrupt
Der PinChangeInterupt (PCI) kann durch einen Pegelwechsel auf jeden Pin des Arduinos getriggert werden. Man kann die Pins maskieren, damit nur gewünschten Pins einen Interrupt werfen. Aber im Gegensatz zum EIR gibt es beim PCI nur eine Interruptroutine. In der muss man alle Pins behandeln. D.h. zunächst muss man feststellen welcher Pin überhaupt den Interrupt geworfen hat.
Für unsere RC Fernbedienung:
Wichtig wird das ganze, wenn man mehr als 1 Kanal des Empfängers einer Fernsteuerung überwachen will. z.B. für einen Kreuzmischer. Mit dem EIR kann man beim UNO max. 2 Kanäle verwenden, beim Mega 6. Mit dem PCI kann man soviele Kanäle überwachen wie man freie Pins hat.
Mir persönlich reichten 2 Kanäle bisher eigentlich immer. Deswegen implementiert auch die RCReceiver Bibliothek auch der Weg über den EIR und nicht den PinChange Interrupt.
Wie kann ich das benutzen? EIR
Für die Interrupt Behandlung gibt es 2 Funktionen. Diese heben wir hier schon kennen gelernt. Um jetzt z.B. einen Wechsel am Pin 3 zu behandeln, müssen wir folgendes programmieren:
void setup() { ... // RC Interrupthandler registrieren attachInterrupt(1, RcIntHandler, CHANGE); // Interrupt 1 ist Pin 3 ... } /* Interrupthandler zum messen der RC Implusdauer */ void RcIntHandler() { ... }
Nun wird bei jedem Wechsel des Pegels am Pin 3 unsere Funktion RcIntHandler()
aufgerufen. Das können wir uns natürlich für die Messung des Empfängersignales zu nutze machen.
Mit LOW als letzten Parameter, eine kleinen Lichtschranke und einem kleinen Schutz gegen Prellen, könnten wir z.B. auch einen Rundenzähler für eine Autorennbahn bauen.
Und der Pin Change Interrupt (PCINT)?
Der Pin Change Interrupt kann auf jeden Pin angewendet werden und wird bei jedem Flankenwechsel ausgelöst. Es gibt auch dazu Bibliotheken, aber hier werde ich mal den Weg der WEigenimplementierung gehen. Zunächst einmal müssen wir wissen, auf welche Pins wir horchen wollen. Dazu hilft das Pin Assigment:
Für dieses Beispiel wird der Pin 11 gewählt. (Zur Überwachung mehrere Pins kommt später noch mehr) Aus dem obigen Bild kann man ablesen, das der Pin 11 auf dem Port B Bit 3 liegt und PCINT3 bedient.
Um den Pin Change Interrupt zu aktivieren, muss zunächst der PCINT für den jeweiligen Port eingeschaltet werden. Das macht man mit
PCICR |= 0b00000001; // Port B // PCICR |= 0b00000010; // Port C // PCICR |= 0b00000100; // Port D
PCICR ist das Pin Change Interrupt Control Register, hier mal ein Auszug aus dem Datenblatt:
zusätzlich muss man jeden Pin des Ports nochmal einzeln aktivieren:
PCMSK0 |= 0b00001000; // Einschalten Pin PB3, PCINT3, Physikalisch Pin 17, Arduino Pin 11 // PCMSK1 |= 0b00010000; // Einschalten Pin PC4, PCINT12, Physikalisch Pin 27, Arduino Pin Analog A4, bzw. Digital 18 // PCMSK2 |= 0b10000001; // Einschalten Pins PD0 & PD7, PCINT16 & PCINT23, Physikalisch Pin 2 und 13, Arduino 0 und 7
Auch hier mal der Datenblattausschnitt:
Nun muss ja auch noch Code für den Sprung angegeben werden, dazu gibt es 3 vordefinierte Marken:
ISR(PCINT0_vect){} // Port B, PCINT0 - PCINT7 // ISR(PCINT1_vect){} // Port C, PCINT8 - PCINT14 // ISR(PCINT2_vect){} // Port D, PCINT16 - PCINT23
Alles zusammen, sieht das ganze jetzt so aus:
void setup() { cli(); PCICR |= 0b00000001; // Port B PCMSK0 |= 0b00001000; // Einschalten Pin PB3, PCINT3, Physikalisch Pin 17, Arduino Pin 11 sei(); } ISR(PCINT0_vect) // Port B, PCINT0 - PCINT7 { // hier kommt dein Code hin }
Wichtig ist hier, man sollte beim Aktivieren von Interrupts in der Setup Routine immer zunächst die Interrupts abschalten (cli()
) und nachdem alles eingestellt ist wieder aktivieren (sei()
). Vor dem Start der Service Routine (ISR) werden alle Interrupt ausgeschaltet und danach wieder eingeschaltet. Will man während der ISR andere Interrupts weiter verarbeiten, kann man das auch mit cli()
und sei()
steuern. Will man nur auf einen bestimmten Pegel, low oder high, muss zunächst einmal der aktuelle Pegel abgefragt werden. Denn der PCINT wird ja bei jedem Flankenwechsel ausgelöst. (Im Gegensatz zum ISR, wo man das per Maske setzen kann)
Statt sei();
und cli();
kann an gerne auch interrupts();
und noInterrupts();
benutzen, wenn einem cli und sei zu kryptisch sind.
Setzt man den PCINT für mehrere Pins (auf einem Port), muss als erstes natürlich zunächst festgestellt werden, welcher Pin der Auslöser war.