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