Skyduino:~#
Articles
arduino, programmation, projet, tutoriel

[Tuto/Info] Matrice de leds RGB – partie 2

Bonjour tout le monde !

La partie 2 de ma série d’articles sur les matrices de led RGB s’est fait attendre, mais là voici enfin !

Au programme aujourd’hui : un code de démonstration ayant pour but de vous faire comprendre comment se contrôlent ces fameuses matrices.
À la fin de cet article, vous devriez être en mesure de contrôler une (ou plusieurs) matrice(s), en théorie.

Attention : le code présentait ci-dessous n’est pas « exploitable » en l’état. Il n’a pour but que de vous présenter la théorie.
Une version réellement utilisable sera disponible d’ici peu (sous-entendu demain si tout va bien) dans un prochain article 😉

Câblage et hardware

DSCF2280DSCF2239

On ne change pas une équipe qui gagne ! Le câblage est le même que celui de mon précédent article.
Si vous étiez en train de vous amuser avec la librairie RGB_Matrix d’Adafruit vous n’aurez rien à modifier 😉

Le code

Ne tournons pas autour du pot, voici le code complet que je vais détailler morceau par morceau :

/* Constantes de compilation */
static const byte NB_HORIZONTAL_MATRIX = 1;
static const byte NB_VERTICAL_MATRIX = 2;

static const byte NB_LINES_PER_MATRIX = 32;
static const byte NB_COLUMNS_PER_MATRIX = 32;
static const byte MATRIX_SCANLINE_SIZE = 16;

static const byte NB_LINES_COUNT = NB_VERTICAL_MATRIX * NB_LINES_PER_MATRIX;
static const byte NB_COLUMNS_COUNT = NB_HORIZONTAL_MATRIX * NB_COLUMNS_PER_MATRIX;

/* Pin mapping */
static const byte PIN_R1 = 2;   // R1
static const byte PIN_G1 = 3;   // G1
static const byte PIN_B1 = 4;   // B1
static const byte PIN_R2 = 5;   // R2
static const byte PIN_G2 = 6;   // G2
static const byte PIN_B2 = 7;   // B2
static const byte PIN_A = A0;   // A
static const byte PIN_B = A1;   // B
static const byte PIN_C = A2;   // C
static const byte PIN_D = A3;   // D
static const byte PIN_CLK = 8;  // CLK
static const byte PIN_OE = 9;   // OE
static const byte PIN_LAT = 10; // LAT

/** 
 * Buffer pour les pixels
 *
 * Format du buffer : [index de ligne] [index de colonne / 8] [données des couleurs R, V, B]
 * Format des données : ... [R1 V1 B1][R2, V2, B2] ...
 */
static volatile byte framebuffer[NB_LINES_COUNT][NB_COLUMNS_COUNT / 8][3];

/**
 * Énumération des couleurs possibles.
 */
enum {
  COLOR_RED,
  COLOR_GREEN,
  COLOR_BLUE,
  COLOR_YELLOW,
  COLOR_CYAN,
  COLOR_PINK,
  COLOR_WHITE,
  COLOR_BLACK  
};

/**
 * Change la couleur d'un pixel.
 * 
 * @param x La position X du pixel.
 * @param y La position Y du pixel.
 * @param color La nouvelle couleur du pixel.
 */
static void setPixelAt(const byte x, const byte y, const byte color) {
  volatile byte *pixel = framebuffer[y][x / 8];
  byte mask = 1 << (x & 7);
  switch(color) {
  case COLOR_RED:
    pixel[0] |= mask;
    pixel[1] &= ~mask;
    pixel[2] &= ~mask;
    break;
  case COLOR_GREEN:
    pixel[0] &= ~mask;
    pixel[1] |= mask;
    pixel[2] &= ~mask;
    break;
  case COLOR_BLUE:
    pixel[0] &= ~mask;
    pixel[1] &= ~mask;
    pixel[2] |= mask;
    break;
  case COLOR_YELLOW:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] &= ~mask;
    break;
  case COLOR_CYAN:
    pixel[0] &= ~mask;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  case COLOR_PINK:
    pixel[0] |= mask;
    pixel[1] &= ~mask;
    pixel[2] |= mask;
    break;
  case COLOR_WHITE:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  default:
    pixel[0] &= ~mask;
    pixel[1] &= ~mask;
    pixel[2] &= ~mask;
    break;
  }
}

