Timers, alarmas y el iPhone

Con esta entrada vamos a matar tres pájaros de un tiro y evitar que dios, en su infinita sabiduría, mate más gatitos. Porque sí, porque cada vez que falla la alama de un iPhone o un iPod, dios se carga uno. Pero antes de llegar al meollo de la cuestión tenemos que ver otras cosas. Aprovecharé para contaros cómo funcionan los timers en un microprocesador (el primer ave defenestrada), os explicaré el concepto para construir una aplicación que sea un reloj con alarma (el segundo pajarraco) y dejaremos una pregunta en el aire para mofa y escarnio de la gente del iOs.

Todos hemos oído hablar de la frecuencia a la que funciona un microprocesador. Los, primero megaherzios, y luego gigahercios. Y no oiremos hablar de los terahercios porque están en el rango de los efectos cuánticos y no nos valen para mover un microprocesador… (Chiste para físicos cuánticos: tengo un ordenador a 3 terahercios pero nunca estoy seguro de dónde está).

Ese reloj genera una serie de pulsos (parecidos a los que aparecen en un cardiograma pero más regulares) y son para el procesador y aledaños como nuestro corazón. O como un mecanismo de cuerda, que hace girar los engranajes. Imaginaros la rueda de escape de un reloj. Pues el funcionamiento interno de un procesador es igual: ante cada punto de escape (que en informática se llama ciclo máquina) las ruedas de engranajes se van moviendo poco a poco y al final vemos cómo la aguja del segundero va trazando su arco.

Si nos da por mirar una placa base en detalle, no vamos a ver un reloj de los gigahercios que se supone tiene nuestro equipo. Con suerte veremos uno de 40 o 60 MHz. Lo demás se realiza dentro de chips especiales que dividen esa frecuencia en otras mayores mediante electrónica. Y más dentro del micro, ya que muchas partes dentro de él no necesitan tanta velocidad y a veces es contraproducente, como veremos.

Uno de los elementos que comparten absolutamente todos los microprocesadores son los timers, nombre con el que suelen conocerse en la literatura técnica. El mínimo es uno, el máximo viene determinado por el tipo y destino del microprocesador, más todavía en los microcontroladores. Existen dos modos generales de funcionamiento. A veces ambos están presentes, a veces un mismo timer se puede comportar de las dos formas, y a veces sólo hay de un tipo.

En uno de ellos tenemos acceso a un contador que se va incrementando periódicamente y que nosotros podemos leer para comparar con nuestro valor. En el otro, llegado a un punto (generalmente cero), se dispara una interrupción.

Es decir, cuando nosotros queremos activar un timer, lo que hacemos es escribir en una serie de registros del microprocesador para configurarlo. Decimos registros pero lo habitual son direcciones de memoria que están mapeadas dentro del procesador. Si os acordáis de otra entrada, nosotros escribimos en una dirección del procesador y las patillas del mismo cambian de estado. Pues ahora es lo mismo, pero en lugar de subir o bajar la tensión de una de ellas, configuramos el timer.

O en otras palabras: conectamos una serie de ruedas de engranajes que estaban detenidas, ruedas que nos permitirán tener un evento periódico, ya sea un contador que se vaya incrementando solo o una interrupicón que se lanzará con cierta frecuencia.

Pero hay un problema. En un procesador de 8 bits, los registros serán, a lo sumo, de 16 bits. En uno de 16, de 32. En uno de 64 lo habitual es que sean de 64 bits, no de 128. Para ponerlo en marcha o detenerlo sólo necesitamos un bit, pero para programar el prescaler, cuantos más bits tengamos, mejor.

Suponiendo que tenemos un registro de 16 bits, tendremos 65536 posibles valores respecto a la base de tiempos. Por decirlo de otro modo, si en ese registro ponemos el valor de 10, el procesador hará la acción (ya sea incrementar otro registro o generar una interrupción) cada vez que ese valor llegue a cero. Si tenemos una base de tiempos de 1 ns, nuestro timer será de 10 ns. Si ponemos un valor de 50000, entonces tendremos un tick cada 50 us (microsegundos).

