Files
2026-10-07 11:59:07 +02:00

534 lines
21 KiB
Arduino
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* =============================================================================
* 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 <TimerOne.h>
// =============================================================================
// 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
}