Für das Thema RC-Steuerungen über Arduino musste ich mir die Grundlagen zur Timerinterruptsteuerung aneignen. Ich lasse meine Leser gerne an den zusammengefassten Informationen teilhaben:
Die Grafiken sind überwiegend aus Publikationen im Internet. Hauptsächlich sind hier die technischen Datenblätter der MEGA-Reihe von ATMEL zum Zug gekommen. Ich möchte aber trotzdem die wichtigste Quelle nennen: http://maxembedded.com/2011/07/avr-timers-ctc-mode/
Zunächst ist die Erkenntnis wichtig, dass die kleineren Arduinos über 3 Timer verfügen. Davon ist Timer0 und Timer2 ein 8bit-Timer und Timer1 ist ein 16bit-Timer. Hier betrachte ich nur den Timer1.
So ist dann auch verständlich, warum das Register TCCR1 logisch in zwei 8-bittige Register unterteilt ist, zumindest programmtechnisch.
In diesen beiden Teilregistern interessieren wir uns für die markierten Bits:
Es gibt einen speziellen Modus für den Timer, der bewirkt, dass er immer hochzählt bis zu einem Vergleichswert und dann wieder von 0 anfängt zu zählen: CTC Clear Timer and Compare. Dieser Timer läuft übrigens rein in der Hardware und braucht programmtechnisch nicht nachgebildet werden.
Den CTC-Mode setzt man so:
TCCR1A = 0; //Register definiert zurücksetzen
TCCR1B = 0; //zuerst Register definiert zurücksetzen
TCCR1B |= (1 << WGM12); //CTC gesetzt durch WGM12
Da die beiden Bits WGM11 und WGM10 in TCCR1A Low sind, kann man einfach das ganze Register 0 setzen. Sicherheitshalber macht man das Anfangs mit TCCR1B auch um dann gezielt das Bit WGM12 zu setzen. Bitschieberei wie oben gezeigt ist bekannt? Wenn nicht, dann bitte selbst erarbeiten, denn das brauchen wir noch öfters.
Timer bedeutet Takt! Systemtakt beim Arduino ist 16MHz. Nun will man aber nicht immer mit dem Systemtakt den Timer laufen lassen und kann deshalb Teiler definieren. Die heißen hier Prescaler und werden ebenfalls in Register TCCR1B gesetzt:
Das X in CSX2/1/0 steht für den benutzten Timer. Eine gängige Schreibweise bei den Definitionen zum Arduino. Wir benutzen Timer1 und deshalb ergibt sich …
TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024
Mit dem geteilten Systemtakt zählt der Timer jetzt hoch bis zum Vergleichswert OCR1A (OCR1B gibt es auch noch).
Berechnung: CPU-Frequenz (XTAL) / Prescale / Interruptfrequenz = OCR1A
Beispiel: OCR1A = 31250; //rechne 16MHz / 256 / 2Hz = 31250
Bei 16MHz und einem Prescale von 256 gibt es eine Frequenz von 62500Hz. Wenn man mit dieser Frequenz zählt ist man nach einer halben Sekunde (=2Hz=2/s) bei 31250. Der Interrupt löst also alle halbe Sekunde aus.
Ein weiteres Beispiel:
Bei 16MHz und Prescale 8 erhält man einen Zähltakt von 2MHz. Wird der Vergleichswert OCR1A auf 20 gesetzt, bekommt man alle 10µs einen Impuls.
16MHz mit Prescale 8 gibt 2MHz
2MHz / 20 (OCR1A) = 100kHz; 100kHz = 100.000/1s = 100.000 / 1000ms =
100.000 / 1.000.000µs = 1 / 10µs also 1 Interrupt pro 10µs
Wichtig: Nur Timer 1 (16bit) zählt bis 65535 = 2^16-1. Alle anderen Timer (8bit)zählen nur bis 255 = 2^8-1. Prescaler entsprechend wählen.
Neben OCR1A gibt es noch OCR1B. Man kann beide verwenden. Im Register TIFR gibt es korrespondierende Flags, die gesetzt werden, wenn der betreffende Werte erreicht wurde:
Sofern das im Interrupt passiert, braucht man die entsprechenden Bits nicht manuell zurücksetzen. Ohne Interrupt müssen die Bits jeweils mit ‚1‘ zurückgesetzt werden. Kein Scherz, kein Fehler! 1 wird zum Löschen benutzt …
TIFR |= (1 << OCF1A);
Der Test könnte so aussehen:
if (TIFR & (1 << OCF1A)) { …
Der Wert des Zählers lässt sich aus TCNT1 auslesen.
Im TCCR1A Register liegen noch die Flags für den Compare Output Mode = COM:
Das bezieht sich auf die beiden Ausgänge OC1A(PD5) und OC1B(PD4) am Prozessor. Nimmt man die gelbe Zeile, so schaltet der PD5 Ausgang jeweils um (z.Bsp.) LED-Blinken, wenn der Zählerstand erreicht wird:
TCCR1A |= (1 << COM1A0); // PD5 toggelt
TCCR1A |= (0 << COM1A0) | (0 << COM1A1); // PD4 und PD5 unbenutzt
Nutzung der Timer mit Interrupts
Das TIMSK Register ist für die Nutzung der Timer im Rahmen von Interrupts verantwortlich:
Die gelb markierten OCIE1A und OCIE1B sind hier von Interesse.
TIMSK |= (1 << OCIE1A); // aktiviert den Interrupt
Im obigen Beispiel wird der Interrupt immer ausgelöst, wenn der Zähler TCNT1 gleich dem Vergleichswert OCR1A wird. Das geht auch mit OCIE1B für den Vergleich von TCNT1 und OCR1B.
Die Interrupt Service Routine (ISR) wird dann wie eine Funktion beschrieben:
ISR (TIMER1_COMPA_vect) {…}
Ein kompletter Programmcode könnte dann so aussehen:
long i = 1;
void setup() {
cli(); // disable interrupts
// reset
TCCR1A = 0; // set TCCR1A register to 0
TCCR1B = 0; // set TCCR1B register to 0
TCNT1 = 0; // reset counter value
OCR1A = 31249; // compare match register
// set prescaler
TCCR1B |= (1 << CS12) | (1 << CS10); //Prescale 1024
// 16MHz/1024=15625Hz mit OCR1A=31249 Interrupt alle 2s
TCCR1B |= (1 << WGM12); // turn on CTC mode
TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
sei(); // allow interrupts
Serial.begin(9600); // start serial connection
}
ISR(TIMER1_COMPA_vect) { // function which will be called when an interrupt occurs at timer 1
Serial.println(i); // send current value of i to the pc
}
void loop() {
i++; // increment i
}
In diesem Dokument habe ich alles mal zusammengefasst: