Atmel AVR Mikrocontroller mit OpenSuse

Mikrocontroller der Firma Atmel beinhalten eine CPU sowie RAM und EEPROM auf einem Chip. Es gibt Controller mit unterschiedlichen Leistungsmerkmalen, allen gemeinsam sind aber Grundgegebenheiten (Aufbau, Assembler-Dialekt). Im Umfeld dieser Controller hat sich eine starke, weltweite Community gebildet, so dass sowohl hardwaremäßig als auch softwaremäßig auf zahlreiche Vorleistungen zurückgegriffen werden kann.

Im folgenden ist der generelle Umgang mit AVR Mikrocontrollern beschrieben. Es wurde ausschließlich Open Source Software benutzt.

Wegen der Verbreitung der AVR-Controller gibt es viele C-Bibliotheken, die komplexere Tätigkeiten ausführen können. So gibt es zum Beispiel Bibliotheken für LCD-Ansteuerung, Echtzeituhransteuerung, RS232-Ansteuerung etc., so dass man nicht alles selbst programmieren muss.

Für fast jedes Problem mit dem AVR findet man im Internet schon Informationen oder fertige Lösungen. Ein bisschen hiervon wird im folgenden vorgestellt.

Inhaltsübersicht

Das „Hello World“ für AVR

Im folgenden ist ein einfache Programm dargestellt, das eine LED auf dem Board blinken lässt.

Eine Verzögerungsschleife (Funktion delay_ms) sorgt dafür, dass man das An- und Ausgehen der LED auch wahrnehmen kann. Die dort genannten Werte variieren nach der Taktfrequenz des Controllers, sollten aber für 8-12 Mhz tun.

Die LED hängt im Beispiel an Port D, Bit 1. Die ganzen Spezifika des Controllers sind in Include-Files avr/*.h abgelegt (Teil der Software von Atmel). Dort ist auch festgelegt, welches Register bei einem bestimmten Controller (z.B. ATMega8) für Port D, Pin 1 angesprochen werden soll. Dies verbirgt sich hinter der Präprozessor-Makro „PD1“. Inhaltlich wird das Bit 1 des Ports D auf Ausgabe geschaltet, und dann in einer while-Schleife abwechselnd auf 0 und auf 1 gesetzt. Zwischen den Wechseln wird jeweils gewartet. Im Ergebnis ist das ein Blinken.

#include <avr/io.h>
 
 void delay_ms(unsigned short ms)
 /* delay for a minimum of <ms> */
 /* with a 1Mhz clock, the resolution is 1 ms */
 {
    unsigned short outer1, outer2;
    outer1 = 50*12;
 
    while (outer1) {
       outer2 = 1000;
       while (outer2) {
          while ( ms ) ms--;
             outer2--;
       }
                 outer1--;
    }
 }
 
 void main(void)
 {
    /* INITIALIZE */
    /* enable PD1 as output */
    DDRD|= _BV(PD1);
 
    /* BLINK, BLINK ... */
    while (1) {
       /* pin=0, means LED on */
       PORTD &= ~_BV(PD1);
       delay_ms(500);
       /* pin=1, means LED off */
       PORTD|= _BV(PD1);
       delay_ms(500);
    }
 }

Port Pins abfragen und beschreiben

Jeder AVR besitzt eine Menge von Ports. Pro Port gibt es normalerweise 8 Pins. Ein Port kann als Eingang oder als Ausgang konfiguriert werden.

Wenn es ein Eingang ist, kann man dort den anliegenden logischen Wert abfragen (z.B. von einem Taster/Schalter).

Wenn es ein Ausgang ist, kann man dort einen logischen Wert setzen, der von der Peripherie (z.B. eine angeschlossene LED) genutzt wird.

Ein Port korrespondiert zu einem 8-Bit-Register des AVR. D.h. wenn man z.B. dort einen Wert hineinschreibt, hat dies Auswirkungen auf den zugeordneten Pin. So korrespondiert z.B. das niederwertigste Bit von Port A zu dem Pin namens PA0. Falls dieses Port nun als Ausgabeport konfiguriert wurde und man schreibt nun eine „1“ in die Bitposition 0 des Registers von Port A, so wird der Pin PA0 logisch 1.

Vor Nutzung eines Ports muss man es zunächst als Ein- oder Ausgang konfigurieren. Es gibt in der Sprache C und bei Nutzung der libavrc diverse Makros für Register und Bitpositionen, die man nutzen sollte:

PORTA, PORTB, … :  Portnamen für Ausgangsports
PINA, PINB, … : Portnamen für Eingangsports
DDRA, DDRB, … : Register, die die Konfiguration (Eingang, Ausgang) festlegen
PA0, PA1, …, PB0, PB1, … : Bitpositionen bzw. Pin-Namen (Ausgang)
PINA0, PINA1, … : Bitpositionen bzw. Pin-Namen (Eingang)

Alles folgende am Beispiel des Ports D (DDRD, PORTD, PIND), Pin 3 (PD3,PIND3). Die Makros PORTD und PD3 sind dabei für die Ausgabe, die Makros PIND und PIND3 für die Eingabe zu nutzen.

