Comme tous les domaines de l'informatique, le domaine du temps réel n’est pas épargné par le besoin de développer de plus en plus vite des systèmes de plus en plus complexes. Traditionnellement, l’utilisation de langages de bas niveau était de rigueur dans le développement de tels systèmes afin de garantir un contrôle total de leur comportement. Cependant, au fil des années, cette complexité accrue, résultant de l’évolution des besoins et l’arrivée de nouvelles technologies, a rendu le développement de ces systèmes beaucoup plus difficile. Cette difficulté à gérer efficacement la complexité, freine l’évolution des systèmes et introduit un nombre important d’erreurs dans leur conception et leur réalisation, entraînant ainsi un coût de production beaucoup plus conséquent et un temps de mise en oeuvre beaucoup plus long. Vient alors le besoin d’introduire de nouvelles méthodologies, qui faciliteraient la conception et la gestion des systèmes temps réel. Ce besoin de nouvelles méthodologies vient surtout du besoin de capitaliser le logiciel afin d’améliorer sa productivité. Cependant, une capitalisation efficace du logiciel requiert une méthodologie reposant notamment sur « l’augmentation du niveau d’abstraction » afin d’alléger la complexité logicielle et sur « la réutilisation du logiciel » pour faciliter et accélérer le développement et, aspect plus important dans le domaine du temps réel, pour faciliter la certification des logiciels. De plus, l’optique d’une réutilisation logicielle dans ce domaine demande une caractérisation claire de ce qu’est un « élément réutilisable » et aussi des moyens pour vérifier que cet élément satisfait les exigences du système. L'approche composant est la méthodologie de choix pour répondre à ces besoins, car elle apporte de moyens de structuration efficaces permettant d'avoir une plus grande compréhension, réutilisation et fiabilité du logiciel.
TinyOS est un système d’exploitation open-source conçu pour des réseaux de capteur sans fil. Il respecte une architecture basée sur une association de composants, réduisant la taille du code nécessaire à sa mise en place. Cela s’inscrit dans le respect des contraintes de mémoires qu’observent les réseaux de capteurs.
Les réseaux de capteurs sans fil (RCSF) sont une nouvelle technologie qui a surgi après les grands progrès technologiques concernant le développement des capteurs intelligents, des processeurs puissants et des protocoles de communication sans fil, leurs composants de base. Ce type de réseau, composé de centaines ou de milliers d’éléments, a pour but la collecte de données de l’environnement, leur traitement et leur dissémination vers le monde extérieur. Les éléments de réseau, nommés noeuds capteurs, ont de petites dimensions et de sévères contraintes de ressources, notamment énergie, traitement et communication. On s’attend à ce que les RCSF soient intelligents, autonomes et connaisseurs du contexte où ils s’insèrent. Afin d’atteindre cet objectif, ils doivent s’autogérer.
Les réseaux de capteurs sans fil sont constitués de noeuds disposant de peu d’énergie avec des capacités limitées de traitement et de transmission. Les principaux travaux de recherche dans ce domaine portent sur la prolongation de la durée de vie du réseau par une réduction de la consommation d’énergie dans son exploitation. Divers mécanismes d’agrégation de données, comme le groupage, ont montré que l’on pouvait faire d’importantes économies d’énergie.
Les évolutions techniques des microsystèmes électromécaniques ( MEMS ) et des communications sans fil permettent la réalisation de réseaux de capteurs sans fil comportant un très grand nombre de noeuds capteurs à bas coût. Chacun de ces petits noeuds communique à courte distance et collectivement l’ensemble des noeuds réalise une application pour laquelle des protocoles de communication efficaces sont nécessaires. Nombreuses sont les applications envisagées qui traitent de l’audio ou de la vidéo et qui impliquent donc de relever les défis propres au multimédia en matière de fiabilité et de contraintes de temps notamment.
Les réseaux ad hoc, en général, se distinguent des autres réseaux mobiles par la propriété d'absence d'infrastructure préexistante et de tout genre d'administration centralisée. Les hôtes mobiles sont responsables d'établir et de maintenir la connectivité du réseau d'une manière continue.
Des réseaux de 10000 noeuds peuvent être envisagés.
Dans plusieurs applications, les noeuds de capteurs sont placés dans des surfaces distantes, le service du noeud peut ne pas être possible, dans ce cas la durée de vie du noeud peut être déterminée par la vie de la batterie, ce qui exige la minimisation des dépenses énergétiques.
Les capteurs peuvent être attachés à des objets mobiles qui se déplacent d'une façon libre et arbitraire rendant ainsi, la topologie du réseau fréquemment changeante.
Ceci peut être nécessaire dans plusieurs cas. Par exemple, un réseau comportant un grand nombre de noeuds placés dans des endroits hostiles où la configuration manuelle n’est pas faisable, doit être capable de s’auto organiser. Un autre cas est celui où un noeud est inséré ou retiré (à cause d’un manque d’énergie ou de destruction physique), ainsi le réseau doit être capable de se reconfigurer pour continuer sa fonction.
Les RCSF mobiles sont plus touchés par le paramètre de sécurité que les réseaux filaires classiques. Cela se justifie par les contraintes et limitations physiques qui font que le contrôle des données transférées doit être minimisé.
Les RCSF mobiles peuvent être constitués de différents types de capteurs capables de surveiller une variété de paramètres, tels que, la température, l’humidité, la pression, le mouvement des véhicules, le niveau de bruit, la présence ou l’absence d'objets, etc.
L’évolution de la structure d'un ouvrage d'art, la gestion de la température et de la lumière dans un immeuble, la domotique, les interrupteurs autonomes non câblés, etc. constituent quelques exemples d’applications dans le domaine du bâtiment.
La gestion du trafic, la déformation de structure, les capteurs de pression des pneus, etc. en sont quelques exemples.
Dans le domaine de l’environnement, nous pouvons citer : la détection de polluants dans l'air ou le sol, le suivi des mouvements des oiseaux, des animaux et des insectes, la détection des incendies, la détection du niveau d'eau dans le sol, etc.
Le domaine médical peut lui aussi intégrer des applications pertinentes. Comme par exemple : l’aide à la médication et le suivi des patients à distance (rythme cardiaque, pression du sang, etc), l’identification des allergies et des médicaments administrés aux patients, la localisation des docteurs et des patients dans l'hôpital, etc.
Le domaine militaire ne sera pas épargné non plus. Il pourra utiliser les RCSF par exemple dans la détection et la collecte d’informations sur la position de l’ennemi et ses mouvements, la détection d'agents chimiques ou bactériologiques, etc.
La conception des RCSF, leurs protocoles et algorithmes sont guidés par plusieurs facteurs:
Le réseau doit être capable de maintenir ses fonctionnalités sans interruption en cas de défaillance d'un de ses capteurs. Cette défaillance peut être causée par une perte d'énergie, par un dommage physique ou par interférence de l'environnement. Le degré de tolérance dépend du degré de criticité de l'application et des données échangées.
Une des caractéristiques des RCSF est qu'ils peuvent contenir des centaines voir des milliers de noeuds capteurs. Le réseau doit être capable de fonctionner avec ce nombre de capteurs tout en permettant l'augmentation de ce nombre et la concentration (densité) des noeuds dans une région.
Le coût des RCSF, qui sont constitués d'un grand nombre de noeuds, dépend de celui d'un seul noeud qui ne doit pas, par conséquent, être cher.
Un noeud capteur est constitué des composantes principales suivantes :
Unité de détection :est composée de deux sous unités, capteurs (sensors) et ADC (Analog to Digital Converters) qui permet de convertir le signal produit par le capteur, sur la base du phénomène observé, en un signal digital qui est par la suite fournit à l'unité de calcul ;
Unité de calcul :gère les procédures permettant au noeud de collaborer avec les autres noeuds pour accomplir la requête assignée ;
Transceiver unit:connecte le noeud au réseau ;
Unité d'énergie :est une composante importante du noeud capteur ;
Location finding system:fournit des informations sur la localisation requise par les techniques de routage ;
Mobilizer:est nécessaire si le noeud capteur doit être déplacé pour accomplir la requête assignée.
Ces composantes nécessitent d'être regroupées dans un module convenable avec la contrainte de la taille qui ne doit pas être très petite, et ne dépasse pas quelques centimètres.
Les RCSF connaissent actuellement une grande extension et une large utilisation dans différents types d'applications, dont celles exigeant une grande sécurité. Nous disposons des capacités mémoire, énergétique et de calcul très limitées. Ces contraintes font que l'application des mesures classiques de sécurité est restreinte.
Les réseaux de capteurs sans fil possèdent certaines vulnérabilités:
La première vulnérabilité est liée à la technologie sans fil sous jacente. Quiconque possédant le récepteur adéquat peut potentiellement écouter ou perturber les messages échangés ;
Les noeuds eux-mêmes sont des points de vulnérabilité du réseau car une attaque peut compromettre un composant laissé sans surveillance ;
L'absence d'infrastructure fixe pénalise l'ensemble du réseau dans la mesure où il faut faire abstraction de toute entité centrale de gestion pour l'accès aux ressources ;
Les mécanismes de routage sont d'autant plus critiques dans les réseaux RCSF que chaque noeud participe à l'acheminement des paquets à travers le réseau. De plus les messages de routage transitent sur les ondes radio.
TinyOS est un système principalement développé et soutenu par l’université américaine de Berkeley, qui le propose en téléchargement sous la licence BSD et en assure le suivi. Ainsi, l’ensemble des sources sont disponibles pour de nombreuses cibles matérielles.
TinyOS est un système d’exploitation open-source conçu pour des réseaux de capteur sans fil. Il respecte une architecture basée sur une association de composants, réduisant la taille du code nécessaire à sa mise en place. Cela s’inscrit dans le respect des contraintes de mémoires qu’observent les réseaux de capteurs.
Pour autant, la bibliothèque de composant de TinyOS est particulièrement complète puisqu’on y retrouve des protocoles réseaux, des pilotes de capteurs et des outils d’acquisition de données. L’ensemble de ces composants peuvent être utilisés tels quels, ils peuvent aussi être adaptés à une application précise.
En s’appuyant sur un fonctionnement événementiel, TinyOS propose à l’utilisateur une gestion très précise de la consommation du capteur et permet de mieux s’adapter à la nature aléatoire de la communication sans fil entre interfaces physiques.
Le fonctionnement d’un système basé sur TinyOS s’appuie sur la gestion des évènements. Ainsi, l’activation de tâches, leur interruption ou encore la mise en veille du capteur s’effectue à l’apparition d’évènements, ceux-ci ayant la plus forte priorité. Ce fonctionnement évènementiel (event-driven) s’oppose au fonctionnement dit temporel (time-driven) où les actions du système sont gérées par une horloge donnée.
TinyOS a été programmé en langage NesC [23]. Le caractère préemptif d’un système d’exploitation précise si celui-ci permet l’interruption d’une tâche en cours. TinyOS ne gère pas ce mécanisme de préemption entre les tâches mais donne la priorité aux interruptions matérielles. Ainsi, les tâches entre-elles ne s’interrompent pas mais une interruption peut stopper l’exécution d’une tâche.
Lorsqu’un système est dit « temps réel » celui-ci gère des tâches caractérisées par des priorités et par des échéances à respecter dictées par l’environnement externe. Dans le cas d’un système strict, aucune échéance ne tolère de dépassement contrairement à un système temps réel mou. TinyOS se situe au-delà de ce second type car il n’est pas prévu pour avoir un fonctionnement temps réel. TinyOS a été conçu pour réduire au maximum la consommation en énergie du capteur. Ainsi, lorsque aucune tâche n’est active, il se met automatiquement en veille.
Il est important de préciser de quelle façon un système d’exploitation aborde la gestion de la mémoire. C’est encore plus significatif lorsque ce système travaille dans un espace restreint.
TinyOS a une empreinte mémoire très faible puisqu’il ne prend que 300 à 400 octets dans le cadre d’une distribution minimale. En plus de cela, il est nécessaire d’avoir 4 Ko de mémoire libre qui se répartit suivant le schéma ci-contre.
La pile : sert de mémoire temporaire au fonctionnement du système notamment pour l’empilement et le dépilement des variables locales.
Les variables globales : réservent un espace mémoire pour le stockage de valeurs pouvant être accessible depuis des applications différentes.
La mémoire libre : pour le reste du stockage temporaire.
La gestion de la mémoire possède de plus quelques propriétés. Ainsi, il n’y a pas d’allocation dynamique de mémoire et pas de pointeurs de fonctions. Bien sur cela simplifie grandement l’implémentation. Par ailleurs, il n’existe pas de mécanisme de protection de la mémoire sous TinyOS ce qui rend le système particulièrement vulnérable aux crashs et corruptions de la mémoire.
Le choix d’un ordonnanceur déterminera le fonctionnement global du système et le dotera de propriétés précises telles que la capacité à fonctionner en temps réel.
L’ordonnanceur TinyOS c’est :
2 niveaux de priorité (bas pour les tâches, haut pour les évènements)
1 file d’attente FIFO (disposant d’une capacité de 7)
Par ailleurs, entre les tâches, un niveau de priorité est défini permettant de classer les tâches, tout en respectant la priorité des interruptions (ou évènements). Lors de l’arrivée d’une nouvelle tâche, celle-ci sera placée dans la file d’attente en fonction de sa priorité (plus elle est grande, plus le placement est proche de la sortie). Dans le cas ou la file d’attente est pleine, la tâche dont la priorité est la plus faible est enlevée de la FIFO.
TinyOS est prévu pour fonctionner sur une multitude de plates-formes, disponibles dès l’installation. En effet, TinyOS peut être installé à partir d’un environnement Windows (2000 et XP) ou bien GNU/Linux (Red Hat essentiellement, mais d’autres distributions sont également possibles). Deux principales versions de TinyOS sont disponibles : la version stable ( v. 1.1.0 ) et la version actuellement en cours de tests ( v. 1.1.15 ).
On appelle généralement Mote la carte physique utilisant TinyOS pour fonctionner. Celle-ci a pour coeur le bloc constitué du processeur et des mémoires RAM et Flash. Cet ensemble est à la base du calcul binaire et du stockage, à la fois temporaire pour les données et définitif pour le système TinyOS.
TinyOS est prévu pour mettre en place des réseaux sans fils, les équipements étudiés sont donc généralement équipés d’une radio ainsi que d’une antenne afin de se connecter à la couche physique que constituent les émissions hertziennes.
TinyOS est prévu pour mettre en place des réseaux de capteurs, on retrouve donc des équipements bardés de différents types de détecteurs et autres entrées.
Comme tout dispositif embarqué, ceux utilisant TinyOS sont pourvus d’une alimentation autonome telle qu’une batterie.
Le système d’exploitation TinyOS s’appuie sur le langage NesC. Celui-ci propose une architecture basée sur des composants, permettant de réduire considérablement la taille mémoire du système et de ses applications. Chaque composant correspond à un élément matériel (LEDs, timer, ADC …) et peut être réutilisé dans différentes applications. Ces applications sont des ensembles de composants associés dans un but précis. Les composants peuvent être des concepts abstraits ou bien des interfaces logicielles aux entrées-sorties matérielles de la cible étudiée ( carte ou dispositif électronique ). L’implémentation de composants s’effectue en déclarant des tâches, des commandes ou des évènements.
Les tâches sont utilisées pour effectuer la plupart des blocs d’instructions d’une application. A l’appel d’une tâche, celle-ci va prendre place dans une file d’attente de type FIFO (First In First Out) pour y être exécutée. Comme nous l’avons vu, il n’y a pas de mécanisme de préemption entre les tâches et une tâche activée s’exécute en entier. Par ailleurs, lorsque la file d’attente des tâches est vide, le système d’exploitation met en veille le dispositif jusqu’au lancement de la prochaine.
Les évènements sont prioritaires par rapport aux tâches et peuvent interrompre la tâche en cours d’exécution. Ils permettent de faire le lien entre les interruptions matérielles ( pression d’un bouton, changement d’état d’une entrée, …) et les couches logicielles que constituent les tâches.
Dans la pratique, NesC permet de déclarer deux types de composants : les modules et les configurations. Un module contient le code d’un composant élémentaire et implémente une ou plusieurs interfaces. Les interfaces sont des fichiers décrivant les commandes et évènements proposés par le composant qui les implémente. L’utilisation des mots clefs « Uses » et « Provides » au début d’un composant permet de savoir respectivement si celui-ci fait appel à (requiert) une fonction de l’interface ou redéfinit (fournit) son code. Une application peut faire appel à des fichiers de configuration pour regrouper les fonctionnalités des modules. Un fichier top-level configuration permet de faire le lien entre tous les composants.
Toutes les applications ont besoin d’un fichier de configuration de haut niveau qui est normalement nommé d’après l’application elle-même. Dans ce cas, NomApplication.nc est le fichier de configuration de l’application et le fichier source que le compilateur nesC utilise pour générer l’exécutable. NomApplicationM.nc quant à lui correspond à l’implémentation à proprement parler de l’application. Comme nous l’avons expliqué dans la partie précédente, les composants nesC peuvent se relier les uns aux autres, et ici c’est le fichier NomApplication.nc qui permet de faire cette connexion entre le module NomApplicationM.nc et les autres composants auxquels l’application fait appel.
Dans une application, nous avons donc une configuration et un module qui vont ensemble. La convention de TinyOS veut alors que NomApplication.nc représente la configuration et NomApplicationM.nc représente le module correspondant.
Il faut donc respecter cette convention pour utiliser le Makefile livré avec la distribution de TinyOS.
NomApplication.nc :
       configuration NomApplication {
        }
        implementation {
                components ……………………………………………………;
                …………………………………………………………… ;
                …………………………………………………………… ;
                ………………
        }
