Nachdem ich etliche Male danach gefragt wurde, wie sich eine Pulsbreitenmodulation auf einem 8051-Controller am effektivsten implementieren lässt, wollte ich es wissen: auf dieser Seite werden drei verschiedene Varianten dazu vorgestellt und bewertet. | |||
Allgemeines |
|||
Die Pulsbreitenmodulation (Pulse Width Modulation, PWM) dient an dieser
Stelle dazu, einen Gleichstrommotor über einen digitalen Ausgang nicht
nur ein- oder auszuschalten, sondern 'analog' zu regeln. Dabei wird
aber nicht die Spannung geregelt, denn dazu wäre eine vergleichsweise
aufwendige Hardware notwendig. Stattdessen wird über den
Digitalausgang eine Pulsfolge erzeugt. Das Verhältnis zwischen
High-Puls und Low-Puls über die Zeit ist dabei die Stellgröße. Dieses Prinzip funktioniert bei einer schnell ausgegebenen Pulsfolge, da der Motor wie alle mechanischen Komponenten im Vergleich zur Elektronik sehr träge ist. Die Frage ist nur, wie groß sollte die Frequenz der Pulsfolge am besten sein? Eine hohe Frequenz führt zu erhöhten Verlusten in den Leistungstreibern der Elektronik, die die digitale Pulsfolge für den Motor verstärkt. Andererseits führt eine zu niedrige Frequenz zu Vibrationen im Motor. Diese Vibrationen werden bei hohen Frequenzen durch die Masseträgheit der mechanischen Komponenten ausgeglichen. |
Ein nicht unwesentlicher
Punkt ist auch die Akustik: bei ungünstiger Frequenz ist ein
nervtötendes Summen die Folge. Für den Hausgebrauch genügt es sicher,
es beginnend mit der höchstmöglichen erzeugbaren Frequenz einfach
auszuprobieren. Um eine hohe Frequenz zu ermöglichen, darf der Code zur PWM-Erzeugung nicht zu lang werden. Ausserdem soll der Controller ja auch noch andere Aufgaben ausführen, und nicht nur mit der Erzeugung des Pulssignals beschäftigt sein. Darum sind die hier vorgestellten PWM-Codes darauf ausgelegt, über Interrupts oder Timer zu laufen, und ihre Parameter über Variablen von einem langsam ablaufenden Hauptprogramm zu beziehen. Diese Parameter können dann entweder zur Laufzeit errechnet oder aus einer vorberechneten Tabelle im Programmspeicher bezogen werden.
Hier werden nun drei Varianten vorgestellt, die zum Teil tief in die
Trickkiste greifen, um eine möglichst optimale PWM-Ausgabe zu
erzielen.
|
||
Variante 1: Timer mit fester Frequenz |
|||
Dabei wird ein Timer auf einen festen Wert programmiert, in diesem
Beispiel 0.01 Millisekunden. Bei jedem Aufruf wird ein Zähler mit DJNZ
heruntergezählt. Ist er 0, wird der PWM-Ausgang gelöscht bzw. gesetzt,
und der Zähler auf die Zeitspanne für diesen Puls gesetzt. Die Interruptroutine braucht für jeden Aufruf 6 Zyklen, wenn keine Änderungen anstehen, und 11 Zyklen für das Toggeln des PWM-Ausgangs. |
Da
der Interrupt viele Male umsonst aufgerufen wird, verschwendet diese
Art der Pulserzeugung sehr viel Prozessorzeit. Damit bietet sich diese
Vorgehensweise nur dann an, wenn kein Timer mehr exklusiv für die
PWM-Erzeugung zur Verfügung steht, da man den Code leicht an eine
bereits für andere Aufgaben erstellte Timer-Routine anhängen kann. An
sonsten geht es auch besser, wie die nächsten beiden Varianten
zeigen.
zurück zum Anfang |
||
| |||
Variante 2: Timer mit variabler Frequenz |
|||
Die Idee hinter dieser Variante ist, die Timerlaufzeit direkt zur
Ausgabe der Pulse heranzuziehen. Bei jedem Timer-Interrupt braucht dann
nur noch der PWM-Ausgang mit CPL getoggelt und ein neuer
Timer-Ladewert -- in Abhängigkeit des Ausgangs für die Dauer der Ein-
oder Aus-Zeit -- in die Timerkontrollregister geschrieben werden. Dafür lässt sich der 8 Bit Autoreload-Modus des Timers sehr gut gebrauchen. Bei einem 16 Bit-Timer muss man den Ladewert stets in das TL- und TH-Register schreiben. Beim Autoreload-Modus erledigt den einen Schreibvorgang die Hardware des Timers selbst. Es genügt also, den Wert für die nächste Zeitperiode in das TH-Register zu schreiben.
|
Insgesamt benötigt die Interruptroutine nur 9 Maschinenzyklen inklusive
Aufruf und verlassen mit RETI. Dieser Wert bestimmt dann auch den
Maximalwert für den Timer: bei größeren Werten als 245 ist der eine
Interrupt noch nicht abgearbeitet, wenn der nächste schon generiert
wird.
Der Interruptcontroller im uC gerät dadurch aber nicht aus dem Tritt
-- solange ein Interrupt noch nicht abgearbeitet ist, werden nur höher
priorisierte Interrupts abgearbeitet, alle anderen gesperrt. Um
Ungleichmäßigkeiten bei der Pulsfolge zu vermeiden, muss man dennoch
darauf achten, diesen Zustand zu vermeiden.
|
||
| |||
Variante 3: Verwendung des UART |
|||
Bisher wurden die Pulse stets direkt von einer Timer-Routine erzeugt.
Die zweite Variante ist auch schon recht elegant - es muss aber auch
noch besser gehen :-) Eine hochinteressante Möglichkeit bietet dazu der UART. Mit ihm lassen sich auf einen Rutsch 8 Pulse programmieren, die dann von der Hardware selbständig ausgegeben werden. Dabei gibt's zwei Probleme: zum Einen kommt man um das Start- und Stopbit nicht herum. Man muss also stets eine 1 und eine 0 pro Übertragungszyklus mit in die Berechnung des Bitmusters einbeziehen. Möchte man den PWM-Port ganz auf 0 oder 1 setzen, muss man den UART abstellen und den Ausgang selbst auf den gewünschten Wert setzen. |
Die andere Schwierigkeit liegt darin begründet, dass man ein Muster
ausgeben muss, das nicht direkt von einem Zähler abhängig ist. Abhilfe
schafft da eine kleine Tabelle im RAM, sofern man die Ausgabedaten
nicht umständlich zur Laufzeit berechnen will. Hier im Beispiel sind zwei Byte für die Ausgabe vorgesehen, die ohne vollständiges Aus oder Ein 17 Abstufungen erlauben. Wenn man statt des Bits, das zwischen zwei Bytes umschaltet, einen zähler einsetzt, lassen sich auch größere Bitmuster ausgeben.
Die UART-Modi mit fester Baudrate, die ihre Ausgabe direkt aus der
Quarzfrequenz ableiten, kommen für PWM leider nicht in Frage. Bei
mindestens 1/64 Quarzfrequenz bleiben nur 5 Maschinenzyklen zum Laden
des Wertes, ohne das Unterbrechungen entstehen. Da Int-Aufruf und RETI
allein schon 4 MSC verbrauchen, bleibt für das Laden der Werte nicht
genug Rechenzeit.
|
||
|