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

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

Bonjour tout le monde !

Le temps passe vite et c’est déjà l’heure de la sixième et dernière partie de ma série d’articles techniques sur les matrices de leds RGB.

Dans cet ultime article, je vais vous présenter la modification de code que j’ai effectué pour implémenter les nuances de couleurs.
On passera ainsi de 8 couleurs à pas moins de 32768 couleurs, avec correction de luminosité, ça devient sérieux.

Préparer les cachets d’aspirine, il y a une partie théorique dans cet article (mais elle est assez courte) 😉

Rapide parenthèse avant de commencer :
J’avais initialement prévu un article en plus pour vous parler de double/triple buffering, de « page flipping », de « page copying » et d’autres choses sympathiques que l’on peut faire avec des pointeurs et un peu de mémoire RAM.

Je verrai si j’ai le temps (c’est un sujet assez long et un pas très amusant), pour le moment je préfère mettre le plus de temps libre possible sur la finition de mes projets en cours 😉

Le principe

Commençons par l’astuce qui va nous permettre de faire des nuances de couleurs à partir de leds ne pouvant être qu’allumées ou éteintes.

Cette astuce est basée sur un principe physique bien connu qu’on nomme la modulation.
Ici ce sera de la modulation BCM (Binary Coded Modulation), une forme dérivée de modulation PWM.

J’en avais parlé dans un précédent article, je vais donc juste reprendre quelques slides :
https://skyduino.wordpress.com/2013/09/08/presentation-modulation-codee-binaire-b-c-m/

modulation_bases

Moduler un signal (ici lumineux) revient à créer des états intermédiaires à partir d’un signal simple.

On peut prendre l’exemple d’une lampe (ou d’une led). Celle-ci peut être soit allumée, soit éteinte.
Le but de la modulation est de faire en sorte que cette lampe soit « éteinte », « un peu allumée », « un peu beaucoup allumé » ou carrément « allumée » par exemple.
On remarque ainsi que deux nouveaux états sont apparus en plus des deux états de base « allumé » et « éteint ».

Voilà pour le principe de base de la modulation.

modulation_suite

Si on reprend cet exemple à 4 états, on remarque que celui-ci peut se coder sous forme binaire avec 2 bits, car 2 ^ 2 = 4.
On peut donc dire que l’on a fait de la modulation d’intensité lumineuse sur 2 bits de résolution.

Dans le cas d’une matrice RGB, un pixel revient à avoir 3 leds séparées, soit 2 x 2 x 2 = 8 possibilités de couleurs sans modulation.
Si on reprend notre exemple à 4 états on obtient 4 x 4 x 4 = 64 couleurs !
De manière générale pour chaque bit de résolution ajouté par led, on multiplie par 8 (2 ^ 3) le nombre de possibilités, donc de couleurs.

À ce stade vous devez surement vous dire : « chouette ! On a qu’à mettre 8 bits de résolution par leds et ça fera 16 millions de couleurs comme sur un vrai écran ! ».
… sauf que ce n’est pas aussi simple 😉

mod_pwm_p1

Pour faire de la modulation d’intensité lumineuse tout repose sur repose sur une fréquence de modulation élevée et un rapport temps haut / temps bas réglable.

Ainsi au lieu de dire que la lampe est « un peu allumée » on va dire (par exemple) que la lampe est « allumée 1/3 de temps ».

Si le temps T sur le graphique ci-dessus est trop long (disons 1 seconde pour l’exemple) vous ne verrez qu’une led qui clignote. Ce n’est pas notre but !

Pour ne voir qu’une led avec une luminosité stable, il faut être rapide. T doit faire tout au plus 40ms (soit 25Hz) … en théorie. Mais 25Hz, ça marche pour une vidéo, pas pour une lampe.

La fréquence idéale de modulation pour une lampe se situe entre 50Hz et 60Hz.
Vous seriez tenté de monter dans des fréquences bien plus élevées (un moteur se module à des fréquences supérieures à 20KHz pour éviter tout bruit audible par exemple), mais n’oubliez pas : il va falloir générer ce signal, et cela prend (beaucoup) du temps processeur.

mod_pwm_p2

Comme énoncé précédemment, ce qui fait que la led n’est pas complètement éteinte ou complètement allumée c’est le rapport t/T.
Or, pour pouvoir modifier ce rapport t/T et donc la luminosité de la led il faut diviser T en petit morceau.

