Développement d'applications embarquées avec OSEK/VDX : exemple du Robot Lego NXT

Auteur

Georges KEMAYO

Une application embarquée est un programme informatique intégré dans un système électronique muni généralement d'un microprocesseur, d'espaces mémoires de faible capacité et de péripheriques d'entrée/sortie, ou alors d'un microcontrôleur directement composé de ces differents éléments. Ce programme est très souvent destiné à faire fonctioner de manière autonome (intervention humaine nulle ou très limitée) ce système. De tels programmes sont généralement critiques du fait que le respect des contraintes fonctionnelles et temporelles des tâches qui s'exécutent doivent être vérifiées. Lorsque le respect des contraintes temporelles des tâches est une condition de validation de l'application embarquée, on dit qu'elle est temps réel. Généralement, l'architecture matérielle sur laquelle doit s'exécuter l'application, est dotée d'un noyau ou système d'exploitation (très souvent temps réel) conforme à une norme qui définit des services ou API nécessaires au développement. Cette norme impose ainsi les règles aux travers desquelles l'application doit être codée pour pouvoir interagir avec ce système d'exploitation. La norme OSEK/VDX fait partir de ce contexte. Elle est très vaste. Pour cela, notre objectif dans ce cours est de vous faire une présentation rapide de cette norme qui spécifie un mécanisme de développement complet d'applications embarquées temps réel. Nous terminons ce cours avec un exemple de développement d'une application embarquée destinée à s'exécuter sur un Robot Lego NXT.

Cet article est à destination de personnes ayant déjà de bonnes notions en développement (surtout avec le langage C), en systèmes temps réel embarqués et/ou en systèmes d'exploitation.

Réagissez à cet article en faisant vos remarques ici : Commentez

- Titre : Développement d'applications embarquées avec OSEK/VDX : exemple du Robot Lego NXT

- Auteur : Georges KEMAYO

- Parution : 23 avril 2014

- Mise à jour : 8 juin 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 misterkool. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Qu'est-ce que OSEK/VDX ?

- II. Les objets OSEK/VDX et leurs services

- II-A. L'objet APPMODE

- II-A-1. Déclaration dans le fichier OIL

- II-B. L'objet TASK

- II-B-1. Déclaration dans le fichier OIL

- II-B-2. Utilisation dans le fichier C

- II-C. L'objet Event

- II-C-1. Déclaration dans le fichier OIL

- II-C-2. Utilisation dans le fichier C

- II-D. L'objet RESOURCE

- II-D-1. Déclaration dans le fichier OIL

- II-D-2. Utilisation dans le fichier C

- II-E. L'objet ISR

- II-E-1. Déclaration dans le fichier OIL

- II-E-2. Utilisation dans le fichier C

- II-F. L'objet COUNTER

- II-F-1. Déclaration dans le fichier OIL

- II-F-2. Utilisation dans le fichier C

- II-G. L'objet ALARM

- II-G-1. Déclaration dans le fichier OIL

- II-G-2. Utilisation dans le fichier C

- II-H. L'objet OS

- II-H-1. Déclaration dans le fichier OIL

- II-I. L'objet CPU

- II-I-1. Déclaration dans le fichier OIL

- III. Exemple applicatif : robot Lego NXT

- III-A. Robot Lego NXT ?

- III-B. Plate-forme de développement croisé

- III-B-1. Installation des outils de base

- III-B-2. Développement avec Eclipse

- III-C. Exemple d'application

- III-C-1. Fichier OIL de l'application

- III-C-2. Fichier C de l'application

- IV. Références

- V. Remerciements

I - Qu'est-ce que OSEK/VDX ?

OSEK/VDX est une norme qui permet de définir les architectures et les interfaces destinées à la gestion des systèmes embarqués dans l'automobile. Il a été créé en 1993 par un consortium de constructeurs automobiles allemands avant d'être rejoint plus tard par les constructeurs français. Une architecture complète d'OSEK/VDX est composée de trois parties :

- OSEK COM , permet de gérer les aspects communication (échange de messages dans et entre composants électroniques automobiles) du système ;

- OSEK NM , permet de gérer le réseau permettant la communication dans le système ;

- OSEK OS , permet de spécifier les services d'un système d'exploitation temps réel pour la gestion des applications embarquées s'exécutant dans le système. C'est cette dernière partie qui nous intéresse dans ce cours.

OSEK OS est actuellement à sa version 2.2.3. C'est une norme qui définit les services d'un système d'exploitation temps réel s'exécutant sur une architecture monoprocesseur. Contrairement à la norme POSIX par exemple (voir mon cours sur Posix), la norme OSEK/VDX est destinée à la spécification des applications multitâches utilisant des ressources limitées. Par exemple des applications multitâches qui doivent s'exécuter sur une architecture à base de microcontrôleur. Pour OSEK/VDX, les moyens d'interaction entre l'application multitâche embarquée et le système d'exploitation qui s'exécute sur le matériel, sont les services. C'est donc la standardisation de ces services dont il est question et que nous allons apprendre le long de ce cours. De plus, notons que cette standardisation des services offre la relative facilité de la portabilité des applications développées, d'une architecture matérielle à une autre.

Pour spécifier une application multitâche embarquée, OSEK/VDX impose que sa définition soit statique et doit suivre la procédure suivante :

- La définition d'un fichier d'extension .oil, appelé fichier OIL (OSEK Implementation Language) dans lequel sera renseigné suivant une structuration bien définie, l'ensemble des différents objets utilisés par l'application. Ces objets sont définis au travers d'un langage également appelé langage OIL ;

