Interrupciones de Timer con Arduino

Interrupciones de timer con Arduino

Las interrupciones de timer con Arduino (también interrupciones de software o de tiempo), ya fueron explicadas en formar básica en la página sobre la librería TimerOne a la que puede acceder desde acá. La página a continuación, está dedicada a los que quieren explorar el tema en mayor profundidad, por lo que requiere cierto conocimiento básico sobre microcontroladores o microprocesadores, matemática y programación.

El presente explicativo cubre al Arduino UNO y al Arduino Mega 2560 pero les dejaré suficiente información para que ustedes solos puedan buscar la información específica de otros miembros de la familia Arduino (Atmel), la que encontrarán en las hojas de datos (datasheets) de los procesadores que cada placa integra.

Más información sobre las interrupciones de timer con Arduino

Las interrupciones de timer con Arduino (la que integra microcontroladores Atmel) son similares a otras interrupciones de timer de otras marcas y tipos de microcontroladores ya que persiguen la misma finalidad y utilidad provistas por los micros. Cuando hablo de interrupciones de timer con Arduino me estoy refiriendo en realidad a las interrupciones de timer de los micros Atmel ATmega328/P y ATmega2560/V.

Puntualmente lo que les presento lo probaré en un Arduino UNO con ATmega328P y un Arduino Mega 2560 con ATmega2560. Para referencia de quienes quieren ver las hojas de datos originales acá les dejo los enlaces a ellas:

ATmega328P -> https://www.atmel.com/Images/Atmel-42735-8-bit-AVR-Microcontroller-ATmega328-328P_Datasheet.pdf
ATmega2560 -> https://www.atmel.com/Images/Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-1280-1281-2560-2561_datasheet.pdf

Las interrupciones de timer con Arduino son solo uno de los usos de los timers en estos microcontroladores. Hay muchas implicancias al comenzar a explorar y usar los timers con Arduino, pero en algunos casos los beneficios valen la pena, y en otros, no queda otra alternativa!

Los microcontroladores en general, son microprocesadores con capacidades extra como ser memoria RAM, ROM (o Flash) y conversores ADC. Como todo procesador ellos no comprenden mucho de lenguajes sofisticados como C/C++ que el proyecto Arduino promociona. En su lugar, los mismos hablan otro lenguaje llamado código de máquina cuyo primo más cercano que es usado por los programadores industriales es el Assembler (o lenguaje ensablador).

El motivo por el que esplico esto es debido a que para que un lenguaje como C/C++ pueda ser usado para programar un microcontrolador, usualmente su implementación requiere sacrificios a la funcionalidad del mismo. Por ejemplo, funciones como analogWrite(), millis() y delay(), son funciones que utilizan los timers del micro para su misión.

Lo que significa que cuando decida usar el timer0 en un Arduino UNO, deberá olvidarse de las funciones millis() y delay() en sus Sketch a menos que quiera comportamientos erráticos.

Profundizando sobre los timers

Sin intentar dar un curso completo sobre timers (créame que el tema da para mucho…), quiero explicar algunos conceptos internos del funcionamiento de los timers para que usted sea más consciente de lo que ocurre y como, cuando use interrupciones de timer con Arduino.

Contadores

Los timers usan internamente contadores. Los contadores son registros que dependiendo de como se los configuren se irán incrementando en 1 con cada ciclo del reloj. Estos contadores son especialmente útiles ya que son la base de los timers. Los micros del Arduino UNO y MEGA 2560 poseen contadores de 8 bits y de 16 bits (que luego veremos en más detalle).

Los contadores de 8 bits pueden contar hasta 255 y luego volverán a 0 en lo que se denomina “desborde” (overflow en Ingles), mientras que los de 16 bits pueden contar hasta 65535 antes de volver a 0. Los contadores están íntimamente relacionados con el reloj del micro que en las placas mencionadas hablamos de 16Mhz o 16 millones de ciclos por segundo.

Esto significa que los contadores de 8 bits desbordan 16.000.000 / 256 = 62.500 veces por segundo o 1 vez cada 16μs aproximadamente, y los contadores de 16 bits desbordan 16.000.000 / 65536 ≅ 244 veces por segundo o 1 vez cada 4ms aproximadamente. Como estas dos alternativas no son muy prácticas o útiles, necesitaremos una herramienta que nos permita “contar” de manera diferente. Es decir, contar en la escala que nosotros necesitemos, y acá entra la combinación de los comparadores y los pre-escaladores (prescalers en Inglés) .

