Licence CC BY

Éléments de programmation

Publié :

Les commentaires

Les commentaires, même s'ils n'agissent pas directement sur le comportement du programme, sont des éléments essentiels dans le travail de programmation. Souvent ils sont perçus comme une contrainte et on en met parce qu'il faut en mettre, sinon c'est mal vu, alors que c'est une richesse et un complément au code qui s'avèrent utiles en particulier pour élaborer et faire évoluer un programme.

La forme

Deux façons de mettre des commentaires sont possibles :

1
2
3
4
/*
  Ceci est un commentaire
  sur plusieurs lignes
*/

Cette façon de commenter permet d'utiliser plusieurs lignes ce qui est très pratique quand on veut décrire, par exemple, un algorithme.

1
// Ceci est un commentaire court

Avec la deuxième façon, les commentaires sont limités à la ligne courante. On les place généralement à la droite du code pour préciser l'effet que cette ligne de code va produire. Le texte est libre dans les commentaires sauf pour la première forme où */ est interdit puisque cela signifie fin de commentaire. Il est également interdit d'imbriquer les commentaires longs, c'est-à-dire :

1
/* /* ceci est une construction invalide */ */

Quand mettre des commentaires ?

Il y a trois écoles :

  • jamais ;
  • quand on estime que c'est utile ;
  • à chaque ligne de code son commentaire.

Si on élimine les extrêmes, il ne reste plus que « quand on estime que c'est utile ». Or, ce n'est pas forcément toujours facile de déterminer si c'est utile ou pas.

Utile pour qui ?

Déjà pour soi ! Les débutants pensent souvent que ce n'est pas la peine, juste une perte de temps, parce que c'est mon code, c'est moi qui l'ai écrit, je sais comment ça marche, etc. Quand on revient, ne serait-ce que trois mois plus tard, sur son code et que l'on veut le modifier c'est à ce moment que l'on s'aperçoit que des commentaires auraient été utiles. Bien sûr on peut se remettre dans le bain avec un programme de 20 lignes, mais quand il y a 2000 lignes, 20000 lignes ou 200000 lignes c'est une toute autre histoire…

Ensuite le code est de plus en plus partagé et donc lu par des personnes différentes. Qu'on le veuille ou non, chaque programmeur a son style et « entrer » dans le code d'une autre personne n'est pas toujours immédiat, il y a besoin de guides comme une documentation et… des commentaires.

Donc « l'audience » va aussi déterminer la quantité de commentaires nécessaires et utiles. Si le programme s'adresse à des débutants qui apprennent le langage en plus de la programmation, mettre un commentaire par ligne, éventuellement redondant avec le code lui-même, est utile. En effet, cela permet de conforter le lecteur dans son apprentissage. En revanche pour un programme destiné à une audience de développeurs professionnels, c'est une pollution (essayez de proposer un correctif pour le noyau Linux avec un commentaire par ligne pour voir la réaction que cela suscite !).

Néanmoins, dès lors que l'on commence à être à l'aise et que l'audience du code s'adresse à des personnes au moins du même niveau, il convient de prendre l'habitude de mettre des commentaires et d'écouter le retour des lecteurs de façon à vérifier la pertinence des commentaires.

Les commentaires subliminaux

On n'y pense pas toujours mais les noms que l'on donne aux choses sont essentiels pour la compréhension. Un nom de variable doit indiquer la nature de l'objet ou de la grandeur qui est représentée, sa fonction, sa raison d'être… Pareil pour un nom de fonction. Toutefois, afin de raccourcir les noms il est fréquent d'utiliser des acronymes ou d'enlever la plupart des voyelles, d'utiliser un préfixe commun afin d'identifier une famille de fonctions, cela demande un petit peu d'entraînement.

Note : En programmation, dans le code, il est d'usage d'utiliser des mots anglais, je ne dérogerai pas à cette règle dans les exemples, en revanche les commentaires seront en français.

