Creación de módulos e interfaces en nesC para TinyOS 2

embeddedArco

Después de pelearme con tinyOS he conseguido comprender mas o menos como va el tema de los módulos y wiring (conexionado lógico entre módulos). Aquí dejo un ejemplo muy básico y la explicación que ilustra lo suficiente para poder comenzar a hacer cosillas.

Introducción

tinyOS se basa en el uso de módulos que se "cablean" (wiring) entre sí para dar lugar a la aplicación en sí. Este cableado se lleva a cabo en lo que se conoce como configuraciones y podemos tener montones de configuraciones distintas para una misma aplicación. De este modo, podemos conseguir que una misma aplicación pueda correr en distintas arquitecturas hardware sin apenas tocar código, solo modificando las configuraciones, lo que hará que se utilicen unos módulos u otros que se encargan del control a mas bajo nivel.

Básicamente, aquí hablaré de:

  • Módulos
  • Interfaces
  • Configuraciones

Todo el código de ejemplo que se usa en esta receta puede descargarse, vía subversión, del repositorio público de ARCO de la siguiente forma:

javieralso@richie:~$ svn co https://arco.inf-cr.uclm.es/svn/public/prj/tinyOS

y con ésto ya podremos seguir todos los ejemplos dados a continuación.

Interfaces

Las interfaces se pueden definir como la parte "visible" de los módulos. No se puede acceder a ninguna función de un módulo si antes no ha sido definida en una interfaz.
Una interfaz puede ser provista por varios módulos distintos. Por ejemplo, podemos tener una interfaz de comunicaciones que abstraiga los procedimientos de envío y recepción de mensajes y varios módulos que hagan uso de esa interfaz, uno para cada tipo de hardware subyacente (por ejemplo no se maneja igual el hardware de comunicaciones de una mica2 que el de una telos). Los métodos serán los mismos y se llamarán igual (la interfaz es la misma), pero la implementación interna puede ser totalmente distinta. Nosotros lo veremos como una caja negra.

En nuestro ejemplo se utilizan dos interfaces: interfaz2, en el archivo interfaz2.nc e interfazPRB en el archivo interfazPRB.nc. Las implementaciones son las que siguen:

interface interfaz2 {
  command void i2c1();
  command uint16_t i2c2(int c);
  command uint16_t i2c3(char d);
}

para interfaz2.nc y

interface interfazPRB {
  command void comando1();
  command int comando2(int c);
}

para interfazPRB.nc

Bueno. Se puedes ver, escribir una un interfaz es bastante simple. Lo único que hay que hacer es especificar el nombre de la y los comandos de que consta. Estos comandos son los prototipos (como se llama en ANSI C) de las funciones precedidos de la palabra reservada command. Si quisiesesmos declarar señales, utilizariamos el modificador signal. El nombre del archivo debe de ser el mismo que el del interfaz, respetando las mayúsculas y con extensión .nc.

Módulos

Los módulos implementan bloques funcionales generalmente de un nivel de abstracción más bajo que el bloque que los referencia. Un mismo módulo puede proveer varias interfaces y por lo tanto puede implementar distintas funcionalidades (Por ejemplo el módulo AMSender provee las interfaces Packet, AMPacket, AMSend, etc...). Lo lógico es que todos los conjuntos de funciones (interfaces) que implementa un módulo guarden algún tipo de relación.

En nuestro caso, utilizamos dos módulos: modulo2C, implementado en modulo2C.nc y moduloC implementado en moduloC.nc.
Éste es el contenido de los archivos:

module modulo2C {
  provides {
    interface interfaz2;
    /*
      Única interfaz proporcionada por el módulo 
    */
  }
}
 
implementation
{
  command void interfaz2.i2c1() {
    int v1 = 5;
    int v2;
    v2 = v1;
  }
 
  command uint16_t interfaz2.i2c2(int c) {
    return (uint16_t)2*c;
  }
 
  command uint16_t interfaz2.i2c3(char d) {
    return (uint16_t)d*d*d;
  }
}

para modulo2C y

module moduloC {
  provides interface interfazPRB;
  uses {
    interface interfaz2;
    interface Leds;
  }
}
 
implementation {
  command void interfazPRB.comando1() {
    call Leds.led0Toggle();
    call Leds.led1Toggle();
    call Leds.led2Toggle();
  }
 
  command int interfazPRB.comando2(int c) {
    call Leds.set((uint8_t)c);
    return 0;
  }
}

Cada uno de los módulos se compone de dos partes. La primera es la declaración. En ella se especifica el nombre del módulo, así como las interfaces que provee y las interfaces que usa. En el caso de moduloC, por ejemplo, podemos ver que la interfaz que proporciona es la interfaz interfacePRB y utiliza las interfaces interfaz2 y Leds.
A continuación sigue la implementación. Aquí debe hacerse la implementación de cada uno de los comandos especificados en la interfaz. En éste caso no hay señales. Si las hubiese, la forma de lanzarlas es llamarlas como funciones normales (precedidas de la palabra reservada signal) cuando sea necesario. En éste caso, como se trata solo de un ejemplo, las funciones son totalmente triviales.

