AVR - Die genaue Sekunde / RTC

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Einleitung

Oftmals sieht man Projekte, bei denen ein externer RTC-Baustein (engl. Real Time Clock, Echtzeituhr; z. B. PCF8583 mit I2C-Anschluss Datenblatt (PDF)) angeschlossen ist, ohne dessen Vorteil wirklich zu nutzen.

Die wichtigsten Vorteile einer externen RTC sind

  1. Die Zeitzählung läuft während eines Ausfalls der Hauptstromversorgung über eine kleine Stützbatterie bei geringem Strombedarf weiter.
  2. Es gibt hochgenaue RTCs, die man im Gegensatz zu Selbstbaulösungen nicht abgleichen muss.
  3. Der Strombedarf eines externen RTC ist in der Regel etwas niedriger als die RTC Funktion eines Prozessors. Als Vergleich diene ein DS1302(300nA@2V) zu einem Mega32(5uA@2.7V) als nicht ganz neuer Vertreter, bzw. ein Mega324P(500nA@1.8V) als moderner Vertreter. Dieser Vorteil kommt bei Datenschreibern (engl. data logger) zum Tragen, welche die meiste Zeit inaktiv im Sleep Mode sind.

In allen anderen Fällen ist die zusätzliche Hardware nicht unbedingt nötig und man kann eine präzise Zeitbasis bequem mit dem Hauptquarz des Mikrokontrollers programmieren. Daraus ergeben sich sogar noch zusätzliche Vorteile, wie eine geringere Temperaturabhängigkeit und höhere Güte, die Quarze im MHz-Bereich gegenüber Quarzen im kHz-Bereich besitzen. Ich möchte hiermit die Angst vor dem bisschen Mathematik nehmen, welche zur Berechnung der Teilerfaktoren benötigt wird.

Lösung

Speziell für den AVR kommen Quarze im Bereich 1MHz bis 20MHz zum Einsatz, d.h. in einer Sekunde werden 1.000.000 bis 20.000.000 Zyklen durchlaufen (Zyklen pro Sekunde = Frequenz). Möchte man eine Sekunde messen, kann man somit die entsprechende Anzahl Zyklen von Null an hochzählen oder von dem entsprechenden Wert bis auf 0 runterzählen. Für das Zählen bietet sich im µC der "Clear on compare match" (CTC) Modus eines Timers an.

Die obigen Zahlen lassen sich jedoch nicht in einer 16 Bit Variablen ausdrücken da sie zu gross sind und somit ist ein direktes Setzen des Compare-Wertes des Timers T1 im AVR nicht möglich. Deshalb unterteilt man die Quarzfrequenz in zwei Faktoren; der erste (Softwareteiler) bestimmt die Anzahl der Interrupts pro Sekunde von Timers T1 und der zweite den Reloadwert von Timer 1, welcher die Anzahl von Timertakten pro Interrupt definiert.

Beispiel

Im Beispiel AVR wird ein 11,0592-MHz-Quarz verwendet, was einem üblichen Baudratenquarz entspricht, d.h. damit können die UART-Standardbaudraten erzeugt werden.

Der Softwareteiler wird mit 256 gewählt, d.h. 256 Timerinterrupts pro Sekunde, und kann somit mit nur einem einzigen Byte realisiert werden. Für größere Werte muß der Softwareteiler als int (2 Byte) deklariert werden. Bei kleinen Werten für den Softwareteiler muss man beachten, dass der zweite Faktor immer noch in 2 Byte passt, um als Comparewert für den 16-Bit-Timer 1 verwendet werden zu können. Das wird im unten gezeigten Quelltext automatisch geprüft und ggf. eine Fehlermeldung erzeugt.

Die Timer-ISR wird in einer Sekunde F_TIMER mal aufgerufen. Mit diesen 256 Hz ergibt sich eine Periodendauer von ca. 4ms, die auch sehr gut zum Entprellen von Tasten benutzt werden kann. Eine Entprellroutine kann also bequem in den Timerinterrupt mit eingefügt werden. Beachten muss man nur noch, dass das Nullsetzen des Timers erst einen Zyklus nach dem Comparematch erfolgt, d.h. eine Periode der ISR ist ORC1A + 1 Timertakte lang.

Anmerkung: In diesem Beispiel wird die Zahl 256 in einer 8-Bit Variablen gespeichert. Eigentlich geht das nicht, denn bei der Initialisierung wird die Variable aif 0 gesetzt. Praktisch geht es hier doch, weil eben durch das Dekrementieren die Variable beim nächsten Durchlauf der ISR einen normalen Wert bekommt (arithmetischer Überlauf). Allerdings ist das nicht der beste Programmierstil.

Berechnung

Die Berechnung des Comparewertes ist sehr einfach:

Periodendauer     = 11059200 / 256    = 43200, Rest 0. 
Comparewert OCR1A = Periodendauer - 1 = 43199 (siehe Datenblatt, Timer 1 CTC mode)

Da haben wir ja noch mal Glück gehabt, es gibt keinen Rest bei der Division und die Sekunde ist exakt 256 * 43200 = 11059200 Zyklen lang. Theoretisch. Denn selbst wenn auf dem Quarz eine Frequenz von 11,0592 Mhz aufgedruckt ist, so schwingt er doch auf einer etwas anderen Frequenz. Der Grund dafür sind Fertigungstoleranzen und natürlich die Tatsache, dass die Frequenz eines Quarzes auch von der Temperatur abhängig ist. Es gilt also zunächst einmal herauszufinden, auf welcher Frequenz der Quarz wirklich schwingt.

Dazu wird eine Uhr programmiert und mit dem nominalen Wert laufen gelassen. Nun habe ich die Uhr exakt einen Tag laufen lassen und festgestellt, dass sie 1,5 s nach geht. Dazu kann man beispielsweise die Tagesschau nutzen, welche jeden Tag um 20:00 (+einer konstanten Zeitverschiebung durch das Übertragungsmedium SAT,DVB-T, ...) beginnt und sogar eine Uhr dabei einblendet. D.h. die Quarzfrequenz beträgt in Wirklichkeit:

11059200 * (1 - 1,5 / 24 / 60 / 60) = 11059008 Hz.

Ausführliche Rechnung:

(Zyklen in 24h - Zyklen Verspätung) / Sekunden pro 24h = korrekte Frequenz
((24 * 60 * 60 * 256 * 43200) - (1,5 * 256 * 43200)) / (24 * 60 * 60) = 11059008 Hz.

Also die ganze Rechnung nochmal:

Periodendauer     = 11059008 / 256    = 43199, Rest 64.
Comparewert OCR1A = Periodendauer - 1 = 43198

Nun haben wir einen Rest und es würden uns jede Sekunde 64 Timer-Zyklen fehlen. Das geht natürlich nicht.

Deshalb wird jedesmal, wenn der Softwareteiler Null ist und die Sekunde weitergezählt wird, ein anderer Comparewert geladen. Dieser ist dann um den Rest größer. Und beim nächsten Timerinterrupt wird dann wieder der Comparewert geladen, der das Ergebnis der Division war.

Es ergeben sich somit:

255 * (43198 + 1) + 1 * (43198 + 64  + 1) = 11059008 Zyklen

Exakt so, wie wir es wollten.

Das Programm

Nachfolgend nun das C-Programm. Da wir ja alle nicht gerne rechnen, lassen wir das einfach den C-Compiler erledigen. D.h. wir brauchen nur noch per Definition für F_CPU den entsprechenden Wert eintragen und der Compiler rechnet alle nötigen Konstanten ganz alleine aus. So ein Compiler ist auch ziemlich faul, der merkt sofort, wenn die Operanden für eine Berechnung alles Konstanten sind. Und ehe er sich damit abquält, extra Code für diese Berechnungen zu erzeugen, rechnet er es lieber selber aus und fügt das Ergebnis direkt in den Code ein. Für den eher seltenen Sonderfall, daß Rest == 0 ist, wird ein Teil des Codes mit #if Rest==0 . . . #endif automatisch entfernt (bedingte Kompilierung mittels Präprozessordirektiven), damit spart man dort minimal CPU-Zeit und Programmspeicher.

Der Assembler kann auch 32-Bit Konstanten-Berechnungen ausführen. Allerdings muß man dann die entsprechenden Präprozessoroperationen benutzen. Man könnte auch eine Divisionsroutine aufrufen, aber dann würde ja echter Code erzeugt.

Beispiel in C

/************************************************************************/
/*                                                                      */
/*  Die genaue Sekunde / RTC                                            */
/*                                                                      */
/*  Erzeugt einen Timerinterrupt mit beliebiger Frequenz                */
/*                                                                      */
/*              Author: Peter Dannegger                                 */
/*                      danni@specs.de                                  */
/*                                                                      */
/************************************************************************/

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

// hier alles eintragen
//#define F_CPU      11059201L   // Nominalwert des CPU-Taktes
#define F_CPU        11059008L   // gemessener Wert, Abweichung -1.5s/d
#define F_TIMER      256L        // Timer1 ISR-Frequenz
#define T1_PRESCALER 1           // Hardware-Vorteiler für Timer 1, 
                                 // muss mit der Konfiguratiom übereinstimmen!  

// hier wird alles berechnet
#define OCRx_RELOAD ((F_CPU / (T1_PRESCALER * F_TIMER)) -1) // Periodendauer pro Interrupt /Timertakte
                                                            // -1, weil die Timerperiode OCR1x +1 ist
