Raspberry Pi Pico : Gestione del PWM in MicroPython

In questo articolo vediamo come è gestito il PWM sul Raspberry Pi Pico, soprattutto in MicroPython, ma anche in C e faremo dei confronti su cosa succede ad utilizzare questa periferica con l’uno o con l’altro linguaggio. Il PWM è molto importante perchè è una funzionalità che ci permette, ad esempio, di controllare la velocità di motori DC a spazzole, luminosità dei led, ma anche per generare segnali analogici variabili nel tempo e per controllare tutta una serie di dispositivi che necessitano di segnali ad onda quadra di frequenza e/o duty cycle variabile. In questo articolo non spiegherò le basi del PWM perchè è un argomento di cui ho parlato tante volte qui sul blog, ma se proprio siete a digiuno e volete saperne di più, vi consiglio di leggere questi due PDF scritti da due cari amici:

Ringraziamenti

Ringrazio Alessandro Grande, Ecosystem Manager della ARM, nonchè la ARM stessa, per avermi regalato una Raspberry Pi Pico. Le opinioni qui riportate sono del tutto personali e non influenzate in alcun modo dal fornitore del prodotto.

Un PWM “a fette”

L’RP2040, il microcontrollore a bordo del Raspberry Pi Pico, dispone di 8 Slices (io direi Moduli ma è così, Slices, che vengono chiamati sul datasheet) PWM: ovvero 8 unità separate che possono funzionare sia per generare un segnale PWM (uscita) sia come ingresso per misurare valori di Frequenza o di duty cycle di segnali provenienti dall’esterno. In questo articolo mi focalizzerò soltanto sulla funzionalità PWM ovvero la capacità di generare segnali (onde quadre) a determinati valori di frequenza e duty cycle (rapporto tra la durata dell’impulso a livello alto e durata del livello basso).

Ogni Slice è indipendente dagli altri, per cui vuol dire che possiamo impostare la frequenza di uno Slice senza che vengano influenzati gli altri. In aggiunta ogni Slice, in modalità PWM,  può controllare uno o due pin con la possibilità di variare il Duty Cycle dei due pin in maniera indipendente.

Possiamo, quindi, disporre di 16 canali PWM con frequenze diverse due a due.

Sul Datasheet dell’RP2040, a pag. 544, c’è una tabella in cui viene indicato, per ogni pin, a quale Slice/Uscita è associato:

Interpretiamo la tabella: PWM Channel riporta il numero di Slice + la linea di uscita A o B, per cui 0A indica Slice 0 uscita A (o semplicemente: canale 0A). Vediamo che i canali PWM sono in sequenza (0A, 0B, 1A, 1B ….) a partire dal GPIO0 fino al GPIO15, dopodichè si ricomincia daccapo da GPIO16 a GPIO29: questo vuol dire che ogni canale ha la possibilità di fare sbocco su due diversi pin a scelta (ma anche su tutti e due insieme). Solo lo slice n°7 ha i pin di uscita fissi.

Sul libro ufficiale Get Started with MicroPython on Raspberry Pi Pico, a pag.101, c’è un diagramma che mostra in maniera immediata l’appartenenza di ogni pin del Raspberry Pi Pico al relativo canale PWM:

Il numero di Slice in questa immagine è riportato tra parentesi quadre nelle etichette arancioni. Prendiamo ad esempio i pin 21 (GP16) e 22 (GP17): entrambi appartengono allo stesso slice (lo [0]) e GP16 è l’uscita A e GP17 l’uscita B.

Ricordo che, una volta settato lo Slice [0] per avere un segnale PWM ad una determinata frequenza, potremo utilizzare GP16 e GP17 (ma anche uno solo dei due, oppure addirittura insieme gli altri due pin speculari GP0 e GP1) per avere un segnale PWM alla frequenza scelta e potremo anche variare il duty cycle in maniera separata per i due canali. Così GP18 e GP19, che appartengono allo slice [1], potranno avere la loro frequenza che sarà indipendente dagli altri Slice e così via.

PWM sull’ RP2040

In modalità PWM il periodo (la frequenza) viene controllata da un registro contatore, TOP, che, essendo a 16bit, può contare da 0 a 65535, e un altro registro, CC, contiene il valore con cui confrontare il contatore per invertire il livello logico in uscita dal pin PWM.

Dal Datasheet: The counting period is controlled by the TOP register, with a maximum possible period of 65536 cycles, as the counter and TOP are 16 bits in size. The input values are configured via the CC register.

Grafico superiore: in rosso il valore assunto dal registro contatore TOP, in azzurro il valore di confronto impostato nel registro CC. Grafico inferiore: segnale in uscita dal pin PWM

Dal grafico si vede anche che, normalmente, il contatore una volta raggiunto il valore impostato, torna a zero immediatamente per poi ricominciare. E’ presente un flag di correzione di fase, CSR_PH_CORRECT, che posto a 1, permette di fare in modo che il contatore, una volta raggiunto il valore di riferimento, anzichè tornare a zero immediatamente, ci torni contando al contrario (decrementando):

Qual’è l’effetto di questo settaggio? E’ quello di avere un segnale PWM simmetrico: ovvero al variare del Duty Cycle, la parte centrale del livello alto del segnale si trova allo stesso punto. Chiaramente questa impostazione ha anche un effetto secondario: a parità di tutti gli altri settaggi la frequenza del segnale viene dimezzata perchè il cambio di stato del livello alto del segnale in uscita dal pin PWM viene eseguito dopo il doppio del tempo rispetto a prima (2T), per cui volendolo utilizzare con un preciso valore di frequenza, nella formula per calcolare la frequenza, si tiene conto anche del settaggio di questo flag.

Ho parlato di questa cosa del PWM simmetrico circa 9 anni fa in un articolo relativo al PWM sui PIC24. Potete fare eventualmente riferimento a questo vecchio articolo in  cui spiego la differenza tra un segnale PWM allineato a sinistra e un segnale PWM allineato al centro (o simmetrico): ci sono anche dei video che fanno capire all’istante questo concetto riuscendo la dove le parole falliscono.

Altra cosa interessante del PWM sull’RP2040 è la possibilità di avere dei valori di Duty Cycle allo 0% (segnale costantemente a livello basso) e al 100% (segnale costantemente a livello alto) Glitch-Free ovvero senza picchi spurii come succede su altri microcontrollori. Un valore del registro CC pari a 0 produce un segnale sempre basso, un valore di CC pari a TOP+1 produce un segnale sempre alto.

Quindi potendo mettere CC al valore TOP+1 ed essendo TOP un registro a 16bit, normalmente se si vuole ottenere nel proprio programma un Duty Cycle perfettamente del 100%, TOP sarà impostato al massimo a 65534 in modo da poter mettere CC a 65535 rientrando nel limite dei 16bit.

Questa può sembrare una cosa da poco ma è importante in molte applicazioni e alcuni microcontrollori non riescono a produrre il segnale costante basso o alto per il 100% del tempo senza che siano presenti spike.

Dal Datasheet: A CC value of 0 will produce a 0% output, i.e. the output signal is always low. A CC value of TOP + 1 (i.e. equal to the period, in non-phase-correct mode) will produce a 100% output. For example, if TOP is programmed to 254, the counter will have a period of 255 cycles, and CC values in the range of 0 to 255 inclusive will produce duty cycles in the range 0% to 100% inclusive. Glitch-free output at 0% and 100% is important e.g. to avoid switching losses when a MOSFET is controlled at its minimum and maximum current levels.

Anche se in MicroPython non ci servirà, la formula per calcolare la frequenza del PWM si trova a pag.541 del Datasheet dell’RP2040:

formula [1]
Ho riportato la formula perchè ne terrò conto successivamente per alcune considerazioni.

