LOCODUINO

Aide
Forum de discussion
Dépôt GIT Locoduino
Flux RSS

mardi 19 mars 2024

Visiteurs connectés : 38

Les chaînes de caractères

Du texte, toujours du texte...

.
Par : Thierry

DIFFICULTÉ :

Avoir bon caractère n’est pas donné à tout le monde. Mais avec l’Arduino, un caractère bien trempé est nécessaire !
Trêve de plaisanterie, voyons comment nos hiéroglyphes graphiques se sont frayés un chemin dans les ordinateurs entièrement numériques !

<

Un peu d’histoire...

Depuis toujours, les processeurs, qu’ils soient simples comme les Atmel, ou complexes comme ceux de nos ordinateurs reposent tous sur des octets (des bytes en Anglais). Comment à partir de ces octets qui peuvent prendre n’importe quelle valeur entre 0 et 255 a-t-on pu y stocker des caractères ?
Il faut comprendre que les processeurs n’ont que faire de ces choses-là. Les caractères sont destinés à l’échange de données ou à l’affichage, par exemple pour être sûr que le nom de client du programme 1 est lu et interprété correctement par le programme 2, ou qu’un écran affiche correctement ce qu’on lui demande. Ainsi si je demande à un écran d’afficher les caractères correspondants à la suite d’octets 79 75, il faut qu’il utilise la même méthode de conversion, sinon au lieu de voir ’OK’, on risque d’avoir n’importe quoi !

Très tôt il a fallu décider d’une norme pour convertir n’importe quel caractère en octet et inversement. Les premiers ordinateurs reposaient sur une table de conversion appelée table ASCII (pour American Standard Code for Information Interchange). Cette table utilisaient 7 bits et donc des valeurs comprises entre 0 et 127.

PNG - 49.9 kio

La première partie de la table comprise entre 0 et 31 comprend des caractères non graphiques de gestion. Par exemple une tabulation est représentée dans une chaîne par un 9. Le 10 correspond à un saut de ligne (Line Feed) tandis que le 13 est un retour chariot (Carriage Return). Si cette terminologie vous rappelle quelque chose, c’est normal. Sur une antique machine à écrire, pour passer à la ligne il fallait remonter le chariot tout à droite (Carriage return) en poussant le grand levier qui montait la feuille d’une ligne (Line Feed). Ces caractères sont encore utilisés aujourd’hui par les éditeurs de texte (Notepad, PSPad, Sublime Text, Emacs ...) pour représenter un passage à la ligne.

PNG - 321.9 kio

D’autres caractères particuliers sont présents dans cette zone, et pour beaucoup plus du tout utilisés, notamment toute la partie semi—graphique qui permettait de primitifs dessins sur des écrans purement texte :

PNG - 6.2 kio
LOCODUINO écrit uniquement avec des caractères de la table ASCII !

Le code 32 est l’espace. C’est le premier caractère affichable (façon de parler...) de la table. Viennent ensuite les caractères de ponctuation et/ou particuliers comme les opérateurs arithmétiques de base ou les parenthèses, A partir de 48 nous avons les chiffres arabes. De 58 à 64, d’autres caractères spéciaux comme le ’@’ ou de ponctuation. Viennent ensuite les 26 lettres de l’alphabet latin en majuscule de 65 à 90. Encore d’autres caractères spéciaux, puis les minuscules latines de 97 à 122 pour finir par d’autres caractères spéciaux jusqu’à 127 qui n’est pas un caractère affichable.

à é ê ??

Les plus francophiles d’entre vous auront remarqué qu’à aucun moment il n’est question d’accent, de cédille et autre tréma... C’est que d’une part le standard a été édicté par des américains, et que d’autre part il n’y avait pas assez de place ! C’est pourquoi on a créé l’ASCII étendu qui repousse le nombre de caractères à 256. Mais là encore, pas assez de place pour loger toutes les variantes locales des caractères de tous les langages de la planète, même en se limitant aux alphabets latins... Alors on a fixé des variantes de la table ASCII pour les codes au delà de 127 selon la langue .

Et les autres ?

Bien sûr, nos amis russes, grecs et/ou asiatiques se sont réveillés et ont demandé à généraliser le concept pour y inclure leurs caractères exotiques. Après quelques tergiversations sur des caractères quelque fois sur 8 bits, quelque fois sur 16 (le MBCS), le standard Unicode a été publié. Aujourd’hui largement utilisé, il regroupe sur 16 bits, soient 65536 valeurs possibles, la table ASCII classique sur les 127 premiers caractères, puis tous les caractères locaux, les accentués latins, le Kanji, le Chinois, le Grec et tous les autres... La moitié seulement de l’espace disponible est utilisé, et si demain des Aliens nous rendent visite, une place leur est réservée !
Pour information, il y a au moins une table de conversion inventée par IBM pour ses mainframes et encore utilisée aujourd’hui pour les AS/400 , c’est l’EBCDIC (pour Extended Binary Coded Decimal Interchange Code). Cette table a été créée pour l’utilisation de cartes perforées, c’est ce qui explique sa forme peu orthodoxe.

