Translate

martes, 17 de julio de 2012

ADC con interrupciones


En la entrada dedicada al conversor AD vimos como optimizar el código ejemplo del compilador de C18. Aprovechando las características especificas de cada controlador, usando macros en vez de llamadas a funciones, etc. conseguimos que el tiempo necesario para una conversión bajase desde unos 50 usec a 25 usec. Eso equivalía a duplicar el posible ritmo de muestreo de 20000 a 40000 muestras por segundo.

En esta entrada vamos a explorar el uso de interrupciones junto con el conversor AD.

En primer lugar veremos como usar la interrupción de un timer para asegurar un ritmo de muestreo exacto. Posteriormente veremos como usar la interrupción asociada al ADC para no tener que esperar mientras el conversor realiza su tarea. Esto nos permitirá liberar gran parte de los recursos del micro, lo que será especialmente útil si estamos cerca del ritmo de muestreo máximo.

Codigo asociado a esta entrada:  adc0_timer.c, adc1_timer.c


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

Uso de interrupciones del temporizador para marcar el ritmo del ADC

Los programas que presentamos en la entrada del ADC tenían dos problemas principales:

Entre conversiones no se hacía nada: el tiempo entre conversiones estaba ocupado por un delay por lo que el micro no hacía nada. Se queda esperando 200 usec, hace la conversión y repite el ciclo. Obviamente  querríamos aprovechar el espaciado entre muestras para procesarlas de alguna manera. Podríamos substituir el delay por la tarea que tengamos que hacer con nuestros datos. Pero si queremos mantener un ritmo de muestreo constante tendríamos que añadir el delay necesario hasta completar el tiempo de espera. Pero eso significa que cualquier cambio en el código nos obligaría a tirar de osciloscopio y reajustar el delay "remanente". Claramente inaceptable.

El intervalo programado no se cumple:  habíamos programado 200 usec entre toma de muestras (5000 Hz), pero debido al tiempo dedicado a la conversión el espaciado es mayor (entre 225 y 250 usec, dependiendo del código usado).  

Afortunadamente la solución para ambos problemas es la misma y muy sencilla: usar una interrupción asociada a un temporizador para lanzar las conversiones cada cierto tiempo. Dentro de la interrupción del timer programaremos el código de la conversión AD. Dentro del programa principal estaremos libres (salvo el tiempo de la conversión) para procesar los datos. El código dentro del programa principal quedaría:

void main(void)
  {
   uint8 PS_MASK,pre;
  
   TRISC=0; PORTC=0; TRISB=0; PORTB=0; TRISA=0xFF;
  
   OpenADC(ADC_FOSC_16 & ADC_RIGHT_JUST & ADC_4_TAD,
           ADC_INT_OFF & ADC_VREFPLUS_VDD & ADC_VREFMINUS_VSS, 7);

   TMR0_reset=get_delay(200L,20000,&pre);
   PS_MASK= (pre>0)?  (0xF0|(pre-1)): T0_PS_1_1;
   OpenTimer0(T0_16BIT&T0_SOURCE_INT&PS_MASK);
  
   enable_TMR0_int; enable_global_ints;  // Enable ints
   start_TMR0;                           // starts timer

    while(1)
     {
      PORTB=res>>2;
      Delay10KTCYx(10);
     }
}

Simplemente configuramos TMR0 para un delay de 200 usec usando un oscilador de 20 MHz (ayudándonos de la función get_delay que escribimos al hablar de los timers). Luego configuramos el ADC con un reloj de Fosc/16 y un retardo programado de 4 Tad. Como ya vimos, estos parámetros son adecuados para los dispositivos 2520/4520.

Tras la configuración del ADC y de TMR0, habilitamos las interrupciones globales y la del TMR0 y arrancamos el contador TMR0. A partir de ahí ya no tenemos que hacer nada, todo lo va a hacer el servicio de la interrupción. Si os fijáis, en el bucle del main solo mostramos (cada 20 msec) los 8 bits más significativos de la conversión en PORTB. Ahí es donde podríamos insertar nuestro código manipulando los datos recibidos. Por supuesto, somos responsables de que lo que tengamos que hacer con cada dato no me lleve más de unos 200 usec, que es el tiempo disponible antes de que reciba una nueva muestra.

El trabajo de la conversión se lleva a cabo en la interrupción del TMR0. El código es:

