AVR – Warten ohne Delay
Als Anfänger benutzt man ja meist Warteschleifen, um eine gewisse Zeit abzuwarten. Oft mit der _delay_ms()-Funktion, die zu den Standardfunktionen gehört.
Beispielsweise:
while(1) { led_on(); _delay_ms(200); led_off(); _delay_ms(200); }
Dies ist natürlich nicht gut, da der Prozessor während den Delays blockiert ist und nichts anderes machen kann. Möchte man nun eine zweite LED mit anderer Frequenz blinken lassen, dann hat man ein Problem.
Eine gute Möglichkeit, dies zu umgehen ist eine globale Zeitbasis einzuführen. Im Arduino gibt es beispielsweise die millis()-Funktion, das ist nichts anderes.
Was man dazu benötigt ist eine passende Frequenz zur Zeitbasis. Man kann sich die Register von Hand ausrechnen. Oder aber man nimmt einen Online-Rechner, wie den von DieElektronikerSeite: *klick*
Wenn man nun also beispielsweise den internen RC-Oszillator, eingestellt auf 8Mhz (CLKDIV8-Fuse bei z.B. Atmega88 nicht vergessen rauszunehmen) nutzt und 1ms will, dann kommt man auf folgende Werte:
Prescale: 1 TCNTx (L): &HC0 TCNT1H: &HE0 Interrupt: 0.001 Sek. Fehlerquote: 0%
Ich habe den 16-Bit-Modus für den Timer1 gewählt. Mit 8bit würde man bei diesen Beispiel aber auch ans Ziel kommen. Sogar mit mehreren Prescaler-Varianten. Der Fehler ist hier 0,0% – perfekt!
Der Preload-Wert ist: HEX: E0C0 = DEC: 57536
Also nochmal kurz nachrechnen, ob das passt:
f = TAKTFREQUENZ/(PRESCALER/(MAX_UINT-PRELOAD+1)) f = 8000000/(1/(65535-57536+1)) f = 1000Passt!
Der Code
Das ganze mal in Code übertragen:
#include <avr/io.h> #include <avr/interrupt.h> void timer1_init(void) { // Timer 1 konfigurieren TCCR1B = (1<<CS10); // Prescaler 1 TCNT1 = 57536; // -> Preload // Overflow Interrupt erlauben TIMSK |= (1<<TOIE1); // Global Interrupts aktivieren sei(); } uint32_t volatile timer1_var; ISR(TIMER1_OVF_vect) { // Interrupt jede ms TCNT1 = 57536; //E0C0 -> Preload timer1_var++; }Wie man bestimmt gesehen hat läuft die Variable timer1_var irgendwann über. Nämlich nach 4.294.967.295ms ≈ 49,71d. Das ist aber halb so tragisch, da die Variable sich immer noch auswerten lässt!
uint32_t led1_timer_last = 0; uint32_t led2_timer_last = 0; void main() { if (timer1_var - led1_timer_last >= 200) { //200ms led1_timer_last = timer1_var; if (led1_is_on()) { led1_off(); } else { led1_on(); } } if (timer1_var - led2_timer_last >= 175) { //175ms led2_timer_last = timer1_var; if (led2_is_on()) { led2_off(); } else { led2_on(); } } //sonstiger Code }Nun haben wir zwei unabhängig voneinander blinkende LEDs und keine blockierenden Funktionen mehr!
Sollte nun timer1_var übergelaufen sein, so ist dies kein Problem, da man ja die letzte Zeit abzieht. Denn 0 – 1 =4294967295 oder praxisnaher 100 – 4294967195 = 200. Also funktioniert der Vergleich auch noch nach einem Überlauf…
Das ganze funktioniert auch mit einem Arduino, dort halt mit der millis()-Funktion, statt unserer Variable.
Genauigkeit
Keine Frage, ein _delay_ms() ist genauer. Warum?
Die Abfrage, der hier vorgestellten Variante findet in der Main statt. D.h. je häufiger die if-Bedingung abgefragt wird, desto genauer. Im Worst Case entsteht hier eine Abweichung von der Durchlaufzeit der Main-Haupt-Schleife. Dies ist aber eine relative Abweichung der Zeitauswertung, der Zeitgeber ist immer noch genau.
Preload braucht Zeit. Das ist mir bei dieser Variante bei meiner Binäruhr aufgefallen. Im Interrupt wird als erstes der Preload geladen. Aber genau dieses Laden benötigt auch ein paar Takte. Daraus folgt eine Absolute Abweichung, der Timer ist also minimal langsamer als 1ms. Bei einer Uhr fällt dies auf. Man könnte die genaue Abweichung durch Simulieren herausfinden, den Preload anpassen und ggf. noch ein paar Assembler-NOPs einfügen. Dies habe ich aber noch nicht ausprobiert.
Und zu guter Letzt kommt noch die Ungenauigkeit des Taktgebers dazu. Dies ist beim internen RC-Oszillator relativ viel. Das betrifft aber natürlich auch _delay_ms().
Andere Möglichkeiten
- Einen Scheduler nehmen (mit/ohne komplettes Betriebssystem), ist aber für viele Dinge schon overpowered
- In einem Timer-Interrupt verschiedene Software-Prescaler machen (nur jeder x-te Interrupt wird bestimmter Code ausgeführt)