Konfiguration als Ausgang:

DDRD |= _BV(PD3); // Bit auf 1 setzen

_BV(x) ist hierbei ein Makro, welches  in einem Byte die Bit-Position x auf logisch 1 setzt, alle anderen Bits sind auf 0. Dieser Wert wird nun mit dem momentanen Inhalt von DDRD geodert und damit an dieser Stelle logisch 1. Alle anderen Werte bleiben unverändert.

Setzen des Bits:

PORTD |= _BV(PD3);

Löschen des Bits:

PORTD &= ~_BV(PD3);

Konfiguration als Eingang:

DDRD &= ~_BV(PIND3);

Lesen des Bits:

bitvalue = bit_is_set(PIND, PIND3);
// oder: 
bitvalue = PIND & _BV(PIND3) 
// oder auch mit
bitvalue = bit_is_clear(PIND, PIND3);

wobei bit_is_set() und bit_is_clear() wieder Makros der avrlibc sind.

Damit lassen sich alle Pins des AVR für Ein- und Ausgabezwecke nutzen. Allerdings muss beachtet werden, dass manche Pins zum Teil mit mehreren Funktionen belegt sind und daher nicht völlig frei genutzt werden können.

Schließlich besitzen die ATmega AVRs auch noch pro Input-Pin einen internen Pullup-Widerstand der irgendwo zwischen 20 und 50KOhm bemessen ist. Defaultmäßig ist dieser Pullup-Widerstand abgeschaltet. Wenn man ihn nutzen will, kann er eingeschaltet werden, indem man auf den bereits als Eingang konfigurierten Pin eine logische 1 schreibt. Dies versteht der AVR als Aufforderung, den Pullup-Widerstand zuzuschalten. Code hierzu:

DDRD &= ~_BV(PIND3);
PORTD |= _BV(PD3);

Tasteneingabe

Wie kann man mit einem Taster oder einem Schalter eine zuverlässige Eingabe für den AVR machen? Einfach ein Port abfragen langt nicht, denn der elektrische Vorgang des „Prellens“ eines Schalters produziert kein klares und eindeutiges Ein- oder Aus-Signal. Es werden pro Einschalten oder Ausschalten gewissermaßen mehrere Einsen und Nullen erzeugt, bis der Schalter/der Taster zur Ruhe kommt.


Im Bild dargestellt ist der Vorgang des Prellens beim Drücken (Übergang von 0 nach 1) und beim Loslassen (Übergang von 1 nach 0) des Tasters. Dasselbe physikalische Problem existiert auch bei der Verwendung von Schaltern statt Tastern oder auch wenn man einfach zwei Leitungen zusammendrückt.

Man hat also die Aufgabe des Entprellens. Dies kann softwaretechnisch wie folgt gelöst werden (Beispiel für Taste an Port C0):

void wait_until_key_pressed(void)
 {
     unsigned char temp1, temp2;
     unsigned int i;
 
     do {
         temp1 = PINC;                  // read input
         for(i=0;i<65535;i++)
                 ;
         temp2 = PINC;                  // read input
         temp1 = (temp1 & temp2);       // debounce input
     } while ( temp1 & _BV(PINC0) );
 
     loop_until_bit_is_set(PINC,PINC0); // wait until key is released
 }

Das oben dargestellte Entprellen von Tasten wird durch „aktives“ Warten, also das Durchlaufen von Verzögerungsschleifen implementiert. Während des Abarbeitens der Schleife kann der AVR im Hauptprogramm nichts anderes tun, es wird also Rechenleistung verschwendet. Dies kann durch die Abfrage des Tasterzustands innerhalb einer Interrupt-Routine verbessert werden. Im folgenden ist die Implementierung für einen Taster gezeigt.

 // in main()
 // Werte, die die key1-Variable annehmen kann 
 #define KEY_CLEAR 0
 #define KEY_SHORT_PRESSED 1
 
 // key1 enthaelt den Zustand des Tasters/Schalters
 volatile uint8_t key1;
 // Zaehler fur "Taste gedrueckt"
 volatile key1_presscount;
 
 
 // Zaehlerwert, ab dem ein Tastendruck erkannt wird
 #define SHORT_PRESS_VAL 100
 
 ...
   // Abfrage des Tasters
  if (key1==KEY_SHORT_PRESSED) {
   execute_some_command();
   key1 = KEY_CLEAR; // re-enable key input
 
 ...

Und im folgenden der Codeteil für die (Timer-) Interrupt Service Routine

// in ISR
 ...
 if (key1==KEY_CLEAR) {
  // read pin
  bitval = bit_is_clear(PINC,PINC0);
  if (bitval)
   key1_presscount++; // key is pressed, increment press count
  else
   key1_presscount=0; // key is released, reset press count
 
  // if key is pressed at least for the time required to increment key1_presscount to
  // the value of SHORT_PRESS_VAL, we have detected a (short) key press
  if (key1_presscount>SHORT_PRESS_VAL)
         key1=KEY_SHORT_PRESSED;
 }
 
 ...
 
 }

