ARM: Temporizadores

Los temporizadores son unos registros internos en la memoria del microcontrolador capaces de realizar retardos de precisión o tareas periódicas, valiéndose de la entrada de reloj para contar el número de veces que le llega un pulso y, tras dividir por el preescaler (para que no vaya demasiado rápido), sumarlo al registro.

Una vez dicho registro se “desborda”, es decir, excede el máximo o mínimo número posible, puede crear una interrupción.

Como conocemos con precisión la frecuencia a la que trabajan y nosotros le asignamos el preescaler, somos capaces de hacer un cálculo del tiempo aproximado que necesita para desbordar.

Un contador funciona de la misma forma, pero utilizando como reloj un pin externo, de forma que podemos contar eventos que ocurren en ése pin.

Además de crear retardos, el timer puede averiguar la frecuencia o periodo de algunos sensores y señales externas como códigos infrarrojos o de radiofrecuencia.

Uno de los pocos problemas de usar timers es que tienen que “hacer cola” a la hora de crear una interrupción por el tema de las prioridades asignadas. Aunque la solución es darles la prioridad máxima cuando sea totalmente necesario.

Entre las ventajas, la CPU no gasta recursos ni tiene que prestar atención a un timer hasta que éste desborda. Los timers tienen funciones listas para arrancar y parar la cuenta cuando sea necesario.

Los ARM Cortex-M tienen un timer principal de 24 bits (systick) y otros 14 de distintas capacidades y frecuencias.

¿Qué es el preescaler? Como he mencionado anteriormente, sirve para ralentizar la cuenta del timer. Si ponemos un preescaler de 2, por ejemplo, al registro del timer sólo se sumará 1 cuando hayan llegado 2 pulsos de reloj. En la tabla anterior podemos ver que llega hasta los 65536 pulsos, por lo que se puede alargar mucho más un sólo registro de timer.

El timer SysTick

Es el temporizador principal y más simple de hacer funcionar, no tiene preescaler. Éste timer, al contrario que los demás, no puede pararse. Sin embargo, sólo es necesaria una función para configurarlo al tiempo que necesitemos.

Por defecto, tiene la prioridad más alta entre las interrupciones.

El valor con el que se debe cargar la función del timer se calcula mediante la fórmula:

Valor_Systick = Systick_Clock (168 MHz) * Tiempo (s)

Teniendo en cuenta que no puede exceder 0xFFFFFF. Las funciones a utilizar son:

  • SysTick_Config(uint16_t Valor_Systick); //Inicializa el contador con el valor, y lo habilita junto a su interrupción
  • NVIC_SetPriority(SysTick_IRQn, Número_prioridad); //Si se desea cambiar la prioridad
  • En stm32f4xx_it.c se crea la función void SysTick_Handler(void) que es la ISR de éste timer

El resto de timers

Éstos ya no son tan simples de configurar, si bien tienen varias ventajas sobre el SysTick. Veamos un ejemplo donde se configura el TIM3 (16 bit, 84 MHz) para hacer una cuenta de 2 segundos, y cada vez que se desborde que cambie el estado de un LED:

Podemos hacer la configuración tanto en el archivo Main como en cualquier otra librería.

void main(){
   //Inicialización de los LED (no voy a dar detalles)
   LED_Init();
   TIM3_Config(); //Configura el timer 3
   TIM_Cmd(TIM3, ENABLE); //Habilita la cuenta e interrupción del timer 3

   while(1); //Se espera al desbordamiento
}

void TIM3_Confif(){
   //En primer lugar se decide el prescaler que se le va a asignar
   //Lo que busco es una frecuencia de reloj de forma que multiplicada
   //por un número menor que 2^16 - 1 de 2 segundos
   //Pongamos por ejemplo 10 KHz:
   //TIM_Pre = (84MHz / 10 KHz) - 1 = 8399
   //TIM_Value = 10 KHz * 2 s = 20000

   TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //La variable correspondiente

   TIM_TimeBaseStructure.TIM_Prescaler = 8399; //Asignamos prescaler
   TIM_TimeBaseStructure.TIM_Period = 20000; //Y el tiempo correspondiente
   TIM_TimeBaseStructure.TIM_ClockDivision = 0; //No divide el reloj del bus
   TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//La cuenta es hacia arriba

   TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //Se configura
   
   //Estructura de NVIC para la interrupción
   NVIC_InitTypeDef NVIC_InitStructure;

   // Habilitar el reloj del TIM3
   RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

   //Configuración de la interrupción
   NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
   NVIC_Init(&NVIC_InitStructure);

   //Habilitar la interrupción
   TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
} 

