← Volver al inicio

Laboratorio 3: Gestión de Periféricos: TIMER

Temporización precisa y generación de eventos en el STM32F103RB

1. ¿Qué es un Timer?

Un Timer (Temporizador) es un periférico de hardware fundamental. En esencia, es un contador digital que se incrementa o decrementa de forma síncrona con una señal de reloj (frecuencia de bus del sistema).

Principales Usos:

Temporización Precisa

Generar retardos exactos (delays) sin bloquear la ejecución de la CPU, al contrario de lo que ocurre con los bucles vacíos.

Medición de Tiempo

Medir el ancho de un pulso o el periodo de una señal externa (Input Capture).

Generación de Señales

Crear señales de salida como PWM (Pulse Width Modulation) para control de brillo, velocidad de motores o sonido.

Triggers de Eventos

Disparar interrupciones periódicas o iniciar conversiones en otros periféricos (como el ADC) de forma automática.

Cuenta de Eventos

Contar pulsos procedentes de señales externas (External Clock) para tareas como el conteo de piezas o revoluciones.

2. TIMERS en el STM32F103RB

El STM32F103RB (línea de densidad media) ofrece recursos de temporización muy equilibrados. Dispone de un total de 4 Timers de propósito general/avanzado más el temporizador de sistema:

Avanzado

TIM1

Timer de 16 bits. El más potente y completo. Diseñado específicamente para control de motores (soporta señales complementarias, frenado por hardware y gestión de dead-time).

Uso General

TIM2, TIM3, TIM4

Timers de 16 bits. Son los caballos de batalla para la mayoría de tareas: generación de PWM de 4 canales, Input Capture y Output Compare.

Sistema

SysTick

Temporizador dedicado de 24 bits integrado directamente en el núcleo Cortex-M3. Se utiliza fundamentalmente para la base de tiempos del sistema operativo o delays básicos.

Dato técnico: Los timers TIM2 a TIM4 cuelgan del bus APB1 (máximo 36MHz/72MHz según config), mientras que TIM1 cuelga del bus APB2. Esto es vital para calcular los tiempos correctamente en los registros de pre-escalado.

3. Arquitectura de Registros

Para configurar un Timer de forma básica (contador/temporizador), debemos manipular un conjunto de registros clave. A continuación se detallan los más importantes para la operación básica:

TIMx_CR1 (Control Register 1)

Es el registro maestro de control. Define cómo se comporta el contador.

  • CEN (Bit 0): Counter Enable. 1 = Activa el contador; 0 = Lo detiene.
  • DIR (Bit 4): Direction. 0 = Cuenta hacia arriba (Upcounter); 1 = Hacia abajo.
  • OPM (Bit 3): One Pulse Mode. Si se activa, el contador se detiene automáticamente tras el primer evento.

TIMx_PSC (Prescaler)

Divide la frecuencia de entrada del reloj para ralentizar el conteo. Es un registro de 16 bits.

F_cnt = F_clk / (PSC + 1)

Si PSC = 71, y el reloj es 72MHz, el contador incrementará a 1MHz (cada 1 µs).

TIMx_ARR (Auto-Reload Register)

Define el valor de carga automática (el límite superior del conteo en modo Upcounter). Define el periodo de la señal o evento.

Cuando el contador `CNT` alcanza el valor de `ARR`, se genera un evento de actualización (Update Event) y el contador vuelve a cero.

TIMx_CNT (Counter Register)

Es el registro de 16 bits que contiene el valor actual del conteo. Puede leerse o escribirse en cualquier momento mientras el timer está funcionando.

TIMx_SR (Status Register)

Contiene los flags de estado. El más usado es el UIF (Bit 0), que se pone a '1' cuando el contador desborda (Update Interrupt Flag).

TIMx_DIER (DMA/Interrupt Enable)

Permite activar las interrupciones. Poniendo el bit UIE (Bit 0) a '1', el micro saltará a la rutina de interrupción cada vez que el timer llegue al valor de ARR.

Importante: Casi todos los registros de los timers STM32 son buffered/shadowed. Esto significa que si cambias el valor de ARR mientras el timer cuenta, el nuevo valor no suele aplicarse hasta que el contador llega al final del ciclo actual para evitar errores de sincronía.

4. Ejemplo Práctico: Generación de 100ms

Supongamos que queremos configurar el TIM2 para que genere una interrupción o evento cada 100ms partiendo de un reloj del sistema de 72MHz.

Paso 1: Habilitar el reloj del periférico

Antes de tocar cualquier registro del Timer, debemos darle energía a través del registro RCC correspondientes al bus APB1.

RCC->APB1ENR |= (1 << 0); // Habilitar reloj para TIM2
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // CMSIS: Habilitar reloj TIM2

Paso 2: Configurar el Prescaler (PSC)

Queremos que el contador sea fácil de manejar. Si dividimos 72,000,000 entre 7200, obtendremos una frecuencia de conteo de 10kHz (cada "tick" será 0.1ms).

TIM2->PSC = 7200 - 1; // Frec = 72MHz / 7200 = 10kHz

Restamos 1 porque el hardware suma 1 automáticamente al valor del registro.

Paso 3: Configurar el Auto-Reload (ARR)

Para llegar a 100ms necesitamos contar 1000 ticks (0.1ms * 1000 = 100ms).

TIM2->ARR = 1000 - 1;

Al llegar a 999, el contador vuelve a 0 y genera el evento.

Paso 4: Inicialización y Arranque

Finalmente, limpiamos el contador, activamos las interrupciones (si se desea) y encendemos el timer.

