Translate

viernes, 22 de junio de 2012

Uso de temporizadores (timers)

La mayoría de los microcontroladores tienen uno o varios timers. Serán muy útiles para medir el tiempo que ha pasado entre dos eventos, establecer tareas para ejecutarse a intervalos regulares, etc. Dependendo del modelo los PICs cuentan con un número variable de timers.

Vamos a describir con cierto detalle el timer 0 (TMR0). Los demás son muy similares, aunque siempre conviene mirarse el manual correspondiente, sobre todo porque algunos de ellos están asociados a otras tareas y puede que no estén disponibles para su uso general si se están usando ciertos periféricos.

Describiremos los registros asociados para configurar los temporizadores. El compilador C18 tiene disponibles varias rutinas para configurar y operar con los timers de una forma bastante cómoda. Sin embargo, la idea es ser capaces por nuestra cuenta de reproducir dichas rutinas lo que puede ser importante si lo que queremos hacer no se puede hacer exactamente con las rutinas suministradas. Como siempre es una cuestión de usar lo más conveniente en cada caso (pero para ello hay que conocer las posibilidades).

La configuración de los timers está basada en ciclos del oscilador.  Escribiremos alguna rutina que nos permita especificar un tiempo en usec o msec conocida la frecuencia de oscilador usado.

Código asociado a esta entrada: tipos.h  timer_1.c timer_2.c

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

Un timer no es más que un contador cuya entrada está conectada al reloj del sistema. De hecho, la mayoría de los timers pueden reconfigurarse como contadores. En ese caso, en lugar de contar pulsos de reloj cuentan los pulsos que llegan a un determinado pin.

Por defecto la señal que van a contabilizar los timers corresponde a la frecuencia del oscilador dividida por cuatro. Por lo tanto en realidad cuentan ciclos máquina, no ciclos de reloj. Con un reloj de 20 Mhz tendríamos una frecuencia de ciclos máquina de 20/4 = 5 MHz, por lo que un ciclo máquina corresponde a 0.2 usec. En principio, el contador del timer se incrementará cada 0.2 microsegundos o 5 veces en 1 usec.

Tenemos 4 registros asociados al TMR0:

T0CON: el más importante de cara a la configuración del timer: 


T0CON.TMR0ON  (bit 7)      -> arranca(1) o para (0) el TIMER
T0CON.T08bit     (bit 6)      -> selecciona modo 8 bits (1) o 16 bits (0)
T0CON.T0CS       (bit 5)      -> selecciona modo TIMER (0) o contador externo (1)
T0CON.T0SE       (bit 4)      -> en caso de contador externo decide si cuenta en flanco subida (0) o bajada (1).
T0CON.PSA         (bit 3)      -> uso (0) o no (1) de un divisor (prescaler) previo.
T0CON.PS0-2      (bits 0-2)  -> bits que definen el valor del divisor previo, desde 1:2 (000) hasta 1:256 (111)

TMR0L y TMR0H: permiten acceder (lectura/escritura) al valor del contador (TMR0L para el byte menos significativo y TMR0H para el más significativo).

INTCON:  bits para activar la interrupción asociada al Timer0 (ver la entrada sobre interrupciones).

Las decisiones importantes de cara a la configuración del timer son:

·          Uso de contador de 8 o 16 bits. Si se escoge 8 bits solo se usa TMR0L como contador y obviamente se reseteará cada 256 incrementos. Esto es importante también de cara a las posibles interrupciones asociadas, ya que la interrupción del TIMER0 se produce al pasar por 0 el contador.
·          Si queremos hacer más lento el contador podemos usar el prescaler, que no es más que un divisor previo que hace que sólo se cuenten 1 de cada N ciclos. N puede ser 1 (sin prescaler), 2, 4, 8, etc. En ese caso  hay que poner a 0 el bit PSA (usar PRESCALER) y poner el valor correspondiente en los bits asociados. La fórmula es simplemente que divisor N deseado será = 2^(bits_PS+1).

