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.
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.
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.
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.
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.
/************************************************************************/
/* */
/* 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)
}
} |