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 del STM32F1 con libOpenCM3 [Parte 2] – PWM

Nivel: Intermedio

¿Qué tengo que saber para este post?

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

De a poco vamos adentrándonos cada vez un poco más en las funciones y periféricos que vienen en la BluePill. En el post anterior vimos cómo se configura un timer para interrumpir cada X tiempo periódicamente utilizando las interrupciones. Hoy vamos a ver cómo utilizar los Output Control (OC) para sacar una señal PWM, la cual como vimos puede estar alineada al centro o al flanco.

Los pines que tienen salida asociada a los timers son los siguientes (sin remapear pines, eso lo veremos en otro momento):

Los TIMs de uso genral 2, 3 y 4 tiene 4 salidas OC1,2,3,4 respectivamente, en el grafico aparecen como TXC1/2/3/4. Mientras que el TIM1 (avanzado) tiene 4 salidas más las T1C1/2/3N, que son las salidas complementadas. Es decir que cuando T1C1 esté en alto (3v3), T1C1N estará en bajo (0v). Vamos a seguir trabajando por el momento con los de uso general.

PWM alineado al flanco

Lo más normal es utilizar el PWM alineado al flanco, pero no siempre es lo más conveniente. Antes que nada veamos qué es lo que necesitamos hacer y las configuraciones que nos ofrece el STM32F103. En principio debemos configurar el pin correspondiente en su modo de función alternativa con gpio_set_mode() y el timer con la funcion timer_set_mode(), que vimos en el post anterior. Es básicamente lo mismo:

rcc_periph_clock_enable(RCC_GPIOB);
gpio_set_mode(
    GPIOB,
    GPIO_MODE_OUTPUT_2_MHZ,        // Deben revisar la frecuencia según cómo configuren el timer
    GPIO_CNF_OUTPUT_ALTFN_PUSHPUL, // Función alternativa
    GPIO6);                        // TIM4 OC1
rcc_periph_clock_enable(RCC_TIM4);
timer_set_mode(
    TIM4, 
    TIM_CR1_CKD_CK_INT, // Clock interno (72Mhz p.e.)
    TIM_CR1_CMS_EDGE,   // Alineado al flanco
    TIM_CR1_DIR_UP);    // Cuenta ascendente

