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:
- 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).
- 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").
- 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.
- 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:
- Reusar un código que andaba por ahí y que funciona.
- 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.
- 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)
unsigned char smp_phase)
sync_mode:
--> master: SPI_FOSC_4, SPI_FOSC_16, SPI_FOSC_64, SPI_FOSC_TMR2 (clock)
--> 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_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)
--> 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:
- Inmediatamente x se transfiere al verdadero registro de desplazamiento del puerto SSPSR y el buffer SSPBUF queda vacío, esperando los datos de llegada.
- Se inicia la transmisión (y simultánea recepción) de datos.
- 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)
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;
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).
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.
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:
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.
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:

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:
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:
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
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
}
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.
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.
ResponderEliminarEl código completo de este ejemplo lo tienes enlazado al principio del artículo.
EliminarUsa 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
Magnífica explicación, por la claridad con que has expuesto el tema. Un gran aporte!!
Eliminartengo una duda,
ResponderEliminarcuando 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?
No entiendo tu pregunta ni muchos de tus acrónimos. ¿Qué es SPDR, amm, interrupción ISP?
EliminarSi se trata de ver como un slave devuelve unos datos eso es dependiente del protocolo establecido para el dispositivo en cuestión.
Antonio
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
ResponderEliminarAlgun tutorial basico para I2C me podrias aconsejar. muchas gracias por adelantado un gran trabajo
ResponderEliminarHola Antonio,
ResponderEliminarTe 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!!!!!!
Hola Antonio,
ResponderEliminarTe 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!!!!!!
En realidad ejecuta cada instruccion cada 4 cicos de reloj que es un ciclo maquina y debes considerar la Fosc es decir los 48Mhz
Eliminarhola
ResponderEliminarHermano 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:
ResponderEliminarD:\2MPLAB\C_18\SPI\tipos.h:7:Error: syntax error
Si me puedes ayudar estare agradecido.
Excelente, ha sido de gran ayuda. Muchas Gracias
ResponderEliminarEXCELENTE 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.
ResponderEliminarHola...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?
ResponderEliminarBuena 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!
ResponderEliminarHola 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
ResponderEliminarHola , una pregunta no se que quiere decir el termino: uint8 en esta parte del codigo C para C18
ResponderEliminarvoid spi_mode(uint8 CPOL,uint8 CPHA,uint8 sample)
{
SSPCON1bits.CKP=CPOL; SSPSTATbits.CKE=1-CPHA;
SSPSTATbits.SMP=sample;
}