Translate

viernes, 5 de abril de 2013

Comunicaciones Serie SPI

La familia PIC18 dispone de varias posibilidades de comunicaciones serie. Además del puerto USART que ya describimos, dispone de un puerto dedicado a comunicaciones síncronas serie, el SSP (Serial Synchronous Port). Dicho puerto puede dedicarse a varios protocolos, tales como SPI o I2C. Ambos son excluyentes, esto es, si se configura el periférico para SPI no podrá usarse para I2C y viceversa. Si se precisan de forma conjunta comunicaciones I2C y SPI la única solución es un microcontrolador con dos puertos SSP o bien, implementar algunos de los dos protocolos a través de software.


En este tutorial vamos a examinar en el protocolo SPI (Serial Protocol Interface), describiendo los registros SFR involucrados y detallando los procedimientos para transmitir y recibir.  Lo ilustraremos con un ejemplo muy sencillo de comunicación  con un periférico SPI, un conversor DAC (MCP4822 de Microchip).

En entradas posteriores veremos ejemplos de comunicaciones SPI con otros periféricos con protocolos de comunicación más complicados.

Archivos de código asociados a esta entrada:  spi_mcp4822.c

---------------------------------------------------------------------------------------------


Es importante entender los conceptos básicos detrás de una comunicación SPI, que son diferentes de otros tipos de comunicaciones. 

Empezaremos recordando las propiedades de un registro de desplazamiento o shift register (ver figura):


En un registro de desplazamiento, con cada clock del reloj, un nuevo bit entra en el registro y desplaza a todos los bits una posición. El último bit sale del registro. Los registros de desplazamiento son la base de las conversiones paralelo/serie y viceversa. Por ejemplo, en la UART existe un TSR (Tx Shift Register) donde se carga el byte a enviar (de forma paralela) y van saliendo sucesivamente (serie) los bits a enviar. Dicho TSR se carga como vimos con el dato colocado en TXREG. De forma totalmente análoga en el circuito de recepción de la UART tenemos otro registro RSR (Rx Shift register) donde van entrando sucesivamente (serie) los bits recibidos. Al llenarse, el dato se transfiere (en paralelo) al registro buffer de recepción (RCREG).  Otro uso similar (se suele denominar SIPO, Serial In, Parallel Out) de un SR se da en un “port expander”, donde un registro (puerto extendido) se va llenando sucesivamente con datos recibidos de forma serie.

Vemos que en la UART tenemos sendos registros de desplazamiento para la recepción como para la transmisión. Dichos registros son independientes entre sí. En el caso de SPI solo tendremos un registro SSP SR (accesible a través de un buffer SSPBUF).

¿Cómo podemos recibir y transmitir con un solo registro? Consideremos ahora un registro de desplazamiento circular, donde la salida del registro se usa como entrada del mismo:


Supongamos que nuestro registro tiene 16 bits y al ser circular, el bit que entra “empujando” a los demás es justo el que acaba de salir por el otro lado. Ahora pensar que este registro se parte en dos, cada uno de 8 bits, pero formando conceptualmente un único registro de 16 bits:




Lo que tenemos es justo la base de una comunicación SPI entre dos dispositivos. Cada una de las partes del registro circular es el registro SSPSR de cada dispositivo y ambos comparten el reloj. La idea es que si en el SSPSR1 hay un dato A y en el SSPSR2 un dato B, tras 8 ticks de reloj, los datos A y B se habrán intercambiado entre los dispositivos. Esta es la razón por la que al contrario que la UART sólo se dispone de un registro, sin diferenciar entre entrada y salida. En el protocolo SPI no hay realmente transmisiones ni recepciones, solo intercambios de datos A y B, ya que por cada dato enviado debe haber siempre uno recibido. Depende de las circunstancias el cómo se interprete una transferencia SPI:


  1. Será una transmisión  si el 1er dispositivo tenía por objetivo era mandar el dato A al 2do dispositivo, mientras que el dato B recibido era basura (pero no puedo "evitar" recibir dicho dato).
  2. Será una recepción si el dato A enviado es irrelevante y solo lo mando para obtener a cambio el dato B (pero no hay forma de recibir nada si yo no mando algo "a cambio").
  3. Puede ser una transmisión/recepción simultánea si tanto el dato A como el B son significativos para la comunicación. Pensar por ejemplo en un DigitalSignalProcesor (DSP) que recibe una serie de muestras de una señal y efectúa algún tipo de procesado sobre ella. Tras un cierto retraso, inherente al procesado, empezará a mandar muestras de vuelta. A partir de ese momento, por cada muestra de la señal original que mande el host recibirá una muestra procesada en una comunicación full-duplex.
  4. Finalmente, hay situaciones donde los datos intercambiados no le interesan a ningún dispositivo. Por ejemplo, en las especificaciones del protocolo SPI de las tarjetas SD se requiere mandar 8 clocks de reloj tras un intercambio comando/respuesta para que la tarjeta pase a ejecutar el comando recibido. En ese caso el microcontrolador y la tarjeta se intercambiarán un byte que a ninguno de los dos interesa solo para que le lleguen los 8 pulsos de reloj necesarios a la tarjeta.

La única asimetría entre ambos dispositivos es que uno de ellos debe generar los pulsos de reloj que hacen “avanzar” el registro de desplazamiento. Dicho dispositivo (a la izquierda en la gráfica anterior) es el master y será quien controle la transmisión.

En la figura siguiente  (extraída del datasheet de Microchip para el PIC18F252) se ilustra lo que acabamos de contar. Como se ve es totalmente análoga a la figura anterior, añadiendo el hecho de que el usuario (al igual que sucedía en el caso de la UART)  no puede acceder al verdadero registro de desplazamiento SSPSR, trabajando en su lugar con un buffer SSPBUF.


La línea de SDO (master) a SDI (slave) también se suele etiquetar MOSI (Master Out Slave In). Igualmente la línea que conecta SDO (slave) con SDI (master) es denominada MISO (Master In Slave Out):





Además de las dos líneas de datos (MOSI y MISO) y el reloj (SCK), en la figura anterior se muestra una cuarta línea (CS, Chip Select, o SS, Slave Select) que se usa para indicar al slave que se va a iniciar una comunicación. También permite la comunicación de un master con varios slaves:


La barra encima de SS indica negación, y es la forma standard de expresar que si queremos seleccionar al esclavo #2 debemos poner a nivel bajo SS2 y mantener altas SS1 y SS3. De esta forma los esclavos #1 y #3 ignoraran educadamente la conversación entre Master y Slave #2.

Como se ve, si empieza a haber muchos esclavos el número de líneas dedicadas a la selección de dispositivos crece. Además, el master tiene que estar continuamente preguntando a los esclavos si desean algo, ya que un esclavo no tiene ninguna forma de iniciar la conversación.

Esta es la razón por la cual SPI es el protocolo preferido por su simplicidad cuando sólo tenemos una única conexión master-slave. Cuando hay que manejar varios esclavos se prefiere el protocolo I2C. Este protocolo también es de tipo serie y síncrono (en la familia PIC18, SPI e I2C comparten el mismo puerto de comunicaciones serie síncronas SSP), pero implementa un sistema de direcciones, por lo que no es preciso añadir líneas adicionales para los nuevos dispositivos. 


COMUNICACIONES SPI en el PIC:

Pasemos ahora a detallar como implementar el protocolo SPI al trabajar sobre un PIC (asumimos que dispone del hardware adecuado, el puerto síncrono paralelo SSP). Como todo periférico del PIC su configuración y manejo están controlado por una serie de registros SFR (Special Function Registers. Para el puerto SPI dichos registros SFRs son:

SSPCON1, SSPSTAT  y  SSPBUF 

los dos primeros son registros de configuración, mientras que el segundo es donde se ponen los datos a transmitir (y como hemos explicado, de donde se recogerán los datos recibidos). 

Pasamos ahora a describir las opciones posibles en la configuración del puerto SPI en un PIC, que se determinan con una serie de bits en los registros SSPCON1 y SSPSTAT.


1) Elección  Slave/Master

Obviamente la primera elección es decidir si el PIC será el master o un dispositivo slave en la comunicación. Los contenidos de los 4 bits más bajos de SSPCON1 determinan esta elección. Sus posibles valores son:

Opciones modo master:   00 11   -->  clock = TMR2/2
                                   00 10   -->  clock = Fosc/64
                                   00 01   -->  clock = Fosc/16
                                   00 00   -->  clock = Fosc/4


Opciones en modo slave: 01 01    -> No se usa SS
                                    01 00    -> Se usa SS


Como se ve el primer bit (SSPCON1.SSPM3) es siempre 0 para ambos modos (esto sucede porque al estar compartido el puerto SSP, estos cuatro bits también son usados para la configuración del modo I2C).

El segundo bit (SSPCON1.SSPM2) determina si el dispositivo es master (0) o slave (1).

En modo master los dos últimos bits (SSPCON1.SSPM1 y SSPCON.SSPM0) determinan las cuatro posibles frecuencias del reloj.  La frecuencia del reloj será una fracción (4, 16, 64) del oscilador principal o puede asociarse al ritmo del Timer2.

Por ejemplo, con un cristal de 20 MHz podríamos tener un master con un reloj de 5MHz (0000), 1.25MHz (0001) y 312KHz (0010). La opción del TMR2/2 (0011) nos permite programar otras frecuencias a través del timer TMR2.


Si hemos escogido el modo slave, los bits restantes determinan si usaremos o no el pin dedicado para SS (Slave Select). En la familia PIC18F4520, dicho pin es el RA4.  Si el valor es 01 no se usará SS y RA4 podrá usarse como un pin normal. Si el valor es 00 se habilita RA4 como pin de control SS.

Si vamos a ser un dispositivo SLAVE  ya no hay nada más que configurar. Lo único recomendable es hacer SSPSTAT.SMP=0 aunque no es estrictamente necesario ya que ese es su valor por defecto.


En cambio, si nuestro dispositivo va a actuar como MASTER debemos configurar el modo SPI en el que vamos a trabajar.


Modos SPI (master): relación reloj/datos

Aunque tengamos establecida la frecuencia del reloj, todavía hay varias opciones para el master, referidas a la polaridad de la señal de reloj, y la fase entre dicha señal y los datos de entrada/salida.

Los bits que determinan estos aspectos son:

SSPCON1.CKP   (Clock polarity)
SSPSTAT.CKE    (Clock Edge)
SSPSTAT.SMP    (Sample bit)

El primero (CKP) define la polaridad de la señal de reloj (su IDDLE_LEVEL, si está a nivel alto o bajo cuando el puerto este inactivo). 

El segundo bit (CKE) especifica la fase de los datos de salida con respecto al reloj.

Por último, el tercer bit(SMP) determina el momento en que se muestrean los datos de entrada (también referido a la señal de reloj).

El parámetro más sencillo es la polaridad del reloj (SSPCON1.CKP) que en la literatura SPI se suele denotar como CPOL (Clock Polarity). Si es 0 indica que el reloj esta bajo mientras no se manda nada. Si es 1 el IDDLE_STATE del reloj será un nivel alto (1).

El segundo parámetro (SSPSTAT.CKE) determina la fase de los datos de salida con respecto al reloj.  En la literatura standard nos encontramos con un parámetro totalmente equivalente CPHA (Clock Phase), aunque su definición es inversa de CKE.  Esto es, CPHA = 1-CKE.

Juntos, CPOL y CPHA determinan lo que se conoce como el modo SPI usado. Generalmente se expresa como un par de número. Así, el modo SPI (0,1) indica que debemos hacer CPOL=0 y CPHA=1, o traducido a la nomenclatura PIC

SSPCON1.CKP=    CPOL   =  0
SSPSTAT.CKE = (1-CPHA)=  0

Hemos dicho que CPHA determina el momento en el que los datos de salida están estables (y deberían ser muestreados por el otro dispositivo), pero no hemos explicado cual es su relación ni que significa un valor de 0 o 1.
  
 Para entenderlo, veamos la siguiente figura (adaptada del datasheet de Microchip), ilustrando las posibilidades del reloj y su relación con los datos de entrada/salida:






Las cuatro primeras trazas ilustran las cuatro posibilidades de reloj y la traza etiquetada como SDO la posición de los datos de salida. Las líneas verdes indican el momento en que los datos debería ser muestreados.

Como se ve, la interpretación de CPOL=CKP es inmediata. CKP=0 indica un estado de reposo bajo (azul) y CKP=1 indica un estado de reposo (antes y después de enviar datos) alto (color rojo).

Mirando la gráfica (primeras dos trazas de reloj)  podemos ver que si CKE=0  (CPHA=1) el "centro" del bit de salida corresponde a las "segundas" transiciones del reloj. Por el contrario si CKE=1 (CPHA=0) el centro del bit está alineado con la primera transición del reloj.

El problema es que dicha interpretación no es muy intuitiva. A veces se prefiere describir el protocolo en términos de si los bits estarán estables con las subidas o bajadas de reloj. Con la descripción anterior si escogemos CKE=1 sabemos que el dato está listo en la primera transición de reloj, pero dicha transición puede ser de subida (traza 3) o de bajada (traza 4), dependiendo de la polaridad del (CPOL).

Si queremos formalizarlo, podemos definir un nuevo parámetro Low2High, (L2H=1 si el bit esta listo en las subidas y L2H=0 si está disponible en las bajadas) y determinar CKE como:

                                                                CKE = L2H xor CKP

Finalmente queda decidir el valor de SPSSTAT.SMP que determina el momento de muestreo de los datos entrantes, como se aprecia en la parte baja de la gráfica anterior:

SMP=0  los bits de entrada están alineados con el centro del periodo de reloj
SMP=1  los bits de entrada están disponibles al final del periodo de reloj.


En la práctica, ¿cómo elegir estos parámetros para comunicar nuestro PIC con un cierto dispositivo? Como siempre hay varias posibilidades:


  1. Reusar un código que andaba por ahí y que funciona.
  2. Leer las especificaciones del dispositivo donde por algún lado vendrá descrito que modos SPI acepta. Normalmente se describen con la pareja (CPOL,CPHA). Recordar que CKE=1-CPHA.
  3. A veces en vez de especificar el modo SPI nos dan un diagrama de tiempos con el reloj y la posición esperada de datos de salida y entrada. Con lo que hemos explicado deberíamos ser capaces de determinar los tres parámetros necesarios (CKP, CKE, SMP).
Aunque la opción 1 es muchas veces un buen punto de partida, no debemos fiarnos del todo. Hay algunos casos en los que es posible que una elección incorrecta de datos funcione (a medias o intermitentemente). Por ejemplo, imaginad un caso donde los datos de entrada deberían muestrearse  en el centro del periodo (SMP=0) pero usamos SMP=1. Aunque el punto de muestreo se ha llevado al momento de cambio de datos, posiblemente seguirá funcionando porque el otro dispositivo necesitará un tiempo para cambiar los datos y puede que los datos recogidos sean los correctos. Sin embargo está claro que estamos muestreando en un momento en el que los datos pueden cambiar de repente, por lo que cualquier cambio de timings, etc. puede hacer que lo recibido sea basura. Si tenemos la documentación del dispositivo no costará mucho determinar la elección de parámetros correcta.

Una vez establecidas las opciones solo queda habilitar el puerto SSP (con SSPCON1.SSPEN=1) y establecer las correspondientes direcciones de los pines involucrados (a través del correspondiente registro TRIS).

En la mayoría de los PICs los pines asociados a las líneas SCL, SDI, SD0 son respectivamente RC3, RC4 y RC5.
El pin RC4 (SDI) deberá ser configurado como entrada. 


Implementación de las comunicación SPI

Al igual que hicimos en el caso de la UART presentaremos las típicas funciones de las que disponemos en un compilador y luego las reescribiremos usando nuestros recién adquiridos conocimientos.

Las funciones de un compilador respecto al módulo SPI se dividen en rutinas de inicialización (que afectarán a SSPCON1 y SSPSTAT) y rutinas de transferencia de datos (básicamente poner/sacar datos de SSPBUF). Por ejemplo, en el compilador de MikroC Pro encontramos las siguientes funciones básicas:

INICIALIZACION:  SPI_Init_advance,
TRANSFERENCIA:  SPI_read, SPI_write.

La primera inicializa y configuran el puerto SSP en modo SPI.  Los parámetros que se pasan a SPI_Init_advance son bastante descriptivos y corresponden a las opciones explicadas con anterioridad. Por ejemplo:

_SPI_CLK_IDLE_HIGH,_SPI_CLK_IDLE_LOW            -> Polaridad de reloj (CKP=CPOL)
_SPI_DATA_SAMPLE_MIDDLE,_SPI_DATA_SAMPLE_END    -> Muestreo de datos entrada SMP=0/1
_SPI_LOW_2_HIGH,_SPI_LOW_2_HIGH                 -> Transición datos transmitidos (L2H=1/0)
_SPI_MASTER_OSC_DIV4, DIV16, DIV64, TMR2        -> Setup as master y elección de reloj
_SPI_SLAVE_SS_ENABLE,_SPI_SLAVE_SS_DIS          -> Setup as slave con y sin SS pin 

Notad que MikroC Pro usa la convención de especificar la fase reloj/datosTX a través de L2H (especificar si están disponibles en las subidas/bajadas de reloj) y no directamente a través de CKE o CPHA=1-CKE.

En el caso del compilador C18, las rutinas son:

INICIALIZACIÓN:  OpenSPI
TRANSFERENCIA:  ReadSPI, WriteSPI o de forma equivalente getcSPI, putcSPI

Los parámetros de la función de inicialización son:

void OpenSPI(unsigned char sync_mode, unsigned char bus_mode, 
unsigned char smp_phase)

sync_mode:

--> master: SPI_FOSC_4, SPI_FOSC_16, SPI_FOSC_64, SPI_FOSC_TMR2 (clock)
--> slave:     SLV_SSON, SLV_SSOFF (uso o no del pin dedicado para SS)

bus_mode:

-->  MODE_00 (CKP=0, CKE=1)
-->  MODE_01 (CKP=0, CKE=0)
-->  MODE_10 (CKP=1, CKE=1)
-->  MODE_11 (CKP=1, CKE=0)

smp_phase 

--> SMPEND   datos de entrada disponible al final del ciclo (SMP=1)
--> SMPMID   datos de entrada disponible en el medio del ciclo (SMP=0)

Como se observa la inicialización de C18 (parámetro bus_mode) sigue la nomenclatura típica de los modos SPI (CKP, CPHA).

Reescribir por nuestra cuenta una función de inicialización es sencillo: basta poner los bits de los registros SSPCON1 y SSPSTAT a los valores adecuados.  

Las siguientes funciones pueden ser usadas para conseguir el mismo objetivo:



void spi_enable(void)    // Enable SSP port and set TRIS register
{
 TRISCbits.TRISC3=0; TRISCbits.TRISC5=0; TRISCbits.TRISC4=1; // SCL out, SDO, out, SDI in
 SSPCON1bits.SSPEN=1;   // Enable SPI port
}

// Sets SPI mode (CPOL,CPHA,SMP)
void spi_mode(uint8 CPOL,uint8 CPHA,uint8 sample)  
{
 SSPCON1bits.CKP=CPOL; SSPSTATbits.CKE=1-CPHA;
 SSPSTATbits.SMP=sample;
}

// Sets clock frequency for SPI in master mode 
// clock =3 (TMR2/2) =2 (Fosc/64) =1 (Fosc/16)  =0 (Fosc/4)
void spi_master(uint8 clock) 
{
 SSPCON1 = (SSPCON1 & 0xF0) | clock;
}

// Set SPI port as slave. 
// ss=0 -> no dedicated SS pin,
// ss=1 -> dedicated SS pin (RA5 in PIC18F4520)
void spi_slave(uint8 ss) 
{
 ss=1-ss; ss = ss+4;
 SSPCON1 = (SSPCON1 & 0xF0) | ss;
}

Remarcar que estas funciones no aportan nada que las funciones de C18 o MikroC no puedan hacer. Las listamos para mostrar que configurar un periférico no es complicado y para beneficio de aquellos que usen un compilador sin soporte para SPI.

Una vez inicializado el puerto con unas u otras funciones, estamos listos para mandar/recibir datos. Tanto en C18 (ReadSPI, WriteSPI) como en MikroC (SPI_read, SPI_write) tenemos un par de funciones que leen/escriben un byte en la línea SPI.

La siguiente función sería nuestro equivalente:


uint8 spi_transfer(uint8 x)   // basic SPI transfer
{
 SSPBUF = x;  while(SSPSTATbits.BF==0);  return(SSPBUF);
}

La función recibe un byte de datos x y lo coloca en el buffer SSPBUF. Esto provoca las siguientes acciones:


  1. Inmediatamente x se transfiere al verdadero registro de desplazamiento del puerto SSPSR y el buffer SSPBUF queda vacío, esperando los datos de llegada.
  2. Se inicia la transmisión (y simultánea recepción) de datos.
  3. Cuando se han transmitido los 8 bits (y por lo tanto se han recibido 8 bits) el byte recibido se pasa a SSPBUF y la bandera SSPSTAT.BF se pone a 1 (buffer full).
En nuestra función tras poner los datos en SSPBUF monitorizamos SSPSTAT.BF hasta que se haga 1 (señal de que los datos recibidos están listos). En ese momento se devuelve el valor recibido (en SSPBUF). La acción de leer SSPBUF pone a 0 el bit SSPBUF.BF (indicando buffer empty).

Notad que la función se llama spi_transfer, sin especificar si es TX o RX, porque nosotros ya sabemos que en SPI no hay transmisiones ni recepciones propiamente dichas, sólo transferencias. 

Si por una mayor legibilidad del código queremos disponer de una función de escritura y otra de lectura podemos simplemente usar unos #define:

// SPI functions aliases
#define spi_tx(x)   spi_transfer(x)      // sends TX data, ignores return value.
#define spi_rx()    spi_transfer(0xFF)   // sends dummy data, returns RX data.
#define spi_clock() spi_transfer(0xFF)   // send 8 clocks (nobody cares about the data)

Y eso es todo. Hemos reproducido la funcionalidad de un compilador típico. Al igual que hicimos en el caso de la UART, conociendo los detalles podríamos adaptar las rutinas a nuestras necesidades especificas. 

Por ejemplo, la rutina de transferencia anterior (al igual que las de MikroC o C18) son bloqueantes. El bucle while(SSPSTAT.BF==0) asegura que la función vuelve con un dato. Pero la comunicación se interrumpe en mitad de un byte, nuestro programa se bloquearía.  El PIC cuenta con una interrupción del puerto SSP. La bandera SSPIF se levanta cuando una recepción se ha completado y el dato está listo para ser recogido en SSPBUF. Usando interrupciones la función anterior podría volver sin haber completado la transferencia y sería responsabilidad de la interrupción rescatar el dato recibido.


Definición de la línea CS: estrategias:

En el apartado anterior vimos las funciones necesarias para una comunicación SPI, pero dejamos un aspecto de lado: la definición de la línea CS a usar.

Esto es necesario puesto que al contrario del resto de los pines (SCL,SDO,SDI = RC3,RC5,RC4) para la línea CS podemos usar cualquier pin I/O genérico que tengamos libre. El puerto SPI no depende de donde está ni le importa. Habrá situaciones donde no se use u otras donde tengamos varias líneas al haber más de un dispositivo.  Es por eso por lo que la declaración y manejo de dicho línea debe hacerse dentro de las rutinas del dispositivo SPI específico. Sin embargo, como con la mayoría de los dispositivo SPI vamos a necesitar una línea CS, este puede ser un buen lugar para explorar como hacerlo y las alternativas posibles.

La forma más sencilla (aunque como veremos no la más recomendable) es hacer un par de #defines indicando el pin que va a ser usado como línea CS y su correspondiente bit TRIS.  Por ejemplo, para usar el pin RC1 como CS en C18 usaríamos:

// CS line
#define device_CS      LATCbits.LATC1
#define device_CS_dir  TRISCbits.TRISC1
#define select_device   device_CS=0
#define deselect_device device_CS=1

De esta forma solo tenemos que añadir la línea device_CS_dir=0; (output) durante la inicialización del dispositivo. Posteriormente, cada vez que queramos hacer un intercambio SPI haríamos: 

select_device;
SPI_talk ...
deselect_device;

El problema de este enfoque viene si en otro montaje nos viene mejor usar un pin diferente como línea CS. Si estamos usando nuestro propio código no hay problemas, cambiamos el #define y recompilamos. El problema es si estamos escribiendo una librería. En ese caso el usuario final tendría que estar buceando en nuestro código y cambiando los ficheros fuente de la librería. Eso en el caso de que disponga de los ficheros fuente. Si solo publicamos la librería ya compilada forzamos al usuario a usar siempre el pin RC1 cuando use un dispositivo SPI. 

Obviamente esto no es muy conveniente, pero parece ser que en C18 no hay otra alternativa.  De hecho, en el manual de librerías del C18  (pag. 80) indica que si se desea p.e, usar un LCD con una selección de pines diferente de la asignada por defecto se deben modificar las definiciones del correspondiente fichero xlcd.h.

Este es un caso donde el compilador de MikroC presenta una ventaja frente al C18. En MikroC tenemos una solución más flexible: se trata de declarar dos variables (p.e. devicename_CS y devicename_CS_dir) para informar al resto de las rutinas de que pin estamos usando como ChipSelect (CS) y su correspondiente bit de dirección TRIS.

En MikroCPro podemos declarar dichas variables de tipo sbit y asociarlas a los correspondientes pines:

sbit devicename_CS at LATC.B1;        // Will use RC1 as CS line
sbit devicename_CS_dir at TRISC.B1;

Esto parece muy  similar a los #defines de antes. La ventaja es que si estamos escribiendo una librería, en el fichero fuente de la librería declararíamos las variables devicename_CS, devicename_CS_dir como externas, sin asignarles ningún valor en particular. Sería el usuario en su programa quien diera valores a dichas variables, en función de su configuración hardware:

// Within library source file
extern sfr sbit devicename_CS;
extern sfr sbit devicename_CS_dir;

En el fichero del usuario

sbit devicename_CS     at LATC.B1;        // Choose your own pin here
sbit devicename_CS_dir at TRISC.B1;

De esta forma el usuario puede elegir el pin a usar para cada montaje o definir diferentes pines para varios dispositivos, sin tener que modificar la librería original.

Terminaremos este tutorial usando las rutinas dadas en un programa muy sencillo que establezca una simple comunicación SPI entre un PIC (master) y un periférico (slave). 



Conversor Digital Anaógico (DAC) MCP4822:

En la entrada Audio con PWM vimos como era posible usar la salida PWM del PIC como un conversor DAC de conveniencia.  Si se desean mejores resultados existen por supuesto dispositivos DAC específicos.

Vamos a mostrar como usar uno de estos dispositivos (MCP4822 de Microchip). ES un conversor DAC de 12 bits (4096 niveles) alimentado con 5V y que cuenta con dos canales independientes de salida. 



Lo que nos importa en este caso es que la comunicación con dicho DAC es a través de SPI. Además se trata de 
una comunicación muy sencilla, perfecta para una primera prueba. El periférico recibe un sólo tipo de mensaje (de 2 bytes de tamaño, donde se le especifica el voltaje deseado en uno u otro canal) y no envía ninguno.

Es por lo tanto una comunicación unidireccional, en la que esencialmente el PIC manda mensajes con los voltajes deseados y no hay que preocuparse de ningún protocolo de respuestas, validaciones, tiempos de espera, etc.



Hardware:


El DAC MCP4822 es un integrado de 8 pines cuya descripción se muestra en la figura adjunta. La alimentación (Vdd) es a 5V por lo que no tendremos problemas de cambios de nivel con respecto al PIC. Los pines 2(CS line) ,3 (Clock) y 4 (SDI) se usarán en la comunicación SPI. Notad que no hay un pin SDO, al ser la comunicación unidireccional. El pin 7 (AVss) es tierra y los pines 8 (OUT_A) y 9 (OUT_B) corresponden a los dos canales analógicos disponibles. Finalmente el pin 5 (~LDAC) puede usarse para sincronizar la salida de ambos canales. Nosotros lo pondremos a tierra, lo que hace que el voltaje en la salida se actualice cuando el valor de la línea CS vuelve a un valor alto tras acabar la comunicación.

La conexión PIC-DAC será por lo tanto la correspondiente al esquema siguiente:





Notad que estamos usando RC1 como línea CS y que no usamos el pin SDI (RC4) del PIC, puesto que no vamos a recibir ningún dato. La salida de los canales A y B las conectamos al osciloscopio para ver los resultados. 



Software:



El programa es muy sencillo y consiste en poco más de las funciones que ya hemos visto. 

Lo primero que hay que hacer es determinar el modo SPI con el que trabaja el periférico. En su datasheet nos dicen que soporta los modos SPI (0,0) y (1,1). Del momento de muestreo de los datos de entrada no debemos preocuparnos porque no existen.


En la docu también se nos informa de que el dispositivo acepta hasta 20 MHz de reloj en la comunicación SPI. 
En este montaje estoy usando un reloj de 8 MHz, por lo que puedo usar la frecuencia máxima (Fosc/4 = 2 MHz) y aún así estar muy por debajo de las capacidades del dispositivo.

Una rutina que inicializase la comunicación SPI con los parámetros indicados usando las rutinas presentadas antes sería:



void DAC_spi_init(byte clock)
{
 DAC_CS_dir=0; deselect_DAC; // Configure CS line as output and set it low.
 spi_master(clock);          // Configure SPI as master and set clock
 spi_mode(0,0,0);            // SPI mode (0,0). Third arg doesnt matter
 spi_enable();               // Enable SPI
}

Respecto a la comunicación en si, el único tipo de comando reconocido por el DAC es un mensaje de 16 bits:




Los 12 bits menos significativos contienen simplemente el voltaje deseado (un valor de 0 a 4095). De los otros 4 bits, solo 3 tienen una función:



  • El más significativo (Ã/B) indica el canal donde debe aparecer el voltaje (0-->A, 1-->B).  
  • El tercero (~GA) nos permite especificar una ganancia. Si es 0 cubrimos el rango completo de voltaje (aprox de 0 a 4V) y si es 1 solo barremos la mitad (de 0 a 2V).
  • Finalmente (~SHDW) nos permite poner al dispositivo en un modo "apagado" (0)

Por lo tanto si usamos el rango completo de voltaje los valores de esos 4 bits serán 0001 si queremos usar el canal A y 1001 si queremos mandar un voltaje al canal B



Finalmente las especificaciones también nos indican que debemos mandar primero el byte más significativo. Dado un uin16 (2 bytes) que contenga el mensaje a mandar, el siguiente macro lo mandaría por la línea:

#define send_msg(msg) { select_DAC; spi_tx(msg>>8); spi_tx(msg&0xFF); deselect_DAC;  }

Primero avisamos al DAC de que se inicia una comunicación y  luego mandamos el byte más significativo seguido del menos significativo. Finalmente devolviendo a nivel alto la línea CS terminamos la conversación.

Vamos a escribir un programa que incremente un contador de 0 a 4095 y mande dicho contador al canal A, por lo que en dicho canal deberíamos ver un voltaje rampa.  En el canal B vamos a crear una sinusoide. Para ello creamos una tabla de 256 valores de la función seno cubriendo un periodo. Los valores están escalados entre 0 y 255 para que entren en una variable de tipo uint8 (byte):

uint8 tabla[256]= {
0x80,0x83,0x86,0x89,0x8C,0x8F,0x92,0x95,0x98,0x9B,0x9E,0xA2,0xA5,0xA7,0xAA,0xAD,
0xB0,0xB3,0xB6,0xB9,0xBC,0xBE,0xC1,0xC4,0xC6,0xC9,0xCB,0xCE,0xD0,0xD3,0xD5,0xD7,
0xDA,0xDC,0xDE,0xE0,0xE2,0xE4,0xE6,0xE8,0xEA,0xEB,0xED,0xEE,0xF0,0xF1,0xF3,0xF4,
0xF5,0xF6,0xF8,0xF9,0xFA,0xFA,0xFB,0xFC,0xFD,0xFD,0xFE,0xFE,0xFE,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFE,0xFE,0xFE,0xFD,0xFD,0xFC,0xFB,0xFA,0xFA,0xF9,0xF8,0xF6,
0xF5,0xF4,0xF3,0xF1,0xF0,0xEE,0xED,0xEB,0xEA,0xE8,0xE6,0xE4,0xE2,0xE0,0xDE,0xDC,
0xDA,0xD7,0xD5,0xD3,0xD0,0xCE,0xCB,0xC9,0xC6,0xC4,0xC1,0xBE,0xBC,0xB9,0xB6,0xB3,
0xB0,0xAD,0xAA,0xA7,0xA5,0xA2,0x9E,0x9B,0x98,0x95,0x92,0x8F,0x8C,0x89,0x86,0x83,
0x80,0x7C,0x79,0x76,0x73,0x70,0x6D,0x6A,0x67,0x64,0x61,0x5D,0x5A,0x58,0x55,0x52,
0x4F,0x4C,0x49,0x46,0x43,0x41,0x3E,0x3B,0x39,0x36,0x34,0x31,0x2F,0x2C,0x2A,0x28,
0x25,0x23,0x21,0x1F,0x1D,0x1B,0x19,0x17,0x15,0x14,0x12,0x11,0x0F,0x0E,0x0C,0x0B,
0x0A,0x09,0x07,0x06,0x05,0x05,0x04,0x03,0x02,0x02,0x01,0x01,0x01,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x02,0x02,0x03,0x04,0x05,0x05,0x06,0x07,0x09,
0x0A,0x0B,0x0C,0x0E,0x0F,0x11,0x12,0x14,0x15,0x17,0x19,0x1B,0x1D,0x1F,0x21,0x23,
0x25,0x28,0x2A,0x2C,0x2F,0x31,0x34,0x36,0x39,0x3B,0x3E,0x41,0x43,0x46,0x49,0x4C,
0x4F,0x52,0x55,0x58,0x5A,0x5D,0x61,0x64,0x67,0x6A,0x6D,0x70,0x73,0x76,0x79,0x7C};

Los valores anteriores se han obtenido con el siguiente comando MATLAB:

>>  t=2*pi*[0:255]/256; tabla = round(127.5*sin(t)+127.5);

y se representan en la figura adjunta:



A partir de aquí el programa principal queda simplemente:


void main()
{
 uint16 d=0;
 uint16 msg;
 uint16 v;

 DAC_spi_init(0); Delay1KTCYx(10);  // Configure SPI @ Fosc/4

 while(1)
  {
   msg = d + 0x1000; send_msg(msg); // Value of d to chan A  

   v = tabla[d&0xFF]; v<<=4;         // v = 16 x value in sin array  
   msg = v + 0x9000; send_msg(msg);  // Value of v to chan B  

   d++; d&=0x0FFF;  // increment d and makes sure it remains within [0,4095]  
  }
}


El resultado, visto en el osciloscopio para dos escalas de tiempo, es el siguiente:





El canal A (arriba) muestra la rampa del contador (de 0 a 4095 y vuelta a empezar) y el canal B la sinusoide. La sinusoide es más rápida que la rampa porque cada ciclo se repite cada 256 valores del contador, en vez de cada 4096. Notad también que no estamos usando toda la resolución del DAC, ya que para el seno usamos un valor entre 0 y 255 que posteriormente multiplicamos por 16. Por lo tanto los 4 bits menos significativos del voltaje está a 0. 

En el siguiente tutorial pondremos a prueba nuestras rutinas SPI con un ejemplo un poco más complicado de interfaz con un dispositivo SPI, en particular una memoria flash (M25P80 de 1 Mbyte de capacidad), donde tendremos comunicaciones en ambos sentidos.



