Skyduino:~#
Articles
arduino, C/C++, programmation, projet

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

Bonjour tout le monde !

Avec un peu beaucoup de retard voici venu la partie 5 de ma série d’articles sur les matrices de leds RGB ;)
Dans cette cinquième partie, je vais vous parler de mon nouveau code et de comment j’ai divisé par ~10 son temps d’exécution par rapport à l’ancienne version de la partie 4 (aka "demo 3" sur le github).

Attention : dans cet article il y aura des calculs mathématiques et de l’assembleur (mais pas beaucoup), âmes sensibles s’abstenir :)

Préface

Avant de commencer, je souhaiterai mettre certaines choses au clair concernant les matrices et mes codes pour celles-ci.
J’ai eu un bon nombre de mails et de commentaires sur divers points auxquels je souhaiterai répondre une bonne fois pour toutes.

Premièrement, mes codes des parties 1 à 4 ont été conçus pour fonctionner avec des matrices de 32×32 pixels et une scanline de 1/16.
Même si théoriquement ces codes sont suffisamment "génériques" pour marcher avec n’importe quel type de matrice. Il faut obligatoirement revérifier tous les calculs et réécrire la fonction d’envoi de données parallèles en cas de changement de scanline.

Pour faire simple : si vous pas avoir matrices 32×32 avec scanline 1/16, code pas marcher. Même si vous changer constantes dans fichier, car constantes pas magiques.

C’est d’autant plus vrai que certaines matrices fonctionnent en logique inversée (0 = led allumée, 1 = led éteinte).
Et que d’autres sont même câblées à l’envers pour éviter d’avoir à envoyer les données à l’envers (ce qui au final est plus énervant qu’autre chose).

Donc non, je ne compte pas reprendre mon code pour chaque scanline, taille et type de matrice disponible.
On m’a demandé du 1/16, 1/8, 1/4, en 32×32, 16×32, 16×16, le tout avec et sans logique inversée.
Faites le calcul, ça ferait 18 variantes du même code à écrire/optimiser à la main …
Je n’ai ni le temps, ni l’envie et encore moins le matériel pour cela.

Deuxièmement, les versions du code fournies dans les articles précédents se suivent et se remplacent. La version 2 est une évolution de la version 1 avec une amélioration en terme de vitesse. Il en va de même pour la version 3 et le code optimisé que je vais vous présenter aujourd’hui.

Par conséquent, il est inutile de me demander "pourquoi le code donne des résultats pas génial ?" si vous utilisez la version 1 ou 2 !
Si vous voulez un résultat correct il faudra au minimum le code de la partie 3, ou mieux, la dernière version du code disponible sur mon github.

Pour finir, le code que je vais vous présenter dans cet article n’est PAS la dernière version du code.
J’ai pris énormément de retard dans mes articles, la dernière version du code a bien évolué depuis et gère maintenant jusqu’à 32768 couleurs.
La dernière version du code fera l’objet d’une description détaillée dans un autre article dédié "as soon as possible".

Voilà, j’espère avoir répondu à la plupart des questions récurrentes ;)

Objectifs

Les objectifs pour ce 5iéme article sont de :
– Préparer le terrain pour l’ajout du support de plusieurs nuances de couleurs.
– Réduire autant que possible le temps d’exécution de la routine d’affichage.
– Vous faire avoir des cauchemars la nuit avec du code assembleur inline (niark niark niark).

J’espère que vous avez le cœur bien accroché ;)
Leeeet’s go !

Etude de l’existant

Pour rappel le code de la partie 4 était le suivant :

/* 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;   // MUST be a power of 2
static const byte NB_COLUMNS_PER_MATRIX = 32; // MUST be a power of 2
static const byte MATRIX_SCANLINE_SIZE = 16;  // MUST be 8, 16 or 32

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 */
#if defined(__AVR_ATmega2560__) // For Arduino Mega2560
// R1, G1, B1, R2, G2, B2 hard-wired on PA2~PA7
// A, B, C, D hard-wired on PF0~PF3
// CLK, OE, LAT hard-wired on PB3~PB1 and LED PB7
#define DATA_PORT PORTA
#define DATA_DDR DDRA
#define ADDR_PORT PORTF
#define ADDR_DDR DDRF 
#define CTRL_PORT PORTB
#define CTRL_PIN PINB
#define CTRL_DDR DDRB
#define CTRL_CLK_PIN (1 << 3)
#define CTRL_OE_PIN (1 << 2)
#define CTRL_LAT_PIN (1 << 1)
#define CTRL_LED_PIN (1 << 7)
#elif defined(__AVR_ATmega1284P__) // For ATmega1284P based controller board
// R1, G1, B1, R2, G2, B2 hard-wired on PC2~PC7
// LED, CLK, LAT, OE hard-wired on PD4~PD7
// A, B, C, D hard-wired on PB0~PB3
#define DATA_DDR DDRC
#define DATA_PORT PORTC
#define ADDR_DDR DDRB 
#define ADDR_PORT PORTB
#define CTRL_DDR DDRD
#define CTRL_PIN PIND
#define CTRL_PORT PORTD
#define CTRL_LED_PIN (1 << 4)
#define CTRL_CLK_PIN (1 << 5)
#define CTRL_LAT_PIN (1 << 6)
#define CTRL_OE_PIN (1 << 7)
#else  // For Arduino UNO
// 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)
#endif
#define CTRL_MASK (CTRL_CLK_PIN | CTRL_OE_PIN | CTRL_LAT_PIN  | CTRL_LED_PIN)

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

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

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