#define REST        ((F_CPU % (T1_PRESCALER * F_TIMER)) / T1_PRESCALER)     // Divisionsrest 

#if (OCRx_RELOAD+REST) > 65535
  #error Überlauf in Timer1! F_TIMER oder T1_PRESCALER erhöhen.
#endif

uint8_t volatile second;         // Sekundenzaehler

ISR(TIMER1_COMPA_vect) {
    static uint8_t prescaler=(uint8_t)F_TIMER;

    if( --prescaler == 0 ) { 
        prescaler = (uint8_t)F_TIMER;
        second++;                               // eine Sekunde vorueber
        if( second == 60 ) second = 0;          // eine Minute vorbei    
#if REST==0
    }                                           // Sonderfall spart ein wenig Code
#else                            
        OCR1A = OCRx_RELOAD + REST;             // Rest behandeln
    } else {
        OCR1A = OCRx_RELOAD;                    // OCRx jeweils neu laden
    }
#endif

/************************************************************************/
/*          Hier Entprellung für Tasten etc. einfügen                   */
/************************************************************************/

}

int main( void ) {

    DDRB = 0xFF;      // alles Ausgänge mit LEDs zur Anzeige

    // Timer 1 initialisieren, mode 4, CTC, Vorteiler 1
    // Periode durch OCR1A definiert

    TCCR1B = (1<<WGM12) | (1<<CS10);
    OCR1A = OCRx_RELOAD;
    // OCR1A Interrupt freigeben, wird für Timer CTC mode benutzt
    TIMSK = 1<<OCIE1A;                  

    sei();

    while(1) {
        PORTB = second;  // einfache Anzeige der Sekunden in Binärform
    }
}

Beispiel in Assembler

 
;************************************************************************/
;*                                                                      */
;*          Precise one second timebase                                 */
;*                                                                      */
;*              Author: Peter Dannegger                                 */
;*                      danni@specs.de                                  */
;*                                                                      */
;************************************************************************/
.nolist
.include <m8def.inc>

.equ    F_CPU        = 11059008
.equ    F_TIMER      = 256
.equ    T1_PRESCALER = 1  ; must be identical to initialisation!
.equ    ISR_PERIOD   = (F_CPU / (F_TIMER * T1_PRESCALER))
.equ    REMAINDER    = ( (F_CPU - (ISR_PERIOD * F_TIMER * T1_PRESCALER)) / T1_PRESCALER)

.if (ISR_PERIOD + REMAINDER -1) > 65535
.error "Timer 1 overflow! Increase F_TIMER or T1_PRESCALER."
.endif

.def    isr_sreg   = r15
.def    isr_tmp    = r17
.def    tmp        = r16
.def    prescaler  = r18
.def    second     = r19

.list
    rjmp    Reset
.org    OC1Aaddr
    rjmp    OC1Aint
;-------------------------------------------------------------------------

OC1Aint:
    in  isr_sreg, sreg

;************************************************************************/
;*          Insert Key Debouncing Here                                  */
;************************************************************************/

    dec prescaler
    brne _oci1

    ldi prescaler, (F_TIMER & 0xFF)
    inc second
    cpi second, 60
    brne _oci3

    ldi second, 0
_oci3:
    ldi isr_tmp, high( ISR_PERIOD + REMAINDER - 1 )
    out ocr1ah, isr_tmp
    ldi isr_tmp, low( ISR_PERIOD + REMAINDER - 1 )
    out ocr1al, isr_tmp
    rjmp _oci2

_oci1:
    ldi isr_tmp, high( ISR_PERIOD - 1 )
    out ocr1ah, isr_tmp
    ldi isr_tmp, low( ISR_PERIOD - 1 )
    out ocr1al, isr_tmp

_oci2:
    out sreg, isr_sreg
    reti

;-------------------------------------------------------------------------


Reset:
    ; Stack initialaisieren
    ldi tmp, high( ramend )
    out sph, tmp
    ldi tmp, low( ramend )
    out spl, tmp

    ; PORTB auf Ausgang
    ldi tmp, 0xFF
    out ddrb, tmp

    ; Timer 1 initialisieren, mode 4, CTC, prescaler 1
    ldi tmp, 1<<WGM12 | 1<<CS10
    out TCCR1B, tmp
    
    ; Timer 1 Periode = OCR1A +1
    ldi tmp, high( ISR_PERIOD - 1 )
    out ocr1ah, tmp
    ldi tmp, low( ISR_PERIOD - 1 )
    out ocr1al, tmp
    out tcnt1l, tmp

    ldi prescaler, (F_TIMER & 0xFF)
    ldi second, 0

    ; OCR1A Interrupt freigeben
    ldi tmp, 1<<OCIE1A
    out TIMSK, tmp
    sei

