NeoPixel - Décompte visuel et surprise sur le timing

Il y a quelques jours, nous faisions un petit projet autour des rubans NéoPixel (LED RGB Adressable) dans l'article "NeoPixel - Interpolation de couleur et dégradé"
Nous avions besoin de faire une petite interpolation de couleur entre VERT et ROUGE pour une application.... nous avons donc sorti un Ruban NéoPixel de 144 LEDs/mètre et tester notre petit algorithme.

Idée projet
Cette fois, nous allons décompter 5 secondes sur le ruban en débutant le ruban dans vert... puis passant progressivement au rouge au fur et à mesure que le temps s'écoule. Le but est également de diminuer le nombre de LEDs pour indiquer l'écoulement du temps.
Décompte visuel à base de ruban NeoPixel
Attention à la couleur verte
Nous avons commencé par faire une progression de la couleur RGB (0,255,0) vert complet à RGB (255,0,0) rouge complet.

Le problème de ce premier prototype était que le ruban restait vert jusqu'au milieu du ruban alors que, selon l'interpolation des couleurs, il aurait dut être jaune/orange.

Quel est donc le problème?
Le problème ce n'est pas le programme mais notre oeil. En effet, l'oeil est beaucoup plus sensible au vert qu'aux autres couleurs.
Par conséquent, il faut doser le vert avec soin! C'est pour cette raison que l'encodage des couleurs RGB sur 16 bits offre bit supplémentaire au vert.

La solution
Pour avoir une meilleure répartition des couleurs, il faudrait moins de vert!
Il est possible d'opter pour une correction Alpha MAIS IL EST AUSSI de débuter le dégradé de couleur avec un vert plus ténu comme la couleur RGB (0,128,0) que l'oeil verra comme un vrai vert.

Respecter le timing, c'est un problème!

En définissant #define COUNTDOWN_SEC 5 , l'animation devait prendre 5 secondes à l'aide de l'algorithme suivant dans la boucle:
void loop() {
  // Nombre de ms par LEDs
  float ratio = (float)(COUNTDOWN_SEC * 1000) / strip.numPixels();

  // Initialisation du temps de décompte
  if (countdown == 0) {
      countdown = millis();
  }
  while( (millis() - countdown) < (COUNTDOWN_SEC * 1000) ){
      unsigned long remaining = (COUNTDOWN_SEC*1000) - ( millis()+millifix - countdown ); // en milli-seconde
      //Serial.println( remaining );
      
      float nbr_led = (float)remaining / ratio; 
      
      color = colorInterpolate( red, green, COUNTDOWN_SEC*1000, (uint16_t) remaining );
      colorStrip( color, (int) nbr_led );
  }
  
  delay( 3000 ); 
  
  // redémarrer un nouveau cycle
  countdown = millis();
}

En gros l'algorithme regarde combien de milli-secondes se sont écoulées et en déduit le nombre de de LED à allumer et le la couleur des LEDs.
La fonction millis() indique le nombre de milli-secondes écoulées depuis la mise sous-tension de votre Arduino.

Le hic, c'est que le traitement (décompte sur le ruban) prend environ 9 secondes au lieu de 5 secondes.

Qu'est ce qui ne va pas?
Ne cherchez pas de bug dans le code, il n'y en a pas!
En prenant une approche tordue, c'est comme si notre Arduino prenait 9 secondes tout en pensant que seuls 5 secondes se sont écoulées. Et c'est là que ce situe le problème!
Le problème provient du fonctionnement des rubans NeoPixels (voir détails dans ce tutoriel).
Comme le protocole est très exigeant sur le temps, la bibliothèque interrompt tous toutes les interruptions pendant l'envoi des données sur le ruban NeoPixels.
Hors, le compteur de milliseconde d'Arduino est basé sur un Timer... et un Timer ça balance des interruptions pour exécuter le code qui compte les milli-secondes.
Par conséquent, lors d'une mise-à-jour du ruban, Arduino oublie de compter le temps!!!!

Solution
Justement, étant donnée que le protocole NeoPixel est si stricte sur le timing, nous pouvons aussi calculer ce temps nécessaire à l'émission de donnée et appliquer une correction à la fonction millis() .
Et figurez vous que cela marche au poil!

Calculer une correction pour Millis()
Les NéoPixels reçoivent le flux de donnée à la fréquence fixée de 800 KHz (excepté pour les pixels Flora “V1” qui utilisait un fréquence de 400 KHz). Un bit à donc besoin de de 1/800.000 de sec — soit 1.25 microsecondes pour être transmit. Un pixel à besoin de 24 bits pour fixer sa couleur (8 bits pour chaque couleur de base rouge, vert, bleu) — soit 30 microsecondes. Après l'envoi des données du dernier pixels, le flux de donnée doit être arrêté pendant au mois 50 microsecondes avant de relancer un nouveau flux de donnée (et recommencer le cycle d'initialisation des couleurs du premier au dernier pixel)