Pre-escaladores

Son registros que nos permiten configurar nuestros contadores para compararlos no solo contra su límite o desborde, sino contra el resultado de una operación matemática que multiplica a nuestro contador con una fórmula específica que examinaremos más adelante, y que nos permite alcanzar un rango de numeros más amplio (dependiendo si el contador es de 8 bits o 16 bits), lo que nos amplia nuestras opciones de uso de los timers mucho más.

Timer0/1 (Arduino UNO y Mega 2560) y Timer3/4/5 (Mega 2560 solamente)

CSn2CSn1CSn0Descripción
000Sin fuente de reloj (Timer parado)
001clk i/o /1 (sin pre-escalador)
010clk i/o /8 (pre-escalador)
011clk i/o /64 (pre-escalador)
100clk i/o /256 (pre-escalador)
101clk i/o /1024 (pre-escalador)

Timer2 (Arduino UNO y Mega 2560)

CS22CS21CS20Descripción
000Sin fuente de reloj (Timer parado)
001clk i/o /1 (sin pre-escalador)
010clk i/o /8 (pre-escalador)
011clk i/o /32 (pre-escalador)
100clk i/o /64 (pre-escalador)
101clk i/o /128 (pre-escalador)
110clk i/o /256 (pre-escalador)
111clk i/o /1024 (pre-escalador)

Nota: a los efectos de este instructivo no explicaremos el uso de reloj externo.

La fórmula que usaremos para configurar nuestro pre-escalador es la siguiente (usando Arduinos a 16Mhz):

Registro comparador de salida = (16.000.000 / (pre-escalador * frecuencia de interrupcion)) – 1

Teniendo en cuenta que el pre-escalador debe ser uno de los disponibles para el timer que queremos usar, y el registro comparador de salida debe ser menos a 256 para Timer0 y Timer2 y menos a 65536 para los demás presentados en el presente instructivo. Ejemplo de cálculo para timer0 y para 2Hz (2 veces por segundo):

Como timer0 es de 8 bits, sin pre-escalador y con el registro comparador de salida la frecuencia más lenta sería 16.000.000 / 256 = 62.500Hz, muy rápido! asi que hay que incrementar el divisor usando un pre-escalador.

Probemos con 8: (16.000.000 / (8 * 2)) – 1 = 999.999 <- muy grande! para timer0 debe ser menor a 256!
Probemos con 64: (16.000.000 / (64 * 2)) – 1 = 124.999 <- aun es muy grande!
Probemos con 256: (16.000.000 / (256 * 2)) – 1 = 31.249 <- aun es muy grande!
Probemos con 1024: (16.000.000 / (1024 * 2)) – 1 ≅ 7.811 <- aun es muy grande!

Como no tenemos pre-escaladores más grandes no podemos resolver esta necesidad con el timer0. Esto es porque el timer0 es de 8 bits y su pre-escalador más grande es de 1024 (lo mismo para timer2), lo que nos deja una regla basica que dice: timer0/2 son de 8 bits y la frecuencia mínima de interrupción es de 16MHz/(256*1024)=61Hz y timer1/3/4/5 son de 16 bits y la frecuencia mínima de interrupción es de 16MHz/(65536*1024)=0.2Hz o lo que es lo mismo 5 veces por segundo.

Comparadores

Los comparadores son el tercer elemento importante de los timers los que básicamente comparan el estado del contador del timer, con el resultado de una configuración matemática que nosotros usamos mediante el pre-escalador (si es que lo usamos…) y realiza una acción si la comparación resulta en una igualdad.

A modo de referencia, así es como el lenguaje C/C++ resuelve las salidas PWM de nuestras placas entre otras cosas.

Configuración de timers

Para el presente instructivo nos interesa la configuración de timers con Arduino en modo CTC (Clear Timer on Compare match o reiniciar timer cuando el comparador coincida) que nos permitirá “interrumpir” cada cierto período de tiempo que nosotros mismos definiremos o más específicamente el timer interrumpirá cuando el registro contador (TCNTn) coincida con el valor del registro de comparación de salida (OCRnA/B).

La configuración se hace a través de registros del micro que son posiciones específicas de memoria RAM (al principio del mapa de memoria). Para facilitar su manipulación el pre-compilador de C/C++ del Arduino IDE nos entrega ya definidos los registros correspondientes a nuestra placa, pero cuidado, los registros varian de micro a micro.

