I2C.

Scopo e principio di funzionamento.

Lo standard è stato inventato dagli ingegneri Philips, e quindi - con varie modifiche e sotto diversi nomi - è molto diffuso nei dispositivi elettronici per vari scopi. Forse la seconda interfaccia più popolare nell'ambiente Arduino dopo UART, dove viene utilizzata più spesso per scambiare dati tra controller, sensori e attuatori.
Tra gli aspetti positivi, differisce da UART per una velocità di trasferimento dati più stabile e un trasferimento dati più stabile ad alta velocità. Inoltre, grazie alla sua architettura, consente di collegare fino a 127 dispositivi contemporaneamente su un unico bus composto da due fili SDA (dati) e SCL (clock), senza l'utilizzo di apparecchiature aggiuntive, ad eccezione di due resistori di pull-up.

A differenza di UART, I2C è un protocollo di comunicazione sincrono, il che significa che lo scambio di dati avviene su un segnale di clock comune per tutti i dispositivi collegati. Un solo dispositivo Master è impegnato nella generazione del segnale. Il dispositivo "parla" e "dà la parola" agli altri, che si chiamano Slave. Tutti i dispositivi Slave hanno un numero univoco. Il Master non ha un numero, tutti lo conoscono già. I Slave tacciono, ascoltano ciò che dice il Master e rispondono solo quando il Master glielo chiede, chiamandoli per nome (numero), quindi l'ordine esemplare regna sempre nella rete.

Capiamo in termini generali come funziona il protocollo I2C, questo aiuterà ad utilizzarlo correttamente, capendo cosa succede nel profondo dei dispositivi e in che ordine. 

Leggi di più qui e qui.

S è lo START bit (la linea SDA viene forzata bassa dal Master mentre il clock SCL è a livello logico alto). Segue, quando SCL è basso il settaggio del primo bit B1 (in blu) la commutazione di SCL indica che il dato è stabile e può essere letto (verde). La stessa procedura prosegue fino all'ultimo bit Bn. La transazione termina con lo STOP bit (P) in giallo in cui SDA viene commutato da basso ad alto quando SCL è alto. 

L'interfaccia I2C è progettata per una trasmissione abbastanza veloce e affidabile su brevi distanze, solitamente all'interno dello stesso dispositivo. La lunghezza dei fili è limitata a pochi metri. Occupa una sorta di posizione intermedia tra UART e SPI in termini di velocità, affidabilità, distanza e utilizzo delle risorse.

Implementazione in Arduino.

Ogni scheda Arduino ha pin che supportano l'interfaccia i2c a livello hardware. Per UNO, Nano, Pro Mini è A4 - SDA, A5 - SCL. Mega ha SDA sul pin 20, SCL sul pin 21.
Per lavorare comodamente con le interfacce nell'ambiente Arduino, è disponibile una libreria Wire standard che crea una classe con lo stesso nome. Diamo un'occhiata ad alcune delle sue caratteristiche.

.begin(address) - avvia la classe, connetti al bus. Se l'indirizzo non è specificato, allora siamo sul Master, se è specificato, allora questo è l'indirizzo dello Slave.
.write() - trasferisce un byte o una sequenza di byte, a seconda dei parametri.
.read() - restituisce byte ricevuto.
.available() - restituisce il numero di byte ricevuti disponibili per la ricezione.

Funzioni solo per Master:
.beginTransmission(address) - inizio del trasferimento dei dati al dispositivo Slave con l'indirizzo specificato.
.endTransmission() - terminazione del trasferimento dei dati al dispositivo Slave.

Le seguenti sono due funzioni solo per il dispositivo Slave:
.onReceive(handler) - come parametro viene specificata la funzione da richiamare alla ricezione dei dati dal Master.
.onRequest(handler) - come parametro viene specificata una funzione che viene chiamata quando è necessario inviare i dati al Master.

Come si può vedere da elenco di funzioni, il funzionamento del protocollo sul Master e il funzionamento sullo Slave sono molto diversi. Per il Master, tutte le azioni vengono eseguite in modo direttivo, il comando viene dato quando è necessario. Per lo Slave, la ricezione e l'invio vengono effettuate automaticamente su richiesta del Master. Per ricevere è necessario scrivere una funzione che risponda ai comandi, per inviare è necessario avere sempre i dati pronti in anticipo. Il dispositivo Master non può mai inviare informazioni di propria iniziativa, se il Master ha bisogno di essere costantemente a conoscenza degli eventi, dovrà "calciare" regolarmente il dispositivo corrispondente per ricevere nuovi dati. In una tale organizzazione, senza dubbio, ci sono vantaggi e svantaggi.

