Skip to content
Open
2 changes: 1 addition & 1 deletion components/hub75/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ set(HUB75_REQUIRES
esp_hw_support
freertos
)
if(IDF_TARGET STREQUAL "esp32p4" OR IDF_TARGET STREQUAL "esp32c6")
if(IDF_VERSION_MAJOR GREATER_EQUAL 5)
list(APPEND HUB75_REQUIRES esp_mm)
endif()
# ESP-IDF 6.0+ split drivers into separate components
Expand Down
13 changes: 13 additions & 0 deletions components/hub75/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,19 @@ menu "HUB75 RGB LED Matrix Driver"
Recommended for best performance. Uses ~2-4 KB of IRAM.
Disable only if IRAM space is critically constrained.

config HUB75_EXTERNAL_FRAMEBUFFERS
bool "Use External Framebuffers"
depends on SOC_SPIRAM_SUPPORTED
default y if IDF_TARGET_ESP32P4
default n
help
Allocates the Framebuffers in the external memory.
It reduces internal memory usage.

If enabled, the HUB75 clock speed may need to be lowered to
avoid overloading the SPIRAM bandwidth, especially since it
may also be shared with other modules such as WiFi.

endmenu

# ========================================
Expand Down
23 changes: 23 additions & 0 deletions components/hub75/include/hub75_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
extern "C" {
#endif

#include "sdkconfig.h"
#include "soc/soc_caps.h"

/**
* IRAM optimization
* Place hot-path code in instruction RAM to prevent flash cache stalls
Expand Down Expand Up @@ -78,6 +81,26 @@ extern "C" {
#endif
#endif

/**
* Use external framebuffer in PSRAM / SPIRAM
* Ser via menuconfig or override: -D HUB75_EXTERNAL_FRAMEBUFFERS
*/
#ifndef HUB75_EXTERNAL_FRAMEBUFFERS
#ifdef CONFIG_HUB75_EXTERNAL_FRAMEBUFFERS
#define HUB75_EXTERNAL_FRAMEBUFFERS CONFIG_HUB75_EXTERNAL_FRAMEBUFFERS
#elif defined(CONFIG_SPIRAM) && defined(CONFIG_IDF_TARGET_ESP32P4)
#define HUB75_EXTERNAL_FRAMEBUFFERS 1
#else
#define HUB75_EXTERNAL_FRAMEBUFFERS 0
#endif
#else // HUB75_EXTERNAL_FRAMEBUFFERS
#if !defined(SOC_SPIRAM_SUPPORTED) && HUB75_EXTERNAL_FRAMEBUFFERS != 0
#pragma message "SOC does not support external framebuffer, disabling..."
#undef HUB75_EXTERNAL_FRAMEBUFFERS
#define HUB75_EXTERNAL_FRAMEBUFFERS 0
#endif
#endif // HUB75_EXTERNAL_FRAMEBUFFERS

#ifdef __cplusplus
}
#endif
54 changes: 51 additions & 3 deletions components/hub75/src/platforms/gdma/gdma_dma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
// Header location changed in ESP-IDF 5.0
#if (ESP_IDF_VERSION_MAJOR >= 5)
#include <esp_private/periph_ctrl.h>
#include <esp_memory_utils.h>
#include <esp_cache.h>
#else
#include "rom/cache.h"
#include <driver/periph_ctrl.h>
#endif
#include <esp_heap_caps.h>
Expand Down Expand Up @@ -387,10 +390,17 @@ bool GdmaDma::allocate_row_buffers() {
size_t pixels_per_bitplane = dma_width_; // DMA buffer width (all panels chained horizontally)
size_t buffer_size_per_row = pixels_per_bitplane * bit_depth_ * 2; // uint16_t = 2 bytes
size_t total_buffer_size = num_rows_ * buffer_size_per_row;
total_buffer_bytes_ = total_buffer_size;

// Always allocate first buffer (buffer A, index 0)
ESP_LOGI(TAG, "Allocating buffer A: %zu bytes for %d rows", total_buffer_size, num_rows_);
dma_buffers_[0] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, MALLOC_CAP_DMA);
#if HUB75_EXTERNAL_FRAMEBUFFERS == 1
static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_SPIRAM;
ESP_LOGI(TAG, "Allocating buffer A: %zu bytes for %d rows (PSRAM)", total_buffer_size, num_rows_);
#else
static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL;
ESP_LOGI(TAG, "Allocating buffer A: %zu bytes for %d rows (internal RAM)", total_buffer_size, num_rows_);
#endif
dma_buffers_[0] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, DMA_MEM_CAPS);
if (!dma_buffers_[0]) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for buffer A", total_buffer_size);
return false;
Expand All @@ -417,7 +427,7 @@ bool GdmaDma::allocate_row_buffers() {
// Conditionally allocate second buffer (buffer B, index 1)
if (config_.double_buffer) {
ESP_LOGI(TAG, "Allocating buffer B: %zu bytes (double buffering enabled)", total_buffer_size);
dma_buffers_[1] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, MALLOC_CAP_DMA);
dma_buffers_[1] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, DMA_MEM_CAPS);
if (!dma_buffers_[1]) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for buffer B", total_buffer_size);
// Continue in single-buffer mode
Expand Down Expand Up @@ -671,6 +681,9 @@ HUB75_IRAM void GdmaDma::draw_pixels(uint16_t x, uint16_t y, uint16_t w, uint16_
HUB75_PROFILE_PIXEL();
}
}
if (!config_.double_buffer) {
flush_cache_to_dma();
}
}

void GdmaDma::clear() {
Expand All @@ -692,6 +705,9 @@ void GdmaDma::clear() {
}
}
}
if (!config_.double_buffer) {
flush_cache_to_dma();
}
}

HUB75_IRAM void GdmaDma::fill(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t r, uint8_t g, uint8_t b) {
Expand Down Expand Up @@ -783,6 +799,9 @@ HUB75_IRAM void GdmaDma::fill(uint16_t x, uint16_t y, uint16_t w, uint16_t h, ui
}
}
}
if (!config_.double_buffer) {
flush_cache_to_dma();
}
}

void GdmaDma::flip_buffer() {
Expand All @@ -791,6 +810,10 @@ void GdmaDma::flip_buffer() {
return;
}

// Flush CPU cache for active buffer BEFORE swap (buffer we were drawing to)
// Only needed in double buffer mode (draw/clear skip flush, defer to here)
flush_cache_to_dma();

// Seamless descriptor chain redirection (no stop/start!)
//
// DMA is continuously traversing a circular descriptor chain (front buffer).
Expand Down Expand Up @@ -1086,6 +1109,8 @@ void GdmaDma::set_brightness_oe() {
}
}

flush_cache_to_dma();

ESP_LOGD(TAG, "Brightness OE configuration complete");
}

Expand Down Expand Up @@ -1279,6 +1304,29 @@ void GdmaDma::calculate_bcm_timings() {
ESP_LOGI(TAG, "BCM timing calculated (lsbMsbTransitionBit used by set_brightness_oe for OE control)");
}

void GdmaDma::flush_cache_to_dma() {
#if HUB75_EXTERNAL_FRAMEBUFFERS == 1
// Only flush for PSRAM (external RAM) - internal SRAM doesn't need cache sync
// This handles ESP32-C6 automatically: C6 uses internal RAM, so esp_ptr_external_ram()
// returns false and we skip the msync (which would be unnecessary overhead).
#if (ESP_IDF_VERSION_MAJOR >= 5)
if (!dma_buffers_[active_idx_] || !esp_ptr_external_ram(dma_buffers_[active_idx_])) {
return;
}

// Flush cache: CPU cache → PSRAM (C2M = Cache to Memory)
// Flush the active buffer (CPU drawing buffer)
esp_err_t err = esp_cache_msync(dma_buffers_[active_idx_], total_buffer_bytes_,
ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_UNALIGNED);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Cache sync failed: %s", esp_err_to_name(err));
}
#else
Cache_WriteBack_Addr((uint32_t) &p[x_coord], sizeof(ESP32_I2S_DMA_STORAGE_TYPE));
#endif
#endif
}

// ============================================================================
// Compile-Time Validation (ESP-IDF 5.x only - requires consteval/GCC 9+)
// ============================================================================
Expand Down
4 changes: 4 additions & 0 deletions components/hub75/src/platforms/gdma/gdma_dma.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ class GdmaDma : public PlatformDma {
// BCM timing calculation (calculates lsbMsbTransitionBit for OE control)
void calculate_bcm_timings();

void flush_cache_to_dma();

size_t total_buffer_bytes_; // Cached total buffer size per buffer (computed once, never changes)

gdma_channel_handle_t dma_chan_;
const uint8_t bit_depth_; // Bit depth from config (6, 7, 8, 10, or 12)
uint8_t lsbMsbTransitionBit_; // BCM optimization threshold (calculated at init)
Expand Down
62 changes: 56 additions & 6 deletions components/hub75/src/platforms/i2s/i2s_dma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
// Header location changed in ESP-IDF 5.0
#if (ESP_IDF_VERSION_MAJOR >= 5)
#include <esp_private/periph_ctrl.h>
#include <esp_memory_utils.h>
#include <esp_cache.h>
#else
#include "rom/cache.h"
#include <driver/periph_ctrl.h>
#endif
#include <soc/gpio_sig_map.h>
Expand Down Expand Up @@ -320,12 +323,12 @@ HUB75_CONST uint32_t I2sDma::resolve_actual_clock_speed(Hub75ClockSpeed clock_sp
//
// The ESP32's lower base clock (80 MHz vs 160 MHz) combined with the same
// divider constraints results in half the max frequency of ESP32-S2.
if (requested_hz > 10000000) {
if (requested_hz > 20000000) {
ESP_LOGW(TAG, "Requested %u Hz exceeds ESP32 max (10 MHz), using 10 MHz", (unsigned int) requested_hz);
return 10000000;
return 20000000;
}
uint32_t divider = (80000000 + requested_hz * 2) / (requested_hz * 4); // Round to nearest
return 80000000 / (std::max(divider, uint32_t{2}) * 4);
return 80000000 / (std::max(divider, uint32_t{1}) * 4);
#endif
}

Expand Down Expand Up @@ -443,10 +446,17 @@ bool I2sDma::allocate_row_buffers() {
size_t pixels_per_bitplane = dma_width_; // DMA buffer width (all panels chained horizontally)
size_t buffer_size_per_row = pixels_per_bitplane * bit_depth_ * 2; // uint16_t = 2 bytes
size_t total_buffer_size = num_rows_ * buffer_size_per_row;
total_buffer_bytes_ = total_buffer_size;

// Always allocate first buffer (buffer A, index 0)
ESP_LOGI(TAG, "Allocating buffer A: %zu bytes for %d rows", total_buffer_size, num_rows_);
dma_buffers_[0] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, MALLOC_CAP_DMA);
#if HUB75_EXTERNAL_FRAMEBUFFERS == 1
static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_SPIRAM;
ESP_LOGI(TAG, "Allocating buffer A: %zu bytes for %d rows (PSRAM)", total_buffer_size, num_rows_);
#else
static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL;
ESP_LOGI(TAG, "Allocating buffer A: %zu bytes for %d rows (internal RAM)", total_buffer_size, num_rows_);
#endif
dma_buffers_[0] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, DMA_MEM_CAPS);
if (!dma_buffers_[0]) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for buffer A", total_buffer_size);
return false;
Expand All @@ -473,7 +483,7 @@ bool I2sDma::allocate_row_buffers() {
// Conditionally allocate second buffer (buffer B, index 1)
if (config_.double_buffer) {
ESP_LOGI(TAG, "Allocating buffer B: %zu bytes (double buffering enabled)", total_buffer_size);
dma_buffers_[1] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, MALLOC_CAP_DMA);
dma_buffers_[1] = (uint8_t *) heap_caps_calloc(1, total_buffer_size, DMA_MEM_CAPS);
if (!dma_buffers_[1]) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for buffer B", total_buffer_size);
// Continue in single-buffer mode
Expand Down Expand Up @@ -903,6 +913,8 @@ void I2sDma::set_brightness_oe() {
}
}

flush_cache_to_dma();

ESP_LOGD(TAG, "Brightness OE configuration complete");
}

