Drucken

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:

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 außerdem 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

Arduino mit Breadboard

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:

Serielle Ausgabe des Sketch  

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.