Browse Category: Arduino

EEPROM Programmierung

Die Programmierung eines Speicherchips war in den 1980er Jahren eines der spannendsten Themen wenn man sich mit dem Bau eines eigenen Computers beschäftigt hat. Heute stehen neben den klassischen RAM Bausteinen auch EEPROMS zur Verfügung, die einfacher zu programmieren sein sollen.

Die Abkürzung EEPROM steht für „Electrically Erasable Programmable Read-Only Memory“. Es handelt sich um einen Halbleiterspeicher, der sich durch Spannungsimpulse beschreiben und löschen lässt. Die auf dem Chip abgelegten Informationen sind nicht-flüchtig und bleiben ohne eine angelegte Versorgungsspannung erhalten.

Man unterscheidet verschiedene Arten von EEPROMS. Die EPROMS (z.B. 28F…, 29C…, 29F…) lassen es zu einzelne Bytes zu lesen und zu schreiben. Gleichzeitig lassen sich diese Bausteine auch komplett oder blockweise elektrisch löschen und einige auch blockweise (wie die AT28C-Serie) programmieren.

Daneben gibt es serielle EEPROMS (z.B. 24C…, 93C…). Seriell bedeutet bei diesen Bausteinen, das die Datenausgabe sowie die Adressangabe Bit für Bit (=seriell) erfolgt. Damit kann zwar nur ein Bit zur Zeit ausgegeben werden und die angesteuerte Adresse muss auch Bit für Bit übertragen werden aber dafür hat es den großen Vorteil das dass serielle EEPROM mit einem kleinen 8 Pin Gehäuse auskommt. Diese Bausteine werden demnach gerne eingesetzt, wenn Platz oder Ansteuerungsleitungen gespart werden sollen und dabei keine großen Datenmengen oder große Geschwindigkeit gefordert sind.

Programmiergerät für parallele EEPROMS

Für das EEPROM Programmiergerät soll ein EEPROM vom Typ 28C64-150 und einer vom Typ 28C256 verwendet werden. Wie man dem Datenblatt entnehmen kann handelt es sich um ein Baustein der 64k bzw. 256kByte Speicherkapazität enthält und eine Zugriffszeit von 150ns erlaubt.
Wenn mann sich die Pin-Belegung eines EEPROMs anschaut erkennt man folgende grundsätzliche Aufteilung die man bei allen Typen dieser Familie findet:

• Spannungsversorgungs Anschlüsse (GND, Vcc) • Adress-Leitungen (A0…Ax)
• Daten-Leitungen (D0-D7)
• Steuer-Leitungen (CE, OE und WE)

Pin Belegung gängiger EEPROM Bausteine
Pin Belegung gängiger EEPROM Bausteine

Die Idee besteht nun darin, einen Arduino Mega mit seinen ausreichenden Schnittstellen zum programmieren von EEPROM Bausteinen zu verwenden. Dabei wurden die Pins so gewählt, dass man über die Port-Manipulations-Mechanismen die Ein- und Ausgabe der Daten steuern kann. Da das größte mir vorliegende EEPROM über 15 Adressleitungen verfügt, habe ich mich entschieden, dafür die Analogen-Pins A0 bis A14 zu verwenden. Hinzu kommt, dass diese Pins über die beiden Ports F und K gesteuert werden können, was die Handhabung sehr erleichtert.

Lesen von EEPROM Daten

Wie das gemacht werden muss ist im Datenblatt mehr oder weniger verständlich beschrieben. Man muss sich die Beschreibung in Ruhe mehrmals durchlesen, dann ergibt sich folgender Ablauf:

  1. Setze ChipEnable CE auf auf LOW, d.h verbinde mit Masse. Da wir ständig mit dem Chip arbeiten kann dies eine statische Verbindung sein.
  2. Setze OutputEnable OE auf LOW, d.h. Daten werden ausgegeben.
  3. Lege die Adresse an, die ausgelesen werden soll
  4. Setze WriteEnable W E auf HIGH , dass heisst Daten können nur gelesen werden.

Damit sind wir in der Lage, Daten aus einem EEPROM auszulesen.

Schreiben von Daten in das EEPROM

Um Daten auszulesen muss CE und OE auf LOW und WE auf High gesetzt werden. Dann muss die entsprechende Adresse des auszulesenden Bereichs angelegt werden und an den 8 Datenbits werden dann die gespeicherten Datenbits ausgegeben.

In den Datenblättern der Chip-Hersteller wird ausführlich auf die einzuhaltenden Zeiten für ein reibungsloses Lesen und Schreiben eingegangen.

Erforderliche Funktionen für das EEPROM Programmiergerät

Bereitstellen einer Adresse an den Arduino-Pins

Man benötigt eine Funktion setAddress deren Aufgabe es ist, eine vorgegebene Speicheradresse als 16bit Adresse über den Arduino MEGA bereitzustellen. Die Herausforderung besteht nun darin, zu einer gegebenen Adresse Adr (die eine 16 Bit Breite aufweist), die einzelnen Bitwerte an die definierten Adressleitungen A_0,.... bis A_{14} überträgt. Dazu muss zuerst eine Aufteilung in die oberen-8 Bit-Werte und die unteren 8-Bit Werte erfolgen:

Mit dem Schiebe-Operator >>8 werden die oberen 8-Bit der Variable  wert um 8 Stellen nach rechts verschoben. Die bereits bestehenden Daten gehen dabei verloren. Somit steht in der wert nun nur noch das obere Byte. Um das untere Byte zu identifizieren, wird eine sogenannte Maskierung durchgeführt. Die UND-Verknüfung mit 0x00 übernimmt diese Aufgabe. Anschliessend hat man die 16-Bit breite Variable wert in zwei 8-Bit breite Daten aufgeteilt.

Diese können nun an die Ports übertragen werden. Dazu geht man wie folgt vor:

  void setAdress(word adresse){
    // Setze 16Bit Adresse über Port-Register F und K
    //        Bit7|Bit6|Bit5|Bit4|Bit3|Bit2|Bit1|Bit0 
    // PortF:  A7 | A6 | A5 | A4 | A3 | A2 | A1 | A0 
    // PortK:  A15| A14| A13| A12| A11| A10| A9 | A8
    
    // setze Port-Bits auf Ausgang
       DDRF = B11111111;
       DDRK = B11111111;
    
    // Bestimmung Low- und High-Bytes
       byte high_byte = adresse >>8;  
       byte low_byte  = adresse & 0x00ff;
    
    // Übertragung der beiden Bytes auf die PortRegister
       PORTF = low_byte;
       PORTK = high_byte;
    }// end setAdress

Der zentrale Teil der Übertragung der einzelnen Bits an die Ausgabe-Pins kann durch eine einfache Und-Verknüpfung mit den entsprechenden Port-Registern erfolgen.

EEPROM Daten einlesen

Nach dem Setzen des Output_Enable-Bits auf Low (= Daten Lesen) und dem Setzen des Port-registers auf Eingang können die an den Ausgangs-Pins anstehenden EEPROM-Daten einfach in die Variable data eingelesen werden. Die entsprechende Funktion könnte so umgesetzt werden:

    byte readEEPROM(int address) {
      // setze Output_Enable auf LOW = ermöglicht das Lesen von Daten
      digitalWrite(Output_EN, HIGH);
      delayMicroseconds(1);
      digitalWrite(Output_EN, LOW);
      
      // Setze Portregister C auf Input
      DDRC = B00000000;
      
      setAdress(address);
      // Lese Daten
      byte data = 0;
      data=PINC;
      return data;
    }//end readEEPROM

