Está en la página 1de 37

Modulación PWM (Pulse Width Modulation)

Nuestro controlador (digital) eventualmente tendrá que interactuar con el mundo exterior
(analógico). Al margen de usar dispositivos externos (convertidor digital analógico o DAC)
muchos microcontroladores cuentan con un módulo PWM (Pulse Width Modulation) que puede
usarse (con ciertas limitaciones) para mandar "ordenes" analógicas.

En este tutorial describiremos en que consiste la modulación por ancho de pulsos (PWM),
veremos las rutinas disponibles en C18 y, como hacemos habitualmente, describiremos los
registros asociados y su funcionalidad.

Aprovecharemos los conocimientos adquiridos para escribir una rutina para inicializar el
módulo PWM especificando la frecuencia deseada y escribiremos un pequeño programa para
usar los dos módulos PWM de un PIC para crear transiciones de colores en un LED bicolor.

En una entrada posterior aplicaremos lo que hemos aprendido a una aplicación más
interesante usando el módulo PWM como conversor DAC para un archivo de audio.

Archivos de código asociados a esta entrada: pwm1.c y pwm2.c

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

Descripción de la modulación por ancho de pulso (PWM)

En algunos de los tutoriales anteriores usábamos un truco para estimar la ocupación del PIC.
Consistía en poner a 1 un cierto pin mientras estábamos haciendo una cierta tarea. Luego,
nos bastaba con medir el voltaje medio (con un voltímetro) en dicho pin. Dicho voltaje
(dividido por los 5V de alimentación) nos daba el % del tiempo que el pin estaba alto.

La modulación PWM consiste precisamente en eso. El módulo PWM del micro genera una onda
cuadrada con una frecuencia dada (típicamente bastante alta, por ejemplo 10 KHz). Luego
nosotros podemos ir cambiando el ciclo de trabajo (% del periodo en ON) de la señal:

 Siusamos un dispositivo externo con un ancho de banda suficiente (p.e. un osciloscopio)


veremos la señal al completo, esto es, la modulación rápida (o portadora) de 10 KHz y las
variaciones más lentas (señal a transmitir) del ciclo de trabajo.

Si por el contrario aplicamos un filtro paso-bajo a la señal PWM, los cambios rápidos (10 KHz)
de la señal se eliminarán y simplemente veremos los cambios lentos del ciclo de trabajo,
observando un voltaje "medio" entre 0 y 5V dependiendo del ciclo de trabajo (0% -> 100%)
programado.

No siempre es necesario implementar un filtro paso-bajo de forma explícita. Muchas veces


usamos un dispositivo externo (voltímetro, motor, etc) con suficiente "inercia", de forma que
no es capaz de seguir los cambios rápidos de la señal (los 10 KHz). Por así decirlo el
dispositivo (motor, altavoz) lleva incorporado su propio filtro paso-bajo.
Lo que hemos conseguido es una especie de conversor digital analógico que nos permite
traducir una orden digital (ciclo de trabajo del periodo PWM) en una variable analógica (el
voltaje medio a la salida entre 0 y 5V).

Obviamente tendremos algunas limitaciones. Como queremos que desaparezca la frecuencia


de modulación (los 10 KHz) de antes los cambios (frecuencia) de la señal que queremos
transmitir deben ser lo suficientemente lentos (frecuencia baja) para que no desaparezcan
también en el filtrado paso-bajo (explícito o implícito) de nuestro dispositivo.

El módulo(s) PWM de un PIC

El parámetro fundamental de una modulación PWM es la frecuencia (o su inverso el periodo)


de modulación. En los PIC dicha frecuencia es programable (con ciertas limitaciones) en base
a varias variables:

  La frecuencia del oscilador principal Fosc


 El pre-scaler (PRE) o divisor previo del timer TMR2 que puede tomar los valores 1:1, 1:4 o 1:16
  El registro PR2 (0-255) asociado al timer TMR2

La frecuencia PWM responde a la fórmula:  


                                    
                        F_pwm  =  F_osc / [4 x PRE x (PR2+1)] 

o lo que es lo mismo, el periodo del PWM será el inverso de dicha frecuencia:

                        T_pwm =  [ (PR2+1) x 4 x PRE ]  x Tosc

El valor máximo del divisor previo PRE es 16 y el de (PR2+1) es 256. Por lo tanto la
frecuencia PWM más baja posible será Fosc/16384. Para un oscilador de 20 MHz tenemos una
Fpwm mínima de 1.22 KHz  (20000/16384).

Notad que el módulo PWM usa el timer TMR2, por lo que éste no podrá usarse como
temporizador de propósito general mientras se esté usando PWM. Si que es posible usarlo (y
ahorrarnos gastar otro timer) si queremos hacer saltar una interrupción cada cierto tiempo. El
postscaler del TMR2 no tiene efecto sobre la frecuencia PWM, pero si influye sobre cuando
salta (si está habilitada)  la correspondiente interrupción (TMR2_flag). Si por ejemplo el post-
scaler  es 1:16 entonces la interrupción del TMR2 saltará cada 16 periodos del PWM.

Lo primero que tenemos que hacer para usar el módulo PWM es habilitarlo indicando que va a
usarse como generador de una onda PWM, ya que dicho módulo es compartido con otras
funciones (Capture/Compare). La forma de hacerlo es poner a 11XX los 4 bits menos
significativos del registro CCP1CON. Los PIC18 suelen tener 2 módulos PWM por lo que
existe un segundo registro CCP2CON.

Podemos habilitar uno o los dos módulos independientemente. Sin embargo, como ambos
usan el registro PR2 y el timer TMR0 como base de tiempos, la frecuencia programada será la
misma en ambos módulos.

Lo que si es posible variar por separado es el ciclo de trabajo (duty cicle o DC) de cada
módulo. El ciclo de trabajo se codifica con un número de hasta 10 bits (0-1023) almacenado
de la siguiente forma:

CCPR1L : 8 bits más significativos del ciclo de trabajo.

CCP1CON.DC1B0 y DC1B1  (bits 5 y 6 de CCP1CON): Guardan los 2 bits menos


significativos.
  
La programación del % ON del segundo módulo es similar pero usando los registros CCP2CON
y CCPR2L.

Con los 10 bits dados el ciclo de trabajo se podrá especificar en principio con 1024 niveles (0
corresponde a 0%  y 1023 al 100%). Sin embargo, los valores válidos pueden ser menores
que los 1024 posibles.

La razón es que el valor de DC (duty_cicle) determina el tiempo que la señal se mantiene alta
(ON) de la forma:

                             T_on    =  [ DC x PRE]  x  Tosc

Recordando que el tiempo total del periodo es:        

                            T_pwm =  [ (PR2+1) x 4 x PRE ]  x Tosc

Comparando ambas fórmulas y siendo obvio que el tiempo total ON no puede exceder el
tiempo total del periodo tenemos que el valor máximo de DC es (PR2+1) x 4. Por lo tanto,
aunque podemos dar a DC cualquier valor entre 0 y 1023 está claro que en realidad debemos
limitarnos al rango [ 0, (PR2+1)x4 ]. Valores más altos van a hacer que Ton > Tpwm, o lo
que es lo mismo, la señal PWM se mantiene alta todo el rato (100%).

En resumen, para mantener la máxima resolución (10 bits) a la hora de especificar DC es


preciso usar PR2=255. Para una cierta frecuencia del oscilador Fosc podemos optar por tres
frecuencias con la máxima resolución:

Registro PRE (divisor previo F_pwm Para Fosc = 20


PR2 de TMR2) MHz
255 1 Fosc / 19.75 KHz
1024
255 4 Fosc / 4.88 KHz
4096
255 16 Fosc / 1,22 KHz
16384

Frecuencias por debajo de Fosc/16384 no son posibles porque los valores de PR2 y PRE están
ya en su máximo posible.

Frecuencias por encima de Fosc/1024 son posibles pero a costa de bajar PR2 y por lo tanto
disponer de menor resolución para el ciclo de trabajo. Por ejemplo si aceptamos trabajar con
8 bits (valores de DC de 0 a 255) podemos llegar a una frecuencia de:
                                                                                                                                        
      
          F_pwm  =  F_osc / [PRE x 4 x (PR2+1)] = Fosc/(1 x 256) = 78 KHz  

para un oscilador de 20 MHz.

La razón por la que para frecuencias muy altas no podemos especificar con tanta precisión el
ciclo es que el periodo empieza a hacerse muy pequeño. En el caso anterior (Fpwm =
Fosc/256) es obvio que en un ciclo del PWM sólo entran 256 ciclos del oscilador. Como es
imposible que el micro haga algo entre ciclos de reloj, está claro que sólo puede bajar la línea
del PWM en 256 puntos como mucho (esto es, con una resolución de 8 bits).

Valores de frecuencias intermedios son también posibles, pero de nuevo van a exigir
PR2<255. Por ejemplo para conseguir 10 KHz con un oscilador de 20 MHz
 Fosc / Fpwm =20000/10 = 2000 = 4 x PRE x (PR2+1) -> PRE x (PR2+1) = 500

lo que puedo conseguir con PRE=2 y PR2 = 249. Pero entonces el valor del ciclo de trabajo
(DC) tiene que moverse en el rango 0 a 4(PR2+1) = 1000. Con DC=1000 ya alcanzamos un
100% del ciclo de trabajo y valores superiores no tendrán ningún efecto extra.

En cuanto al pin de salida al que se manda la señal PWM, usualmente la salida PWM1 va al
pin RC2 y la del PWM2 al RC1 (notad el cambio 1-2). En algunos dispositivos (consultar
datasheet) es posible cambiar la salida de PWM2 a otro pin mediante un bit de configuración.

 Las rutinas básicas del compilador C18 para manejar los módulos PWM son las siguientes
(las declaraciones se encuentran en pwm.h):

OpenPWM1(uint8 periodo)          : habilita el módulo y hace PR2=periodo


SetDCPWM1(uint16 duty_cicle)   : establece ciclo de trabajo 0% -> 100%
ClosePWM1();                          : deshabilita modulo PWM

Notad que las rutinas anteriores no tocan el timer TMR2. Es responsabilidad del usuario
invocar a la rutina OpenTimer2 para fijar el valor del divisor o pre-scaler y arrancar el
temporizador.

El siguiente programa (código en pwm1.c) pone en marcha ambos módulos y va variando el


ciclo de trabajo de PWM1 entre 0 y DC_max (valor máximo = 1023, correspondiente a un
100%). Simultáneamente el ciclo de PWM2 se establece como el valor complementario. Tras
los #include (no olvidar añadir pwm.h, timer.h y delays.h) y los #pragma de configuración
habituales el programa principal es simplemente:

void main()
{
 uint16 DC_max, dd=0;
 int8 inc=1;
 
 DC_max=1023;

 OpenPWM1(255); OpenPWM2(255);   // Set PWM1 and PWM2 with PR2 = 255


 OpenTimer2(TIMER_INT_OFF & T2_PS_1_1); // Starts TMR2 with 1:1 prescaler

 while(1)
 {
  SetDCPWM1(dd); SetDCPWM2(DC_max-dd);   // Set complementary DC in PWM1 and PWM2
  dd+=inc;                               // Increase duty cicle
  if ((dd==DC_max) || (dd==0)) inc=-inc; // If we get to DC_max or 0 reverse direction.
  Delay10KTCYx(5);
 } 
}

Vemos que hemos usado PR2=255 y pre-scaler = 1. Esto nos da una frecuencia PWM de :

            Fpwm = Fosc/(4 x 256 x1) = 20000 KHz / 1024 = 19.5 KHz

El siguiente video es una captura de pantalla del osciloscopio monitorizando PWM1 y PWM2.
Se observa que el periodo del PWM (unos 51 usec, correspondientes a 19.5 KHz) no cambia y
es común a ambos canales. El tiempo en ON de PWM1 (arriba) es justo el tiempo OFF del
PWM2 (abajo) ya que los hemos programado para ser complementarios: dd y (DC_max-dd)

  
 
Los saltos que se observan en las transiciones del duty_cicle son debidas a la frecuencia
(baja) con la que se refrescan en pantalla los datos del osciloscopio. En el osciloscopio se ve
una variación gradual, como corresponde a un incremente de 1 en 1 en el ciclo de trabajo.
Vamos a cambiar la frecuencia haciendo OpenPWM1(199);OpenPWM2(199); lo que
corresponde a (PR2+1)=200 y a una frecuencia de 20000/(4x200) = 25 KHz.  En el siguiente
video vemos la captura del osciloscopio:

La frecuencia son justo los 25 KHz esperados. Sin embargo, se aprecia que algo va mal.
Ahora ambos canales no son complementarios. El canal PWM1 llega al 100% y permanece
allí, no empezando a bajar hasta después de un rato. El comportamiento de PWM1 y PWM2
no parece ser el que hemos programado.  La evolución del ciclo de trabajo antes describía la
función de la izquierda (como correspondía a nuestro programa), y ahora parece describir la
de la derecha.

La razón es que al haber bajado PR2 hemos subido la frecuencia (correcto) pero sin darnos
cuenta también hemos alterado el rango de valores posible para DC. El valor máximo para el
que alcanzamos e 100% es de 4x(200) = 800. Al llegar dd a 800, la señal PWM alcanza un
100% y se mantiene mientras dd sigue subiendo hasta 1023 y vuelve a bajar. Sólo cuando
volvemos a entrar en el rango [0 800] volvemos a notar variación.

En resumen, recordar que si PR2 no es 255 el valor máximo de DC no será 1024 sino
DC_max = 4 x (PR2+1).

Nuestras propias rutinas

Ahora que entendemos como funciona el módulo PWM y conocemos los registros
involucrados, vamos a escribir nuestras propias rutinas de manejo del PWM. En primer lugar
las rutinas para ajustar el duty cicle:

void set_pwm1(uint16 duty)
    {
     CCP1CONbits.DC1B0=(duty& 0x01); duty>>=1;  //Least Significant bit
     CCP1CONbits.DC1B1=(duty& 0x01); duty>>=1;  // 2nd Least Significant bit
     CCPR1L=(duty);  // 8 Most Significant bits
    }
   
void set_pwm2(uint16 duty)
    {
     CCP2CONbits.DC2B0=(duty& 0x01); duty>>=1;
     CCP2CONbits.DC2B1=(duty& 0x01); duty>>=1;
     CCPR2L=(duty);
    }

Vemos que sólo es cuestión de poner los 2 bits menos significativos del argumento en los bits
DC1b0 y DC1b1 de CCP1CON y los 8 más significativos en CCPR1L. Lo mismo para los
registros CCP2CON y CCPR2L para PWM2.
Obviamente, estas rutinas no aportan nada sobre las suministradas por C18, sólo nos
permiten confirmar que lo que se está haciendo no es nada complicado. 

Veamos una rutina con algo de "valor añadido". Era un poco incomodo tener que acordarnos
de configurar y arrancar TMR2 por separado. Vamos a escribir una rutina que combine la
configuración del módulo y el arranque del timer TMR2. Además, en vez de aportar como
argumentos los valores de PR2 y del prescaler de TMR2 vamos a especificar la frecuencia
Fpwm deseada (en KHz) y dejar que la rutina calcule y configure los registros adecuados.
Como siempre está rutina puede ser combinada con las del C18. Podemos usar esta rutina
para inicializar el módulo y luego usar las rutinas de C18 para fijar el ciclo de trabajo. El
código es el siguiente:

 uint16 setup_PWM(uint16 Fosc, uint8 Fpwm, uint8 ch)


 //Fosc -> F oscillator in KHz,   Fpwm -> desired Fpwm in KHz
 // ch  -> configure channel 1 (1), 2 (2) or both (3)
 // Returns max posible value of duty cicle
    {
     uint16 x, DC_max;
     uint8 pre;
     uint8 log2,pr2;
    
     x = Fosc>>1; x=(x/Fpwm)+1; x>>=1;  // Computes round((Fosc/4)/Fpwm)
   
     if (x>16384) {pre=16; pr2=255;}    // Requested Fpwm too low -> set Fpwm = Fosc/16384
     else
      {
       pre=0;
       while(x>256) {x>>=2; pre++;}  // Find pr2 and pre so that (pr2+1)*pre=(Fosc/4)/Fpwm
       pr2=(x-1);                    // pre 0,1,2 -> 1:1, 1:4, 1:16
      }
     
     if (ch&1) { TRISCbits.TRISC2=0; CCPR1L=0; CCP1CON = 0b00001100; } // SET channel 1
     if (ch&2) { TRISCbits.TRISC1=0; CCPR2L=0; CCP2CON = 0b00001100; } // Set channel 2 
   
     PR2=pr2;
     T2CON = 0b00000100 | pre;  // start TMR2 with prescaler pre and postscaler 1:1
      // T2CON = 0b0 1111 1 00 ;
        //         |    | |  |
        //         |    | |  |_ Prescale: 00 (1)  01 (4)  1X(16)
        //         |    | |____ TIMER2 on/off (1=on, 0=off)
        //         |    |______ PostScaler: 1:(bbbb+1) 0000: 1:1  1111: 1:16
        //         |___________ Not used
   
     DC_max = pr2; DX_max++; DC_max<<=2;  // 4 x (PR2+1)  // MAX value for Duty cicle
     return DC_max;    
    }

La rutina recibe la frecuencia del oscilador y la frecuencia PWM deseada (ambas en KHz,
uint16 para Fosc y uint8 para Fpwm) y el canal PWM que deseamos inicializar (1 para PWM1,
2 para PWM2 o 3 para ambos). Todo esto pensando en PICs con dos módulos PWM, aunque
sería fácilmente modificable para otros casos.  El tener una sola función para inicializar todos
los canales tiene sentido porque los parámetros calculados (PR2, pre-scaler) son comunes
entre canales.

La función calcula el valor de PR2 y PRE que consiguen la frecuencia pedida y los usa para el
registro PR2 y para configurar el divisor previo del TMR2. También arranca el timer por lo que
no es necesaria una llamada adicional.
Si la frecuencia requerida es demasiado baja se fija la frecuencia permitida más baja posible.
Como es posible que en PR2 resulte un valor < 255, la función devuelve el valor de DC que
corresponde a un 100% del ciclo ON.

Usando las nuevas rutinas podríamos reescribir el programa anterior como:

void main()
{
 uint16 DC_max,dd=0;
 int8 inc=1;
 
 DC_max=1023;  // Nominal Value for DC max

 DC_max=setup_PWM(20000,10,3); // Set both channels @ Fpwm=10 KHz for a 20 MHz oscillator

 while(1)
 {
  set_pwm1(dd); set_pwm2(DC_max-dd);
  dd+=inc;                            // Increase duty cicle
  if ((dd==DC_max) || (dd==0)) inc=-inc;// Reverse direction.
  Delay10KTCYx(5);
 } 
}
Vemos como para DC_max no usamos el valor nominal 1023 sino el que nos devuelve la
función setup_PWM.

La aplicación más sencilla que podemos ver del uso de PWM es modular la luminosidad de un
LED. Al contrario que con una lámpara incandescente no podemos atenuar un LED bajando su
voltaje ya que al ser esencialmente un diodo, pasará de no conducir (OFF) a conducir (ON)
con una muy pequeña variación de voltaje. Lo que podemos hacer con PWM es encenderlo y
apagarlo muy rápidamente (a la frecuencia del PWM). El tiempo ON del ciclo (duty)
determinará la luminosidad aparente del LED (en este caso el elemento integrador o paso
bajo es nuestro ojo, que es incapaz de apreciar como el LED se enciende y se apaga).

En el ejemplo siguiente usamos la salida de PWM1 y PWM2 para modular el color e intensidad
de un Led bicolor RG. Usaremos RC1 y RC2 conectados al positivo de los leds R y G y
pondremos el negativo común a tierra:

El diferente valor de las resistencias usadas (800K, 100K) es para compensar la mayor
eficiencia del LED rojo e intentar que la luminosidad de ambos LED estén equilibradas.

El código (pwm2.c) es muy similar al anterior, pero ahora los valores del ciclo de trabajo los
sacamos de un par de tablas (básicamente una oscilación sinusoidal dando más preferencia a
los niveles cerca del 0). El tamaño de ambas tablas corresponde a dos números primos y se
ha elegido así para que se de una mayor combinación de colores, antes de que empiecen a
repetirse las combinaciones. También se han definido un par de macros que incrementan los
respectivos punteros p1 y p2 a las tablas, haciéndolos voltear al llegar al final.

Como ambas tablas son de sólo lectura una posibilidad sería colocarlas en la memoria de
programa (usando el calificador const rom) para no gastar memoria de datos.   

#define N1 61
#define N2 59
uint16 duty1[N1]={ 
   121, 153, 192, 239, 294, 357, 428, 507, 590, 676, 761, 840, 909, 964,1001,1018,
  1012, 985, 939, 876, 801, 719, 633, 548, 467, 392, 324, 265, 215, 172, 137, 108,
    84,  65,  50,  38,  29,  22,  16,  11,   8,   5,   3,   2,   1,   0,   0,   0,
     1,   2,   4,   6,   9,  13,  18,  25,  33,  44,  57,  74,  95};

uint16 duty2[N2]={
   121, 155, 195, 244, 302, 369, 444, 526, 613, 702, 789, 867, 934, 983,1012,1018,
  1000, 961, 902, 829, 746, 658, 569, 484, 405, 334, 272, 219, 174, 137, 107,  83,
    64,  49,  37,  27,  20,  14,  10,   7,   4,   2,   1,   0,   0,   0,   1,   2, 
     3,   5,   8,  12,  17,  24,  32,  42,  56,  73,  95};

uint8 p1=0;
uint8 p2=0;

#define inc_p1 {p1++; if(p1==N1) p1=0; }


#define inc_p2 {p2++; if(p2==N2) p2=0; }

void main()
 {
  setup_PWM(20000,5,3);

  while(1)
   {
    set_pwm1(duty1[p1]); inc_p1;
    set_pwm2(duty2[p2]); inc_p2;
  
    Delay10KTCYx(5);
   }
 }

En la siguiente película podemos ver el resultado. A la derecha, los LEDs de la placa EasyPic6
correspondientes a RC1 y RC2 se van encendiendo y apagando. A la izquierda el LED Red-
Green conectado a ambas salidas va cambiando de color. 

El problema del video es que los sensores de las cámaras digitales son muy sensibles al rojo
(de hecho más al infrarrojo), saturandose el canal rojo y no apreciandose correctamente los
detalles de las transiciones de color.

Interrupciones (conceptos básicos)


Una interrupción es un aviso provocado por un módulo del PIC, por un cambio en el estado de
un pin o un recordatorio de que ha pasado un cierto tiempo. Como su nombre indica este
aviso interrumpirá la tarea que se este haciendo en ese momento y pasaremos a ejecutar una
rutina de servicio o gestión de la interrupción.