main_loop:
    out PORTB, second
    rjmp    main_loop

;-------------------------------------------------------------------------

Verbesserte Version mit durchlaufendem Hardwarezähler

Wird im Timer die Option "Clear On Compare Match" verwendet, so verliert man den Overflow Interrupt. Oder möchte man nebenher noch eine Zeit mit der Input Capture Funktion messen, so benötigt man einen durchlaufenden Timer. Um dies zu erreichen wird OCR1A nicht fest eingestellt sondern bei jedem Aufruf um den gleichen Wert erhöht.

In vorherigen Code wird der Rest auf einmal abgearbeitet. Damit erspart man sich bei jedem Interrupt einen Vergleich und die Verarbeitungszeit verkürzt sich geringfügig. Der Unterschied zwischen kurzem und langem Interrupt ist hier die Anzahl REST Timertakte, welche je nach mathematischer Konstellation zwischen 0 und F_TIMER-1 liegen kann (Divisionsrest, Modulo-Operation). Bei dem nachfolgenden Beispiel wird der Rest gleichmäßig abgearbeitet, d.h. die Resttakte werden gleichmäßig über eine Sekunde verteilt. Der Unterschied zwischen kurzem und langem Interrupt beträgt nur 1 Timertakt. Der Preis dafür ist ein klein wenig mehr CPU-Last im Interrupt und eine zusätzliche 16 Bit Variable.

/************************************************************************/
/*                                                                      */
/*  Die genaue Sekunde / RTC                                            */
/*                                                                      */
/*  Erzeugt einen Timerinterrupt mit beliebiger Frequenz                */
/*                                                                      */
/*  verbesserte Version mit durchlaufendem Timer 1                      */
/*  und gleichmäßiger Verteilung der restlichen Timertakte              */
/*                                                                      */
/************************************************************************/

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

// hier alles einstellen
//#define F_CPU      11059200L   // Nominalwert des CPU-Taktes
#define F_CPU        11059008L   // gemessener Wert, Abweichung 1.5s/d
#define F_TIMER      256L        // Timer 1 ISR-Frequenz
#define T1_PRESCALER 1           // Hardware-Vorteiler für Timer 1
                                 // muss mit der Konfiguration übereinstimmen

// hier wird alles berechnet
#define OCRx_INC     (F_CPU / (T1_PRESCALER * F_TIMER))     // Timertakte pro Interrupt 
#define REST         ((F_CPU % (T1_PRESCALER * F_TIMER)) / T1_PRESCALER)     // Divisionsrest 

#if (OCRx_INC+1) > 65536
  #error Überlauf in Timer1! F_TIMER oder T1_PRESCALER erhöhen.
#endif

uint8_t volatile second;         // Sekundenzaehler

ISR(TIMER1_COMPA_vect) {
    static uint8_t prescaler=(uint8_t)F_TIMER;

    if( --prescaler== 0 ) { 
        prescaler= (uint8_t)F_TIMER;
        second++;                               // eine Sekunde vorueber
        if( second == 60 ) second = 0;          // eine Minute vorbei                    
    }

#if REST !=0    
    static uint16_t restakku;

    restakku += REST;
    if (restakku >= F_TIMER) {
        restakku -= F_TIMER;
        OCR1A += OCRx_INC + 1;      // lange Periode
    } else {   
        OCR1A += OCRx_INC;          // kurze Periode
    }
#else                               // kein REST
    OCR1A += OCRx_INC;              // immer kurze Periode
#endif

/************************************************************************/
/*          Hier Entprellung für Tasten etc. einfügen                   */
/************************************************************************/

}

int main( void ) {

    DDRB = 0xFF;      // alles Ausgänge mit LEDs zur Anzeige

    // Timer 1 initialisieren, mode 0, normal, Vorteiler 1
    TCCR1B = (1<<CS10);
    OCR1A  = OCRx_INC;
    // OCR1A Interrupt freigeben
    TIMSK = 1<<OCIE1A;                  

    sei();

    while(1) {
        PORTB = second;  // einfache Anzeige der Sekunden in Binärform
    }
}

Durchlaufender Hardwarezähler und fortlaufende Addition

Gänzlich ohne Hin- und Hertogglen eines Bits kommt man aus, wenn irgendein Timer mit einer Überlauf-Interruptfrequenz > 1Hz arbeitet. Dann kann die ISR eine 32-bit-Konstante auf einen 32-bit-Akkumulator addieren; bei Überlauf ist eine Sekunde vergangen. Zugegeben, es gibt "lange" und "kurze" Sekunden, aber der Fehler summiert sich nicht, und der Code ist sehr einfach.


