Portexpander (PCF8574) – Interruptgesteuerte Abfrage

Im letzten Artikel über den PCF8574 Baustein wurden einzelne Eingänge erkannt und einzelne Ausgänge geschaltet. Dafür war es notwendig etwas tiefer in das Thema der Bitmanipulation einzusteigen um zu zeigen, wie einzelne Bits gezielt verändert werden können.

Das Portexpander-Modul ist Teil des GREETBoard Projekts, mit dem ich mich dem Thema AVR Mikrocontroller nähern möchte. Es besteht aus einzelnen Modulen mit unterschiedlichen Funktionen, die über Kabel und RJ12-Stecker verbunden werden.

Diesmal werde ich mich der interruptgesteuerten Abfrage des PCF8574 widmen. Der Unterschied zur Abfrage und Ansteuerung im letzten Artikel liegt darin, dass erst dann ein Lesevorgang gestartet wird, wenn des Portexpander-Modul ein Interrupt-Signal sendet.

Der Interrupt am PCF8574

Sobald an einem der Ein-/Ausgänge des PCF8574 eine steigende oder fallende Flanke entdeckt wurde, wird ein Interrupt ausgelöst. Das bedeutet, dass die Interrupt-Leitung auf 0 (GND) gezogen wird. Damit eine logische 1 (Vcc, bzw. 5V) sicher erkannt wird, ist es nötig die Interrupt-Leitung über einen Pull-Up Widerstand auf +5V zu ziehen.

PCF8574 - Pull Up Widerstand der Interrupt-Leitung

PCF8574 – Pull Up Widerstand der Interrupt-Leitung

Das Interrupt-Signal wird aber auch von alleine wieder zurückgesetzt, wenn eine der folgenden zwei Bedingungen erfüllt werden:

  1. Der zuvor geänderte Eingang wird wieder auf seinen ursprünglichen Wert zurückgesetzt
  2. Daten wurde vom PCF gelesen oder der PCF wurde mit Daten beschrieben

Wurde ein anderer PCF8574 in I²C-Netz beschrieben oder gelesen, hat dies keinen Einfluss auf das Interrupt-Signal anderer PCF-Bausteine im gleichen Netz.

Sehr interessant finde ich die Erkenntnis, dass das Interrupt-Signal wieder verschwindet, wenn ein Taster des PCF8574-Portexpander Moduls nach dem Drücken losgelassen wird. Ebenso interessant ist aber auch, dass am besagten Modul nichts verändert oder eingestellt werden muss, damit Interrupts genutzt werden können. Es ist “lediglich” ein programmiertechnisches Problem am Mikrocontroller. Wenn’s weiter nichts ist…

Den Interrupt aktivieren

Mit Interrupt-Routinen, den sogenannten ISR’s, habe ich bereits gearbeitet, so dass diese Hürde zu schaffen ist. Diesmal wird jedoch ein externes Interrupt verwendet, also ein Signal, dass von außen auf den Mikrocontroller einwirkt. Dazu schaue ich mir nochmal das Schaltbild des GREETBoard ATMega32 an um herauszufinden, welchen der drei Interrupts für I²C benutzt wird.

Version 0.6 des GREETBoard ATMega32

Version 0.6 des GREETBoard ATMega32

OK, es ist also der INT2 (am PB2). Diesen gilt es zu aktivieren, damit ein Interrupt überhaupt erst erkant werden kann. Und dafür werfe ich ein Blick in das Datenblatt des ATMega32 – um genau zu sein auf Seite 67. Dort werden zwei Register angesprochen, die für die Aktivierung des INT2 zuständig sind – MCUCSR und GICR. Hinzu kommt noch das GIFR.

MCUCSR – MCU Control and Status Register

Der INT2 ist ein sogenannter asynchroner externe Interrupt, was soviel bedeutet, dass er keinen externen Takt benötigt, wie es, laut Datenblatt, bei INT0 und INT1 nötig ist. Ich lasse das mal so im Raum stehen, da ich noch nicht mit diesen Interrupts gearbeitet habe. Dies ist aber der Grund, warum der INT2 u.a. mit dem MCUCSR aktiviert wird. Die beiden anderen benötigen hierfür den MCUCR (MCU Control Register) – klingt sehr ähnlich, oder nicht?