La façon la plus répandue consiste à diviser ce temps T en une puissance de 2 et de garder le signal à 1 ou 0 durant t intervalles.
Cette façon de faire ce nomme « PWM », « Pulse Width Modulation », soit « modulation par longueur d’impulsion ».
La durée T est toujours la même, seule la durée de t change – dans la limite fixée par le nombre et la taille des intervalles.

TOUS les timers des microcontrôleurs modernes (et moins modernes) supportent ce mode de fonctionnement.
Il est vraiment très simple à implémenter, il suffit d’un compteur qui s’incrémente et de trois tests logiques :
Si valeur > T : valeur = 0
Si valeur < t : sortie = « 0 »
Si valeur >= t : sortie = « 1 »

Imaginons que l’on souhaite avoir une lampe à mi-intensité (50%).
Si l’on dispose d’un timer 8 bits avec sortie PWM (le cas le plus classique au monde) il suffit de mettre le point de basculement t à 255 / 2 = 127.

À noter qu’un timer compte toujours de 0 à 2 ^ N – 1 et non 2 ^ N.
0 ~ 255 = 256 valeurs, ne vous faites pas avoir 😉

modulation_a

Bon c’est bien joli de dire qu’on découpe un temps en petits morceaux, mais au final comment la led fait pour être « un peu allumée » ?

C’est très simple : elle ne l’est pas !
En réalité elle est complètement allumée un bref instant, puis éteinte.
C’est juste que notre œil a une persistance rétinienne de plusieurs dizaines de millisecondes.
On ne voit donc pas une led qui clignote très rapidement, mais une led « un peu allumée ».

Dans mon exemple ci-dessus, si j’allume ma lampe 10ms et que je l’éteinte 10ms elle paraîtra à moitié allumée (en théorie, vous comprendrez plus tard pourquoi ce n’est pas vraiment le cas en réalité).

En jouant avec ce rapport allumée / éteinte on peut créer autant de nuances que nécessaire.
Reste que cette méthode a un GROS, très GROS défaut : il faut découper un temps très court en plein de petits morceaux encore plus courts.
Puis exécuter les trois tests cités précédemment à des fréquences qui deviennent vite impossibles à respecter par logiciel.
C’est pour cela que les microcontrôleurs embarquent des timer PWM matériels, pas de bol, il ne sont pas en nombre infini.

Pas de bol x2, comme on a pu le voir dans la partie 1, avec une matrice de led toutes les leds se contrôlent au travers d’un bus série, une ligne à la fois.
Il n’est donc pas possible d’utiliser un signal PWM généré par un timer PWM matériel.
Et si le matériel ne peut pas le faire, cela signifie que le logiciel va devoir s’en charger, sauf qu’on vient de dire que cela n’était pas franchement possible à de hautes fréquences.

Pire encore, même si on arrivez à atteindre des fréquences élevées pour avoir un nombre conséquent de couleurs cela prendrait l’intégralité du temps processeur. Il ne resterait donc pas de temps libre pour faire le reste …

modulation_b

Le point clef qui rend possible l’impossible est très simple : on ne cherche pas à contrôler un quelconque périphérique externe nécessitant un signal parfaitement périodique (= 1 temps haut + 1 temps bas).

Tant que le rapport t/T est respecté on peut bien découper le signal de mon exemple précédent en deux morceaux de durées quelconques tant que la somme de ces durées donne le même résultat que précédemment.
On pourrez même découper le signal en 4 morceaux de (par exemple) 3ms, 3ms, 1ms et 2ms au final cela donnerait toujours 10ms donc un rapport 1/2 comme précédemment.

mod_bcm

La modulation BCM travaille sur ce principe.
Le signal n’est pas découpé en 2 ^ N morceaux de tailles fixes comme pour un signal PWM, mais en seulement N morceaux de tailles exponentielles (1, 2, 4, 8, 16, 32, …).

Le principe est le même que pour coder un chiffre en binaire : le premier bit vaut 1, le second bit vaut 2, le troisième bit vaut 4, etc.
Au final le rapport t/T est toujours le même qu’en PWM, il est juste découpé de manière beaucoup plus efficace pour une génération par logiciel.

Ainsi même s’il est toujours nécessaire d’avoir un premier temps très court (aussi court qu’en PWM classique) le reste du temps cela laisse énormément de temps processeur libre pour faire autre chose.

Code précédent

Reprenons maintenant le code précédent qui ne pouvait afficher que 8 couleurs :

/* Includes */
#include <avr/interrupt.h> /* For timer 2 */
#include <util/delay.h>    /* For delay */
#include <avr/io.h>        /* For registers and IO */
#include <stdint.h>        /* For hardcoded type */
#include <string.h>        /* For memset() */

/* Compile time constants */
static const uint8_t NB_HORIZONTAL_MATRIX = 2;
static const uint8_t NB_VERTICAL_MATRIX = 1;

static const uint8_t NB_LINES_PER_MATRIX = 32;   // MUST be 32 (hard-coded assembly)
static const uint8_t NB_COLUMNS_PER_MATRIX = 32; // MUST be 32 (hard-coded assembly)
static const uint8_t MATRIX_SCANLINE_SIZE = 16;  // MUST be 16 (hard-coded assembly)

static const uint8_t NB_MATRIX_COUNT = NB_VERTICAL_MATRIX * NB_HORIZONTAL_MATRIX;
static const uint8_t NB_LINES_COUNT = NB_VERTICAL_MATRIX * NB_LINES_PER_MATRIX;
static const uint8_t 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 DIY 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 (hardcoded for 1/16 scanline with 32x32 matrix)
 *
 * Array format : [scan line index] [column data stream]
 * Data format : ... [R1 G1 B1, R2, G2, B2, x, x] ...
 */
static volatile uint8_t framebuffer[MATRIX_SCANLINE_SIZE][NB_VERTICAL_MATRIX * NB_COLUMNS_COUNT];

/**
 * Possible color enumeration
 */
enum {
  COLOR_RED,    // R
  COLOR_GREEN,  // G
  COLOR_BLUE,   // B
  COLOR_YELLOW, // R + G
  COLOR_CYAN,   // G + B
  COLOR_PINK,   // R + B
  COLOR_WHITE,  // R + G + B
  COLOR_BLACK   // nothing
};

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

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

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

Le but de cette nouvelle version va être :
– de remplacer les couleurs « en dure » par des couleurs sur N bits,
– d’implémenter une modulation BCM pour gérer l’affichage des dites couleur.

Rappel : toutes les versions du code sont disponibles sur mon github : https://github.com/skywodd/RGB_Matrix_Arduino_AVR/tree/master/M1284_Software

Implémentation de la modulation BCM

// Nombre de bits de résolution
#define NB_RESOLUTION_BITS 4

// Table de correction gamma
#include "gamma.h"

Dans un premier temps on va ajouter une constante « NB_RESOLUTION_BITS » pour définir le nombre de bit de résolution par composante Rouge/Vert/Bleu.

1 bit de résolution = 8 couleurs (2 x 2 x 2),
2 bits de résolution = 64 couleurs (4 x 4 x 4),
3 bits de résolution = 512 couleurs (8 x 8 x 8),
4 bits de résolution = 4096 couleurs (16 x 16 x 16),
5 bits de résolution = 32768 couleurs (32 x 32 x 32)

On va ensuite ajouter un fichier d’entête « gamma.h », je vous expliquerai son utilité plus tard.

Au niveau du buffer stockant les données des pixels à afficher il va y avoir du changement.

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

/**
 * Énumération des couleurs possibles
 */
enum {
  COLOR_RED,    // R
  COLOR_GREEN,  // G
  COLOR_BLUE,   // B
  COLOR_YELLOW, // R + G
  COLOR_CYAN,   // G + B
  COLOR_PINK,   // R + B
  COLOR_WHITE,  // R + G + B
  COLOR_BLACK   // nothing
};

Tout d’abord l’énumération « en dure » des couleurs disparaît afin d’être remplacée par un codage des couleurs sur « NB_RESOLUTION_BITS » bits.
En parallèle de cela, la taille du buffer est multipliée par ce même nombre de bits.

On obtient ainsi :

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

La fonction de dessin est elle aussi modifiée :

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

Le nombre de couleurs étant désormais définis sur « NB_RESOLUTION_BITS » bits il n’est plus possible d’utiliser une table de correspondance.

On revient donc à des calculs bit par bit, en fonction de « NB_RESOLUTION_BITS » et avec les trois composantes rouge/vert/bleu séparées :

/**
 * Set the color of a pixel in the framebuffer.
 * 
 * @param x X position of the pixel.
 * @param y Y position of the pixel.
 * @param r Color to set (Red).
 * @param g Color to set (Green).
 * @param b Color to set (Blue).
 */
