Hacemos Envíos a Todo el País

Para cotizaciones y pedidos mándanos un mensaje en este enlace. En Cochabamba visítanos en nuestras oficinas, estamos a tu servicio, tu consulta no nos molesta.

Imprimir

El "Código Espagueti" y Los Patrones Avanzados de Programación

Escrito por Raúl Alvarez.

Código EspaguetiMuchos estudiantes o egresados de electrónica que trabajan con sistemas embebidos (microcontroladores / microprocesadores) desconocen patrones o paradigmas de programación avanzados. Si bien algunos utilizan frecuentemente y con cierta familiaridad el patrón Plano Secundario / Plano Principal (programa principal + interrupciones), muchos todavía siguen escribiendo "Código Espagueti" y una gran mayoria desconocen los patrones más sofisticados de programación como las Máquinas de Estado Finito, los Planificadores de Tareas o los Sistemas Operativos de Tiempo Real.

A fin de llegar a escribir código cada vez más profesional y acorde con estándares de calidad, seguridad y confiabilidad, un desarrollador de software para sistemas embebidos debe conocer y dominar gradualmente las idiosincracias de cada uno de los patrones o métodos de programación más usados. En este artículo explicaremos brevemente en qué consiste cada uno de ellos. 

El "Código Espagueti"

El código espagueti no es en realidad un patrón de programación, de hecho es más correcto clasificarlo como un "anti patrón". Para entender mejor el origen de su nombre, consideremos el siguiente ejemplo de pseudocódigo (muy simplificado, por cierto) para el software de una lavadora: 
/* Ejemplo de Código Espagueti*/
// Controlar que la puerta esté cerrada
main() {
     while(TRUE) {
         while (!(puerta_cerrada()))
              ;
         // Empezar con el llenado de agua
         while (!(NIVEL_AGUA_OK)) {
             if (puerta_cerrada()) {
                 valvula_agua(ABRIR);
                 compartimento_detergente(ABRIR);
             }
             else
                 valvula_agua(CERRAR);
        }

         //Calentar el agua
         if (SELECCION_AGUA_CALIENTE) {
             while (tiempo <= TIEMPO_CALENTAMIENTO) {
                 if (puerta_cerrada())
                     calentador(ON);
                 else
                     calentador(OFF);
                 ++tiempo;
             }
         }

         // Empezar ciclo de lavado
         tiempo = 0;
         while (tiempo <= TIEMPO_LAVADO) {
             if (puerta_cerrada())
             ciclo_lavado(ON);
             else
                 ciclo_lavado(OFF);
             ++tiempo;
        }
     }
}

Del análisis del código anterior se desprenden las siguientes observaciones:
  • Se observa un uso recurrente de sentencias condicionales anidadas (if - then - else, while, etc.), a esta característica se la denomina "Código Convolucionado", la cual complica en gran manera el seguimiento y la comprensión del algoritmo, siendo este además el ambiente propicio para errores de programación (bugs).
  • Se puede notar que dentro de cada uno de las secciones principales del programa (llenado de agua, calentado y ciclo de lavado) se hace importante revisar que la puerta de la lavadora esté cerrada, ya que si la puerta es abierta por algún motivo, la operación en curso debe ser inmediatamente detenida por seguridad; ya sea que se trate del llenado de agua, el calentamiento del mismo o el movimiento del tambor de lavado. Esto conlleva necesariamente a una repetición de cierto código de comprobación (comprobación de puerta abierta), lo cual hace al programa no solo mas largo, sino también más complejo de entender. Añadir funciones adicionales (segundo ciclo de lavado, enjuague, centrifugado, etc.), complicaría aún más el seguimiento del programa.
  • En las tres funciones implementadas se observa lo que en la jerga de programación se conoce como "Código de Bloqueo". En el primer caso, la instrucción condicional while (!(NIVEL_AGUA_OK)) bloquea al procesador, ya que este último debe esperar a que la condición se cumpla para poder continuar con la ejecución de las otras tareas restantes o de otras tareas más críticas, como por ejemplo: Comprobar el funcionamiento de sensores, el monitoreo de un teclado de control, o la actualización de una pantalla de visualización. En el mejor de los casos ya es de entrada muy dificil añadir esas funciones para que se ejecuten de manera paralela. ¿Qué pasaría por ejemplo si ocurre una falla del sensor de nivel de agua? Existe el peligro de que el procesador se quede "ciclado" permanentemente en la instruccion "while", a menos que se implemente una función adicional de temporización (timeout) o se active el "watchdog timer".
Otra desventaja evidente del Código Espagueti es que carece de estructura y modularidad, lo cual dificulta enormemente la expansión del sistema al momento de añadirle mayor funcionalidad. Una vez que el código llega a tener un tamaño considerable, se hace dificil entenderlo, mantenerlo, depurarlo y mejorarlo, incluso para el mismo autor del código, y cuanto más para otros programadores que pudieran quedar eventualmente a cargo del mantenimiento de dicho código.

El Patrón "Super Lazo" (Super Loop)

El siguiente código:
/* Ejemplo de un Super Loop */
main() {
     while (TRUE) {
         // Muchas y muchas instrucciones aquí
         // ..........
     }
}

Ejemplifica básicamente el patrón Super Lazo, el cual se caracteriza por un gran lazo cerrado (super loop), el cual se repite indefinidamente y encierra dentro de sí un gran número de instrucciones y posiblemente otros lazos cerrados.

El ejemplo descrito más arriba como "Código Espagueti" es de hecho también un Super Lazo. No hay nada intrínsecamente malo con el Super Lazo, ya que si se le provee de cierta estructura, el código llega a clarificarse bastante y para ciertas aplicaciones simples este patrón de programación podría ser todo lo que se necesite. Para dotarle de mejor estructura y claridad al Super Lazo podría empezarse con agrupar el código en funciones que encapsulen apropiadamente el comportamiento del código:
/* Ejemplo de un Super Loop estructurado */
main() {
     while (TRUE) {
         inicialización();
         cargar_agua();
         calentar_agua();
         ciclo_lavado_1();
         ciclo_lavado_2();
         centrifugado();
     }
}


En el ejemplo anterior, si bien el código se ve mejor estructurado, aún se asume la presencia de código de comprobación de puerta cerrada y retardos de tiempo en todas o casi todas las funciones que lo requieran.

Patrón Plano Secundario / Plano Principal (Background / Foreground)

El patrón Plano Secundario / Plano Principal se basa en el Super Lazo, con la adición del uso de Interrupciones; en este patrón el código se agrupa básicamente en dos segmentos principales:
  • El  Lazo Principal (Main Loop), llamado Plano Secundario o "Background", y
  • La Sección de Rutina(s) de Interrupción, llamado también Plano Principal o "Foreground".
Nota: Podría parecer contradictorio llamarle Plano Secundario a la rutina principal main(), sin embargo esto no es así. Las Rutinas de Interrupción son denominadas Plano Principal debido a que en comparación con la rutina main() siempre reciben una atención inmediata del procesador, o dicho de otro modo, las interrupciones tienen siempre mayor prioridad en su atención y pueden "interrumpir" en cualqueir instante a la rutina main(), asumiendo obviamente que las interrupciones estén habilitadas.
El Lazo Principal (Background) Está formado por el código contendio en la sección main() { } del programa. En esta sección se ubican generalmente todas las operaciones de importancia secundaría o que no requieren un tratamiento en tiempo real.

En la Sección de Rutina(s) de Interrupción se ubican todas las funciones que requieren una operación en tiempo real (tratamiento inmediato) en el sistema. Para el mismo ejemplo de la lavadora, si se desea aplicar este patrón de programación, se podría extraer tanto el código de comprobación de puerta abierta y las rutinas de retardo de tiempo de las diferentes funciones principales y ubicarlas en una rutina de interrupción. A esta operación de simplificación y modularización se la conoce como "Factorización". El código resultante quedaría separado en dos secciones, a saber:
/* Ejemplo de Patrón Background/Foreground */