Mais l’Arduino ?

Comme dit plus haut, l’Arduino en tant que tel ne gère que des octets et ne s’occupe pas de caractères. Les besoins sont donc fixés par l’interface utilisateur. Le premier et le plus connu des besoins est la console série. Lorsque vous voulez afficher ’OK’ sur cette console, vous utilisez Serial.println("OK"); . Cette instruction va transmettre les octets 79, 75 et 0 (on verra pourquoi un zéro en plus un peu plus tard...) à l’objet Serial qui va l’envoyer à son tour à une fenêtre de votre système (Windows, Mac, Linux...). Cette fenêtre va faire la conversion octet->ASCII et afficher OK ! On voit donc que si l’élément recevant les octets le décide, il peut afficher ce qu’il veut... Comment savoir à quoi correspond chaque code ASCII pour un Arduino ? Il suffit de coder un petit programme :

void setup()
{
  Serial.begin(115200);

  Serial.println("      0  1  2  3  4  5  6  7  8  9");
  Serial.print  ("----------------------------------");

  for (byte car = 30; car < 127; car++)
  {
    if (car % 10 == 0) // Le % ou 'modulo' donne le reste de la division entière.
    {
      Serial.println(""); // passe à la ligne
      Serial.print(car, DEC);
      if (car <100)
      {
        Serial.print("  :"); // Besoin d'un peu d'espace...
      }
      else
      {
        Serial.print(" :");
      }
    }
    Serial.print(" ");
    Serial.print((char) car);
    Serial.print(" ");
  }
}

void loop()
{}

Il affiche dix lignes de dix caractères chacune. On va retrouver exactement la table ASCII simple telle que décrite plus haut. Si vous utilisez un écran LCD du commerce, vous retrouverez sans doute le même type de caractères.

PNG - 5.9 kio

Petit exercice : étendez maintenant l’affichage jusqu’à 255 pour inclure la fin de la table ASCII.

Un caractère c’est bien, une chaîne c’est mieux !

Pour une véritable chaîne de caractères, il faut créer un tableau de char :

char text[10];. Dans cette chaîne, vous pourrez mettre 9 caractères. Pourquoi neuf ? Parce que l’on a besoin de signaler la fin de cette chaîne. Cela découle d’un comportement de base des langages C et C++ : un tableau ne connait pas sa propre taille ! Même après avoir explicitement créé un tableau de dix caractères, vous pouvez écrire sans problème text[12] = 'E';. Il n’y aura pas d’erreur de compilation, pas de warning, rien qui vous permette de vous rendre compte immédiatement de votre méprise. Et le plus grave, c’est que ça va marcher ! Vous allez sans doute écraser quelque chose, peut être sans importance, peut-être pas... Mais l’emplacement mémoire text+12 contiendra bien le code ASCII de E, 69 .
Une chaîne de caractères en C, c’est toujours la liste des caractères suivie d’un zéro qui matérialise la fin réelle de la chaîne. Si vous voulez une chaîne de dix caractères maximum, il vous faut réserver la place pour onze octets : char text[11]; .

Faire du traitement de texte...

Manipuler une chaîne revient à manipuler un tableau en s’assurant que son contenu ne dépasse pas la taille maximale, et que la liste des caractères est bien terminée par un zéro.

Créons une nouvelle chaîne :

void setup()
{
  char text[10];

  text[0] = 'B';  // on peut aussi écrire avec le code ASCII text[0] = 66;
                     // mais c'est (un peu) plus clair comme ça !
  text[1] = 'o';
  text[2] = 'n';
  text[3] = 'j';
  text[4] = 'o';
  text[5] = 'u';
  text[6] = 'r';
  text[7] = 0; // on s’arrête là. Ce qui se trouve derrière le 0 n'a pas d'importance.
}

On obtiendra la même résultat avec

void setup()
{
  char text[10] = "Bonjour";
}

Le compilateur comprend grâce aux guillemets qu’il s’agit d’une chaîne et va tout seul ajouter le zéro. C’est quand même plus lisible. Cette syntaxe ne fonctionne que pour la déclaration de la variable. Notez l’utilisation de simple quote (sous la touche 4 du clavier) pour un seul caractère, alors qu’on utilise le guillemet (sous le 3 du clavier) pour une chaîne de caractère, même si elle n’en comprend qu’un seul !

