Eventos

Los sistemas de tiempo real realizan acciones acorde con eventos ocurridos. Poir ejemplo, ha llegado un dato del servidor o ha detectado un cambio de temperatura.

Para que ningún evento se pierda, se suele usar la suguiente estrategia:

  • Las interrupciones detectan el evento.
  • Las rutinas de las interrupciones son lo más cortas posibles con el objeto de no perder otro evento.

Se suele usar ciertas estrategias para detectar eventos:

  • ¿Puede detectarse por interrupción o por muestreo?
  • ¿Cuánto código necesita la interrupción? siempre es recomendable usar el mínimo posible
  • ¿Cómo se tiene que comunicar el evento con el código principal? ¿este código está preparado para procesar eventos asíncronos?

FreeRTOS no tiene implementada ninguna estrategia, pero permite realizar cualquiera de ellas de una manera sencilla.

Las funciones y macros que terminen en "FromISR" o "From_ISR" son las que se usan para manejar interrupciones.

Procesado de interrupciones en diferido

Uso de semáforos binarios para sincronización

Podemos usar un semáforo para desbloquear una tarea una vez haya ocurrido una interrupción, logrando una sincronización entre la tarea y la interrupción. Con este método, logramos que el código de la interrupción sea el mínimo posible. Este método de procesado de interrupciones se llama en diferido.

Si el procesado de interrupciones es crítico, se puede modificar la prioridad de la tarea para asegurar que se ejecuta en su debido momento.

Podemos implementar la interrupción para que, al terminar, salte directamente a la tarea apropiada. Con esto nos aseguramos que el evento se procesa continuo en el tiempo.

La tarea asociada usa la llamada al semáforo "take" que bloquea dicha tarea. Cuando ocurre el evento, la interrupción usa un "give" en el mismo semáforo para desbloquear la tarea.

Tomar un semáforo y dar un semáforo son conceptos con distintos significados, dependiendo del escenario. En la terminología clásica, tomar un semáforo es equivalente a P() y dar es V().

En sincronización de interrupciones, el semáforo se puede considerar conceptualmente como un cola de longitud uno. La cola puede contener el máximo de un ítem a la vez, por lo que siempre está vacía o llena.

Al llamar a xSemaphoreTake(), la tarea asociada intenta leer la cola con un tiempo de bloqueo, provocando que dicha tarea entre en estado bloqueado si está vacía.

Cuando ocurre un evento, la interrupción usa la función xSemaphoreGiveFromISR() para colocar un token en la cola, logrando que se llene.

Esto provoca que la tarea salga del estado bloqueado y elimine el token, limpiando la cola otra vez.

Cuando la tarea completa el proceso, esta intenta volver a leer la cola y, al encontrarla vacía, vuelve al estado bloqueado.

Función vSemaphoreCreateBinary()

Los enlaces a todos lo tipos de semáforos admitidos en FreeRTOS se almacenan en el tipo de variable xSemaphoreHandle.

Antes de poder usar un semáforo hay que crearlo. Para ello se usa la función:

void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore);

  • xSemaphore El semáforo que se va a crear.

Hay que saber que la función vSemaphoreCreateBinary() realmente es un macro, por lo que el semáforo se crea directamente.

Función xSemaphoreTake()

Tomar un semáforo significa obtener o recibir el semáforo. Sólo se puede tomar si está disponible.

Salvo los semáforos recursivos se pueden tomar usando la función xSemaphoreTake().

Esta función no se puede usar en una rutina de interrupción.

portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait);

  • xSemaphore el semáforo que se va a tomar. Tiene que haberse creado antes de poder tomarlo.
  • xTicksToWait El tiempo máximo que la tarea va a permanecer en estado bloqueado si el semáforo no está disponible.
    Si es 0, la función vuelve inmediatamente aunque no tenga un dato.
    El tiempo de bloqueo se especifica en ticks. Para convertir a milisegundos se puede usar porTICK_RATE_MS.
    Si ponemos este valor a porMAX_DELAY provoca que la tarea espere indefinidamente si el valor de INCLUDE_vTaskSuspend es 1 en FreeRTOSConfig.h.

