ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

Débuts avec Arduino : Partie 3

07 Jul 2013

Voici le troisième article consacré à Arduino, le thème est : "SD, LCD", avec, comme fil conducteur, la réalisation d'un lecteur de carte SD sur un écran LCD (accompagné de quelques boutons afin de se diriger). En tant que tel, le projet n'a pas tant d'utilité (les autres projets non plus...) mais on peut imaginer l'intégrer dans un montage plus important, et autonome (j'ai quelques idées que je développerai à la fin de cet article), néamoins, il présente des notions de programmation et d'éléctronique interessantes. Dans cet article, j'utilise:

  • Un module SD, j'ai trouvé celui ci sur pas mal de site, pour un prix abordable
  • Un écran LCD avec connexion I2C
  • Quelques resistances d'une valeur de 10K Ohms (4)
  • Des boutons (4)

Pour ce qui est de l'écran, j'ai décidé ce prendre un modèle avec une connexion i2c afin de limiter le nombre de pin's nécessaire pour faire fonctionner l'écran, le fonctionnement de ce système est largement detaillé et documenté sur internet, nous en resterons à son utilisation (cependant comprendre le phénomène est également interessant). Comme d'habitude, on va séparer le projet en plusieurs parties, dans notre cas, trois, une pour le lecteur de carte, une pour l'écran LCD et une pour les controles et les boutons. Le projet est bien plus gros que les petits tests que je faisait avant, le code final tourne plus autour des 150lignes.

Lecture de la carte

Le lecteur SD comporte six connexions, dont deux pour une entrée sur 3.3V ou 5V et pour le ground. Les autres, sont : CLK (Clock), que l'on connecte au PIN 13, DO sur le PIN 12 et DI sur le PIN 11, CS quand à lui peut être branché à n'importe quel PIN de l'arduino. Le choix des PIN's n'est pas arbitraire, en effet, pour fonctionner, le lecteur utilise une connexion SPI (Serial Peripherical Interface) et on voit un peu plus bas sur cette page, que les connexions SPI ont besoin d'être connectée sur des PIN précis en fonction de l'Arduino que l'on utilise. DO doit être connecté sur MISO, DI sur MOSI et CLK sur le pin n°13.

Dans la suite de cet article, nous utiliserons la bibliothèque SD déjà présente sur l'IDE par defaut. On remarque d'ailleurs sur cette page que la carte doit être formatée en FAT16 et, par conséquent, avoir une taille inférieure à 2Go, à priori le FAT32 fonctionne également, mais j'ai eu quelques problèmes à l'utilisation. Voici la commande pour effectuer ce formattage:

mkdosfs -F 16 /dev/sdc1

Afin de vérifier que tout fonctionne, pré-chargez l'exemple SD/CardInfo et modifiez le numéro du PIN utilisé pour CS (à priori le n°10), puis tester le sur la carte, si cette dernière est bien detectée, alors nous allons pouvoir commencer à coder notre lecteur de carte. La première étape est de construire une liste de tous les fichiers et dossiers contenus dans le repertoire, ce qui simplifiera grandement la partie pour la gestion de l'écran (afficher uniquement deux entrées à partir d'un index i du tableau). Néanmoins, il nous faut un moyen de nous repérer par rapport à la racine de la carte SD (afin d'ouvrir les sous dossiers, tout en pouvant revenir à celui précedent). Pour la liste des fichiers, il faut également pouvoir distinguer un fichier d'un dossier, on ne peut donc pas se contenter d'un

char **

Nous allons créer une structure basique nommée directory :

struct directory{
  char *name;
  boolean isDir;
};

Petit détail (qui m'a demandé pas mal d'effort pour trouver la solution), une structure (pour pouvoir être utilisées dans un retour de fonction par exemple), doit être déclarée dans un .h séparé ! (sur l'IDE arduino, il faut créer un nouvelle onglet à l'aide de la flèche à droite du nom de votre fichier). source. Ajoutons une variable globale afin de sauvegarder le contenu du répertoire ouvert:

directory *openDir = NULL;

Pour ce qui est du setup, on a :

void setup(){
    Serial.begin(9600);
    pinMode(10, OUTPUT);
    if(!SD.begin(10)){
        Serial.println("Error opening SD");
        return;
    }
}

Pour se souvenir du dossier actuel, on va simplement déclarer une nouvelle variable globale

char **path = NULL;

Ainsi que d'ajouter la ligne suivante dans le setup():

path = (char**)malloc(sizeof(path[0]));

Il faut maintenant coder une petite fonction qui nous permettra de construire le vrai chemin à partir de cette variable (concatener les différentes parties de ce dernier), en n'oubliant pas la variable globale int pathLength=0 qui nous permet de connaitre le nombre de dossier parcouru jusque là.

char *concatPath(){
    int sizePath = 1;
    int slashes = 0;
    for(int i=0; i < pathLength; i++)
        sizePath+=strlen(path[i]);
    if(pathLength>1)
        sizePath+=pathLength-1;
    char *tmp = (char*)malloc(sizePath*sizeof(char));
    strcpy(tmp,path[0]);
    for(int i=1;i<pathLength;i++){
        if(i>=1)
            strcat(tmp,"/");
        strcat(tmp, path[i]); 
    } 
    return tmp;
}

Créons maintenant une fonction qui récupère le nombre de fichiers/dossiers contenus dans un dossier, cela facilitera la création du tableau de structure, ajoutons également deux variables globales filesInDirectory :

int filesInDirectory = 0;
File dir;
void nbFile(){
    File entry;
    filesInDirectory = 0;
    dir.rewindDirectory();
    if(!dir.isDirectory()){
     Serial.println("Not a directory");
    return;
  }
  while((entry = dir.openNextFile())){
    filesInDirectory++;
    entry.close();
  }
  dir.rewindDirectory();
}

File dir représente le dossier courant, que nous parcourons afin de compter l'éléments qu'il contient, rewindDirectory permet de remettre à 'le curseur' au début du dossier, pour pouvoir le parcourir à nouveau. Nous avons maintenant toutes les fonctions auxiliaires necéssaires au stockage des informations du dossier dans lequel nous nous situons, codons maintenant la gestion de l'ouverture d'un chemin quelconque. La variable globale permettant de sauvegarder l'index du fichier/dossier actuellement selectionné:

int cSelect=0

void initDir(char *dirName){
    File entry;
    cleanOpenDir();
    dir = SD.open(dirName);
    nbFile();
    openDir = (directory *)malloc(filesInDirectory*sizeof(struct directory));
    for(int i=0;i<filesInDirectory;i++){
      entry = dir.openNextFile();
      openDir[i].name = strdup(entry.name());
      openDir[i].isDir = entry.isDirectory();
      entry.close();
    }
    cSelected = 0;
    dir.rewindDirectory();
    dir.close();
    displayDir(0);
}

On compte donc le nombre d'éléments, on nettoie la variable globale openDir() (nous verrons cette fonction par la suite), car allouée dynamiquement par strdup() ! La fonction displayDir elle, est liée à l'écran LCD, et fera donc l'objet de la seconde partie de cet article, que nous allons d'ailleurs attaquer de suite, car la gestion de la lecture de la carte SD est terminée ! Un dernier élement avant de passer à la partie suivante : fonction cleanOpenDir, afin de nettoyer proprement la variable globale openDir.

void cleanOpenDir(){
   if(openDir != NULL){
      for(int i=0;i<filesInDirectory;i++)
        free(openDir[i].name);
   }
  free(openDir);
}

Gestion de l'écran

Comme énoncé dans l'introduction, je n'utilise pas une connexion "normale" pour l'écran LCD, afin d'économiser les PINs, mais une connexion I2C, hors, ce mode de communication n'est pas géré par default avec les bibliothèques Arduino, et la plupart de celle qu'on trouve sur internet commencent à vieillir et à ne pas être supportées par les cartes récentes (ou du moins, je n'ai pas réussi à les faires fonctionner avec). Cependant, AdaFruit a réaliser une bibliothèque fonctionelle sur mon modèle d'écran, qui est disponible sur GitHub, pour ceux qui n'ont jamais installé de bibliothèques sur Arduino, voici la procédure sur le site officiel.

La connexion I2C comporte (en plus du courant et du ground), deux connexions : SDA et SCL, une fois de plus, d'après le site officiel, il faut connecter SDA à l'analogique 4 (A4) et SCL à l'analogique 5 (A5), et fournir à l'écran une tension de 5V (probablement fonctionel avec 3.3 mais étant donné que le reste du montage tourne sur du 5V autant ne pas se compliquer la vie). Modifions maintenant le setup afin d'initier l'écran LCD :

lcd.init();
lcd.backlight();
lcd.blink();

Pour ce qui est de l'affichage, nous allons afficher le contenu, deux éléments à chaque fois (un seul pour le dernier si le nombre total est un nombre impaire), voici donc la fonction displayDir.

void displayDir(int index){
  lcd.clear();
  if(filesInDirectory != 0){
    for(int i=0;i<min(2,filesInDirectory-index);i++){
      lcd.setCursor(0,i);
      lcd.print(openDir[index+i].name);
      if(openDir[index+i].isDir)
        lcd.print("/");
    }
  }else{
    lcd.print("No file found"); 
  }
  lcd.setCursor(0,0);
}

Cette fonction prend un argument : l'index du fichier à afficher, et determine par la suite s'il faut afficher 1 ou 2 documents, puis replace le curseur à sa position originale (en haut à gauche). Afin de distinguer les dossiers des fichiers, on ajoute un "/" à la fin de ces premiers. Vous n'allez peut être pas me croire, mais la partie sur la gestion de l'écran LCD est terminée, l'utilisation des bilbiothèque a rendu le processus bien plus aisé ! Nous attaquons maintenant la partie qui demande le plus de branchement (d'un point de vue éléctronique) et qui constitue la majeur partie du loop() (vous aviez remarqué qu'il était manquant n'est ce pas !) : les boutons !

Controles

Pour cette partie, il nous faut brancher quatres boutons, un pour monter, un pour descendre, un pour valider (changer de dossier) et un pour revenir en arrière, chacun a besoin d'une résistance, afin de faire un montage de résistance en pull down pour eviter le court circuit et les interferences sur le montage, tout ceci est detaillé sur cet article, comme ca, quand le bouton est pressé, sa valeur est égale à HIGH, pour nos quatres boutons, le montage ressemble donc à quelque chose tel que :

buttons_control
Nous utilisons donc les PINs 4 à 7 dans cet exemple, mais de toute facon, nous les définirons dans une variable globale représentant, dans l'ordre : bas, validation, haut, retour

const int buttons[] = {5,6,7,2};

Ainsi qu'une second variable pour leur état respéctif:

int buttonsStates[  = {0,0,0,0};

Attaquons nous maintenant à la gestion de ces boutons, à l'intérieur du loop().

void loop(){
  for(int i = 0;i < 4; i++){
   int cState = digitalRead(buttons[i]);
   if(cState != buttonsStates[i]){
       buttonsStates[i] = cState;
       if(cState == HIGH){
          switch(i){

          }
       }
   } 
  }
}

Voici pour la base de la lecture sur chaque bouton, on compare donc l'état actuel avec le précedent, pour chaque bouton, et le switch va nous servir à determiner quel est le bouton qui est préssé et donc, quel action doit être demarrée, nous aurions très bien pu utiliser une succession de test de i, mais je dois avouer apprécier le switch :). Dans notre tableau, comme indiqué precedement, le premier index est le bouton du bas, le second, la validation, le troisème le bouton du haut, et le quatrième le retour en arrière. Voici le code pour le bouton du bas :

case 0:
  if(cSelected < filesInDirectory-1){
    if(cSelected == 1 || cSelected%2){
      cSelected++;
      displayDir(cSelected);
    }else{
      cSelected++;
      lcd.setCursor(0,1);
    }        
  }
break;

Donc concrètement, on regade s'il reste des fichiers/dossiers à afficher, et la position actuelle du curseur (impaire en bas, pair en haut, étant donné que nous n'avons que deux lignes), et si nous sommes en bas, il faut charger les nouveaux fichiers à afficher, et mettre à jour l'affichage, dans le cas ou le cuseur est en haut, nous avons juste à changer sa position ainsi que l'index de l'élément selectionné. Pour ce qui est du bouton de validation, il doit nous servir à changer de dossier (et eventuellement à charger un fichier, dans une application plus complète), le code ressemble donc a quelque chose comme:

case 1:
  if(openDir[cSelected].isDir){
    path = (char**)realloc(path, (pathLength+1)*sizeof(char *));
    path[pathLength] = strdup(openDir[cSelected].name);
    pathLength++;
    initDir(concatPath());
  }
break;

On verifie donc que la selection est un dossier, et si c'est le cas, on ajouter un élément à notre path (vous l'aviez oublié n'est ce pas ?) et puis on initialise ce nouveau dossier, afin de charger son contenu l'afficher à l'écran. Le bouton haut fait également l'inverse du premier, si ce n'est qu'il faut s'assurer que l'index de le selection ne soit pas inférieur à 0.

case 2:
  if(cSelected > 0){
    if(cSelected%2){
      lcd.setCursor(0,0);
    }else{
      displayDir(cSelected-2);
      lcd.setCursor(0,1);
    }
    cSelected--;
  }
break;

Enfin, pour le bouton de retour, il faut vérifier que nous ne sommes pas déjà à la racine de la carte, et puis mettre à jour la variable path.

case 3:
  if(pathLength){
    pathLength--;
    free(path[pathLength]);
    if(pathLength == 0)
      initDir("/");
    else
      initDir(concatPath());
  }
break;

Et voilà, en mettant le tout ensemble (disponible ici, vous devriez pouvoir explorer le contenu de votre carte SD sur l'écran LCD, et naviguer dedans à l'aide des boutons de controle ! Génial n'est ce pas ? Seul, ce projet n'est pas très utile, mais on pourrait imaginer un embarqué indépendant, ou alors (sur un robot), un moyen de charger des parcours pre-définis dans des fichiers, ou alors des configurations à adopter. On peut également imaginer créer des fichiers ayant une certaines structure, que l'on pourrait charger directement et qui joueraient un morceau directement sur un haut parleur branché à l'Arduino, enfin je pense que les applications sont multiples, et puis permettent de manipuler une grande diversité de bibliothèque sous Arduino, ce qui est toujours interessant (et amusant). J'espère que cet article n'est pas trop long (je n'aime pas trop écrire des articles aussi long, mais ce projet me tenait à coeur), et, qu'une fois de plus, j'ai réussi à éveiller votre curiosité.

Bonne vacances à tous !