Setzen der Fuse-Bits, so dass ein externer Quarz genutzt wird

Der AVR kann entweder von einem internen Taktgeber oder einem externen Takt gespeist werden. Fabrikneu ist der AVR auf seinen internen Taktgeber eingestellt. Dies ist ein RC-Glied, welches auf 1 MHz eingestellt ist. Damit kann man schon vieles machen und spart sich eine externe Beschaltung. Wenn man eine RS232-Schnittstelle bedienen will, sind so allerdings nur bis maximal 4800 Baud möglich. Wenn man den AVR höher takten will, muss man ihn so umstellen, dass er einen externen Takt nutzt. Dieser kann von einem externen RC-Glied, einem externen Quarz oder einem externen Oszillator kommen. Je nach externer Beschaltung müssen die zugehörigen Fuse–Bits (CKSEL*) passend gesetzt werden. Das Setzen und Auslesen der Fuse-Bits kann mit dem Programm ponyprog geschehen. Für das Beispiel eines ATmega 32 hier der fabrikneue Zustand (zum Auslesen den Button „Read“ drücken):


Fuses im ATMega 32 in fabrikneuem Zustand. Ein Häkchen heißt bei ponyprog, dass die Fuses nicht gesetzt sind. Bei anderen Tools ist dies u.U. genau anders herum.

Und hier der Zustand nach dem Umstellen auf einen externen Quarz. Das Schreiben der Fuses erfolgt nach Drücken des Buttons „Write“.
Achtung, je nach AVR-Typ kann das anders aussehen. Entweder Handbuch oder verlässliche Internet-Quelle konsultieren. Andernfalls kann es passieren, dass der AVR nicht mehr weiter verwendet werden kann.

Der externe Quarz wird auf „übliche Weise“ mit 2x22pf Keramikkondensatoren gegen Masse an den Pins XTAL1 und XTAL0 angeschlossen.

Auch mit dem Tool „avrdude“ können die Fuses gelesen und beschrieben werden. Leider können die Fuses aber nicht mit ihrem Namen angesprochen werden.

Beispiele:
Abfragen der Fuses bei einem ATmega 644 über den seriellen Anschluss:

avrdude -v -p m644 -c ponyser -P /dev/ttyS1 -U lfuse:r:-:i

Abfragen der Fuses  für einen 644P via USB-Anschluss

avrdude -v -p m644p -c avrisp2 -P usb:884:92 -U lfuse:r:-:i

Umstellung eines ATmega32 auf Nutzung eines externen Quarzes über seriellen Anschluss:

avrdude -v -p m32 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xcf:m

Umstellung eines ATmega644 auf Nutzung eines externen Quarzes über über seriellen Anschluss:

avrdude -v -p m644 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xdf:m

Umstellung eines ATmega644 auf Nutzung eines externen Quarzes über USB:

avrdude -v -p m644 -c avrisp2 -P usb:884:92 u -U lfuse:w:0xff:m -U hfuse:w:0xdf:m

Umstellung eines ATmega8 auf Nutzung eines externen Quarzes über USB:

avrdude -v -p m8 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xd9:m

Umstellung eines ATmega644P auf Nutzung eines externen Quarzes über USB:

avrdude -v -p m644p -c avrisp2 -P usb:884:92 -U lfuse:w:0xff:m -U hfuse:w:0xdf:m -U efuse:w:0xff:m

Die aus der Belegung der Fuses resultierenden HEX-Werte und auch die Kommandozeile für AVR Dude können mittels des „AVR Fuse Calculator berechnet werden: http://www.engbedded.com/fusecalc

Verwendung von Timern

Die AVR Controller besitzen meist mehrere Timer. Mit diesen lassen sich zyklisch wiederkehrende Aufgaben bewältigen. Es gibt 8-Bit und 16-Bit Timer. Ein Timer kann z.B. so programmiert werden, dass er bei Ablauf eines voreingestellten Sollwerts einen Interrupt auslöst. Es existieren zahlreiche andere Funktionen in Verbindung mit Timern. Ausprobiert habe ich den Interrupt-Fall mit einem 8-Bit Timer am Mega 32.

Man kann mithilfe des Timers eine Interrupt-Routine aufrufen und/oder spezielle Pins des AVRs (OCn) periodisch setzen und löschen. Mit letzterer Funktion kann man ziemlich einfach eine stabile Frequenz an einem Pin des AVRs erzeugen und muss sich nach der Initialisierung des Timers nicht mehr um die Frequenzerzeugung kümmern (Waveform Generation Mode).

Mittels des folgenden Fragments wird eine Variable definiert, die vom der Timer Interrupt Service Routine verändert wird. Variable, die von Interrupts beschrieben werden und die auch vom Hauptprogramm genutzt werden sollen, müssen als „volatile“ definiert werden:

volatile uint8_t timer2;    // timer value

Die eigentliche Service Routine zählt die definierte Variable hoch:

// Interrupt Service Routine 
ISR(TIMER2_COMP_vect) {
 timer2++; 
}

Die folgende Funktion kann nun von der Anwendung aus genutzt werden, um einen definierten Zeitraum nichts zu tun:

inline void sleep(uint16_t milli ) {
     uint8_t t=4;
     int i;
     
     for(i=0; i<milli; i++) {
        timer2 = 0;
        while (timer2 < t);
     }
 }

Hier als Anwendungsbeispiel eine Art „Uhr“, die in einer Endlosschleife alle Sekunde eine Art Uhrzeit ausgibt:

int main(void) {
     char buf[32];
     int h=0,m=0,s=0;
 
     ...
 
     // CTC mode - clear on compare match
     TCCR2 =  _BV(WGM21);   
     // clock prescaler - CS21 means prescaler 8, see atmega manual
     TCCR2 = _BV(CS21);  
      
     // compare value - to be calculated, see atmega manual
     OCR2  = 250; 
 
     // if interrupt is required, enable it - Ooutput Compare Interrupt
     TIMSK |= _BV(OCIE2);    
     sei(); // Interrupts freigeben
 
     //  if output pin OC2 shall be used, set register flag - toggle on match
     // not used here
     //TCCR2 |= _BV(COM20);
 
     ...
 
     for(;;) {
         sleep(1000);
         s++;
         if (s>=60) {
             s=0;
             m++;
             if (m>=60) {
                m=0;
                 h++;
                 if (h>=24) {
                     h=0;
                 }
             }
         }
         sprintf( buf, "%02d:%02d:%02dr", h, m, s);
         DO(buf);
      }
 }

CTC Modus

Die Zeiten für den Timer werden durch den Prescaler-Wert und den Compare-Wert des Timers festgelegt.

Der Prescaler-Wert ist ein Teiler, der auf die Taktfrequenz des Controllers angewandt wird. Z.B. lässt sich so die Taktfrequenz von 8 MHz mit dem Teiler 64 auf 125 Khz herunterteilen oder mit dem Teiler 8 auf 1Mhz. Mit dieser Frequenz wird der Timer getaktet.

Der Compare-Wert ist der Wert, bei dessen Erreichen der Interrupt ausgelöst wird. Wenn man also z.B. einen Teiler von 64 hat und den Compare-Wert auf 62 einstellt, wird der Interrupt bei 8Mhz Takt 992,06-mal pro Sekunde ausgelöst, also fast genau jede Millisekunde.

Die zugrundeliegende Formel beim ATmega32 ist laut Handbuch:

f_OCn = f_CLK / (2*N*(1+OCRn))

wobei:
f_OCn: Frequenz an Pin OCn
f_CLK: Taktfrequenz des AVRs
N: Prescaler Wert
OCRn: Wert des Registers OCRn

Umgestellt nach OCRn gilt:

OCRn = (f_CLK / (2*N*f_OCn)) – 1

Im folgenden  sind diese beiden Formeln als JavaScript verfügbar.

CTC Modus: Berechnung der Frequenz aus Taktfrequenz, Prescaler und OCR-Wert

OCR value (0..255 for 8-Bit-OCR, 0..65535 for 16 Bit OCR)
Prescaler value
CPU clock value [Hz]Resultierende Frequenz an OCn berechnenFrequency [Hz]

CTC Modus: Berechnung von OCR aus Taktfrequenz, Prescaler und gewünschter Frequenz

Desired frequency value [Hz]
Prescaler value
CPU clock value [Hz]Benötigten OCR-Wert berechnenOCR value (0..255 for 8-Bit-OCR, 0..65535 for 16 Bit OCR)

Wer allerdings auf der Basis eine Uhr bauen will, wird feststellen, dass die Zeiten nicht genau stimmen. Dies liegt daran, das durch die Integerrechnung Ungenauigkeiten entstehen. Durch Probieren mit den Werten kann man eine gewisse Genauigkeit erreichen. Rein rechnerisch kommen für 8Mhz Taktfrequenz und dem Ziel, eine millisekundengenaue Uhr zu bauen folgende Werte heraus: Vorteiler 8 ergibt bei 8 MHz CPU-Takt 1 MHz Takt für den Zähler. Wenn die Interrupt-Service alle Millisekunde ausgelöst werden soll, müsste man den OCR2-Wert auf 499 setzen, was aber wegen der Beschränkung auf 8 Bit (bei einem 8-Bit-Timer) nicht geht. Man kann ihn aber auf 250 setzen und eine umliegende Schleife 2 mal ausführen. Wenn man das so macht, wird der Zähler etwas zu langsam laufen. Ein Vergleich mit einer Stoppuhr zeigt nach 10 Minuten eine Abweichung (zu langsamer Lauf) von etwa einer Sekunde, also 0,17%.

16Bit-Timer: Unter http://www.mikrocontroller.net/articles/AVR_-_Die_genaue_Sekunde_/_RTC ist die Nutzung des 16-Bit-Timers für eine Uhr gut beschrieben. Eine damit aufgebaute Uhr zeigt bei mir auch nach 40 Minuten keine Gangabweichung. Der Compare-Wert des 16Bit-Timers kann als 16Bit-Wert deutlich höhere Werte annehmen.  Ansonsten ist die Programmierung vom Ablauf her ähnlich wie beim 8Bit Zähler.

