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:

  • TMOD wählt die Betriebsart. Das niedrige Nibble ist Timer 0, das hohe Nibble Timer 1.
  • TCON hä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
Zeitdiagramm über vier Millisekunden mit Software-PWM aus Timer 0. Eine Reihe gleichmäßiger Taktmarken unten markiert je einen Timer-0-Interrupt alle 108,5 Mikrosekunden bei TH0 gleich 156. Darüber wechselt ein Rechtecksignal zwischen 0 und 5 Volt, bleibt für die ersten sieben Ticks jeder Zwanzig-Tick-Periode auf High und für die übrigen dreizehn auf Low. Ein Doppelpfeil spannt eine volle Periode auf und ist mit zwanzig Ticks gleich 2,17 Millisekunden gleich 460,8 Hertz beschriftet, ein zweiter kürzerer Pfeil spannt den High-Anteil auf und ist mit sieben von zwanzig gleich 35 Prozent beschriftet.
Zwanzig Ticks ergeben eine Periode; der Tastverhältnis-Wert entscheidet, wie viele davon High sind. Die Interruptrate ist fest – nur das Verhältnis ändert sich. SVG herunterladen

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 TMOD komplett 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 TH0 Richtung 200, füge using 1 hinzu und verlagere jede Rechnung aus dem Interrupt.
  • Tastverhältnis-Änderungen erzeugen Störungen. duty_a wird von main geschrieben, während die ISR es liest. Ein Ein-Byte-Schreibzugriff ist auf dieser CPU atomar, ein char ist 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

Quellen