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.
—————————————
En esta tercera entrega ya viene siendo tiempo de explorar las opciones que tenemos para temporizar con interrupciones y las bondades de PlatformIO para hacer debugging/depurado de nuestro firmware. Para ello debemos adentrarnos un poco más en el ARM Cortex-M que tiene este µControlador.
El SysTick
Para empezar, todos los Cortex-M traen consigo (dentro del CPU) un timer denominado SysTick de 24-bits con cuenta descendiente, pensado para utilizarse como la referencia de tiempo en Sistemas Operativos de Tiempo Real (RTOS). Lo bueno es que sin importar el fabricante, sabemos que contamos con él. Sea con libOpenCM3 o utilizando CMSIS la interfaz para éste no cambiará. Para que este mecanismo funcione ARM establece disponer los elementos en las mismos rangos de memoria a los fabricantes. Pero para hablar de eso nos deberíamos meter en la arquitectura, y quizás eso lo hable en un post aparte.
libOpenCM3 dispone las siguisten funciones para configurar el SysTick:
void systick_set_clocksource (uint8_t clocksource);
void systick_set_reload (uint32_t value);
bool systick_set_frequency (uint32_t freq, uint32_t ahb);
void systick_counter_enable (void);
void systick_counter_disable (void);
void systick_interrupt_enable (void);
void systick_interrupt_disable (void);
Para setear la cuenta del timer tenemos 2 opciones, utilizar systick_set_clocksource()
para decirle si queremos que tome el clock del AHB directamente o pasado por un divisor por 8 (esto se puede ver en el gráfico del post anterior). Luego de definir el clock debemos pasarle el valor que se cargará automáticamente después de llegar a 0 con systick_set_reload()
. La segunda opción es utilizar systick_set_frequency()
con la frecuencia deseada, pero diciéndole a que velocidad está el AHB. Esta última opción utiliza las 2 primeras mencionadas internamente.
Luego queda habilitar el contador (systick_counter_enable()
) y, en el 99.99% de los casos, la interrupción asociada al SysTick (systick_interrupt_enable()
). Hay otras funciones definidas como systick_clear()
o systick_get_value()
, pero dejo que lo investiguen por su cuenta (creo que son bastante obvias).
Una vez habilitada la interrupción debemos definir qué haremos cuando esta ocurra, es decir nuestra famosa ISR (Sub-Rutina de Interrupción). En los ARM Cortex-M las funciones que serán rutinas de interrupción ya están declaradas con el modificador WEAK
, es decir, que podemos redefinirla en nuestro código. Por eso debemos respetar el nombre de la misma como la definieron en libOpenCM3 (lo mismo pasa si utilizamos CMSIS, solo difieren los nombres). En el caso del SysTick es:
void sys_tick_handler(void)
{
// Tu código va aquí :)
}
ACLARACIÓN: Si bien utilizamos una interrupción aquí, las interrupciones en sí, las vamos a tratar en post aparte. Porque necesitamos hablar del módulo NVIC presente en los Cortex-M. Como el SysTick es parte del CPU no necesitamos utilizarlo.
ACLARACIÓN 2: El SysTick utiliza el clock del sistema, así que si mandamos a bajo consumo o cortamos el clock del CPU, obviamente este timer se detiene. Ténganlo presente.
El Blinky 3.0 + debugging
En el siguiente código se programa un ineficiente delay con el SysTick y una variable que lleva los ticks del sistema:
#include<libopencm3/stm32/rcc.h>
#include<libopencm3/stm32/gpio.h>
#include<libopencm3/cm3/systick.h>
#include<libopencm3/cm3/nvic.h>
volatile uint32_t ticks; ///< @brief ticks de la última cuenta (cada 1ms)
/**
* @brief Delay bloqueante utilizando el SysTick
*
* @param ms duración de la demora
*/
void delay_ms(uint32_t ms);
int main()
{
// SYSCLK a 72Mhz
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);
/*** Configuración del SysTyck ***/
// Se toma la velocidad del AHB (SYSCLK) y se activa el divisor por 8:
systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8); // 72M/8 = 9M
// Se carga el valor a cargar cuando el SysTick llega a 0 (downflow):
systick_set_reload(8999); // 9M / 9000 = 1k => T = 1ms
// Se habilita la interrupción del SysTick:
systick_interrupt_enable();
// El SysTick empieza a contar:
systick_counter_enable();
while(true)
{
// Se alterna el LED en PC13
gpio_toggle(GPIOC,GPIO13);
// Esperamos 500ms
delay_ms(500);
}
}
void delay_ms(uint32_t ms)
{
uint32_t tm = millis + ms;
while(millis < tm);
}
/**
* @brief Sub-Rutina de interrupción del SysTick (cada 1ms)
*/
void sys_tick_handler(void)
{
ticks++; // Se incrementan los ticks
}
Ahora tenemos un blinky que tiene frecuencia de 1Hz, y es un ejemplo muy sencillo, por eso aprovecho para hablar de cómo iniciar una sección de debugging en PlatformIO.
El debugger
Utilizar el debugger para correr paso a paso es como ser un acosador de micros. Nos metemos de forma invasiva y espiamos el estado en que se encuentran todos los registros del SFR, la memoria, variables, etc. Es manosearlo de la manera menos elegante, pero no discutimos sus resultados. Es muy útil para saber si un programa está ejecutando ese pedazo de código, si la ISR está ejecutándose o si se está ejecutando recursivamente (pasa en las mejores familias) entre otras utilidades.
Para iniciar la sección de depurado debemos acceder a la barra de VSCodium correspondiente y darle al play:
A continuación explicaré como funciona el debug, si ya utilizaste uno alguna vez, lo próximo te va a parecer demasiado obvio, y quizás quieras terminar la lectura acá, para vos, nos vemos en la próxima. Ahora si venís de Arduino y no sabés de que se trata el debugger, te invito a ver y entender porque los usuarios de esa plataforma reclamaron por años esta herramienta.
Automáticamente, después de darle al Play, iniciará el firmware en nuestra BluePill y podrá un breakpoint en el main (en la primera instrucción dentro del main):
Contamos con una barra muy simple para controlar la sesión de debugging:
- Run: ejecuta nuestro programa continuamente y se detiene con una pausa volviendo a hacer clic o en un breakpoint.
- Step over: Ejecuta la instrucción de la linea, si es una función ejecutará todo el bloque de la función y se parará en la próxima instrucción.
- Step into: Ejecuta la instrucción de la linea, si es una función se meterá adentro de ella y quedará en la primera instrucción dentro.
- Step out: Sale de la función donde se encuentre y queda en la próxima linea después del llamado a función. Si estamos en el main es como hacer Run.
- Reset: Mada una señal de reset al dispositivo y vuelve a empezar desde la primera instrucción del main.
- Stop: Se termina la sesión de depurado, se resetea el micro y sigue ejecutandose normalmente.
Cabe destacar que el CPU estará esperando cada step como un pulso de clock. No es lo mismo que cuando corre normalmente, pero al menos podemos analizar porqué algún sector de código no funciona. Podemos poner un breakpoint a voluntad donde creamos necesario. Por ejemplo, si quisiéramos saber si la interrupción se está ejecutando podemos hacer clic al lado del número de linea y mandar un Run y esperar a que se detenga en esa linea para asegurarnos de que todo esté funcionando como corresponde:
Además, podemos ver qué pasa con las variables globales y locales cuando estamos dentro de alguna función. Eso aparece en la barra lateral de la izquierda que podemos expandir y redimensionar de ser necesario:
No hice una captura porque no tenía una variable con cual hacerlo, pero en WATCH
podemos escribir el nombre de una variable y ver como va cambiando (cada vez que apretemos pausa o vayamos paso a paso). Y en CALL STACK
ver la cola de llamadas a función para ver cuanto estamos anidando. Y para los estudiantes avanzados que quieran saber más o estén estudiando a fondo la arquitectura ARM Cortex-M, pueden ver el estado de los registros y los SFR de los periféricos además del desensamblado, es decir, el assembler equivalente al código de C mostrado, y ejecutar linea a linea el mismo:
Como podemos observar PlatformIO es un IDE con todas las letras bien puestas. Con la integración de una de las herramientas más valiosas con las que podemos contar.
Como siempre, les dejo el link a GitLab: https://gitlab.com/tute-avalos/libopencm3-03-blinky-systick
En la próxima nos vamos a poner más hardcore (?)
Ángel Di Lazzaro
Hola que tal, vi que la función de interrupción predefinida systick_handler(); se encuentra en el header nvic.h. El compilador de por sí no me reconocía la función hasta haber agregado dicha librería. Y luego, que no sé si está bien, le agregué a las configuraciones dentro del main lo siguiente:
nvic_enable_irq(NVIC_SYSTICK_IRQ);
Grabé el código en la placa y funciona. De igual forma lo hice a ciegas. O sea, sin entender bien lo que hace. Saludos !
msavalos
Gracias por tu comentario, efectivamente es necesario agregar el header `nvic.h` para que no dé error de compilación, voy a corregirlo en mi entrada. Cuando escribí este post no hacía falta, el compilador se daba cuenta solo que faltaba ese .h y solo daba un warning. En cuanto a la habilitación en el nvic con `nvic_enable_irc()` no sería necesario, porque la interrupción del systick es de alta prioridad y está habilitada por default.
Gracias por comentar tu experiencia. Si te quedan dudas, avisame y espero poder contestartelas. Saludos!