Contenido>Indice>Intro CursoC51

REPARTO DE TAREAS



Aplicaciones con el 8051

Muchas personas han empezado a programar utilizando el lenguaje BASIC en un PC o máquina similar. Los programas que realizan inicialmente no suelen ser muy complicados. Empiezan a correr cuando se teclea "RUN" y terminan en un END o STOP. Mientras tanto, el PC se dedica totalmente a la ejecución del típico programa  "HELLO WORLD". Cuando el programa termina, se retorna al editor BASIC, o al entorno de trabajo utilizado.

La experiencia es muy buena y el nuevo programador cree que ya sabe programar. Sin embargo, cuando se escribe un programa para un microcontrolador como el 8051, el problema del comienzo y final del programa se presenta rápidamente. Típicamente, el software de un sistema basado en el 8051 está formado por múltiples programas individuales, que ejecutados conjuntamente, contribuyen a la consecución del objetivo final. Entonces, un problema fundamental es asegurarse de que todas las partes del programa se ejecutan.

Sistemas sencillos con el 8051

El enfoque más sencillo es llamar a cada una de las sub-funciones del programa de una forma secuencial, de tal forma que después de un cierto tiempo, cada parte se habrá ejecutado el mismo número de veces. Esto constituye el bucle de fondo (background loop), o programa principal. En primer plano (foreground) aparecen las funciones de interrupción, iniciadas por sucesos producidos en tiempo real, tales como señales de entrada o desbordamiento de temporizadores.

El intercambio de datos entre las interrupciones y el programa principal se realiza usualmente a través de variables globales y flags. Este tipo de programas puede funcionar correctamente si se tiene cuidado en el orden y frecuencia de ejecución de cada sección.

Las funciones llamadas por el programa principal deben escribirse de tal forma que en cada llamada se ejecute una sección particular de su código. Así al entrar en esta función, se toma la decisión de la tarea que le toca ejecutar, se ejecuta esa parte y se sale de la función, cambiando posiblemente algunos flags que indicarán la tarea a realizar en la siguiente llamada. De esta forma, cada bloque funcional debe mantener su propio sistema de control que asegure la ejecución del código adecuado en cada entrada al mismo.

En un sistema de este tipo, todos los bloques funcionales tienen la misma importancia, y no se entra en un nuevo bloque hasta que no le toque su turno dentro del programa principal. Sólo las rutinas de interrupción pueden romper este orden, aunque también están sujetas a un sistema de prioridades. Si un bloque necesita una cierta señal de entrada, o se queda a la espera de la misma impidiendo que otros bloques puedan ejecutarse, o cede el control hasta que le vuelva a tocar su turno dentro del bucle principal. En este último caso se corre el riesgo de que el suceso esperado se produzca y no tenga una respuesta, o que la respuesta llegue demasiado tarde. No obstante, los sistemas de este tipo funcionan bien en programas en las que no hay secciones que tengan exigencias críticas en los tiempos de respuesta.

Los llamados sistemas en tiempo real no son de este tipo. Típicamente contienen código cuya ejecución origina o es originada por sucesos del mundo real, que trabajan con datos procedentes de otras partes del sistema, cuyas entradas pueden cambiar lenta o rápidamente.

El código que contribuye en mayor medida a la funcionalidad del sistema, debe tener precedencia sobre las secciones cuyos objetivos no son críticos respecto al objetivo final. Sin embargo, muchas de las aplicaciones con el 8051 tienen partes con grandes exigencias de tiempo de respuesta, que suelen estar asociadas a interrupciones. La necesidad de atender a las interrupciones tan rápido como sea posible, requiere que el tiempo de ejecución de las mismas sea muy corto, ya que el sistema dejará de funcionar si el tiempo de respuesta a cada interrupción supera al intervalo de tiempo en la que las interrupciones se producen. 

Por regla general se consigue un nivel aceptable de prestaciones si las funciones complejas se llevan al programa principal, y se reservan las interrupciones para las secciones con exigencias de tiempo críticas. En cualquier caso, aparece el problema de la comunicación entre el programa principal y las rutinas de interrupción.

Este sencillo sistema de reparto de tareas trata a todas ellas de igual forma. Cuando la CPU se encuentra muy cargada de trabajo por tener que atender a entradas que cambian con rapidez, puede ocurrir que el programa principal no corra con la suficiente frecuencia y la respuesta transitoria del mismo se degrade.