Par exemple voici quelques noms de fonctions utilisées en langage C pour manipuler des chaînes (cela n'a aucun intérêt pour l'environnement Arduino, c'est juste une illustration):

1
2
3
4
5
strcmp   // STRing CoMPare - comparaison de chaînes
strcpy   // STRing CoPY - copie d'une chaîne
strdup   // STRing DUPlicate - duplication d'une chaîne
strlen   // STRing LENgth - longueur d'une chaîne
...

Le nom doit suffisamment être évocateur pour que même une personne n'ayant pas connaissance de cette fonction puisse deviner à quoi elle sert et continuer la lecture du code.

En contre exemple voici ce qu'il faut éviter à tout prix :

1
int var = 0; // j'initialise var à 0

Aucune sémantique, ni côté code ni côté commentaire.

En revanche :

1
int fill_70_flag = 0; // indique seuil de remplissage 70% atteint

Possède de la sémantique tant côté code que côté commentaire.

Où mettre un commentaire ?

Au début du programme

Outre un descriptif sommaire, le nom de l'auteur et la licence applicable. C'est l'endroit idéal pour expliquer ce que vous allez faire. Ce type de commentaire s'écrit avant de coder et permet de vérifier que vous avez une idée précise de ce que vous allez faire tout en étant capable de l'expliquer à quelqu'un d'autre. C'est souvent le moment où l'on découvre des problèmes auxquels on n'avait pas pensé ! Par ailleurs, c'est aussi l'occasion de documenter un algorithme un peu compliqué ou de mettre des références vers des ressources (liens vers des sites web, références à des livres…) qui vous ont servi pour le travail préparatoire. Il faudra probablement revenir compléter ce commentaire une fois le programme réalisé car nul doute qu'il y aura des précisions ou des choses de découvertes entre temps qui méritent d'y figurer.

Voici un exemple (fictif et tronqué) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*
  Pilotage d'une fraiseuse pour réaliser des circuits imprimés
  Copyright 2014 © fb251
  Code sous licence WTF 2.0 : http://wtflicence.com

  Nous nous appuyons sur le format RS-274X (Gerber) révision H
  les spécifications sont téléchargeables sur : http://www.ucamco.com/

  Note : ce format est assez vieux (~ 1970) et est dérivé de la 
  norme RS-274. Il faut faire très attention aux arrondis selon
  que le fichier source utilise comme unité les pouces ou le 
  système métrique. Par ailleurs, certains logiciels comme Eaggle
  ne respectent pas strictement le format ce qui oblige à des
  contorsions (signalés dans le code avec le marqueur $HACK$).

  La difficulté principale consiste à créer les pistes d'isolement
  car il faut inverser la polarité RS-274 mais ce n'est pas suffisant
  aussi nous utilisons l'algorithme...
*/

Notez qu'une bonne pratique consiste à indiquer que l'on utilise des marqueurs dans les commentaires du code (ici $HACK$) pour signaler que le code a un comportement particulier et non naturel dicté par des conditions spécifiques. Des marqueurs standards existent, citons :

  • TODO - il reste du code à faire,
  • FIXME - il y a un bug à fixer.

Personnellement j'encadre le marqueur avec des $ comme ça c'est très rapide à trouver avec la fonction de recherche de l'éditeur de texte.

Voici un exemple d'utilisation de marqueur :

1
if (0) // $FIXME$ : on n'exécute jamais la branche if !
Avant une fonction

Lorsque l'on utilise du code modulaire découpé en fonctions, dès lors que celles-ci ne sont pas triviales, il convient de placer en en-tête de fonction un commentaire sous forme multi lignes qui va expliquer le rôle de la fonction.

Voici un exemple tiré cette fois de code assembleur (le code ne figure pas, on ne s'intéresse qu'aux commentaires) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/*! int fss_strcmpa (const char * s1, const char * s2)

  @brief Compare rapidement deux chaînes de caractères selon les
  même conventions que strcmp()

  @param s1 est la première chaîne
  @param s2 est la deuxième chaîne
  @return > 0 si s1 > s2, < 0 si s1 < s2 et 0 si s1 == s2

  @remark ATTENTION ! Cette fonction ne peut fonctionner qu'avec des 
  chaînes alignées sur des frontières modulo 16 octets, la fin de la 
  chaîne doit être complétée avec des zéros jusqu'à la frontière de 
  16 octets qui suit. Les registres xmm0, xmm1, xmm2, rcx et rdx sont 
  écrasés. Le processeur doit obligatoirement disposer de SSE > 2.

  @remark Le code a été testé et optimisé sur AMD Phenom II X6, X8 
  et Intel Core i3, Xeon, Pentium et Atom.
*/

Évidemment, c'est long et ça paraît fastidieux à taper, mais le code qui suit derrière (58 lignes) a demandé 3 jours de travail donc tout est relatif ! Par ailleurs, et cela peut s'appliquer à du code Arduino, il est possible de créer automatiquement la documentation technique avec des outils comme DOxygen qui va interpréter les mots clés qui commencent par @ à l'intérieur des commentaires.

Donc s'astreindre à mettre des commentaires normalisés en en-tête de fonction peut faire gagner énormément de temps en plus de la clarté que cela procure, car chaque fonction peut-être vue comme un contrat : voilà ce que je fais, dans quelles conditions et ce dont j'ai besoin.

C'est, bien sûr, démesuré dans le cadre des projets de ce MOOC, mais si un jour vous vous lancez dans la réalisation d'une fraiseuse numérique…

Que mettre dans un commentaire (pour du code) ?

Nous avons vu que cela dépendait de l'audience concernée par la lecture du code aussi nous allons essayer à chaque fois de donner des exemples dans différents contextes.

1
i ++; // on incrémente i

Le commentaire paraphrase le code, si l'audience connaît l'opérateur ++ l'intérêt est nul. Si l'opérateur ++ n'est pas connu ça apporte une information mais dans ce cas on préferera probablement :

1
i ++; // i ++ est équivalent à i = i + 1

Qui contient plus de sémantique et qui oblige le lecteur à se concentrer pour assimiler cette nouvelle information.

Si maintenant i n'est pas simplement une « vulgaire variable de boucle » mais sert à trouver le premier emplacement non vide dans un tableau il est peut-être préférable : de changer son nom et d'expliquer ce que l'on fait.

1
idx ++; // on va tester si l'emplacement suivant est non vide

idx se comprend comme la contraction du mot index et le commentaire indique à quel endroit dans l'exécution de l'algorithme nous en sommes (algorithme qui a bien sûr été décrit préalablement en en-tête de fonction ou de programme, n'est-ce pas ?).

Autre cas de figure : une construction de code parfaitement valide mais peu usuelle qui peut être difficile à déchiffrer, même pour un développeur qui n'est pas débutant (que Glenn me pardonne mais dans ce cas précis j'ai à utiliser du code un peu tricky) :

1
flag ^= 1;

Ça ira peut-être mieux avec un commentaire ?

1
flag ^= 1; // inversion : 0 -> 1 ou 1 -> 0 via un ou exclusif

Pour terminer cette partie voici un exemple complet avec du code simple utilement commenté :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
  int fahr2celsius (int fahrenheit)

  Convertit des degrés Fahrenheit en degrés Celsius.

  Paramètre : fahrenheit la température en degrés Fahrenheit
  Sortie : la valeur convertie en degrés Celsius

  Notes :
   - le résultat est un entier et nous utilisons des entiers
   pour le calcul donc la valeur est arrondie et approximative
   - si le résultat conduit à une température plus basse 
   que le zéro absolu nous retournons le zéro absolu soit
   -273°C

  La formule permettant la conversion °F -> °C est la suivante :

  °C = (°F - 32) / 1,8

  Toutefois, comme nous travaillons avec des entiers et souhaitons
  minimiser les approximations nous allons réaliser les opérations
  suivantes dans cet ordre :

  °C = ((°F - 32) * 5) / 9       

  Résultats attendus :

  0 °F    -> -17 °C
  32 °F   -> 0 °C
  50 °F   -> 10 °C
  212 °F  -> 100 °C
  -458 °F -> -272 °C
  -475 °F -> -273 °C        

  Documentation utilisée : https://fr.wikipedia.org/wiki/Fahrenheit
*/

int fahr2celsius (int fahrenheit)
{
  int temp;               // résultat de la conversion

  temp = fahrenheit - 32; // on retranche le décalage
  temp = (temp * 5) / 9;  // on s'assure que la multiplication
                          // est faite avant la division pour
                          // limiter la perte de précision

  if (temp < -273)        // en dessous du zéro absolu ?
      temp = -273;        // il faisait un peu (trop) froid !

  return temp;
}    

En résumé

Commenter bien est un exercice difficile et demande de la pratique et des lecteurs. Cela peut sembler curieux mais quand on a l'habitude l'essentiel du travail intelligent de programmation se fait pendant que l'on écrit les commentaires, le reste, dans le jargon, ça s'appelle « pisser de la ligne ».

Programmation structurée (programmation avancée)

Programmation structurée

De l'utilité de la programmation structurée

Dans les premiers cours de ce MOOC nous avons appris à donner des instructions séquentielles à la carte Arduino. Toutefois, il est facile d'imaginer que lorsque le problème est d'une nature complexe et nécessite beaucoup d'instructions pour sa résolution, l'approche séquentielle (ou linéaire) n'est plus satisfaisante.

Heureusement, les langages informatiques ainsi que les processeurs permettent depuis quasiment le début de faire de la programmation structurée qui facilite, en particulier, la lisibilité et la compréhension, la maintenance du code et le travail à plusieurs sur un projet.

C'est quoi ?

Il y a deux grands axes :

  • au niveau macroscopique : la modularité, c'est-à-dire découper un gros morceau en plus petit morceaux,
  • au niveau microscopique : le contrôle du flux d'instructions.

Modularité

Pour l'essentiel, et dans le cadre de ce MOOC, il y a deux leviers :

La fonction

Une fonction permet d'encapsuler du code avec une petite latitude de paramétrage. L'intérêt est que cela évite de dupliquer des instructions et qu'en découpant le code en série de fonctions, celui-ci devient plus clair et plus compréhensible.

Une fonction peut accepter des paramètres en entrée et renvoyer une valeur en sortie. Aussi bien les paramètres que la valeur retournée sont facultatifs. La déclaration générique d'une fonction se fait comme ceci :

1
2
3
4
sortie nom (paramètre_1 arg1, paramètre_2 arg2, ..., paramètre_n argn)
{
  corps de la fonction
}

sortie et paramètre_* sont des types. Nous allons prendre comme exemple une fonction qui allume une LED pendant une durée exprimée en millisecondes. Cette fonction ne retournera pas de valeur mais prendra deux paramètres : la broche où la LED est raccordée et la durée pendant laquelle la LED devra être allumée. Cela se traduit de la façon suivante :

1
2
3
4
5
6
void lightLED (int pin, int duration)
{    
  digitalWrite (pin, HIGH);
  delay (duration);
  digitalWrite (pin, LOW);
}

Si on reprend le TP n°2 mais en utilisant cette nouvelle fonction, loop() se réduit à :

1
2
3
4
5
6
void loop ()
{
  lightLED (led_verte, 3000);
  lightLED (led_orange, 1000);         
  lightLED (led_rouge, 3000);        
}

Même avec cet exemple très simple l'intérêt est assez évident et immédiat.

Pour retourner une valeur l'instruction return est utilisée, par exemple pour une fonction qui réalise une addition :

1
2
3
4
int add (int a, int b)
{
  return a + b;
}
La bibliothèque

Une bibliothèque (mais c'est souvent - à tort - le mot librairie qui est utilisé), peut être vue comme un module contenant une collection de fonctions spécifiquement dédiées à une tâche (par exemple : piloter un écran à cristaux liquides, commander un moteur pas-à-pas, …),

Nous avons déjà utilisé une bibliothèque (celle qui pilote le port USB pour afficher des valeurs sur l'écran de l'ordinateur et qui commence par le préfixe Serial).

Nous en resterons là pour les bibliothèques.

Contrôle du flux d'instructions

Le flux d'instruction est séquentiel mais des constructions permettent de casser cette séquentialité : les conditionnelles, les cas et les boucles.

Les conditionnelles

Nous avons déjà vu une forme de conditionnelle : l'instruction if :

1
2
3
4
5
6
7
8
if ( *condition est à vrai* )
{
  faire quelque chose
}
else
{
  faire autre chose
}

Cette forme de conditionnelle permet de choisir la branche d'instructions à exécuter :

  • Si la condition est évaluée à vrai le code contenu dans la branche if est exécuté.
  • Si elle est évaluée à fausse ce premier bloc de code n'est pas exécuté et si une branche else est présente c'est le code de cette branche qui sera exécuté.

Remarque : une condition est vraie dès lors que son évaluation donne une valeur différente de zéro, une valeur égale à zéro correspond à une condition fausse.

Ainsi if (1) sera évaluée à vraie et if (0) à fausse. On peut utiliser des variables ou des expressions arithmétiques complexes.

Par exemple :

1
if ((variable % 2) == 0) ... // si la variable est paire

L'opérateur modulo % teste le reste de la division. Si la variable est divisible par deux le résultat de l'expression donnera zéro qui comparée à zéro donnera vrai, c'est-à-dire un.

Ou sous une forme plus compacte :

1
if (! (variable % 2)) ... // si la variable N'est PAS IMpaire

Sous cette forme, plutôt que de faire une comparaison avec zéro nous inversons le résultat : si la variable n'est pas impaire (donc expression vraie si la variable est paire). Cela demande un peu d'habitude pour être à l'aise avec ce type de formulation aussi il est préférable d'utiliser les formes explicites pour commencer.

L'opérateur ! inverse le résultat de la condition, attention aussi à l'opérateur de comparaison == qu'il ne faut pas confondre avec l'opérateur d'affectation =.

Remarque : c'est un choix personnel mais je préfère toujours utiliser des parenthèses pour indiquer l'ordre dans lequel les opérations doivent être effectuées.

1
2
3
4
5
6
7
int i = 2;

if (i)
{
  fonction () ;
  i = i - 1;
}

Le code ci-dessus permet d'appeler deux fois une fonction, c'est ce que l'on appelle une boucle et comme c'est très utilisé en programmation il existe des instructions spéciales, ce qui permet d'éviter la construction ci-dessus qui n'est pas recommandable. Nous aborderons les boucles un peu plus loin.

Les cas

Les cas permettent une écriture plus légère d'une forme de conditionnelles. C'est beaucoup utilisé avec les automates. Voci la syntaxe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
switch ( expression )
{
  case ( valeur_1 ) :
    traitement
    break;

  case ( valeur_2 ) :
    traitement
    break;

  ...

  case ( valeur_n ) :
    traitement
    break;

  default :
    traitement
    break;
}

L'expression est évaluée puis en fonction de la valeur obtenue on va se brancher sur le cas qui correspond à cette valeur. Si aucun cas ne correspond et si le cas default est présent (c'est optionnel) alors c'est le traitement qui correspond à default qui sera exécuté.

Remarque : l'instruction break permet de bien cloisonner les cas, car une fois un cas traité on sort du switch. Sans les instructions break, si nous avions à traiter le cas valeur_1 nous exécuterions en fait l'ensemble du code du switch ! Cela peut avoir à certaines occasions un intérêt mais avant d'en arriver là n'oubliez pas de mettre break après chaque cas !

Les boucles

La forme, à mon avis, la plus simple de boucle est la boucle while. While signifie en anglais « tant que ». Elle s'utilise de la façon suivante :

1
2
3
4
while ( condition )
{
  faire quelque chose
}

Deux constructions « extrêmes » sont possibles : while(1) la boucle est exécutée sans fin, while(0) la boucle n'est jamais exécutée. Comme avec les conditionnelles condition peut être une expression complexe.

Une autre forme de boucle, très proche de la boucle while est la boucle do...while, ce qui signifie « faire tant que ». La façon dont cette boucle est construite implique que le code de la boucle sera exécuté au moins une fois ; en effet la condition n'est testée qu'à la fin de la boucle. Voici comment elle s'utilise :

1
2
3
4
do
{
  faire quelque chose
} while ( condition );

Notez que la syntaxe du langage exige un ; dans ce cas après le while.

Enfin la dernière forme de boucle présentée dans ce document: la boucle for ce qui signifie « pour ». Bien maniée elle permet des constructions impressionnantes mais nous nous contenterons dans ce document d'un usage simple et sage ! Voici sa syntaxe :

1
2
3
4
for (initialisation ; condition ; action)
{
  faire quelque chose
}

L'utilisation la plus courante de cette boucle est d'itérer x fois, voici un exemple :

1
for (int i = 0 ; i < 10 ; i ++) ...  // on exécute 10 fois la boucle

Décomposons ce qui se passe. L'initialisation de la boucle consiste en la déclaration d'une variable i initialisée à 0, l'action à chaque fois que l'on passe dans la boucle est d'incrémenter i (la notation i ++ est équivalente à i = i + 1) et enfin la condition pour que la boucle s'exécute est que i soit strictement inférieure à 10.

Mais souvent on comprend mieux visuellement en réécrivant une boucle for sous la forme d'une réécriture avec une boucle while, voici ce que cela donne :

1
2
3
4
5
6
7
int i = 0 ;             // initialisation

while (i < 10)          // condition
{
  faire quelque chose
  i ++;                 // action
}

Enfin et c'est valable pour toutes les boucles décrites dans ce document il existe des instructions spéciales qui permettent de mettre fin à une boucle ou d'éviter de continuer l'exécution du code de la boucle pour passer à l'itération suivante.

L'instruction break, que nous avons déjà vue en conjonction avec switch, « rompt » la boucle, elle s'utilise comme ceci :

1
2
3
4
5
6
7
8
9
while ( condition_1 )        
{
  faire quelque chose

  if ( condition_2 )
    break; // on casse la boucle et on sort

  suite de faire quelque chose
}

C'est une instruction très utile car cela facilite le traitement d'un cas exceptionnel. La deuxième instruction est continue qui donne ordre à la boucle de passer immédiatement à l'itération suivante :

1
2
3
4
5
6
while ( condition_1 )
{
  if ( condition_2 )
      continue;
  faire quelque chose
}

Si condition_2 est vraie pour l'itération courante faire quelque chose ne sera pas exécuté, continue forcera une nouvelle itération. Cette instruction peut-être très utile pour gérer des cas particuliers.

J'espère qu'avec ces quelques « munitions » vous serez armés pour faire du beau code bien structuré.

Arduino Thérémine et Processing (programmation avancée)

Le TP sur le thérémine semblait une bonne occasion de tester la liaison Arduino Processing afin de faire varier un son géré par Processing à partir du niveau de lumière détecté par Arduino. Sans aucune expérience sur Processing cela semblait difficile mais à partir de deux exemples fournis dans l’environnement Processing, l’objectif semblait atteignable.

Elément positifs :

Processing s’installe facilement (mais le temps d’installation est plus grand), l’éditeur est très semblable à celui d’Arduino, la programmation utilise un langage identique et la forme générale des programmes est également très proche d’Arduino.

Cahier des charges :
À partir de deux LDR mesurant le niveau de lumière (main droite et main gauche pour faire varier la luminosité sur les LDR) nous désirons faire varier la fréquence et l’amplitude d’un son produit par la carte son du PC.

Plan de travail :
  • Faire générer alternativement deux valeurs numériques par Arduino, l’une représentative du niveau de lumière sur la LDR de droite l’autre représentative de la LDR de gauche.
  • Communiquer ces deux valeurs à Processing en utilisant la liaison série.
  • Implanter dans Processing une lecture de ces deux valeurs et les utiliser pour générer le son, cette fonctionnalité étant prévue dans processing (voir l’exemple fourni dans « File/Exemples/Librairies/minim/SynthesizeSound »).

Première question : comment échanger des données entre Arduino et Processing ?

Deuxième question : comment modifier les caractéristiques du son produit par Processing à partir de ces deux valeurs ?

Troisième question liée à la seconde : quelles sont les contraintes de la programmation dans Processing ?

Reponse à la question 3
Tous les exemples commencent par inclure une ou plusieurs bibliothèques spécifiques aux éléments à mettre en œuvre (liaison série, son, images, vidéo …).

Pour les déclarations et les initialisations (setup()), identique à Arduino.

Pour loop(), il semble que cette procédure soit systématiquement remplacée par draw() qui a la même fonction mais doit certainement inclure l’appel à d’autres procédures pouvant être déclarées dans les programmes mais sans jamais être appelées. Il semble possible de remplacer draw() par loop() dans plusieurs programmes testés mais il est nécessaire de rajouter dans cette boucle les appels à toutes les procédures déclarées dans le programme.

Réponse à la question 1
La lecture par processing de valeurs numériques fournies par Arduino est relativement simple, l’exemple ci-dessous permet de vérifier cette fonctionnalité :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*Programme pour Arduino
  Le programme place sur la sortie série des valeurs de 0 à 255 et
  de 255 à 0 avec une pause de 10ms entre chaque nombre.

  Ce programme n'est pas commenté car il est simple
  avec des instructions connues
*/

int val=0;

void setup()
{
  Serial.begin(9600);
}

void loop() 
{
    for (int i=0;i<255;i++)
    {
    Serial.write(i);
    delay(10);
    }
    for (int i=255;i>0;i--)
    {
    Serial.write(i);
    delay(10);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*Programme pour Processing
  Le programme fait changer la couleur d’un rectangle affiché dans une fenêtre.
  Les couleurs du noir au blanc sont définies par la valeur
  reçue sur la liaison série
  Ce programme est largement inspiré de l'exemple "serial/simpleread"
*/

import processing.serial.*; // importation de la bibliothéque serial
Serial myPort; // Création d’un l’objet Serial class
int val; // Donnée en provenance de la liaison série

void setup() 
{
  // taille de la fenêtre d’affichage
  size(200, 200);
  // déclaration du port de la liaison série (ne pas modifier même pour CM3)
  String portName = Serial.list()[0];
  // déclaration des caractéristiques de la liaison série
  myPort = new Serial(this, portName, 9600);
}

void draw()
{
  if ( myPort.available() > 0) // si une donnée est présente
  {
    // lecture de la donnée
    val = myPort.read();
  }
  // mise à blanc du fond
  background(255);
  // choix de la couleur du rectangle en fonction de la valeur lue
  fill(val);
  // tracé du rectangle
  rect(50, 50, 100, 100);
}
Utilisation des deux programmes

1°) Charger le programme Arduino et l’exécuter.

2°) Charger le programme Processing et l’exécuter

Si tout est OK, la fenêtre s’ouvre et le rectangle change de couleur toutes les 10ms (2mn pour passer progressivement du noir au blanc).

Gérard04

L'indentation

Beaucoup de participants au MOOC l'ont constaté : les codes sont parfois difficilement lisibles. Il existe une manière de présenter son code de façon à le rendre compréhensible par quelqu'un d'autre : l'indentation.

Il ne s'agit que de forme : cela n'a aucune incidence sur le fonctionnement du programme.

Par exemple, ce code est valide mais illisible par un humain :

1
const unsigned char a = 0; unsigned char b = a; unsigned int c; if ((a + b) == c) a = a + 1; else a = 2;

Le même, correctement indenté :

1
2
3
4
5
6
7
  const unsigned char a = 0; 
  unsigned char b = a;  
  unsigned int c; 
  if ((a + b) == c) 
    a = a + 1; 
  else  
    a = 2; 

Cette manière de décaler le texte, de faire ainsi apparaître sa structure, c'est l'indentation.

Les principes sont simples :

  1. Quand on rentre dans une section de code, on décale tout le contenu de cette section d'une tabulation.
  2. Quand on sort d'une section de code, on retire une tabulation pour la suite.

Ainsi, voici un code correctement indenté :

1
2
3
4
5
6
7
int a = 0;
int b = 0;
for (a = 0; a < 10; a++)
  if (a < 5)
    b = b + 1;
  else
    b = b + 2;

Pour identifier facilement les sections, il est recommandé d'utiliser les accolades {}. Le code devient alors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int a = 0;
int b = 0;
for (a = 0; a < 10; a++) {
  if (a < 5) {
    b = b + 1;
  }
  else {
    b = b + 2;
  }
}

Le problème, c'est que l'on se retrouve avec une série d'accolades fermantes sans savoir ce qu'elles ferment. Il est alors très utile de commenter son code… CQFD ;)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int a = 0; // Variable pour le FOR
int b = 0; // Variable résultat
for (a = 0; a < 10; a++) { // Boucle de traitement
  if (a < 5) { // Test sur la valeur de a
    b = b + 1; 
  } // Fin si a < 5
  else { // a >= 5
    b = b + 2;
  } // Fin si a >= 5
} // Fin boucle de traitement