static void setPixelAt(const uint8_t x, const uint8_t y, uint8_t r, uint8_t g, uint8_t b) {

  /* Gamma correction */
  //r = (r) >> (8 - NB_RESOLUTION_BITS);
  r = gamma(r);
  //g = (g) >> (8 - NB_RESOLUTION_BITS);
  g = gamma(g);
  //b = (b) >> (8 - NB_RESOLUTION_BITS);
  b = gamma(b);

  /* Viva el offset */
  uint16_t pixelOffset = x + (y / NB_LINES_PER_MATRIX * NB_COLUMNS_COUNT);
  uint8_t scanlineOffset = y & (MATRIX_SCANLINE_SIZE - 1);
  uint8_t bitsOffset = ((y & (NB_LINES_PER_MATRIX - 1)) > 15) ? 5 : 2;

  /* Resolution bit 0 */
  volatile uint8_t* pixel0 = &framebuffer[scanlineOffset][pixelOffset];
  *pixel0 = (*pixel0 & ~(0b111 << bitsOffset)) | (!!(r & 1) << bitsOffset) | (!!(g & 1) << (bitsOffset + 1)) | (!!(b & 1) << (bitsOffset + 2));
  
  /* Resolution bit 1 */
  if(NB_RESOLUTION_BITS > 1) {
    volatile uint8_t* pixel1 = &framebuffer[scanlineOffset + MATRIX_SCANLINE_SIZE][pixelOffset];
    *pixel1 = (*pixel1 & ~(0b111 << bitsOffset)) | (!!(r & 2) << bitsOffset) | (!!(g & 2) << (bitsOffset + 1)) | (!!(b & 2) << (bitsOffset + 2));
  }
  
  /* Resolution bit 2 */
  if(NB_RESOLUTION_BITS > 2) {
    volatile uint8_t* pixel2 = &framebuffer[scanlineOffset + MATRIX_SCANLINE_SIZE * 2][pixelOffset];
    *pixel2 = (*pixel2 & ~(0b111 << bitsOffset)) | (!!(r & 4) << bitsOffset) | (!!(g & 4) << (bitsOffset + 1)) | (!!(b & 4) << (bitsOffset + 2));
  }
  
  /* Resolution bit 3 */
  if(NB_RESOLUTION_BITS > 3) {
    volatile uint8_t* pixel3 = &framebuffer[scanlineOffset + MATRIX_SCANLINE_SIZE * 3][pixelOffset];
    *pixel3 = (*pixel3 & ~(0b111 << bitsOffset)) | (!!(r & 8) << bitsOffset) | (!!(g & 8) << (bitsOffset + 1)) | (!!(b & 8) << (bitsOffset + 2));
  }
  
  /* Resolution bit 4 */
  if(NB_RESOLUTION_BITS > 4) {
    volatile uint8_t* pixel4 = &framebuffer[scanlineOffset + MATRIX_SCANLINE_SIZE * 4][pixelOffset];
    *pixel4 = (*pixel4 & ~(0b111 << bitsOffset)) | (!!(r & 16) << bitsOffset) | (!!(g & 16) << (bitsOffset + 1)) | (!!(b & 16) << (bitsOffset + 2));
  }
}

Vous remarquerez que je transforme les valeurs r, g, b au moyen d’une fonction nommée « gamma ».
J’en reparlerai plus tard, en attendant j’ai mis en commentaire l’équivalent de ce que fait cette fonction gamma() mais de manière plus subtile.

Vous remarquerez aussi que je travaille avec des valeurs r, g, b sur 8 bits en entrée, qu’importe le nombre de couleurs affichable.
En faisant cela, je sépare la partie affichage de la partie dessin. Le code utilisateur n’a ainsi aucun besoin de savoir le nombre de couleurs réellement possibles lors de l’affichage. Il travaille toujours avec le maximum possible (16 millions de couleurs), le code fait la conversion de lui même en interne.

Tous ces changements impliquent d’autres changements dans le code d’initialisation :

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

La fonction d’initialisation est ainsi doublement modifiée.

Premièrement, la taille du buffer pour le memset() est modifiée afin de correspondre à la nouvelle taille du buffer.

Deuxièmement, le calcul de fréquence d’interruption du timer est revu pour être en relation avec le nombre de bits de résolution.
Plus le nombre de bits de résolution est élevé, plus l’interruption doit se faire à une fréquence élevé pour découper le temps de base (60Hz x 16 lignes = 960Hz) en plus petit morceaux.