Spezielle Infos zu AVR Timern:  http://www.roboternetz.de/wissen/index.php/Timer/Counter_(Avr)

Analog-Digital Umwandlung

In den meisten AVR Mikrocontrollern ist auch ein AD-Konverter (ADC) enthalten. Dieser kann mit 8- oder 10-Bit-Auflösung Spannungen, die an mehreren Kanälen (Pins) anliegen, konvertieren. Er kann einmalig konvertieren oder auch fortlaufend.

Für das Testen dieses Features startet man mit einer Funktion, die die eigentliche AD-Konversion durchführt. Ich habe dazu die Funktion ReadChannel, wie bei www.mikrocontroller.net beschrieben ( http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Der_interne_ADC_im_AVR) verwendet. Die Werte für ADPS2, ADPS1 und ADPS0 müssen aus der Taktfrequenz der CPU berechnet werden. Die Formel ist ebenfalls unter obiger URL zu finden.

#include <inttypes.h>
#include <avr/io.h>
 
/* Funktion übernommen aus http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Der_interne_ADC_im_AVR */
uint16_t ReadChannel(uint8_t channel ) {
   uint8_t i;
   uint16_t result;
 
   ADCSRA = _BV(ADEN) | _BV(ADPS2) | _BV(ADPS1);    // Frequenzvorteiler setzen auf 64 und ADC aktivieren
 
   ADMUX = channel; // Kanal waehlen
   ADMUX |= _BV(REFS1) | _BV(REFS0); // interne Referenzspannung nutzen
 
   /* nach Aktivieren des ADC wird ein "Dummy-Readout" empfohlen, man liest
      also einen Wert und verwirft diesen, um den ADC "warmlaufen zu lassen" */
   ADCSRA |= _BV(ADSC); // eine ADC-Wandlung
   while ( ADCSRA & _BV(ADSC) ) {
      ;     // auf Abschluss der Konvertierung warten
   }
   result = ADCW;  
   // ADCW muss einmal gelesen werden,
   // sonst wird Ergebnis der nächsten Wandlung
   // nicht übernommen.
 
   /* Eigentliche Messung - Mittelwert aus 4 aufeinanderfolgenden Wandlungen */
   result = 0;
   for( i=0; i<4; i++ ) {
      ADCSRA |= _BV(ADSC); // eine Wandlung "single conversion"
      while ( ADCSRA & _BV(ADSC) ) {
         ;   // auf Abschluss der Konvertierung warten
      }
      result += ADCW; // Wandlungsergebnisse aufaddieren
   }
   ADCSRA &= ~_BV(ADEN); // ADC deaktivieren (2)
   result /= 4; // Summe durch vier teilen = arithm. Mittelwert
   return result;
}

Im Hauptprogramm:

#define VREF 2.56 /* value of (interna) reference voltage) */
 
/*
 ** function prototypes
 */
uint16_t ReadChannel(uint8_t channel);
 
int main(void) {
   int value;
   char buf[32];
   float f;
 
   DDRC &=~ (1 << PC3); /* Pin PC3 input */
 
   DI();
   /* now enable interrupt, since UART library is interrupt controlled */
   sei();
   DO("after init usartnr");
 
   while (1) {
      value = ReadChannel(3); // channel 3 is PC3
      f = value;
      f = (f*VREF)/1024.0; // 256 for 8 bits, 1024 for 10 bits; VREF=2.56 for internal ref.
      sprintf( buf, "Measuring %.3f Volt (raw value = %d)r", (double)(f), value  );
      DO(buf);
      dowait();
   }
}

Beim Linken muss eine zusätzliche printf-Bibliothek, die auch Floats ausgeben kann, hinzugebunden werden (libprintf_flt). Diese Bibliothek wird normalerweise nicht eingebunden, weil sie deutlich größere Executables produziert.

Projekt -> Properties -> C/C++ Build -> Settings -> Tool Settings, dort unter „Linker“ -> Other options“ folgendes eintragen:

und unter „Linker -> Libraries“ die zusätzliche Library für Floats einbinden:

Dann produziert das Programm bei Anlegen einer Spannung an Pin C3 folgende Ausgabe:

Ein parallel angeschlossenes Messgerät zeigt 1,869 Volt an, also so ziemlich korrekt 🙂

Schwingt mein Quarz?

Beim Basteln mit Quarzen ist manchmal unklar, ob der Quarz überhaupt etwas tut. So habe ich Probleme gehabt, einen DS1302 mit einem Uhrenquarz (32768Hz) zum Laufen zu bringen. Letztendlich habe ich das Thema aufgegeben. Die folgende Schaltung ist schwingfreudig und einfach aufzubauen.

Ich habe damit Quarze mit Frequenzen zwischen 1,8Mhz und 10Mhz problemlos zum Schwingen gebracht.

Integrierte Quarzoszillatoren lassen sich ebenfalls zuverlässig als Schwingungsgeber benutzen und sind besonders einfach zu beschalten. Das Bild unten zeigt die Beschaltung. Ein Oszillator mit 49,4563 Mhz bringt beispielsweise 2,5V Amplitude bei einer Betriebsspannung von 7,5V.


