ex0ns

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

Détection d'un débugger (int 0x2D)

07 Apr 2014

INT 02Dh, vous avez dit debugger ?

Je me suis plongé un peu dans les différentes techniques de detection des debuggers, et souhaite faire partager le premier : l'interruption 2d. Cette interruption provoque la levée d'une exception, si le debugger est présent il va capturer cette exception et le programme ne sera jamais capable de le récupérer, on imagine déjà que l'on pourrait effectuer des opérations qui doivent être executées uniquement si le debugger n'est pas présent dans cette exception, voyons voir comment ceci fonctionne. Tout d'abord, pour le reste de cet article, nous utiliserons MASM et IMM Debugger, ainsi qu'un code assez simple (que je detaillerai à la fin de cet article car ce n'est pas le sujet de cet article) qui fait simplement apparaitre une fenêtre basique avec uniquement un titre. Afin d'avoir un retour visuel de nos différentes expériences, le but sera de modifier le titre de cette fenêtre. Le code de base est le suivant:

.386
.model flat, stdcall
option casemap:none

include windows.inc

include user32.inc
includelib user32.lib

include kernel32.inc
includelib kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName   BYTE    "SimpleWinClass", 0
NoDebugger  BYTE    "A great title",0
Debugger    BYTE    "Please close the debugger",0
Secret      BYTE    "This is a secret title",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
AppName LPCTSTR ?

.code
start:
 invoke GetModuleHandle,NULL
 mov hInstance, eax

 invoke GetCommandLine
 mov CommandLine, eax

 invoke WinMain, hInstance, NULL, CommandLine, SW_SHOWDEFAULT
 invoke ExitProcess,eax

 WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
 LOCAL wc:WNDCLASSEX
 LOCAL msg:MSG
 LOCAL hwnd:HWND

 mov wc.cbSize, SIZEOF WNDCLASSEX
 mov wc.style, CS_HREDRAW OR CS_VREDRAW
 mov wc.lpfnWndProc, OFFSET WndProc
 mov wc.cbClsExtra, NULL
 mov wc.cbWndExtra, NULL
 push hInstance
 pop wc.hInstance
 mov wc.hbrBackground, COLOR_WINDOW+1
 mov wc.lpszMenuName, NULL
 mov wc.lpszClassName, OFFSET ClassName
 invoke LoadIcon,NULL,IDI_APPLICATION
 mov wc.hIcon, eax
 mov wc.hIconSm, eax
 invoke LoadCursor,NULL,IDC_ARROW
 mov wc.hCursor, eax
 invoke RegisterClassEx, addr wc
 mov AppName, offset NoDebugger

 ; Partie importante, génère une interruption
 xor eax, eax
 int 02dh
 inc eax


 startWindow:
 invoke CreateWindowEx,NULL,\
                ADDR ClassName,\
                AppName,\
                WS_OVERLAPPEDWINDOW,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                NULL,\
                NULL,\
                hInst,\ 
                NULL
 mov hwnd, eax
 invoke ShowWindow,hwnd,CmdShow
 invoke UpdateWindow, hwnd
 .WHILE TRUE
    invoke GetMessage, ADDR msg, NULL, 0, 0
    .BREAK .IF (!eax)
    invoke TranslateMessage, ADDR msg
    invoke DispatchMessage, ADDR msg
 .ENDW
 mov eax, msg.wParam
 invoke ExitProcess,eax
 ret
 WinMain ENDP

 WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .IF uMsg==WM_DESTROY
        invoke PostQuitMessage, NULL
    .ELSE
        invoke DefWindowProc, hWnd, uMsg, wParam, lParam 
        ret
    .ENDIF
    xor eax, eax
    ret 

 WndProc endp
end start

Ce code est largement inspiré du magnifique document de NoteWorthy. Essayer de le compiler, une fenêtre blanche avec le titre "A great title".

Afin de connaitre le fonctionnement des exceptions sur Windows, je vous conseil fortement de lire (ou au moins d'y jetter un coup d'oeil) : Microsoft Exception et Exceptions Handling histoire de savoir un peut comment fonctionne la gestions des exceptions sur Windows. Pour les parresseux, un petit récapitulatif: les exceptions sont envoyés à un gestionnaire d'exceptions, qui contient une liste de structure pouvant gérer ces exceptions, si la structure renvoit le code ExceptionContinueExecution (définit dans EXCPT.H) le système considère que l'exception a été gérée et elle n'est pas passée à la structure suivante (et l'execution du programme reprend). La structure qui doit gérer l'exception possède une méthode de callback qui ressemble à :

