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

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

Bonjour tout le monde !

Je continu aujourd’hui ma série d’articles sur les matrices de leds RGB, avec dans cet article une seconde version optimisée de mon code.

Cette version servira de transition entre le code pour cartes Arduino que je vous ai présenté jusqu’à présent et le code AVR bas niveau que je vous présenterai dans un prochain article.

Le code Arduino n’était qu’une étape vers la fabrication d’un contrôleur de matrices « maison » supportant bien plus que 8 malheureuses couleurs.
Libre à vous de choisir si vous voulez rester sur la version Arduino que je vais vous présenter dans cet article, ou si vous voulez passer à la version bas niveau de mon prochain article 😉

Dans cet article je vais vous montrer comment encore plus optimiser la version déjà bien optimisée de mon précédent article.

Comme toujours le code (compatible Arduino UNO et Arduino Mega2560) est disponible sur mon github :
https://github.com/skywodd/RGB_Matrix_Arduino_AVR

Pour rappel voici le code de mon précédent article qui va nous servir de base :

/* Dependencies */
#include <MsTimer2.h>

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

/* Pin mapping */
// R1, G1, B1, R2, G2, B2 hard-wired on PD2~PD7
// A, B, C, D hard-wired on PC0~PC3
// CLK, OE, LAT hard-wired on PB0~PB2 and LED 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)

/** 
 * 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 color 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
  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();
}

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

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

Note : Le code est commenté en français dans mon précédent article 😉

Comme vous pouvez le voir, on passe plus de temps à manipuler des pointeurs et à faire des boucles qu’a réellement afficher des pixels.
C’est comment dire … assez bête 😉

Notre mission d’optimisation sera donc de supprimer ces boucles disgracieuses et d’améliorer l’utilisation de la mémoire pour éviter de traîner des pointeurs un peu partout.

Parenthèse rapide : une boucle, ça semble anodin, mais une boucle mal codée peut massacrer totalement les performances d’un code.
Essayez de faire une boucle de 0 à 300 000 par exemple, cela devrait prendre au moins une seconde sur une carte Arduino classique 😉

Optimisation « Le retour » – Etape 1

La première étape d’optimisation est vraiment très simple : utiliser des puissances de 2 pour les constantes de tailles.

La déclaration des constantes :

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

Deviens donc :

static const byte NB_LINES_PER_MATRIX = 32;   // DOIT être une puissance de 2
static const byte NB_COLUMNS_PER_MATRIX = 32; // DOIT être une puissance de 2
static const byte MATRIX_SCANLINE_SIZE = 16;  // DOIT être une puissance de 2

En gros, il suffit ici d’ajouter un commentaire pour ne pas l’oublier par la suite 😉

Les puissances de 2 ont un gros avantage : elles sont facilement manipulables.
Un décalage de bit revient à faire une multiplication ou une division par 2^x suivant le sens.
Un modulo (division avec reste) peut se faire avec un simple ET bit à bit (très rapide !).

Les puissances de 2 sont aussi idéales pour faire des masques binaires et ça, le compilateur apprécie énormément.
Et plus le compilateur apprécie, plus il optimise et surtout il optimise mieux.

Optimisation « Le retour » – Etape 2

La seconde optimisation est beaucoup plus complexe : gérer à la main les dimensions du tableau « framebuffer » qui contient les données à afficher.

Dans l’ancien code on laissez le compilateur gérer la matrice à 3 dimensions :

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

Pour le développeur c’est super pratique : on a les deux dimensions X et Y, plus une troisième dimension pour les 3 composantes R, G, B.
Tout les calculs d’index sont gérés par le compilateur, on a juste à utiliser les crochets pour accéder à un élément.
Tout va bien dans le meilleur des mondes possibles.

… ou pas.
Le compilateur optimise l’agencement du tableau suivant des règles qui lui sont propres
Rien dans la norme C/C++ ne force le compilateur à suivre un agencement bien précis.

Du coup cela pose deux (gros) problèmes :
– On ne peut pas accéder au tableau sans passer par les crochets de chaque dimension.
(en réalité, on peut en « castant » le tableau en byte*, mais bon je ne vous le conseille pas)
– On ne sait pas comment le compilateur fait son truc. Si ça se trouve, le tableau est agencé de telle manière que nos accès « à l’envers » ne sont pas du tout optimisés.

Comment résoudre ces problèmes ? Tout simplement en gérant à la main l’accès aux différentes dimensions.
Et quitte à gérer à la main le tableau, autant faire en sorte qu’il soit de base dans le bon sens pour l’affichage.
Cela supprimera ainsi toutes les boucles « à l’envers ».

La déclaration du tableau devient donc :

/** 
 * Framebuffer for RGB
 *
 * Array format : [scan line index] [interlaced pixels data stream R, G, B]
 * Data format : ... [R1 G1 B1][R2, G2, B2] ...
 */
static volatile byte framebuffer[MATRIX_SCANLINE_SIZE][(NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (NB_COLUMNS_COUNT / 8) * 3];

On garde deux dimensions :
– une pour chaque ligne de la scanline,
– une pour les données de la ligne en question.
Cela simplifiera énormément l’accès aux données de chaque ligne dans la routine d’affichage.

Concernant la taille de la seconde dimension, rien de bien compliqué :
(1) = le nombre de lignes total / le nombre de lignes de la scanline (pour avoir chaque ligne multiple de la scanline entrelacée)
(2) = le nombre de colonnes divisé par 8 (8 bits = 1 octet) * (1)
taille totale = 3 composantes rouge, vert, bleu * (2)

Optimisation « Le retour » – Étape 2 (suite)

Comme on doit maintenant gérer le tableau à la main il va nous falloir une fonction pour déterminer l’offset (le décalage en nombre d’octets) par rapport au début du tableau pour une coordonnée XY donnée.

Pour faire simple on donne une coordonnée XY à la fonction et elle nous retourne l’index dans le tableau qui nous intéresse.

Voici la fonction en question :

/**
 * Compute the linear offset in the matrix buffer for the given coordinates.
 *
 * @param x X position of the pixel.
 * @param y Y position of the pixel.
 * @return The linear offset in the matrix buffer for the given coordinates.
 */
static inline unsigned int matrixLinearOffset(byte x, byte y) {
  unsigned int xOffset = (x / 8) * (NB_LINES_PER_MATRIX / MATRIX_SCANLINE_SIZE) * 3;
  unsigned int yOffset = (y / NB_LINES_PER_MATRIX) * (NB_LINES_PER_MATRIX / MATRIX_SCANLINE_SIZE) * (NB_COLUMNS_COUNT / 8) * 3;
  unsigned int yInterlacedOffset = ((y & (NB_LINES_PER_MATRIX - 1)) / MATRIX_SCANLINE_SIZE) * 3;
  return yOffset + xOffset + yInterlacedOffset;
}

Elle se décompose en trois calculs :
– le calcul d’offset pour X,
– le calcul d’offset pour Y,
– le calcul d’offset pour les lignes entrelacées.

En mémoire on obtient quelque chose de ce style :
[R1, G1, B1 (ligne N, colonnes 0~7)] [R2, G2, B2 (ligne N+16, colonnes 0~8)] [R1, G1, B1 (ligne N, colonnes 8~15)] [R2, G2, B2 (ligne N+16, colonnes 8~15)] …

L’entrelacement des lignes N et N + 16 est pénible à gérer, mais permettra par la suite de lire un flot de données en continu sans avoir plusieurs pointeurs comme c’était le cas avant.

Je vous laisse le plaisir de comprendre vous même les calculs 🙂
J’avoue avoir mis un certain temps avant d’avoir le bon résultat, et je ne suis pas encore totalement sûr que ce soit bon.

Remarque : dans l’expression « A & (B – 1) », si B est une puissance de deux cela équivaut à « A % B » 😉
Normalement le modulo est une opération très lente, mais avec des puissances de 2 c’est ultra rapide.

Optimisation « Le retour » – Étape 2 (suite et fin)

Maintenant que l’on dispose d’une fonction magique pour calculer l’index dans le tableau il suffit de l’intégrer dans le code des fonctions setPixelAt() et getPixelAt().

Ceci :

/**
 * 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];
  // ...
}

/**
 * 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 color of the pixel.
 */
static byte getPixelAt(const byte x, const byte y) {
  volatile byte* pixel = framebuffer[y][x / 8];
  // ...
}

Deviens donc cela :

/**
 * 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 & (MATRIX_SCANLINE_SIZE - 1)] + matrixLinearOffset(x, y);
  // ...
}

/**
 * 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 color of the pixel.
 */
static byte getPixelAt(const byte x, const byte y) {
  volatile byte* pixel = framebuffer[y & (MATRIX_SCANLINE_SIZE - 1)] + matrixLinearOffset(x, y);
  // ...
}

Vous remarquerez le magnifique « y modulo MATRIX_SCANLINE_SIZE  » en puissance de deux 😉

Optimisation « Le retour » – Etape 3

Le plus dure est derrière nous, la mémoire est maintenant agencée pile-poil comme il faut !
Il ne reste plus qu’as faire le ménage dans la fonction d’affichage.

Avons tout reposé sur des pointeurs et sur des boucles :

      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
      }

Les pointeurs sont des casses-tête sans nom à gérer et les boucles font perdre en performances quand elles sont appelées trop souvent.

Nous allons donc faire la peau à la boucle de 7 à 0 pour l’envoi des bits et aux différents pointeurs.

/** Send a whole column pixels bundle */
static void sendColumnBundle(byte **lineBufferPtr) {

  // Hardcoded 1/16 scanline with 32x32 matrix

  // Get RGB values for line N and N + 16
  byte b2 = *(*lineBufferPtr);
  byte g2 = *(--(*lineBufferPtr));
  byte r2 = *(--(*lineBufferPtr));
  byte b1 = *(--(*lineBufferPtr));
  byte g1 = *(--(*lineBufferPtr));
  byte r1 = *(--(*lineBufferPtr));
  --(*lineBufferPtr);

  // MSB FIRST
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 128) << 2) | (!!(g1 & 128) << 3) | (!!(b1 & 128) << 4) | (!!(r2 & 128) << 5) | (!!(g2 & 128) << 6) | (!!(b2 & 128) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 64) << 2) | (!!(g1 & 64) << 3) | (!!(b1 & 64) << 4) | (!!(r2 & 64) << 5) | (!!(g2 & 64) << 6) | (!!(b2 & 64) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 32) << 2) | (!!(g1 & 32) << 3) | (!!(b1 & 32) << 4) | (!!(r2 & 32) << 5) | (!!(g2 & 32) << 6) | (!!(b2 & 32) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 16) << 2) | (!!(g1 & 16) << 3) | (!!(b1 & 16) << 4) | (!!(r2 & 16) << 5) | (!!(g2 & 16) << 6) | (!!(b2 & 16) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 8) << 2) | (!!(g1 & 8) << 3) | (!!(b1 & 8) << 4) | (!!(r2 & 8) << 5) | (!!(g2 & 8) << 6) | (!!(b2 & 8) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 4) << 2) | (!!(g1 & 4) << 3) | (!!(b1 & 4) << 4) | (!!(r2 & 4) << 5) | (!!(g2 & 4) << 6) | (!!(b2 & 4) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 2) << 2) | (!!(g1 & 2) << 3) | (!!(b1 & 2) << 4) | (!!(r2 & 2) << 5) | (!!(g2 & 2) << 6) | (!!(b2 & 2) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 1) << 2) | (!!(g1 & 1) << 3) | (!!(b1 & 1) << 4) | (!!(r2 & 1) << 5) | (!!(g2 & 1) << 6) | (!!(b2 & 1) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
}

Premièrement, la boucle est « unrolled » (déroulée).
En faisant cela on copie du code, ce qui n’est normalement pas bien du tout et totalement contraire à la sacro-sainte philosophie du « Ne vous répétez pas ».

Certes, c’est moche, c’est une source de bug si on rate son copié/collé, mais c’est ULTRA RAPIDE.
Plus de test de boucle, plus de variable, plus de calcul intermédiaire de masque, tout est en dure à la compilation.
Le processeur interprète le bloc à la vitesse de la lumière sans faire le moindre saut d’adresse.
Du coup ça va forcément beaucoup plus vite 😉

Mais ce n’est pas tout.
Comme la mémoire est agencée de manière optimale nous n’avons plus besoin de gérer un index.
On lit un octet puis on passe au suivant, et ainsi de suite jusqu’à la fin du tableau.

Vous remarquerez que je lis le tableau en décrémentant le pointeur à chaque fois au lieu de l’incrémenter.
En partant de la fin du tableau je fais ultra simplement l’équivalent des boucles « à l’envers » de l’ancien code.
Quand j’arrive au début du tableau c’est que j’ai fini l’affichage 😉

Vous remarquerez aussi que je fais une « pré-décrémentation » et non une « post-décrémentation ».
Faire :

  byte b2 = *((*lineBufferPtr)--);
  byte g2 = *((*lineBufferPtr)--);
  byte r2 = *((*lineBufferPtr)--);
  byte b1 = *((*lineBufferPtr)--);
  byte g1 = *((*lineBufferPtr)--);
  byte r1 = *((*lineBufferPtr)--);

semblerait beaucoup plus logique, mais aurait été deux fois plus lent 😉

Post-décrémenté (ou post-incrémenté) nécessite de stocker la valeur actuelle avant de la modifier, ce qui prend du temps.
Dans un code « normal » ~62 nanosecondes de plus ou de moins n’est pas un problème. Mais dans notre cas, cela peut vite devenir un très gros problème quand on doit respecter des timings assez serrés.

Optimisation « Le retour » – Etape 4

Maintenant que la partie « sensible » du code est optimisée, il suffit de nettoyer le reste des boucles de la fonction d’affichage.

Ainsi le corps de la fonction refreshDisplay() :

/** 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) {
      // ...
    }
  }

  // Trigger latch
  CTRL_PORT = CTRL_PORT & ~CTRL_MASK;

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

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

Deviens :

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

  // Get line buffer
  byte *lineBuffer = (byte*) framebuffer[scanlineIndex];
  lineBuffer += (NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (NB_COLUMNS_COUNT / 8) * 3 - 1;

  // For each pixels bundle of matrix 4/4, 3/3, 2/2 or 1/1
  sendColumnBundle(&lineBuffer);
  sendColumnBundle(&lineBuffer);
  sendColumnBundle(&lineBuffer);
  sendColumnBundle(&lineBuffer);

  // For each pixels bundle of matrix 3/4, 2/3, 1/2
  if (NB_HORIZONTAL_MATRIX * NB_VERTICAL_MATRIX >= 2) {
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
  }

  // For each pixels bundle of matrix 2/4, 1/3
  if (NB_HORIZONTAL_MATRIX * NB_VERTICAL_MATRIX >= 3) {
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
  }

  // For each pixels bundle of matrix 1/4
  if (NB_HORIZONTAL_MATRIX * NB_VERTICAL_MATRIX >= 4) {
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
  }

  // 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 la ligne permettant d’obtenir l’unique pointeur « lineBuffer » pour les données de la ligne à afficher.
De même qu la ligne juste après permettant de se déplacer à la fin du tableau de données de cette ligne.

Vous remarquerez aussi que les boucles de traitement de chaque matrice et de chaque lot de 8 colonnes ont été déroulées.

Le code est maintenant ultra rapide à exécuter.
Le processeur passe d’une traite à travers le code de la fonction d’affichage sans faire le moindre détour ou boucle.

Remarque : Les if permettant de transmettre le bon nombre de colonnes en fonction du nombre de matrices sont automatiquement optimisés à la compilation (garder un if avec une valeur fixe en argument n’a strictement aucun intérêt donc le compilateur fait le ménage).

Le code final

/* 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;   // DOIT être une puissance de 2
static const byte NB_COLUMNS_PER_MATRIX = 32; // DOIT être une puissance de 2
static const byte MATRIX_SCANLINE_SIZE = 16;  // Ne pas modifier sinon un désastre planétaire se produira.

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 de scanline] [flot de données entrelacées des pixels de la ligne]
 * Format de données : ... [R1 G1 B1][R2, G2, B2] ...
 */
static volatile byte framebuffer[MATRIX_SCANLINE_SIZE][(NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (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  
};

/**
 * Calcul l'index dans le tableau pour les coordonnées données.
 *
 * @param x La position X du pixel.
 * @param y La position Y du pixel.
 * @return L'index dans le tableau pour les coordonnées données.
 */
static inline unsigned int matrixLinearOffset(byte x, byte y) {
  unsigned int xOffset = (x / 8) * (NB_LINES_PER_MATRIX / MATRIX_SCANLINE_SIZE) * 3;
  unsigned int yOffset = (y / NB_LINES_PER_MATRIX) * (NB_LINES_PER_MATRIX / MATRIX_SCANLINE_SIZE) * (NB_COLUMNS_COUNT / 8) * 3;
  unsigned int yInterlacedOffset = ((y & (NB_LINES_PER_MATRIX - 1)) / MATRIX_SCANLINE_SIZE) * 3;
  return yOffset + xOffset + yInterlacedOffset;
}

/**
 * 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 & (MATRIX_SCANLINE_SIZE - 1)] + matrixLinearOffset(x, y);
  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 & (MATRIX_SCANLINE_SIZE - 1)] + matrixLinearOffset(x, y);
  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, MATRIX_SCANLINE_SIZE * (NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (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);
}

/** Transmet un lot de 8 colonnes en une seule fois */
static void sendColumnBundle(byte **lineBufferPtr) {

  // Scan line 1/16 avec matrices 32x32

  // Récupère les valeurs RVB des deux lignes du lot de colonnes en cours
  byte b2 = *(*lineBufferPtr);
  byte g2 = *(--(*lineBufferPtr));
  byte r2 = *(--(*lineBufferPtr));
  byte b1 = *(--(*lineBufferPtr));
  byte g1 = *(--(*lineBufferPtr));
  byte r1 = *(--(*lineBufferPtr));
  --(*lineBufferPtr);

  // MSB FIRST
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 128) << 2) | (!!(g1 & 128) << 3) | (!!(b1 & 128) << 4) | (!!(r2 & 128) << 5) | (!!(g2 & 128) << 6) | (!!(b2 & 128) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 64) << 2) | (!!(g1 & 64) << 3) | (!!(b1 & 64) << 4) | (!!(r2 & 64) << 5) | (!!(g2 & 64) << 6) | (!!(b2 & 64) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 32) << 2) | (!!(g1 & 32) << 3) | (!!(b1 & 32) << 4) | (!!(r2 & 32) << 5) | (!!(g2 & 32) << 6) | (!!(b2 & 32) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 16) << 2) | (!!(g1 & 16) << 3) | (!!(b1 & 16) << 4) | (!!(r2 & 16) << 5) | (!!(g2 & 16) << 6) | (!!(b2 & 16) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 8) << 2) | (!!(g1 & 8) << 3) | (!!(b1 & 8) << 4) | (!!(r2 & 8) << 5) | (!!(g2 & 8) << 6) | (!!(b2 & 8) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 4) << 2) | (!!(g1 & 4) << 3) | (!!(b1 & 4) << 4) | (!!(r2 & 4) << 5) | (!!(g2 & 4) << 6) | (!!(b2 & 4) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 2) << 2) | (!!(g1 & 2) << 3) | (!!(b1 & 2) << 4) | (!!(r2 & 2) << 5) | (!!(g2 & 2) << 6) | (!!(b2 & 2) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
  DATA_PORT = (DATA_PORT & 0b11) | (!!(r1 & 1) << 2) | (!!(g1 & 1) << 3) | (!!(b1 & 1) << 4) | (!!(r2 & 1) << 5) | (!!(g2 & 1) << 6) | (!!(b2 & 1) << 7);
  CTRL_PIN = CTRL_CLK_PIN;
  CTRL_PIN = CTRL_CLK_PIN; // CLK pulse
}

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

  // Récupère un pointeur sur les données de la ligne à afficher et déplace celui-ci à la fin des données
  byte *lineBuffer = (byte*) framebuffer[scanlineIndex];
  lineBuffer += (NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (NB_COLUMNS_COUNT / 8) * 3 - 1;

  // Pour chaque colonnes de la matrice 4/4, 3/3, 2/2 ou 1/1
  sendColumnBundle(&lineBuffer);
  sendColumnBundle(&lineBuffer);
  sendColumnBundle(&lineBuffer);
  sendColumnBundle(&lineBuffer);

  // Pour chaque colonnes de la matrice 3/4, 2/3 ou 1/2
  if (NB_HORIZONTAL_MATRIX * NB_VERTICAL_MATRIX >= 2) {
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
  }

  // Pour chaque colonnes de la matrice 2/4 ou 1/3
  if (NB_HORIZONTAL_MATRIX * NB_VERTICAL_MATRIX >= 3) {
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
  }

  // Pour chaque colonnes de la matrice 1/4
  if (NB_HORIZONTAL_MATRIX * NB_VERTICAL_MATRIX >= 4) {
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
    sendColumnBundle(&lineBuffer);
  }

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

NewFile3

Si on upload le nouveau code et que l’on garde le même zoom sur l’oscilloscope qu’avec l’ancien, on peut voir une amélioration de presque 50% en terme de vitesse.

NewFile4

Un petit zoom sur le signal nous confirme l’amélioration significative de vitesse.
Le code est maintenant 13 fois plus rapide que le code Arduino de mon premier article 😉

En deux phases d’optimisation on est passé d’un code extrêmement lent et inutilisable, à un code aussi rapide que l’éclair et 100% fonctionnels en situation réelle.

Il ne vous reste maintenant plus qu’à remplacer mon code de démonstration dans loop() par quelque chose de plus intéressant.

Si j’ai des démonstrations sympa j’en ferai un article.
Alors soyez créatif, vendez-moi du rêve 😉

Pour les plus téméraires, je vous retrouve ce week-end avec la suite du processus d’optimisation.
Comme je vous le disais en introduction, je vais passer à la vitesse supérieure, fini le code Arduino.

J’entame le développement de mon contrôleur de matrices, la première version sera à base d’ATmega1284p 😉
Mon objectif est d’atteindre les 4096 couleurs avec une matrice et (au minimum) 64 couleurs avec 4 matrices reliées à un même contrôleur.
Pour cela je vais ressortir mon manuel du parfait petit développeur en assembleur, ça va être autre chose que du C/C++ de fillette 😉

Pour les autres, je vous souhaite bon WE en avance 😉

Discussion

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

  1. Du très beau travail, un tout grand merci à toi !

    Publié par Marlock | 6 février 2014, 20 h 36 min
  2. Encore des maux de tête en perspective.
    Moi je reste admiratif devant tant de maîtrise et de technique.
    Moi à chaque que je m’élève je me brulle les ailes 🙂

    Publié par Icare Petibles | 6 février 2014, 21 h 23 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.