// High priority interruption
#pragma interrupt high_ISR
void high_ISR (void)
{
 if (TMR0_flag) //TMR0_isr(); //ISR de la interrupcion de TMR0
  {
   PORTCbits.RC0=1;   
    set_TMR0(TMR0_reset);  // reset counter 
     select_ADC(0);
     ADCON0bits.GO=1;
     while (ADCON0bits.GO);
     res=ADRESH; res<<=8; res+=ADRESL;
   PORTCbits.RC0=0;
   TMR0_flag=0;           // clear flag
  }
}

// Code @ 0x0008 -> Jump to ISR for high priority ints
#pragma code high_vector = 0x0008
  void high_interrupt (void){_asm goto high_ISR _endasm}
#pragma code

En la interrupción lo primero que se hace es resetear el contador. Esto es fundamental para que los siguientes 200 usec empiecen a contarse ya, sin esperar a que termine la conversión AD o el resto de las tareas. Así me aseguro que la interrupción vuelva a entrar a los 200 usec exactos. De nuevo es mi responsabilidad que las tareas de la interrupción no se alarguen más del periodo estipulado.

Como veis tenemos también las mismas líneas RC0=1; RC0=0 enmarcando la tarea del ADC lo que nos permite estimar el tiempo dedicado al ADC (y por lo tanto el tiempo libre del procesador) con un osciloscopio o simplemente midiendo el voltaje medio en RC0 con un voltímetro. Con un salto de 200 usec entre muestras obtengo un voltaje de  V=0.55 lo que nos da un porcentaje de ocupación de un

                           0.55 / 4.85  = 0.11 (11%)

Como el periodo es ahora de 200 usec justos, el tiempo de la tarea se estima en 0.11x200 = 22 usec que es similar al esperado para una conversión. La conclusión es que con un ritmo de muestreo de 5000 Hz (muestras/sec) dispongo de aproximadamente un 90% del tiempo del procesador para el resto de las tareas.

Obviamente las cosas empeoran si aumento la frecuencia de muestreo. Si reduzco el periodo de muestreo a 100 usec (10000 muestras/sec = 10 KHz de frecuencia de muestreo) el voltaje medido en RC0 se incrementa a casi 1V lo que supone un 20% (1/4.85) de ocupación. Esto era de esperar porque de los 100 usec del periodo ya sabemos que unos 20-25 usec siguen dedicados a la conversión AD.

                         

A la derecha el caso de 200 usec en el osciloscopio. A la derecha, espaciado de 100 usec. El ancho del pulso de la conversión AD sigue siendo de alrededor de 20 usec, pero supone el doble en porcentaje en el segundo caso.

Está claro donde está el límite si seguimos así. El tiempo (t) dedicado a la conversión AD no cambia (unos 20/25  usec como mínimo) y está claro que el periodo T entre muestras no puede ser inferior a t. Si bajo el periodo a T=25 usec, del orden del 80% del tiempo se dedica a la conversión y solo quedaría un 20% escaso para procesar los datos.  Con ese situación mido 3.75V en RC0 lo que equivale a un 80% de ocupación (3.75/4.8 = 0.8). En la pantalla vemos como el ciclo de trabajo es del orden del 80%.

Con este enfoque el límite de muestreo se hallaría en unas 40000/50000 muestras por segundo (T=25/20 usec). El problema es que para dicho ritmo apenas tendríamos tiempo de hacer nada con los datos adquiridos, ya que del orden del 80% se gasta en el proceso de adquisición.


El uso de la interrupción del ADC

No hay nada que podamos hacer para aumentar el ritmo de muestreo por encima de esos 40-50 KHz máximo. El ADC del PIC (de los modelos 2520/4520) necesita esos 20-25 usec por conversión y no hay forma de hacerlas más rápidas, ya que hay que respetar los límites que nos dan en la documentación para el tiempo de adquisición y de conversión.  Incluyendo a ambos para esta familia el tiempo total sería de unos (4+12) Tad y como Tad debe ser como mínimo de 0.8 usec el proceso no puede tardar menos de unos 12 usec. Si añadimos los retardos de entrada a interrupción, selección del canal, recuperación del resultado de ADREAH/ADRESL, retorno, etc. llegamos a los 20 usec que estamos viendo en el osciloscopio.

Ni siquiera subir la frecuencia del oscilador funcionaría, ya que seguiríamos sin poder incrementar el reloj del módulo AD por encima de 1/Tad.