Return Value:

  1.     pdPASS. devuelve este valor si se ha obtenido el semáforo.
  2.     pdFALSE. El semáforo no está disponible.
        Si se ha especificado un tiempo de bloqueo, es posible que el semáforo haya devuelto un valor y la tarea siga bloqueada hasta que termine su periodo.

Función xSemaphoreGiveFromISR()

Excepto los semáforos recursivos pueden "dar" usando la función xSemaphoreGiveFromISR().

xSemaphoreGiveFromISR() es una forma especial de xSemaphoreGive() pero usado para interrupciones.

portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken);

  • xSemaphore: el semáforo que se va a dar. Tiene que haberse creado antes de poder tomarlo.
  • pxHigherPriorityTaskWoken Es posible que un sólo semáforo tenga una o varias tareas bloqueadas esperando a que esté disponible. Llamando esta función, se pueden liberar todas las tareas que dependan de dicho semáforo.
    Si al invocar esta función, la tarea tiene una prioridad mayor o igual a la prioridad de la tarea que está en ejecución, el puntero *pxHigherPriorityTaskWoken se pondrá a pdTRUE y se ejecutará la interrupción de mayor prioridad.

Return Value:

  1.     pdPASS. devuelve este valor si la llamada a la función es correcta.
  2.     pdFAIL. Si el semáforo ya está disponible y no se puede "dar", la función devolverá pdFAIL.

Contando Semáforos

La rutina de los semáforos explicada hasta ahora, es la siguiente:

  1. Ocurre una interrupción
  2. Se ejecuta la rutina de la interrupción "dando" un semáforo para desbloquear una o varias tareas.
  3. La tarea se ejecuta en cuanto termine la interrupción. Lo primero que hace es "coger" el semáforo.
  4. La tarea se procesa y se vuelve a bloquear hasta que el semáforo se vuelva a ejecutar.

Esta secuencia es perfecta para interrupciones que se ejecuten con poca frecuencia.

Si entra otra interrupción durante el procesado de la tarea, el semáforo binario congela la interrupción provocando que, nada más terminar la tarea, vuelva otra vez a la misma.

Esto ocurre porque el semáforo binario puede lachear una interrupción y se perderá cualquier evento ocurrido desde el momento del latch. Para evitar todo esto, se cuentan semáforos.

El hecho de contar semáforos lo podemos entender como una cola que tiene una longitud de uno. A las tareas no les importa qué datos tiene la cola, sino sólo si están vacías o no.
Cada vez que se "da" un semáforo, se usa otro espacio en la cola. El número de items en la cola está definido por "count".

Para poder usar la cuenta de semáforos, hay que poner a uno configUSE_COUNTING_SEMAPHORES en el archivo FreeRTOSConfig.h.

Este tipo de semáforos se utilizan para:

  1. Contar eventos: cada vez que ocurre un evento provoca un incremento al "dar" el semáforo. Cada vez que una tarea "coge" el semáforo, decrementa. Así, podemos calcular la diferencia entre los eventos ocurridos y los procesados.
  2. Administración de recursos: la cuenta indica el número de recursos disponibles. Para controlar los recursos, una tarea obtien un semáforo, decrementando la cuenta. Al llegar a cero, no hay recursos disponibles, la tarea "dará" el semáforo incrementando la cuenta.
    En este caso, los semáforos se crean inicializándolos con el valor máximo de los recursos disponibles.

Función xSemaphoreCreateCounting()

Los enlaces a todos lo tipos de semáforos admitidos en FreeRTOS se almacenan en el tipo de variable xSemaphoreHandle.

Antes de poder usar un semáforo hay que crearlo. Para ello se usa la función:

xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount. unsigned portBASE_TYPE uxInitialCount);

  • uxMaxCount el valor máximo de la cuenta, es el equivalente a la longitud de la cola.
  • uxInitialCount la inicialización de la cuenta.

Return Value:

  1.     NULL: si el semáforo no se ha creado.
  2.     Distinto de NULL: si el semáforo se ha creado.

Uso de colas con el servicio de interrupciones

xQueueSendToFrontFromISR(), xQueueSendToBackFromISR() y xQueueSendReceiveFromISR() son versiones de xQueueSendToFront(), xQueueSendToBack() y xQueueSendReceive() respectivamente.

Los semáforos se utilizan para comunicar eventos, las colas para lo mismo y, además, transferir datos.