/**
 * Retourne la couleur actuelle d'un pixel.
 * 
 * @param x La position X du pixel.
 * @param y La position Y du pixel.
 * @return La couleur actuelle du pixel.
 */
static byte getPixelAt(const byte x, const byte y) {
  volatile byte* pixel = framebuffer[y][x / 8];
  byte mask = 1 << (x & 7);
  byte r = !!(pixel[0] & mask);
  byte g = !!(pixel[1] & mask);
  byte b = !!(pixel[2] & mask);
  if(r && !g && !b)
    return COLOR_RED;
  if(!r && g && !b)
    return COLOR_GREEN;
  if(!r && !g && b)
    return COLOR_BLUE;
  if(r && g && !b)
    return COLOR_YELLOW;
  if(!r && g && b)
    return COLOR_CYAN;
  if(r && !g && b)
    return COLOR_PINK;
  if(r && g && b)
    return COLOR_WHITE;
  if(!r && !g && !b)
    return COLOR_BLACK;
}

/** Setup */
void setup() {

  // Initialisation des broches
  pinMode(13, OUTPUT);
  pinMode(PIN_R1, OUTPUT);
  pinMode(PIN_G1, OUTPUT);
  pinMode(PIN_B1, OUTPUT);
  pinMode(PIN_R2, OUTPUT);
  pinMode(PIN_G2, OUTPUT);
  pinMode(PIN_B2, OUTPUT);
  pinMode(PIN_A, OUTPUT);
  pinMode(PIN_B, OUTPUT);
  pinMode(PIN_C, OUTPUT);
  pinMode(PIN_D, OUTPUT);
  pinMode(PIN_CLK, OUTPUT);
  pinMode(PIN_OE, OUTPUT);
  pinMode(PIN_LAT, OUTPUT);

  // Initialisation des broches de contrôle
  digitalWrite(PIN_A, LOW);
  digitalWrite(PIN_B, LOW);
  digitalWrite(PIN_C, LOW);
  digitalWrite(PIN_D, LOW);
  digitalWrite(PIN_CLK, LOW);
  digitalWrite(PIN_OE, HIGH);
  digitalWrite(PIN_LAT, LOW);

  // Initialisation du buffer (tous les pixels éteints)
  memset((void*) framebuffer, 0, NB_LINES_COUNT * (NB_COLUMNS_COUNT / 8) * 3);
}

/** Loop */
void loop() {

  // Rafraîchi l'affichage des matrices aussi vite que possible pour mesurer la vitesse d’exécution du code
  refreshDisplay();
}

