kramann.info
© Guido Kramann

Login: Passwort:










kramann.info
© Guido Kramann

Login: Passwort:




LEDs der Licht-Klangkugel ansprechen

(EN google-translate)

(PL google-translate)

Um eine LED blinken zu lassen, reicht das folgende Programm aus:

Hinweis: Die Anode bei PA1 muß auf High-Potential gesetzt werden, die Kathode bei PA0 auf Low-Potential. Das bedeutet, dass PA0 und PA1 mit DDRA als digitale Ausgänge konfiguriert werden müssen. Aber dann wird in Register PORTA das Bit0 (PA0) auf 0 gesetzt und Bit1 (PA1) auf 1.

//Blinken grüne LED

#include <avr/io.h>               
int main(void)                    
{      
   long pause;                    

   DDRA = 0;
   DDRB = 0;
   DDRC = 0;
   DDRD = 0;
                           
   DDRA |= 0b00000011;  //PA0 und PA1 als Ausgang konfigurieren          

   PORTA &= 0b11111100; //PA0 und PA1 auf Masse ziehen.      

   while(1)                       
   {                              
       PORTA |= 0b00000010; //PA1 auf 1 setzen       
       for(pause=0;pause<50000;pause++)
       {   
           DDRD = 0;    
       }
       PORTA &= 0b11111101; //PA1 auf 0 setzen       
       for(pause=0;pause<50000;pause++)   
       {
           DDRD = 0;    
       }
   }                              
}

Code 0-1: Quelltext: 001_gruen1_blink.c

Um das Hauptprogramm übersichtlicher gestalten, werden in der folgenden Programmvariante C-Funktionen eingesetzt, um die Blinkfunktion zu realisieren.

//Blinken grüne LED

#include <avr/io.h>  

void einLEDgruen1()
{
    PORTA |= 0b00000010; //PA1 auf 1 setzen       
}

void ausLEDgruen1()
{
    PORTA &= 0b11111101; //PA1 auf 0 setzen       
}

void pause(long dauer)
{
    while(dauer>0)
    {
        DDRC=0;
        dauer--;
    }
}
             
int main(void)                    
{      
   DDRA = 0;
   DDRB = 0;
   DDRC = 0;
   DDRD = 0;
                           
   DDRA |= 0b00000011;  //PA0 und PA1 als Ausgang konfigurieren          

   PORTA &= 0b11111100; //PA0 und PA1 auf Masse ziehen.      

   while(1)                       
   {                              
       einLEDgruen1();
       pause(50000);
       ausLEDgruen1();
       pause(50000);
   }                              
}

Code 0-2: Quelltext: 002_gruen1_blink_funktion.c

Nachteile der beiden vorangegangenen Lösungen sind:

  1. Beim Einfügen weiterer Programmzeilen ändert sich die eingestellte Pausendauer.
  2. Die zeitliche Pausendauer läßt sich schwer vorherbestimmen.
  3. In der Pausenschleife kann der Mikrocontroller keine weiteren Aufgaben erledigen.

Bei Beschäftigung mit diesem Thema bewet man sich im Bereich "Echtzeitsysteme". Hier geht es u.a. darum, intelligentes Task-Management zu verwirklichen, bei dem die oben genannten Probleme vermieden, oder zumindest veringert werden.

Realisierung einer Zeitbasis mit Hilfe eines Timers

Das nachfolgende Programm arbeitet grundlegend anders, um die Pause zwischen den geänderten Blinkzuständen zu realisieren, als die vorangehenden beiden:

Im nachfolgenden Programm wird einer der drei verfügbaren Timer, nämlich Timer1, so konfiguriert, dass er kontinuierlich bis zu einer vorgeebenen Grenze aufwärts zählt. In dem eingestellten Zählermodus "CTC - Clear Timer on Compare Match" kann diese Obergrenze in einem der verfügbaren Register gespeichert werden. Duech diesen "Trick" gelingt es leicht, eine Periode des Zählvorgangs (einmal bis zur Obergrenze zählen), sehr flexibel festzulegen. Der Timer wird dadurch zu einer Zeitbasis, eine Art interne Uhr, die im Programm eingesetzt werden kann, um beispielsweise Pausen eine exakte Länge zu geben.

Die möglichen Zählermodi können im Datenblatt des ATmega32 auf Seite 107 nachgelesen werden. Der hier verwendete Modus ist Modus 4.

Neben der Einstellung des Arbeitsmodus (hier 4), der hier in Register OCR1A speicherbaren Obergrenze des Zählvorgangs muß außerdem noch die Vorteilung bezüglich dem Systemtack (z.B. Quarzfrequenz des externen Quarzes, oder interne System-Clock-Frequenz) festgelegt werden. Diese Möglichkeiten sind über die Bits CS12, CS11 und CS10 des Konfigurationsregisters TCCR1B einstellbar und in einer Tabelle auf Seite 108/109 im Datenblatt beschrieben. In untigem Beispiel ist 101, also die maximal mögliche Vorteilung von 1024 eingestellt.

Somit finden beispielsweise die Zählschritte des Timers bei Taktung mit dem internen RC-Oszilator mit fclock=1MHz mit einer Frequenz von fzähl=fclock/1024 = 976,5625Hz statt. Somit dauert ein Zählschritt dt=0,001024s, also ungefähr eine Millisekunde.

Der gesamte Zählvorgang von 0 bis zur Obergrenze, die hier mit OCR1A==97 festgelegt ist, dauert dann 97*dt=0,099328s, also ungefähr eine zehntel Sekunde.

Somit wurde hier eine Zeitbasis von 0,099328s realisiert, wenn man einen Systemtakt von 1MHz voraussetzt.

Die bis hierher betrachteten Programmzeilen sind:

TCCR1A = (0<<COM1A1) | (0<<COM1A0) | (0<<COM1B1) | (0<<COM1B0) | (0<<FOC1A) | (0<<FOC1B) | (0<<WGM11) | (0<<WGM10);
TCCR1B = (0<<ICNC1) | (0<<ICES1) | (0<<WGM13) | (1<<WGM12) | (1<<CS12) | (0<<CS11) | (1<<CS10);
OCR1A = 97; //97=> ungefähr im 0,1 Sekunden-Takt wird Interrupt ausgelöst.

Code 0-3: Konfiguration von Timer1.

Hinweis: Die Register OCR1A und OCR1B werden im PWM-Modus des Timer1 dazu verwendet, die PWM-Pulsbreite festzulegen. Hier arbeitet der Timer aber nicht im PWM-Modus und so werden die beiden Register nicht zu diesem Zweck gebraucht. So kann hier OCR1A zu einem anderen Zweck benutzt werden, nämlich die obere Zählgrenze festzulegen.

Hinweis: Tabellen und Angaben zu den Arbeitsmodi, wie sie hier für den Timer1 aus dem Datenblatt entnommen wurden, existieren dort natürlich für die beiden anderen Timer auch und auch für die ganze restliche interne Peripherie.

Dabei lassen sich die in TCC1A und TCC1B zu setzenden Konfigurationsbits COM1A0, COM1B1, WGM13, WGM12 der Tabelle auf Seite 107 im Datenblatt entnehmen.

Verwendung eines Interupts zur zeitlichen einer internen Uhr

Man könnte nun den Zählstand des Timers1 direkt in seinem eigenen Programm benutzen. Das Zählregister des Timers1 TCNT1 und auch das aller anderen (TCNT0 und TCNT2) kann wie eine globale Variable überall im Programm verwendet werden. Es ist sogar möglich im eigenen Programm den Zählerstand zu verändern, z.B. ihn auf Null setzen. Da der Timer1 ein 16-Bit-Zähler ist, ist TCNT1 zwei Byte groß. Dagegen sind die Zählregister der anderen beiden 8-Bit-Timer nur ein Byte groß.

Die Änderungsgeschwindigkeit des Zählregisters ist aber gegenüber dem Blinkvorgang einer LED viel zu hoch: Einmal hochzählen dauert wie oben beschrieben lediglich eine zehntel Sekunde.

Statt dessen wird in dem nachfolgenden Programm ein interner Interrupt eingesetzt, um eine zyklisch zählende "interne Uhr" zu erhalten. Die Funktion SIGNAL(..) ist eine Interruptfunktion. Als Bedingung, wann diese aufgerufen wird, findet sich der Übergabewert SIG_OUTPUT_COMPARE1A.

Hinweis: Tatsächlich ist SIGNAL(..) ein Makro, wird also beim Kompilieren durch etwas anderes ersetzt. Deshalb findet sich hier scheinbar keine korrekte C-Syntax (fehlender Datentyp beim Übergabeparameter).

Mit SIG_OUTPUT_COMPARE1A wird der Interrupt so konfiguriert, dass er immer dann aufgerufen wird, wenn das Zählregister TCNT1 von Timer1 den in OCR1A gespeicherten Vergleichswert erreicht, sprich, wenn ein Flag gesetzt wird und das dieses anzeigt und der Timer daraufhin wieder automatisch auf Null zurückgesetzt wird.

Es sind ganz unterschiedliche Ursachen anwählbar für die Ausführung der Interrupt-Funktion SIGNAL. SIG_OUTPUT_COMPARE1A ist nur eine davon. Eine Liste der Optionen findet sich im Datenblatt auf Seite 42. Die genauen Bezeichnungen der Interrupt-Optionen für C finden sich jedoch in der Dokumentation avr_libc144.pdf von Atmel.

SIGNAL(SIG_OUTPUT_COMPARE1A)
{
    ZEIT++;
    ZEIT%=10000;    
}

Code 0-4: Interruptfunktion aus 003_gruen1_blink_timer2pause.c

Oben ist die Interrupt-Funktion aus 003_gruen1_blink_timer2pause.c gezeigt. Mit jedem Erreichen des Zählerregisters TCNT1 von 97 (Register-Belegung von OCR1A) wird die globale Variable ZEIT inkrementiert. Durch die nachfolgende Modulo-Division mit 10000 wird erreicht, dass ZEIT zyklisch in Schritten von ca. 0,1s bis 9999 zählt und dann wieder bei 0 beginnt. Somit kann ein Gesamtzyklus von 1000 Sekunden zur Programmsteuerung benutzt werden.

