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.

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
Clone this wiki locally