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:
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).
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.
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.
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.
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.
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).
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.
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;
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.