La configuración de timers la haremos dentro de nuestro sección setup() del Sketch. Además, deberemos desactivar las interrupciones antes de comenzar a configurar el/los timer/s y re-activarlas luego. Finalmente la configuración del código de la interrupción la haremos con la función:

ISR(interrupcion) {
    // Su codigo va aqui
}

y la interrupción la configuraremos usando las constantes pre-configuradas para cada placa que para nuestro caso será TIMERn_CAPT_vect

 

Finalmente, los registros que usaremos son:

TCCRnA/B

Registros de control. Lo que nos importa de ellos es configurar el timer en modo CTC y configurar el pre-escalador segun nuestra necesidad

Timer0/2 (Arduino UNO y Mega 2560)

TCCRnA debe estar todo en 0 excepto por el bit WGM01 que debe estar en 1 para configurar el modo CTC.
TCCRnB debe estar todo en 0 excepto los bits que vayamos a usar en 1 para nuestro pre-escalador, si es que usaremos uno.

Timer1 (Arduino UNO y Mega 2560), Timer3/4/5 (Arduino Mega 2560)

TCCRnA debe estar todo en 0 para nuestro uso
TCCRnB debe estar todo en 0 excepto por el bit WGMn2 que debe estar en 1 para configurar el modo CTC y los bits del pre-escalador que necesitemos si es que usaremos uno

TCNTn

Registro contador. Por lo general lo inicializaremos en 0 al comenzar. En el caso del Timer0 y Timer2 el contador es de 8 bits y puede tomar valores de 0 a 255. En el caso de los Timer 1/3/4/5 el contador es de 16 bits y puede tomar valores de 0 a 65535.

OCRnA

Registro comparador de salida. Luego de definir nuestro pre-escalador, este registro lo usaremos con un valor específico para que las interrupciones ocurran cuando el valor del contador (TCNTn) coincida con éste registro.

TIMSKn

Registro de configuración de interrupción. En nuestro caso lo usaremos con todos los bits en 0 menos el bit OCIEnA que deberá estar en 1 para indicarle a nuestro timer que usaremos el registro OCRnA para comparar.

Sketch de ejemplo

En el siguiente ejemplo usaremos el típico parpadeo del led interno del pin13 el cual queremos que parpadee 1 vez por segundo (en realidad lo prenderemos 1/2 segundo y lo apagaremos otro 1/2 segundo para lograr el efecto). El Sketch es compatible Arduino UNO y Mega 2560.

// Ejemplo de configuración de timer1 a 2Hz para parpadear el led interno (pin 13)
// ArduinoHobby.com
void setup(){

  pinMode(LED_BUILTIN, OUTPUT);
  
  // Paramos todas las interrupciones antes de contigurar un timer
  noInterrupts();

  // El registro de control A queda todo en 0
  TCCR1A = 0;

  // Activamos el modo CTC en Timer1
  TCCR1B = 0;
  TCCR1B |= (1 << WGM12);

  // y nuestro pre-escalador en 1024
  TCCR1B |= (1 << CS12) | (1 << CS10);
  
  // Inicializamos el contador en 0
  TCNT1 = 0;
  
  // El registro comparador de salida segun nuestra formula deberia ser 7812,5
  OCR1A = 7812;
  
  // Inicializamos el comparador para el registro A
  TIMSK1 |= (1 << OCIE1A);
  
  // Activamos interrupciones nuevamente
  interrupts();
}

boolean led = false;

// Código que ejecutamos en cada interrupción
ISR(TIMER1_COMPA_vect) {
  if (led) {
    digitalWrite(LED_BUILTIN, LOW);
    led = false;
  }
  else {
    digitalWrite(LED_BUILTIN, HIGH);
    led = true;
  }
}

void loop() {  
}

Consideraciones finales

Recuerde que las interrupciones de timer con Arduino son tema avanzado y no deben usarse a la ligera. Las funciones millis() y delay() dependen del Timer0, por lo tanto si usa el Timer0 asegúrese de no usar dichas funciones. El intercambio de datos entre un programa principal y una interrupción es muy sensible y debe hacerse con variables globales configuradas como volatiles (volatile) para asegurarse de que puedan ser accesibles por las interrupciones.

Además, cuando acceda a una variable volatile compartida con una interrupción asegúrese de desactivar las interrupciones antes y re-activarlas luego con noInterrupt() y Interrupt().

Enlaces de referencia sobre interrupciones de timer con Arduino

 

Deja un comentario