Principe

Les boutons sont des éléments d’interface qui fonctionnent en ouvrant ou en coupant un circuit comme des interrupteurs.

Les boutons sont de 2 natures :

  • Normalement ouvert : sans toucher au bouton, le circuit est ouvert
  • Normalement fermé : sans toucher au bouton, le circuit est fermé.

Les pins de l’esp32 permettent de lire des tensions. En fonction du type de bouton, 0V peut dire que le bouton est appuyé ou pas… Pas si simple, car pour lire une tension, il faut qu’il y en ait une ! Pour avoir une tension à lire, les concepteurs ajoutent donc des résistances.

Les esp32 vont avec des résistances misent soit entre

  • pin et le Gnd : mode Pull UP.
    • Circuit ouvert : la tension sera haute : donc booléen à 1
    • Circuit fermé : la tension ira au 0, donc basse : le booléen à 0
  • pin et le + : mode Pull Down
    • Circuit ouvert : la tension ira au 0, donc basse : le booléen à 0
    • Circuit fermé : la tension sera haute : donc booléen à 1

Il y a un deuxième élément à tenir en compte, lors du déclenchement d’un interrupteur, la tension ne va pas basculer instantanément, pendant quelques millisecondes, la tension va fluctuer et pourra être interprétée comme haute ou basse. Il faut donc ne pas interpréter les variations d’une durée inférieure à cette latence.

Pour manipuler un interrupteur, on a besoin de différentes méthodes :

  • pinMode : Permet d’indiquer que le pin est en lecture ou écriture.
    • Dans notre cas, on veut lire une tension pour savoir si le bouton a été pressé ou non. et si c’est en lecture, il faut indiquer s’il en mode pullup ou pulldown.
  • digitalRead : permet de lire la tension sur un pin. A interpréter en fonction du type de bouton et du mode choisi !

Code

Les boutons seront gérés par du code dédié, car j’ai besoin de détecter les appuis longs, et en fonction de la longueur de l’appui avoir des comportements différents (un peu comme le chargement de l’énergie pour une arme, le rechargement du chargeur, déclencher le mode administratif).

  • Bouton.h :
#include <Arduino.h>

#ifndef BUTTON_H
#define BUTTON_H
#define BUTTON_DUREE_ACTION 10 // en ms, durée minimum pour action sur le bouton soit prise en compte

class Button
{
private:
  uint32_t lastMillis;
  uint32_t dureeAction;

public:
  /* data */
  uint8_t PIN;
  uint32_t numberKeyPresses;
  bool actionne; //A true quand le bouton est actionné
  bool actionne1SenCours; //A true quand le bouton est actionné depuis 1 seconde
  bool actionne5SenCours; //A true quand le bouton est actionné depuis 5 secondes

  bool relache; //A true quand le bouton est relaché
  bool relache1S; //A true quand le bouton est relaché depuis 1 seconde
  bool relache5S; //A true quand le bouton est relaché depuis 5 secondes

  
  int etatHaut = HIGH; //Etat logique du bouton quand il est relaché
  Button(uint8_t _PIN); //Constructeur
  Button(uint8_t _PIN, bool modePullUp); //Constructeur avec précision du mode de pullup du bouton. Les boutons sont normalement ouverts ou fermés, il faut donc préciser le mode de pullup
  void MAJ(); //Mise à jour de l'état du bouton, à faire chaque cycle de setup();
  void AfficherDebug(); //Affiche les informations de debug du bouton
  uint32_t DureeEtatEnCours(); //Retourne la durée en ms depuis le dernier changement d'état du bouton
};


#endif
  • Button.cpp
#include <Arduino.h>
#include "Button.h"

Button::Button(uint8_t _PIN)
{
  PIN = _PIN;
  numberKeyPresses = 0;
  actionne = false;
  relache = true;
  pinMode(PIN, INPUT_PULLUP);
  dureeAction = 0;
}

Button::Button(uint8_t _PIN, bool modePullUp)
{
  PIN = _PIN;
  numberKeyPresses = 0;
  actionne = false;
  relache = true;
  pinMode(PIN, INPUT_PULLUP);
  if (modePullUp == true)
   // pinMode(PIN, INPUT_PULLUP);
   etatHaut = HIGH;
  else
    etatHaut = 0x0;
    //pinMode(PIN, INPUT_PULLDOWN);
  dureeAction = 0;
}

void Button::MAJ()
{
  int etatBouton = digitalRead(PIN);
  u_long millisAct = millis(); //Stocke la valeur de millis() pour éviter de la lire plusieurs fois

  if (etatBouton == etatHaut) //Teste si le bouton a un niveau logique HAUT
  {
    if (actionne == false)
    {
      actionne = true;
      relache = false;
      relache1S = false;
      relache5S = false;

      numberKeyPresses++;

      lastMillis = millisAct;
    }
    else{
      //L'action est en cours et l'on va détecter depuis combien de temps
      if(millisAct - lastMillis > 1000)
        actionne1SenCours = true;
      if(millisAct - lastMillis > 5000)
        actionne5SenCours = true;
    }
    //Serial.printf("etatBouton %d\n", etatBouton);
  }
  else // test si le bouton a un niveau logique différent de HAUT (donc BAS)
  {
    if (actionne == true)
    {
      actionne = false;
      actionne1SenCours = false;
      actionne5SenCours = false;
      dureeAction = millisAct - lastMillis;

      lastMillis = millisAct;
      if (dureeAction > BUTTON_DUREE_ACTION)
      {
        relache = true;
        if (dureeAction > 1000)
          relache1S = true;
        if (dureeAction > 5000)
          relache5S = true;
      }
      else
        numberKeyPresses--;
    }
    else
    {
      relache = false;
      relache1S = false;
      relache5S = false;
    }
  }
}

