L'Ada

Le language

1 - L'environnement de développement

Pour pouvoir utiliser Ada sur le robot, il faut d'abord compiler son programme.

Dans le robot, on n’utilise pas un bon vieux Pentium, mais un SH4. C'est un microprocesseur comme les Pentium, mais il ne parle pas le même langage machine. Si on utilise un compilateur Ada "standard", ça ne marche pas, ce compilateur va générer un exécutable pour Pentium et pas pour SH4. Pour pouvoir lancer un programme sur le robot, il faut le compiler avec un compilateur spécial, qui génère du code pour SH4.

Dans le cas de l'ENST, c'est facile, ce compilateur est installé sur les machines de COMELEC, il se trouve dans /comelec/softs/opt/gnu_tools_for_sh4/gcc_gnat_sh4/bin. Donc sur une machine COMELEC, avant toute chose, il faut taper :

export PATH=/comelec/softs/opt/gnu_tools_for_sh4/gcc_gnat_sh4/bin:$PATH

Ensuite, on peut compiler un programme avec

sh4-unknown-linux-gnu-gnatmake mon_programme.adb 

Cela crée un exécutable pour SH4 mon_programme, qu'il faut copier sur une carte Stratix et lancer depuis cette carte. Il n'est pas toujours facile de trouver une carte Stratix juste pour tester un hello world, mais heureusement, pour tester quelque chose qui n'est pas propre au robot (et qui n'a pas besoin d'être exécuté sur une carte Stratix parce ça n'utilise pas les capteurs du robot ni rien), on peut utiliser un compilateur Ada "standard". À l'ENST, il y en a un installé sur certaines machines, par exemple fracasse. Sinon, on peut aussi l'installer facilement, par exemple apt-get install gnat sur une debian marche très bien. Pour compiler un programme pour l'utiliser sur un Pentium :

 gnatmake mon_programme.adb 

2 - Premier programme

Voici un premier programme en Ada, hello.adb

with Text_IO; use Text_IO;
procedure hello is
begin
  Put("hello world!");
end hello;

Remarques générales :

Remarque spécifique:

La première ligne ressemble un peu à un #include du C. with some_package veux dire que l'on va utiliser des fonctions de some_package. Ici, on veut utiliser Put qui affiche un texte à l'écran. Afficher un texte fait partie des entrées-sorties de textes et donc cette fonction se trouve logiquement dans le package Text_IO.

Maintenant qu'on a dit au compilateur qu'on allait utiliser des fonctions de Text_IO, deux choix s'offrent à nous : soit on préfixe chaque appel de fonction par Text_IO., ce qui donnerait Text_IO.Put("hello world!");, soit on dit au compilateur de se débrouiller tout seul avec use Text_IO;. Cette directive indique au compilateur de chercher dans ce package s’il ne connaît pas une fonction. Ici, lorsqu'il va voir Put("hello world!"), il ne connaît pas de fonction Put, mais comme on lui a dit d'aller chercher dans Text_IO, il va regarder dans ce package et la trouver.

À la 2ème ligne, on définit une procédure, c'est à dire une fonction qui ne renvoie rien. La définition d'une procédure suit le schéma suivant :

procedure nom_de_la_procedure(liste des arguments) is 
  declaration des variables 
begin 
  liste d'instructions
end nom_de_la_procedure; 

Ici, notre procédure hello n'a pas d'argument donc on ne met rien, même pas les parenthèses. La liste d'instructions se réduit à une instruction : Put("hello world");. Cette instruction est un appel de fonction, la fonction Put qui a un argument : une chaîne de caractères. Comme en C, les chaînes de caractères s'écrivent entre guillemets doubles ", "hello world!" est donc une chaîne de caractères. Les caractères seuls s'écrivent entre apostrophes ', toujours comme en C.

Voilà pour le 1er programme.

3 - Les variables

Où déclarer ses variables ?

Pour déclarer une variable, il faut être à un endroit où on a le droit de déclarer des variables, typiquement juste après leisde définition d'une fonction. Si on veut déclarer une variable à un autre endroit, il existe une autre méthode : on peut remplacer une instruction par un bloc.

declare 
  declaration de variables
begin
 liste d'instructions pour laquelle la variable existe
end; 

Ceci déclare de nouvelles variables valides uniquement pour les instructions situées entre lebeginet leend;. Après leend;, les variables déclarées entre ledeclareet lebeginn'existent plus.

La déclaration d'une variable

Une déclaration de variable suit le schéma suivant :

 nom_de_la_variable : [constant] type_de_la_variable [:= valeur_initiale]; 

La valeur initiale est facultative. Si cette variable ne change jamais, il est conseillé (avec certaines options de compilation, il est même obligatoire) de la noterconstant. Si on veut déclarer plusieurs variables du même type, on peut les regrouper :

 variable1, variable2 [...] : [constant] type_des_variables [:= valeur_initiale]; 

Les types de base

En Ada, on peut faire des types très contraints, comme par exemple, les entiers pairs entre 10 et 50. On reviendra sur ce point plus tard. Les types de bases sont

Booleanavec deux valeurs:trueetfalse,

Integerpour les entiers,

Floatpour les réels,

Characterpour les caractères,

Stringpour les chaînes de caractères.

Les entiers

Il existe plusieurs façons de noter un entier en Ada, par exemple :

 e1 : Integer := 1E6; e2 : Integer := 1_000_0_00; e3 : Integer := 16#12AB#; 

