El Contador y Temporizador TIMER0

En anteriores artículos hemos aprendido como hacer contadores, donde vemos como en uno (o varios) display de siete segmentos se representa el valor numérico de una variable que tenemos definida dentro del programa que hemos escrito, también en varias ocasiones usamos retardos de tiempo con el fin de saber cuándo ha pasado cierta cantidad de tiempo para poder realizar una acción en específico.

Las líneas de código de los programas descritos estaban en su mayoría corriendo dentro del programa principal, es decir que consumían ciclos del funcionamiento del microcontrolador que en programas más largos es posible que no tengan el mismo desempeño o en el peor de los casos no se puedan atender procesos más delicados en el instante que son requeridos.

El microcontrolador cuenta con una herramienta muy útil y poderosa, el TIMER0 es un contador ascendente de eventos digitales, el registro donde se almacena su cuenta es TMR0, el cual es de ocho bits.

Una de sus ventajas es que trabaja en segundo plano, es decir que no ocupa el programa principal en su funcionamiento.

Cuando decimos que es un contador / temporizador nos referimos a que puede realizar una de esa dos funciones según como se configure, el TIMER0 puede funcionar como contador, contando los pulsos en el pin T0CKI (RA4) o puede funcionar como temporizador cuando asociamos su conteo a los ciclos de reloj de nuestro oscilador.

Para configurar el TIMER0 debemos conocer el registro OPTION_REG, este ya lo habíamos visto un poco en el artículos pasados donde hablamos de las interrupciones, ahora veamos los bits que nos interesan para este punto.

Registro OPTION_REG

WPUEN INTEDG TMR0CS TMR0SE PSA PS2 PS1 PS0
Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
Los bits WPUEN e INTEDG no tienen que ver en la configuración del TIMER0, por tanto los vamos a omitir, para conocer su funcionamiento puedes ver Interrupción por Pin Externo o Interrupción por Cambio en Puerto.

TMR0CS: Selección de fuente de conteo para TIMER0

1 = Conteo por pulso externo en el pin T0CKI (RA4)

0 = Conteo por ciclos de reloj del oscilador interno (Fosc/4).

TMR0SE: Selección de flanco para conteo por pulso externo en pin T0CKI

1 = Incremento por flanco de bajada en T0CKI

0 = Incremento por flanco de subida en T0CKI

PSA: Asignación de preescala

1 = La preescala se asigna al perro guardián (WDT)

0 = La preescala se asigna al módulo TIMER0

PS<2:0> Relación de incremento según la preescala

PS2 PS1 PS0 Relación Preescala
0 0 0 1:2
0 0 1 1:4
0 1 0 1:8
0 1 1 1:16
1 0 0 1:32
1 0 1 1:64
1 1 0 1:128
1 1 1 1:256

Entendamos un poco más qué hace cada bit.

TMR0CS: Selección de fuente de conteo para TIMER0, nos permite seleccionar de qué modo vamos a usar el TIMER0, si como contador de eventos externos a través del pin T0CKI o como temporizador basándonos en los ciclos del reloj del microcontrolador generados a través del oscilador.

La frecuencia que toma el temporizador será de una cuarta parte de la frecuencia de nuestro oscilador, es decir que si tenemos uno de 4MHz, la frecuencia del TIMER0 será de 1MHz. Partiendo de eso podemos calcular tiempos.

TMR0SE: Selección de flanco para conteo por pulso externo en pin T0CKI, como contador de eventos digitales el TIMER0 puede registrar pulsos o cambios de flanco en el pin T0CKI destinado para tal fin, se pueden contar las pulsaciones de un botón, las vueltas de un motor a través de un encoder, entre otros usos. Este bit nos permite según nuestra necesidad, realizar el conteo por flanco de subida o flanco de bajada.

PSA, PS y Qué es la preescala: Anteriormente comentamos que el registro donde se realiza el conteo es de ocho bits, es decir que como máximo puede realizar conteos desde 0 hasta 255, que a la hora de la verdad es una cifra muy pequeña, es por eso que se ha llegado a la solución de dar una preescala al módulo con fin de realizar conteos más amplios.

