Piloter un clavier matriciel 16 touches (classe MatrixKeypad)
Dans les articles précédents, ce sont plutôt les sorties de données qui ont été abordées. Mais qu'en est il des entrées ?
Pour les sorties de données, c'est le micro-contrôleur qui en a la maîtrise. Et le problème est réglé par l'invocation de telle ou telle méthode d'écriture sur le composant récepteur. Mais pour les sorties, il en est autrement.
En fait deux problèmes se posent :
- Le premier est relatif à la nature des données à récupérer. Celles-ci, issues de périphériques aussi différents que le sont un bouton poussoir déclenché par un utilisateur et un capteur de température, peuvent être numériques ou analogiques.
- Le second est relatif à l’opportunité de la lecture. En effet, certains capteurs peuvent être lus en permanence en temps réels. D'autre ne le pourront que lorsque la donnée à lire est pertinente. Encore faut-il trouver un moyen d'alerter le micro-contrôleur lorsque qu'il peut procéder à la réception.
Cet article présente la mise en oeuvre d'un clavier matriciel à 16 touches. Si le câblage est très simple, la programmation pour récupérer la valeur de la touche pressée est un peu plus ardues. La classe MatrixKeypad y sera présentée pour venir à bout de tous les problèmes posés.
Qu'est-ce qu'un clavier matriciel ?
Architecture d'un clavier matriciel
Le projet Horloge, qui fut l'objet de plusieurs articles, utilisait trois boutons pour le réglage de l'heure. Chaque bouton, piloté par la classe Button, mobilise une pin de l'Arduino.
On pourrait considérer les touches d'un clavier comme autant de boutons et les programmer à l'aide de la classe Button. Mais pour un clavier de 16 touches, cela mobiliserait 16 pins numériques sur micro-contrôleur... qui n'en compte que 12. Aussi la réalisation d'un clavier est-elle structurellement différente.
Sur un clavier, les touches sont disposées en lignes et en colonnes. C'est pourquoi on parle de clavier matriciel. Les boutons étant constitués de deux pôles A et B, les pôles A des touches positionnées sur une même ligne sont reliés entre eux. Il en est de même pour les pôles B des touches situées sur une même colonne. Chaque ligne et chaque colonne est reliée à une pin du micro-contrôleur. De cette façon, on ne mobilise que 8 pins sur un micro-contrôleur pour piloter un clavier de 16 touches.
Et pour savoir si une touche est pressée, on positionne une tension alternativement sur chaque ligne et on observe colonne par colonne celle-qui est sous tension. Et on procède ainsi ligne par ligne, colonne par colonne. Lorsque le courant est passant pour une ligne et une colonne données, on connait la position de la touche pressée.
Traitement des rebonds
Lors de la présentation de la classe Button, le problème des rebonds avait été abordé et résolu de façon électronique en positionnant en parallèle un condensateur de 10 nF en tant qu'amortisseur. Cette solution est assez facile à mettre pour un bouton isolé. Si, pour les claviers d'ordinateur, l'amortissement des rebonds est prévu par le fabriquant, le clavier à membrane 16 touches utilisé dans cet article en est complètement dépourvu. Aussi faut-il assurer cette fonction par logiciel.
Le traitement logiciel des rebonds est effectué par un automate à quatre états. Le principe est que l'état de la touche doit être maintenu pendant un temps déterminé (ici 10 ms) pour être pris en compte.
L'automate fonctionne de la façon suivante : une évaluation de l'état de chaque touche est effectuée de façon itérative. En fonction de l'état de la touche, appuyée (A), ou relachée (B), et de l'état de l'automate, un traitement approprié est effectué :
- Pour une touche donnée, au repos, l'automate se trouve dans l'état IDLE.
- Si la touche est pressée par l'utilisateur (événement A), l'automate passe à l'état PRESSED.
- Si au bout de 10 ms, la touche est relachée (événement B), l'automate revient à l'état IDLE.
- Sinon, si la touche est maintenue enfoncée (événement A), l'automate passe à l'état HOLD et un événement est émis pour indiquer que la touche a changé d'état.
- La touche se trouve alors dans l'état enfoncée
- Si la touche est relachée (événement B), l'automate passe à l'état RELEASED.
- Si au bout de 10 ms, la touche est de nouveau enfoncée (événement A), l'automate revient à l'état HOLD.
- Sinon, si la touche est maintenue relachée (événement B), l'automate passe à l'état IDLE et un événement est émis pour indiquer que l'état e la touche a changé.
- La touche se trouve à nouveau au repos.
Pour résumer, pour que le changement d'état d'une touche soit pris en compte, il faut qu'il soit maintenu pendant au moins 10&nbps;ms.
Câblage du clavier matriciel
L'amortissement des rebonds étant pris en charge au niveau logiciel, le câblage du clavier en est simplifié. Il se résume à connecter les huit pins de celui-ci à huit pins numériques encore libres de l'Arduino. Il suffira de consulter la documentation technique pour repérer où sont connectées les pins correspondant au lignes et celles relatives au colonnes. Le choix de telle ou telle pin de l'Arduino n'a en fait aucune importance, puisque leur numéro respectif sera paramétré par le constructeur de la classe MatixKeypad lors de l'utilisation de celle-ci.
Définition des classes MatrixKeypad
La utilisation d'un clavier matriciel dans un sketch Arduino passe par l'instanciation de la classe MatrixKeypad. Celle-ci reprend le Design Pattern Observer de la bibliothèque Event, selon le même plan que l'exemple traité pour la classe Button, pour émettre des événements de classe KeypadEvent. La classe KeypadListener peut être implémentée pour réceptionner les événement émis par le clavier et leur associer un traitement. La définition de toutes ces classes se trouve dans le fichier MatrixKeypad .h qui doit être inclus dans le code des applications par une directives #include.
Enum KeyStatus
enum KeyStatus { KEY_IDLE, KEY_PRESSED, KEY_HOLD, KEY_RELEASED };
L'énumération KeyStatus définit l'état de l'automate pour chaque touche du clavier. Elle peut prendre les valeurs suivantes :
- KEY_IDLE — Cet état correspond à l'état repos de la touche. Elle est alors relâchée.
- KEY_PRESSED — Cet état est un état intermédiaire signalant que la touche a été enfoncée. Cet état doit être maintenu au moins 10 ms pour pouvoir passer à l'état KEY_HOLD.
- KEY_HOLD — Cet état correspond à l'état stable où la touche est maintenue appuyée.
- KEY_RELEASED — Cet état est un état intermédiaire signalant que la touche a été relachée. Cet état doit être maintenu au moins 10 ms pour pouvoir repasser à l'état KEY_IDLE.
Classe Key
struct Key {
KeyStatus status;
char value;
uint32_t millis0;
uint8_t rowPin;
uint8_t colPin;
uint8_t row;
uint8_t column;
};
KeyStatus status;
char value;
uint32_t millis0;
uint8_t rowPin;
uint8_t colPin;
uint8_t row;
uint8_t column;
};
La classe Key est définie par l'instruction struct (au lieu de class) car il s'agit d'un simple tuple pour lesquels tous les attributs sont publiques. Il y a autant d'instances de cette classe que de touches sur le clavier.
- status, de type KeyStatus, est l'état de la touche dans l'automate.
- value, de type char, est la valeur de la touche, c'est à dire le caractère correspondant à la touche appuyée.
- millis0, de type uint32_t, est l'instant du passage de l'état KEY_IDLE à l'état KEY_PRESSED (ou de l'état KEY_HOLD à l'état KEY_RELEASED) pour compter les 10 ms.
- rowPin est le numéro de pin de l'Arduino pour les lignes à laquelle la touche est connectée.
- colPin est le numéro de pin de l'Arduino pour les colonnes à laquelle la touche est connectée.
- row est l'index de la ligne de la touche.
- column est l'index de la colonne de la touche.
Classe KeypadEvent
La classe KeypadEvent définit l'événement émis par le clavier lorsqu'un utilisateur appui ou relâche une touche.
/* 1 */
class KeypadEvent : public Event<MatrixKeypad> {
/* 2 */
Key& __key;
public:
/* 3 */
KeypadEvent(MatrixKeypad* keypad, Key& key) : Event(keypad), __key(key) { }
/* 4 */
const Key& getKey() { return this->__key; }
};
class KeypadEvent : public Event<MatrixKeypad> {
/* 2 */
Key& __key;
public:
/* 3 */
KeypadEvent(MatrixKeypad* keypad, Key& key) : Event(keypad), __key(key) { }
/* 4 */
const Key& getKey() { return this->__key; }
};
- La classe KeypadEvent hérite de la classe Event de la bibliothèque Event. Cette classe est une classe paramétrable (classe template en C++). Ici, elle est paramétrée pour que les événement soient émis par la classe MatrixKeypad.
- La référence de la touche à l'origine de l'événement est reçue par le constructeur de la classe et rangé dans l'attribut privé __key.
- Le constructeur de la classe KeypadEvent reçoit en paramètre la référence de la touche manipulée par l'utilisateur à l'origine de l'événement. Il invoque le constructeur de la classe parente (Event) et initialise l'attribut privé __key avec la référence de la touche reçue en paramètre.
- L'attribut privé __key est exposé en lecture seule par la méthode getKey(). Le modifieur const indique que, bien que le résultat de la méthode getKey() revoie une référence, les attributs de celle-ci ne sont pas modifiables par le programme qui l'invoque. Effectivement les attributs du tuple Key sont déterminés par l'automate anti-rebonds. Et modifier de l'extérieur cet attribut perturbe nécessairement cet automate ce qui risque de déclencher une suite d'événement incohérents.
Interface KeypadListener
L'interface KeypadListener doit être dérivée par toutes les classes chargées de la réception des événement KeypadEvent. Ces dernières peuvent implémenter divers trois handlers pour effectuer à réception le traitement approprié. Elle doivent s'enregistrer auprès du clavier émetteur en invoquant la méthode addListener() de celui-ci. Les trois handlers sont implémentés vides pour ne pas forcer le récepteur à les implémenter tous les trois alors que seul l'un d'entre eux est utile.
class KeypadListener : public EventListener<KeypadEvent> {
protected:
virtual void receive(KeypadEvent*);
virtual void onKeyPressed(MatrixKeypad*, const Key&) { }
virtual void onKeyRelease(MatrixKeypad*, const Key&) { }
virtual void onChar(MatrixKeypad*, char) { }
};
protected:
virtual void receive(KeypadEvent*);
virtual void onKeyPressed(MatrixKeypad*, const Key&) { }
virtual void onKeyRelease(MatrixKeypad*, const Key&) { }
virtual void onChar(MatrixKeypad*, char) { }
};
- La méthode receive(), héritée de la classe EventListener, est invoquée par le clavier émetteur sur tous les récepteurs enregistrés pour signaler qu'une touche a été appuyée ou relâchée. Celle-ci interprète l'événement reçu en paramètre pour invoquer le handler approprié.
- Le handler onKeyPressed() est invoqué lorsqu'une touche est appuyée. Il transmet en paramètre un pointeur sur le clavier émetteur et la référence de la touche.
- Le handler onKeyRelease() est invoqué lorsqu'une touche est relâchée. Il transmet en paramètre un pointeur sur le clavier émetteur et la référence de la touche.
- Le handler onChar() est invoqué lorsqu'une touche est relâchée. Il transmet en paramètre un pointeur sur le clavier émetteur et le code du caractère correspondant de la touche.
La classe KeypadListener peut être dérivée pour fournir une autre interprétation de l'événement KeypadEvent en surchargeant la méthode receive() pour transmettre des codes différents par le handler onChar() par exemple lorsque les touches * ou # sont utilisées comme touches joker conjointement avec une autre touche
Classe MatrixKeypad
/* 1 */
class MatrixKeypad : public Component, public EventListenable<KeypadEvent>
{
/* 2 */
uint8_t __iRow, __iCol;
/* 3 */
Key* __keys;
protected:
/* 4 */
uint8_t _countRows, _countCols;
public:
/* 5 */
static const uint8_t DEBOUNCE_TIME = 10;
/* 6 */
MatrixKeypad(char *userKeymap, uint8_t *rowPins, uint8_t *colPins, uint8_t countRows, uint8_t countCols);
/* 7 */
~MatrixKeypad();
/* 8 */
virtual void begin();
/* 9 */
virtual void loop();
/* 10 */
const Key& getKey(uint8_t row, uint8_t col) { return this->_getKey(row, col); }
protected:
/* 10 */
Key& _getKey(uint8_t row, uint8_t col) { return __keys[row * _countCols + col]; }
/* 11 */
virtual void _pinMode(uint8_t pin, uint8_t mode);
virtual byte _pinRead(uint8_t pin);
virtual void _pinWrite(uint8_t pin, byte value);
private:
/* 12 */
void __scanKey(Key&);
};
class MatrixKeypad : public Component, public EventListenable<KeypadEvent>
{
/* 2 */
uint8_t __iRow, __iCol;
/* 3 */
Key* __keys;
protected:
/* 4 */
uint8_t _countRows, _countCols;
public:
/* 5 */
static const uint8_t DEBOUNCE_TIME = 10;
/* 6 */
MatrixKeypad(char *userKeymap, uint8_t *rowPins, uint8_t *colPins, uint8_t countRows, uint8_t countCols);
/* 7 */
~MatrixKeypad();
/* 8 */
virtual void begin();
/* 9 */
virtual void loop();
/* 10 */
const Key& getKey(uint8_t row, uint8_t col) { return this->_getKey(row, col); }
protected:
/* 10 */
Key& _getKey(uint8_t row, uint8_t col) { return __keys[row * _countCols + col]; }
/* 11 */
virtual void _pinMode(uint8_t pin, uint8_t mode);
virtual byte _pinRead(uint8_t pin);
virtual void _pinWrite(uint8_t pin, byte value);
private:
/* 12 */
void __scanKey(Key&);
};
- La classe MatrixKeypad hérite des classes suivantes :
- L'interface Component définit le polymorphisme de tous les composants programmés en C++. Cela oblige la classe MatrixKeypad à fournir une implémentation pour les méthode begin() et loop().
- L'interface EventListenable expose les méthodes dispatch(), qui permet à la classe MatrixKeypad d'envoyer des événements à tous les récepteurs enregistrés, et la méthode addListener() qui permet d'enregistrer des récepteurs.
- La méthode loop() lit l'état de chaque touche une à une par itération. Ce qui permet de ne prendre que très peu de temps machine à chaque itération. La classe MatrixKeypad a donc besoin de deux itérateurs __iRow et __iCol, pour respectivement indexer la ligne et le colonne de la touche à traiter lors de l'itération en cours.
- Un clavier est composé d'un nombre variable de touches. Le nombre de touches est passé en paramètre du constructeur de la classe MatrixKeypad. Un tableau de touches, identifié par l'attribut __keys, est construit dynamiquement à la construction en fonction des paramètres reçus à la construction de l'instance du clavier.
- Les attributs _countRows et _countCols contiennent respectivement le nombre de lignes et le nombre de colonnes qui structurent le clavier. Ces paramètres sont reçus à la construction de l'objet. Il sont déclarés protected pour pouvoir être utilisés dans les classes dérivées.
- Les claviers matriciels, sauf pour quelques versions haut de gamme, ne disposent pas de dispositif anti-rebonds électronique. La constante de classe DEBOUNCE_TIME contient le nombre de millisecondes pendant lesquelles la touche doit être maintenue pour que le changement d'état soit pris en compte. Cette constante est utilisée par l'automate anti-rebonds.
- Le constructeur de la classe MatrixKeypad reçoit les paramètres suivants :
- userKeymap est un tableau de caractères correspondant à la valeur de chaque touche. Sa taille doit être égale à countCols*countRows.
- rowPins est un tableau d'entiers contenant le numéro des pins de l'Arduino qui sont connecté au pin des lignes du clavier. Sa taille doit être égale à countRows.
- colPins est un tableau d'entiers contenant le numéro des pins de l'Arduino qui sont connecté au pin des colonnes du clavier. Sa taille doit être égale à countCols.
- countRows contient le nombre de ligne du clavier. Ce paramètre est rangé dans l''attribut _countRows.
- countCols contient le nombre de colonnes du clavier. Ce paramètre est rangé dans l'attribut _countCols.
- A partir de des paramètres reçus par le constructeur, un tableau d'objets Key est construit dynamiquement par l'opérateur new et identifié par l'attribut __keys. A la destruction, ce tableau doit être détruit par l'opérateur delete. C'est pourquoi, la classe MatrixKeypad doit implémenter un destructeur.
- La classe MatrixKeypad dérive l'interface Component. Elle doit donc fournir une implémentation de la méthode begin(). Ce qui permet de configurer en OUTPUT les pins de l'Arduino reliées aux pins des lignes et en INPUT les pins de l'Arduino reliées aux colonnes du clavier pour pouvoir détecter l'état de chaque touche.
- La classe MatrixKeypad dérive l'interface Component. Elle doit donc fournir une implémentation de la méthode loop(). A chaque itération, l'état de la touche dont la position est repérée par les itérateurs iRow et iCol est évalué pour être traité par l'automate anti-rebond pour déclencher éventuellement un événement KeypadEvent. Une seule touche est traitée à chaque itération pour ne pas charger la boucle loop() en temps machine.
- La classe MatrixKeypad dispose de deux méthodes pour repérer une touche par sa ligne et sa colonne :
- _getKey() (avec un souligné) renvoie en résultat la référence de la touche repérée par sa ligne et sa colonne passées en paramètres. Cette méthode est protégée pour ne pas pouvoir être utilisée de l'extérieur. En effet, les attributs de la référence sont modifiables et cette méthode est réservée à un usage interne.
- getKey() (sans souligné) renvoie en résultat, comme la précédente, la référence de la touche repérée par sa ligne et sa colonne passées en paramètres. Elle invoque celle-ci, mais contrairement à cette dernière, elle est déclarée publique. Mais pour ne pas pouvoir modifier de l'extérieur les attributs de la touche, ce qui risquerait de perturber l'automate anti-rebonds, le résultat est déclaré const Key&.
- Ces trois méthodes effectuent les opérations de bas niveau. Elle sont déclarée protected pour qu'elles puissent être surchargées dans des classes dérivées prenant en charge des connexions différentes passant par des interfaces comme le PCF8574. Par défaut, elles invoque les fonctions standard assumant que le clavier est connecté directement sur les pins de l'Arduino.
- _pinMode() permet d'initialiser le sens de fonctionnement OUTPUT ou INPUT de la pin de connexion passée en paramètre. Par défaut, elle invoque la fonction standard pinMode() de l'Arduino.
- _pinWrite() permet d'écrire une valeur HIGH ou LOW sur la pin passée en paramètre. Par défaut, elle invoque la fonction standard digitalWrite() de l'Arduino.
- _pinRead() permet de lire l'état de la pin passée en paramètre. Par défaut, elle invoque la fonction standard digitalRead() de l'Arduino.
- La méthode privée __scanKey() évalue l'état de la touche passée en paramètre pour déclencher éventuellement un événement KeypadEvent. Elle est invoquée par la méthode loop() à chaque itération pour une touche différente. Elle contient le code de l'automate anti-rebonds.
Implémentations de la classe KeypadMatrix
Constructeur de la classe KeypadMatrix
MatrixKeypad::MatrixKeypad(char *userKeymap, uint8_t *rowPins, uint8_t *colPins, uint8_t countRows, uint8_t countCols) {
this->_countRows = countRows;
this->_countCols = countCols;
this->__keys = new Key[countRows * countCols];
this->__iRow = 0;
this->__iCol = 0;
for (int row = 0; row < countRows; row++) {
for (int col = 0; col < countCols; col++) {
this->_getKey(row, col).row = row;
this->_getKey(row, col).column = col;
this->_getKey(row, col).value = userKeymap[row * countCols + col];
this->_getKey(row, col).rowPin = rowPins[row];
this->_getKey(row, col).colPin = colPins[col];
this->_getKey(row, col).status = KEY_IDLE;
}
}
}
this->_countRows = countRows;
this->_countCols = countCols;
this->__keys = new Key[countRows * countCols];
this->__iRow = 0;
this->__iCol = 0;
for (int row = 0; row < countRows; row++) {
for (int col = 0; col < countCols; col++) {
this->_getKey(row, col).row = row;
this->_getKey(row, col).column = col;
this->_getKey(row, col).value = userKeymap[row * countCols + col];
this->_getKey(row, col).rowPin = rowPins[row];
this->_getKey(row, col).colPin = colPins[col];
this->_getKey(row, col).status = KEY_IDLE;
}
}
}
- Les attributs _countRows et _countCols sont respectivement initialisés avec les paramètres countRows et countCols.
- Un tableau __keys d'instances de la classe Key est initialisé dynamiquement avec l'opérateur new pour une tailles countRows*countCols.
- Les itérateurs __iRow et __iCol sont initialisés à 0.
- Pour chaque touche instanciée dans le tableau __keys, les attributs du tuples sont initialisés à partir des paramètres reçus par le constructeur :
- l'attribut row de la touche par la ligne en cours,
- l'attribut column de la touche par la colonne en cours,
- l'attribut value de la touche par le caractère du rang correspondant dans le tableau de valeur userKeymap reçu en paramètre,
- l'attribut rowPin à partir du numéro de pin du numéro de ligne du rang correspondant issus du tableau rowPins passé en paramètre,
- l'attribut colPin à partir du numéro de pin du numéro de colonne du rang correspondant issus du tableau colPins passé en paramètre,
- l'attribut status initialisé par l'état repos de l'automate KEY_IDLE.
Destructeur de la classe KeypadMatrix
MatrixKeypad::~MatrixKeypad() {
delete [] this->__keys;
}
delete [] this->__keys;
}
- Comme le tableau d'instances de Key identifié par l'attribut __keys est créé dynamiquement par l'opérateur new dans le constructeur, il doit être explicitement détruit par l'opérateur delete [] dans le destructeur de la classe KeypadMatrix.
La méthode begin()
void MatrixKeypad::begin() {
for (int row = 0; row < this->_countRows; row++) {
this->_pinMode(this->_getKey(row, 0).rowPin, OUTPUT);
this->_pinWrite(this->_getKey(row, 0).rowPin, HIGH);
}
for (int col = 0; col < this->_countCols; col++) {
this->_pinMode(this->_getKey(0, col).colPin, INPUT_PULLUP);
}
}
for (int row = 0; row < this->_countRows; row++) {
this->_pinMode(this->_getKey(row, 0).rowPin, OUTPUT);
this->_pinWrite(this->_getKey(row, 0).rowPin, HIGH);
}
for (int col = 0; col < this->_countCols; col++) {
this->_pinMode(this->_getKey(0, col).colPin, INPUT_PULLUP);
}
}
- Pour chaque ligne, la pin est configurée en sortie OUTPUT et initialisée à HIGH pour qu'aucune tension ne puisse être détectée sur les pin en entrée par défaut. Le numéro de la pin sont extrait des touches de la colonne 0, car toutes les touches d'une même ligne sont connectées à la même pin sur l'Arduino.
- Pour chaque colonne, la pin est configurée en entrée INPUT_PULLUP. Elle est automatiquement positionnée sur HIGH. Le numéro de la pin est extrait des touches de ligne 0 car toutes les touche d'une même colonne sont connectées à la même pin sur l'Arduino.
- A l'issue de la méthode begin(), toutes les pins sont positionnées à l'état HIGH.
La méthode loop()
void MatrixKeypad::loop() {
Key& key = this->_getKey(this->__iRow, this->__iCol);
this->__scanKey(key);
this->__iCol++;
if (this->__iCol >= this->_countCols) {
this->__iCol = 0;
this->__iRow++;
}
if (this->__iRow >= this->_countRows) {
this->__iRow = 0;
}
}
Key& key = this->_getKey(this->__iRow, this->__iCol);
this->__scanKey(key);
this->__iCol++;
if (this->__iCol >= this->_countCols) {
this->__iCol = 0;
this->__iRow++;
}
if (this->__iRow >= this->_countRows) {
this->__iRow = 0;
}
}
- A chaque itération uniquement la touche de rang (iRow, iCol) est traitée pour ne pas charger la boucle loop() en temps machine.
- La touche de rang (iRow, iCol), retrouvée par l'invocation de la méthode _getKey() et rangée dans une variable locale key.
- Puis la méthode privée __scanKey() est invoquée pour la touche key. Cette méthode exécute l'automate anti-rebonds sur la touche key passée en paramètre. Ce qui déclenche éventuellement un événement KeypadEvent si l'état stable de la touche a changé.
- L'itérateur de colonne iCol est incrémenté.
- Si l'itérateur de colonne iCol dépasse le nombre de colonne countCols, il repasse à 0 et l'itérateur de ligne iRow est à son tour incrémenté.
- Si l'itérateur de ligne iRow dépasse le nombre de ligne, il repasse à 0.
La méthode privée __scanKey()
La méthode __scanKey() exécute l'automate dont le graphe est présenté ci-dessus sur la touche key passée en paramètre.
void MatrixKeypad::__scanKey(Key& key) {
this->_pinWrite(key.rowPin, LOW);
uint8_t keyValue = this->_pinRead(key.colPin);
if (keyValue == LOW) {
switch (key.status) {
case KEY_IDLE:
key.status = KEY_PRESSED;
key.millis0 = millis();
break;
case KEY_PRESSED:
if ((millis() - key.millis0) > DEBOUNCE_TIME) {
this->_dispatch(new KeypadEvent(this, key));
key.status = KEY_HOLD;
}
break;
case KEY_HOLD:
break;
case KEY_RELEASED:
key.status = KEY_HOLD;
break;
}
} else {
switch (key.status) {
case KEY_IDLE:
break;
case KEY_PRESSED:
key.status = KEY_IDLE;
break;
case KEY_HOLD:
key.status = KEY_RELEASED;
key.millis0 = millis();
break;
case KEY_RELEASED:
if ((millis() - key.millis0) > DEBOUNCE_TIME) {
this->_dispatch(new KeypadEvent(this, key));
key.status = KEY_IDLE;
}
break;
}
}
this->_pinWrite(key.rowPin, HIGH);
}
this->_pinWrite(key.rowPin, LOW);
uint8_t keyValue = this->_pinRead(key.colPin);
if (keyValue == LOW) {
switch (key.status) {
case KEY_IDLE:
key.status = KEY_PRESSED;
key.millis0 = millis();
break;
case KEY_PRESSED:
if ((millis() - key.millis0) > DEBOUNCE_TIME) {
this->_dispatch(new KeypadEvent(this, key));
key.status = KEY_HOLD;
}
break;
case KEY_HOLD:
break;
case KEY_RELEASED:
key.status = KEY_HOLD;
break;
}
} else {
switch (key.status) {
case KEY_IDLE:
break;
case KEY_PRESSED:
key.status = KEY_IDLE;
break;
case KEY_HOLD:
key.status = KEY_RELEASED;
key.millis0 = millis();
break;
case KEY_RELEASED:
if ((millis() - key.millis0) > DEBOUNCE_TIME) {
this->_dispatch(new KeypadEvent(this, key));
key.status = KEY_IDLE;
}
break;
}
}
this->_pinWrite(key.rowPin, HIGH);
}
- La pin de la ligne de la touche (configurée OUTPUT) est initialisée à LOW (alors qu'elle est à HIGH par défaut) en invoquant la méthode _pinWrite().
- L'état de la pin de la colonne de la touche (configurée INPUT_PULLUP) est alors évalué en invoquant la méthode _pinRead().
- Si l'état de la pin de la colonne est LOW, c'est que la touche est enfoncée. Alors tout dépend de l'état de l'automate propre à la touche...
- Si l'état était KEY_IDLE c'est que la touche a été appuyée depuis la dernière évaluation. Dans ce cas, l'automate passe dans l'état intermédiaire KEY_PRESSED et l'instant topé par la fonction millis() est mémorisé dans l'attribut millis0 de la touche. La touche doit être maintenue au moins pendant DEBOUNCE_TIME millisecondes pour pouvoir passer à l'état KEY_HOLD.
- Si l'état était KEY_PRESSED et que cet état a été maintenu pendant au moins DEBOUNCE_TIME millisecondes, alors l'automate peut passe à l'état KEY_HOLD.
Un événement KeypadEvent est alors émis à destination des récepteurs enregistrés par l'invocation de la méthode _dispatch() héritée de la classe EventListenable. Le tuple décrivant l'état complet de la touche et passé en paramètre de l'événement. - Si l'état était KEY_HOLD, la touche se trouve dans un état stable enfoncée. Rien ne se passe.
- Si l'état était KEY_RELEASED, c'est que la touche n'a pas été maintenue relâchée assez longtemps. Probablement s'agit-il d'un rebond. L'automate reprend donc son état stable précédent KEY_HOLD, considérant que la touche est toujours enfoncée.
- Si l'état de la pin de la colonne est HIGH, c'est que la touche est relâchée. Là encore, tout dépend de l'état de l'automate propre à la touche...
- Si l'état était KEY_IDLE, la touche se trouve dans un état stable relâchée. Rien ne se passe.
- Si l'état était KEY_PRESSED, c'est que la touche n'a pas été maintenue appuyée assez longtemps. Probablement à cause d'un rebond. L'automate reprend donc son état stable précédent KEY_IDLE, considérant que la touche est toujours relâchée.
- Si l'état était KEY_HOLD, c'est que la touche a été relâchée depuis la dernière évaluation. Dans ce cas l'automate passe dans l'état intermédiaire KEY_RELEASED et l'instant topé par la fonction millis() est mémorisé dans l'attribut millis0 de la touche. La touche doit être maintenue au moins pendant DEBOUNCE_TIME millisecondes pour pouvoir passer à l'état KEY_IDLE.
- Si l'état était KEY_RELEASED et que cet état a été maintenu pendant au moins DEBOUNCE_TIME millisecondes, alors l'automate peut passe à l'état KEY_IDLE.
Un événement KeypadEvent est alors émis à destination des récepteurs enregistrés par l'invocation de la méthode _dispatch() héritée de la classe EventListenable. Le tuple décrivant l'état complet de la touche et passé en paramètre de l'événement. - Pour finir, afin de ne pas créer de confusion avec la touche traitée à l'itération suivante, la ligne de pin de la touche, initialisée à LOW au début de cette itération, est rétablie dans son état initial HIGH en invoquant à nouveau la méthode _pinWrite().
La méthode _pinMode()
La méthode _pinMode() de la classe MatrixKeypad initialise le mode de fonctionnement entrée ou sortie de la pin passée en paramètre. Par défaut, c'est la fonction standard pinMode() qui est invoquée, assumant que le clavier est connecté directement sur les pins de l'Arduino. Mais elle peut être surchargée dans une classe dérivée pour initialiser un autre mode de connexion.
void MatrixKeypad::_pinMode(uint8_t pin, uint8_t mode) {
pinMode(pin, mode);
}
pinMode(pin, mode);
}
La méthode _pinRead()
La méthode _pinRead() de la classe MatrixKeypad évalue l'état HIGH ou LOW de la pin passée en paramètre. Par défaut, c'est la fonction standard digitalRead() qui est invoquée, assumant que le clavier est connecté directement sur les pins de l'Arduino. Mais elle peut être surchargée dans une classe dérivée pour fonctionner dans un autre mode de connexion.
byte MatrixKeypad::_pinRead(uint8_t pin) {
return digitalRead(pin);
}
return digitalRead(pin);
}
La méthode _pinWrite()
La méthode _pinWrite() de la classe MatrixKeypad modifie l'état de la pin passée en paramètre. Par défaut, c'est la fonction standard digitalWrite() qui est invoquée, assumant que le clavier est connecté directement sur les pins de l'Arduino. Mais elle peut être surchargée dans une classe dérivée pour fonctionner dans un autre mode de connexion.
void MatrixKeypad::_pinWrite(uint8_t pin, byte value) {
digitalWrite(pin, value);
}
digitalWrite(pin, value);
}
La méthode receive() de l'interface KeypadListener
La méthode receive() de l'interface KeypadListener est invoquée sur chaque récepteur enregistré lors de l’exécution de la méthode _dispatch(), à l'émission d'un événement KeypadEvent. Afin de faciliter la prise en charge de cet événement, cette méthode évalue l'état de la touche reçue en paramètre pour déclencher l'un des trois handlers d'événement onKeyReleased(), onKeyPressed() et onChar(). Ces trois handlers peuvent être surchargés dans la programmation des récepteurs pour effectuer le traitement adéquat.
void KeypadListener::receive(KeypadEvent* event) {
const Key& key = event->getKey();
switch (key.status) {
case KEY_RELEASED:
onKeyReleased(event->getSender(), key);
onChar(event->getSender(), key.value);
break;
case KEY_PRESSED:
onKeyPressed(event->getSender(), key);
break;
}
}
const Key& key = event->getKey();
switch (key.status) {
case KEY_RELEASED:
onKeyReleased(event->getSender(), key);
onChar(event->getSender(), key.value);
break;
case KEY_PRESSED:
onKeyPressed(event->getSender(), key);
break;
}
}
- La touche key est extraite de l'événement event (de classe KeypadEvent). Lorsque cet événement est émis, la touche ne peut être que dans deux états (voir code de la méthode __scanKey() ci-dessus).
- Si l'état de la touche est KEY_RELEASED, c'est qu'une touche vient d'être relâchée dans un état stable. Le handler onKeyReleased() est alors invoqué pour signaler le changement d'état de la touche aux récepteurs enregistrés. Le handler onChar() est également invoqué pour communiquer le code de la touche tapée.
- Si l'état de la touche est KEY_PRESSED, c'est qu'une touche vient d'être appuyée dans un état stable. Le handler onKeyPressed() est alors invoqué pour signaler le changement d'état de la touche aux récepteurs enregistrés.
Il faut remarquer ici que le code du caractère de la touche n'est communiquée aux récepteurs que lorsque celle-ci est relâchée et non lorsqu'elle est appuyée. D'autre Listeners peuvent être programmés, sans avoir à modifier les autres classes pour prendre en charge un autre fonctionnement comme la répétition des caractères lorsqu'une touche est maintenue enfoncée pendant un certain temps, par exemple. Ceci est possible grâce au découplage assuré par le Design Pattern Observer modélisant les classes de la bibliothèque Event.
Utilisation de la classe MatrixKeypad
La classe MatrixKeypad peut être utilisée comme n'importe quel émetteur d'événement soit directement dans un sketch Arduino, soit à l'intérieur d'un composite. Se référer à l'article sur le Timer pour consulter des exemples des ces deux utilisations.
Dans l'exemple ci-dessous, la classe MatrixKeypad est utilisée directement dans un sketch. Ce qui oblige d'instancier la classe abstraite du listener dans une classe anonyme C++. Le caractère des touches tapées sur le clavier est affiché sur le canal Série de l'Arduino.
Dans l'exemple ci-dessous, la classe MatrixKeypad est utilisée directement dans un sketch. Ce qui oblige d'instancier la classe abstraite du listener dans une classe anonyme C++. Le caractère des touches tapées sur le clavier est affiché sur le canal Série de l'Arduino.
/* 1 */
#include "MatrixKeypad.h"
/* 2 */
const byte ROWS = 4; // 4 lignes
const byte COLS = 4; // 4 colonnes
/* 3 */
char keyValues[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'},
};
/* 4 */
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
/* 5 */
struct : public KeypadListener {
void onChar(MatrixKeypad* keypad, char value) {
Serial.println(value);
}
} keypadListener;
/* 6 */
MatrixKeypad keyboard(makeKeymap(keyValues), rowPins, colPins, ROWS, COLS);
/* 7 */
void setup() {
Serial.begin(9600);
keyboard.addListener(&keypadListener);
keyboard.begin();
}
/* 8 */
void loop() {
keyboard.loop();
}
#include "MatrixKeypad.h"
/* 2 */
const byte ROWS = 4; // 4 lignes
const byte COLS = 4; // 4 colonnes
/* 3 */
char keyValues[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'},
};
/* 4 */
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
/* 5 */
struct : public KeypadListener {
void onChar(MatrixKeypad* keypad, char value) {
Serial.println(value);
}
} keypadListener;
/* 6 */
MatrixKeypad keyboard(makeKeymap(keyValues), rowPins, colPins, ROWS, COLS);
/* 7 */
void setup() {
Serial.begin(9600);
keyboard.addListener(&keypadListener);
keyboard.begin();
}
/* 8 */
void loop() {
keyboard.loop();
}
- En premier, il faut inclure la définition des classes de la bibliothèque MatrixKeypad.
- Le nombre de lignes et de colonnes, respectivement ROWS et COLS, sont initialisé, en tant que constante, à 4 pour le clavier de 16 touches utilisé dans l'exemple.
- Le tableau keyValues est initialisé avec le code des touches du clavier. Ce tableau est transmis comme premier paramètre du constructeur de la classe MatrixKeypad.
- Deux tableaux contiennent les numéros de pins de l'Arduino mobilisées par le clavier :
- rowPins contient les quatre pins pour les lignes du clavier. Ces quatre pins sont configurées en écriture (OUTPUT).
- colPins contient les quatre pins pour les colonnes du clavier. Ces quatre pins sont configurées en lecture (INPUT_PULLUP).
- L'interface KeypadListener est un classe abstraite comportant plusieurs méthodes pour les handlers. Ces méthodes y sont implémentées vide. Autrement dit, par défaut elles ne font rien. S'il faut leur donner un rôle spécifique, il est nécessaire de dériver l'interface dans une classe anonyme C++ dans laquelle la méthode du handler souhaité est surchargée.
Dans cet exemple, seul le handler onChar() est surchargé pour afficher le caractère de la touche tapée sur le canal Série de l'Arduino. Le listener ainsi instancié est affecté à la variable keypadListener. - Une instance keyboard de la classe MatrixKeypad est instanciée. Les paramètres suivants sont communiqué au constructeur de la classe pour initialiser l'instance :
- Le premier paramètre est le tableau keyValues contenant le code des touches. En C++ (comme en C) les tableaux sont identifiés par l'adresse de leur premier membre. keyValues est donc un pointeur, mais un pointeur sur un tableau donc un pointeur sur un pointeur. Or le premier paramètre du constructeur est déclaré char*. keyValues étant un tableau de char*, les deux types sont normalement compatibles. Pour éviter un message désagréable du compilateur, la macro ci-dessous, définie dans MatrixKeypad.h assure la conversion :
#define makeKeymap(map) ((char *)map) - Le second est rowPins, le tableau des numéros de pins connectées aux lignes du clavier.
- Le troisième est colPins, le tableau des numéros de pins connectées aux colonnes du clavier.
Ces deux tableaux sont identifiés par l'adresse de leur premier membre. - Le quatrième et le cinquième paramètre sont respectivement le nombre de ligne et de colonnes du clavier.
- Dans la fonction setup(), après avoir initialiser la console Série de l'Arduino à 9600 bauds, le listener keypadListener est enregistré dans le clavier comme récepteur des événements KeypadEvent. Puis la méthode begin() du clavier est invoquée. Les pins de l'Arduino sont initialisées par pinMode() à ce moment là.
- Dans la fonction loop(), la méthode loop() du clavier est invoquée. C'est là que les automates respectifs des touches fonctionne pour émettre les événements KeypadEvent lorsque l'état d'une touche change.
Conclusion
La classe MatrixKeypad est une nouvelle application de la bibliothèque Event pour piloter un clavier matriciel. Elle expose également une solution logicielle (alternative au condensateur d'amortissement utilisé pour la classe Button) au problème des rebonds, par l'implémentation d'un automate anti-rebonds.
En ce qui la connexion du clavier à l'Arduino, il n'échappe à personne que celle-ci est particulièrement gourmande en pin numériques. En effet, il en faut une par ligne et une par colonne. Ce qui encore possible pour un clavier de 16 touches qui ne va en mobiliser que 8, va devenir problématique pour des claviers plus importants avec un micro-contrôleur qui n'en comporte que 12. Aussi la connexion en direct d'un clavier matriciel est-elle très exceptionnelle. On préférera passer par des circuits annexes, comme le PCF8574, qui fournit 8 entrées/sorties numériques supplémentaires en passant par le bus I2C, comme cela avait été décrit dans une article consacré au module LCD 1602, lui aussi très gourmand en pins numériques.
La connexion d'un clavier matriciel au bus I2C à travers un PCF8574 fera l'objet d'un prochain article. Pour piloter cette configuration, il suffira de dériver la classe MatrixKeypad pour surcharger les méthodes de bas niveau _pinMode(), _pinRead() et _pinWrite() à la lecture et l'écriture de l'état des pins du PCF8574 sur le bu I2C.
En ce qui la connexion du clavier à l'Arduino, il n'échappe à personne que celle-ci est particulièrement gourmande en pin numériques. En effet, il en faut une par ligne et une par colonne. Ce qui encore possible pour un clavier de 16 touches qui ne va en mobiliser que 8, va devenir problématique pour des claviers plus importants avec un micro-contrôleur qui n'en comporte que 12. Aussi la connexion en direct d'un clavier matriciel est-elle très exceptionnelle. On préférera passer par des circuits annexes, comme le PCF8574, qui fournit 8 entrées/sorties numériques supplémentaires en passant par le bus I2C, comme cela avait été décrit dans une article consacré au module LCD 1602, lui aussi très gourmand en pins numériques.
La connexion d'un clavier matriciel au bus I2C à travers un PCF8574 fera l'objet d'un prochain article. Pour piloter cette configuration, il suffira de dériver la classe MatrixKeypad pour surcharger les méthodes de bas niveau _pinMode(), _pinRead() et _pinWrite() à la lecture et l'écriture de l'état des pins du PCF8574 sur le bu I2C.
Commentaires
Enregistrer un commentaire