Hier ein Beispiel:

Es gibt ein Programm gettick(), das einen Wert holt. Dieser Wert wird von Zeit zu Zeit von der Hardware (Beispiel AVR: der Timer) oder dem Betriebssystem ermittelt. (Beispiel Linux: gettick ruft getmsec, getmsec ruft gettimeofday)

Dieser aufgerufene Wert sollte pro Sekunde um TICKPERSEC erhöht werden, was aber mit einer gewissen Ungenauigkeit geschieht.

Die Laufzeitkorrektur wird dadurch ausgeführt, dass der von gettick() zurückgegebene Wert mit einem Faktor TFAK multipliziert wird. Der Faktor sollte so gewählt werden, dass:

  • Das Ergebnis actual_value immer in 32 Bit hineinpasst.
  • Die Multiplikation durch Schiebeopertionen ersetzt werden kann, weil der Faktor eine 2-er-Potenz ist. (Das erledigt avr-gcc)

Immer wenn tsdaytim_calibrated() erkennt, dass eine Sekunde vergangen, wird last_value um die Zahl onesecond erhöht.

Der Zahlenwert onsecond wird mit TICKPERSEC*TFAK initalisiert und kann angezeigt oder neu eingegeben werden (Die Bedienung der seriellen Schnittstelle hierfür ist in diesem Beispiel nicht enthalten).

Wenn festgestellt wird, dass die Uhr pro Zeit (P) um (E) zu schnell geht, dann kann der Wert onsecond korrigiert werden.

c = d * (P+E)/P Beispiel Pro 7 Tage 120 Sekunden zu viel:

7 Tage = 7*86400= 604800 sekunden c = 992000000 * (604800+120)/604800=991803175 Dann wird die Uhr genauer gehen.

/****************************************************************/
/* File home/cc/qq/danclock.cpp                                 */
/*          Precise 1 Second Timebase                           */
/*                                                              */
/*      Author: Peter Dannegger / Hjherbert                     */
/*          danni@specs.de                                      */
/*                                                              */
/****************************************************************/
// Target: atmega8, (2313?)

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


#ifndef OCR1A
#define OCR1A OCR1  // 2313 support
#endif

#ifndef WGM12
#define WGM12 CTC1  // 2313 support
#endif

#ifndef PINC
#define KEY_INPUT   PIND    // 2313
#else
#define KEY_INPUT   PINC    // Mega8
#endif

//#define F_CPU     11059201L   // nominal value


#define DIVISOR 256

#define TICKPERSEC  (F_CPU/DIVISOR)

uint8_t volatile second;            // count seconds



// TFAK ? TICKPERSEC must not exceed 2^32
#define TM (0x7FFFFFFFUL/2/TICKPERSEC)  // max of TFAK


#define TFAK ( (  (TM>>1) | (TM>>2) | (TM>>3)  | (TM>>4)  | (TM>>5)  | (TM>>6)  | (TM>>7) \
                | (TM>>8) | (TM>>9) | (TM>>10) | (TM>>11)  | (TM>>12)  | (TM>>13)  | (TM>>14)  | (TM>>15) \
                | (TM>>16) | (TM>>17) | (TM>>18) | (TM>>19)  | (TM>>20)  | (TM>>21)  | (TM>>22)  | (TM>>23) \
                | (TM>>24) | (TM>>25) | (TM>>26) | (TM>>27)  | (TM>>28)  | (TM>>29)  | (TM>>30)  | (TM>>31) \
               ) + 1 )
// TFAK is a power the biggest power of two which is less than TM


uint32_t onesecond = 1*TICKPERSEC*TFAK ;    // correct this value if see the clock is late / too fast
uint32_t last_value ;
uint32_t ticks ;


SIGNAL (SIG_OUTPUT_COMPARE1A)
{
    ++ticks ;
}


uint32_t gettick( void )
{
    uint32_t l ;
    cli();
    l = ticks ;
    sei();
    return l ;
}


void tsdaytim_calibrated(void)
// Keep the time-of-the-day actual
// Add a second, if a second if gone
{
    uint32_t actual_value ;         // gettick() multiplied by factor 2^n


    actual_value = gettick() * TFAK ;   // because TFAK is a power of two
                                        // The compiler will create some shift commands

    if ( actual_value - last_value > onesecond )
    {                           // once per second
        if ( ++second >= 60 )
        {
            second = 0 ;
        }
        last_value += onesecond ;   // One second more
    }
}