- Puis la définition d'un fichier programme (d'extension .c) qui implémente en langage C les objets définis dans le fichier OIL. Notons que OSEK/VDX est une norme qui s'associe au langage de programmation C.

Ces deux fichiers correspondent aux sources (programme) de l'application embarquée. Ils sont donc tous les deux utilisés par le compilateur lors de la phase de compilation pour générer l'exécutable de l'application destinée à l'architecture cible sur laquelle elle s'exécutera.

Les différents objets OSEK/VDX que peuvent utiliser une application et qui doivent être déclarés dans le fichier OIL sont les suivants :

- l'ojet TASK : correspond à une tâche de l'application ;

- l'objet RESOURCE : nécessaire pour la protection d'une ressource critique dans l'application ;

- l'objet APPMODE : permet de définir les différents modes de fonctionnement de l'application embarquée ;

- l'ojet EVENT : permet de gérer les événements pouvant survenir pendant l'exécution du système. Il est aussi utilisé pour gérer la synchronisation entre tâches ;

- l'objet COUNTER : il s'agit d'un compteur basé sur une horloge système et qui permet de gérer les alarmes ;

- l'objet ALARM : il est basé sur l'objet COUNTER et permet de déclencher un événement (périodiquement ou non) ou d'activer une tâche (périodiquement ou non) ;

- l'objet OS : permet de spécifier comment le système d'exploitation OSEK/VDX qui s'exécute sur le CPU doit interagir avec l'application embarquée ;

- l'objet ISR : permet de gérer les routines de service d'interruption supportées par le système d'exploitation ;

- l'objet MESSAGE : définit les moyens d'échange de messages entre tâches et/ou entre ISR ;

- l'objet CPU : c'est le conteneur de tous les objets cités ci-dessus.

Dans le paragraphe suivant, nous présenterons ces différents objets. Nous montrerons comment ils sont déclarés dans le fichier OIL avec les différents attributs qui les définissent et par ailleurs, les services via lesquels ils sont appelés ou traités dans le fichier programme C.

II - Les objets OSEK/VDX et leurs services

II-A - L'objet APPMODE

Nous commençons par définir l'objet APPMODE qui est le plus simple. Il est utilisé pour permettre au système d'exploitation OSEK/VDX de fonctionner en mode application.

II-A-1 - Déclaration dans le fichier OIL

L'objet APPMODE ne dispose pas d'attribut et se déclare dans le fichier OIL de la façon suivante :

APPMODE nom_appmode
{
};

Notons que lors de la spécification d'une application embarquée sur un processeur, au moins un objet APPMODE doit être défini.

II-B - L'objet TASK

L'objet TASK correspond à une tâche qui s'exécute dans l'application. La norme OSEK/VDX définit deux types de tâches : les tâches basiques et les tâches étendues. Une tâche étendue est l'équivalent d'un processus dans le domaine des systèmes d'exploitation. C'est-à-dire que pendant son exécution elle peut se retrouver dans quatre états possibles : bloquée, endormie, prête et en cours d'exécution.

Contrairement aux tâches étendues, les tâches basiques ne disposent pas de l'état bloqué. C'est-à-dire que les tâches basiques ne peuvent pas disposer d'une section critique dans leurs codes sources. Ceci donne l'avantage que les tâches basiques peuvent s'exécuter en utilisant une même pile (optimisation des ressources), alors que chaque tâche étendue dispose de sa propre pile. C'est pourquoi pour faciliter la correspondance entre une application que l'on veut développer et les ressources dont doit disposer le système d'exploitation pour permettre son exécution, la norme OSEK/VDX définit un certain nombre de classes de conformité :
- La classe de conformité BCC1 correspond aux applications embarquées supportant uniquement des tâches basiques. Chaque tâche est limitée à une demande d'activation et dispose d'une priorité. Par ailleurs plusieurs tâches ne peuvent pas avoir la même priorité.
- La classe de conformité BCC2 est l'extension de la classe BCC1 avec la possibilité d'activation multiple d'une tâche et la possibilité pour plusieurs tâches d'avoir la même priorité.
- La classe de conformité ECC1 correspond à la classe BCC1 mais pour des tâches étendues.
- Enfin, la classe de conformité ECC2 est l'extension de la classe ECC1 avec la possibilité pour plusieurs tâches d'avoir la même priorité. Pour plus d'information sur les classes de conformité, vous pouvez vous référer ici.

Maintenant nous allons apprendre comment est déclarée une tâche dans le langage OIL du fichier .oil.

II-B-1 - Déclaration dans le fichier OIL

Une tâche OSEK/VDX se déclare de la façon :

TASK nom_tache
{
AUTOSTART = TRUE
PRIORITY = 1;
ACTIVATION = 1;
SCHEDULE = FULL;
STACKSIZE = 512;
};

- l'attribut AUTOSTART indique si la tâche est activée ou non au démarrage de l'application. Il prend les valeurs TRUE ou FALSE ;

- l'attribut PRIORITY donne la priorité de la tâche où 1 correspond à la priorité la plus basse du système ;

- l'attribut ACTIVATION indique le nombre d'activations que la tâche peut retenir à un moment donné et les prendre en compte plus tard. Dans ce cas une seule activation ;

- l'attribut SCHEDULE indique le type d'ordonnancement de la tâche. OSEK/VDX est basée sur un ordonnancement à priorité fixe avec traitement FIFO pour les tâches de même priorité. Les politiques d'ordonnancement possibles sont : FULL pour un ordonnancement avec préemption et NON sans préemption ;

- l'attribut STACKSIZE indique la taille en octets de la pile utilisée par la tâche. Dans ce cas la tâche a une pile de taille 512 octets ;

II-B-2 - Utilisation dans le fichier C