La preescala es una relación de equivalencia entre el número actual en el registro TMR0 y el valor de cuenta real, por ejemplo, si se tiene una preescala definida de 1:4, quiere decir que por cada cuatro conteos, se registrará solo uno en TMR0, de ese modo se cuadruplica la cantidad de conteos.

Esto no quiere decir que el registro TMR0 se haga más grande, él siempre va a ser de ocho bits, lo que tendremos es una cantidad representada en un número que luego debemos multiplicar por la preescala. Para una preescala de 1:4 y un valor en TMR0 de 100, el valor real del conteo es de 400. Cada unidad en TMR0 equivaldrá a 4.

Los valores de preescala están presentados en la tabla anterior donde se configuran los bits PS2, PS1 y PS0 del registro OPTION_REG, dichos valores van desde 1:2 hasta 1:256, para poder usarlos debemos poner el bit PSA en 0, pero si por lo contrario no necesitamos preescala, es decir que vamos a contar de 1 en 1, el bit PSA debe estar en 1.

Interrupción en el TIMER0

El modulo del TIMER0 también puede generar una interrupción si se es necesario. El modo en que lo hace es por desbordamiento, entiéndase por desbordamiento al momento en el que llega a su número máximo 255 y pasa de nuevo a 0. En ese punto el modulo puede emitir una señal de interrupción al microcontrolador, la cual podemos usar en distintas aplicaciones.

Para habilitar las interrupciones debemos poner en 1 el bit TMR0IE del registro INTCON, recordemos que también debe estar en 1 el bit GIE del mismo registro. La bandera que nos indica cuando se ha presentado la interrupción es TMR0IF, presente igualmente en registro ya mencionado.

Ahora veamos un par de ejemplos de cómo funciona el módulo TIMER0, como contador y como temporizador.

TIMER0 como contador de eventos externos:

Configurar el TIMER0 como contador de eventos externos es muy fácil, en este ejemplo usaremos un switch con resistencia de pull up conectado al pin T0CKI (RA4), este simulará los pulsos que le llegan en cualquier momento a dicho pin. El programa principal del microcontrolador solo tendrá la tarea de mostrar en un display de 7 segmentos la cuenta actual en el registro TMR0 y no dejar que supere el valor de 9.

La secuencia de configuración del módulo TMR0 para funcionar como contador de eventos es la siguiente:

  • Establecer el pin RA4 del puerto A como entrada digital.
  • Poner en estado lógico 1 el bit TMR0CS del registro OPTION_REG para que la fuente de incremento sea por el pin T0CKI.
  • Según nuestra necesidad poner en estado lógico 1 o 0 el bit TMR0SE (OPTION_REG), para elegir el tipo de flanco por el cual se dará el incremento, 1 para flanco de bajada, 0 para flanco de subida.
  • En el bit PSA (OPTION_REG) escogemos a que modulo asignaremos el conteo, si al mismo TIMER0 (Requiere Preescala) o al perro guardián (WDT) si queremos hacer conteo uno a uno. Estado lógico 1 para WDT, estado 0 para TIMER0.
  • En caso seleccionar el modulo del TIMER0 en el paso anterior, debemos escoger la relación de preescala en los bits PS2, PS1 y PS0 (OPTION_REG).
  • Limpiar el registro TMR0 asignándole el valor de 0 (TMR0 = 0).

El circuito a usar es el siguiente:

Este es el código:

#include <xc.h>

#pragma config FOSC = XT        // Oscilador con cristal de cuarzo de 4MHz conectado en los pines 15 y 16
#pragma config WDTE = OFF       // Perro guardian (WDT deshabilitado)
#pragma config MCLRE = ON       // Master clear habilitado (pin reset)

#define _XTAL_FREQ 4000000      // Oscilador a 4MHz

// Definimos macros
#define DISPLAY PORTB           // Display de 7 segmentos conectado a los bits
                                // menos significativos del puerto B