Si os fijais, en esta situación, nuestro valor más alto será de un poco más de 65 microsegundos. Esta es la limitación de la que hablábamos antes. Algunos fabricantes lo solucionan ofreciendo DOS registros para programar el timer, o que cada salto de 1 en en ese registro sea de 10 en la base de tiempos, o ambas cosas, e incluso tendremos dentro de un mismo microprocesador varios tipos de timers.

Al final, el tema está en que lo habitual es que no podamos tener ticks mayores de unos cientos de milisegundos lo que para los programas puede ser un tiempo excesivamente largo, pero para un reloj normal son demasiado cortos.

En ambos casos la limitación es la misma: tanto la interrupción como el contador interno del microprocesador se van a disparar demasiado rápido para lo que queremos. No he dicho que en el caso del incremento de la variable, normalmente es otro registro del procesador el que se va incrementado (o decrementando), y cuando decimos registro queremos decir dirección de memoria, que podremos leer y consultar. El mayor problema de este tipo de temporizadores está en que cuando la variable llega a su mínimo o su máximo, o bien vuelve a empezar o bien el timer se detiene. Y si no lo consultamos con la frecuencia suficiente, no sabremos que se ha detenido y habremos perdido nuestra base de tiempos.

Si os acordáis, hubo en Windows 95 un fallo que hacía que el ordenador se colgara si estaba más de unos días sin reiniciar. El problema era este mismo: Windows tenía un contador interno que se iba incrementando y cuando llegaba al máximo, volvía a empezar por cero. Y entonces todo se iba al garete porque lo que quiera que lo usara para su funcionamiento esperaba un incremento, no un decremento del orden de 230 ó 231.

Incidentalmente el problema tiene solución, y encima no es compleja en exceso pero sí que necesita código y saber la frecuencia media de consulta al timer, solución que debe ser compartida entre el timer y la parte que lo consulta. De hecho existen TRES soluciones. Os lo dejo como ejercicio de clase.

Por lo tanto yo no suelo recomendar el uso de este tipo de timers, aunque a veces es inevitable, y entonces es obligatorio evaluar si se puede producir dicho desbordamiento, o al menos es lo que hacen los buenos programadores. De todos modos, al final, veremos que es muy posible que siempre nos pueda ocurrir. Y no, incidentalmente, no creo que sea este el problema con los iPhone.

***

Bueno, ahora que sabemos qué es un timer y cómo funciona, vamos a construir un ejemplo práctio… en C.

//Este es el contador global de nuestro timer.

int g_timerCntr=0;

//Esto se debe llamar una sola vez al principio de todo

//Configura y pone los registros adecuados para que nuestro

//timer sea de 1 ms.

void InstallItmer(void)

{

       //Configurar los registros

       //…

}

//Arranca el timer con un intervalo

void SetTimer(int interval)

{

       g_timerCntr=interval;

       //Arrancar el timer

}

//Devuelve "cierto" si el timer ha terminado.

int HasEnded(void)

{

       return g_timerCntr==0;

}

//La interrupción que se lanzará periódicamente.

//Aquí el linker deberá poner su dirección en el lugar adecuado.

INTERRUPT_TIMER TimerInterrupt(void)

{

       g_timerCntr–;

       if(g_timerCntr==0)

              ; //Detener el timer

       else

              ;//Recargar el timer

}

La llamada a InstallTimer() debe hacerse al arranque del procesador, o al menos antes de que vayamos a usarlo por primera vez. Lo que hace esta función es configurar los registros según lo hemos explicado, de modo que, en base a la frecuencia de trabajo del procesador, más los registros adecuados, se nos dispare una interrupción cada milisegundo (en nuestro ejemplo). Pero no lo pone en marcha, simplemente lo configura.

TimerInterrupt() es la interrupción que nos saltará cada vez que transcurra un milisegundo. Fijaros que decrementamos en una unidad una variable local y que si llega a cero, detenemos el timer y si no lo recargamos. Esto es un ejemplo, porque a veces no será necesario recargarlo, es decir, el procesador disparará la interrupción cada 1 ms, otras veces lo hará sólo una vez y se detendrá, todo depende del modelo y características de casa procesador y del timer que estemos usando.