/* Sección Lazo Principal (Background) - En esta sección se ubican
 todas las operaciones del sistema que no necesitan tratamiento 
 crítico inmediato, que duran mucho tiempo y son generalmente
 sincrónicas */
main() {
  while (TRUE) {
   // Funciones principales sin código de comprobación
   // y retardos de tiempo
   inicialización();
   cargar_agua();
   calentar_agua();
   ciclo_lavado_1();
   ciclo_lavado_2();
   centrifugado();
  }
}

/* Sección de Rutina(s) de Interrupción - En esta sección se ubican
 las operaciones en el sistema que requieren tratamiento crítico inmediato, son de comparativamente corta duración y generalmente
 asíncronas (pueden suceder de manera impredecible) */
void interrupt isr(void) {
  /* Código que se ejecuta si la puerta de la lavadora
  se abre por algún motivo */
  if (pin_puerta_lavadora == OPEN)
   // Bandera para comunicar a todas las funciones que
   // la puerta está abierta
   puerta_lavadora = OPEN;
 
 /* Código único para generar retardos de tiempo a ser
 usados por las distintas funciones del sistema */
  if (timer_interrupt_flag)
   // Bandera para comunicar a la función en turno
   // que el retardo de tiempo ha concluido
   timer_over = TRUE;
}

Sistemas de Tiempo Real

Dentro de toda la gama de aplicaciones embebidas con microcontroladores o microprocesadores existe una categoría especial a la que pertenecen los Sistemas de Tiempo Real. Estos sistemas se caracterizan por la necesidad de responder de manera inmediata a los estímulos del medio ambiente en el cual trabajan, debido a que tienen más estrictos requerimientos de tiempo (puntualidad) para la respuesta a los estímulos.

AvionicaSi existe la posibilidad de que a causa de un error de cómputo, o de una respuesta tardía del sistema se produzca la muerte de una persona, a este tipo de sistema se lo conoce como Sistema Crítico de Tiempo Real (Hard Real Time System). A esta categoría pertenecen por ejemplo los siguientes sistemas: Un módulo de piloto automático en un avión, un equipo de soporte de vida en un hospital, un sistema de alarma de seguridad industrial o doméstica, un sistema militar de defensa, etc.

Otros sistemas de respuesta de tiempo crítica cuya falla no resulte fatal se denominan Sistema Semicrítico de Tiempo Real (Soft Real Time System). A esta categoría pertenecen por ejemplo: Envasadoras automáticas de bebidas, decodificadores de video, etc. En ambos casos, todavía se quiere que una botella no pase sin contenido en la banda transportadora, o que un usuario de televisión digital no pierda la cantidad de cuadros por segundo para una imagen clara, sin embargo si esto sucede, nadie resulta herido.

Los sistemas "Hard" se caracterizan por ser altamente críticos y no admiten fallas triviales (si el sistema falla, el avión se cae, el paciente muere, o se produce un incendio). De ahí que esta categoría de aplicaciones sea la más difícil de diseñar e implementar correctamente y requiere de los ingenieros, tanto de hardware como de software, que tengan una alta capacitación y experiencia; y posean una variada colección de herramientas y técnicas avanzadas de programación.

En la práctica, casi todos los Sistemas de Tiempo Real se implementan mediante el uso de Kernels de Sistemas Operativos de Tiempo Real (RTOS Kernel), en algunos casos solamente con el uso de Planificadores de Tareas (Task Schedulers) diseñados a medida y otras técnicas avanzadas de programación.

Sistemas Conducidos Por Eventos (Sistemas Reactivos)

Los Sistemas Conducidos Por Eventos (Event Driven Systems) son sistemas cuya función primaria es la interacción constante con su entorno mediante la recepción y envío de eventos. La dificultad principal en estos sistemas radica en el hecho de que, el orden, tipo y momento de llegada de los eventos es impredecible. Ejemplo: un sistema de protección de bolsa inflable (airbag) en un automóvil (no se puede predecir cuando ocurrirá un choque que active la bolsa de aire).

Sistemas Conducidos Por Tiempo

Los Sistemas Conducidos Por Tiempo (Time Driven Systems) son sistemas cuyo comporatimiento es generalmente predecible y responde principalmente a una programación interna llevando a cabo un flujo específico de tareas con tiempos de duración predefinidos. Ejemplo: Un dinosaurio mecatrónico animado en un museo. Existe una categoría especial de Sistemas Conducidos Por Tiempo que permite el diseño de una respuesta rápida para sistemas reactivos. En este método el sistema realiza una consulta por software (polling) intensiva a fin de reaccionar inmediatamente a eventos asincrónicos externos o internos.

Máquinas de Estado Finito (Finite State Machines)

Las Máquinas de Estado Finito o Autómatas Finitos pertenecen a un diferente paradigma de programación, el cual describe el sistema mediante una abstracción que provee las siguientes características:
  • Un número finito de estados o condiciones de existencia para el sistema.
  • Cada estado realiza un número definido de acciones, o muestra un comportamiento específico a ese estado.
  • Son conocidos de antemano un determinado grupo de eventos, los cuales a su vez causarán cambios de un estado a otro de acuerdo a reglas específicas.
De esta manera las Máquinas de Estado Finito (MEF) definen el comportamiento específico del sistema. Cuando el sistema está en un determinado estado, el sistema por definición, responde a un subgrupo definido de entradas, produce un subgrupo definido de salidas y puede transicionar a un subgrupo definido de estados.

La manera más usual de implementar las MEF en lenguaje C es mediante bloques switch - case:

/* Ejemplo de Patrón con Máquina de Estado Finito */
enum eEstados {
     INICIALIZACION,
     CARGAR_AGUA,
     CALENTAR_AGUA,
     CICLO_LAVADO_1,
     CICLO_LAVADO_2,
     CENTRIFUGADO } estado;
 
main() {
    estado = INICIALIZACION;
    while (TRUE) {
        switch (estado) {
             case INICIALIZACION:{
                if (inicialización())
                    estado = CARGAR_AGUA;
             } break;
             case CARGAR_AGUA:{
                 if (cargar_agua())
                     estado = CALENTAR_AGUA;
             } break;
             case CALENTAR_AGUA:{
                 if (calentar_agua())
                     estado = CICLO_LAVADO_1;
             } break;
             case CICLO_LAVADO_1:{
                 if (ciclo_lavado_1())
                     estado = CICLO_LAVADO_2;
             } break; 
             case CICLO_LAVADO_2:{
                 if (ciclo_lavado_2())
                     estado = CENTRIFUGADO;
             } break;
             case CENTRIFUGADO:{
                 if (centrifugado())
                     estado = INICIALIZACION;
             } break;
         }
     }
}

Algunas ventajas en el Uso de MEFs

  1. Provee un grado de abstracción del sistema muy conveniente, además de cierta estructuración, lo cual hace al código más entendible. Ayuda en la minimización del código espagueti.
  2. En cierto modo cada estado "guarda memoria", o es una "instantánea" de eventos y estados pasados. Debido a esta característica las MEFs ayudan a reducir la cantidad de variables globales y banderas en la aplicación.
Si bien las MEFs no son apropiadas para todas las aplicaciones posibles, es muy recomendable su uso en aquellas que se prestan para este tipo de modelo.

Patrón con Planificadores de Tarea (Task Schedulers)

Un Planificador de Tareas es la parte de un sistema operativo multitarea encargado de decidir cual tarea correrá próximamente. Esta decisión se basa en tres parámetros básicos:
  • El estado actual de la tarea que corre
  • La prioridad relativa asignada a otras tareas que están listas para correr, y
  • El algoritmo específico con el cual se implementó el Planificador de Tareas.
