Alla pari dell’SPI, analizzato in un articolo precedente, l’I2C (Inter-Integrated Circuit) è un bus di comunicazione sincrono utilizzato per la connessione e lo scambio dati tra un microprocessore e le periferiche esterne; sviluppato dalla Philips, oggi NXP, è diventato uno standard “de facto”.
Descrizione del bus
l’I2C è anche noto come bus two-wire in quanto è caratterizzato a tutti gli effetti da due soli “fili” :
Le linee suddette sono sempre caratterizzate da una resistenza di pull-up che ha il compito di mantenere il segnale “alto” (1 logico) in condizioni di idle mentre i componenti interconnessi (master e slave) hanno il compito di abbassarne il livello per trasferire uno 0 logico e di rilasciarlo per riportarlo in idle e trasferire un 1 logico; questo comportamento è tipico delle linee open-drain.
Analogamente all’SPI, è possibile avere più slave connessi al bus ed un unico master con cui comunicare; la differenza principale è che non esiste un segnale di SS (Slave Select) ma il master seleziona lo slave con cui comunicare attraverso un indirizzamento. Infatti, il master trasmette l’indirizzo dello slave sulla linea SDA prima di iniziare il trasferimento dei dati veri e propri; tale indirizzo è tipicamente a 7 bit (fino a 128 slave) ma è prevista un’estensione fino a 10 bit (fino a 1024 slave).
Una caratteristiche fondamentale dell’I2C è che permette la presenza di più master sul bus a differenza dell’SPI (modalità multi master).
Il protocollo di comunicazione
Il protocollo di comunicazione è caratterizzato dai seguenti passi :
-
START Condition : il master abbassa l’SDA tenendo ancora alto l’SCL per indicare la condizione di START allo slave e quindi l’inizio di una trasmissione;
-
Indirizzamento : il master invia un byte (MSB first) sul bus, in cui i primi 7 bit rappresentano l’indirizzo dello slave con cui comunicare e l’ultimo bit indica il tipo di operazione da voler effettuare (0 = write, 1, = read);
-
Slave acknowledge : se sul bus esiste uno slave con tale indirizzo, esso risponde con un bit di ACK (0 logico);
-
Comunicazione : a questo punto, il master può inviare e/o ricevere dati dallo slave in maniera sincrona grazie al movimento dell’SCL. Per ogni byte scambiato è sempre previsto un ACK dalla controparte;
-
STOP Condition : il master alza l’SCL tenendo ancora basso l’SDA per indicare la STOP condition allo slave e quindi il termine della trasmissione;
Il clock è sempre pilotato dal master ma in alcuni casi lo slave può mantenerne il valore basso per introdurre del delay ed evitare che il master gli invii altri dati (magari ha bisogno di più tempo per elaborare i dati già ricevuti) : questa funzionalità si chiama “clock stretching”.
.Net Micro Framework : le classi per utilizzare il bus
Il .Net Micro Framework semplifica notevolmente l’utilizzo del bus I2C mediante la classe I2CDevice (namespace Microsoft.SPOT.Hardware, assembly Microsoft.SPOT.Hardware.dll), il cui costruttore prevede un parametro del tipo I2CDevice.Configuration per poter essere opportunamente configurata; tale configurazione permette di impostare :
Tutti i parametri di configurazione suddetti sono sempre strettamente legati al device con il quale si intende comunicare e vanno ricercati all’interno del datasheet.
I2CDevice.Configuration config =
new I2CDevice.Configuration(I2C_ADDRESS, I2C_CLOCK_RATE_KHZ);
I2CDevice i2c = new I2CDevice(config);
La classe I2CDevice mette a disposizione un unico metodo Execute() per poter effettuare una o più “transazioni” sul bus che rappresentano le operazioni di lettura e scrittura con lo slave. Tale metodo prevede in ingresso un array di oggetti I2CDevice.I2CTransaction ed un timeout. La classe I2CDevice.I2CTransaction è la classe base per la classe I2CDevice.I2CReadTransaction, nel caso di una transazione di lettura, e per la classe I2CDevice.I2CWriteTransaction, nel caso di una transazione di scrittura.
La creazione di un’istanza per ciascuna delle due classi suddette può essere effettuata attraverso i due seguenti metodi statici della classe I2CDevice :
-
CreateReadTransaction() : crea un’istanza della classe I2CDevice.I2CReadTransaction associando ad essa l’array di byte ricevuto in ingresso come buffer per la ricezione dati dallo slave (inizialmente vuoto);
-
CreateWriteTransaction() : crea un’istanza della classe I2CDevice.I2CWriteTransaction associando ad essa l’array di byte ricevuto in ingresso come buffer contenente i dati da trasmettere allo slave;
In definitiva, la procedura d’uso dell’I2C prevede di creare un array di “transazioni” di lettura e/o scrittura (ovviamente anche mixate) ed eseguire queste transazioni in un solo colpo, ritrovandosi i dati trasmessi allo slave ed i buffer di ricezione con i dati richiesti.
Immaginiamo di aver un componente I2C caratterizzato da una serie di registri interni e di voler leggere il contenuto di uno di essi. Questo tipo di comunicazione è caratterizzata da due “transazioni” I2C; la prima di scrittura per poter inviare allo slave l’indirizzo del registro da leggere (attenzione !! non parliamo dell’indirizzo dello slave stesso che viene inviato in precedenza) e la seconda di lettura per poterne leggere il contenuto.
byte[] write = { REG_ADDRESS };
byte[] read = new byte[1];
// create I2C write and read transaction
I2CDevice.I2CTransaction[] i2cTx = new I2CDevice.I2CTransaction[2];
i2cTx[0] = I2CDevice.CreateWriteTransaction(write);
i2cTx[1] = I2CDevice.CreateReadTransaction(read);
// execution
i2c.Execute(i2cTx, I2C_TIMEOUT);
Uno sguardo verso il basso : lo strato di HAL
Anche nel caso dell’I2C (come già visto per l’SPI), ciascun OEM deve implementare uno strato HAL (e PAL) che faccia da “ponte” tra la parte CLR (fino al codice managed ad alto livello) ed il particolare hardware sottostante.
Prendiamo come riferimento la board Netduino (generazione 1) che ha come microcontrollore un Atmel AT91. Scaricando i sorgenti del firmware (sono open source) dal sito ufficiale, possiamo individuare l’implementazione in codice managed C# della classe I2CDevice (Framework\Core\Native_Hardware\I2C.cs) all’interno della quale viene invocato il metodo GetI2CPins() sull’istanza corrente dell’HardwareProvider.
public I2CDevice(Configuration config)
{
this.Config = config;
HardwareProvider hwProvider = HardwareProvider.HwProvider;
if (hwProvider != null)
{
Cpu.Pin scl;
Cpu.Pin sda;
hwProvider.GetI2CPins(out scl, out sda);
if (scl != Cpu.Pin.GPIO_NONE)
{
Port.ReservePin(scl, true);
}
if (sda != Cpu.Pin.GPIO_NONE)
{
Port.ReservePin(sda, true);
}
}
Initialize();
m_disposed = false;
}
Dopo una serie di invocazioni a cascata attraverso il CLR fino all’implementazione dell’HAL, sarà invocato il metodo GetPins() sulla classe AT91_I2C_Driver(DeviceCode\Targets\Native\AT91\DeviceCode\AT91_I2C\AT91__I2C.cpp) che ritorna gli identificativi dei pin del processore associati alla porta I2C.
void AT91_I2C_Driver::GetPins(GPIO_PIN& scl, GPIO_PIN& sda)
{
NATIVE_PROFILE_HAL_PROCESSOR_I2C();
scl = AT91_TWI_SCL;
sda = AT91_TWI_SDA;
}
Nel caso della board Netduino (generazione 2) che ha un processore STM32, la funzione di lettura dei pin I2C è I2C_Internal_GetPins() (DeviceCode\Targets\Native\STM32\DeviceCode\STM32_I2C\STM32_i2c_functions.cpp).
void I2C_Internal_GetPins(GPIO_PIN& scl, GPIO_PIN& sda)
{
scl = I2Cx_SCL_Pin;
sda = I2Cx_SDA_Pin;
}
Conclusione
Il bus I2C a differenza dell’SPI non è ovviamente full duplex essendo caratterizzato da una sola linea dati ed è anche più lento in termini di velocità. Il vantaggio principale è quello di non avere la complessità di un segnale di selezione dello slave e di poter lavorare in modalità multi master.
Il .Net Micro Framework permette di utilizzare questo bus con estrema semplicità con una sola classe ed il concetto di “transazioni” lettura/scrittura I2C in modo da eseguire la comunicazione in un solo “colpo”.
Molto presto vedrete un esempio reale di applicazione di questo bus (come per il bus SPI) con un managed driver che ho sviluppato per un chip NFC della NXP !