Expand Down Expand Up @@ -1138,6 +1150,9 @@ HUB75_IRAM void I2sDma::draw_pixels(uint16_t x, uint16_t y, uint16_t w, uint16_t
HUB75_PROFILE_PIXEL();
}
}
if (!config_.double_buffer) {
flush_cache_to_dma();
}
}

void I2sDma::clear() {
Expand All @@ -1159,6 +1174,9 @@ void I2sDma::clear() {
}
}
}
if (!config_.double_buffer) {
flush_cache_to_dma();
}
}

HUB75_IRAM void I2sDma::fill(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t r, uint8_t g, uint8_t b) {
Expand Down Expand Up @@ -1245,6 +1263,9 @@ HUB75_IRAM void I2sDma::fill(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uin
}
}
}
if (!config_.double_buffer) {
flush_cache_to_dma();
}
}

void I2sDma::flip_buffer() {
Expand All @@ -1253,6 +1274,10 @@ void I2sDma::flip_buffer() {
return;
}

// Flush CPU cache for active buffer BEFORE swap (buffer we were drawing to)
// Only needed in double buffer mode (draw/clear skip flush, defer to here)
flush_cache_to_dma();

// Seamless descriptor chain redirection (no stop/start!)
//
// DMA is continuously traversing a circular descriptor chain (front buffer).
Expand All @@ -1276,6 +1301,31 @@ void I2sDma::flip_buffer() {
// DMA seamlessly transitions at next frame boundary - no interruption!
}

void I2sDma::flush_cache_to_dma() {
#if HUB75_EXTERNAL_FRAMEBUFFERS == 1
// Only flush for PSRAM (external RAM) - internal SRAM doesn't need cache sync
// This handles ESP32-C6 automatically: C6 uses internal RAM, so esp_ptr_external_ram()
// returns false and we skip the msync (which would be unnecessary overhead).
#if (ESP_IDF_VERSION_MAJOR >= 5)
if (!dma_buffers_[active_idx_] || !esp_ptr_external_ram(dma_buffers_[active_idx_])) {
return;
}

// Flush cache: CPU cache → PSRAM (C2M = Cache to Memory)
// Flush the active buffer (CPU drawing buffer)
esp_err_t err = esp_cache_msync(dma_buffers_[active_idx_], total_buffer_bytes_,
ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_UNALIGNED);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Cache sync failed: %s", esp_err_to_name(err));
}
#else
#ifndef CONFIG_IDF_TARGET_ESP32 // ESP32 has no cache writeback in IDF < v5
Cache_WriteBack_Addr((uint32_t) &p[x_coord], sizeof(ESP32_I2S_DMA_STORAGE_TYPE));
#endif
#endif
#endif
}

// ============================================================================
// Compile-Time Validation (ESP-IDF 5.x only - requires consteval/GCC 9+)
// ============================================================================
Expand Down
4 changes: 4 additions & 0 deletions components/hub75/src/platforms/i2s/i2s_dma.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ class I2sDma : public PlatformDma {
// BCM timing calculation (calculates lsbMsbTransitionBit for OE control)
void calculate_bcm_timings();

void flush_cache_to_dma();

size_t total_buffer_bytes_; // Cached total buffer size per buffer (computed once, never changes)

volatile i2s_dev_t *i2s_dev_;
const uint8_t bit_depth_; // Bit depth from config (6, 7, 8, 10, or 12)
uint8_t lsbMsbTransitionBit_; // BCM optimization threshold (calculated at init)
Expand Down
Loading
Loading