On peut naturellement écrire l'entier tel quel. Pour améliorer la lisibilité, on peut aussi mettre le caractère_n'importe où pour séparer différents groupes de chiffres (dans l'exemple ci-dessus, la lisibilité n'est pas vraiment meilleure, c'est juste pour montrer qu'on peut mettre un_n'importe où). On peut aussi utiliser la notationbase#nombre#pour spécifier un nombre dans une base autre que le décimal. La notationnombreEexposantdésigne le nombrenombre*base^exposantavecbase=10 par défaut.

Les chaines de caractères

Le typeStringd'Ada est très différent des "classes"stringque l'on rencontre en C++ ou Java, il se rapproche plus du typechar[]. En effet une chaîne a une taille fixe qui ne peut pas changer. Par exemple:

procedure taille_string is 
  s : String := "coucou";
begin
  s := "test"; -- Ceci ne compile pas
end;

échoue à la compilation avec un message du genre

taille_string:4:09: warning: wrong length for array of subtype of "Standard.String" defined at line 2.

En effet, la ligne 2 déclare une chaîne de caractères de taille 6, on ne peut donc pas lui affecter une chaîne de caractères de taille 4 comme"test". Pour déclarer une chaîne sans l'initialiser, on utilise la syntaxe

 nom_de_la_variable : String(indice_du_premier_element..indice_du_dernier_element); 

Exemple

Voici un petit exemple de programme avec une "variable" :

with Text_IO; use Text_IO;  
 
procedure put_var is 
begin
  Put("Veuillez entrer votre nom: ");
  declare
    name : constant String := Get_Line;
  begin
    Put("Bonjour ");
    Put_Line(name);
  end;
end put_var; 

Dans ce programme,Get_Linelit une ligne entrée au clavier. Ici, on ne connaît pas à l'avance la taille de la chaîne de caractèresname. On ne peut donc pas la déclarer avant de la connaître, c'est pourquoi on utilise un blocdeclare ... begin ... end

L'affectation

Comme on peut s'en douter en regardant la façon dont on donne une valeur initiale à une variable, l'affectation se fait grâce au symbole ":=". Pour donner une valeur à une variable, on utilise une instruction du genre :

 nom_de_la_variable := expression_donnant_la_nouvelle_valeur; 

4 - Les structures de contrôle

Les conditions

Les conditions doivent être des booléens, on ne peut pas comme en C tester si un entier est différent de 0 juste en mettant "if mon_entier", il faut explicitement dire que l'on veut vérifier que cet entier est différent de 0. Les opérateurs de comparaison sont les suivants :

  •  
= : égalité (== en C)
  •  
/= : différent (!= en C)
  •  
< : inférieur strict
  •  
> : supérieur strict
  •  
<= : inférieur ou égal
  •  
>= : supérieur ou égal
  •  
in : appartenance à un intervalle (noté borne_inf..borne_sup)
  •  
not in : non appartenance à un intervalle

On peut ensuite combiner les résultats obtenus avec les opérateurs suivant :

  •  
not : négation
  •  
and : "et" (attention, les deux conditions sont évaluées ! Différent du && du C)
  •  
or : "ou" (attention, les deux conditions sont évaluées ! Différent du || du C)
  •  
xor : "ou exclusif" (équivalent du ^^ en C)
  •  
and then : "et", évalue la 1ère expression (celle de gauche) puis si elle est vrai la seconde et renvoie true si les 2 sont vraies. (équivalent du && en C)
  •  
or else : "ou", évalue la 1ère expression (celle de gauche) puis si elle est fausse la seconde et renvoie true si l'une ou l'autre est vrai (équivalent du || en C)

Le if then else

if condition then
 liste d'instructions
[elsif condition
 liste d'instructions]
 [...]
[else
 liste d'instructions] 
end if; 

Le switch

case expression is
 when valeur_1 =>; liste d'instructions 
 [when valeur_2|valeur_3|valeur_4 =>; listes d'instructions] 
 [...]
 [when others =>; listes d'instructions] 
end case; 

Contrairement au C, un break est contenu implicitement à la fin des différents cas.

Les boucles

La boucle de base, la boucle infinie (cela peut sembler absurde, mais c'est très utile, surtout en robotique) se présente comme ceci :

loop 
 liste d'instructions
end loop; 

Maintenant, si on veut faire une boucle mais en sortir à un moment donné, on peur utiliser l'instruction : exit when condition;

Le for

for indice in [reverse] intervalle loop
 liste d'instructions
end loop; 

L'intervalle est de la forme debut..fin avec debut inférieur à fin. Si on veut faire une boucle avec un indice décroissant, il faut utiliser le mot clef reverse, un intervalle du style 5..1 est vide et donc la boucle ne sera jamais exécutée. NOTE: indice n'est pas une vraie variable, il ne faut pas le déclarer en même temps que les autres variables et on ne peut pas le modifier.

Le while

while condition loop 
 liste d'instructions 
end loop;

5 - Les attributs, les énumerations

Les attributs

Les attributs sont un peu comme les membres static d'une classe en Java. C'est à dire que les attributs sont des fonctions ou des valeurs rattachées à un type. Par exemple, le plus petit Integer est une propriété des Integer. La syntaxe pour les utiliser est la suivante : nom_de_type'nom_de_l'attribut[(liste des arguments)] par exemple :

 plus_petit_entier : Integer := Integer'First; plus_grand_entier : Integer := Integer'Last; 

Parmi les attributs les plus utiles, on trouve

  •  
type'Image(x : type) : renvoie x sous forme de chaîne de caractères (équivalent de la fonction toString de Java)
  •  
type'Value(s : String) : attribut réciproque de Image, parse la chaîne de caractères s pour créer un objet de type type

Exemple de petit programme (il existe de meilleures façons de lire un entier, c'est juste pour l'exemple)

with Text_IO; use Text_IO; 
procedure attrib is
  nombre1 : Integer;
  nombre2 : Integer;
begin 
  Put(&quot;Entrez un nombre (entre &quot; &amp; Integer'Image(Integer'First) 
      &amp; &quot; et &quot; &amp; Integer'Image(Integer'Last) &amp; &quot;) : &quot;);
  nombre1 := Integer'Value(Get_Line);
  Put(&quot;Entrez un 2nd nombre : &quot;);
  nombre2 := Integer'Value(Get_Line);
  Put_Line(&quot;Le plus grand est &quot; &amp; Integer'Image(Integer'Max(nombre2, nombre2)));
end attrib; 

Les énumérations

Une énumération est un ensemble fini d'éléments, défini par la liste de ses éléments (enum en C), par exemple (rouge, vert, bleu) est une énumération. La syntaxe Ada pour déclarer une énumération est la suivante :

 type nom_de_mon_enumeration is (liste_des_valeurs); 

La définition d'une énumération définit implicitement un ordre : le premier est le plus petit, le dernier le plus grand. Une énumération à n éléments est donc très proches de l'ensemble des entiers de 1 à n. Il existe d'ailleurs un certain nombre d'attributs qui s'appliquent aux énumérations et aux entiers (ici i est un entier et x et y des éléments de l'énumération) :

  •  
Pos(x) : donne le numéro de x parmis les valeurs de l'énumération
  •  
Val(i) : donne le ième élément de l'énumération
  •  
First : donne le 1er élément de l'énumération
  •  
Last : donne le dernier élément de l'énumération
  •  
Succ(x) : donne l'élément suivant dans l'énumeration
  •  
Prec(x) : donne l'élément précedent dans l'énumeration
  •  
Min(x, y) : donne l'élément defini en 1er dans la liste
  •  
Max(x, y) : donne l'élément defini en dernier dans la liste

NOTE1: Par défaut, les éléments sont indicés à partir de 0 NOTE2: On peut définir une valeur numérique pour chaque élément :

type abc is (a, b, c); for abc use (a=&gt;12, b=&gt;37, c=&gt;42); 

NOTE3: Tenter d'accéder au successeur du dernier, au prédécesseur du premier, ou encore à l'élément à partir d'un indice invalide lève bien sûr une exception.
Exemple de petit programme (comme d'habitude, on peut faire beaucoup mieux : en Ada il exite un moyen de représenter les ensembles $ \frac{\mathbb{Z}}{n\mathbb{Z}} $ facilement)

 with Text_IO; use Text_IO;
 procedure enum is
   type Zsur4Z is (Zero, Un, Deux, Trois);
   Acc : Zsur4Z := Zero;
 begin
   loop 
     Put_Line(&quot;Acc vaut &quot; &amp; Zsur4Z'Image(Acc));
     Put(&quot;Combien voulez vous ajouter ? &quot;);
     declare
       number : Integer := Integer'Value(Get_Line);
     begin
       exit when number = 0;
       Acc := Zsur4Z'Val((Zsur4Z'Pos(Acc) + number) mod 4);
     end;
   end loop;
 end enum; 

6 - Les tableaux

Un tableau est un ensemble fini d'objets du même type indicé. En Ada, on définit d'abord le type du tableau avant de déclarer un élément de ce type. La définition d'un type peut se placer n'importe où où la déclaration d'une variable est possible. En Ada, il y a deux types de tableaux : les tableaux contraints et les tableaux non contraints.

Définition d'un type de tableau

Les tableaux contraints

Un type tableau est contraint si sa taille est précisée lors de sa définition. La définition d'un type tableau contraint suit le schéma suivant :  type nom_du_type is array (ensemble_d_indice) of type_des_elements; Ensuite, on déclare une variable de se nouveau type :  nom_de_la_variable : nom_du_type; L'ensemble d'indices est « n'importe quoi » du moment que c'est fini. Pour utiliser un intervalle, on utilise la notation debut..fin. On peut aussi utiliser des tulles en les séparant par des virgules pour définir un tableau à plusieurs dimensions. L'ensemble d'indices doit être défini lors de la déclaration du type, mais il n'est pas forcement « statique », il peut dépendre d'une variable. Voici quelques exemples de type de tableau contraint 

type abc is (a, b, c);
for abc use (a=&gt;12, b=&gt;37, c=&gt;42); 
type t1 is array (1..10) of integer; 
type t2 is array (boolean, 5..12) of float; 
type t3 is array (abc) of t2; 
procedure p(N : Natural) is type t4 is array (1..N); 
... 

Les tableaux non contraints

Un type tableau non contraint est un type de tableau qui ne précise que le type des éléments et les types des indices, mais pas explicitement l'ensemble d'indices, par exemple un ensemble de booléens indexé par des entiers, sans donner explicitement les entiers en question. En Ada, cela s'écrit :  type nom_du_type is array (type_des_indices range &lt;&gt;) of type_des_elements; Un objet de ce type doit être contraint lors de sa définition, c'est-à-dire qu'il faut donner un moyen au compilateur de savoir quel est l'ensemble d'indices. Pour cela, il existe 2 méthodes

Exemple:

type t is array (Interger range &lt;&gt;) of float;
tbl1 : t(7..10);
-- indices: 7..10
tbl2 : t := tbl1;
-- meme indice que tbl1
tbl3 : t := (1.0, 2.0, 5.0)
-- les indices commencent a Integer'First
tbl4 : t(5..8) := tbl1
-- indices: 5..8; les valeurs sont copiees depuis tbl1 

Utilisation des tableaux

Accès aux éléments

Une fois qu'une variable de tableau a été déclarée, peu importe que le type d'origine soit contraint ou non, le tableau s'utilise de la même façon. Pour accéder à un élément d'un tableau t on utilise la syntaxe t(indice)

Les attributs des tableaux

Comme les types, les tableaux ont des attributs. Il se note de la même façon que les autres attributs : nom_du_tableau'nom_de_l_attribut. Les plus importants sont :

  •  
First indice du premier élément du tableau
  •  
First(n) indice du premier élément de la nième dimension du tableau
  •  
Last indice du dernier élément de la perrière dimension du tableau
  •  
Last(n) indice du dernier élément de la nième dimension du tableau
  •  
Range intervalle des indices de la perrière dimension du tableau
  •  
Range(n) intervalle des indices de la nième dimension du tableau
  •  
Length longueur de l'intervalle des indices de la perrière dimension du tableau
  •  
Length(n) longueur de l'intervalle des indices de la nième dimension du tableau

Les agrégats

Pour définir les valeurs d'un tableau, on peut remplir le tableau une case par une case ou utiliser une syntaxe beaucoup plus puissante : les agrégats. Le plus simple reste un exemple :

type t is (integer range &lt;&gt;) of integer;
tbl1 : t(1..6) := (1|3=&gt;2, others=&gt;12);
-- les elements 1 et 3 valent 2, les autres 12
tbl2 : t := (1..10 =&gt; 4); 
-- 10 elements qui valent 4 

Les sous-tableaux

On peut créer un nouveau tableau à partir d'un tableau existant en spécifiant un intervalle à la place d'un indice. Ce nouveau tableau est un tableau comme un autre. Attention cependant, les indices des éléments de ce tableau sont les même que dans le tableau initial. Par exemple, si on dispose d'un tableau tbl indicé par 1..10, le sous-tableau tbl(tbl'Last-2..tbl'Last) sera indicé par 7..10. On peut changer ceci en déclarant explicitement les bornes d'un tableau avant de lui affecter un sous-tableau :

type t1 is array (integer range &lt;&gt;) of integer;
tbl1 : t1(1..10);
tbl2 : t1(1..3) := tbl1(tbl1'Last-2..tbl1'Last); 

La concaténation

L'opérateur & permet de concaténer 2 tableaux de même type. On retrouve d'ailleurs la concaténation de chaines de caractères. En effet, une chaine de caractères est un élément du type String prédéfini par type String is array (Positive range <>) of Character;, concaténer 2 chaines revient donc à concaténer 2 tableaux et se fait donc par l'intermédiaire de l'opérateur &. ATTENTION : La taille d'une variable tableau est définie lors sa déclaration et ne peut plus jamais changer ensuite. Ansi, si T est un tableau T := T & T ne marche pas, car T & T a une taille 2 fois plus grande que T. De même avec les chaines de caractère :

with Text_IO; use Text_IO;
procedure deux_ligne is 
  line : String := Get_Line;
begin
  Put_Line(&quot;1ere ligne: &quot; &amp; line);
  line := Get_Line;
  -- NE MARCHE PAS: line a deja une taille differente de celle 
  -- de la nouvelle ligne Put_Line(&quot;2eme ligne: &quot; &amp; line);
end deux_ligne;  

Ce code ne marche que si les 2 lignes lues ont exactement la même taille, sinon le programme lève une exception CONSTAINT_ERROR lorsque l'on tente d'affecter à line une chaine d'une autre taille.

7 - Les sous-programmes

Généralement, un programme est un peu compliqué avec des morceaux qui se répètent. Pour représenter cela, au lieu de réécrire 50 fois le même bout de code, on utilise des sous-programmes. En Ada, il existe deux types de sous-programmes, les procédures et les fonctions. En gros c'est la même chose, avec juste ce qu'il faut de différences pour que ce soit rigolo et que ca prenne la tête (on fait de l'Ada quand même).

Les procédures

Tous les programmes rencontrés jusqu'ici se composent d'une unique procédure, qui est exécutée au lancement du programme. Ces procédures ne prennent pas d’arguments mais c’est bien sûr un cas particulier. Dans le cas général, une procédure se définit là ou on peut définit une variable ou un type et suit le schéma suivant :

procedure nom_de_la_procedure(liste des arguments) is
  declaration des variables, des types, des sous-procedure; 
begin
  liste d'instructions 
end nom_de_la_procedure; 

Comme on l’a déjà vu, si la liste des arguments est vide, on omet les parenthèses (et pour le fun, elles sont même interdites). La liste des arguments ressemble fortement à une liste de déclarations de variables. En effet, une liste d’arguments s’écrit comme ceci :  nom_de_la_variable : [type_d_acces] type_de_la_variable; list_des_arguments_suivants Rien que du classique, excepté le type d’accès. Il y a 3 types d’accès à une variable :

Ces mode d’accès ressemblent au passage par valeur ou par référence des autres langages mais attention, ici on dit juste ce qu’on veut faire de ce paramètre, la façon dont il sera effectivement passé (par valeur ou par référence) sera déterminé par le compilateur, pas besoin de s’en préoccuper (ce qui veux dire que ceux qui ne savent pas ce que veux dire passage par valeur ou par référence n’ont aucune question à se poser).

Les fonctions

Les fonctions sont des procédures avec UNIQUEMENT des paramètres en mode in mais qui renvoient une unique valeur. Un e fonction s’écrit de manière similaire à une procédure :

function nom_de_la_fonction[(liste_des_arguments)] return type_de_la_valeur_retourne is
  declaration des variables, des types, des sous-procedure;
begin
  liste d'instructions;
end nom_de_la_fonctione; 

Le renvoie d’une valeur s’effectue vie l’instruction return. Il faut que dans tous les cas possibles, la fonction passe par un return, sinon le compilateur criera au scandale. Un petit mot sur les surcharges : il est possible, en Ada, de définir 2 fonctions ou procédures ayant le même nom du moment qu’il est possible de les différencier parce que leurs arguments ne sont pas du même type ou leur type de retour est différent.

Valeur par défaut

Il est possible de donner une valeur par défaut aux arguments de type in. Cela se fait, comme pour les déclarations de variable, simplement en ajoutant := valeur après la type de la variable. Par exemple

function pv(n : in Float := 1.0; T : in Float := 273.0) return Float is
  R : constant Float := 8.314472;
begin
  return n*R*T;
end pv; 

Appels de sous-programmes

L’appel de sous-programmes se fait simplement en écrivant son nom, suivit, entre parenthèses, de la liste des arguments. Pour cette liste d’arguments, il existe 2 méthodes. Soit on les donne dans l’ordre défini par le sous-programme ( c'est mal!), soit on utilise l’opérateur =>. Par exemple, pour appeler pv définis ci-dessus, on peut faire

La 3ème méthode est un mix des 2 premières. On peut donner les 1ers arguments dans l’ordre de la définition et ensuite les suivant par leur nom. Attention, une fois que l’on a donne un argument par son nom, on ne peut pas donner les suivant sans préciser leur nom. pv(2.0, T=>10.0) est valide mais pas pv(n=2.0, 10.0).

Déclaration de sous-programmes

Il peut arriver qu’on ait besoin de dire au compilateur qu’une fonction existe, sans vouloir dire tout de suite ce qu’elle fait. L’exemple typique est une récurrence croisée : une procédure p1 appelle p2 qui elle-même appelle p1. Comment faire ? La réponse est assez simple, il suffit de déclarer p2 avant la définition de p1, puis celle de p2. Une déclaration reprend la 1ere ligne de la définition d’un sous-programme, en remplaçant le is par un ; Par exemple pour p1 et p2 :

procedure p2;
 
procedure p1 is 
begin
  p2;
end p1;
 
procedure p2 is
begin
  p1;
end p2; 

8 - Les types

En plus des types prédéfinis, Ada permet de déclarer de nouveau types pour répondre précisément à nos besoins. De plus, on peut réutiliser un type existant et le spécialiser pour le restreindre aux valeurs qui nous intéressent. Si jamais une variable de se type venait à recevoir une valeur invalide, une exception serait levée.

La définition de type

Comme on l’a déjà vu, en particulier pour les tableaux, définir un nouveau type est très facile et peut se faire n’importe ou ou on peut déclarer une nouvelle variable. Pour définir un nouveau type : type nom_du_type is nouveau_type;

Les types élémentaires

Les types dit élémentaires sont ceux qui ne sont pas composite. C’est à dire qu’il ne représente qu’une seule valeur, par opposition au tableau par exemple qui sont un ensemble de valeur.

Les intervalles

Les plus simples des types élémentaires sont les intervalles, on les définis grâce au mot clef range comme par exemple type un_a_dix is range 1..10; En plus de l'intervalle, on peut aussi spécifier l'écart entre les valeures grace au mot clef delta (par defaut 1) ou encore le nombre de chiffres significatifs avec le mot clef digits, comme par exemple : type note is delta 0.5 range 1..20; type juste_5_chiffre is digits 5 range 1.0E-16..1.0E25;
En fonction de la description de l'intervalle, le compilateur choisira le type qui correspond le mieux, entre entier, float.... Attention: 2 types sont incompatible sauf si l’un est un sous type de l’autre (voir plus loin pour les sous-types). Par exemple, le programme suivant ne marche pas :

type un_a_dix1 is range 1..10;
type un_a_dix2 is range 1..10;
a : un_a_dix1 := 5;
b : un_a_dix2 := a; -- NE MARCHE PAS 

Remarque: Les bornes de l’intervalle n’ont pas besoin d’être fixe, ce peut etre la valeur d’une variable. Par exemple, si on sait qu’il y a n personne, on peut definir un type type numero_de_personne is range 1..n;.

Les sous-types

Un sous –type est un sous ensemble d’un type de base. Ce nouveau type est compatible avec le type de base, c’est à dire que l’on passer de l’un à l’autre sans problème (tant que l’on respecte les bornes). Pour définir un sous type, on utilise subtype et non type:

procedure sous_types is 
  type r1 is range 1..100;
  subtype r2 is r1 range 5..15;
  a : r1 := 6;
  b : r2 := 7;
begin
  a := a + b; -- a = 13, ca reste dans les bornes de r1 
  b := a + b; -- b = 20, &gt; 15, on va avoir une exception 
end sous_types; 

Ce petit programme compile sans problème parce qu’on peut affecter un r1 à un r2 et vis versa mais à l’exécution, la 2eme ligne après le begin va lever une exception car a+b va valoir 20, ce qui sort des bornes de r2. Remarque: Il est légale (i.e. ca va compiler) de définir un sous type avec un intervalle qui n’est pas inclus dans le type père, mais cela déclenche une exception à l’exécution. Il existe quelque sous types prédéfinis très utiles, comme par exemple :

subtype Natural is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last; 

Les types dérivés

Un type dérivé est un type nouveau, c’est à dire qu’on ne pourra pas affecter un élément de se type a une variable du type père, qui hérite des opérations définie pour le père (comme par exemple l’addition pour les entiers). On déclare un type dérive avec la syntaxe suivante : type type_derive is new type_de_base [range ..]; Comme pour les sous-types, l’intervalle d’un type dérivé doit être inclus dans celui du père. On ne peut pas affecter un élément d’un type à une variable d’un type dérive de ce type. Par exemple, le programme suivant est invalide :

type un_a_dix is range 1..10;
type un_a_cinq is new un_a_dix range 1..5;
a : un_a_dix := 2;
b : un_a_cinq:= a;-- NE MARCHE PAS 

En revanche, un type dérivé hérite des opérations définie pour le type de base, par exemple si on a, dans un package, une fonction (ou procédure) qui prend des un_a_dix en arguments, il existe automatiquement une nouvelle version de cette fonction qui prend des un_a_cinq en argument.

Conversion de type

Pour convertir un élément d’un type dans un autre, il suffit d’utiliser la notation suivant : nouveau_type (valeur_d_un_autre_type) Si jamais la valeur est invalide pour ce nouveau type, une exception sera levée.

Les records

Un record est comme une structure en C, c'est à dire que c'est un type composite qui encapsule plusieur élément dans un seul type.

Les records basique

Un record se définis comme suit :type nom_du_record is record liste des elements du record end record; La listes des éléments est une bête liste de variables. Par exemple :

type position is record 
  x : Integer; 
  y : Integer; 
  theta: Float; 
end record; 

Utilisation

Pour utiliser un record, c'est simple, on déclare un élement du type du record et ensuite on peut acceder à chaque élement avec la même notation qu'en C : nom_de_la_variable_record.nom_de_l_element

9 - Les packages

Pour organiser nos programmes, on peut regrouper différentes fonctions dans un package come Ada.Text_IO qui contient toute les fonctions d'entrée/sortie de texte.

Les packages

Un package se compose de 2 partie :

Attention: un package nommé "toto" doit être dans les fichiers "toto.ads" et "toto.adb"

La spécification

La spécification d'un package est la liste des fonctions/procédure/types/... que ce package veux faire connaitre au reste du monde. La syntaxe est la suivante:

package Package_Name is
  procedure Une_Procedure;
end Package_Name;

Le corps

Le corps du package est son implémentation. Il faut au minimum implémenter les fonctions déclarées dans la spécification. On peut rajouter des fonctions qui ne seront visibles que par les autres fonctions du package, pas par les utilisateurs.

La syntaxe est la suivante :

package body Package_Name is
  procedure Une_Procedure is
  begin
    null;
  end Une_Procedure;
end Package_Name;

La partie private

Un package peut contenir une partie private :

package Package_Name is
  procedure Une_Procedure;
private
  procdure Une_Procedure_Private;
end Package_Name;

Cette partie "private" peut contenir un peu tout et n'importe quoi. Ce qui y sera définit n'est visible que par les membres du package et de ses sous-packages (cf. plus loin). Mais surtout, grâce à cette partie "private", on peut définir des types "private".

Exemple : on veut faire un package "Stack", avec 2 méthodes "Push" et "Pop" et un type : "Stack_Type". Ce type va être un record avec un tableau et un pointeur sur le 1er élément de la pile. Idéalement, les utilisateurs du package ne devrait pas savoir ce qu'il y a dans ce type, comme ça on est sur qu'ils ne le modifieront pas, et si jamais on change de façon de faire (avec par exemple une allocation dynamique de la mémoire) on est sur que tout marchera encore. Pour faire cela, on va déclarer notre type "private":

package Stack is
  type Stack_Type is private;
  procedure Push (Stack : in out Stack_Type; Value : Integer);
  procedure Pop (Stack : in out Stack_Type; Value : out Integer);
private
  Max_Size : constant Integer := 100;
&nbsp; type Buffer_Type is array (1 .. Max_Size) of Integer;<br />&nbsp; type Stack_Type is record<br />&nbsp;&nbsp;&nbsp; Buffer : Buffer_Type;<br />    Top : Integer := 0;
  end record;
end Stack;

Un utilisateur pourra déclarer un élément de type Stack.Stack_Type mais ne pourra pas savoir ce que cette élément contient.

Initialisation

Un package peut aussi comporter une partie d'initialisation. La syntaxe est très simple:

package body Package_Name is
  procedure Une_Procedure is
  begin
    ...
  end Une_Procedure;
  ...
begin
  Code_D_Initialisation;
end Package_Name;

Le code entre le begin et le end Package_Name sera exécuté une seule fois, au début du programme.

Ordre d'élaboration

L'élaboration d'un programme consiste à initialiser ses variables et exécuter son éventuelle partie d'initialisation.

L'ordre d'élaboration peut poser un problème puisque les parties d'initialisation peuvent appeler des procédures : il faut être sur que toute les variables utilisé par ces procédures sont initialisé.

Une règle simple aurait pu être d'élaborer un package dès que quelqu'un en fait un "with" mais cela empêcherait les packages qui se "with" l'un l'autre : lequel initialiser en premier ? Ada utilise des règles pour définir un ordre d'élaboration intelligeant. Généralement, ca se passe bien. Pour influer sur l'ordre d'élaboration, on peut utiliser quelque pragma :

pragma Elaborate_All

Le plus courant : lorsque le compilateur se rend compte qu'on utilise une fonction qui risque de ne pas être élaborée, on reçoit un message de ce type :

        >>> warning: call to "Set_X" may raise Program_Error
        >>> warning: missing pragma Elaborate_All for "Shix.Engines_Base"
        >>> warning:  called at line 18
Dans ce cas, il faut rajouter pragma Elaborate_All (Shix.Engines_Base)après le with Shix.Engines_Base.

pragma Elaborate_Body

Normalement, un programme est une procédure. Il arrive qu'il soit plus simple d'utiliser un package. Dans ce cas, l'initialisation du package est le programme principale. L'ads du package est donc vide. Hors, si le compilateur voir un ads vide, il ne comprend pas qu'il puisse y avoir un adb. Pour dire au compilateur que l'ads est vide et que c'est normal, on met un pragma dans l'ads, par exemple :

package Simulator is
  pragma Elaborate_Body;
end Simulator;

Pour aller plus loin

Pour plus d'info, voir par exemple la :http://gnu.huihoo.org/gcc/gcc-3.3.6/gnat_ug_unx/Elaboration-Order-Handling-in-GNAT.html#Elaboration-Order-Handling-in-GNAT

Sous-package

Un package peut avoir des packages enfants. Cela permet d'organiser un peu ses packages. Par exemple, pour notre robot, tous les packages qui ce rapport à la shix sont des sous packages de Shix.

Nom du package

La hiérarchie des packages se représente en utilisant un "." pour séparer les noms de packages. Attention : dans les noms de fichiers, le "." devient un "-". Ainsi, un sous-package B d'un package A a pour nom A.B et il se trouvera dans les fichiers a-b.ads et a-b.adb. 

Visibilité

Un sous-package fait automatiquement un "with" de tous ses parents. En revanche, il n'est pas possible de "wither" un sous-package dans son parent.

De plus, un sous-package peut accéder à la partie "private" de ses parents. Ceci permet, entre autre de mettre des choses pour le débogage/test dans la partie private d'un package. Les utilisateurs standards n'y auront donc pas accès alors que les programmes de tests feront un "with" d'un sous-package qui aura lui accès à ses fonctions et pourra même éventuellement les exporter.

10 - Le multi-tache

Lancer plusieurs threads

En Ada, il est très facile de lancer plusieurs tâches simultanément : il faut déjà déclarer la tâche : task Ma_Tache; ensuite, il faut dire ce qu'elle fait en respectant la syntaxe suivante :

task body Ma_Tache is
 ...
begin
 ...
end Ma_Tache;

et c'est tout, rien de plus facile.

Attendre un peu

Souvent, une tâche consiste à faire quelque chose, attendre un peu, refaire le quelque chose, attendre un peu, etc, etc... Pour attendre un peu, on pourrait faire une grosse boucle qui ne fait rien, mais le processeur serait très utilisé et donc les autres tâches rameraient. Pour nous aider, Ada propose un package "Ada.Real_Time" qui contient une méthode "Clock" qui renvoie l'heure qu'il est. A cette heure, on peut ajouter des "Time_Span" qu'on obtient en convertissant un nombre flotant de secondes avec la fonction "To_Time_Span". Ensuite, on dit à Ada de ne rien faire jusqu'à cette nouvelle heure. Cela donnedelay until Clock +&nbsp;To_Time_Span (1.2); pour attendre 1.2 s sans rien faire.

Les problèmes de synchronisation

Le problème lorsqu'on lance plusieurs tâches, en même temps, c'est la synchonisation : imaginons qu'on dispose d'un robot avec une fonction pour le faire avancer. 2 tâches veulent le faire bouger mais bien sûr, il ne faut pas que les 2 tâches le fasse bouger en même temps. Pour éviter cela, on déclare un Boolean Busy qui sera à True lorsque le robot bouge et à Zero le reste du temps. Chaque tâche aura un code qui ressemblera à ça :

if not Busy then
 Busy = True;
 Move;
 Busy = False;
end if;

Malheureusement, si les 2 tâches s'exécutent en même temps, il se peut que les 2 tâches lisent la valeur de Busy en même temps, elles trouveront donc False toutes les 2. Ensuite, toutes les 2 vont mettre Busy à False puis faire bouger le robot, ce qu'on voulait éviter.

La solution en Ada pour éviter ça est les objets "protected"

Les protected

Un protected est un objet qui contient des variables (non accessibles directement) et des fonctions/procédures. Le fait que ces procédures soit dans un protected assure qu'une seule tâche peut l'executer à tout moment. Les autres tâches attendent que celle qui est en cours ait fini d'exécuter la procédure.

Comme pour les tâches, il faut déclarer les objets protected puis les implémenter. Example:

protected Busy is
 procedure Set_Value (Value : Boolean);
 function Value return Boolean;
private
 Current_Value : Boolean;
end Busy;
 
protected body Busy is
 procedure Set_Value (Value : Boolean) is
 begin
 Current_Value := Value;
 end Set_Value;
 function Value return Boolean is
 begin
 return Current_Value;
 end Value;
end Busy;

 Cet exemple de protected ne sert à rien : il faut rajouter une petite fonction :

function Can_Move return Boolean is
 if not Current_Value then
 Current_Value := True;
 return True;
 else
 return False;
 end if;
end Can_Move;

  et là, ca compile pas :( :  protected function cannot modify protected objectEn effet, j'ai dit une petite bêtise plus haut : dans un protected, une procédure sera executer par une seule tâche mais une fonction peut être executer par plusieur tâche en même temps, sous reserve qu'il n'y ait pas de tâche en train d'executer une procédure. L'avantage, c'est la rapidité : pas la peine d'attendre pour executer des fonctions. Le désavantage, c'est que pour que ca marche, les variables du protected sont read-only dans les fonctions.

Qu'a cela ne tienne, on va mettre une procédure :

procedure Can_Move (Result : out Boolean) is
 if not Current_Value then
 Current_Value := True;
 Result := True;
 else
 Result := False;
 end if;
end Can_Move;

et voilà, on dispose d'un moyen pour s'assurer qu'une seule tache va déplacer notre robot.

Les "entry"

Notre protected dispose d'une méthode "Can_Move" qui nous dis si on peut y aller. Maintenant, si on ne peut pas, que fait-on ? On attend un peu et on redemande... Cela n'est pas très optimal : si jamais l'autre tache finie juste après notre teste, il faut attendre quand même. Pour ne pas perdre trop de temps, on va mettre un delai d'attente faible, ce qui va impliquer une plus grosse charge CPU.

Pour résoudre ce problème, il existe un méchanisme : les "entry". Une "entry", c'est comme une procédure sauf que pour s'executer, elle attend qu'une condition soit vrai. Dès que la condition devient vrai et qu'il n'y a plus de tache qui execute des fonctions/procédure du protected, le code de l'entry est executé.

Exemple:

protected Busy is
 procedure Set_Value (Value : Boolean);
 function Value return Boolean;
 entry Wait_Until_Ready;
private
 Current_Value : Boolean;
end Busy;

protected body Busy is
 ...
 entry Wait_Until_Ready when not Current_Value is
 begin
 Current_Value := True;
 end Wait_For_Ready;
end Busy;

11 - Ecrire un programme pour le robot

Maintenant que les bases de l'Ada sont plus ou moins maitrisées, il est temps de jouer avec le robot.

Sur le robot, il y a déjà plein de packages pré-écrit par les vieux cons du club. Ces packages servent à différentes choses, comme accéder aux images des caméras, déplacer le robot, etc.  Pour une description des packages disponibles, voir la page « bas niveau » dans la partie « software ».

L’arborescence

Le code du robot se trouve dans le répertoire « shix » du repository mercurial « soft ». Ce répertoire se subdivise en plusieurs :

 La compilation

Comme c’est un peu le bordel (en fait non, c’est bien rangé, mais y’a beaucoup de répertoire mis en jeu), pour compiler toto.adb qui utilise des packages d’un peu partout, un simple gnatmake toto.adb ne fonctionnera pas : il ne va pas trouver la moitié des fichiers nécessaires. Pour que cela marche, il faut lui rajouter plein d’option pour lui indiquer tout les répertoires à inclure ainsi que les bonnes options de compilation.
Pour simplifier le processus, nous disposons d’un script python makeMakefile.py qui cherche tout les programmes dans shix/contest, shix/contest/main et shix/contest/master et génère un Makefile automatiquement pour tout compiler. En gros, pour compiler, il suffit de se placer dans shix/contest et de taper makeSi on veut compiler uniquement les programmes pour la camera, on va dans shix/contest/camera et on fait make. Pour les programmes de la carte master, on va dans shix/contest/main. Ces make vont générer des executables x86 dans le même répertoire que leur fichier .adb correspondant.
C’est bien beau tout ça, mais comment on cross-compile se demande immédiatement ceux qui ont suivit depuis le début. Et bien c’est facile, on tape make sh4 au lieu de make. Facile, non ? Le résultat d’un make sh4 se trouvera dans le répertoire shix/contest/bin-sh4, bien rangé dans 2 sous-répertoire « main » et « camera ». Ces fichiers sont à lancer depuis le robot (voir la page « Branchement / démarrage du robot ».
Tant qu’on parle des options de make, il existe 3 autres options importantes :

Application : déplacer le robot

Package utile

Pour mettre en pratique tout ceci, écrivons un petit programme qui déplace le robot : on va lui faire faire des carrés jusqu'à qu’il n’y ait plus de batteries. Pour déplacer le robot, on ne va pas réinventer la roue, on va regarder ce que les vieux ont déjà écrit. En farfouillant un peu, on tombe sur shix/contest/main/move.ads. Ce fichier porte un nom prometteur… En regardant un peu dedans, on s’apperçoit qu’il définit des fonctions assez utile dans notre cas :

procedure Forward (Distance_In_Mm : in Integer);
-- Go forward by a distance in millimeters (may be negative)
procedure Turn (Degrees : in Float);
-- Turn counter clockwise (may be negative)

Version 1

On va donc utiliser ce package pour faire notre programme. Ce programme va devoir déplacer le robot, donc utiliser les moteurs. Les moteurs sont branché sur la carte master, ce sera donc un programme de la carte master. On se place donc dans le répertoire shix/constest/master et on crée le programme test_move.adb

with Move;                use Move;
procedure Test_Move is
begin
  loop
    Forward(1000);
    Turn(90.0);
  end loop;
end Test_Move;

ensuite, on le compile avec un bête make ou make sh4. C’est genial, ca compile !

Version 2

On se précipite pour le lancer et là, c’est le drame : il ne se passe rien, le robot ne bouge pas (ou alors très peu et en faisant n’importe quoi). Que c’est il passé ? C’est simple, « forward » et « turn » donne l’ordre au robot de faire un mouvement, mais n’attendent pas que cet ordre soit executé pour rendre la main. On donne donc l’ordre au robot d’avancer, à peine a-t-il commencé à le faire qu’on lui dit « laisse tomber, tourne plutôt », puis « finalement, non avance », etc, etc.
Pour attendre la fin d’un mouvement, il faut reprendre « move.ads » plus en détails. Là on tombe sur

function Wait_End_Move return End_Move_Record;
function Wait_End_Move(Max_Time : Duration) return End_Move_Record;

Oh! Des fonctions qui permette d’attendre la fin d’un mouvement!. Si on étudie un peu leur valeur de retour, on se rend compte que les anciens ont quand même bien bossé puisqu’ils nous disent même l’état du robot lorsque cette fonction rend la main.
Mettons vite à jour notre programme

with Move;                use Move;
 
procedure Test_Move is
  Reason : End_Move_Record;
begin
  loop
    Forward(1000);
    Reason := Wait_End_Move;
    Turn(90.0);
    Reason := Wait_End_Move;
  end loop;
end Test_Move;

Version 3
Sur le papier, cette version semble pas mal. Seulement, lors de la compilation, on se fait insulter :

     4.   Reason : End_Move_Record;
          |
        &gt;&gt;&gt; warning: variable &quot;Reason&quot; is assigned but never read
     8.     Reason := Wait_End_Move;
            |
        &gt;&gt;&gt; warning: useless assignment to &quot;Reason&quot;, value overwritten at line 10

Le deuxième message d’erreur vient du fait que le compilateur se rend compte qu’on fait un truc bizarre: on donne une valeur à une variable, et on l’écrase tout de suite après sans même la lire ! Le 1er message est du même ordre : on declare une variable, on lui donne une valeur, et on la relit jamais. Le compilateur Ada est un peu chiant (en fait, c’est du aux options de compilation) mais il a pas tort que souvent c’est à cause d’un bête oubli qu’on ne lit pas les valeurs de certaines variables. Dans notre cas, c’est tout à fait normal, on fait un programme débile qui ne réagira pas s’il se prend un mur. On le sait et on en assume les conséquences donc on l’indique au compilateur :

with Move;                use Move;
 
procedure Test_Move is
  Reason : End_Move_Record;
  pragma Warnings(Off, Reason);
begin
  loop
    Forward(1000);
    Reason := Wait_End_Move;
    Turn(90.0);
    Reason := Wait_End_Move;
  end loop;
end Test_Move;

Et voilà un beau programme qui fait bouger le robot.
Attention : Le pragma Warnings(Off, Reason)&nbsp;; utilisé ici l’est parce qu’on sait ce qu’on fait. Il ne fait surtout pas en mettre partout juste parce que le compilo gueule !

Et maintenant, au travail !

12 - Utilisation de la lib PNG_Interface

Comme tout le monde sait maintenant ce que sont les packages, les types, les procedures, tout ça, on va pouvoir se servir des caméras du robot !

On commence par un with PNG_Interface; use PNG_Interface;qui contient les fonctions dont on va se servir,

puis with Shix.Types; use Shix.Types, le package qui déclare pleins de types utiles dont Pixel, Color, Image, etc ...

pour charger une image, il y a deux façons de faire :

    Pour les images stockées dans le système de fichier

On utilise la fonction Read_PNG.

Celle-ci est définie dans shix/hwinterface/camera/png_interface.adb par :

procedure Read_PNG (File_Name : in String; I : out Generic_Image)

Pour le filename, c'est l'adresse du fichier (en relatif  ou en absolu). Il faut aussi lui donner le nom de la variable dans laquelle on stoque l'image. En fait, une Generic_Image a des sous types bien pratiques qui correspondent à nos images. En effet, voici la définition de Generic_Image : 

type Generic_Image is array (Positive range &lt;&gt;, Positive range &lt;&gt;) of Pixel;

tandis qu'une image c'est ça :

subtype Image is Generic_Image (y_range, x_range);

ou (y_range, x_range) ont étées mises aux valeurs de nos caméras (240*320)...

Voila, maintenant l'image est chargée dans la variable et c'est un tableau de 240*320 pixels, sachant qu'un pixel c'est ça :

type Pixel is record
 R : Unsigned_8;
 G : Unsigned_8;
 B : Unsigned_8;
end record;

donc par exemple, j'accede à la composante rouge du pixel (134, 23) (attention, le pixel en haut à gauche est le pixel (0,0) et la premiere composante est verticale, la deuxième horizontale) par mon_image(134,23).R

    Pour prendre direct une image de la camera

On utilise Acquire_Image qui n'a besoin donc que la variable dans laquelle stoquer l'image. Par exemple,

myImage : Image := Acquire_Image; -- c'est faux !!
myImage : Image;
Acquire_Image(myImage); -- c'est bon :)

et ça roule. Acquire_Image est dans le package shix-camera donc on rajoute au début un petit with Shix.Camera; use Shix.Camera;

Il y a 4 caméras sur le robot 2008, et seule une caméra peut être utilisée pour un Acquire_Image à la fois. Pour changer de camera, on utilise la fonction de Shix.Camera

 procedure Set_Active_Camera(Position : Camera_Position)

sachant qu'une Camera_Position c'est ça (dans Shix.Types) :

type Camera_Position is (Upper_Right, Inside, Upper_Left, None);

Voila, vous savez tout ! A vous les joies du traitement des images :)

    Ecrire une image dans le système de fichiers

Bon maintenant, si on a envie d'écrire une image sur le disque, on utilise la fonction Write_PNG (vous l'aviez deviné) définie toujours dans PNG_Interface :

procedure Write_PNG (File_Name : in String; I : in Generic_Image)

Avec ça vous devriez savoir comment faire :)

n'hésitez pas à regarder des exemples dans le dépot mercurial (png_utils.adb est un bon début).

13 - Exercice : SimpleForth

Pour mettre en œuvre un peu d'Ada, rien ne vaut un bon exercice. Pour entrer dans l'esprit du club tout de suite, on se propose de réaliser un petit interpréteur Forth (cf. le tutorial qui va bien).

Squelette de base

Pour commencer, il faut récupérer le squelette de base du programme. Pour cela, on va cloner un depot mercurial qui contient mes réponses aux questions : 
hg clone http://bitbucket.org/telrob/simpleforth/">http://bitbucket.org/telrob/si... -r 0
Pour obtenir mon exemple de programme répondant à la question "n", il suffit d'executer la même commande en changeant le 0 par "n" (cf tuto Mercurial pour plus d'info)
Pour que notre interpréteur soit plus user friendly, il va utiliser la librairie bien connu readline. Il n'existe malheureusement pas de package standard pour utiliser cette librairie. Il faut donc importer "à la main" les fonctions qui nous intéressent. Heureusement, je l'ai déjà fait ;) (Merci Sam) il suffit de récupérer mon package ici:
hg clone http://bitbucket.org/telrob/areadline
La configuration par défaut demande que "areadline" soit dans le répertoire parent par rapport à notre interpréteur Forth.
Ensuite, un rapide parcourt du code s'impose. On trouve 3 fichiers : forth.ads, forth.adb et test_forth.adb.
·         forth.ads : L'ads de notre package. On y déclare 2 fonctions :
o    Register_Ada_Word: Pour jouer avec notre robot, on aura besoin de fonction qui n'existe pas en Forth de base. Pour pouvoir les utiliser, on va écrire une petite procédure Ada et on dira ensuite à notre interpréteur (via Register_Ada_Word) que lorsque l'utilisateur tape un "mot" particulier, il faut exécuter cette fonction.
o    Interpret_Line: Cette fonction est chargé d'interpréter une ligne de Forth
·         forth.adb: un fichier un peu vide pour l'instant, à compléter...
·         test_forth.adb: Pour pouvoir tester notre interpréteur, il faut un programme de test. Ce programme se réduit à une boucle qui lit une ligne au clavier, l'interprète puis recommence.

1ère partie

Notre interpréteur va travailler avec des "mots". Une ligne sera une suite de mots qui seront interprétés les uns après les autres. Pour l'instant, on va définir 2 types de mots : les nombres et les mots Ada. Les nombres seront des flottants pour plus de généralité et les mots ada sont ceux qui vont appeler une fonction Ada.
Question 1: définir le type Word_Type
Maintenant, on va s'occuper de la liste des mots connus.
Question 2: écrire la procédure Register_Ada_Word
Indication: Pour simplifier, on va se donner un nombre de mot maximal et on définira un tableau de mot de cette taille.
Note: Par convention on choisit de remplacer l'ancienne valeur d'un mot s'il est défini plusieurs fois
Ensuite, on va commencer à traiter notre ligne
Question 3: Ecrire une fonction qui découpe la ligne en mot.
Indication: On pourra utiliser une variable qui enregistre où on en est dans la ligne.
Question 4: Compléter la fonction Interpret_Line pour qu'elle exécute les mots ou empile les nombres.
Note: De même que pour la liste des mots, on prendre une pile de taille finie.
Question 5: Ecrire quelques mots de base, comme par exemple +, -, .s
Et voilà ! On dispose maintenant d'une belle calculatrice !

2ème partie

Une calculatrice, c'est bien, mais bon, en polonais inversé c'est un peu limité...
Pour aller plus loin, il va falloir complexifier un peu les choses. Une bête interprétation des mots au fur et à mesure pose de gros problème pour faire des choses comme la définition de nouveaux mots ou des boucles imbriquées complexes. On va donc introduire un nouveau mode : le mode compilation. 
Au lieu d'interpréter les mots, on va "compiler" l'ensemble de la ligne pour l'exécuter. Pour les wariors, on peut compiler la ligne en assembleur x86 directement (ou en assembleur PIC, cf. rfoth1). Sinon, on peut se définir notre propre langage et écrire une machine virtuelle pour exécuter ce langage. Ca a l'air compliqué mais c'est très simple en fait. Pour notre langage virtuelle, on va prendre nos mot de type Word_Type (ça tombe bien, on les a déjà sous la main) et pour exécuter notre byte-code, on va reprendre notre fonction qui exécute les mots.
Finalement, au niveau des changements, nous faut un endroit où stocker les mots compilé, une mémoire contenant notre "programme" (un simple tableau fini suffira). Il faut aussi créer un nouveau type de mot, les mots de type "Number" dont l'exécution consiste à empiler la valeur correspondante. Et voilà.
Question 6 : Modifier notre interpréteur pour qu'il compile la ligne avec de l'exécuter.
Ajoutons maintenant les structures de type "IF THEN". Avec notre interpréteur de la 1ere partie, il n'est pas simple d'ajouter ces structures. Avec notre compiler, ça va se passer beaucoup mieux.
L'idée est la suivante : on va transformer
toto if tata then titi
en
toto "jump_if_false titi" tata titi
On remplace le "if" par un "goto" après le "then" si la condition est fausse. Le problème consiste à connaître l'adresse de ce qui suit le "then". Au moment de la compilation du "if", cette adresse est inconnue (ce qui pose de gros problème pour l'interpréteur de la partie 1). Ce qu'on va faire, c'est laisser l'adresse vide et la remplir au moment de la compilation du "then".
La compilation du "if" est plus compliqué que celle des autres mots vu jusqu'ici : il ne suffit pas d'écrire le "if" dans notre mémoire, il faut enregistrer l'adresse mémoire du "if" pour pouvoir le mettre à jour lors de la compilation du "then". On pourrait faire un test pour chaque mot à compiler et faire quelque chose de différent lorsque c'est un "if", un "then", etc. mais il est plus simple de définir un nouvel attribut des mots : on dit qu'un mot est "immédiat" s'il demande une action spécifique, sinon, il se compile comme précédemment. Pour compiler un mot, on regarde s'il est immédiat. S'il l'est, on l'exécute immédiatement :), sinon on l'ajoute dans notre mémoire des mots compilés.
Question 7 : Ajouter l'attribut "immediat" aux mots
Question 8 : Ajout les mots "if" et "then"
Indication : On ajoutera un mot "Jump_If_False" qui sautera ou ne sautera pas à l'adresse sur le sommet de la pile en fonction du 2ème élément de la pile.
Indication 2: Lors de la compilation, la pile ne sert à rien, on peut donc s'en servir pour enregistrer les endroits où mettre les adresses à compléter par le "then". L'avantage, c'est que ça permet de gérer les "if" imbriqué sans réfléchir.
Question 9: Juste pour la forme, rajouter un "else"
Indication : On va compiler
toto if tata else titi then tutu
en
toto Adresse(titi) Jump_If_False tata Adresse(tutu) Jump titi tutu
Il "suffit" maintenant d'écrire le mot immédiat "else"
Pour finir, attaquons nous à la définition de mot en Forth. Il va nous falloir définir 2 nouveaux mots ":" et ";". L'idée de base est assez simple : on défini un nouveau type de mots, les mots Forth, par analogie aux mots ada. A la place d'un pointeur vers une procédure Ada, ces mots contiendront une position dans notre mémoire, l'endroit où le mot en question à été compilé (on peut aussi compiler un mot en l'adresse de destination, suivi d'un nouveau mot "Call")
Pour la compilation du mot ":", prenons un exemple :
toto : titi tata ; tutu titi
Lors de l'exécution, il ne faut pas exécuter "tata" après toto. Ce qu'on va faire, c'est de rajouter un "Jump" après le toto pour sauter directement à "tutu" et passer la définition de "titi".
Pour la compilation du mot ":", il faudra compléter l'adresse du "Jump" introduit par le ":" et ajouter un mot spécial "Exit" qui, lors de l'exécution indique qu'il faut sortir du mot en cours.
Finalement, on obtiendra
toto Adresse(tutu) Jump tata Exit tutu titi
Question 10: Ajouter la création de nouveau mot.
Note: Au lieu de mettre un "jump" après la définition du mot, on peut employer une autre stratégie : on interprète les mots, comme dans la 1ere partie, jusqu'à ce que trouve un ":". La on change de monde, on passe en mode compilation jusqu'au ";". Ensuite, on reprend notre interprétation. Cette technique a l'avantage d'économiser de la mémoire : seuls les mots sont compilés et prennent de la place dans notre mémoire.
Question 11: Rajouter tous les mots qui vous font plaisir !
 