18 comentarios:

  1. Gracias por esta excelente introducción a las comunicaciones serie SPI, la verdad me ha sido de gran ayuda. Me pregunto si puedes compartir el código completo del ejemplo ya que aún tengo algunas dudas sobre las directivas de preprocesamiento con las que trabaja la comunicación SPI. Gracias nuevamente y espero tu respuesta.

    ResponderEliminar
    Respuestas
    1. El código completo de este ejemplo lo tienes enlazado al principio del artículo.
      Usa un par de ficheros .h con definiciones que pueds encontrar también aquí. Te dejo los enlaces:

      http://artico.lma.fi.upm.es/numerico/antonio/blog/tipos.h
      http://artico.lma.fi.upm.es/numerico/antonio/blog/int_defs_C18.h

      Un saludo, Antonio

      Eliminar
    2. Magnífica explicación, por la claridad con que has expuesto el tema. Un gran aporte!!

      Eliminar
  2. tengo una duda,
    cuando mando datos SPDR para mi slave no se le mando un 31 quiero que mi slave me regrese el dato que tiene el slave
    para esto debo de hacer un buffer tengo la idea de que se hacen con interrupcions ISP podrias resolver mi duda?
    tengo varias sobre amm si el vector isr del slave es el mismo que el master y como hago para regresar el dato de mi slave al master?

    ResponderEliminar
    Respuestas
    1. No entiendo tu pregunta ni muchos de tus acrónimos. ¿Qué es SPDR, amm, interrupción ISP?
      Si se trata de ver como un slave devuelve unos datos eso es dependiente del protocolo establecido para el dispositivo en cuestión.

      Antonio

      Eliminar
  3. hola, puedo usar comunicacion SPI en un pic con un ENC28J60, el pic es el maestro y el ENC28J el esclavo??trabajo en ccs compiler, un abrazo y buen tuto

    ResponderEliminar
  4. Algun tutorial basico para I2C me podrias aconsejar. muchas gracias por adelantado un gran trabajo

    ResponderEliminar
  5. Hola Antonio,
    Te tengo una consulta. YO tengo un PIC18F4550 con un cristal de 4MHz. Gracias al PLL llego a una frecuencia de oscilacion de 48MHz. Por otro lado, los micros de 8 bits de Microchip ejecutan una instruccion cada 8 ciclos de reloj (es decir, ciclos de reloj 48MHz y ciclo de instruccion 12MHz). Mi pregunta es: ¿que frecuencia de oscilación debería considerar para el calculo de la señal de clock del protocolo SPI?. Es decir, por ejemplo para Fosc/16:
    -Si considero 48MHz (ciclos de reloj) -> tengo un clock de SPI de 48MHz/16=3MHz
    -Si considero 12MHz (ciclos de instruccion) -> tengo un clock de SPI de 12MHz/16=750KHz

    ¿Cual sería el correcto?

    Muchsc gracias!!!!!!

    ResponderEliminar
  6. Hola Antonio,
    Te tengo una consulta. YO tengo un PIC18F4550 con un cristal de 4MHz. Gracias al PLL llego a una frecuencia de oscilacion de 48MHz. Por otro lado, los micros de 8 bits de Microchip ejecutan una instruccion cada 8 ciclos de reloj (es decir, ciclos de reloj 48MHz y ciclo de instruccion 12MHz). Mi pregunta es: ¿que frecuencia de oscilación debería considerar para el calculo de la señal de clock del protocolo SPI?. Es decir, por ejemplo para Fosc/16:
    -Si considero 48MHz (ciclos de reloj) -> tengo un clock de SPI de 48MHz/16=3MHz
    -Si considero 12MHz (ciclos de instruccion) -> tengo un clock de SPI de 12MHz/16=750KHz

    ¿Cual sería el correcto?

    Muchsc gracias!!!!!!

    ResponderEliminar
    Respuestas
    1. En realidad ejecuta cada instruccion cada 4 cicos de reloj que es un ciclo maquina y debes considerar la Fosc es decir los 48Mhz

      Eliminar
  7. Hermano gracias por compartir tus conocimientos, estoy intentando compilar el ejemplo de este tutorial y utilizo el Mplab con C18, descargue los archivos y los adjunte a la carpeta del proyecto y al momento de compilar me sale el siguiente error en el fichero de tipos.h:
    D:\2MPLAB\C_18\SPI\tipos.h:7:Error: syntax error
    Si me puedes ayudar estare agradecido.

    ResponderEliminar
  8. Excelente, ha sido de gran ayuda. Muchas Gracias

    ResponderEliminar
  9. EXCELENTE TUTORIAL!!! Solo una consulta, para usar spi entre varios pic, es necesario que el master y los esclavos tengan un cristal con la misma velocidad? o no es necesario. Yo usaría el master con 20mhz y los esclavos con 32mhz algunos y otros con 8mhz. funcionara? gracias.

    ResponderEliminar
  10. Hola...disculpen tengo que hacer una práctica con la comunicación spi y necesito que un pic reciba el dato y el otro cuente en un display. Todo esto con un pic 16f88. Pero estoy totalmente perdido. Alguien me puede ayudar?

    ResponderEliminar
  11. Buena introducción al manejo de SPI, tendrás un ejemplo en lenguaje ensamblador? ya que no manejo el C18, lo aplicaré para comunicar un driver 7 seg max7219 Gracias!

    ResponderEliminar
  12. Hola amigo; excelente tutorial;estoy haciendo un proyecto en el cual se usa el protocolo SPI y UART, mi pregunta es; ¿interfieren algunos registros de SPI y UART entre si? Estoy trabajando con el PIC16F877a, de antemano gracias

    ResponderEliminar
  13. Hola , una pregunta no se que quiere decir el termino: uint8 en esta parte del codigo C para C18

    void spi_mode(uint8 CPOL,uint8 CPHA,uint8 sample)
    {
    SSPCON1bits.CKP=CPOL; SSPSTATbits.CKE=1-CPHA;
    SSPSTATbits.SMP=sample;
    }

    ResponderEliminar