Un écran à base de LEDs RGB WS2812B, une mauvaise idée ?

Ou comment j'ai passé trop de temps sur un projet pour un résultat pas tip top.

Bonjour à tous,

Aujourd'hui je vais un peu détailler un des (nombreux) projets qui trainent dans mes tiroirs depuis plusieurs mois. Pour vous mettre l'eau à la bouche, rappelons nous qu'il est important de savoir rater.

Sans échecs pas d'apprentissage, je pense donc qu'il est intéressant de vous les partager afin de pouvoir aller de l'avant dans le monde sans fin du Do It Yourself.

La matrice de LEDs WS2812B

Ce projet démarre lors de l'achat d'une matrice de 10x10 LEDs RGB WS2812B qui peuvent être chaînées puis adressées manuellement tout en utilisant un seul fil pour les données et deux autres fils pour l'alimentation. On trouve de nombreux vendeurs de ce genre de circuits, en voici un exemple.

Voici à quoi ressemble le circuit sur la page du vendeur :

Matrice de LEDs WS2812B

La « matrice » de LEDs proposée sur la page du magasin.

Aucune des LEDs n'est reliée à la suivante, et l'alimentation des LEDs n'est également pas reliée. En fait, ce dont je me suis rendu compte bien plus tard est que ce circuit n'est pas destiné à être une matrice de LEDs mais un set de LEDs individuelles fournies sur leur petit PCB destiné à être brisé.

Pads CMS led WS2812B

Pads à souder au dos de chaque LED, 3 entrées + 3 sorties.

Mon enthousiasme (et surtout mon ignorance) ne m'a cependant pas retenu de l'utiliser en tant que matrice. Notons qu'il existe des matrices déjà câblées et que les quelques dollars supplémentaires vous sauveront énormément de temps de soudure.

Un petit problème de fixation

Une fois brisé, le petit PCB accueillant la LED ne possède aucune fixation quelconque. De plus, la connexion des deux fils d'alimentation ainsi que du fil de données se fait sur une surface CMS, ce qui veut dire que l'on ne peut pas utiliser ni le dessus ni le dessous du PCB afin de pouvoir fixer la LED individuelle.

La seule solution que je vois actuellement est d'utiliser un pistolet à colle pour pouvoir la fixer une fois les câbles soudés. Ce n'est pas très propre, on peut peut-être également envisager un petit support en 3D sous forme de berceau accueillant la LED avec des mords sur les côtés au dessus et en dessous afin d'éviter tout mouvement.

Cependant, nous nous éloignons du sujet car je viens de réaliser uniquement récemment qu'il s'agissait de LEDs individuelles. Ayant acheté cette « matrice » il y a plus d'un an, j'avais entrepris de l'utiliser comme une matrice quitte à tout souder, et c'est ce que nous allons voir.

Notons tout de même que même en utilisant le tout, aucun système de fixation n'est fourni. Les trous sur les côtés du PCB sont minuscules et sont probablement utilisés pour la fabrication. Ils ne sont pas utilisables pour fixer correctement la matrice sur un support.

Soudure des 100 LEDs de la matrice

J'ai du laisser pas mal d'étain et de temps sur cette partie. Absolument rien n'est relié si on compte utiliser ce PCB comme une matrice. Il faut donc effectuer les soudures suivantes :

  • Un fil tout le long des pistes 5V ;
  • Un fil tout le long des pistes à la masse ;
  • Un fil reliant le DATA OUT de la LED précédente au DATA IN de la LED suivante.

C'est ce troisième point qui prendra le plus de temps, même en utilisant un très long fil et en le coupant plus ou moins habilement entre les DIN et DOUT de la même LED.

Voyons le résultat.

Dos de la matrice avec toutes les pistes soudées

Oui, ça fait beaucoup, beaucoup de soudures.

La technique se base sur le dénudage de fil sur toute la longueur puis la soudure point par point pour chaque piste de chaque LED. On remarquera les fils jaunes traversant tout en longueur afin de relier le DOUT de la dernière LED au DIN de la LED sur la colonne suivante. De plus, l'alimentation est reportée par le haut et par le bas, une colonne sur deux, l'angle de courbure du câble étant trop fort pour pouvoir relier toutes les colonnes par un seul côté.

Le condensateur à l'arrivée de l'alimentation des LEDs permet d'encaisser les pics d'intensité lors de changements de couleurs ; il est fortement recommandé. J'ai pour ma part pris le plus gros que j'ai trouvé dans mes tiroirs de récupération.

Condensateur de découplage au début de la matrice

Un condo tout gros tout moche mais qui fait le travail.