C'est tout de suite plus clair. Et d'expérience, c'est indispensable quand on reprend son propre code après quelques jours.

Encore une fois, toutes ces astuces ne constituent que de la forme et n'affectent pas le bon fonctionnement du programme. Mais cela permet de corriger très vite du code en identifiant rapidement les sections à problème.

Il y a cependant quelques exceptions à cette règle esthétique : les langages Python et Cobol par exemple, qui font de l'indentation du code un élément indispensable, tellement indispensable qu'on peut même se passer d'accolades ! (mais pas des commentaires…)

En Python, un code mal indenté dysfonctionnera à coup sûr.

TPE

Les tableaux

Un tableau est une série de données stockées consécutivement. Dit comme cela c'est peu vendeur et pourtant le tableau est un outil très utile en programmation, en particulier lorsque l'on fait des automates (des feux de circulation au hasard).

Plutôt que de multiplier les variables, comme malheureusement on le trouve fréquemment dans du code, typiquement :

1
2
3
int etat1 = 0 ;
int etat2 = 0 ;
int etat3 = 0 ;

Il existe une autre méthode qui consiste à regrouper des variables dans une même zone. Cette zone sera accessible comme une variable par son nom, mais vient s'ajouter un index précisant l'élément du tableau sur lequel on désire travailler. Un tableau se déclare de cette façon dans un programme :