Formatierte Ausgabe im Monitor-Stil

 Aus Remineszenz an die frühen Tage der Computertechnik wo es nur sehr eingeschränkte Ein- und Ausgabe-Möglichkeiten gab, stellten die damaligen Consolen die Inhalte in Form von Hexadezimalen Datenblöcken dar. Es beginnt immer mit der Startadresse gefolgt von 8 aufeinanderfolgenden Daten-Werten ohne explizite Adress-Angabe. Dann folgt ein Zeilenumbruch und es wird die um 8 Stellen erhöhte Adresse-Angabe wieder gefolgt von 8 einzelnen Datenblöcken, zu denen man sich die zugehörige Speicheradresse leicht durch abzählen ermitteln kann.

Wie gesagt, die Idee stammt aus der Vergangenheit aber ist bis heute immer noch ein gutes Instrument in der maschinennahen Programmierung, daher habe ich so eine Funktion zur Datenansicht geschrieben.

void Monitor (int Startadresse, word Endadresse) {
// Ausgabe der Speicherstellen von Startadresse bis Endadresse
Serial.println();
Serial.println("==============================");
Serial.println("  **  Monitor-Programm  **");
Serial.println("==============================");
for (int adr=Startadresse;adr< Endadresse;adr++){
   randomData = random(255); // Lese Soeicherstelle muss ersetzt werden
   
   if ((adr == 0)||(adr % 8 == 0)) // zeige 8 Stellen in einer Reiche
       Serial.print(Int2Hex(adr,4)+": ");
   Serial.print(Int2Hex(randomData,2)+" ");
   if ((adr+1)%8 == 0) // stelle fest ob Zeilenumbruch erforderlich
      Serial.println();
  }// end for adr
}// end Monitor

String Int2Hex(int wert, int stellen){
  String temp = String(wert,HEX);
  String prae = "";
  int len = temp.length();
  int diff = stellen-len;
  for (int i=0;i< diff;i++)
      prae = prae +"0";
  return prae+temp;
}// end Int2Hex
Beispiel eines sogenannten Hex Dumps

Beispiel EPPROM Programmer Prototyp

Der erste Entwurf für ein komplettes Programm mit dem man EEPROMs lesen und schreiben können kann ist im folgenden dargestellt. Es basiert auf den bereits vorgestellten Teil-Programmen und ist erweitert um die Enable Funktionen für die Output- und die Write-Funktion. Dazu kommt ein Arduino Mega zum Einsatz, da er über genügend IO-Ports verfügt und man so ohne Shift-Register auskommt.

   //============================================
    // EEPROM Programme 
    // Monitor: Anzeige von Speichstellen
    // SetAdress SetData setzen von Adressen und Daten
    // PHOF Nov 2020
    //============================================
    //   ----    Arduino MEGA Portbelegung    ----
    //        Bit7| Bit6| Bit5| Bit4| Bit3| Bit2| Bit1| Bit0 
    //PortA:  P29 | P28 | P27 | P26 | P25 | P24 | P23 | P22 
    //PortL:  P42 | P43 | P44 | P45 | P46 | P47 | P48 | P49
    //==============================================================
    
    #define WRITE_EN 12
    #define Output_EN 10
     
    // 4-bit hex decoder for common cathode 7-segment display
    byte data[] = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0xff, 0x6f, 0x77, 0x7c, 0x58, 0x5e, 0x79, 0x71 };
    
    void setup() {
      DDRA = B11111111; // alle Ports output
      DDRC = B11111111;
      DDRL = B11111111;
      DDRF = B11111111;
      DDRK = B11111111;
      Serial.begin(9600);
      digitalWrite(WRITE_EN, HIGH); pinMode(WRITE_EN, OUTPUT);
      digitalWrite(Output_EN, HIGH);pinMode(Output_EN, OUTPUT);

     // Test Prozedur 
      Serial.print("Programming EEPROM");
      for (int address = 0; address < sizeof(data); address += 1) {
        writeEEPROM(address, data[address]);
         Serial.println();Serial.print("Schreibe Daten Byte ... Adresse: ");
         Serial.print(address,HEX); Serial.print("  : "); Serial.print(data[address],BIN);Serial.print(" ... ");
         Serial.println(data[address],HEX);
        if (address % 64 == 0) {
             Serial.print(".");
           }
        delay(500);
      }
      Serial.println(" done");  
   }// end setup
     
    
   void Monitor (int Startadresse, word Endadresse) {
    // Ausgabe der Speicherstellen von Startadresse bis Endadresse
      Serial.println();
      Serial.println("==============================");
      Serial.println("  **  Monitor-Programm  **");
      Serial.println("==============================");
    
      for (int adr=Startadresse;adr< Endadresse;adr++){
  //       randomData = random(255); // Lese Speicherstelle muss ersetzt werden
         
         if ((adr == 0)||(adr % 8 == 0)) // zeige 8 Stellen in einer Reiche
             Serial.print(Int2Hex(adr,4)+": ");
      
         Serial.print(Int2Hex(randomData,2)+" ");
      
         if ((adr+1)%8 == 0) // stelle fest ob Zeilenumbruch erforderlich
            Serial.println();
        }// ennd for adr
    
    }// end Monitor
    
  String Int2Hex(int wert, int stellen){
      String temp = String(wert,HEX);
      String prae = "";
      int len = temp.length();
      int diff = stellen-len;
      for (int i=0;i< diff;i++)
        prae = prae +"0";
      return prae+temp;
  }// end Int2Hex
    
 void setAdress(word adresse){
    // Setze 16Bit Adresse über Port-Register F und K
    //        Bit7|Bit6|Bit5|Bit4|Bit3|Bit2|Bit1|Bit0 
    // PortF:  A7 | A6 | A5 | A4 | A3 | A2 | A1 | A0 
    // PortK:  A15| A14| A13| A12| A11| A10| A9 | A8
    
    // setze Port-Bits auf Ausgang
       DDRF = B11111111;
       DDRK = B11111111;
    
    // Bestimmung Low- und High-Bytes
       byte high_byte = adresse >>8; // oder (adresse & 0xff00) >> 8
       byte low_byte  = adresse & 0x00ff;
    
    // Übertragung der beiden Bytes auf die PortRegister
       PORTF = low_byte;
       PORTK = high_byte;
    }// end setAdress
   
    
    byte readEEPROM(int address) {
      // setze Output_Enable auf LOW = ermöglicht das Lesen von Daten
      digitalWrite(Output_EN, HIGH);
      delayMicroseconds(1);
      digitalWrite(Output_EN, LOW);
      
      // Setze Portregister C auf Input
      DDRC = B00000000;
      
      setAdress(address);
      // Lese Daten
      byte data = 0;
      data=PINC;
      return data;
    }//end readEEPROM

    void writeEEPROM(int address, byte data) {
      // setze Output_Enable auf HIGH = ermöglicht das Schreiben von Daten
      digitalWrite(Output_EN, LOW);
      delayMicroseconds(1);
      digitalWrite(Output_EN, HIGH);
      setAdress(address);
  
      // PortC:  P30 | P31 | P32 | P33 | P34 | P35 | P36 | P37  
       DDRC = B11111111; // setze Port-Bits C auf Ausgang
       PORTC = data; // damit stehen die Daten an den Pins von Port C
     
      // set write Enable um die Daten ins EEPROM zu schrieben
      digitalWrite(WRITE_EN, LOW);
      delayMicroseconds(1);
      digitalWrite(WRITE_EN, HIGH);
      delay(10);
    }//end writeEEPROM
    
   void loop() { }

Dabei handelt es sich um einen ersten Prototypen, realisiert auf einem Steckbrett. Man kann hier nach belieben Erweiterungen wie beispielsweise LED Anzeigen dazu bauen. Oder andere Arduino-PINs verwenden. Wenn dann alles so funktioniert wie ausgedacht, steht einer Version mit einer speziell angefertigten Platine und einem Schnellpannsockel für das unproblematische Einsetzen und Herausnehmen der EEPROMs nichts mehr im Weg.

