Skip to content

RTOS: Real Time Operating System

Gonzalo G. Fernández edited this page Jan 24, 2020 · 22 revisions

Introducción

Dado que mi experiencia en Sistemas Operativos de Tiempo Real es nula, el objetivo de este proyecto es estudiarlos mientras lo aplico en el control de un brazo robótico básico. Por su amplia difusión, el kernel que he decidido utilizar es freeRTOS sobre la placa de desarrollo EDU-CIAA-NXP.

Para su estudio, me guiaré con el libro "Masteringthe FreeRTOS™ Real Time Kernel: A Hands-On Tutorial Guide" de Richard Barry, en paralelo con el FreeRTOS V10.0.0 Reference Manual (Ambos se pueden encontrar en el siguiente link).

Dentro del repositorio del firmware_v3 del proyecto CIAA, se encuentran aplicados todos los ejemplos del libro. La idea de este proyecto no es realizar exactamente esos ejemplos, sino orientar cada concepto aprendido al objetivo final del control de un brazo robótico, con lo que se generarán nuevos "ejemplos" más específicos que serán mis distintas pruebas a lo largo del desarrollo del proyecto.

En resumen, todo lo que sigue son mis apuntes teóricos y ejemplos o ejercicios con los que fui trabajando. Cabe aclarar, que dichos ejemplos serán específicos para la placa, utilizando la librería sAPI que provee el firmware.

Para más información sobre freeRTOS, el link a la página principal.

Diferencia entre soft real-time y hard real-time

Al plantear una aplicación embebida con requisitos de tiempo real, es necesario definir si esos requisitos son soft real-time (tiempo real "blando") o hard real-time (tiempo real "duro").

Un requerimiento de tiempo real blando es aquel que tiene un determinado deadline que de no cumplirse no provoca que el sistema falle. Caso contrario, un requerimiento de tiempo real duro es aquel que de no cumplirse su deadline el sistema fallará.

La diferencia entre ambos tipos de requerimientos es difícil de marcar ya que de cierta manera es subjetiva. Los requerimientos de tiempo, el hecho de que un sistema falle o no y la magnitud de esa falla, son impuestos a criterio del desarrollador.

Por ejemplo en este proyecto, se analizan las siguientes dos tareas:

  • La primera consiste en refrescar un display con los datos actuales del robot.
  • La segunda es el cumplimiento de consignas del usuario moviendo los motores del robot a una determinada velocidad.

Se puede decir como desarrollador, que el primera es un requisito mucho más blando que el segundo, ya que un par de milisegundos más o menos en la actualización del display no impactarán demasiado en el usuario, mientras que esa misma diferencia en el movimiento de un motor desecadenará en un mal desempeño del robot.

*Explicar guías de cómo escribir el código

Manejo de tareas

*Falta explicación de que es una tarea

Las tareas como funciones en C

Las tasks o tareas, son simplemente funciones implementadas en lenguaje C. Su estructura es similar a la función main que suele implementarse en todos los sistemas embebidos, es decir, dentro existe un bucle de ejecución infinito como el que sigue:

void ATaskFunction( void *pvParameters )
{

    int32_t lVariableExample = 0;
    
    for( ;; )
    {
        /* Bucle de ejecución de la tarea */
    }

    vTaskDelete( NULL ); // Eliminación de la tarea
}

Se puede observar que no existe return, sino que es una fución void, y que además recibe un puntero void donde se puede recibir información. Puede observarse también que al final de la función la tarea se elimina a sí misma, sin embargo, la función no debería llegar a ese punto sino que explícitamente debería eliminarse desde algún punto en la aplicación.

Creación de tareas

Para crear tareas se requiere llamar la siguiente función xTaskCreate:

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
                        const char * const pcName,
                        uint16_t usStackDepth,
                        void *pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t *pxCreatedTask );

Donde los parámetros que recibe son los siguientes:

Parámetro Descripción
pvTaskCode Puntero a la función donde se encuentra implementada la tarea.
pcName Un nombre descriptivo de la tarea (solo como ayuda en debugueo).
usStackDepth Tamaño del stack a ser asignado para dicha tarea*.
pvParameters Parámetros que se le pasarán a la tarea al ser creada.
uxPriority Prioridad que se le asignará a la tarea. Desde 0, prioridad más baja, hasta (configMAXPRIORITIES-1), prioridad más alta.
pxCreatedTask *Explicar

