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:
TMODkiest de modus. De lage nibbel is timer 0, de hoge nibbel timer 1.TCONbevat 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
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
TMODin éé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
TH0richting 200, voegusing 1toe, en verhuis al het rekenwerk uit de interrupt. - Dutywijzigingen laten de uitvoer schokken.
duty_awordt doormaingeschreven terwijl de ISR hem leest. Een schrijfactie van één byte is op deze CPU atomair, dus eencharis 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