La première chose à noter est le mot clé « configuration » qui indique qu’il s’agit d’un fichier de configuration dont le nom est « NomApplication ». A l’intérieur des accolades, il est possible de spécifier des interfaces requises ou offertes par la configuration, tout comme on le fait à l’intérieur d’un module.
La véritable configuration est implémentée au sein des deux accolades qui suivent le mot clé « implementation ».
La ligne « components » spécifie les différents composants auxquels la configuration fait référence. Le reste de l’implémentation consiste à connecter les interfaces utilisées par certains composants aux interfaces fournies par les autres.
Le composant Main est le premier exécuté dans une application TinyOS. Pour être plus précis, la commande Main.StdControl.init() est la première à être exécuté suivi de Main.StdControl.start(). Donc toute application TinyOS doit avoir un composant "Main" dans sa configuration. StdControl est une interface de base utilisée afin d’initialiser et de démarrer des composants TinyOS. Regardons le fichier qui la définit (StdControl.nc qui se trouve dans le répertoire tos/interfaces/ de TinyOS):
        interface StdControl {
                command result_t init();
                command result_t start();
                command result_t stop();
        }
Nous pouvons voir que « StdControl » définit trois commandes, « init() », « start() » et « stop() ». La première est appelée quand un composant est initialisé pour la première fois, « start() » quand il est démarré, c’est à dire exécuté pour la première fois et « stop() » quand le composant est arrêté, par exemple pour éteindre le dispositif physique qu’il contrôle.
Un appel à une de ces commandes dans un composant doit être répercuté dans tous les composants auxquels il se rattache.
        Main.StdControl -> NomApplicationM.StdControl;