__cdecl _except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext
);

Nous devons maintenannt définir cette fonction qui sera se chargée de gérer l'exception levée par l'interruption, pour cela voici le code:

PTExceptionHandler proc ExceptionRecord:DWORD, EstablisherFrame:DWORD, ContextRecord:DWORD, DispatcherContext:DWORD
    mov eax, 0
    ret
PTExceptionHandler endp

Avec MASM, le mot clé "proc" décrit une procédure (fonction) suivit de ses arguments, dans notre cas, ceux requis par une fonction de gestion d'exception, la signification de ces arguments n'est pas essentiel mais elle est tout de même detaillée dans les liens précedents.

L'instruction mov eax, 0 permet de renvoyer le signal décrit précedemment : ExceptionContinueExecution afin de dire au système que l'exception a été correctement gérée (elle ne sera donc pas envoyés aux autres structures). Il ne manque plus qu'une étape afin que notre exception soit utilisée par le système, il nous faut l'ajouter à la liste des interruptions, de préférence au début, pour cela, il existe deux directives (code à placer en premier, juste après l'étiquette "start") :

assume FS:NOTHING
push offset PTExceptionHandler
push FS:[0]
mov FS:[0], esp

La première ligne charge le gestionnaire d'exception, la seconde ajoute sur la pile l'addresse de notre fonction de callback chargée de gérer les interruptions la troisième sauvegarde l'addresse de la fonction de callback par défaut (afin de la restaurer à la fin du programme), et la dernière ligne charge notre fonction (ajoutée au sommet de la pile, et donc pointée par ESP) en tant que fonction de gestion d'exception par défaut. Ce que nous voulons faire maintenant est afficher un titre différent en fonction de la présence ou non d'un debugger, si un debugger est présent, nous laisserons le titre par défaut "A great title" dans le cas contraire, nous afficherons le titre "This is a secret title".

Par défaut le titre est "A great title" et nous savons également que si un débugger est présent l'exception ne sera pas exécutée, il nous suffit donc de changer le titre dans ce code de gestion de l'exception, pour le changer en "This is a secret title", ce code se résume en une ligne à placer dans PTExceptionHandler en première ligne :

mov AppName, offset Secret

Ce code résulte en un changement de l'addresse pointée par AppName en faveur de celle contenant le titre spécial. Vous pouvez maintenant essayer, si le code est éxecuté depuis un debugger (IMM par exemple), le titre de la fenêtre est : "A great title", cependant s'il est éxecuté sans debugger, le titre de la fenêtre est changé en "This is a secret title", cela fonctionne bien et nous savons maintenant comment detecter la présence d'un debugger.

Méthode alternative

L'avantage de cette instruction (int 2d) c'est qu'elle nous permet de detecter d'une autre façon un debugger, en effet, lorsque qu'une exception est gérée par le système, le registre EIP (qui pointe la prochaine instruction qui doit être executée) est decrementé de un (et pointe de nouveau vers l'exception que l'on considère comme corrigée), dans le cas d'une interruption int 2d, le registre eip est incrementé de un, ce qui fait que EIP pointe vers l'instruction située juste apprès l'appel à EIP (execution normale du programme). Cependant, si l'exception est gérée par le debugger, EIP n'est pas decrementé de un, mais uniquement incrementé, ce qui engendre le "saut" d'un byte par le registre EIP, ainsi si un debugger est présent l'instruction située après int 2d sera soit tronqué (si elle tient sur plus d'un byte), soit sautée (si elle tient sur un byte). Comment utiliser ceci pour detecter la présence d'un debugger ? Pour cela il nous faut placer une instruction (sur un byte) à la suite de notre interruption afin de différencier les deux cas, si vous regarder le premier code, on remarque la présence de l'instruction juste après notre interruption:

inc eax ; (opcode : 0x40)

Ainsi si le programme est éxécuté normalement, le registre EAX sera incrementé, et vaudra 1, dans le cas contraire, sa valeur ne sera pas changée et restera 0. Voici le code que nous allons utiliser par la suite, il est à mettre juste après l'incrémentation du registre :

.IF eax == 1 ; Pas de débugger
    mov AppName, offset NoDebugger
.ELSE ; Présence d'un débugger
    mov AppName, offset Debugger
.ENDIF

Notez que les macros de MASM simplifient nettement le code, mais nous aurions très bien pu utiliser des instructions de jump avec des étiquettes pour effectuer le même test. Les instructions conditionnelles précédente vont changer le titre de la fenêtre en fonction de la valeur du registre EAX (et donc de l'éxécution ou non de l'instruction inc eax).

A l'execution de ce code, la fenêtre affichera "Please close the debugger" si le debugger est présent, "A great title" dans le cas contraire. Cette méthode permet donc elle aussi de modifier l'execution du programme en fonction des conditions de lancement. Cependant, les techniques précédentes peuvent facilement être contrées facilement par la plupart des débugger. En effet en lui précisant qu'il faut passer cette exception au programme et ne pas la capturer. Par exemple, pour IMM, il suffit lors de l'execution de l'interruption, d'appuyer sur "MAJ+F9" au lieu de "F9" , qui affichera le titre "This is a secret title" même depuis le debugger, nous devons donc trouver quelque chose de plus puissant si nous voulons détécter de façon plus efficace la présence d'un debugger.

Tous les fichiers (en fonction des étapes) sont disponible à cette adresse.

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 !

Débuts avec Arduino : Partie 2

24 Feb 2013

Après de longues semaines (mois ? ) d'absence, me voilà de retour, et motivé ! Voici donc le second numéro de la suite d'articles basés sur Arduino, je tiens à préciser que j'en ai profité pour rajouter des vidéos sur le premier article.

Au programme de ce deuxième article, il y a les ponts diviseurs de tension (et leur utilité), les photorésistances (résistance variable en fonction de l'intensité lumineuse), mais également les buzzers (pour faire du bruit !), en outre, encore des applications relativement simples, mais qui n'en restent pas moins ludiques et intéressantes.

Liste du matériel nécessaire:

  • Des photorésistances (au moins 2)
  • Des résistances (500 et 10k par exemple)
  • Un bouton poussoir
  • Des fils

Le projet final de cet article est de créer un "clavier tactile" (photosensible), permettant de libérer différents sons (comme à chaque fois, les parties sont étudiées séparément, une à une, avant d'être rassemblées).

0x01] Le Matériel

Les photorésistances

Une photorésistance est une résistance variable (résistance photodépendante) qui varie en fonction de l'intensité de la lumière captée. La résistance de cette dernière diminue lorsque l'intensité lumineuse augmente. Ainsi, nous pouvons utiliser ces photorésistances comme des capteurs lumineux, définir un seuil de luminosité et effectuer des actions en conséquence (en domotique, la gestion de l'intensité de l'éclairage dans une pièce par exemple). Dans cet article, nous utiliserons ces photorésistances pour créer des touches "tactiles". En plaçant le doigt entre la source lumineuse et la résistance, il est possible de modifier brutalement l'intensité captée, et donc de déclencher une action en avale.

Les buzzers

Ce sont grossièrement des "haut-parleurs" qui vont donc nous permettre d'émettre des sons. Pour cela, nous utiliserons la fonction tone(pin, frequency, duration) dont un exemple est disponible sur le site officiel. Les buzzers, tout comme les LEDs possèdent une grande patte (+) et une petite (-). L'utilisation d'un buzzer est donc ce qu'il y a de plus basique, un branchement (avec une petite résistance, disons 100ohm (comme sur le site Arduino)), une fonction Tone(), et le tour est joué !

Dans notre montage, nous aurons donc deux circuits, le premier avec une photorésistance, dont nous pourrons lire la valeur à l'aide de la fonction analogRead (vous vous en souvenez ? J'en avais parlé dans mon premier article :)). Dans notre cas, anaogRead permet de lire une tension aux bornes d'un pin analogique (A0-A5), pour autant, nous ne pouvons pas lire directement la valeur à la sortie de notre photorésistance, pour cela, nous allons utiliser un pont diviseur de tension. L'autre circuit est celui qui contrôlera le buzzer (ou les buzzers, nous verrons après pourquoi).

0x02] Ponts diviseurs de tension

