TapTempo pour Arduino Uno

Posté par  (site Web personnel) . Édité par Ysabeau et Benoît Sibaud. Modéré par Ysabeau. Licence CC By‑SA.
Étiquettes :
34
23
déc.
2020
Matériel

Puisqu’elle n’existe pas encore, voici une version de TapTempo pour Arduino Uno, utilisant Arduino IDE sous Linux.

Arduino est une marque italienne proposant des cartes de développement open-source basées sur des micro-contrôleurs AVR, ARM et Cortex-A3.

L’Arduino Uno est la carte la plus connue et la plus accessible : compter environ deux euros en Chine pour des copies d’Arduino, et moins de dix euros en France. Elle est équipée d’un micro-contrôleur Atmel ATmega328P, dont les caractéristiques techniques sont : architecture Atmel AVR, 16MHz, 8bit, 32ko Flash, 1ko EEPROM, 2ko de SRAM. On est donc loin des PC avec CPU en GHz et RAM en Go.

Sommaire

Les avantages de la carte

Les avantages de cette carte sont :

  • 14 entrées/sorties numériques, dont 6 PWM ;
  • 6 entrées analogiques ;
  • port série, bus I2C et SPI ;
  • consommation électrique autour de 50mA sous 5V ;
  • un nombre phénoménal de capteurs et accessoires en tous genres et pas chers (shield moteur, capteurs ultrason, température, pression, humidité, vibration…) ;
  • un port USB pour transférer le programme dans le micro-contrôleur, et retourner des infos au PC via le port série de la carte ;
  • un IDE (Arduino IDE sous GPL 2.0) qui fonctionne sous Linux et prend en charge toute la partie transfert du code. Il fait également office de moniteur série, et plotter/grapheur bien pratique pour suivre l’évolution de valeurs de capteurs dans le temps.

C’est donc une carte simple et idéale pour apprendre à faire des robots, automatiser sa serre de jardin ou son aquarium, faire une serrure à carte sans contact, etc.

Pour l’exemple TapTempo, j’utilise une « Funduino UNO », copie chinoise à bas prix, un écran LCD de deux lignes de 16 caractères avec interface I2C, et un bouton poussoir câblé entre l’entrée numérique n°2 et la masse.

Copie chinoise d’Arduino Uno

Comme je n’aime pas les câbles Dupont mâles, j’utilise ici une carte supplémentaire qui vient se poser sur l’Arduino Uno, qui ne fait que rendre plus accessible les pins de connexion (un sensor shield). C’est facultatif et ça marche très bien sans. Le câblage est d’ailleurs simple à réaliser.

Câblage du TapTempo sur Arduino Uno

Le programme

Le programme est écrit dans Arduino IDE qui permet de faire du C et C++. On peut utiliser d’autres IDE et d’autres langages (LUA, microPython). Voici le TapTempo basique :

    #include <LiquidCrystal_I2C.h>
    #include <Wire.h>

    LiquidCrystal_I2C lcd(0x27,16,2);  // parametres de l'ecran à cristaux liquides

    // quelques constantes et variables globales
    const byte TAP_PIN = 2;  // bouton poussoir connecté sur l'entrée 2
    bool bouton_status = true;
    unsigned long t[6]; // stocke 6 taps pour avoir 5 intervalles
    byte i;
    byte n;
    unsigned long tempo;

    void setup() {
        lcd.init();
        lcd.backlight();
        lcd.setCursor(0,0);
        lcd.print("TapTempo Arduino");
        lcd.setCursor(0,1);
        lcd.print("Tempo : ....");
        pinMode(TAP_PIN, INPUT_PULLUP);    // on s'évite une résistance
        for (i = 0; i < 6; i++) t[i] = 0;
    }

    void loop() {
        if (! bouton_status) {
            bouton_status = digitalRead(TAP_PIN);
        } else {
            bouton_status = digitalRead(TAP_PIN);
            if (! bouton_status) {
                // ok, un appui est detecté
                for (i = 0; i < 5; i++) t[i] = t[i + 1];
                t[5] = millis();

                if (t[5] - t[4] < 3000) {
                    // calcul de la moyenne des 5 derniers taps
                    tempo = 0;
                    n = 0;
                    for (i = 1; i < 6; i++) {
                        if (t[i - 1] > 0) {
                            tempo += t[i] - t[i - 1];
                            n++;
                        }               
                    }
                    tempo = 60000 / (tempo / n);

                    // affichage du resultat
                    lcd.setCursor(8, 1);
                    lcd.print(tempo);
                    lcd.print("   ");
                } else {
                    // premier tap, on attend le suivant
                }
            } else {
                if (millis() - t[5] > 3000) {
                    // rien depuis 3s (tempo < 20), on reinitialise
                    lcd.setCursor(8, 1);
                    lcd.print("....");
                    for (i = 0; i < 6; i++) t[i] = 0;
                }
            }
        }
        delay(10);  // pour ne pas se taper le rebond du bouton
    }