void main(){
    // Configuración de puertos
    TRISA = 0xFF;               // Puerto A como entrada
                                // RA4 es la entrada por donde se hace el conteo
    ANSELA = 0x00;              // Puerto A digital
    TRISB = 0x00;               // Puerto B como salida
    ANSELB = 0x00;              // Puerto B digital
    PORTA = 0;                  // Limpiamos puerto A
    PORTB = 0;                  // Limpiamos puerto B
    
    // Configuramos el funcionamiento del TIMER0
    OPTION_REGbits.TMR0CS = 1;  // Fuente será el pin T0CKI (RA4)
    OPTION_REGbits.TMR0SE = 1;  // El incremento se dará por flanco de bajada
    // El pin estará siempre en 1 debido  a la resistencia de pull up, cuando se pulse el boton
    // pasa a 0 y se presenta el conteo.
    OPTION_REGbits.PSA = 1;     // Preescala asignada al modulo WDT (Perro guardian)
    TMR0 = 0;                   // Limpiamos el registro TMR0
           
    // Este ciclo mostrara el valor que toma TMR0 cada que se registra un
    // flanco de bajada en el pin T0CKI (RA4), en el Display.
    // El contador se reinicia cuando llegue a 9.
    while(1){                   // Programa que se ejecuta continuamente
        if(TMR0 > 9){           // Preguntamos si el contador llegó a 9
            TMR0 = 0;           // Si supero el 9, lo reiniciamos a 0
        }
        DISPLAY = TMR0;         // Mostramos el valor de TMR0 en el Display
    }
}

TIMER0 como temporizador:

 La configuración del TIMER0 como temporizador requiere un poco más de atención, pues hay que tener en cuenta otros factores, como las interrupciones y realizar una pequeña fórmula para hallar el valor con que debemos iniciar el registro TMR0 dependiendo de la preescala que escojamos. Pero no es nada complejo.

En este ejemplo vamos a generar una onda cuadrada con una frecuencia de 1KHz, donde tendremos un ciclo útil (tiempo de estado lógico 1), del 50%.

Una onda con frecuencia de 1KHz significa que su periodo o tiempo de duración será de 1 milisegundo. Cuando hablamos de que el ciclo útil será del 50%, nos referimos entonces a que durante 0,5 milisegundos estará en estado lógico 1 y el tiempo restante en estado lógico 0.

Para poder determinar la configuración de preescala y el valor con el que se debe precargar el registro TMR0 debemos usar esta fórmula.

Donde:

T: Tiempo expresado en segundos que va a tomar el temporizador, cuando pase ese tiempo se debe generar el desborde del TMR0 y por ende disparar la interrupción.

Fosc: Es la frecuencia expresada en Hertz de nuestro oscilador, en todos nuestros ejemplos hasta ahora hemos usado uno de 4MHz (4’000.000Hz).

Preescala: Es la relación de preescala que vamos a escoger, mientras más grande más tiempo podemos calcular.

x: Es el valor que debemos precargar en el registro TMR0 para que el tiempo sea lo más exacto posible.

Como nos damos cuenta, es muy fácil saber cuánto tiempo necesitamos contabilizar, y la preescala la podemos escoger de un modo casi que arbitrario. El valor que nos interesa es el que debe tomar la x, por eso usaremos la fórmula que aparece a continuación:

Así podemos calcular el valor correcto que debe estar en TMR0 para que el tiempo sea exacto, el valor resultado debe ser positivo, si es negativo debemos probar con una preescala más alta.

Para este ejemplo, donde mitad del tiempo nuestra onda estará en 1 y la otra mitad en 0, requerimos que la interrupción se genere cada 0,5 milisegundos, de ese modo podemos cambiar el estado del pin de salida por donde vamos a generar la onda.

Entonces:

Usamos la preescala mínima de 2 para empezar y nos ha dado el valor de 6, que es un valor positivo, por eso lo usaremos y lo cargaremos en el registro TMR0 en nuestro programa.

¿Qué quiere decir todo lo anterior?, al seleccionar una preescala de 2, por cada 2 ciclos de reloj de nuestro microcontrolador, el registro TMR0 aumentará en 1, al ser iniciado en 6, significara que contara 250 veces y como cada unidad equivale a 2, su conteo real será de 500.