1
int etats [3] = { 0, 0, 0 } ;

L'« équivalence conceptuelle » est assez simple entre les deux formes, mais celle-ci n'apporte rien de concret ou de directement perceptible :

1
2
3
etat1 = etats [0] ;
etat2 = etats [1] ;
etat3 = etats [2] ;

Dans la plupart des langages de programmation l'index du premier élément est à zéro et le dernier vaut n-1 pour n éléments. Cela demande un peu de gymnastique mais nous allons voir que c'est très pratique car c'est de l'adressage indexé. Au lieu d'écrire par exemple :

1
2
3
4
5
6
7
8
if (etat1 == 1)
  ...
else if (etat2 == 1)
  ...
else if (etat3 == 1)
  ...
else
  ... // Ooppps : c'était pas prévu

On va utiliser cette forme :

1
2
if (etats [index] == 1)
  ...

En maintenant une variable index qui permet de savoir où l'on se trouve et sur quoi on travaille, et donc de réduire la combinatoire qui existe lorsque l'on travaille avec des variables indépendantes et les risques d'erreurs associés.

Utilisés conjointement avec une boucle, les tableaux permettent de réaliser des initialisations quelque peu fastidieuses de façon économique, par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# define PINS 3

int outputs [PINS] = { 1, 2, 3 } ;  // broches en sortie
int inputs [PINS] = { 4, 5, 6 } ; // broches en entrée