On obtient ainsi :

 /* Init the framebuffer (all pixels black) */
  memset((void*) framebuffer, 0, MATRIX_SCANLINE_SIZE * NB_RESOLUTION_BITS * NB_VERTICAL_MATRIX * NB_COLUMNS_COUNT);
  
  /* Setup refresh timer (16 bits) */
  cli();
  TCCR1A = 0;                      // CTC mode
  TCCR1B = _BV(WGM12) | _BV(CS10); // No prescaler
  TCCR1C = 0;
  TCNT1 = 0;                       // Counter reset
  OCR1A = (F_CPU / 60 / 16 / ((1 << NB_RESOLUTION_BITS) - 1)) - 1; // ISR
  TIMSK1 = _BV(OCIE1A);            // Enable timer 1's compare match A ISR
  sei();

Au passage on remarquera que j’utilise désormais le timer 1 et non plus le timer 2.
Le timer 1 sur les microcontrôleurs AVR est un timer 16 bits, alors que le timer 2 est un timer 8 bits.
En passant de 8 bits à 16 bits je me donne la possibilité de laisser tomber le calcul du prescaler.
Celui-ci est ainsi toujours fixé à /1 (le timer tourne à la même fréquence que le processeur) et n’intervient plus dans les calculs.

Le code d’exemple est lui aussi modifié par ce changement radical de gestion des couleurs :

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

Après :

/* Main loop */
  for(;;) {
  
    // Demo code
    static uint8_t x = 0;
    static uint8_t y = 0;
    static uint8_t r = 0;
    static uint8_t g = 0;
    static uint8_t b = 0;

    setPixelAt(x, y, r, g, b);

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

      if(++y == NB_LINES_COUNT) {
        y = 0;
      }
    }
	
    /* Draw color pattern */
#define COLOR_STEP 15
    if((r += COLOR_STEP) >= 256 - COLOR_STEP) {
      r = 0;
		  
      if((g += COLOR_STEP) >= 256 - COLOR_STEP) {
        g = 0;
		  
        if((b += COLOR_STEP) >= 256 - COLOR_STEP) {
          b = 0;
        }
      }
    }

    // No flood delay
    _delay_ms(1);
  }

Au lieu de travailler avec l’ancienne énumération de couleurs, le nouveau code d’exemple travaille sur des couleurs au format RGB888.

Peut importe que le code d’affichage ne puisse afficher que 512 ou 4096 couleurs c’est la fonction setPixelAt() qui se charge de la conversion 8 bits vers N bits.

Qui dit changement de timer dit aussi changement d’interruption.
La déclaration de la routine d’affichage change donc de nom …

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

… pour devenir :

/**
 * Interruption routine - line refresh at 60Hz
 */
ISR(TIMER1_COMPA_vect) {
  // ...
}

Et paf, ze bug

bug_signal_ctrl

Vous vous rappelez de cette capture d’écran ?
Il s’agit d’une capture des signaux de contrôle des matrices faite avec mon analyseur logique.
Sur cette capture on peut voir un bug gênant : les temps entre chaque interruption sont décalés d’un intervalle.

Cela est dû à la façon dont je gère la fin de l’affichage.

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

  // ...

  // Trigger latch
  CTRL_PORT = CTRL_PORT & ~CTRL_MASK;

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

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

Dans l’ancien code, je vérifiai la fin d’affichage après le dernier affichage.
Comme le temps entre chaque interruption était fixe (pas de modulation) cela ne posait pas de problème.

Sauf que maintenant que les temps entre chaque interruption sont exponentiels (modulation BCM oblige) cela pose un gros souci.

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

  // Scan line index & resolution bit index
  static uint8_t scanlineIndex = MATRIX_SCANLINE_SIZE - 1;
  static uint8_t resolutionBitIndex = NB_RESOLUTION_BITS - 1;
  
  // Handle resolution bit index overflow
  if (++resolutionBitIndex == NB_RESOLUTION_BITS) {
	
	// Reset resolution bit index
	resolutionBitIndex = 0;
	
	// Reset timer frequency
	OCR1A = (F_CPU / 60 / 16 / ((1 << NB_RESOLUTION_BITS) - 1)) - 1;
	
    // Handle scanline index overflow
    if (++scanlineIndex == MATRIX_SCANLINE_SIZE) {

	  // Reset scanline index counter
	  scanlineIndex = 0;
    }
	
  } else {
	
    // Divide frequency by two
    OCR1A <<= 1;
  }
  
  // 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;

  // ...

  // Trigger latch
  CTRL_PORT = CTRL_PORT & ~CTRL_MASK;
}