Aquí, con INTERRUPT_TIMER he ejemplificado el hecho de que debemos indicarle al compilador y al enlazador que dicha función es una interrupción y que debe ser instalada en el lugar adecuado. De nuevo estamos ante las particularidades de cada compilador y enlazador, aunque cuanto más moderno sea, más fácil nos lo pondrá.

Básicamente, una interrupción está localizada en una dirección de memoria exacta, y es ahí dónde se debe poner la dirección en la que está la rutina a modo de puntero a función: cuando se dispare el timer, el hardware del micro hará un call a lo que quiera que esté apuntando esa dirección. Si hay código bueno se ejecutará. Si no, lo habitual es que se cuelgue, aunque a veces también se puede producir una excepción hardware que nos permitirá resumir, de algún modo, el funcionamiento. Además, en algunos microprocesadores la instrucción de retorno de función (ret, para entendernos) será alguna instrucción especial como iret, retorno de interrupción. De nuevo estamos ante el hecho de que hay tantas variaciones como familias y fabricantes de electrónica.

Bueno, la parte difícil ya está. La fácil son las dos funciones que nos quedan. SetTimer() asignará un valor a la variable global y HasEndend() nos devolverá cierto cuando el contador haya llegado a cero. Si, por ejemplo, pasamos 1000 a SetTimer(), habremos obtenido un metrónomo de 1 segundo. Terminar nuestro reloj es casi trivial:

int _tmain(int argc, _TCHAR* argv[])

{

       int segundo=0, minuto=0, hora=0;

       InstallTimer();

       for(;;)

       {

              //NOTA: Con esto simulamos la interrupción

              if(g_timerCntr!=0)

                      TimerInterrupt();

              //FIN DE NOTA

              if(HasEnded())

              {

                      SetTimer(1000);

                      segundo++;

                      if(segundo>=60)

                      {

                             minuto++;

                             segundo-=60;

                      }

                      if(minuto>=60)

                      {

                             hora++;

                             minuto-=60;

                      }

              }

       }

       return 0;

}

¿Lo veis, no? Lo único a destacar en el ejemplo es que hemos puesto la interrupción en nuestro bucle for (para evitar que el autor tenga que crear un thread para el timer o suscribirse a uno de windows, etc). Obviando eso, tenemos un reloj en nuestro PC que ejemplifica cómo podríamos construir uno si TimerInterrupt() realmente se disparara cada milisegundo.

Copiaros el código a un proyecto de C++ de consola de Visual C++, definid

#define INTERRUPT_TIMER void

Al principio de todo, y os debe funcionar.

Añadir alarmas es relativamente fácil. Deberíamos tener una lista de variables hora, minuto y segundo y, en el bucle, comprobar si la hora actual es igual o superior a la programada, y si lo es, lanzar el sonido.

***

Bueno, ya tenemos nuestro reloj. Bonito, ¿no? Pues NO. Ese programa es una mierda de programa, y desde luego no creo que esté hecho así en iOS, y si lo está sería para matarlos lentamente (aunque el tipo de problema que tiene me hace pensar que…). Ese programa nos sube la CPU al 100% y en un dispositivo con batería se la iba a chupar en un tris.

No, la solución es algo más compleja, y pasa por mover bastantes cosas a la interrupción. Algo así:

//Este es el contador global de nuestro timer, y los datos

//Para mantener el reloj.

int g_timerCntr=0;

int hora=0,minuto=0,segundo=0;

//La interrupción que se lanzará periódicamente.

//Aquí el linker deberá poner su dirección en el lugar adecuado.

INTERRUPT_TIMER TimerInterrupt(void)

{

       g_timerCntr–;

       if(HasEnded())

       {

              SetTimer(1000);

              segundo++;

              if(segundo>=60)

              {

                      minuto++;

                      segundo-=60;

              }

              if(minuto>=60)

              {

                      hora++;

                      minuto-=60;

              }

       }

       //Recargar el timer

}