Reparto de tareas simple - Una solución parcial

Los problemas de los sistemas de bucle simple pueden solucionarse parcialmente, controlando el orden y la frecuencia de las llamadas a funciones. Una posible solución pasa por asignar una prioridad a cada función y establecer un mecanismo que permita a cada función especificar la siguiente función a ejecutar. Las funciones de interrupción no deben seguir este orden y deben ser atendidas con rapidez. Los sistemas de este tipo pueden resultar útiles, si el tiempo de ejecución de las funciones no es excesivo.

Una solución alternativa consiste en utilizar un temporizador, que asigne un tiempo de ejecución a cada trabajo a realizar. Cada vez que el tiempo asignado se supere, la tarea en curso se suspende y comienza otra tarea.

Desafortunadamente todas estas posibilidades suelen intentarse tarde, cuando los tiempos de respuesta se alargan en exceso. En estos casos, lo que había sido un programa bien estructurado termina degenerando en código spaghetti, plagado de ajustes y modos especiales, tendentes a superar las diferencias entre las demandas de los sucesos del mundo real, y la respuesta del sistema. En muchos casos, los mecanismos de control de las funciones llamadas generan una sobrecarga que acentúa aún más el desfase entre las exigencias y  la respuesta.

La realidad es que los sucesos del mundo real no siguen ningún orden ni pueden predecirse. Algunos trabajos son más importantes que otros, pero el mundo real produce sucesos a los que hay que responder inmediatamente.

Un enfoque práctico

Si no se recurre a un sistema operativo en tiempo real como RTX51, ¿Qué se puede hacer?

Un mecanismo sencillo para controlar el programa principal, puede ser una sentencia switch, con la variable switch controlada por algún suceso externo en tiempo real. Idealmente, éste debe ser la rutina de interrupción de mayor prioridad. Las tareas del programa principal con mayor prioridad se colocan en los case de menor valor numérico, y las de menor prioridad en los case con mayor valor numérico. Cada vez que se ejecuta una tarea, se incrementa la variable switch, permitiendo que se ejecuten las tareas de menor prioridad. Si se produce la interrupción, se asigna a la variable switch el valor correspondiente a la tarea de mayor prioridad. Pero si la interrupción tarda bastante en producirse nuevamente, la variable switch permitirá la ejecución de la tarea de menor prioridad, para después comenzar automáticamente con la de mayor prioridad.

Si la interrupción se produce en el case de nivel 2, la variable switch se pone a 0 y así sucesivamente. En este caso las tareas de menor prioridad se ignoran. El sistema no es obviamente ideal, ya que solo la tarea de mayor nivel se ejecuta con la suficiente frecuencia. Sin embargo, bajo condiciones normales puede ser una forma útil de asegurar que las tareas de baja prioridad no se ejecuten con mucha frecuencia. Por ejemplo, no tiene mucho sentido medir la temperatura ambiente más de una vez cada segundo. En un sistemas de este tipo, la tarea encargada de medir la temperatura ambiente estaría situada a nivel 100, en el sistema de reparto de tareas.

Este método falla cuando una tarea de baja prioridad tiene un tiempo de ejecución muy largo. Incluso si se produce la interrupción que exige que el bucle regrese a la tarea de mayor prioridad, el salto a la misma no se producirá mientras no termine la tarea en curso. Para que se produzca la situación deseada se necesita de un mecanismo de rebanado de tiempo (time-slice).

Un truco útil consiste en utilizar una interrupción libre para garantizar que la tarea de alta prioridad se ejecute a tiempo. Para ello se asigna la tarea de alta prioridad a la rutina de servicio de la interrupción libre o sobrante. Así cuando se produzca la interrupción en tiempo real, y justo antes de salir de la rutina de servicio de la misma, se pondrá a uno el flag de petición de la interrupción libre, con lo cual, tras el RETI comenzará la ejecución de la tarea de alta prioridad. Por supuesto, la interrupción libre debe ser de baja prioridad.

Hay que tener en cuenta que en estos casos el factor más importante es conseguir el menor tiempo de ejecución posible, y particularmente en las rutinas de interrupción. Ello significa que se debe hacer un uso completo de las extensiones de C51, tales como los punteros específicos, los bits de funciones especiales y las variables locales de tipo register. 


   Contenido>Indice>Intro CursoC51