14 - Exercice : Startup_Manager

Introduction

Pour continuer à faire de l'Ada, mais en rentrant un peu plus dans le vif du sujet, on va s'occuper de la procédure de démarrage du robot. Lorsqu'on démarre le robot, le programme principale se lance automatiquement. Il attend que l'utilisateur suive une procédure bien précise avant de lancer le match, puis coupe tout 90s plus tard. L'objectif de cet exercice est d'écrire un package qui va gérer ça.

Comme entrées, on dispose de 2 boutons : le bouton rouge (power_switch) et le jack de démarrage (startup_switch). La procédure de demarrage consiste à enfoncer le bouton rouge, mettre le jack de démarrage, poser le robot sur la table, quand tout est prêt, on retire le bouton rouge, finalement, au top de l'arbitre, on retire le jack. Il y a donc 3 états importants : "en vrac", au début : on ne sais pas du tout où est le robot (en l'air, dans les mains du porteur, sur la table, ou n'importe où), "prêt à partir", et "en match".

Pour le programmeur qui va écrire une stratégie, il faut que notre package lui permette d'exécuter une fonction de son choix lorsque le robot est "prêt à partir" et une autre pour gérer le match.

L'ads de notre package va donc être le suivant:

package Startup_Manager is
 type Main_Function is access procedure;
 type Init_Function is access procedure;
 
 procedure Start_Match (Init : Init_Function; Main : Main_Function);
