Comunicazione seriale sincrona (I2C)

Valuta questo articolo 0 Voti

Autori: Lorenzo Maiorfi e Gianluca Ruta

Il terzo ed ultimo modello di comunicazione con dispositivi esterni che prendiamo in esame è il protocollo I2C (acronimo di Inter Integrated Circuit), Sviluppato dalla Philips nel 1982 e divulgato circa 10 anni dopo. Come nel caso dell'SPI, anche il protocollo I2C prevede l'utilizzo di un segnale di clock e anche in questo caso è possibile individuare i ruoli di master e di slave in base a quale dei dispositivi presenti sul canale genera effettivamente tale segnale ma, a differenza del caso SPI, la comunicazione I2C permette di utilizzare un bus costituito da due soli segnali, per di più condivisi da tutti i dispositivi presenti sul bus. Utilizzando infatti degli ingressi di tipo "open-drain" ed una coppia di resistenze di pull-up sulle due linee dati (SDA) e clock (SCL) è possibile realizzare dei canali di comunicazione multi-slave, in cui un master comunica con slave differenti o addirittura multi-master. La presenza di più dispositivi su un'unica linea dati comporta però ovviamente una maggiore complessità del protocollo e l'impossibilità di una comunicazione full-duplex: ciascun dispositivo slave è infatti identificato univocamente sul bus da un indirizzo logico (tipicamente a 7 bit) e le "transazioni" di comunicazione avvengono secondo uno schema simplex in cui si avvicendano sul bus scritture e letture (viste dalla prospettiva del master).

Le principali classi responsabili della gestione della comunicazione I2C nel .NET Micro Framework sono "I2CDevice", che rappresenta il riferimento al bus I2C su cui si effettueranno le transazioni, "I2CDevice.Configuration", che rappresenta un set di parametri di configurazione utilizzato per comunicare con un determinato slave (ossia indirizzo e frequenza del clock) e "I2CTransaction" che, attraverso le due classi derivate "I2CWriteTransaction" e "I2CReadTransaction", rappresenta un messaggio inviato rispettivamente dal master allo slave indirizzato o viceversa. 

Una tipica sessione di comunicazione I2C avviene secondo quanto descritto dal frammento di codice che segue:

I2CDevice.Configuration config=new I2CDevice.Configuration(0x49, 100);

I2CDevice i2c=new I2CDevice(config);


I2CDevice.I2CTransaction[] tran = new I2CDevice.I2CTransaction[2];


byte[] tx = new byte[1] { 0 };  // tipicamente questo è l'indice di un registro

byte[] rx = new byte[2] { 0, 0 };


tran[0] = _i2c.CreateWriteTransaction(tx);

tran[1] = _i2c.CreateReadTransaction(rx);


int retval = _i2c.Execute(tran, 1000);  // Timeout a 1 secondo


/*ora possiamo leggere la risposta nel buffer rx[]*/

 

Notiamo che:

  • viene utilizzato un oggetto di tipo I2CDevice.Configuration inizializzato con l'indirizzo (a 7 bit) 0x49 e con la frequenza di clock di 100 KHz e con questo oggetto poi viene inizializzato il riferimento al bus I2C
  • viene definito un array di 2 transazioni I2C, un oggetto costituito da due messaggi distinti (uno di richiesta ed uno di risposta)
  • viene valorizzato il buffer che costituirà l'effettiva richiesta e quello che ospiterà la transazione di risposta
  • l'array di transazioni viene popolato rispettivamente da una transazione di scrittura ed una di lettura
  • viene effettuata la comunicazione vera e propria sul bus. Il valore restituito rappresenta l'effettivo numero di byte veicolati dal bus durante la transazione

Ci proponiamo ora di utilizzare il supporto al protocollo I2C del .NET Micro Framework per comunicare con due diversi dispositivi: il MCP9800, un sensore di temperatura a 12 bit, e il TC1321, un DAC (ossia un convertitore da digitale ad analogico) a 10 bit, entrambi prodotti da Microchip e riportati, insieme ad altri dispositivi, all'interno della scheda dimostrativa denominata "PICKit Serial I2C Demo Board".

Analogamente a quanto fatto per l'esempio precedente, abbiamo realizzato due apposite classi "driver" che semplifichino l'utilizzo dei suddetti dispositivi da parte di un'applicazione client. Il datasheet del sensore di temperatura MCP9800 riporta il protocollo relativo alla funzionalità di lettura della temperatura corrente come sequenza di due transazioni I2C: la prima, in scrittura, seleziona il registro da leggere nella successiva transazione, di lettura, in cui il dispositivo invia al microcontrollore due byte che esprimono in forma "complemento a 2" la temperatura rilevata, con una risoluzione di 0.0625°C ed un'accuratezza di 0.5°C. L'implementazione del protocollo descritto viene realizzata dal frammento di codice seguente, in cui viene riportata la classe relativa al driver di dispositivo:

public class MCP9800_TemperatureSensor

{

     I2CDevice.Configuration _config;

     I2CDevice _i2c;


     public MCP9800_TemperatureSensor(I2CDevice I2CBus, ushort I2CAddress, int I2CClockFrequencyKHz)