Ejemplo 1

Para implementar la creación de tareas, se realizó el ejemplo 1 "Creating Tasks".

En este ejemplo, ambas tareas se crean en la función main, pero no tiene porque ser el caso, ya que, por ejemplo, una tarea podría crear a la otra.

Ejemplo 2

En el ejemplo 2 "Task Parameter", se expone una forma de pasar parámetros a una tarea, simplificando el código del ejemplo 1.

Prioridad

El parametro uxPriority en la función xTaskCreate() al crear una tarea, le asigna su prioridad. Esa prioridad puede cambiarse posteriormente aunque ya se haya iniciado el scheduler llamando a la función vTaskPrioritySet() (ver más información en el manual de usuario de freeRTOS).

Un número bajo de prioridad indica baja prioridad y viceversa. El rango de prioridades va de 0 a (configMAX_PRIORITIES - 1), y más de una tarea pueden tener la misma prioridad. Es recomendable tener el menor número de prioridades posible, ya que este número mientras mayor sea mayor será la RAM necesaria por el scheduler.

Lo más importante a tener en cuenta sobre las prioridades es que el scheduler de freeRTOS siempre asegurará que la tarea con mayor prioridad que esta disponible para pasar a estado RUN sea la seleccionada para pasar a ese estado. (Mejor explicación en el funcionamiento del squeduler)

Medidas de tiempo e interrupción por Tick

En los dos ejemplos anteriores puede observarse que ambas tareas tienen la misma prioridad y parecen ejecutarse al mismo tiempo, es decir, el scheduler pasa de una tarea a la otra muy rapidamente. Este mecanismo del scheduler para repartir el tiempo entre tareas con igual prioridad y las cuales estan disponibles para pasar al estado Running, se llama time slice (estos mecanismos se estudian más adelante). Esa interrupción periódica se denomina interrupción por tick, y esta dada por la frecuencia de tick en Hz, definida en configTICK_RATE_HZ.

Como desarrollador entonces, al trabajar con RTOS la unidad de tiempo será el periodo de tick. Entonces, todos los intervalos de tiempo deben pasarse a ticks para que sean fijos y determinados. A la hora de programar es recomendable expresar los intervalos de tiempo en milisegundos, y pasarlos a ticks por medio de la función pdMS_TO_TICKS(), de esta forma puede cambiarse la frecuencia de ticks sin necesidad de cambiar el código.

Ejemplo 3

En el ejemplo 3 "Task Priorities", el código es igual al ejemplo 2, la unica diferencia es que las tareas tienen distinta prioridad.

Como la tarea de mayor prioridad nunca pasa a un estado donde no este disponible para pasar a estado Running, la tarea de menor prioridad nunca se ejecuta.

Estados de una tarea

En general, una tarea puede estar en un estado Running (ejecutándose) o Not Running (sin ejecutarse). Dado que una tarea puede encontrarse en estado Not Running por diversos motivos, el estado puede dividirse en otros estados más específicos y descriptivos de la situación en que se encuentra dicha tarea.

Estado Bloqueado o Blocked

Una tarea se encuentra en estado bloqueado si se encuentra esperando un evento determinado. Es un subestado del estado Not Running.

Las tareas pueden entrar en estado Blocked para esperar dos tipos de eventos diferentes:

  • Eventos temporales: Expiración de un delay o un tiempo absoluto.
  • Eventos de sincronización: Eventos desde otras tareas o interrupciones.

Estado Suspendido o Suspended

Es un subestado del estado Not Running, y son aquellas tareas que no estan disponibles para el scheduler.

La única forma de que una tarea entre en este estado es a través de la función vTaskSuspend(), y la única forma de que salga es a través de las funciones vTaskResume() o xTaskResumeFromISR().

Estado Listo o Ready

También es un subestado del estado Not Running. Se encuentran en este estado las tareas que están en estado Not Running pero no bloqueadas ni suspendidas, es decir, aquellas que están disponibles para ser ejecutadas pero todavía no entran en estado Running.

En la figura anterior puede observarse una máquina de estado correspondiente a una tarea, y los modos en que puede pasar de un estado a otro.

Estado bloqueado para crear un delay

Hasta el ejemplo 3, para crear un delay se utiliza un bucle vacío. Esto no solo implica el consumo de procesamiento sino que, como se observó en el ejemplo 3, las tareas de mayor prioridad al no entrar nunca en estado Not Running no perminen la ejecución de tareas de menor prioridad.