Ein integrierter Quarz Oszillator

Beschaltung eines integrierten Quarz Oszillators (Die Beschaltung der Pins scheint bei allen diesen Bausteinen gleich zu sein, aber genau weiss ich das nicht, also im Zweifelsfall Datenblatt nutzen). Unten rechts geht 0V dran, oben links die Betriebsspannung. Das Ausgangssignal geht oben rechts ab. Eine Ecke des Bausteins ist wirklich eckig, die anderen nur abgerundet, damit hat man die Grundorientierung

Schwingt der Quarz an meinem AVR?

Falls der AVR völlig tot scheint und auf nichts mehr reagiert, z.B. bei der ISP-Programmierung, ist es sinnvoll, zu prüfen ob der Quarz am AVR überhaupt schwingt. Wenn der AVR auf Quarzbetrieb umgestellt ist und keine Schwingungen vom Quarz kommen, kommt vom AVR keine Reaktion mehr.

Am Quarz fliessen allerdings so geringe Ströme, dass bei niederohmigen Meßmethoden die Schwingung abreist. Dies ist also ein schönes Beispiel, bei dem die Messung das Meßergebnis vollkommen bestimmen kann.

Mit einem Oszilloskop mit hohem Eingangswiderstand ist die Messung möglich. Die Tastköpfe von Oszilloskopen sind bei der Stellung 10:1 (oder gar 100:1, wenn man das hat) sehr hochohmig. Dann habe ich an einem ATmega644 mit 3,3V Betriebsspannung beispielsweise eine Spannungsamplitude direkt am Quarz gemessen. Bei 8Mhz waren es 1,7V an einem und 0,72V an dem anderen Pin.

Austausch der vorhandenen avr-libc durch eine neuere Version

Falls man Quellcode aus dem Internet bezieht, kann es vorkommen, dass dieser mit neueren Versionen der avr-libc entwickelt wurde. Man merkt das daran, dass Funktionen vom Compiler und dem Linker als unbekannt gemeldet werden. Man kann dann versuchen, ein neueres Paket für seine Plattform zu installieren. Wenn man OpenSuse und Yast regelmäßig nutzt, hat man allerdings schon das neueste Paket für diese Plattform. Man kann dann aber die avr-libc selbst compilieren. Ich habe dies wie folgt gemacht:

  1. Download der neuesten avr-libc von http://www.nongnu.org/avr-libc/. Die Source findet man unter  http://download.savannah.gnu.org/releases/avr-libc/
  2. Compilieren der avr-libc. Hier kann man nach dem README und dem INSTALL vorgehen. Diesen Schritt würde ich als Non-Root machen, um zu verhindern, dass man sich mit falschen Parametern die vorhandene avr-libc zerschießt. Ich habe das configure-Kommando wie folgt aufgerufen:
    ./configure –build=`./config.guess` –host=avr –prefix=/home/dennis/avrlibc
    Damit wird die libc unterhalb meines Homeverzeichnisses gebaut.
  3. Statt einem „make install“ habe ich dann per Hand die beiden erzeugten Verzeichnisse (avr/include, avr/lib) in das Verzeichnis kopiert, in dem die alte Bibliothek liegt. Dies ist bei OpenSuse /opt/cross. Die dort vorhandenen Verzeichnisse avr/lib und avr/include habe ich vorher gesichert.
  4. Zum Schluss noch aus dem gesicherten lib-Verzeichnis die ldscripts in das neue lib-Verzeichnis kopieren:
    cp -a ./lib.orig/ldscripts ./lib

Danach sollte ein Kompilieren wieder möglich sein und die neue Bibliothek wird benutzt.

„Echte Projekte“

Über obige Funktionstests hinausgehende Aktivitäten:

Weiterführende Links

DCF77 Zeitsignal Erkennung mit AVR Microcontroller

DCF77 ist das Signal, welches Funkuhren ansteuert. Es ist ein Langwellensignal auf 77,5 Khz. In einem Zeitraum von einer Minute werden 59 Informationsbits in dem Langwellensignal codiert. Dieser Datensatz von 59 Bits enthält Uhrzeit, Datum und ein paar weitere Informationen. Jedes Bit wird in einem Zeitraum von einer Sekunde gesendet. Innerhalb dieser Sekunde bedeutet ein Low-Pegel von der Dauer von 10ms eine logische „0“ und ein Low-Pegel von der Dauer von 20ms eine logische „1“. Die negative Flanke markiert dabei den Beginn eines Bits und auch den Beginn einer Sekunde. Nach dem 58. Bit bleibt das Signal „1“ bis zur negativen Flanke der Sekunde 0 (und des ersten Bits) der nächsten Minute. Somit bleibt das Signal mindestens eine Sekunde auf „1“, damit kann man den Beginn der Übertragung eines neuen Datensatzes erkennen. Genaueres zu DCF77 kann man hier nachlesen.

Kaum ein AVR-Fan wird um die Implementierung einer Uhr herumkommen, die das DCF77-Signal decodiert.