/** Routine d'affichage des matrices */
void refreshDisplay() {

  // Index de la scan line
  static byte scanlineIndex = 0;
  digitalWrite(13, HIGH);

  // Mise en place de l'adresse de la ligne en cours et des broches de contrôle
  digitalWrite(PIN_OE, HIGH);
  digitalWrite(PIN_LAT, HIGH);
  digitalWrite(PIN_A, (scanlineIndex & 1) ? HIGH : LOW);
  digitalWrite(PIN_B, (scanlineIndex & 2) ? HIGH : LOW);
  digitalWrite(PIN_C, (scanlineIndex & 4) ? HIGH : LOW);
  digitalWrite(PIN_D, (scanlineIndex & 8) ? HIGH : LOW);

  // Pour chaque matrice verticale
  for (int vMatrixIndex = NB_VERTICAL_MATRIX - 1; vMatrixIndex >= 0; --vMatrixIndex) {

    // Scan line 1/16 avec matrices 32x32

    // Stock un pointeur vers les deux lignes à afficher
    unsigned int lineOffset = vMatrixIndex * NB_LINES_PER_MATRIX + scanlineIndex;
    volatile byte (*lineBufferA)[3] = framebuffer[lineOffset];
    volatile byte (*lineBufferB)[3] = framebuffer[lineOffset + MATRIX_SCANLINE_SIZE];

    // Pour chaque colonne (en bundle de 8 pixels)
    for (int bundleIndex = (NB_COLUMNS_COUNT / 8) - 1; bundleIndex >= 0; --bundleIndex) {

      // Stock un pointeur vers les deux séries de 3 couleurs à afficher
      volatile byte (*bundleBufferA) = lineBufferA[bundleIndex];
      volatile byte (*bundleBufferB) = lineBufferB[bundleIndex];

      // Récupère les valeurs RVB des deux lignes à la colonne en cours
      byte r1 = bundleBufferA[0];
      byte g1 = bundleBufferA[1];
      byte b1 = bundleBufferA[2];
      byte r2 = bundleBufferB[0];
      byte g2 = bundleBufferB[1];
      byte b2 = bundleBufferB[2];

      // Pour chaque bit des valeurs
      for(int bitIndex = 7; bitIndex >= 0; --bitIndex) {

        // Envoi les bits
        byte mask = 1 << bitIndex;
        digitalWrite(PIN_R1, (r1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_G1, (g1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_B1, (b1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_R2, (r2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_G2, (g2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_B2, (b2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_CLK, HIGH);
        digitalWrite(PIN_CLK, LOW);
      }
    }
  }

  // Active le "latch" (déclenche l'affichage de la ligne)
  digitalWrite(PIN_OE, LOW);
  digitalWrite(PIN_LAT, LOW);

  // Gestion de la fin d'affichage
  if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

    // Reprise à la première ligne
    scanlineIndex = 0; 
    
    // Code de démonstration (rempli les matrices avec une même couleur)
    static byte x = 0;
    static byte y = 0;
    static byte color = COLOR_RED;

    setPixelAt(x, y, color);

    if(++x == NB_COLUMNS_COUNT) {
      x = 0;

      if(++y == NB_LINES_COUNT) {
        y = 0;

        if(color == COLOR_BLACK)
          color = COLOR_RED;
        else
          ++color; 
      }
    }
  }

  // Pour debug 
  digitalWrite(13, LOW);
}

Le code complet est disponible en téléchargement sur mon Github, en plus du code en cours de développement :
https://github.com/skywodd/RGB_Matrix_Arduino_AVR

EDIT : La version du code ci-dessus actuellement sur mon Github est compatible avec les cartes Arduino Mega2560.
Merci Icare pour le pin mapping sélectif à la compilation 😉

Vous êtes prêt ?
Allez c’est parti pour le détail du code 😉

/* Constantes de compilation */
static const byte NB_HORIZONTAL_MATRIX = 1;
static const byte NB_VERTICAL_MATRIX = 2;

static const byte NB_LINES_PER_MATRIX = 32;
static const byte NB_COLUMNS_PER_MATRIX = 32;
static const byte MATRIX_SCANLINE_SIZE = 16;

static const byte NB_LINES_COUNT = NB_VERTICAL_MATRIX * NB_LINES_PER_MATRIX;
static const byte NB_COLUMNS_COUNT = NB_HORIZONTAL_MATRIX * NB_COLUMNS_PER_MATRIX;

On commence tranquillement avec quelques constantes, rien de bien méchant.

Les deux premières constantes permettent de choisir le nombre de matrices verticales et horizontales.
Pour ma part j’ai fait mes tests avec 2 matrices en ligne, soit verticalement, soit horizontalement.

Les autres constantes sont là pour définir la taille en pixels des matrices et la scanline utilisée.
Vous n’avez pas à modifier ces lignes 😉

/* Pin mapping */
static const byte PIN_R1 = 2;   // R1
static const byte PIN_G1 = 3;   // G1
static const byte PIN_B1 = 4;   // B1
static const byte PIN_R2 = 5;   // R2
static const byte PIN_G2 = 6;   // G2
static const byte PIN_B2 = 7;   // B2
static const byte PIN_A = A0;   // A
static const byte PIN_B = A1;   // B
static const byte PIN_C = A2;   // C
static const byte PIN_D = A3;   // D
static const byte PIN_CLK = 8;  // CLK
static const byte PIN_OE = 9;   // OE
static const byte PIN_LAT = 10; // LAT

Pour cette première version, tout se fera avec l’API Arduino de base.
Pas d’optimisation précoce, pas de registres AVR, rien que du code Arduino pure et dure.

Du coup vous pouvez théoriquement choisir n’importe quelles broches pour le contrôle des matrices.
En réalité je vous conseille de rester sur la configuration ci-dessus, car elle correspond à des ports d’entrées / sorties bien précis que j’utiliserai dés le prochain article 😉

/** 
 * Buffer pour les pixels
 *
 * Format du buffer : [index de ligne] [index de colonne / 8] [données des couleurs R, V, B]
 * Format des données : ... [R1 V1 B1][R2, V2, B2] ...
 */
static volatile byte framebuffer[NB_LINES_COUNT][NB_COLUMNS_COUNT / 8][3];

Fini les trucs de noob, les choses sérieuses commencent ici.

« framebuffer » est donc un tableau d’octets à 3 dimensions.
Première dimension : les coordonnées en Y
Deuxième dimension : les coordonnées en X
Troisième dimension : les 3 composantes de base rouge, vert, bleu.

Remarque : Les colonnes sont groupées par lot de 8. 8 colonnes = 1 octet.
(ça devrez vous rappeler un article sur le « Bitwise » normalement ;))

Le tout est « static volatile » pour éviter que le compilateur ne vienne mettre le bouzin dans le tableau lors de l’optimisation du code.

Comme c’est un code d’exemple, je n’ai volontairement pas fait de double, voir triple buffering.
Vous pouvez regarder sur Wikpedia comment ça marche si vous êtes curieux, mais pour le moment on dessinera dans le même buffer que pour l’affichage.

/**
 * Énumération des couleurs possibles.
 */
enum {
  COLOR_RED,
  COLOR_GREEN,
  COLOR_BLUE,
  COLOR_YELLOW,
  COLOR_CYAN,
  COLOR_PINK,
  COLOR_WHITE,
  COLOR_BLACK  
};

Pas de commentaire, c’est juste une énumération pour les différentes couleurs possibles.

/**
 * Change la couleur d'un pixel.
 * 
 * @param x La position X du pixel.
 * @param y La position Y du pixel.
 * @param color La nouvelle couleur du pixel.
 */
static void setPixelAt(const byte x, const byte y, const byte color) {
  volatile byte *pixel = framebuffer[y][x / 8];
  byte mask = 1 << (x & 7);
  switch(color) {
  case COLOR_RED:
    pixel[0] |= mask;
    pixel[1] &= ~mask;
    pixel[2] &= ~mask;
    break;
  case COLOR_GREEN:
    pixel[0] &= ~mask;
    pixel[1] |= mask;
    pixel[2] &= ~mask;
    break;
  case COLOR_BLUE:
    pixel[0] &= ~mask;
    pixel[1] &= ~mask;
    pixel[2] |= mask;
    break;
  case COLOR_YELLOW:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] &= ~mask;
    break;
  case COLOR_CYAN:
    pixel[0] &= ~mask;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  case COLOR_PINK:
    pixel[0] |= mask;
    pixel[1] &= ~mask;
    pixel[2] |= mask;
    break;
  case COLOR_WHITE:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  default:
    pixel[0] &= ~mask;
    pixel[1] &= ~mask;
    pixel[2] &= ~mask;
    break;
  }
}

Là ça devient plus marrant 😉
Cette fonction setPixelAt(x, y, couleur) permet, comme son nom l’indique, de fixer une couleur à un pixel donné en fonction de ses coordonnées X et Y.

La première ligne permet de récupérer un pointeur vers un tableau de 3 octets contenant les 3 composantes rouge, vert, bleu.
La seconde ligne permet de calculer un « masque » qui permettra de placer un bit (= une colonne) à 1 ou 0. C’est du « bitwise« .
Le switch permet quand à lui de placer les bits des composantes rouge, vert et bleu correctement pour obtenir la couleur voulue.

/**
 * Retourne la couleur actuelle d'un pixel.
 * 
 * @param x La position X du pixel.
 * @param y La position Y du pixel.
 * @return La couleur actuelle du pixel.
 */
static byte getPixelAt(const byte x, const byte y) {
  volatile byte* pixel = framebuffer[y][x / 8];
  byte mask = 1 << (x & 7);
  byte r = !!(pixel[0] & mask);
  byte g = !!(pixel[1] & mask);
  byte b = !!(pixel[2] & mask);
  if(r && !g && !b)
    return COLOR_RED;
  if(!r && g && !b)
    return COLOR_GREEN;
  if(!r && !g && b)
    return COLOR_BLUE;
  if(r && g && !b)
    return COLOR_YELLOW;
  if(!r && g && b)
    return COLOR_CYAN;
  if(r && !g && b)
    return COLOR_PINK;
  if(r && g && b)
    return COLOR_WHITE;
  if(!r && !g && !b)
    return COLOR_BLACK;
}

La fonction getPixelAt(x, y) est l’inverse de la fonction setPixelAt(x, y, couleur). Elle retourne la couleur du pixel aux coordonnées données.

« STOP ! Ya un truc louche dans ton code ! Où se trouve le pixel (0, 0) ? »
En bas à gauche 😉

Et oui j’ai volontairement passé outre les conventions informatiques qui veulent que le point d’origine (0, 0) se trouve en haut à gauche avec l’axe Y vers le bas.
MAIS, avant que vous ne me lanciez des cailloux à la tronche, j’ai une excuse.

La plupart de mes lecteurs ne sont pas des informaticiens chevronnés, respecter les conventions du pixel (0, 0) en haut à gauche aurait fait un massacre.
Le nombre de neurones grillés par surchauffe de calcul mathématique aurait été trop important.

En plaçant le point d’origine (0, 0) en bas à gauche avec l’axe Y vers le haut et l’axe X vers la droite je reste dans un plan orthonormé classique comme en mathématique.
Quiconque ayant donc suivi en cours de mathématique de troisième/terminal devrait pouvoir faire de jolis dessins avec mon code 😉

/** Setup */
void setup() {

  // Initialisation des broches
  pinMode(13, OUTPUT);
  pinMode(PIN_R1, OUTPUT);
  pinMode(PIN_G1, OUTPUT);
  pinMode(PIN_B1, OUTPUT);
  pinMode(PIN_R2, OUTPUT);
  pinMode(PIN_G2, OUTPUT);
  pinMode(PIN_B2, OUTPUT);
  pinMode(PIN_A, OUTPUT);
  pinMode(PIN_B, OUTPUT);
  pinMode(PIN_C, OUTPUT);
  pinMode(PIN_D, OUTPUT);
  pinMode(PIN_CLK, OUTPUT);
  pinMode(PIN_OE, OUTPUT);
  pinMode(PIN_LAT, OUTPUT);

Qui dit « code Arduino » dit « fonction setup » !
Et pour bien commencer une fonction setup() rien ne vaut une bonne tartine de pinMode() 😉
Pour le coup c’est simple : toutes les broches sont en sorties.

PS : la led pin13 servira plus tard pour le debug.

  // Initialisation des broches de contrôle
  digitalWrite(PIN_A, LOW);
  digitalWrite(PIN_B, LOW);
  digitalWrite(PIN_C, LOW);
  digitalWrite(PIN_D, LOW);
  digitalWrite(PIN_CLK, LOW);
  digitalWrite(PIN_OE, HIGH);
  digitalWrite(PIN_LAT, LOW);

On continue ensuite avec une pincée de digitalWrite() pour placer les broches de contrôles dans un état de départ cohérent.

  // Initialisation du buffer (tout les pixels éteints)
  memset((void*) framebuffer, 0, NB_LINES_COUNT * (NB_COLUMNS_COUNT / 8) * 3);
}

Et pour conclure la fonction setup() on initialise le contenu du « framebuffer » avec plein de jolis zéros (= toutes leds éteintes).

/** Loop */
void loop() {

  // Rafraîchi l'affichage des matrices aussi vite que possible
  // pour mesurer la vitesse d’exécution du code
  refreshDisplay();
}

La fonction loop() est pour le moment très sommaire.
Elle se contente uniquement d’appeler la fonction d’affichage des matrices aussi vite que possible pour mesurer la vitesse d’exécution du code.

Comme vous pourrez le voir plus tard dans mon prochain article, et comme je le répète tout le temps : le framework Arduino est fait pour être simple, pas pour être rapide.
Et pour le coup c’est tout juste assez rapide pour avoir un truc fonctionnel si on n’utilise que des fonctions Arduino.

/** Routine d'affichage des matrices */
void refreshDisplay() {

  // Index de la scan line
  static byte scanlineIndex = 0;
  digitalWrite(13, HIGH);

  // Mise en place de l'adresse de la ligne en cours et des broches de contrôle
  digitalWrite(PIN_OE, HIGH);
  digitalWrite(PIN_LAT, HIGH);
  digitalWrite(PIN_A, (scanlineIndex & 1) ? HIGH : LOW);
  digitalWrite(PIN_B, (scanlineIndex & 2) ? HIGH : LOW);
  digitalWrite(PIN_C, (scanlineIndex & 4) ? HIGH : LOW);
  digitalWrite(PIN_D, (scanlineIndex & 8) ? HIGH : LOW);

  // Pour chaque matrice verticale
  for (int vMatrixIndex = NB_VERTICAL_MATRIX - 1; vMatrixIndex >= 0; --vMatrixIndex) {

    // Scan line 1/16 avec matrices 32x32

    // Stock un pointeur vers les deux lignes à afficher
    unsigned int lineOffset = vMatrixIndex * NB_LINES_PER_MATRIX + scanlineIndex;
    volatile byte (*lineBufferA)[3] = framebuffer[lineOffset];
    volatile byte (*lineBufferB)[3] = framebuffer[lineOffset + MATRIX_SCANLINE_SIZE];

    // Pour chaque colonne (en bundle de 8 pixels)
    for (int bundleIndex = (NB_COLUMNS_COUNT / 8) - 1; bundleIndex >= 0; --bundleIndex) {

      // Stock un pointeur vers les deux séries de 3 couleurs à afficher
      volatile byte (*bundleBufferA) = lineBufferA[bundleIndex];
      volatile byte (*bundleBufferB) = lineBufferB[bundleIndex];

      // Récupère les valeurs RVB des deux lignes à la colonne en cours
      byte r1 = bundleBufferA[0];
      byte g1 = bundleBufferA[1];
      byte b1 = bundleBufferA[2];
      byte r2 = bundleBufferB[0];
      byte g2 = bundleBufferB[1];
      byte b2 = bundleBufferB[2];

      // Pour chaque bit des valeurs
      for(int bitIndex = 7; bitIndex >= 0; --bitIndex) {

        // Envoi les bits
        byte mask = 1 << bitIndex;
        digitalWrite(PIN_R1, (r1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_G1, (g1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_B1, (b1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_R2, (r2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_G2, (g2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_B2, (b2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_CLK, HIGH);
        digitalWrite(PIN_CLK, LOW);
      }
    }
  }

  // Active le "latch" (déclenche l'affichage de la ligne)
  digitalWrite(PIN_OE, LOW);
  digitalWrite(PIN_LAT, LOW);

  // Gestion de la fin d'affichage
  if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

    // Reprise à la première ligne
    scanlineIndex = 0; 
    
    // Code de démonstration (rempli les matrices avec une même couleur)
    static byte x = 0;
    static byte y = 0;
    static byte color = COLOR_RED;

    setPixelAt(x, y, color);

    if(++x == NB_COLUMNS_COUNT) {
      x = 0;

      if(++y == NB_LINES_COUNT) {
        y = 0;

        if(color == COLOR_BLACK)
          color = COLOR_RED;
        else
          ++color; 
      }
    }
  }

  // Pour debug 
  digitalWrite(13, LOW);
}

Ceci est une révolution fonction d’affichage par rafraîchissement (en l’occurrence ici de 16 lignes).
Ne vous inquiétez pas si vous n’avez strictement rien compris je vais détailler ligne par ligne cette partie.

/** Routine d'affichage des matrices */
void refreshDisplay() {

  // Index de la scan line
  static byte scanlineIndex = 0;
  digitalWrite(13, HIGH);

La fonction d’affichage commence par la déclaration d’une variable statique (= qui conserve sa valeur entre deux appels de la fonction) « scanlineIndex ».
Cette variable prend successivement les valeurs 0, puis 1, 2, 3, … jusqu’à 15 avant de revenir à 0.
C’est cette variable qui permet de savoir quelle ligne rafraîchir.

Au passage on allume la led pin13 pour pouvoir mesurer par la suite le temps que met cette fonction à s’exécuter 😉

  // Mise en place de l'adresse de la ligne en cours et des broches de contrôle
  digitalWrite(PIN_OE, HIGH);
  digitalWrite(PIN_LAT, HIGH);
  digitalWrite(PIN_A, (scanlineIndex & 1) ? HIGH : LOW);
  digitalWrite(PIN_B, (scanlineIndex & 2) ? HIGH : LOW);
  digitalWrite(PIN_C, (scanlineIndex & 4) ? HIGH : LOW);
  digitalWrite(PIN_D, (scanlineIndex & 8) ? HIGH : LOW);

La première étape pour rafraîchir une ligne est d’éteindre la matrice et d’armer le signal « latch » qui permet de charger les données en mémoire.
Ensuite on place les signaux A, B, C et D en fonction du contenu de « scanlineIndex » pour sélectionner la ligne voulue.

Les « (blabla & quelquechose) ? HIGH : LOW » permettent de placer les broches à HIGH ou LOW en fonction de l’état d’un bit (0 -> LOW, 1 -> HIGH).

  // Pour chaque matrice verticale
  for (int vMatrixIndex = NB_VERTICAL_MATRIX - 1; vMatrixIndex >= 0; --vMatrixIndex) {

    // Logique de traitement pour chaque matrice ...
  }

La seconde étape est de traiter chaque matrice verticale (les matrices horizontales sont traitées comme des lignes) … en partant de la dernière.

Pourquoi commencer de la fin ? Tout simplement parce que les matrices sont physiquement des registres à décalage.
Ce qui rentre en premier s’affiche en dernier sur les matrices.

Le « int » est ici obligatoire car la valeur de « vMatrixIndex » doit pouvoir devenir négative.
(j’aurai pu utiliser un « signed char », mais j’aurai eu une avalanche de questions sur le pourquoi d’un signed char)

    // Stock un pointeur vers les deux lignes à afficher
    unsigned int lineOffset = vMatrixIndex * NB_LINES_PER_MATRIX + scanlineIndex;
    volatile byte (*lineBufferA)[3] = framebuffer[lineOffset];
    volatile byte (*lineBufferB)[3] = framebuffer[lineOffset + MATRIX_SCANLINE_SIZE];

Chaque matrice est constituée de … lignes ! (merci Captain Obvious)

Comme on travaille avec une scanline de 1/16 et avec des matrices de 32×32 pixels on a besoin de garder en mémoire deux pointeurs simultanément : un vers la ligne N et un vers la ligne N + 16.
Voilà le pourquoi de l’utilité de « lineBufferA » et « lineBufferB » 😉

    // Pour chaque colonne (en bundle de 8 pixels)
    for (int bundleIndex = (NB_COLUMNS_COUNT / 8) - 1; bundleIndex >= 0; --bundleIndex) {

      // Logique de traitement pour chaque colonne ...
    }

Et chaque ligne est constituée de … colonnes ! Woaa, vous êtes trop fort ! 🙂

Et comme sur ce blog on n’est pas des petits dégueulasses, chaque lot de 8 colonnes prend 1 octet.
Utiliser 1 octet pour stocker des booléens c’est bien pour les débutants, mais nous ont est des ninjas maîtres du bitwise 😉
(celui qui me plante un commentaire du style « c’est quoi le bitwise ? » se prend une double claque ;))

Même principe que pour les matrices : il faut envoyer les colonnes en partant de la dernière.

      // Stock un pointeur vers les deux séries de 3 couleurs à afficher
      volatile byte (*bundleBufferA) = lineBufferA[bundleIndex];
      volatile byte (*bundleBufferB) = lineBufferB[bundleIndex];

Encore un pointeur ? Promis c’est le dernier 😉
Maintenant qu’on a deux pointeurs vers le début des lignes N et N + 16, il faut un pointeur vers le début des lots de 3 octets qui contiennent les composantes rouge, vert, bleu de chaque lot de 8 colonnes.

Remarque : Le « volatile » ici ne sert à rien, c’est juste pour faire plaisir au compilateur.


      // Récupère les valeurs RVB des deux lignes à la colonne en cours
      byte r1 = bundleBufferA[0];
      byte g1 = bundleBufferA[1];
      byte b1 = bundleBufferA[2];
      byte r2 = bundleBufferB[0];
      byte g2 = bundleBufferB[1];
      byte b2 = bundleBufferB[2];

Et maintenant qu’on a nos deux pointeurs vers les composantes rouge, vert, et bleu des lignes N et N + 16 il suffit de lire tout ça en mémoire.
On se retrouve alors avec 6 octets pour les 6 lignes de données R1, G1, B1, R2, G2, B2 des matrices.

      // Pour chaque bit des valeurs
      for(int bitIndex = 7; bitIndex >= 0; --bitIndex) {

        // Envoi les bits
        byte mask = 1 << bitIndex;
        digitalWrite(PIN_R1, (r1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_G1, (g1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_B1, (b1 & mask) ? HIGH : LOW);
        digitalWrite(PIN_R2, (r2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_G2, (g2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_B2, (b2 & mask) ? HIGH : LOW);
        digitalWrite(PIN_CLK, HIGH);
        digitalWrite(PIN_CLK, LOW);
      }
    }
  }

Il ne reste donc plus qu’à sortir une belle boucle du chapeau pour envoyer chaque bit un à un.
Pareil que précédemment tout se fait à l’envers. On envoie donc le bit de point fort en premier (MSBFIRST) au lieu du bit de point faible (LSBFIRST).

  // Active le "latch" (déclenche l'affichage de la ligne)
  digitalWrite(PIN_OE, LOW);
  digitalWrite(PIN_LAT, LOW);

Maintenant que la ligne est complètement transférée, il suffit de déclencher le signal « latch » pour basculer les données dans les matrices et de réactiver l’affichage.

  // Gestion de la fin d'affichage
  if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

    // Reprise à la première ligne
    scanlineIndex = 0; 
    
    // Code de démonstration ...
  }

  // Pour debug 
  digitalWrite(13, LOW);
}

Mais ce n’est pas fini pour autant !

Il ne faut pas oublier d’incrémenter « scanlineIndex » et de vérifier au passage si la valeur ne dépasse pas 16.
Si c’est le cas on reprend de zéro et on en profite pour mettre à jour l’affichage avec un petit code de démonstration de mon cru 😉
En faisant cela le rafraîchissement des matrices se fait tout seul en tache de fond.

    // Code de démonstration
    static byte x = 0;
    static byte y = 0;
    static byte color = COLOR_RED;

    setPixelAt(x, y, color);

    if(++x == NB_COLUMNS_COUNT) {
      x = 0;

      if(++y == NB_LINES_COUNT) {
        y = 0;

        if(color == COLOR_BLACK)
          color = COLOR_RED;
        else
          ++color; 
      }
    }

Ce code de démonstration – d’une complexité extrême – rempli l’écran avec une couleur unie, puis une autre, puis une autre, etc.
Rien de bien passionnant je vous l’accorde.

Toujours vivant ?
Si oui bravo 🙂

Vous venez de voir comment faire communiquer une carte Arduino avec une matrice de led RGB. C’est déjà un bon début 😉

Mon prochain article présentera une version optimisée du code ci dessus, utilisable pour vos projets cette fois.
Ce sera l’occasion idéale de vos montrer pourquoi le framework Arduino n’est pas adapté aux codes nécessitant de respecter des timings.
En plus je pourrai vous montrer que l’AVR-C c’est pas si compliqué finalement et que vous êtes bien idiot de ne pas vous y intéresser 😉

Discussion

7 réflexions sur “[Tuto/Info] Matrice de leds RGB – partie 2

  1. Bonsoir,
    Je suis toujours avec plaisir ce RV (quasi hebdomadaire).
    Créativité, enthousiasme et compétence, que du bonheur.

    Il y a-t-il un moyen de prendre le train en marche?
    Autrement dit: Où trouver les matrices au meilleur coût?

    Publié par Daniel | 1 février 2014, 20 h 42 min
    • Les matrices étaient disponible en pré-commande dans le courant du moi de novembre dernier. Il n’y a plus de stock pour le moment.

      Plusieurs personnes sont partantes pour une nouvelle pré-commande, il faudrait que je discute avec Brandon de Be-electronique pour voir si il veut refaire une commande groupée.

      Publié par skywodd | 1 février 2014, 21 h 08 min
      • Bien, si cela se fait j’en serais, pour un nombre de matrices fonctions de mon budget du moment.

        Bon dimanche

        Publié par Daniel | 2 février 2014, 8 h 24 min
      • Ok je note 😉

        Quand j’aurai fini ma série d’article je ferai un petit sondage pour voir qui serait intéressé par une seconde préco.

        Si on atteint les 20 pièces on pourra faire un truc (mais pas à 35€, plus 40€. 35€ c’est uniquement si on arrive à 40 pièces comme pour la première préco).

        Publié par skywodd | 2 février 2014, 17 h 11 min
      • A suivre, merci.

        Publié par Daniel | 2 février 2014, 19 h 18 min
  2. comment peut-on faire pour allumer uniquement un pixel ? afin de pouvoir afficher un message voulue

    Publié par jerem | 1 avril 2014, 10 h 50 min
  3. Hi, shame !
    The RGB MATRIX 16×32 are not all equal, even though the magglior part of the pins are equal, those I have bought, not have connections R1, G1, B1 (consinderando; R0, G0, B0) of which there present no connection to the chips (ICN 74HC245, ICN 2026DF ….) to pin D is connected to GND.
    I have using the libraries Arduino, Adafruit and RGBmatrix and you have the image repetition the first or the last 8 lines. Adjusting some code you can make it work better, I think the registers used are different or Shiftregister longer and that their speed or frequeza is very high.

    Publié par Ettore | 12 juin 2015, 11 h 33 min

Skyduino devient Carnet du Maker

Le site Carnet du Maker remplace désormais le blog Skyduino pour tout ce qui touche à l'Arduino, l'informatique et au DIY.