Tutorial · Mittelstufe · 35 Min.
8051-Timer und Software-PWM zur Motorsteuerung
Der 8051 hat keine PWM-Hardware, also baust du sie aus einem Timer-0-Interrupt. Die Auto-Reload-Rechnung, der Frequenz-Auflösungs-Kompromiss und die H-Brücke.
Einführung
Ein Arduino setzt eine Motordrehzahl mit analogWrite(pin, 180). Hinter diesem einen Aufruf steckt eine Hardware-Peripherie, die kontinuierlich ein Signal erzeugt – gratis, während die CPU etwas anderes tut.
Der 8051 hat keine solche Peripherie. Nichts auf dem Chip kann ein Signal mit variablem Tastverhältnis erzeugen. Willst du einen Motor mit 70 % laufen lassen, musst du das Signal selbst erzeugen – aus einem Timer-Interrupt – und zahlst dafür mit CPU-Zeit. Genau das zu bauen ist das Lehrreichste, was du auf diesem Chip tun kannst, weil es sichtbar macht, was analogWrite() für dich getan hat.
Dies setzt voraus, dass du die Ports schon kennst. Ist PWM selbst neu, erklärt die PWM- und H-Brücken-Anleitung, warum schnelles Ein- und Ausschalten eines Motors einer variablen Spannung entspricht.
Die Timer, die du hast
Der klassische 8051 hat zwei 16-Bit-Timer/Zähler, Timer 0 und Timer 1; der AT89S52 fügt einen dritten hinzu. Jeder zählt Maschinenzyklen – je zwölf Oszillatorperioden –, bei 11,0592 MHz tickt ein Timer also alle 1,085 µs.
Praktisch bekommst du einen davon geschenkt. Timer 1 ist fast immer von der Serienschnittstelle belegt, die ihre Baudrate aus Timer-1-Überläufen ableitet. Damit bleibt Timer 0 als Allzweck-Timer, und auf ihm baust du PWM auf.
Zwei Register konfigurieren sie:
TMODwählt die Betriebsart. Das niedrige Nibble ist Timer 0, das hohe Nibble Timer 1.TCONhält die Startbits (TR0,TR1) und die Überlaufflags (TF0,TF1).
Timer 0 hat vier Betriebsarten, aber nur eine passt für einen periodischen Takt:
| Modus | TMOD-Low-Nibble |
Was er tut |
|---|---|---|
| 0 | 0x00 |
13-Bit-Zählung – Kompatibilitätsrelikt |
| 1 | 0x01 |
16-Bit-Zählung, Nachladen per Hand bei jedem Überlauf |
| 2 | 0x02 |
8-Bit-Zählung mit automatischem Nachladen |
| 3 | 0x03 |
Teilt Timer 0 in zwei 8-Bit-Timer |
Modus 2 ist der richtige. Bei Überlauf kopiert die Hardware TH0 selbstständig zurück nach TL0, die Periode bleibt also exakt. Modus 1 lässt deine Interruptroutine den Zähler nachladen, und jeder Maschinenzyklus zwischen Überlauf und deinem Schreibzugriff ist driftende Zeit, die sich für immer aufsummiert.
Den Takt einstellen
In Modus 2 zählt der Timer von TH0 bis 255 aufwärts und läuft über, die Periode ist also die Strecke, die er zurücklegen muss:
cycles per tick = 256 − TH0
tick period = (256 − TH0) × 12 / f_osc
TH0 = 156, f_osc = 11.0592 MHz
cycles = 100
tick = 100 × 12 / 11 059 200 = 108.5 µs
Das Einschalten braucht fünf Zeilen:
#include <reg52.h>
TMOD = (TMOD & 0xF0) | 0x02; // Timer 0 -> mode 2, leave Timer 1 alone
TH0 = 156; // auto-reload value: 100 machine cycles
TL0 = 156; // first count starts from the same place
ET0 = 1; // enable the Timer 0 interrupt
EA = 1; // global interrupt enable
TR0 = 1; // start counting
Das TMOD & 0xF0 zählt. TMOD ist nicht bitadressierbar, also löscht TMOD = 0x02 nebenbei die Modusbits von Timer 1 – und falls der UART läuft, hast du gerade deine serielle Ausgabe zerstört, während du einen Motor konfigurierst.
PWM aus einem Zähler
Mit einem periodischen Interrupt in der Hand ist PWM Buchhaltung. Zähle Ticks bis zu einer Periodenlänge N hoch und halte den Ausgang auf High, solange der Zähler unter dem Tastverhältnis-Wert liegt:
#define PWM_LEVELS 20 // ticks per PWM period
sbit ENA = P2^0; // H-bridge enable — the PWM'd pin
volatile unsigned char tick = 0;
volatile unsigned char duty_a = 0; // 0 .. PWM_LEVELS
void timer0_isr(void) interrupt 1 {
if (++tick >= PWM_LEVELS) tick = 0;
ENA = (tick < duty_a); // high for the first duty_a ticks of each period
}
interrupt 1 ist der Vektor von Timer 0 (Adresse 0x000B). Das Überlaufflag TF0 löscht die Hardware beim Einsprung, anders als Timer-Flags, die du per Hand abfragst, gibt es hier also nichts zurückzusetzen.
Eine Drehzahl zu setzen ist jetzt nur eine Variable:
duty_a = 14; // 14/20 = 70% duty
Der Kompromiss, dem du nicht entkommst
PWM-Frequenz, Tastverhältnis-Auflösung und CPU-Last sind eine Beziehung mit drei Namen:
f_pwm = f_osc / (12 × C × N)
C = machine cycles per tick (256 − TH0)
N = ticks per PWM period (also your number of duty steps)
Halte C auf 100 und variiere N, und Auflösung kauft sich mit Frequenz:
N |
Tastverhältnis-Auflösung | PWM-Frequenz | Interrupts/s |
|---|---|---|---|
| 10 | 10 % | 921,6 Hz | 9 216 |
| 20 | 5 % | 460,8 Hz | 9 216 |
| 50 | 2 % | 184,3 Hz | 9 216 |
| 100 | 1 % | 92,2 Hz | 9 216 |
Die Interruptrate ändert sich nie, weil sie nur von C abhängt. Was sich ändert, ist, wie viele Ticks du verbrauchst, bevor sich das Muster wiederholt – und unterhalb von rund 100 Hz mittelt ein kleiner Gleichstrommotor die Pulse nicht mehr, sondern arbeitet sie hörbar einzeln ab.
Also der naheliegende nächste Schritt: Behalte 460,8 Hz und hole 1 % Auflösung, indem du N = 100 und C = 20 setzt. Die Rechnung stimmt – und der Chip stirbt. Bei C = 20 feuert der Interrupt alle 20 Maschinenzyklen, während die ISR oben etwa 25 braucht, nur um Register zu sichern, zu vergleichen und zurückzukehren. Sie wird nie fertig, bevor der nächste kommt, und nichts anderes wird je ausgeführt.
Das ist die echte Obergrenze. Bei ISR-Kosten von etwa 25 Maschinenzyklen verbraucht C = 100 ein Viertel der CPU für PWM und C = 50 die Hälfte. Hardware-PWM auf einem Arduino kostet null – bei jeder Frequenz und Auflösung. Dieser eine Vergleich ist mehr wert als jede Funktionstabelle.
Zwei Dinge schaffen billig Luft: Wechsle in der ISR auf Registerbank 1 (using 1), damit der Compiler zwei Bits tauscht statt acht Register zu sichern, und halte die ISR bei Zählen und Pin-Schreiben. Alles Rechnende gehört in main.
Die Motoren treiben
Eine H-Brücke braucht zwei Richtungseingänge und ein Enable pro Motor. Speise die Richtungspins von normalen Port-Pins und das Enable vom PWM-Pin:
sbit IN1 = P2^1;
sbit IN2 = P2^2;
sbit ENA = P2^0; // PWM'd by the ISR
void motor_forward(unsigned char duty) {
IN1 = 1; IN2 = 0;
duty_a = duty;
}
void motor_reverse(unsigned char duty) {
IN1 = 0; IN2 = 1;
duty_a = duty;
}
void motor_stop(void) {
duty_a = 0;
IN1 = 0; IN2 = 0;
}
Ein zweiter Motor ist eine zweite Tastverhältnis-Variable und eine zweite Ausgangsleitung in derselben ISR – der Tick-Zähler ist gemeinsam, beide Kanäle bleiben also im Takt:
void timer0_isr(void) interrupt 1 using 1 {
if (++tick >= PWM_LEVELS) tick = 0;
ENA = (tick < duty_a);
ENB = (tick < duty_b);
}
Ein Verdrahtungsdetail speziell für diesen Chip. Wie in der GPIO-Anleitung behandelt, liefert ein 8051-Pin nur Dutzende Mikroampere. Das reicht locker für einen CMOS-Eingang, aber ein L298N oder L293D hat bipolare Eingänge, die bei logisch High bis zu 100 µA ziehen können – genau an der Grenze dessen, was der Pin liefern kann. Ein 4,7-kΩ-Pull-up von jedem Treibereingang nach +5 V beseitigt die Unsicherheit für wenige Cent und macht aus einem Board, das auf dem Tisch, aber nicht auf dem Roboter funktioniert, eines, das einfach läuft.
Wenn es schiefgeht
- Der Motor brummt, dreht aber kaum. Deine PWM-Frequenz ist zu niedrig oder das Tastverhältnis unter der Haftreibungsschwelle des Motors. Hebe die Frequenz über ca. 100 Hz und erwarte, dass ein echter Motor 25–40 % braucht, bevor er sich überhaupt bewegt.
- Serielle Ausgabe brach ab, als du den Timer hinzufügtest. Du hast
TMODkomplett geschrieben und die Modusbits von Timer 1 gelöscht. Maskiere:TMOD = (TMOD & 0xF0) | 0x02;. - Die PWM-Periode driftet. Du bist in Modus 1 und lädst per Hand nach. Wechsle in Modus 2 und lass die Hardware ran.
- Das ganze Programm kriecht. Die ISR ist zu teuer für die Tickrate. Hebe
TH0Richtung 200, fügeusing 1hinzu und verlagere jede Rechnung aus dem Interrupt. - Tastverhältnis-Änderungen erzeugen Störungen.
duty_awird vonmaingeschrieben, während die ISR es liest. Ein Ein-Byte-Schreibzugriff ist auf dieser CPU atomar, eincharist also sicher, aber alles Breitere braucht den gesperrten Interrupt rund um die Aktualisierung. - Ein Motor ignoriert dich völlig. Prüfe, ob der Enable-Pin wirklich der ist, den die ISR treibt, und ob die Treibereingänge ihre Pull-ups haben.
Den Graphen erkunden
Teil dieser Bauprojekte
Projekte und Lernpfade, die dieses Tutorial enthalten.
Weiterführende Literatur