www.mikrocontroller.net

AVR - Die genaue Sekunde / RTC

Inhaltsverzeichnis

[bearbeiten] Einleitung

Oftmals sieht man Projekte, bei denen ein externer RTC-Baustein (Real Time Clock z.B. PCF8583 mit I2C-Anschluss Datenblatt (PDF)) angeschlossen ist, ohne dessen Vorteil wirklich zu nutzen. Der einzige Vorteil eines externen RTC ist nämlich nur, dass man die Zeitzählung während eines Ausfalls der Hauptstromversorgung über eine kleine Stützbatterie bei geringem Strombedarf fortsetzen kann.

In allen anderen Fällen ist die zusätzliche Hardware völlig unnö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.

[bearbeiten] Lösung

Speziell für den AVR kommen Quarze im Bereich 1MHz bis 16MHz zum Einsatz, d.h. in einer Sekunde werden 1.000.000 bis 16.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 Compare-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 Periode des Timers T1 und der zweite den Reloadwert eines Registers, welches im Timerinterrupt runtergezählt wird.

[bearbeiten] Beispiel

Im Beispiel AVR wird ein 11,0592MHz 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 nur mit einem einzigen Byte realisiert werden. Für größere Werte muß der Softwareteiler als int (2 Byte) deklariert werden. Bei kleineren Werten als 256 muß man beachten, daß der 2.Faktor immer noch in 2 Byte paßt, um als Comparewert für T1 verwendet zu werden.

Die Timerroutine wird in einer Sekunde Softwareteiler-Mal aufgerufen. Mit diesen 256Hz ergibt sich eine Timerinterruptzeit 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 muß man nur noch, dass das Nullsetzen des Timers erst einen Zyklus nach dem Comparematch erfolgt. D.h. der Comparewert ist der gewünschte Teilerfaktor - 1.

[bearbeiten] Berechnung

Die Berechnung des Comparewertes ist also sehr einfach:

Comparewert OCR1A = 11059200 / 256 - 1 = 43199, Rest 0.

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, daß die Frequenz eines Quarzes auch von der Temperatur abhängig ist. Es gilt also zunächst mal herauszufinden auf welcher Frequenz der Quarz wirklich schwingt.

Dazu wird eine Uhr programmiert und mit dem theoretischen Wert laufen gelassen. Nun habe ich die Uhr einen Tag laufen lassen und festgestellt, daß sie 1,5s nach geht. 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:

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

Nun habe wir einen Rest und es würden uns jede Sekunde 64 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.

[bearbeiten] 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 XTAL 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.

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.

[bearbeiten] Beispiel in C

/************************************************************************/
/*                                                                      */
/*			Precise 1 Second Timebase			*/
/*                                                                      */
/*              Author: Peter Dannegger                                 */
/*                      danni@specs.de                                  */
/*                                                                      */
/************************************************************************/
// Target: Mega8, 2313
 
#include <io.h>
#include <interrupt.h>
#include <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	LED_DIR		DDRB
 
//#define XTAL		11059201L	// nominal value
#define XTAL		11059008L	// after measuring deviation: 1.5s/d
 
#define DEBOUNCE	256L		// debounce clock (256Hz = 4msec)
 
#define uchar unsigned char
#define uint unsigned int
 
uchar prescaler;
uchar volatile second;			// count seconds
 
 
SIGNAL (SIG_OUTPUT_COMPARE1A)
{
/************************************************************************/
/*			Insert Key Debouncing Here			*/
/************************************************************************/
 
#if XTAL % DEBOUNCE                     // bei rest
  OCR1A = XTAL / DEBOUNCE - 1;		// compare DEBOUNCE - 1 times
#endif
  if( --prescaler == 0 ){
    prescaler = (uchar)DEBOUNCE;
    second++;				// exact one second over
#if XTAL % DEBOUNCE			// handle remainder
    OCR1A = XTAL / DEBOUNCE + XTAL % DEBOUNCE - 1; // compare once per second
#endif
  }
}
 
 
int main( void )
{
  LED_DIR = 0xFF;
  while( KEY_INPUT & 1 );               // start with key 0 pressed
 
  TCCR1B = (1<<WGM12) | (1<<CS10);      // divide by 1
					// clear on compare
  OCR1A = XTAL / DEBOUNCE - 1;          // Output Compare Register
  TCNT1 = 0;                            // Timmer startet mit 0
  second = 0;
  prescaler = (uchar)DEBOUNCE;          //software teiler
 
  TIMSK = 1<<OCIE1A;                    // beim Vergleichswertes Compare Match                    
                                        // Interrupt (SIG_OUTPUT_COMPARE1A)
  sei();
 
  for(;;){
    if( second == 60 )
      second = 0;
    PORTB = second;			// display second (binary)
  }
}