E’ possibile anche avere le due uscite di uno stesso slice con i segnali a fase invertita per poter pilotare, ad esempio, ponti H con due ingressi per il pilotaggio in modalità LAP; per questo argomento ho scritto un altro articolo.

PWM in MicroPython

La gestione del PWM in MicroPython è ultra-semplificata e ridotta al minimo indispensabile: si imposta un pin da designare come uscita PWM e in background, dal numero di GPIO scelto, l’interprete conosce quale Slice e quale uscita deve configurare. Si scrivono quindi la frequenza del segnale e il Duty Cycle e ancora una volta l’interprete prende in mano la situazione gestendo e impostando per noi tutti i registri (tra cui i registri TOP e CC di cui parlavo prima) per fare in modo di accontentarci, o quanto meno cercare di accontentarci come vedremo dopo.

Il livello di astrazione è massimo e come sempre questo porta a due facce della medaglia: da un lato, dovendo disporre di un linguaggio di programmazione universale come il MicroPython, che utilizza cioè gli stessi costrutti qualsiasi sia il microcontrollore scelto, le funzioni devono essere necessariamente poche, semplici ed immediate senza mettere mano ai registri o utilizzare funzioni specifiche e la gestione del codice è quindi molto semplice.

In realtà questo è vero in parte dato che il MicroPython per RP2040 utilizza delle funzioni a 16bit che non ho trovato nella documentazione ufficiale del MicroPython (che forse sarà aggiornata per includere anche questo nuovo microcontrollore?). Ad esempio l’RP2040 per settare il duty utilizza il metodo duty_u16() che accetta un numero a 16bit, mentre altri dispositivi, non utilizzando un numero a 16bit per questa funzione, hanno il metodo duty che accetta un numero a 10bit (che già è strano perchè non esistono, ovviamente, formati numerici a 10bit, internamente viene comunque utilizzato un intero a 16 o 32bit). Così anche la lettura del modulo ADC, sull’RP2040 si fa con il metodo read_u16() che restituisce 0-65536 mentre gli altri microcontrollori hanno read() che restituisce 0-1023. Per il MicroPython del Raspberry Pi Pico, quindi, incontrerete dei metodi che hanno il suffisso _u16 (che sta, chiaramente, per unsigned 16bit ovvero un numero da 0 a 65535) a differenza di quelli utilizzati su altri microcontrollori.

Ogni microcontrollore ha il suo sistema per generare il PWM, ha i suoi registri, i suoi clock, i suoi pre/post-scaler che funzionano in maniera spesso unica e a noi, utenti finali, tutto quello che c’è dietro viene nascosto dal MicroPython perchè vogliamo solo scegliere il pin e avere un segnale con quella frequenza e con quel duty cycle.

Dall’altro lato, via MicroPython (e intendo il MicroPython stock, così come viene fornito di serie attualmente), non abbiamo la possibilità di gestire determinati aspetti più sottili, come appunto la correzione di fase che permette di avere il segnale simmetrico. Certo abbiamo la possibilità di includere una parte di codice in assembler per poterlo fare, per carità, ma già in C, ad esempio, non avremmo la necessità di scomodare l’assembler sebbene i passaggi per ottenere il nostro segnale PWM siano quasi il triplo rispetto all’equivalente in MicroPython e in più ci saranno da fare dei calcoli usando le formule che ho messo sopra.

Insomma: stiamo usando il MicroPython e ci accontentiamo, ma magari diamo sempre un occhio su quello che il microcontrollore può realmente fare perchè spesso una piattaforma potente viene etichettata come scadente da molti che si fermano in superficie solo perchè è il linguaggio usato che presenta delle limitazioni necessarie per la semplificazione della programmazione.

In MicroPython, per poter utilizzare il PWM quindi utilizzeremo 3 banali istruzioni:

pwm=PWM(Pin(16)) # GP16
pwm.freq(150000) # 150kHz
pwm.duty_u16(32768) # duty 50% (65535/2)