Una de las formas de que una tarea ingrese en estado Blocked es a través de la API vTaskDelay(), disponible solo si en el archivo de configuración está en 1 la opción INCLUDE_vTaskDelay. Esta API lleva a la tarea a estado bloqueado durante un determinado número de interrupciones por tick.

void xTaskCreate( TickType_t xTicksToDelay );

Donde el parámetro que recibe es el siguiente:

Parámetro Descripción
xTicksToDelay Número de interrupciones por tick que la tarea permanecerá en estado Blocked antes de pasar a estado Ready. Se puede utilizar el macro pdMS_TO_TICKS() ver sección Medidas de tiempo e interrupción por Tick

Ejemplo 4

En el ejemplo 4 "Using the Blocked state to create a delay", se soluciona el ejemplo 3, utilizando la API vTaskDelay() para llevar a las tareas a estado Blocked con lo que la tarea de mayor prioridad ya puede dar paso a la ejecución de la tarea de menor prioridad.

La función API vTaskDelayUntil()

La cantidad de tiempo que la tarea permanece en estado bloqueado al utilizar la API vTaskDelay() es relativa al momento en que es llamó a vTaskDelay(). El problema de ésto es que, como desarrollador, uno no tiene conocimiento del momento exacto en que se llama a la función.

En cambio, la API vTaskDelayUntil() tiene como parámetro el momento exacto, como conteo de ticks absoluto en la aplicación, en el cuál la tarea debe moverse de estado Blocked a estado Ready. Se debe utilizar esta API cuando se requiere que la ejecución de la tarea sea periódica con una frecuencia fija.

void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement );

Donde los parámetros que recibe son los siguientes:

Parámetro Descripción
pxPreviousWakeTime El momento en el que la tarea dejó (por última vez) el estado Blocked. Se utiliza como referencia para calcular el próximo momento en el que se deberá dejar el estado Blocked.
xTimeIncrement La cantidad de interrupciones por ticks en que la tarea permanecerá en estado Blocked respecto a pxPreviousWakeTime.

Todas aquellas tareas que en ningún momento llaman una API que les haga entrar en estado Blocked, se denominan de procesamiento continuo o simplemente continuas. Ejemplos de tareas de este tipo son las tareas implementadas en los ejemplos 1, 2 y 3.

NOTA: Es importante tener en cuenta que las tareas continuas no deben tener alta prioridad, ya que no permitirían la ejecución de tareas de menor prioridad como se observó en el ejemplo 3.

Ejemplo 5

En el ejemplo 5 "Converting the example tasks to use vTaskDelayUntil()", se modifica el ejemplo 4 para utilizar la API * vTaskDelayUntil()*. De esta forma, ahora se asegura que el intermitente de los LED sea periódico de frecuencia fija.

Ejemplo 6

En el ejemplo 6 "Combining blocking and non-blocking tasks" del libro, se combinan tareas de naturaleza continuas con tareas periódicas que entran en estado Blocked.

En este caso, como se mencionó en la introducción, en el proyecto se posee una tarea de baja prioridad y también de naturaleza continua que es el display de información del estado del sistema. Por lo tanto, se aprovecha este ejemplo para implementar dicha tarea en conjunto con las mismas tareas de intermitencia de los LEDs con las que se viene trabajando.

La tarea Idle y el Idle Task Hook

Al lanzar el scheduler (vTaskStartScheduler()) se crea automáticamente una tarea denominada Idle. Dicha tarea se ejecuta cuando todas las demás se encuentran en estado Not Running (es el caso del ejemplo 4). Esto es así porque siempre debe haber al menos una tarea en estado Running.

La tarea Idle es básicamente un bucle vacío y, por lo tanto, siempre esta disponible para ejecutarse. Tiene la menor prioridad posible, entonces nunca se impondrá sobre otra tarea.

Funciones de Idle Task Hook

El Idle Hook se utiliza para agregar funcionalidades a la tarea Idle. Esto a través de una función idle hook que se ejecutará una vez por iteración en el bucle de la tarea Idle.

Usos comunes del Idle Task Hook:

  • Ejecutar procesos continuos o de muy baja prioridad.
  • Medir la cantidad de capacidad de procesamiento disponible. La tarea Idle, al ejecutarse cuando ninguna otra tarea puede ejecutarse, implica que la cantidad de tiempo que está la tarea Idle en estado Running es una medida de cuánto tiempo de procesamiento está disponible.
  • Poner al procesador en un modo de baja o ahorro de energía (Hay otros métodos de realizar esto).