Dans le fichier programme C, avant d'utiliser un objet TASK du fichier OIL, il faut le déclarer dans le fichier C. Pour cela, il faut appeler la fonction (ou service) suivante :

DeclareTask(nom_tache)

Cette fonction est généralement appelée en tout début de programme directement après les lignes #include.

Puis la définition effective de la tâche doit se présenter de la façon suivante :

TASK(nom_tache)
{
//...
TerminateTask();
}

Le corps de la tâche s'écrit à l'aide du langage C. En outre, une tâche OSEK/VDX se termine toujours par le service TerminateTask() dont le prototype est le suivant :

StatusType TerminateTask(void)

Ce service (ou fonction si vous voulez) est obligatoire, car il permet de terminer l'instance de la tâche en cours d'exécution. StatusType est un type de donnée globale du système qui permet de savoir si le service qui vient d'être appelé a généré une erreur ou pas (c'est un peu l'équivalent de la variable errno en C standard). Ceci est vrai pour plusieurs autres services.

Quelques fois, si une tâche doit se terminer en activant une autre tâche de l'application, le service TerminateTask() est remplacé par le service chainTask(nom_tache_a_activer) dont le prototype est le suivant :

StatusType chainTask(nom_tache_a_activer)

Il existe d'autres services qui peuvent être appliqués aux tâches. Leurs prototypes sont les suivants :

1. Le service permettant d'activer une tâche dont l'identifiant nom_tache_a_activer est donné en paramètre :

StatusType ActivateTask(nom_tache_a_activer)

2. Le service permettant de mettre dans la variable globale nom_tache du programme, le nom de la tâche en cours d'exécution dans le système. Ce service peut être appelé par une tâche de l'application ou une ISR (Interrupt Service Request) définie dans l'application :

StatusType GetTaskID(nom_tache)

3. Le service permettant de mettre dans la variable globale etat du programme, l'état (bloqué, endormie, en cours d'exécution ou prête) de la tâche dont l'identifiant est nom_tache. Ce service peut être appelé par une tâche de l'application ou une ISR (Interrupt Service Request) définie dans l'application :

StatusType GetTaskState(nom_tache, etat)

4. Le service appelé par la tâche en cours d'exécution pour s'endormir et lancer l'exécution de l'éventuelle tâche la plus prioritaire qui attend dans l'état prêt :

StatusType Schedule(void)

II-C - L'objet Event

L'objet EVENT permet de synchroniser deux tâches entre elles de façon à créer un ordre de précédence de leurs exécutions. Un événement permet à une tâche de passer dans l'état bloqué. Étant donné que seules les tâches étendues peuvent se retrouver dans cet état, elles sont donc les seules à pouvoir utiliser l'objet EVENT. Une tâche possédant l'objet EVENT est celle qui attend sur cet événement. Toute autre tâche peut lui transmettre l'événement attendu pour la faire passer à l'état prêt.

II-C-1 - Déclaration dans le fichier OIL

L'objet EVENT est déclaré dans le fichier OIL de la façon suivante:

EVENT un_evt
{
MASK = AUTO;
};

TASK nom_tache_a_declencher
{
//...
EVENT = un_evt;
};

L'attribut MASK correspond à un entier de type UINT64 qui identifie de manière unique l'événement dans le système. Comme sur l'exemple précédent, il peut être initialisé par la valeur AUTO. Dans ce cas, c'est le système qui se charge de lui affecter une valeur unique.

II-C-2 - Utilisation dans le fichier C

Dans le fichier programme C, avant d'utiliser un objet EVENT du fichier OIL, il faut le déclarer dans le fichier C. Pour cela, il faut appeler la fonction (ou service) suivante :

DeclareEvent(un_evt)

Plusieurs services sont offerts pour le traitement de l'objet EVENT. Ce sont :

1. Le service appelé dans le corps d'une tâche ou d'une ISR quelconque pour lui permette de transmettre l'événement nommé un_evt à la tâche nom_tache_a_declencher qui le possède :

StatusType SetEvent(nom_tache_a_declencher, un_evt)

2. Le service permettant à une tâche (exemple, nom_tache_a_declencher) possédant l'événement un_evt d'attendre sur ce dernier :

StatusType WaitEvent(un_evt)

3. Le service permettant d'effacer l'attente sur un événement :

StatusType ClearEvent(un_evt)

4. Le service appelé dans le corps d'une tâche ou d'une ISR quelconque pour récupérer l'état courant de l'attribut MASK de l'événement un_evt que possède la tâche nom_tache_a_declencher :

StatusType GetEvent(nom_tache_a_declencher, un_evt)

L'exemple ci-dessous montre la structuration de deux tâches manipulant un événement :

TASK(nom_tache_declencheur_evt)
{
//...
SetEvent(nom_tache_a_declencher, un_evt);
TerminateTask();
}

TASK(nom_tache_a_declencher)
{
WaitEvent(un_evt);
//...
ClearEvent(un_evt) //effacer l'événement pour permettre sa réutilisation par la suite
TerminateTask();
}

II-D - L'objet RESOURCE

L'objet RESOURCE permet de gérer les accès concurrents à des ressources partagées. Ces ressources peuvent être une donnée (zone mémoire), un matériel (ex : périphérique d'entrée/sortie), l'ordonnanceur du système d'exploitation. Il permet ainsi de mettre en place une section critique dans le code d'une tâche. Cet objet peut être traité par une tâche étendue ou un ISR.

II-D-1 - Déclaration dans le fichier OIL

L'objet RESOURCE est déclaré dans le fichier OIL de la façon suivante :

RESOURCE une_ressource
{
RESOURCEPROPERTY = STANDARD;
};