Abbiamo scelto il pin, senza curarci della questione dello slice/canale, abbiamo scelto la frequenza e il duty cycle. Stop. La classe PWM della prima riga, così come la classe Pin, deve essere importata da machine all’inizio del codice:

from machine import Pin, PWM

Ho quindi richiamato la classe dandogli semplicemente il nome pwm (minuscolo), quindi non confondetevi perchè avrei potuto chiamarlo anche pippo.

Confronto PWM tra MicroPython e C

Se prendete il codice di esempio che ho scritto sopra, che dovrebbe generare un segnale PWM con una frequenza di 150kHz (duty 50%), vedrete che all’oscilloscopio viene fuori una frequenza leggermente più alta:

L’oscilloscopio mi misura, infatti, 154.107kHz. Ho fatto quindi varie prove per capire se era possibile ottenere la frequenza precisa che volevo variando un po’ il parametro di pwm.freq() e ho notato delle cose davvero strane, che riporto in questa tabella:

pwm.freq()Frequenza all'oscilloscopio
149000148998
149100154107
149200148998
149300154107
149400154107
149500154107
149600150705
149700156006
149800151236
149900154107
149500154107

Potete notare che, a parte non riuscire ad ottenere perfettamente la frequenza che desidero (ora capite il condizionale che ho usato sopra), all’aumentare del valore che imposto in pwm.freq() non sempre mi ritrovo un aumento nella realtà, addirittura con 149700 ottengo in uscita il valore più alto (156kHz).

Questo accade perchè in MicroPython, come dicevo sopra, vengono fatte tutta un serie di semplificazioni e proporzioni che, a quanto pare, non portano a risultati lineari ma forniscono invece tutta una serie di valori con un certo margine di errore.

I più curiosi possono andarsi a vedere nel modulo machine_pwm come sono implementate le funzioni MicroPython per il PWM (ricordo che il MicroPython è scritto in C).

Ho chiesto quindi a Roberto D’Amico, che sta sperimentando pure lui con il Raspberry Pi Pico, ma in C, di buttarmi giù un pezzetto di codice per fare la stessa cosa che ho fatto nel mio esempio in MicroPython: ottenere un segnale PWM di 150kHz con duty al 50%.

Con il C già ci sono delle differenze a livello di versatilità del codice: dovendo settare alcuni registri che da MicroPython vengono “nascosti”, ci sono più modi di ottenere lo stesso valore sia cambiando il registro TOP, sia cambiando il divisore del clock utilizzato dallo Slice PWM il quale è composto da una parte intera (DIV_INT) e un parte frazionaria (DIV_FRAC). Volendo ottenere (circa) 150kHz, ci sono quindi almeno due modi:

  1. TOP=832, DIV_INT=1, DIV_FRAC=0
  2. TOP=100, DIV_INT=8, DIV_FRAC=4

Applicando le formule che illustrato sopra, prese dal datasheet, tenendo conto che non stiamo utilizzando la correzione di fase (CSR_PH_CORRECT=0) e che il Clock di sistema è 125MHz, per il caso 1 otteniamo in uscita una frequenza di 150.060kHz, per il caso 2 otteniamo 150.015kHz. Ebbene, andando a controllare con l’oscilloscopio il segnale PWM in uscita con il codice scritto in C, per il caso 1 si ottiene:

Segnale PWM in uscita con TOP=832, DIV_INT=1 e DIV_FRAC=0

Notate che la frequenza rilevata dall’oscilloscopio è precisa fino al terzo decimale! Questa cosa mi ha sbalordito, ed è incredibile come il segnale in uscita sia stabile, senza fluttuazioni.

Con i parametri del caso 2, l’altra impostazione per avere 150kHz, ottengo:

Segnale PWM in uscita con TOP=100, DIV_INT=8 e DIV_FRAC=4

Vedete che ci misuro 150.015kHz… Ancora una volta lo stesso valore che viene fuori dalla formula!

