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, Electrónica, Nivel: Básico, STM32

Adentrandonos en las entrañas del STM32F1

Nivel: Básico

¿Qué tengo que saber para este post?

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

En el post anterior estuvimos analizando las herramientas de Software y Hardware que utilizaremos en las siguientes entrada del blog para aprender a programar un Cortex-M utilizando Software Libre. Pero no hicimos un análisis de cómo es la programación o la API de libOpenCM3 (la cual se encuentra en desarrollo actualmente y no es definitiva). Lo que seguro no va a cambiar es el µControlador, es decir, el STM32F103x que lleva 10 años en el mercado recibirá quizás alguna revisión, pero en sí no cambiará el Hardware. Por lo tanto analizaremos algunas cuestiones específicas de éste, ya que sería mala idea no conocer lo que programamos, para eso usa Arduino y agarrate la cabeza cuando no sepas porqué no funciona.

El RCC

Lo primero que vamos a analizar es el RCC (Reset and Clock Control), que es el periférico/módulo (casi) principal después del CPU en estos micros. No me voy a detener mucho en el Reset, pero es bueno que sepan que hay varias fuentes de Reset, y un registro que levanta un flag dependiendo de cual fue el que lo causo (para ejecutar diferentes rutinas de inicialización). Por ejemplo, además de la (obvia) señal externa del Reset, podría haberse causado por alguno de los Watchdogs (es decir que nuestro programa se colgó en algún punto) y quizás queremos reportar o aplicar un log a esa situación.

Lo que me parece más relevante en este punto, más introductorio, es hablar del intrincado sistema de Clocks que poseen esta familia de microcontroladores. Empecemos fácil, tenemos las siguientes alternativas como fuente generadoras de clock:

  • Clock del sistema (SYSCLK):
    • HSI (High Speed Internal): Un RC interno de 8Mhz.
    • HSE (High Speed External): Cristal o resonador externo entre 4-16Mhz.
    • PLL (Phase Locked Loop): Puede tener como fuente el HSI o el HSE pasados por algun divisor x2 y ajustable hasta 72Mhz.
  • Clock del RTC:
    • LSI (Low Speed Internal): Un resonador RC de 40Khz que puede utilizarse tanto para el RTC y para el Watchdog Independiente.
    • LSE (Low Speed External): Dos pines externos para poner un cristal o resonador (preferentemente de 32,768kHz).
    • HSE/128: Puede tomar del mismo cristal/resonador externo pasado por un divisor por 128.
Cuando digo intrincado me refiero a esto…

Como la configuración del SYSCLK, como se lo pueden imaginar, es bastante compleja, y consta de varias instrucciones, libOpenCM3 nos ofrece funciones intuitivas que nos permiten configurarlo con algunas alternativas (si no invocamos ninguna de estas se utiliza el HSI como fuente del SYSCLK):

void 	rcc_clock_setup_in_hsi_out_64mhz (void);
void 	rcc_clock_setup_in_hsi_out_48mhz (void);
void 	rcc_clock_setup_in_hsi_out_24mhz (void);
void 	rcc_clock_setup_in_hse_8mhz_out_24mhz (void);
void 	rcc_clock_setup_in_hse_8mhz_out_72mhz (void);
void 	rcc_clock_setup_in_hse_12mhz_out_72mhz (void);
void 	rcc_clock_setup_in_hse_16mhz_out_72mhz (void); 
void 	rcc_clock_setup_in_hse_25mhz_out_72mhz (void);

Del SYSCLK se cuelgan todos los demás periféricos, ya que este se dirije directamente al AHB (Advanced High-performance Bus), el cual se distribuye a los periféricos a través de dos APBs (Advanced Peripheral Bus) independientes, uno con una frecuencia de trabajo máxima de 36Mhz y otro de 72Mhz. A continuación listaré los periféricos:

  • APB1 (36Mhz máx):
    • BackUp Domain (RTC)
    • TIM2, 3 y 4 (propósito general)
    • USART2 y 3
    • SPI2
    • I2C1 y 2
    • CAN
    • USB 2.0
    • SRAM512B
    • Window Watchdog
  • APB2 (72Mhz máx):
    • EXTI (Wakeup)
    • GPIOs
    • TIM1 (timer «avanzado»)
    • SPI1
    • ADC1 y 2

Cada uno de los periféricos nombrados tiene su habilitación de clock manejada por los registros del RCC, y están todos desconectados por default. Por ello tenemos la función de libOpenCM3:

void rcc_periph_clock_enable(enum rcc_periph_clken clken);

Los enum tiene el siguiente formato RCC_<PERIFÉRICO>, así por ejemplo, si queremos habilitar el clock del GPIOC debemos pasar como argumento el RCC_GPIOC. Des esta forma habilitamos los clocks de cualquier periférico sin importarnos o recordar si estaba en el APB1 o APB2 (eso lo determina la función internamente).

Los GPIOs

Lo principal en cualquier µControlador es empezar a manejar las entradas y salidas, si bien hablamos un poco de esto en el post anterior, formalicemos algunas cuestiones interesantes que tiene estos dispositivos. Para empezar, este integrado funciona con 3v3 pero tiene pines que soportan 5v, como nunca recuerdo cuales son y cada vez le encuentro menos sentido a utilizar lógicas de 5v, recomiendo utilizar todo 3v3 para evitar dolores de cabeza (y quemar pines). Y saber que si necesitan algo de 5v ir a buscar el pin que más les convenga.