Estas funciones serán de la forma:

void vApplicationIdleHook( void );

Para utilizar estas funciones e necesario configUSE_IDLE_HOOK en 1.

Limitaciones de la implementación de funciones Idle Task Hook

Las funciones Idle Task Hook demen respetar las siguientes reglas:

  • Nunca intentar bloquear o suspender.
  • La tarea Idle responsable también de limpiar los recursos del kernel después de que una tarea es eliminada, por lo tanto si se utiliza en la aplicación la API vTaskDelete() hay que tener en cuenta que debe haber tiempo disponible para la ejecución de la tarea Idle, y que la tarea no puede quedarse permanente en las funciones Idle Task Hook.

Cambiar prioridad de una tarea

La función API vTaskPrioritySet() permite cambiar la prioridad de cualquier tarea una vez que se haya lanzado el scheduler. Solo se encuentra disponible si en el archivo de configuración INCLUDE_vTaskPrioritySet se encuentra en 1.

La API se utiliza de la siguiente manera:

void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );

Donde los parámetros que recibe son los siguientes:

Parámetro Descripción
pxTask El handle de la tarea cuya prioridad se desea cambiar. Si se pasa NULL se cambia la prioridad de la tarea desde donde se llama la función.
uxNewPriority La prioridad a la que se desea cambiar.

Por el otro lado, la función API uxTaskPriorityGet() puede utilizarse para obtener la prioridad de una tarea. Solo se encuentra disponible si en el archivo de configuración INCLUDE_uxTaskPriorityGet se encuentra en 1.

La API se utiliza de la siguiente manera:

UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );

Donde los parámetros que recibe son los siguientes:

Parámetro Descripción
pxTask El handle de la tarea cuya prioridad se desea cambiar. Si se pasa NULL se cambia la prioridad de la tarea desde donde se llama la función.
Valor devuelto La prioridad a la tarea solicitada.

Eliminar una tarea

La función API vTaskDelete() permite eliminar una tarea, ya sea la tarea desde donde se llama la API o cualquier otra a través de su handle. La API solo está disponible si en el archivo de configuración INCLUDE_vTaskDelete esta seteado a 1.

La API se utiliza de la siguiente manera:

void vTaskDelete( TaskHandle_t pxTaskToDelete );

Donde el parámetro que recibe es el siguiente:

Parámetro Descripción
pxTaskToDelete El handle de la tarea a eliminar. NULL para eliminar la tarea desde donde se llama la API.

Algoritmos de Scheduling

El algoritmo de scheduling es la rutina de software que decide cuál tarea en estado Ready pasará a estado Running.

Archivo de configuración FreeRTOSConfig.h

3.4 Creating Tasks

  • Máximo longitud que puede tener el nombre (parámetro pcName en xTaskCreate()) de una tarea: configMAX_TASK_NAME_LEN
  • Tamaño del stack utilizado por la tarea Idle: configMINIMAL_STACK_SIZE (creo que se lo puede tomar como referencia a la hora de asignar el tamaño de stack a las tareas implementadas en la aplicación).

3.5 Task Priorities

  • Máximo número de prioridades admitidas: configMAX_PRIORITIES
  • Método de selección de tarea de parte del scheduler: configUSE_PORT_OPTIMISED_TASK_SELECTION. En 0 si se desea el método genérico, y 1 si se desea el metodo optimizado por arquitectura.

3.6 Time Measurement and the Tick Interrupt

  • Interrupción por tick en Hz: configTICK_RATE_HZ

3.8 The Idle Task and the Idle Task Hook

  • Prevenir que la tarea Idle consuma tiempo de procesamiento que podría estar alocada más productivamente en tareas de aplicación: configIDLE_SHOULD_YIELD
  • Utilización de función Idle Hook: configUSE_IDLE_HOOK en 1

3.9 Changing the Priority of a Task

  • Permitir el cambio de prioridad de una tarea: INCLUDE_vTaskPrioritySet en 1
  • Permitir la obtención de prioridad de una tarea: INCLUDE_uxTaskPriorityGet en 1

3.10 Deleting a Task

  • Permitir la eliminación de tareas: INCLUDE_vTaskDelete en 1

3.12 Scheduling Algorithms