Posso quindi concludere dicendo che se dovete realizzare un generatore di funzioni, vi conviene sicuramente scriverlo in C, altrimenti col MicroPython otterrete sempre dei valori che si discostano da quelli impostati. Se invece dovete pilotare semplicemente un motore in PWM o modulare la luminosità di lampade/led, potete anche usare il MicroPython perchè il driver non si arrabbierà se la frequenza di pilotaggio è leggermente diversa da quello che si aspetta.

Se vi interessa il codice in C che ha scritto Roberto, l’ho messo nel mio repository Github, qui. Ci sono anche i due files compilati, UF2, se volete verificare pure voi con l’oscilloscopio senza aver bisogno di compilare il codice. Vi ricordo però che se avete installato l’interprete MicroPython sulla vostra Raspberry Pi Pico, e caricate questi UF2perderete l’interprete e dovrete rimetterlo daccapo (vabbè è una cosa semplicissima che ho già illustrato qui). Per gli esempi in MicroPython, invece, potete andare al paragrafo successivo.

Esempi pilotaggio Motore in PWM con MicroPython

Il motivo per cui sto facendo gli esempi con la frequenza di 150kHz è che stavo sperimentando il controllo di  motoriduttori mediante ponti H basati sul vecchio LMD18200.

Ne avevo 4 presi molti anni fa da Robot-Italy per il mio robot OR10n che portai alla Maker Faire Rome 2014 (che si svolse all’Auditorium Parco Della Musica)  e intendo riutilizzarli anche se ormai sono obsoleti, e li è specificato che si consiglia di pilotarli a 150kHz.

Per controllo in PWM dei motori è sempre buona norma utilizzare dei segnali con frequenze superiori al limite udibile dell’orecchio umano, ovvero al di sopra dei 20kHz, altrimenti sentiremo sempre dei fischi che disturbano. Per questo il PWM standard di Arduino, ad esempio, non è adatto a controllare i motori senza fare delle modifiche più a basso livello, come spiegai tempo fa in un altro articolo.

Ho quindi buttato giù alcuni codici di esempio che tirano fuori questa frequenza fissa, 150kHz, con la possibilità di variare il Duty Cycle utilizzando un potenziometro o un trimmer e quindi, di conseguenza, variare la velocità di rotazione del motore (e anche il verso se la modalità di controllo è Locked Anti Phase).

In questi esempi ho collegato anche un piccolo display OLED 128×32 allo stesso, identico, modo di come ho mostrato in un articolo precedente. Sul Display viene mostrato il valore attuale del Duty Cycle.

Esempio 1

In questo esempio leggo un potenziometro. Il potenziometro è collegato con i due poli esterni a +3.3V e GND, e il polo centrale è collegato al GP28 (pin n°34, corrispondente al canale 2 del modulo ADC). Eseguo la lettura del valore del potenziometro e la uso per regolare il Duty Cycle dell’uscita PWM sul pin n°21 (GP16, corrispondente all’uscita A dello Slice 0). L’altro pin associato allo Slice 0, GP17, viene tenuto a livello basso.

Qualcuno potrebbe obiettare che, in realtà, il riferimento negativo (GND) del potenziometro non si sarebbe dovuto trovare a GND ma piuttosto su AGND (riferimento negativo del modulo ADC) che si trova sul pin n°33: questa osservazione è corretta ma AGND e GND sono collegati insieme, quindi non cambia nulla, mi trovavo più comodo a disegnare lo schema in quel modo perchè i pin sono tutti vicini.

All’oscilloscopio vedremo questo:

Esempio 1. GP16 (canale giallo) emette segnale PWM con Duty Cycle variabile. GP17 (canale viola), che è associato allo stesso Slice, è usato come normale IO

Dal segnale dell’oscilloscopio si notano dei piccolissimi spike sul segnale di GP17 in corrispondenza dei cambi di stato del pin compagno: io credo siano dovuti al fatto che sto utilizzando una breadboard e si catturano comunque interferenze

