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, Electrónica, Nivel: Intermedio, STM32

Interrupciones externas con el STM32F1

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.

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

El NVIC

Ya vimos como configurar el clock principal y los puertos de entrada/salida, utilizamos el SysTick y el debugger. Ahora, para poder seguir avanzando necesitamos hablar del NVIC (Nested Vectored Interrupt Control), que para decirlo mal y pronto de forma reducida, es el módulo o periférico que maneja las interrupciones en un Cortex-M. Cada fabricante aplica la cantidad de interrupciones y prioridades dependiendo de sus necesidades. En este caso tenemos hasta 43 fuentes de interrupción con 16 prioridades. Si tuvieramos que hablar de esto a fondo, ameritaría toda una sección, por eso no lo vamos a hacer.

Nuestro querido libOpenCM3 nos ofrece una abstracción lo suficientemente potente como para que necesitemos escencialmente estas tres funciones:

void nvic_enable_irq (uint8_t irqn);
void nvic_disable_irq (uint8_t irqn);
void nvic_set_priority (uint8_t irqn, uint8_t priority);

A éstas funciones hay que pasarle el número o vector de interrupción correpondiente, que porsupuesto tiene un define compuesto por NVIC_<periférico>_IRQ por ejemplo:

// Se habilita la interrupción de la USART1:
nvic_enable_irq(NVIC_USART1_IRQ);

En cuanto a las prioridades, si nunca las utilizaron, significa que si una interrupción se está ejecutando y llega otra con mayor prioridad, la primera queda a mitad de ejecución y se atiende la que tiene prioridad más alta (es uno de los casos). Siendo 0 (cero) la prioridad más alta, y 15 la más baja en este microcontrolador.

// Se baja la prioridad de la interrupción:
nvic_set_priority(NVIC_USART1_IRQ, 1);
Representación de una interrupción anidada

Cuando estamos usando muchas interrupciones al mismo tiempo se vuelve casi una obligación setear al menos un poco la prioridades (dependiendo de la implementación).

El NVIC es un módulo del Cortex-M, por lo tanto está presente en todos los micros de todos los fabricantes, y hay 16 vectores de interrupción que están reservados para este y tiene un máximo de 512 vectores (contando los 16), por lo tanto el fabricante podría tener hasta 496 interrupciones en su micro. Como anticipé, el STM32F103 tiene 43. Y donde y cómo se implementan depende del fabricante.

Las EXTI

Las EXTI o Iinterrupciones EXTernas, son las clásicas interrupciones que se ejecutan al alternar un pin del microcontrolador. En los microcontroladores de 8 bits más antiguos solo algunos pines tenían esta capacidad, pero en este cualquier pin puede ser fuente de interrupción. Como los GPIOs tiene 16 pines (del 0 al 15), tenemos del EXTI0 a EXTI15, pero debemos tener en cuenta que sólo las del 0 al 4 tienen vectores de interrupción independientes. Las que son del pin 5 al 9 comparten el mismo vector y ocurre lo mismo del 10 al 15. Pero todas pueden ser configuradas separadamente (flanco ascendente, descendente o ambos).

Cuando se configura el EXTI0, debemos seleccionar de qué puerto tomaremos la fuente de interrupción, del GPIOA o GPIOB, ya que no disponemos de otro GPIO que tenga disponible el pin 0 en la BluePill. Pero en otro modelo de la misma familia con más pines (este integrado tiene 48 pines, pero hay de 100 pines) se abre el abanico de posibilidades. Es importante que recordemos que antes de utilizar las EXTI debemos habilitar el periférico AFIO (Alternative Functions Input/Output) a través del RCC:

rcc_periph_clock_enable(RCC_AFIO);

La API para manejar las EXTI consta de las siguiente funciones básicas:

void exti_select_source(uint32_t exti, uint32_t gpioport);
void exti_set_trigger(uint32_t extis, enum exti_trigger_type trig);
void exti_enable_request(uint32_t extis);
void exti_disable_request(uint32_t extis);
void exti_reset_request(uint32_t extis);
uint32_t exti_get_flag_status(uint32_t exti);

Por ejemplo, si quisiéramos utilizar la EXTI1, una alternativa de configuración podría ser la siguiente:

// (...)
rcc_periph_clock_enable(RCC_AFIO);
// (...)
rcc_periph_clock_enable(RCC_GPIOB);
gpio_set_mode(GPIOB,GPIO_MODE_INPUT,GPIO_CNF_INPUT_FLOAT,GPIO0);
// Se configura la interrupción externa EXTI0:
exti_select_source(EXTI0, GPIOB);
exti_set_trigger(EXTI0, EXTI_TRIGGER_FALLING);
exti_enable_request(EXTI0);
// Se habilita la interrupción:
nvic_enable_irq(NVIC_EXTI0_IRQ);
// (...)

Las ISRs (Interruption Sub-Rutins)