TASK nom_tache
{
//...
RESOURCE = une_ressource;
};

L'attribut RESOURCEPROPERTY correspond à une variable de type ENUM qui peut prendre les valeurs suivantes :
- STANDARD indique que la ressource est une ressource normale, c'est-à-dire non liée à une autre et n'est pas une ressource interne ;
- LINKED indique que la ressource est liée à une autre ressource de type STANDARD ou LINKED. Laquelle ressource doit disposer d'un attribut supplémentaire LINKEDRESOURCE de type RESOURCE_TYPE ;
- INTERNAL indique que la ressource est interne et ne peut donc être accessible par l'application.

II-D-2 - Utilisation dans le fichier C

Dans le fichier programme C, avant d'utiliser un objet RESOURCE du fichier OIL, il faut le déclarer dans le fichier C. Pour cela, il faut appeler la fonction (ou service) suivante :

DeclareResource(une_ressource)

Plusieurs services sont offerts pour le traitement de l'objet RESOURCE. Ce sont :

1. Le service appelé dans le corps d'une tâche ou d'une ISR quelconque pour entrer dans la section critique dans l'objet une_ressource permet de mettre en place l'exclusion mutuelle :

StatusType GetResource(une_ressource)

2. Le service permettant à une tâche ou à une ISR de sortir de la section critique :

StatusType ReleaseResource(une_ressource)

L'exemple ci-dessous montre la mise en place dans le fichier C d'une exclusion mutuelle pour protéger une donnée partagée dans le corps de la tâche nom_tache :

typedef struct{
float temperature ;
float pression ;
}type_donnee_partagee ;

type_donnee_partagee donnee_partagee;  //variable globale dans le programme

TASK(nom_tache)
{
//...
GetResource(une_ressource);
//section critique : utilisation de donnee_partagee
ReleaseResource(une_ressource);
//...
TerminateTask();
}

II-E - L'objet ISR

L'objet ISR représente les routines de traitement d'interruption. Il permet de traiter les interruptions qui surviennent dans le système. Ces interruptions peuvent provenir d'un signal apparaissant sur une entrée du microcontrôleur, d'un port réseau ou série, de l'expiration d'un compteur qui déclenche une action, etc.

Il existe deux types d'ISR : les ISR de type 1 et les ISR de type 2. Les ISR de type 1 ne font pas appel aux services du système d'exploitation, car elles s'exécutent dans la pile de la tâche qu'elle préempte. Les ISR de type 2 disposent chacune de leur propre pile et peuvent donc faire appel aux services du système.

II-E-1 - Déclaration dans le fichier OIL

L'objet ISR est déclaré dans le fichier OIL de la façon suivante :

ISR traiteur_interruptions
{
CATEGORY = 2;
PRIORITY = 7;
//...
};

L'attribut CATEGORY définit la catégorie de l'ISR. Il correspond à une variable de type UINT32 qui ne peut prendre que les valeurs 1 et 2. L'attribut PRIORITY est un entier qui indique la priorité (1 étant la plus petite) de l'ISR parmi les ISR définies dans l'application. Par ailleurs, une ISR peut utiliser d'autres objets tels que des RESOURCE, EVENT, MESSAGE, etc. Pour cela, ils doivent être déclarés à l'intérieur de ce dernier.

II-E-2 - Utilisation dans le fichier C

Dans le fichier programme C, une ISR est déclarée de la façon suivante :

ISR(traiteur_interruptions)
{
//..
}

Plusieurs services sont offerts pour la gestion des ISR dans le système. Ce sont :

1. Le service appelé pour désactiver les interruptions masquables du système :

void DisableAllInterrupts(void)

2. Le service permettant de réactiver les interruptions désactivées par le service DisableAllInterrupts :

void EnableAllInterrupts(void)

3. Le service appelé pour désactiver les ISR de type 2 :

void SuspendOSInterrupts(void)

4. Le service permettant de réactiver les interruptions de type 2 mais aussi de type 1 :

void ResumeOSInterrupts(void)

Notons également qu'il existe aussi deux autres services que sont : SuspendAllInterrupts et ResumeAllInterrupts.

II-F - L'objet COUNTER

L'objet COUNTER sert de base à l'objet ALARM (que nous verrons par la suite). Il permet comme son nom l'indique de créer un décompte dans le but de déclencher une alarme. L'essentiel de sa définition est effectué dans le fichier OIL.

II-F-1 - Déclaration dans le fichier OIL

L'objet COUNTER est déclaré dans le fichier OIL de la façon suivante :

COUNTER nom_compteur
{
MINCYCLE = 1;
MAXALLOWEDVALUE = 500000;
TICKSPERBASE = 1;
};

- L'attribut MINCYCLE de type UINT32, il permet de définir le nombre minimum de ticks (unité de temps de base de l'OS OSEK/VDX) obligatoire pour une alarme qui y est reliée ;

- L'attribut MAXALLOWEDVALUE , de type UINT32, il définit la valeur maximale du compteur ;

- L'attribut TICKSPERBASE de type UINT32, il définit le nombre de ticks d'horloge entre chaque comptage (dans l'exemple, on compte avec un pas de 1).

II-F-2 - Utilisation dans le fichier C

L'utilisation de l'objet COUNTER dans le fichier programme C est simple et ne nécessite que la fonction suivante :

DeclareCounter(nom_compteur)

II-G - L'objet ALARM

L'objet ALARM est un outil qui peut être utilisé dans l'application pour activer une tâche de façon ponctuelle ou périodique. Il utilise un compteur qui est un objet de type COUNTER. Une alarme ne peut utiliser qu'un seul compteur. Mais un compteur peut être utilisé par plusieurs alarmes.

II-G-1 - Déclaration dans le fichier OIL

L'objet ALARM est déclaré dans le fichier OIL de la façon suivante :

ALARM nom_alarme
{
COUNTER = nom_compteur; //le compteur doit avoir été défini
ACTION = ACTIVATETASK
{
TASK = nom_tache; //la tâche doit avoir été définie
};
AUTOSTART = TRUE
{
ALARMTIME = 5;
CYCLETIME = 9;
APPMODE = appmode1;
};
};

Trois attributs sont importants à sa définition : COUNTER, ACTION et AUTOSTART.

- L'attribut COUNTER pour indiquer le compteur utilisé par l'alarme.

- L'attribut ACTION peut être utilisé pour réaliser trois opérations : la simple activation d'une tâche comme sur l'exemple précédent ; le déclenchement d'une tâche via la transmission d'un événement sur lequel elle attend. Dans ce cas, le code précédent traitant de l'action de l'alarme est remplacé par le suivant :

ACTION = SETEVENT
{
TASK = nom_tache; //la tâche qui attend sur l'événement. Doit déjà être définie
EVENT = un_evt; //l'événement à transmettre. Doit déjà être défini
};

L'attribut ACTION permet aussi d'appeler une routine de type alarm-callback. Nous n'entrons pas en détail pour ce cas.

- L'attribut AUTOSTART de type boolean permet de définir si l'alarme s'active ou non automatiquement au démarrage du système. Il prend donc les valeurs TRUE ou FALSE. Lorsque sa valeur est TRUE, il faut spécifier les paramètres suivants :

- ALARMTIME de type UINT32 qui représente la date au bout de laquelle l'alarme expirera pour la toute première fois ;

- CYCLETIME de type UINT32 qui représente la période de l'alarme si elle est cyclique (cas de l'exemple : 5 + 9; (5 + 9) + 9; ...) ;

- APPMOD la liste des modes dans lesquels le démarrage de l'alarme doit être effectué.

II-G-2 - Utilisation dans le fichier C

Dans le fichier programme C, un objet ALARM est préalablement déclaré avant toute utilisation via la fonction suivante :

DeclareAlarm(nom_alarme)

Plusieurs services sont offerts pour la gestion des ALARM dans le système. Ce sont :

1. Le service appelé pour mettre dans la variable info les informations sur l'alarme nommée nom_alarm :

StatusType GetAlarmBase(nom_alarme, info)

2. Le service appelé pour mettre dans la variable nombre_ticks_restant le nombre de ticks restants avant l'expiration de l'alarme nommée nom_alarm :

StatusType GetAlarm(nom_alarme, nombre_ticks_restant)

3. Le service permettant de configurer l'alarme nommée nom_alarme pour qu'elle expire au bout d'un certaine temps. La variable nombre_ticks correspond à son nombre relatif de ticks pour faire un cycle. Si l'alarme est cyclique/périodique, alors sa période est indiquée dans la variable temps_cycle. Cette dernière vaut zéro si l'alarme n'est pas cyclique :

StatusType GetAlarm(nom_alarme, nombre_ticks, temps_cycle)

4. Le service permettant de configurer l'alarme nommée nom_alarme pour qu'elle expire à une date donnée. La variable nombre_ticks correspond à son nombre absolu de ticks pour faire un cycle. Si l'alarme est cyclique/périodique, alors sa période est indiquée dans la variable temps_cycle. Cette dernière vaut zéro si l'alarme n'est pas cyclique :

StatusType GetAlarm(nom_alarme, nombre_ticks, temps_cycle)

II-H - L'objet OS

L'objet OS est utilisé pour définir les propriétés que le système d'exploitation OSEK/VDX doit mettre à disposition de l'application embarquée.

II-H-1 - Déclaration dans le fichier OIL

Un exemple de déclaration de l'objet OS dans le fichier OIL est la suivante :

OS nom_OS
{
STATUS = EXTENDED;
STARTUPHOOK = FALSE;
ERRORHOOK = FALSE;
SHUTDOWNHOOK = FALSE;
PRETASKHOOK = FALSE;
POSTTASKHOOK = FALSE;
USEGETSERVICEID = FALSE;
USEPARAMETERACCESS = FALSE;
USERESSCHEDULER = FALSE;
};

Comme vous l'avez constaté, tous les attributs définis dans l'objet OS sont de type booléen à l'exception de l'attribut STATUS.

- STATUS de type ENUM, il permet de spécifier le mode de fonctionnement du système (standard ou étendu). Il prend les valeurs STANDARD ou EXTENDED ;

- STARTUPHOOK , permet de spécifier si l'application se lance exactement à la fin de l'initialisation du système d'exploitation et avant le début d'exécution de l'ordonnanceur ;

- ERRORHOOK , permet de spécifier si le système d'exploitation prend la main à la fin de l'exécution de tout service pour savoir s'il s'est bien exécuté ou pas ;

- SHUTDOWNHOOK , permet de spécifier si le système d'exploitation s'arrête à la fin de l'application ;

- PRETASKHOOK , traite du changement de contexte des tâches. Il permet de spécifier si le système d'exploitation prend la main juste avant que le processeur soit donné à une tâche, pour contrôler son état ;

- POSTTASKHOOK , traite du changement de contexte des tâches. Il permet de spécifier si le système d'exploitation prend la main juste après que le processeur soit retiré à la tâche en cours d'exécution, pour contrôler son état ;

- USEGETSERVICEID et USEPARAMETERACCESS , sont liés à ERRORHOOK. Ils permettent de spécifier si l'on autorise l'accès à certaines informations ;

- USERESSCHEDULER , permet de spécifier si la ressource RES_SCHEDULER (l'ordonnanceur du système d'exploitation) est utilisée par l'application comme une ressource ordinaire.

II-I - L'objet CPU

L'objet CPU est utilisé comme conteneur de tous les autres objets définis plus haut : APPMODE, TASK, OS, COUNTER, ALARM, RESOURCE, COUNTER, MESSAGE.

II-I-1 - Déclaration dans le fichier OIL

La déclaration de l'objet CPU dans le fichier OIL est la suivante :

CPU nom_du_cpu
{
//ici la description OIL de tous les objets OSEK/VDX dont utilise
//l'application embarquée
};

III - Exemple applicatif : robot Lego NXT

III-A - Robot Lego NXT ?

Le robot Lego NXT est un outil académique multifonctions programmable sur lequel s'exécute le système d'exploitation nxtOSEK conforme à OSEK/VDX. L'exemple d'un Robot Lego NXT est donné par la figure ci-dessous :

Il est obtenu par assemblage d'un ensemble de briques de base en fonction du type d'opération à réaliser par l'application qui lui est embarquée. Les robots Lego NXT sont munis d'un microcontrôleur ARM7 32 bits, de trois servo-moteurs, de quatre ports d'entrée (numérotés de 1 à 4) pour la connexion de différents capteurs, de trois ports de sortie (numérotés de A à C) pour la commande des servomoteurs, d'un port USB 2.0, des fonctionnalités Bluetooth, etc. Les différents capteurs qui peuvent lui être connectés sont par exemple un capteur à ultrasons, un capteur de contact, un capteur lumineux, un capteur sonore, etc. Enfin, le robot NXT dispose d'une API (Application Programmable Interface) qui doit être associée à du code C pour la programmation de l'ensemble de ses différents composants (capteurs, servomoteurs...). Cette API est consultable sur le lien suivant http://lejos-osek.sourceforge.net/ecrobot_c_api_frame.htm.

III-B - Plate-forme de développement croisé

III-B-1 - Installation des outils de base

Il est possible de développer un programme embarqué conforme à OSEK/VDX, tant sur Linux que sur Windows. Nous nous focalisons ici sur le cas de Windows. La mise en place de la plateforme de développement OSEK/VDX sur Windows nécessite trois technologies :

- Cygwin : c'est un outil qui permet d'émuler certaines fonctionnalités de Linux sur le système d'exploitation Windows ;

- GNU ARM : c'est une distribution du compilateur GCC (GNU Compiler Collection) permettant de compiler du code destiné à s'exécuter sur des microcontrôleurs de type ARM. Il supporte ainsi le microcontrôleur ARM7 dont dispose le robot Lego NXT ;

- nxtOSEK : c'est le système d'exploitation conforme à OSEK/VDX s'exécutant sur le robot Lego NXT et qui est nécessaire dans la phase de compilation du code sur la machine hôte de développement.

La procédure d'installation de la plateforme de développement de programmes basés sur OSEK/VDX sur Windows est décrite sur le lien suivant : http://lejos-osek.sourceforge.net/installation_windows.htm.
Nous résumons ici en français cette procédure d'installation :

- Téléchargez la dernière version de Cygwin ici et installez le (par exemple dans le répertoire C:\cygwin). Pendant la phase d'installation de Cygwin, en vous servant des captures d'écran présentées dans le lien d'installation d'origine, choisissez la version la plus récente de make (version supérieure ou égale à la 3.8) ainsi que les bibliothèques libintl1 à libintl8 et libncurves7 à libncurves10 ;

- Téléchargez la dernière version de l'installateur (le .exe) de GNUARM ici et installez-le (par exemple dans le répertoire C:\GNUARM). Pendant la phase d'installation de GNUARM, en vous servant des captures d'écran présentées dans le lien d'installation d'origine, décochez l'installation de FPU (Floating Point Unit) et Big Endian, car le processeur ARM7 du robot Lego n'en a pas besoin. Dans la fenêtre suivante, GNUARM vous proposera d'installer une nouvelle fois Cygwin, décochez cette case, car vous l'avez déjà installé ;

- Téléchargez ici le fichier compressé permettant d'installer sur votre machine les pilotes du robot Lego NXT. Ces pilotes sont nécessaires dans la phase de compilation sur votre machine. Dézippez-le dans un répertoire de votre choix (sur ma machine, je l'ai fait dans E:\OSEK). Puis vous rendre dans le répertoire suivant E:\OSEK\NXT Fantom Drivers\Windows\1.2\1.2.0\Products. En fonction que votre machine est une 32 ou une 64 bits, choisissez le bon setup d'installation des pilotes dans ce répertoire et exécutez le ;

- Lorsque le codage de votre application sera terminé sur votre machine de développement, vous aurez besoin de le flasher sur le robot pour qu'il puisse l'exécuter. C'est l'outil NXTTool qui gère cette opération de transfert de code vers le robot. Téléchargez ici le fichier compressé de NXTTool et dézippez-le dans le répertoire d'installation de Cygwin (exemple C:\cygwin\nxttool) ;

- Pour que NXTTool fonctionne correctement, il a besoin du firmware NXT. Téléchargez ici le fichier compressé du firmware NXT. Dézippez-le dans un répertoire de votre choix et copiez son contenu (constitué des fichiers d'extension .rfw) dans le répertoire d'installation de NXTTool (exemple C:\cygwin\nxttool) ;

- Il reste maintenant à installer nxtOSEK. Téléchargez ici le fichier compressé d'nxtOSEK et dézippez-le dans le répertoire d'installation de Cygwin (exemple C:\cygwin\nxtOSEK). Dans les dernières versions de nxtOSEK (par exemple le nxtOSEK 2.18), les auteurs ont décidé de ne plus le livrer avec le fichier exécutable sg.exe qui doit normalement se situer dans le répertoire \nxtOSEK\toppers_osek\sg. Cet exécutable joue un rôle important, car il permet de parser et de générer le code cible destiné au robot. Pour rajouter sg.exe dans le répertoire d'installation de nxtOSEK (C:\nxtOSEK\toppers_osek\sg) sur votre machine, téléchargez le fichier compressé osek_os-1.1.lzh ici. Dézippez le dans un répertoire de votre choix, copiez le fichier sg.exe et collez le dans votre répertoire C:\nxtOSEK\toppers_osek\sg.

- Pour flasher effectivement votre code sur le robot NXT, vous aurez besoin d'un câble USB à brancher entre votre machine et le robot. Pour faire que votre machine puisse reconnaître le robot NXT (à l'autre bout du câble USB) comme un périphérique externe (un peu comme une clé USB), vous devez installer l'outil libusb dont le setup se trouve dans le répertoire d'installation de nxtOSEK (exemple : C:\cygwin\nxtOSEK\lejos_nxj\3rdparty\lib).

Les opérations ci-dessus vous ont permis d'installer tous les logiciels qu'il faut pour compiler vos programmes et pouvoir les flasher sur le robot, mais ce n'est pas terminé. Pour que tout fonctionne correctement, rendez-vous dans le dossier ecrobot (situé dans le répertoire C:\cygwin\nxtOSEK), ouvrez (par exemple avec notePad++) le fichier tool_gcc.mak et renseignez les chemins d'installation de GNUARM et NXTTool, respectivement dans les variables GNUARM_ROOT et NXTTOOL_ROOT (exemple : GNUARM_ROOT=C:/GNUARM et NXTTOOL_ROOT=C:/cygwin/nexttool). Ces répertoires sont nécessaires pendant la phase de compilation.

Dans le dossier sample_c appartenant au répertoire d'installation de nxtOSEK (C:\cygwin\nxtOSEK\sample_c), il existe plusieurs programmes exemples que vous pouvez compiler pour tester votre installation complète de la plateforme. Si vous choisissez le programme helloworld par exemple, démarrez le terminal de Cygwin. Dans ce terminal, saisissez la commande cd C:/cygwin/nxtOSEK/samples_c/helloworld pour vous déplacer dans le répertoire helloworld. Puis saisissez la commande make all pour compiler le programme exemple helloworld. Si la compilation se déroule bien, c'est que votre installation de la plateforme de développement OSEK/VDX s'est bien passée. Vous pouvez ainsi commencer le codage de vos propres programmes embarqués.

III-B-2 - Développement avec Eclipse

Après avoir installé toute la plateforme de développement ci-dessus, vous pouvez coder vos applications via un EDI (Environnement de Développement Intégré) tel que Eclipse CDT. Avec Eclipse : vous bénéficiez de la coloration de code qui vous aidera à rapidement détecter des erreurs ; vous disposez de la possibilité de compiler et de déboguer votre code en un clic, ce qui vous affranchit des commandes à connaître et à saisir dans le terminal de Cygwin pour effectuer le même traitement ; vous disposez enfin de la possibilité de configurer le flashage de votre code sur le robot afin de rendre ce processus beaucoup plus simple que les commandes à saisir dans le terminal de Cygwin. Le lien suivant http://lejos-osek.sourceforge.net/eclipse.htm vous décrit la procédure à suivre pour l'installation d'Eclipse CDT et sa configuration. Cette procédure d'installation et de configuration d'Eclipse CDT est simple et nous ne la redécrivons pas ici. Par contre, l'étape 1 peut être omise dans cette procédure si vous téléchargez directement Eclipse dédié au développement C/C++ nommé Eclipse IDE for C/C++ Developers.

III-C - Exemple d'application

Pour mettre en pratique les notions vues précédemment, nous allons développer dans cette partie, une petite application embarquée. Le sujet est le suivant :

Programmation d'un robot Lego qui roule librement et qui freine et change de direction lorsqu'il détecte un obstacle à une distance inférieure à 20cm devant lui.

Pour résoudre ce problème, nous considérons que le robot dispose : de deux roues qui lui permettent de se déplacer ; d'un capteur à ultrasons qui lui permet de détecter les éventuels obstacles sur son chemin. L'application doit alors disposer de :
- deux tâches : la tâche acquerir_ultrason qui permet de récupérer de manière régulière la distance entre le robot et un éventuel obstacle ; et la tâche rouler qui permet au robot de se déplacer en essayant d'éviter les obstacles ;
- une donnée partagée entre les deux tâches et devant contenir l'information (fournie par acquerir_ultrason) selon laquelle il y a un obstacle ou pas devant le robot. La tâche rouler doit donc régulièrement scruter cette donnée afin de savoir l'action à réaliser (continuer de rouler ou freiner pour tourner et rouler).

III-C-1 - Fichier OIL de l'application

Étant donné que l'application est composée de deux tâches, il faut bien entendu les déclarer dans le fichier OIL. Ces dernières accèdent à une même donnée. Un accès en exclusion mutuelle est alors nécessaire pour garder sa cohérence, d'où le besoin de l'objet RESOURCE. De même les tâches acquerir_ultrason et rouler doivent se réveiller de manière périodique pour respectivement écrire et lire la donnée informant de la présence d'un obstacle ou non. D'où le besoin des objets COUNTER et ALARM pour réaliser la périodicité. Notons qu'au démarrage de l'application, c'est l'alarme qui doit déclencher les tâches.

Le fichier OIL de l'application est le suivant :

#include "implementation.oil"

