Tutorial · Intermedio · 35 min
Timer dell'8051 e PWM software per il controllo dei motori
L'8051 non ha PWM hardware: generalo con un interrupt di Timer 0. Gestisci il ricaricamento, il compromesso frequenza-risoluzione e pilota un H-bridge.
Introduzione
Un Arduino imposta la velocità di un motore con analogWrite(pin, 180). Dietro quella singola chiamata c’è una periferica hardware che genera continuamente una forma d’onda, gratuitamente, mentre la CPU fa qualcos’altro.
L’8051 non ha una periferica di questo tipo. Niente sul chip può produrre una forma d’onda con duty cycle variabile. Se vuoi far girare un motore al 70% devi generare tu la forma d’onda, da un interrupt del timer, e pagherai il costo in tempo CPU. Costruirla è la cosa più istruttiva che puoi fare su questo chip, perché rende visibile esattamente ciò che analogWrite() faceva per te.
Questo tutorial presuppone che tu abbia già incontrato le porte. Se il PWM ti è nuovo, il tutorial su PWM e H-bridge spiega perché accendere e spegnere rapidamente un motore equivale a modificarne la tensione.
I timer disponibili
L’8051 classico ha due timer/contatori a 16 bit, Timer 0 e Timer 1; l’AT89S52 ne aggiunge un terzo. Ognuno conta cicli macchina—dodici periodi dell’oscillatore ciascuno—quindi a 11.0592 MHz un timer scatta ogni 1.085 µs.
In pratica ne ottieni uno gratis. Il Timer 1 è quasi sempre occupato dalla porta seriale, che deriva la velocità in baud dagli overflow del Timer 1. Rimane quindi il Timer 0 come timer generico, ed è quello su cui costruire il PWM.
Due registri li configurano:
TMODseleziona la modalità. Il nibble basso è Timer 0, quello alto è Timer 1.TCONcontiene i bit di avvio (TR0,TR1) e i flag di overflow (TF0,TF1).
Il Timer 0 ha quattro modalità, ma solo una è adatta a uno scatto periodico:
| Modalità | Nibble basso di TMOD |
Cosa fa |
|---|---|---|
| 0 | 0x00 |
Conteggio a 13 bit—un relitto di compatibilità |
| 1 | 0x01 |
Conteggio a 16 bit, ricaricato manualmente a ogni overflow |
| 2 | 0x02 |
Conteggio a 8 bit con ricaricamento automatico |
| 3 | 0x03 |
Divide il Timer 0 in due timer a 8 bit |
La modalità 2 è quella che vuoi. All’overflow l’hardware copia da solo TH0 in TL0, così il periodo resta esatto. La modalità 1 impone alla routine di servizio dell’interrupt di ricaricare il conteggio e ogni ciclo macchina tra l’overflow e la tua scrittura è un ciclo di deriva che si accumula per sempre.
Impostare lo scatto
In modalità 2 il timer conta da TH0 a 255 e va in overflow, quindi il periodo è la distanza che deve percorrere:
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
L’attivazione richiede cinque righe:
#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
Quel TMOD & 0xF0 è importante. TMOD non è indirizzabile a bit, quindi scrivere TMOD = 0x02 sovrascrive anche i bit di modalità del Timer 1 e, se la UART è in funzione, hai appena rotto l’uscita seriale mentre configuri un motore.
Produrre PWM da un contatore
Una volta pronto un interrupt periodico, il PWM è solo contabilità. Conta gli scatti fino a una lunghezza di periodo N e mantieni alta l’uscita mentre il conteggio è inferiore al valore di duty cycle:
#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 è il vettore del Timer 0 (indirizzo 0x000B). Il flag di overflow TF0 viene azzerato dall’hardware quando il chip entra nel vettore, quindi, a differenza dei flag dei timer che controlli manualmente, non c’è nulla da reimpostare.
L’impostazione di una velocità ora è solo una variabile:
duty_a = 14; // 14/20 = 70% duty
Il compromesso da cui non puoi scappare
Frequenza PWM, risoluzione del duty cycle e carico della CPU sono una relazione con tre nomi:
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)
Tieni C a 100 e fai variare N: la risoluzione si compra con la frequenza:
N |
Risoluzione del duty cycle | Frequenza PWM | Interrupt/secondo |
|---|---|---|---|
| 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 |
La frequenza degli interrupt non cambia mai, perché dipende soltanto da C. Cambia il numero di scatti che impieghi prima che lo schema si ripeta e, sotto circa 100 Hz, un piccolo motore CC smette di mediare gli impulsi e inizia a percorrerli in modo udibile.
Fai quindi il passo successivo ovvio: mantieni 460.8 Hz e ottieni una risoluzione dell’1% impostando N = 100 e C = 20. L’aritmetica è d’accordo—e il chip muore. Con C = 20 l’interrupt scatta ogni 20 cicli macchina, mentre la ISR qui sopra ne richiede circa 25 solo per salvare i registri, confrontare e restituire. Non finisce mai prima che arrivi quella successiva e non viene mai eseguito nient’altro.
Questo è il limite reale. Con una ISR da circa 25 cicli macchina, C = 100 spende un quarto della CPU sul PWM e C = 50 ne spende metà. Il PWM hardware su un Arduino costa zero, a qualsiasi frequenza e risoluzione. Questo confronto vale più di qualsiasi tabella di funzionalità.
Due accorgimenti ti danno margine di manovra a basso costo: passa al banco di registri 1 dentro la ISR (using 1), così il compilatore scambia due bit invece di eseguire push di otto registri, e limita la ISR al conteggio e alla scrittura dei pin. Tutto ciò che calcola appartiene a main.
Pilotare i motori
Un H-bridge richiede due ingressi di direzione e un enable per ogni motore. Alimenta i pin di direzione con normali pin di porta e l’enable con il pin pilotato dal PWM:
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;
}
Un secondo motore richiede una seconda variabile di duty cycle e una seconda linea di uscita nella stessa ISR—il contatore degli scatti è condiviso, così entrambi i canali restano sincronizzati:
void timer0_isr(void) interrupt 1 using 1 {
if (++tick >= PWM_LEVELS) tick = 0;
ENA = (tick < duty_a);
ENB = (tick < duty_b);
}
Un dettaglio di cablaggio specifico di questo chip. Come spiegato nel tutorial GPIO, un pin dell’8051 fornisce solo decine di microampere. Sono più che sufficienti per un ingresso CMOS, ma un L298N o un L293D ha ingressi bipolari che possono assorbire fino a 100 µA con un livello logico alto—esattamente al limite di ciò che il pin può fornire. Un pull-up da 4.7 kΩ da ciascun ingresso del driver verso +5 V elimina l’ambiguità per pochi centesimi e trasforma una scheda che funziona sul banco ma non sul robot in una che semplicemente funziona.
Quando qualcosa va storto
- Il motore ronzia ma ruota appena. La frequenza PWM è troppo bassa oppure il duty è al di sotto della soglia di attrito del motore. Porta la frequenza sopra circa 100 Hz e aspettati che un motore reale necessiti del 25–40% prima di muoversi.
- L’uscita seriale si è rotta quando hai aggiunto il timer. Hai scritto
TMODper intero e azzerato i bit di modalità del Timer 1. Usa una maschera:TMOD = (TMOD & 0xF0) | 0x02;. - Il periodo PWM deriva. Sei in modalità 1 e ricarichi manualmente. Passa alla modalità 2 e lascia fare all’hardware.
- L’intero programma rallenta. La ISR è troppo costosa per la frequenza degli scatti. Alzare
TH0verso 200, aggiungiusing 1e sposta fuori dall’interrupt qualsiasi operazione aritmetica. - Le variazioni del duty producono glitch sull’uscita.
duty_aviene scritta damainmentre la ISR la legge. Una scrittura di un byte è atomica su questa CPU, quindi uncharè sicuro, ma qualsiasi valore più largo richiede l’interrupt disabilitato durante l’aggiornamento. - Un motore ignora completamente i comandi. Controlla che il pin enable sia davvero quello pilotato dalla ISR e che gli ingressi del driver abbiano i loro pull-up.
Esplora il grafo
Fa parte di queste costruzioni
Progetti e percorsi di apprendimento che includono questo tutorial.
Per approfondire