Das Bit 6 ist für das ISC2 (Interrupt Sense Control 2) zuständig und regelt, ob der Interrupt bei steigender oder fallender Flanke des Interrupt-Signals ausgelöst wird. Der Standardwert ist 0 (Low).

MCUCSR -> Bit 1 -> ISC2

High => steigende Flanke
Low  => fallende Flanke

GICR – General Interrupt Control Register

Das zweite Register dient der eigentlichen Aktivierung des Interrupts. Dafür muss lediglich das Bit 5 (INT2) auf 1 gesetzt werden. Dadei sollte auch beachtet werden, dass ein Interrupt auch dann ausgelöst wird, wenn der INT2 am Mikrocontroller als Ausgang betrieben wird und dieser auf Low gesetzt wird.

GICR -> Bit 5 -> INT2

High => INT2 eingeschaltet
Low  => INT2 ausgeschaltet

GIFR – General Interrupt Flag Register

Das GIFR wird in dem Moment gesetzt, wenn ein Interrupt-Signal entdeckt wird. Ist dann auch noch der INT2 eingeschaltet (GICR = High), wird ein Sprung in die entsprechende ISR (Interrupt Service Routine) durchgeführt. Ist die ISR vollständig verarbeitet worden, wird GIFR wieder auf Low gesetzt. Wer bereits ein paar Informationen zum INT2 in Internet gelesen hat wird sich vielleicht fragen, was das GIFR bei der Aktivierung zu suchen hat. Nun, zumindest ich war zu Beginn etwas irritiert. Das Datenblatt des ATMega32 liefert auf Seite 67, Kapitel MCUCSR, die Antwort. Dort wird die folgende Vorgehensweise zur Aktivierung des INT2 empfohlen:

  1. Deaktivierung des INT2, indem das INT2-Bit im GICR Register auf 0 gesetzt wird
  2. Änderung des Bit 6 (ISC2) im Register MCUCSR (0=fallende Flanke, 1=steigende)
  3. Beschreiben des INT2-Bits im Register GIFR mit einer 1
  4. Reaktivierung des INT2, indem das INT2-Bit im GICR Register auf 1 gesetzt wird

Als Programm-Schnipsel sieht das dann wie folgt aus:

void init_INT2(void) {   // ##### INT2 AKTIVIERUNGS-ROUTINE
  GICR &= ~(1<<INT2);    // INT2 deaktivieren
  MCUCSR &= ~(1<<ISC2);  // Bei fallender Flanke Interrupt aktivieren
  GIFR |= (1 << INTF2);  // Interrupt Flag setzen
  GICR |= (1 << INT2);   // INT2 aktivieren
}

int main(void) {
...
  init_INT2();	   	  // Interrupt2 einschalten
...
}

Den Interrupt Eingang vorbereiten

Der INT2 ist nun aktiv und wartet auf einen Interrupt auf dem Pin für INT2, dem PB2. Die schematische Darstellung wie die Interrupt-Leitung auszuführen ist (zu Beginn dieses Artikels) zeigt, dass die Interrupt-Leitung einen Pull Up benötigt. Da das „Grundsignal“ (Interrupt ist nicht ausgelöst) eine logische 1 ist, muss hier der Pegel, über den 10k-Widerstand, auf +5V „hoch gezogen“ (hoch ziehen [dt.] = pull up [engl.]) werden.

Leider verfügt weder das GREETBoard I²C Portexpander-Modul noch das GREETBoard ATMega32 über einen Pull Up Widerstand auf der INT2-Leitung. Das ist aber überhaupt nicht schlimm. Um den Interrupt zu erkennen muss der Pin PB2 erstmal als normaler Eingang definiert werden. Im Anschluss wird, wie bei (fast) jedem Eingang, der interne Pull Up Widerstand aktiviert und damit ist ein externe Widerstand nicht mehr nötig.

Im nun folgenden Code-Schnipsel wird auch noch PD7 als Ausgang definiert und gleich auf Low gesetzt. Die hier angeschlossene LED dient lediglich dazu, das Aufrufen der Interrupt Service Routine sichtbar zu machen. Aber das wird im nächsten Abschnitt näher erläutert.