// Initialise les entrées / sorties du microcontrôleur

void setup ()
{
  int i ;

  for (i = 0 ; i < PINS ; i ++)
  {
    pinMode (outputs [i], OUTPUT) ;
    pinMode (inputs [i], INPUT) ;
  }
}

On peut mettre tout ce que l'on veut dans un tableau dès lors qu'il y a assez de mémoire et que les éléments sont du même type. Par exemple des caractères :

1
char message [5] = { 'M', 'O', 'O', 'C', 0 } ;

Cette suite de caractères est terminée par un zéro qui n'est pas le caractère représentant le zéro (sinon il aurait été placé entre apostrophes), mais la valeur zéro qui va nous servir de marqueur de fin de message.

Utilisé avec une boucle, voici une façon d'utiliser le tableau (on suppose qu'une fonction magique putc () permet d'afficher un caractère sur un dispositif quelconque) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void loop ()
{
  int idx = 0 ;

  while (message [idx])
  {
    putc (message [idx]) ;
    idx = idx + 1 ;
  }

  for (;;)    // forever : boucle infinie        
    ; 
}

La décomposition sous une forme « plus littéraire » de la fonction est comme suit :

  • tant que l'on ne tombe pas sur un élément du tableau ayant comme valeur zéro :
    • on lit l'élément du tableau pointé par l'index courant (idx),
    • on affiche ce caractère,
    • on passe à l'index suivant,
  • on arrête d'afficher (en partant volontairement en boucle infinie).

