Un’uscita digitale che cambia nel tempo
Un’uscita digitale dell’ATmega328P può assumere due livelli: basso e alto. In un sistema alimentato a 5 V, il segnale ideale passa quindi tra circa 0 V e 5 V.
La modulazione PWM, Pulse Width Modulation, non aggiunge livelli intermedi. Il pin continua a commutare tra basso e alto; ciò che viene modificato è la durata relativa dei due stati all’interno di un periodo.
Un segnale può quindi rimanere alto per una parte breve del periodo, per metà del periodo oppure per quasi tutto il periodo. Questa proporzione viene indicata come duty cycle.
Periodo, frequenza e duty cycle
Il periodo è il tempo necessario per completare un ciclo del segnale. La frequenza indica quanti cicli vengono ripetuti in un secondo.
frequenza = 1 / periodo
Il duty cycle esprime la percentuale del periodo durante la quale il segnale rimane alto:
duty cycle = tempo alto / periodo × 100
Un duty cycle del 25% significa che il segnale rimane alto per un quarto del periodo. Al 50% il tempo alto e quello basso sono uguali. Al 100% il segnale rimane costantemente alto e non esiste più una commutazione periodica.
Il valore medio non è una tensione analogica reale
Su un carico che integra o media le commutazioni, un segnale PWM può produrre un effetto simile a quello di un valore intermedio. Un LED può apparire meno luminoso; un motore può ricevere una potenza media inferiore.
Questo non significa che sul pin sia presente una tensione continua stabile, per esempio 2,5 V. Con un duty cycle del 50%, il pin continua a passare tra circa 0 V e 5 V. Un multimetro può mostrare un valore medio, ma un oscilloscopio mostra gli impulsi reali.
Per ottenere una vera tensione continua a partire dal PWM serve un filtro adeguato e il risultato dipende dal carico, dalla frequenza e dall’ondulazione ammessa.
analogWrite() e PWM sull’ATmega328P
Nell’ambiente Wiring, analogWrite() utilizza normalmente un valore compreso tra 0 e 255:
analogWrite(pin, valore);
Il valore 0 corrisponde allo stato sempre basso; 255 allo stato sempre alto; i valori intermedi modificano il duty cycle.
Il nome della funzione può creare confusione: sull’ATmega328P non genera una tensione analogica tramite convertitore digitale-analogico. Configura invece i timer per produrre un segnale PWM sui pin predisposti.
Il servomotore utilizzato in questo articolo non viene però comandato con analogWrite(). Il suo segnale contiene impulsi la cui durata rappresenta la posizione richiesta.
Il comando di un servomotore RC
Un servomotore RC standard contiene un motore, una riduzione meccanica, un sensore di posizione e un circuito di controllo. Il microcontrollore non alimenta direttamente il motore attraverso il filo di segnale: invia soltanto il riferimento di posizione.
Il comando viene ripetuto periodicamente. Valori tipici sono:
- circa 1000 µs per una posizione vicina al limite minimo;
- circa 1500 µs per la posizione centrale;
- circa 2000 µs per una posizione vicina al limite massimo.
Questi valori sono indicativi. I limiti effettivi e l’angolo meccanico dipendono dal servomotore.
L’intervallo di ripetizione è normalmente vicino a 20 ms. Il servo interpreta soprattutto la durata dell’impulso alto, non la tensione media del segnale.
PWM generale e segnale del servo
Nel PWM utilizzato per regolare potenza, il parametro principale è il rapporto tra tempo alto e periodo. Nel comando del servomotore la larghezza dell’impulso rappresenta direttamente una posizione.
Poiché il periodo rimane approssimativamente costante, anche il duty cycle varia. Tuttavia descrivere il servo soltanto attraverso il duty cycle nasconde l’informazione utile: il circuito interno misura la durata dell’impulso in microsecondi.
La libreria Servo
La libreria Servo genera gli impulsi in background mediante un timer hardware. Il programma può quindi continuare a leggere ingressi ed eseguire altre operazioni tra un impulso e il successivo.
Il servomotore viene associato a un pin con:
Servo servo;
servo.attach(PIN_SERVO);
La durata dell’impulso viene impostata con:
servo.writeMicroseconds(1500);
La libreria permette anche di utilizzare write() con un angolo indicativo. In questo programma viene impiegato writeMicroseconds() perché rende esplicito il parametro realmente trasmesso al servo.
Sull’ATmega328P la libreria utilizza il timer a 16 bit. Quando il primo servo viene collegato tramite attach(), il PWM prodotto da analogWrite() sui pin associati a quel timer non è più disponibile nello stesso modo. Il programma usa il pin 8 per il segnale e non impiega analogWrite().
Collegamento del potenziometro
Il potenziometro fornisce una tensione variabile all’ingresso analogico A0:
- un estremo a 5 V;
- l’altro estremo a GND;
- il cursore ad A0.
Ruotando l’albero, la tensione sul cursore varia tra i due estremi. Il convertitore ADC dell’ATmega328P restituisce normalmente un valore compreso tra 0 e 1023.
Alimentazione del servomotore
Il filo di segnale del servo viene collegato al pin digitale 8. Non è necessario utilizzare uno dei pin contrassegnati con il simbolo PWM, perché gli impulsi vengono generati dalla libreria Servo tramite timer.
Il servomotore non deve essere alimentato da un pin digitale. Anche un piccolo servo può richiedere picchi di corrente capaci di provocare cadute di tensione, riavvii del microcontrollore e disturbi.
È opportuno utilizzare un’alimentazione a 5 V adeguata al servo. Il negativo dell’alimentatore deve essere collegato al GND dell’ATmega328P, in modo che il segnale abbia lo stesso riferimento elettrico.
Che cosa fa il programma
Il programma:
- legge il potenziometro collegato ad A0;
- calcola una media mobile su otto campioni;
- converte il valore filtrato in un impulso compreso tra 1000 e 2000 µs;
- aggiorna il riferimento del servo ogni 20 ms;
- evita piccoli aggiornamenti inferiori a 4 µs per ridurre il movimento causato dal rumore;
- stampa periodicamente valore ADC, impulso e posizione indicativa;
- non utilizza
delay().
Ruotando il potenziometro, il servomotore si sposta tra le posizioni corrispondenti agli impulsi minimo e massimo configurati.
Filtro a media mobile
La lettura analogica può variare di alcuni conteggi anche quando il potenziometro rimane fermo. Senza filtro queste variazioni possono produrre piccoli movimenti continui del servo.
Il programma conserva gli ultimi otto campioni. A ogni nuova lettura:
- sottrae dalla somma il campione più vecchio;
- inserisce la nuova lettura;
- aggiunge il nuovo valore alla somma;
- divide la somma per otto.
Questo metodo aggiorna la media senza sommare ogni volta l’intero vettore.
Conversione da ADC a microsecondi
Il valore ADC compreso tra 0 e 1023 viene trasformato nell’intervallo 1000–2000 µs:
impulso = 1000
+ valoreADC × (2000 - 1000) / 1023
La conversione utilizza un calcolo intero a 32 bit per evitare la perdita del risultato intermedio.
I limiti 1000 e 2000 µs sono prudenziali e adatti a molti servi, ma non costituiscono una garanzia universale. Forzare un servo oltre i limiti meccanici può provocare vibrazioni, aumento della corrente e danneggiamento degli ingranaggi.
Temporizzazioni indipendenti
Il programma gestisce tre intervalli:
- campionamento del potenziometro ogni 5 ms;
- aggiornamento del comando del servo ogni 20 ms;
- stampa sul monitor seriale ogni 250 ms.
Ogni funzione conserva separatamente il momento della propria ultima esecuzione. Il loop() rimane libero e non viene bloccato da attese.
Programma completo
Download: pwm.ino
/*
PWM e comando di un servomotore RC.
Hardware:
- potenziometro da 10 kOhm:
estremo 1 -> 5 V
cursore -> A0
estremo 2 -> GND
- servomotore RC:
segnale -> pin 8
positivo -> alimentazione 5 V adeguata
GND -> massa dell'alimentatore
La massa dell'alimentatore del servo deve essere collegata
alla massa dell'ATmega328P / UNO R3.
Il programma:
- legge il potenziometro;
- filtra le letture con una media mobile;
- converte il valore ADC in un impulso da 1000 a 2000 us;
- aggiorna il comando del servo senza usare delay().
*/
#include <Servo.h>
const uint8_t PIN_POTENZIOMETRO = A0;
const uint8_t PIN_SERVO = 8;
const uint8_t NUMERO_CAMPIONI = 8;
const unsigned long INTERVALLO_CAMPIONAMENTO_MS = 5UL;
const unsigned long INTERVALLO_SERVO_MS = 20UL;
const unsigned long INTERVALLO_STAMPA_MS = 250UL;
const uint16_t IMPULSO_MIN_US = 1000;
const uint16_t IMPULSO_CENTRO_US = 1500;
const uint16_t IMPULSO_MAX_US = 2000;
const uint8_t SOGLIA_AGGIORNAMENTO_US = 4;
Servo servo;
uint16_t campioni[NUMERO_CAMPIONI];
uint32_t sommaCampioni = 0;
uint8_t indiceCampione = 0;
uint16_t valoreFiltrato = 0;
uint16_t ultimoImpulsoUs = IMPULSO_CENTRO_US;
unsigned long ultimoCampionamentoMs = 0;
unsigned long ultimoAggiornamentoServoMs = 0;
unsigned long ultimaStampaMs = 0;
uint16_t convertiInImpulso(uint16_t valoreAdc)
{
uint32_t intervalloUs =
IMPULSO_MAX_US - IMPULSO_MIN_US;
return IMPULSO_MIN_US
+ (
static_cast<uint32_t>(valoreAdc)
* intervalloUs
) / 1023UL;
}
uint8_t convertiInAngoloIndicativo(
uint16_t impulsoUs
)
{
uint32_t posizione =
static_cast<uint32_t>(
impulsoUs - IMPULSO_MIN_US
) * 180UL;
return posizione
/ (IMPULSO_MAX_US - IMPULSO_MIN_US);
}
void inizializzaFiltro()
{
uint16_t primaLettura =
analogRead(PIN_POTENZIOMETRO);
sommaCampioni = 0;
for (
uint8_t i = 0;
i < NUMERO_CAMPIONI;
i++
)
{
campioni[i] = primaLettura;
sommaCampioni += primaLettura;
}
valoreFiltrato = primaLettura;
}
void acquisisciCampione()
{
uint16_t nuovaLettura =
analogRead(PIN_POTENZIOMETRO);
sommaCampioni -= campioni[indiceCampione];
campioni[indiceCampione] = nuovaLettura;
sommaCampioni += nuovaLettura;
indiceCampione++;
if (indiceCampione >= NUMERO_CAMPIONI)
{
indiceCampione = 0;
}
valoreFiltrato =
sommaCampioni / NUMERO_CAMPIONI;
}
void aggiornaServo()
{
uint16_t nuovoImpulsoUs =
convertiInImpulso(valoreFiltrato);
int16_t differenza =
static_cast<int16_t>(nuovoImpulsoUs)
- static_cast<int16_t>(ultimoImpulsoUs);
if (differenza < 0)
{
differenza = -differenza;
}
if (
differenza >= SOGLIA_AGGIORNAMENTO_US
)
{
servo.writeMicroseconds(nuovoImpulsoUs);
ultimoImpulsoUs = nuovoImpulsoUs;
}
}
void stampaDati()
{
Serial.print(F("ADC filtrato: "));
Serial.print(valoreFiltrato);
Serial.print(F(" | impulso: "));
Serial.print(ultimoImpulsoUs);
Serial.print(F(" us"));
Serial.print(F(" | posizione indicativa: "));
Serial.print(
convertiInAngoloIndicativo(
ultimoImpulsoUs
)
);
Serial.println(F(" gradi"));
}
void setup()
{
Serial.begin(115200);
inizializzaFiltro();
servo.attach(PIN_SERVO);
servo.writeMicroseconds(
IMPULSO_CENTRO_US
);
ultimoImpulsoUs = IMPULSO_CENTRO_US;
unsigned long tempoCorrenteMs = millis();
ultimoCampionamentoMs = tempoCorrenteMs;
ultimoAggiornamentoServoMs = tempoCorrenteMs;
ultimaStampaMs = tempoCorrenteMs;
Serial.println(
F("Controllo servomotore avviato")
);
}
void loop()
{
unsigned long tempoCorrenteMs = millis();
if (
tempoCorrenteMs - ultimoCampionamentoMs
>= INTERVALLO_CAMPIONAMENTO_MS
)
{
ultimoCampionamentoMs =
tempoCorrenteMs;
acquisisciCampione();
}
if (
tempoCorrenteMs
- ultimoAggiornamentoServoMs
>= INTERVALLO_SERVO_MS
)
{
ultimoAggiornamentoServoMs =
tempoCorrenteMs;
aggiornaServo();
}
if (
tempoCorrenteMs - ultimaStampaMs
>= INTERVALLO_STAMPA_MS
)
{
ultimaStampaMs = tempoCorrenteMs;
stampaDati();
}
}
Errori comuni
Il servo provoca il riavvio della scheda
L’alimentazione non fornisce corrente sufficiente oppure la caduta di tensione disturba il microcontrollore. Utilizzare un’alimentazione adeguata e collegare le masse.
Il servo vibra anche con il potenziometro fermo
Le cause possono essere rumore sulla lettura analogica, alimentazione instabile, collegamenti lunghi, potenziometro usurato oppure giochi meccanici. Il filtro e la soglia del programma riducono ma non eliminano problemi elettrici o meccanici.
Il servo raggiunge il fermo meccanico e continua a forzare
L’intervallo degli impulsi è troppo ampio per quel modello. Aumentare il valore minimo oppure ridurre il valore massimo fino a eliminare lo sforzo agli estremi.
Il servo non segue tutta la corsa del potenziometro
Il servo può avere un’escursione inferiore a 180 gradi oppure utilizzare limiti di impulso differenti. L’indicazione in gradi stampata dal programma è una conversione lineare di riferimento, non una misura reale dell’angolo.
Il servo si muove senza utilizzare un pin PWM
È normale. La libreria Servo genera gli impulsi mediante un timer e può utilizzare il pin digitale 8. Non sta chiamando analogWrite().
Conclusioni
Il PWM è una sequenza digitale nella quale la durata dello stato alto viene controllata rispetto al periodo. Non produce automaticamente una tensione analogica continua.
Nel controllo di potenza il parametro principale è normalmente il duty cycle. Nel servomotore RC, invece, il circuito interno interpreta la larghezza dell’impulso come riferimento di posizione.
Il programma legge un potenziometro, filtra il dato, lo converte in microsecondi e aggiorna il servo senza bloccare l’esecuzione. Il collegamento richiede particolare attenzione all’alimentazione e alla massa comune.
Il servomotore mostra quindi un uso specifico della modulazione temporale: il segnale non alimenta direttamente il carico, ma codifica un comando mediante la durata dell’impulso.
Fonti principali
Microchip Technology — ATmega328P: 8-bit AVR Microcontroller with 32K Bytes In-System Programmable Flash, datasheet.
Arduino — documentazione ufficiale della libreria Servo.
Arduino Libraries — codice sorgente ufficiale di Servo.h, definizioni di writeMicroseconds(), intervallo di aggiornamento e utilizzo dei timer.
Documentazione tecnica del servomotore effettivamente utilizzato per tensione, corrente, intervallo degli impulsi e limiti meccanici.