Una vez en marcha el timer podemos consultar su valor accediendo a los registros TMR0L y TMR0H. El byte alto TMR0H no es el verdadero byte alto del contador sino un buffer de dicho valor. La razón de esto es asegurar la consistencia al hacer lecturas/escrituras de un contador de 16 bits en un procesador de 8 bits. Siempre habría la
posibilidad de que entre la lectura de TMR0L y la de TRMR0H el contador se actualizase invalidando la lectura.
La solución es hacer que una lectura de TMR0L cause una copia simultánea del byte alto del contador a TMR0H. De la misma forma una escritura de TMR0L causa la escritura simultánea de TMR0H al byte alto del contador.

¿Consecuencias para nosotros? Si queremos leer el valor del  timer (16 bits) debemos siempre leer primero TMR0L y luego TMR0H. En la escritura es lo contrario, primero escribimos TMR0H (lo que no tiene ningún efecto sobre el contador, ya que TMR0H es un buffer) y luego TMR0L (momento en el que TMR0L y lo que tuviéramos en TMR0H se vuelcan al contador. Podríamos definir una macro o función para no tener que estar pendientes. Por ejemplo, para escribir el valor de TMR0 (16 bits) haríamos:

#define set_TMR0(x) {TMR0H=(x>>8); TMR0L=(x&0x00FF);}

Como vemos primero fijamos TMR0H (con la parte alta de x) y luego TMR0L.  Igualmente podríamos definir algunas macros para arrancar/parar el timer sin tener que acordarnos de los bits involucrados como hicimos con las interrupciones:

#define start_TMR0 T0CONbits.TMR0ON=1;
#define stop_TMR0  T0CONbits.TMR0ON=0;

Con esto ya tenemos suficiente para escribir nuestro primer programa (timer_1.c) usando timers. Como siempre, empezaremos incluyendo los .h necesarios y las opciones de configuración a través de #include y #pragma config, añadiendo las definiciones anteriores:

En el main(), configuraremos el timer en modo 16 bits con un prescaler de 256, lo que supone que se incrementa cada 256 x 0.2 usec (@20 Mhz), esto es, cada 51.2 usec más o menos. Con la macro set_TMR0 inicializamos el contador y con start_TMR0 arrancamos el reloj. Durante el bucle (cada 10,000 ciclos = 2 msec) mostramos el contenido de TMR0L y TMR0H en PORTB y PORTC respectivamente. 

void main(void)
  {
   unsigned int cont=0;
 
   TRISC=0; PORTC=0; TRISB=0; PORTB=0;

   T0CON = 0b00000111;   // 256 prescaler, 16 bits mode, 1 tick = 256*0.2 = 50 usec
   set_TMR0(0); start_TMR0;   // Ponemos a 0 contador y arrancamos

   while(1)
    {
     Delay10KTCYx(1); //Delay 2 msec @ 20 MHz
     PORTB=TMR0L; PORTC=TMR0H;
     cont++; if(cont==0) stop_TMR0;
   }
}

Al ejecutarse PORTB se ve estático, ya que cambia demasiado rápido (50 usec) para poder ser apreciado. En cambio en PORTC (el byte alto del timer) si se aprecia el incremento. PORTC se incrementa cada 256 x 51.2 usec, es decir, unos 13 msec. Cada vez que se actualiza los datos (2 msec) PORTC habrá cambiado en unas 6/7 unidades.

Trás un rato (65536 x 2 msec = 128 sec) las lucecitas se detienen, ya que el contador cont da la vuelta completa y la condición establecida (cont==0) detiene el timer.

Como otros compiladores, C18 tiene algunas rutinas para facilitar el manejo de los timers:

OpenTimer0  -> da valores a T0CON a través de mascaras predefinidas.
                        También pone a 0 el contador y lo pone en marcha.

CloseTimer0 -> desactiva el contador TMR0 y su interrupción asociada.

WriteTimer0 -> equivalente a  la macro set_TMR0()

Abajo se lista el mismo programa usando estas rutinas. Algunos comentarios sobre los cambios:
  • Es preciso incluir <timers.h> para tener acceso a las declaraciones de las funciones usadas
  • No es necesario resetear el contador ni arrancarlo explícitamente (lo hace OpenTimer0)
  • Las mascaras de configuración se combinan con AND. Es posible combinarlas con OR definiendo previamente #define USE_OR_MASKS en el programa. Esto afecta a las opciones que se establecen por defecto. En el modo AND el valor por defecto de los bits es 1 (modo 8 bits, contador de pulsos en un pin, etc.). En modo OR el valor por defecto es 0 (16 bits, contador de reloj)

#include <timers.h>

void main(void)
  {
   unsigned int cont=0;

   TRISC=0; PORTC=0; TRISB=0; PORTB=0;
  
   OpenTimer0(T0_16BIT&T0_SOURCE_INT&T0_PS_1_256); // 256 prescaler, 16bit mode, source Clock

   while(1)
    {
     Delay10KTCYx(1); // 2 msec delay
     PORTB=TMR0L; PORTC=TMR0H;
     cont++; if(cont==0) closeTimer0();
   }
}

Obviamente es más cómodo y legible usar OpenTimer que dar valores directamente a T0CON. Sin embargo conocer los detalles siempre es interesante por si lo que queremos hacer no se puede hacer exactamente con las rutinas suministradas. Por ejemplo, closeTimer0() no sólo para el contador sino que desactiva la interrupción asociada, por lo que no es exactamente equivalente a nuestra macro stop_TMR0. Las llamadas a funciones son más costosas que una macro, por lo que en ciertas aplicaciones críticas son preferibles, etc.  Como siempre es una cuestión de usar lo más conveniente en cada caso (pero para ello hay que conocer las posibilidades).


Uso de TIMERS con interrupciones (timer_2.c)

Tras verificar que los TIMERS funcionan pasemos a usarlos en combinación con las interrupciones para poder establecer tareas a intervalos regulares. Por descontado, para poder usar interrupciones tenemos que habilitar las interrupciones globales y la interrupción del TMR0 en particular (aunque esto también se puede en la función OpenTimer con la máscara TIMER_INT_ON/OFF. Como vimos en el tutorial de interrupciones es conveniente trabajar con macros para no tener que recordar que bits habilitaban que interrupciones. Haríamos algo como esto:

enable_TMR0_int; enable_global_ints;  // Enable ints

Ciertos timers están considerados periféricos, por lo que también tendríamos que habilitar las interrupciones periféricas, aunque no es el caso del TMR0.

El fundamento del proceso es sencillo. La interrupción del TMR0 se producirá cuando el contador del timer pase por cero (cada 256 o 65536 ciclos dependiendo de si estamos en 8 o 16 bits). La idea es arrancar con un valor del contador tal que en el tiempo deseado alcance el valor máximo (256 o 65536).

Consideremos el modo 8 bits (sin divisor) y arranquemos con un valor 156 en TMR0L. Necesitaremos 100 ciclos (256-156) para rebosar el contador y provocar la interrupción. Al cabo de 100 x 0.2 usec = 20 usec se producirá la primera interrupción.

¿Cuándo será la próxima? El contador estaba en 0 y para volver a llegar a  rebosar deben pasar 256 ciclos, lo que corresponde a unos 51.2 usec, que ya no son los 20 usec de antes. La solución es que en el código de la ISR (rutina de servicio de la interrupción) del TMR0, lo primero que hacemos es volver a poner el contador a 156. Esto nos garantiza la repetición de la tarea que codifiquemos en la interrupción cada 20 usec.

Puede que un periodo de repetición de 20 usec sea demasiado frecuente. Podemos usar como valor de reseteo TMR0L=56, lo que nos daría 200 ciclos (40 usec) entre repeticiones. ¿Qué pasa si queremos p.e. 100 usec?
100 usec son 500 ciclos máquina, que son más que el margen de 256 que teníamos. En este caso podemos usar un divisor de 2 y solo tenemos que saltar cada 250 incrementos, lo que podemos conseguir poniendo TMR0L a 6 .
La alternativa sería usar un modo de 16 bits y poner el valor del contador a 65536-500 = 65036, usando p.e. set_TMR0(65036). Tras 500 ciclos el contador llegará a 65536 rebosará y tendremos nuestra interrupción.
La diferencia es que usando un divisor perdemos algo de precisión, ya que no podemos ser tan finos a la hora de especificar nuestro intervalo. Por ejemplo, si usáramos el máximo divisor de 256 cada incremento del contador correspondería a 256 ciclos máquina = 51.2 usec y no podríamos especificar intervalos con mayor resolución.

El mínimo intervalo efectivo (@ 20 Mhz) es de unos 4-5 usec, ya que hay que descontar el tiempo perdido en
guardar registros, saltar a la interrupción, gestionar el tipo de interrupción, por no hablar de la tarea a realizar.
El intervalo máximo correspondería a usar un divisor 256 y modo 16 bits. Serían necesarios 256x65536 ciclos máquina para rebosar el contador, lo que a 20 MHz corresponde a más de 3 segundos. Si queremos espaciar aún más las tareas podríamos llevar un contador por nuestra cuenta y sólo lanzar la tarea cada cierto número de interrupciones.

Podemos hacer estos cálculos por nuestra cuenta, pero es sencillo escribir una rutina que nos evite hacerlos. Sólo tenemos que elegir un intervalo T de repetición (en usec) e informar a la rutina de la velocidad del oscilador. La rutina calculará  el divisor (si es preciso) y el valor de reset adecuados, para asegurar una interrupción cada T usec. Usaremos el modo de 16 bits para asegurarnos la máxima resolución a la hora de fijar el intervalo.

En la rutina siguiente usamos los tipos uint8 (8 bit sin signo, equivalente a unsigned char) y uint32 (32 bit sin signo, equivalente a ulong). Dichas definiciones están dentro de un fichero tipos.h que debemos incluir en el programa. La razón de usar estas definiciones es que no son ambiguas. Es posible encontrar diferentes compiladores para los que un short int corresponda a 8 o a 16 bits. Con unas definiciones no ambiguas (int8, uint8, int16, uint16, int32, uint32) evitaremos posibles problemas al portar el código entre compiladores.


// Computes prescaler(log2) y reset value of 16bit Timer
// for a period of T usec using an oscillator of Fosc KHz (20MHz -> 20000)
// Returns reset value as 16 bit unsigned int
// The value placed in *prescaler relates to the actual pre-scaler as:
// Value         0    1    2    3     4    5     6     7      8      9     ...
// Timer pre:   NO   1:2  1:4  1:8  1:16  1:32  1:64  1:128  1:256  1:512  ...
// The routine doesnt apply those values to timer, neither starts timer.
// Depending on the intended Timer the values could be invalid.
// For instance Timer1 and Timer3 only admits up to 1:8 prescaler.  
// Values higher than 8 (1:256) means that the interval is too long even for Timer0
uint16 get_delay(uint32 T, uint16 Fosc, uint8* prescaler)
{
  uint32 F;
  uint8 k;

  // Remove 4 usec to account for delays entering INT, resetting TMR, etc
  if (T<=4) T=1; else T-=4;

  F = Fosc*T; F=F/4000;  // F=(Fosc/1000)/4; F=F*T;  // Get # machine cycles during desired period

  // Tries to fit # machine cycles within 16 bits using prescaler
  k=0; while(F>65536L){k++; F>>=1;}
  F=65536L-F;  // Complement = Initial value of Timer  so it takes T usec to overflow in 16bit mode

  *prescaler=k;
  return (uint16)F;
}


  • Quitamos 4 usec al periodo pedido para compensar el tiempo perdido en la llamada y gestión previa de la interrupción (normalmente tendremos varias posibles fuentes de interrupciones y hay que comprobar si efectivamente es la del timer TMR0). Esto hace que el periodo mínimo fiable sea de unos 5 usec para un oscilador de 20 MHz.
  • La rutina comprueba si el número de ciclos máquina correspondiente a T para el reloj dado entra en 65536. Si es así determinamos su complementario y ese será nuestro valor de reset. Si es mayor de 65536, dividimos por 2, incrementando el divisor hasta conseguirlo.
Escribamos ahora la rutina de gestión de la interrupción:

uint16 TMR0_reset;

#pragma interrupt high_ISR  // High priority interruption
void high_ISR (void)
{
 if (TMR0_flag) //ISR de la interrupcion de TMR0
  {
   set_TMR0(TMR0_reset);  // reset counter
   PORTC=~PORTC;          // Blink PORTC
   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

Algunos detalles:

  • TMR_reset es la variable (uint16) donde guardaremos el valor de reset dado por get_delay. Está declarada global ya que debemos usar su valor dentro de la interrupción y las ISR no admiten parámetros.
  • En el código de la ISR del Timer0, lo primero que hacemos es resetear el contador, para que el tiempo que tardemos en realizar la tarea de la interrupción no incremente el próximo periodo de repetición.
  • La interrupción solo hace parpadear el puerto C.
  • Como siempre en la ISR es responsabilidad nuestra poner a 0 la bandera (TMR_flag) antes de salir.
Ahora sólo queda escribir el programa principal:

 void main()
 {
  uint8 PS_MASK, prescaler;

  TRISC=0; PORTC=0;

  TMR0_reset=get_delay(100000L,20000,&prescaler);
  PS_MASK= (prescaler>0)?  (0xF0|(prescaler-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);
}

  • Llamamos a get_delay para obtener los parámetros para un periodo de 0.1 seg = 100000 usec con un oscilador de 20 MHz (20000 KHz).
  • Damos valores a la mascara de configuración en función del valor de prescaler obtenido.
  • Inicializamos el Timer0 en modo Timer, 16 bits y con los datos del divisor calculados.
  • Habilitamos la interrupción del TMR0 y las interrupciones globales.
  • El programa principal entra en un bucle sin hacer nada (while(1)). Sin embargo veremos como el puerto C parpadea 10 veces por segundo. El trabajo lo está haciendo la interrupción.
  • El primer intervalo será más largo, porque OpenTimer inicializa el contador a 0. Si es crítico, deberíamos poner el contador a TMR0_reset nada más llamar a OpenTimer. Igualmente hay que tener en cuenta que OpenTimer también pone en marcha el contador. Si es critico deberíamos pararlo (stop_TMR0) y luego arrancarlo cuando queramos.
  • El código no comprueba si el valor de prescaler es valido (un valor > 8 indica un prescaler de 1:512 o superiores, que no pueden ser conseguido con el TIMER0)
Podríamos escribir un código similar para usar TMR1 y TMR3. Habría que tener cuidado porque sólo admiten un divisor máximo de 1:8 (prescaler <=3). También hay que tener en cuenta de que al contrario que TMR0, TMR1 y TMR3 pueden estar dedicados a otras funciones.

El caso de TMR2 también tiene algunas peculiaridades especiales debido a su dedicación específica a ciertas tareas como PWM:
  • Es un contador exclusivamente de 8 bits y sólo cuenta con un prescaler de 1:1, 1:4 o 1:16.
  • La condición de interrupción se provoca no por hacerse 0 el contador TMR0, sino cuando este contador coincide con el registro PR2. Tras alcanzar la condición TMR2 automáticamente se resetea a 0.
  • Este timer también cuenta con tiene 16 posibles postscalers desde 1:1 a 1:16. El postscaler consiste en que no todas las condiciones TMR2=PR2 provocan una interrupción, sino sólo los múltiplos del postscaler.  Debido a estas peculiaridades TMR2 será generalmente nuestra última elección para un timer de propósito general.



5 comentarios:

  1. graciass Antonio tenes una forma de explicar my facil de entender soy Fernando de Buenos Aires Argentina

    ResponderEliminar
  2. este tutorial esta super interesante me ha sido de grana ayuda.... gracias

    ResponderEliminar
  3. una pregunta, se pueden usar ambos TMR0 y TMR1 al mismo tiempo en el caso del pic16f887???, quiero usar uno como temporizador y otro como contador

    ResponderEliminar
    Respuestas
    1. Hola, excelente pregunta... yo ando en lo mismo y no encuentro nada explicado en la web.
      he configurado ambos timers, pero si uno funciona, el otro no y viceversa... agrdeaco enormemente si alguien los pudo hacer caminar juntos, es decir, como ejemplo, togglear 2 leds con cada interrupcion.

      Eliminar