CPU ATMEL_AT91SAM7S256
{
OS LEJOS_OSEK
{
STATUS = EXTENDED;
STARTUPHOOK = FALSE;
ERRORHOOK = FALSE;
SHUTDOWNHOOK = FALSE;
PRETASKHOOK = FALSE;
POSTTASKHOOK = FALSE;
USEGETSERVICEID = FALSE;
USEPARAMETERACCESS = FALSE;
USERESSCHEDULER = FALSE;
};

// Définition du mode de l'application
APPMODE appmode1{};

// Définition de la tâche acquerir_ultrason
TASK acquerir_ultrason
{
AUTOSTART = FALSE;
PRIORITY = 2;
ACTIVATION = 1;
SCHEDULE = FULL;
STACKSIZE = 512;
RESOURCE = obstacle;
};

// Définition de la tâche rouler
TASK rouler
{
AUTOSTART = FALSE;
PRIORITY = 1;
ACTIVATION = 1;
SCHEDULE = FULL;
STACKSIZE = 512;
RESOURCE = obstacle;
};

// Définition de la ressource pour l'exclusion mutuelle
RESOURCE obstacle
{
RESOURCEPROPERTY = STANDARD;
};

// Définition du compteur utilisé par l'alarme
COUNTER compteur
{
MINCYCLE = 1;
MAXALLOWEDVALUE = 100000;
TICKSPERBASE = 1;
};

// Définition de l'alarme périodique utilisée pour réveiller la tâche acquerir_ultrason
ALARM alarme_reveil_acquerirUltrason
{
COUNTER = compteur;
ACTION = ACTIVATETASK
{
TASK = acquerir_ultrason;
};
AUTOSTART = TRUE
{
ALARMTIME = 10;
CYCLETIME = 10;
APPMODE = appmode1;
};

};

// Définition de l'alarme périodique utilisée pour réveiller la tâche rouler
ALARM alarme_reveil_rouler
{
COUNTER = compteur;
ACTION = ACTIVATETASK
{
TASK = rouler;
};
AUTOSTART = TRUE
{
ALARMTIME = 10;
CYCLETIME = 10;
APPMODE = appmode1;
};

};

};

III-C-2 - Fichier C de l'application

Ci-dessous le code C de l'application. Il est important de se référer à l'API du robot NXT pour comprendre certaines fonctions utilisées dans le programme.

#include "kernel.h"
#include "kernel_id.h"
#include "ecrobot_interface.h"

DeclareResource(obstacle);
DeclareTask(acquerir_ultrason);
DeclareTask(rouler);
DeclareCounter(compteur);
DeclareAlarm(alarme_reveil_acquerirUltrason);
DeclareAlarm(alarme_reveil_rouler);

//structure de donnée informant de l'existence ou non d'un obstacle
typedef struct{
int obstacleEnVue; // vaut 1 si obstacle et 0 sinon
}type_Obstacle;

//routine de traitement d'interruption nxtOSEK de type 2,
//elle est fournie par l'API du robot et doit figurer dans
//la liste des fonctions du programme
void user_1ms_isr_type2(void){
StatusType ercd;
// Increment System Timer Count
ercd = SignalCounter(compteur);
if(ercd != E_OK){
ShutdownOS(ercd);
}
}

//fonction d'initialisation des pilotes de matériels utilisés par le robot
//dans l'application. Elle est fournie par l'API du robot et doit obligatoirement
//figurer dans la liste des fonctions du programme
void ecrobot_device_initialize(void){
// Initialize ECRobot used devices
ecrobot_init_sonar_sensor(NXT_PORT_S1);
}

//déclaration de la variable globale partagée par les deux tâches
type_Obstacle donneeObstacle;

//corps de la tâche acquerir_ultrason
TASK(acquerir_ultrason){
int distance = ecrobot_get_sonar_sensor(NXT_PORT_S1);
GetResource(obstacle);
if(distance > 20){
donneeObstacle.obstacleEnVue = 0;
}else{
donneeObstacle.obstacleEnVue = 1;
}
ReleaseResource(obstacle);
TerminateTask();
}

//corps de la tâche rouler
TASK(rouler){
int vitesseDeplacementRobot = 70;
int angleRotationRobot = 45;
int obs = 0;
GetResource(obstacle);
obs = donneeObstacle.obstacleEnVue;
ReleaseResource(obstacle);
if(obs == 0){
//commander les deux roues du robot à rouler :
nxt_motor_set_speed(NXT_PORT_A, vitesseDeplacementRobot, 0);
nxt_motor_set_speed(NXT_PORT_B, vitesseDeplacementRobot, 0);
}else{
//commander les roues du robot à tourner :
nxt_motor_set_speed(NXT_PORT_A, vitesseDeplacementRobot - 30, 1); //freiner en réduisant de 30 la vitesse de la 1ère roue du robot
nxt_motor_set_speed(NXT_PORT_B, -40, 1); //freiner en essayant d'imobiliser (vitesse négative) la 2ème roue, pour permettre au robot de pivoter
nxt_motor_set_count(NXT_PORT_A, angleRotationRobot);
}
TerminateTask();
}

IV - Références

Pour plus de détail sur la norme OSEK/VDX, vous pouvez consulter les sites suivants :

- http://portal.osek-vdx.org/files/pdf/specs/os223.pdf

- http://portal.osek-vdx.org/files/pdf/specs/oil25.pdf

- http://www.cs.ru.nl/lab/nxt/nxtosek/OSEK/OSEK_API.html#0.1_60

V - Remerciements

Je tiens à remercier LittleWhite pour sa relecture technique de cet article. Un grand merci à f-leb pour sa minutieuse relecture orthographique.