int _tmain(int argc, _TCHAR* argv[])

{

       InstallTimer();

       Sleep(INFINITE);

       return 0;

}

Este ejemplo ya no nos va a funcionar en el PC, pero gasta un montón menos de batería y nos deja la CPU al 0% o casi.  Ahora sí que tenemos un reloj de verdad, optimizado al 100%. Y ahora es cuando entra el tío de la rebaja: tenemos que medir (con un sistema de traza, por ejemplo), cuánto tiempo tarda la ejecución del bloque que hay dentro del primer if, y si es superior al milisegundo, debemos anotarlo y corregir el contador para evitar que nuestro reloj atrase. También debemos comprobar si la interrupción es reentrante (en el caso de que no haya acabado y si llega el momento de volver a ejecutarla, el procesador lo hará o no) y actuar en consecuencia. De todos modos, por el código usado, os aseguro que se tarda menos de 1 ms a hacer todo lo que hay dentro del if.

¿De dónde viene el ahorro de energía? Es muy sencillo: si os fijás en este último caso, sólo se ejecuta código cuando es estrictamente necesario, y cuando no el programa está durmiendo sin hacer nada. En el caso anterior el programa estaba continuamente comprobando si el timer había llegado a cero o no, de continuo, usando el 100% de la CPU.

Una solución intermedia al primer programa podría haber sido poner un else y dormir el programa durante 1 ms, pero aun así sigue siendo bastante inefectivo (y en Windows esa dormida será de, al menos, 15 ms).

***

Ya, ahora sí, ahora ya está todo perfecto… ¡NO! ¡Ni mucho menos! Lo de arriba sigue siendo una enorme pérdida de tiempo, un absurdo sólo válido como explicación teórica y poco más. ¿Realmente necesitamos tres variables para el reloj? Pues no, esas tres variables sólo son necesarias para cuando tengamos que verlas los seres humanos, por lo tanto debemos plantearnos la pregunta de qué precisión necesitamos. ¿1 segundo? Pues hagamos lo siguiente:

//Este es el contador global de nuestro timer, y los datos

//Para mantener el reloj.

int g_timerCntr=0;

unsigned long g_hora=0;

//La interrupción que se lanzará periódicamente.

//Aquí el linker deberá poner su dirección en el lugar adecuado.

INTERRUPT_TIMER TimerInterrupt(void)

{

       g_timerCntr–;

       if((g_timerCntr%1000)==0)

              g_hora++;

       //Recargar el timer

}

void MuestraHoraActual(void)

{

       int hora, min,seg;

       hora=g_hora/3600;

       min=g_hora/60;

       seg-=(hora*3600+min*60);

       printf("%02d:%02d:%d2d",hora,min,seg);

}

Fijaros en la interrupción, reducida a la mínima expresión a costa de crearnos una función de muestra de hora algo más compleja (que no estoy seguro si funcionará bien o no, pero sin lo hace pocas diferencias tiene que haber con la correcta). A simple vista hemos perdido, pero realmente hemos ganado, y no poco. ¿Qué relación hay entre las veces que vemos el reloj y las que se ejecuta la interrupción? Pues prácticamente infinitas.

Ahora me diréis que el iPhone muestra la hora arriba del todo. Sí, pero en minutos. Es decir, que podemos reducir incluso el contador de la interrupción a minutos en lugar de segundos. Y entonces añadimos el truco del almendruco, que es un flag para indicar a la shell que debe actualizar el reloj. Algo así:

INTERRUPT_TIMER TimerInterrupt(void)

{

       g_timerCntr–;

       if((g_timerCntr%1000)==0)

              g_hora++;

       if((g_hora)%60==0)

              g_flagCambiaMinuto=1;

       //Recargar el timer

}

Y será tarea de la interfaz del iPhone la de pintar cada vez que cambie el minuto.

