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

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

Bonjour tout le monde !

Dans mon précédent article, je vous ai présenté un code permettant de contrôler des matrices de leds RGB.
Bien que celui-ci soit fonctionnel, il n’en reste pas moins inutilisable en situation réelle de par sa lenteur.

Aujourd’hui je vais vous présenter une première version optimisée du code de mon précédent article.
Cette version sera suivie par une deuxième version encore plus optimisée que je détaillerai dans un article dédié.

Remarque : Ces deux versions optimisées sont d’or et déjà disponibles sur mon github pour les plus impatients ;)
(le code a été conçu pour être compatible avec les cartes Arduino UNO et Mega2560. Encore merci à Icare pour le debug de la version Mega2560)

Etude du code existant

Si vous avez testé le code de mon précédent article, vous aurez remarqué une chose flagrante : il est très lent.

Tellement lent que même avec une unique matrice l’affichage est déjà scintillant.
Et avec 2 matrices (ou plus) l’affichage est carrément clignotant !

NewFile0

Ce problème se confirme en regardant l’état du signal en broche 13 de l’arduino avec un oscilloscope.
Pour rappel : Dans mon code j’utilise la broche 13 comme broche de debug pour mesurer le temps d’exécution de la routine d’affichage.

Même avec 100% du temps processeur alloué à l’affichage le code est toujours trop lent pour donner un résultat "fluide".
Pire encore, le temps entre deux rafraîchissements est si court (pour ne pas dire inexistant, voir l’image ci-dessus) que les leds n’ont pas le temps de s’allumer complètement.

Ce n’est donc vraiment pas génial …

Heureusement ce code n’avait pour but que de vous expliquer la théorie.
Nous allons maintenant passer à la pratique ;)

Pourquoi le code actuel est si lent ?

J’ai dû répéter cette phrase une bonne centaine de fois, mais elle est toujours aussi vraie :
"Le framework Arduino est conçu pour être simple, pas pour être rapide/optimisé."

Et comme vous pouvez le voir, on atteint rapidement les limites de ce qu’il est possible de faire avec le framework Arduino !

Dans le code actuel, ce qui consomme le plus de temps processeur ce sont les appels à la fonction digitalWrite().
Notre mission sera donc de remplacer ces digitalWrite() par quelque chose de (beaucoup) plus rapide.

La solution

"Quelque chose de plus rapide", oui, mais quoi ?
La solution est simple : la manipulation (bas niveau) de ports d’entrées/sorties.

Quoi de plus rapide que de demander directement au processeur de placer telle broche, de tel port, à tel niveau ?
Rien.

La manipulation de ports est la méthode la plus rapide pour changer l’état d’une ou plusieurs broches.
Et contrairement aux idées reçues, ce n’est pas aussi compliqué que ce que l’on peut lire sur certains sites.

Reprenons le code de l’article précédent :

/* Compile time constants */
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

/** 
 * Framebuffer for RGB
 *
 * Array format : [line index] [column pixels bundle index] [color data R, G, B]
 * Data format : ... [R1 G1 B1][R2, G2, B2] ...
 */
static volatile byte framebuffer[NB_LINES_COUNT][NB_COLUMNS_COUNT / 8][3];

/**
 * Possible color enumeration
 */
enum {
  COLOR_RED,
  COLOR_GREEN,
  COLOR_BLUE,
  COLOR_YELLOW,
  COLOR_CYAN,
  COLOR_PINK,
  COLOR_WHITE,
  COLOR_BLACK  
};

/**
 * Set the color of a pixel in the framebuffer.
 * 
 * @param x X position of the pixel.
 * @param y Y position of the pixel.
 * @param color Color to set.
 */
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;
  }
}

/**
 * Get the color of a pixel in the framebuffer.
 * 
 * @param x X position of the pixel.
 * @param y Y position of the pixel.
 * @return The state of the 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() {

  // Setup pins
  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);

  // Init pins
  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);

  // Init frame buffers (all pixels black)
  memset((void*) framebuffer, 0, NB_LINES_COUNT * (NB_COLUMNS_COUNT / 8) * 3);
}

/** Loop */
void loop() {

  // Refresh the display as fast as possible
  refreshDisplay();
}

/** Display scaline refresh routine */
void refreshDisplay() {

  // Scan line index
  static byte scanlineIndex = 0;
  digitalWrite(13, HIGH);

  // Setup control lines and address lines
  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);

  // For each vertical matrix
  for (int vMatrixIndex = NB_VERTICAL_MATRIX - 1; vMatrixIndex >= 0; --vMatrixIndex) {

    // Hardcoded 1/16 scanline with 32x32 matrix

    // Get the current lines buffers
    unsigned int lineOffset = vMatrixIndex * NB_LINES_PER_MATRIX + scanlineIndex;
    volatile byte (*lineBufferA)[3] = framebuffer[lineOffset];
    volatile byte (*lineBufferB)[3] = framebuffer[lineOffset + MATRIX_SCANLINE_SIZE];

    // For each column pixels bundle (8 columns = 1 bundle)
    for (int bundleIndex = (NB_COLUMNS_COUNT / 8) - 1; bundleIndex >= 0; --bundleIndex) {

      // Get column pixels bundle
      volatile byte (*bundleBufferA) = lineBufferA[bundleIndex];
      volatile byte (*bundleBufferB) = lineBufferB[bundleIndex];

      // Get RGB values for line N and N + 16
      byte r1 = bundleBufferA[0];
      byte g1 = bundleBufferA[1];
      byte b1 = bundleBufferA[2];
      byte r2 = bundleBufferB[0];
      byte g2 = bundleBufferB[1];
      byte b2 = bundleBufferB[2];

      // For each bits of each values
      for(int bitIndex = 7; bitIndex >= 0; --bitIndex) {

        // Shift out 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);
      }
    }
  }

  // Trigger latch
  digitalWrite(PIN_OE, LOW);
  digitalWrite(PIN_LAT, LOW);

  // Handle scan line overflow
  if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

    // Reset scan line index
    scanlineIndex = 0; 
    
    // Demo code
    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; 
      }
    }
  }

  digitalWrite(13, LOW);
}

Note 1 : Le code est disponible en téléchargement sur mon github (voir le lien dans l’introduction).
Note 2 : Les commentaires sont en anglais, car j’ai copié/collé mon code directement de Github.

Optimisation – étape 1

Premièrement, faisons disparaître les déclarations de numéros de broches "Arduino".
Nous allons utiliser à la place les ports d’entrées/sorties du microcontrôleur qui correspondent à ces broches.
Cela semble être une étape complexe, mais vous allez le voir c’est assez simple en réalité.

Remarque : si la bibliothèque d’Adafruit et mon code de l’article précédent utilisent les mêmes broches, c’est qu’il a une raison.
La première raison est purement "user-friendly" : ne pas vous perdre en ayant un câblage différent de celui d’Adafruit (qui me sert de référence).
La seconde raison est plus technique, je l’avais évoqué dans l’article précédent : faire en sorte que les broches d’utilités semblables se trouvent sur un même port. Ça tombe bien c’est le cas, quelle coïncidence ;)

Avant de nous lancer dans la manipulation de ports, il faut savoir quels ports manipuler, ça semble logique.
Pour cela nous allons utiliser des tables de correspondance "numéro de broche Arduino" -> "Port AVR" :
Version UNO : http://arduino.cc/en/Hacking/PinMapping168
Version Mega2560 : http://arduino.cc/en/Hacking/PinMapping2560

Grace à ces tables ont obtient les relations suivantes (version UNO uniquement pour simplifier l’article) :
Pour R1, G1, B1, R2, G2, B2 :

Broche Arduino 2 3 4 5 6 7
Broche AVR PD2 PD3 PD4 PD5 PD6 PD7

Pour A, B, C, D :

Broche Arduino A0 A1 A2 A3
Broche AVR PC0 PC1 PC2 PC3

Pour CLK, OE, LAT + led de debug :

Broche Arduino 8 9 10 13
Broche AVR PB0 PB1 PB2 PB5

Juste pour être clair :
PB0 (par exemple) = port B, bit 0 = première broche du port B
PB2 (par exemple) = port B, bit 2 = troisième broche du port B

Du coup le gros pavé de numéros de broches :

/* 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

Deviens :

/* Pin mapping */
// Broches R1, G1, B1, R2, G2, B2 câblées sur PD2~PD7
// Broches A, B, C, D câblées sur PC0~PC3
// Broches CLK, OE, LAT câblées sur PB0~PB2 + led de debug sur PB5
#define DATA_PORT PORTD
#define DATA_DDR DDRD
#define ADDR_PORT PORTC
#define ADDR_DDR DDRC
#define CTRL_PORT PORTB
#define CTRL_PIN PINB
#define CTRL_DDR DDRB
#define CTRL_MASK 0b100111
#define CTRL_CLK_PIN (1 << 0)
#define CTRL_OE_PIN (1 << 1)
#define CTRL_LAT_PIN (1 << 2)
#define CTRL_LED_PIN (1 << 5)

Informations à retenir concernant les ports d’entrées / sorties sur les microcontrôleurs AVR :
- DDRx = Registre de configuration du port "x". Bit à "1" = sortie, bit à "0" = entrée.
- PORTx = Registre du port en sortie du port "x". Bit à "1" = HIGH, bit à "0" = LOW.
- PINx = Registre du port en entrée du port "x". En gros c’est pareil que PORTx, mais en lecture ;)

Du coup, pour chaque port utilisé (trois au total) nous avons besoin :
- du registre de configuration,
- du registre de sortie.

Petite exception cependant concernant le port servant aux broches de contrôles (CLK, OE, LAT) : nous aurons aussi besoin du registre d’entrée (PINx).
Je ne vais pas faire de lecture avec, ho que non, ce serait trop simple (et logique). Je vais faire un truc beaucoup plus tordu. Vous verrez plus tard quoi ;)
Nous aurons aussi besoin des bits représentant chaque broche de contrôle séparément dans un #define.
De même que d’un "masque" permettant de ne modifier QUE les broches de contrôle sans toucher aux autres broches du port.

Pour être franc j’aurai pu appliquer ce principe de "registres + define pour chaque broche + masque" à tout les ports.
Cela aurait eu l’avantage de permettre une configuration de broches plus souple, mais bon. Il faut bien que je vous laisse un peu de travail :)

Hors sujet : Pourquoi définir chaque port utilisé avec un #define ?
Tout simplement parce que dans le "vrai" code disponible sur github ce bloc de #define est entouré d’une macro permettant de sélectionner la bonne carte (UNO ou Mega2560). Les #define changent en fonction de la carte, mais pas le reste du code ;)

Optimisation – étape 2

La vie est belle, passons maintenant à la fonction setup().

Avant la fonction setup() n’était pas franchement jolie :

/** Setup */
void setup() {

  // Setup pins
  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);

  // Init pins
  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);

  // Init frame buffers (all pixels black)
  memset((void*) framebuffer, 0, NB_LINES_COUNT * (NB_COLUMNS_COUNT / 8) * 3);
}

Maintenant elle est beaucoup plus "pro" :

/** Setup */
void setup() {

  // Setup pins
  DATA_DDR |= 0b11111100; // Data port
  DATA_PORT = DATA_PORT & 0b11;

  ADDR_DDR |= 0b1111; // Addr port
  ADDR_PORT = ADDR_PORT & 0b11110000;

  CTRL_DDR |= CTRL_MASK; // Ctrl port + debug led
  CTRL_PORT = (CTRL_PORT & ~CTRL_MASK) | CTRL_OE_PIN;
  
  // Init frame buffers (all pixels black)
  memset((void*) framebuffer, 0, NB_LINES_COUNT * (NB_COLUMNS_COUNT / 8) * 3);

  // Init refresh timer
  MsTimer2::set(1, refreshDisplay);
  MsTimer2::start();
}

Ces deux versions sont strictement identiques (excepté une petite chose à la fin, j’y reviendrai plus tard).

Décomposons un morceau de l’opération :

  DATA_DDR |= 0b11111100; // Place les broches PD2 ~ PD7 en sortie
  DATA_PORT = DATA_PORT & 0b11; // Place les broches PD2 ~ PD7 à LOW

DATA_DDR est un #define pointant sur le registre de configuration du port pour les lignes de données R1, G1, B1, etc.
DATA_PORT est un autre #define pointant lui sur le registre de sortie de ce même port.

Comme vous pouvez le voir, j’ai mis des constantes en dure dans le code pour les masques et les bits des broches (c’pa bien !).

La première ligne place les broches PD2 à PD7 en sortie, et uniquement ces broches. Les autres restent comme elles sont.
L’astuce ici est d’utiliser un OU bit à bit. Vu que "1" = broche en sortie, seuls les "1" dans mon masque modifient la valeur de DATA_DDR.
Les autres broches restent inchangées.

La seconde ligne permet de faire l’inverse, au lieu de mettre des "1" je mets des "0" dans DATA_PORT.
J’utilise donc un ET bit à bit pour garder uniquement les broches qui ne m’intéressent pas. Les broches qui m’intéressent seront à "0" grâce au ET, car elles ne font pas partie du masque.

Vous remarquerez que le masque que j’utilise avec le ET est l’inverse du masque que j’utilise avec le OU.
C’est du calcul binaire : OU + masque = mise à "1", ET + inverse(masque) = mise à "0".

Le principe est le même pour CTRL_DDR et CTRL_PORT, sauf qu’au lieu d’utiliser les constantes en dure j’utilise les #define déclarés en début de code.
Je vous conseille de lire mon article sur le "bitwise" pour plus de détails (c’est des maths, ni plus, ni moins).

Optimisation – étape 3

Avant la fonction loop() ressemblait à ceci :

/** Loop */
void loop() {

  // Refresh the display as fast as possible
  refreshDisplay();
}

Avec ce code le microcontrôleur rafraîchit l’affichage aussi vite que possible puisque loop() ne fait qu’appeler refreshDisplay().
Dans le code "théorique" cela n’était pas un problème. Mais pour une utilisation "réelle" il serait intéressant de pouvoir faire un dessin en fonction d’une action (ou autre).

Du coup on va enlever l’appel de la fonction refreshDisplay() de loop() et utiliser une librairie Arduino bien pratique : MsTimer2

Cette librairie permet d’appeler une fonction à un intervalle de temps précis (exprimé en millisecondes, d’où son nom).
On va se servir de cette librairie pour appeler refreshDisplay() à une fréquence de 1KHz.

Pourquoi 1KHz ?
Tout simplement parce que : 1000Hz / 16 lignes (la taille de la scanline) = 62.5 (en gros 60 images par seconde).
L’idéal aurait été d’avoir 960Hz (pile 60 images par secondes), mais MsTimer2 est limité à des intervalles en millisecondes sans virgule.

Important : il faut obligatoirement avoir la dernière version de la librairie MsTimer2 si vous utilisez une carte Arduino Mega2560.
Si vos matrices restent éteintes à l’exécution du code, c’est que vous n’avez pas la bonne version.

En utilisant cette librairie, on fait d’une pierre deux coups :
- La fonction loop() est maintenant libre pour mettre le code de dessin de notre choix.
- Le rafraîchissement de l’image se fait à une fréquence constante, plus de scintillement possible.

Bonus : la librairie MsTimer2 utilise une horloge interne du microcontrôleur qui génère des interruptions.
Pour faire simple, le code de loop() est automatiquement mis en pause le temps de faire l’affichage puis reprend automatiquement une fois l’affichage effectué.
Tout cela est géré par le hardware, il nous suffit donc de coder la partie dessin.

Pour résumer le code de la fonction loop() deviens (par exemple) :

/** Loop */
void loop() {

  // Demo code
  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; 
    }
  }

  // No flood delay
  delay(10);
}

Optimisation – étape 4

Pour rappel la routine d’affichage ressemblait à ceci :

/** Display scaline refresh routine */
void refreshDisplay() {

  // Scan line index
  static byte scanlineIndex = 0;
  digitalWrite(13, HIGH);

  // Setup control lines and address lines

  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);

  // For each vertical matrix
  for (int vMatrixIndex = NB_VERTICAL_MATRIX - 1; vMatrixIndex >= 0; --vMatrixIndex) {

    // Hardcoded 1/16 scanline with 32x32 matrix

    // Get the current lines buffers
    unsigned int lineOffset = vMatrixIndex * NB_LINES_PER_MATRIX + scanlineIndex;
    volatile byte (*lineBufferA)[3] = framebuffer[lineOffset];
    volatile byte (*lineBufferB)[3] = framebuffer[lineOffset + MATRIX_SCANLINE_SIZE];

    // For each column pixels bundle (8 columns = 1 bundle)
    for (int bundleIndex = (NB_COLUMNS_COUNT / 8) - 1; bundleIndex >= 0; --bundleIndex) {

      // Get column pixels bundle
      volatile byte (*bundleBufferA) = lineBufferA[bundleIndex];
      volatile byte (*bundleBufferB) = lineBufferB[bundleIndex];

      // Get RGB values for line N and N + 16
      byte r1 = bundleBufferA[0];
      byte g1 = bundleBufferA[1];
      byte b1 = bundleBufferA[2];
      byte r2 = bundleBufferB[0];
      byte g2 = bundleBufferB[1];
      byte b2 = bundleBufferB[2];

      // For each bits of each values
      for(int bitIndex = 7; bitIndex >= 0; --bitIndex) {

        // Shift out 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);
      }
    }
  }

  // Trigger latch
  digitalWrite(PIN_OE, LOW);
  digitalWrite(PIN_LAT, LOW);

  // Handle scan line overflow
  if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

    // Reset scan line index
    scanlineIndex = 0; 
  }

  digitalWrite(13, LOW);
}

Pas de pitié pour les digitalWrite(), on va tous les remplacer par de la manipulation de port.
Vous verrez plus tard que cela augmente significativement la performance globale du code ;)

Voilà le résultat :

/** Display scaline refresh routine */
void refreshDisplay() {

  // Scan line index
  static byte scanlineIndex = 0;

  // Setup control lines and address lines
  CTRL_PORT = (CTRL_PORT & ~CTRL_MASK) | CTRL_OE_PIN | CTRL_LAT_PIN | CTRL_LED_PIN;
  ADDR_PORT = (ADDR_PORT & 0b11110000) | scanlineIndex;

  // For each vertical matrix
  for (int vMatrixIndex = NB_VERTICAL_MATRIX - 1; vMatrixIndex >= 0; --vMatrixIndex) {

    // Hardcoded 1/16 scanline with 32x32 matrix

    // Get the current lines buffers
    unsigned int lineOffset = vMatrixIndex * NB_LINES_PER_MATRIX + scanlineIndex;
    volatile byte (*lineBufferA)[3] = framebuffer[lineOffset];
    volatile byte (*lineBufferB)[3] = framebuffer[lineOffset + MATRIX_SCANLINE_SIZE];

    // For each column pixels bundle (8 columns = 1 bundle)
    for (int bundleIndex = (NB_COLUMNS_COUNT / 8) - 1; bundleIndex >= 0; --bundleIndex) {

      // Get column pixels bundle
      volatile byte (*bundleBufferA) = lineBufferA[bundleIndex];
      volatile byte (*bundleBufferB) = lineBufferB[bundleIndex];

      // Get RGB values for line N and N + 16
      byte r1 = bundleBufferA[0];
      byte g1 = bundleBufferA[1];
      byte b1 = bundleBufferA[2];
      byte r2 = bundleBufferB[0];
      byte g2 = bundleBufferB[1];
      byte b2 = bundleBufferB[2];

      // For each bits of each values
      for(int bitIndex = 7; bitIndex >= 0; --bitIndex) {

        // Shift out bits
        byte mask = 1 << bitIndex;
        DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & mask) << 2) | (!!(g1 & mask) << 3) | (!!(b1 & mask) << 4)| (!!(r2 & mask) << 5) | (!!(g2 & mask) << 6) | (!!(b2 & mask) << 7);
        CTRL_PIN = CTRL_CLK_PIN;
        CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
      }
    }
  }

  // Trigger latch
  CTRL_PORT = CTRL_PORT & ~CTRL_MASK;

  // Handle scan line overflow
  if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

    // Reset scan line index
    scanlineIndex = 0;
  }
}

Vous remarquerez que je fais toujours : PORT = (PORT & ~masque) | valeur;
En faisant cela, je garde la valeur courante des bits que je n’utilise pas.
Je pourrai tout aussi bien assigner la valeur directement dans le PORT, mais cela poserait de gros problèmes si vous voulez utiliser les autres broches des ports en question.

Typiquement, si je ne faisais pas : DATA_PORT = (DATA_PORT & 0b11) | blablabla;
Je viendrai modifier systématiquement les valeurs des broches PD0 et PD1 à chaque tour de boucle.
Pas de bol, les broches PD0 / PD1 correspondent au port série qui sert à programmer les cartes Arduino.
Je vous laisse donc imaginer le problème si le port série reste bloqué au démarrage ;)

Vous remarquerez aussi ces deux lignes :

        CTRL_PIN = CTRL_CLK_PIN;
        CTRL_PIN = CTRL_CLK_PIN; // CLK pulse

Plus tôt je vous disais que PINx était le registre en lecture d’un port.
Écrire dans quelque chose en lecture peut sembler incohérent, mais en fait pas totalement ;)

Quand on écrit un "1" dans un registre PINx d’un microcontrôleur AVR, le bit (ou les bits) correspondant dans le registre PORTx sont inversé(s).

Exemple :
(1) PORTx = 0 0 0 0 1 0 0 0
(2) PINx = 0 0 0 0 1 0 0 0
-> PORTx = 0 0 0 0 0 0 0 0
(3) PINx = 0 0 0 0 0 0 1 0
-> PORTx = 0 0 0 0 0 0 1 0
(4) PINx = 0 0 0 0 0 0 1 0
-> PORTx = 0 0 0 0 0 0 0 0

Cela permet de manière très rapide de basculer l’état d’une broche sans faire d’opération complexe.
Pour un signal d’horloge (qui doit être le plus rapide possible) comme ici c’est très pratique.

C’est une des nombreuses fonctionnalités que le framework Arduino n’implémente pas pour rester un minimum générique.
Il y a plein d’autres fonctionnalités de ce type. Ce qu’implémente le framework Arduino n’est qu’une infime proportion de ce qu’un microcontrôleur AVR peut faire.

Le code final de cette première version optimisée

Commenté en français pour l’occasion :

/* Dépendances */
#include <MsTimer2.h>

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

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;

/* Brochage des matrices */
// Broches R1, G1, B1, R2, G2, B2 câblées sur PD2~PD7
// Broches A, B, C, D câblées sur PC0~PC3
// Broches CLK, OE, LAT câblées sur PB0~PB2 + led de debug câblées sur PB5
#define DATA_PORT PORTD
#define DATA_DDR DDRD
#define ADDR_PORT PORTC
#define ADDR_DDR DDRC
#define CTRL_PORT PORTB
#define CTRL_PIN PINB
#define CTRL_DDR DDRB
#define CTRL_CLK_PIN (1 << 0)
#define CTRL_OE_PIN (1 << 1)
#define CTRL_LAT_PIN (1 << 2)
#define CTRL_LED_PIN (1 << 5)
#define CTRL_MASK (CTRL_CLK_PIN | CTRL_OE_PIN | CTRL_LAT_PIN  | CTRL_LED_PIN)

/** 
 * Buffer de dessin
 *
 * Format de tableau : [index de ligne] [index de colonne / 8] [composantes R, G, B]
 * Format de données : ... [R1 G1 B1][R2, G2, B2] ...
 */
static volatile byte framebuffer[NB_LINES_COUNT][NB_COLUMNS_COUNT / 8][3];

/**
 * Énumération des différentes couleurs utilisables.
 */
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
  DATA_DDR |= 0b11111100; // Data port
  DATA_PORT = DATA_PORT & 0b11;

  ADDR_DDR |= 0b1111; // Addr port
  ADDR_PORT = ADDR_PORT & 0b11110000;

  CTRL_DDR |= CTRL_MASK; // Ctrl port + debug led
  CTRL_PORT = (CTRL_PORT & ~CTRL_MASK) | CTRL_OE_PIN;

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

  // Initialisation de l'horloge de rafraîchissement de l'affichage
  MsTimer2::set(1, refreshDisplay);
  MsTimer2::start();
}

/** Loop */
void loop() {

  // 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; 
    }
  }

  // Délai d'attente avant le dessin suivant
  delay(10);
}

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

  // Index de la scan line
  static byte scanlineIndex = 0;

  // Mise en place de l'adresse de la ligne en cours et des broches de contrôle
  CTRL_PORT = (CTRL_PORT & ~CTRL_MASK) | CTRL_OE_PIN | CTRL_LAT_PIN | CTRL_LED_PIN;
  ADDR_PORT = (ADDR_PORT & 0b11110000) | scanlineIndex;

  // 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;
        DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & mask) << 2) | (!!(g1 & mask) << 3) | (!!(b1 & mask) << 4)| (!!(r2 & mask) << 5) | (!!(g2 & mask) << 6) | (!!(b2 & mask) << 7);
        CTRL_PIN = CTRL_CLK_PIN;
        CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
      }
    }
  }

  // Active le "latch" (déclenche l'affichage de la ligne)
  CTRL_PORT = CTRL_PORT & ~CTRL_MASK;

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

    // Reprise à la première ligne
    scanlineIndex = 0;
  }
}

Résultat

NewFile1

Si on upload le nouveau code et que l’on garde le même zoom sur l’oscilloscope on se rend compte de suite de l’amélioration !
Trois affichages ont le temps de s’effectuer (à très exactement 1KHz grâce à MsTimer2) là où avant un seul affichage était possible.

NewFile2

Et si l’on zoom un peu plus de manière à n’avoir qu’un seul cycle d’une milliseconde (1KHz), on remarque qu’un peu plus de 50% du temps processeur est libre pour notre fonction de dessin.

L’amélioration globale est terme de performance est donc de 6.5x !
C’est à la fois énorme et assez mitigé.

Vous verrez dans mon prochain article que l’on peut encore doubler les performances du code en évitant les boucles "à l’envers" lors de l’affichage.

About these ads

Discussion

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

  1. Salut,
    Le cerveau est en feu ;)
    Très beau travail, toutes mes félicitations

    Publié par Icare Petibles | 6 février 2014, 1 h 16 min
  2. Super intéressant. Ca va bien me servir!

    Publié par Nicolas | 6 février 2014, 7 h 36 min
  3. Bonjour,

    Comment faire pour allumer un seul pixel de notre choix ? car j’aimerais afficher un message sur 2 matrice mais je souhaite d’abord allumer 1 pixel de mon choix.

    Je vous remercie de votre aide

    Publié par gé1307 | 1 avril 2014, 17 h 14 min
    • Bonjour,
      Je pense que Fabien est mieux placé que moi pour répondre, mais il suffit de faire :
      x = ta valeur (0<= x <= 63)
      y = ta valeur (0 <= y <= 31
      color = ta couleur (voir liste)
      et lors du passage de la boucle par setPixelAt(x, y, color)
      ton pixel sera allumé avec la couleur choisie.
      Pour éteindre un pixel tu fait la même chose mais avec la couleur noir.

      Publié par Icare Petibles | 1 avril 2014, 18 h 54 min
      • Bonsoir,

        Oui j’ai étudier le code, je pensais à la même chose.
        Je vais essayer demain, il me suffirais donc d’appeler a répétition la fonction setPixel ?
        Également lors de la compilation du code j’ai une erreur qui revient: " error: ‘MsTimer2′ has not been declared"
        j’ai essayer de mettre le timer2.h dans le dossier du code, mais rien a faire … peut-être du à la déclaration je ne c’est pas du tout
        j’utilise le mega2560

        Publié par gé1307 | 1 avril 2014, 21 h 41 min
  4. Re,
    Exact poursetPixel
    Pour MsTimer2, tu peux faire ceci:
    - copie le .h et .cpp dans le même répertoire que ton .ino et tu mets #include "MsTimer2"
    - ou bien tu installes la librairie dans l’environnement Arduino et tu mets #include
    @+

    Publié par Icare Petibles | 1 avril 2014, 22 h 08 min
  5. Bonjour,

    je te remercie de ton aide j’ai résolue mon problème du timer, mais en faisant setpixel en permanence ex : void loop() { setPixelAt(2, 2, COLOR_RED);}, rien ne s’affiche sur les dalles, si vous auriez une idée du problème ?

    Publié par gé1307 | 2 avril 2014, 8 h 46 min
    • Bonjour,
      Tu utilises quoi comme carte UNO, MEGA ou 1284P pour que je regarde ?

      Publié par Icare Petibles | 2 avril 2014, 9 h 43 min
      • Re,
        j’utilise la MEGA 2560

        Publié par gé1307 | 2 avril 2014, 9 h 51 min
      • Re,
        Tu as mis quoi en nombre de matrices verticale et horizontale ?

        Publié par Icare Petibles | 2 avril 2014, 9 h 52 min
      • Re,
        j’ai mis 1 et 1 j’essaye de pouvoir le faire déjà sur une dalles.

        Mais le code de base ne fait rien aussi je viens de voir, alors que celui de la partie 2 fonctionne bien. et j’ai bien changer les ports pour le MEGA qui se situe en portA pour les données et portB pour les signaux de commande (latch, oe et clk)

        Publié par gé1307 | 2 avril 2014, 10 h 10 min
  6. Re,
    Ton problème vient certainement de là.
    Essaye de tout remettre dans l’état d’origine (comme dans le tuto de Skywodd) pour voir le fonctionnement.
    Après tu me dis ce que tu souhaites faire et on verra comment y arriver.
    Je pense qu’il est urgent de procéder par étapes. S’il y a une démo 1, 2 et 3 (+ la version 1284P) c’est qu’il y a des différences.
    @+

    Publié par Icare Petibles | 2 avril 2014, 13 h 39 min
    • Re,

      oui je suis repartit des 3 versions, il y à uniquement cette version 3 qui me pose des problèmes car absolument rien ne ce passe …

      Quand je passe avec la version2 j’arrive bien à allumer un pixel (pas au coordonnées voulu mais j’y arrive), alors que avec la version3 même le code fournis dans le tuto les dalles ne clignote pas au différentes couleur.

      En tout cas je te remercie de ton aide que tu m’apporte :)

      Publié par gé1307 | 2 avril 2014, 14 h 33 min
      • Tout les codes sur le github devrait fonctionner sans modifications.
        Si il ne se passe rien vérifie que tu utilises bien la version modifiée de MsTImer2 que m’as fourni Icare (disponible sur le github).

        Publié par skywodd | 9 avril 2014, 12 h 31 min
      • Salut,
        J’ai recontrôlé tout les codes de github et ils fonctionnent tous sans aucun problème.

        Publié par Icare Petibles | 9 avril 2014, 19 h 56 min
      • Salut tous le monde,
        Je pense que le problème viens de mes dalles que j’ai pas pris au même endroit (ce que j’ai fait maintenant ^^).
        La gestion semble être tous à fait autrement malgré que les signaux de commande sois identique (ABCD…) et qu’elle étais en 16X16 ce que j’avais changer dans le programme.
        Enfin maintenant je me suis commande celle sur SparkFun en 16X32 et je vous donnerais retour à la réception mon problème étais bien ceci.

        Et je vous remercie pour toute vos réponse et votre aide :)

        Publié par gé1307 | 9 avril 2014, 21 h 14 min

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Archives

Wow. Dogecoin. Amaze.

Laissez un tip en Dogecoin

DMMNFk6WBVTpx2Wu1Z35GL61QXSd6r6WQx

Suivre

Recevez les nouvelles publications par mail.

Joignez-vous à 678 followers