EEPROM Programmer Prototyp Aufbau

Timer Programmierung

Beitrag befindet sich in Bearbeitung

1. Grundlagen der Timer-Programmierung

Ein Timer ist ein Funktionsbaustein eines Mikrocontrollers und kann verwendet werden, um Zeitereignisse zu messen. Vom Aufbau her ist ein Timer ist im Grunde nichts anderes als ein Register im Mikrocontroller, das hardwaregesteuert fortlaufend um 1 erhöht (oder verringert) wird. Dazu wird üblicherweise der Timer mit dem Systemtakt verbunden, um so die Genauigkeit des Quarzes auszunutzen, um ein regelmäßiges und vor allen Dingen vom restlichen Programmfluss unabhängiges Zählen zu ermöglichen.  

Um die verschiedenen Timer, die ein Microcontroller zur Verfügung stellt verwenden zu können, müssen bestimmte Eigenschaften konfiguriert werden. Dazu sind jedem Timer bestimmte Register zugeordnet. Zum Teil sind diese Timer spezifisch, es gibt aber auch Register, die von allen Timern gemeinsam genutzt werden. Im nachfolgender Abbildung sind diese Register am Beispiel des 16-Bit Timers1 dargestellt:

Übersicht aller dem Timer1 zugeordneten Register

Neben diesen spezifischen Registern die alle Timer haben, gibt es wie gemeinsam genutzte Register. Dabei handelt es sich zum einen um das sogenannte TIMSK-Register. Das TIMSK-Register definiert, bei welchen Ereignissen welche Art Interrupts ausgelöst werden. Das Register hat für den Timer1 den nachfolgend dargestellten Aufbau.

Dazu kommt noch ein weiteres Register das gemeinsam genutzt wird. Im TIFR- Register wird festgelegt, wie reagiert werden soll, wenn ein Zähler überläuft, bzw. sein Zielwert erreicht hat.

Sind die jeweiligen Interrupts aktiviert, können diese dann über die Interrupt Service Routinen (ISRs) benutzt werden. Dazu müssen die entsprechenden Interruptvektoren an die ISR übergeben werden:

  • TIMER1_CAPT_vect für Input Capture
  • TIMER1_COMPA_vect / TIMER1_COMPB_vect für Compare Match
  • TIMER1_OVF_vect für Timer Overflow

Das ist die Theorie der Timer Programmierung. Für unerfahrene Programmierer sicher alles andere als einfach nachzuvollziehen. Aus diesem Grund soll die Arbeit mit Timern nachfolgend Schritt für Schritt erläutert werden. Dazu ist es wichtig, zu wissen, wie die eben beschriebenen Register im Zusammenhang stehen.

2. Kontrollregister

Um die Timer Funktion zu spezifizieren, werden bestimmte Bits in den für die Timer-Konfiguration vorgesehenen Registern gesetzt. Die wesentlichen Register sind :

  • TCCRx – Timer/Counter Control Register
  • TCNTx – Timer/Counter Register
  • OCRx – Output Compare Register
  • ICRx – Input Capture Register (only for 16bit timer)
  • TIMSKx – Timer/Counter Interrupt Mask Register
  • TIFRx – Timer/Counter Interrupt Flag Register

Im folgenden werden diese Register erklärt und aufgezeigt wie deren innere Bit-Struktur aussieht. Um die Timer zu konfigurieren, müssen diese Register mit entsprechenden Werten gesetzt werden. Das mutet auf den ersten Blick vielleicht etwas verwirrend an, wird am Ende aber hoffentlich transparent.

2.1 Das Timer Control Register

In den Timer Control Registern werden die wesentlichen Einstellungen für das Timer-Verhalten vorgenommen. Dazu gehören u.a.:

  • Wahl des Wave Form Generation Modes über die WGMx Bits
  • Festlegung, was bei einem Compare Match passiert (COM xy Bits)
  • Prescaler bzw. external Clock über die Chip Select Bits CS1x

Im folgenden ist eine Übersicht über Anzahl und den Aufbau sämtlicher Timer Kontroll-Register der Timer des Arduino UNO dargestellt.

Übersicht der Kontrollregister der Timer0, Timer1 und Timer2

2.2 Das Counter Register TCNTx

Das Timer-Register zählt im Systemtakt oder verlangsamt über den gewählten Prescaler. Die untere Grenze wird als Bottom (Null) bezeichnet, die obere Grenze als Top. Top ist, je nach Modus, festgelegt oder kann variabel definiert werden. Für alle Timer gibt es je nach Größe ein oder zwei 8Bit Counter Register. Sie haben die Bezeichnungen: TCNT0, TCNT1H und TCNT1L sowie TCNT2.

2.3 Das Output Compare Register OCRx

In den beiden Output Compare Registern OCR1A und OCR1B kann man Werte definieren, die permanent mit dem Timer Daten Register TCNTx verglichen werden. Je nach Einstellung und Modus löst eine Übereinstimmung (Compare Match) bestimmte Aktionen aus. Die entsprechenden Aktionen werden über die Timer/Counter 1 Control und Status Register konfiguriert.

Der Compare Output Mode ist in alle Timer integriert. Die Bits COMx0 und COMx1 steuern das Verhalten des OCx-Pins (der Ausgabepin des CompareOutput Modes). Wenn beide Bits auf 0 gesetzt sind, behält der OCx-Pin seine Standardfunktion im Mikrocontroller.

Ist das COMx1 Register gesetzt wird der OCx-Pin bei einem Compare Match auf 0 gesetzt (bei PWM wird er bei Erreichen vom maximalen Wert wieder auf 1 gesetzt). Sind beide Register gesetzt ist diese Funktion invertiert (also bei jedem Compare Match wird OCx auf 1 gesetzt). 

Output-Compare-Pins

Jedem Timer sind im Timer-Kontroll-Register sogenannte Output-Compare-Pins zugeordnet. Dem Timer1 sind beispielsweise beim Arduino UNO die zwei Output Compare Pins OC1A (Pin 9) und OC1B (Pin 10) zugeordnet. Die Tabelle gibt eine Übersicht der Zuordnung für UNO und MEGA:

Übersicht der OutputCompare-Pins

2.4 Das Input Capture Register ICR1

Dieses Register hat nur der Timer1. Es hat zwei Funktionen: Bei einem Ereignis an ICP1 wird der Zählerstand von TCNT1 in ICR1 geschrieben. Das ICES1 Bit (Bit6 im TCCR1B Kontroll-Register) legt fest, ob dies bei steigender (ICES1 = 1) oder fallender Flanke (ICES1 = 0) passieren soll.

ICR1 dient, wie OCR1A, in einigen WGM1 Modi als Top Wert. In diesen Fällen ist die Input Capture Register Funktion deaktiviert. Im Gegensatz zu OCR1A ist ICR1 nicht gepuffert, sondern wird sofort überschrieben. Welche Folgen das hat, besprechen wir bei den PWM Modi. 

2.5 Das TIMSK Register

Dieses Register wird von allen Timern verwendet. Das TIMSK-Register definiert, bei welchen Ereignissen welche Art Interrupts ausgelöst werden. Das Register hat den nachfolgend dargestellten Aufbau. Die Bedeutung der einzelnen Bits wird unten beschrieben:

Struktur des TIMSK Register
  • OCIE2 (Timer/Counter2 Output Compare Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter2 Compare Match Interrupt aktiviert (vorrausgesetzt die Interrupts sind global aktiviert).
  • TOIE2 (Timer/Counter2 Overflow Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter2 Overflow Interrupt aktiviert.
  • TICIE1 (Timer/Counter1, Input Capture Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Input Capture Interrupt aktiviert.
  • OCIE1A (Timer/Counter1 Output Compare A Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Output Compare A Match Interrupt aktiviert .
  • OCIE1B (Timer/Counter1 Output Compare B Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Output Compare B Match Interrupt aktiviert.
  • TOIE1 (Timer/Counter1 Overflow Interrupt Enable)
  • Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Overflow Interrupt aktiviert.
  • OCIE0 (Timer/Counter0 Output Compare Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, so wird der Timer/Counter0 Compare Match Interrupt aktiviert.
  • TOIE0 (Timer/Counter0 Overflow Interrupt Enable)
    Wenn dieses Bit gesetzt wird, so wird der Timer/Counter0 Overflow Interrupt aktiviert.

Sind die jeweiligen Interrupts aktiviert, können diese dann über die Interrupt Service Routinen (ISRs) benutzt werden. Dazu müssen die entsprechenden Interruptvektoren an die ISR übergeben werden:

  • TIMER1_CAPT_vect für Input Capture
  • TIMER1_COMPA_vect / TIMER1_COMPB_vect für Compare Match
  • TIMER1_OVF_vect für Timer Overflow

2.6 Das Timer Interrupt Flag Register TIFRx

Die Timer des ATMega16 können verschiedene Interrupts auslösen um auf verschiedene Timerereignisse zu reagieren. Um die Interrupts zu aktivieren, muss das das entsprechende Bit im TIMSK (Timer/Counter Interrupt Mask Register) gesetzt werden. Bei einem Interrupt Request wird das entsprechende Flag (Bit) im TIFR (Timer Interrupt Flag Register) gesetzt, welches dann den Interrupt auslöst. Man kann also auch durch Setzen des entsprechenden Bits im TIFR einen Interrupt “von Hand” auslösen.

2.6.1 Output Compare Match Interrupt (OCI)

Erreicht das Register TCNTx den Wert, der im Register OCRx voreingestellt wurde, so tritt ein Timer Compare Match ein. Dabei wird ein Output Compare Interrupt ausgelöst. Auf Wunsch wird der Counter dabei zurückgesetzt. Dazu dient das CTC-Bit (Clear Timer on Compare) im TCCRx-Register.

2.6.2 Timer Overflow Interrupt (TOC)

Ist das Register TCNTx voll, so wird ein Timer/Counter Overflow und damit ein Timer/Counter Overflow Interrupt ausgelo ̈st. Der Timer zählt dabei von 0 aufwärts weiter. So kann periodisch eine Interruptroutine ausgeführt werden.

Bei PWM kann bei Bedarf der Timer Overflow Interrupt aktiviert werden um z.B. das OCRx Register zu ändern, wodurch man einen sinusförmigen Ausgang generieren kann.

2.6.3 Timer Input Capture Interrupt (TICI)

Der TICI wird bei einem Input Capture Event ausgelöst, welches eintritt, wenn am ICP1-Pin eine steigende/fallende Flanke (je nach Einstellung) gemessen wird. Es besteht die Möglichkeit einen Noice Canceler einzuschalten, der dafür sorgt, dass der TICI erst ausgelöst wird, wenn das Signal 4 Takte lang stabil anliegt. Dieser Modus kann genutzt werden um Ereignisse zu zählen oder auf sie zu reagieren.

3. Festlegung der Timer-Betriebsart

Timer können in verschiedenen Modi konfiguriert werden. Man unterscheidet dabei zwischen folgenden Modi:

  • Normal-Mode
    Der einfachste Betriebsmodus ist der normale Modus. Die Zählrichtung des Timers ist immer aufsteigend, bis zum Überlauf – da fängt der Zähler wieder bei 0 an. Der Überlauf kann einen Interrupt (Timer-Overflow) auslösen. Der Timer kann so konfiguriert werden, dass beim Erreichen dieses Maximums der TIMERn_OVF_vect ausgelöst wird. Die Frequenz, mit der ein Overflow bei Verwendung des Prozessortakts als Taktquelle auftritt ergibt sich mit:

        \[f=\frac{Systemtakt}{Prescaler\cdot N}\]


    wobei N =256 bei 8Bit Zählern und N=65536 bei 16Bit-Zählern gesetzt wird.
  • CTC-Mode
    Hier zählt der Timer nach oben bis zum Erreichen des OCRn Registers. Das Register TCNTn wird beim Erreichen zurückgesetzt. Der Timer kann so konfiguriert werden, dass beim Erreichen des OCRn Wertes der TIMERn_COMPx_vect ausgelöst wird. Die Frequenz, mit der ein Compare Match bei Verwendung des Prozessortakts als Taktquelle auftritt ergibt sich mit:

        \[f=\frac{Systemtakt}{Prescaler\cdot OCR_n}\]

  • PWM-Mode
    Im PWM Modus werden die OCxy Ausgabe-Pins verwendet um PWM Signale zu erzeugen und auszugeben. Die jeweiligen Pins sind genau festgelegt und lassen sich aus der oben stehenden Übersicht entnehmen.
  • Fast-PWM-Mode
    Beim Fast PWM zählt der Timer bis zum Maximum seines Zählberreichs. Das Register OCRn dient als Vergleich und abhängig davon, ob TCNTn kleiner oder größer OCRn ist, kann der OCn Pin auf logisch 0 oder 1 gesetzt werden.

Um die beschriebenen Modi zu setzen sind im Timer-Kontroll-Register die WGMx-Bits vorgesehen. der verschiedenen Timer sogenannte Waveform Bits vorgesehen. Für den Timer2 sind das die Bits WGM20 und WGM21. Diese befinden sich im TCCR2A Register an der Position 0 und 1 siehe Abbildung:

Festlegen des Betriebsmodus “Normal” im Kontroll-Register des Timers 2

Wenn man den Modus bei Timer0 und Timer1 konfigurieren möchte, würde man analog vorgehen, und die entsprechenden WGM0x- und WGM1x-Bits mit den WaveForm-Bit-Kombinationen belegen. Da wie oben erwähnt jeder Timer sein spezifischer zugeordnetes Kontrollregister besitzt.

4. Erstes Beispiel

Als Beispiel soll der Timer 2 so konfiguriert werden, dass eine LED Leuten soll. Der Timer2 soll dabei im Normal-Modus betreiben werden mit einem Prescaler von 1024. Dazu müssen die Parameter in den zughörigen Kontroll-Rister entsprechend gesetzt werden:

Als erstes wird Kontrollregister A des Timer 2, also TCCR2A auf 0 setzen, damit ist der Pin OC2A inaktiv und der Timer aktiviert:

Die für das Beispiel relevanten Kontroll-Register des Timers 2

Anschliessend werden im Kontrollregister TCCR2B alle CS2x Bits auf HIGH gesetzt (Gelb markiert). Der Prescaler ist damit auf 1024 gesetzt.

Da die WGM2x Bits standardmässig auf 0 gesetzt sind, braucht man hier keine weiteren Aktivitäten durchführen, um den Normal-Modus zu konfigurieren.

Als nächstes wird das Bit OIE2 für den Timer Overflow Interrupt gesetzt. Dadurch wird jedesmal, wenn das TCNT2-Register überläuft (> 255), ein Interrupt ausgelöst.

Nun fehlt noch die Festlegung des LED-Pins. Hierzu nutzen wir die Port-Manipulations-Anweisungen, um Pin 7 als Ausgang zu definieren.

Und abschliessend wird noch die Interrupt-Service Routine definiert, die den Timer2 Overflow Interrupt TIMER2_OVF_vect abfängt.

Das Programm ist dann trotz der vielen Vorüberlegungen alles in allem sehr Kompakt. Das liegt natürlich auch daran, dass die ganzen Bezeichnet bereits vordefiniert wurden, und man dadurch sehr effizient auf die Register und die Bits zugreifen kann:

void setup(){ 
  TCCR2A = 0x00;        // Set register TCCR2A from Timer2 to LOW; 
  TCCR2B = (1<<CS22) + (1<<CS21) + (1<<CS20); // prescaler = 1024
  TIMSK2 = (1<<TOIE2);  // if TCNT2 > 255, wird ein Interrupt ausgelöst
  DDRD |= (1<<PD7);     // Portmanipulation: replaces pinMode(7, OUTPUT); 
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
    PORTD ^= (1<<PD7); // toggle PD7
}

Nach dem Start des Programms erkennt man, dass die LED sehr schnell leuchtet. Nach der oben genannten Formel lässt sich die Frequenz auch leicht berechnen:

    \[f=\frac{Systemtakt}{1024\cdot 256} = 61,03 \,Hz\]

Wenn man einen kleinere Frequenz haben möchte, müsste man weitere Methoden zum verlangsamen, wie beispielsweise einen Scalefaktor, einführen.

4. Timer im CTC Modus

Im Modus Clear-Timer on Compare wird ein Vergleichswert in ein Compare-Register geschrieben. Für den Timer0 wird das Register OCR0 verwendet. Sobald der Timer0 diesen Wert erreicht (TOP-Wert) wird ein Flag gesetzt. Der Timer0 zählt also nicht mehr von 0 bis 255 (MAX-Wert), wie im Normalmodus, sondern kann irgendwo zwischen 0 und 255 zum Zählabbruch gezwungen werden, um dann wieder bei 0 anzufangen. Analog verhält es sich natürlich bei den anderen Timern. Mit dem Unterschied, dass beim 16Bit-Timer1 die Top- und Max-Werte entsprechend größer sind.

Eine zweite Variante im Normalmodus, die noch nicht angesprochen wurde, besteht darin, dass man den Timer nicht bei 0 sondern bei einem Wert k zwischen 0 und 255 starten lässt. Damit lassen sich ebenfalls die Zählzeiten verkürzen.

4. 1 Timer Interrupt Berechnung

Da die Timer wie oben beschrieben, immer an den Systemtakt gekoppelt sind, werden die Zählregister beim Arduino UNO und MEGA mit einer Takt-Frequenz von 16 Mhz erhöht. Dies bedeutet, dass die Incrementierung eines Registers T=\frac{1}{16\cdot 10^6}=62,5  ns benötigt. Dementsprechend würde ein 8 Bit Register nach 16 Microsekunden überlaufen und ein 16 Bit Register nach 4,9 ms, wie aus den nachfolgenden Rechnungen hervorgeht.

Register BreiteZeit bis Überlauf
8 Bit Timer Register256 * 62,5 ns = 16 µs
16 Bit Timer Register65536 * 62,5 ns = 4,90 ms
Maximale Laufzeiten der Zählregister

Für die meisten Anwendungen ist das natürlich zu schnell. Deshalb gibt es den so genannten Vorteiler oder auch Prescaler genannt, mit dem die Zeitbasis reduziert werden kann.

Ein Prescaler ist einfach ein binärer Teiler für die Taktfrequenz. Mögliche Werte liegen bei 8, 32, 64, 256 oder 1024.  Seine Aufgabe ist es, den Systemtakt um den angegebenen Faktor zu teilen. Steht der Vorteiler also auf 1024, so wird nur bei jedem 1024-ten Impuls vom Systemtakt das Timerregister um 1 erhöht. Entsprechend weniger häufig kommen dann natürlich die Overflows. Die allgemeine Formel für die Frequenz f mit der ein Zähler einen Interrupt auslöst lautet:

 

(1)   \begin{equation*} f=\frac{Systemtakt}{2^{TimerBit}\cdot Prescaler}\end{equation*}

In der folgenden Tabelle sind für den Timer1 (16Bit) und Timer 2 (8Bit) mögliche Prescaler-Werte und die daraus folgenden Zeiten für einen Register Überlauf zusammengestellt. Die Idee hinter der Berechnung ist folgende: Bei 16 MHz benötigt ein Zähl-Impuls t_T = 62,5 ns. Dementsprechend benötigt ein 8-Bit Register 256\cdot t_T Zeit für einen Überlauf und somit einen Interrupt.

Prescaler18642561024
8 Bit Reg16,00 µs128,00 µs1,024 ms4,096 ms16,38 ms
16 Bit Reg4,90 ms32,76 ms256,14 ms1,048 s4,19 s
Zeiten in Abhängigkeit vom prescale Faktor bis Überlauf

4.2 Setzen der Prescalerwerte

Die Prescaler Werte werden über die CSx-Bits im TCCRx Register definiert. Das x in obiger Tabelle steht wie immer für die Timer-Nummer. Für den Timer1 sind folgende Bits definiert: CS12, CS11 und CS10.

Um nun die Zählgeschwindigkeitfestzulegen, ist der jeweilige Pressale-Wert zu wählen und die entsprechende Bit-Kombination in dem Kontrollregister des Timers zu setzen. Ein entsprechender C-Code wie diese Bits konfiguriert werden ist nachfolgend dargestellt:

TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024

Weiteres Beispiel

Das von oben bekannte Beispiel mit dem Timer2, soll nun auf den 16Bit Timer 1 übertragen werden. Nun aber mit der Vorgabe, dass die LED mit einer vorgegebenen Taktzeit von 1,5s aufleuchten soll.

void setup(){ 
  DDRD |= (1<<PD7);             // Portmanipulation: replaces pinMode(7, OUTPUT);  
  TCCR1A = 0;			// clear ctrl register A
  TCCR1B = 0;			// clear ctrl register B
  TCCR1B |= (1 << WGM12);	// set bit for CTC mode
  TCCR1B |= (1 << CS12);	// set bit 2 of prescaler for 1024x
  TCCR1B |= (1 << CS10);	// set bit 0 of prescaler for 1024x
  OCR1A = 23437;		// set L & H bytes to 23437 (1.5 sec)
  TIMSK1 |= (1 << OCIE1A);	// enable interrupt on OCR1A
  TCNT1 = 0;			// reset counter to zero
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
    PORTD ^= (1<<PD7); // toggle PD7
}

Betrachtet man das Ergebnis des obigen Programms am Oszillographen, so erkennt man dass saubere Flanken in einer Frequenz von rund 30,6 Hz angezeigt werden.

Gemäß obiger Formel

 

(2)   \begin{equation*} f=\frac{Systemtakt}{2^{TimerBit}\cdot Prescaler} = \frac{16 000000}{2^8\cdot 1024} = 61.0352  Hz\end{equation*}

Möchte man nun genaue Frequenzen einstellen, benötigt man einen weiteren Skalierungsfaktor und einen Startwert, mit denen sich dann folgende Formel ergibt:

 

(3)   \begin{equation*} f_{wunsch} \cdot ScaleFaktor =\frac{Systemtakt}{(2^{TimerBit}-Startwert)\cdot Prescaler} \end{equation*}

Für eine Wunsch-Frequenz von 1 Hz nutzen wir den Umstand, dass das TCNTx Register auch mit einem Startwert beschrieben werden kann. Es werden dann nur noch (2^{TimerBit}-Startwert) Schritte bis zum Überlauf durchgeführt. Wie in der obigen Formel beschrieben, bleibt aber immer noch eine Gleichung mit mehreren Unbekannten:

 

(4)   \begin{equation*} f_w \cdot ScaleFaktor\cdot (2^{TimerBit}-Startwert)\cdot Prescaler=Systemtakt\end{equation*}

 

(5)   \begin{equation*} (2^{TimerBit}-Startwert)=\frac{Systemtakt}{f_w \cdot ScaleFaktor\cdot Prescaler}\end{equation*}

 

(6)   \begin{equation*}   Startwert = 2^{TimerBit} -\frac{Systemtakt}{f_w \cdot ScaleFaktor\cdot Prescaler}\end{equation*}

Wir kennen aber den Systemtakt, die möglichen Prescalewerte und das der Startwert ganzzahlig und kleiner 2^{TimerBit} in diesem Beispiel 256 sein muss. Mit nachfolgendem Zusammenhang und etwas rumprobieren erhält man folgende Ergebnisse:

Etwas systematischeres rumprobieren mit Mathemathematica

Aus der Tabelle lassen sich 3 gute Kombinationen entnehmen: Mit einem Prescaler von 256 finden sich die Startwerte 250 und 500. Bei einem Presclaewert von 64 würde nur der Startwert 1000 passen. Bei einem Prescaler von 1024 haben wir nur annähernd passende Werte, die man aufrunden müsste.

Mit f_{wunsch} :=1 und einem 8-Bit Timer ergibt sich dann

 

(7)   \begin{equation*}Startwert = 256 -\frac{16 000000}{500\cdot 256}=131\end{equation*}

Damit folgt folgendes überarbeitete Programm, dass die Diode mit genau 1 Hz zum leuchten bringt:

byte counterStart = 131;  
unsigned int scaleFactor = 500; 

void setup(){ 
  TCCR2A = 0x00; 
  TCCR2B = (1<<CS22) + (1<<CS21); // prescaler = 256
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  TCNT2 = counterStart;
  DDRD |= (1<<PD7);
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
  static int counter = 0;
  TCNT2 = counterStart;
  counter++;
  if(counter==scaleFactor){
    PORTD ^= (1<<PD7);
    counter = 0; 
  }
}
Programm-Sequenz um Timer zu programmieren
// Timmer aktivieren
TCCR0A |= (1 <<  WGM01);   // enable timer0 CTC mode
TIMSK0 |= (1 <<  OCIE0A);  // enable timer0 compare interrupt

TCCR1B |= (1  <<  WGM12);   // enable timer1 CTC mode
TIMSK1 |= (1  <<  OCIE1A);  // enable timer1 compare interrupt

TCCR2A |= (1  <<  WGM21);   // enable timer2 CTC mode
TIMSK2 |= (1  <<  OCIE2A);  // enable timer2 compare interrupt

// Timmer Rücksetzen
// reset a timer unit (replace X by timer number)
TCCRXA = 0;  // set TCCRXA register to 0
TCCRXB = 0;  // set TCCRXB register to 0
TCNTX  = 0;  // reset counter value

//Timer Vergleichswert setzen
OCR0A = 124;   // set compare match register of timer 0 (max. value: 255 = 2^8 - 1)
OCR1A = 20233; // set compare match register of timer 1 (max. value: 65535 = 2^16 - 1)
OCR2A = 20;    // set compare match register of timer 2 (max. value: 255 = 2^8 - 1)

//Prescaler Werte setzen 
//am Beispiel Timer1
TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024

TCCR0B |= (1  <<  CS00);  // no prescaling for timer0

TCCR2B |= (1  <<  CS22) | (1  <<  CS20);  //Prescale auf 1024 for timer2
void setup(){
//set pins as outputs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(13, OUTPUT);

cli();//stop interrupts

//set timer0 interrupt at 2kHz
  TCCR0A = 0;// set entire TCCR2A register to 0
  TCCR0B = 0;// same for TCCR2B
  TCNT0  = 0;//initialize counter value to 0

// set compare match register for 2khz increments
  OCR0A = 124;// = (16*10^6) / (2000*64) - 1 (must be <256)
  // turn on CTC mode
  TCCR0A |= (1 << WGM01);
  // Set CS01 and CS00 bits for 64 prescaler
  TCCR0B |= (1 << CS01) | (1 << CS00);   
  // enable timer compare interrupt
  TIMSK0 |= (1 << OCIE0A);

//set timer1 interrupt at 1Hz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 15624;// = (16*10^6) / (1*1024) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS12 and CS10 bits for 1024 prescaler
  TCCR1B |= (1 << CS12) | (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);

//set timer2 interrupt at 8kHz
  TCCR2A = 0;// set entire TCCR2A register to 0
  TCCR2B = 0;// same for TCCR2B
  TCNT2  = 0;//initialize counter value to 0
  // set compare match register for 8khz increments
  OCR2A = 249;// = (16*10^6) / (8000*8) - 1 (must be <256)
  // turn on CTC mode
  TCCR2A |= (1 << WGM21);
  // Set CS21 bit for 8 prescaler
  TCCR2B |= (1 << CS21);   
  // enable timer compare interrupt
  TIMSK2 |= (1 << OCIE2A);


sei();//allow interrupts

}//end setup

// Interrupt Service Routine 

ISR(TIMER0_COMPA_vect){
//timer0 interrupt 2kHz toggles pin 8
}

ISR(TIMER1_COMPA_vect){
//timer1 interrupt 
}
  
ISR(TIMER2_COMPA_vect){
//timer1 interrupt 8kHz toggles pin 9
}


void loop(){
  //do other things here
}

Quellen

  1. AVR-GCC-Tutorial/Die Timer und Zähler des AVR https://www.mikrocontroller.net/articles/AVR-GCC-Tutorial/Die_Timer_und_Zähler_des_AVR
  2. Die Timer/Counter des AVR (Uni Regensburg)
  3. ATMega Datenblatt (deutsche Version)
  4. Timer Teil1 und Teil2 Wolles Elektronik Kiste https://wolles-elektronikkiste.de/tag/timer
  5. und die vielen übrigen Beiträge in Foren

Bit-Manipulations Mechanismen

Das Dualsystem spielt in der Welt der Steuerungstechnik eine fundamentale Rolle. Daher ist es wichtig zu wissen, wie man mit Binären Zahlen und Logischen Operationen umgehen kann. Hier sollen die wesentlichen  Bit-Operationen vorgestellt werden. Im Folgenden werden die Bit-Operatoren der Sprache C vorgestellt.

Logische Operationen

UND Verknüpfung

Die UND-Verknüpfung wird in C mit dem Operator & durchgeführt. Bei der bitweisen UND-Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen beide Vergleichswerte eine 1 besitzen. 

ODER Verknüpfung

Bei der bitweisen ODER-Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen mindestens einer der beiden Vergleichswerte eine 1 besitzt. Das Operatorzeichen dafür ist das einfache Pipe-Zeichen |.

Exklusiv-Oder Verknüpfung

Bei der bitweisen XOR Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen entweder der eine oder der andere Vergleichswert eine 1 besitzt. Das Operatorzeichen dafür ist das Dach-Zeichen ^.

NICHT Verknüpfung

Bei der bitweisen Negation wird jedes Bit umgekehrt: aus 0 wird 1 und aus 1 wird 0. Das Operator-Zeichen dafür ist die Tilde ~.

Beispiel-Programm

Im folgenden sollen die oben vorgestellten Verknüpfungen innerhalb eines Beispielprogramms umgesetzt werden:

    int x = 5;       // binary: 101
    int y = x & 1;   // now y == 1
    x = 4;           // binary: 100
    y = x & 1;       // now y == 0
    y = x | 2;       // now y == 110

    int x = 12;     // binary: 1100
    int y = 10;     // binary: 1010
    int z = x ^ y;  // binary: 0110, or decimal 6

    int a = 103;    // binary:  0000000001100111
    int b = ~a;     // binary:  1111111110011000 = -104

Schiebe-Operationen

Durch Verschieben, nach links mit << und nach rechts mit >>, wird ein Binärwert um eine bestimmte Anzahl von Bits nach links oder rechts verschoben. Die durch die Verschiebung freiwerdenden Stellen werden mit 0 gefüllt.

01101011 << 2 ergibt: 10101100
01101011 >> 4 ergibt: 00000110

Bit-Manipulation

In den folgenden Abschnitten wird gezeigt, wie man in einer Bitfolge gezielt einzelne Bits setzt, löscht oder umkehrt.

Setzen eines Bits

Um ein bestimmtes Bit innerhalb eines Bytes zu setzen kann man entweder eine Kombination aus einer Shift- und einer ODER-Operation verwenden oder den Maskierungsansatz verwenden.

Um das n-te Bit einer Variable A zu setzen wird folgende Formel verwendet:

Setzen von Bit 0:
0100 |= (1 << 0) ergibt: 0100 | 0001 ergibt: 01001
Setzen von Bit 3:
0100 |= (1 << 2) ergibt: 0100 | 0100 ergibt: 0100 (keine Veränderung)

Alternativ lässt sich ein Bit auch direkt über eine Maske setzen. Dazu verknüpft man den zu prüfenden Wert mit einer sogenannten Maske, in der genau das Bit (und nur das Bit) gesetzt ist, nach dem man setzen bzw. das man prüfen möchte. Der einzige Unterschied zur vorherigen Methode ist, dass die Maske manuell erstellt wird und nicht durch eine shift-Operation generiert wird.

Überprüfung ob ein BIT gesetzt ist
// Bit Operationen - Beispiel

  int Wert  = 0b01001111;
  int Maske = 0b00001000;
  int Pin = 9;
  
void setup() {
  Serial.begin(9600);
  pinMode(Pin,OUTPUT);

  // wenn Bit gesetzt leuchtet LED
  digitalWrite(Pin, (Wert & Maske) == Maske);
  
   if ( (Wert & Maske) == Maske) {
       Serial.println("Bit ist gesetzt");
    }
}

void loop(){
}

Löschen eines Bits

Das Löschen eines Bits kann durch die Kombination aus einem Shift, dem Komplement und der UND-Verknüpfung realisiert werden. Auch hier besteht die Möglichkeit anstelle des Verschiebens wieder Manuell eine Bit-Maske zu erzeugen und einzusetzen. Um das n-te Bit in der Variable A zu löschen (von 0 an gezählt, also vom niederwertigsten Bit) wird folgender Ausdruck verwendet:

Beispiel wie das Bit 0 der Variable A gelöscht (auf LOW) gesetzt werden kann:

A =  65  ==> 1000001
A &= ~ (1 << 0)   ==> 1001000 // lösche Bit 0

Togeln eines Bits

Das Wechseln eines Bits geschieht durch die Kombination der Shift-Funktion und der Exklusiv-ODER-Verknüpfung. Dabei wird das entsprechende Bit von 0 auf 1 oder von 1 auf 0 gesetzt. Um den Zustand des n-ten Bits in der Variable A zu wechseln (von 0 an gezählt) wird folgender Ausdruck verwendet:

byte A =  65;       //  ==> 1000001 
Serial.println(A);  // A=65
// toggle Bit 0
A = (A^=(1 << 0));  // ==> 1001000 
Serial.println(A);  // A=64

Bestimmung des Low-Bytes und High-Bytes

In verschiedenen Fällen benötigt man von einer 16-Bit-Variable die unteren 8-Bit und die oberen 8 Bit separiert. Hierzu können die oben beschriebenen Mechanismen verwendet werden:

 word wert = 27543;
  byte high_byte = wert >> 8; // oder (wert & 0xff00) >> 8
  byte low_byte  = wert & 0x00ff;

Arduino Funktionen

Da die oben beschriebenen Bit-Manipulationen relativ häufig vorkommen, und nicht immer jederzeit von weniger geübten programmieren abgerufen werden können, haben sich die Entwickler entschlossen, die häufig benötigten Funktionen in den Sprach-Umfang zu integrieren:

FunktionErklärung
bit(n)Berechnet den Wert des angegebenen Bits (Bit 0 ist 1, Bit 1 ist 2, Bit 2 ist 4, etc.).
bitClear(var,n)var: Die Zahlenvariable, deren Wert manipuliert werden soll.
n: Bit gelöscht werden soll; Startet bei 0 für das least-significant (rechteste) Bit.
bitRead(var,n)Liest n-tes Bit aus der Variable var
bitSet(var,n)Setze Bit n in der Variable var
highByte(var)Liest das most-significant (linkeste) Bit eines Wortes (oder das zweitkleinste Bit eines größeren Datentypes).
lowByte(var)Liest das least-significant (rechteste) Bit eines Wortes oder größeren Datentypes.
Vordefinierte Bit-Manipulations-Funktionen beim Arduino

Weitere Details finden sich in der Arduino-Referenz Dokumentation.

Arduino Grundlagen

Arduino ist eine offene Mikrocontroller-Plattform die aus einer Programmierumgebung und dem Mikrocontroller-Board. Aufgrund der einfachen Bedienbarkeit ist eine weltweite Community an Arduino-Freunden entstanden, die alle verschiedenste Projekte realisieren und die Ergebnisse mit allen Interessierten teilen.

Die Basis für Projekte ist meist der Arduino Uno für gut 20 Euro. Das Board verfügt über einen ATmega328P-Microcontroller, läuft mit den typischen 5 Volt, hat 14 Input/Output-Anschlüsse (I/O-Pins), 32 Kilobyte Flash-Speicher, USB-Anschluss und läuft auf moderaten 16 Megahertz.

Neben dem Uno gibt es aber noch viele weitere Boards in unterschiedlichen Größen und Leistungsklassen, die sich aber alle über die gleiche Entwicklungsumgebung (Arduino-IDE genannt) programmieren lassen. Als Programmiersprache wir C++ verwendet, die um einige spezifische Funktionen erweitert wurde.

Im folgenden sollen einige grundlegende Mechanismen vorgestellt werden, die das Fundament in verschiedenen Blog-Beiträgen bilden.

Ein- und Ausgabe von Daten

Wie oben erwähnt stellt jedes Arduino Board eine bestimmte Menge von Anschlüssen (PINs) bereit über die Daten eingelesen oder ausgelesen werden können. Dazu müssen die Anschlüsse-Pins über die Funktion pinMode entsprechen konfiguriert werden:

Nach dem die Konfiguration der Pins erfolgt ist, kann man Daten ein- bzw. ausgeben. Dazu stehen ebenfalls Funktionen mit den Namen digitalRead und digitalWrite zur Verfügung. Am sogenannten LED-Blinken-Beispiel lässt sich die Funktion sehr anschaulich darstellen:

void setup() {
  pinMode(13, OUTPUT); // Setzt den Digitalpin 13 als Outputpin
}

void loop() {
  digitalWrite(13, HIGH); // Setzt den Digitalpin 13 auf HIGH = "Ein"
  delay(1000);            // Wartet eine Sekunde
  digitalWrite(13, LOW);  // Setzt den Digitalpin 13 auf LOW = "Aus"
  delay(1000);            // Wartet eine Sekunde
}

Alle Funktionen sind in den Referenz-Seiten sehr gut beschrieben, so dass hier für weitere Details darauf referenziert werden soll.

Pull-up oder Pull-Down Widerstände

Ein Pullup- oder Pulldown-Widerstand wird dazu verwendet, einen Eingang auf einen definierten Wert zu “ziehen”. Normalerweise befindet sich der Eingang im Zustand “schwebend/hochohmig”, welcher sich irgendwo zwischen High und Low befindet.

Bei Arbeiten im Bereich der Mikrocontroller haben sich Werte von 4,7 kOhm für Pullup- bzw. 10 kOhm für Pulldown-Widerstände in den meisten Fällen bewährt.

Pull-Up und Pull-Down-Widerstands Schaltbilder

Es gibt zwei Möglichkeiten einen Schalter oder Taster mit einem logischen Eingangs-Pin zu verbinden. Will man dafür sorgen, dass der Eingangspin logisch LOW erhält, wenn die Taste gedrückt wird, so muss das Pull-Up-Prinzip verwendet werden. Der Pullup-Widerstand liegt zwischen dem Eingang und +Vcc. Beim Öffnen des Tasters zieht der Pullup-Widerstand die Spannung am Eingang hoch bis zum Betriebsspannungswert +Vcc, was logisch HIGH entspricht.

Will man dafür sorgen, dass der Eingang logisch HIGH erhält, wenn die Taste gedrückt wird, so muss das Pull-Down-Prinzip verwendet werden. Der Kontakt liegt zwischen dem Eingang und +Vcc. Der Pulldown-Widerstand liegt zwischen dem Eingang und GND. Beim Öffnen des Kontaktes zieht der Pulldown-Widerstand die Spannung am Eingang hinunter auf GND, was logisch LOW entspricht.

Interne Pull-x Widerstände

Um den zusätzlichen Bauteil- und Verdrahtungsaufwand beispielsweise beim ersten Prototypenaufbau zu vermeiden, sind im Microcontroller des Arduino-Boards bereits interne Pull-Up-Widerstände integriert. Sie lassen sich sehr einfach in der Pindeklaration hinzuschalten.

pinMode(8, INPUT_PULLUP);

Entstellen von Schaltern

Jeder mechanische Schalter schaltet nicht perfekt. Beim Schließen des Kontakts “hüpft” dieser einige Male hin und her, d.h. er schließt und öffnet, bis er ganz geschlossen ist.

Prell-Verhalten eines Schalters

Dieses Schalter-Prellen kann sich je nach Schaltertyp sehr negativ auf das Verhalten des Programms auswirken. Daher sollte man Schlater entstellen. Dazu gibt es Hardware- und Software-technische Lösungsansätze.

Ein sehr sicheres Mittel zum Entprellen von Schaltern ist die Zeit nach dem schliessen quasi auszublenden in dem man ein RC-Glied einführt. Dieses sorgt dann für eine “Glättung” der Spannungsspitzen und führt zu einem stabilen Eingangspegel.

Hardware-Tasten Entprellung

Weitere Möglichkeiten bieten Flipflops, was aber wiederum einen größeren Hardware-Aufwand zur Folge hat.

Software-technische Lösungen

Ohne Lib

gggg

const int buttonPin = 2;    
const int ledPin =  13;     
int ledState = LOW;
boolean buttonState = LOW; 

int pressed=0;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
}

void loop() {
  if(debounceButton(buttonState) == HIGH && buttonState == LOW){
    pressed++;
    buttonState = HIGH;
  }
  else if(debounceButton(buttonState) == LOW && buttonState == HIGH{
       buttonState = LOW;
  }
  if(pressed == 10){
    digitalWrite(ledPin,HIGH);
  }
}

boolean debounceButton(boolean state){
  boolean stateNow = digitalRead(buttonPin);
  if(state!=stateNow){
    delay(10);
    stateNow = digitalRead(buttonPin);
  }
  return stateNow; 
}

Dazu gibt es eine Bibliothek mit dem Namen “Bounce2”, die entsprechende Funktionen bereit stellt, um das mehrfache unkontrollierte senden von Signalen durch das Prellen zu verhindern.

Zunächst einmal muss für jeden Button eine Instanz von “Bounce” erzeugt werden. Im Konstruktor des Bounce Objektes wird der Pin an welchem der Taster angeschlossen ist und zusätzlich ein Wert für eine Pause (in Millisekunden)  übergeben.

#include <Bounce2.h>
#define BTN 2
int index = 0;

Bounce btnBouncer = Bounce(BTN, 50);

void setup() {
  Serial.begin(9600);
  pinMode(BTN, INPUT);
  digitalWrite(BTN, HIGH);  
}

void loop() {
  btnBouncer.update();

  if(btnBouncer.fell()){
    index = index+1;
    Serial.println(index);
  }
  
}

Port-Manipulation

Die Pins des Mikrocontroller sind in sogenannten Ports organisiert. Ein Port ist kein Pin sondern bezeichnet eine “Pin-Gruppe”. Die 14 Pins des Arduino Uno sind in 3 Ports gegliedert: Port B, C und D. Über die Port Manipulation schaltet man also die Pins eines Ports beziehungsweise einen ganzen Port auf einmal.

Port und Port-Register des Arduino UNO

Die Ports werden von jeweils 3 Registern gesteuert, die mit DDRx, PORTx und PINx bezeichnet werden. Wobei das x für die Ports Namen B,C und D stehen. Dh. für jeden Port gibt es 3 Steuer-Register Port, also zum Beispiel existiert für Port B ein DDRB-, ein PORTB- und ein PINB-Register. Analog gilt das auch für Port C und Port D.

Über das DDRx Register legt man fest, ob die einzelnen Pins eines Port OUTPUTs oder INPUTs sein sollen. Beispielsweise würde die Zeile DDRC = B00000111 die analog Eingänge A0 bis A2 als Input definieren und A3 bis A7 als Output definieren. 0 steht für Input und 1 für Output.

Über das PORTx Register legt man fest, ob die Pins auf LOW oder HIGH geschaltet werden sollen. Dabei steht das x wieder für den jeweiligen Port. Wenn wir also Pin 8, 10 und 12 auf HIGH und Pin 9, 11 und 13 auf LOW setzen wollen, schreiben wir PORTB = B101010.

Das PINx Register ist dazu da, um den aktuellen Status der Input Pins, also LOW oder HIGH, festzustellen. Dazu wählt man für x wieder den aktuellen Port und vergleicht dann das Register mit einer Abfolge. Beispielsweise überprüft if(PINB == 0b000000), ob alle Pins, des Ports B, auf LOW geschaltet sind. 

Ein etwas erweitertes Blink-Beispiel soll die praktische Anwendung illustrieren:

// Port-Manipulations Beispiele
char my_var =0;

void setup(){
  DDRB  = 0B11111111;   // alle Bits als Ausgang
  DDRC  = 0B11111111;   // Alle PINs im Port C sind Output
  DDRD  = 0B00000000;   // Alle PINs im Port D sind Input
  PORTD = 0B11111111;   // Alle PINs im Port D sind HIGH
  my_var = PIND     // Read Port D, put value in my_var
 }
void loop() {
  PORTB = B10101010; // Wechselblinker mit allen Ausgaengen
  delay(300);
  PORTB = B01010101;
  delay(300);
}

Damit stehen im Prinzip die identischen Mechanismen für die Konfiguration und die Ein- und Ausgabe von Daten wie Eingangs beschrieben bereit. Der Vorteil der Port-Manipulation liegt in der kompakten Formulierung, der Möglichkeit mehrere Pins gleichzeitig zu steuern und der sehr hohen Ausführungsgeschwindigkeit.  

Für jeden digitalWrite()-Befehl gibt es eine direkte Entsprechung: digitalWrite(16,HIGH) kann z.B. durch PORTC |= (1‹‹PC2) ersetzt werden, und digitalWrite(16,LOW) entsprechend durch PORTC &= ~(1‹‹PC2). Diese Schreibweise ist weniger einprägsam, dafür geht die Ausführung schneller. Die dafür zusätzlich erforderlichen Kenntnisse der Bit-Manipulation werden in einem separaten Beitrag erklärt. Der Nachteil gegenüber den leicht eingängigen Funktionsnamen (pinMode, digitalWrite, digitalRead) liegt sicher in der deutlich komplizierteren Handhabbarkeit.

Das gleiche Prinzip gibt es natürlich auch bei den leistungsfähigeren Boards wie dem Arduino Mega. Nur mit dem Unterschied, dass es hier natürlich wesentlich mehr Pins gibt. Die nachfolgende Graphik veranschaulicht die Pin-Zuordnung zu den Ports des Arduino Mega.

Arduino Mega Port-Belegung

Die einzelnen Register müssen nicht deklariert werden, sie sind in der Arduino-Entwicklungsumgebung bereits als Namen vordefiniert, und können direkt im Programmcode verwendet werden.