Comunicazione seriale sincrona (SPI)

Valuta questo articolo 0 Voti

Autori: Lorenzo Maiorfi e Gianluca Ruta

Lo standard SPI prevede una comunicazione sincrona punto-punto tra un dispositivo "master" ed un dispositivo "slave". La distinzione tra i due ruoli è determinata da quale dei due interlocutori genera gli impulsi di clock. In una soluzione tipica, quale quella che prenderemo in considerazione negli esempi che esamineremo a breve, il ruolo del master viene interpretato dal microcontrollore che, oltre ad avere la responsabilità della generazione del clock, genera su propria iniziativa le richieste da inviare al dispositivo slave.

Dal punto di vista dei segnali, a differenza del caso della UART in cui abbiamo solo il segnale RX ed il segnale TX, il protocollo SPI utilizza quattro "canali" digitali:

  • MOSI - Master Output Slave Input, rappresenta il segnale che veicola l'informazione (sotto forma di bit, ovviamente) che va dal dispositivo master alla periferica slave
  • MISO - Master Input Slave Output, rappresenta il segnale che veicola l'informazione che va dalla periferica slave al dispositivo master
  • SCK - Rappresenta il segnale di clock. Tipicamente il campionamento di MISO e MOSI avviene sul fronte di salita del segnale di clock
  • SSEL - Slave Select (indicato spesso anche come CS, Chip Select), tipicamente attivo quando si trova al livello logico basso, serve per attivare la comunicazione nei confronti di uno specifico slave qualora ve ne fosse più di uno collegato sullo stesso bus SPI, ossia che condividesse  gli stessi MOSI, MISO e SCK

Come esempio, abbiamo realizzato un firmware in grado di colloquiare su bus SPI con due diversi dispositivi: un ADC ed un IO Expander. Come ADC abbiamo usato un MCP3201, prodotto da Microchip, in grado di campionare segnali di ingresso alla frequenza di 100 ksps (100.000 campioni al secondo, sufficienti per campionare un segnale di ingresso con una occupazione di banda massima di 50 KHz), con una risoluzione di quantizzazione massima di 12 bit. Come IO Expander invece abbiamo utilizzato un MCP23S08, sempre prodotto da Microchip, caratterizzato dalla possibilità di gestire 8 GPIO, ciascuno configurabile come ingresso o come uscita, insieme ad un sistema di interrupt relativi a notifiche di variazioni degli ingressi digitali. La scelta dei componenti non è casuale: entrambi sono infatti installati a bordo di una scheda dimostrativa prodotta da Microchip e denominata "PICkit Serial SPI Demo Board", in cui oltre ai dispositivi citati sono presenti anche una EEPROM, un sensore di temperatura, un DAC, un potenziometro digitale ed un amplificatore a guadagno programmabile, tutti connessi al medesimo bus SPI, che riteniamo un dispositivo prezioso per chi voglia imparare provando sul campo quanto noi esponiamo. La scheda in questione è progettata con il segnale di Slave Select condiviso fra tutte le periferiche: è previsto l'uso di un apposito jumper sulla scheda stessa  per veicolare detto segnale verso il dispositivo che si vuole utilizzare. Ovviamente, qualora si volesse utilizzare più di un dispositivo all'interno di una stessa applicazione, sarebbe necessario collegare ai singoli pin di Slave Select dei dispositivi altrettante uscite digitali del dispositivo master. 

Analogamente a quanto visto per l'esempio di firmware relativo alla comunicazione UART, il progetto creato all’interno di Visual Studio è di tipo "Console Application", e include i riferimenti agli stessi assembly esterni già utilizzati nell'esempio precedente. L'utilizzo della comunicazione SPI nel .NET Micro Framework avviene tramite la classe "Microsoft.SPOT.Hardware.SPI" e la classe annidata "Microsoft.SPOT.Hardware.SPI.Configuration", che descrive i parametri di configurazione di una comunicazione.  Come nel caso dell'oggetto SerialPort, utilizziamo un'istanza della classe SPI ed una della classe SPI.Configuration come membri statici della classe Program, che rappresenta il contesto "logico" della nostra applicazione. Quanto detto si traduce nel frammento di codice che segue: 

