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
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 :
-- commentaires. Tout ce qui suit -- sur la même ligne est considéré comme étant des commentaires. NOTE: En Ada on met en générale les commentaires après la spécification de la fonction et non avantRemarque 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.
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.
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];
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.
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.
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);
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
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;
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) |
if condition then liste d'instructions [elsif condition liste d'instructions] [...] [else liste d'instructions] end if;
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.
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;
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.
while condition loop liste d'instructions end loop;
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("Entrez un nombre (entre " & Integer'Image(Integer'First) & " et " & Integer'Image(Integer'Last) & ") : "); nombre1 := Integer'Value(Get_Line); Put("Entrez un 2nd nombre : "); nombre2 := Integer'Value(Get_Line); Put_Line("Le plus grand est " & Integer'Image(Integer'Max(nombre2, nombre2))); end attrib;
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=>12, b=>37, c=>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
facilement)
with Text_IO; use Text_IO; procedure enum is type Zsur4Z is (Zero, Un, Deux, Trois); Acc : Zsur4Z := Zero; begin loop Put_Line("Acc vaut " & Zsur4Z'Image(Acc)); Put("Combien voulez vous ajouter ? "); 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;
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.
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=>12, b=>37, c=>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); ...
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 <>) 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 <>) 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
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)
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 |
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 <>) of integer; tbl1 : t(1..6) := (1|3=>2, others=>12); -- les elements 1 et 3 valent 2, les autres 12 tbl2 : t := (1..10 => 4); -- 10 elements qui valent 4
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 <>) of integer; tbl1 : t1(1..10); tbl2 : t1(1..3) := tbl1(tbl1'Last-2..tbl1'Last);
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("1ere ligne: " & line); line := Get_Line; -- NE MARCHE PAS: line a deja une taille differente de celle -- de la nouvelle ligne Put_Line("2eme ligne: " & 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.
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).
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 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.
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;
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).
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;
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.
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 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 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;.
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, > 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;
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.
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.
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.
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;
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
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.
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 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 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;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; type Buffer_Type is array (1 .. Max_Size) of Integer;<br /> type Stack_Type is record<br /> 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.
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.
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 :
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.
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 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
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.
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.
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.
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.
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 + To_Time_Span (1.2); pour attendre 1.2 s sans rien faire.
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"
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.
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;
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 ».
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.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.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 ».make clean : efface tout le résultat de la compilation pour x86.make sh4-clean : efface tout le résultat de la compilation pour sh4.make install : effectue un make sh4 et copie le résultat sur schtroumpf/v12procedure 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)
with Move; use Move; procedure Test_Move is begin loop Forward(1000); Turn(90.0); end loop; end Test_Move;
make ou make sh4. C’est genial, ca compile !function Wait_End_Move return End_Move_Record; function Wait_End_Move(Max_Time : Duration) return End_Move_Record;
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;
4. Reason : End_Move_Record; | >>> warning: variable "Reason" is assigned but never read 8. Reason := Wait_End_Move; | >>> warning: useless assignment to "Reason", value overwritten at line 10
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;
pragma Warnings(Off, Reason) ; 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 !
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 :
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 <>, Positive range <>) 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
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 :)
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).
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;
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 :
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;
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.
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 :)
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"
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
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.
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
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.
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.
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 :(
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