El problema es que incluso esos ritmos de muestreo parecen poco realistas, ya que no nos dejan tiempo libre para procesar los datos adquiridos. Veremos que, aunque no podamos incrementar el ritmo de muestreo si que podemos liberar al micro de la mayor parte del trabajo, de forma que podamos muestrear a 40000 muestras por segundo y tener libre al micro en un 90% (en vez de estar ocupado al 90%) para poder procesar los datos y hacer otras tareas.

Observando la rutina de la interrupción lectura anterior nos podemos preguntar cuanto tiempo "perdemos" mientras el módulo AD está trabajando. Esto es, desde que ponemos en marcha el proceso (ADCON0.GO=1) hasta que dicho bit se pone a cero, indicando que el resultado está listo. Debería ser del orden de los 12 usec de los que hemos hablado antes (unos 16 Tad @ 0.75 usec). 

Podemos comprobarlo añadiendo unas líneas del tipo PORTCbits.RC1=1; y PORTCbits.RC1=0;  antes y después de la sentencia de espera (while). 
Si lo hacemos y medimos el voltaje en RC1 (para el caso de T=25 usec) resulta ser de 2.7V, frente al obtenido en RC0 (3.75V). Eso indica que del orden de un 70% (2.7/3.7) del tiempo de la rutina se pierde mientras el ADC trabaja (primero esperando y luego convirtiendo).  


A la izquierda podemos verlo en la pantalla del osciloscopio. El pulso amarillo representa el tiempo total de la conversión. El pulso azul, el tiempo que el microcontrolador está bloqueado en la sentencia while(), esperando a que el ADC le informe de que la conversión se ha completado.

¿Podríamos aprovechar todo ese tiempo de alguna forma? Si, ya que durante ese tiempo es el módulo AD quien está trabajando. El micro podría estar haciendo otra cosa, siempre que le avisen cuando el proceso termine. Para ello tenemos la interrupción asociada al ADC. La idea es que no tenemos que quedarnos a esperar que ADCON0.GO vuelva a ser 0. Podemos habilitar la interrupción del ADC, y está nos avisará cuando el resultado esté listo (esto es, cuando el modulo AD haya puesto a cero el bit GO).

Tendremos que romper el código de conversión, repartiendo el trabajo entre dos interrupciones. Dentro de la interrupción del timer TMR0 se selecciona el canal y arranca la conversión (GO=1). Luego salimos y esperamos a que nos llame la interrupción del ADC, donde se recoge el resultado de ADRESH/ADRESL. 

El código de la ISR queda:

#pragma interrupt high_ISR
void high_ISR (void)
{
 if (TMR0_flag) // TMR0 ISR
  {
   PORTCbits.RC0=1;
   set_TMR0(TMR0_reset);  // reset counter
   select_ADC(0);
   ADCON0bits.GO=1;
   PORTCbits.RC0=0;
   TMR0_flag=0;           // clear flag
   return;
  }

 if (AD_flag) // ADC ISR
  {
   PORTCbits.RC1=1;
   res=ADRESH; res<<=8; res+=ADRESL; 
   PORTCbits.RC1=0;
   AD_flag=0; 
   return;
  }
}


La línea RC0 (amarillo, arriba) monitoriza el tiempo usado en seleccionar canal y lanzar la conversión (unos 2.5 usec) dentro de la interrupción del TMR0. La línea RC1 (azul) monitoriza el tiempo usado en extraer el resultado de la conversión (unos 3 usec) dentro de la interrupción del ADC. El tiempo entre  ambos sucesos (unos 15 usec), marcado con los cursores, es el tiempo que antes estabamos esperando y que ahora hemos liberado.


De los 25 usec entre muestras el proceso de conversión solo ocupa unos 5 usec (algo más si incluimos el tiempo de procesar interrupciones). Vemos que se ha invertido la situación, pasando de tener un 80% de ocupación a tener del orden de un 80% libre para procesar las muestras.

El programa principal queda prácticamente igual, salvo que ahora tenemos que habilitar también la interrupción del ADC y las interrupciones periféricas:

 enable_TMR0_int;  AD_flag=0; enable_AD_int;
 enable_global_ints;  // Enable ints
 enable_perif_ints;



2 comentarios:

  1. Hola me gustaria realizar un vumeter de 10 leds me podrias ayudar ??

    ResponderEliminar
  2. Justo lo que buscaba y muy bien explicado. Muchas gracias.

    ResponderEliminar