Raspberry Pi Pico – Utilizzare il display OLED con MicroPython

Scrivo questo breve articolo per rispondere ad un paio di utenti che su Instagram mi hanno chiesto come ho fatto a far funzionare il display OLED dato che loro non c’erano riusciti.

Nel repository Github del Raspberry Pi Pico, infatti, sono presenti due esempi per poter utilizzare display OLED basati sui controllers SSD1306 e SH1106. Questi due esempi normalmente non funzionano così da soli perchè è necessario installare, direttamente sul Raspberry Pi Pico, le librerie apposite che di default non sono contenute nell’interprete MicroPython che abbiamo installato all’inizio. E’ una cosa normale ma, da parte mia, dico che almeno sul Repository Github questa nota dovevano metterla, perchè se si realizza un qualcosa per i principianti, è sempre bene dare tutte le informazioni.

Per questo esempio, a parte la Raspberry Pi Pico (che potete acquistare su Melopero o su Pimoroni) ho utilizzato:

Sulla breadboard a me piace utilizzare i kit di jumper già pronti (Amazon | Futura Elettronica), perchè passata una certa età non ho più la pazienza+vista di una volta per mettermi a fare i pezzetti di fili. In più dovrete saldare gli header maschio con spaziatura da 2.54mm sul Raspberry Pi Pico. Basta una sola striscia da 40pin dato che il Pico ha due file da 20; su Amazon si trovano delle confezioni varie, anche misti maschio femmina. Futura Elettronica invece ha anche gli strip da 20 pin, più economici, in questo caso ne potete acquistare solo due.

In molti anzichè saldare gli header maschio rivolti al di sotto (cioè per mettere la Raspberry Pi Pico su una breadboard), preferiscono saldare, invece, degli header femmina rivolti verso l’alto. In questo caso non possiamo più utilizzare la scheda su una breadboard ma saranno necessari dei cavetti Jumper per collegare i GPIO all’esterno.

Collegamento display

Il pin Vcc del display oled va messo sulla linea dei 5V, che preleveremo dal pin VBus (pin n°40) del Raspberry Pi Pico (5V presa dalla porta USB). GND è chiaramente in comune (sul Raspberry Pi Pico i vari pin di GND hanno la piazzola quadrata anzichè tonda). Il pin SDA del display va collegato direttamente sul GPIO8 (pin n°11) e SCL (alcuni display hanno scritto invece SCK) sul pin GPIO9 (pin n°12): questi due pin del Raspberry Pi Pico sono quelli utilizzati dal modulo I2C numero 0.

Non vanno messe resistenze di pull-up sulle linee I2C dato che sono già integrate sul PCB del display. In aggiunta non c’è bisogno di adattamenti di tensione (ricordo che i pin del Raspberry Pi Pico NON sono 5V-tolerant) in quanto le resistenze di pull-up del display sono collegate verso la 3.3V generata sul display stesso.

Installare librerie MicroPython sul Raspberry Pi Pico

Per le librerie ci viene in aiuto il PyPI (Python Package Index) : un repository dedicato esclusivamente al Python (e che quindi include anche materiale in MicroPython e in CircuitPython). Si possono installare le librerie, prese dal PyPI, da Thonny andando in Tools Manage Packages

Nella nuova finestra che si apre, cerchiamo la stringa ssd1306:

Si presenteranno una serie di risultati:

Come dicevo sopra, qui verranno fuori tutti i risultati, anche relativi a schede e varianti di python diverse da quelle che stiamo utilizzando e quindi non subito utilizzabili. Per essere sicuri di fare il minor sforzo possibile, nell’elenco dei risultati andremo sempre a cercare le librerie in cui è esplicitamente dichiarato che sono state realizzate per MicroPython. Nel nostro caso specifico, il primo risultato, per me, è stato quello corretto: è difatti specificato: ssd1306 module for MicroPython. Clicchiamoci sopra, compare questa descrizione:

Premiamo sul pulsante Install in basso a sinistra. Compare una piccola finestra che illustra il progresso di installazione. Alla fine, nel riquadro bianco a sinistra (quello in cui in cima c’è scritto <INSTALL>) compare anche la nostra libreria, appena installata. In questo riquadro compariranno tutte le librerie che abbiamo installato sul nostro Raspberry Pi Pico, clicchiamo sul nome ssd1306:

Vediamo che a destra compaiono delle informazioni. Vediamo in particolare che la libreria, composta da un unico file dal nome ssd1306.py è stata installata nella cartella /lib.

