Este blog está dedicado a mis experiencias, proyectos, dificultades y demás en todo lo relacionado a la electrónica y la programación en general sobre GNU/Linux

ARM Cortex-M, Digital, Nivel: Intermedio, STM32

Programando los TIMs (Timers) del STM32F1 con libOpenCM3 [Parte 1]

Nivel: Intermedio

¿Qué tengo que saber para este post?

  • Haber leído el post anterior.
  • Tener conocimientos sobre programación en C.
  • Saber qué son y como funcionan las interrupciones en un µControlador.
  • Haber programado un timer en algún µC.

—————————————

Aspectos básicos, una introducción

Bienvenidos una vez más a este tutorial de programación de STM32F1 utilizando la BluePill y libOpenCM3 con PlatformIO. En este post veremos como manejar los siempre chotos complejos timers del microcontrolador. En general, en mi experiencia como docente (con adolescentes), los timers son periféricos difíciles de enseñar y comprender para los jóvenes. Pero este tutorial está dirigido a personas que ya alguna vez programaron micros y sus periféricos (timers, adc, pwm, serial, etc). Por ello no me adentré mucho, pero voy a explicar los aspectos básico que debemos conocer.

Como sabrán (a esta altura) libOpenCM3 nos proporciona una interfaz básica para manejar estos periféricos y gracias al autocompletado del VSCodium no tenemos que pensar tanto, con un vistazo a la API y la hoja de datos ya nos daremos cuenta de por donde viene los tiros. De todas formas, dare un poco de información básica.

Hay 3 tipos de TIMs (como llaman los de ST a los timers), los avanzados, los de propósito general y básicos. En la BluePill (STM32F103C) viene uno avanzado (TIM1) y 3 de propósito general (TIM2, 3 y 4). Todos ellos son de 16-bits y tiene características interesantes como salidas PWM, entrada para encoders de cuadratura o sensores efecto hall, entre otras.

TIMs de propósito general

Para no complicarnos demasiado de entrada, veremos los más sencillos que disponemos, los TIMs de propósito general. Como mencioné anteriormente, son de 16-bits y pueden contar ascendente o descendentemente con un valor de autorrecarga (ARR) y un prescaler (PSC) también de 16-bits (de 1 a 65536) y poseen 4 canales (de entrada o salida) totalmente independientes y la potente función de poder interconectarse entre timers (que un timer dispare a otro, por ejemplo). Además tiene circuitos internos preparados para la lectura de sensores efecto hall y encoders de cuadratura para determinar posiciones. Así como la posibilidad de contar alineado al flanco o al centro (para manejo de motores, p.e.)

Diagrama en bloque de un TIM de propósito general

Veamos la función más simple, que el timer cuente ascendentemente hasta llegar al valor de autorrecarga y eso nos genere una interrupción. Con libOpenCM3 usaríamos las siguientes funciones de la API:

void timer_set_mode(uint32_t timer_peripheral, uint32_t clock_div, uint32_t alignment, uint32_t direction);
void timer_set_prescaler(uint32_t timer_peripheral, uint32_t value);
void timer_set_period(uint32_t timer_peripheral, uint32_t period);
void timer_enable_counter(uint32_t timer_peripheral);
void timer_enable_irq(uint32_t timer_peripheral, uint32_t irq);
void timer_clear_flag(uint32_t timer_peripheral, uint32_t flag);

El funcionamiento es como uno pensaría, por ejemplo, si quisiéramos que el TIM2 desborde cada 58 (0x40) ciclos de clock después del prescaler (CK_CNT), entonces debemos cargar 57 (0x39) con timer_set_period(TIM2, 57); y el Update Event (UEV) ocurriría al momento del desborde levantando el Update Interrupt Flag (UIF):

Como podemos observar, la cuenta siempre será el valor en ARR+1, pero antes debemos definir el prescaler. El clock, como se observa en la primer figura puede venir de diferentes fuentes, nosotros usaremos el Internal Clock (CK_INT) que viene del RCC.

Configurando el TIMx

Para configurar el TIM debemos habilitar el clock en el RCC (como con cualquier periférico) e indicar cuál vamos a utilizar, qué fuente de clock, modo de cuenta (alineado al flanco o al centro) y dirección (ascendente o descendente) utilizando timer_set_mode(), veamos un ejemplo:

rcc_periph_clock_enable(RCC_TIM2);
timer_set_mode(
    TIM2,               // Timer2
    TIM_CR1_CKD_CK_INT, // Fuente: Clock Interno
    TIM_CR1_CMS_EDGE,   // Alineado por flanco
    TIM_CR1_DIR_UP);    // Cuenta ascendente