int main( void )
{
    DDRB = 0xFF;
    while( KEY_INPUT & 1 );         // start with key 0 pressed

    TCCR1B = 1<<WGM12^1<<CS10;      // divide by 1
                                    // clear on compare
    OCR1A = DIVISOR ;               // Output Compare Register
    TCNT1 = 0;                      // Timer start value
    second = 0;

    TIMSK = 1<<OCIE1A;              // beim Vergleichswertes Compare Match
                                    // Interrupt (SIG_OUTPUT_COMPARE1A)
    sei();
    last_value = gettick() * TFAK ;
    for(;;)
    {
        tsdaytim_calibrated();
        PORTB = second;             // display second (binary)
    }
}

Echtzeituhr mit Uhrenquarz

RTC mit wenig Stromverbrauch

Das vorgestellte Verfahren ist leider nicht direkt anwendbar, wenn man eine RTC mit einem 32,768 KHz Uhrenquarz realisieren möchte. Dieser Weg ist dann notwendig, wenn der Controller batteriebetrieben sehr lange laufen soll und durch Verwendung des Sleep Mode Strom gespart wird.

Begrenzung der Auflösung

Wo liegt das Problem? Das obige Verfahren nutzt recht hochfrequente Quarze mit 1 MHz und mehr. Wenn man die Frequenz eines Quarzes von 1 MHz = 1.000.000 Hz mit einer Auflösung von 1 Hz angibt, entspricht das einem maximalen Fehler von 1/1.000.000 oder 1ppm (engl. parts per million, millionstel Teil). Das ist sehr wenig. Eine RTC mit einem Fehler von 1ppm hat eine Gangabweichung von 86,4ms pro Tag, oder 2,5s pro Monat. Das ist ein sehr guter Wert. Wenn ich nun aber die Frequenz 32768 Hz mit 1 Hz Auflösung angebe, ist das schlimmstenfalls ein Fehler von 1/32768 = 30,5ppm, was einer Abweichung von 2,5s pro Tag und 75s pro Monat entspricht!

Uhrentakt genau messen und digital korrigieren

Wie kann man das Problem lösen? Die AVRs können im Sleep Mode den Uhrenquarz nur am Timer 2 betreiben, welcher ein 8 Bit Timer ist. D.H. ein Überlauf passiert alle 256 Takte, sprich 7,8125ms (=128 Hz). Man kann auch einen Prescaler verwenden, welcher das Problem aber nicht löst.

Wie bereits festgestellt, müssen wir die Frequenz des 32768Hz Uhrenquarzes genauer messen und darstellen. Wir wollen hier annehmen, dass wir die Frequenz auf 0,001Hz = 1mHz (Millihertz) auflösen, ein guter Frequenzzähler kann das problemlos und ist auch so genau. Wir können beispielsweise feststellen, dass ein Quarz mit 32768,423Hz schwingt, das entspricht einem Fehler von

[math]\displaystyle{ F_r=(\frac{f_{ist}}{f_{soll}} -1 )\cdot 10^6=(\frac{32768,423}{32768} -1 )\cdot 10^6=12,9ppm }[/math]

Dabei ist zu beachten, daß diese Messung nicht direkt am Quarz erfolgen darf, auch nicht mit kapazitätsarmen 10:1 Tastköpfen! Denn so ein Quarz wird von kleinsten Kapazitäten im Bereich von weniger als ein pF messbar verstimmt. Darum muss man in so einem Fall den Uhrentakt auf ein anderes IO-Pin ausgeben und dort messen. Beim MSP430 ist das einfach, man kann ACLK auf ein Pin direkt ausgeben. Der AVR kann das leider nicht. Hier muss man zu einem Trick greifen. Man benutzt die Output Compare Funktion, um mit Timer 2 einen 128 Hz Takt zu erzeugen. Dieser ist fest mit dem Uhrentakt verbunden und kann anstellte dessen gemessen werden, ohne den Quarz zu verstimmen. Die Messung ergibt in diesem Fall eine Frequenz von 128,00165 Hz.

OK, jetzt haben wir den Takt gemessen, wir wissen, daß pro Sekunde 128 Timerüberläufe passieren (Prescaler =1). Wir wissen, dass unser Quarz pro Sekunde um 0,423 Takte zu schnell ist. Wir können aber keine halben Takte mehr oder weniger zählen? Doch! Mit Festkommaarithmetik! Denn nach 1000s ist unser Quarz um 423 Takte zu weit gelaufen und muss um diese Anzahl zurückgestellt werden. Also könnte man im ersten Ansatz nach 1000 Sekunden den Timer 2 um diesen Betrag korrigieren. Das Problem dabei ist nur, dass der Zähler nur 8 Bit breit ist. Ein Addieren oder Subtrahieren von 423 würde mehrfache Überläufe verursachen, welche programmtechnisch nur schwer zu handhaben wären. Also muss eine etwas bessere Methode her.

Bresenham für RTCs

