¿Sabías que puedes tener un system tick en Arduino con una base de tiempo de 1ms?

¿Sabías que puedes tener un system tick en Arduino con una base de tiempo de 1ms?

Sigue leyendo para que veas cómo lo puedes implementar tú mismo.

Tabla de contenidos

¿Qué es un system tick?

Un system tick es una interrupción que el procesador genera a intervalos de tiempo regulares.

¿Para qué queremos un system tick?

Un system tick nos sirve para infinidad de situaciones:

  • Llevar la hora para un reloj.
  • Leer y decodificar un teclado.
  • Hacer que el led backlight de nuestro LCD prenda y apague a intervalos de tiempo regulares.
  • Hacer que el búzer suene por un intervalo de tiempo predeterminado y se apague al finalizar éste.
  • Tener un heart beat. Éste es un led que parpadea en intervalos de 0.5 segundos y me hace saber que el sistema está funcionando. Recuerda que depurar programas en Arduino es muy complicado.

¿Qué beneficios obtenemos con un system tick?

  • Simplifica los procesos asíncronos.
  • Tenemos una base de tiempo estable.
  • Nos permite tener código más profesional.

¿Arduino incluye un soporte nativo para el system tick?

No. Nosotros debemos modificar el código fuente de Arduino para tener un system tick (lo cual, debo admitir, es increíblemente divertido).

¿La función yield() serviría para implementar un system tick?

Sí…​ pero no. Esta función de biblioteca es llamada por delay(), y surgen muchos problemas:

  • Debemos llamar a delay() incluso cuando no hay necesidad de hacerlo.
  • La función yield() se invoca cada 7 microsegundos. Raro pero cierto.
  • El tiempo se va recorriendo y, por lo tanto, no podemos obtener una señal regular.

Implementación de un system tick con una base de tiempo de 1ms en Arduino

En primer lugar vamos a ver cómo podemos implementar este mecanismo en Arduino, y después veremos un pequeño ejemplo.

Implementación

Debemos modificar 3 archivos de Arduino. Pero no se preocupen, son unas cuantas líneas y no tenemos que recompilar la plataforma.

Arduino genera una interrupción por reloj cada milisegundo, pero no está a la vista del cliente (nosotros). Lo que vamos a hacer incluir código que llame a una función callback nuestra en cada interrupción.

Callback
Una callback es una función nuestra, no de Arduino, que le vamos a inyectar al sistema.

Las referencias a los archivos son con respecto a la instalación de Arduino en Linux, y para esta entrada del blog, utilicé la versión 1.8.5. No deberían existir problemas con versiones más recientes.

/ruta/de/instalación/arduino-1.8.5/hardware/arduino/avr/cores/arduino

hooks.c

Al final de este archivo debemos escribir un place holder para nuestra callback. La podemos nombrar como queramos, pero usaré el nombre usrTickHook():

/**
 * User tick hook.
 *
 * This function is called every system tick directly from the ISR0 interrupt.
 * The user can through this function implement any strictly timed code.
 */
static void __empty2() { 
	// Empty 
}
void usrTickHook( void ) __attribute__ (( weak, alias( "__empty2" )));

Arduino.h

En este archivo debemos declarar a nuestra callback usrTickHook() casi al principio del mismo.

void yield(void);

void usrTickHook();

#define HIGH 0x1
#define LOW  0x0

#define INPUT 0x0

Debemos insertar la declaración de nuestra función usrTickHook() después de la declaración de la función yield() y antes de la declaración de las macros HIGH y LOW. El lugar no es crítico, de hecho puede ser en cualquier lugar válido de este archivo, pero vamos a hacerlo así.

wiring.c

Para que nuestra callback usrTickHook() sea llamada cada milisegundo debemos insertarla en la interrupción del timer de Arduino (TIM0_OVF ó TIMER0_OVF), al final de la ISR. Ésta nos debe quedar así:

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
	// copy these to local variables so they can be stored in registers
	// (volatile variables must be read from memory on every access)
	unsigned long m = timer0_millis;
	unsigned char f = timer0_fract;

	m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}

	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;

	usrTickHook();
}

Si son curiosos en este archivo podrán ver cómo se implementan las funciones delay(), micros(), delayMicroseconds() e init(); todas cruciales para la plataforma Arduino.

Prueba

Eso es todo en cuanto a la plataforma Arduino. Toca probar que hicimos las cosas bien. Abrimos Arduino, preparamos y subimos el siguiente sketch:

// esta es la función que se llama cada 1 ms. Hagamos que el led on-board
// parpadee cada 500 ms
void usrTickHook()
{
	static uint16_t ticks = 500;
	static bool state = false;

	--ticks;
	if( ticks == 0 ){
		ticks = 500;
		state = state == false ? true : false;
		digitalWrite( 13, state );
	}
}

void setup()
{
	pinMode( 13, OUTPUT );
}

void loop()
{
	// nada!
}

Si todo fue bien el led on-board debería parpadear cada medio segundo:

IMPORTANTE: En la esquina superior izquierda del osciloscopio podrán observar que el periodo de nuestro tick (multiplicado por dos) es 976.58 ms, un poquitín alejado de 1000 ms. Esto no es un problema de software, sino de hardware, ¿porqué? Muy fácil, Arduino NO puede generar una señal exacta de 1000 ms debido al cristal y a los prescalers internos. Aún así, esta señal constante nos puede servir para multitud de aplicaciones, y como ya lo comenté, simplifica la programación como no tienen una idea. ¡Ah!, y muy importante, nuestra señal no se recorre como lo hace la función de biblioteca delay().

Ejemplo

Este ejemplo muestra un búzer que se activa por el tiempo que determine la llamada y, sin intervención del usuario, se apaga.

volatile uint16_t g_buzzer_ticks;
// :debe ser global porque es una variable compartida.
// :deber ser volatile porque será modificada dentro de una ISR.


// esta es la función que se llama cada 1 ms.
void usrTickHook()
{
   if( g_buzzer_ticks > 0 ){
      --g_buzzer_ticks;
      if( g_buzzer_ticks == 0 ){
         digitalWrite( 2, LOW );
      }
   }
}

/**
 * @brief Activa el búzer por un determinado tiempo.
  * @param ticks El tiempo en milisegundos que estará activo el búzer.
  * @post El búzer se desactivará "por sí solo" una vez que el tiempo termine.
 */
void buzzer( uint16_t ticks )
{
	digitalWrite( 2, HIGH );
	g_buzzer_ticks = ticks;
}

void setup()
{
	pinMode( 2, OUTPUT );
	// conectamos un búzer a D2
}

void loop()
{
	buzzer( 100 );
	delay ( 500 );
}

Palabras finales

Es todo por esta vez. Si tienen preguntas o comentarios, por favor háganmelos llegar. Y si quieren ver algún otro tema avanzado de Arduino no dejen de avisarme.

(La versión original y en inglés de esta entrada la pueden consultar en mi blog alternativo.)