Le type int
Revenons à notre jeu de cache-cache, et faisons compter Arduino à notre place.
Une variable
Il va nous manquer une notion importante, la mémoire. Quand Arduino se met à compter, il faut qu'il se rappelle quel était le dernier nombre pour pouvoir calculer et afficher le suivant (vous faites pareil, vos nombres sont rangés dans des neurones). Pour cela, on va utiliser des variables. Une seule aujourd'hui!
Une variable est un espace réservé dans la mémoire du processeur. En réalité les cases mémoires ont des numéros (on dit adresse), mais ce n'est pas très pratique. Ainsi le C va nous permettre de mettre un nom. Ce nom, c'est nous qui le choisissons, mais il doit débuter par une lettre et ne contenir que des lettres (non accentuées) et des chiffres et le symbole _. Par convention, on fera comme pour les fonctions, on ne mettra que des minuscules et si il y a plusieurs mots, on mettra une majuscule en première lettre des mots à partir du deuxième. A la compilation le nom sera remplacé par l'adresse (le numéro), si bien que l'on peut donner des noms courts ou longs cela n'a pas d'influence sur le temps d'exécution ou sur la taille du code.
Par choix des concepteurs du C, il faut déclarer les variables avant de les utiliser et préciser le type. Aujourd'hui, nous allons utiliser un des types entier. Il existe d'autres types (booléens, tableau, objet...). Si pour l'homme un nombre peut avoir autant de chiffres que nécessaire, en informatique, il faut que le nombre rentre dans la (ou les) cases qui ont été réservées. Un entier a donc une taille qui imposera alors les valeurs minimale et maximale représentables.
int et les autres
int vient de integer entier. Ce n'est pas celui que j'utilise le plus, mais c'est celui que l'on voit partout (ce n'est pas toujours le bon choix!).
Il existe en fait plusieurs entiers. Nous allons les passer en revue. Tout d'abord, il y a deux catégories, les entiers signés et les entiers non signés. Si on compte le nombre de moutons qui sautent une barrière, la valeur ne sera jamais négative. Il est de bon ton de choisir un entier non signé pour les compter. Mais cela fonctionnerai pareil si on utilise des entiers signés. On peut compter deux fois moins de moutons c'est tout. Mais si on s'endort au 100ème mouton qui saute la barrière, cela n'a aucune importance, signé ou pas, on peut forcément compter jusqu'à 100 quel que soit l'entier choisi.
La deuxième différences entre les entiers c'est leur taille mémoire. Avec Arduino les cases mémoires sont de 8 bits. Pour ranger un entier on peut utiliser une seule case mémoire. Mais cela ne fait que 256 valeurs différentes, ce n'est pas toujours assez. Avec un écran graphique 320x240 points, il nous faut dans un sens avoir au moins 320 possibilités. On va donc pouvoir choisir des entiers plus grands sur 2 octets (16 bits). On a a lors 65536 possibilités. Si ce n'est pas encore suffisant (par exemple comptage des millisecondes depuis la mise sous tension) on va devoir utiliser 4 octets (32 bits). Les entiers de 3 octets (24 bits) ne sont pas gérés. Il existe aussi des entiers 64 bits, mais ce n'est pas très utilisé. Si le processeur est de 8 bits, les opérations 8 bits se font en une fois. Les opérations en 16, 32 ou 64 bits vont se faire en plusieurs fois. Plus la taille est importante, plus le calcul sera complexe (plus le code sera grand) et plus le calcul sera long (ralentit l'exécution). Je conseille donc de prendre la plus petite taille qui permet le fonctionnement.
Dans les noms, il y a pleins de synonymes, cela vient en partie de la définition du C. Par exemple le type int désigne en C un entier, mais en fonction du système qui l'implante, il peut avoir une taille différente. Avec Uno/Nano/Mega, les types entiers sont:
noms | signe | taille | min | max |
---|---|---|---|---|
int8_t char | signé | 8 bits | -128 | +127 |
uint8_t unsigned char byte | non signé | 8 bits | 0 | 255 |
int16_t int | signé | 16 bits | -32768 | +32767 |
uint16_t unsined int word | non signé | 16 bits | 0 | 65535 |
int32_t long | signé | 32 bits | -2 millions | +2 millions |
uint32_t unsigned long | non signé | 32 bits | 0 | 4 millions |
int64_t | signé | 64 bits | -9.1018 | +9.1018 |
uint64_t | non signé | 64 bits | 0 | 18.1018 |
?int??_t sont les entiers de base du compilateur. Ils ont l'avantage d'indiquer clairement si ils sont signés (par défaut) ou
non signés (lettre u comme unsigned) ainsi que la taille en bits.
byte, word, et long sont définis à partir des précédents. byte veut dire octet, c'est donc 8 bits.
char est un peu particulier. Pour les calculs, c'est un nombre (signé pour Arduino), mais il est censé contenir une
lettre (char = character = caractère). Si on l'affiche avec Serial.print() c'est une lettre qui est affichée.
Déclaration d'une variable
Je décide de compter le cache-cache. Il faudrait donc que je trouve un nom explicite pour une variable, idéalement cacheCache (à la rigueur seulement cache). Avant de pouvoir l'utiliser, il faut que j'informe le compilateur que c'est une variable et le type de celle-ci. Cela se fait en écrivant
int cacheCache;
Il serait judicieux de choisir un entier non signé, genre word, mais on va prendre le générique int.
Cette phrase indique au compilateur de garder deux cases mémoire (int) et de donner à cacheCache l'adresse de ces cases. En plus, avec Arduino, la variable est initialisée à 0 (tous les C ne le font pas forcément).
Quand on donne sa définition, on peut aussi l'initialiser a une valeur connue. Il y a deux écritures
int cacheCache = 12;
et
int cacheCache(12);
La première définition est plus lisible, je la conseille au début. La deuxième est imposée à cause du C++
Utilisation
Une fois que l'on a déclaré la variable, on peut:
- lui affecter une valeur:
cacheCache = 15;
Notez que si cacheCache contient 13, cela peut sembler bizarre car on est en train d'écrire 13 = 15. En fait, ne prenez pas = comme un test d'égalité, c'est une affectation, il faudrait lire "la variable cacheCache reçoit la valeur 15".
- lui donner le résultat d'un calcul:
cacheCache = 15 + 3;
- l'utiliser dans un calcul
cacheCache = 2 + cacheCache * 3;
- passer une variable comme paramètre pour une fonction:
delay(cacheCache);
- l'envoyer vers la console:
Serial.print(cacheCache);
Comme son nom l'indique, une variable peut voir sa valeur varier au cours du programme.
Et faisons compter notre Arduino
Nous avons toutes les briques qu'il nous faut. Reprenons la base des exercices précédents. Serial.begin sera mis comme d'hab
dans le setup. Pour qu'il compte il va falloir dans l'ordre (si on change l'ordre, cela fera peu de changements):
- calculer la nouvelle valeur à partir de l'ancienne: si l'ancienne valeur était cacheCache, la nouvelle valeur sera
cacheCache + 1 On écrira donc
cacheCache = cacheCache + 1;
Ne simplifiez pas par cacheCache, ce n'est pas une égalité. Cela se lit cacheCache prend la valeur cacheCache plus un. On verra
d'autres façons d'ajouter 1 plus tard.
- afficher cette nouvelle valeur: on en a parlé
Serial.println(cacheCache);
- attendre une seconde (le delay)
Si on lance ainsi le programme, l'ide répondra par un cacheCache was not declared in this scope (je vous engage à faire l'essai, plus vous aurez l'habitude de voir des erreurs connues, plus ce sera facile quand une vraie erreur arrivera). Cette erreur est normale, on ne lui a pas dit qui était cacheCache. Il faut le déclarer. Il faut donc rajouter
int cacheCache;
quelque part. Mettez pour l'instant cette déclaration en tout début de programme sur la première ligne, avant le setup. On discutera de cette place dans la page prochaine.
Essayez votre programme. Solution? int cacheCache; void setup() { Serial.begin(115200); } void loop() { cacheCache = cacheCache + 1; Serial.println(cacheCache); delay(1000); } On doit voir dans la console la succession les nombres défiler. Le premier nombre affiché doit être un "1" car en déclarant la variable, le C l'initialise à zéro, et avant d'afficher nous avons ajouté 1. C'est donc le 1 que l'on voit d'abord.
Pendant qu'on voit les nombres défiler, appuyez sur le bouton de la carte Arduino. Cela va réinitialiser le programme qui va repartir du début. Ce sera utile ici pour recommencer si on a raté le démarrage.
Fermez la fenêtre de la console et rouvrez-la. Le comptage redémarre aussi. C'est une deuxième façon de réinitialiser la carte.
Les limites
Si vous aviez vraiment la patience, et si vous m'avez cru, au bout de 32767 secondes (un peu plus de 9 heures), la valeur affichée doit atteindre 32767 (0b01111111). A la seconde d'après le nombre binaire va s'incrémenter de 1 et on obtient 0b10000000, soit -32768. Puis on devrait voir ensuite -32767, -32766... Je suppose que vous n'aurez pas la patience d'attendre, et pour le voir, il suffit de compter plus vite, par exemple en retirant la ligne qui contient delay(). Au bout d'une vingtaine se secondes, on doit voir apparaître les nombres négatifs. Remontez (en utilisant l'ascenseur) tant qu'il y a des nombres négatifs, pour voir la limite. Mettez vous à la limite nombres positifs/négatifs, et vous devriez voir les deux nombres 32767 et -32768 qui se suivent.
Bien sûr je vous engage à tester aussi les autres types d'entiers. C'est plus facile avec int8_t et uint8_t, au besoin remettez un délai de 500ms ou moins. Essayez le type char, on verra une succession de caractères. Certains ne sont pas imprimables et donnent soit un carré soit un point d'interrogation. Pour les long et unsigned long, si vous voulez voir les limites, on pourrait de nouveau s'armer de patience (de l'ordre de 25mn et 1h si on a supprimé le délai). Pour éviter cette attente, au lieu de démarrer à 1 (en laissant la variable s'initialiser à 0 par défaut), initialisez la à 2 097 145 pour les long ou 4 194 290 pour les unsigned long. Laissez le délai d'une seconde, on est presque à la limite.