Este acercamiento para el aviso de cambio de minuto es muy burdo. Lo que yo haría es tener llamar a un callback que me repinte la hora si el iPhone no está a pantalla completa y si eso no se puede hacer (que no se podrá, porque pintar en pantalla desde una interrupción como que es chapucero y a veces simplemente el hardware no lo permite), ese callback invalidaría el rectánculo de pintado y entonces el controlador de pintado, al recibir dicha invalidación, repintaría el área… con la nueva hora.

Fijaros cómo una tarea en principio simple tiene más miga de la que parece, sobre todo si quieres hacer las cosas bien. De hecho no sé cómo lo está haciendo Apple (y de paso Microsoft) con el tema de las horas, pero si yo tuviera que hacer algo así esa sería MI forma.

***

Ya está, ya terminamos. ¿Cómo podríamos añadir alarmas (y que suenen) al último ejemplo? La solución más sencilla es tener un array de variables del mismo tipo que g_hora. El tamaño podría ser dinámico con una lista enlazada, o un array estático con, pongamos, 16 variables que nos permitirian tener un máximo de 16 alarmas diferentes. Eso ya depende de la implementación. Esta sería la nueva interrupción, ya definitiva:

#define NUM_ALARMAS 16

unsigned long g_alarmas[NUM_ALARMAS];

INTERRUPT_TIMER TimerInterrupt(void)

{

       g_timerCntr–;

       if((g_timerCntr%1000)==0)

              g_hora++;

       for(int i=0;i<NUM_ALARMAS;i++)                                                    

              if(g_alarmas[i]!=0 && g_alarmas[i]<g_hora)

                      PreparaAvisoAlarma(i);      

if((g_hora)%60==0)

              g_flagCambiaMinuto=1;

       //Recargar el timer

}

Fijaros qué código más minimalista y completo: tenemos alarmas, tenemos reloj en tiempo real, actualización de la hora en el escritorio con apenas unas líneas y que apenas consumen una miseria de energía y tiempo de proceso.

Cuando g_alarmas[i] vale cero, es que esa alarma está desactivada, y si no vale cero y su valor es inferior a la hora actual, es que debe sonar. De nuevo PreparaAvisoAlarma() debe ser una función mínima que simplemente indique al sistema que la alarma debe tocar, ya sea despertando un hilo dormido o activando cualquier flag que luego alguien leerá y actuará en consecuencia.

Luego, la tarea del programa de interfaz con el usuario que le permita añadir, quitar o modificar alarmas, será la de “tocar” dichas variables a gusto del consumidor.

¿Cuál es, pues el fallo del iPhone? Pues que ante el cambio de hora, al programador de turno se le olvidó actualizar también ese array de alarmas (o su equivalente), con lo que ellas tienen el valor antiguo y sonarán cuando el valor de disparo antiguo se corresponda con el nuevo…

Peeeeeeeeeeeeeeeeeeeero, hay un gran pero, y aquí es donde vienen las risas. En mi caso, ninguna solución propuesta funciona. Ni en mi flamante iPhone 4G, ni en mi iPod Touch 2G. Ni por cierto en el de mi jefe, lo que supuso una seria discusión sobre lo inútil que era (yo) y que antes de hablar debería asegurarme de que la culpa es mía y no de los demás, como siempre hago (palabras textuales), a lo que yo le dije que probáramos el tema… Cuando vio que ni en el mío ni en el suyo funcionaban… se fue a recoger el correo (en fin, que la cosa se va acercando a su final lógico…).

La solución que me funciona es poner una alarma y activarla para TODOS LOS DÍAS de la semana, de lunes a domingo. Si no se hace así suena una hora más tarde, aunque sean alarmas antiguas o recién creadas, o apagues y reinicies el iPhone, o incluso lo restaures desde la copia de seguridad: falla.

Por lo tanto, alguna chapuza remiendosa debe de haber dentro del software del iPhone, porque, como os he demostrado, tener alarmas en un dispositivo como el que hablamos es, a nivel de desarrollo, completamente trivial, y si regenerarlas desde cero no funciona…

[NOTA: El código de ejemplo, salvo la primera parte, no está probado, así que lo mismo puede haber algún gazapo si a alguien se le ocurriera probarlo].

Un comentario sobre “Timers, alarmas y el iPhone”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *