FreeRTOS Discovery

=Introducción=

Este proyecto busca implementar el conocido juego Road Fighter en una matriz de leds de 8x8 a través de un sistema operativo en tiempo real (RTOS) sobre una tarjeta Discovery. Este documento pretende describir de manera clara el código implementado en el proyecto para que sea aprovechado por estudiantes y demás personas interesadas en reforzar sus conocimientos sobre sistemas operativos en tiempo real.

En este proyecto, en general, se aprovecha la simulación de ejecución en paralelo entre diferentes procesos que ofrece FreeRTOS para ejecutar diferentes estructuras en C que se encargan de diferentes funciones tales como mostrar el "sprite" del carro y el de los enemigos, verificar que el carro no choque con los enemigos y multiplexar todos los carros para una correcta visualización en la matriz de leds. De esta forma, en conjunto conforman el juego. Así que, ademas se pretende emplear de forma adecuada herramientas tales como las colas y semáforos, las cuales son estructuras de datos cuya función es garantizar que las tareas se comuniquen entre si de forma que la ejecución simulada en paralelo no afecte la sincronización entre las mismas. A continuación se explica con mas detalles las herramientas de sincronización utilizada y el código implementado.

=Sistemas Operativos (SO)=

Un sistema operativo es un software grande y complejo que realiza la función de intermediario entre el usuario de un computador y su respectivo hardware y su propósito principal es el de proporcionar un entorno en el que el usuario pueda ejecutar programas de forma práctica y/o eficiente. Entre las principales funciones u objetivos que realiza un sistema operativo son la de ejecutar los programas del usuario, asignar recursos del ordenador a los programas, dar acceso a los dispositivos del computador y a los periféricos, proporcionar un sistemas organizado de almacenamiento de datos, entre otros. Un sistema operativo gestiona y administra recursos a través de métodos y provee servicios básicos para que un programa realice sus funciones sin la interacción directa con el hardware.

El sistema operativo proporciona los medios para hacer un uso adecuado de recursos como: el hardware, la unidad central de procesamiento (CPU), la memoria y los dispositivos I/O. Un SO está generalmente organizado en dos partes o se puede abordar desde dos puntos de vista:


 * Lado del Usuario: varía de acuerdo con la interfaz que se esté utilizando. Generalmente se tiene un sistema diseñado de tal manera que los recursos sean monopolizados por el usuario. Pero igualmente se tiene el caso en el que múltiples usuarios acceden de forma simultánea por distintos terminales o un usuario no tiene interacción alguna (o muy mínima) con el computador.


 * Lado del Sistema: desde este punto de vista el sistema operativo es el programa que tiene mayor relación con el hardware. Desde aquí, se puede nombrar al SO como el asignador de recursos o un programa control. Un sistema como este tiene muchos recursos que pueden ser necesarios para solucionar un problema:
 * Tiempo de CPU.
 * Espacio de Memoria.
 * Espacio de Almacenamiento de archivos.
 * Dispositivos I/O.

=Sistemas Operativos en Tiempo Real (RTOS)=

Es un sistema que no sólo requiere que las funciones de realicen de manera correcta sino que éstos se realicen dentro de un periodo de tiempo especificado. Se utiliza principalmente cuando es necesario administrar múltiples tareas simultáneamente con plazos de tiempo estrictos. Entre las características típicas de muchos RTOS son: tienen un único propósito y tienen requisitos de temporización específicos, son de pequeño tamaño, de bajo costo y de producción en masa.

=Descripcion del Proyecto=

A continuación se muestra una descripción detallada del código utilizado, el cual no es mas que software que se ejecuta sobre el sistema FreeRTOS que se encarga de abstraer el hardware de la tarjeta para que el software que implementemos no tenga relación directa con este, brindando tan solo las herramientas necesarias para acceder a toda su funcionalidad, sin comprometer su buen funcionamiento. De esta forma se implementan diferentes estructuras en c con diferentes funcionalidades, las cuales al ejecutan sobre la capa de software del sistema operativo que gestiona el hadware para que las ejecute por turnos, permitiendo así simular un paralelismo entre los diferentes procesos ligados a los diferentes programas implementados en codigo C. Garantizando ademas que se cumplan restricciones de tiempo indicadas por el programador el cual puede estar seguro que cualquier tipo de retardo temporal que se ejecute, sera llevado a cabo por el sistemas operativo de forma que corresponda a un intervalo de tiempo real.

Para un mejor entendimiento del lector. A continuación se muestran el diagrama que describe las estructuras implementadas en código C :



Descripcion de bloques:

Main: En la clase main se encuentra toda la información concerniente a la ejecución de las tareas. En primera instancia se deben referenciar las tareas, al crear la tarea esta retorna una xTasHandle, por medio de parámetros como se verá posteriormente, esta referencia puede ser usada después cómo parámetro por ejemplo para eliminar la tarea (vTaskDelete). Las referencias de las tareas del juego se muestran a continuación:

xTaskHandle hMemsTask; xTaskHandle hMostrarEnemigosTask; xTaskHandle hMostrarCarroTask; xTaskHandle hMux_JuegoTask; xTaskHandle hLector_MemoriaTask; xTaskHandle hControlTask;

En segundo lugar se crean los prototipos de las tareas que permitir la sintaxis del compilador para cada tarea.

portTASK_FUNCTION_PROTO( vMemsTask, pvParameters ); portTASK_FUNCTION_PROTO( vMostrarEnemigosTask, pvParameters ); portTASK_FUNCTION_PROTO( vMostrarCarroTask, pvParameters ); portTASK_FUNCTION_PROTO( vCont_ColumnasTask, pvParameters ); portTASK_FUNCTION_PROTO( vLector_MemoriaTask, pvParameters ); portTASK_FUNCTION_PROTO( vControlTask, pvParameters );

Las tareas en vez de tener el método main tienen el siguiente método:

portTASK_FUNCTION( vControlTask, pvParameters )

Donde el primer parámetro es el prototipo de la tarea.

Finalmente en el método main (único en el proyecto) se crean las tareas y si se desea se puede llamar al método HwInit que provee la librería utils.h para inicializar cierto puertos y algunas interrupciones, este método se puede modificar a gusto en la clase utils.c. Despues de esto se hace el llamado a al método vTaskStartScheduler, quién organiza las tareas según sus prioridades y toma el control de toda la ejecución.

int main( void ) {   HwInit;

// Tasks get started here...

xTaskCreate( vMemsTask, (signed char *) "MEMS", configMINIMAL_STACK_SIZE, NULL, mainMEMS_TASK_PRIORITY, &hMemsTask ); xTaskCreate( vMostrarCarroTask, (signed char *) "MOSTRAR_CARRO", configMINIMAL_STACK_SIZE, NULL, mainMEMS_TASK_PRIORITY, &hMostrarCarroTask ); xTaskCreate( vMostrarEnemigosTask, (signed char *) "MOSTRAR_ENEMIGOS", configMINIMAL_STACK_SIZE, NULL, mainMEMS_TASK_PRIORITY, &hMostrarEnemigosTask ); xTaskCreate( vLector_MemoriaTask, (signed char *) "LECTOR_MEMORIA", configMINIMAL_STACK_SIZE, NULL, mainINTEGER_TASK_PRIORITY, &hLector_MemoriaTask ); xTaskCreate( vControlTask, (signed char *) "CONTROL", configMINIMAL_STACK_SIZE, NULL, mainTIME_TASK_PRIORITY, &hControlTask ); vTaskStartScheduler; // This should never return. // Will only get here if there was insufficient memory to create // the idle task.

for; }

Cabe resaltar 3 valores que entran por parámetro al método xTaskCreate los cuales son por ejemplo: vControlTask, hControlTask, mainINTEGER_TASK_PRIORITY. El primero es el prototipo de la tarea que debe tener el mismo nombre en la tarea para que se sintetice correctamente, el segundo es la referencia a la tarea esta se modifica por dentro del método xTaskCreate, y finalmente en tercer lugar está la prioridad de la tarea esta se define de la igual forma en el main:


 * 1) define mainINTEGER_TASK_PRIORITY  ( tskIDLE_PRIORITY + n )

Puede tener el nombre que se desee, y n es un entero que indica la prioridad de la tarea, esta prioridad puede ir desde 0 hasta “configMAX_PRIORITIES – 1” donde “ configMAX_PRIORITIES “ puede ser configurado  dentro de la librería  FreeRTOSConfig.h

Control: Esta tarea está encargada de dirigir el funcionamiento y orden del juego, se comunica con las otras tareas mediante el uso de colas, colas donde se transporta en específico un valor de tipo uint8_t (unsigned integer of length 8 bit), este valor indica el estado del juego, para ello se definen como constantes en la librería control.h de la siguiente forma:


 * 1) define ESTADO_JUEGO  			1
 * 2) define ESTADO_NIVEL	  			2
 * 3) define ESTADO_PERDIO  			3
 * 4) define ESTADO_GANO  			4

Se deben crear 3 colas para la comunicación, pues ya que a pesar de que se está enviando la misma información, cada vez que una tarea recibe el mensaje de la cola, va liberando esta última (FIFO) y por lo tanto las otras tareas con menor prioridad o de ejecución consecutiva no estarán informadas del estado del juego. El código para enviar el estado a la cola se muestra a continuación:

xQueueSend(get_xCola1, &state, portMAX_DELAY);

Los parámetros que controla esta tarea son: el estado del juego. El tope de la memoria que le indica al contador de la tarea lector_memoria hasta donde debe detenerse y reiniciarse. La velocidad de ejecución de la tarea lector_memoria, de esto depende la velocidad de lectura de cualquier memoria. El nivel en el que se encuentra el juego. Y finalmente una variable que indica cuando debe reiniciarse la lectura de la memoria; estas son las variables definidas:

uint8_t state; uint16_t tope_meoria; uint16_t velocidad; uint8_t nivel; uint8_t reinicio;

Mux_Juego: Esta es una librería del juego. La cual es implementada por 2 tareas que son las encargadas de multiplexar la pantalla cuando se está mostrando el carro manejado por el usuario y los enemigos que son tomados de la clase memoria_enemigos. Ya que estas dos tareas no pueden ser ejecutadas en “paralelo” pues de eso se trata la multiplexación, estas se comunican mediante el uso de un semáforo que es la herramienta que provee el sistema operativo para la coordinación de tareas. En este caso se desea la sincronización de estas tareas para la toma de un recurso común que en este caso son los puertos de salida a la matriz.

Esta tarea es encargada de enviar por los puerto de salida la posición del carro en la matriz. Para esto primero debe recibir la cola proveniente del control la cual informa el estado del juego, si el juego se encuentra en ESTADO_JUEGO se puede realizar esta tarea. Para recibir dicha cola se usa el método: xQueueReceive(get_xCola1, &estado, portMAX_DELAY)
 * Mostrar_Carro:

Donde ‘estado’ es una variable definida dentro de la tarea actual. De encontrarse en el estado correspondiente se procede a mostrar el carro en la matriz de juego, para esto se importa la libereria carro.h que contiene los métodos para obtener la posición del carro, y luego de esto se envía el semáforo indicando que ya se ejecutó esta tarea: xSemaporeGive(xSemaforoMux);

El semáforo debió crearse previamente e inicializarse antes del ciclo de ejecución de la tarea: xSemaphoreHandle xSemaforoMux; //Declaración vSemaphoreCreateBinary(xSemaforoMux); //Incialización

Esta tarea muestra los enemigos en la matriz, para ello se importa o incluye la librería “enemigos.h” la cual es una memoria leída por la clase lector_memoria, que incluye los métodos para obtener la posición de los enemigos. Está claro que para mostrar los enemigos se debe capturar el semáforo, es decir, hasta que mostrar_Carro no envíe el semáforo, no se podrán visualizar los enemigos en la matriz de juego. Para recibir el semáforo se usa la siguiente signatura:
 * Mostrar_Enemigos:

xSemaphoreTake(xSemaforoMux, portMAX_DELAY);

Mems:

(Microelectromechanical sensors) Esta tarea es la encarda de controlar la información que provee el acelerómetro de la tarjeta stm32f4 discovery, gracias a los métodos que brinda la librería stm32f4_discovery_lis302.h. Para la ejecución de esta tarea se recibe la cola enviada por el control informando el estado del juego, pues se desea que el carro solo se puede mover mientras el juego se encuentra en ESTADO_JUEGO. Para controlar el carro de acuerdo a la información del acelerometro se usa la siguiente variable creada de la forma: enum accelAxis { xAxis, na1, yAxis, na2, zAxis, na3 }; Esta variable contiene la información concerniente a los 3 ejes del acelerometro para mover el carro se muestra el siguiente código de ejemplo:

if ( accel[xAxis] == 0) { } else {   if ( accel[xAxis] > 0 ) {   moverIzquierda; vTaskDelay( 120*portTICK_RATE_MS ); } else {   moverDerecha; vTaskDelay( 120*portTICK_RATE_MS ); } }

Donde los métodos moverIzquierda y moverDerecha están dados por la librería ‘carro.h’ y el método vTaskDelay es un retraso de tiempo para evitar que el carro se mueva cada vez que se ejecute el ciclo de la tarea, con un tiempo esperado de 1ms y mejor se mueva cada 120 ms.

Memorias:

La memoria ganó, memoria perdió, memoria nivel, representan memorias de sólo lectura, las cuales tienen almacenada el texto que debe salir al ganar y al perder, ademas de los "unos" lógicos que representan los leds prendidos correspondientes a los obstáculos ("enemigos") que se acercan al carro en cada nivel.

Por otra parte, como se menciona anteriormente, es necesario que las tareas se comuniquen, compartiendo información entre si y se mantengan sincronizadas sin que la ejecución en paralelo afecte su funcionamiento.

En este proyecto usamos 2 tipos de estructuras de datos que permiten esta sincronización:

Una cola es una estructura de datos al cual modela una especie de cajones en los cuales ingresan los datos se secuencialmente desplazando a los datos anteriores de forma que el primer dato en entrar sea el primero en salir (Estructura FIFO). El uso de estas es estándar en la comunicación de tareas en sistemas operativos porque permiten que un tarea le envié un mensaje a la otra y esta recoja la información tal cual como se le va enviando. Estas pueden ser representadas en diagrama de relaciones de la siguiente forma:
 * Las Colas:



En FREERTOS el método crea una cola y asigna un espacio en memoria suficiente es: xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize );

Donde:
 * xQueueHandle=> almacena la referencia a la instancia
 * uxQueueLength=> Representa la cantidad de posiciones que tendra la cola
 * uxItemSize => Representa cantidad de datos que consumirá en bytes cada elemento.

Para enviar mensajes el metodo es: portBASE_TYPE xQueueSend( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );

Para recibir mensajes el metodo es: portBASE_TYPE xQueueReceive( xQueueHandle xQueue, void *pvBuffer, portTickType xTicksToWait );

Donde:
 * xTicksToWait=> representa el tiempo que se esperara para posicionar el mensaje en la cola o esperar por un mensaje de la misma.
 * Al establecer NCLUDE_vTaskSuspend en 1 con el valor portMAX_DELAY se suspende por tiempo indefinido la tarea hasta que se libere espacio en la cola o se reciba un mensaje dependiendo el caso.
 * xQueue => Representa la cola por donde se mueven los mensajes
 * pvItemToQueue => Representa el mensaje.



[2]

Son una forma de representar el envió de datos y su uso es estándar en sistemas operativos para que las tareas se sincronicen. Son muy usados para: Sincronizar dos tareas que usan un recurso común, de esta forma si una tarea lo esta usando el semáforo lo indicará y la otra tarea no interferirá en el proceso. Otro uso común, es el de indicar que un evento ocurrió, para asegurarse que otras tareas que dependen del evento estén informadas del suceso de dicho evento. Pueden ser representados en diagrama de relaciones de la siguiente forma:
 * Los Semaforos:



A continuación se muestra el diagrama del macro algoritmo usado para la sincronización de tareas por medio de semáforos:



Este método se encarga de crear un semaforo BINARIO:

vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore )

Este método garantiza que la tarea esperará "xBlockTime" de tiempo por el semaforo "xSemaphore", si no se libera después de ese tiempo se reanuda la ejecución de la tarea. Si se libero se captura el semáforo para asegurar que nadie lo utilizara:

xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xBlockTime )

Este método libera al semáforo para que otras tareas puedan usar el recurso asociado con la misma:

xSemaphoreGive( xSemaphoreHandle xSemaphore )



[2]

A continuación se muestra el diagrama de relaciones entre las tareas ejecutadas del proyecto y como se sincronizan por medio de varias colas y un semáforo:



=Video de funcionamiento= A continuación se muestra un video en el que se aprecia el funcionamiento del proyecto. La tarjeta discovery esta incluida dentro de una caja plástica que le da un aspecto de juego portátil. Ademas, permite versatilidad para jugar con el acelerometro.

http://www.youtube.com/embed/GTkXQPJGMSU GTkXQPJGMSU

=Conclusiones:=


 * Los sistemas operativos permiten gran versatilidad para implementar software variado sobre hardware diferente.


 * Los sistemas operativos en tiempo real permiten emular un buen grado de paralelismo entre los procesos, sin embargo el programador debe tener siempre presente que esto se ejecuta de forma secuencial.


 * El programador debe tener presente que el hardware es limitado, de esta forma, se garantizará que 2 tareas no accedan a un mismo recurso a las vez. Como se observó en este documento, una forma estándar en al que se maneja este tipo de situaciones es el uso de semáforos.

=Bibliografía=
 * [1] Silberschatz, Abraham, Peter Baer. Galvin, Greg Gagne, and Allende Jesús. Sánchez. Fundamentos De Sistemas Operativos. Madrid [etc.: McGraw-Hill, 2006. Print.
 * [2] http://sistemasembebidos.com.ar/