Vediamo come questo accade nella pratica, esempi:
Come accennato in precedenza, il più delle volte il protocollo i2c viene utilizzato per comunicare il controller (come Master) con sensori e attuatori. Questo è conveniente quando è richiesta alta velocità e affidabilità dello scambio di dati. Ad esempio, indicatori, display, orologi in tempo reale, sensori di temperatura, umidità e altri parametri dell'aria e di altri ambienti, ricevitori GPS, lettori RFID e molti altri preferiscono questo metodo di comunicazione. Spesso ogni dispositivo ha una propria libreria scritta, che include tranquillamente le funzioni di protocollo sopra descritte, ma succede anche che devi occuparti del dispositivo da solo. Quindi prendiamo il foglio dati ed eseguiamo comandi basati su di esso per trasmettere e ricevere dati.
L'indirizzo del dispositivo non è sempre noto in anticipo, i produttori si dimenticano semplicemente di indicarlo, in particolare i nostri amici cinesi peccano con questo. In questo caso, utilizziamo il programma già pronto i2c_scanner disponibile nell'esempio per la libreria Wire, che calcola gli indirizzi dei dispositivi collegati e rispondenti tramite una semplice enumerazione.
Tuttavia, il protocollo può essere utilizzato con successo per lo scambio di dati tra i controller. Naturalmente, uno di loro, secondo la dottrina, sarà il Master e l'altro o gli altri, i Slave. Consideriamo alcuni esempi di tale interazione.
Prendiamo due schede Arduino e le colleghiamo secondo lo schema SDA-SDA, SCL-SCL. Certo, due resistori di pull-up da 1-10KΩ andrebbero bene, ma per test e fili corti è possibile senza queste difficoltà.
L'opzione è semplice: il Master trasmette, lo Slave riceve.

Master.
#include <Wire.h>                         // collegare la libreria.
void setup() 
     Wire.begin();                              // avviamo il bus i2c senza un indirizzo, perché è il Master.
byte x = 0; 
void loop() 
     Wire.beginTransmission(5);  // avviare il trasferimento al dispositivo numero 5. 
     Wire.write("x is ");                     // inviare una stringa di byte di testo. 
     Wire.write(x);                              // inviare un byte da una variabile.
     Wire.endTransmission();         // interrompere la trasmissione.
     x++;                                                 // aumentare il valore della variabile di 1.
     delay(500);                                   // aspetta mezzo secondo. 
}

Slave.
#include <Wire.h>                         // collegare la libreria.  
void setup() 
     Wire.begin(5);                           // avviamo il bus con un perimetro 5, questo è il numero del dispositivo.
     // associare una funzione che viene eseguita automaticamente alla ricezione dei dati.
     Wire.onReceive(receiveEvent);
     Serial.begin(9600);                  // avviare la porta seriale per monitorare il risultato nel monitor. 
void loop() 
     // il ciclo principale è vuoto.
void receiveEvent()                        // funzione, richiamata automaticamente alla ricezione dei dati.
     while (1 < Wire.available())  // se i dati ricevuti sono superiori a 1 byte.
     { 
          char c = Wire.read();         // quindi questi sono byte di testo.
          Serial.print(c);                     // visualizzarli sul monitor.
     } 
     int x = Wire.read();                // accettiamo l'ultimo byte come numero int, questi sono i dati del contatore.
     Serial.println(x);                     // visualizza sul monitor.
}

Avviamo il monitor sul dispositivo Slave e osserviamo.
Ogni mezzo secondo compare una nuova riga con un contatore incrementato, mentre il conteggio vero e proprio viene effettuato su un altro controllore.

Nell'esempio inverso, il Master richiede informazioni dallo Slave e le invia.

Master.
#include <Wire.h>                   // collegare la libreria.
void setup() 
     Wire.begin();                       // avviamo il bus i2c senza un indirizzo, perché è il Master.  
     Serial.begin(9600);             // avviare la porta seriale per monitorare il risultato nel monitor.  
void loop() 
     Wire.requestFrom(5, 5);  // richiedere 5 byte dal dispositivo numero 5.
     while (Wire.available())   // finché c'è qualcosa da leggere.
     
          char c = Wire.read();   // leggere.
          Serial.print(c);               // e visualizza sul monitor. 
     
     delay(500);                          // aspetta mezzo secondo.  
} 

Slave.
#include <Wire.h>     // collegare la libreria.  
void setup() 
     Wire.begin(5);      // avviamo il bus con un perimetro 5, questo è il numero del dispositivo.
     // associare una funzione che viene eseguita automaticamente alla ricezione dei dati.  
     Wire.onRequest(requestEvent);
