← 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.

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.

Activación del modo 32 bits: En el STM32F103RB, TIM2 puede configurarse para usar sus registros CNT, ARR y PSC como una unidad de 32 bits. Esto es útil para temporizaciones muy largas, pero los canales PWM siguen limitados a 16 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
Nota técnica: Usando un prescaler de 3 reducimos la resolución efectiva del timer, pero sigue siendo más que suficiente para aplicaciones de audio. EI STM32F103RB no tiene un modo nativo de 32 bits para PWM, pero TIM2 puede funcionar como contador de 32 bits para temporización si se usa de forma encadenada.

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

440 Hz
Frecuencia PWM
50%
Duty Cycle
PA3
Pin de Salida (CH4)
3
Prescaler
#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
    }
}