/**
 * 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);
  byte mask = 1 << (x & 7);
  switch(color) {
  case COLOR_RED:
    pixel[0] |= mask;
    pixel[1] &= ~masque;
    pixel[2] &= ~masque;
    break;
  case COLOR_GREEN:
    pixel[0] &= ~masque;
    pixel[1] |= mask;
    pixel[2] &= ~masque;
    break;
  case COLOR_BLUE:
    pixel[0] &= ~masque;
    pixel[1] &= ~masque;
    pixel[2] |= mask;
    break;
  case COLOR_YELLOW:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] &= ~masque;
    break;
  case COLOR_CYAN:
    pixel[0] &= ~masque;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  case COLOR_PINK:
    pixel[0] |= mask;
    pixel[1] &= ~masque;
    pixel[2] |= mask;
    break;
  case COLOR_WHITE:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  default:
    pixel[0] &= ~masque;
    pixel[1] &= ~masque;
    pixel[2] &= ~masque;
    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 & (MATRIX_SCANLINE_SIZE - 1)] + matrixLinearOffset(x, y);
  byte mask = 1 << (x & 7);
  byte r = !!(pixel[0] & masque);
  byte g = !!(pixel[1] & masque);
  byte b = !!(pixel[2] & masque);
  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, MATRIX_SCANLINE_SIZE * (NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (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);
}

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

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

Rappel : le code est disponible sur mon github.

Problèmes et optimisations possibles

Le premier problème avec le code précédent est ultra simple et tient en un mot : Arduino.

Arduino c’est génial pour faire du prototypage et du bricolage sans avoir à lire de documentation constructeur.
Vous codez avec l’API Arduino, ça marche, tout le monde est content.

Mais comme on a pu le voir dans le 1er article, l’API arduino est extrêmement inefficace, c’est fait pour être simple, pas pour être rapide.
C’est bête parce que justement nous cherchons à faire du code rapide et efficace.

La solution ? On l’a vu dans l’article 2 : laissé tomber l’API arduino pour du code bas niveau beaucoup plus rapide.
Oui, mais cela ne suffit pas.

Le compilateur AVR-GCC fourni avec la version la plus récente de l’ide arduino date de 2008, ça fait un sacré paquet de versions majeures en retard !
Et à chaque version majeure en retard, c’est tout autant d’optimisation en moins.

D’où ma question : pourquoi continuer à utiliser l’ide arduino avec son compilateur d’un autre temps pour faire du code qui, au final, et de moins en moins "Arduino" ?
La réponse est toute trouvée : il n’y a aucune raison valable.

Je vous invite donc à installer la dernière version d’avr-gcc sur votre PC ;)
Pour les linuxiens ce sera l’affaire de quelques minutes avec votre gestionnaire de paquets préféré.
Pour les Windowsiens c’est ici que ça ce passe : https://infernoembedded.com/products/avr-tools/release

Une autre chose qui va s’attirer les foudres de mon clavier de programmeur, c’est cette fonction :

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

C’est long, c’est peu compréhensible, c’est quasiment que du copier/coller et par dessus tout : c’est une avalanche de calculs logiques !
Si c’était une fonction annexe ça pourrait aller, mais là c’est LA fonction qui fait tout l’affichage et qui prend le plus de temps processeur.
Un bon coup de nettoyage et d’optimisation s’impose !

Remarque : le code final pourra toujours être compilé avec l’ide Arduino si besoin, car celui ci manipule tout code possédant une fonction main() comme si il s’agissait d’un code "non arduino", mais l’optimisation sera moindre à cause du compilateur de 2008.

Implémentation

Avant de commencer à coder comme un fou, il va nous falloir un "makefile".
C’est ce fichier qui permet de compiler sans avoir à taper la ligne de commande qui va bien à chaque fois.

# Nom du microcontrôleur cible :
# atmega1284p pour une carte contrôleur DIY (conseillé)
# atmega328p pour une carte arduino UNO
# atmega2560 pour une carte arduino mega2560
TARGET ?= atmega1284p

# Fréquence du quartz sur la carte, 16MHz par défaut
F_CPU ?= 16000000

# Liste des fichiers sources à compiler
SOURCES = main.c

# Nom du fichier de sortie (sans extension)
OUTPUT = rgbLedMatrix

# Nom du programmateur d'AVR
AVRDUDE_PROGRAMMER ?= usbtiny

# Options compilateur et linkeur
CCFLAGS += -Wall -Os -ffunction-sections
LDFLAGS += -Wl,-gc-sections

# ----- Rien à modifier en dessous de cette ligne
OBJECTS =  $(SOURCES:.c=.o)
COMPILER = avr-g++ -mmcu=$(TARGET) -DF_CPU=$(F_CPU)UL
OBJCOPY = avr-objcopy -O ihex
OBJSIZE = avr-size -C --mcu=$(TARGET)
AVRDUDE_ICSP = avrdude -p $(TARGET) -c $(AVRDUDE_PROGRAMMER)

all: elf hex size

%.o: %.c
	$(COMPILER) -c $< -o $@ $(CCFLAGS)
	
elf: $(OBJECTS)
	$(COMPILER) -o $(OUTPUT).elf $(LDFLAGS) $^
	
hex: $(OUTPUT).elf
	$(OBJCOPY) $(OUTPUT).elf $(OUTPUT).hex
	
size: $(OUTPUT).elf
	$(OBJSIZE) $(OUTPUT).elf

flash: $(OUTPUT).hex
	$(AVRDUDE_ICSP) -U flash:w:$(OUTPUT).hex
	
fuses:
	$(AVRDUDE_ICSP) -U lfuse:w:0xf7:m -U hfuse:w:0xd9:m -U efuse:w:0xff:m  -U lock:w:0x0f:m
	
clean:
	rm -f $(OBJECTS) $(OUTPUT).elf $(OUTPUT).hex

Avec ce makefile il suffit de faire :
– "make all" pour compiler,
– "make clean" pour nettoyer une ancienne compilation,
– "make fuses" pour programmer les fusibles du microcontrôleur,
– "make flash" pour envoyer le programme dans le microcontrôleur.

NB L’envoi du programme dans le microcontrôleur se fait obligatoirement au moyen d’un programmateur adapté.
J’utilise un UsbTiny personnellement, mais vous pouvez trouver des UsbAsp sur ebay pour moins de 10€ fdp compris.
Si vous êtes têtu et que vous restez fidèle à votre carte Arduino il vous suffit d’utiliser "arduino" comme nom de programmateur ;)

Et comme je sais que j’ai des lecteurs curieux, voici quelques détails de fonctionnement du makefile (c’est un peu hors sujet, mais tant pis).
La première partie du makefile contient la définition de plusieurs variables permettant au makefile de savoir quoi faire :

# Nom du microcontrôleur cible :
# atmega1284p pour une carte contrôleur DIY (conseillé)
# atmega328p pour une carte arduino UNO
# atmega2560 pour une carte arduino mega2560
TARGET ?= atmega1284p

# Fréquence du quartz sur la carte, 16MHz par défaut
F_CPU ?= 16000000

# Liste des fichiers sources à compiler
SOURCES = main.c

# Nom du fichier de sortie (sans extension)
OUTPUT = rgbLedMatrix

# Nom du programmateur d'AVR
AVRDUDE_PROGRAMMER ?= usbtiny

# Options compilateur et linkeur
CCFLAGS += -Wall -Os -ffunction-sections
LDFLAGS += -Wl,-gc-sections

Vous remarquerez que la plupart des variables sont écrites avec la syntaxe NOM ?= VALEUR.
Le ? signifie "valeur si non spécifiée", cela permet de faire "make all TARGET=atmega328p" par exemple si pour une raison quelconque vous vouliez compiler le programme pour une carte arduino UNO alors que dans le makefile vous aviez configuré TARGET pour une autre carte.

Concernant :

# Options compilateur et linkeur
CCFLAGS += -Wall -Os -ffunction-sections
LDFLAGS += -Wl,-gc-sections

La syntaxe est NOM += VALEUR, car ce sont des valeurs qui s’ajoutent à une possible valeur par défaut.
Je ne vais pas entrer dans le détail de ces options, mais en gros cela permet d’avoir une optimisation du code maximum en terme de taille.
Cela permet aussi d’avoir l’affichage de TOUS les avertissements de compilation.

La suite du makefile définit diverses variables utiles pour la compilation :

# ----- Rien à modifier en dessous de cette ligne
OBJECTS =  $(SOURCES:.c=.o)
COMPILER = avr-g++ -mmcu=$(TARGET) -DF_CPU=$(F_CPU)UL
OBJCOPY = avr-objcopy -O ihex
OBJSIZE = avr-size -C --mcu=$(TARGET)
AVRDUDE_ICSP = avrdude -p $(TARGET) -c $(AVRDUDE_PROGRAMMER)

Parmi ces variables, on retrouve la liste des fichiers compilés OBJECTS (*.o) générait à partir de la liste des fichiers sources SOURCES (*.c).
La ligne de commande pour appeler le compilateur COMPILER.
Deux ligne de commande pour générer le fichier .hex contenant le code compilé final OBJCOPY et OBJSIZE.
Et pour finir la ligne de commande pour envoyer le code dans la carte ou programmer les fusibles AVRDUDE_ICSP.

La fin du makefile définit une série de cibles "make quelquechose" standard :

# Compile le programme complet, génère le .hex et affiche la taille du programme compilé
all: elf hex size

# Compile en fichier .c en un fichier binaire .o
%.o: %.c
	$(COMPILER) -c $< -o $@ $(CCFLAGS)
	
# Génère le fichier elf (fichier binaire intermédiaire entre les divers .o et le .hex)
elf: $(OBJECTS)
	$(COMPILER) -o $(OUTPUT).elf $(LDFLAGS) $^
	
# Génère le fichier .hex pour la programmation
hex: $(OUTPUT).elf
	$(OBJCOPY) $(OUTPUT).elf $(OUTPUT).hex
	
# Affiche la taille du programme compilé
size: $(OUTPUT).elf
	$(OBJSIZE) $(OUTPUT).elf

# Lance la programmation du programme
flash: $(OUTPUT).hex
	$(AVRDUDE_ICSP) -U flash:w:$(OUTPUT).hex
	
# Lance le programmation des fusibles
fuses:
	$(AVRDUDE_ICSP) -U lfuse:w:0xf7:m -U hfuse:w:0xd9:m -U efuse:w:0xff:m  -U lock:w:0x0f:m

# /!\ POUR ATMEGA1284P /!\
# lfuse = 0xf7 -> Oscillateur quartz sans division de fréquence
# hfuse = 0xd9 -> pas de bootloader, programmation ICSP possible
# efuse = 0xff -> pas d’arrêt en cas de chute de tension 
# lock = 0x0f -> pas de protection du code

# Efface tous les fichiers compilés
clean:
	rm -f $(OBJECTS) $(OUTPUT).elf $(OUTPUT).hex

Maintenant que l’on peut compiler du code, il faut l’écrire ;)
Tout ce passe dans désormais le fichier "main.c", fini les .ino on ne travaille plus avec l’ide Arduino.

Premier changement de taille :

#include <MsTimer2.h>

Deviens :

#include <avr/interrupt.h> /* Pour le timer 2 */
#include <util/delay.h>    /* Pour les fonction delay */
#include <avr/io.h>        /* Pour les registres et les entrées/sorties */
#include <stdint.h>        /* Pour les types dures */
#include <string.h>        /* Pour la fonction memset() */

De base l’ide arduino cache bien des choses.

Ainsi une bonne partie des #include sont ajoutés par l’ide dans notre dos, sans l’ide il faut les ajouter à la main.
L’avantage c’est qu’au lieu d’une trentaine de #include on en a besoin ici que de cinq.
La compilation sera donc bien plus efficace et rapide. Bye bye le temps perdu à compiler des dépendances inutiles.

Pour les diverses constantes et définitions de brochage, rien ne change, ouf ;)

Second changement de taille :

static volatile byte framebuffer[MATRIX_SCANLINE_SIZE][(NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (NB_COLUMNS_COUNT / 8) * 3];

Deviens :

static volatile uint8_t framebuffer[MATRIX_SCANLINE_SIZE][NB_VERTICAL_MATRIX * NB_COLUMNS_COUNT];

Pourquoi une telle simplification ?
Tout simplement parce que je vais complètement laisser tomber l’aspect générique du code !
Généricité et optimisation ne sont pas compatible, pour pouvoir optimiser à mort le code je vais devoir coder beaucoup de choses en dur.
Tout ce qui peut être calculé à la compilation ne doit pas être fait à l’exécution.
Les personnes qui me demandaient des versions 1/8, 1/4, 32×16, 16×16, etc. vont à présent comprendre pourquoi je n’ai pas envie de faire 18 variantes du code ;)

Avec une matrice de 32×32 pixels et une scanline de 1/16 chaque lot de 6 bits R0, G0, B0, R1, G1, B1 permet de décrire deux pixels : un sur la ligne N et un autre sur la ligne N+16.
En utilisant un octet par lot de 6 bits je décris donc 2 pixels par octet.

Les plus minutieux d’entre vous diront que je perds 2 bits à chaque fois, c’est vrai. Il existe un moyen d’éviter cela, mais ce serait vraiment problématiques pour la suite avec la gestion des nuances de couleurs.

Comme chaque octet décris deux pixels sur deux lignes, le nombre de lignes de la scanline * le nombre de matrices verticales permet de décrire la taille de l’axe Y des matrices. Le tout multiplié par le nombre total de colonnes on obtient la taille finale du tableau pour les axes X et Y.
Par souci de facilité (et d’évolution pour la suite) je garde séparés la scanline et le reste du tableau avec le contenu des lignes, d’où le tableau à deux dimensions.

Concernant l’agencement en mémoire des données rien ne change : l’écriture se fait toujours dans un ordre naturel 0, 1, 2, 3, … et la lecture pour l’affichage "à l’envers".

L’ancien code de setup() et loop() (c’est de l’arduino ça, beurk ;)) fait peau neuve :

/** 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, MATRIX_SCANLINE_SIZE * (NB_LINES_COUNT / MATRIX_SCANLINE_SIZE) * (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);
}

Et deviens une magnifique fonction main() toute belle :

/** Main */
int main(void) {

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

  /* Init the framebuffer (all pixels black) */
  memset((void*) framebuffer, 0, MATRIX_SCANLINE_SIZE * NB_VERTICAL_MATRIX * NB_COLUMNS_COUNT);
  
  /* Setup refresh timer */
  cli();
  TCCR2A = _BV(WGM21);             // CTC mode
  TCCR2B = _BV(CS22) | _BV(CS21);  // Prescaler /256
  TCNT2 = 0;                       // Counter reset
  OCR2A = (F_CPU / 256 / 960) - 1; // 960Hz ISR
  TIMSK2 = _BV(OCIE2A);            // Enable timer 2's compare match A ISR
  sei();
  
  /* Main loop */
  for(;;) {
  
    // Demo code
    static uint8_t x = 0;
    static uint8_t y = 0;
    static uint8_t 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_ms(10);
  }
  
  /* Compiler fix */
  return 0;
}