void loop() 
void requestEvent()  // funzione, richiamata automaticamente alla ricezione dei dati.
{  
     // inviamo un messaggio lungo fino a 5 byte (meno è possibile, più verrà ridotto a 5).
     Wire.write("hello"); 


Sul monitor del Master, vediamo apparire due volte al secondo la stessa riga, ricevuta dallo Slave.

È possibile ricevere e inviare dati in entrambe le direzioni? Certo. Per dimostrarlo, non diventiamo troppo pigri per assemblare un circuito di due Arduino con un pulsante e un LED collegati a ciascuno di essi.
L'idea è di premere il pulsante su un dispositivo per accendere il LED sull'altro e viceversa.

Master.
#include <Wire.h>                     // libreria i2c. 
#define BUT 7                              // pulsante.
#define LED 6                              // LED 
byte but[2];                               // tracciamento dei pulsanti.

void setup() 
     pinMode(BUT, INPUT); 
     pinMode(LED, OUTPUT); 
     Wire.begin();                         // avviare il bus i2c. 
}

void loop() 
     butt();                                     // pulsanti di polling. 
     Wire.requestFrom(5, 1);   // richiedere 1 byte dallo Slave numero 5. 
     while (Wire.available())   // se ci sono dati,
     
          byte c = Wire.read();   // leggere,
           digitalWrite(LED, c);   // invia a LED. 
     } 
     delay(100);                        // un po' di riposo e chiede di nuovo.
} 

void butt() 
     static unsigned long timer; 
     if (timer + 50 > millis()) return;   //pulsante di polling dopo 50 ms (rimbalzo).
     but[0] = but[1]; 
     but[1] = digitalRead(BUT); 
     if (but[0] && !but[1]) 
    { 
         Wire.beginTransmission(5);     // invia lo stato del pulsante allo Slave numero 5. 
          Wire.write(1); // 1 
         Wire.endTransmission();          // finito.
     } 
     else if (!but[0] && but[1]) 
     { 
         Wire.beginTransmission(5);    // invia lo stato del pulsante allo Slave numero 5.
          Wire.write(0); // 0 
          Wire.endTransmission();         // finito.
     } 
     timer = millis(); 


Slave.
#include <Wire.h>      // libreria i2c 
#define BUT 7               // pulsante
#define LED 6               // LED 
byte but[2];                   // tracciamento dei pulsanti.
byte byteSendSlave;  // byte da inviare allo slave.

void setup() 
     pinMode(BUT, INPUT); 
     pinMode(LED, OUTPUT); 
     Wire.begin(5);                                    // avviare il bus i2c come slave numero 5.
     Wire.onRequest(requestEvent);   // funzione di invio. 
     Wire.onReceive(receiveEvent);     // funzione di ricezione.
}

void loop() 
     butt();    // pulsante di polling.
}

void butt() // interroghiamo i pulsanti, memorizziamo lo stato nella variabile byte byteSendSlave;  per l'invio.
     static unsigned long timer; 
     if (timer + 50 > millis()) return; 
     but[0] = but[1]; 
     but[1] = digitalRead(BUT); 
     if (but[0] && !but[1]) 
     { 
          byteSendSlave  = 1; 
     } 
     else if (!but[0] && but[1]) 
     { 
          byteSendSlave  = 0; 
     } 
}

void requestEvent()                       // funzione di invio, attivata da un segnale dal master.
     Wire.write(byteSendSlave);  // invia stato del pulsante. 


void receiveEvent() 
     while (0 < Wire.available())  // mentre ci sono dati in arrivo (1 byte per comando).
     { 
         byte c = Wire.read();          // ricevere e inviare a LED.
          digitalWrite(LED, c); 
     } 

Assembliamo il circuito.

Pertanto, il trasferimento dei dati in entrambe le direzioni viene eseguito correttamente. Lascia che ti ricordi che possono esserci fino a 127 dispositivi collegati a un bus, il che apre ottime opportunità di creatività. 

Conclusione.

Se dobbiamo collegare un gran numero di dispositivi di piccole dimensioni, utilizzando un minimo di cavi, scegliamo audacemente I2C come interfaccia affidabile, veloce e abbastanza semplice. Inoltre, non puoi farne a meno quando colleghi attivamente moduli di produttori di terze parti e ora ce ne sono la maggior parte. In ogni caso, qualsiasi maestro del fai da te che si rispetti dovrebbe conoscere il protocollo I2C ed essere in grado di usarlo. 

Crea il tuo sito web gratis! Questo sito è stato creato con Webnode. Crea il tuo sito gratuito oggi stesso! Inizia