Wenn man sich die Idee der Festkommaarithmetik mal eine Weile durch den Kopf gehen lässt, kommt man vielleicht auf folgende Idee. Der Frequenzfehler beträgt in unserem Beispiel 0,423 Hz, also Takte pro Sekunde. Oder aber 423 Tausendstel Takte pro Sekunde. Nach drei Sekunden sind es schon 1,269 Takte oder 1269 Tausendstel Takte. Moment! Einen Takt können wir korrigieren, indem wir den Zähler um 1 Takt zurück setzen. Den Restfehler von 269 Tausendstel merken wir uns und akkumulieren weiter. Wenn dann wieder die 1000er Marke überschritten wird machen wir das Gleiche. Analog dazu natürlich auch bei negativem Vorzeichen, sprich wenn der Quarz zu langsam schwingt wird er um einen Takt vorgestellt. Et voilà! Dieses Verfahren ist sehr ähnlich zum Bresenham-Algorithmus zum Zeichnen von Linien auf Computermonitoren.

Beispielprogramm

Das folgende Beispiel stellt eine RTC mit sehr niedrigem Stromverbrauch zur Verfügung. Laut Datenblatt beträgt die Stromaufnahme nur ca. 6µA bei 3V Versorgungsspannung, besser als einige kommerzielle RTCs! Möglich macht das die Nutzung des Power Save Sleep Modes. Die Frequenz kann auf [math]\displaystyle{ 10^{-5} }[/math] Hz genau abgeglichen werden, was in diesem Fall bei einer Messfrequenz von 128Hz einer Auflösung von 0,078ppm entspricht. Das bedeutet eine Abweichung von 6,7ms pro Tag, 0,2s pro Monat oder 2,46s pro Jahr!

Die Abweichung des Quarzes wird in einer vorzeichenbehafteten 16 Bit Variable in der Einheit [math]\displaystyle{ 10^{-3} }[/math] Hz gespeichert. D.h. es kann ein Frequenzfehler von +/- 32,768 Hz korrigiert werden, das sind 1000ppm! Normale Quarze haben Abweichungen von max. 100ppm, meist viel weniger. Jede Sekunde wird der Frequenzfehler neu berechnet und in den darauffolgenden 128 Interrupts ggf. mehrfach korrigiert. Wichtig ist dabei, dass die Korrektur wirklich ganz am Anfang der ISR steht, weil hier das Timing wirklich kritisch ist. Denn innerhalb eines Uhrenquarztaktes von ~30µs muss der Timer 2 neu beschrieben werden. Bei 1 MHz RC-Oszillatortakt sind das nur 30 Takte, wovon 6 zum Aufwachen und 4 zum Anspringen der ISR benötigt werden. Dazu kommen noch das Sichern einiger Register am Anfang der ISR, was man im C nicht direkt sieht. Sicherheitshalber sollte man auch einen höheren Takt einstellen, z. B. 2 MHz, dann ist das Timing wesentlich entspannter.

/*
************************************************************************
*
* Stromsparende Echtzeituhr mit 32768Hz Uhrenquarz
*
* ATmega88 mit internem 2 MHz Oszillator + 32,768 kHz Quarz
*
* Fuses bleiben auf Standardeistellungen
* LOW Fuse Byte = 0x62
* HIGH Fuse Byte = 0xDF
* EXT Fuse Byte = 0xF9
*
* LED mit 1K Vorwiderstand an PB5
* Kalibriertaktausgang an PB3
************************************************************************
*/

#define F_CPU 2000000L          // Systemtakt in Hz
#define F_CAL 12800000L         // gemessener Kalibriertakt in 10^-5 Hz
                                // Endung L ist wichtig!

#define LED_TOGGLE PORTB ^= (1<<PB5);

#include <avr/io.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <avr/eeprom.h>
#include <util/delay.h>

// EEPROM Daten

int16_t ee_rtc_cal EEMEM = (F_CAL-12800000)*256/100;        // Kalibrierung für RTC, Frequenzfehler in 1/1000 Hz

// globale Variablen

// beidseitiger Zugriff durch ISR und Hauptprogramm

volatile uint32_t time;             // 24h Zeitstempel, 1s Auflösung
volatile int16_t rtc_cal;           // Kalibrierung des 32K Quarzes
volatile uint8_t flag_1s;           // Flag für 1s Intervall

void long_delay(uint16_t ms) {
    for (; ms>0; ms--) _delay_ms(1);
}


int main (void) {

// Clock divider auf /4

    CLKPR = 0x80;       // update  enable
    CLKPR = 2;          // Prescaler /4

// IO konfigurieren

    DDRB  = (1<<PB3) | (1<<PB5);
    PORTB = ~((1<<PB3) | (1<<PB5)); // Pull ups 
    PORTC = 0xFF;       
    PORTD = 0xFF;

// Analogcomparator ausschalten

    ACSR = 0x80;

// Timer2 konfigurieren

    ASSR   = (1<< AS2);             // Timer2 asynchron takten
    long_delay(1000);               // Einschwingzeit des 32kHz Quarzes
    TCCR2A = (1<<COM2A1) | (1<<WGM21) | (1<<WGM20); // Fast PWM, non inverted
    TCCR2B = 1;                     // Vorteiler 1 -> 7,8ms Überlaufperiode
    OCR2A  = 128;                   // PWM, Tastverhältnis 50%
    while((ASSR & (1<< TCR2BUB)));  // Warte auf das Ende des Zugriffs
    TIFR2  &= ~(1<<TOV2);           // Interrupts löschen
    TIMSK2 |= (1<<TOIE2);           // Timer overflow Interrupt freischalten

// EEPROM Werte auslesen

    rtc_cal= eeprom_read_word(&ee_rtc_cal);

// Interrupts freigeben

    sei();

// Endlose Hauptschleife

    while(1) {

        while((ASSR & (1<< OCR2AUB)));  // Warte auf das Ende des Zugriffs
        set_sleep_mode(SLEEP_MODE_PWR_SAVE);
        sleep_mode();                   // in den Schlafmodus wechseln

        // hier wachen wir wieder auf, nach Ausführung des 7,8ms Timerinterupt

        if (flag_1s) {  
            flag_1s =0;
            LED_TOGGLE                      // Test
        }

    }
}

// Timer2 overflow Interrupt

ISR(TIMER2_OVF_vect) {
    static uint8_t ticks;               // Hilfsvariable für Messintervall
    static int16_t time_error;          // RTC Fehlerkompensation

    // Zeitkritische Dinge, welche am Anfang der ISR stehen müssen!

    OCR2A=128;                          // Dummy-Write zur Sicherung des Timings
                                        // von Timer 2 im asynchronen Modus 

    // RTC Fehler korrigieren

    if (time_error>999) {               // RTC zu schnell
        TCNT2 = 2;                      // Zähler einen Schritt zurück setzen (2 Takte Verzögerung!)
        time_error -= 1000;
    } else if (time_error<-999) {       // RTC zu langsam
        TCNT2 = 4;                      // Zähler einen Schritt vor setzen (2 Takte Verzögerung!)
        time_error += 1000;     
    }

    // ab hier ist es nicht mehr zeitkritisch

    // Echtzeituhr
    ticks++;                            // 1/128tel Sekunde
    if (ticks==128) {                   // Sekundenintervall
        time_error += rtc_cal;          // RTC Fehler akkumulieren

        // 24h Timer
        time++;
        if (time==86400) time=0;        // 24h Überlauf

        ticks=0;
        flag_1s =1;                     // setzte Flag für 1s Verarbeitung in main Endlosschleife
    }
}

Weitere Verbesserungen

Der Algorithmus ermöglicht eine sehr hochauflösende Kalibrierung der RTC. Allerdings heisst das leider noch lange nicht, daß die RTC dann auch wirklich so genau ist. Denn diese Kalibrierung gilt nur für eine exakte Temperatur! Auch ein Quarz hat eine Temperaturabhängigkeit der Schwingfrequenz. Wenn man nun eine sehr genaue RTC bauen möchte, welche über einen grossen Temperaturbereich genau läuft, muss man periodisch die Temperatur messen und in die Kalibrierung einbeziehen. Einen typischen Temperaturverlauf des Frequenzfehlers zeigt das folgende Bild.

Temperaturgang eines Uhrenquarzes


Weiterhin kann man natürlich die Kalibrierung ohne erneutes Kompilieren des Quelltextets vornehmen, indem man den Kalibrierwert per UART oder I2C an den AVR sendet und im EEPROM speichert.

Kernpunkt der Kalibrierung ist natürlich die Messung der realen Quarzfrequenz. Die oben beschriebene Methode per Tagesschau und 1 Tag warten ist sehr zeitaufwändig und hat eine begrenzte Genauigkeit von ca. 1/2 Sekunde, das entspricht 5,8ppm. Hier muss zwangsläufig ein guter Frequenzzähler her. Da dieser nicht jedem Hobbybastler zur Verfügung steht, muss man sich in Lehrwerkstätten, Universitäten oder Firmen im Elektronikbereich umsehen und nachfragen, ob man einen Frequenzzähler vor Ort kurze Zeit nutzen kann.

Einige GPS-Module bieten auch einen hochgenauen 1PPS (Pulse Per Second) Ausgang an, der sich für den Abgleich gut heranziehen lässt. Auch das Sekunden-Zeichen eines DCF77-Funkuhr-Moduls ist sehr genau und kann als Referenz dienen.

Siehe auch

Links