Pour faire simple, j’ai remplacé "setup" par "main" et "loop" par "for(;;)", ce que fait en interne l’ide arduino.
Rien de bien méchant ;)

La taille du buffer dans le memset() à changer pour correspondre à la nouvelle taille du tableau, et delay() est devenu _delay_ms().
Et oui, de base il existe une fonction pour attendre n millisecondes, voir n microsecondes en AVR-C. Bonus ultime : elles marchent mieux et sont plus précises que leurs cousines Arduino delay() et delayMicroseconds().

STOP !
Arthur ! Qu’est ce à dire que ceci !

/* Setup refresh timer */
cli();
TCCR2A = _BV(WGM21);             // CTC mode
TCCR2B = _BV(CS22) | _BV(CS21);  // Prescaler /256
TCNT2 = 0;                       // Counter reset
OCR2A = (F_CPU / 256 / 960) - 1; // 960Hz ISR
TIMSK2 = _BV(OCIE2A);            // Enable timer 2's compare match A ISR
sei();

Pas de panique c’est juste :

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

Mais en utilisant directement les registres du processeur.

Vous remarquerez qu’en utilisant directement les registres il est possible de choisir très précisément la fréquence du timer.
Au lieu de 1KHz (fréquence la plus haute permise par ma librairie Arduino MsTimer2), je peux maintenant avoir ma fréquence idéale de 960Hz.
Et je pourrais encore augmenter cette fréquence par la suite pour ajouter les nuances de couleurs ;)

Pour la configuration des registres, je ne vais pas chercher à expliquer comment ça marche. Il me faudrait un article dédié rien que pour ça.

C’est maintenant que les choses deviennent sympa ;)
Vous vous souvenez de cette fonction ?

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

C’est pas bien beau tout ça, on va lui faire un petit lifting sauce assembleur ;)

Le premier truc à comprendre c’est que je n’ai plus des données séries, mais parallèles.
En gros je ne travaille plus avec des données "en ligne", mais "en colonne", et ça change tout.

