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

msavalos

Soy un hobbista: toco la guitarra, mandolina, descompongo en partituras; también soy aficionado a la fotografía, he hecho algún cursillo por ahí; me encanta la programación, sea de un programa de PC para procesar datos, una interfaz gráfica o un microcontrolador; evidentemente linuxero; y, cayéndose de maduro, geek. Hincha del más grande, técnico en electrónica, ex-estudiante de ingeniería en electrónica en la UTN FRBA, Técnico Superior en Informática y Profesor. Doy clases en el nivel medio en el Gobierno de la Ciudad de Buenos Aires e instituciones privadas y a nivel terciario en una sede dependiente de la UTN. Sobre mi afinidad política, bueno, podría decir que soy militante del Software Libre y medio zurdito (no me ofende).

Ver comentarios

Entradas recientes

Páginas embebidas con ESP8266 (WebServer)

Nivel: Básico ¿Qué tengo que saber para este post? Diseño básico de páginas con HTML…

3 años hace

Modificación de fuente ATX para laboratorio.

Nivel: Avanzado. La modificación de fuentes ATX para su utilización en laboratorio o comunicaciones es…

3 años hace

FLISoL Shield ARDUINO

El pasado 23 de abril se celebró la 18° edición del FLISoL en la que…

3 años hace

Regulador de tensión MOSFET para Moto.

Circuito del regulador, con los elementos de simulación. Típicamente, las motocicletas de baja cilindrada utilizan…

3 años hace

Review Analizador Lógico de menos de u$s10 en Linux

¿Qué puedo decir? Siempre quise tener un analizador lógico, había visto estos pequeños y baratos…

4 años hace

Oversampling con bajos recursos (+Bits ADC)

Nivel: Intermedio ¿Qué tengo que saber para este post? Entender el uso de un ADC.Programación…

4 años hace

Esta web usa cookies.