/* * ============================================================================= * MATRICE LED RGB 3 x 3 MULTIPLEXÉE — Arduino Uno * Workshop « Vitrine pixels » * ============================================================================= * * CE QUE FAIT CE PROGRAMME * ------------------------ * Il affiche des animations de couleur sur un petit écran de 9 pixels * (3 lignes × 3 colonnes). Chaque pixel est une LED RGB : une seule LED * qui contient en réalité 3 petites LED, une rouge (R), une verte (G pour * « green ») et une bleue (B). En les allumant ensemble, on mélange la lumière : * * rouge + vert = jaune * rouge + bleu = magenta * vert + bleu = cyan * rouge + vert + bleu = blanc * * (C'est la synthèse ADDITIVE, celle des écrans — à l'inverse de la peinture, * où mélanger les couleurs assombrit.) * * * LE PROBLÈME : PAS ASSEZ DE BROCHES (PINS) * ---------------------------------------- * 9 LED × 3 couleurs = 27 petites LED à commander. * L'Arduino Uno n'a que 20 broches utilisables… et on veut, plus tard, * construire une matrice de 7 × 39 = 273 pixels ! * Impossible de brancher chaque LED sur sa propre broche. * * * LA SOLUTION : LE MULTIPLEXAGE (ou « balayage ») * ----------------------------------------------- * On n'allume JAMAIS toute l'image en même temps. On allume UNE SEULE LIGNE * à la fois, très vite, l'une après l'autre : * * temps → ligne 0 | ligne 1 | ligne 2 | ligne 0 | ligne 1 | ... * 1 ms 1 ms 1 ms 1 ms 1 ms * * Chaque ligne est allumée environ 333 fois par seconde. L'œil est trop * lent pour voir le clignotement : c'est la PERSISTANCE RÉTINIENNE, le même * principe que le cinéma (24 images/seconde) ou les vieux écrans cathodiques. * Le cerveau « recolle » les trois lignes et voit une image fixe. * * Grâce à ça, il suffit de : * - 9 broches pour les couleurs : 3 colonnes × (R, G, B) * - 3 broches pour choisir quelle ligne est allumée * = 12 broches au lieu de 27. * * * LE CÂBLAGE EN DEUX MOTS * ----------------------- * Nos LED sont à « CATHODE COMMUNE » : les 3 couleurs d'une LED partagent * la même patte « moins » (la cathode, la patte la plus longue). * * - Les 3 cathodes d'une même LIGNE sont reliées ensemble, puis vers le GND * (le « moins » de l'Arduino) à travers un TRANSISTOR. Le transistor est * un interrupteur électronique : quand l'Arduino envoie du courant sur sa * patte « base », il laisse passer le courant de la ligne vers le GND. * → broche de ligne à HIGH = ligne allumée. * (On utilise un transistor car une ligne entière en blanc consomme * ~180 mA, bien trop pour une broche d'Arduino qui supporte ~20-40 mA.) * * - Les anodes (« plus ») de même couleur dans une même COLONNE sont reliées * ensemble, puis vers une broche de l'Arduino à travers une RÉSISTANCE * (220 Ω pour le rouge, 150 Ω pour le vert et le bleu). La résistance * limite le courant pour ne pas griller la LED. * → broche couleur à HIGH = cette couleur est allumée dans la colonne. * * Un pixel s'allume donc quand SA ligne est active ET que SA colonne * envoie de la couleur. C'est le croisement des deux qui choisit le pixel. * * * ORGANISATION DU PROGRAMME * ------------------------- * 1. Réglages : quelles broches, combien de lignes/colonnes. * 2. Les couleurs, codées par des nombres. * 3. L'IMAGE : un tableau de 3 × 3 cases qui contient la couleur de chaque * pixel. C'est notre « toile ». * 4. Le BALAYAGE : une fonction déclenchée automatiquement toutes les * millisecondes, qui recopie l'image sur les LED, ligne par ligne. * 5. Des fonctions pour DESSINER dans l'image (effacer, remplir, pixel). * 6. Les ANIMATIONS : elles ne font que modifier l'image ; le balayage * s'occupe de l'afficher tout seul, en arrière-plan. * * → Pour créer vos propres animations, vous n'avez à toucher qu'aux * parties 5 et 6. Le reste est le « moteur » de l'écran. * * * BIBLIOTHÈQUE NÉCESSAIRE : TimerOne * ---------------------------------- * Dans l'IDE Arduino : menu Outils → Gérer les bibliothèques… * → chercher « TimerOne » → Installer. */ // Une « bibliothèque » est un morceau de code écrit par quelqu'un d'autre, // qu'on réutilise. TimerOne sert à déclencher une fonction à intervalle // régulier, comme un métronome, sans bloquer le reste du programme. // (Elle utilise le « Timer1 » du Uno : du coup analogWrite() ne marche plus // sur les broches 9 et 10. Ici on ne s'en sert pas, donc pas de souci.) #include // ============================================================================= // 1. RÉGLAGES DE LA MATRICE // ============================================================================= // #define donne un nom à une valeur. Partout où le programme lit NB_LIGNES, // c'est comme s'il lisait 3. Avantage : pour changer la taille de la // matrice, on ne modifie qu'un seul endroit. #define NB_LIGNES 3 #define NB_COLONNES 3 // Numéros des broches qui commandent les couleurs, rangés dans un tableau // à deux dimensions : pinsCouleur[numéro de colonne][couleur] // couleur 0 = rouge, 1 = vert, 2 = bleu // // Exemple : pinsCouleur[1][2] = 7 → le bleu de la colonne 1 est sur la broche 7. // // « const » veut dire que ces valeurs ne changeront jamais pendant le programme. // « byte » est un petit nombre entier entre 0 et 255 (1 octet de mémoire). // // ⚠ Si votre câblage est différent, c'est ICI qu'il faut corriger. const byte pinsCouleur[NB_COLONNES][3] = { // R G B { 2, 3, 4 }, // colonne 0 (à gauche) { 5, 6, 7 }, // colonne 1 (au milieu) { 8, 9, 10 } // colonne 2 (à droite) }; // Broches reliées aux bases des transistors (une par ligne, via une // résistance de 1 kΩ). pinsLigne[0] = ligne du haut. // Remarque : la broche 13 est aussi reliée à la petite LED soudée sur // la carte Arduino. Elle va scintiller doucement : c'est normal. const byte pinsLigne[NB_LIGNES] = { 11, 12, 13 }; // Temps pendant lequel chaque ligne reste allumée, en MICROsecondes // (1000 µs = 1 ms = un millième de seconde). // - Plus petit → image plus stable, mais l'Arduino travaille davantage. // - Plus grand (essayez 10000 !) → on commence à voir le scintillement, // et à 200000 on voit carrément les lignes s'allumer une par une. // C'est une bonne expérience pour comprendre le multiplexage. #define DUREE_LIGNE_US 1000 // ============================================================================= // 2. LES COULEURS // ============================================================================= // // Chaque couleur est un nombre de 0 à 7. Pourquoi ces nombres-là ? // Parce qu'on les écrit en BINAIRE (en 0 et 1), sur 3 chiffres (« bits ») : // chaque bit dit si une des trois LED est allumée (1) ou éteinte (0). // // bit 2 bit 1 bit 0 // BLEU VERT ROUGE valeur // NOIR 0 0 0 → 0 // ROUGE 0 0 1 → 1 // VERT 0 1 0 → 2 // JAUNE 0 1 1 → 3 (rouge + vert) // BLEU 1 0 0 → 4 // MAGENTA 1 0 1 → 5 (rouge + bleu) // CYAN 1 1 0 → 6 (vert + bleu) // BLANC 1 1 1 → 7 (tout allumé) // // Astuce : une couleur mélangée = la SOMME des couleurs qui la composent. // JAUNE = ROUGE + VERT = 1 + 2 = 3 // BLANC = ROUGE + VERT + BLEU = 1 + 2 + 4 = 7 // // Pour l'instant chaque LED est soit allumée, soit éteinte (« tout ou rien »), // donc on n'a que ces 8 couleurs. Pour faire des dégradés (orange, rose // pâle…), il faudra faire varier l'intensité : ce sera l'étape suivante // (le « PWM »). #define NOIR 0 #define ROUGE 1 #define VERT 2 #define JAUNE 3 #define BLEU 4 #define MAGENTA 5 #define CYAN 6 #define BLANC 7 // Une « palette » : la liste des 7 couleurs (sans le noir), dans l'ordre // de l'arc-en-ciel. Pratique pour les parcourir avec une boucle : // couleurs[0] = ROUGE, couleurs[1] = JAUNE, … couleurs[6] = BLANC. const byte couleurs[] = { ROUGE, JAUNE, VERT, CYAN, BLEU, MAGENTA, BLANC }; #define NB_COULEURS 7 // ============================================================================= // 3. L'IMAGE (la « toile ») // ============================================================================= // // Un tableau de 3 × 3 cases. Chaque case contient la couleur d'un pixel. // On y accède par image[ligne][colonne] : // // colonne 0 colonne 1 colonne 2 // ligne 0 image[0][0] image[0][1] image[0][2] // ligne 1 image[1][0] image[1][1] image[1][2] // ligne 2 image[2][0] image[2][1] image[2][2] // // Attention : en informatique on compte à partir de 0, pas de 1 ! // // « volatile » prévient l'Arduino que cette variable peut changer // « dans son dos » : elle est modifiée par les animations et lue en même // temps par le balayage. Sans ce mot, l'Arduino pourrait garder une vieille // copie de l'image et ne pas voir les changements. volatile byte image[NB_LIGNES][NB_COLONNES]; // Numéro de la ligne actuellement allumée (0, 1 ou 2). volatile byte ligneCourante = 0; // ============================================================================= // 4. LE BALAYAGE (le « moteur » de l'écran) // ============================================================================= // // Cette fonction n'est jamais appelée directement par nous : c'est TimerOne // qui la déclenche automatiquement toutes les DUREE_LIGNE_US microsecondes. // On appelle ça une INTERRUPTION : l'Arduino met en pause ce qu'il est en // train de faire (même au milieu d'un delay !), exécute cette fonction, // puis reprend là où il en était. // // À chaque appel, elle passe à la ligne suivante. Il faut 3 appels pour // afficher l'image complète une fois. // // Règle d'or d'une interruption : être COURTE et RAPIDE. Pas de delay(), // pas de Serial.print() ici. void rafraichirLigne() { // --- Étape 1 : éteindre la ligne actuellement allumée --- // Il faut le faire AVANT de changer les couleurs. Sinon, pendant un très // court instant, la ligne actuelle s'afficherait avec les couleurs de la // ligne suivante : on verrait des « fantômes », de légers reflets de // couleur aux mauvais endroits. digitalWrite(pinsLigne[ligneCourante], LOW); // --- Étape 2 : passer à la ligne suivante --- // ligneCourante++ ajoute 1 : 0 → 1 → 2 → 3… // Arrivé à 3 (qui n'existe pas), on revient à 0 : on boucle. ligneCourante++; if (ligneCourante >= NB_LIGNES) { ligneCourante = 0; } // --- Étape 3 : préparer les couleurs de cette ligne --- // On parcourt les 3 colonnes. Pour chacune, on lit la couleur dans // l'image et on allume ou éteint les broches R, G et B correspondantes. for (byte col = 0; col < NB_COLONNES; col++) { // Couleur du pixel (ligne courante, colonne col), entre 0 et 7. byte c = image[ligneCourante][col]; // Le symbole & (« ET bit à bit ») permet de tester UN bit de la couleur. // Exemple avec c = MAGENTA = 5 = 101 en binaire : // 5 & ROUGE (001) = 001 → non nul → le rouge est allumé // 5 & VERT (010) = 000 → nul → le vert est éteint // 5 & BLEU (100) = 100 → non nul → le bleu est allumé // // L'écriture (condition) ? HIGH : LOW veut dire : // « si la condition est vraie, HIGH, sinon LOW ». digitalWrite(pinsCouleur[col][0], (c & ROUGE) ? HIGH : LOW); digitalWrite(pinsCouleur[col][1], (c & VERT) ? HIGH : LOW); digitalWrite(pinsCouleur[col][2], (c & BLEU) ? HIGH : LOW); } // --- Étape 4 : allumer la nouvelle ligne --- // Les couleurs sont prêtes, on ouvre le transistor de la ligne : // le courant circule, les LED de cette ligne s'allument. // Elles resteront allumées jusqu'au prochain appel (dans 1 ms). digitalWrite(pinsLigne[ligneCourante], HIGH); } // ============================================================================= // 5. LES OUTILS DE DESSIN // ============================================================================= // // Ces petites fonctions modifient l'image. Elles n'allument rien // directement : c'est le balayage qui affichera le résultat à son prochain // passage (moins de 3 ms plus tard, donc instantané pour l'œil). // Remplit toute l'image avec une seule couleur. // Deux boucles imbriquées : pour chaque ligne l, pour chaque colonne c… void remplir(byte couleur) { for (byte l = 0; l < NB_LIGNES; l++) { for (byte c = 0; c < NB_COLONNES; c++) { image[l][c] = couleur; } } } // Éteint tout (= remplir en noir). void effacer() { remplir(NOIR); } // Colore un seul pixel. // x = colonne (horizontal, de gauche à droite : 0, 1, 2) // y = ligne (vertical, de haut en bas : 0, 1, 2) // Comme sur un écran d'ordinateur, le point (0, 0) est en HAUT à gauche. // // x=0 x=1 x=2 // y=0 (0,0) (1,0) (2,0) // y=1 (0,1) (1,1) (2,1) // y=2 (0,2) (1,2) (2,2) // // Le « if » vérifie qu'on ne dessine pas en dehors de la matrice : // écrire image[5][5] irait abîmer d'autres données en mémoire. // Attention à l'ordre : on écrit image[y][x] (ligne d'abord) ! void pixel(byte x, byte y, byte couleur) { if (x < NB_COLONNES && y < NB_LIGNES) { image[y][x] = couleur; } } // ============================================================================= // 6. LES ANIMATIONS // ============================================================================= // // Une animation, c'est une suite d'images, comme un flip-book : // on dessine une image → on attend un peu (delay) → on dessine la suivante… // // delay(150) met le programme en pause 150 millisecondes. Pendant ce temps, // le balayage continue de tourner grâce à l'interruption : l'image reste // affichée. (Sans interruption, l'écran s'éteindrait pendant le delay !) // // Chaque animation dure quelques secondes puis se termine ; loop() lance // alors la suivante. // // À VOUS DE JOUER : copiez une de ces fonctions, renommez-la, modifiez-la, // puis ajoutez son nom dans loop() tout en bas. // --- Animation 1 : test du câblage --- // Allume chaque pixel, un par un, en rouge, puis en vert, puis en bleu. // À lancer en premier sur une nouvelle maquette : si un pixel ne s'allume // pas, ou s'allume au mauvais endroit / dans la mauvaise couleur, vous savez // exactement quel fil vérifier. void animTestPixels() { const byte primaires[] = { ROUGE, VERT, BLEU }; // les 3 couleurs de base for (byte p = 0; p < 3; p++) { // pour chaque couleur de base for (byte y = 0; y < NB_LIGNES; y++) { // pour chaque ligne for (byte x = 0; x < NB_COLONNES; x++) { // pour chaque colonne effacer(); // on éteint tout pixel(x, y, primaires[p]); // on allume un seul pixel delay(150); // on laisse le temps de voir } } } } // --- Animation 2 : aplats de couleur --- // Toute la matrice prend successivement les 7 couleurs de la palette. void animCouleursPleines() { for (byte i = 0; i < NB_COULEURS; i++) { // i va de 0 à 6 remplir(couleurs[i]); // la i-ème couleur de la palette delay(400); // 0,4 seconde par couleur } } // --- Animation 3 : la chenille --- // Un petit serpent de 2 pixels fait le tour du bord de la matrice, pendant // que le pixel central reste blanc. Il change de couleur à chaque tour. void animChenille() { // Les 8 cases du bord, dans l'ordre des aiguilles d'une montre. // Chaque case est donnée par ses coordonnées {x, y}. // // 0 → 1 → 2 // ↑ ↓ // 7 · 3 // ↑ ↓ // 6 ← 5 ← 4 const byte tour[8][2] = { {0, 0}, {1, 0}, {2, 0}, // le haut, de gauche à droite {2, 1}, // le côté droit {2, 2}, {1, 2}, {0, 2}, // le bas, de droite à gauche {0, 1} // le côté gauche }; for (byte t = 0; t < 4; t++) { // 4 tours complets byte couleur = couleurs[t % NB_COULEURS]; // une couleur différente par tour // (% = reste de la division : garde // le numéro entre 0 et 6) for (byte i = 0; i < 8; i++) { // 8 étapes par tour effacer(); pixel(1, 1, BLANC); // le centre, fixe // La tête du serpent, sur la case i du tour. pixel(tour[i][0], tour[i][1], couleur); // La queue, sur la case juste avant. // (i + 7) % 8 donne « i - 1 », mais en revenant à 7 quand i vaut 0 : // la queue de la case 0 est la case 7, on boucle autour du carré. byte prec = (i + 7) % 8; pixel(tour[prec][0], tour[prec][1], couleur); delay(100); // vitesse du serpent } } } // --- Animation 4 : le damier --- // Un damier cyan / magenta qui s'inverse régulièrement. void animDamier() { for (byte n = 0; n < 8; n++) { // 8 images for (byte y = 0; y < NB_LIGNES; y++) { for (byte x = 0; x < NB_COLONNES; x++) { // L'astuce du damier : on additionne x et y. // Si le résultat est pair, une couleur ; s'il est impair, l'autre. // (0,0) → 0 pair (1,0) → 1 impair (2,0) → 2 pair … // En ajoutant n (qui augmente à chaque image), pair et impair // s'échangent à chaque fois : le damier s'inverse. if ((x + y + n) % 2 == 1) { pixel(x, y, MAGENTA); } else { pixel(x, y, CYAN); } } } delay(300); } } // --- Animation 5 : balayages --- // Une barre verticale traverse la matrice de gauche à droite, puis une barre // horizontale de haut en bas. On recommence avec chaque couleur de la palette. void animBalayages() { for (byte i = 0; i < NB_COULEURS; i++) { byte couleur = couleurs[i]; // Barre verticale : on allume toute la colonne x, puis on avance. for (byte x = 0; x < NB_COLONNES; x++) { effacer(); for (byte y = 0; y < NB_LIGNES; y++) { pixel(x, y, couleur); } delay(120); } // Barre horizontale : on allume toute la ligne y, puis on descend. for (byte y = 0; y < NB_LIGNES; y++) { effacer(); for (byte x = 0; x < NB_COLONNES; x++) { pixel(x, y, couleur); } delay(120); } } } // --- Animation 6 : scintillement aléatoire --- // À chaque étape, un pixel pris au hasard prend une couleur au hasard // (le noir compris, pour que des pixels s'éteignent aussi). // random(3) donne un nombre au hasard parmi 0, 1, 2. // random(8) donne un nombre au hasard entre 0 et 7 = une de nos 8 couleurs. void animAleatoire() { for (int n = 0; n < 40; n++) { // 40 changements pixel(random(NB_COLONNES), random(NB_LIGNES), random(8)); delay(80); } } // ============================================================================= // SETUP : exécuté UNE SEULE FOIS, à l'allumage de l'Arduino // ============================================================================= void setup() { // Toutes nos broches servent à ENVOYER du courant (sortie = OUTPUT), // pas à en lire (ce serait INPUT, pour un bouton par exemple). // On les met toutes à LOW au départ pour que tout soit éteint. // Les 3 broches de lignes for (byte l = 0; l < NB_LIGNES; l++) { pinMode(pinsLigne[l], OUTPUT); digitalWrite(pinsLigne[l], LOW); } // Les 9 broches de couleurs (3 colonnes × 3 couleurs) for (byte c = 0; c < NB_COLONNES; c++) { for (byte k = 0; k < 3; k++) { pinMode(pinsCouleur[c][k], OUTPUT); digitalWrite(pinsCouleur[c][k], LOW); } } // On part d'une image toute noire. effacer(); // Le hasard d'un ordinateur n'est pas vraiment du hasard : c'est une // longue suite de nombres calculée à partir d'un point de départ // (la « graine »). Si la graine est toujours la même, la suite aussi. // On lit donc la broche A0, qui n'est branchée à rien : elle capte du // bruit électrique, différent à chaque démarrage → vrai point de départ // imprévisible. randomSeed(analogRead(A0)); // On démarre le métronome : toutes les DUREE_LIGNE_US microsecondes, // TimerOne appellera rafraichirLigne(). À partir de maintenant, l'écran // s'affiche tout seul, en arrière-plan. Timer1.initialize(DUREE_LIGNE_US); Timer1.attachInterrupt(rafraichirLigne); } // ============================================================================= // LOOP : exécutée EN BOUCLE, à l'infini, après setup() // ============================================================================= // // C'est la « playlist » : les animations s'enchaînent dans cet ordre, puis // on recommence au début. // Pour ne garder qu'une animation, mettez les autres en commentaire en // ajoutant // devant la ligne. Pour ajouter la vôtre, écrivez son nom ici. void loop() { animTestPixels(); // 1. test du câblage (à retirer une fois validé) animCouleursPleines(); // 2. aplats de couleur animChenille(); // 3. serpent autour du bord animDamier(); // 4. damier qui s'inverse animBalayages(); // 5. barres qui traversent animAleatoire(); // 6. scintillement aléatoire }