AVR-Tutorial: Servo
Diese Seite ist noch im Entstehen und, bis sie einigermaßen vollständig ist, noch kein Teil des Tutorials.
Vorbemerkung
Dieses Tutorial überschneidet sich inhaltlich mit dem Artikel Modellbauservo Ansteuerung. Wir beschreiben hier die grundsätzliche Vorgehensweise zur Ansteuerung von Servos und geben ein Beispiel in Assembler, während der genannte Artikel ein in C formuliertes Beispiel zur Ansteuerung mehrerer Servos liefert.
Allgemeines über Servos
Stromversorgung
Werden Servos an einem Mikrocontroller betrieben, so ist es am besten, sie aus einer eigenen Stromquelle (Akku) zu betreiben. Manche Servos erzeugen kleine Störungen auf der Versorgungsspannung, die einen Controller durchaus zum Abstürzen bringen können. Muss man Servos gemeinsam mit einem µC von derselben Stromquelle betreiben, so sollte man sich gleich darauf einrichten, diesen Störimpulsen mit Kondensatoren zu Leibe rücken zu müssen. Unter Umständen ist hier auch eine Mischung aus kleinen, schnellen Kondensatoren (100 nF) und etwas größeren, aber dafür auch langsameren Kondensatoren (einige µF) notwendig.
Die eindeutig beste Option ist es aber, die Servos strommäßig vom Mikrocontroller zu entkoppeln und ihnen ihre eigene Stromquelle zu geben. Servos sind nicht besonders heikel. Auch im Modellbau müssen sie mit unterschiedlichen Spannungen zurechtkommen, bedingt durch die dort übliche Versorgung aus Akkus, die im Laufe der Betriebszeit des Modells natürlich durch die Entladung ihre Spannung immer weiter reduzieren. Im Modellbau werden Akkus mit 4 oder 5 Zellen verwendet, sodass Servos mit Spannungen von ca. 4 V bis hinauf zu ca. 6 V zurecht kommen müssen, wobei randvolle Akkus diese 6 V schon auch mal überschreiten können. Bei sinkender Spannungslage verlieren Servos naturgemäß etwas an Kraft bzw. werden in ihrer Stellgeschwindigkeit unter Umständen langsamer.
Die Servos werden dann nur mit ihrer Masseleitung und natürlich mit ihrer Impulsleitung mit dem Mikrocontroller verbunden.
Das Servo-Impulstelegramm
Das Signal, das an den Servo geschickt wird, hat eine Länge von ungefähr 20 ms. Diese 20 ms sind nicht besonders kritisch und sind ein Überbleibsel von der Technik, mit der mehrere Kanäle über die Funkstrecke einer Fernsteuerung übertragen werden. Für das Servo wichtig ist die Impulsdauer in der ersten Phase eines Servosignals. Nominell ist dieser Impuls zwischen 1 ms und 2 ms lang. Wobei das jeweils die Endstellungen des Servos sind, an denen es noch nicht mechanisch begrenzt wird. Eine Pulslänge von 1,5 ms wäre dann Servomittelstellung. Für die Positionsauswertung des Servos haben die 20 ms Wiederholdauer keine besondere Bedeutung, sieht man einmal davon ab, dass ein Servo bei kürzeren Zeiten entsprechend öfter Positionsimpulse bekommt und daher auch öfter die Position gegebenenfalls korrigiert, was möglicherweise in einem etwas höheren Stromverbrauch resultiert.
Umgekehrt lässt sich definitiv Strom sparen, indem die Pulse ganz ausgesetzt werden: Der Servo bleibt in der Position, in der er sich gerade befindet – korrigiert sich aber auch nicht mehr. Kommen die Impulse selten, also z. B. alle 50 ms, läuft der Servo langsamer in seine Zielposition (praktische Erfahrungen, vermutlich nirgends spezifiziert). Dieses Verhalten lässt sich nutzen, um die manchmal unerwünschten ruckartigen Bewegungen eines Servos abzumildern.
Den meisten Servos macht es nichts aus, wenn die Länge des Servoprotokolls anstelle von 20 ms auf z. B. 10 ms verkürzt wird. Bei der Generierung des Servosignals muss man daher den 20 ms keine besondere Beachtung schenken. Eine kleine Pause nach dem eigentlichen Positionssignal reicht in den meisten Fällen aus und es spielt keine allzugroße Rolle, wie lange diese Pause tatsächlich ist. Generiert man das Impulsdiagramm z. B. mit einem Timer, so orientiert man sich daher daran, dass man den 1,0…2,0-ms-Puls gut generieren kann und nicht an den 20 ms.
Reale Servos haben allerdings in den Endstellungen noch Reserven, so dass man bei vielen Servos auch Pulslängen von 0,9 bis 2,1 ms oder sogar noch kleinere/größere Werte benutzen kann. Allerdings sollte man hier etwas Vorsicht walten lassen. Wenn das Servo unbelastet in einer der Endstellungen deutlich zu „knurren“ anfängt, dann hat man es übertrieben. Das Servo ist an seinen mechanischen Endanschlag gefahren worden und auf Dauer wird das der Motor bzw. das Getriebe nicht aushalten.
Programmierung
Einfache Servoansteuerung mittels Warteschleifen
Im folgenden Programm wurden einfache Warteschleifen auf die im Tutorial übliche Taktfrequenz von 4 MHz angepasst, so dass sich die typischen Servo-Pulsdauern ergeben. Ein am Port D, beliebiger Pin angeschlossenes Servo dreht damit ständig vor und zurück. Die Servoposition kann durch Laden eines Wertes im Bereich von 1 bis ca. 160 in das Register r18 und anschließendem Aufruf von servoPuls
in einen Puls für ein Servo umgewandelt werden.
.include "m8def.inc"
.equ XTAL = 4000000
rjmp init
init:
ldi r16, HIGH(RAMEND) ; Stackpointer initialisieren
out SPH, r16
ldi r16, LOW(RAMEND)
out SPL, r16
ldi r16, 0xFF
out DDRD, r16
loop: ldi r18, 0
loop1: inc r18
cpi r18, 160
breq loop2
rcall servoPuls
rjmp loop1
loop2: dec r18
cpi r18, 0
breq loop1
rcall servoPuls
rjmp loop2
servoPuls:
push r18
ldi r16, 0xFF ; Ausgabepin auf 1
out PORTD, r16
rcall wait_puls ; die Wartezeit abwarten
ldi r16, 0x00 ; Ausgabepin wieder auf 0
out PORTD, r16
rcall wait_pause ; und die Pause hinten nach abwarten
pop r18
ret
;
wait_pause:
ldi r19, 15
w_paus_1: rcall wait_1ms
dec r19
brne w_paus_1
ret
;
wait_1ms: ldi r18, 10 ; 1 Millisekunde warten
w_loop2: ldi r17, 132 ; Es müssen bei 4 Mhz 4000 Zyklen verbraten werden
w_loop1: dec r17 ; die innerste Schleife umfasst 3 Takte und wird 132
brne w_loop1 ; mal abgearbeitet: 132 * 3 = 396 Takte
dec r18 ; dazu noch 4 Takte für die äußere Schleife = 400
brne w_loop2 ; 10 Wiederholungen: 4000 Takte
ret ; der ret ist nicht eingerechnet
;
; r18 muss mit der Anzahl der Wiederholungen belegt werden
; vernünftige Werte laufen von 1 bis ca 160
wait_puls:
w_loop4: ldi r17, 10 ; die variable Zeit abwarten
w_loop3: dec r17
brne w_loop3
dec r18
brne w_loop4
rcall wait_1ms ; und noch 1 Millisekunde drauflegen
ret
Wie meistens gilt auch hier: Warteschleifen sind in der Programmierung nicht erwünscht. Der Prozessor kann in diesen Warteschleifen nichts anderes machen. Etwas ausgeklügeltere Programme, bei denen mehrere Dinge gleichzeitig gemacht werden sollen, sind damit nicht vernünftig realisierbar. Daher sollte die Methode mittels Warteschleifen nur dann benutzt werden, wenn dies nicht benötigt wird, wie z. B. bei einem simplen Servotester, bei dem man die Servoposition z. B. durch Auslesen eines Potis mit dem ADC festlegt.
Außerdem ist die Berechnung der Warteschleifen auf eine bestimmte Taktfrequenz unangenehm und fehleranfällig :-)
Einfache Servoansteuerung mittels Timer
Ansteuerung mit dem 16-Bit-Timer 1
Im Prinzip programmiert man sich hier eine Software-PWM. Beginnt der Timer bei 0 zu zählen (nach Overflow-Interrupt oder Compare-Match-Interrupt im CTC-Modus (Clear Timer on Compare Match)), so setzt man den gewünschten Ausgangspin auf 1. Ein Compare-Match-Register wird so mit einem berechneten Wert versorgt, daß es nach der gewünschten Pulszeit einen Interrupt auslöst. In der zugehörigen Interrupt-Routine wird der Pin dann wieder auf 0 gesetzt.
Auch hier wieder: Der Vorteiler des Timers wird so eingestellt, dass man die Pulszeit gut mit dem Compare-Match erreichen kann; die nachfolgende Pause, bis der Timer dann seinen Overflow hat (oder den CTC-Clear macht) ist von untergeordneter Bedeutung. Man nimmt, was vom Zählbereich des Timers übrig bleibt. Beispiel:
- F_CPU = 16 MHz
- Vorteiler = 8
- 16 Bit Registerbreite → Overflow nach 216 Zyklen
- CTC-Modus aus, steigende Flanke durch Overflow-Interrupt erzeugen
- Overflow-Interrupt: 16 MHz / 216 / 8 = 30,52 Hz, 32,8 ms, Ausgangspin auf 1 setzen
- Servostellung links…rechts = Pulsbreite 1…2 ms = 1 s / 16.000.000 * 8 * OCR1 → OCR1 = 2.000…4.000
- Output-Compare-Match-Interrupt: Ausgangspin auf 0 setzen
Alternativ zum Overflow-Interrupt könnte, um eine Gesamtpulsbreite von exakt 20 ms zu erreichen, z. B. im Output-Compare-Match-Interrupt (sofern nur einer pro Timer verfügbar ist) im Wechsel
- der Ausgangspin auf 1 und OCR1 auf 1.000…2.000 gesetzt sowie das CTC-Bit gelöscht werden,
- der Ausgangspin auf 0 und OCR1 auf 40.000 gesetzt sowie das CTC-Bit gesetzt werden.
Neuere ATmegas verfügen über zwei Output-Compare-Register (Suffix A und B) ebenso wie zwei OCR-Interrupt-Vektoren pro Timer, wobei das CTC-Bit nur bei einem Match mit dem Output-Compare-Register A wirkt. Damit können für die Pulserzeugung zwei getrennte Interrupt-Handler angelegt werden, und ein Umbelegen von OCR und CTC-Bit entfällt.
Bei einem Arduino kann man auch die Pins 9 und 10 direkt per Hardware ganz ohne Interrupts ansteuern:
setup() {
DDRB |= _BV(DDB1) | _BV(DDB2); // set pins OC1A = PortB1 -> PIN 9 and OC1B = PortB2 -> PIN 10 to output direction
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM11); // FastPWM Mode mode TOP determined by ICR1 - non-inverting Compare Output mode
TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS11); // set prescaler to 8, FastPWM Mode mode continued
ICR1 = 40000; // set period to 20 ms
OCR1A = 3000; // set count to 1500 us - 90 degree
OCR1B = 3000; // set count to 1500 us - 90 degree
TCNT1 = 0; // reset timer
}
// If value is below 180 then assume degree, otherwise assume microseconds
void setServoPulse(int aValue) {
if (aValue <= 180) {
//aValue = map(aValue, 0, 180, 1000, 5200); // values for a SG90 MicroServo
aValue = map(aValue, 0, 180, 1088, 4800); // values compatible with standard arduino values
} else {
// since the resolution is 1/2 of microsecond
aValue *= 2;
}
OCR1A = aValue; // Pin9
// OCR1B = aValue; // Pin 10
}
Ansteuerung mit dem 8-Bit-Timer 2
Mit einem 8-Bit-Timer ist es gar nicht so einfach, sowohl die Zeiten für den Servopuls als auch die für die Pause danach unter einen Hut zu bringen. Abhilfe schafft ein Trick.
Der Timer wird so eingestellt, dass sich der Servopuls gut erzeugen lässt. Dazu wird der Timer in den CTC-Modus gestellt und das zugehörige Vergleichsregister so eingestellt, dass sich die entsprechenden Interrupts zeitlich so ergeben, wie es für einen Puls benötigt wird. In einem Aufruf des Interrupts wird der Ausgangspin für das Servo auf 1 gestellt, im nächsten wird er wieder auf 0 gestellt. Die kleine Pause bis zum nächsten Servoimpuls wird so erzeugt, dass in einer gewissen Anzahl an Interrupt-Aufrufen einfach nichts gemacht wird. Ähnlich wie bei einer PWM wird also auch hier wieder ein Zähler installiert, der die Anzahl der Interrupt-Aufrufe mitzählt und immer wieder auf 0 zurückgestellt wird.
Die eigentliche Servoposition steht im Register OCR2. Rein rechnerisch beträgt ihr Wertebereich:
1 ms 4.000.000 / 64 / 1.000 OCR2 = 62,5 2 ms 4.000.000 / 64 / 500 OCR2 = 125
mit einer Mittelstellung von (62,5 + 125) / 2 = 93,75.
.include "m16def.inc"
.equ XTAL = 4000000
rjmp init
.org OC2addr
rjmp Compare_vect
init:
ldi r16, HIGH(RAMEND) ; Stackpointer initialisieren
out SPH, r16
ldi r16, LOW(RAMEND)
out SPL, r16
ldi r16, 0x80
out DDRB, r16 ; Servo-Ausgangspin -> Output
ldi r17, 0 ; Software-Zähler
ldi r16, 120
out OCR2, r16 ; OCR2 ist der Servowert
ldi r16, 1<<OCIE2
out TIMSK, r16
ldi r16, (1<<WGM21) | (1<<CS22) ; CTC, Prescaler: 64
out TCCR2, r16
sei
main:
rjmp main
Compare_vect:
in r18, SREG
inc r17
cpi r17, 1
breq PulsOn
cpi r17, 2
breq PulsOff
cpi r17, 10
brne return
ldi r17, 0
return: out SREG, r18
reti
PulsOn: sbi PORTB, 0
rjmp return
PulsOff: cbi PORTB, 0
rjmp return
Ansteuerung mehrerer Servos mittels Timer
(to be continued …)