void init_IO(void){      // ##### PIN-KONFIGURATIONS-ROUTINE
  DDRB &= ~(1 << DDB2);  // PB2 = Eingang (für Interrupt)
  PORTB |= (1 << PB2);   // internen Pull-Up an PB2 aktivieren
  DDRD |= (1 << DDD7);   // PD7 = Ausgang => LED 4
  PORTD &= ~(1<<PD7);    // PD7 = Low => LED4 aus
}

Die Interrupt Service Routine (ISR)

Zu guter Letzt wird die eigentliche Aufgabe definiert die, durch den Interrupt ausgelöst, durchgeführt werden soll. Für die verschiedenen Interrupt-Ereignisse (u.a. Timer, etc.) gibt es separate ISR-Routinen, sogenannte Interrupt-Vektoren. Beim INT2 ist es „ISR(INT2_vect) {…}“. Auf der folgenden Liste stehen Vektornamen für weitere externe Interrupts. Der ATMega32 verfügt über die Vektoren INT0-2. Die anderen Vektoren sind nur bei „größeren“ AVR Mikrocontroller vorhanden, wie z.B. dem ATMega128. Diese Tabelle ist nur ein kleiner Auszug aus einer viel umfangreicheren Tabelle. Unter „Choosing the vector: Interrupt vector names“ befindet sich das Gesamtwerk, auf das ich mich hier beziehe.

Vector name Old vector name Description
INT0_vect SIG_INTERRUPT0 External Interrupt 0
INT1_vect SIG_INTERRUPT1 External Interrupt Request 1
INT2_vect SIG_INTERRUPT2 External Interrupt Request 2
INT3_vect SIG_INTERRUPT3 External Interrupt Request 3
INT4_vect SIG_INTERRUPT4 External Interrupt Request 4
INT5_vect SIG_INTERRUPT5 External Interrupt Request 5
INT6_vect SIG_INTERRUPT6 External Interrupt Request 6
INT7_vect SIG_INTERRUPT7 External Interrupt Request 7

Während der Ausführung des Interrupts sollen keine anderen Interrupts ausgeführt werden. Daher werden zu Beginn Interrupts mit cli(); deaktiviert. Nun wird die LED 4 eingeschaltet um festzustellen ob die ISR tatsächlich ausgeführt wird. Der dritte Schritt ist eine 10 Millisekunden lange Wartezeit um das Prellen des Tasters abzuwarten. Erst danach wird der Lesevorgang gestartet und das Ergebnis in der Variable read_data gespeichert.

Dabei ist zu beachten, das Warteschleifen oder -befehle in einer Interrupt Routine eigentlich nicht gut sind. Da zuvor die Interrupts abgeschaltet wurden, wird jedes Interrupt, dass während der Ausführung auftritt, nicht erfasst. Je länger die Ausführung eines Interrupts dauert, desto länger ist die Zeit ohne aktive Erfassung von Interrupts. Daher sollte eine ISR so kurz wie möglich sein. Für dieses Beispiel ist es jedoch nicht wichtig, da es keine anderen zeitkritischen Funktionen gibt, die beachtet werden müssen.

Besser wäre es mit der INT2 Routine einen Timer zu starten, der wiederum einen Lesevorgang nach 10ms startet. Damit wird die INT Routine schnell beendet und die Interrupts wieder aktiviert. Da mich interessiert, wie eine solche Lösung aussehen könnte, werde ich hier noch weiterarbeiten und den verbesserten Code nachreichen.

Der letzte Befehl schaltet die LED 4 wieder aus. Dadurch flackert die Leuchtdiode kurz auf, wenn ein Taster gedrückt wurde. Das Flackern beweist, dass die ISR nicht ständig ausgeführt wird, sondern NUR wenn ein Taster gedrückt wurde.

Hier der entsprechende C-Code:

ISR(INT2_vect){
  cli();                       // Interrupts deaktivieren
  PORTD |= (1 << PD7);         // PD7 = High => LED4 an
  lcd_wait_ms(10);             // Warte 10ms
  i2c_start(adr1_r);           // Starte Lesezugriff
  read_data = i2c_readNak();   // read_data mit Leseergebnis beschreiben
  PORTD &= ~(1<<PD7);          // PD7 = Low => LED4 aus
}

Dies sind die notwendigen Schritte, die nötig sind um den PCF8574 interruptgesteuert auszulesen. Der große Vorteil liegt darin, dass nur dann auf den I2C-Bus zugegriffen werden muss, wenn zuvor auch wirklich eine Aktion (hier der Tastendruck) erfolgt ist. Dies spart erheblich die Ressourcen, sowohl für den Mikrocontroller und auch auf dem Bus. Um ehrlich zu sein, habe ich sehr lange gebraucht um hinter die Anwendung des INT2 zu kommen, inklusive viler Besuche auf vielen verschiedenen Websites, Blogs und Foren. Ich hoffe, dass dieser Artikel dabei hilft die Suche anderer Hobby-Elektroniker ein wenig zu unterstützen und zu beschleunigen.

In diesem Sinne, alles Gute!

Das Programm

/*
* PCF8574_Interrupt.c
* Created: 27.03.2013
* Author: Timo Gruß (timogruss.de)
*
* Dieser Code liest das GREETBoard I2C Portexpander Modul aus, wenn ein Taster gedrückt
* wurde. Der Tastendruck löst einen Interrupt aus, der vom Mikrocontroller erkannt wird
* und den Lesevorgang anstößt. So wird, im Gegensatz zum Pollen, nur dann der PCF8574
* ausgelesen, wenn tatsächliche eine Aktion erfolgte.
*/

#ifndef F_CPU            //Wenn CPU-Takt nicht bereits definiert wurde...
#define F_CPU 16000000   //...dann definiere ihn auf 16MHz
#endif

#include "i2clcd.h"
#include "i2cmaster.h"
#include <avr/io.h>
#include <avr/interrupt.h>

volatile unsigned char adr1_w = 0x42;  // Device 1 write-address
volatile unsigned char adr1_r = 0x43;  // Device 1 read-address
unsigned char read_data;               // Variable für Leseergebnis

void init_INT2(void) {
  GICR &= ~(1<<INT2);      // INT2 deaktivieren
  MCUCSR &= ~(1<<ISC2);    // Bei fallender Flanke Interrupt aktivieren
  GIFR |= (1 << INTF2);    // Interrupt Flag setzen (s.S 67 ATMEGA32 Datasheet)
  GICR |= (1 << INT2);     // INT2 aktivieren
}
void init_prog(void) {
  cli();                     // Interrupts deaktiviert
  i2c_init();                // Starte I2C Bus
  lcd_init();                // Starte I2CLCD
  lcd_command(LCD_CLEAR);    // Leere Display
  lcd_wait_ms(30);           // Warte 30ms
}
  void init_IO(void){
  DDRB &= ~(1 << DDB2);      // PB2 = Eingang für Interrupt
  PORTB |= (1 << PB2);       // internen Pull-Up an PB2 aktivieren
  DDRD |= (1 << DDD7);       // PD7 = Ausgang => LED 4
                             //LED 4 ist die Interrupt-Kontrollleuchte
  PORTD &= ~(1<<PD7);        // PD7 = Low => LED4 aus
}

int main(void) {
  cli();              // Interrupts deaktiviert
  init_IO();          // Ein-/Ausgänge initiieren
  init_INT2();        // Interrupt2 einschalten
  init_prog();        // I2C und I2CLCD aktivieren
  read_data = 0xff;   // Grundwert für 'read_data'
  sei();              // Interrupts aktiviert
  while(1){
    sei();            // Interrupts aktiviert
    if (read_data == 0xff){              // Wenn read_data = 0xff (keine Taste gedrückt)
      lcd_printlc(1,1,"Keine Taste ");   // dann schreibe Text
    }
    if (read_data == 0x7f){              // Wenn read_data = 0xff (Taste S4 gedrückt)
      lcd_printlc(1,1,"S1 gedrueckt ");  // dann schreibe Text
    }
    if (read_data == 0xdf){              // Wenn read_data = 0xff (Taste S3 gedrückt)
      lcd_printlc(1,1,"S3 gedrueckt ");  // dann schreibe Text
    }
  }
}

ISR(INT2_vect){
  cli();                       // Interrupts deaktiviert
  PORTD |= (1 << PD7);         // PD7 = High => LED4 an
  lcd_wait_ms(10);             // Warte 10ms
  i2c_start(adr1_r);           // Starte Lesezugriff
  read_data = i2c_readNak();   // read_data mit Leseergebnis beschreiben
  PORTD &= ~(1<<PD7);          // PD7 = Low => LED4 aus
}