En los ARM Cortex-M utilizando CMSIS se utilizaba una técnica que es darle el atributo de compilación WEAK a las funciones que servían de ISR, para poder redefinirlas en nuestro código. En libOpenCM3 hacen lo mismo, solo cambian los nombres para coincidir con su estilo snake_case y lo podemos encontrar en la documentación. Se componen del nombre como <vector/interrupción>_isr(), para las EXTI son las siguientes:

void exti0_isr (void);
void exti1_isr (void);
void exti2_isr (void);
void exti3_isr (void);
void exti4_isr (void);
void exti9_5_isr (void);
void exti15_10_isr (void);

Ejemplo de código

Tenemos toda la información necesaria para adentrarnos en un ejemplo concreto. En el siguiente vamos a hacer un programa que utilizando una interrupción externa cuenta (en milisegundos) la longitud del pulso que entra por el pin asociado a la misma. Luego, se hará titilar un LED conectado a PB6 una cantidad de veces igual a la cantidad de segundos que duró el pulso. Para ello, se reconfigurará la interrupción para que empiece a contar cuando llega el flanco descendente y termine de contar cuando llegue el ascendente, utilizando una variable que se incrementa dentro de la interrupción del SysTick.

El gráfico anterior ilustra cómo sería el comportamiento, NO ESTÁ A ESCALA, obviamente. El pin de interrupción que elegí fue el PB15, porque allí tengo un pulsador en mi placa básica de entradas y salidas, pero pueden utilizar cualquier otro:

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

#define FALLING false // Flanco descendente
#define RISING true   // Flanco ascendete

volatile uint32_t millis;      // cantidad de milisegundos
volatile bool trig;            // sentido del trigger
volatile uint32_t pulse_width; // cantidad de ms que dura el pulso

/**
 * @brief Demora bloqueante en milisegundos
 * 
 * @param ms cantidad de milisegundos a demorar.
 */
void delay_ms(uint32_t ms);

int main(void)
{
    // Configuración del SYSCLK
    rcc_clock_setup_in_hse_8mhz_out_72mhz();

    // PC13 (LED de la BluePill) como salida
    rcc_periph_clock_enable(RCC_GPIOC);
    gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
    gpio_set(GPIOC, GPIO13);

    // LED en PB6 (activo en alto), el PB15 como entrada flotante (default)
    rcc_periph_clock_enable(RCC_GPIOB);
    gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO6);
    gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO15);

    /* Para configurar las EXTI es necesario activar el 
       Alternative Functions Input/Output              */
    rcc_periph_clock_enable(RCC_AFIO);

    /* Se configura la interrupción EXTI15 en el GPIOB con flanco
       inicialmente descendente.                                 */
    exti_select_source(EXTI15, GPIOB);
    trig = FALLING;
    exti_set_trigger(EXTI15, EXTI_TRIGGER_FALLING);
    exti_enable_request(EXTI15);

    // Habilitación de la interrupción en el NVIC:
    nvic_enable_irq(NVIC_EXTI15_10_IRQ);

    // Se configura el SysTick para interrumpir cada 1ms:
    systick_set_frequency(1000, rcc_ahb_frequency);
    systick_counter_enable();
    systick_interrupt_enable();

    while (true)
    {
        if (pulse_width) // Si el pulso es distinto de 0:
        {
            uint32_t s = pulse_width / 1000; // Cantidad de segundos que duro el pulso
            s *= 2;                          // Doble para prender y apagar
            while (s--)                      // El LED PB6 titila la cantidad de
            {                                //    segundos que duró el pulso:
                gpio_toggle(GPIOB, GPIO6);
                delay_ms(300);
            }
            pulse_width = 0; // se resetea para que no se repita el "titileo"
        }
    }
}


void delay_ms(uint32_t ms)
{
    uint32_t tm = millis + ms;
    while (millis < tm);
}

void exti15_10_isr(void)
{
    static uint32_t lm = 0;
    exti_reset_request(EXTI15); // Se baja el flag
    if (trig == FALLING)        // Si fue un flanco descendente
    {
        lm = millis;               // Tomo la referencia de tiempo
        gpio_clear(GPIOC, GPIO13); // prende el LED de la BluePill
        trig = RISING;             // Se configura la interrupción por flanco ascendente
        exti_set_trigger(EXTI15, EXTI_TRIGGER_RISING);
    }
    else
    {
        pulse_width = millis - lm; // Se carga la longitud del pulso
        gpio_set(GPIOC, GPIO13);   // Se apaga el LED de la BluePill
        trig = FALLING;            // Se configura la interrupción por flanco descendente
        exti_set_trigger(EXTI15, EXTI_TRIGGER_FALLING);
    }
}

void sys_tick_handler(void)
{ // cada 1ms:
    millis++;
}

No lo he dicho, pero saben que ante cualquier duda pueden utilizar los comentarios. Les dejo el repo de GitLab y hasta la próxima.

https://gitlab.com/tute-avalos/libopencm3-04-extis

Dejar una respuesta