public static class Program

 {

    // parametri configurazione SPI

    static SPI.Configuration _spiConfig = new SPI.Configuration(

        USBizi.Pin.IO43,    // CS Pin

        false,              // CS attivo a LOW

        0,                  // CS setup time

        0,                  // CS hold time

        false,              // SCK idle a LOW

        true,               // campionamento MISO/MOSI su fronti di salita di SCK

        250,                // clock a 250 KHz

        SPI.SPI_module.SPI1);


    // istanza bus SPI

    static SPI _spi = new SPI(_spiConfig);

 }

 

 

E' opportuno sottolineare che:

  • il pin IO43, indicato come Slave Select (o Chip Select), corrisponde nella scheda di sviluppo utilizzata al pin SSEL0 del microcontrollore, pertanto l'attivazione e la disattivazione di tale segnale avverranno in maniera automatica da parte del modulo SPI hardware built-in, ovvero senza che debba essere utilizzata a questo scopo una OutputPort
  • l'oggetto SPI può utilizzare le quattro diverse modalità previste dal protocollo (relative alle quattro varianti di CPOL e CPHA illustrate nella figura seguente)
  • utilizziamo un clock con frequenza 250KHz. Sebbene il parametro permetta di indicare una qualsiasi velocità multipla di 1 KHz, solo alcune velocità sono effettivamente utilizzabili dal microcontrollore. Se si usa una velocità non consentita la runtime del .NET Micro Framework solleverà un'eccezione di tipo "argomento errato" al primo tentativo di lettura o scrittura sul bus
  • qualora il microcontrollore disponga di più moduli SPI indipendenti, è possibile utilizzarle specificando per ciascuna configurazione un diverso identificativo. Nel caso del nostro esempio il valore simbolico "SPI.SPI_module.SPI1" corrisponde al modulo SPI0 hardware
  • il riferimento all'oggetto di tipo SPI denominato "_spi" viene inizializzato con una nuova istanza valorizzata attraverso il costruttore che prevede un parametro di tipo SPI.Configuration

Una volta istanziato l'oggetto SPI, è possibile effettuare delle transizioni sul bus attraverso i metodi Write() e WriteRead(). Entrambi effettuano l'invio sulla linea MOSI dei dati in uscita dal master, ma in più il secondo metodo permette anche di raccogliere i dati di ingresso. Il numero di cicli di clock generati dalla transazione dipende sia dalla dimensione del buffer di ingresso (o di uscita, nel caso di utilizzo di WriteRead(), dato che i due buffer devono avere la stessa dimensione) che dal tipo di dato, a 8 o 16 bit, utilizzato per il buffer.

La comunicazione con il dispositivo ADC del nostro esempio avviene tramite il codice riportato nel frammento di codice 10, in cui viene ricavata la tensione di ingresso a partire dal valore 12 bit restituito sul segnale MISO da parte del componente una volta che il master abbia generato 16 impulsi di clock:

ushort[] tx = new ushort[1] { 0 };

ushort[] rx = new ushort[1] { 0 };


_spi.WriteRead(tx, rx);


return 3.3f * ((float)((rx[0] / 2) & 0x1fff)) / 4096.0f;

Il calcolo effettuato tiene conto di una tensione di riferimento per l'ADC di 3.3V e di una quantizzazione a 12 bit (2^12=4096). 

Per incapsulare la logica della comunicazione all'interno di una classe che astragga le funzionalità del componente, è possibile realizzare una sorta di "driver" modulare, che permetta il riutilizzo della porzione di firmware responsabile della comunicazione e del protocollo. L'implementazione di tale driver è rappresentata dal frammento di codice seguente:

public class MCP3201_ADC

{

     // riferimento al bus SPI da utilizzare

     SPI _spi = null;


     // costruttore che accetta in ingresso un bus SPI pre-inizializzato

     public MCP3201_ADC(SPI spi)

     {

         _spi = spi;

     }


     // proprietà che restituisce la tensione misurata dall'adc

     public float Vin

     {

         get

         {

             ushort[] tx = new ushort[1] { 0 };

             ushort[] rx = new ushort[1] { 0 };


             _spi.WriteRead(tx, rx);


             return 3.3f * ((float)((rx[0] / 2) & 0x1fff)) / 4096.0f;

         }

     }

}

In questo modo, il programma principale potrà rilevare la tensione di ingresso al dispositivo ADC nel modo illustrato dal frammento di codice che segue:

float vin;


 MCP3201_ADC adc = new MCP3201_ADC(_spi);    // _spi già inizializzato

 vin=adc.Vin;                                // in "vin" metto la tensione misurata

 Analogamente, è possibile implementare un componente software che astragga le funzionalità del dispositivo IO Extender di cui sopra nel modo illustrato dal frammento di codice 13, che consigliamo di seguire insieme al datasheet del MCP23S08 per poter meglio comprendere il protocollo di comunicazione utilizzato da quest'ultimo:

public class MCP23S08_IOEXP

 {

     // Indici registri

     const byte REGISTER_IODIR_ADDR = 0x0;

     const byte REGISTER_GP_INT_EN_ADDR = 0x2;

     const byte REGISTER_INT_FLAG_ADDR = 0x7;

     const byte REGISTER_INT_CAP_ADDR = 0x8;

     const byte REGISTER_GPIO_ADDR = 0x9;


     // indirizzo logico sul bus

     const byte MCP23S08_A0 = 0x0;

     const byte MCP23S08_A1 = 0x0;


     // indirizzi logici di lettura e scrittura

     const byte MCP23S08_R_ADDRESS = ((1 << 6) | (MCP23S08_A1 << 2) | (MCP23S08_A0 << 1) | 0x1);

     const byte MCP23S08_W_ADDRESS = ((1 << 6) | (MCP23S08_A1 << 2) | (MCP23S08_A0 << 1) | 0x0);


      // riferimento al bus SPI da utilizzare

     SPI _spi = null;


     // costruttore che accetta in ingresso un bus SPI pre-inizializzato

     public MCP23S08_IOEXP(SPI spi)

     {

         _spi = spi;

     }


     // imposta i GPIO come ingressi (bit=1) o uscite (bit=0)

     public void SetIODirection(byte input_mask)

     {

         byte[] tx = new byte[3] { MCP23S08_W_ADDRESS, REGISTER_IODIR_ADDR, input_mask };


         _spi.Write(tx);

     }


     // rileva la direzione (ingresso o uscita) dei GPIO

     public byte GetIODirection()

     {

         byte[] tx = new byte[2] { MCP23S08_R_ADDRESS, REGISTER_IODIR_ADDR };

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


         _spi.WriteRead(tx, rx);


         return rx[1];

     }


     // imposta lo stato dei GPIO (ha un effetto solo per quelli di uscita)

     public void WriteIO(byte output_mask)

     {

         byte[] tx = new byte[3] { MCP23S08_W_ADDRESS, REGISTER_GPIO_ADDR, output_mask };


         _spi.Write(tx);

     }


     // rileva lo stato di tutti i GPIO

     public byte ReadIO()

     {

         byte[] tx = new byte[3] { MCP23S08_R_ADDRESS, REGISTER_GPIO_ADDR, 0 };

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


         _spi.WriteRead(tx, rx);


         return rx[2];

     }

 }

 Anche in questo caso l'utilizzo da parte del programma principale è molto semplificato rispetto all'interfacciamento diretto con il bus SPI. Il frammento di codice seguente illustra una sessione di utilizzo del componente in questione:

MCP23S08_IOEXP ioexp = new MCP23S08_IOEXP(_spi);


ioexp.SetIODirection(0xF0);     // 4 ingressi e 4 uscite


Debug.Assert(_ioexp.GetIODirection() == 0xF0,"IOEXP SetIODirection() FALLITO!");


// impostazione tutte uscite ON

_ioexp.WriteIO(0x0F);


// lettura stati ingressi

byte stati_ingressi = _ioexp.ReadIO();

Veniamo al circuito di test e alle prove pratiche del firmware implementato: nella figura seguente vedete rappresentato il semplice circuito da realizzare, che prevede ancora la scheda USBizi e il convertitore USB/seriale, oltre alla scheda di sviluppo SPI Demo Board di Microchip. Il firmware che abbiamo presentato consente la comunicazione attraverso porta seriale/USB del valore di tensione convertito dal componente ADC (la scheda Microchip a tal scopo ha un trimmer che consente di regolare la tensione in ingresso al convertitore per sperimentare la corretta lettura del dato), pertanto con il software TeraTerm potremo leggere il valore di tensione rilevato. Per i test con l’IO Expander , invece, la scheda di Microchip prevede un led per ogni uscita parallela del MCP23S08 e il firmware fa accendere in sequenza i primi quattro led mentre configura come ingressi digitali i restanti quattro; ancora con il software TeraTerm potremo vedere la conversione in numero decimale del numero binario a quattro bit che impostiamo su tali ingressi. Ad esempio collegando alla alimentazione positiva l’ingresso che viene interpretato come bit meno significativo e quello che invece è il bit più significativo dei quattro otterremo il numero 9.