Pour un ruban de 144 LEDs néopixels, nous aurons besoin de...

50µs + 144 x 30µs = 4370 µs

soit 4.37 milli-secondes pour faire une mise-à-jour du ruban NéoPixel. Donc Millis() prend environ 4.4ms dans les dents à chaque fois que le ruban est rafraîchit.

Voici un algorithme qui se propose de faire la correction de millis()... en cumulant 5ms (en gros) de correction à millis() pour  rafraîchissement du ruban.

Le décompte visuel est maintenant parfait :-)





Code Corrigé

Voici une seconde proposition de code fonctionnel... je vous laisse le soin de peaufiner les nettoyages

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define PIN 6
#define COUNTDOWN_SEC 5 // from 0 to 4 = 5 seconds

// Parameter 1 = number of pixels in strip
// Parameter 2 = Arduino pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_KHZ400  400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)
//   NEO_RGB     Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
//   NEO_RGBW    Pixels are wired for RGBW bitstream (NeoPixel RGBW products)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(144, PIN, NEO_GRB + NEO_KHZ800);

unsigned long countdown = 0; 

// Correction pour la fonction millis()
unsigned long millifix = 0; 

void setup() {
  // This is for Trinket 5V 16MHz, you can remove these three lines if you are not using a Trinket
  #if defined (__AVR_ATtiny85__)
    if (F_CPU == 16000000) clock_prescale_set(clock_div_1);
  #endif
  // End of trinket special code
  Serial.begin(9600);
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'
}

void loop() {
  // Affiche un interpolation entre VERT (green) corrigé
  // et ROUGE (red) 
  uint32_t green = strip.Color(0,128,0);
  uint32_t red   = strip.Color(255,0,0);
  uint32_t color = 0;
  
  // Nombre de LEDs par ms
  float ratio = (float)(COUNTDOWN_SEC * 1000) / strip.numPixels();
  Serial.println( (int)ratio );
  
  // Initialise le temps de au démarrage
  if (countdown == 0) {
      countdown = millis();
  }
  while( (millis()+millifix - countdown) < (COUNTDOWN_SEC * 1000) ){
      unsigned long remaining = (COUNTDOWN_SEC*1000) - ( millis()+millifix - countdown ); // in milli-second
      //Serial.println( remaining );
      
      float nbr_led = (float)remaining / ratio; 
      
      color = colorInterpolate( red, green, COUNTDOWN_SEC*1000, (uint16_t) remaining );
      colorStrip( color, (int) nbr_led );
      // strip.show() disable interrupts that makes millis() not reliable
      // Each shows cost 144 Leds * 30microSec + 50 microSec = 4370 microSec = 43ms
      millifix = millifix + 5; // time fixup of 50ms per strip.show()
      // a delay of 10 ms for Arduino programming boot strap
      delay(10);
  }
  
  delay( 3000 ); 
  
  // Démarrer un nouveau cycle
  countdown = millis();
  millifix = 0;

}

// Fill x Dots of a given color and turn-off the others
void colorStrip( uint32_t color, uint16_t len ) {
   for( uint16_t i=0; i<len; i++ ){
       strip.setPixelColor( i, color );
   }
   for( uint16_t i=len; i<strip.numPixels(); i++ ){
       strip.setPixelColor( i, 0 ); // black
   }
   strip.show();
}

// Interpolate a color between 2 other colors
uint32_t colorInterpolate( uint32_t fromColor, uint32_t toColor, uint16_t maxSteps, uint16_t position ){
  // fromColor
  uint8_t r = ( fromColor & ((uint32_t)0xFF << 16) ) >> 16;
  uint8_t g = ( fromColor & ((uint32_t)0xFF << 8) ) >> 8;
  uint8_t b = ( fromColor & ((uint32_t)0xFF) );
  // toColor
  uint8_t r2 = ( toColor & ((uint32_t)0xFF << 16) ) >> 16;
  uint8_t g2 = ( toColor & ((uint32_t)0xFF << 8) ) >> 8;
  uint8_t b2 = ( toColor & ((uint32_t)0xFF) );
  
  // Interpolate
  float ri = r - ( ((float)(r-r2)) /maxSteps * position);
  float gi = g - ( ((float)(g-g2)) /maxSteps * position);
  float bi = b - ( ((float)(b-b2)) /maxSteps * position);

  return strip.Color( (int)ri, (int)gi, (int)bi );
}