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.
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.
graciass Antonio tenes una forma de explicar my facil de entender soy Fernando de Buenos Aires Argentina
ResponderEliminarMuy buena explicación ¡Gracias!
ResponderEliminareste tutorial esta super interesante me ha sido de grana ayuda.... gracias
ResponderEliminaruna 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
ResponderEliminarHola, excelente pregunta... yo ando en lo mismo y no encuentro nada explicado en la web.
Eliminarhe 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.