Cette ligne relie l’interface « StdControl » de « Main » à l’interface « StdControl » de « NomApplicationM » et ainsi « NomApplicationM.init() » sera appelée par « Main.StdControl.init() ».
En ce qui concerne les interfaces requises ( c’est à dire utilisées par un composant ), il est important de noter que les fonctions d’initialisation doivent être explicitement appelées par les composants qui les utilisent. Par exemple si le module « NomApplicationM » utilise l’interface « Leds », « Leds.init() » est appelé explicitement à l’intérieur de « NomApplicationM.init() ».
Nous utilisons des flèches (->) pour déclarer les relations entre les interfaces. Il faut penser à cette flèche comme la relation « lie à ». Dans l’exemple précédent, on lie l’interface « StdControl » de « Main » à son implémentation qui se trouve dans le module « NomApplicationM ». En d’autres mots, le composant qui utilise l’interface (Main) se trouve à gauche dans cet exemple et le composant qui fournit (implémente) l’interface est à droite (NomApplicationM). Le sens de la flèche n’est pas fixe. On aurait ainsi pu écrire pour un résultat équivalent :
        NomApplicationM.StdControl <- Main.StdControl;
Le lien entre interfaces peut aussi se faire de manière implicite. Dans l’exemple précédent, le lien était explicite: nous lions l’interface « StdControl » de « Main » à l’interface « StdControl » de « NomApplicationM ». Dans la ligne suivante, nous le faisons de manière implicite:
        Main.StdControl -> NomApplicationM;
