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:

  • TMOD seleziona la modalità. Il nibble basso è Timer 0, quello alto è Timer 1.
  • TCON contiene 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
Diagramma temporale su quattro millisecondi che mostra il PWM software costruito con il Timer 0. Una fila di tacche equidistante lungo il fondo indica un interrupt del Timer 0 ogni 108.5 microsecondi con TH0 impostato a 156. Sopra, un'onda quadrata alterna tra 0 volt e 5 volt, restando alta per i primi sette scatti di ogni periodo di venti scatti e bassa per i restanti tredici. Una freccia a doppia punta attraversa un periodo completo ed è etichettata venti scatti = 2.17 millisecondi = 460.8 hertz; una seconda freccia più corta attraversa la parte alta ed è etichettata sette su venti = 35 percento.
Venti scatti formano un periodo; il valore di duty decide quanti di essi siano alti. La frequenza degli interrupt è fissa—cambia solo il rapporto. Scarica SVG

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 TMOD per 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 TH0 verso 200, aggiungi using 1 e sposta fuori dall’interrupt qualsiasi operazione aritmetica.
  • Le variazioni del duty producono glitch sull’uscita. duty_a viene scritta da main mentre la ISR la legge. Una scrittura di un byte è atomica su questa CPU, quindi un char è 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

Fonti