Timer expired, o eso creo…

A menudo disponemos de varias formas de realizar una misma tarea. A veces la elección es por gusto o simpatía y otras el contexto suele dictar algunas normas que limitan las elecciones seguras.

En el caso de los timers por software, mi elección personal suele ser emplear un viejo esquema de timers individuales que luego de visitar assembler de varios micros ha terminado en C. Otras veces, dado que dispongo de 32-bits, por algún motivo u otro me inclino a simplemente chequear el valor de un contador. En ambos casos, tanto los timers individuales como el contador suelen ser decrementados e incrementado (respectivamente) por un interrupt handler.

Esto requiere que, por un lado, (en C) declaremos la variable de interés como volatile para que el compilador sepa que aún dentro de una función esta variable puede cambiar su valor. Caso contrario, loops de espera serán eliminados por el optimizador.

extern volatile uint32_t tickcounter;

Por otra parte, y por la misma razón, es necesario que el acceso a dicha variable sea atómico, es decir, ocurra en una instrucción del microprocesador o microcontrolador (o una secuencia no interrumpible de éstas). Caso contrario, podría aceptarse una interrupción en medio de la cadena de instrucciones que lee los bytes individuales que componen dicha variable y si dicha interrupción altera su contenido, obtendríamos un valor equivocado. Esto ocurre por ejemplo si utilizamos timers/contadores de 32-bits en la mayoría de los micros de 8-bits; ahondo en este tema en otro post alegórico.

Una vez resueltos estos temas, nos queda el problema de la finitud. No sólo de nuestra existencia, me refiero a la cantidad de bits que dan la longitud de los enteros que forman los timers.

El esquema de timers que les comenté es muy simple: cada timer es decrementado si su valor es distinto de cero, por lo que su uso se remite a escribir un valor y revisar frecuentemente si llegó a cero. Existirá obviamente un jitter dado por el tiempo que transcurre entre la escritura y el primer decremento en el interrupt handler, y luego desde que el timer llega a cero hasta que la función interesada lo observa.

El observar el valor de un contador, por el contrario, presenta algunos inconvenientes cuya solución motiva este post. Por problemas de atomicidad podemos vernos forzados a emplear una variable más chica y nos vamos a encontrar con que el contador desborda en un tiempo corto.

¿Qué ocurre cuando el contador desborda? Bueno, puede que nada si tomamos las precauciones necesarias.

Todo funciona perfectamente y pasa todos los tests si el contador no desborda entre chequeos, es decir, si el contador es lo suficientemente “largo” como para soportar todo el tiempo de vida de la aplicación. Por ejemplo, un contador de segundos de 32-bits tarda unos 136 años en desbordar. Sin embargo, un contador de milisegundos de 16-bits desborda una vez cada poco más de un minuto (65536 milisegundos).

Si el tiempo entre chequeos de dicho contador es mayor, existe la posibilidad de que el contador desborde más de una vez entre que lo chequeamos una y otra vez. En un caso como éste, deberemos valernos de otro recurso dado que, así solo, esto no nos permite controlar el tiempo.

Si en cambio podemos asegurar que el contador a lo sumo sólo desbordará una vez entre chequeos, deberemos escribir correctamente el código para que nuestra comparación sobreviva al desborde del contador. Caso contrario, como dijimos, todo funcionará bien pero “a veces” (cada vez que el tiempo deseado involucre un desborde intermedio), ocurrirán “cosas raras” con la operación del timer.

A los fines prácticos consideremos el siguiente esquema de variables, donde usamos mayúsculas violando tal vez algunos coding styles pero lo hacemos para resaltar el contador:

extern volatile uint32_t MS_TIMER;
uint32_t interval = 5000; // milisegundos
uint32_t last_time;   

Las opciones mostradas en la siguiente porción de código funcionan correctamente, dado que la diferencia sobrevive al overflow del contador y es comparada correctamente con el valor deseado:

	last_time = MS_TIMER;

	if(MS_TIMER - last_time > interval)
	    printf("Timer expired\n");

	if(MS_TIMER - interval > last_time)
	    printf("Timer expired\n");

Por el contrario, esto no funciona correctamente

uint32_t deadline;

	/* no debo hacer esto
	deadline = MS_TIMER + interval;
	if(MS_TIMER > deadline)
	    printf("Timer expired\n");
	*/

Si, por ejemplo, MS_TIMER tiene el valor 0xF8000000 e interval es 0x10000000, entonces deadline será igual a 0x08000000 y MS_TIMER es claramente mayor a deadline desde el inicio, lo cual ocasiona que el timer expire inmediatamente (no es lo que queremos…).

Una solución de compromiso para utilizar un esquema como el anterior requiere emplear enteros con signo y funciona para intervalos de hasta MAXINT32 unidades:

int32_t deadline;
int32_t interval = 5000; // milisegundos

	deadline = MS_TIMER + interval;
	if((int32_t)(MS_TIMER - deadline) > 0)
	    printf("Timer expired\n");

Otras formas más creativas pueden resultar en timers que nunca expiran cuando el contador supera MAXINT32, dado que se ve como un número negativo y es por ende menor que un tiempo solicitado (que es positivo); pero que sin embargo funcionan cuando se inicia (o reinicia) el equipo. En general las podemos agrupar en errores al elegir el largo de la variable (incluyendo al signo) y los vamos a dejar de lado.

Como pudimos observar, algunos problemas se manifiestan al referirse a tiempos MAXINT32 unidades temporales luego de iniciado el sistema. Por lo general este valor es 2^31, lo cual, para un contador de milisegundos son unos 25 días y para uno de segundos unos 68 años. Algunos errores en aplicaciones Linux y similares se manifestarán cuando reporten a tiempos iniciando a partir del año 2038 (el contador de segundos cuenta a partir del inicio del año 1970).

Si por el contrario utilizamos esquemas de 16 ú 8-bits…

En los ejemplos vistos, el uso de la comparación por ‘mayor’ nos garantiza que el intervalo va a ser mayor al solicitado. Si por ejemplo lo seteamos en este momento pidiendo un intervalo de un segundo y un milisegundo después el contador de segundos es incrementado, si utilizáramos comparación por ‘mayor o igual’ nuestro timer expiraría inmediatamente (bueno, en 1ms…). De modo similar, si lo solicitamos 1ms luego de la interrupción demorará 1,999s en expirar en vez de 1s. En un esquema donde la comparación y el incremento no son asíncronos, podemos evitar el jitter y emplear comparación por ‘igual o mayor’.

Want to share this ?

Leave a Reply

Your email address will not be published. Required fields are marked *

Enter Captcha Here : *

Reload Image