Pulsbreitenmodulation

  Allgemeines   Timer mit fester Frequenz   Timer mit variabler Frequenz   Verwendung des UART
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.
zurück zum Anfang

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
 CPU 8051
 INCLUDE stddef51

PWM_OFFVALUE    EQU   10
PWM_ONVALUE     EQU   11
PWM_COUNT       EQU   12
PWM_OUT         EQU P3.0

 SEGMENT code
 ORG 0h
        jmp start

; Interruptbehandlungsroutine Timer 0
; Jeder Durchlauf braucht 9 Zyklen
 ORG 0Bh
        djnz PWM_COUNT, pwm_exit
        jb PWM_OUT, pwm_on
pwm_off:
        mov PWM_COUNT, PWM_ONVALUE
        setb PWM_OUT
        reti
pwm_on:        
        mov PWM_COUNT, PWM_OFFVALUE
        clr PWM_OUT
pwm_exit:
        reti

; Initialisierung
start:
        mov TL0, #235
        mov TH0, #235
        mov SP,  #20h
        mov TMOD,#2
        mov TCON,#16
        mov IE,  #130

; Hauptprogramm
main:
        ; quick 'n dirty Parameter als Test von P1 holen
        mov a,P1
        subb a,#20
        mov PWM_ONVALUE, a
        subb a,#255
        mov PWM_OFFVALUE, a
        jmp main
END

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.
zurück zum Anfang

 CPU 8051
 INCLUDE stddef51

PWM_OFFTIME     EQU   10
PWM_ONTIME      EQU   11
PWM_OUT         EQU P3.0

 SEGMENT code
 ORG 0h
        jmp start

; Interruptbehandlungsroutine Timer 0
; Jeder Durchlauf braucht 9 Zyklen
 ORG 0Bh
        cpl PWM_OUT            
        jb PWM_OUT, pwm_on
pwm_off:
        mov TH0, PWM_ONTIME        
        reti
pwm_on:        
        mov TH0, PWM_OFFTIME        
        reti

; Initialisierung
start:
        mov TL0, PWM_ONTIME
        mov TH0, PWM_OFFTIME
        mov SP,  #20h
        mov TMOD,#2
        mov TCON,#16
        mov IE,  #130

; Hauptprogramm
main:
        mov a,P1
        subb a,#20
        mov PWM_ONTIME, a
        subb a,#255
        mov PWM_OFFTIME, a
        jmp main
END

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.
zurück zum Anfang

 CPU 8051
 INCLUDE stddef51

PWM_FLAG    EQU  0 ; Bit 20.0 h
PWM_B0      EQU 10
PWM_B1      EQU 11

 SEGMENT code
 ORG 0h
        jmp start

; Interruptbehandlungsroutine serieller Port
; Jeder Durchlauf braucht 10 Zyklen
 ORG 023h
        clr SCON.1
        cpl PWM_FLAG
        jb PWM_FLAG, pwm_l1
pwm_l0:
        mov SBUF,PWM_B1
        reti 
pwm_l1:
        mov SBUF,PWM_B0
        reti

pwm_data:
    db 00000000b, 00000000b    ; 0
    db 00000001b, 00000000b    ; 1
    db 00000001b, 00000001b    ; 2
    db 00010001b, 00000001b    ; 3
    db 00010001b, 00010001b    ; 4
    db 00010010b, 01001001b    ; 5
    db 01001001b, 01001001b    ; 6
    db 01010101b, 01001001b    ; 7
    db 01010101b, 01010101b    ; 8
    db 01011101b, 01010101b    ; 9
    db 01011101b, 01011101b    ;10
    db 01011111b, 01011101b    ;11
    db 01110111b, 01110111b    ;12
    db 01111111b, 01110111b    ;13
    db 01111111b, 01111111b    ;14
    db 11111111b, 01111111b    ;15
    db 11111111b, 11111111b    ;16

start:
        mov SP,  #30h
        mov TL1, #250
        mov TH1, #250
        mov TCON,#64
        mov SCON,#82
        mov TMOD,#32
        mov IE,  #144

        mov PWM_B0, #0b
        mov PWM_B1, #0b
        mov DPTR, #pwm_data
main:
        mov a,P1
        cpl a
        anl a,#15
        rl a
        push Acc
        movc a,@A+DPTR
        mov PWM_B0,a
        pop Acc
        inc a
        movc a,@A+DPTR
        mov PWM_B1,a
        jmp main
END


Seite zurück Startseite
Erik Buchmann
EMail an: Owner@ErikBuchmann.de