Y ahora en el stm32f4xx_it.c:

void TIM3_IRQHandler (void)  {

   if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
   {
      TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
      LED_Toggle(LED4);	
   }	
}

ARM: Interrupciones

En ocasiones, en vez de esperar a que llegue el turno del if que muestrea el estado del botón que estamos pulsando para que ocurran cosas, nos interesa que el microcontrolador esté atento al botón y actúe inmediatamente cuando se pulsa.

Recuerdo el concepto de interrupción en la parte de PICs:

La interrupción se definiría como decirle al PIC “deja absolutamente todo lo que estés haciendo y atiende a la función que he escrito para cuando ésto ocurra”

Un ARM se comporta casi de la misma forma, lo que se añade de nuevo es una lista de prioridades para las distintas interrupciones, debido a que éstos microcontroladores son capaces de llevar muchas más, y están diseñados de forma que pueden atender a todas, si no en el acto, un poco después al finalizar con la anterior.

Por ejemplo, la placa STM32F4Discovery soporta 240 fuentes de interrupción distintas y 256  niveles de prioridad.

Las interrupciones de las que hablaremos ahora son las exteriores, dadas por los pines (EXTI0,1,2,…)

Los ARM disponen de un controlador de interrupciones llamado Nested Vectored Interrupt Controller (NVIC), que compara la prioridad de la interrupción que acaba de saltar con la prioridad de la que se está ejecutando.

CMSIS utiliza unos números (IRQn) para identificar las interrupciones, siendo IRQn = 0 la primera y los valores negativos “excepciones del núcleo del procesador”. Por ejemplo el timer principal es el -1 y la línea 0 de interrupciones externas  el 6.

Una vez definida una interrupción, solo hay que ir a la librería stm32f4xx_it.c y añadir la función handler (ISR) correspondiente a dicha interrupción.

Veamos un ejemplo en el que se configura el pin PA0 (botón de usuario) para encender/apagar un LED mediante interrupción:

void main(){
   LED_Init(LED3) //Configura el pin del LED como salida, no voy a entrar en detalle
   EXTILine0_Config() //Configura el pin PA0 como entrada y su interrupción

   while(1); //Esperar a que llegue la interrupción
}

void EXTILine0_Config(){

   //Se declaran las estructuras
   GPIO_InitTypeDef GPIO_InitStructure;
   EXTI_InitTypeDef EXTI_InitStructure;
   NVIC_InitTypeDef NVIC_InitStructure;
 
   //Se le da reloj a GPIOA
   RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
   //Y también a SYSCFG con APB2
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

   //Se configura el pin PA0 como entrada
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
   GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   //Se conecta el pin PA0 a la línea 0 de EXTI
   SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);

   //Se configura la línea 0
   EXTI_InitStructure.EXTI_Line = EXTI_Line0; //Nos referimos a la línea 0
   EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //Va a generar interrupción
   EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //Con el flanco de subida del botón
   EXTI_InitStructure.EXTI_LineCmd = EXTI_LineCmd_ENABLE; //Habilitamos la línea
   EXTI_Init(&EXTI_InitStructure); //Función de configuración con los parámetros dados

   //Ahora se habilita la interrupción y se le da el nivel de prioridad más bajo
   NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;  //A qué EXTI nos referimos
   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; //Define la prioridad
   //con la que se ejecuta esta interrupción si ya hay otra en marcha, dejando la
   //otra para otro momento si es necesario. A menor número mayor prioridad
   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;
   //Si hay 2 interrupciones con la misma PreemptionPriority se define aquí la
   //prioridad que tiene ésta sobre la otra, el número más bajo es el más urgente
   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //Habilita
   NVIC_Init(&NVIC_InitStructure); //Configura con los parámetros dados
}

A continuación, nos vamos a la librería stm32f4xx_it.c  y abrimos una nueva función que será el handler:

void EXTI0_IRQHandler(){
   if(EXTI_GetITStatus(EXTI_Line0) != RESET){   //Esto es como una bandera
   //Si no está en RESET significa que se ha activado la interrupción
      LED_Toggle(LED3) //Cambia el estado del LED al contrario del que estaba
      EXTI_ClearITPendingBit(EXTI_Line0) //Pone la bandera a RESET
   }
}