end Statup_Manager;

Squelette de base

Pour commencer, on va récupérer le squelette de base du programme : pour cela, entrer la commande suivante dans un terminal (pour des explications sur hg, c'est ici).

hg clone <a>cd" title="http://bitbucket.org/telrob/startup<br />
cd">http://bitbucket.org/telrob/startup<br />
cd</a> startup
hg update -r 0

Voici la liste des fichiers important :

Première version

Question 1: Ecrire un "protected" pour l'état du robot. L'état du robot est une variable qui va être manipulé par plusieurs threads. Il faut donc la proteger avec un protected. Ce protected contiendra une fonction pour obtenir l'état courante et une procédure pour le définir.

Question 2: Ecrire une tache qui met à jour l'état du robot en fonction des boutons

(ma solution: hg update -r 1)

Question 3:Ajouter une famille d'"entry" à notre protected qui attend d'être dans un état donné.

Ensuite, on peut écrire une 1ère version de Start_Match:

procedure Start_Match (Init : Init_Function; Main : Main_Function) is
begin
 State.Wait_For (Ready_For_Match);
 Init.all;
 State.Wait_For (In_Match);
 Main.all;
end Start_Match;

Deuxième version

Cette 1ère version est pas mal mais si jamais Clement est abscent, on risque de se retrouver avec une personne un peu a la rue devant le robot. Ce qui serait bien, c'est que

Question 4: Changer la procedure "Start_Match" pour interrompre le travail en cours lors d'un changement d'état et lancer la nouvelle procédure.

Note : On appréciera la puissance du "select ... then abort ... end select" de l'Ada...

(ma solution: hg update -r 3)

Question 5 : Gérer la fin du match au bout de 30s.

Note: On pensera à stopper le timer si on change d'état avant la fin du match.

15 - Exercice : Gestion des déplacements

Maintenant qu'on a un interpréteur Forth et une procédure de démarrage sous la main, il sera bon de faire quelque chose... Pour commencer, je propose donc de faire bouger le robot un peu :)

Rappel sur l'architecture du robot

Architecture global

La partie soft du robot repose sur 2 carte "shix". Ces cartes contiennent un processeur (un sh4) et un FPGA. Traditionnellement, une carte est dédiée au traitement de la vidéo alors que l'autre sert à tout le reste, soit la gestion du déplacement, des capteurs/effecteurs du robot et la "stratégie". La première carte est nommé "camera" alors que l'autre est souvent désigné sous le doux nom de "master"

La carte master

Historique

Il y fort fort longtemps, un groupe d'élèves d'exception, encadré par un professeur tout aussi exceptionnel ont suivit un cours à Telecom nommé AMEN. Leur projet était un asservissement sur FPGA (un asservissement, parce que sans ca le robot n'est rien, sur FPGA parce qu'il fallait un projet sur FPGA). Ce projet fut un tel succès que nous en utilisons toujours le résultat. La gestion du déplacement est donc répartie entre le FPGA et le processeur. 

Dans ce tutorial, on va s'occuper uniquement de la partie processeur, en supposant que la partie FPGA existe déjà et fonctionne :). Pour info, rapidement, le FPGA se pilote en écrivant à des endroits spécifiques en mémoire. Heureusement, pas besoin de savoir où écrire quoi, on va utiliser un package tout fait

Note sur les unités

Le FPGA est super puissant, mais bon faire des multiplications, il aime pas trop donc on s'arrange pour choisir des unités les plus facile à manipuler pour lui. Ces unités sont le "top" de roues codeuse pour les distances (et non des bon vieux mètres) et la période d'asservissement pour l'unité de temps

Les vitesses s’expriment donc naturellement en tops de roue codeuse/période d'asservissement :)

Les angles s’expriment aussi en top de roue codeuse : 360° sera représenté par la distance parcourue par une roue pour que le robot fasse un tour, en laissant l'autre roue fixe.

Les trucs importants

Notre FPGA réalise un asservissement "polaire" (cf. les cours de RCVA), c'est à dire qu'il prend en entrée une distance à parcourir et un angle, ainsi que quelques paramètres, type vitesse et vitesse angulaire max. Ensuite, il se démerde pour parcourir cette distance et cet angle.

Conclusion, pour avancer tout droit, il faut définir une distance, mettre l'angle à 0 et lancer le processus.

Pour tourner, il y a une petite ruse : on pourrait mettre la distance à 0 et l'angle à autre chose, ce devrait marcher (à tester). Pour des raisons historiques, on ne fait pas comme ça : avant de réaliser notre asservissement, le FPGA multiplie la distance parcourue par la roue droite par un coefficient et celle parcourue par le roue gauche par un autre coefficient.

Si on met les 2 coefficients à "1", tout est logique, si on en met un à "1" et l'autre à "-1", tout se passe comme si on avait "retourné" un roue : quand on veut avancer, une roue par à l'envers et on tourne ! Donc pour tourner, la méthode testé est de mettre un coefficient à "1", l'autre à "-1", mettre une distance à parcourir et l'angle à 0

Simulation

Comme tout le monde n'a pas un robot à la maison, ou qu'un #{[^|\^~\@@~@ de mécano est en train de tout démonter, il nous faut un moyen de tester notre code sans robot physique. Pour cela, il existe un petit simulateur codé à l'arrache et récupérablecomme ceci :

<span style="color: #666666; font-size: 9pt">hg clone <a href="http://bitbucket.org/telrob/robotjavacc">http://bitbucket.org/telrob/robotjavacc</a></span>

ensuite, il faut importer ce projet dans Eclispe et y'a plus qu'à le lancer.

Note: Pour ceux qui aurait du temps : réécrire ce simulateur ne serait pas du luxe.

Squelette de base

Comme d'hab, on va déjà récupérer un peu de code histoire de pas tout refaire de zéro :

hg clone <a href="http://bitbucket.org/telrob/move">http://bitbucket.org/telrob/move</a>; cd move; hg update -r 0

après ça, on se retrouve avec un joyeux bordel. Il y a beaucoup de répertoire pour rester le plus proche possible de la structure du "vrai" code du robot (voir la page 'Ecrire un programme pour le robot').

Les répertoires shix/contest et shix/hwinterface ont un sous-répertoire "main" (et "camera" sur le "vrai" code) qui contient le code spécifique à la shix master.

Une bonne lecture pour commencer est "shix/hwinterface/main/shix-engines_base.ads" qui contient toute les fonctions nécessaire pour parler au FPGA.

1ers mouvements

Question 1: Completer les fonctions de move.adb

Indication: utiliser le package shix-units dont l'ads est dans shix/hwinterface/main

Notre robot roule !!

Maintenant, tentons un truc plus sympa : un bon vieux carré. On modifie donc test_move.adb pour y introduire:

for I in 1 .. 4 loop
 Move.Forward (500);
 Move.Turn (90.0);
end loop;

on lance et, et ça marche pas :(

En effet, Forward et Turn lance un mouvement mais n'attendent pas le fin de celui-ci. Lorsqu'on donne un nouvel ordre, le précédent est oublié (enfin pas tout à fait : le FPGA d'arrange pour limiter l'accélération du robot donc si on demande au robot de tourner à fond sur 360° et qu'au bout de 180° on lui dit d'aller tout droit, la trajectoire va être un peu bizarre puisqu'une roue va freiner doucement pendant que l'autre freine puis ré accélère dans l'autre sens)

On va donc introduire une nouvelle fonction : "Wait_Move_End" qui, comme son nom l'indique attend la fin d'un mouvement. Cette fonction va nous renvoyer la raison de la fin de se mouvement, ainsi que la distance qu'il reste à parcourir. Oui je sais, les boulets qui vont écrire la strategie ignoreront ces valeurs de retour, mais bon, c'est plus propre quand même.

Pour commencer, on définie 3 raisons de fin de mouvement : le mouvement est vraiment fini, on est bloqué quelque part (c'est à dire que les roues codeuses ne tournent plus) ou le mouvement prend trop de temps.

Question 2: Définir un type pour les raisons de fin de mouvement, et un autre pour la valeur de retour de Wait_Move_End

Maintenant, on va écrire cette fonction Wait_Move_End. Pour le 1er cas d'arret, c'est facile : le FPGA nous donne un bit "Moving" qui dit qu'il est en train de travailler. Quand ce bit passe à 0 c'est que tout va bien.

Question 3: Ecrire un premier "Wait_End_Move"

Indication: On écrira une tache qui verifie périodiquement l'état du bit "Moving". On prendra soin de faire communiquer les 2 taches par des objets "protected" et ca va rouler.

Théoriquement, c'est bon, on est les rois du monde, d'ailleurs en simulation, ca marche nickel. Maintenant, dans la vrai vie, ca devrait marcher souvent mais dans certain cas, le robot va taper dans un mur (ou un epis de mais, ou un robot adverse) et donc si on attend indéfiniement que le robot soit arrivé, on va rester planté dans le mur...

Question 4: Ajouter une gestion de timeout pour qu'on laisse tomber un mouvement qui mettrait trop de temps.

Indication: Pour aider l'utilisateur qui ne veut pas avoir à spécifier un temps maximal à chaque fois qu'il demande un mouvement, on va definir un vitesse minimale pour le robot. On en déduira un temps de parcourt maximal (on rajoutera un petit quelque chose par sécurité).

Pour amélirorer notre robot, on va maintenant détecter les bloquages. Pour cela, on va regarder la valeur des roues codeuses, si ca change trop peu entre 2 coups d'oeils, c'est qu'on est bloqué. Les roues codeuses sont des compteurs 16bits qui font régulirement des overflows : apres 2^16 - 1 vient 0. Pour une fois, on va voir un désavantage d'Ada : il est trop pointilleux et on ne peut pas faire une bête soustraction des 2 valeurs pour avoir la différence.

Par exemple, supposons qu'un compteur soit à 2^16 - 523, soit 65013. Ensuite, le robot avance de 742 tops le compteur va donc être 2^16 - 523 + 742 = 2^16 + 216. Le compteur étant sur 16 bits, il vaudra donc 216. Notre programme va lire 65013 puis 216 et il faudra qu'il en déduise 742. En Ada, on ne peut pas faire (216 - 65013) de façon simple : cette opération provoque un overflow que gentiement le programme nous remonte via une exception. La solution ? A vous de voir...

Question 5: Ajouter une tache qui verifie périodiquement la valeur des compteurs