Como todas estas opciones se cargan en el Control Register 1 (CR1) las etiquetas de libOpenCM3 siguen siempre las definiciones de las hojas de datos, por ello es TIM_CR1. Como se ve en el código he configurado el TIM2 que es uno de los de propósito general. Ahora que lo tenemos configurado debemos hacer las cuentas para que desborde cuando queremos.

Al tomar el clock interno y si configuramos el SYSCLK para 72Mhz, entonces esa es la frecuencia que entrará a nuestro periférico. Por lo tanto, si quisiera un tiempo de 0.5s, es decir, 2Hz (para hacer un blink) debería utilizar un prescaler que me permita un cuenta fácil. Cuando el registro PSC está en 0 divide por 1, y cuando está en 65535 (0xFFFF) divide por 65536, por lo tanto la división será el valor PSC+1. Así si quiero dividir por 36000, debo cargar 35999, para eso utilizamos timer_set_prescaler():

timer_set_prescaler(TIM2, 35999); // 72Mhz/36000

Como dijimos, si el CK_INT es 72Mhz, entonces al dividirlo por 36000 obtenemos 2kHz o 2000Hz de CK_CNT. Así que si quisiéramos 2Hz simplemente debemos dividir por 1000. Pero recuerden que el cero cuenta, la interrupción es cuando lleguemos a ARR+1 (valor de autorrecarga+1), por ello debemos pasarle 999, lo cual hacemos con timer_set_preload():

timer_set_period(TIM2, 999); // 2kHz / 1000

Para utilizar la interrupción de Update, que salta ocurre cuando hay un overflow o un downflow (cuando la cuenta es descendiente), debemos utilizar timer_enable_irq() con la interrupción mencionada, y además habilitarla en el NVIC como vimos anteriormente:

timer_enable_irq(TIM2, TIM_DIER_UIE);
nvic_enable_irq(NVIC_TIM2_IRQ);

Y ahora sí, ya tenemos todo lo que necesitamos. Debemos declarar la subrutina de interrupción tim2_isr(), y ejecutar lo que queremos que ocurra allí. Como hay varias fuentes de interrupción, pero un solo vector, debemos tener en cuenta que debemos bajar los flags de interrupción a mano con timer_clear_flag():

void tim2_isr(void)
{
    timer_clear_flag(TIM2, TIM_SR_UIF);
    // ...
}

Si hubiéramos habilitado más de una interrupción debemos verificar cuál fue la fuente con timer_get_flag().

El ejemplito de código (blinky forever)

Como lo más sencillo, para no perder el enfoque de lo que estamos practicando (en este caso el uso de los TIMs), la funcionalidad va a ser un simple blinky, como solemos hacer acá. Aquí está el código completo:

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/cm3/nvic.h>

int main(void)
{
    // Se configura el clock del sistema a 72Mhz
    rcc_clock_setup_in_hse_8mhz_out_72mhz();

    // Se configura el LED de la BluePill (PC13) como Salida Push-Pull
    rcc_periph_clock_enable(RCC_GPIOC);
    gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);

    // Se habilita el clock del periférico del TIM2
    rcc_periph_clock_enable(RCC_TIM2);

    // Se configura el modo de funcionamiento del Timer2
    timer_set_mode(
        TIM2,               // Timer2
        TIM_CR1_CKD_CK_INT, // fuente Clk interno
        TIM_CR1_CMS_EDGE,   // Alineado por flanco
        TIM_CR1_DIR_UP);    // Cuenta ascendente

    timer_set_prescaler(TIM2, 35999); // 72MHz / 36000 => 2KHz

    // Se setea el valor hasta donde se cuenta:
    timer_set_period(TIM2, 999); // 2KHz / 1000 = 2Hz => T = 0.5s

    // Se habilita al interrupción por overflow
    timer_enable_irq(TIM2, TIM_DIER_UIE);

    // Empieza a contar el Timer2
    timer_enable_counter(TIM2);

    // Se habilita la interrupción desde el NVIC
    nvic_enable_irq(NVIC_TIM2_IRQ);

    while (1)
        ; // Nada por hacer, esperando la interrupción
}

void tim2_isr(void)
{                                       // Cada 0.5s:
    timer_clear_flag(TIM2, TIM_SR_UIF); // Se baja el flag de la interrupción
    gpio_toggle(GPIOC, GPIO13);         // Se alterna el LED
}

Como siempre, les dejo un link a GitLab con el proyecto ya hecho con PlatformIO listo para clonar.

https://gitlab.com/tute-avalos/libopencm3-05-tim-blinky

Dejar una respuesta