Matrices LED — dalles HUB75#
Les dalles HUB75 sont les panneaux de LEDs RGB des écrans géants : 64 × 32 ou 64 × 64 pixels, pas de 2 à 5 mm, chaînables. Elles n'ont aucune mémoire : le microcontrôleur doit les rafraîchir en permanence (plusieurs centaines de fois par seconde), ligne par ligne, avec une modulation pour les niveaux de couleur. Sur ESP32, c'est le DMA qui s'en charge, sans occuper le CPU.
On utilise la librairie ESP32-HUB75-MatrixPanel-DMA : elle exploite le périphérique LCD de l'ESP32-S3 (et l'I2S des autres ESP32) et propose toute l'API de dessin d'Adafruit GFX.
lib_deps =
https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA.git
adafruit/Adafruit GFX Library
Câblage#
Le connecteur HUB75 (2 × 8 broches, celui marqué IN sur la dalle) :
| Signal | Rôle | GPIO (exemple) |
|---|---|---|
| R1 G1 B1 | couleurs de la moitié haute | 1, 2, 4 |
| R2 G2 B2 | couleurs de la moitié basse | 5, 6, 7 |
| A B C D | numéro de ligne | 8, 9, 10, 11 |
| E | 5e bit de ligne, dalles 64 × 64 seulement | 43 |
| CLK | horloge | 12 |
| LAT | validation de la ligne | 13 |
| OE | extinction (luminosité) | 44 |
| GND | masse commune avec l'ESP32 | GND |
- Les broches sont libres : adaptez le tableau dans le code à votre montage. Évitez 0, 3, 45, 46 (démarrage), 19 et 20 (USB), 48 (LED intégrée).
- La dalle s'alimente à part, en 5 V par son connecteur d'alimentation : comptez jusqu'à 4 A pour une 64 × 32 tout en blanc. Jamais par la broche 5V de l'ESP32.
- Les entrées de la dalle sont en 5 V, mais acceptent en général le 3,3 V de l'ESP32 sur des fils courts. Si l'image scintille ou se décale : fils plus courts, ou un adaptateur de niveau (74HCT245).
Ouvrir ce montage dans Plot Circuits ↗
Exemple#
#include <Arduino.h>
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
// Taille de la dalle et nombre de dalles chaînées
#define WIDTH 64
#define HEIGHT 32
#define CHAIN 1
// Câblage HUB75 → GPIO de la Super Mini (adaptez à votre montage, -1 = non utilisé)
HUB75_I2S_CFG::i2s_pins pins = {
1, 2, 4, // R1 G1 B1 (moitié haute)
5, 6, 7, // R2 G2 B2 (moitié basse)
8, 9, 10, 11, // A B C D (sélection de ligne)
-1, // E (seulement pour les dalles 64×64 : par ex. 43)
13, 44, 12 // LAT OE CLK
};
MatrixPanel_I2S_DMA* panel = nullptr;
void setup() {
Serial.begin(115200);
HUB75_I2S_CFG config(WIDTH, HEIGHT, CHAIN, pins);
config.double_buff = true; // dessin dans un 2e tampon : pas de scintillement
panel = new MatrixPanel_I2S_DMA(config);
if (!panel->begin()) {
Serial.println("Mémoire insuffisante pour la dalle");
while (true) delay(1000);
}
panel->setBrightness8(60); // 0 à 255 : l'alimentation 5 V suit !
}
float t = 0;
void loop() {
panel->clearScreen();
// Dessin : toutes les fonctions d'Adafruit GFX sont disponibles
int x = WIDTH / 2 + sin(t) * 22;
panel->fillCircle(x, HEIGHT / 2, 7, panel->color565(255, 120, 0));
panel->drawRect(0, 0, WIDTH, HEIGHT, panel->color565(0, 60, 255));
panel->setTextColor(panel->color565(255, 255, 255));
panel->setCursor(2, 2);
panel->print("ENJMIN");
panel->flipDMABuffer(); // affiche le tampon qu'on vient de dessiner
t += 0.05;
delay(16); // ~60 images par seconde
}
double_buff = true: on dessine dans un tampon caché, puisflipDMABuffer()l'affiche d'un coup. Sans lui, on voit les dessins se construire (scintillement).- Toute l'API Adafruit GFX :
drawPixel,drawLine,fillRect,drawCircle,drawBitmap,setTextSize, polices… - Couleurs en RGB565 :
panel->color565(r, g, b); ou directementpanel->drawPixelRGB888(x, y, r, g, b). - Plusieurs dalles :
CHAIN= nombre de dalles chaînées (sortie OUT de l'une vers IN de la suivante). Pour les disposer en grille : voir plus bas. - Mémoire : une 64 × 32 en double tampon tient largement dans la SRAM. Au-delà de quelques dalles, baissez la profondeur de couleur (
setPixelColorDepthBits). - Si les couleurs sont inversées ou l'image décalée d'une ligne, certaines dalles demandent un autre
driver(FM6126A…) ouclkphase = falsedans la config. - Afficher sur la dalle l'image d'un jeu Godot : voir plus bas.
Afficher une image envoyée par Godot#
Godot capture son propre écran, le réduit à 64 × 32 pixels et l'envoie en binaire par WebSocket, 20 fois par seconde : la dalle devient un second écran du jeu.
Côté ESP32#
#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
#define WIDTH 64
#define HEIGHT 32
const size_t FRAME_SIZE = WIDTH * HEIGHT * 3; // une image = 64 × 32 pixels × 3 octets (R, G, B)
HUB75_I2S_CFG::i2s_pins pins = {1, 2, 4, 5, 6, 7, 8, 9, 10, 11, -1, 13, 44, 12};
MatrixPanel_I2S_DMA* panel = nullptr;
WebSocketsServer ws(81);
void onWsEvent(uint8_t client, WStype_t type, uint8_t* data, size_t len) {
if (type == WStype_BIN && len == FRAME_SIZE) {
// Message binaire = une image brute, ligne par ligne
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
const uint8_t* p = data + (y * WIDTH + x) * 3;
panel->drawPixelRGB888(x, y, p[0], p[1], p[2]);
}
}
panel->flipDMABuffer(); // affiche l'image complète d'un coup
} else if (type == WStype_CONNECTED) {
Serial.printf("Godot connecté (client %u)\n", client);
}
}
void setup() {
Serial.begin(115200);
HUB75_I2S_CFG config(WIDTH, HEIGHT, 1, pins);
config.double_buff = true;
panel = new MatrixPanel_I2S_DMA(config);
panel->begin();
panel->setBrightness8(60);
panel->clearScreen();
WiFi.begin(SSID, PASS);
while (WiFi.status() != WL_CONNECTED) delay(100);
WiFi.setSleep(false); // latence plus faible : utile pour de la vidéo
Serial.printf("ws://%s:81\n", WiFi.localIP().toString().c_str());
ws.begin();
ws.onEvent(onWsEvent);
}
void loop() {
ws.loop();
}
Côté Godot#
extends Node2D
## Envoie l'image du jeu à une dalle LED 64 × 32 (ESP32) par WebSocket, 20 fois par seconde
const URL = "ws://172.20.10.5:81" # ← l'adresse affichée par l'ESP32
const PANEL_SIZE = Vector2i(64, 32)
const FPS = 20
var ws = WebSocketPeer.new()
var elapsed = 0.0
var t = 0.0
func _ready():
RenderingServer.set_default_clear_color(Color.BLACK)
ws.connect_to_url(URL)
func _process(delta):
ws.poll()
t += delta
queue_redraw()
elapsed += delta
if elapsed >= 1.0 / FPS and ws.get_ready_state() == WebSocketPeer.STATE_OPEN:
elapsed = 0.0
var image = get_viewport().get_texture().get_image() # capture de l'écran du jeu
image.resize(PANEL_SIZE.x, PANEL_SIZE.y, Image.INTERPOLATE_BILINEAR)
image.convert(Image.FORMAT_RGB8) # 3 octets par pixel, sans alpha
ws.send(image.get_data()) # message binaire de 64 × 32 × 3 = 6144 octets
func _draw():
var size = get_viewport_rect().size
var center = size / 2 + Vector2(cos(t), sin(t * 1.5)) * size * 0.3
draw_circle(center, size.y * 0.2, Color.from_hsv(fmod(t * 0.1, 1.0), 1, 1))
- Une image = 6 144 octets ; 20 images par seconde ≈ 120 ko/s : le Wi-Fi suit sans problème.
- Pour éviter l'image déformée, donnez au jeu les proportions de la dalle (fenêtre en 2:1, par exemple 640 × 320), ou découpez une zone avec
image.get_region(Rect2i(...)). - Pour n'envoyer qu'une partie de la scène : placez-la dans un
SubViewportde 64 × 32 et capturezsub_viewport.get_texture().get_image()(pas de redimensionnement, pixels nets).
Plusieurs dalles disposées en grille#
Les dalles sont chaînées par un câble (OUT de l'une vers IN de la suivante), mais on peut les disposer en grille : VirtualMatrixPanel_T remet les pixels dans le bon ordre, et on dessine sur un seul grand écran.
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#include <ESP32-HUB75-VirtualMatrixPanel_T.hpp>
// 4 dalles 64×32 disposées en 2 × 2 → un écran virtuel de 128 × 64 pixels
#define PANEL_W 64
#define PANEL_H 32
#define ROWS 2 // nombre de rangées de dalles
#define COLS 2 // dalles par rangée
// Ordre du câble : la 1re dalle (reliée à l'ESP32) est en haut à droite,
// puis on serpente vers la gauche et on descend (voir les autres valeurs de PANEL_CHAIN_TYPE)
#define CHAIN_TYPE CHAIN_TOP_RIGHT_DOWN
HUB75_I2S_CFG::i2s_pins pins = {1, 2, 4, 5, 6, 7, 8, 9, 10, 11, -1, 13, 44, 12};
MatrixPanel_I2S_DMA* chain = nullptr; // la chaîne physique (256 × 32 pour la librairie)
VirtualMatrixPanel_T<CHAIN_TYPE>* screen = nullptr; // l'écran tel qu'on le voit (128 × 64)
void setup() {
Serial.begin(115200);
HUB75_I2S_CFG config(PANEL_W, PANEL_H, ROWS * COLS, pins); // dernier paramètre : nombre total de dalles
config.double_buff = true;
chain = new MatrixPanel_I2S_DMA(config);
if (!chain->begin()) {
Serial.println("Mémoire insuffisante");
while (true) delay(1000);
}
chain->setBrightness8(60);
screen = new VirtualMatrixPanel_T<CHAIN_TYPE>(ROWS, COLS, PANEL_W, PANEL_H);
screen->setDisplay(*chain);
screen->drawDisplayTest(); // affiche le numéro de chaque dalle : pour vérifier le câblage
screen->flipDMABuffer();
delay(3000);
}
float t = 0;
void loop() {
screen->clearScreen();
// On dessine en coordonnées de l'écran entier : la librairie répartit sur les dalles
int x = screen->width() / 2 + sin(t) * 50;
int y = screen->height() / 2 + cos(t * 1.3) * 22;
screen->fillCircle(x, y, 9, screen->color565(255, 120, 0));
screen->drawRect(0, 0, screen->width(), screen->height(), screen->color565(0, 60, 255));
screen->setCursor(4, 4);
screen->print("128 x 64");
screen->flipDMABuffer();
t += 0.04;
delay(16);
}
- Le chaînage en serpentin (une rangée sur deux à l'envers, le câble court d'une dalle à la voisine) est le plus courant :
CHAIN_TOP_RIGHT_DOWN,CHAIN_TOP_LEFT_DOWN,CHAIN_BOTTOM_LEFT_UP… selon la dalle de départ. Les variantes_ZZ(zig-zag) gardent toutes les dalles à l'endroit, avec des câbles plus longs. drawDisplayTest()affiche le numéro de chaque dalle : si l'ordre ne correspond pas, changezCHAIN_TYPE.- Alimentation : 4 dalles = jusqu'à 16 A en blanc plein. Alimentation 5 V costaude, câbles épais, et
setBrightness8raisonnable. - Mémoire : au-delà de 4 dalles en double tampon, réduisez la profondeur de couleur (
config.setPixelColorDepthBits(5)) ou passez en simple tampon.