Il codice sorgente di questo esempio è presente qui.

Ho fatto anche un breve video per far vedere come, tramite il ponte H, il circuito presente al centro dell’inquadratura, sia possibile utilizzare il PWM per regolare la velocità di rotazione di un motore a spazzole:

Vedete dal codice che eseguo 500 letture dal pin collegato al potenziometro e ne faccio la media. La lettura del modulo ADC in Python restituisce un valore a 16bit, quindi da 0 a 65535 sebbene il modulo ADC sia a 12bit: viene fatta una proporzione come spiegavo più in alto.

Un po’ a causa della proporzione fatta dal MicroPython, un po’ a causa di disturbi normalmente presenti, quando si porta il pin dell’ADC a GND non si potrà mai leggere 0, personalmente leggo un valore fluttuante tra 100 e 500 che, riportato ai 16bit che restituisce la funzione MicroPython, corrisponde ad un offset di 5 ÷ 25mV. Prendendo quindi direttamente il valore medio della lettura del modulo ADC, non lo possiamo riportare direttamente come valore da dare al Duty Cycle altrimenti non otterremmo mai una situazione in cui il Duty Cycle vale 0% (segnale sempre basso, senza spikes).

Quindi cosa faccio? Riporto il valore letto dal modulo ADC in percentuale, però rispetto a 65000 anzichè 65535: questo mi consente di ottenere una certa stabilità e avere anche un valore del 100% stabile che altrimenti non avrei mai (potete pure provare). Chiaramente dividendo per 65000 posso ottenere una percentuale superiore al 100%, quindi riporto a 100% in caso in cui si sfori.

Il valore di percentuale lo mostro quindi sul display e successivamente faccio un’altra proporzione tra la percentuale ed il valore a 16bit da assegnare al Duty Cycle: in questo modo il segnale PWM mi risulta abbastanza stabile e riesco soprattutto ad ottenere dei valori di Duty Cycle stabili dello 0% e del 100%. Tutto questo perchè sto utilizzando un potenziometro per regolare il Duty Cycle: settando invece il Duty Cycle in maniera digitale assegnando direttamente valori da 0 a 65535 in maniera continua, il problema non sussiste.

Esempio 2

Come sopra ma uso anche GP17 come uscita PWM, giusto per far vedere come scrivere il codice volendo usare due pin dello stesso Slice. Come spiegavo, appartenendo allo stesso Slice, la frequenza dei due pin sarà uguale ma posso variare il Duty Cycle in maniera indipendente per i due pin. In questo esempio utilizzo un unico potenziometro: il segnale su GP16 ha il Duty Cycle assegnato dal potenziometro, il segnale su GP17 ha il Duty Cycle pari a 100% – la percentuale assegnata al canale gemello: per cui se un’uscita ha un Duty Cycle del 20%, l’altro avrà un Duty Cycle dell’80%. Non metto lo schema perchè è uguale a quello precedente, con la differenza che da GP17 verrà fuori un segnale anzichè 0V.

Il codice sorgente è presente qui.

All’oscilloscopio vedremo due segnali:

Esempio 2. GP16 (canale giallo) emette segnale PWM con Duty Cycle variabile. GP17 (canale viola), emette segnale PWM alla stessa frequenza (in quanto facente parte dello stesso slice di GP16), con un Duty Cycle che vale 100% meno il Duty Cycle di GP16

Esempio 3

Ho aggiunto un secondo potenziometro collegato sul pin n°32 (GP27, corrispondente al canale 1 del modulo ADC) per regolare il Duty Cycle del segnale su GP17.

Il codice sorgente è presente qui.

Links

Se questo articolo ti è piaciuto, condividilo su un social:
Se l'articolo ti è piaciuto o ti è stato utile, potresti dedicare un minuto a leggere questa pagina, dove ho elencato alcune cose che potrebbero farmi contento? Grazie :)