Types, constantes et variables

. Par : Jean-Luc. URL : https://www.locoduino.org/spip.php?article11

Il est de notoriété publique que les ordinateurs, et l’Arduino ne déroge pas à la règle, effectuent leurs calculs en binaire. Les nombres sont des nombres où chaque chiffre ne peut prendre que 2 valeurs, 0 ou 1. Du côté de l’électronique, cela correspond à des tensions. Sur votre Arduino alimenté en 5V, le chiffre binaire 0 correspond à une tension de 0V et le chiffre binaire 1 à une tension de 5V. La mémoire est le dispositif permettant de stocker ces deux tensions et donc le chiffre binaire correspondant.

Un chiffre binaire est communément appelé un bit, contraction de l’anglais Binary digIT. Comme en base 10, on va utiliser plusieurs chiffres pour représenter des grands nombres. Les bits ne sont pas utilisés individuellement, ils sont rassemblés par paquet : 8 bits (1 octet), 16 bits (2 octets) ou 32 bits (4 octets), voire 64 bits (8 octets).

L’Arduino Uno intègre un micro-contrôleur 8 bits. C’est à dire que l’octet est sa taille de donnée préférée. Il existe également des Arduino intégrant des micro-contrôleurs 32 bits. Pour information, nos ordinateurs modernes manipulent, eux, des mots de 64 bits.

La grande différence entre notre mode de calcul et celui des ordinateurs est que l’ordinateur calcule sur des nombres ayant un nombre de bits fixés alors que nous ajoutons les chiffres que le calcul rend nécessaires. Imaginez qu’en effectuant une addition vous soyez limitez à deux chiffres. Si le résultat dépasse 99, vous ne pouvez pas noter les centaines. C’est exactement ainsi que procède un ordinateur, quand le résultat a plus de chiffres que ceux disponibles, ceux qui débordent sont oubliés et cela peut bien entendu conduire à un résultat erroné. Quand le programme manipule des nombres, il faut faire attention aux valeurs maximum et minimum nécessaires.

Dans l’article précédent, « La programmation, qu’est ce que c’est » les nombres que nous avons manipulés, 13 pour le numéro de broche et HIGH et LOW (oui ce sont aussi des nombres), sont des constantes. C’est à dire des quantités qui sont fixes et définies une bonne fois pour toutes. 13 est ce que l’on appelle une constante littérale, HIGH et LOW sont des constantes symboliques, c’est à dire que le symbole LOW est un autre nom pour la constante 0 et le symbole HIGH un autre nom pour la constante 1.

Qu’est-ce qu’un type

Un type correspond à une taille, en nombre de d’octets, nécessaires pour définir une constante ou bien une variable. Il définit donc les valeurs minimum et maximum qu’il est possible de coder. Il correspond également à la façon dont on effectue un calcul. Les types peuvent être signés, c’est à dire qu’ils peuvent coder des valeurs positives, nulles et négatives ou bien non signés, c’est à dire qu’il ne peuvent coder que des valeurs positives ou nulles.

Le type int

int est un type signé ayant une taille de 2 octets [1]. int est l’abréviation de integer qui signifie nombre entier. Les valeurs possibles vont de -32768 à 32767. On peut par exemple définir une constante de type int et lui donner la valeur 13. Pour ensuite l’utiliser à la place de la valeur littérale 13 dans pinMode. Comme ceci :

const int pinLED = 13;
 
void setup() {
  pinMode(pinLED, OUTPUT);
}

const signifie qu’il s’agit d’une constante, int est son type, pinLED est son nom et 13 est sa valeur. Notez le ; à la fin pour terminer la définition.

Quel intérêt de faire cela ? Eh bien tout d’abord, en relisant le programme, on sait à quoi sert la broche 13. Ensuite, si on considère le reste du programme :

void loop() {
  digitalWrite(pinLED, HIGH);
  delay(500);
  digitalWrite(pinLED, LOW);
  delay(500);
}

Et que par la suite on décide de brancher une LED sur une autre broche que la 13, par exemple la 9, il suffira de changer la définition de la constante comme ceci :

const int pinLED = 9;

pour que le programme soit modifié pour faire clignoter la LED sur la broche 9 au lieu de la 13. Alors qu’en utilisant la constante littérale 13, il faudrait chercher dans le programme tous les endroits concernés pour modifier le 13 en 9. Ça serait pénible et un oubli provoquerait un dysfonctionnement qui serait difficile à repérer si le programme est gros.

Alors que les numéros de broche de l’Arduino Uno vont de 0 à 13, nous employons un int pour en désigner une. C’est un peu surdimensionné et il n’est pas non plus nécessaire d’avoir un nombre négatif.

Lorsqu’un calcul produit un débordement vers le haut sur un nombre signé, c’est à dire un résultat plus grand que le plus grand des nombres positifs que l’on puisse coder, le résultat est négatif ! De même si le débordement est vers le bas, c’est à dire un résultat plus petit que le plus petit négatif que l’on puisse coder, le résultat est positif. En effet, les deux extrêmes se rejoignent et, par exemple, ajouter 1 à 32767 avec un int donne -32768.

Le type byte

