Amo il protocollo I2C e quando ho bisogno di un sensore, ogni volta provo a trovarne uno con questo protocollo, ho anche scritto alcune librerie per vari sensori che utilizzano I2C. Quindi voglio scrivere alcuni articoli che spiegano (Arduino, Arduino SAMD MKR, esp8266 e esp32) alcune interessanti funzionalità e cercherò di spiegare come risolvere i problemi che puoi avere quando lavori con più dispositivi I2C.
In questo primo articolo vedremo come creare una rete con i nostri Arduino UNO e MEGA. Arduino ha una caratteristica interessante che può funzionare sia come Master che come Slave.
Introduzione al protocollo I2C
I2C (Inter-Integrated Circuit, eye-squared-C) in alternativa noto come I2C o IIC. È un bus di comunicazione seriale sincrono, multi-master, multi-slave, a commutazione di pacchetto, single-ended. È stato inventato nel 1982 da Philips Semiconductors. È ampiamente utilizzato per collegare circuiti integrati periferici a bassa velocità a processori e microcontrollori nelle comunicazioni a breve distanza all’interno della scheda. (cit. WiKi)
Velocità
L’I2C supporta 100 kbps, 400 kbps e 3,4 Mbps. Alcune varianti supportano anche 10 Kbps e 1 Mbps.
Modalità | Velocità massima | Capacità massima | Drive | Direzione |
---|---|---|---|---|
Modalità standard (Sm) | 100 kbit/s | 400 pF | Open drain | Bidirezionale |
Modalità veloce (Fm) | 400 kbit/s | 400 pF | Open drain | Bidirezionale |
Modalità veloce Plus (Fm+) | 1 Mbit/s | 550 pF | Open drain | Bidirezionale |
Modalità ad alta velocità (Hs) | 1.7 Mbit/s | 400 pF | Open drain | Bidirezionale |
Modalità ad alta velocità (Hs) | 3.4 Mbit/s | 100 pF | Open drain | Bidirezionale |
Modalità ultra veloce (UFm) | 5 Mbit/s | Push-pull | Unidirezionale |
Interfaccia
Come la comunicazione UART, I2C utilizza solo due fili per trasmettere dati tra dispositivi:
- SDA (Serial Data) – La linea per il master e lo slave per inviare e ricevere dati.
- SCL (Serial Clock) – La linea che trasporta il segnale di clock (segnale di clock standard tra più master e più slave).
I2C è un protocollo di comunicazione seriale, quindi i dati vengono trasferiti bit per bit lungo un unico filo (la linea SDA).
Come SPI, I2C è sincrono, quindi l’uscita dei bit è sincronizzata al campionamento dei bit da un segnale di clock condiviso tra il master e lo slave. Il master controlla sempre il segnale di clock.
Ci saranno più slave e più master e tutti i master potranno comunicare con tutti gli slave.
- Inizio: La linea SDA passa da un livello di alta tensione a un livello di bassa tensione prima che la linea SCL passi da un livello alto a basso.
- Stop: la linea SDA passa da un livello di bassa tensione a un livello di alta tensione dopo che la linea SCL è passata da un livello basso ad alto.
- Frame di indirizzo: una sequenza di 7 o 10 bit univoca per ogni slave identifica lo slave quando il master vuole parlargli.
- Bit di lettura/scrittura: un singolo bit che specifica se il master sta inviando dati allo slave (livello di bassa tensione) o richiedendo dati da esso (livello di alta tensione).
- Bit ACK/NACK: ogni frame in un messaggio è seguito da un bit di riconoscimento/non riconoscimento. Se un frame di indirizzo o un frame di dati è stato ricevuto correttamente, un bit ACK viene restituito al mittente dal dispositivo ricevente.
Connessioni dei dispositivi
Poiché I2C utilizza l’indirizzamento, più slave possono essere controllati da un unico master. È possibile collegare/indirizzare fino a 27 dispositivi slave nel circuito di interfaccia I2C. Con un indirizzo a 7 bit sono disponibili 128 (27) indirizzi univoci. L’utilizzo di indirizzi a 10 bit è raro ma fornisce 1.024 (210) indirizzi univoci.
È possibile collegare più master a un singolo slave o più slave. Il problema con più master nello stesso sistema si verifica quando due master tentano di inviare o ricevere dati contemporaneamente sulla linea SDA. Ogni master deve rilevare se la linea SDA è bassa o alta prima di trasmettere un messaggio per risolvere questo problema. Se la linea SDA è bassa, significa che un altro master ha il controllo del bus e il master deve attendere per inviare il messaggio. Se la linea SDA è alta, è possibile trasmettere il messaggio. Per collegare più master a più slave
Distanza
La stragrande maggioranza delle applicazioni utilizza I2C nel modo in cui è stato progettato inizialmente: circuiti integrati periferici collegati direttamente a un processore sulla stessa scheda a circuito stampato e quindi su distanze relativamente brevi inferiori a 30 cm (1 piede) senza connettore. Tuttavia, utilizzando un driver diverso, una versione alternativa di I2C può comunicare fino a 20 metri (possibilmente oltre 100 metri) su CAT5 o altri cavi.
How to con Arduino
Ecco i miei Arduino utilizzati negli esempi Arduino UNO - Arduino MEGA 2560 R3 - Arduino Nano - Arduino Pro Mini
Come tutte le persone sanno per usare il protocollo i 2 c, è necessaria la libreria Wire .
Ogni scheda Arduino ha hardware i2c integrato ma utilizza pin diversi.
Board | I2C / TWI pins |
---|---|
Uno, Ethernet | A4 (SDA), A5 (SCL) |
Mega2560 | 20 (SDA), 21 (SCL) |
Leonardo | 2 (SDA), 3 (SCL) |
Due | 20 (SDA), 21 (SCL), SDA1, SCL1 |
Arduino UNO aveva solo un i2c e i pin sono statici A4 (SDA) e A5 (SCL) e Wire e non può specificare altri pin, quindi la gestione è diventata semplice.
Ecco un semplice codice che legge da Wire e stampa su Serial.
#include <Wire.h>
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
}
void loop() {
Wire.requestFrom(8, 6); // request 6 bytes from slave device #8
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
}
È possibile specificare l’indirizzo come parametro della begin.
Wire.begin(0x22);
Rete
Arduino ha la possibilità di autoassegnare un indirizzo I2C in modo da poter creare una rete.
Un master, uno slave
L’esempio più comune che puoi trovare su Internet è questo.
Codice Master
Il master chiede allo slave con indirizzo 0x08, 6 byte,
Wire.requestFrom(0x08, 6); // request 6 bytes from slave device with address 0x08
e se c’è qualcosa nel buffer
while (Wire.available()) { // slave may send less than requested
Legge il byte e scrive sulla console.
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
Ecco lo sketch completo.
#include <Wire.h>
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
}
void loop() {
Wire.requestFrom(0x08, 6); // request 6 bytes from slave device with address 0x08
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
delay(500);
}
Codice Slave
Lo slave ha impostato il proprio indirizzo su 0x08
Wire.begin(0x08); // join i2c bus with address 0x08
e quando si riceve una singola richiesta,
Wire.onRequest(requestEvent); // register event
eseguire un evento che scrive 6 byte (la stringa “ciao”) nel buffer.
Wire.write("hello "); // respond with message of 6 bytes
Ecco il codice sketch completo.
#include <Wire.h>
void setup() {
Wire.begin(0x08); // join i2c bus with address 0x08
Wire.onRequest(requestEvent); // register event
}
void loop() {
delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
Wire.write("hello "); // respond with message of 6 bytes
// as expected by master
}
Ecco l’uscita seriale del master.
Connetti alla porta seriale COM5 a 9600
hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello
Inviare una parametro nella request
Se si desidera inviare un parametro al dispositivo slave, è necessario stabilire una connessione, quindi inviare i dati e avvisare che la comunicazione è terminata.
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
Lo slave deve registrare un evento per gestire questa operazione.
Wire.onReceive(receiveEvent); // register an event handler for received data
È necessario gestire la ricezione dei dati nella funzione. Ho inviato un’istruzione di lunghezza di un byte, quindi numBytes era uno, perciò devi recuperare solo le informazioni di un byte e convertirle nel formato che ti serve e salvarle nello scope principale dello sketch.
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
Ora il Master è pronto per richiedere i dati. Ha bisogno di
Wire.requestFrom(0x08, 14); // request 14 bytes from slave device 0x08
Lo slave alla richiesta fatta dal master restituisce le relative informazioni.
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("ArduinoUNO "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)43); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
Ecco lo sketch completo del master.
/**
* i2c network: send parameter to client and receive response
* with data relative to the request. MASTER SKETCH
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* Arduino UNO <------> Arduino UNO
* GND GND
* A4 A4
* A5 A5
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
while (!Serial){}
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
// Now the request
Wire.requestFrom(0x08, 14); // request 14 bytes from slave device 0x08
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_AGE); // send data to the slave
Wire.endTransmission(); // End transmission
// Now the request
Wire.requestFrom(0x08, 1); // request 1 bytes from slave device 0x08
while (Wire.available()) { // slave may send less than requested
int c = (int)Wire.read(); // receive a byte as character
Serial.println(c); // print the character
}
}
void loop() {
}
Ed ecco lo schizzo completo dello slave.
/**
* i2c network: send parameter to client and receive response
* with data relative to the request. SLAVE SKETCH
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* Arduino UNO <------> Arduino UNO
* GND GND
* A4 A4
* A5 A5
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void requestEvent();
void receiveEvent(int numBytes);
REQUEST_TYPE request = NONE;
void setup() {
Wire.begin(0x08); // join i2c bus with address #8
Serial.begin(9600); // start serial for output
while (!Serial){}
// event handler initializations
Wire.onReceive(receiveEvent); // register an event handler for received data
Wire.onRequest(requestEvent); // register an event handler for data requests
}
void loop() {
// delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("ArduinoUNO "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)43); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
L’output Seriale del master è questa.
ArduinoUNO
43
E qui, lo slave.
Received request -> 0
Received request -> 1
Un master e slaves multipli
Quando si comprende il concetto di base, le modifiche aggiuntive diventano semplici. Ora aggiungeremo un altro dispositivo che funziona come uno slave.
Qui lo sketch dello SLAVE secondario l’Arduino MEGA (o altro Arduino).
/**
* i2c network: send parameter to client and receive response
* with data relative to the request. SLAVE SKETCH 2
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* Arduino UNO <------> Arduino MEGA
* GND GND
* A4 20
* A5 21
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void requestEvent();
void receiveEvent(int numBytes);
REQUEST_TYPE request = NONE;
void setup() {
Wire.begin(0x09); // join i2c bus with address #8
Serial.begin(9600); // start serial for output
while (!Serial){}
// event handler initializations
Wire.onReceive(receiveEvent); // register an event handler for received data
Wire.onRequest(requestEvent); // register an event handler for data requests
}
void loop() {
// delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("ArduinoMEGA "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)45); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
Come puoi vedere, cambio solo l’indirizzo 0x09, il nome “ArduinoMEGA” e l’età 45
.
Nello sketch Master, aggiungiamo la nuova richiesta.
/**
* i2c network: send parameter to client and receive response
* with data relative to the request
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* Arduino UNO <------> Arduino UNO
* GND GND
* A4 A4
* A5 A5
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
while (!Serial){}
Serial.flush();
Serial.println();
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x08, 14); // request 14 bytes from slave device 0x08
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x09); // Start channel with slave 0x09
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x09, 14); // request 14 bytes from slave device 0x09
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_AGE); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x08, 1); // request 1 bytes from slave device 0x08
while (Wire.available()) { // slave may send less than requested
int c = (int)Wire.read(); // receive a byte as character
Serial.println(c); // print the character
}
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x09); // Start channel with slave 0x09
Wire.write(GET_AGE); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x09, 1); // request 1 bytes from slave device 0x09
while (Wire.available()) { // slave may send less than requested
int c = (int)Wire.read(); // receive a byte as character
Serial.println(c); // print the character
}
}
void loop() {
}
Il risultato sulla Serial del Master diventa così.
ArduinoUNO
ArduinoMEGA
43
45
Scanner di indirizzi I2C
Uno degli sketch più utili quando usi i2c è lo scanner di indirizzi. Questo semplice programma cerca di trovare tutti i dispositivi collegati al bus I2C.
#include <Wire.h>
void setup()
{
Wire.begin();
Serial.begin(9600);
Serial.println("\nI2C Scanner");
}
void loop()
{
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++ )
{
// The i2c_scanner uses the return value of
// the Write.endTransmisstion to see if
// a device did acknowledge to the address.
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print("I2C device found at address 0x");
if (address<16)
Serial.print("0");
Serial.print(address,HEX);
Serial.println(" !");
nDevices++;
}
else if (error==4)
{
Serial.print("Unknow error at address 0x");
if (address<16)
Serial.print("0");
Serial.println(address,HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
else
Serial.println("done\n");
delay(5000); // wait 5 seconds for next scan
}
La sua logica è fondamentalmente molto semplice, usa i pin SDA e SCL standard e inizia a provare a stabilire una connessione
Wire.beginTransmission(address);
error = Wire.endTransmission();
per tutti i 27 indirizzi, quando l’errore è uguale a 0 significa che hai stabilito correttamente una connessione.
Il risultato per l’Arduino master nella connessione multi slave superiore diventa così.
for all 27 address, when an error is equal to 0 mean that you successfully establish a connection.
Il risultato per il master Arduino nella connessione multi slave diventa così.
I2C Scanner
Scanning...
I2C device found at address 0x08 !
I2C device found at address 0x09 !
done
Grazie
- i2c Arduino: come creare una rete, parametri e scanner di indirizzi
- i2c Arduino SAMD MKR: interfaccia aggiuntiva SERCOM, rete e scanner di indirizzi
- i2c esp8266: how to, rete 5v, 3.3v, velocità e scanner di indirizzi