Les fonctions setup et loop

Arduino IDE impose la présence de deux fonctions : setup et loop.

La fonction setup ne s’exécute qu’une seule fois, lorsque l’Arduino Uno est mis sous tension ou après un reset (le tranfert d’un nouveau code dans le micro-contrôleur est toujours automatiquement suivi d’un reset).

Elle est assez facile à comprendre, elle initialise l’écran (facultatif), les entrées/sorties utilisées (ici uniquement le pin 2 pour le bouton poussoir), et le tableau des taps. On peut juste s’appesantir un peu sur le INPUT_PULLUP qui indique au micro-contrôleur qu’il doit activer la résistance interne de pullup de cette entrée. Si cette entrée est déclarée en INPUT seulement, il faut alors ajouter une résistance (10kOhm par exemple) entre l’entrée 2 et le 5V, afin de ramener l’entrée proche de 5 volts (HIGH, true) lorsque le bouton n’est pas pressé. Sinon l’entrée ne serait connectée à rien, donc avec un potentiel électrique indéterminé. Lorque le bouton est pressé, l’entrée est en connexion directe avec la masse, donc quasiment à 0V (LOW, false).

La fonction loop, comme son nom l’indique, s’exécute en boucle.

On commence par regarder si le bouton était pressé à la fin de la boucle précédente (bouton_status à false), si oui, on lit l’état du bouton jusqu’à relâchement. Sinon, le bouton était relâché, on peut donc regarder si maintenant il est pressé ou pas.

Suit le classique calcul du tempo basé sur les 5 dernières mesures et l’affichage. L’affichage et le tableau se réinitialisent si pas de tap pendant plus de 3 secondes.

La fonction loop se termine par un mystérieux délai de 10 ms, dont l’unique but est de ne pas capter le rebond du bouton. En effet, un bouton poussoir ne passe pas toujours franchement d’un état ouvert à l’état fermé, il rebondit, pouvant donner des séries d’ouvertures-fermetures perturbantes pendant quelques millisecondes. Sur ce bouton, la transition est normale (avec légère surtension pendant 2ms) au moins 9 fois sur 10, mais des rebonds sont présents sur environ un dixième des taps. Ces rebonds ne dépassent toutefois jamais les 3ms.

Rebonds du bouton

Avec un délai de 10 ms, il n’y a donc pas de risques que les rebonds viennent perturber le programme.
Patienter un centième de seconde ne gêne pas à l’usage, j’ai mesuré avec précision (à l’unité près, je ne pense pas que descendre sous la virgule soit d’un grand intérêt musical) entre 20 BPM et 240 BPM, l’erreur la plus grande vient de mon doigt qui appuie sur le bouton (et du cerveau qui commande le doigt), bref je n’arrive pas à suivre, faudrait faire un robot (à base d’Arduino Uno ?) qui appuie sur le bouton à ma place.

Montage en fonctionnement

Usage mémoire

En incluant les bibliothèques I2C (wire.h) et LiquidCrystal pour l’écran LCD, la compilation donne :

  • 4092 octets de FLASH utilisés sur les 32ko disponibles ;
  • 323 octets de SRAM utilisés sur les 2ko disponibles.

En n’utilisant pas l’écran LCD et en envoyant le tempo mesuré sur le port série :

  • 2454 octets de FLASH utilisés ;
  • 218 octets de SRAM utilisés.

Pour comparaison, pour un programme vide (juste les deux fonctions sans autre ligne de code), la compilation donne respectivement 444 octets de FLASH et 9 octets de SRAM utilisés. Ça peut sembler bizarre, mais le compilateur ajoute toujours un appel pour surveiller les événements sur le port série.
Pas de drame, tout cela reste léger.

Avec d’autres micro-contrôleurs

Les ESP8266 et ESP32, qui sont des micro-contrôleurs wifi de la société Expressif, ont aussi la cote en ce moment, principalement pour les remontées de mesures à distance, alertes par courriel… Pour ces micro-contrôleurs bien mieux équipés (160 MHz, 4 Mo RAM) qu’un Arduino Uno, le programme serait le même, Arduino IDE se chargeant de compiler et transférer le programme au micro-contrôleur. Il faudra penser cependant à ajouter une résistance de pullup suivant les modèles, tous les micro-contrôleurs ne sont pas pourvus de ces résistances internes. L’ESP32 est pourvu d’entrées tactiles (capacitive touch sensor), il serait intéressant de remplacer le bouton poussoir par une simple plaque métallique et regarder s’il est possible de s’affranchir des problèmes de rebonds.

