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.