En un sistema embedido con Planificador de Tareas o con Sistema Operativo de Tiempo Real, las funciones del sistema se ejecutan en forma de Tareas concurrentes ("paralelas") independientes. En realidad, ningún procesador que no sea de núcleo múltiple (multicore) pude ejecutar más de una tarea al mismo tiempo; sin embargo con el uso de los Planificadores de Tareas o Sistemas Operativos, sumado a la gran velocidad de trabajo del procesador, se crea la apariencia de una concurrencia real (es decir, que más de una tarea se ejecuta al mismo tiempo).

Los algoritmos de planificación más populares son:

Planificador Equitativo (Round-Robin Scheduler): En este algoritmo el planificador asigna a cada tarea una "rebanada" de tiempo (time slice), la cual tiene una duración constante para todas las tareas, de esta manera distribuye el tiempo del procesador equitativamente entre todas las tareas.

Planificador Equitativo

Planificador Cooperativo (Cooperative Scheduler): En este algoritmo el planificador ejecuta las tareas en un orden asignado en base al principio de "Correr Hasta Terminar" (Run To Completion), permitiendo que cada tarea se tome el tiempo requerido para completar todas sus instrucciones; sin embargo, bajo el principio cooperativo se asume que cada tarea no tardará demasiado tiempo para culminar (recuerden el código de bloqueo), de modo que comprometa la ejecución puntual de las otras tareas, rescindiendo voluntariamente el uso del procesador al Planificador cuando ya no lo necesite.

Planificador Cooperativo

Planificador Preemptivo (Preemptive Scheduler): El Planificador Preemptivo se caracteriza por realizar la interrupción de tareas de menor prioridad toda vez que una tarea de mayor prioridad está lista para correr, garantizando de esta manera que las tareas de mayor prioridad (tareas críticas) corran, siempre cuando ya estén listas para hacerlo.

Planificador Preemptivo

Los Planificadores de Tarea pueden ser considerados, en mayor o menor grado, como sistemas operativos de tiempo real simplificados (aunque no todos los entendidos en el tema están de acuerdo con esta afirmación), en función también a la complejidad de la implementación de sus respectivos algoritmos y a la cantidad/calidad de servicios adicionales que puedan ofrecer (típicamente, servicios de temporización, comunicación entre tareas, cambio de contexto, etc.) Algunos los llaman también Sistemas Operativos "A Medida".

Sistemas Operativos de Tiempo Real (Real Time Operating Systems)

