Brutpix
Brutpix est un projet initié par Ben Farey, constructeur et plasticien sonore du collectif Tricyclique Dol et Yves Petit, photographe. Le projet s'appuie sur le projet Brutbox, une série de logiciels et d'instruments numériques à destination des publics en situation de handicap mental. Brutpix est la version photographique de ce projet. Il sera constitué de différents éléments matériels:
- des appareils photographiques DIY solides, simples et peu coûteux, basés sur des Raspberry Pi
- un système automatique d'affichage en direct des images capturées par les appareils
- une télécommande autonome, simple, qui permettra de traiter en temps réél les images affichées
Télécommande
Description technique: il s'agit de réaliser une télécommande à 5 potentiomètres infinis (encodeurs), et si possible deux boutons poussoirs. Cette télécommande devra être autonome, sans fils, et communiquer ses donnés en wifi à la appareil chargé d'afficher les photos prises.
Étape 1: premiers tests et déconvenues
Le choix technique s'est porté sur des ESP8266, carte de type Wemos. Nous avons l'habitude de l'utiliser, elle est peu coûteuse et d'un faible encombrement, il y a en théorie (nous verrons plus loin pourquoi le “en théorie”) 11 broches GPIO. Les encodeurs sont de type “Encodeur 24 impulsions EC12E24204A9, marque ALPS (environ 1.80 euro pièce)”.
Dessin de la carte électronique du prototype sous Kicad, usinage sur la CNC du fablab. Après nettoyage des pistes (la gravure n'est jamais parfaite), puis soudure des composants, la programmation peut commencer (et les ennuis).
Problème n°1: dans un souci de bien faire, la broche RST a été mise à la masse pour éviter de la laisser en l'air. Erreur. C'est quand elle est à la masse qu'elle redémarre l'ESP. Un coup de pince coupante, problème résolu.
Problèmes n°2: pour utiliser les 11 GPIO disponibles, il faut détourner certaines broches de leur usage. Pour référence, nous nous sommes appuyés sur le tableau des affectations de broche de cette page. On y voit que les broches RX et TX peuvent devenir respectivement les GPIO 1 et 3. Certes, on perdra le bénéfice du debug série, mais ces deux broches supplémentaires nous sont nécessaires. Selon cette page, nous pouvons activer les différents modes des broches avec cette commande: ``` pinMode(pin, FUNCTION1) ```
Pour changer l'utilisation de RX et TX, il nous faudra mettre dans le setup: ``` pinMode(3, FUNCTION_3); pinMode(3, INPUT); pinMode(1, FUNCTION_3); pinMode(1, INPUT); ``` Après essai, cela fonctionne, problème 2 résolu.
Problème n°3: celui était inattendu. En effet, à la lecture du tableau cité plus haut, nous pouvons nous apercevoir que certaines broches ont des comportements particuliers au boot et à l'upload, D3, D4 et D8 . Mauvaise surprise puisque ces broches sont notées comme les autres, nous pensions les utiliser pour la lecture de nos encodeurs. La lecture de cet article ne nous rassure pas, et en effet, difficile de faire fonctionner l'esp, sauf à le brancher sur la carte APRÈS son démarrage. Très peu pratique ! Qu'à cela ne tienne, le prochain prototype utilisera le composant cité dans l'article précédent, un MCP23017. Problème … pas encore résolu!
Néanmoins, nous avons 3 encodeurs sur 5 qui sont prêts à être programmés.
Étape 2: code pour lire les encodeurs
Le fonctionnement des encodeurs à quadrature est abondamment documenté sur le net, mais je vais malgré cela tenter d'en illustrer le fonctionnement. En réalité, c'est surtout le prétexte d'utiliser l'Adalm2000 que le Père Noël nous as apporté cette année. Voyons d'abord comment se comportent ces encodeurs: ADALM 2000, d'Analog Devices .
Maintenant que nous avons bien saisi le fonctionnement électronique de ce composant, nous allons pouvoir rédiger un programme pour lire le sens de rotation. Notre base sera le code proposé sur https://bildr.org/2012/08/rotary-encoder-arduino/ . Il nécessite néanmoins quelques adaptations pour fonctionner sur un ESP. Nous expliciterons ensuite certains aspects de son fonctionnement.
``` Les broches de l'encodeur int encoderPin1 = D5; int encoderPin2 = D6; volatile int lastEncoded = 0; volatile long encoderValue = 0; long lastencoderValue = 0; int lastMSB = 0; int lastLSB = 0; void setup() { Serial.begin (9600); pinMode(encoderPin1, INPUT); pinMode(encoderPin2, INPUT); Commenté car les résistances pullup sont integrées au montage
//digitalWrite(encoderPin1, HIGH); //turn pullup resistor on //digitalWrite(encoderPin2, HIGH); //turn pullup resistor on
//Nous appelon updateEncoder() à chaque changement d'état d'une ou l'autre pin attachInterrupt(digitalPinToInterrupt(encoderPin1), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(encoderPin2), updateEncoder, CHANGE);
}
void loop() {
Serial.println(encoderValue); delay(1000); //Seulement pour ralentir la sortie et montrer que cela fonctionne même lorsque la loop est en pause
}
ICACHE_RAM_ATTR void updateEncoder() {
int MSB = digitalRead(encoderPin1); //MSB = most significant bit int LSB = digitalRead(encoderPin2); //LSB = least significant bit int encoded = (MSB << 1) | LSB; //converting the 2 pin value to single number int sum = (lastEncoded << 2) | encoded; //adding it to the previous encoded value //Serial.print(sum); if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue ++; if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue --; lastEncoded = encoded; //store this value for next time
} ```
Utiliser les interruptions
Le programme ci-dessus utilise des interruptions. Les interruptions externes permettent d'exécuter un petit morceau de code lors d'un événement précis sur une broche. Leur particularité est, comme leur nom l'indique, de pouvoir intervenir à n'importe quel moment du programme, interrompant l'action en cours. Cela garantit le fait ne pas rater d'événement sur cette broche précise. Nous nous servons donc de ces interruptions pour analyser la direction de l'encodeur à chaque fois que l'un des crans change d'état.
Il faut d'abord déclarer nos broches comme des entrées: ``` pinMode(encoderPin1, INPUT); pinMode(encoderPin2, INPUT); ``` puis les lier à une interruption. il faudra également définir quelle fonction sera appelée à chaque fois qu'un événement sera détecté (ici *updateEncoder*), et choisir le type d'événement (soit *CHANGE*, un changement d'état, soit *RISING*, de *LOW* vers *HIGH*, soit *FALLING*, de *HIGH* vers *LOW*). ``` attachInterrupt(digitalPinToInterrupt(encoderPin1), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(encoderPin2), updateEncoder, CHANGE);
``` et enfin, créer la fonction qui sera effectuée à chaque interruption. Celle-ci doit rester la plus brève possible, pour ne pas ralentir le programme. L'horrible ICACHE_RAM_ATTR qui précède la fonction en question est nécessaire uniquement sur ESP. Quand au contenu de cette fonction, non moins obscur, il est parfaitement explicité ici: https://bildr.org/2012/08/rotary-encoder-arduino/ . En résumé, il s'agit de considérer l'état de chaque broche comme une paire de 0 ou de 1. Soit il constitue une paire 00, soit 01, soit 10, soit 11. Le reste consiste en des opérations binaires pour comparer l'état précédent à l'état actuel, et en déduire le sens de rotation. ``` ICACHE_RAM_ATTR void updateEncoder() {
int MSB = digitalRead(encoderPin1); //MSB = most significant bit int LSB = digitalRead(encoderPin2); //LSB = least significant bit int encoded = (MSB << 1) | LSB; //converting the 2 pin value to single number int sum = (lastEncoded << 2) | encoded; //adding it to the previous encoded value //Serial.print(sum); if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue ++; if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue --; lastEncoded = encoded; //store this value for next time
} ```
NB: Les variables globales que nous voudrons manipuler à l'intérieur de la fonction appelée *updateEncoder()* sont des variables *volatile*
Étape 3: deuxième prototype
Le deuxième prototype utilisera donc un multiplexeur, et pourquoi pas, peut être, quelques condensateurs pour lisser les rebonds lors de la lecture, comme on peut le lire ici ou là. Tests à réaliser avec cette librairie.
Notes:
https://cdn-shop.adafruit.com/datasheets/mcp23017.pdf https://medium.com/@wilko.vehreke/more-gpios-for-the-esp8266-with-the-mcp23017-b89f5e15cde3
Voici les tests réalisés avec les composants reçus récemment. La puce MCP23017 fonctionne en i2c, et peut transmettre via cette connexion l'état de 16 entrées/sorties à un instant T. Sachant que plusieurs puces peuvent être connectées en série, cela ouvre de jolies perspectives de multiplexage. L'autre intérêt majeur de cette puce est que l'on peut déclencher la lecture des pattes en détectant une interruption. Ainsi, nous allons pouvoir mimer le comportement du code précédent et utiliser cette routine qui a prouvé son efficacité.
Schéma à venir.
Voici le code qui permet de tester le fonctionnement de ce multiplexeur. À noter, une info très importante et particulièrement étonnante: l'une des entrées est défectueuse, ce problème semble reconnu par le fabricant. Il ne faut donc pas utiliser l'entrée B7. L'information a été trouvée ici: https://forum.arduino.cc/index.php?topic=437248.0, merci les forums.
Voici ce que dit le fabricant:
``` On MCP23008/MCP23017 SDA line change when GPIO7 input change Mar 4, 2017•Knowledge Title On MCP23008/MCP23017 SDA line change when GPIO7 input change Article URL https://microchipsupport.force.com/s/article/On-MCP23008-MCP23017-SDA-line-change-when-GPIO7-input-change Question On MCP23008 device, if the GPIO7 input changes, or on MCP23017 if GPIOA7 or GPIOB7 input changes while the I2C master is reading this bit from the GPIO register, the SDA signal can change and look like a STOP condition on the bus. Answer The solution is to use a different pin as input, no other workaround available now. ``` Bref, on évite les pin 7. Un maker averti en vaux deux, et nous avions prévu de mettre des borniers sur les entrées non utilisées du MCP23017. Le nouveau routage du prototype est donc assez facile. En revanche, vous remarquerez que l'ajout de la carte de gestion de la batterie est un échec cuisant, puisque sa prise USB est pratiquement inaccessible. Une erreur qui justifie à elle seule le mot de prototype. Dommage de ne pas avoir poussé la simulation 3D des composants (mais cela nous servira de leçon).
On notera également l'ajout d'un interrupteur et d'une LED, qui nous permettra de donner quelques informations quant à l'état de la connexion wifi.
Le code ci-dessous est une adaptation finalement assez simple de celui qui précède, le stockage des valeurs des 5 encodeurs se fait dans un tableau, qui sera réutilisé ensuite pour l'affichage des valeurs.
``` #include <Wire.h> #include <Adafruit_MCP23017.h> Adafruit_MCP23017 mcp; byte arduinoIntPinA = 14; byte arduinoIntPinB = 12; Les broches de l'encodeur Rot1 byte mcpPin1A = 6; byte mcpPin1B = 7; Rot2 byte mcpPin2A = 4; byte mcpPin2B = 5; Rot3 byte mcpPin3A = 2; byte mcpPin3B = 3; Rot4 byte mcpPin4A = 0; byte mcpPin4B = 1; Rot5 byte mcpPin5A = 14; byte mcpPin5B = 11; There is an issue with thin pin, don't use GPB7, microchip is aware of it !!!! https://forum.arduino.cc/index.php?topic=437248.0 https://www.raspberrypi.org/forums/viewtopic.php?f=44&t=91209&sid=f8679026e1943ee9b7d05e15f8b36f02&start=25 byte mcpPins[10] = {mcpPin1A, mcpPin1B, mcpPin2A, mcpPin2B, mcpPin3A, mcpPin3B, mcpPin4A, mcpPin4B, mcpPin5A, mcpPin5B}; int encoded[5]; volatile int lastEncoded[5]; volatile long encoderValue[5]; long lastencoderValue[5]; int MSB[5]; int LSB[5]; int lastMSB[5]; int lastLSB[5]; uint16_t allGPIO; 2 SWITCHES Rot5 byte sw1 = 12; byte sw2 = 13; boolean sw1Pressed, sw2Pressed; long lastPrint = 0; int delayPrint = 50; void setup() { Serial.begin(115200); Serial.println(“MCP23007 Interrupt Test”); pinMode(arduinoIntPinA, INPUT); pinMode(arduinoIntPinB, INPUT); mcp.begin(); use default address 0
mcp.pinMode(mcpPin1A, INPUT); mcp.pinMode(mcpPin1B, INPUT); mcp.pinMode(mcpPin2A, INPUT); mcp.pinMode(mcpPin2A, INPUT); mcp.pinMode(mcpPin3A, INPUT); mcp.pinMode(mcpPin3A, INPUT); mcp.pinMode(mcpPin4A, INPUT); mcp.pinMode(mcpPin4A, INPUT); mcp.pinMode(mcpPin5A, INPUT); mcp.pinMode(mcpPin5A, INPUT); //SW mcp.pinMode(sw1, INPUT); mcp.pinMode(sw2, INPUT); //other pins mcp.pinMode(8, INPUT); mcp.pinMode(9, INPUT); mcp.pinMode(10, INPUT); mcp.pinMode(15, INPUT); mcp.pullUp(8, INPUT); mcp.pullUp(9, INPUT); mcp.pullUp(10, INPUT); mcp.pullUp(15, INPUT); mcp.setupInterrupts(false, false, LOW); // Mirroring (first param) must be set to false, may cause crashes when turning multiples buttons //SW1 mcp.setupInterruptPin(mcpPin1A, CHANGE); mcp.setupInterruptPin(mcpPin1B, CHANGE); //SW2 mcp.setupInterruptPin(mcpPin2A, CHANGE); mcp.setupInterruptPin(mcpPin2B, CHANGE); //SW3 mcp.setupInterruptPin(mcpPin3A, CHANGE); mcp.setupInterruptPin(mcpPin3B, CHANGE); //SW4 mcp.setupInterruptPin(mcpPin4A, CHANGE); mcp.setupInterruptPin(mcpPin4B, CHANGE); //SW5 mcp.setupInterruptPin(mcpPin5A, CHANGE); mcp.setupInterruptPin(mcpPin5B, CHANGE); //BTN1/2 mcp.setupInterruptPin(sw1, CHANGE); mcp.setupInterruptPin(sw2, CHANGE); mcp.readGPIOAB(); attachInterrupt(digitalPinToInterrupt(arduinoIntPinA), updateEncoderA, FALLING); attachInterrupt(digitalPinToInterrupt(arduinoIntPinB), updateEncoderB, FALLING);
} ICACHE_RAM_ATTR void updateEncoderA() {
readGPIO_MCP();
} ICACHE_RAM_ATTR void updateEncoderB() {
readGPIO_MCP();
} ICACHE_RAM_ATTR void readGPIO_MCP() {
// on lit toutes les entrées d'un coup, // Reads all 16 pins (port A and B) into a single 16 bits variable. allGPIO = mcp.readGPIOAB(); // On utilise bitRead pour récupérer la valeur pour chaque bouton //SW5 MSB[4] = bitRead(allGPIO, 11); LSB[4] = bitRead(allGPIO, 14); //SW4 MSB[3] = bitRead(allGPIO, 0); LSB[3] = bitRead(allGPIO, 1); //SW3 MSB[2] = bitRead(allGPIO, 2); LSB[2] = bitRead(allGPIO, 3); //SW2 MSB[1] = bitRead(allGPIO, 4); LSB[1] = bitRead(allGPIO, 5); //SW1 MSB[0] = bitRead(allGPIO, 6); LSB[0] = bitRead(allGPIO, 7); //btn sw1Pressed = bitRead(allGPIO, 12); sw2Pressed = bitRead(allGPIO, 13); for (int i = 0; i < 5; i++) { encoded[i] = (MSB[i] << 1) | LSB[i]; //converting the 2 pin value to single number int sum = (lastEncoded[i] << 2) | encoded[i]; //adding it to the previous encoded value //Serial.print(sum); if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue[i] ++; if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue[i] --; lastEncoded[i] = encoded[i]; //store this value for next time }
} void loop() {
if (millis() - lastPrint > delayPrint) { for (int i = 0; i < 5; i++) { Serial.print(encoderValue[i]); Serial.print(" "); } Serial.print(" // "); Serial.print(sw1Pressed); Serial.print(" "); Serial.print(sw2Pressed); Serial.println(); lastPrint = millis(); }
} ```