Nel tutorial di oggi impareremo ad interfacciare il chip esp32 a dispositivi esterni (sensori, display…) utilizzando un bus molto diffuso: il bus I2C.
I2C
I2C (si pronuncia i-quadro-c) è un bus di comunicazione seriale – inventato da Philips nel 1982 – che consente a due o più dispositivi di comunicare tra loro. I dispositivi connessi al bus si dividono in master (sono i dispositivi che “gestiscono” il bus) e in slave. Normalmente un bus ha un solo master e più slave, ma sono possibili anche topologie più complesse. Ogni dispositivo slave connesso al bus deve avere un proprio indirizzo univoco.
Sono disponibili due velocità di trasmissione: standard (100Kbit/s) e fast (400Kbit/s).
Il bus I2C richiede solo due linee di connessione tra i dispositivi:
- SDA, Serial DAta – dove transitano i dati
- SCL, Serial CLock – dove il master genera il segnale di clock
Le due linee devono essere collegate ad una tensione di riferimento (Vdd) tramite resistenze di pull-up:
Per approfondire il funzionamento del bus I2C, vi consiglio l’ottimo sito www.i2c-bus.org.
esp32
Il chip esp32 offre due controller I2C, entrambi in grado di agire sia come master che come slave e di comunicare con velocità standard e fast.
I controller I2C sono collegati internamente alla matrice IO_MUX quindi, come vi ho spiegato in un precedente articolo, è possibile assegnare loro via software i diversi pin del chip (con alcune eccezioni).
Il framework esp-idf include un driver che consente di gestire tali controller ad alto livello, senza preoccuparsi di come devono essere configurati i diversi registri. Per utilizzare tale driver all’interno del proprio programma, è sufficiente includere il suo header file:
#include "driver/i2c.h" |
Per prima cosa dobbiamo procedere alla configurazione del controller (port) che vogliamo utilizzare. La configurazione avviene utilizzando il metodo i2c_param_config() a cui va passato il numero del controller da configurare e una struttura i2c_config_t che contiene i diversi parametri:
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t* i2c_conf); |
Le due possibili porte sono definite in un enum all’interno del file i2c.h:
Anche la struttura i2c_config_t è definita nel medesimo file header:
typedef struct{ i2c_mode_t mode gpio_num_t sda_io_num; gpio_pullup_t sda_pullup_en; gpio_num_t scl_io_num; gpio_pullup_t scl_pullup_en; union { struct { uint32_t clk_speed; } master; struct { uint8_t addr_10bit_en; uint16_t slave_addr; } slave; }; } i2c_config_t; |
Vediamo il significato dei diversi parametri:
- mode è la modalità di funzionamento (può essere I2C_MODE_SLAVE o I2C_MODE_MASTER)
- sda_io_num e scl_io_num specificano quali pin saranno utilizzati per i segnali di SDA e SCL
- sda_pullup_en e scl_pullup_en consentono di abilitare o disabilitare le resistenze di pullup interne (possono essere GPIO_PULLUP_DISABLE o GPIO_PULLUP_ENABLE)
- master.clk_speed indica la velocità in hertz del clock se si è scelta la modalità master (100000 se standard e 400000 se fast)
- slave.slave_addr indica l’indirizzo del dispositivo se si è scelta la modalità slave
- slave.addr_10bit_en indica se si vuole o meno utilizzare un indirizzo esteso a 10bit (se il parametro è 0 la modalità indirizzo esteso è disabilitata)
Se ad esempio vogliamo configurare il primo controller I2C in modalità master con velocità standard e utilizzare i pin 18 e 19 senza resistenze di pullpup interne scriveremo questo codice:
i2c_config_t conf; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = 18; conf.scl_io_num = 19; conf.sda_pullup_en = GPIO_PULLUP_DISABLE; conf.scl_pullup_en = GPIO_PULLUP_DISABLE; conf.master.clk_speed = 100000; i2c_param_config(I2C_NUM_0, &conf); |
Una volta configurato il controller, possiamo installare il driver con il metodo i2c_driver_install():
esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags) |
Oltre al numero del controller e alla modalità, dobbiamo specificare le dimensioni del buffer di ricezione e trasmissione (solo se in modalità slave) ed eventuali flags da usare per allocare l’interrupt (normalmente tale parametro viene lasciato a 0).
Per il controller configurato sopra, il driver sarà installato nel seguente modo:
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0) |
Master
Vediamo ora come utilizzare il controller in modalità master, per inviare comandi e leggere dati da uno slave.
Per prima cosa, dobbiamo creare un command link, ovvero un oggetto “logico” che conterrà l’elenco delle azioni da compiere per interagire con il dispositivo slave. Utilizziamo quindi il metodo i2c_cmd_link_create() che restituisce un puntatore all’handler del command link:
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); |
Abbiamo ora a disposizione diversi metodi per aggiungere al command link diverse azioni:
- i2c_master_start e i2c_master_stop
- i2c_master_write e i2c_master_write_byte
- i2c_master_read e i2c_master_read_byte
Per capire il loro significato, dobbiamo analizzare la modalità con cui il dispositivo master comunica con gli slave. Per prima cosa, il master invia sul bus il segnale di START, seguito dall’indirizzo (7bit) del dispositivo slave e da un bit che indica l’operazione richiesta (0 per WRITE, 1 per READ). Dopo ogni byte inviato (incluso il byte che rappresenta indirizzo+operazione) il dispositivo slave risponde con un bit di ACK:
Lato codice si traduce in:
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, true); |
Il metodo i2c_master_start() aggiunge all’handler cmd l’invio del segnale di START, mentre i2c_master_write_byte() invia sul bus un byte. Il byte inviato è composto dai 7bit dell’indirizzo (slave_addr) spostati (shiftati) a sinistra di 1bit (<< 1) e dal bit 0 (= IC2_MASTER_WRITE). Se avessi voluto effettuare una operazione di READ, avrei potuto usare la costante I2C_MASTER_READ.
L’ultimo parametro impostato a true indica al master di attendere che lo slave invii il bit di ACK.
Se l’operazione è write, a questo punto il master può inviare n bytes allo slave. Al termine dei dati, invia il segnale di STOP:
i2c_master_write(cmd, data_array, data_size, true); i2c_master_stop(cmd); |
Ho utilizzato il comando i2c_master_write che consente di inviare un array (uint8_t*) di dati. Il parametro data_size rappresenta la dimensione di tale array. In alternativa avrei potuto chiamare più volte il metodo i2c_master_write_byte usato in precedenza.
Per “eseguire” il command link, si utilizza il metodo i2c_master_cmd_begin():
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS); |
a cui va passato come parametro il numero del controller I2C, l’handler al command link e il numero massimo di ticks di attesa (il metodo è infatti bloccante; nell’esempio il metodo attende al massimo 1 secondo).
Infine è possibile liberare le risorse del command link con il metodo i2c_cmd_link_delete(cmd).
L’operazione di read è leggermente più complessa. Per prima cosa va inviato allo slave il comando che indica quale valore vogliamo leggere. Nel prossimo articolo vedremo una applicazione reale, per ora ipotizziamo che lo slave sia un sensore di temperatura con indirizzo 0x40 e che il comando misura la temperatura corrisponda al byte 0xF3.
L’invio del comando avviene come spiegato sopra:
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (0x40 << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, (0xF3, true); i2c_master_stop(cmd); i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS); i2c_cmd_link_delete(cmd); |
Terminato l’invio del comando (ed eventualmente atteso il tempo necessario perché il sensore lo esegua) è possibile leggere il risultato dal sensore creando un nuovo command link, sempre con l’indirizzo del sensore (ma con modalità READ) e inserendo una o più azioni di read (in base a quanti bytes il sensore ci restituirà):
uint8_t first_byte, second_byte; cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (0x40 << 1) | I2C_MASTER_READ, true); i2c_master_read_byte(cmd, &first_byte, ACK_VAL); i2c_master_read_byte(cmd, &second_byte, NACK_VAL); i2c_master_stop(cmd); i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS); i2c_cmd_link_delete(cmd); |
La differenza principale è che dopo aver letto l’ultimo byte, il master genera il segnale di NACK. Con questo segnale viene comunicato allo slave che non sono attesi ulteriori bytes e che quindi deve interrompere la trasmissione.
Le costanti per i segnali di ACK e NACK sono così definite:
#define ACK_VAL 0x0 #define NACK_VAL 0x1 |
Demo
Al termine di questo articolo vi voglio presentare un classico esempio dell’utilizzo della modalità master del bus I2C: uno scanner. Compito del programma è quello di analizzare il bus alla ricerca di eventuali dispositivi slave e di visualizzarne l’indirizzo.
Sono disponibili i sottotitoli in italiano
Il suo funzionamento è molto semplice, a voi il compito di comprendere il listato del programma su Github. Nel prossimo articolo vedremo invece come interfacciarsi ad un sensore I2C e ottenerne i dati.
Ciao Luca,
ottimo articolo, come del resto tutti gli altri. Sono curioso di leggere i prossimi, relativi all’I2C, ma anticipo un problema che mi sta tormentando e che è in parte legato al tuo esempio di scanner. Ho letto il codice che fa ovviamente il suo mestiere, lo “scanner”, cicla su tutti gli indirizzi alla ricerca di una risposta. In pratica un polling su tutti gli indirizzi in attesa di un sensore attivo. Funziona, ma oneroso in termini computazionali, e soprattutto non garantisce la pronta identificazione, qualora risulti necessaria. Quello che voglio dire è che, a parte il carico sul master legato al polling, il sensore potrebbe essere rilevato con un certo ritardo. Trattandosi della rilevazione di un sensore nella stragrande maggioranza dei casi questo non è un problema, ma se invece della rilevazione di un sensore passiamo alla lettura del suo valore, allora le cose cambiano, in quanto potrei voler reagire immediatamente ad esempio ad un cambio del valore di un potenziometro o alla pressione di un tasto. E qui vengo al mio problema che mi cruccia. Come fare in modo che il master riceva immediatamente la notifica di un valore che cambia (o nel caso della scanner che un nuovo sensore è attivo) ? Vedo due soluzioni:
– L’uso di interrupt, con linee da affiancare ai due pin I2C. Ma in questo caso come fare ? Un interrupt per sensore ? Un pin condiviso ?
– L’uso di un protocollo multi-master (I2C dovrebbe esserlo, giusto ?) nel quale un nuovo sensore si “annuncia” nell’esempio dello scanner e comunica immediatamente di aver a disposizione un nuovo valore.
Che ne pensi ? Hai affrontato questi temi ?
Grazie
Antonio
Ciao Antonio! Normalmente è sempre il master che va in polling sui dispositivi slave. Non esiste qualcosa di nativo per i2c (a differenza ad es di SMBus), se quindi il tuo sensore è i2c slave non vedo altre possibilità che l’uso “standard” che il bus ti mette a disposizione. Se stai invece sviluppando tu un dispositivo slave, potresti si pensare ad una forma di notifica “out of bus”, tipo un altro PIN che lo slave può utilizzare per dire al master che c’è un dato pronto x lui.
Ciao Luca.
Come posso testare se ho realmente letto ?
La funzione sotto ritorna sempre 0
i2c_master_cmd_begin();
No, no, scusa, cancella il messaggio, ritorna -1 quando non va.