4 de enero de 2016

TIMER STM32F4 Discovery

Los sistemas electrónicos digitales destacan siempre por la capacidad para trabajar con tiempos con gran precisión. Esta funcionalidad recae sobre los Timers, periféricos imprescindible en todo microcontrolador, que partiendo de su funcionalidad genérica, permite desarrollar otras como la generación de un interrupciones, control PWM, input capture (permite determinar la frecuencia y ancho de pulso de una señal de entrada) o output compare (principio del PWM, generación de formas de ondas).
En esta ocasión pasaremos a ver unos ejemplos de la generación de una interrupción y el control de PWM, ya que no requieren de hardware externo para la comprobación de su funcionamiento. Mas adelante intentaré incluir algunos ejemplos para poder ver sus otras aplicaciones.

En primer lugar vamos a ver una introducción a la arquitectura del microcontrolador STM32F407 con respecto a los Timers. Como siempre partimos del diagrama de bloques en el que se aprecian todos los periféricos de los que disponemos.


Así pues, nuestro microcontrolador dispone de 14 timers, los cuales clasificaremos en tres grupos según su capacidad funcional (mas información de esto en RM0090 - Reference manual):
  • Timers de control avanzado (TIM1 y TIM8): consisten en un contador de 16 bits con autorecarga controlado por un prescaler programable. Permiten realizar todas las funcionalidades que se han comentado al inicio además de otras también relacionadas con el control PWM.

  • Timers de propósito general (TIM2 a TIM5 y TIM9 a TIM14): consisten en un contador de 16 o 32 bits con autorecarga controlado por un prescaler programable. Realizan las mismas funcionalidades que las comentadas al inicio pero con limitaciones en otras de las que si disponen los timers anteriores.

  • Timers básicos (TIM6 y TIM7): consisten en un contador de 16 bits con autorecarga controlado por un prescaler programable. Ademas de la generación básica de tiempos, permiten interactuar con otros periféricos (al igual que los anteriores), para el control de estos (como ADC y DAC).

En las imágenes presentadas se puede ver claramente como la complejidad del diagrama de bloques disminuye de una clasificación a la siguiente, y por descontado, la funcionalidad de los timers.

Un aspecto en común a todos los timers es el cálculo del valor que se cargará en el registro del contador (registro de autorecarga). Este se obtiene de forma muy sencilla a partir de la siguiente expresión:

$$CK_{ cnt }=\frac { { CK }_{ psc } }{ { TIMx }_{ psc }+1 }$$

Si despejamos el tiempo a cargar en el prescaler nos queda:

$${ TIMx }_{ psc }=\frac { { CK }_{ psc } }{ CK_{ cnt } } -1$$

Ahora tenemos el contador programado para una frecuencia específica. A partir de dicha frecuencia es sencillo calcular el valor que se cargará en el registro del este, pues simplemente se habrá de buscar la relación entre el periodo que queremos para la interrupción del timer y el periodo de la frecuencia seleccionada.

Visto esto, pasamos a exponer un ejemplo de configuración de uno de estos timers, en concreto el TIM7 (dentro de los timers básicos), para generar una interrupción con la que controlaremos la secuencia de encendido de los leds disponibles en la placa.

Empezaremos por configurar los puertos de entrada/salida para comandar los leds de la placa. Esto podemos verlo en entradas anteriores, GPIO STM32F4 Discovery.
Tras esto procedemos a la configuración del TIM7 y de su interrupción.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void TIMER_Configure (void)
{
    TIM_TimeBaseInitTypeDef TIMER_TimeBaseInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM7, ENABLE);

    TIMER_TimeBaseInitStructure.TIM_Prescaler = 8400 - 1;
    TIMER_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIMER_TimeBaseInitStructure.TIM_Period = 10000 - 1;
    TIMER_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIMER_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit (TIM7, &TIMER_TimeBaseInitStructure);

    TIM_ITConfig (TIM7, TIM_IT_Update, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);

    TIM_Cmd (TIM7, ENABLE);
}

Para este caso, tenemos programada la frecuencia del contador a 10 KHz (hay que tener en cuenta que este timer tiene un PLL interno que dobla la frecuencia de entrada, siendo esta de la del APB1, 42 MHz)

$${ TIMx }_{ psc }=\frac { { CK }_{ psc } }{ CK_{ cnt } } -1=\frac { 84000000 }{ 10000 } -1=8400-1$$

Para obtener una animación de los leds apreciable al ojo humano, programaremos la interrupción del timer con un periodo de 1 segundo, de modo que el valor que habremos de cargar es 10000 - 1. Para facilitar el manejo de este valor, el contador se configura en modo ascendente, de forma que cuando este llega al valor cargado vuelve automáticamente a cero y comienza de nuevo la cuenta.
Dicha animación se ejecuta en la rutina de atención a la interrupción del TIM7 como se ve a continuación  (el bucle principal del microcontrolador se encuentra vacío):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void TIM7_IRQHandler (void)
{
    if (TIM_GetITStatus (TIM7, TIM_IT_Update ) != RESET)
    {
        if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_12) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12);
        }
        else if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_13) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_14);
            GPIO_ResetBits (GPIOD, GPIO_Pin_13);
        }
        else if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_14) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_15);
            GPIO_ResetBits (GPIOD, GPIO_Pin_14);
        }
        else if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_15) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_12);
            GPIO_ResetBits (GPIOD, GPIO_Pin_15);
        }
    TIM_ClearITPendingBit (TIM7, TIM_IT_Update);
    }
}

Aquí simplemente se detecta que led esta encendido para pasar a activar el siguiente y apagar el actual.

A continuación podemos ver un pequeño vídeo donde se ve el efecto de esta animación.


Y finalmente, el proyecto para importar a SW4STM32: TIMER Interrupt STM32F4 Discovery Dropbox


Vista la funcionalidad genérica de los timers, pasamos a ver otro ejemplo, en este caso sobre PWM, en el que se irá variando la intensidad lumínica de un led auxiliar (se aprecia mejor que directamente sobre los leds de la placa).

Para este ejemplo haremos uso del TIM4. También nos valdremos de la interrupción programada anteriormente para variar progresivamente el ancho del pulso del PWM.
Así pues, lo primero que haremos será configurar los puertos de entrada y salida.  En este caso usaremos solo el pin del led azul, GPIO_Pin_15, en modo función alternativa, como se ve a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void GPIO_Configure (void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_GPIOD, ENABLE);

    GPIO_PinAFConfig (GPIOD, GPIO_PinSource15 , GPIO_AF_TIM4);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init (GPIOD, &GPIO_InitStructure);
}

Con esto conseguimos que la salida output compare 4 del timer en cuestión se configure para este cometido y podemos con ello ver el efecto sobre el led conectado al mismo pin.
Lo siguiente será establecer la configuración del TIM4 para la generación del PWM. Antes de ver el codigo correspondiente a este aspecto, veremos una imagen donde se muestra el funcionamiento del output compare.


Fuente: PWM

Como podemos ver, y veremos a continuación, la linea roja de la gráfica superior se corresponde con el valor que cargaremos en el registro del contador (en nuestro caso 8400 - 1) y la linea en verde es el ancho del pulso del PWM (su valor máximo sera el anterior). El funcionamiento es muy sencillo: cuando el contador supera el valor escogido para el ancho del pulso, la salida  digital cambia de estado para generar el PWM y vuelve a cambiar con el final de cuenta para completar el pulso. Seguidamente se muestra el código implementado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void TIMER_Configure (void)
{
    TIM_TimeBaseInitTypeDef TIMER_TimeBaseInitStructure;
    TIM_OCInitTypeDef  TIMER_OCInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM7, ENABLE);

    TIMER_TimeBaseInitStructure.TIM_Prescaler = 8400 - 1;
    TIMER_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIMER_TimeBaseInitStructure.TIM_Period = 10000 - 1;
    TIMER_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIMER_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit (TIM7, &TIMER_TimeBaseInitStructure);

    TIM_ITConfig (TIM7, TIM_IT_Update, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);

    TIM_Cmd (TIM7, ENABLE);

    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM4, ENABLE);

    TIMER_TimeBaseInitStructure.TIM_Prescaler = 0;
    TIMER_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIMER_TimeBaseInitStructure.TIM_Period = 8400 - 1;
    TIMER_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIMER_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit (TIM4, &TIMER_TimeBaseInitStructure);

    TIMER_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
    TIMER_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIMER_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
    TIMER_OCInitStructure.TIM_Pulse = 1 - 1;
    TIM_OC4Init (TIM4, &TIMER_OCInitStructure);
    TIM_OC4PreloadConfig (TIM4, TIM_OCPreload_Enable);

    TIM_Cmd (TIM4, ENABLE);
}

Inicialmente dejaremos intacta la configuración del TIM7 para, en su interrupción, cambiar el valor del ancho del pulso. También podríamos, en vez de reutilizar la interrupción, cambiar el valor del ancho del pulso del PWM cada vez que se pulse el botón de usuario, esto se deja comentado para quien quiera probarlo.

Para el TIM4 mantenemos la frecuencia de entrada para el contador, 84 MHz, y es en el registro de este donde determinamos el ancho máximo del pulso del PWM. Tras esto configuramos el PWM. El aspecto mas interesante aquí es la elección de la polaridad de comparación, de modo que obtenemos la forma de la segunda gráfica de la imagen anterior al seleccionar TIM_OCPolarity_Low (TIM_OCPolarity_High genera la forma de la tercera gráfica). Finalmente establecemos un valor para el ancho del pulso.

Como dijimos antes, la variación del ancho del pulso lo haremos mediante la interrupción del TIM7 como se ve a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void TIM7_IRQHandler (void)
{
    static int i = 0;
    if (TIM_GetITStatus (TIM7, TIM_IT_Update ) != RESET)
    {
        TIM_SetCompare4 (TIM4, pulse [i]);

        i++;
        if (i == 9)
     i = 0;

 TIM_ClearITPendingBit (TIM7, TIM_IT_Update);
    }
}

Donde

1
uint32_t pulse [9] = {0, 1050 - 1, 2100 - 1, 3150 - 1, 4200 - 1, 5250 - 1, 6300 - 1, 7350 - 1, 8400 - 1};

Simplemente iremos incrementando el indice del vector anterior, con cada iteración de la interrupción, para conseguir la variación del PWM que podemos apreciar en el siguiente vídeo. Dado que en el led de la placa era difícil de apreciar, y mas aun en la grabación, se ha colocado un led de alta luminosidad en paralelo para poder observar el efecto.


Y de nuevo, el proyecto listo para importar: TIMER Pwm STM32F4 Discovery Dropbox

Como ya dije anteriormente, existen otras funcionalidades también interesantes que requieren de hardware externo para poderlas comprobar, así que si dispongo de estos, me gustaría continuar actualizando esta entrada. De mientras continuaré realizando estos pequeños post sobre los demás periféricos disponibles.

2 comentarios:

  1. Hola, oye en esta linea "NVIC_InitTypeDef NVIC_InitStructure;" me marca error "unknown type name 'NVIC_InitTypeDef'" ¿Qué librería tengo que incluir para que lo reconozca?

    ResponderEliminar
    Respuestas
    1. Hola Ilsem,

      Que raro, debería de reconocerlo sin problema. A lo mejor hubo algun problema al cargar el proyecto. Prueba con un Rebuild: click derecho en el proyecto -> Index -> Rebuild.
      Si con eso no funcionara, la librería es misc.h
      Espero que te sirva.

      Saludos.

      Eliminar