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:
![i2c-01]()
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:
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:
![i2c-02]()
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)
Il chip esp32 ci consente, tramite la matrice IO_MUX, di assegnare ai due controller I2C “quasi” tutti i suoi pin. Il driver I2C è in grado di verificare se i pin specificati in fase di configurazione del controller siano utilizzabili o meno e di segnalarcelo con un errore.
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:
![i2c-04]()
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.