Planificación de protothreads para bajo consumo.

ProtothreadsLas protohebras simplifican enormemente la programación orientada a eventos en sistemas empotrados con poca memoria. Utilizar dispositivos con poca RAM ayuda a disminuir el consumo.

Planificar las protohebras es sencillo si se va a programar en bare metal. Se pueden ejecutar de forma secuencial en un bucle infinito obteniendo una planificación round robin. Pero esto no es una buena idea en sistemas alimentados con baterías. Tener la CPU siempre activa consume mucha energía.

En esta entrada presento una modificación de las protohebras de Adam Dunkels para que estas indiquen el caso en el que todos los hilos estén interbloqueados esperando algún evento hardware. En este caso se puede poner la CPU en modo bajo consumo. También presento un sencillo planificador que utilice esta característica.

El código fuente de esta entrada se puede obtener en este enlace. Es a su vez un módulo de un proyecto de demostración que se puede obtener en este otro enlace y del que hablaré en una entrada posterior.

Protohebras

Las funciones protohebras retornan el estado de estas. En la siguiente sección de código se muestra la definición de los posibles estados. El nuevo estado que se ha incluido es PT_BLOCKED.

#define PT_BLOCKED  0 /**< It's waiting the same event than last time.  */
#define PT_WAITING  1 /**< It's waiting a new event from last time.     */
#define PT_YIELDED  2
#define PT_EXITED   3
#define PT_ENDED    4

Cuando una protohebra espera un evento devuelve al planificador PT_WAITTING. La siguiente vez que esta hebra sea invocada, si el evento que estaba esperan aun no ha ocurrido entonces devolverá PT_BLOCKED. En la siguiente sección de código se muestra una función protohebra a modo de ejemplo.

PT_THREAD( thread_funtion( struct pt* pt, void* conntext) ) {
  PT_BEGIN( pt );
  _eventA = _eventB = false;
  while( 1 ) {
    PT_WAIT_UNTIL( pt, _eventA == true );
    _eventA = false;
    PT_WAIT_UNTIL( pt, _eventB == true );
    _eventB = false;
  }
  PT_END( pt );
}

En el ejemplo de arriba la protohebra espera dos eventos en un bucle infinito. Si el primer evento no ha ocurrido cuando la función se invoca por primera vez devuelve PT_WAITTING. Cuando el planificador invoque de nuevo la función, al llegar su turno, si el primer evento aún no ha ocurrido la función devuelve PT_BLOCKED. Si sí ha ocurrido devolverá PT_WAITING, porque esperará por primera vez el segundo evento o si este ha ocurrido esperará nuevamente el primer evento.

Planificador

En la sección de código de abajo se muestra el interfaz del módulo del planificador.

En este interfaz también se define el tipo puntero a protohebra (thread_t). Es un puntero a función que devuelve el estado de la hebra (arriba definidos). Como argumentos tiene un puntero a la estructura protothread y un puntero a un posible contexto si se necesita que la hebra sea reentrante.


//------------------------------------------------------------------------------
/** @file scheduler.h
  * @brief Simple scheduler.
  * @date 04/01/2014.
  * @author Rafa García.                                                    */
//------------------------------------------------------------------------------

#ifndef _SCHEDULER_
#define _SCHEDULER_
//------------------------------------------------------------------------------

#include "protothreads/pt.h"
#include "protothreads/pt-sem.h"

//------------------------------------------------------------------------------
/** @brief Defines the pointer to protothread function.
  * When the scheduler invokes the protothread passes the following arguments:
  *  (1) Pointer to the protothread structure.
  *  (2) Pointer to the context.                                            */
typedef PT_THREAD( (*thread_t)( struct pt*, void* )  );

//------------------------------------------------------------------------------
/** @brief Creates a task from protothread.
  * @param thread: Pointer to the protothread function.
  * @return The task identifier.                                            */
int* scheduler_createNewTask( thread_t thread );

//------------------------------------------------------------------------------
/** @brief Executes all active tasks until all get blocked.                 */
void scheduler_run( void );

//------------------------------------------------------------------------------
/** @brief Sets the contexot of a previously created task.
  * @param id: The task identifier.
  * @param context: Pointer to context.                                     */
void scheduler_setContext( int* id, void* context );

//------------------------------------------------------------------------------
#endif

Para crear una hebra nueva se utiliza «scheduler_createNewTask». Como argumento se le pasa un puntero a la función protohebra. Si lo consigue devuelve el identificador de la hebra en forma de puntero a entero, si no lo consigue devuelve 0.

Cuando se crea una nueva hebra el planificador reserva 8 bytes para control, entre los cuales se encuentra la estructura pt y se inicializa.

Es posible crear varias hebras gemelas que controlen diferentes objetos o contextos. Para asignar contextos a las hebras se utiliza  «scheduler_setContext». Para ello se le pasa como argumentos el identificador de la hebra obtenido cuando se creó y el puntero formateado a void del contexto.

Para ejecutar todas las hebras se utiliza «scheduler_run». Esta función ejecuta todas las hebras mediante round robin hasta que todas las hebras devuelvan PT_BLOCKED, que entonces sale. Si una hebra devuelve PT_EXITED o PT_ENDED el planificador destruye la hebra liberando la memoria reservada.

En el siguiente bloque de código se muestra un ejemplo de uso:


#include "cpu.h"
#include "scheduler.h"
#include "tasks.h"

int array_A[10];
int array_B[10];

int main() {

  scheduler_createNewTask( task1 );
  int* id = scheduler_createNewTask( task2 );
  scheduler_setContext( id, (void*)array_A )
  id = scheduler_createNewTask( task2 );
  scheduler_setContext( id, (void*)array_B );

  while( 1 ) {
    scheduler_run();
    cpu_idle();
  }

  return 0;
}

En el ejemplo, al inicio se crean tres procesos. Dos de ellos utiliza la misma función reentrante. La diferencia entre estos dos procesos es que un maneja el array A y el otro el B.  Luego entra en un bucle infinito para ejecutar los procesos.

Como ya se ha dicho previamente, la función «scheduler_run» ejecuta todos los procesos mediante round robin hasta que todos queden bloqueados al mimo tiempo. En este momento ningún procesos puede producir ningún evento nuevo, no se pueden producir eventos software. Entonces sólo se pueden producir eventos hardware. Por eso se espera los eventos hardware con la CPU en modo bajo consumo.

Es importante configurar los eventos hardware que puedan bloquear los procesos para que sean capaces de sacar la CPU del modo de bajo consumo.

Memoria

En la versión actual este planificador utiliza memoria dinámica para crear procesos en tiempo de ejecución. Cada proceso requiere 8 bytes.

Como posible mejora se puede dar las opciones de reservar memoria estáticamente, dimensionar esta memoria y deshabilitar opciones como destruir procesos o He estado asignar contextos para disminuir los 8 bytes requeridos por cada proceso.

Si sólo el planificador utiliza memoria dinámica. El heap nunca se va a fragmentar porque sólo se reserva bloques de un tamaño dado.

Deja un comentario