[Proteus versión 8.1]
En la lección anterior (Lección 16) descubrimos los problemas que resultaban de utilizar la función delay(). Cuando tenemos un código de varias líneas que contiene una instrucción delay(1000) para esperar a que pase un segundo, nos encontramos que nuestro Arduino está inoperativo la mayor parte del tiempo. Ejecutar el resto del código le puede llevar apenas unos milisegundos y tiene que estar esperando el resto del tiempo sin capacidad de responder a ninguno de los eventos que suceden a su alrededor. Por esa razón, nuestros pulsadores para cambiar la hora, minuto y segundo no respondian de forma satisfactoria.
Además, en la lección 2 tuvimos ocasión de ver cómo se usaba también una instrucción delay() para lograr intermitencias de leds. Seguro que el lector ha visto ejemplos similiares en muchos otros cursos de Arduino. Sin embargo esta técnica tiene un segundo problema añadido al mencionado. Supongamos que queremos generar intermitencias en varios leds con frecuencias diferentes (uno parpadea cada un segundo y otro cada dos y medio, por ejemplo). Evidentemente utilizando la técnica de la función delay() es imposible.
Por eso, en esta lección vamos a abordar la construcción de funciones de tiempo con una filosofía diferente que solucione los dos problemas anteriores. Empezaremos, como es habitual en este curso, con un mínimo de teoría. El primer concepto importante para entender las funciones de tiempo es el de "timer". En general, todos los microprocesadores tienen disponibles varios temporizadores (timer). En concreto el Arduino Uno tiene tres: timer0, timer1, timer2. Un timer no es más que un contador cuya entrada está conectada al reloj del sistema y que nos permite "contar" los tics (ciclos de reloj) que han transcurrido.
Nota aclaratoria: Por defecto la señal que van a contabilizar los timers corresponde a la frecuencia del oscilador dividida por cuatro. Por lo tanto, en realidad cuentan ciclos máquina, no ciclos de reloj. Con un reloj de 20 Mhz tendríamos una frecuencia de ciclos máquina de 20/4 = 5 MHz, por lo que un ciclo máquina corresponde a 0.2 usec. En principio, el contador del timer de un micro que funcione a 20Mhz se incrementará cada 0.2 microsegundos o 5 veces en 1 usec. De todas formas, el lector podrá comprobar que con el método propuesto en esta lección puede olvidarse de estas consideraciones.
El segundo concepto que necesitamos conocer para construir nuestras funciones de tiempo, es el de interrupción de timer. Básicamente, todos los microprocesadores nos permiten generar interrupciones asociadas a los timer. Eso significa que podemos configurar nuestro Arduino para que se genere una "interrupción" cada vez que un timer ha contado un número concreto de tics. Como sabemos la frecuencia con la que se produce cada tic, evidentemente, podemos decir que se genere una interrupción cada vez que hayan transcurrido una serie de "unidades de tiempo" determinadas. Estas "unidades de tiempo" pueden ser microsegundos, milisegundos, centésimas, décimas o segundos. La gran ventaja de utilizar interrupciones, es que todo el código que se esté ejecutando se detiene cada vez que se produce una interrupción y se ejecuta el código que hayamos escrito para "atender" a la interrupción. Por ello la precisión obtenida es muy grande.
Por lo tanto, con los timer y las interrupciones podemos lograr construir un reloj (con la precisión que necesitemos acorde a nuestras necesidades) independiente del código de nuestro programa y no sujeto a que el micro esté con más o menos carga de trabajo. La técnica propuesta consiste en mantener un "reloj maestro" que sepa exactamente el tiempo transcurrido en cada momento y utilizar tantos "relojes secundarios" como sea necesario para cada una de las tareas temporizadas que queramos llevar a cabo de forma independiente.
Por defecto, Arduino utiliza el timer0 para las funciones de tiempo incorporadas por el software báse: delay(), millis() y micros(). El timer1 lo utiliza la librería servo. Y el timer2 lo utiliza la función tone(). Por lo tanto, el usuario que no profundice más puede llegar a pensar que su utilización para los fines que pretendemos está comprometida a menos que sacrifiquemos alguna de las funcionalidades mencionadas. Para evitarlo, la solución será aprovechar la función millis() para implementar nuestro reloj maestro en el que se basan las funciones de tiempo.
Básicamente, la función millis() nos devuelve el tiempo en milisegundos transcurridos desde que se arranco la placa Arduino con el programa actual. Por lo tanto Arduino ya nos facilita el reloj maestro. La función millis() nos devuelve un unsigned long, es decir un número de 32 bits. Así que todos nuestros relojes secundarios deben utilizar variables de este mismo tipo. Puesto que la función millis() cuenta milisegundos, la precisión de nuestros relojes secundarios será, precisamente de un milisegundo (más que suficiente para la mayoría de los proyectos habituales).
La primera función que vamos a escribir se llamará iniTemp() y se encarga de arrancar un cronómetro (reloj secundario) indicando el tiempo que deseamos que se cuente. La segunda función será chkTemp() y nos dirá los milisegundos que restan para que el cronómeto alcance el tiempo deseado.
El código de ambas funciones es el siguiente:
Antes de utilizar estas dos funciones y ver con detalle cómo están escritas, vamos a construir una tercera, a la que llamaremos parpadeo() y que utiliza las dos anteriores para generar una intermitencia de una frecuencia determinada. Su código es el siguiente:
Ahora veamos un ejemplo de su uso. En primer lugar, el esquema electrónico que vamos a utilizar en nuestra lección es muy sencillo. Nuestro Arduino y dos leds para hacer las pruebas.
Y el código de nuestro programa:
Cada vez que utilicemos la función intermitencia() necesitamos pasarle cuatro parámetros. El primero es una señal que indica cuando debe ejecutarse la función (in) y llevar a cabo la intermitencia. En nuestro ejemplo, como deseamos que la intermitencia se esté generando sobre el led todo el tiempo, escribimos directamente un 1. Si quisiéramos que la intermitencia se produjera sólo cuando se cumpliera una determinada condición, utilizaríamos este primer parámetro para llevar a cabo esta tarea sólo cuando se cumpla la condición deseada.
El segundo parámetro es el tiempo en milisegundos que permanecerá alto (y, por lo tanto, también bajo) nuestra cadena de pulsos intermitentes. En nuestro caso para el led situado en el pin IO2 hemos fijado como tiempo 1000 mseg y para el led situado en el pin IO3 hemos fijado como tiempo 2500mseg.
El tercer parámetro es una variable auxiliar de trabajo que utilizaremos para almacenar nuestro cronómetro. Debe ser del tipo unsigned long como explicamos antes. Hemos creado las variables temporizador1 y temporizador2 para este fin. La primera la utilizamos para un led y la segunda para el otro.
El cuarto y último parámetro es una variable auxiliar que utilizamos para indicar el estado de nuestra salida de la intermitencia. Es la variable que usamos para indicar si el led se iluminará o apagará, según el momento de la intermitencia en que nos encontremos. Hemos utilizado las variables intermitencia1 e intermitencia2 para este fin. La primera la utilizamos para un led y la segunda para el otro.
El código de nuestro bucle principal, loop(), no puede ser más sencillo. Dos llamadas a la nueva función intermitencia() y dos sentencias para escribir el resultado que nos devuelven para activar o desactivar las salidas donde se conectan nuestros leds. Ejecutamos la compilación y simulamos nuestro programa. Podemos comprobar que hay dos intermitencias a frecuencias de 1 y 2,5 segundos que no se interfieren una con la otra.
Conviene aquí que dediquemos un momento a estudiar el código de la función parpadeo() y algunas técnicas de programación utlizadas en él.
En primer lugar, utilizamos AND y NOT en lugar de && y !. La razón es que en la zona superior del código hemos utilizado tres sentencias #define para fijar las definiciones de AND, NOT y OR y utilizar estos nombres más intuitivos que &&, || o !.
En segundo lugar las variables auxiliares crono y out las hemos utilizado precedidas de un asterisco. Esto signfica que estamos utilizando punteros a las variables en lugar de las propias variables. El puntero indica la dirección que ocupa una variable. Así, el código de la misma función puede ser utilizada diversas veces con temporizadores diferentes sin que entren en conflicto unos con otros. Por eso cuando utilizamos la función parpadeo la llamamos utilizando como parámetros las variables precedidas del símbolo &.
El código de la función realiza lo siguiente. Si el parámetro IN es verdad (lanzador de la función) y el cronométro ya ha cumplido su tiempo (la primera vez que se utiliza la función es siempre es así por el reloj maestro siempre es mayor que 0 que es el valor del cronómetro en ese momento) arrancamos nuestro cronómetro con el tiempo establecido en el parámetro TIEMPO y ponemos alto el parámetro utilizado para indicar el estado de la intermitencia -OUT-. Si no es verdad ponemos bajo el parámetro OUT para indicar el estado de la intermitencia.
Para arrancar y comprobar el estado de nuestro cronómetro hemos utilizado las otras dos funciones auxiliares que hemos creado initTemp() y chkTemp(). La primera asigna a la variable que utilizamos como cronómetro el valor actual devuelto por la función millis() -nuestro reloj maestro- más el tiempo que deseamos controlar y que le pasamos a la función como parámetro.
La segunda comprueba si el valor almacenado en nuestro cronómetro es mayor que el reloj maestro -la función millis()- y devuelve un cero si ya ha transcurrido el tiempo o el valor en milisegundos que falta, en caso contrario.
El código de una nueva función retraso() que sustituya a la estándar delay() pero que no paraliza a nuestro Arduino mientras se ejecuta, se muestra a continuación como otro ejemplo de las funciones temporales que podemos escribir. También se apoya en el uso de las funciones iniTemp y chkTemp, que siempre son la base de todas las demás.
Dejamos como problema para el usuario, corregir el programa de la lección anterior para que los botones de nuestro reloj ya estén activos todo el tiempo. Esperamos que vuestras soluciones las compartáis en nuestro facebook (https://www.facebook.com/pages/Hubor-Proteus/294446180592964?ref=hl).
A continuación, vamos a mostrar, como ejemplo final, el código de una nueva función llamada pulso() que permite generar un pulso de una duración determinada siempre que se cumpla una determinada condición. De esta manera tenemos otro ejemplo de funciones de tiempo que podemos construir con ayuda de nuestra técnica de cronómetro maestro y cronómetros derivados. De la misma forma no nos resultará difícil construir otras que se adapten a nuestras necesidades concretas de cada proyecto.
No queremos terminar sin hacer dos consideraciones importantes. La función millis() utiliza, como ya mencionamos, números de 32 bits. Como cada unidad es un milisegundo, eso significa que nuestro cronómetro puede contar hasta 4.294.967.296 mseg = 4.294.967 seg = 71.582 min = 1.193 horas = 49 días.
Es decir que cada 49 días nuestro reloj maestro se reiniciará a cero. En la práctica, suele ser suficiente y no tendremos problemas. Pero es posible que en ciertos proyectos tengamos que tener esta circunstancia en cuenta porque un retraso, un pulso o un parpadeo que se produzca justo en el momento en que se reinicia el reloj maestro podría no funcionar correctamente. Si en nuestro proyecto se diera este caso, podemos solucionar el problema utilizando dos variables unsigned long combinadas.
La segunda consideración es que la precisión de nuestro cronómetro es de un milisegundo. Si necesitáramos precisiones mayores (de hasta un microsegundo) podemos utilizar la función micros() que devuelve microsengundos transcurridos en lugar de milisegundos como hace la función millis(). En este caso la limitación máxima temoral es de unos 70 minutos.
Esperamos que esta lección le haya resultado útil e interesante.