Un pin de un STM32F103 es más o menos así

Tenemos entonces funciones de GPIO normales, entrada y salida, como funciones alternativas tanto en salida como entrada. Las función que nos permite esto es la siguiente:

void gpio_set_mode(uint32_t gpioport, uint8_t mode, uint8_t cnf, uint16_t gpios);

Las alternativas son las siguientes para el STM32F103Cx:

  • gpioport: GPIOA, GPIOB, GPIOC y GPIOD
  • mode: GPIO_MODE_INPUT, GPIO_MODE_OUTPUT_10_MHZ, GPIO_MODE_OUTPUT_2_MHZ, GPIO_MODE_OUTPUT_50_MHZ
  • cnf: GPIO_CNF_INPUT_ANALOG, GPIO_CNF_INPUT_FLOAT, GPIO_CNF_INPUT_PULL_UPDOWN, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN
  • gpios: GPIO0|...|GPIO15

Lo que tiene de interesante, es que nos permite darle a varios pines a la vez del mismo puerto la misma configuración separando a los gpios deseados con | (OR). Porque debemos pensar que esta funciones son como escribir en los registros directamente pero sin aprendernos los nombres de los mismos ni cuantos son realmente. Luego tenemos las siguientes funciones para la manipulación de los puertos:

void gpio_set (uint32_t gpioport, uint16_t gpios);
void gpio_clear (uint32_t gpioport, uint16_t gpios);
void gpio_toggle (uint32_t gpioport, uint16_t gpios);
uint16_t gpio_get (uint32_t gpioport, uint16_t gpios);

Aquí solo dejo las de más alto nivel, donde podemos pasar el puerto y los pines (grupo de pines) sobre el cual efectuamos la acción. Recalco lo de «grupo de pines», ya que cuando usamos gpio_get() nos devolverá un uint16_t donde solo los bits (representando a los pines) que le pedimos tendran un valor significativo.

Como este micro tiene muchos periféricos, también podemos remapear algunas funciones, por ejemplo los pines de la USART1 pueden ser el PA9 y PA10 o el PB6 y PB7. Los protocolos de programación y debugger JTAG y SWD son las funciones principales de los pines, y alternativamente son GPIOs. Para poder utilizar estas funcionalidades con libOpenCM3 tenemos la función:

void gpio_primary_remap(uint32_t swjdisable, uint32_t maps);

swjdisable es justamente para inhabilitar alguna señal de programación que no utilicemos con el SWD que se utilizan con JTAG (ya dejaré un ejemplo con eso en otro post).

Ejemplo de código

Como no me gustaría dejarles solo información sin un ejemplo, les dejo un programita donde utilizaré 3 LEDs y un pulsador. Les dejo el conexionado y el main.c.

El LED1 está en la placa
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>

int main()
{
    // SYSCLK a 72Mhz
    rcc_clock_setup_in_hse_8mhz_out_72mhz();

    // Se habilitan los clocks de los puertos a utilizar:
    rcc_periph_clock_enable(RCC_GPIOC);
    rcc_periph_clock_enable(RCC_GPIOB);

    // Configuramos el LED de la BluePill PC13
    gpio_set_mode(
        GPIOC,
        GPIO_MODE_OUTPUT_2_MHZ,
        GPIO_CNF_OUTPUT_PUSHPULL,
        GPIO13);

    gpio_set(GPIOC,GPIO13);     // LED1 apagado

    // Configuramos los LED2 y LED3 (PB5 y PB6)
    gpio_set_mode(
        GPIOB,
        GPIO_MODE_OUTPUT_2_MHZ,
        GPIO_CNF_OUTPUT_PUSHPULL,
        GPIO5 | GPIO6);

    gpio_set(GPIOB, GPIO5);     // LED2 prendido
    gpio_clear(GPIOB, GPIO6);   // LED3 apagado

    // Configuramos el pulsador PB15
    gpio_set_mode(
        GPIOB,
        GPIO_MODE_INPUT,
        GPIO_CNF_INPUT_FLOAT,   // tiene pull-up externo
        GPIO15);                

    while (1)
    {
        if (gpio_get(GPIOB, GPIO15) == 0)   // Si el pulsador está presionado
            gpio_toggle(GPIOB, GPIO6);      //   invertimos el LED

        // Siempre se invierte el LED2
        gpio_toggle(GPIOB, GPIO5);  

        // El PC13 está prendido cuando el PB5 está prendido
        gpio_toggle(GPIOC, GPIO13); 

        for (uint32_t i = 0; i < 7200000; ++i)
            __asm__("nop");
    }
}

Entonces los LED1 y LED2 prenden alternadamente (cuando uno está prendido el otro está apagado) y el LED3 solo alterna si el pulsador está presionado.

Repo de GitLab: https://gitlab.com/tute-avalos/libopencm3-basic-gpios

Hojas de datos: https://www.st.com/resource/en/datasheet/stm32f103c8.pdf https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf

Documentación de libOpenCM3: http://libopencm3.org/docs/latest/stm32f1/html/modules.html

Dejar una respuesta