Die Aktivierung der Interrupts erfolgt mit der Bibliotheksfunktion sei(), die Deaktivierung kann mit cli() erfolgen. Um den Mikrocontroller so zu konfigurieren, dass bei "Compare-Match" zwischen TCNT1 und OCR1A auch ein Interrupt ausgelöst wird, wird ein entsprechendes Konfigurationsbit (Flag) gesetzt: Bit OCIE1A im Register TIMSK. Das Register TIMSK wird im Datenblatt im Kapitel zu Timer0 beschrieben (Seite 80).

Um die Interruptfunktion zu konfigurieren und zu aktivieren, dienen also die folgenden Befehle in dem nachfolgenden Programm:

    TIMSK |= (1<<OCIE1A); //Interrupt-Flag-Register, hier wird 
	                  //Timer/Counter1, Output Compare A Match Interrupt Enable gesetzt.
    sei();                //Interrupts allgemein erlauben.

Code 0-5: Konfiguration und Aktivierung der Interrupt-Funktion.

Nach diesen ganzen Konfigurationen, dient nun der folgende Code-Abschnitt dazu, die LED mit 1Hz blinken zu lassen:

       if(ZEIT%10==0)                 
           einLEDgruen1();
       
       if(ZEIT%10==5)                 
           ausLEDgruen1();

Code 0-6: Blinke mit 1Hz.

Hinweis: Das für die Variable verwendete Schlüsselwort "volatile" kann für globale Variablen eingesetzt werden und weist den Kompiler an, die entsprechende Variable für einen schnellen Zugriff im gesamten Programm verfügbar zu machen. Wird "volatile" nicht gesetzt, kann es vorkommen, dass Änderungen bei globalen Variablen gar keine Wirkung zeigen.

..und hier folgt der besprochene Quelltext:

//Blinken grüne LED

#include <avr/io.h>  
#include<avr/interrupt.h>

volatile unsigned int ZEIT = 0;

SIGNAL(SIG_OUTPUT_COMPARE1A)
{
    ZEIT++;
    ZEIT%=10000;    
}


void init()
{
   DDRA = 0;
   DDRB = 0;
   DDRC = 0;
   DDRD = 0;
                           
   DDRA |= 0b00000011;  //PA0 und PA1 als Ausgang konfigurieren          

   PORTA &= 0b11111100; //PA0 und PA1 auf Masse ziehen.      

    //Normaler Zählermode
	//Vorteilung 1024
	//1000000/1024=976,5625Hz

    //Mode 4 (s. Datenblatt S. 107):
    //CTC: Clear Timer on Compare Match
	//Vergleichswert in OCR1A

	//16Bit Zählregister: 0..65535
    TCCR1A = (0<<COM1A1) | (0<<COM1A0) | (0<<COM1B1) | (0<<COM1B0) | (0<<FOC1A) | (0<<FOC1B) | (0<<WGM11) | (0<<WGM10);
    TCCR1B = (0<<ICNC1) | (0<<ICES1) | (0<<WGM13) | (1<<WGM12) | (1<<CS12) | (0<<CS11) | (1<<CS10);

    TIMSK |= (1<<OCIE1A); //Interrupt-Flag-Register, hier wird 
	                     //Timer/Counter1, Output Compare A Match Interrupt Enable gesetzt.

    OCR1A = 97; //97=> ungefähr im 0,1 Sekunden-Takt wird Interrupt ausgelöst.
                //genauer sind es 10,067654639Hz, also 0,099328Sekunden pro Zählschritt.

    sei();

}

void einLEDgruen1()
{
    PORTA |= 0b00000010; //PA1 auf 1 setzen       
}

void ausLEDgruen1()
{
    PORTA &= 0b11111101; //PA1 auf 0 setzen       
}
             
int main(void)                    
{      
   unsigned int zaehler = 0;

   init();
   while(1)                       
   {             
       if(ZEIT%10==0)                 
           einLEDgruen1();
       
       if(ZEIT%10==5)                 
           ausLEDgruen1();
   }                              
}

Code 0-7: Quelltext: 003_gruen1_blink_timer2pause.c

Übung
  • Realisieren Sie ein Blinken der LED mit möglichst exakten 4Hz. Verwenden Sie auch dazu eine Taktung mit dem internen RC-Oszillator von 1MHz.
  • Da OCR1A ein 16-Bit-Register ist, kann darin ein maximaler Wert von 65536 stehen. Nutzen Sie dies aus, um ein exaktes 4Hz-Blinken auch ohne Interrupts zu realisieren.
  • Variieren Sie die vorangehenden beiden Aufgaben, indem Sie Modus 0 anstatt Modus 4 benutzen (vergl. Datenblatt).
musterloesungen.zip - Musterlösungen zu obiger Übung.