La ligne précédente est équivalente à la suivante :
        Main.StdControl -> NomApplicationM.StdControl;
Lorsque aucune interface n’est donnée du côté vers lequel la flèche pointe, le compilateur nesC essaie par défaut de faire un lien avec le même nom d’interface que l’on a donné de l’autre côté.
Il est possible pour un composant de fournir plusieurs instances d’une interface et de leur donner des noms distincts. Par exemple dans le fichier « NomApplicationM.nc », nous écrivons :
        uses {
                ...
                interface Timer as TimerVerif;
                interface Timer ;
        }
Cela nous permet de distinguer les deux instances, et dans un fichier de configuration de coupler les interfaces de façon différente :
        NomApplicationM.TimerVerif -> TimerC.Timer[unique("Timer")];
        NomApplicationM.Timer -> TimerC.Timer[unique("Timer")];
L’interface « paramétrée » :
        NomApplicationM.Timer -> TimerC.Timer[unevaleur];
Une interface paramétrée permet à un composant de fournir plusieurs instances de la même interface. Ainsi, le composant TimerC déclare :
        provides interface Timer[uint8_t id];
Autrement dit, il fournit 256 instances possibles de l’interface Timer, une pour chaque valeur « uint8_t »
Quand nous écrivons « TimerC.Timer[unevaleur] », nous spécifions que « NomApplicationM.Timer » doit être relié à l’instance de l’interface Timer spécifié par la valeur à l’intérieur des crochets. Nous pouvons donc spécifier une valeur particulière, comme 3 ou 4. Dans ce cas, il peut arriver que nous rentrions en conflit avec un Timer utilisé par un autre composant qui aurait déclaré la même valeur dans les crochets. Pour éviter cela, nous pouvons utiliser la fonction « unique() ». Cette dernière génère à chaque compilation un entier 8-bit unique à partir de la chaîne donné en paramètre.
NomApplicationM.nc
        includes ......;
        module NomApplicationM {
                provides {
                        interface StdControl;
                }
                uses {
                        interface Timer ;
                        .........
                }
        }
        implementation {
                ………………………… ;
                …………
        }