A partir de acá debemos hacer los cálculos para determinar la frecuencia (si es algo que nos importa. Si solo nos interesa la cantidad de «pasos» del PWM, entonces ponemos arbitrariamente un valor que nos queda cómodo. Para entender el funcionamiento veamos que pasa si ponemos un ciclo de 8 (ARR=7) y configurando el OC1 como PWM.

timer_set_period(TIM4, 7); // contando de 0 a 7...
timer_set_oc_mode(TIM4, TIM_OC1, TIM_OCM_PWM1);

Aquí hace falta hacer una aclaración, la gente de ST nos dio 2 opciones de PWM, una activa en alto (PWM1) y otra activa en bajo (PWM2), con esos nombres de mierda poco intuitivos. Eso quiere decir que mientras el valor en el CCR1 no llegue al de la cuenta este estará en 1 (PWM1) o estará en 0 hasta llegar a él (PWM2). Para que quede más claro, veamos el siguiente ejemplo donde ponemos el valor del OC1 en 2 (CCR1=2).

timer_enable_oc_output(TIM4, TIM_OC1);
timer_set_oc_value(TIM4, TIM_OC1, 2);
Funcionamiento en modo PWM1 y PWM2

Necesitamos habilitar la salida con timer_enable_oc_output() antes de poder utilizarlo, y luego, cambiando el valor con la función timer_set_oc_value() modificamos el ancho del pulso. Si la cuenta es hasta 7, entonces al poner el valor 7 quedará el último ciclo inactivo. Y si cuenta hasta 8 o más estará todo el ciclo activo. Por eso mi consejo es usar una etiqueta para definir el valor máximo:

#define MAX_COUNT 10000
// (...)
timer_set_period(TIM4, MAX_COUNT-1);
// (...)
// `val` es un valor entre 0 y MAX_COUNT:
timer_set_oc_value(TIM4, TIM_OC1, val);

En el código anterior vemos como 0 representa el 0% del ciclo y MAX_COUNT es el 100% del ciclo.

Este modo alineado al centro es ideal para manejar servomotores y que todos se muevan al mismo «mismo tiempo», por ejemplo. Para iluminación con LEDs yo recomendaría lo siguiente:

PWM alineado al centro

Como cantaba Reina Reech cuando era niño, vamos a hacer colores. Sí, tengo a disposición un LED RGB ánodo común, en mi placa de experimentación. En este caso lo coloqué en PB7 (T4C2), PB8 (T4C3) y PB9 (T4C4), y les voy a dejar un ejemplo de degradé utilizando el PWM alineado al centro.

¿Por qué alineado al centro? Es fácil, con un solo LED quizás el impacto no se note, pero si fuese una tira de LEDs y unos MOSFets conmutando a alta velocidad, imagínense el impacto para la fuente cuando para formar un color todos los leds prendan al mismo tiempo y luego se van apagando de a uno. El resultado es un pico de consumo. Que podemos evitar muy fácilmente con el PWM alineado al centro, porque de esta forma los LEDs prenden de a uno a la vez y se apagan de la misma manera.

Imaginemos que configuramos el TIM4 para contar hasta 4 en modo alineado al centro y los OC2/3/4 en los valores 1, 2 y 4. Entonces obtendríamos las siguientes señales:

Como vemos el contador del TIM sube y baja respectivamente (el bit de dirección funciona solo como lectura en este modo). Y vemos como las señales tiene su pico en el medio, crecen y se angostan desde el medio. Esto es muy utilizado para manejar motores. Pero, como ya les mencioné, me parece que sería importante que lo empiecen a considerar para iluminación.

A su vez, este micro tiene 3 modos centrados, que para PWM no interesan, ya que la diferencia está en cuando se genera el evento de interrupción (1- cuando está contando para arriba, 2- cuando esta contando para abajo y 3- en ambos instantes). Por eso, en este caso es irrelevante.

timer_set_mode(
    TIM4,                 // Timer general 4
    TIM_CR1_CKD_CK_INT,   // Clock interno como fuente
    TIM_CR1_CMS_CENTER_1, // Modo centrado 1
    TIM_CR1_DIR_UP);      // Indistinto, es ignorado...

Como el LED es ánodo común, este estará constantemente a VCC (3v3) y los cátodos a los pines del micro (con sus respectivas resistencias limitadoras). Por lo tanto, nos convendría utilizar el modo PWM2 (activo en bajo):

// MAX_COUNT = 10000
timer_set_period(TIM4, MAX_COUNT-1); // 72M/2/10000 = 3,6khz
// Configuramos las salidas del timer OC: 
timer_set_oc_mode(TIM4, TIM_OC2, TIM_OCM_PWM2);
timer_set_oc_mode(TIM4, TIM_OC3, TIM_OCM_PWM2);
timer_set_oc_mode(TIM4, TIM_OC4, TIM_OCM_PWM2);

Noten que (obviamente) cuando usamos el timer alineado al centro al contar para arriba y abajo esto nos dividirá la frecuencia a la mitad, por eso el calculo es /2 para sacar el ciclo de trabajo del PWM.

Para hacer el degradé arranqué empecé con el rojo al 100% y mientras lo decremento, incremento el azul hasta llagar al 100% y luego lo decremento mientras incremento el verde hasta llegar al 100% y luego vuelve a prender el rojo mientras se apaga el verde. Para este proceso utilizo una mini maquina de estado. Algo que valdría la pena mencionar en un post adicional.

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/cm3/systick.h>
#define MAX_COUNT 10000 // Máximo valor a contar del PWM.
volatile uint32_t millis;
void delay_ms(uint32_t ms)
{
    uint32_t lm = millis;
    while ((millis - lm) < ms);
}
int main()
{
    // CPU = 72Mhz; APB1 = 36Mhz; APB2 = 72Mhz
    rcc_clock_setup_in_hse_8mhz_out_72mhz();
    /* Se configura el Systick para interrumpir c/1ms */
    systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8);
    systick_set_reload(8999);
    systick_interrupt_enable();
    systick_counter_enable();
    /* Se configuran los pines de los puertos correspondientes */
    rcc_periph_clock_enable(RCC_GPIOB);
    // PB7, PB8 y PB9 => TIM4_CH2, TIM4_CH3 y TIM4_CH4
    gpio_set_mode(
        GPIOB,                          // Puerto correspondiente
        GPIO_MODE_OUTPUT_2_MHZ,         // Máxima velocidad de switcheo
        GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, // Función alternativa
        GPIO7 | GPIO8 | GPIO9);         // Pines asociados al OC2, OC3 y OC4
    /* Se configura el TIM como PWM alineado al centro */
    rcc_periph_clock_enable(RCC_TIM4);
    timer_set_mode(
        TIM4,                 // Timer general 4
        TIM_CR1_CKD_CK_INT,   // Clock interno como fuente
        TIM_CR1_CMS_CENTER_1, // Modo centrado
        TIM_CR1_DIR_UP);      // Indistinto, esto es ignorado...
    /*  Seteamos la cuenta del Timer
        
        Recordemos que como el PWM está alineado al centro el timer
        cuenta para arriba y luego para abajo, por lo tanto, debemos
        dividir la frecuencia x2.
    */
    timer_set_period(TIM4, MAX_COUNT - 1); // 72M/2/10000 = 3,6khz
    // Configuramos las salidas del timer:
    timer_set_oc_mode(TIM4, TIM_OC2, TIM_OCM_PWM2); // PWM2: active LOW
    timer_set_oc_mode(TIM4, TIM_OC3, TIM_OCM_PWM2);
    timer_set_oc_mode(TIM4, TIM_OC4, TIM_OCM_PWM2);
    // Habilitamos las salidas de los canales:
    timer_enable_oc_output(TIM4, TIM_OC2);
    timer_enable_oc_output(TIM4, TIM_OC3);
    timer_enable_oc_output(TIM4, TIM_OC4);
    // El timer empieza a contar:
    timer_enable_counter(TIM4);
    uint16_t pwm_val = 0;
    uint8_t state = 0;
    while (true)
    {
        switch (state)
        {
            case 0:
                timer_set_oc_value(TIM4, TIM_OC4, MAX_COUNT - pwm_val);
                timer_set_oc_value(TIM4, TIM_OC2, pwm_val++);
                break;
            case 1:
                timer_set_oc_value(TIM4, TIM_OC2, MAX_COUNT - pwm_val);
                timer_set_oc_value(TIM4, TIM_OC3, pwm_val++);
                break;
            case 2:
                timer_set_oc_value(TIM4, TIM_OC3, MAX_COUNT - pwm_val);
                timer_set_oc_value(TIM4, TIM_OC4, pwm_val++);
                break;
            default:
                state = 0;
                continue;
        }
        if (pwm_val == MAX_COUNT)
        {
            pwm_val = 0;
            state++;
        }
        delay_ms(1);
    }
}
void sys_tick_handler(void)
{
    millis++; // Se incrementan los millis
}

Como es la costumbre les dejo el repo de gitlab para que se lo puedan clonar y correr en PlatformIO directamente: https://gitlab.com/tute-avalos/libopencm3-06-pwm-rgb

Dejar una respuesta