El tutorial de hoy es de suma importancia si quieres hacer proyectos profesionales con Arduino. Las funciones millis() y micros() te van a permitir dos cosas:
- Saber cuánto tiempo ha pasado desde que se inició el programa.
- Poder cronometrar tiempos dentro de un código de Arduino.
Imagina que tienes un proyecto con tres componentes: un LED, un sensor de temperatura y un sensor de CO2 y quieres que el LED parpadee cada segundo, obtener la temperatura cada 5 segundos y el nivel de CO2 cada 15 segundos.
Quizás lo primero que te venga a la cabeza sea utilizar delay(). Normal, estamos muy acostumbrados a utilizarlo. Sin embargo, esta función está pensada para hacer retardos de tiempo muy pequeños.
Cuando estamos hablando de segundos utilizar la función delay() hace que se imposible ejecutar nada más durante ese tiempo impidiendo que Arduino haga otra tareas.
Para poder ejecutar eventos en diferentes momentos es necesario usar otras herramientas como millis() y micros(). ¿Qué no sabes qué son o cómo se utilizan?
No problem, en menos de 5 segundos contados con millis() empezamos este tutorial para aprender cómo hacer cronómetros y temporizadores con Arduino.
Ojo, sin utilizar un RTC con Arduino.
Indice de contenidos
¿Qué es un hardware clock?
Antes de analizar las funciones millis() y micros() es necesario conocer qué es un reloj de hardware (en inglés hardware clock) y qué es un temporizador/contador (timer/counter).
Un hardware clock es un circuito electrónico que genera una señal de voltaje cuadrada a una frecuencia constante. Es decir, que la señal se mantiene en un estado (alto o bajo) durante un tiempo fijo y luego cambia, tal y como se muestra en la figura siguiente.
En la imagen los cambios de estado ocurren cada medio segundo, pero en la práctica estas señales suelen ser más veloces, en el orden de los cientos de nanosegundos.
Existen una infinidad de circuitos capaces de generar señales de este tipo. Sin embargo, en la actualidad, la gran mayoría de microcontroladores cuentan con estos circuitos internamente. Y como era de esperar el microcontrolador presente en las placas Arduino también lo posee.
Teniendo en cuenta que los pulsos ocurren a frecuencia constante es posible contar los pulsos que han ocurrido. También podrás determinar el tiempo que ha transcurrido desde que se empezó a contar. Es decir, que si los pulsos ocurren cada 0.5 segundos y se han contado 4 pulsos hace un total de 2 segundos.
Afortunadamente, el microcontrolador de las placas Arduino ya cuenta con un módulo que es capaz de realizar esto por sí solo. Estos módulos son denominados contadores o temporizadores y su tarea es muy simple: mantener un registro con los pulsos generados por el hardware clock.
Todo esto se encuentra en el archivo de código del core de Arduino que está en la carpeta de Windows
1 |
C:/Program Files(x86)/Arduino/hardware/arduino/avr/cores/arduino/wiring.c |
Y si tu sistema operativo es de 32-bit
1 |
C:/Program Files/Arduino/hardware/arduino/avr/cores/arduino/wiring.c |
Dentro de este mismo código se realizan las correcciones pertinentes para que la función millis() y micros() sea lo más preciso posible.
Cuando se enciende una placa de Arduino comienza a generar pulsos que son contados por el módulo contador. Todo esto ocurre de forma automática, es decir, que no es necesario decirle al contador que comience a operar. Él se mantiene contando mientras la placa está encendida.
Por supuesto, el contador tiene un límite, o dicho de otra forma, un valor máximo. Cuando el contador alcanza este valor vuelve a comenzar desde cero. A este suceso se le conoce como desbordamiento, luego veremos en qué consiste.
La función millis() con Arduino
La función millis() permite obtener la cantidad de milisegundos que han pasado desde que comenzó la ejecución del código, es decir, que toma el registro de pulsos generados por el contador y los emplea para calcular el tiempo en milisegundos.
La sintaxis de la función es muy simple:
1 |
unsigned long tiempo = millis(); |
Como puedes ver, no requiere parámetros y retorna un valor entero, de tipo unisgned long, que representa la cantidad de milisegundos transcurridos desde que la placa fue encendida.
Para comprobar el funcionamiento de millis() por ti mismo puedes subir el siguiente código a tu Arduino y abrir el monitor serie.
1 2 3 4 5 6 7 8 |
void setup() { Serial.begin(9600); } void loop() { Serial.println( millis() ); delay(500); } |
Deberías obtener un resultado como el que se muestra en la siguiente imagen.
Como puedes ver, el valor se va incrementando y cuando la placa es reiniciada este comienza nuevamente de cero.
Ojo, que cuando abres el monitor serie la placa también se reinicia por eso, si pruebas a subir el código, esperas 10 segundos y abres el monitor serie verás que se inicia justo cuando se abre la ventana.
La función micros() con Arduino
En caso de que necesites manejar tiempos con mayor precisión es posible utilizar la función micros(). Esta función es similar a millis() pero devuelve el tiempo en microsegundos.
La sintaxis es muy similar a la de millis():
1 |
unsigned long tiempo = micros(); |
De igual manera no requiere parámetros y retorna un entero, de tipo unsigned long.
Para comprobar el funcionamiento de micros() puedes utilizar el ejemplo anterior y sustituir la función millis() por micros(). Quedaría de la siguiente manera.
1 2 3 4 5 6 7 8 9 |
void setup() { Serial.begin(9600); } void loop() { Serial.println( micros() ); delay(500); } |
En el monitor serie deberías obtener una salida similar a la siguiente imagen.
Advertencias con Arduino millis() y micros()
Antes de comenzar a trabajar con estas funciones es necesario conocer algunas particularidades que pueden provocar errores prácticamente indetectables.
Desbordamiento (overflow) millis() y micros()
El tiempo de desbordamiento (overflow en ingles) está dado por el máximo valor que pueden retornar las funciones millis() y micros().
En Arduino existen distintos tipos de variables para almacenar números enteros. En la siguiente tabla puedes ver un resumen de los tipos más significativos.
Tipos de datos | Valor mínimo | Valor máximo |
---|---|---|
byte | 0 | 255 |
integer (int) | -32768 | 32767 |
unsigned int | 0 | 65535 |
long | −2147483648 | 2147483647 |
unsigned long | 0 | 4294967295 |
Teniendo en cuenta que las funciones millis() y micros() retornan un entero de tipo unsigned long se puede determinar su tiempo de desbordamiento.
millis(): 4294967295 ms /1000 = 4294967,3 s /60 = 71582,78 min /60 = 1193,04 horas /24 = 49,7 días.
micros(): 4294967295 microsegundos = 4294,97 segundos = 71,58 minutos = 1,19 horas.
Esto indica que después de aproximadamente 50 días el valor de millis() comenzará desde cero. Con micros() se obtiene un valor mucho menor, de tan solo 1,19 horas.
Es importante esto así que lo voy a repetir. Cuando la función millis() esté cerca del límite superior establecido por el máximo valor de un unsigned long, comenzará desde cero. A esto se le llama overflow o desbordamiento.
Imagínate que la función millis() devuelve este valor 4294967295. ¿Cuál sería el siguiente que devolvería?
Efectivamente, cero.
Lo mismo ocurre con la función micros().
Esto permite identificar cual es la máxima diferencia de tiempo que se puede apreciar cuando se emplean estas funciones, pero eso ya se verá con detalle en los ejemplos.
Resolución millis() y micros()
Cuando se habla de resolución se refiere al menor tiempo que se puede percibir utilizando estas funciones. Es decir, es la mínima diferencia de tiempo que se puede calcular.
La función millis() retorna un valor entero que representa los milisegundos, por lo tanto, se puede intuir que la resolución de esta es de 1ms. Lo que quiere decir es que esta función nunca devolverá un tiempo con decimales del estilo 3,2ms.
Con la función micros() no ocurre esto, en este caso la resolución es de cuatro microsegundos (4 us). La razón de esto es que el hardware clock utilizado no es tan veloz, y los pulsos ocurren cada 4 us, por lo tanto, no es posible medir tiempos más pequeños.
¿Qué tipo de dato devuelve millis() y micros()?
Para almacenar los valores que devuelven estas funciones es necesario utilizar variables de tipo unsigned long como indica la referencia del lenguaje.
Si, por ejemplo, se intenta almacenar su valor en una variable de tipo int el desbordamiento se producirá antes ya que este tipo de dato solo puede almacenar números positivos hasta el 32.767.
Puedes hacer los cálculos y verás cómo se reduce el tiempo que puede contar.
Operaciones con millis() y micros()
Otro punto a tener en cuenta es que todas las operaciones a realizar deben hacerse utilizando variables de tipo unsigned long. En otras palabras, si es necesario restar, sumar o realizar otra operación aritmética se deben utilizar variables de ese mismo tipo.
En caso de que se utilicen literales para las operaciones es necesario agregarle el formateado de tipo UL al final del literal. En el ejemplo siguiente se pueden ver ambos casos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
unsigned long prev; // prev es de tipo unsigned long void setup(){ pinMode(LED_BUILTIN, OTUPUT); } void loop() { if( millis() - prev > 1000UL ){ // formateador UL al final del literal prev = millis(); // almacenando en prev if( digitalRead(LED_BUILTIN) ) digitalWrite(LED_BUILTIN, false); else digitalWrite(LED_BUILTIN, true); } } |
Un literal es un número indicado como tal y no como una variable. Por ejemplo, en la operación int v = 5 se está asignando un literal (5) a una variable (v).
Interrupciones con millis() y micros()
Si tu código hace uso de alguna interrupción es importante que prestes mucha atención a esto: mientras que el código de la interrupción se está ejecutando el valor de millis() y de micros() no es actualizado. Eso significa que si realizas varias llamadas a estas funciones desde la interrupción siempre obtendrás el mismo valor.
Es importante mantener el código de las interrupciones lo más corto posible y sobre todo no utilizar funciones para generar retardos como delay().
Cómo cronometrar tiempos con Arduino
En este primer ejercicio práctico vamos a ver cómo hacer un cronómetro de tiempo con Arduino que nos permitirá calcular cuánto tarda en ejecutarse una función.
Ya sea por necesidad o curiosidad es posible que quieras saber cuánto tiempo tarda una función en ejecutarse. Es algo muy importante si, por ejemplo, quieres trabajar en modo ahorro de energía o deep sleep.
Lo más fácil es utilizar la función micros() o millis() dependiendo de la precisión que necesites.
Para realizar esta aplicación utilicé una placa Arduino UNO, aunque es compatible con Arduino MEGA, Arduino Nano e incluso con el ESP8266.
El procedimiento a seguir es muy simple y consta solo de cuatro pasos:
- Obtener y almacenar el tiempo utilizando millis() o micro().
- Ejecutar la función a cronometrar con Arduino.
- Obtener y almacenar el tiempo en una variable diferente
- Restar los dos valores obtenidos para obtener el tiempo de ejecución.
Este algoritmo funciona ya que mientras el código se está ejecutando el hardware clock y el contador están operando.
Ahora verás cómo implementar este procedimiento mediante un ejemplo. En este caso se calcula el tiempo que tarda en ejecutarse la función test().
El código comienza declarando las variables a emplear:
- Previo y actual: son empleadas para almacenar los valores que devuelve la función micros().
- raw_0, raw_1 y raw_2: son empleadas por la función test() para almacenar valores analógicos.
1 2 |
unsigned long previo, actual; int raw_0, raw_1, raw_2; |
La función test() simplemente invierte el estado de los pines digitales del 2 al 13 y lee los valores analógicos de los pines A0, A1 y A2. Ten presente que es solo un ejemplo, tú puedes sustituir esta función por la deseas cronometrar con Arduino.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void test(){ for( int i = 2; i <= 13; i++ ){ if( digitalRead( i ) ){ digitalWrite( i, LOW ); } else{ digitalWrite( i, HIGH ); } } raw_0 = analogRead(A0); raw_1 = analogRead(A1); raw_2 = analogRead(A2); } |
En la función setup() únicamente se inicializa el puerto Serial y se configuran los pines digitales como salidas.
1 2 3 4 5 6 |
void setup() { Serial.begin(9600); for( int i = 2; i <= 13; i++ ) pinMode( i, OUTPUT ); } |
En la función loop() es donde ocurre la magia, es decir, que aquí es donde se calcula el tiempo que tarda la función en ejecutarse.
1 2 3 4 5 6 7 8 9 10 11 12 |
void loop() { previo = micros(); test(); actual = micros(); Serial.print( "La función tardó:" ); Serial.print( actual - previo ); Serial.println( " us" ); delay(200); } |
Como puedes ver, la idea es obtener los valores de micros() antes y después de ejecutar la función test(), entonces se restan ambos valores para obtener y enviar el tiempo en microsegundos al monitor serie.
Utilizando un Arduino UNO se obtienen valores que oscilan entre 488 y 492 microsegundos. Pero este valor puede ser menor o mayor si se emplea otra placa.
Bueno, aquí te dejo el código completo, listo para cargarlo en tu Arduino. Te recomiendo que “juegues” un poco con el código, o incluso que intentes calcular el tiempo de ejecución de tus propias funciones.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
unsigned long previo, actual; int raw_0, raw_1, raw_2; void test(){ for( int i = 2; i <= 13; i++ ){ if( digitalRead( i ) ){ digitalWrite( i, LOW ); } else{ digitalWrite( i, HIGH ); } } raw_0 = analogRead(A0); raw_1 = analogRead(A1); raw_2 = analogRead(A2); } void setup() { Serial.begin(9600); for( int i = 2; i <= 13; i++ ) pinMode( i, OUTPUT ); } void loop() { previo = micros(); test(); actual = micros(); Serial.print( "La función tardó:" ); Serial.print( actual - previo ); Serial.println( " us" ); delay(200); } |
Eventos periódicos o temporizadores con millis() y micros()
Cuando se realizan proyectos con Arduino que incluyen sensores por lo general es necesario estar supervisando sus valores continuamente. Por ejemplo, supón que tienes un sensor de temperatura LM35 conectado al pin A0 y necesitas obtener su valor cada 30 segundos.
Si solamente necesitas realizar esta acción es posible utilizar la función delay() tal y como se muestra en el siguiente segmento de código.
1 2 3 4 5 6 7 |
void setup(){ } void loop(){ Serial.println( analogRead(A0) ); delay(30000); } |
El problema con este método es que se desperdicia la potencia del procesador: pasa 30 segundos sin hacer nada, simplemente esperando. Además, se bloquea la ejecución del código y no es posible realizar más operaciones durante ese periodo.
Por ejemplo, si quieres obtener la temperatura de un sensor conectado al A0 cada cinco segundos y que parpadee un LED a la vez cada segundo. En este caso, con la función delay() no se consigue hacer a la vez. Se ejecuta una cosa tras otra como puedes ver en el siguiente código.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void setup() { Serial.begin(); pinMode(LED_BUILTIN, OUTPUT); } void loop() { // Leer sensor cada 5 segundos Serial.println("Leer sensor"); Serial.println( analogRead(A0) ); delay(5000); // Parpadeo del LED cada segundo Serial.println("Parpadeo LED"); digitalWrite(LED_BUILTIN, HIGH); delay(1000); digitalWrite(LED_BUILTIN, HIGH); delay(1000); } |
Para solventar estos problemas es posible utilizar la función millis() o micros().
La idea es utilizar la función millis() para generar eventos periódicos o temporizadores, es decir, conseguir que una cierta acción se realice a intervalos fijos pero sin bloquear el código como lo hace la función delay(). De esta forma, puedes medir la temperatura cada 30 segundos y aprovechar ese tiempo de espera para que Arduino realice otras actividades.
Para comprender esta técnica es conveniente pensar que la función millis() se está moviendo sobre una línea de tiempo. Dicha línea de tiempo comienza en cero y termina en 4.294.967.295 (máximo valor que puede tomar esa función que equivale a 49,7 días). Por lo tanto, cuando la función millis() es ejecutada el valor que devuelve es la posición donde se encuentra en la línea.
Teniendo en cuenta esto, una forma de programar un evento es darle una posición en la línea de tiempo y ejecutarlo cuando millis() pase por esa posición.
Imagina que quieres que algo se ejecute 10 segundos después de iniciarse la ejecución del código. Cuando millis() devuelva 10000 sabrás que han pasado 10 segundos.
Pero, ¿qué pasa si quiero que algo se ejecute siempre cada 10 segundos y no solo cuando hayan pasado 10 segundos desde el inicio del programa?
Si quieres que el evento se ejecute de forma periódica es necesario darle una nueva posición en la línea de tiempo. En otras palabras, cada vez que el evento se ejecuta se actualiza su tiempo de ejecución moviendo la posición más adelante en la línea de tiempo.
La idea es reiniciar de nuevo el contador cada vez que se alcance el tiempo configurado.
Como ejemplo, vamos a cambiar un estado de un LED cada segundo y enviar al monitor serie la temperatura de un sensor LM35 cada 5 segundos. Son dos temporizadores periódicos en el mismo código.
Por supuesto, todo esto sin utilizar la función delay() u otro método que bloquee el microcontrolador.
Para implementar este ejemplo vas a necesitar:
- 1x Placa Arduino UNO.
- 1x LED rojo (es posible utilizar el LED integrado que traen la mayoría de placas Arduino)
- 1x resistencia de 460 Ω (solo es necesario si se utiliza un LED externo)
- 1x sensor de temperatura LM35 (si quieres puedes utilizar cualquier otro sensor analógico)
Circuito eventos periódicos y temporizadores
En la siguiente figura se muestra el circuito a implementar.

Como puedes ver, el circuito es muy simple:
- Se ha utilizado el pin A0 para conectar el sensor de temperatura LM35. Este además se ha conectado a los pines GND y 5V de la placa Arduino para ser alimentado.
- Para controlar el LED se utiliza el pin digital 3. Es importante siempre utilizar una resistencia para limitar la corriente y evitar que el LED se queme.
Código eventos periódicos y temporizadores
Lo primero es establecer los intervalos de los eventos, es decir, cada cuánto tiempo se van a ejecutar dichos eventos. Para esto se crean las constantes INTERVALO_LED e INTERVALO_TEMP, en este caso con un valor de 1000 (1 segundo) y 5000 (5 segundos).
También es necesario tener una variable para cada evento que permita especificar la posición del mismo en la línea de tiempo. Las variables evento_led y evento_temp son utilizadas para esto.
Además, se ha declarado una constante pinLED para especificar el pin al que se ha conectado el LED.
1 2 3 4 5 6 |
const unsigned long INTERVALO_LED = 1000UL; const unsigned long INTERVALO_TEMP = 5000UL; const int pinLED = 3; unsigned long evento_led; unsigned long evento_temp; |
En la función setup() se configura el monitor serie para operar a 9600 baudios. Este será utilizado para enviar los valores de temperatura obtenidos desde el LM35.
También se configura el pin correspondiente al LED como salida.
1 2 3 4 |
void setup() { Serial.begin(9600); pinMode(pinLED, OUTPUT); } |
Se ha implementado la función toggleLed(), que invierte el estado del pin utilizado para controlar el LED. Esta función será ejecutada cada vez que se ejecute el evento correspondiente al LED.
1 2 3 4 5 6 |
void toggleLed(){ if( digitalRead( pinLED ) ) digitalWrite(pinLED, LOW); else digitalWrite(pinLED, HIGH); } |
Para el evento del LM35 se ha implementado una función, en este caso, nombrada sensor(). Esta función realiza la lectura del pin analógico A0 y luego aplica una fórmula para obtener y almacenar el valor de temperatura en la variable temp de tipo float.
Luego utiliza el puerto Serial para enviar dicha temperatura al monitor serie del IDE de Arduino.
1 2 3 4 5 6 7 8 |
void sensor(){ int raw = analogRead(A0); float temp = ((float)raw)* 5.0 * 100.0 / 1024; Serial.print( "la temperatura es: " ); Serial.print( temp, 2 ); Serial.println(" C"); } |
En la función loop() lo primero que se hace es obtener el tiempo actual ejecutando la función millis() y almacenando su valor en la variable actual. El valor obtenido representaría la posición de millis() sobre la línea de tiempo.
Luego, para cada evento, se comprueba si actual es mayor a la variable del evento, es decir, si millis() ya pasó por la posición del evento en la línea temporal. En caso afirmativo se ejecuta el evento y luego se actualiza su posición utilizando la constante correspondiente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void loop() { unsigned long actual = millis(); if( actual > evento_led ){ toggleLed(); evento_led += INTERVALO_LED; } if( actual > evento_temp ){ sensor(); evento_temp += INTERVALO_TEMP; } // otras acciones } |
Al sumar el intervalo a las variables evento_led y evento_temp estamos evitando el problema del overflow o desbordamiento ya que este suceso también se producirá en las variables que almacenan el tiempo del evento. ¿Se te ocurre otra manera de resolverlo? Abajo en los comentarios :)
Lo bueno de utilizar esta técnica es que puedes añadir más eventos o incluso agregar más código en el loop() después de estos.
Aquí tienes el código completo. Puedes intentar adicionar otros eventos para que compruebes por ti mismo que efectivamente estos se ejecutan de acuerdo a los tiempos establecidos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
const unsigned long INTERVALO_LED = 1000UL; const unsigned long INTERVALO_TEMP = 5000UL; const int pinLED = 3; unsigned long evento_led; unsigned long evento_temp; void setup() { Serial.begin(9600); pinMode( pinLED, OUTPUT ); } void toggleLed(){ if( digitalRead( pinLED ) ) digitalWrite(pinLED, LOW); else digitalWrite(pinLED, HIGH); } void sensor(){ int raw = analogRead(A0); float temp = ((float)raw)* 5.0 * 100.0 / 1024; Serial.print( "la temperatura es: " ); Serial.print( temp, 2 ); Serial.println(" C"); } void loop() { unsigned long actual = millis(); if( actual > evento_led ){ toggleLed(); evento_led += INTERVALO_LED; } if( actual > evento_temp ){ sensor(); evento_temp += INTERVALO_TEMP; } // otras acciones } |
Conclusión sobre función millis() y micros()
En este tutorial has podido ver cómo poder contar el tiempo dentro de un microcontrolador con Arduino. Realmente lo que hemos hecho es un cronómetro utilizando las funciones millis() y micros().
Es importante recalcar que la función delay() hay que evitar utilizarla a toda costa para evitar bloqueos en el código. Quizás con un Arduino esto no tenga importancia pero con microcontroladores como los que lleva NodeMCU (ESP8266) esto es de suma importancia si no quieres perder la conexión WiFi.
Ahora solo falta que sigas practicando. Me encantaría saber dónde tienes pensado utilizar millis() o micros(), ¿se te ocurre algún proyecto?
Lo puedes dejar aquí abajo en los comentarios.
Gracias por tu atención.