Los Sistemas Operativos de Tiempo Real (SOTR) ofrecen, aparte de un Planificador de Tareas, los servicios adicionales característicos de un sistema operativo, como ser: Temporización, Comunicación Entre Tareas y Conmutación de Contexto.
  • Servicios de Temporización: El SOTR pone a disposición de todas las tareas que lo requieran servicios de temporización para generar retardos de tiempo, sin bloquear o retrasar la ejecución de otras tareas (sin "código de bloqueo").
  • Comunicación Entre Tareas: Provee los servicios necesarios para señalización entre tareas (semáforos, banderas), pase de eventos y datos (mailboxes, messege queues) los cuales no solo tienen como propósito el intercambio de información entre tareas, sino también la correcta coordinación entre las mismas, a fin de no entorpecerse mutuamente, como cuando dos tareas diferentes necesitan acceder a un mismo recurso (un puerto, por ejemplo) al mismo tiempo.
  • Conmutación de Contexto: En un sistema embebido con SOTR las diferentes tareas del sistema corren de manera concurrente ('paralela"). Cada vez que una tarea corre, la misma recibe la impresión de que es la única que corre, o que todo el procesador y los recursos del mismo están a su sola disposición. Esto lo logra el SOTR con la llamada Conmutación de Contexto, la cual consiste básicamente en almacenar la información de contexto (registros de trabajo y estado, stack y contador de programa en el procesador) pertinente de cada tarea. Antes que una nueva tarea corra, el procesador almacena la información de contexto correspondiente a la tarea actual y carga la información de contexto de la nueva tarea. La Conmutación de Contextos añade cierta carga de trabajo al procesador, siendo esta una de las desventajas de los SOTRs, aunque con un diseño apropiado del software y hardware esto no influye en el desempeño del sistema.
De acuerdo a como ha sido diseñado, un SOTR puede usar un Planificador de Tareas Cooperativo, Equitativo, Preemptivo o alguna combinación de los tres anteriores, aunque el Preemptivo es el más popular, sobre todo en sistemas complejos y grandes.

En un diseño preemptivo típico, a todas las tareas se les asigna un órden o grado de prioridad para su ejecución. Siempre que una tarea de más alta prioridad esté lista para correr, la tarea de menor prioridad que corre en ese instante es interrumpida por el Planificador para dar lugar a la de mayor prioridad. Una vez que la tarea de mayor prioridad ha concluido, o no tiene nada más que hacer, cede voluntariamente el uso del procesador al Planificador de Tareas y este, a su vez lo cede a la tarea de menor prioridad a fin de que pueda correr hasta su culminación, siempre que no haya otra tarea de más alta prioridad lista para correr.

Los SOTR son generalmente desarrollados en lenguaje C y ofrecen la API respectiva para que el usuario pueda implementar su aplicación. Toda la funcionalidad del sistema embebido se la diseña en base a tareas que se codifican en forma de funciones de lazo infinito (for ( ; ; )), que el Planificador de Tareas se encarga de hacer correr en su momento.  A diferencia de los diseños basados en patrones Plano Principal / Plano Secundario, o con Máquinas de estado Finito, en un diseño con SOTR, es el SOTR el que está en control del sistema, no así la aplicación.

Otra ventaja importante en el uso de SOTRs y los Planificadores de Tareas es la Reusabilidad, debido a que estos patrones ofrecen un kernel o plataforma básica de software, el cual a su vez abstrae convenientemente la plataforma de hardware, de tal modo que este mismo kernel puede usarse una y otra vez en otros sistemas y diseños, con poca o ninguna modificación.

Los SOTR son implementados generalmente en ANSI C y el fabricante ofrece un porte a diferentes plataformas o procesadores. Actualmente existen una amplia gama de SOTRs de licencia comercial, licencia gratuita, de código abierto, código cerrado o una combinación de las anteriores.

Algunos ejemplo de SOTRs los encuentras en el siguiente enlace:  http://en.wikipedia.org/wiki/List_of_real-time_operating_systems

Conclusión

El "Código Espagueti" es el estilo de programación que identifica a todo principiante en el ámbito de la programación de sistemas embebidos (microcontroladores / microprocesadores). Todos hemos escrito "Código Espagueti" en mayor o menor grado cuando dabamos nuestros primeros pasos en la programación de microcontroladores.

Para llegar a ser un buen ingeniero de software para sistemas embebidos de tiempo real, es necesario estudiar, practicar y dominar progresivamente el diseño e implementación con patrones avanzados de programación, eso hace de uno un programador más competente. Los diferentes patrones son simplemente herramientas en la caja de herramientas de un ingeniero o desarrollador de software y no existe la herramienta perfecta que sea ideal para todas las aplicaciones.

Algunas herramientas son más sofisticadas  que otras, sin embargo no todas las aplicaciones se benefician de los SOTR, sobre todo cuando hay muchos factores técnicos, financieros y operativos que considerar. Para las aplicaciones más pequeñas podría bastar simplemente un buen diseño con Máquinas de Estado Finito, un patrón Plano Secundario / Plano Principal o un Planificador de Tareas. El Código Espagueti, definitivamente no tiene lugar en los diseños profesionales.

Raúl Alvarez Torrico
www.TecBolivia.com

Deseo recibir noticias de nuevos proyectos, artículos, materiales y promociones especiales.

Nombre:

Correo Electrónico:


http://www.google.com/url?sa=t&source=web&cd=1&ved=0CBUQFjAA&url=http%3A%2F%2Fen.wikipedia.org%2Fwiki%2FList_of_real-time_operating_systems&rct=j&q=list%20of%20rtos&ei=Nz9tTZ7wEMSRgQeI6fmXBA&usg=AFQjCNHQ_EZSlMACYnqq-hvx63vao5293g&sig2=RIaEAkW7rOd6P0oz99ISRQ&cad=rja