Je n’ai plus besoin de faire le moindre traitement pour transformer les données série en données parallèle. J’ai juste besoin de balancer les données aussi vite que possible sur les broches de sorties.
Pour l’écriture des données en mémoire les calculs sont là aussi grandement simplifié, car il n’y a plus besoin de faire de la sérialisation de données.
Seul désavantage : je perds un peu de mémoire (2 bits par octet), mais ce n’est rien par rapport au gain de performance réalisé grâce à cela.

Bon, ben du coup, pour envoyer un lot de deux pixels il suffit de faire :

DATA_PORT = *(--(*lineBufferPtr));
CTRL_PIN = CTRL_CLK_PIN;
CTRL_PIN = CTRL_CLK_PIN; // CLK pulse

Oui, mais non.

Comme on travaille maintenant avec des données parallèles ça veut dire qu’il va falloir gérer les données bit par bit et non octet par octet.
Ces trois jolies lignes de code vont donc être appelées pour chaque colonne des matrices, soit 32 fois par matrice, le tout multiplié par le nombre de matrices verticales et horizontales (et plus tard par le nombre de bits de résolution pour faire les nuances de couleurs).

Avec 8 couleurs et 4 matrices, ça fait 128 appels en 1.04ms, pas de problème.
Avec 4 matrices et 4096 couleurs, ça fait … 1920 appels … en 69.4µs, oups. On va avoir un problème Houston !

On va donc tout de suite optimiser ces trois lignes grâce à la magie du code assembleur ;)

  // Constant variable for the inline assembly
  const uint8_t clkPinMask = CTRL_CLK_PIN; // CLK pin mask
  
  // One pixel macro
#define LD_PX "ld __tmp_reg__, -%a2\n\t" \
			  "out %0, __tmp_reg__\n\t"  \
			  "out %1, %3\n\t"           \
			  "out %1, %3\n\t" // 2 + 1 + 1 + 1 = 5 ticks
#define LD_MX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX \
			  LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX \
			  LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX \
			  LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX // 32x LD_PX

  // For each pixels bundle of matrix 4/4, 3/3, 2/2 or 1/1
  asm volatile(LD_MX :: "I" (_SFR_IO_ADDR(DATA_PORT)), 
						"I" (_SFR_IO_ADDR(CTRL_PIN)), "e" (lineBuffer), "r" (clkPinMask));

Ce morceau de code :
– définis une macro en assembleur permettant de faire la même chose que les trois lignes plus haut,
– définis une autre macro qui exécute 32 fois la macro au-dessus,
– appel cette super macro en lui passant en argument le port de sortie, le port pour la broche CLK, le pointeur vers les données et un masque qui sert juste à faire basculer CLK de LOW à HIGH puis de HIGH à LOW.

La syntaxe est barbare, mais c’est comme ça qu’AVR-GCC manipule du code assembleur inline.
Ne vous inquiétez pas, même avec la doc sous les yeux j’ai passé un temps fou à mettre les arguments dans le bon sens.

En soi le code assembleur est assez simple :

ld r0, -Z ; décrémente le pointeur de 1, lit un octet dans le buffer et place la valeur lue dans le registre r0
out DATA_PORT, r0 ; place les sorties suivant la valeur dans r0
out CTRL_PIN, CTRL_CLK_PIN ; fais basculer la broche CLK de LOW à HIGH
out CTRL_PIN, CTRL_CLK_PIN ; fais basculer la broche CLK de HIGH à LOW

Ces 4 instructions assembleurs demandent 5 tics d’horloge processeur, même le compilateur ne peut pas arriver à un tel niveau d’optimisation.
On ne peut physiquement pas faire plus rapide.

Parenthèse : Pourquoi une variable "clkPinMask" dans le code assembleur inline et pas directement CTRL_CLK_PIN ?
Tout simplement parce que l’instruction "out" ne peut pas transférer une valeur constante vers un port de sortie.
Il faut obligatoirement que la valeur soit dans un registre, et pour cela il faut dire à AVR-GCC d’utiliser un registre libre. Ce qui n’est possible qu’en utilisant une variable.
C’est bête, mais c’est comme ça …

Même si la modification du code est majeure, le reste de la fonction d’affichage n’as pas bien changé :

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

Après :

/**
 * Interruption routine - line refresh at 60Hz
 */
