Tutorial · Gevorderd · 35 min lezen

8051-timers en software-PWM voor motorregeling

De 8051 heeft geen PWM-hardware, dus bouw je die uit een timer 0-interrupt. Herbelaadberekening, afweging frequentie-resolutie en het aansturen van een H-brug.

Inleiding

Een Arduino stelt een motorsnelheid in met analogWrite(pin, 180). Achter die ene aanroep zit een hardwareperipheral die continu een golfvorm opwekt, gratis, terwijl de CPU iets anders doet.

De 8051 heeft zo’n peripheral niet. Niets op de chip kan een golfvorm met variabele duty cycle opwekken. Wil je een motor op 70% draaien, dan moet je die golfvorm zelf opwekken, vanuit een timerinterrupt, en daar betaal je CPU-tijd voor. Dat bouwen is het meest leerzame wat je met deze chip kunt doen, omdat het precies zichtbaar maakt wat analogWrite() voor je deed.

Dit gaat ervan uit dat je de poorten al kent. Is PWM zelf nieuw, dan behandelt de tutorial over PWM en H-bruggen waarom een motor snel aan- en uitzetten equivalent is aan zijn spanning variëren.

De timers die je hebt

De klassieke 8051 heeft twee 16-bits timer/tellers, timer 0 en timer 1; de AT89S52 voegt er een derde toe. Elk telt machinacycli — elk twaalf oscillatorperioden — dus bij 11,0592 MHz tikt een timer elke 1,085 µs.

In de praktijk krijg je er gratis één. Timer 1 is vrijwel altijd al in gebruik door de seriële poort, die zijn baudrate afleidt uit overstromingen van timer 1. Dat laat timer 0 over als algemene timer, en dat is de waarop je PWM bouwt.

Twee registers configureren ze:

  • TMOD kiest de modus. De lage nibbel is timer 0, de hoge nibbel timer 1.
  • TCON bevat de runbits (TR0, TR1) en de overflowvlaggen (TF0, TF1).

Timer 0 heeft vier modi, maar er is er maar één geschikt voor een periodieke tik:

Modus lage nibbel van TMOD Wat hij doet
0 0x00 13-bits telling — een reliquie uit compatibiliteitsoverwegingen
1 0x01 16-bits telling, met de hand herbelaad bij elke overstroming
2 0x02 8-bits telling met automatische herlading
3 0x03 Splitst timer 0 in twee 8-bits timers

Modus 2 is wat je wilt. Bij een overstroming kopieert de hardware zelf TH0 terug naar TL0, zodat de periode exact blijft. Modus 1 laat je interrupt-service routine de telling zelf herladen, en elke machinacyclus tussen de overstroming en jouw schrijfactie is een cyclus drift die zich eeuwig ophoopt.

De tik instellen

In modus 2 telt de timer op van TH0 naar 255 en loopt hij over, dus de periode is de afstand die hij moet afleggen:

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

Aanzetten kost vijf regels:

#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

Die TMOD & 0xF0 is belangrijk. TMOD is niet bit-adresseerbaar, dus TMOD = 0x02 schrijven overschrijft ook de modusbits van timer 1 — en als de UART draait, heb je net je seriële uitvoer kapotgemaakt terwijl je een motor configureerde.

PWM uit een teller

Met een periodieke interrupt in de hand is PWM boekhouding. Tel tikken op tot een periodelengte N, en houd de uitgang hoog zolang de telling onder de dutywaarde zit:

#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 is de vector van timer 0 (adres 0x000B). De overflowvlag TF0 wordt door de hardware gewist zodra hij zijn vectorpunt binnenloopt, dus anders dan bij de timervlaggen die je met de hand pollen, is er niets om te resetten.

Een snelheid instellen is nu alleen nog een variabele:

duty_a = 14;     // 14/20 = 70% duty
Een tijddiagram over vier milliseconden met software-PWM gebouwd op timer 0. Onderaan een rij gelijkmatig verdeelde tikken markeert elke 108,5 microseconde één timer 0-interrupt met TH0 op 156. Daarboven wisselt een blokgolf tussen 0 volt en 5 volt, hoog gedurende de eerste zeven tikken van elke periode van twintig tikken en laag gedurende de overige dertien. Een dubbelgerichte pijl overspant één volledige periode en is gelabeld twintig tikken is gelijk aan 2,17 milliseconden gelijk aan 460,8 hertz, en een tweede kortere pijl overspant het hoge deel en is gelabeld zeven twintigste is gelijk aan 35 procent.
Twintig tikken vormen een periode; de dutywaarde bepaalt hoeveel ervan hoog zijn. De interruptfrequentie ligt vast — alleen de verhouding verandert. SVG downloaden