Si prestamos atención al principio, dijimos que el módulo TIMER0 trabaja a un cuarto de la frecuencia de nuestro oscilador. Al trabajar con uno de 4MHz tendremos un TIMER0 trabajando a 1MHz, de tal modo que cada ciclo de reloj será de 1 microsegundo y 500 microsegundos serán lo mismo que 0,5 milisegundos.

Este es el circuito que usamos para el ejemplo, es muy básico, solo usamos un osciloscopio virtual para visualizar la onda cuadrada que generamos a través del pin RB7 del puerto B, además del cristal oscilador de 4MHz y la resistencia del master clear.

Y aquí está el programa, en el ciclo infinito no se ejecuta ningún código adicional, el programa está destinado solo a generar la onda cuadrada, la interrupción por desborde de TIMER0 está habilitada y tras cada una de ellas debemos de volver a cargar el registro TMR0 con el valor que hallamos y como siempre, borrar la bandera de interrupción asociada.

#include <xc.h>

#pragma config FOSC = XT        // Oscilador con cristal de cuarzo de 4MHz conectado en los pines 15 y 16
#pragma config WDTE = OFF       // Perro guardian (WDT deshabilitado)
#pragma config MCLRE = ON       // Master clear habilitado (pin reset)

#define _XTAL_FREQ 4000000      // Oscilador a 4MHz

// Definimos macros
#define ONDA PORTBbits.RB7      // Pin por donde saldrá la onda cuadrada
#define DISPLAY PORTB           // Display de 7 segmentos conectado a los bits
                                // menos significativos del puerto B

// Declaramos la función del vector de interrupciones
void interrupt isr(void);       // Se declara la función que ejecuta al momento
                                // de presentarse cualquier interrupción

void main(){
    // Configuración de puertos
    TRISA = 0xFF;               // Puerto A como entrada
    ANSELA = 0x00;              // Puerto A digital
    TRISB = 0x00;               // Puerto B como salida
    ANSELB = 0x00;              // Puerto B digital
    PORTA = 0;                  // Limpiamos puerto A
    PORTB = 0;                  // Limpiamos puerto B
    
    // Configuramos el funcionamiento del TIMER0
    OPTION_REGbits.TMR0CS = 0;  // Fuente de reloj entregada por el oscilador
    OPTION_REGbits.PSA = 0;     // Preescala asignada al modulo TIMER0
    OPTION_REGbits.PS0 = 0;     // Preescala de 1:2
    OPTION_REGbits.PS1 = 0;
    OPTION_REGbits.PS2 = 0;
    // También podemos escribir el valor de la preescala asi:
    //OPTION_REGbits.PS = 0;
    
    // Configuramos las interrupciones
    INTCONbits.TMR0IE = 1;      // Habilitamos las interrupción por desborde de TIMER0
    INTCONbits.GIE = 1;         // Habilitamos las interrupciones globales
    
    TMR0 = 6;                   // Inicializamos el registro TMR0 la primera vez
    
    while(1){                   // Programa que se ejecuta continuamente
        // El programa no hace nada en especial, solo el microcontrolador
        // se encarga de generar la onda cuadrada.
    }
}

// Vector de interrupciones
void interrupt isr(void){
    // Como no tenemos otras interrupciones activas no realizamos la comprobación de bandera
    ONDA = !ONDA;               // Al valor presente el ONDA lo negamos
                                // De este modo 0 para a 1 luego 1 a 0
    TMR0 = 6;                   // Por cada vez que se desborde el Timer debemos de
                                // volver a cargarlo.
    INTCONbits.TMR0IF = 0;      // Borramos la bandera de interrupción.
}

Como nota final, veamos cuales son los valores máximos y mínimos que se pueden obtener para un cristal oscilador de 4MHZ y otro de 20MHz.

Para 4MHZ:

Mínimo: 2 microsegundos.

Máximo: 65,536 milisegundos

Para 20MHZ:

Mínimo: 0,4 microsegundos.

Máximo: 13,1072 milisegundos