La première ligne nous est relativement familière, c’est l’inclusion d’un fichier d’en tête. La deuxième signale qu’il s’agit d’un module appelé « NomApplicationM » et déclare les interfaces fournies et utilisées. Nous voyons qu’il fournit l’interface « StdControl » par exemple. Cela veut dire que ce module implémente cette interface. Comme expliqué plus haut, cela est nécessaire pour initialiser et démarrer le composant. Il utilise ensuite plusieurs interfaces: par exemple : le Timer qui comme son nom l’indique permet de contrôler des timers.
Ces déclarations donnent accès à « NomApplicationM » aux commandes de ces interfaces, et l’oblige à implémenter les événements (events) déclarés dans ces interfaces.
        interface Timer {
                command result_t start(char type, uint32_t interval);
                command result_t stop();
                event result_t fired();
        }
La commande start() est utilisée pour définir le type de timer ainsi que l’intervalle à laquelle le timer expire (exprimé en milliseconde). Les deux types valides de timer sont « TIMER_REPEAT » et « TIMER_ONE_SHOT ». Comme leur nom l’indique, le premier continue indéfiniment jusqu’à ce qu’on l’arrête grâce à la commande « stop() », et le second s’arrête dès que l’intervalle de temps expire.
A chaque période, le timer envoie un « event » qui est « attrapé » par l’application grâce à l’implémentation de la fonction « fired() ».
En d’autres termes, un event est une fonction que l’implémentation d’une interface va signaler quand un certain événement arrive. Dans ce cas, l’event « fired() » est signalé à chaque fois que l’intervalle est révolue.
Un module qui utilise une interface doit donc obligatoirement implémenter les events que cette interface utilise.