Travaux d`Étude et de Recherche Support système pour l
Transcription
Travaux d`Étude et de Recherche Support système pour l
Travaux d’Étude et de Recherche Support système pour l’ordonnancement d’applications synchrones dans les noyaux de systèmes critiques embarqués Tatiana CURELLI 2009 Table des matières 1 Programmation et ordonnancement des tâches dans un système réel 3 1.1 Systèmes temps-réel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2 Les langages synchrones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2.1 Les principes des langages synchrone . . . . . . . . . . . . . . . . . . 4 1.2.2 Le langage Lustre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Ordonnancement de tâches dans un système temps-réel . . . . . . . . . . . . . 6 1.3.1 Ordonnancement statique . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.3.2 Ordonnancement avec un système d’exploitation . . . . . . . . . . . . 7 1.3 2 nxtOSEK 9 2.1 LEGO MINDSTORMS NXT . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2 nxtOSEK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3 3 2.2.1 Les différentes parties de nxtOSEK . . . . . . . . . . . . . . . . . . . 10 2.2.2 Les objets nxtOSEK . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.3 Le langage OIL (OSEK Implementation Language) . . . . . . . . . . . 11 2.2.4 L’ordonnancement des tâches par le noyau . . . . . . . . . . . . . . . 11 2.2.5 L’ordonnancement choisi . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2.6 La gestion des RESOURCES . . . . . . . . . . . . . . . . . . . . . . 12 2.2.7 Le sytème nxtOSEK complet . . . . . . . . . . . . . . . . . . . . . . . 13 Automatisation de la programmation multi-tâches de systèmes temps-réel . . . 14 Réalisations 16 3.1 Se familiariser avec nxtOSEK . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.2 Extraire les briques de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.3 Exécution de tâches simples sur la brique . . . . . . . . . . . . . . . . . . . . 17 3.4 Une solution alternative au compilateur OIL . . . . . . . . . . . . . . . . . . . 18 3.5 Un exemple complet : le TP d’automatique . . . . . . . . . . . . . . . . . . . 19 1 Introduction Le TER est un module optionnel permettant aux étudiants de découvrir le travail de chercheur dans un laboratoire. Les étudiants choisissent un sujet parmi les sujets proposés par différents laboratoires. Le TER dure 12 semaines à raison d’une demi-journée par semaine. J’ai choisi de faire mon TER à Verimag au sein de l’équipe Synchrone. L’équipe Synchrone effectue des recherches sur le développement et la validation des systèmes critiques embarqués. Elle a notamment défini, il y a 20 ans, le langage Lustre, un langage synchrone à flots de données dédié à la programmation de ces systèmes. L’équipe étudie également les techniques de validation comme le model-checking, le test, la simulation ou le debugging. Ces techniques ont été implémentées dans des outils connectés à Lustre. Lustre et les outils associés ont été appliqués à l’industrie, dans l’outil de développement SCADE. Ils peuvent être utilisés pour une grande variété de systèmes embarqués. J’ai été encadrée pendant mon TER par Pascal Raymond et Christophe Rippert. Mon sujet "Support système pour l’ordonnancement d’applications synchrones dans les noyaux de systèmes critiques embarqués" aborde le problème du compromis entre déterminisme et ajout de support système dans les systèmes critiques embarqués. Les avantages de l’utilisation de support système en embarqué sont présentés dans le premier chapitre. Dans le même chapitre, sont également présentées plusieurs notions tenant à l’embarqué et qui sont nécessaires à la compréhension du sujet. Concrètement, l’objectif du TER est de faire tourner plusieurs tâches lustre en parallèle sur la brique LEGO MINDSTORMS NXT en récupérant des briques de base de nxtOSEK, une plate-forme open source qui est décrite dans le deuxième chapitre. Une première étude a déjà été réalisée par Gaspard Armagnat, dans le cadre de son TER. Il a extrait les drivers de nxtOSEK et a réussi à faire tourner une tâche périodique sur la brique. Mais, il n’a pas pu aborder le problème des systèmes multi-tâches. Le troisième chapitre rend compte des réalisations qui ont été faites pour l’exécution de systèmes multi-tâches sur la brique LEGO et des résultats obtenus. 2 Chapitre 1 Programmation et ordonnancement des tâches dans un système réel L’objectif de ce chapitre est de présenter les notions générales qui sont utilisées dans le TER. Il commence par une présentation des systèmes temps-réel. La deuxième partie est ensuite consacrée à la programmation des systèmes temps-réel au moyen des langages synchrones. Le cas de Lustre, langage synchrone que nous avons utilisé pour la programmation des tâches du robot, est étudié plus en détail. Enfin, le chapitre termine par une description des types d’ordonnancement dans les systèmes embarqués. Cette dernière partie montre dans quelle mesure l’utilisation de support système dans un système embarqué peut avoir un intérêt. 1.1 Systèmes temps-réel Les systèmes embarqués sont très souvent des systèmes temps-réel. Un système temps-réel est un programme qui cherche à contrôler le comportement de l’environnement auquel il est connecté. Un système temps-réel doit donc continuellement : mesurer l’état de son environnement, faire des calculs et produire des commandes en respectant les contraintes temporelles imposées par la dynamique de son environnement. La plupart du temps, les systèmes embarqués et les systèmes temps-réel sont aussi des systèmes critiques, c’est-à-dire des systèmes pour lesquels un non respect des contraintes temporelles ou une erreur fonctionnelle peut avoir des conséquences désastreuses. 3 F IG . 1.1 – Représentation schématique d’un système temps-réel 1.2 1.2.1 Les langages synchrones Les principes des langages synchrone Les langages synchrones ont été développés afin de représenter les systèmes temps réels de façon simple et efficace. Un langage synchrone permet de décrire de façon déterministe les réactions d’un système tout en prenant en compte les contraintes de temps imposées par l’environnement. L’une des caractéristique essentielle des langages synchrones est de pouvoir spécifier des comportements déterministes même en présence de parallélisme et de communication. Les langages synchrones supposent que les entrées sont échantillonnées à intervalles réguliers. Le système temps-réel est transformé en une machine à états synchronisée sur l’échantillonnage des entrées. Du point de vue des langages synchrone, un système temps-réel est donc une boucle infinie : Initialisation Boucle infinie lit les entrées calcule sorties affecte sorties où le temps de boucle est déterminé par la durée maximum d’exécution d’un état. Avec les langages synchrones, le programmeur se consacre uniquement à la description des réactions du système sans définir d’ordre d’exécution. Le compilateur déterminera un ordre d’exécution correct à partir des dépendances entre variables. 4 Un autre intérêt des langages synchrones est qu’on peut vérifier formellement les programmes. Les programmes écrits en langages synchrones peuvent donc être rigoureusement validés. 1.2.2 Le langage Lustre Lustre est un langage synchrone à flots de données. Le temps est supposé discret. Les variables en Lustre dénotent des séquences virtuellement infinies de valeurs. Un programme lustre est un ensemble de nœuds qui mettent des variables d’entrée en relation avec des variables de sortie. Voici un exemple de nœud Lustre : node EDGE (X : bool) returns (Y : bool); let Y= false -> X and not pre(X); tel; L’opération pre(Z) retourne le flot Z décalé d’un instant, la valeur initiale étant indéterminée. L’opération a− > Z substitue la valeur a à la première valeur de Z. Résultat de l’exécution pour un X particulier : X 0 Y 0 0 0 0 0 0 0 1 1 1 0 1 0 0 0 0 0 1 1 1 0 0 0 TAB . 1.1 – Résultat de l’éxécution de EDGE Il s’agit d’un nœud détectant les fronts montant. Un nœud Lustre peut-être compilé en C. On utilise pour cela les commandes suivantes : lustre mon_noeud mon_noeud.lus ec2c mon_ noeud.ec On obtient 2 fichiers : mon_ noeud.c et mon_ noeud.h Ces fichiers contiennent principalement : – une structure mon_ noeud_ ctx : c’est le contexte du nœud, il contient notamment les entrées et les sorties du nœud, mais aussi les mémoires (correspondant aux opérations pre) – des fonctions mon_ noeud_ I_ nom_ entree : ces fonctions permettent de fixer la valeur des entrées du nœud 5 – des fonctions mon_ noeud_ O_ nom_ sortie : fonctions déclarées externes, à écrire par le programmeur ; elles doivent permettre d’appliquer les sorties sur l’environnement. – une fonction mon_ noeud_ step : fonction contenant le corps du nœud ; à la fin de la fonction mon_ noeud_ step, on appelle les fonctions mon_ noeud_ O_ nom_ sortie. /* Contexte du noeud */ struct EDGE_ctx; /* Corps du noeud */ void EDGE_step(struct EDGE_ctx*); /* Lecture des entrées */ void EDGE_I_X(struct EDGE_ctx* ctx, _integer _X); /* Ecriture des sorties */ void EDGE_O_Y(void * cdata, _integer _Y) Pour exécuter le nœud sur un système d’exploitation, on peut appeler les fonctions mon_ noeud_ I_ nom_ entree et mon_ noeud_ step dans une boucle infinie : while (1) { EDGE_I_X(ctx, ...); // en général lecture sur un capteur EDGE_step(ctx); } Au lieu de construire une telle boucle avec les appels aux fonctions mon_ noeud_ step et mon_ noeud_ I_ nom_ entree, on peut aussi les regrouper dans une tâche et faire en sorte que cette tâche soit appelée de façon périodique soit en ordonnançant la tâche "à la main" (ordonnancement statique), soit en utilisant un système d’exploitation. 1.3 1.3.1 Ordonnancement de tâches dans un système temps-réel Ordonnancement statique L’intérêt principal de l’ordonnancement statique est qu’il permet une maîtrise totale de l’entrelacement des tâches : l’exécution du système est donc entièrement déterministe. Le principe de l’ordonnancement statique est d’associer chaque tâche à ses dates d’exécution dans une table. Cette table sera ensuite déroulée pendant l’exécution du système. Un problème de l’ordonnancement statique est la présence de tâches dont le temps d’exécution est long par rapport aux autres tâches du système. Une solution consiste à découper le code de la tâche longue en sous-tâches qu’on va entrelacer avec les tâches plus rapides, de façon à garantir le respect des échéances du système. Traditionnellement, ce découpage est fait "à la 6 main" par le programmeur ce qui est fastidieux car il faut sauvegarder puis restaurer le contexte d’une tâche à chaque découpage. Dans l’exemple ci-dessous, T1 est découpée en un bloc de durée 1 unité et un bloc de durée 3 unités. F IG . 1.2 – Ordonnancement statique de deux tâches Un autre désavantage de l’ordonnancement statique est qu’il ne permet pas la construction incrémentale d’un système : l’ajout d’une tâche périodique nécessite de reconstruire totalement la table. Il n’est pas non plus adapté à la gestion des tâches urgentes, tâches qui arrivent de manière imprévisible et dont les durées d’exécution sont inconnues. L’évolution récente plaide donc en faveur de l’introduction d’un minimum de services système. 1.3.2 Ordonnancement avec un système d’exploitation L’ordonnancement dynamique par un OS à l’avantage de se charger du découpage des tâches longues et des changements de contextes. L’inconvénient, c’est que dans les systèmes classiques, cela rend l’entrelacement des tâches imprévisibles. On utilise donc des OS tempsréel, OS spécialisés, qui garantissent qu’un système informatique respectera certaines contraintes de temps. Dans un système ordonnancé par un OS, l’OS vérifie périodiquement les tâches prêtes et choisi la tâche qui s’exécute. Les tâches ont des priorités ; si plusieurs tâches sont prêtes en même temps, le système d’exploitation donnera la main à la plus prioritaire. Si une tâche prioritaire devient prête alors qu’une autre tâche s’exécute, le système préemptera cette dernière et donnera la main à la plus prioritaire. La tâche préemptée reprendra la main quand la tâche prioritaire aura fini son exécution et si aucune tâche plus prioritaire qu’elle n’est prête. Ce type de mécanisme est appelé ordonnancement dynamique. Voici un exemple d’ordonnancement de deux tâches avec un système d’exploitation : 7 F IG . 1.3 – Ordonnancement dynamique de deux tâches par un système d’exploitation L’utilisation d’un système d’exploitation rend donc le système beaucoup plus souple : une tâche longue peut être interrompue n’importe quand et il est également très aisé de rajouter des tâches au système. D’autre part, le système d’exploitation peut permettre de gérer les tâches urgentes ce qui est impossible avec l’ordonnancement statique. Pour la brique LEGO, il existe un système d’exploitation qui permet à un utilisateur de contrôler l’ordonnancement des tâches en spécifiant en particulier les périodes et les priorités qu’il souhaite pour chaque tâche : ce système s’appelle nxtOSEK. Notre objectif était de prendre ce système, d’en extraire les briques de base permettant l’exécution de plusieurs tâches sur la brique et, à partir de ces briques de base seulement, de réussir nous aussi à programmer un système multi-tâches sur la brique LEGO. 8 Chapitre 2 nxtOSEK Ce chapitre décrit succinctement le kit LEGO MINDSTORM NXT et nxtOSEK, la plateforme que nous avons exploitée pour les besoins du TER. La dernière partie expose les modifications que nous souhaitons apporter au système nxtOSEK ; nous verrons que notre TER s’inscrit dans un projet à long terme dont l’objectif est de proposer une démarche automatisée pour la programmation multi-tâches déterministe des systèmes temps-réels. 2.1 LEGO MINDSTORMS NXT F IG . 2.1 – LEGO MINDSTORMS NXT LEGO MINDSTORMS NXT est un kit, développé par LEGO, pour construire et programmer un robot. Les principaux composants compris dans le kit LEGO MINDSTORM NXT sont : une brique constituée d’un micro-contrôleur Atmel 32-bits et 8 capteurs et 3 servo-moteurs qui se branchent sur la brique. 9 Le micro-contrôleur Atmel 32 bits est constitué d’un processeur ARM de 256 Ko de mémoire Flash et 64 Ko de mémoire RAM fonctionnant à 48 MHz. Parmi les 8 capteurs fournis dans le kit LEGO MINDSTORMS NXT, on trouve en particulier un capteur à ultrasons, un capteur de son, un capteur de contact et un capteur de lumière. Certains capteurs nécessitent d’être initialisés pour pouvoir être utilisés : c’est le cas par exemple du capteur à ultrasons qui peut être utilisé pour permettre à la brique de détecter un obstacle. Les actionneurs du robot LEGO sont les servo-moteurs électriques. La façon la plus simple de commander les servo-moteurs est de leur imposer un pourcentage entre -100% et 100% de leur puissance maximale (un pourcentage négatif ayant pour effet de faire reculer le robot). En outre, le boîtier LEGO propose un écran LCD et 4 boutons permettant à l’utilisateur d’interagir directement avec le robot. 2.2 2.2.1 nxtOSEK Les différentes parties de nxtOSEK nxtOSEK est un système d’exploitation temps-réel open source pour la brique LEGO MINDSTORM NXT. Les sources de nxtOSEK sont réparties en trois parties distinctes : leJOS NXJ contient tout ce qui concerne les drivers, Toppers OSEK contient les sources du noyau et ECrobot contient la "glue" pour faire fonctionner l’ensemble. Dans les sources de nxtOSEK sont également compris des sources pour une machine virtuelle java ; ces sources se trouvent dans les répertoires C++ et Toppers OSEK. 2.2.2 Les objets nxtOSEK nxtOSEK fournit à l’utilisateur un certain nombre d’objets lui permettant de programmer la brique LEGO. Parmi ces objets, on trouve notamment : – – – – les TASK : tâches les COUNTER : compteurs les ALARM : compteurs associé à une action les RESOURCE : verrous (si une tâche T1 a pris un verrou et qu’une tache T2 veut prendre le même verrou, T2 devra attendre que T1 relâche le verrou.) – les EVENT : évènements auxquels une tâche est susceptible de réagir – les ISR : routines d’interruption – les MESSAGE : liste d’objets OSEK auxquels une tâche peut accéder Une tâche TASK est associée à une alarme ALARM qui est elle même associée à un compteur COUNTER. Un compteur et une alarme ont une valeur qui est incrémentée par le système. Un compteur et une alarme possède une valeur entière qui est leur valeur maximum, respectivement MAXALLOWEDVALUE et ALARMTIME. Une alarme possède de plus une valeur 10 CYCLETIME. A chaque fois que le compteur associé à l’alarme atteint un multiple du CYCLETIME le système incrémente la valeur courante de l’alarme. Lorsque l’alarme atteint sa valeur maximum, le système exécute la tâche associée à cette alarme. Enfin, lorsqu’un compteur atteint sa valeur maximum, il arrête de s’incrémenter et les alarmes qui lui sont associées ne peuvent plus être déclenchées. A chaque fois qu’un compteur est incrémenté, le système regarde donc pour chaque alarme associée à ce compteur, en fonction de leur CYCLETIME, si elle doit être incrémentée ; puis, dans le cas où il incrémente une alarme, il regarde en fonction de l’ALARMTIME de cette alarme si la tâche associée à l’alarme doit être appelée. Tout programme utilisateur doit définir un compteur SysTimerCnt et une fonction usr_ 1ms_ isr_ type2 qui est appelée toutes les millisecondes par le système et qui incrémente à chaque fois SysTimerCnt. 2.2.3 Le langage OIL (OSEK Implementation Language) nxtOSEK repose sur l’utilisation d’un langage appelé langage OIL et d’un compilateur, Nextool, du langage OIL vers le C. Le langage OIL permet de spécifier certaines informations sur le noyau, de donner les informations sur les tâches que l’on veut éxécuter, telles que leurs périodes ou leur priorités ainsi que des informations sur tous les objets qui seront à gérer par le noyau (verrous...). Nextool déduit ensuite de toutes ces informations les structures de données C qui seront nécessaires au noyau pour exécuter les tâches. Nextool produit deux fichiers kernel_ cfg.c et kernel_ id.h. Dans le fichier OIL sont fixés notamment les caractéristiques du compteur SysTimerCnt : /* Definition of OSEK Alarm Counter */ COUNTER SysTimerCnt { MINCYCLE = 1; MAXALLOWEDVALUE = 10000; TICKSPERBASE = 1; /* One tick is equal to 1msec */ }; 2.2.4 L’ordonnancement des tâches par le noyau Les tâches TASK de nxtOSEK ont toutes une priorité fixe qui est un entier positif. Plus cet entier est grand plus la tâche est prioritaire. Une tâche prioritaire préempte les autres tâches. 2.2.5 L’ordonnancement choisi C’est l’utilisateur qui décide de la priorité des tâches. Pour que le système soit ordonnançable, il faut que les priorités vérifient quelques contraintes. 11 Nous avons choisi un ordonnancement de type Rate Monotonic, c’est à dire que plus une tâche a une période faible plus nous lui fixons une priorité élevée. Dans ce cas, on a le critère suivant dit critère de Liu et Layland : Le système est ordonnançable si : PN Ci i=1 ( Ti ) 1 < N (2 N − 1) où N est le nombre de tâche Ci est le pire temps d’exécution de la tâche i (WCET en anglais Worst Case Execution Time) Ti est la période la tâche i 2.2.6 La gestion des RESOURCES Le protocole de gestion des ressources utilisé dans nxtOSEK est un ICPP (Immediate Ceiling Priority Protocol). Il faut spécifier dans le fichier OIL les tâches qui utilisent un verrou RESOURCE. A chaque verrou est associé une priorité qui est la priorité maximale des priorités des tâches qui utilisent ce verrou. Lors de l’exécution, lorsqu’une tâche prendra un verrou, elle prendra par la même occasion la priorité associée au verrou. Voici un exemple avec quatre tâches : F IG . 2.2 – Exemple d’ordonnancement avec l’ICPP T1 devient de priorité 4 en faisant P1 et continue à s’exécuter jusqu’à avoir fait un V1. Ainsi, lorsque T2 devient prête, elle ne s’exécute pas alors que normalement elle est plus prioritaire que T1. 12 2.2.7 Le sytème nxtOSEK complet Pour programmer le robot avec nxtOSEK, il suffit donc d’écrire deux fichiers : un fichier OIL avec la configuration du noyau et un fichier C qui contient la déclaration des objets nxtOSEK (tâches, verrous...) et le corps des tâches. Pour cela, le fichier C doit utiliser un certain nombre de macros définie dans OSEK. Par exemple, pour déclarer le counter SysTimerCnt, il faut utiliser la macro : DeclareCounter(SystimerCnt) C’est également dans le fichier C utilisateur que se trouve la fonction user_ 1ms_ isr_ type2 : void user_1ms_isr_type2(void) { StatusType ercd; /* Increment OSEK Alarm System Timer Count */ ercd = SignalCounter( SysTimerCnt ); if( ercd != E_OK ){ ShutdownOS( ercd ); } } Ci-dessous, le schéma complet du projet nxtOSEK. Après la compilation, on obtient un fichier binaire que l’on charge directement dans la ram de la brique LEGO. F IG . 2.3 – Le système nxtOSEK complet 13 2.3 Automatisation de la programmation multi-tâches de systèmes temps-réel Dans nxtOSEK, le corps des tâches est écrit en langage C et la configuration du noyau est faite en langage OIL. Pour notre part, nous souhaitons ne pas avoir à passer par un autre langage pour spécifier la configuration du noyau. L’idéal est de pouvoir spécifier la configuration dans un langage pseudo-Lustre. Ce Lustre spécialisé qu’on note Lustre+, contient toutes les annotations nécessaires pour générer des tâches système (période, priorités, etc). Le corps des tâches est écrit en Lustre classique et la configuration du noyau est écrite, dans un autre fichier, en Lustre+. Puis, avec un Makefile adapté et un compilateur Lustre+, on fabrique à partir du Lustre+e le kernel_ id.h, le kernel_ cfg.c et un fichier C qui contiendrait les appels aux fonctions mon_ noeud_ step et mon_ noeud_ I_ nom_ entree des noeuds lustre ainsi que la définition des fonctions mon_ noeud_ O_ nom_ sortie (l’équivalent du fichier C utilisateur dans nxtOSEK). Voici une représentation schématique de ce que nous voudrions être capable de représenter en pseudo Lustre. F IG . 2.4 – Représentation schématique d’un code écrit en pseudo Lustre Il faudrait un compilateur capable de comprendre que l’ensemble constitué de la boite Période=10ms et de la flèche qui pointe sur T1 signifie que la tâche T1 s’exécute toutes les 10 ms 14 ou encore que C1 signifie que T1 lit un capteur et que la flèche entre T1 et T2 est une variable partagée par les deux tâches. Au dessus du compilateur pseudo Lustre, nous pouvons également envisager une autre étape qui consiste à découper les tâches lustre en plusieurs sous-tâches. Sarah Moussouni, un étudiante ENSIMAG, a abordé le problème du découpage de tâches lustre en sous-tâches cette année en TER. En joignant nos deux TER, nous pourrions obtenir une méthode semi-automatisée pour la programmation multi-tâches de systèmes temps réels. Le schéma complet du système : F IG . 2.5 – Schéma du système Le projet d’automatisation de la démarche, en particulier la définition précise de Lustre+ et la réalisation de son compilateur, sont des objectifs à moyen et long terme. Ce TER est donc une étape qui s’inscrit dans cet objectif. 15 Chapitre 3 Réalisations La partie réalisations du TER s’est déroulée en plusieurs étapes. Il y a d’abord eu une étape où j’ai dû me familiariser avec le système nxtOSEK. L’étape suivante a ensuite été d’extraire de nxtOSEK les briques de bases permettant la programmation multi-tâches. Cette étape a été extrêmement fastidieuse ; elle a été suivie de l’écriture de scripts afin de simuler le compilateur OIL. Enfin, j’ai utilisé le système obtenu pour programmer un exemple de communication déterministe entre deux tâches. 3.1 Se familiariser avec nxtOSEK J’ai tout d’abord dû me familiariser avec le système nxtOSEK. J’ai essayé de comprendre comment l’utiliser pour exécuter des tâches sur la brique. Puis, j’ai essayé de me familiariser avec les objets du langage OIL : les TASK, les ALARM, les RESOURCE... et de déterminer les informations qu’il faut spécifier dans le fichier OIL, c’est-à-dire les informations nécessaires pour qu’on puisse exécuter plusieurs tâches sur un système. J’ai également essayé de comprendre comment le compilateur OIL fabriquait le kernel_ cfg.c à partir des informations qui lui étaient fournies dans le fichier OIL. Enfin, j’ai essayé d’identifier les principaux modules nécessaires, en partant du programme principal et en suivant le chemin des dépendances. 3.2 Extraire les briques de base Au début du TER, la première tâche a été de suivre le chemin de compilation du projet afin d’identifier les briques de base de nxtOSEK et les sources qui ne sont pas absolument nécessaires. Typiquement, j’ai essayé de sortir les sources la machine virtuelle Java qui n’étaient pas nécessaires pour le TER. Concrètement, cette étape consistait à sortir une à une les sources du projet, écrire un nouveau Makefile et réussir à recompiler le projet avec le moins de sources possibles. J’ai rencontré beaucoup de difficultés à cette étape. Le Makefile du projet faisait plusieurs pages et utilisait des fonctions assez évoluées des Makefile. Or, je n’étais pas habituée à la 16 syntaxe des Makefiles. Il a donc fallu que je me plonge dans la doc de GNU pour comprendre comment fonctionnait la compilation du projet. En outre, beaucoup d’outils différents sont utilisés pour compiler le projet. En effet, le projet est composé à la fois de source C, C++, assembleur s et S, à transformer soit en fichier o (binaire translatable), soit en fichier oram (binaire à adresses absolues), de fichiers image (.bmp->.obmp), de fichiers son (.wav->.owav) et de librairies (les sources du Kernel sont recompilées à chaque fois mais les sources de ECrobot et de Lejos-OSEK se trouvent dans deux librairies respectivement LIBECROBOT et LIBLEJOSOSEK). Je n’étais pas forcément à l’aise avec tous les outils utilisés et leurs options, ni avec tous les types de fichiers. Une difficulté particulière a consisté à trouver les bonnes bibliothèques de drivers dont le système avait besoin pour fonctionner. Après plusieurs échecs, j’ai finalement réussi à ne garder que la moitié des sources contenues dans le projet (les sources de la machine virtuelle java ont été sorties). Au lieu de partir de rien et de chercher à compiler, j’ai rajouté toutes les sources qui était compilées dans le Makefile de nxtOSEK ; puis, j’ai rajouté les sources nécessaires pour fabriquer les librairies ; et enfin, j’ai enlevé un par un des fichiers C puis des fichiers h. C’était un travail fastidieux mais enrichissant puisque grâce à cela j’ai appris à faire des Makefiles. Cela m’a aussi permis de mieux comprendre la structure du projet et de me familiariser avec les sources. J’ai pu comprendre les fonctions de plusieurs sources. Parmi les objets de nxtOSEK, les seuls qui ont été conservés sont les TASK, les COUNTER, les ALARM et les RESOURCES. Les sources du noyau qui permettaient la gestion des événements (EVENT, MESSAGE ..) ne sont pas présentes dans la version finale du système. La raison à cela est que je n’ai pas eu le temps de voir en détail comment les événements étaient gérés et j’ai donc préféré ne pas les utiliser. 3.3 Exécution de tâches simples sur la brique J’ai ensuite essayé de fabriquer des tâches en Lustre et de les exécuter sur la brique. J’ai d’abord implémenté des exemples très simples. De petites modifications dans les fichiers h produits par le compilateur lustre sont nécessaires pour pouvoir compiler et exécuter un exemple. Il faut notamment enlever la référence à la bibliothèque C stdlib des fichiers h et redéfinir la fonction malloc qui est utilisée pour réserver la place du contexte des noeuds lustre. Avec la fonction malloc de stdlib le robot ne démarre pas. Une solution est de déclarer un tableau statique et un pointeur et de redéfinir le malloc de telle sorte qu’à chaque appel il renvoit un pointeur vers une bloc différent du tableau. Ces modifications sur les sources produites par le compilateur lustre sont facilement automatisables en utilisant des commandes de manipulation de caractères dans le Makefile. J’ai créé une fonction usr_ init, fonction vide par défaut, que l’on peut redéfinir dans le fichier C utilisateur en lui mettant les initialisations des contextes des noeuds lustre (appel au malloc précédant). Ceci est rendu possible grâce à l’option − − allow − multiple − def inition qui autorise la définition multiple dans les sources C : la première définition sera prise ; il suffit donc d’assembler les sources utilisateur dans le Makefile en premier pour prendre en compte la redéfinition par l’utilisateur de la fonction usr_ init. J’appelle la fonction usr_ init dans la 17 fonction object_ initialize dans le kernel_ cfg. La fonction object_ initialize est appelée au démarrage du système. Ainsi, les contextes des noeuds lustre seront initialisés au démarrage du système. 3.4 Une solution alternative au compilateur OIL A défaut d’avoir fait un compilateur Lustre+, j’ai écrit des scripts qui permettent de simuler le compilateur OIL pour l’exécution de tâches périodiques avec éventuellement l’utilisation de ressources. Il y a 3 scripts : new_ exemple, new_ kernel_ cfg, new_ exemple_ c. new_ exemple new_ exemple produit tout d’abord l’arborescence nécessaire pour un nouvel exemple, puis un petit Makefile à compléter par l’utilisateur (qui appelle un Makefile plus gros), et enfin new_ exemple appelle les scripts new_ kernel_ cfg et new_ exemple_ c. new_ exemple prend en paramètre le nom de l’exemple, le nombre de tâches et le nombre de ressources que contient l’exemple. new_ kernel_ cfg Script qui produit les fichiers kernel_ cfg.c et kernel_ id.h. Il prend en paramètre le nom de l’exemple, le nombre de tâches et le nombre de ressources que contient l’exemple. Le kernel_ cfg.c qui est produit a besoin que l’on définisse des macros pour les priorités et les périodes des tâches et pour les priorités des ressources, elles seront définies dans un fichier exemple.h produit par le script new_ exemple_ c et à compléter à la main par l’utilisateur. new_ exemple_ c Script qui produit les fichiers exemple.h et exemple.c. – exemple.h exemple.h contient des macros pour les priorités et les périodes des tâches et pour les priorité des ressources (macros dont a besoin le kernel_ cfg.c). Les macros sont à compléter à la main par l’utilisateur. – exemple.c exemple.c contient les routines, l’horloge, les fonctions mon_ noeud_ O_ nom_ sortie des noeuds lustre et les tâches OSEK. Il faut compléter les fonctions mon_ noeud_ O_ nom_ sortie et le corps des tâches OSEK avec les appels aux fonctions mon_ noeud_ O_ nom_ entree et mon_ noeud_ step des noeuds lustre. 18 Il prend en paramètre le nom de l’exemple, le nombre de tâches et le nombre de ressources que contient l’exemple. Une fois que l’on a complété les fichiers cités au-dessus, il suffit de mettre les tâches en Lustre dans le répertoire de l’exemple et si on a bien complété le Makefile produit par new_ exemple, en éxécutant la commande make on obtient directement le binaire exécutable sur la brique lego. 3.5 Un exemple complet : le TP d’automatique Afin de voir l’intérêt de la programmation par tâches dans un exemple concret, j’ai repris mon projet de Système de Contrôle du deuxième semestre et je l’ai implémenté sur la brique avec la version simplifiée de nxtOSEK. Le but du projet de Système de Contrôle est de contrôler la position du robot. Nous avons déterminé un contrôleur pour l’angle du robot par rapport à un mur et un contrôleur pour la distance du robot par rapport à un mur. Le robot détecte la présence du mur grâce à un capteur à ultrasons. Quand le robot s’approche à moins de 20cm du mur, il tourne et évite ainsi le mur. Pendant le projet de Système de Contrôle, nous avons testé les contrôleurs sous Simulink ; puis, nous avons transformé les schéma blocs de Simulink en un code lustre grâce à l’outil S2L. F IG . 3.1 – Le contrôleur du robot sous simulink Le code obtenu par S2L est sous la forme d’un seul gros nœud pour les deux contrôleurs. J’ai essayé de découper ce nœud en deux tâches : une tâche T1 qui contrôle l’angle du robot 19 et une tâche T2 qui contrôle la distance du robot par rapport au mur. La tâche qui contrôle la distance, T2, est la plus rapide et la plus prioritaire. On a ainsi un ordonnancement de type Rate Monotonic. C’est T2 qui lit la valeur de θ venant de l’extérieur. Les entrées de T2 sont θext et dext . Elle réécrit la valeur de θ dans une variable où T1 vient lire. T2 ne met à jour θ que quand elle est sûre que T1 n’a pas commencé un calcul. C’est le cas, quand les deux tâches sont prêtes en même temps : T2 prend la main sans que T1 n’ait commencé à s’exécuter. Dans mon exemple, j’ai pris des tâches de périodes de rapport 3. T2 possède un compteur qu’elle incrémente à chaque fois qu’elle prend la main ; quand son compteur arrive à 3, on sait que la tâche T1 était prête quand T2 a commencé son exécution mais que T1 n’a pas pu prendre la main. T2 met alors à jour R2L et remet son compteur à 0. T2 calcule également la somme et la différence des deux résultats des tâches pour obtenir vg et vd la vitesse respectivement droite et gauche du robot et les affecte aux moteurs. Pour récupérer le résultat de T1, elle vient lire soit en L1 soit en L2 selon la valeur indiquée dans ecrire_ dans. C’est T2 qui gère ecrire_ dans_ L1 : T1 lit ecrire_ dans lorsque ecrire_ dans vaut 1, T1 ecrit dans L1 et T2 lit dans L2, l’inverse si ecrire_ dans_ L1 vaut 0. T2 change la valeur de ecrire_ dans_ L1 toutes les trois exécutions. Ainsi, T2 et T1 n’accèdent jamais à la même variable en même temps. F IG . 3.2 – Représentation schématique de la séparation en deux tâches 20 F IG . 3.3 – Valeur de R2L au cours du temps Nous venons de construire un exemple de programme multi-tâches avec communication, complètement déterministe et sans utiliser de verrous. Le mécanisme que nous avons utilisé est un mécanisme général pour garantir le déterminisme des communications sans utiliser de verrous. Dans cet exemple, nous avons mis en place le mécanisme "à la main" mais ceci est entièrement automatisable et, à long terme, ceci sera fait par le compilateur Lustre+. 21 Conclusion Un système temps-réel est un programme qui interagit avec son environnement via des capteurs et des actionneurs et dont la vitesse d’interaction est contrainte par l’environnement. Le système doit répondre suffisamment rapidement aux mesures effectuées afin de ne pas manquer d’informations significatives sur l’évolution de l’environnement. Les systèmes temps-réel peuvent être modélisés au moyen des langages synchrones dont fait partie Lustre. Lustre et les langages synchrones permettent d’exprimer de façon déterministe les comportements d’un système. Les programmes écrits en langage synchrone ont aussi l’avantage de pouvoir être validés rigoureusement au moyen d’outils. Les tâches écrites en langages synchrones peuvent être ordonnancées soit statiquement, soit au moyen d’un système d’exploitation. L’ordonnancement statique a l’avantage par rapport à l’ordonnancement avec un OS de rendre le système totalement déterministe. Cependant, cette technique gère mal la présence de tâches longues et elle n’est pas adaptée pour répondre à l’arrivée d’une tâche urgente. La tendance veut donc qu’on insère du support système mais de façon limitée afin de garantir le déterminisme de l’ensemble. Sur la brique LEGO nous disposons d’un système d’exploitation, nxtOSEK, qui offre à l’utilisateur la possibilité de programmer en multi-tâches. Nous avons repris ce système et nous en avons extrait les briques de base pour faire du multi-tâches. nxtOSEK utilise un langage spécialisé pour spécifier la configuration des tâches : le langage OIL. Dans la version du projet qui existe, la fonction du compilateur OIL et assurée par des scripts. Nous souhaitons dans l’avenir remplacer le compilateur OIL par un compilateur pseudo lustre. Ainsi, la spécification des tâches et leur configuration pourrait être faites dans le même langage. L’autre amélioration au système serait de mettre en commun notre TER avec celui de Sarah Moussouni dont l’objectif était de découper des tâches lustre en plusieurs sous-tâches. Nous obtiendrions alors une démarche quasi automatisée pour la programmation multi-tâches de systèmes embarqués. Le système actuel fonctionne et peut permettre par exemple de contrôler la position du robot. 22 Remerciements Je tiens à remercier Pascal Raymond et Christophe Rippert pour leur aide précieuse tout au long de mon TER. Je remercie également toute l’équipe Synchrone pour son accueil très chaleureux. 23 Annexe Les fichiers exemple.h et exemple.c à compléter /** * exemple.h * */ #ifndef exemple_H #define exemple_H // Priorite et periode des taches // A ADAPTER SELON L’EXEMPLE /* Tache T0 */ #define PRIORITE_T0 0 #define PERIODE_T0 0 /* Tache T1 */ #define PRIORITE_T1 1 #define PERIODE_T1 1 // Priorite d’une resource // A ADAPTER SELON L’EXEMPLE /* Resource R0 */ #define PRIORITE_R0 0 #endif /** * exemple.c * */ #include "kernel.h" #include "kernel_id.h" #include "ecrobot_interface.h" #include "exemple.h" // AJOUTER LES .H DES TACHES 24 /*-------------------------------------------*/ /* OSEK declarations */ /*-------------------------------------------*/ DeclareCounter(SysTimerCnt); DeclareResource(R0); DeclareTask(T0); DeclareTask(T1); /*-------------------------------------------*/ /* Mes definitions */ /*-------------------------------------------*/ /* Contexte des noeuds lustre */ // struct mon_noeud_ctx* ctx; /*------------------------------------------*/ /* Hooks routines */ /*------------------------------------------*/ /** * Initialisation des drivers */ /*void ecrobot_device_initialize(){}*/ /** * Terminaison des drivers */ /*void ecrobot_device_terminate(){}*/ /** * Autres initialisations */ /*void usr_init(void) { // Initialisation des contextes des noeuds lustre // ctx = mon_noeud_new_ctx((void*) 0); }*/ /** * Function to be invoked from a category 2 interrupt */ void user_1ms_isr_type2(void) { StatusType ercd; /* Increment OSEK Alarm System Timer Count */ ercd = SignalCounter( SysTimerCnt ); 25 if( ercd != E_OK ){ ShutdownOS( ercd ); } } /*------------------------------------------*/ /* Mes fonctions */ /*------------------------------------------*/ /** * Affiche a l’ecran "what var" */ /*void show_var(char* what, UINT line, UINT var) { GetResource(R0); display_goto_xy(0, line); display_string(what); display_int(var, 10); display_update(); ReleaseResource(R0); }*/ /** * mon_noeud_O_YY permet de fixer la valeur de la * sortie YY u noeud lustre mon_noeud */ /*void mon_noeud_O_YY(void* _cdata, _integer _YY){}*/ /** * Tache */ TASK(T0) { // A COMPLETER ... TerminateTask(); } /** * Tache */ TASK(T1) { // A COMPLETER ... TerminateTask(); } 26