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.
EI TIM2 tiene características especiales que lo diferencia de TIM3 y TIM4. Una de ellas es que puede funcionar con registros de 32 bits en ciertos modos de operación.
¿Qué significa 32 bits?
- Registros CNT, ARR, PSC: Pueden usar 32 bits de resolución en modo encadenado.
- Mayor precisión temporal: Permite temporizaciones muy largas sin preescaler.
- canales CCRx: Siguen siendo 16 bits cada uno.
Limitación Importante
Los registros CCRx (incluido CCR4 para PWM) son siempre de 16 bits. Esto significa que para frecuencias bajas como 440Hz, necesitamos usar un prescaler.
EI STM32F103RB no puede generar PWM nativo de 32 bits, pero TIM2 sí puede usarse como temporizador de 32 bits.
9. Ejemplo Práctico: Generador de Tono 440Hz con PWM
Vamos a generar un tono de 440Hz (nota La4) usando TIM2 en modo 32 bits. Conectaremos un piezo/zumbador activo al pin PA3 (TIM2_CH4).
Cálculos Preeliminares
Para generar 440Hz necesitamos un periodo de 72,000,000 / 440 = 163,636. Sin embargo,
ARR es un registro de 16 bits (máx. 65.535). La solución es usar un prescaler para
reducir la frecuencia efectiva del timer.
Frecuencia Objetivo
440 Hz (nota musical La)
f_pwm = 440 Hz
Solución: Prescaler
Con PSC = 3 conseguimos que ARR quepa en 16 bits
ARR = 72MHz / 3 / 440Hz = 54,545
Frecuencia Timer
Señal efectiva para el contador
F_timer = 72MHz / (PSC+1) = 18 MHz
Duty Cycle 50%
Mitad del periodo
CCR4 = ARR / 2 = 27,272
Implementación Paso a Paso
Paso 1: Habilitar Periféricos
Activamos el reloj para TIM2 y GPIOA.
// Habilitar reloj para TIM2 y GPIOA RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Reloj TIM2 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // Reloj GPIOA
Paso 2: Configurar PA3 como Salida PWM (TIM2_CH4)
El pin PA3 debe configurarse como Alternate Function para que el timer controle la salida.
// PA3 está en CRL (bits 12-15) // MODE3 = 11 (Output, max 50MHz) // CNF3 = 10 (Alternate Function Push-Pull) GPIOA->CRL &= ~(0xF << 12); // Limpiar configuración de PA3 GPIOA->CRL |= (0xB << 12); // AF Push-Pull, 50MHz
// Usando máscaras CMSIS para PA3 GPIOA->CRL &= ~(GPIO_CRL_MODE3 | GPIO_CRL_CNF3); GPIOA->CRL |= (GPIO_CRL_MODE3_1 | GPIO_CRL_MODE3_0 | GPIO_CRL_CNF3_1);
Paso 3: Configurar TIM2 y PWM
Configuramos PSC=2, ARR=54,545 y CCR4=27,272 para obtener 440Hz al 50% de duty cycle.
// Configuración del Timer 2 TIM2->PSC = 2; // Prescaler: 72MHz/3 = 24MHz TIM2->ARR = 54545 - 1; // Frecuencia = 24MHz / 54545 ≈ 440Hz TIM2->CCR4 = 54545 / 2; // Duty Cycle 50% // Configurar Canal 4 en modo PWM 1 // CCMR2: Canal 3 y 4 TIM2->CCMR2 &= ~(0xFF << 0); // Limpiar TIM2->CCMR2 |= (0x6 << 12); // OC4M = 110 (PWM Mode 1) TIM2->CCMR2 |= (1 << 11); // OC4PE = 1 (Preload Enable) // Habilitar salida del Canal 4 TIM2->CCER |= (1 << 12); // CC4E = 1 (Salida activa) TIM2->CCER &= ~(1 << 13); // CC4P = 0 (Polaridad alta) // Arrancar el timer TIM2->CR1 |= (1 << 0); // CEN = 1
// Configuración usando CMSIS TIM2->PSC = 2; // Prescaler = 3 TIM2->ARR = 54545 - 1; // Periodo TIM2->CCR4 = 54545 / 2; // Duty 50% // Canal 4: PWM Mode 1 con Preload TIM2->CCMR2 &= ~TIM_CCMR2_CC4S; TIM2->CCMR2 |= (TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1); TIM2->CCMR2 |= TIM_CCMR2_OC4PE; // Habilitar salida y polaridad TIM2->CCER |= TIM_CCER_CC4E; // Activar canal 4 TIM2->CCER &= ~TIM_CCER_CC4P; // Polaridad alta // Arrancar TIM2->CR1 |= TIM_CR1_CEN;
Código Completo
#include "stm32f10x.h"
#define PWM_FREQUENCY 440 // Hz (Nota La4)
#define PWM_DUTY 50 // Porcentaje
int main(void) {
//--- 1. Habilitar relojes ---
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // TIM2
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA
//--- 2. Configurar PA3 como AF Push-Pull ---
GPIOA->CRL &= ~(0xF << 12); // Limpiar PA3
GPIOA->CRL |= (0xB << 12); // AF Push-Pull, 50MHz
//--- 3. Configurar TIM2 ---
uint32_t timer_clock = 72000000 / 3; // Prescaler = 3
uint32_t arr_value = timer_clock / PWM_FREQUENCY;
uint32_t ccr_value = (arr_value * PWM_DUTY) / 100;
TIM2->PSC = 2; // Prescaler (72MHz/3 = 24MHz)
TIM2->ARR = arr_value - 1; // Frecuencia PWM
TIM2->CCR4 = ccr_value; // Duty Cycle
//--- 4. Configurar Canal 4 en PWM Mode 1 ---
TIM2->CCMR2 &= ~(0xFF << 0); // Limpiar CCMR2
TIM2->CCMR2 |= (0x6 << 12); // OC4M = 110 (PWM1)
TIM2->CCMR2 |= (1 << 11); // OC4PE (Preload)
//--- 5. Habilitar salida ---
TIM2->CCER |= (1 << 12); // CC4E = 1
//--- 6. Arrancar ---
TIM2->CR1 |= (1 << 0); // CEN = 1
while (1) {
// El PWM se genera por hardware, no necesitamos hacer nada
}
}
#include "stm32f10x.h"
#define PWM_FREQUENCY 440 // Hz (Nota La4)
#define PWM_DUTY 50 // Porcentaje
int main(void) {
//--- 1. Habilitar relojes ---
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // TIM2
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA
//--- 2. Configurar PA3 como AF Push-Pull ---
GPIOA->CRL &= ~(GPIO_CRL_MODE3 | GPIO_CRL_CNF3);
GPIOA->CRL |= (GPIO_CRL_MODE3_1 | GPIO_CRL_MODE3_0 | GPIO_CRL_CNF3_1);
//--- 3. Configurar TIM2 ---
uint32_t timer_clock = 72000000 / 3;
uint32_t arr_value = timer_clock / PWM_FREQUENCY;
uint32_t ccr_value = (arr_value * PWM_DUTY) / 100;
TIM2->PSC = 2;
TIM2->ARR = arr_value - 1;
TIM2->CCR4 = ccr_value;
//--- 4. Canal 4: PWM Mode 1 ---
TIM2->CCMR2 &= ~TIM_CCMR2_CC4S;
TIM2->CCMR2 |= (TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1);
TIM2->CCMR2 |= TIM_CCMR2_OC4PE;
//--- 5. Habilitar salida ---
TIM2->CCER |= TIM_CCER_CC4E;
//--- 6. Arrancar ---
TIM2->CR1 |= TIM_CR1_CEN;
while (1) {
// PWM generado por hardware automáticamente
}
}
Resumen del Ejemplo
#include "stm32f10x.h"
#define PWM_FREQUENCY 440 // Hz (Nota La4)
#define PWM_DUTY 50 // Porcentaje
int main(void) {
//--- 1. Habilitar relojes ---
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // TIM2
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA
//--- 2. Configurar PA3 como AF Push-Pull ---
GPIOA->CRL &= ~(GPIO_CRL_MODE3 | GPIO_CRL_CNF3);
GPIOA->CRL |= (GPIO_CRL_MODE3_1 | GPIO_CRL_MODE3_0 | GPIO_CRL_CNF3_1);
//--- 3. Configurar TIM2 en 32 bits ---
uint32_t arr_value = 72000000 / PWM_FREQUENCY;
uint32_t ccr_value = (arr_value * PWM_DUTY) / 100;
TIM2->PSC = 0;
TIM2->ARR = arr_value - 1;
TIM2->CCR4 = ccr_value;
//--- 4. Canal 4: PWM Mode 1 ---
TIM2->CCMR2 &= ~TIM_CCMR2_CC4S;
TIM2->CCMR2 |= (TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1);
TIM2->CCMR2 |= TIM_CCMR2_OC4PE;
//--- 5. Habilitar salida ---
TIM2->CCER |= TIM_CCER_CC4E;
//--- 6. Arrancar ---
TIM2->CR1 |= TIM_CR1_CEN;
while (1) {
// PWM generado por hardware automáticamente
}
}