On peut même faire plus simple et ne pas spécifier la taille du tableau :

void setup()
{
  char text[] = "Bonjour";
}

Le compilateur connait la taille de la chaîne à mettre dans text et va ajouter le zéro tout seul. Seule le mémoire nécessaire sera utilisée.

Trouver la vraie longueur d’une chaîne n’est pas compliqué :

int len(char inText[])
{
  int length = 0;

  while (inText[length] != 0)
    length++;

  return length;
}

Tant que l’on n’a pas atteint le zéro, on incrémente le compteur. Dès qu’il est atteint, on retourne le compteur. On voit bien que si le zéro a été oublié, on risque de tourner longtemps et de parcourir toute la mémoire !

Si on utilise cette petite fonction, ajouter deux chaînes existantes n’est pas très compliqué :

void ajoutechaine(char inDest[], char inSource[])
{
  int lenDest = len(inDest);
  int lenSource = len(inSource);

  for (int pos = 0; pos < lenSource; pos++)
  {
    inDest[lenDest+pos] = inSource[pos];
  }

  // On oublie pas la fin !
  inDest[lenDest+lenSource] = 0;
}

On ajoute à la fin de la première chaîne (inDest[inDest + pos]) le caractère de la nouvelle chaîne (inSource[pos])
On ne va pas continuer comme ça pour vous démoraliser... Les compilateurs C ont depuis longtemps pris en charge les chaînes de caractères classiques via un jeu de fonctions standardisées qui gèrent toutes le zéro de fin correctement. Le principe reste le même, c’est juste qu’il n’est pas nécessaire de réinventer la roue à chaque fois...

Pour les utiliser vous devez ajouter #include <string.h> à votre croquis. Les fonctions en question sont celles-ci :

  • size_t strlen(const char *text) renvoie la longueur (size_t est un entier...) .
  • char *strcpy(char *dest, const char *source) copie le contenu de source à la place de dest. La fonction retourne le pointeur dest. Le const signifie que l’argument n’a pas le droit d’être modifié par la fonction.
  • char *strcat(char *dest, const char *source) copie le contenu de source à la fin de dest. La fonction retourne le pointeur dest. Attention à la taille de la somme des deux ; dest doit pouvoir contenir les deux chaînes bout à bout, plus le zéro !
  • int strcmp(const char *text1, const char *text2) Comparer deux chaînes n’est pas simple. Ecrire text1 == text2 ne compare que les valeurs des pointeurs qui sont très probablement différents... Pour bien comparer, il faut faire une boucle et comparer les caractères un par un jusqu’au zéro de fin. Cette fonction le fait pour vous. La valeur de retour est soit 0 si ce sont les mêmes, soit 1 ou -1 selon que la première a un code ASCII plus élevé ou moins élevé que la seconde chaîne à la première différence.
  • char *strlwr(char *source) convertit la chaîne en minuscules (lower case). Vu la structure de la table ASCII, passer en minuscule revient à ajouter 32 au code du caractère.
  • char *strupr(char *source) convertit la chaîne en majuscules (upper case). Il faut là retirer 32...

Il en a d’autres, en particulier les mêmes que celles-ci avec un argument supplémentaire de longueur maxi pour éviter de planter si le zéro est absent. Leur nom commence souvent par strn au lieu de str. Je vous laisse le plaisir de les découvrir.

Mais tout ça encombre la mémoire !

Le problème des chaînes de caractères, c’est que ça prend de la place ! Et la place, c’est une denrée rare sur nos petits processeurs. Si vous ajoutez un nouveau texte dans votre source

char text[] = "Bonjour Locoduino";

void setup()
{
  Serial.begin(115200);

  Serial.println(text);
}
 
void loop()
{
}

Il va occuper 18 octets (le texte plus le zéro de fin) de mémoire SRAM très précieuse parce que très limitée. Je vous laisse imaginer si toute une ergonomie utilisant des dizaines de textes doit être faite sur un écran, ou si vous voulez discuter par la liaison série par des commandes textes par exemple. Pour économiser la SRAM, il y a une astuce propre à l’Arduino (cela n’existe pas en C/C++) permettant d’envoyer les déclarations constantes dans la mémoire programme généralement moins remplie :

const char text[] PROGMEM = "Bonjour Locoduino";

Le const est là pour garantir que le contenu de cette chaîne ne sera jamais modifié. Mais c’est bien le mot clé Arduino PROGMEM qui va permettre au compilateur de stocker ce texte dans la mémoire programme. Le tableau ( ;) ) serait idyllique si il n’y avait pas une petite contrepartie. Pendant l’exécution, un pointeur de données est toujours un pointeur sur de la mémoire en SRAM. Comme ce n’est pas le cas de ces pointeurs PROGMEM, il faut passer par une phase de récupération des données. Ce qui signifie que l’on ne peut pas se servir de text comme d’habitude. Il faut passer par une fonction de récupération strcpy_P :