Alcune librerie sono composte da più files, a volte anche organizzati in sottocartelle: vediamo che affianco ai nomi dei files compaiono anche dei segni di spunta (anche in questo caso vediamo che c’è il segno di spunta affianco all’unico file ssd1306.py). Abbiamo infatti la possibilità, quando ci sono molti files che non ci servono (gli esempi o varianti delle librerie per dispositivi specifici) di cancellare solo le parti che non interessano: si lascia il segno di spunta su ciò che vogliamo eliminare e si preme il pulsante in basso Delete Selected.

Premiamo il pulsante Close in basso a destra. Per pura curiosità, ora premiamo il pulsante apri:

Clicchiamo quindi su Raspberry Pi Pico:

Vediamo che nella radice ora è presente una cartella lib

Andando a fare doppio click su questa vediamo che li dentro c’è la libreria che abbiamo appena installato:

Capiamo, quindi, che le librerie vengono copiate tal quali direttamente sul Raspberry Pi Pico: con il MicroPython, ricordo, non c’è compilazione: si tratta di un linguaggio di Scripting, quindi è interpretato direttamente, per cui anche le librerie seguono lo stesso meccanismo.

Premiamo il tasto Close per ritornare all’editor.

Utilizzare la libreria

Copiamo il sorgente per il display SSD1306 (quello con le impostazioni di default), preso dal Repository Github del Raspberry Pi Pico:

# Display Image & text on I2C driven ssd1306 OLED display 
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import framebuf
 
WIDTH  = 128                                            # oled display width
HEIGHT = 32                                             # oled display height
 
i2c = I2C(0)                                            # Init I2C using I2C0 defaults, SCL=Pin(GP9), SDA=Pin(GP8), freq=400000
print("I2C Address      : "+hex(i2c.scan()[0]).upper()) # Display device address
print("I2C Configuration: "+str(i2c))                   # Display I2C config
 
 
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)                  # Init oled display
 