TIM2->CNT = 0;              // Resetear contador
TIM2->DIER |= (1 << 0);     // Habilitar interrupción por actualización (UIE)
TIM2->CR1 |= (1 << 0);      // ¡Arrancar! (CEN = 1)
TIM2->CNT = 0;                  // Resetear contador
TIM2->DIER |= TIM_DIER_UIE;     // Habilitar interrupción de actualización
TIM2->CR1 |= TIM_CR1_CEN;       // Counter Enable - ¡Arrancar!

Resumen técnico del evento:

  • Reloj Entrada: 72,000,000 Hz
  • Divisor (PSC+1): 7,200
  • Frecuencia Contador: 10,000 Hz (10 kHz)
  • Periodo (ARR+1): 1,000 cuentas
  • Tiempo Total: 1,000 / 10,000 = 0.1 segundos (100ms)

5. ¿Cómo saber que el tiempo ha pasado?

Una vez que el Timer está corriendo, tu programa necesita saber cuándo el contador `CNT` ha llegado al valor de `ARR`. Existen dos formas de hacerlo: mediante Interrupciones (automático) o mediante Polling (manual).

El concepto de Flag: Cuando ocurre el evento de actualización (Update Event), el hardware pone automáticamente a '1' un bit específico en el registro de estado (SR).

Control por Software (Polling)

Este es el método más directo. Consiste en interrogar continuamente al registro SR (Status Register) para ver si el bit UIF (Update Interrupt Flag) se ha activado.

// Estructura típica dentro de un bucle principal
while(1) {
    // 1. ¿Ha ocurrido el evento de actualización?
    if (TIM2->SR & (1 << 0)) { 
        
        // --- REALIZA AQUÍ TU ACCIÓN (Ej: conmutar un LED) ---

        // 2. ¡IMPORTANTE! El hardware NO limpia el bit por ti.
        // Si no lo borras, la condición siempre será verdadera.
        TIM2->SR &= ~(1 << 0); 
    }
    
    // El programa sigue su ejecución...
}

Para reflexionar: ¿Qué pasaría si tu código tiene otras tareas muy pesadas fuera del `if`? ¿Seguiría siendo precisa la temporización de tu acción? Esta es la razón por la que en aplicaciones reales preferimos usar el bloque NVIC para gestionar interrupciones.

6. Generación de señales PWM

La Modulación por Ancho de Pulso (PWM) permite simular una señal Analógica variando el tiempo que una señal digital está en alto comparado con su periodo total (Duty Cycle).

Los Timers del STM32 disponen de canales de comparación (hasta 4) que pueden conmutar automáticamente un pin sin intervención de la CPU.

  • Frecuencia: Definida por el registro ARR.
  • Duty Cycle: Definido por el registro CCR.
PWM Duty Cycle

Secuencia Lógica de Configuración:

Paso 1: GPIO en Función Alternativa

El pin físico debe entregarse al control del Timer. No se usa como salida general (GP Output), sino como Alternate Function.

// Ejemplo: PA8 es TIM1_CH1
// Configurar como Alternate Function Push-Pull (Max 50MHz)
GPIOA->CRH &= ~(0xF << 0);
GPIOA->CRH |=  (0xB << 0); // 0xB -> AF Push-Pull

Paso 2: Modo PWM (CCMRx)

En el registro TIMx_CCMR1/2 configuramos el canal como salida y seleccionamos el Modo PWM 1.

// Canal 1 en modo PWM 1
TIM1->CCMR1 |= (0x6 << 4); // PWM mode 1 (OC1M bits)
TIM1->CCMR1 |= (1 << 3);   // Enable Preload (OC1PE)

Paso 3: Polaridad y Salida (CCER)

Activamos el canal físicamente y definimos si el pulso es activo en alto o en bajo.

TIM1->CCER |= (1 << 0); // Habilitar Canal 1 (CC1E)
// Por defecto: Polaridad alta

Paso 4: Duty Cycle (CCRx)

Escribimos en TIMx_CCR1 el valor en el que la señal debe cambiar de estado.

// Si ARR=999, CCR=500 es Duty Cycle 50%
TIM1->CCR1 = 500;
Importante para TIM1: Si usas el Timer 1 (Avanzado), debes activar un bit adicional para que las salidas funcionen: el bit MOE (Main Output Enable) en el registro TIM1_BDTR.
TIM1->BDTR |= (1 << 15); // Solo necesario en TIM1

7. Interrupciones: El poder del NVIC

Usar polling (preguntar continuamente while(flag == 0)) es ineficiente y bloquea la CPU. La forma profesional de gestionar los eventos temporales es mediante el Controlador de Interrupciones (NVIC).

1. Configuración del NVIC (En el main)

Simplemente debemos avisar al "jefe de interrupciones" (NVIC) de que preste atención a la línea del Timer que nos interesa.

// Habilitar la interrupción del TIM2 en el NVIC
NVIC_EnableIRQ(TIM2_IRQn); 

// Opcional: Configurar prioridad (0 a 15, donde 0 es máxima)
NVIC_SetPriority(TIM2_IRQn, 1);

2. La Rutina de Servicio (ISR)

Cuando ocurra el evento (ej: timer llega a 100ms), la CPU saltará automáticamente a esta función mágica. ¡El nombre debe ser exacto!

void TIM2_IRQHandler(void)
{
    // 1. Verificar qué ha causado la interrupción (buena práctica)
    if (TIM2->SR & (1 << 0)) { // Si es bit UIF (Update Event)
        
        // --- CÓDIGO CRÍTICO (Ej: toggle LED) ---
        GPIOC->ODR ^= (1 << 13);

        // 2. ¡IMPORTANTE! Limpiar el flag manualmente
        TIM2->SR &= ~(1 << 0);
    }
}

Regla de oro: Las ISR (Interrupt Service Routines) deben ser lo más cortas y rápidas posible. No pongas delays ni bucles largos aquí dentro.