Aller plus loin

  • # Tableau

    Posté par  . Évalué à 8. Dernière modification le 23/12/20 à 15:17.

    Cool, j'aime bien les Arduino.

    Tu peux initialiser ton tableau à zéro avec :

    unsigned long t[6]={0};
    

    Ça évite la boucle.

    • [^] # Re: Tableau

      Posté par  (site Web personnel) . Évalué à 4.

      Super, ça fait passer le code compilé à 4068 octets, donc 24 octets de gagnés.

    • [^] # Re: Tableau

      Posté par  . Évalué à 6. Dernière modification le 24/12/20 à 08:22.

      Si le compilateur/linker Arduino respecte le standard du C (a vérifier, ce genre de point est parfois changé pour des questions de performance en embarqué), il est même possible de se passer de l’initialisation à 0, car c’est une variable globale et que les variables globales (et les locales statiques) sont implicitement initialisées à 0 avant d’arriver à main().

      Tous les nombres premiers sont impairs, sauf un. Tous les nombres premiers sont impairs, sauf deux.

      • [^] # Re: Tableau

        Posté par  (site Web personnel) . Évalué à 1.

        Tu as raison, avec ou sans le = {0}, le programme compilé fait la même taille et les valeurs du tableau fraîchement créé sont à zéro.

        Confirmation sur StackExchange

      • [^] # Re: Tableau

        Posté par  (site Web personnel) . Évalué à 1.

        Oui c'est vrai (cela fait partie de l'iso C89), il y a de grandes chances que le compilateur respecte au moins cette norme.
        Néanmoins cela reste une bonne pratique, car quand on te relis cela démontre que tu n'as pas oublié les valeurs d'initialisation.
        Et surtout : si un jour tu déplaces tes variables dans une RAM externe, il y a aura la possibilité que l'initialisation implicite ne soit pas respectée.

  • # Intéressant !

    Posté par  . Évalué à 3.

    J'aime beaucoup ce sujet, merci pour l'article.

    Question de débutant : tu as la même instruction quel que soit le résultat de la condition, c'est normal ?

        if (! bouton_status) {
            bouton_status = digitalRead(TAP_PIN);
        } else {
            bouton_status = digitalRead(TAP_PIN);
    • [^] # Re: Intéressant !

      Posté par  . Évalué à 2.

      Oui, ce que tu veux c'est adapter le comportement selon l'état du bouton le tour d'avant :
      - s'il n'était pas pressé, tu lis l'état actuel et tu verra le tour prochain ;
      - s'il était pressé, tu lis l'etat actuel et tu regardes s'il a été relaché pour agir le cas échéant.

      • [^] # Re: Intéressant !

        Posté par  . Évalué à 4. Dernière modification le 24/12/20 à 02:19.

        Après à mon avis, il serait plus approprié d'utiliser des interruptions, il y a des pins qui en sont capables sur les uno. Mais comme je ne pense pas qu'on puisse mettre une uno en standbye, et qu'elle n'a que ça à faire, ce serait surtout pour la beauté du geste…

        • [^] # Re: Intéressant !

          Posté par  (site Web personnel) . Évalué à 4.

          L'Arduino Uno dispose bien d'interruptions qu'on pourrait utiliser pour le mettre en veille et le réveiller, histoire de gagner en consommation électrique.

          Mais pendant la mesure (entre deux taps) on ne pourrait pas utiliser le "Power down mode" car il couperait aussi le timer (millis) dont on a besoin pour calculer le tempo. On ne pourrait utiliser que le "Power save mode" bien moins intéressant énergétiquement.
          De plus, la carte Uno intègre le convertisseur USB/Serial qui ne se mettra pas en veille, ainsi que le régulateur 3.3V, la diode de présence de tension… au final, on ne gagnerait que quelques mA pour un code bien plus complexe.

          Il serait par contre plus intéressant d'activer ce "Power down mode" après les 3s d'inactivité :
          - on tape, pas de mise en veille ;
          - on ne tape plus, affichage du "…." pendant quelques secondes, puis extinction du rétro-éclairage de l'écran et passage en veille du micro-contrôleur ;
          - on retape -> interruption, réveil du micro-contrôleur, allumage de l'écran.

          • [^] # Re: Intéressant !

            Posté par  . Évalué à 4.

            En fait on peut utiliser des interruptions sur les pins 2 et 3, sans mettre en veille, comme ça :

            void mesure()
                {
                // Traitement
                ...
                }
            void setup()
                {
                ...
                attachInterrupt(digitalPinToInterrupt(bouton_status), mesure, RISING);
                ...
                }

            Arduino interrupts

            • [^] # Re: Intéressant !

              Posté par  (site Web personnel) . Évalué à 3.

              C'est une autre façon de faire et qui fonctionnerait aussi. Il y a plein de façon de faire, c'est cool. Dans un programme plus gros, il faudrait faire attention que la fonction appelée par l'interruption soit la plus courte possible, car elle est bloquante.

              Si on utilise une interruption, le attachInterrupt serait plutôt comme ça :

              attachInterrupt(digitalPinToInterrupt(TAP_PIN), mesure, FALLING);

              … car lorsqu'on presse le bouton, le pin 2 passe de HIGH à LOW.

      • [^] # Re: Intéressant !

        Posté par  (site Web personnel) . Évalué à 1. Dernière modification le 05/01/21 à 09:23.

        Donc tu peux mettre ton instruction en amont de ta clause 'if'.
        … ah ok… franchement ça mérite quelques commentaires ce code, avec des noms de variable plus explicites.

  • # Un beau défis en perspective

    Posté par  (site Web personnel) . Évalué à 2.

    faudrait faire un robot (à base d’Arduino Uno ?) qui appuie sur le bouton à ma place.

    J'aime beaucoup se genre d'idée :) Je trouve ça presque poétique.

    J'ai plus qu'une balle

  • # quelques commentaires.

    Posté par  (site Web personnel) . Évalué à 1.

    Hello !
    Merci pour le retour d'expérience. J'ai quelques remarques :
    - Les nombres magiques mériteraient d'avoir leur définition dans le code, par exemple le timeout d'inactivité, la taille de ton tableau. Le code se lirait mieux.
    - Les variables globales pourraient être locales à ta boucle.

    Pour la détection du bouton rebond je trouve ton test astucieux, qui mériterait plus de commentaires, j'aurais pour ma part utilisé deux variables current et old, pour n'avoir qu'une instruction de lecture.

    Pour le décalage des temps à voir si memmove peut aller plus vite , mais le parcours en sens inverse peut faire gagner un peu de temps.
    Je pense que tu gagnerais à utiliser une puissance de 2 pour la taille de ton tableau, tu y gagnerais à la division pour le calcul de moyenne ( décalage de bits VS division, voir division non accélérée ). Je ne ferai même pas de test à 0 pour la somme, ainsi pas de test if et n=constante=8 => optimisation compilateur, ou explicite.

    Pour l'utilisation d'une IT, cela permettrait d'être plus réactif, car tu peux perdre un tap dans ta boucle d'attente de 10 ms.
    Plutôt que de faire appel à l'état retourné par milli, tu pourrais utiliser un compteur local de boucle puisque tu attends un délai fixe. Peut-être moins précis à la longue, car il faut tenir compte de la durée d'exécution de ta boucle.

    Pour la mise en veille je suis plutôt d'accord avec la complexification, sauf si tu te contentes d'une mise en veille pendant ton délai d'attente, Ca fait déjà une grosse économie.

    Pour le test de performances, un GBF à la place du bouton pourra faire l'affaire :).
    cdlt.

    • [^] # Re: quelques commentaires.

      Posté par  (site Web personnel) . Évalué à 2.

      merci pour tes remarques.

      En effet, le code n'est pas tip-top, c'est juste un petit truc vite fait, et je n'ai pas pris la peine de faire comme si c'était un programme de 20000 lignes.
      Ca reste un petit code très basique et simple, ça se comprends sans s'arracher les cheveux.

      Pour le tableau en puissance de 2, j'y avais songé, mais le TapTempo de base (fait précédemment dans plein d'autres langages) se base sur 5 taps, donc j'ai fait comme l'existant.

      Utiliser une interruption est tout à fait faisable.
      Je ne pense pas que le délai de 10ns puisse faire perdre un tap, car le changement d'état du bouton n'est pas aussi rapide, sauf à savoir cliquer plus de 100 fois par seconde sur le bouton, ce qui m'est pas mon cas, et donnerait d'ailleurs des tempos de dingues. 6000 BPM, ce n'est plus de la musique. En cliquant comme un fou, je n'arrive qu'à 600 BPM. A l'usage normal, tout fonctionne bien, pas de ratés, pas de tap non-pris en compte.

      Pour un usage sur batterie/piles, il faudrait en effet initialiser une veille après les 3 secondes d'inactivité. C'est simple à faire, mais je n'ai pas voulu compliquer le code qui avait pour but de faire une simple présentation de l'univers Arduino. Si quelques personnes qui ne connaissaient pas s'y sont intéressés à la lecture de ce TapTempo, je suis comblé.

      Pour les tests de performances, le GBF peut être remplacé par un autre arduino uno, tant qu'on y est. Mais on perd le test sur la partie mécanique (le bouton), d'où l'idée de faire un automate qui appuie physiquement sur le bouton, de manière bien plus régulière et plus rapide que je ne saurais le faire.

      De ce TapTempo sur arduino me vient une autre idée d'usage détourné. Si j'ai le temps, je vais le mettre en oeuvre et en faire un journal.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.