     {

         // Memorizzo la configurazione passata come parametro

         _i2c = I2CBus;

         _config = new I2CDevice.Configuration(I2CAddress, I2CClockFrequencyKHz);

     }


     public MCP9800_TemperatureSensor(I2CDevice I2CBus)

     {

         // Uso la configurazione del bus passato come parametro

         _i2c = I2CBus;

         _config = I2CBus.Config;

     }


     public float Temperature

     {

         get

         {

             // Imposto la configurazione del bus

             _i2c.Config = _config;


             // Creo 2 transazioni, una di scrittura (indice registro da leggere)

             // ed una di lettura (2 byte, contenente la temperatura corrente)

             I2CDevice.I2CTransaction[] tran = new I2CDevice.I2CTransaction[2];


             byte[] tx = new byte[1] { 0 };  // indice registro temperatura=0

             byte[] rx = new byte[2] { 0, 0 };


             tran[0] = _i2c.CreateWriteTransaction(tx);

             tran[1] = _i2c.CreateReadTransaction(rx);


             int retval = _i2c.Execute(tran, 1000);  // Timeout a 1 secondo


             if (retval != tx.Length + rx.Length) throw new ApplicationException("Transazione I2C FALLITA");


             // calcolo temperatura in °C

             if ((rx[0] & 0x80) != 0)

             {

                 // se sotto 0°C applico il complemento a 2

                 return (~rx[0] * 256 + ~rx[1]) / 256.0f;

             }

             else

             {

                 return (rx[0] * 256 + rx[1]) / 256.0f;

             }

         }

     }

}

L'utilizzo tipico di tale classe da parte del programma principale (o di altre parti del firmware che utilizzino le funzionalità del dispositivo) è illustrato nel frammento di codice che segue:

 

// Configurazione default bus I2C (verrà sovrascritta da quella del dispositivo)

I2CDevice.Configuration configEmpty = new I2CDevice.Configuration(0x00, 10);


// Bus I2C

I2CDevice i2c = new I2CDevice(configEmpty);


// Sensore

MCP9800_TemperatureSensor tempSensor = new MCP9800_TemperatureSensor(i2c, 0x49, 10);


float t=tempSensor.Temperature;


// Output di debug in Visual Studio

Debug.Print(t.ToString("F"));

 

Analogamente, è possibile realizzare un driver che piloti il dispositivo DAC di cui sopra attraverso una classe definita come riportato nel frammento di codice seguente:

 

public class TC1321_DAC

{

    I2CDevice.Configuration _config;

    I2CDevice _i2c;


    public TC1321_DAC(I2CDevice I2CBus, ushort I2CAddress, int I2CClockFrequencyKHz)

    {

        _i2c = I2CBus;

        _config = new I2CDevice.Configuration(I2CAddress, I2CClockFrequencyKHz);

    }


    public TC1321_DAC(I2CDevice I2CBus)

    {

        _i2c = I2CBus;

        _config = I2CBus.Config;

    }


    public void SetOutVoltage(float Voltage)

    {

        // Assumo una VREF di 2.5V

        float vref = 2.5f;

        ushort dacvalue = (ushort)((Voltage / vref) * 1024);


        byte msb, lsb;

        msb=(byte)(dacvalue/4);

        lsb = (byte)((dacvalue & 0x00FF) * 64);


        _i2c.Config = _config;

        I2CDevice.I2CTransaction[] tran = new I2CDevice.I2CTransaction[1];

        byte[] tx = new byte[3] { 0, msb, lsb };

        tran[0] = _i2c.CreateWriteTransaction(tx);

        int retval = _i2c.Execute(tran, 1000);

        if (retval != tx.Length) throw new ApplicationException("Transazione I2C FALLITA");

}

}

Di nuovo, l'utilizzo di tale driver da parte del programma principale "nasconde" i dettagli della comunicazione sottostante, permettendo a chi sviluppa il firmware di concentrarsi solamente sulla logica applicativa, come illustrato nel frammento di codice successivo:

 

// Configurazione default bus I2C (verrà sovrascritta da quella del dispositivo)

I2CDevice.Configuration configEmpty = new I2CDevice.Configuration(0x00, 10);


// Bus I2C

I2CDevice i2c = new I2CDevice(configEmpty);


// Sensore

TC1321_DAC dac = new TC1321_DAC(i2c, 0x48, 10);


// Imposta l'uscita a 1.5V

dac.SetOutVoltage(1.5f);

Veniamo ai test che si possono effettuare con il firmware che abbiamo esaminato: nella figura seguente viene presentato il sistema di test, che stavolta utilizza la scheda Tahoe II prodotta da Device Solutions affiancata dalla scheda di Microchip I2C Demo Board, che ospita a bordo il componente DAC e il sensore di temperatura. Stavolta non utilizziamo una comunicazione seriale dei dati; potremo leggere la temperatura rilevata direttamente nell’ambiente di sviluppo Visual Studio nella finestra ‘Debug’, mentre il segnale di tensione che impostiamo (variabile tra 0 e 2 Volt) sul DAC dovremo ovviamente andarlo a rilevare con un voltmetro collegato ai piedini GND e DAC-out della Demo Board di Microchip.