ISR(TIMER2_COMPA_vect) {

  // Scan line index
  static uint8_t 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
  uint8_t *lineBuffer = (uint8_t*) framebuffer[scanlineIndex];
  
  // Go to the end of the buffer + 1
  lineBuffer += NB_VERTICAL_MATRIX * NB_COLUMNS_COUNT;
  
  // Constant variable for the inline assembly
  const uint8_t clkPinMask = CTRL_CLK_PIN; // CLK pin mask
  
  // One pixel macro
#define LD_PX "ld __tmp_reg__, -%a2\n\t" \
			  "out %0, __tmp_reg__\n\t"  \
			  "out %1, %3\n\t"           \
			  "out %1, %3\n\t" // 2 + 1 + 1 + 1 = 5 ticks
#define LD_MX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX \
			  LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX \
			  LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX \
			  LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX LD_PX // 32x LD_PX

  // For each pixels bundle of matrix 4/4, 3/3, 2/2 or 1/1
  asm volatile(LD_MX :: "I" (_SFR_IO_ADDR(DATA_PORT)), 
						"I" (_SFR_IO_ADDR(CTRL_PIN)), "e" (lineBuffer), "r" (clkPinMask));

  // For each pixels bundle of matrix 3/4, 2/3, 1/2
  if (NB_MATRIX_COUNT >= 2) {
    asm volatile(LD_MX :: "I" (_SFR_IO_ADDR(DATA_PORT)), 
						  "I" (_SFR_IO_ADDR(CTRL_PIN)), "e" (lineBuffer), "r" (clkPinMask));
  }

  // For each pixels bundle of matrix 2/4, 1/3
  if (NB_MATRIX_COUNT >= 3) {
    asm volatile(LD_MX :: "I" (_SFR_IO_ADDR(DATA_PORT)), 
						  "I" (_SFR_IO_ADDR(CTRL_PIN)), "e" (lineBuffer), "r" (clkPinMask));
  }

  // For each pixels bundle of matrix 1/4
  if (NB_MATRIX_COUNT >= 4) {
    asm volatile(LD_MX :: "I" (_SFR_IO_ADDR(DATA_PORT)), 
						  "I" (_SFR_IO_ADDR(CTRL_PIN)), "e" (lineBuffer), "r" (clkPinMask));
  }

  // Trigger latch
  CTRL_PORT = CTRL_PORT & ~CTRL_MASK;

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

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

On notera juste le changement de nom de la fonction qui est devenu une routine d’interruption :

ISR(TIMER2_COMPA_vect) {
  // ...
}

Dernier point : les fonctions getPixelAt() et setPixelAt().

Dans le code précédent à cause des calculs de sérialisation des données et de gestion des couleurs cela donnait une fonction assez complexe :

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

/**
 * 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);
  byte mask = 1 << (x & 7);
  switch(color) {
  case COLOR_RED:
    pixel[0] |= mask;
    pixel[1] &= ~masque;
    pixel[2] &= ~masque;
    break;
  case COLOR_GREEN:
    pixel[0] &= ~masque;
    pixel[1] |= mask;
    pixel[2] &= ~masque;
    break;
  case COLOR_BLUE:
    pixel[0] &= ~masque;
    pixel[1] &= ~masque;
    pixel[2] |= mask;
    break;
  case COLOR_YELLOW:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] &= ~masque;
    break;
  case COLOR_CYAN:
    pixel[0] &= ~masque;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  case COLOR_PINK:
    pixel[0] |= mask;
    pixel[1] &= ~masque;
    pixel[2] |= mask;
    break;
  case COLOR_WHITE:
    pixel[0] |= mask;
    pixel[1] |= mask;
    pixel[2] |= mask;
    break;
  default:
    pixel[0] &= ~masque;
    pixel[1] &= ~masque;
    pixel[2] &= ~masque;
    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 & (MATRIX_SCANLINE_SIZE - 1)] + matrixLinearOffset(x, y);
  byte mask = 1 << (x & 7);
  byte r = !!(pixel[0] & masque);
  byte g = !!(pixel[1] & masque);
  byte b = !!(pixel[2] & masque);
  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;
}

Mais maintenant que les données sont stockées sous une forme parallèle, tout devient beaucoup plus simple :

/**
 * 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 uint8_t x, const uint8_t y, const uint8_t color) {
  volatile uint8_t* pixel = &framebuffer[y & (MATRIX_SCANLINE_SIZE - 1)][x + (y / NB_LINES_PER_MATRIX * NB_COLUMNS_COUNT)];
  uint8_t bitsOffset = ((y & (NB_LINES_PER_MATRIX - 1)) > 15) ? 5 : 2;
  const uint8_t colorTable[] = {
    // COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_CYAN, COLOR_PINK, COLOR_WHITE, COLOR_BLACK
       0b001,     0b010,       0b100,      0b011,        0b110,      0b101,      0b111,       0b000
  };
  *pixel = (*pixel & ~(0b111 << bitsOffset)) | (colorTable[color] << bitsOffset);
}

/**
 * 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 uint8_t getPixelAt(const uint8_t x, const uint8_t y) {
  uint8_t pixel = framebuffer[y & (MATRIX_SCANLINE_SIZE - 1)][x + (y / NB_LINES_PER_MATRIX * NB_COLUMNS_COUNT)];
  uint8_t bitsOffset = ((y & (NB_LINES_PER_MATRIX - 1)) > 15) ? 5 : 2;
  const uint8_t colorTable[] = {
    COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_PINK, COLOR_CYAN, COLOR_WHITE
  };
  return colorTable[(pixel >> bitsOffset) & 3];
}

Vous remarquerez au passage que j’ai remplacé le switch/case par un tableau de constante.
Vu que je n’ai plus de sérialisation à gérer, je peux utiliser une table de correspondance sans problème.
En terme de performance, c’est juste parfait ;)

Résultat

Bon et sinon, après toutes ces optimisations qu’est ce que ça donne ?

Avant :

NewFile4

Après :

NewFile5

Si vous ne voyez pas la différence il serait bon de regarder l’info bulle, en particulier la mesure de fréquence.

Diviser par 10 la durée d’exécution du code, ça vaut bien le fait de perdre quelques octets de RAM ;)

Maintenant que le code est optimisé autant que possible il ne reste plus qu’à ajouter les nuances de couleurs.
Ce sera justement le sujet de mon prochain article ;)

En attendant bon lundi de pâque à toutes et à tous !

About these ads

Discussion

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

  1. Salut,
    Enfin de quoi occuper mes insomnies ;)
    Bravo

    Publié par Icare Petibles | 21 avril 2014, 20 h 44 min
  2. Super article ! Et bien clair… Par contre il manque pas la fin ? ? ^^

    Publié par innocuousprototypes | 21 avril 2014, 21 h 38 min
  3. Salut !
    J’ai pu monter la board que tu nous a filé à l’Electrolab (merci encore !) et commencé a bidouiller un peu la dessus et je dois avouer que c’est vraiment chouette !

    J’ai un petit souci pour parvenir à afficher une frame complète en une seule fois.
    En effet, je fais un clear de la matrice avec un memset à chaque passage dans la boucle for(;;;) puis je fais un remplissage avec deux for imbriqués, pour X et Y.
    C’est alors que j’ai un effet de tearing. J’ai l’impression que le remplissage de la matrice en lançant 1024 setpixel par frame prend un peu trop de temps en fait.
    Ai-je raté un truc ? As-tu réussi a faire ce que j’essaie de faire ?

    +++

    Jnat

    Publié par Jnat | 6 mai 2014, 10 h 14 min
    • >> J’ai pu monter la board que tu nous a filé à l’Electrolab (merci encore !) et commencé a bidouiller un peu la dessus et je dois avouer que c’est vraiment chouette !

      Tant mieux :)
      Pas trop de problème avec les borniers et les annotations des broches ?

      >> J’ai un petit souci pour parvenir à afficher une frame complète en une seule fois.
      >> En effet, je fais un clear de la matrice avec un memset à chaque passage dans la boucle for(;;;) puis je fais un remplissage avec deux for imbriqués, pour X et Y.
      >> C’est alors que j’ai un effet de tearing. J’ai l’impression que le remplissage de la matrice en lançant 1024 setpixel par frame prend un peu trop de temps en fait.
      >> Ai-je raté un truc ? As-tu réussi a faire ce que j’essaie de faire ?

      C’est normal que tu ais des artefacts lors de l’affichage ;)
      Dans la version actuelle il n’y a pas de double buffering donc tu dessines et tu affiches en même temps.
      Je vais justement implémenter ça durant le WE après avoir fini mon article n°6.
      Je ferait aussi un peu de mise au propre pour choisir si on utilise ou non la correction gamma, etc.

      Publié par Skywodd | 6 mai 2014, 12 h 48 min
  4. Merci pour cette réponse rapide !

    J’ai pour le moment soudé les alims directement sur la carte, vu que j’ai pas reussi a trouver de bornier 3x assez rapidement, j’étais vraiment pressé de voir fonctionner la chose. Pour tout te dire, l’alim est un bloc alim d’imprimante jet d’encre.
    Pour le reste, les coups de marker sur les détrompeurs ont fait le café. quand j’avais un doute, les images sur le blog ont dissipé les doutes.

    Effectivement, je comprends ce qu’il manque. J’ai hâte de lire ton prochain article !
    Merci pour tout le travail abattu sur ce projet !

    Publié par Jnat | 6 mai 2014, 13 h 06 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.

Rejoignez 753 autres abonnés