La solution consiste à déplacer le test de fin d’affichage en début de routine d’affichage.
Cela nécessite cependant de commencer l’affichage à la dernière ligne pour que le test reparte à la première ligne lors du premier affichage.

Vous remarquerez qu’en plus de reprendre l’affichage à la ligne 0 je remets le timer à sa fréquence de base pour le premier intervalle de temps.
Le reste du temps, je divise cette fréquence par 2 à chaque ligne, créant ainsi les intervalles de temps exponentiels nécessaires à la modulation BCM.

Vous remarquerez aussi que j’affiche les différents bits de résolution de chaque ligne avant de passer à la ligne suivante.
Afficher chaque ligne avant de passer au bit de résolution suivant revient au même.
Cependant cela produit un vilain effet visuel sur les matrices à cause du changement très rapide de ligne.

Au niveau de la fonction d’affichage en elle même il n’y a pas vraiment de changement.

// Get line buffer
  uint8_t *lineBuffer = (uint8_t*) framebuffer[scanlineIndex];
// Get line buffer
  uint8_t *lineBuffer = (uint8_t*) framebuffer[scanlineIndex + resolutionBitIndex * MATRIX_SCANLINE_SIZE];

Le seul changement consiste en l’ajout d’un offset supplémentaire pour afficher le morceau de buffer correspondant au bit de résolution en cours d’affichage pour la ligne voulue.

Correction gamma

Je vous ai parlé de « gamma » plusieurs fois sans vous dire ce que c’était.
Il est temps de corriger cela 😉

La correction gamma est un traitement photo qui permet de donner vie en quelque sorte à une image.

L’œil humain n’est pas parfait, celui-ci ne voit pas la luminosité d’un objet de manière linéaire.
En réalité un changement à une luminosité très faible est bien plus perceptible qu’un changement à une luminosité très élevé.

Sans correction gamma une image avec une luminosité parfaitement linéaire semble être blanchâtre.
Les couleurs sont ternes, car noyées dans la luminosité générale de l’image.
Bref, ce n’est pas joli.

La correction gamma n’est rien d’autre qu’une équation de courbe qui permet de corriger la non-linéarité de l’œil humain.

Pour faire le script ci-dessous j’ai repris une note d’application du fabricant Maxim sur le sujet :
http://www.maximintegrated.com/app-notes/index.mvp/id/3667

from math import pow

arraysize = 256
gamma = float(raw_input("Gamma correction (standard: 2.2, maxim: 2.5): ")) # maxim uses 2.5

NUMBER_PER_LINE = 16

print """/* ----- BEGIN OF AUTO-GENERATED CODE - DO NOT EDIT ----- */

#ifndef GAMMA_H_
#define GAMMA_H_

/* Dependencies for PROGMEM */
#include <avr/pgmspace.h>

/** Gamma table in flash memory. */
static const uint8_t PROGMEM _gamma[] = {"""

for bitres in [1, 2, 3, 4, 5]:
    width = (1 << bitres) - 1

    print "#if NB_RESOLUTION_BITS == %d" % bitres
    
    for index in range(0, arraysize):
        #print hex(int(width * pow((float(width) / arraysize * (index + 1)) / width, gamma))),
        val = str(int((pow(float(index) / 255.0, gamma) * float(width) + 0.5))),
        if index != arraysize - 1:
            if (index % NUMBER_PER_LINE) == 0:
                print ' ',
            print "%s, " % val,
            if (index % NUMBER_PER_LINE) == (NUMBER_PER_LINE - 1):
                print
        else:
            print "%s" % val

    print "#endif"

print """};

/**
 * Turn a 8-bits value into a NB_RESOLUTION_BITS value with gamma correction.
 *
 * @param x The input 8-bits value.
 * @return The output NB_RESOLUTION_BITS value with gamma correction.
 */
static inline uint8_t gamma(uint8_t x) {
  return pgm_read_byte(&_gamma[x]);
}

#endif /* GAMMA_H_ */

/* ----- END OF AUTO-GENERATED CODE ----- */"""