Las funciones xQueueSendToBackFromISR() y xQueueSendToFrontFromISR()

xQueueSendToBackFromISR() se usa para mandar el dato al final de la cola y xQueueSendToFrontFromISR() para mandarlo al principio de la cola.

portBASE_TYPE xQueueSendToFrontFromISR ( xQueueHandle xQueue, const void * pvItemToQueue, portBASE_TYPE *pxHigherPriorityTaskWoken);

portBASE_TYPE xQueueSendToBackFromISR ( xQueueHandle xQueue, const void * pvItemToQueue, portBASE_TYPE *pxHigherPriorityTaskWoken);

  •     xQueue el enlace de la cola. Es el valor que devuelve xQueueCreate().
  •     pvItemToQueue puntero al dato que vamos a copiar.
  •     pxHigherPriorityTaskWoken Es probable que una cola bloquee una o varias tareas hasta que tenga datos disponibles. Llamando estas funciones, podemos obtener datos disponibles y así desbloquear tareas. Si la tarea que se desbloquea tiene una prioridad mayor que la que se está ejecutando, este valor se pone a pdTRUE.
        Si el valor cambia a pdTRUE, al finalizar la interrupción saltará a la tarea con mayor prioridad.

Return Value:

  1. pdPASS. Si el dato se ha mandado satisfactoriamente a la cola.
    Puede ocurrir que si se ha seleccionado un tiempo de bloqueo (xTicksToWait es disinto de cero), los datos se hayan almacenado antes de que termine el temporizador.
  2. errQUEUE_FULL. Si el dato no se ha podido almacenar porque la cola está llena.
    Puede ocurrir que si se ha seleccionado un tiempo de bloqueo (xTicksToWait es disinto de cero), los datos se hayan almacenado antes de que termine el temporizador.

Uso eficiente de Colas

Poner el dato recibido en un buffer de RAM, al desbloquear el semáforo, la tarea podrá recoger todos los datos de la RAM.

Interpretar el dato recibido en la rutina de la interrupción. Sólo es recomendable si el proceso es muy corto.

Anidación de interrupciones

Para ello se necesita las constantes definidas en FreeRTOSConfig.h:

  • configKERNEL_INTERRUPT_PRIORITY Activa la prioridad de la interrupción con el tick de la misma.
    si el puerto no usa configMAX_SYSCALL_INTERRUPT_PRIORITY, ninguna interrupción que ustilice las funciones de interrupción segura se ejecutarán con dicha prioridad.
  • configMAX_SYSCALL_INTERRUPT_PRIORITY indica la prioridad máxima para una interrupción segura.

Si definimos la constante configMAX_SYSCALL_INTERRUPT_PRIORITY con una prioridad mayor que la de configKERNEL_INTERRUPT_PRIORITY, podemos anidar interrupciones.

Si, por ejemplo, definimos configMAX_SYSCALL_INTERRUPT_PRIORITY a 3 y configKERNEL_INTERRUPT_PRIORITY a 1 y la arquitectura el microcontrolador permite 7 niveles de prioridad en la interrupción:

  • Las interrupciones con prioridad de 1 a 3 incluidas, no se ejecutan cuando el kernel o la aplicación está en una zona crítica, y se pueden usar con las funciones interrupt-safe.
  • Las interrupciones con prioridad 4 o mayor, se ejecutan normalmente, con las limitaciones del microcontrolador. Normalmente, las funciones que requieren un tiempo específico, usan prioridad mayor que configMAX_SYSCALL_INTERRUPT_PRIORITY, para asegurar que no exista un jitter provocado por el scheduler.
  • Las interrupciones que no realizan llamadas a la API del FreeRTOS se ejecutan normalmente.

Nota para los ARM Cortex M3

En los cortex M3 la prioridad está definida al contrario, el valor bajo indica priordad alta. No hay que dejar nunca la prioridad a cero (valor pro defecto) u otro valor bajo, porque tendrá más prioridad que el sistema y puede provocar un fallo en sistema si la prioridad es menor que configMAX_SYSCALL_INTERRUPT_PRIORITY.

Los cortex M3 tienen 8 bit para niveles de interrupción, con lo que se consiguen 255 valores posibles. Esto depende del fabricante, por ejemplo, los STM32 sólo disponen de 4 bits.