[bearbeiten] Beispiel in Assembler

;************************************************************************/
;*                                                                      */
;*			Precise 1 Second Timebase			*/
;*                                                                      */
;*              Author: Peter Dannegger                                 */
;*                      danni@specs.de                                  */
;*                                                                      */
;************************************************************************/
.nolist
.include "m8def.inc"
 
.equ    xtal      = 11059008
.equ    debounce  = 256
.equ    remainder = xtal - xtal / debounce * debounce
 
.def    isreg     = r15
.def    wr0       = r16
.def    iwr0      = r17
.def    prescaler = r18
.def    second    = r19
 
.list
	rjmp	init
.org	OC1Aaddr
	rjmp	OC1Aint
;-------------------------------------------------------------------------
OC1Aint:
	in	isreg, sreg
;************************************************************************/
;*			Insert Key Debouncing Here			*/
;************************************************************************/
 
	ldi	iwr0, high( xtal / debounce - 1 )
	out	ocr1ah, iwr0
	ldi	iwr0, low( xtal / debounce - 1 )
	out	ocr1al, iwr0
 
	dec	prescaler
	brne	_oci1
 
	ldi	prescaler, debounce
	inc	second
 
	ldi	iwr0, high( xtal / debounce + remainder - 1 )
	out	ocr1ah, iwr0
	ldi	iwr0, low( xtal / debounce + remainder - 1 )
	out	ocr1al, iwr0
_oci1:
	out	sreg, isreg
	reti
;-------------------------------------------------------------------------
init:
	ldi	wr0, 0xFF
	out	ddrb, wr0
	sbic	pinc, 0
	rjmp	init
 
	ldi	wr0, high( ramend )
	out	sph, wr0
	ldi	wr0, low( ramend )
	out	spl, wr0
 
	ldi	wr0, 1<<WGM12^1<<CS10
	out	TCCR1B, wr0
 
	ldi	wr0, high( xtal / debounce - 1 )
	out	ocr1ah, wr0
	ldi	wr0, low( xtal / debounce - 1 )
	out	ocr1al, wr0
	out	tcnt1l, wr0
	ldi	prescaler, debounce
 
	ldi	wr0, 1<<OCIE1A
	out	TIMSK, wr0
	sei
main:
	ldi	second, 0
_mai1:
	cpi	second, 60
	breq	main
	out	ddrb, second
	rjmp	_mai1
;------------------------------------ 

[bearbeiten] Verbesserte Version mit durchlaufendem Hardwarezähler

Hier sind trotzdem kleine Verbesserungsvorschläge.

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 dem Capture Interrupt 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 Peters Code wurde der Rest auf einmal abgearbeitet. Damit erspart man sich bei jedem Interrupt einen Vergleich und die Verarbeitungszeit verkürzt sich in seinem Code. Der Unterschied zwischen kurzem und langem Interrupt ist hier der Rest. Bei den geänderten Codeschnipseln wird der Rest gleichmäßiger abgearbeitet. Der Unterschied zwischen kurzem und langem Interrupt beträgt 1 Takt.

ungetesteter Code für den durchlaufenden Timer:

SIGNAL (SIG_OUTPUT_COMPARE1A) {
/************************************************************************/
/*      Insert Key Debouncing Here      */
/************************************************************************/
 
  if( --prescaler == 0 ){ 
    prescaler = (uchar) DEBOUNCE;
    second++;      // exact one second over  
  }  
#if XTAL % DEBOUNCE
  if (prescaler <= XTAL % DEBOUNCE) {
    OCR1A += XTAL / DEBOUNCE +1;   /* um 1 Takt längere Periode um 
              den Rest abzutragen */
  } else {
#endif
    OCR1A += XTAL / DEBOUNCE;   /* kurze Periode */
#if XTAL % DEBOUNCE    
  }
#endif
}

[bearbeiten] 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 (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)
    }
}
webmaster@mikrocontroller.netImpressumWerbung auf Mikrocontroller.net