Voilà ce que génère le script si on lui demande un table de correction gamma avec un coefficient de 2.5 (valeur classique) :

/* ----- BEGIN OF AUTO-GENERATED CODE - DO NOT EDIT ----- */

#ifndef GAMMA_H_
#define GAMMA_H_

/* Dependencies for PROGMEM */
#include <avr/pgmspace.h>

/** Gamma table in flash memory. */
static const uint8_t PROGMEM _gamma[] = {
#if NB_RESOLUTION_BITS == 1
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1
#endif
#if NB_RESOLUTION_BITS == 2
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2, 
  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2, 
  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,  3, 
  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3
#endif
#if NB_RESOLUTION_BITS == 3
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2, 
  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2, 
  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,  3,  3,  3,  3,  3,  3, 
  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3, 
  3,  3,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4, 
  4,  4,  4,  4,  4,  4,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5, 
  5,  5,  5,  5,  5,  5,  5,  5,  6,  6,  6,  6,  6,  6,  6,  6, 
  6,  6,  6,  6,  6,  6,  6,  6,  7,  7,  7,  7,  7,  7,  7,  7
#endif
#if NB_RESOLUTION_BITS == 4
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2, 
  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,  3,  3, 
  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  4, 
  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  5,  5, 
  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  6,  6,  6,  6,  6, 
  6,  6,  6,  6,  6,  6,  6,  7,  7,  7,  7,  7,  7,  7,  7,  7, 
  7,  7,  8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  9,  9,  9,  9, 
  9,  9,  9,  9,  9,  10,  10,  10,  10,  10,  10,  10,  10,  10,  11,  11, 
  11,  11,  11,  11,  11,  11,  12,  12,  12,  12,  12,  12,  12,  12,  13,  13, 
  13,  13,  13,  13,  13,  14,  14,  14,  14,  14,  14,  14,  15,  15,  15,  15
#endif
#if NB_RESOLUTION_BITS == 5
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 
  0,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, 
  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2, 
  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,  3, 
  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4, 
  4,  4,  4,  4,  4,  4,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5, 
  6,  6,  6,  6,  6,  6,  6,  6,  6,  7,  7,  7,  7,  7,  7,  7, 
  7,  8,  8,  8,  8,  8,  8,  8,  9,  9,  9,  9,  9,  9,  9,  10, 
  10,  10,  10,  10,  10,  10,  11,  11,  11,  11,  11,  11,  12,  12,  12,  12, 
  12,  12,  13,  13,  13,  13,  13,  14,  14,  14,  14,  14,  14,  15,  15,  15, 
  15,  15,  16,  16,  16,  16,  16,  17,  17,  17,  17,  18,  18,  18,  18,  18, 
  19,  19,  19,  19,  20,  20,  20,  20,  20,  21,  21,  21,  21,  22,  22,  22, 
  22,  23,  23,  23,  23,  24,  24,  24,  24,  25,  25,  25,  26,  26,  26,  26, 
  27,  27,  27,  27,  28,  28,  28,  29,  29,  29,  30,  30,  30,  30,  31,  31
#endif
};

/**
 * Turn a 8-bits value into a NB_RESOLUTION_BITS value with gamma correction.
 *
 * @param x The input 8-bits value.
 * @return The output NB_RESOLUTION_BITS value with gamma correction.
 */
static inline uint8_t gamma(uint8_t x) {
  return pgm_read_byte(&_gamma[x]);
}

#endif /* GAMMA_H_ */

/* ----- END OF AUTO-GENERATED CODE ----- */

On remarquera que le tableau contient toujours 256 valeurs, seule la valeur maximum des dites valeurs change en fonction du nombre de bits de résolution.

Cela permet de faire deux choses en même temps :
1) corriger la luminosité
2) convertir la valeur d’entrée sur 8 bits en une valeur de sortie sur « NB_RESOLUTION_BITS » bits.

Voilà qui conclut ce dernier article technique sur les matrices de leds et mon code d’affichage.

Le code dans sa version finale (voir mon précédent article pour plus d’information) est disponible sur github.

Promis mes prochains articles seront moins techniques, il est maintenant temps de s’amuser avec ces jolies petites matrices 😉

Bon WE à toutes et à tous.

Discussion

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

  1. matrix 32×16 with led_matrix_m1284_Ncolors and Atmega328 software glediator

    Publié par electronicalibra@gmail.com | 3 novembre 2014, 13 h 26 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

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.