# Raspberry Pi logo as 32x32 bytearray
buffer = bytearray(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
 
# Load the raspberry pi logo into the framebuffer (the image is 32x32)
fb = framebuf.FrameBuffer(buffer, 32, 32, framebuf.MONO_HLSB)
 
# Clear the oled display in case it has junk on it.
oled.fill(0)
 
# Blit the image from the framebuffer to the oled display
oled.blit(fb, 96, 0)
 
# Add some text
oled.text("Raspberry Pi",5,5)
oled.text("Pico",5,15)
 
# Finally update the oled display so the image & text is displayed
oled.show()

Incolliamolo nella finestra dell’editor di Thonny:

Premiamo il tasto Play verde (Run Current Script (F5)). Ci viene chiesto dove salvare il file, scegliamo chiaramente Raspberry Pi Pico e diamo il nome oled.py (importantissimo: non dimenticate di mettere .py alla fine! Io faccio sempre questo errore perchè sono abituato male con i software che la aggiungono in automatico).

Nella finestra Shell compare:

La prima riga, I2C Address : 0x3C è generata dalla riga 10 dello script. A riga 9 viene creata un’istanza dell’oggetto i2C a partire dalla libreria I2C (presente di default nell’interprete MicroPython di Raspberry Pi Pico) dicendo che questa istanza deve utilizzare il modulo I2C numero 0, ovvero quello posizionato di default sui GP9 (SCL) e GP8 (SDA). La riga 10 dello script esegue una scansione del bus I2C, in questo caso la libreria I2C rileva un dispositivo correttamente collegato, avente indirizzo ox3C.

La riga successiva, nella shell, restituisce i parametri con i quali è stato configurato il bus I2C: questi sono i parametri di default, l’altro esempio presente su Github mostra come impostare la frequenza. I pin di default per il modulo 0 I2C sono GP8 (SDA) e GP9 (SCL), altrimenti è possibile ridirezionarli su GP20 (SDA) e GP21 (SCL).

Nelle righe 6 e 7 dello script sono impostate le dimensioni, in pixel, del display utilizzato. La riga 14, infine, crea l’oggetto OLED specificando le dimensioni del display e l’oggetto I2C da utilizzare: qui avremmo potuto specificare, ad esempio, un’eventuale oggetto I2c emulato via software. Vediamo che non è specificato l’indirizzo I2C perchè viene utilizzato quello di default che è proprio 0x3C, se abbiamo necessità di cambiarlo perchè il nostro display non utilizza quell’indirizzo (i display OLED 128×64 della Adafruit utilizzano di default ox3D), possiamo fornire l’indirizzo come quarto parametro.

La riga 17 contiene l’immagine definita come bytearray. Vedremo dopo come utilizzare un sistema migliore per caricare immagini e spiegherò il significato di alcune funzioni.

Prima per poter importare la libreria da uno script, dato che si trova nella cartella lib, dovevo scrivere import lib.ssd1306, adesso pare che di default l’interprete cerca i moduli nella cartella lib senza specificarlo.

Le funzioni per scrivere il testo (oled.text) accettano 3 parametri: la stringa da scrivere, coordinata X del punto in cui cominciare a scrivere e coordinata Y.

Il metodo show trasferisce l’immagine dalla RAM al pannello del display. Per chi non è avvezzo con queste librerie: le immagini vengono create nella RAM (il frame buffer) quindi non direttamente sul display. Dopo aver creato tutto (immagini, scritte ecc) si trasferisce il contenuto dalla RAM al pannello del display col metodo show.

Una libreria troppo semplice

Se andiamo ad aprire l’unico file che compone questa libreria del display, notiamo una cosa davvero strana: si tratta di pochissime righe di codice e addirittura il font, così come le funzioni grafiche non sono definite da nessuna parte… quindi come è possibile che del testo venga mostrato sul display se da nessuna parte sto definendo come fare?

La risposta è molto semplice e per niente ovvia a chi non è abituato ad utilizzare il Python. L’interprete MicroPython contiene un modulo chiamato Framebuf (frame buffer), che è descritto qui.

La libreria per il display è costruita al di sopra di Framebuf, che contiene già la definizione di un font standard di 8×8 pixel e le primitive per il disegno: punti, linee, rettangoli vuoti, rettangoli pieni, riempimenti, testo, scrolling e blit (cos’è il blit? vedremo dopo).

Le immagini vengono create in memoria ram utilizzando le funzioni di questo modulo e quindi trasferite sull’oled utilizzando il metodo show della libreria oled.

Creare immagini da mostrare sul display OLED

Come dicevo sopra, possiamo utilizzare un sistema migliore per caricare le immagini. Possiamo caricare delle immagini in formato PBM (Portable BitMap) direttamente sulla memoria del Raspberry Pi Pico e mostrarle. Per fare questo, la cosa migliore è installare The GimpThe Gimp è un programma gratuito per la manipolazione delle immagini. In questo programma prepareremo la nostra immagine. Possiamo disegnare a mano o aprire un’immagine e modificarla. Dobbiamo solo tenere conto della grandezza del display: per il display 128×32, non possiamo chiaramente caricare immagini che siano più alte di 32 pixel e più larghe di 128. Per il mio esempio caricherò un’immagine da 32×32 pixel del logo di Settorezero:

Nel caso in cui si carichino immagini più grandi, da GIMP è possibile ridimensionarle utilizzando la funzione Immagine Scala Immagine

Bisogna innanzitutto convertire l’immagine ad un bit, in modo che i pixel abbiano soltanto 2 colori: bianco e nero. In realtà per l’immagine che vi sto fornendo come esempio, non c’è bisogno perchè l’ho disegnata a mano già con questa caratteristica, ma vi faccio ugualmente vedere il procedimento in caso vorreste convertire un’immagine a colori o in toni di grigio.

Selezioniamo Immagine > ModalitàIndicizzata

Dalla finestra che compare bisogna spuntare l’opzione Usa una tavolozza in bianco e nero (1bit):

Nella sezione Dithering di colore, con l’immagine mia, che era già ad 1 bit, comparirà nessuno, con immagini a colori dobbiamo fare delle prove per vedere quale delle opzioni possibili fornisce i risultati migliori. Premiamo quindi il pulsante Converti.

Salviamo quindi il File. In GIMP, a meno che non vogliamo salvare con l’estensione di default di GIMP, non si utilizza la funzione Salva, bensì Esporta. Clicchiamo quindi su File Esporta come

Dalla finestra che compare, visualizziamo nella parte inferiore questo menù (clicchiamo su Seleziona tipo di file):

Scorriamo la finestra fino a trovare la voce Immagine PBM (è più facile cercare pbm nella colonna estensioni):

Selezioniamo questa riga, in cima, nel campo nome scriviamo semplicemente logo e quindi in basso scegliamo la cartella del computer in cui salvare. Clicchiamo quindi il pulsante Esporta in basso a destra. Si presenta questa finestra:

Lasciamo selezionato Raw e premiamo Esporta. Chiudiamo infine GIMP. Adesso bisogna caricare il file appena creato direttamente nella memoria del Raspberry Pi Pico. Thonny offre una funzione visuale per poter fare questa operazione.

Upload di files da Thonny sul Raspberry Pi Pico

Da Thonny clicchiamo su ViewFiles. Compaiono due finestre sulla sinistra: quella in cima mostra il contenuto del PC e quella sottostante il contenuto del Raspberry Pi Pico. Potete ridimensionare le due finestre trascinando la barra che separa le due zone:

Nella zona di sopra, quella del pc, cerchiamo il file pbm che abbiamo salvato da GIMP, clicchiamoci sopra col tasto destro del mouse e selezioniamo Upload to/

Il file viene quindi caricato sul Raspberry Pi Pico e diventa subito visibile nella finestra in basso:

Incominciate quindi a capire la potenza di Thonny?

Mostrare l’immagine in formato PBM sul display OLED

Per leggere il file in formato PBM utilizzeremo le funzioni di filesystem e la libreria framebuf. Per prima cosa è necessario importare appunto il Framebuffer scrivendo in cima al codice:

import framebuf

Adesso, andremo a leggere il contenuto del file PBM utilizzando il filesystem:

with open('logo.pbm', 'rb') as imgfile:
    for i in range(3): 
        imgfile.readline() 
    data = bytearray(imgfile.read())

I dati dell’immagine vengono memorizzati nell’array data mediante la funzione python bytearray che riceve i dati da convertire in binario dal metodo read. L’iterazione di 3 imgfile.readline serve unicamente per saltare le prime 3 righe del file, che contengono descrittori che non ci interessano. Se apriamo il file pbm con un editor di testo possiamo infatti notare questo:

La prima riga del file PBM contiene P4, è un’identificativo utilizzato soltanto dai programmi di grafica: è per dire che il file PBM è in formato raw a 1 bit (solo due colori), la seconda riga serve ad eventuali annotazioni (in questo caso è stata inserita da GIMP), e la terza riga contiene larghezza e altezza immagine. La quarta riga contiene quindi i bytes che compongono l’immagine.

Mettendo quelle 3  letture, di fatto inutilizzate, non abbiamo fatto altro che  spostare il puntatore alla quarta riga in cui sono contenuti i dati.

I dati vengono quindi trasferiti nel framebuffer con:

imagebuffer = framebuf.FrameBuffer(data, 32, 32, framebuf.MONO_HLSB)

Se i primi parametri passati al metodo FrameBuffer possono essere chiari (puntatore all’array che contiene i dati, larghezza immagine, altezza immagine), il quarto magari è meno chiaro: è un parametro che specifica come è fatta l’immagine (sulla documentazione del MicroPython è spiegato bene). 

Infine trasferiamo il buffer immagine appena creato dentro il buffer del display utilizzando il metodo blit della libreria framebuffer:

oled.blit(imagebuffer, 0, 0)

Blit sta per BLock Transfer (la i è aggiunta probabilmente per facilitare la lettura). In realtà si dovrebbe dire BIT BLIT (trasferimento di blocchi di bit). Chi utilizzava gli Atari ST negli anni ’80 e ’90 sa bene che a bordo della scheda madre era presente un coprocessore grafico, il BLITTER, che aveva il compito di trasferire dati da un blocco di RAM ad un altro senza l’intervento del processore, quindi in modo molto rapido. Un po’ quello che oggi fanno i moduli DMA (Direct Memory Access).  I due parametri che seguono (,0 ,0) servono per specificare le coordinate x, y (base 0) del punto del framebuffer in cui copiare l’immagine.

Il codice completo del mio esempio è il seguente:

# Esempio personalizzato logo settorezero
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import framebuf
from utime import sleep
 
WIDTH  = 128
HEIGHT = 32
 
i2c = I2C(0)                                            # Init I2C using I2C0 defaults, SCL=Pin(GP9), SDA=Pin(GP8), freq=400000
print("I2C Address      : "+hex(i2c.scan()[0]).upper()) # Display device address
print("I2C Configuration: "+str(i2c))                   # Display I2C config
 
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)                  # Init oled display
 
# immagine in formato pbm (32x32) caricata sul Raspberry Pi Pico
# tramite Thonny
with open('logo.pbm', 'rb') as imgfile:
    for i in range(3):    
        imgfile.readline() 
    data = bytearray(imgfile.read()) # file => array
 
# array => framebuffer
imagebuffer = framebuf.FrameBuffer(data, 32, 32, framebuf.MONO_HLSB)
 
# pulisce il display
oled.fill(0)
 
# buffer immagine => framebuffer display oled
oled.blit(imagebuffer, 96, 0)
 
# aggiungo del testo
oled.text("SETTOREZERO",0,0)
oled.text("Play",0,8)
oled.text("Embedded",0,16)
oled.text("Electronics",0,24)
 
# framebuffer display => pannello display
oled.show()
 
# faccio lampeggiare il display
while True:
    sleep(1)
    oled.invert(1)
    sleep(1)
    oled.invert(0)

In questo video è mostrato cosa fa questo esempio:

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 :)