Bien évidemment si l'on change le contenu du tableau message (en prenant soin tout de même de laisser le zéro qui sert de marqueur de fin) il n'y a absolument rien à modifier dans le code de loop (). C'est intéressant car c'est presque changer du code mais en ne touchant qu'à des données…

Maintenant que nous avons un nouvel outil à notre disposition voyons comment l'utiliser. On reprend le TP n°2 (le feu tricolore : 3 secondes au vert, une seconde à l'orange et trois secondes au rouge). C'est un cycle de trois états, et chaque état est caractérisé par un couple couleur et durée. Ça ressemble à des données stockées consécutivement sous la forme d'un tableau contenant trois couples couleur et durée, la couleur ayant un index pair (0, 2 et 4) et la durée un index impair (1, 3 et 5) dans ce tableau :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Six éléments numérotés de 0 à 5

int feu [6] =
{
  // état 0 (vert, 3 s)
  1,                      // broche led verte, index 0
  3000,                   // durée en ms, index 1

  // état 1 (orange, 1 s)
  2,                      // broche led orange, index 2
  1000,                   // durée, index 3

  // état 2 (rouge, 3 s)
  3,                      // broche led rouge, index 4
  3000                    // durée, index 5
} ;

Le programme est quasiment écrit il ne reste plus « qu'à connecter » les données avec un peu de code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// On paramètre les broches dont on a besoin en output

void setup ()
{
  int idx = 0 ;

  // Les numéros de broches sont aux index 0, 2 et 4
  for (idx = 0 ; idx < 3 ; idx = idx + 1)
    pinMode (etat_feu [idx * 2], OUTPUT) ;
}


int state = 0 ; // indique l'état dans lequel nous sommes
                // en déplacant un curseur (index) sur le
                // tableau

void loop ()
{
  digitalWrite (feu [state], HIGH) ;  // la broche
  delay (feu [state + 1]) ;           // le délai
  digitalWrite (feu [state], LOW) ;        

  // On se prépare pour l'état suivant
  state = state + 2 ;                 // couple suivant

  // Si l'on est arrivé en fin de tableau, on recommence depuis le début
  if (state > 4)
    state = 0 ;
}

Nous avons réalisé un automate qui se contente de boucler à partir des données du tableau et qui en fin de tableau recommence au début, ce n'est pas très spectaculaire, en revanche voyons comment on peut utiliser cette technique sur un problème nettement plus complexe. Vous vous souvenez du feu piéton avec bouton ?

En temps normal le feu piéton reste au rouge et le feu voiture enchaîne le cycle vert, orange, rouge habituel. Si un piéton, alors que le feu voiture est au vert, appuie sur le bouton alors le feu voiture enchaîne un cycle vert, orange, rouge mais en ajoutant deux secondes au feu rouge et en faisant passer le feu piéton au vert quand le feu voiture sera au rouge.

Donc un état se résume à la couleur du feu voiture, la couleur du feu piéton et le délai mais il y a deux cycles : le cycle normal et le cycle bouton appuyé.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// les états de notre système