No queda mucho mas que comentar acerca de los módulos. Básicamente, después de ésto solo faltaría escribir la configuración con el wiring entre módulos e interfaces. ¿Y qué es eso?, pues ahora mismo lo explico.

Configuraciones

Como se ha dicho anteriormente, una aplicación escrita para tinyOS se compone de implementación de módulos y de configuraciones. En algunos casos, pueden hacerse aplicaciones solo con archivos de configuraciones. Un archivo de configuración consiste básicamente en un archivo formado por los módulos (o componentes) a utilizar en la aplicación y la relación que hay entre dichos módulos y las interfaces utilizadas por otros módulos.

En un principio esto puede parecer bastante lioso, así que nada mejor que verlo con algún ejemplo. Veamos primero el archivo de configuración moduloEjm.nc:

configuration moduloEjmC {
  provides interface interfazPRB;
}
 
implementation  {
 
  components LedsC;
  components modulo2C;
  components moduloC;
 
  moduloC.Leds -> LedsC;
  moduloC.interfaz2 -> modulo2C;
 
  interfazPRB = moduloC;
}

Paso a paso para no liarnos:

Lo primero que tenemos que hacer es indicar que estamos hablando de una configuración, en éste caso moduloEjmC. Como siempre, dicho nombre coincide con el del archivo en el que se aloja. Ésta configuración, provee una interfaz (aunque podrian ser mas), la interfaz interfazPRB.
En la parte de implementación vemos que utilizamos tres módulos o componentes: LedsC, modulo2C y moduloC. Éstos son los módulos cuya funcionalidad necesitaremos para llevar a cabo las tareas de la configuración en la que estamos trabajando.

Después nos encontramos con la parte de wiring. Aquí tenemos dos conexiones:

  • moduloC.Leds -> LedsC;
  • moduloC.interfaz2 -> modulo2C;

¿Qué hace ésto? fácil, hemos conectado la interfaz Leds utilizada en la implementación del módulo moduloC con el componente (o módulo) LedsC que provee dicha interfaz y que hemos declarado anteriormente en la sección components. Con ésto, cada vez que invoquemos a alguno de los comando de ésta interfaz, lo que en realidad estaremos haciendo será llamar a los métodos con dicho nombre que haya implementados en el módulo LedsC. Si tuviésemos otro módulo con la misma interfaz, podriamos haberlo utilizado indistintamente, con la única diferencia de la implementación que esconda detrás, claro.

La segunda conexión ya queda algo mas clara, conecta el interfaz interfaz2 de moduloC con el módulo modulo2C que provee dicha interfaz y que nosotros mismos escribimos.

Finalmente, en la última línea, podemos ver una asignación distinta a todas las vistas anteriormente. Ésta asignación lo único que hace es conectar la interfaz que se provee (interfazPRB) con el módulo que proveerá la funcionalidad a dicha interfaz, que en éste caso es moduloC.

Ahora, lo único que falta es utilizar todo ésto:

configuration ejmAppC{}
 
implementation
{
  components MainC, ejmC;
  components moduloEjmC;
 
  ejmC -> MainC.Boot;
  ejmC.interfazPRB -> moduloEjmC;
}

Aquí tenemos el contenido de ejmAppC.nc, que tiene la configuración ejmAppC.

Esta configuración podemos ver que no provee interfaz alguna, pero que sí usa componentes, en concreto MainC, ejmC y moduloEjmC. Los dos últimos son familiares Eye-wink

Después tenemos los wirings: uno que conecta la interfaz Boot del módulo MainC (utilizado para informar de que tinyOS ha arrancado y demás) con el módulo de mayor nivel de nuestra aplicación (la aplicación en si), ejmC y otro que conecta la interfaz interfazPRB que utiliza la aplicación, con el módulo (aunque lo hayamos "implementado" en un archivo de configuración, lo estamos tratando como un módulo, ya que esa configuración corresponde a la de un módulo) moduloEjmC.

Con ésto, ya solo nos falta implementar las funciones de nuestro programita propiamente dicho:

module ejmC {
  uses interface Boot;
  uses interface interfazPRB;
}
 
implementation {
  event void Boot.booted() {
    call interfazPrb.comando1();
  }
}

Aquí puedes ver cómo se define un módulo llamado ejmC y que utiliza las interfaces Boot e interfazPRB que ya conectamos anteriormente a cada uno de los módulos que queríamos que nos diesen la funcionalidad.

A continuación, en la parte de la implementación tan solo tenemos que implementar las funciones y comandos que necesitemos para dar la funcionalidad que queramos a nuestro programita.

¿Qué por qué este es el módulo de mayor nivel? Bueno, por nada en especial. Simplemente que implementa la interfaz Boot del módulo MainC, quien lanzará el evento Boot.booted cuando tinyOS termine de arrancar y por tanto entregará el control del hilo principal a dicho evento, comenzando la ejecución del programa por ahí.

Referencias