Der eigentliche Empfänger für das Zeitsignal kann als fertiges Modul beschafft werden, z.B. bei Conrad (ca. 11€). Der Empfänger hat eine kleine Ferritantenne, und gibt das Signal direkt und invertiert ab. An diesen Empfänger habe ich noch eine kleine Transistorstufe angeschlossen und das Signal dann so direkt in den AVR hineingeführt. Wenn man den Empfänger an ein Oszilloskop anschließt, sieht man bei korrekter Synchronisation des Empfängers (Zeitablenkung auf 10ms einstellen) das Zucken der Signale im Sekundentakt. Wenn man da nichts sieht, ist die Antenne nicht sauber ausgerichtet. Obwohl (oder gerade weil?) ich in Frankfurt wohne, muss ich die Antenne nach Süden ausrichten, sonst bekomme ich kein Signal. Der DCF77-Sender steht in Mainflingen ca. 20km südöstlich von Frankfurt am Main.

DCF77 Empfänger. Der Empfänger besitzt 4 Anschlüsse (GND, Vcc, Ausgangssignal und Ausgangssignal invertiert (offener Kollektor)).

Das am invertierten Ausgang vorhandene Signal wird an die Transistorstufe rechts angeschlossen, dort noch mal invertiert so dass am AVR das Signal wie im Text beschrieben anliegt.

Das empfangene Signal ist der Zeitwert für die folgende Minute. Es werden keine expliziten Sekundenwerte geliefert. D.h. wenn das 59. Bit kommt, kann man den bisher gelesenen Zeitwert als korrekte Uhrzeit nehmen und den Sekundenwert der AVR-internen Software-Uhr auf Null setzen. Diese Software-Uhr muss man implementieren, z.B. mit einem Timer und Timer-Interrupt. Die Uhr läuft also auch ohne DCF77 brav vor sich hin.

Die Bedeutung der einzelnen Bits des Signals ist unter http://de.wikipedia.org/wiki/DCF77 ausführlich beschrieben.

Das DCF77-Signal kann man über einen der externen Interrupts (INT0/1) des AVR softwaremäßig aufnehmen. Bei jeder negativen Flanke kann man den Interrupt auslösen lassen und dann einen weiteren Timerwert ab Null hochzählen. Man konfiguriert gleichzeitig die Interruptbedingung so um, dass der nächste Interrupt bei einer positiven Signalflanke ausgelöst wird. Wenn die positive Flanke kommt, kann man dann aus dem Timerwert ablesen, ob es sich um eine Null oder eine 1 handelt. Wenn z.B. 200-mal pro Sekunde der Timerwert hochgezählt wird, bedeutet ein Wert um die 20 dass 10ms verstrichen sind (eine „0“), ein Wert um die 40 dass 20 ms verstrichen sind (eine „1“).

Die einkommende Bitfolge kann man direkt auswerten oder in einem Array ablegen. Aus der Bedeutung der Bits heraus können dann die Werte für Uhrzeit, Datum etc. berechnet werden.

Wenn der Start einer Minute eintritt (negative Flanke für Bit 0), werden die DCF77-Werte in die interne Uhr des AVR kopiert.

Damit hat man die Funktion einer Funkuhr im wesentlichen implementiert.

#define DEBOUNCE 200L /* timer isr is called as many times per second */
 
 volatile uint16_t clock_timer; // clock timer value
 volatile uint16_t bit_timer; // bit timer value
 volatile uint16_t bit_ticks_0; // ticks with value "0"
 volatile uint16_t bit_sequence; // ticks with value "0" or "1", i.e. whole bit sequence 
 volatile uint8_t irq_falling_edge; // 1= irq o falling, 1= irq on raising edge
 
 uint8_t bits[60]; // array for each bit received
 
 // define for "undefined time value"
 #define UNDEF_TIME 99
 
 // vars h,m,s for the timer driven internal clock
 volatile uint8_t s=0, h=0,m=0;
 
 // vars for dcf driven clock and date values
 volatile uint8_t dcf_day=UNDEF_TIME, dcf_y=UNDEF_TIME, dcf_mo=UNDEF_TIME, dcf_d=UNDEF_TIME, 
 dcf_h=UNDEF_TIME, dcf_m=UNDEF_TIME;
 
 // Interrupt Service Routine 
 //  This routine is called when the Timer Value TCNT1 reaches the Output Compare Register Value OCR1A
 // 
 ISR(TIMER1_COMPA_vect) {
     bit_timer++;
 #if SYSCLK % DEBOUNCE
     OCR1A = SYSCLK / DEBOUNCE - 1;
 #endif
     if (--clock_timer==0) {
         clock_timer=DEBOUNCE;
         s++;
 #if SYSCLK % DEBOUNCE
         OCR1A = SYSCLK / DEBOUNCE + SYSCLK % DEBOUNCE - 1;
 #endif
     }
 }
 
 // Interrupt Service Routine 
 //  This routine is called when IRQ0 occurs
 // 
 ISR(INT0_vect) {
     if (irq_falling_edge==1) {
         bit_sequence=bit_timer; // save value
         bit_timer=0; // reset bit_timer
         irq_falling_edge=0; // wait for next raise
         MCUCR = (1<<ISC00)|(1<<ISC01); // raise int0 on rising edge
     } else {
         bit_ticks_0=bit_timer; // save value
         irq_falling_edge=1; // wait for next fall
         MCUCR = (1<<ISC00); // raise int0 on falling edge
     }
 }

