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;
Hola me gustaria realizar un vumeter de 10 leds me podrias ayudar ??
ResponderEliminarJusto lo que buscaba y muy bien explicado. Muchas gracias.
ResponderEliminar