int feux [21] =
{    
  // Pivot
  // -----

  // état 0, vert, rouge, 3 s
  1,              // led verte feu voiture, index 0
  4,              // led rouge feu piéton, index 1
  3000,           // durée en ms, index 2

  // Cycle normal
  // ------------

  // état 1, orange, rouge, 1 s
  2,              // led orange feu voiture, index 3
  4,              // led rouge feu piéton, index 4
  3000,           // durée en ms, index 5

  // état 2, rouge, rouge, 3 s
  3,              // led rouge feu voiture, index 6
  4,              // led rouge feu piéton, index 7
  3000,           // durée en ms, index 8

  // état 3 : marqueur de fin
  0, 0, 0,        // index 9, 10 et 11

  // Cycle bouton appuyé
  // -------------------

  // état 4, orange, rouge, 1 s
  2,              // led orange feu voiture, index 12
  4,              // led rouge feu piéton, index 13
  3000,           // durée en ms, index 14

  // état 5, rouge, vert, 5 s
  3,              // led rouge feu voiture, index 15
  5,              // led verte feu piéton, index 16
  5000            // durée en ms, index 17

  // état 6, marqueur de fin
  0, 0, 0         // index 18, 19 et 20 
} ;

# define    START_NORMAL_CYCLE      0   // état de départ cycle normal    
# define    START_CROSSING_CYCLE    12  // état de départ cycle bouton appuyé

On passe sous silence la partie setup() qui n'est guère passionnante exceptée la mise en place d'une interruption sur le bouton pour se concentrer sur loop()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int state = 0 ;                 // état courant
volatile int crossing = 0 ;     // état du bouton (changé par interruption)


// Effectue l'allumage et l'extinction en fonction de l'état
void action (int index)
{
    digitalWrite (feux [index], HIGH) ;     // voiture
    digitalWrite (feux [index+1], HIGH) ;   // piéton        
    delay (feux [index+2]) ;                // délai
    digitalWrite (feux [index], LOW) ;      // voiture
    digitalWrite (feux [index+1], LOW) ;    // piéton            
}

void loop ()
{

  action (state) ;                    // on traite l'état courant

  if (state == START_NORMAL_CYCLE)    // feu voiture au vert ?
  {
    if (crossing)                     // tester l'état du bouton
      state = START_CROSSING_CYCLE ;  // passer au cycle bouton appuyé
  }
  else
    state = state + 3 ;               // passer au triplet suivant 
                                      // (= état suivant dans le cycle)

  crossing = 0 ;                      // désarmer le bouton

  if (feux [state] == 0)              // marqueur de fin atteint ?
    state = START_NORMAL_CYCLE ;      // on revient au début du cycle
}

Le code est concis car la logique est très simple :

  • traiter l'état courant,
  • déterminer l'état suivant,
  • reboucler.

En fonction de l'état du bouton quand le feu voiture est vert, nous choisissons le cycle à exécuter, nous l'exécutons état par état jusqu'à tomber sur le marqueur de fin puis revenons à l'état de départ. L'état zéro (feu vert pour les voitures est un pivot car il est commun aux deux cycles : c'est à l'issue de l'état zéro que l'on va décider, en fonction du bouton, si on passe à l'état 1 (cycle normal) ou à l'état 4 (cycle bouton appuyé).

Toute la difficulté consiste à concevoir le tableau. Faire un schéma sous la forme d'un graphe est souvent une bonne idée de façon à valider le modèle et à s'assurer qu'aucun cas n'a été oublié. Il existe beaucoup de variantes de cette technique mais précisons que cela ne sert pas qu'à faire des automates pour des feux ; on peut citer la réalisation de protocoles de communication, des compilateurs, etc.

L'inconvénient est que si le code est simple, les données le sont moins. Or, sur un automate un peu compliqué avec un tableau volumineux, une modification peut être délicate à faire et exige de la rigueur dans sa réalisation. Néanmoins, dans beaucoup de situations un tableau peut s'avérer très pratique.

Les pointeurs (programmation avancée)