Im Hauptprogramm macht man dann folgendes:

int main(void) {
     int bit_number=0;
     volatile uint8_t bit_value; // the resulting bit value, 0 or 1
     char *str; // some  string for text
 
     ioinit();
     fdevopen(uart_putchar, NULL );
     
     printf("nnDCF Clocknn");
 
     // Initialisierung:
     // (1<<CS10) : Timer1 Vorteiler = 001 = 1. Der Zähler wird also mit f=8Mhz hochgezählt
 
     // OCR1A=XTAL/DEBOUNCE-1 -> Bei Erreichen dieses Wertes (39999) wird die ISR besucht
     // die ISR wird also alle 1/200s aufgerufen. Wenn man dort also bis 200 hochzählt, ist genau
     // 1 Sekunde um! 200 ist der Wert der Variable clock_timer und wird über das define DEBOUNCE festgelegt
 
     TCCR1B = (1<<CS10) ^ (1<<WGM12);    // Prescaler of 1 | CTC mode
     OCR1A  = SYSCLK/DEBOUNCE-1;        // Output compare register value 
     TCNT1 = 0; // Start value for timer register
     s=0; // Initialize second value (s) to zero
     clock_timer = DEBOUNCE; 
     TIMSK |= (1<<OCIE1A);        // activate timer interrupts which starts timer run
 
     // INT0
     bit_ticks_0=0;
     bit_sequence=0;
     irq_falling_edge=1; // start with falling edge detection
     MCUCR = (1<<ISC00); // raise int0 on falling edge
     GIMSK |= (1<<INT0); // enable external int0
 
     /*
      * now enable interrupt, since UART and TIMER library is interrupt controlled
      */
     sei();
 
 
     for(;;) {
         
         // timer driven clock: the second value (s) is increased by interrupt.
         // set up m and h according to s changes
         if (s>=60) {
             s=0;
             m++;
             if (m>=60) {
                 m=0;
                 h++;
                 if (h>=24) {
                     h=0;
                 }
             }
         }
         //printf( "%02d:%02d:%02dr", h, m, s);
 
         // check what bit value was received (0 or 1)
         if (bit_ticks_0>0) {
             if(bit_ticks_0>25)
                 bit_value=1;
             else
                 bit_value=0; 
             if (bit_sequence>250) {
                 // we are at falling edge after second 59
                 bit_number=0;
                 str="- Start of Minute";
                 // now copy DCF clock values to timer driven clock
                 set_clock();
             } else
                 str="";
             
             // decode bits as far as possible
             decode( bit_number, bit_value, str );
             // clear timers for next bit
             bit_sequence=0;
             bit_ticks_0=0;
             // increase bit counter
             bit_number++;
         }
     }
 }

Die Funktion decode() berechnet die DCF77 Werte aus den Bits, die Funktion set_clock() setzt die internen Uhrenwerte aus den DCF77-Werten:

//
 // incrementally decode bits collected so far.
 // for meaning of bits, see DCF description
 //
 void decode( uint8_t i, uint8_t bit_value, char *string ) {
     printf("bits[%02d]=%d (%d of %d ticks low) %sn", i, bit_value, bit_ticks_0, bit_sequence, string );
 
     // save bit value
     bits[i] = bit_value;
 
     if (i==28) {
         // minute value 0..59 complete
         dcf_m = bits[21]+bits[22]*2+bits[23]*4+bits[24]*8+bits[25]*10+bits[26]*20+bits[27]*40;
     }
     if (i==35) {
         // hour value 0..24 complete
         dcf_h = bits[29]+bits[30]*2+bits[31]*4+bits[32]*8+bits[33]*10+bits[34]*20;
     }
     if (i==41) {
         // date value 1..31 complete
         dcf_d = bits[36]+bits[37]*2+bits[38]*4+bits[39]*8+bits[40]*10+bits[41]*20;
     }
     if (i==44) {
         // weekday 0..6 complete
         dcf_day = bits[42]+bits[43]*2+bits[44]*4;
     }
     if (i==49) {
         // month 1..12 complete
         dcf_mo = bits[45]+bits[46]*2+bits[47]*4+bits[48]*8+bits[49]*10;
     }
     if (i==57) {
         // year 00..99 complete
         dcf_y = bits[50]+bits[51]*2+bits[52]*4+bits[53]*8+bits[54]*10+bits[55]*20+bits[56]*40;
     }
 
     printf( "DCF: %d, %02d.%02d.20%02d %02d:%02d - AVR %02d:%02d:%02dr", 
             dcf_day, dcf_d, dcf_mo, dcf_y, dcf_h, dcf_m,
             h, m, s );
 }
 
 //
 // copy dcf values to internal clock values
 //
 void set_clock( void ) {
     h = dcf_h;
     m = dcf_m;
     s = 0;
 }