Matrices LED — dalles HUB75#

← OTA & NVS · Programme

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

Câblage d'une dalle HUB75

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
}

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))

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.

Grille de 2 × 2 dalles

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);
}
Éditer