De afweging die je niet kunt ontwijken

PWM-frequentie, duty-resolutie en CPU-belasting zijn één verband met drie 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)

Houd C op 100 en varieer N, en de resolutie koopt zichzelf met frequentie:

N Duty-resolutie PWM-frequentie Interrupts/sec
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

De interruptfrequentie verandert nooit, want die hangt alleen van C af. Wat verandert is hoeveel tikken je besteedt voordat het patroon zich herhaalt — en onder ongeveer 100 Hz stopt een kleine gelijkstroommotor de pulsen te middelen en begint hij er hoorbaar doorheen te stappen.

Maak dan de voor de hand liggende volgende stap: houd 460,8 Hz en krijg 1% resolutie door N = 100 en C = 20 te zetten. De rekenkunde klopt — en de chip sterft. Bij C = 20 vuurt de interrupt elke 20 machinacycli af, terwijl de ISR hierboven er ongeveer 25 nodig heeft alleen al om registers op te slaan, te vergelijken en terug te keren. Hij is nooit klaar voordat de volgende binnenkomt, en er wordt nooit iets anders uitgevoerd.

Dat is de echte bovengrens. Met een ISR die ongeveer 25 machinacycli kost, besteedt C = 100 een kwart van de CPU aan PWM, en C = 50 de helft. Hardware-PWM op een Arduino kost niets, bij elke frequentie en elke resolutie. Deze ene vergelijking is meer waard dan elke functietabel.

Twee dingen kopen je ruimte goedkoop: wissel binnen de ISR naar registerbank 1 (using 1), zodat de compiler twee bits wisselt in plaats van acht registers op de stack te zetten, en houd de ISR bij tellen en pinschrijfacties. Alles wat rekent hoort in main.

De motoren aansturen

Een H-brug heeft per motor twee richtingsingangen en één enable. Voed de richtingspinnen vanaf gewone poortpinnen en de enable vanaf je met PWM bestuurde 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;
}

Een tweede motor is een tweede dutyvariabele en een tweede uitgangsregel in dezelfde ISR — de tikteller wordt gedeeld, dus beide kanalen blijven in fase:

void timer0_isr(void) interrupt 1 using 1 {
    if (++tick >= PWM_LEVELS) tick = 0;
    ENA = (tick < duty_a);
    ENB = (tick < duty_b);
}

Eén bedradingsdetail dat specifiek voor deze chip geldt. Zoals in de GPIO-tutorial besproken levert een 8051-pin slechts tientallen microampère. Dat is ruim voldoende voor een CMOS-ingang, maar een L298N of L293D heeft bipolaire ingangen die bij een logisch hoog niveau tot 100 µA kunnen opnemen — precies op de grens van wat de pin kan leveren. Een pull-up van 4,7 kΩ van elke driver-ingang naar +5 V neemt die twijfel voor een paar cent weg en verandert een bord dat op de werkbank werkt maar niet op de robot in een bord dat het gewoon doet.

Het gaat mis

  • De motor zoemt maar draait nauwelijks. Je PWM-frequentie is te laag, of de duty ligt onder de wrijvingsdrempel van de motor. Verhoog de frequentie boven ~100 Hz en reken erop dat een echte motor 25–40% nodig heeft voor hij überhaupt beweegt.
  • Seriële uitvoer brak toen je de timer toevoegde. Je schreef TMOD in één keer over en wist de modusbits van timer 1. Maskeer het: TMOD = (TMOD & 0xF0) | 0x02;.
  • De PWM-periode loopt weg. Je zit in modus 1 en herlaadt met de hand. Ga naar modus 2 en laat het de hardware doen.
  • Het hele programma kruipt. De ISR is te duur voor de tikfrequentie. Verhoog TH0 richting 200, voeg using 1 toe, en verhuis al het rekenwerk uit de interrupt.
  • Dutywijzigingen laten de uitvoer schokken. duty_a wordt door main geschreven terwijl de ISR hem leest. Een schrijfactie van één byte is op deze CPU atomair, dus een char is veilig, maar alles wat breder is heeft de interrupt uitgeschakeld nodig rond de update.
  • Eén motor negeert je volledig. Controleer of de enable-pin inderdaad de pin is die de ISR aanstuurt, en of de ingangen van de driver hun pull-ups hebben.

Verken de graaf

Onderdeel van deze bouwprojecten

Projecten en leerroutes waarin deze tutorial voorkomt.

Verder lezen

Bronnen