Glenn a introduit en catimini lors des cours de la semaine 10 le pointeur. C'est généralement le sujet qui rebute le plus dans l'apprentissage de la programmation. Certains langages de programmation empêchent son utilisation (Java, C#…), d'autres la limitent (Pascal). La seule évocation du mot « pointeur » fait frémir un responsable d'assurance qualité. Seul le goto peut prétendre avoir une réputation aussi sulfureuse, mais on ne va pas s'arrêter à ces rumeurs, découvrons la bête de l'intérieur comme tout bon bidouilleur le ferait.

Par ma foi ! Il y a plus de quarante ans que je dis de la prose sans que j'en susse rien, et je vous suis le plus obligé du monde de m'avoir appris cela.

Molière, le bourgeois gentilhomme

Eh oui ! Que vous le vouliez ou non, indirectement, vous faites usage de pointeurs en écrivant ou en exécutant un programme. C'est tout simplement essentiel et indispensable en informatique.

Un pointeur est une variable qui a pour valeur une adresse vers la mémoire.

C'est une indirection en ce sens que l'on ne travaille pas directement sur une valeur mais sur l'emplacement où est stockée cette valeur. Dans le cas classique d'une variable comme :

1
int a = 1 ;

On manipule implicitement un pointeur puisque ce code signifie « range à l'adresse désignée par a la valeur 1 », simplement tout cela est masqué : on voit a comme une valeur et non pas comme étant l'emplacement où cette valeur est stockée. C'est beaucoup plus facile et naturel à manipuler sous cette forme aussi pour vraiment travailler avec un pointeur il faut le demander explicitement au compilateur.

Pour désigner une adresse, et non pas une valeur, il faut utiliser l'opérateur &, ainsi :

1
& a

ne désignera pas la valeur 1 mais l'adresse en mémoire où est stockée cette valeur de type int aussi notre pointeur p sera déclaré comme un pointeur sur un type int pour notre variable a comme ceci :

1
int * p = & a ;

L'astérisque indique qu'il y a une indirection. p, dans cet exemple, contient l'adresse de a mais à travers p (noté * p) on peut récupérer la valeur contenue dans a. Pour résumer :

1
2
3
4
5
6
7
8
// Déclarations
int a = 1 ;         // la variable
int * p = & a ;     // un pointeur sur cette variable

// Utilisations
if (p == & a)       // vrai : p contient l'adresse de a
...
if (* p == 1)       // vrai : * p revient à lire le contenu de a, c'est-à-dire 1

Cela semble un peu compliqué pour pas grand chose mais nous avons créé de l'abstraction et celle-ci, bien utilisée, est un fantastique levier en programmation comme nous allons le voir.

Quelques exemples

Nous avons vu qu'une fonction ne pouvait retourner qu'au plus une valeur, c'est parfois limitant. Par exemple, s'il est logique de savoir si une fonction a bien réussi à s'exécuter, on aime bien savoir a contrario pourquoi tel n'a pas été le cas en disposant de plus d'informations. Le seul problème c'est qu'on ne peut retourner qu'une valeur. Vraiment ?

1
2
3
4
5
6
7
int dumb_function (int * error_code)
{
  if (error_code != NULL)
    * error_code = -1 ;

  return 0 ;
}

Décomposons le fonctionnement de cette fonction qui n'a aucun intérêt pratique :

1
if (error_code != NULL)

Nous vérifions que le pointeur error_code contient une adresse mémoire a priori valide ; la constante NULL étant une adresse signifiant « pas d'adresse valide » (généralement cette adresse est égale à zéro).

1
* error_code = -1 ;

Nous affectons à travers le pointeur error_code dans la zone de mémoire de type int qu'il désigne la valeur -1

1
    return 0 ;

La valeur zéro est retournée par la fonction.

En situation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int ok ;
int error ;

ok = dumb_function (& error) ;

if (! ok)
{
  if (error == -1)
  ...
}
...

On décompose. Nous invoquons notre fonction en indiquant que le code supplémentaire doit être renvoyé à travers la variable error (notation & error) et récupérons le résultat de l'exécution de la fonction dans ok. Si la fonction retourne zéro nous nous attendons à une information supplémentaire qui sera placée dans notre variable error par la fonction, car celle-ci a eu connaissance de l'adresse où stocker cette valeur par l'intermédiaire du paramètre sous forme de pointeur error_code. Techniquement, notre fonction a donc retourné deux valeurs !

Si vous avez lu l'article Wiki sur les tableaux, vous savez qu'un tableau est une suite consécutive de valeurs, donc une adresse et une longueur pour stocker x objets de type y, par exemple une suite de caractères comme ceci :

1
char msg [5] = { 'M', 'O', 'O', 'C', 0 } ;

Rappel : le zéro (valeur) à la fin est un marqueur signalant que la chaîne de caractères est terminée.

On peut s'amuser avec un pointeur :

1
char * m = & msg [0] ;

Ça devient compliqué, aussi la règle est de toujours décomposer. On va travailler sur une suite de caractères donc le type est char, c'est un pointeur donc *, et l'adresse de début est le premier élément du tableau, donc msg [0] mais préfixé par & pour indiquer que c'est l'adresse qui nous intéresse.

À travers ce pointeur m nous pouvons accéder au tableau, caractère par caractère : * m retournera ainsi le premier caractère de "MOOC" donc 'M'. Mais comme ce pointeur est une variable nous pouvons faire aussi des opérations arithmétiques dessus comme l'incrémentation ++. Voyons ce que cela donne sur un exemple complet :

1
2
3
4
int len = 0 ;

while (* m ++)
  len ++ ;

C'est cryptique mais rien ne résiste à la décomposition. Il y a toutefois une subtilité qui est que l'opérateur ++ s'applique au pointeur et non à la valeur pointée et est postfixé (exécuté en dernier) autrement dit la séquence * m ++ revient aux instructions suivantes :

1
2
x = * m ;
m = m + 1 ;

Là aussi il faut prendre le temps de bien examiner la situation : la première étape est de lire le caractère qui est pointé à l'adresse courante du pointeur, puis ce pointeur est déplacé sur le caractère suivant, c'est-à-dire l'adresse qui est située à un caractère de distance après l'adresse de base de notre tableau - c'est le sens de l'opérateur + qui signifie « adresse courante plus un objet de la taille d'un char ».

Le tout étant dans une boucle while, en une ligne de code nous arrivons mine de rien à exprimer « tant que la chaîne de caractères n'est pas terminée… », c'est puissant l'abstraction ! Quand le marqueur de fin est rencontré la boucle s'arrête et la variable len contient alors la longueur de notre chaîne c'est-à-dire quatre (le marqueur de fin n'est pas comptabilisé).

Si l'on souhaite maintenant copier une chaîne de caractères src (source) vers un autre emplacement dst (destination) le code correspondant est d'une concision extrême :

1
2
while (* dst ++ = * src ++)
  ;

Il faut dé-com-po-ser ! Au ralenti voici la version un peu plus longue du code :

1
2
3
4
5
6
7
8
while (* src)
{
  * dst = * src ;
  src = src + 1 ;
  dst = dst + 1 ;
}

* dst = 0 ;

On récupère le caractère courant de la chaîne source ; si celui-ci à la valeur zéro la boucle s'arrête. Le caractère courant pointé par src est copié dans dst puis on passe au caractère suivant tant pour la source que pour la destination. Enfin, et contrairement à la version courte, il faut penser à ajouter le marqueur de fin à la chaîne de destination lorsque la boucle est terminée.

Beaucoup d'autres utilisations sont possibles, à commencer par les listes chaînées.

Il est bien sûr possible d'utiliser des pointeurs sur des pointeurs, des pointeurs de fonctions, etc. Inutile d'ajouter qu'il faut de l'entraînement car il est facile de s'y perdre dans ces indirections et qu'une erreur conduit généralement à un matraquage de la mémoire en règle qui peut donner des effets curieux et variés très difficilement traçables.

Le pointeur est un outil très puissant mais sa puissance est à la hauteur des dégâts qu'il peut occasionner mal utilisé. Avant d'être vraiment à l'aise avec ces objets, il faut des années de pratique et avoir commis beaucoup de bugs, donc patience !