Le type adéquat pour désigner un numéro de broche est byte il s’agit d’un nombre entier non signé qui a une taille d’un octet. Les valeurs possibles vont de 0 à 255. La définition de notre constante pinLED devient :

const byte pinLED = 13;

Le reste du programme est inchangé.

Et les variables dans tout ça ?

Nous avons vu 2 types, int et byte, ainsi que la manière dont on définit une constante. La définition d’une variable suit la même règle, il suffit de retirer le mot-clé const.

À la différence d’une constante, une variable peut être modifiée lors de l’exécution du programme, elle est même faite pour ça.

Définissons par exemple la variable rythme de type byte. On en profite pour l’initialiser à 0, c’est à dire qu’au démarrage du programme, la variable recevra une valeur de 0 :

byte rythme = 0;

et utilisons cette variable à la place de la durée de 500ms dans delay() puis à la fin de loop, ajoutons lui 8. Voici le programme complet.

const byte pinLED = 13;
byte rythme = 0;
 
void setup() {
  pinMode(pinLED, OUTPUT);
}
 
void loop() {
  digitalWrite(pinLED, HIGH);
  delay(rythme);
  digitalWrite(pinLED, LOW);
  delay(rythme);
  rythme = rythme + 8;
}

l’instruction rythme = rythme + 8 a pour effet de prendre la valeur rangée dans la variable rythme, lui ajouter 8 puis remettre le résultat dans la variable rythme. L’effet est qu’à chaque tour de boucle le contenu de la variable rythme est augmenté de 8 et que la durée d’allumage et d’extinction de la LED augmente. Par conséquent le rythme du clignotement diminue.

Comme la valeur maximum que rythme peu stocker est 255, au bout de 32 exécutions [2] de loop(), rythme va déborder et la valeur qui va y être stockée sera 0, ce qui aura pour effet de recommencer le cycle. J’ai dit au début de l’article que le débordement peut provoquer un résultat erroné si on n’y prend pas garde mais on peut aussi l’utiliser à son bénéfice. Tant que l’on comprend ce que l’on fait, tout va bien.

Quelques autres types

Voyons maintenant quelques autres types.

Le type word

word est un nombre entier non signé sur 2 octets. Les valeurs possibles vont de 0 à 65535.

Le type long

Il s’agit d’un entier signé sur 4 octets. Les valeurs possibles vont de -2 147 483 648 à 2 147 483 647.

Le type unsigned long

Le type unsigned long est un entier non signé sur 4 octets. Les valeurs possibles vont de 0 à 4 294 967 295.

Que se passe-t-il quand un calcul utilise des variables de différents types ?

Tout d’abord, nous n’avons parlé que de nombre entiers. Un autre article sera consacré aux nombres à virgule, encore appelés nombres flottants.

Mais concernant les entiers, il y a un certain nombre de pièges dont il faut être conscient.

Que se passe-t-il si un calcul est effectué sur des variables signées et que la variable où le résultat est stocké est non signée

exemple :

int a = -5; // a est signé sur 2 octets
int b = 2;  // b est signé sur 2 octets
word c;    // c est non signé sur 2 octets
 
void setup() {
  c = a + b; // le résultat est négatif et est stocké dans une variable non signée
}

c contiendra 65533. Hein ?

Oui ! Rappelez vous le débordement. Si on déborde une variable non signée sur 2 octets vers le bas, par exemple en enlevant 1 à 0, on obtiendra 65535. 65533 est donc un débordement vers le bas de -3. Or le résultat du calcul est -3. On retrouve donc dans c la même valeur que si on avait retiré 3 à 0. 65533 et -3 ont le même code sur 2 octets.

Que se passe-t-il si le résultat d’un calcul nécessite une certaine taille de variable et que l’on stocke le résultat dans une variable trop petite pour le contenir ?

exemple :

word a = 133;  // a est non signé sur 2 octets
word b = 123;  // b est non signé sur 2 octets
byte c;            // c est non signé sur 1 octets
 
void setup() {
  c = a + b; // le résultat est 256 et est stocké dans une variable non signée d'un octet
}

c contiendra 0. Quoi ?

Oui ! 133 + 123 donne 256. Un byte ne peut contenir que des nombres compris entre 0 et 255. On a donc un débordement. Si on ajoute 1 à 255, on déborde par le haut et on repart à 0.

Conclusion

Attention aux types. Dans les deux exemples ci-dessus seuls les types ont changé, le programme est resté le même. Il faut donc :

  • être conscient de la manière dont les calculs sont effectués par l’Arduino ;
  • ne pas utiliser les types au hasard ;
  • savoir quelles sont les valeurs minimum et maximum dont on a besoin pour chaque variable pour choisir le bon type ;
  • faire attention aux conversions d’un type dans un autre ;

Il existe quelques autres types que nous verrons au fur et à mesure.

La prochaine fois, nous verrons les instructions conditionnelles.

La suite de la série d’articles avec « Instructions conditionnelles : le if ... else ». Si ce n’est pas déjà fait lisez également « La programmation, qu’est ce que c’est ».

[1Ceci est vrai sur les Arduino 8 bits comme le Un, le Nano ou le Mega. Sur des Arduino 32 bits comme le Due, un int occupe 4 octets. Il faut donc se méfier.

[232 = 256 divisé par 8