const char text[] PROGMEM = "Bonjour Locoduino";

void setup()
{
  Serial.begin(115200);

  char buffer[50];
  strcpy_P(buffer, text);
  Serial.println(buffer);
}

void loop()
{
}

La taille de buffer doit être suffisante pour accueillir le contenu de text avec son zéro de fin.

Même lorsque vous utilisez une chaîne de caractères constante comme argument de fonction, comme un appel à Serial.print() par exemple, la mémoire nécessaire au stockage de cette chaîne par le compilateur est prise sur la SRAM, la mémoire vive, et pas sur la mémoire programme.

// Test avec 30 caractères.
Serial.println("0123456789012345678901234567890123456789");

La compilation pour un Uno donne ça :

PNG - 4.1 kio

Heureusement, il y a aussi une syntaxe directe pour stocker ces chaînes particulières dans la mémoire programme directement :

// Test avec 30 caractères.
Serial.println(F("0123456789012345678901234567890123456789"));

Ce qui donne :

PNG - 4.1 kio

On voit que la mémoire programme a grossi de trente octets, tandis que les variables globales stockées dans la SRAM (limitée à 2048 octets sur le UNO) a diminué du même volume !

Pour pouvoir bénéficier de cette fonctionnalité, la fonction doit être capable de gérer un argument de type const __FlashStringHelper *, ce qui est le cas de toutes les méthodes de la classe Serial par exemple.

Ces différents moyens de stockage (PROGMEM, F("") ) et d’accés à la mémoire programme (strcpy_P et d’autres pour les entiers, les doubles...) sont bien entendus dédiés à l’Arduino et n’existent pas pour d’autres plateformes, comme les programmes Windows, Linux ou iOs...

Gérer des chaînes de caractères dans un programme Arduino n’est pas compliqué. Il faut juste avoir conscience de la façon dont le processeur les perçoit...

17 Messages

Réagissez à « Les chaînes de caractères »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Programmation »

Le monde des objets (1)

Le monde des objets (2)

Le monde des objets (3)

Le monde des objets (4)

Les pointeurs (1)

Les pointeurs (2)

Les Timers (I)

Les Timers (II)

Les Timers (III)

Les Timers (IV)

Les Timers (V)

Bien utiliser l’IDE d’Arduino (1)

Bien utiliser l’IDE d’Arduino (2)

Piloter son Arduino avec son navigateur web et Node.js (1)

Piloter son Arduino avec son navigateur web et Node.js (2)

Piloter son Arduino avec son navigateur web et Node.js (3)

Piloter son Arduino avec son navigateur web et Node.js (4)

Comment gérer le temps dans un programme ?

La programmation, qu’est ce que c’est

Types, constantes et variables

Installation de l’IDE Arduino

Répéter des instructions : les boucles

Les interruptions (1)

Instructions conditionnelles : le if ... else

Instructions conditionnelles : le switch ... case

Comment concevoir rationnellement votre système

Comment gérer l’aléatoire ?

Calculer avec l’Arduino (1)

Calculer avec l’Arduino (2)

Les structures

Systèmes de numération

Les fonctions

Trois façons de déclarer des constantes

Transcription d’un programme simple en programmation objet

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Trucs, astuces et choses à ne pas faire !

Processing pour nos trains

Arduino : toute première fois !

Démarrer en Processing (1)

TCOs en Processing (1)

TCOs en Processing (2)

L’assembleur (1)

L’assembleur (2)

L’assembleur (3)

L’assembleur (4)

L’assembleur (5)

L’assembleur (6)

L’assembleur (7)

L’assembleur (8)

L’assembleur (9)

Les derniers articles

L’assembleur (9)


Christian

L’assembleur (8)


Christian

L’assembleur (7)


Christian

L’assembleur (6)


Christian

L’assembleur (5)


Christian

L’assembleur (4)


Christian

L’assembleur (3)


Christian

L’assembleur (2)


Christian

L’assembleur (1)


Christian

TCOs en Processing (2)


Pierre59

Les articles les plus lus

Les Timers (I)

Les interruptions (1)

Instructions conditionnelles : le if ... else

Ces tableaux qui peuvent nous simplifier le développement Arduino

Calculer avec l’Arduino (1)

Les Timers (II)

Comment gérer le temps dans un programme ?

Instructions conditionnelles : le switch ... case

Piloter son Arduino avec son navigateur web et Node.js (1)

Les Timers (III)