Définition

Un pont diviseur de tension est un montage en série qui comme son nom l'indique, permet d'obtenir une tension plus faible, à partir d'une tension entrée.

Dans cet article, il sera constitué de deux résistances, R1 et R2, la formule liée à une pont diviseur de tension est : ${Vout} = {Vin} * {R2}/(R1+R2)$, avec Vout et Vin en volt; R1 et R2 en Ohm. Voici un schema du pont diviseur de tension que nous allons créer : voltage divisor

Utilité (dans le cadre de cet article)

Comme stipulé précédemment, nous aurons besoin de lire la valeur de la tension aux bornes de la photorésistance, grâce à la formule énoncée ci-dessus, en connaissant Vout (mesuré par AnalogRead), Vin (3.3V ou 5V sur l'Arduino) et la valeur de R1 (ou de R2), c'est-à-dire une résistance connue, nous pourrons déterminer la valeur de la résistance variable (photorésistante). Deux choix s'offrent à nous, dans le premier, nous envoyons directement la valeur de cette résistance, à travers la fonction map(value, fromLow, fromHigh, toLow, toHigh) au buzzer ce qui, en soit, risque de produire des sons relativement atroces et dont je voudrais nous protéger, ou alors, nous pouvons donner fréquences au buzzer, et activer ce dernier lorsque le seuil d'intensité lumineuse minimal (fixé) est dépassé (dans le sens inférieur), dans un soucis pédagogique, j'effectuerai les deux montages et les deux codes seront disponibles.

Lecture analogique sur diviseur

Connectons une première résistance, de 470ohm à la source 5V disponible sur l'Arduino, branchons également notre photorésistance de façon à créer ce pont diviseur de tension (ne pas oublier de relier la photorésistance au sol (GND)), branchons un fil en direction de l'entrée analogique n°0 au milieu du pont, ce qui devrait donner plus ou moins :

diviseur

La valeur de la résistance de la photorésistance varie en fonction de la luminosité, il va de soit (d'après la formule énoncée plus haut) que la tension de sortie varie en même temps, c'est la valeur que nous devons lire sur l'entrée analogique n°0.

Exercice n°1

Histoire de s'habituer une nouvelle fois avec le transfert de données par la fonction Serial, nous allons afficher en temps réel la valeur de la photorésistance (on va faire un peu de mathématiques par la même occasion). Il s'agit premièrement d'isoler R2 dans la relation précédente, pour cela, je vous conseille de procéder à un double passage à l'inverse, on retrouve donc : $${Vin}/{Vout} = {R1+R2}/{R2}$$ $${Vout}/{Vin-Vout} = {R2}/{R1}$$ $${R2} = (Vout*R1)/(Vin-Vout)$$ Les valeurs respectives de chaque variable sont :

  • 5v pour Vin (ou 3.3 en fonction de l'alimentation que vous choisissez)
  • R1, 470ohm, choisi arbitrairement
  • AnalogRead() sur A0 pour connaitre la valeur de Vout

Note : AnalogRead() fournit une valeur située entre 0 (0v) et 1024 (5v), il faut donc convertir Vout avant de l'utiliser dans la formule (sinon les résultats risquent d'être quelques peu étranges). Pour faire cette transformation on utilise : $${Vout} = (5*{Vout})/1024$$ Vous l'aurez compris, le premier exercice consiste à écrire un code qui affiche sur la connexion serial la valeur de la photorésistance (en utilisant le montage précédent)

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

void loop(){
        float vout = analogRead(A0), vin = 5, r1 = 470, r2 = 0;
    vout = (5*vout)/1024;
    r2 = (vout*r1)/(vin-vout);
    Serial.println(r2);
    delay(250);
}

Ici, on utilise des variables de type float pour éviter les imprecisions liées aux arrondis des rapports (imaginez les tronquages qui sont effectués lors de la division par 1024...). Rien de bien sorcier, juste un petit exercice histoire de reprendre les bases. Attention, la valeur affichée peut différer de la réalité (remplacez R2 par une résistance fixe connues) et être liée à de nombreuses incertitudes (celle de R1, de R2, de la lecture analogique, des arrondis et autres), mais elle permet cependant d'avoir une idée approximative de la valeur. Pour autant, en remplacant la photorésistance par une résistance de 470ohm elle aussi, la valeur affichée est tout à fait correct.

Maintenant que nous savons comment réaliser le premier circuit et récupéré la valeur de la résistance, il faut s'attaquer au second circuit, celui du buzzer, de même, ce montage reste tout ce qu'il y a de plus basique :).

Circuit du buzzer

Une résistance de 100ohm, une buzzer, un pin rien de plus pour notre circuit, dont voici le schema : Buzzer.

Avant de finaliser le montage, voici un petit exercice mélodique histoire de découvrir un peu le fonctionnement du buzzer, le but est de jouer la gamme tempérée (do, ré, mi, fa, sol, la, si, do), vous pourrez trouver les fréquences de chaque note facilement sur wikipedia.

int buzzer = 7;
void setup(){
    pinMode(buzzer, OUTPUT);
}

void loop(){
    float frequences[] = {261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25};
    for(int i=0; i<8; i++){
        tone(buzzer, frequences[i]);
        delay(1000);
    }
}

Les plus curieux (ou instruits) auront deviné que la résistance ne modifie que l'intensité du son, plus la résistance sera élevée, plus le son produit sera faible, modifiez la résistance par un photorésistance, et plus l'intensité lumineuse sera élevée, plus l'intensité sonore sera elle aussi importante (cela pourrait être un début de système permettant de jouer une musique lorsque vous allumez la lumière de la pièce par exemple). Maintenant que nous avons nos deux circuits, nous allons fusionner les deux afin de n'en faire plus qu'un ! Mais avant cela, il nous manque un dernier ingrédient avant d'éviter que le son se repete en boucle. Voici une illustration de cette merveilleuse gamme jouée par un buzzer de piètre qualité:

Seuil d'intensité

Il s'agit ici de définir un seuil de luminosité (grâce à la tension lue sur l'entrée analogique) en dessous du quel le buzzer devra être activé (sinon la photorésistance n'est d'aucune utilité), dans un premier temps, je pense que le plus simple et de re-faire le premier montage (pont diviseur de tension, avec une résistance et une photorésistance), de regarder les valeurs de la tension (pas besoin de s'embeter avec la formule) puis de masquer la lumière en placant votre doigt au dessus de la photorésistance, et de re-noter la valeur affichée, puis de définir une variable qui représentera le seuil à dépasser pour activer le son.

Cette méthode est un peu crade et pas portable (l'intensité dépendra du milieu et il faudra le changer à chaque modification de la luminosité (extérieur, nouvelle lampe)). Cependant, on pourrait considérer une autre méthode qui consiste à faire un circuit avec un bouton, et de déclancher la determination du seuil lorsque ce dernier est pressé. Pour autant, je n'ai toujours pas detaillé le fonctionnement des boutons, et je souhaite garder ce sujet pour un prochain article, sans le bacler, mais ne vous inquietez pas, lorsqu'il sera temps, je reviendrai sur cette article afin de présenter le montage complet !

Fusion !

Pour ce qui est du montage, c'est un assemblage des deux précédents, ce qui nous donne : Montage Final Je serais prêt à parier que vous aviez trouvé !

Comme ce que j'avais annoncé au début de l'article, nous allons utiliser ce montage de deux facons, la première avec la fonction map(value, fromLow, fromHigh, toLow, toHigh) afin de transformer les valeurs de la résistances (ou de la tension, ce qui nous évite les calculs) en fréquence audibles (mais loin d'être charmantes :) ), voici donc le code, suivi d'une vidéo

int buzzer = 7, res = A5, seuil = 800, duree = 500; 
int res    = A0;
void setup(){
    pinMode(buzzer, OUTPUT);
}
void loop(){
    int tension = analogRead(res), frequence;
    if(tension > seuil){
        frequence = map(tension, 0, 1024, 260, 525);
        tone(buzzer, frequence);
        delay(duree);
        noTone(buzzer);
    }
}

Tout d'abord, il faut déterminer si la valeur lue est supérieur au seuil arbitraire puis comme nous savons que les valeurs lues par l'entrée analogique varient entre 0 et 1024 et que nous voulons obtenir des fréquences situées entre 260Hz et 525 Hz, on passe ces paramètres à la fonction map. Le reste du code ne change pas, si ce n'est la fonction noTone() qui permet d'arreter le buzzer après qu'il ai sonné (sans quoi le buzzer continuera de sonner sans s'arreter avec un fréquence égale à la dernière fréquence connue). L'autre montage que je vous avez proposé de réaliser est un montage attribuant une fréquence précise à une photorésistance, et de ne jouer cette dernière que si le seuil est depassé. Afin que ce projet soit plus interessant, j'utiliserai 2 photorésistances, une sur le pin analogique A0 et l'autre sur le A2 (les deux autres résistances ne changent pas : 470ohm) voici le code :

int buzzer = 7, seuil = 800, duree = 1000; 
int photores[] = {A0, A2};
float frequences[] = {261.63, 293.66}; // Fréquence attribué à chaque capteur
void setup(){
    pinMode(buzzer, OUTPUT);
}

void loop(){
    for(int i = 0; i< 2; i++){ // On parcours nos différentes photorésistances
        int tension = analogRead(photores[i]), frequence;
        if(tension > seuil){ 
            tone(buzzer, frequences[i]);
            delay(duree);
            noTone(buzzer); // pareil que tout à l'heure, il faut penser à arreter le buzzer.
        }     
    }
    delay(100);
}

Etant donnée que je suis quelqu'un de sympathique, voici une vidéo illustrant le montage finale de cet article :