uint32_t Button::DureeEtatEnCours()
{
  return millis() - lastMillis;
}

void Button::AfficherDebug()
{
  if(actionne || relache)
      Serial.printf("actionne %d, relache %d, dureeAction %d, act1SenCours %d, act5SenCours %d, relache1S %d, relache5S %d \n", actionne, relache, DureeEtatEnCours(), actionne1SenCours, actionne5SenCours, relache1S, relache5S);  
}
  • Code pour tester :
#include <Arduino.h>
#include "Button.h"


Button bouton1 = Button(12,false );
Button bouton2 = Button(14,false );
Button bouton3 = Button(13,false );
void setup() {
  //Pour initialiser la liaison série :
  Serial.begin(115200);
  Serial.print("Projet Bouton 1.0\n") ;

  
}

void loop() {
  /*
  int millisAct = millis();
  for(int i = 0; i < 10000; i++){
    bouton1.MAJ();
  }
  int delta = millis() - millisAct;
  Serial.printf("delta %d\n", delta);
*/
  bouton1.MAJ();
  bouton1.AfficherDebug();

  bouton2.MAJ();
  bouton2.AfficherDebug();

  bouton3.MAJ();
  bouton3.AfficherDebug();
  
}

Vous trouverez un commentaire le code que j’ai utilisé pour tester l’efficacité de la classe : 10 000 appels pour 10 ms, donc plus ou moins un millionième de seconde pour 1 appel (donc tout l’un dans un l’autre 200 cycles horloges)

Optimisation à réfléchir : Tous les boutons n’auront pas besoin de toutes les fonctionnalités développées. Il est possible de proposer des boutons allégés pour gagner quelques cycles. (Pas d’actualité !)

Circuit de tests

Ou en version électronique :

Constat

Nous avons le moyen de détecter que le bouton fonctionne et de trouver les comportements intéressants.

Il va falloir à rendre ce code utilisable :

  • Quid de la consommation d’évènements ?
    • Pour l’instant notre bouton bascule d’état mais on a aucune idée s’il a été traité ou non…

Il va falloir rendre ce code efficace :

  • Une action sur le bouton doit être consommée. Soit on fait de la programmation évènementielle, soit on fait de la séquentielle. En programmation évènementielle, on programme un interruption processeur qui en cas de bascule d’état du pin appelle appelle une fonction pour le traiter. Je n’aime pas cette approche car le code se disperse et qu’il faut que la gestion de l’évènement soit la plus brève possible (pour ne pas occuper le processeur). A la fin, on gère des bouts de codes qui basculent des variables qui seront repris séquentiellement dans la boucle principale séquentiellement… Autant commencer séquentiellement !
    • Attention, le full séquentielle, a comme probléme le temps de traitement de la boucle principale qui peut amener à “râter” des actions. On limitera ce risque avec des évènements sur certaines choses (la réception LED, la réception Wifi) et le multi threading.
    • Programmation objet souple : Je vais proposer une architecture objet qui par la suite permettra d’implanter des boutons fonctionnant sur une interruption évènementielle.

Il va falloir proposer une solution pour simuler un bouton :

  • Pour débugger, il faudra simuler des boutons… Il n’est pas crédible de tout débugger avec des tests fonctionnels.

Diagramme de classe

Depôt git

https://github.com/jeanbaptisteaubry/LasertTag_2_Bouton.git

On s’intéressa au main.cpp

#include
#include “Button.h”
#include “ButtonInterface.h”
#include “ButtonDebug.h”

#ifndef DEBUG
#define DEBUG 1
#endif

#if DEBUG
ButtonDebug::SimulationEntry bouton1Table[] = {
{1000, HIGH},
{2000, LOW},
{3000, HIGH},
{4000, LOW},
{5000, HIGH},
{6000, LOW}};

ButtonDebug bouton1(12, bouton1Table, sizeof(bouton1Table) / sizeof(bouton1Table[0]));

#else
Button bouton1 = Button(12, false);
Button bouton2 = Button(14, false);
Button bouton3 = Button(13, false);
#endif
void setup()
{
// Pour initialiser la liaison série :
Serial.begin(115200);
Serial.print(“Projet Bouton 1.0\n”);
}

void loop()
{
/*
int millisAct = millis();
for(int i = 0; i < 10000; i++){ bouton1.MAJ(); } int delta = millis() - millisAct; Serial.printf("delta %d\n", delta); */ #if DEBUG bouton1.MAJ(); bouton1.AfficherDebug(); #else bouton1.MAJ(); bouton1.AfficherDebug(); bouton2.MAJ(); bouton2.AfficherDebug(); bouton3.MAJ(); bouton3.AfficherDebug(); #endif delay(15); }

Ici, si #define DEBUG prend pour valeur 1 ou 0, on fait basculer le code, et nous pouvons tester l’un ou l’autre des modes de fonctionnements. On constate le fonctionnement des SimulationEntry où l’on indique à quel horaire l’état du bouton bascule. Cette solution permet de reproduire des scénarios précis ou de tester des hypothèses difficilement reproduisables en tests fonctionnels.