Veremos un repaso de los bits y registros de control asociados a las diferentes interrupciones,
como habilitarlas y como escribir rutinas de servicio (ISR). Crearemos definiciones
(#define) que nos permitirán operar con las interrupciones sin tener que recordar los
bits/registros asociados, a la vez que facilitarán la tarea de portar nuestro programa a otro
compilador y/o microcontrolador. 

Es importante familiarizarse con el manejo de interrupciones, ya que nos evita poder manejar
muchos tipos de eventos sin estar pendientes de ello. En sucesivos tutoriales veremos como
el uso de interrupciones nos permite aprovechar de forma mucho más eficiente los recursos
del PIC.

Archivos de código asociado a esta entrada:  int_defs.h   test_int.c 

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

Habilitación de interrupciones:

Antes de entrar en detalles sobre cada interrupción por separado hemos de describir un par
de bits (bits 7 y 6 del SFR INTCON) que tienen un efecto global sobre la activación de bloques
de interrupciones. 

INTCON.GIE  -> habilita (1) o deshabilita (0) todas las interrupciones.


INTCON.PEIE -> habilita (1) o deshabilita (0) las interrupciones asociadas a módulos
periféricos.

Por ejemplo, antes de poder usar la interrupción del temporizador TMR0 debemos
asegurarnos de que las interrupciones globales estén habilitadas (INTCON.GIE=1).  Si lo que
deseamos es usar la interrupción asociada a la recepción del puerto serie, tanto INTCON.GIE
como INTCON.PEIE deben estar a 1, ya que dicha interrupción está declarada como
periférica.

Para usar estos bits de una forma más conveniente incluiríamos los siguientes defines (en el
programa principal o bien en un fichero .h incluido en el proyecto):

// Global flags (without priority levels)


#define enable_global_ints  INTCONbits.GIE=1
#define enable_perif_ints   INTCONbits.PEIE=1
#define disable_global_ints INTCONbits.GIE=0
#define disable_perif_ints  INTCONbits.PEIE=0
Hay varias razones para usar estas (o similares definiciones):
 Siempre es más facil recordar enable_global_ints que acordarse de que hay que poner
a 1 el bit GIE del registro INTCON.
 Si cambiamos a otro compilador donde la forma de direccionar los bits de los registros
es diferente, basta cambiar las definiciones (esto es, usar un fichero .h distinto). En el caso
p.e. del compilador MikroC Pro en vez de INTCONbits.GIE usaríamos INTCON.GIE.
 Si cambiamos a otro controlador, puede que los bits correspondientes cambien de
registro y/o nombre. De nuevo, un cambio en el fichero de encabezamiento hace que no sea
preciso cambiar el resto del código.

Bits asociados a cada interrupción:

Además de los bits anteriores que afectan de forma global a las interrupciones, para cada
fuente de interrupción hay tres bits asociados: 
 IE (interrupt enable): determina si la interrupción está o no habilitada. Si no lo está,
aunque la condición de la interrupción se cumpla,  la interrupción no se producirá.
 IF (interrupt flan): indica si la condición de la interrupción se ha producido.  Es
responsabilidad del usuario borrar dicho bit antes de regresar de la ISR.
 IP (interrupt priority): indica si la prioridad asociada a la interrupción es alta (1) o
baja (0).  Obviamente, solo tiene efecto si está activado el modo de niveles de prioridad.
Consideremos por ejemplo la interrupción asociada la temporizador TMR0. Un temporizador
es simplemente un contador que se incrementa con cada ciclo máquina (4 ciclos del
oscilador). Dicho contador puede configurarse como de 8 o 16 bits. Cuando dicho contador
rebosa y pasa de 0xFF a 0x00 (modo 8 bits) o de 0xFFFF a 0x0000  (modo 16 bits) la
bandera IF asociada a la interrupción del TMR0 se pone a 1.  Para activar o desactivar la
interrupción,  establecer su prioridad o acceder al valor de su bandera de interrupción
definiríamos los siguiente macros:

#define enable_TMR0_int   INTCONbits.TMR0IE=1
#define disable_TMR0_int  INTCONbits.TMR0IE=0
#define TMR0_flag         INTCONbits.TMR0IF
#define set_TMR0_high     INTCON2bits.TMR0IP=1
#define set_TMR0_low      INTCON2bits.TMR0IP=0

Las dos primeras líneas activan o desactivan la interrupción a través del correspondiente bit
de IE. La tercera define el flag (IF) asociado al temporizador como TMR0_flag.  Finalmente las
dos últimas establecen la interrupción del TMR0 como de alta o baja prioridad, modificando el
correspondiente bit IP.

Algunas interrupciones pueden tener algunos bits extras dedicados. Por ejemplo, en la
interrupción INT0 (asociada a detectar cambios en el pin RB0) podemos especificar si la
interrupción salta al pasar de nivel alto a bajo o viceversa.

Como se observa en los ejemplos anteriores todos los bits que hemos visto hasta ahora están
en los registros especiales INTCON e INTCON2. Al ir añadiendo más fuentes de interrupción
se han tenido que crear nuevos registros  para interrupciones de periféricos (PIEx,PIRx),
lugares donde almacenar los bits de prioridades (IPRx), etc.

Podemos repetir las definiciones anteriores para el resto de las interrupciones. Cada
interrupción tendría una serie de definiciones similares a las listadas anteriormente para
TMR0.  Por ejemplo, para la interrupción INT1 tenemos definidas:

#define enable_INT1_int  INTCON3bits.INT1IE=1
#define disable_INT1_int INTCON3bits.INT1IE=0
#define INT1_low2high    INTCON2bits.INTEDG1=1
#define INT1_high2low    INTCON2bits.INTEDG1=0
#define INT1_flag        INTCON3bits.INT1IF
#define set_INT1_high    INTCON3bits.INT1IP=1
#define set_INT1_low     INTCON3bits.INT1IP=0
De esta forma tendríamos una forma sencilla de activar una u otra interrupción, consultar sus
banderas y establecer su prioridad sin tener que recordar la posición de los diferentes bits en
los registros. Lo usual es guardar dichas definiciones en un fichero que incluiríamos en
nuestros proyectos. El fichero que usare en sucesivos programas es int_defs_C18.h.

Para el resto de las interrupciones tenemos similares definiciones, sin más que cambiar TMR0
o INT1 por el código de la interrupción en cuestión. Entre las interrupciones que más
usaremos podemos destacar:

Temporizadores/Contadores  (TMR0, TMR1, TMR2, TMR3) 

  La interrupción se produce al rebosar y pasar por 0 los contadores asociados.

Cambios en pines  (INT0, INT1, INT2, RB)

INTx: La interrupción se produce con un cambio en el nivel de los pines RB0, RB1 y RB2
respectivamente. Es posible establecer si la interrupción se produce en el flanco ascendente o
descendente (ver INT1_low2high e INT1_high2low en los ejemplos citados antes).

RB: se produce ante cualquier cambio en los pines RB4 a RB7. Al contrario que las anteriores
no es posible especificar un pin o una transición determinada.

Puerto serie  (Rx,Tx)

 RX, interrupción producida con la recepción de un carácter.

 TX, interrupción de transmisión que nos avisa cuando el buffer de transmisión está libre para
mandar un nuevo carácter.

Conversor Analógico/Digital (AD)

 AD, nos avisa cuando se ha completado una conversión Analógica-Digital.

Rutinas de servicio de interrupciones  (ISRs)

Como hemos visto, para que una interrupción se produzca, los siguientes bits deben estar a
1:

GIE -> Habilita todas las interrupciones

PEIE -> Necesario (además de GIE) para las interrupciones periféricas.

El bit IE (int enable) de la interrupción deseada, habilitando dicha interrupción en particular.

El bit IF (int flag) de la interrupción, indicando que la condición de la interrupción se ha


producido.

Es responsabilidad del usuario activar los tres primeros bits con los correspondiente
comandos enable que hemos definido. Por ejemplo para activar la interrupción del timer 0,
TMR0, haríamos:

 enable_global_ints;  // Enable global ints


 enable_TMR0_int;     // Enable TMR0 int

Si deseáramos activar la interrupción de recepción del puerto serie (RX) haríamos:


 enable_global_ints;  // Enable global ints
 enable_perif_ints;   // Enable peripheral ints
 enable_RX_int;     // Enable RX int

Con los tres primeros bits en 1, cuando se cumpla una condición de interrupción el
microcontrolador pondrá a 1 el correspondiente bit IF y la interrupción se producirá. El
microcontrolador pasará el control a la posición 0x0008.

Cuando esto suceda es fundamental que en dicha posición tengamos un código válido para
gestionar la interrupción. A dicho código se le denomina rutina de servicio de la interrupción
(Interrupt Service Routine o ISR).

Las funciones mínimas de una ISR son las siguientes:

    Determinar que interrupción se ha producido, ya que en principio todas las interrupciones se


procesan en la misma rutina. Esto lo haremos chequeando que IF bit está a 1. Obviamente no
será necesario chequear las flags de todas las interrupciones, sino sólo de aquellas que estén
habilitadas.

    Una vez determinada que interrupción en particular ha sucedido, ejecutar el código que
sirve a dicha interrupción.

    Finalmente, antes de volver, poner a 0 la correspondiente bandera IF. Si no lo hacemos, en


cuanto devolvamos el control a la rutina principal se volverá a verificar la condición de
interrupción y volveremos a entrar en la ISR.

Hemos dicho que ante una interrupción el PIC saltará a una dirección determinada. ¿Cómo
podemos poner el código de la ISR en la posición de memoria adecuada? Eso va a depender
del compilador.  Usualmente los compiladores nos facilitan dicha tarea teniendo una rutina de
nombre predefinido para las interrupciones. Si se define una rutina con dicho nombre el
compilador la pondrá en la posición adecuada al compilar por lo que se ejecutará  

Veremos como lo hacen el C18 de Microchip y el MikroC Pro de Mikroelektronica.

Compilador C18:

#pragma interrupt high_ISR
void high_ISR (void)
{
 if (TMR0_flag) // ISR de la interrupcion de TMR0
  {
   PORTCbits.RC0=1; Delay10KTCYx(255); PORTCbits.RC0=0;
   TMR0_flag=0;
  }
}

// Code @ 0x0008 -> Jumps to ISR routine


#pragma code high_vector = 0x0008
  void code_0x0008(void) {_asm goto high_ISR _endasm}
#pragma code

La primera parte del código usa  #pragma interrupt para indicarle al compilador que la


rutina siguiente es una interrupción (una interrupción debe volver con una instrucción
especial, Return from Interrupt, en vez de un Return normal). El nombre usado high_ISR() es
arbitrario y podemos cambiarlo por el que queramos.

La rutina high_ISR() se ejecutará al producirse cualquier interrupción. Por eso lo primero


que hacemos es chequear que interrupción ha causado la llamada a la ISR, consultando las
posibles IF de las interrupciones posibles (las habilitadas). En este caso que tenemos una sola
interrupción habilitada podríamos evitar dicha comprobación.
Una vez verificado (TMR0_flag==1) que efectivamente estamos ahí por una interrupción del
TMR0 simplemente escribimos el código que deseemos ejecutar.  ¿Qué es lo que se hace en
la ISR del TMR0? Poca cosa: levantamos el pin RC0, esperamos 255x10000 ciclos de reloj
(unos 0.5 segundos) y ponemos de nuevo a 0 el pin RC0 antes de regresar.  El delay podría
representar el tiempo que estaríamos ocupados haciendo lo que se supone que tendríamos
que hacer cada cierto tiempo. Mientras veamos encendido RC0 es que estamos dentro de la
ISR. Muy importante: antes de regresar reseteamos la bandera (TMR0_flag=0) para evitar
entrar de nuevo en la interrupción.

La segunda parte del código usa la directiva #pragma code que nos permite posicionar un


código en una dirección de memoria dada. Se define una nueva función code_0x0008 (de
nuevo un nombre arbitrario) cuyo código es muy sencillo, simplemente un salto a la rutina
high_ISR anterior.

Se ve que C18 deja ver lo que realmente está pasando. En la posición 0x0008 no podríamos
meter una rutina muy grande (al fin y al cabo en 0x0018 podríamos tener otra rutina, la de la
interrupción de bajo nivel). Lo único que metemos es un salto a la verdadera rutina de
interrupción high_ISR().

MikroC Pro:

En el caso del compilador MikroC Pro, hay una rutina de nombre reservado, interrupt( ).  
Veamos como escribiríamos el código anterior para MikroC Pro.

void interrupt(void)  // High priority interrupt


{
 if (TMR0_flag)      // TMR0 ISR
  {
   PORTC.RC0=1; delay_ms(600); PORTC.RC0=0;
   TMR0_flag=0;
  }
}

Vemos que el código es más sencillo. Basta declarar la función (reservada) interrupt y el
compilador se ocupa de todo. Por debajo el compilador MikroC Pro hará lo mismo que el C18
(escribir el código en algún sitio y poner un salto en 0x0008 a esa dirección), pero la ventaja
(o inconveniente según se mire) es que hace que el usuario se despreocupe de los detalles.

Nosotros seguiremos a partir de ahora la programación en C18. De hecho, las únicas


diferencias eran en la forma de declarar las interrupciones, ya que el programa principal sería
idéntico en ambos compiladores.

Lo único que tenemos que hacer en el main() es configurar de forma adecuada TMR0 para
que salte en el tiempo deseado. Para ello damos cierto valor al registro T0CON (veremos
detalles de cómo se hace en la entrada dedicada a los temporizadores).  Tras programar el
temporizador lo único que queda es habilitar las interrupciones globales y la interrupción del
TMR0 en particular: 

void main() {

  TRISC=0; PORTC=0x00;   // PORTC output


  T0CON = 0b10000110; // Starts TMR0, rolls over every 128x65536 cycles = 1.67 sec @ 20 Mhz
 
  enable_TMR0_int;    // Enable Timer0 interrupt
  enable_global_ints; // Enable global ints
 
  while(1);
}

Vemos que en el bucle principal no hacemos nada. Sin embargo, al ejecutar el programa
veremos como el pin RC0 parpadea: cada 1.6 segundos entra la interrupción en la que RC0
se pone en 1 (LED encendido) y permanece así durante 600 msec, apagándose hasta el
próximo ciclo.

La moraleja: aunque nuestro programa principal no este haciendo nada, el microcontrolador


puede estar haciendo cosas a través del código asociado a las interrupciones habilitadas.
Veremos la aplicación de este principio en otras aplicaciones.

Podéis comprobar que si comentáis cualquiera de las dos líneas enable el LED en RC0
permanece apagado, ya que la interrupción no se ejecuta al no estar habilitada (o al no estar
habilitadas la interrupciones de forma global).

Es importante que siempre que habilitemos una interrupción tengamos definida la


correspondiente función de manejo de interrupciones (posicionada en 0x0008). Si no es así,
al producirse la interrupción y saltar el programa a la dirección 0x0008, al no encontrar un
código valido en esa dirección el comportamiento es impredecible.
Conversor ADC
En otro tutorial vimos como el módulo PWM funcionaba como una especie de conversor digital
-> analógico (DAC). El conversor analógico-digital (ADC) que equipa a muchos
microcontroladores hace justo lo contrario, convirtiendo un voltaje analógico externo en un
número, con el que podremos operar.

Como siempre empezaremos con una introducción sobre los fundamentos de un conversor AD
y los registros usados para su manejo en los PIC. Tras entender el proceso, presentaremos
las rutinas de C18 para el manejo del ADC y escribiremos nuestras propias funciones, que nos
permitirán optimizar el rendimiento del ADC y no depender de un compilador en particular.

En la siguiente entrada veremos como al igual que pasaba con el puerto serie, usando
interrupciones podemos optimizar el rendimiento del PIC mientras estamos usando el
conversor AD.

Codigo asociado a esta entrada:  adc1.c

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

Conceptos básicos de un ADC:

 Un ADC convierte mide el voltaje V en un pin (que tendrá que estar declarado como
entrada con el correspondiente registro TRISA) y lo convierte en un número. El voltaje se
mide en referencia a un voltaje mínimo, Vref(-) , y a un voltaje máximo, Vref (+):

                        V_norm = ( V – Vref(-) ) / (Vref(+) –Vref(-) )

 La fórmula anterior corresponde a un voltaje normalizado. Si el voltaje V alcanza el


máximo (Vref+) tendremos una salida de 1 y si se queda en el mínimo (Vref-) una salida
de 0.
 Normalmente Vref- suele ser Vss=GND=0V y Vref+ = Vcc = 5V, pero pueden usarse
otros voltajes de referencia. Si por ejemplo queremos medir una señal que sabemos que
oscila entre 2 y 3 voltios usaríamos Vref-=2 y Vref+=3. Así aprovecharíamos mejor el
rango dinámico del conversor.

 Como el microcontrolador no va a manejar números en coma flotante, el voltaje


normalizado se expresa con un entero, convirtiendo el intervalo real [0,1] en el intervalo
de niveles enteros entre [0 y Nmax-1]. La resolución del ADC es una característica
fundamental y nos dice el número de niveles con los que cubrimos el intervalo [0,1]. Por
ejemplo, en los PIC solemos tener una resolución de 10 bits, que representan 2^10=1024
niveles. El intervalo real [0,1) se aplicaría al intervalo [0,1023]. Si asumimos un rango de
5V, tendremos que la resolución de cada nivel es de r=5/1024 V=4.88 mV. Según la
documentación de Microchip (esto puede variar para otros microcontroladores) cualquier
voltaje entre [0 y r] (o por debajo de 0, lo que corresponde a  V<Vref-) se cuantificaría en
el nivel 0. Entre r y 2r tendríamos una salida de nivel 1. Así hasta llegar a nivel 1023 que
cuantificaría voltajes por encima de 1023r = 1023x 5/1024 = 4.995V. Como se ve,
voltajes por debajo de Vref- o por encima de Vref+ son posibles y se cuantifican como
nivel mínimo 0 o máximo, 1023.  Niveles por debajo de 0V o por encima de la tensión de
alimentación (normalmente 5V) pueden ser dañar el PIC.

Nivel Voltaje
0 <r
1 [r,2r]
2 [2r,3r]
… …
1023 >1023r

 Aunque un PIC puede tener del orden de 8-12 posibles canales (pines) de entrada
analógica, solo tiene normalmente un único módulo ADC, lo que significa que no podemos
tomar medidas simultáneas de varios canales. Si es necesario, lo que podemos hacer es ir
conectando (seleccionando) los sucesivos canales al ADC para ir midiendo sus voltajes.
 El proceso de una conversión ADC se divide en un tiempo de adquisición Ta (durante
el cual un condensador interno se carga al voltaje exterior) y un tiempo de conversión
Tc (durante el cual se desconecta el pin exterior y se cuantifica el voltaje del
condensador).
 El tiempo de adquisición Ta depende de las características eléctricas del PIC (en
particular de la capacidad del condensador). Si no se respeta este tiempo, el condensador
no habrá alcanzado el nivel del voltaje exterior y la medida será incorrecta. Los datasheet
de los PIC indican los Ta recomendados para diversas familias.
 Otro aspecto importante de la adquisición es recordar que Microchip recomienda
una impedancia máxima para el sensor (o lo que sea) suministrador de voltaje que está
conectado a nuestro pin. En la mayoría de los casos se recomienda que dicha impedancia
no supere los 2.5 Kohmios. Según entiendo con mis escasos conocimientos de
electrónica, impedancias mucho mayores harían que (debido a corrientes de pérdida
siempre presentes) el condensador no llegara nunca a cargarse al voltaje de entrada o
tardase mucho en hacerlo, lo que daría lugar a que la medida posterior fuese incorrecta.
Si la impedancia de nuestra fuente es muy superior a la recomendada deberíamos
plantearnos insertar un driver.
 El tiempo de conversión Tc depende fundamentalmente del número de bits del
conversor. La unidad básica es el llamado Tad, aproximadamente el tiempo necesario para
ganar un bit adicional. La conversión total tarda entre 11 y 14 Tad (contando con la
descarga final del condensador para estar listo para otra medida. El reloj del ADC se debe
ajustar (como una fracción del oscilador principal) para que dicho Tad no sea inferior a un
valor mínimo especificado en los datasheet. Por ejemplo para la familia PIC18F252/452 el
Tad mínimo es de 1.6 usec y Tc = 14 Tad. En cambio para la familia PIC18F2520/4520
tenemos un Tad mínimo de 0.75 usec y un Tc = 11 Tad.

Un comentario final: aunque es muy conveniente tener un ADC integrado dentro del
microcontrolador, si lo pensamos en términos de nivel de ruido, etc. las cercanías de un
microcontrolador con la líneas de reloj, rápidos cambios en las líneas digitales,
comunicaciones, etc. no es el mejor sitio para ubicar un ADC. Esto quiere decir que si
estamos muy interesados en la precisión, tal vez sería conveniente considerar un ADC aparte,
que nos comunicaría los datos adquiridos a través de SPI o similares. Una vez dicha la
advertencia nos centraremos en como usar de la forma más eficiente el ADC de un
microcontrolador (en este caso un PIC).
Los registros de control del ADC en un PIC (familia 18F 2520/4520)

Para estos ejemplos voy a usar código para la familia PIC18F2520/4520 con algunos
comentarios sobre la familia 252/452. El conversor AD es una de los módulos de un PIC
donde hay más diferencias entre modelos: distintos número de canales AD, diferentes formas
de programar que canales son digitales o analógicos, la posibilidad o no de programar
tiempos de adquisición, etc.

Esto hace que la configuración del ADC sea bastante dependiente del modelo y siempre sea
bueno tener a mano la documentación ante comportamientos inesperados. Por ejemplo, los
registros asociados al puerto serie son los mismos entre una amplia gama de dispositivos. Por
el contrario, es fácil que el número de registros de configuración cambie entre modelos.

La familia PIC18F2520 tiene 3 registros de control asociados al ADC (ADCON0, ADCON1 y


ADCON2). Por el contrario, la familia 252/452, al tener menos funcionalidades, sólo necesita
2 (ADCON0 y ADCON1). De todas formas, los nombres y funciones de los bits comunes serán
los mismos que los aquí descritos, aunque pueden estar situados en diferentes registros.

Además de los registros de control tenemos otros dos registros ADRESH y ADRESL (AD result
High/Low) donde se guarda el resultado de la conversión, pero estos si son comunes entre
dispositivos.

Veamos un resumen de estos registros (de nuevo, los datasheet de Microchip son los mejores
manuales de usuario):

Registro ADCON0: permite seleccionar (CHS) el canal del que tomaremos la medida.


También tiene el bit que habilita el conversor (ADON) y que arranca (GO) el proceso de
conversión.

Bit # Bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0


Nombr X X CHS3 CHS2 CHS1 CHS0 GO ADON
e
Función No  No Selección de canal:  Lanza ON/OFF
usado  usado desde 0 (0000) a 12 (1100) ADC

ADON: Encendido (1) o apagado (0) del modulo ADC.

GO  : Se pone a 1 para iniciar el proceso de conversión. Cuando la conversión ha terminado


el módulo lo pone a 0. Así es como sabemos que podemos recoger el resultado (en
ADRESH/ADRESL)

CHS: selecciona cual de los posibles 13 canales analógicos se conecta al ADC para medir.

Registro ADCON1: dedicado a seleccionar cuales de los posibles canales se van a usar como
entradas analógicas y a programar el uso o no de voltajes de referencia externos (distintos de
los valores 0 y 5V por defecto)

Bit # bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0


Nombr X x VCFG1 VCFG0 PCFG3 PCFG2 PCFG0 PCFG0
e
Función No No Uso (1) de  Reparto canales 
usado usado Vref (+),Vref(-) digitales / analógicos

VCFG1: por defecto (0) usamos Vss=Gnd=0V como Vref-. Si es 1 se tomara como Vref-   el
voltaje presente en AN2.

VCFG0: Lo mismo que el anterior pero para Vref+. Por defecto usaremos Vdd (alimentación,
típicamente 5 o 3.3V) como Vref+. Si es 1 se usa el voltaje presente en AN3 como Vref+.

PCFG:  Posiblemente no necesitaremos todos los posibles canales analógicos. Estos bits
permiten decidir cuales de los pines se dedican a canales analógicos y cuales permanecen
como canales digitales. Los valores posibles van desde 0000 (todos analógicos) a 1111 (todos
digitales).

Los canales analógicos se denotan en la documentación como AN0, AN1, AN2, etc. Los
dispositivos con 8 canales (252/452) los reparten entre el puerto A y el E. Los que tienen 13
canales usan también parte del puerto B (2520/4520). Mirar la documentación, porque el
orden no es consecutivo. Por ejemplo, para el 4520

Cana AN AN AN AN AN AN AN AN AN AN AN1 AN1 AN1


l 0 1 2 3 4 5 6 7 8 9 0 1 2
Pin RA RA RA RA RA RE RE RE RB RB RB1 RB4 RB0
0 1 2 3 5 0 1 2 2 3

Registro ADCON2: información para determinar los tiempos Ta y Tc del conversor y la


forma en la que el resultado (10 bits) se guarda en ADRESH:ADRESL (16 bits).

Bit # bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0


Nombr ADFM x ACQT2 ACQT2 ACQT2 ADCS2 ADCS1 ADCS0
e
Función Format No Programación T Reloj del ADC
o usado adquisición

ADFM: Formateo del resultado. El conversor da 10 bits que se guardan en dos registros
(ADRESH:ADRESL) de 8 bits, por lo que 6 de los 16 bits estarán vacíos. Si ADFM=1 el
resultado está desplazado a la derecha, por lo que los 6 bits más significativos de ADRESH
estarán vacíos. Si es 0 se justifica a la izquierda y son los 6 bits menos significativos de
ADRESL los que están vacíos.  El caso ADFM=1 se suele usar cuando nos interesan los 10
bits. El resultado se puede pasar a un entero de 16 bits como: 
                                                                       res = (ADRESH<<8)+ADRESL;

El caso ADFM=0 se suele usar si sólo queremos los 8 bits más significativos. En ese caso
basta hacer: res = ADRESH;

ACQT: bits para la programación del T de adquisición. Recordar que el tiempo de adquisición
es el que debemos esperar después de seleccionar (conectar) un canal al ADC mientras se
carga el condensador cuyo voltaje posteriormente mediremos. Si escogemos 000 el usuario
es responsable de la espera (de unos pocos usec, especificada en la documentación) entre la
selección del canal (poner los bits CHS adecuados en ADCON0) y el inicio del proceso de
conversión (poner a 1 el bit GO de ADCON0).

El modo manual (000) era la norma entre familias anteriores, que de hecho no contaban con
estos bits de configuración. En la familia 2520/4520 es posible programar dicho tiempo de
espera. De esta forma tras seleccionar el canal podemos inmediatamente lanzar la conversión
(ADCON0.GO=1) y el módulo esperara el tiempo programado antes de iniciar la conversión.
Esto permite optimizar el aprovechamiento del ADC como veremos.

El tiempo de adquisición es la suma de varios factores, siendo los principales la capacidad del
condensador a cargar y el tiempo que tarda el amplificador del ADC en estabilizarse. Es
importante consultar el datasheet si queremos optimizar el muestreo porque podemos
encontrarnos bastantes diferencias entre diversos modelos.

ADCS: seleccionan la velocidad del reloj del ADC como una fracción (div = 2, 4, 8, 16, 32,
64) del reloj principal (aunque también es posible asociarlo a un oscilador RC independiente).
El inverso de la frecuencia del ADC determina el llamado Tad, que viene a ser el tiempo
dedicado a obtener cada bit del resultado:

         F_ad = Fosc / div        


         Tad = 1/F_ad =  div / Fosc  (en microsec si Fosc está en MHz)

Obviamente deberíamos escoger un divisor bajo para obtener la frecuencia más alta (Tad
más pequeño) posible para tardar lo menos posible en cada conversión. Sin embargo hay una
limitación: el Tad no puede ser menor que un cierto tiempo mínimo dependiente del modelo.
Por ejemplo para la familia 252/452 se recomienda que Tad sea como mínimo 1.6 usec. En
cambio para la 2520/4520 podemos bajar a 0.75 usec.

Eso significa que si tenemos un cristal de 20 Mhz, el divisor escogido sería:

1:32  si tenemos un 452, ya que Tad = div / Fosc = 32/20 Mhz = 1.6 usec = recomendado

1:16 si tenemos un 4520 ya que Tad = div / Fosc = 16/20 MHz = 0.8 usec > 0.75 usec
recomendados

Nuestro primer programa con el ADC

El proceso a seguir para una conversión (según se describe en la documentación de


Microchip) es el siguiente:
1. Configurar el ADC con la asignación de canales (PCFG), programar el reloj
del ADC (bits ADCS), seleccionar o no voltajes de referencia (VCFG), etc.
2. Una vez configurado, habilitar el ADC (ADON=1)
3. Escoger canal (bits CHS) a usar (el pin dado se conecta al condensador)
4. Esperar (delay) Ta mientras se carga el condensador (este paso puede ser
evitado programamos la espera)
5. Lanzar la conversión (GO=1)
6. Esperar a completar a que la conversión concluya (GO=0).
7. Extraer el resultado de ADRESH:ADRESL
8. Si hemos terminado con el ADC, apagarlo para reducir consumo.
Ahora que conocemos los detalles escribiremos el primer programa para un PIC18F4520
usando las rutinas de C18 y no tendremos dificultades en imaginar que está haciendo cada
rutina por debajo.

Las rutinas básicas para operar en C18 con el ADC pueden verse en este código ejemplo del
manual de C18:

  OpenADC(ADC_FOSC_16&ADC_RIGHT_JUST, ADC_INT_OFF&ADC_VREFPLUS_VDD&ADC_VREFMINUS_VSS,7); // (1)-(2)


  SetChanADC(ADC_CH0);     // Paso 3)                     
  Delay10TCYx(5);          // Paso 4) -> delay de adquisición
  ConvertADC();            // Paso 5)
  while( BusyADC() );      // Paso 6) -> delay de conversion.
  res = ReadADC();         // Paso 7)
  closeADC();              // Paso 8)
Vemos que C18 tiene casi una función para cada paso del proceso. Por ejemplo, el compilador
MikroC Pro es mucho más monolítico, lo que lo hace más simple pero menos adaptable. En
MikroC Pro, todo lo anterior se resume en una sola llamada:

 res = ADCRead(0);   que configura, selecciona canal 0, espera, lanza conversión y cuando
termina devuelve res.

Los parámetros que recibe OpenADC son esencialmente los valores de ADCON0, ADCON1 y
ADCON3. Como siempre la ventaja es que damos valores a dichos registros combinando
diversas banderas más fáciles de recordar. Para aquellos dispositivos con sólo disponen de
ADCON0 y ADCON1 la función sólo usa dos parámetros en vez de tres. Con SetChanADC
seleccionamos (ADC_CH0) AN0=RA0 como el pin cuyo voltaje vamos a medir.

Normalmente la configuración del ADC se hace una sola vez y luego se hacen sucesivas
conversiones (posiblemente de diferentes canales). Podemos agrupar el código anterior en
una función que reciba como argumento el canal a muestrear y haga todas las etapas
(aunque como veremos más adelante, en ciertas circunstancias es más eficiente "romper" la
función para no tener una función bloqueante):

uint16 ADC_read(uint8 ADC_channel)


 {
  uint16 ad_res;
      
  SetChanADC(ADC_channel);
  Delay10TCYx(5);
  ConvertADC();
  while(BusyADC());
  ad_res = ReadADC();
  return ad_res;
}

Una llamada al código anterior mide el voltaje del canal deseado una sola vez. Si el voltaje en
el canal analógico está cambiando rápidamente, querremos muestrearlo muchas veces por
segundo para seguir sus cambios. Supongamos que queremos tomar 2000 muestras por
segundo. A esto es a lo que se suele denominar una frecuencia de muestreo de 2000 Hz. Con
este ritmo, deberemos lanzar tomar una muestra cada 1/2000 seg = 0.2 msec = 200
usec/muestra.

El código quedaría:

 TRISB=0; TRISC=0; TRISAbits.TRISA0=1;


 while(1)
     {
      PORTCbits.RC0=1; res=ADC_read(ADC_CH0); PORTCbits.RC0=0;
      PORTB=res>>2;
      Delay10TCYx(250);
     }
  
Declaramos PORTB y PORTC como salidas y RA0 como entrada. Dentro del bucle, tras el
código de la conversión, simplemente esperamos 500 usec (2500 ciclos @ 20 MHz) antes de
una nueva conversión. Hemos dejado fuera la configuración del ADC (misma línea de antes) y
el apagado del módulo (porque lo estamos usando continuamente).
Para ver si está funcionando hemos conectado un potenciómetro en RA0 y una tira de LEDS
en PORTB, donde mostramos los 8 bits más significativos del resultado.  Moviendo el
potenciómetro veremos cambiar los LEDs.

La orden de poner RC0 a 1 al arrancar la conversión y de bajarlo al terminar es para controlar


el tiempo que tardamos. Con un osciloscopio podríamos ver claramente el tiempo dedicado a
la conversión como la parte en la que este alta la señal del pin RC0. Como ya hemos hecho
en otras ocasiones, incluso con un simple voltímetro podemos estimar dicho tiempo de forma
bastante aproximada. En particular para este programa he medido un voltaje en RC0 de
0.56V. El voltaje que el mismo voltímetro me da para Vcc es de 4.86V. El voltaje medido en
RC0 dividido por 4.86 representa el porcentaje en el que RC0 está en alto del total del
periodo. Por lo tanto podemos escribir que si t es el tiempo dedicado a la conversión
entonces:

   t / (t + 500)   = fracción de tiempo en alto  = 0.56/4.5  = 0.11  (11%)

Despejando obtenemos un valor aproximado para t de unos 60 usec.

Obviamente esto funcionará cuando el periodo sea lo suficientemente rápido para que el
voltímetro responda con el valor medio del voltaje a lo largo del periodo.

        

La captura de la izquierda muestra varios pulsos de conversión. Vemos que el espaciado


entre pulsos no son los 500 usec sino unos 560. La razón es que además de los 500 usec
programados en el delay, precisamos otros 60 usec para completar la conversión AD (lo que
concuerda con la estimación del voltímetro). Vemos que el espaciado entre terminar una
conversión y empezar otra si son los 500 usec exactos que hemos programado.
A la derecha, en un zoom del pulso de captura vemos con mayor precisión el tiempo dedicado
a la conversión, unos 55 usec.

Esta duración del proceso de conversión (55-60 usec) nos impone


un límite máximo de unos 15-18000 muestras por segundo a nuestros proyectos (además, si
nos acercamos a dicho límite el micro no podrá hacer nada más, al estar ocupado en un
100% en tomar muestras).

Si en el programa anterior cambiamos el delay entre muestras a  Delay10TCYx(1);


(equivalente a 2 usec) veremos que el voltaje del pin RC0 sube a 4.42V, lo que corresponde a
una ocupación del 92% (4.42/4.86). En la figura adjunta se aprecia gráficamente dicha
dedicación casi exclusiva al proceso de conversión. El espaciado entre muestras es de 59 usec
(55 + delay) lo que corresponde a unas 17000 muestras por segundo (16950 en la medida
del osciloscopio)

Aunque 15000 muestras por segundo pueden ser más que adecuadas, vamos a ver que es
posible mejorar dichos resultados, aprovechando lo que hemos visto de la programación del
ADC en base a registros de control y el estudio de las especificaciones detalladas del micro
que estamos usando.

Modificaciones del código:

En primer lugar escribiremos una función equivalente a la anterior (ADC_read2) pero


evitando usar las llamadas a las funciones de C18. De esta forma nos damos cuenta de que
implementar dichas funciones es muy sencillo y nos evitamos llamadas a funciones (lo que
esperamos que mejore el tiempo necesario):

#define select_ADC(ch) { ADCON0 &= 0b11000011;  ADCON0 |= (ch<<2); }


uint16 ADC_read2(uint8 ch)
 {
  uint16 ad_res;      
  select_ADC(ch);                                //SetChanADC(ADC_channel);
  Delay10TCYx(5);
  ADCON0bits.GO=1;                               //ConvertADC();
  while(ADCON0bits.GO);                          //while(BusyADC());  
  ad_res =ADRESH; ad_res<<=8; ad_res+=ADRESL;    //ad_res = ReadADC(); 
  return ad_res;
}

Si repetimos el bucle anterior con esta función vemos que el tiempo baja a unos 45 usec, una
ganancia del 15%.
Podemos mejorar un poco más si nos ajustamos a las especificaciones del PIC usado
(18F2520/4520). La documentación indica que para esta familia basta un tiempo de
adquisición (entre seleccionar canal y lanzar conversión) de unos 3 usec, lo que a 20 MHz
equivale a unos 15 ciclos . Claramente los 50 ciclos (10 usec) de antes son excesivos. Nos
bastaría con especificar un retrado de p.e. 20 ciclos:

uint16 ADC_read2b(uint8 ch)


 {
  uint16 ad_res;
      
  select_ADC(ch);                                //SetChanADC(ADC_channel);
  Delay10TCYx(2);
  ADCON0bits.GO=1;                               //ConvertADC();
  while(ADCON0bits.GO);                          //while(BusyADC()); 
  ad_res =ADRESH; ad_res<<=8; ad_res+=ADRESL;    //ad_res = ReadADC(); 
  return ad_res;
}
 
Con esto bajamos a unos 40 usec, otro 10% adicional. Notad que dependiendo del PIC usado
este código tendría que ser modificado.

Finalmente podemos usar una característica adicional de la familia 18F2520/4520. El tiempo


de adquisición puede ser preprogramado, lo que hace que podamos lanzar la conversión nada
más seleccionar el canal. Tras poner a 1 el bit GO, el módulo ADC no lanza la conversión
inmediatamente sino que espera el tiempo programado. El tiempo se especifica en unidades
de Tad y hay que programarlo en la configuración (OpenADC) del módulo:

   OpenADC(ADC_FOSC_16 & ADC_RIGHT_JUST & ADC_4_TAD,


           ADC_INT_OFF & ADC_VREFPLUS_VDD & ADC_VREFMINUS_VSS, 7);

En este caso preprogramamos un delay de 4 x Tad. Como Tad = 16/20 = 0.8 usec eso
equivale a 3.2 usec, que es superior al tiempo de adquisición mínimo recomendado para
estos PIC. La función de conversión queda ahora:

uint16 ADC_read2b(uint8 ch)


 {
  uint16 ad_res;
      
  select_ADC(ch);                                //SetChanADC(ADC_channel);
  ADCON0bits.GO=1;                               // Launch conversion without delay
  while(ADCON0bits.GO);                          //while(BusyADC()); 
  ad_res =ADRESH; ad_res<<=8; ad_res+=ADRESL;    //ad_res = ReadADC(); 
  return ad_res;
}

Como vemos no tenemos que preocuparnos del delay, ya que el módulo lo respetará. Con
este nuevo código la duración de la llamada a la función es de unos 24 usec. Las ganancias se
pueden detectar con la bajada del voltaje del pin RC0 o más gráficamente, monitorizando
RC0 con el osciloscopio. En la figura se adjuntan las capturas de la duración de la función
para el original y las tres modificaciones propuestas (de arriba abajo y de izquierda a
derecha). Vemos que hemos conseguido reducir a la mitad el tiempo, pudiendo llegar hasta
unas 40000 muestras/segundo (24 usec por conversion).

    

En otra entrada veremos como combinar el uso del ADC con interrupciones. En primer lugar
veremos como usar una interrupción de un temporizador para lanzar la conversión AD a
intervalos exactos. En segundo lugar veremos como usar la interrupción del propio módulo
ADC para evitar que la función anterior sea bloqueante. El uso de la interrupción AD nos
permite no tener que esperar (línea while(ADCON0bits.GO)) hasta que el módulo termine:
la interrupción nos avisará cuando el resultado este listo en ADRESH:ADRESL.

Antes de explorar esta combinación de ADC + INTS, vamos a ver un pequeño proyecto
(brújula electrónica ) en el que  usaremos lo que ya sabemos del ADC. Consiste en leer el
voltaje de dos sensores magnéticos (en dos ejes perpendiculares) y combinar su información
para obtener una desviación en grados respecto al norte magnético

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.

También podría gustarte