Celui-ci est tout simplement relié entre le 5V et la masse de la première LED.

Commande de la matrice en Wi-Fi

N'ayant aucune idée de ce pour quoi je vais utiliser la matrice, j'ai écrit un petit bout de code pour un module ESP8266 afin de pouvoir commander toutes les LEDs via des trames UDP en Wi-Fi.

Carte ESPBoard branchée sur la matrice de LEDs

Ma petite réalisation pour utiliser simplement des modules ESP-01.

Le module ESP8266 est sur une plaque ESPBoard, un design maison fabriqué en 10 exemplaires chez Seeed Fusion pour une somme avoisinant les 15 €, frais de port compris (achat effectué il y a plus d'un an, les prix semblent avoir baissé).

Partie ESP8266 en C++/Arduino

Le code pour ESP8266 utilise la surcouche Arduino plutôt que FreeRTOS (qui est le framework officiel fourni par Espressif) pour des raisons de rapidité de prototypage. Il lit le premier octet du paquet UDP, et en fonction de celui-ci effectue une action sur l'écran (tout remplir, tout nettoyer, mettre à jour un seul pixel…).

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

#define NUMPIXELS 100

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, 2, NEO_GRB + NEO_KHZ800);
WiFiUDP Udp;
char data[512];  // buffer for incoming packets

// WiFi connection info
const char* ssid = "my-ssid";
const char* password = "not-a-chance";


void setup() {
  yield();

  Serial.begin(115200);
  pixels.begin();

  WiFi.begin(ssid, password);

  // Connect to WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    yield();
    Serial.print(".");
  }

  pixels.setPixelColor(0, pixels.Color(0, 127, 0));
  pixels.show();

  Serial.println("");
  Serial.println("WiFi connected");
  // Print the IP address
  Serial.println(WiFi.localIP());

  Udp.begin(5005);
}

void handler_fill_screen(int len) {
  for (int i = 0; i < len; i += 3) {
    pixels.setPixelColor(i / 3, data[i + 1], data[i + 2], data[i + 3]);
    Serial.print("pixel[");
    Serial.print(i / 3);
    Serial.print("] = ");
    Serial.print(data[i + 1], HEX);
    Serial.print(", ");
    Serial.print(data[i + 2], HEX);
    Serial.print(", ");
    Serial.print(data[i + 3], HEX);
    Serial.print(" (i = ");
    Serial.print(i);
    Serial.print(", len = ");
    Serial.print(len);
    Serial.print(")");
    Serial.println();
 }

  pixels.show();
}

void loop() {
  int packetSize = Udp.parsePacket();

  if (!packetSize)
    return;

  Serial.println("");
  Serial.print("Received packet of size ");
  Serial.print(packetSize);
  Serial.print(" from ");
  IPAddress remote = Udp.remoteIP();
  Serial.println(remote);

  int len = Udp.read(data, 512);

  if (len < 1)
    return;

  switch (data[0]) {
    case 0x01:
      handler_fill_screen(len);

    default:
      break;
  }
}

On peut voir que seule l'action 0x01 a été implémentée. Celle-ci se contente, pour chaque triplet d'octets, de mettre à jour la couleur RGB du pixel, pour tous les pixels.

Le message pour mettre à jour tout l'écran fait donc 1 + 3 * 100 = 301 octets, ce qui reste suffisamment petit pour nous assurer que tout tiendra dans un seul paquet.

Étant donné que l'on connait la taille des données UDP, on peut mettre uniquement les premiers pixels à jour avec un message de taille réduite. Si la taille n'est pas un multiple de 3 en enlevant l'octet de type de message, le programme ira jardiner dans le tableau pour essayer de finir de mettre la couleur du dernier pixel incomplet.

Une sécurité à ajouter serait de ne pas faire un tour de boucle supplémentaire passant au pixel suivant si il reste moins de 3 octets à lire (et d'afficher un message d'avertissement sur le port série par exemple).

Sur le PC en Python

Un petit programme Python utilisant Pillow permet d'ouvrir des images dans des formats standards et de les envoyer sur l'écran.

import sys
import socket
import itertools

from PIL import Image

UDP_IP = "192.168.42.185"
UDP_PORT = 5005

im = Image.open("test.png").convert("RGB")

MESSAGE = [0x01]

# Aplatit tous les triplets (r,g,b) dans une liste continue
pixels = list(itertools.chain(*im.getdata()))
print(pixels)

MESSAGE = bytes(MESSAGE) + bytes(pixels)

# Test pour allumer les deux premiers pixels en rouge
# MESSAGE = "\x01\x7f\x00\x00\x7f\x00\x00".encode("ascii")

print(MESSAGE)

# Affiche le canal rouge pour chaque pixel
# for i in range(100):
#     print(MESSAGE[3 * i + 2])

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(MESSAGE, (UDP_IP, UDP_PORT))

J'utilise Gimp en ouvrant une image de 10 par 10 pixels et en la sauvegardant au format PNG. Ensuite, l'appel au script Python fait le reste en très peu de lignes.

Une petite difficulté avec Python 3 est que les chaînes de caractères ne sont plus considérées comme une suite d'octets, il faut donc utiliser bytes un peu partout pour dire que l'on fait du binaire.

Des résultats décevants

Les résultats ne sont pas à la hauteur pour plusieurs raisons que nous allons détailler ci-dessous.

Image de test écran

Image de test 10 x 10 pixels agrandie.

Luminosité dangereuse

Les LEDs WS2812B sont à la fois puissantes et directives, ce qui ne nous arrange pas pour un écran.

À pleine puissance, l'écran est douloureux à regarder de face, surtout si on prend en compte le fait que l'on ait 100 LEDs sur une surface réduite. L'intensité lumineuse en devient dangereuse, laissant des marques sur la rétine pendant plusieurs minutes si on regarde l'écran puis une surface sombre.

Cela revient clairement à se mettre une lampe torche dans les yeux, ces LEDs étant faites pour éclairer plutôt qu'être regardées à mon avis.

Test diffusion écran sans filtre

Test de diffusion sans filtre.

Il faut absolument mettre un filtre qui diffuse la lumière pour ne risquer quoi que ce soit avec la rétine, par exemple un papier calque ou toute autre matière semi-transparente. Un gros bout de plastique blanc pas trop épais peut également fonctionner.

Test diffusion écran avec filtre

Test de diffusion avec un filtre planche à pain en plastique

Le fait de mettre un filtre diffusant fait cependant disparaitre la séparation nette entre les pixels, avec un flou qui mélange les couleurs aux intersections.

La distance du filtre par rapport à la LED est importante, une distance trop courte fait apparaitre la forme ronde de chaque pixel, alors qu'une distance trop importante mélange grandement les couleurs pour donner un résultat flou (ce qui est le cas avec ma planche à pain car trop épaisse).

J'ai fait des essais avec un filtre fait pour diffuser la lumière du rétro-éclairage récupéré dans un écran LCD destiné à la casse. Positionner le filtre à une distance correcte n'est pas aisé, il faudrait le tendre comme une peau de tambour à égale altitude entre les quatre coins.

Afin de garder un effet « pixel cubique » et minimiser le flou il faudrait donc réaliser une matrice de petites chambres carrées indépendantes pour chacune des LEDs afin que la couleur ne bave pas sur celle d'à côté, puis poser le filtre juste au dessus des chambres.

Répartition non-linéaire de l'intensité

Si chaque LED est bien éteinte lorsque l'on demande un pixel de couleur (0,0,0), le fait de passer à (1,0,0) va faire apparaitre directement un rouge avec une certaine intensité plutôt que d'introduire la couleur linéairement.

Cela implique que la réalisation de dégradés couleur -> noir est impossible, car la couleur est toujours significativement présente tant que l'on a pas mis les trois canaux RGB à zéro.

Test diffusion dégradé conique écran avec filtre

Essai de dégradé conique non-symétrique.

De plus, le dégradé d'une intensité de 255 vers 1 se fait péniblement ressentir. Ces diodes ne sont juste pas faites pour obtenir une restitution correcte des couleurs et les dégradés de luminosité sont impossibles. Le seul moyen d'utiliser l'écran est à une intensité fixe sur toute la grille et d'éteindre ou d'allumer les LEDs avec une certaine couleur.

Résolution trop faible

La résolution de 10 par 10 pixels est vraiment trop faible pour pouvoir espérer afficher quelque chose de sympa. Une taille de 16 par 16 pixels est un minimum si on souhaite pouvoir faire du pixel art. Faire des icônes de 10 par 10 pixels relève plus du masochisme que du challenge artistique.

Conclusion

Je pense que je vais garder la matrice en l'état et la démanteler au fur et à mesure quand j'aurai besoin de LEDs individuelles pour afficher visiblement des statuts sur des projets.

Reste la question de la fixation de chaque LED qui ne sera pas simple, je pense que le lit imprimé en 3D puis vissé avec du M3 peut être une solution plus viable que le pistolet à colle.

Une question ou remarque ? N'hésitez pas à me contacter en envoyant un mail à microjoe, suivi d'un arobase, puis encore microjoe et enfin un « point org ».