diff --git a/components/hub75/CMakeLists.txt b/components/hub75/CMakeLists.txt index 2ab2dd4..51a133f 100644 --- a/components/hub75/CMakeLists.txt +++ b/components/hub75/CMakeLists.txt @@ -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 diff --git a/components/hub75/Kconfig b/components/hub75/Kconfig index 665d726..5b69852 100644 --- a/components/hub75/Kconfig +++ b/components/hub75/Kconfig @@ -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 # ======================================== diff --git a/components/hub75/include/hub75_config.h b/components/hub75/include/hub75_config.h index 7d0a395..3611d17 100644 --- a/components/hub75/include/hub75_config.h +++ b/components/hub75/include/hub75_config.h @@ -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 @@ -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 diff --git a/components/hub75/src/platforms/gdma/gdma_dma.cpp b/components/hub75/src/platforms/gdma/gdma_dma.cpp index 5f07714..476329b 100644 --- a/components/hub75/src/platforms/gdma/gdma_dma.cpp +++ b/components/hub75/src/platforms/gdma/gdma_dma.cpp @@ -37,7 +37,10 @@ // Header location changed in ESP-IDF 5.0 #if (ESP_IDF_VERSION_MAJOR >= 5) #include +#include +#include #else +#include "rom/cache.h" #include #endif #include @@ -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; @@ -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 @@ -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() { @@ -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) { @@ -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() { @@ -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). @@ -1086,6 +1109,8 @@ void GdmaDma::set_brightness_oe() { } } + flush_cache_to_dma(); + ESP_LOGD(TAG, "Brightness OE configuration complete"); } @@ -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+) // ============================================================================ diff --git a/components/hub75/src/platforms/gdma/gdma_dma.h b/components/hub75/src/platforms/gdma/gdma_dma.h index 144ee2a..0227b36 100644 --- a/components/hub75/src/platforms/gdma/gdma_dma.h +++ b/components/hub75/src/platforms/gdma/gdma_dma.h @@ -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) diff --git a/components/hub75/src/platforms/i2s/i2s_dma.cpp b/components/hub75/src/platforms/i2s/i2s_dma.cpp index 1a455e4..1903037 100644 --- a/components/hub75/src/platforms/i2s/i2s_dma.cpp +++ b/components/hub75/src/platforms/i2s/i2s_dma.cpp @@ -26,7 +26,10 @@ // Header location changed in ESP-IDF 5.0 #if (ESP_IDF_VERSION_MAJOR >= 5) #include +#include +#include #else +#include "rom/cache.h" #include #endif #include @@ -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 } @@ -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; @@ -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 @@ -903,6 +913,8 @@ void I2sDma::set_brightness_oe() { } } + flush_cache_to_dma(); + ESP_LOGD(TAG, "Brightness OE configuration complete"); } @@ -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() { @@ -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) { @@ -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() { @@ -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). @@ -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+) // ============================================================================ diff --git a/components/hub75/src/platforms/i2s/i2s_dma.h b/components/hub75/src/platforms/i2s/i2s_dma.h index bd3ba1b..6b16188 100644 --- a/components/hub75/src/platforms/i2s/i2s_dma.h +++ b/components/hub75/src/platforms/i2s/i2s_dma.h @@ -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) diff --git a/components/hub75/src/platforms/parlio/parlio_dma.cpp b/components/hub75/src/platforms/parlio/parlio_dma.cpp index 11afc53..428ee83 100644 --- a/components/hub75/src/platforms/parlio/parlio_dma.cpp +++ b/components/hub75/src/platforms/parlio/parlio_dma.cpp @@ -61,6 +61,16 @@ constexpr uint16_t RGB_MASK = RGB_UPPER_MASK | RGB_LOWER_MASK; // 0x003F constexpr uint16_t OE_CLEAR_MASK = ~(1 << OE_BIT); constexpr uint16_t RGB_CLEAR_MASK = ~RGB_MASK; // Clear RGB bits 0-5 +#ifndef SOC_PARLIO_TX_SUPPORT_LOOP_TRANSMISSION +IRAM_ATTR bool parlio_trans_done_callback(parlio_tx_unit_handle_t tx_unit, const parlio_tx_done_event_data_t *edata, + void *user_ctx) { + ParlioDma *dma = (ParlioDma *) user_ctx; + size_t total_bits = dma->total_buffer_bytes_ * 8; + parlio_tx_unit_transmit(dma->tx_unit_, dma->dma_buffers_[dma->front_idx_], total_bits, &dma->transmit_config_); + return false; // No Higher level Task woken +} +#endif + ParlioDma::ParlioDma(const Hub75Config &config) : PlatformDma(config), tx_unit_(nullptr), @@ -94,10 +104,15 @@ ParlioDma::ParlioDma(const Hub75Config &config) // Initialize transmit config // Note: For four-scan panels, dma_width_ is doubled and num_rows_ is halved // to match the physical shift register layout - transmit_config_.idle_value = 0x00; + transmit_config_.idle_value = 1 << OE_BIT; // make sure nothing lights up when in idle transmit_config_.bitscrambler_program = nullptr; +#ifdef SOC_PARLIO_TX_SUPPORT_LOOP_TRANSMISSION transmit_config_.flags.queue_nonblocking = 0; transmit_config_.flags.loop_transmission = 1; // Continuous refresh +#else + transmit_config_.flags.queue_nonblocking = 1; // enable the restart loop + transmit_config_.flags.loop_transmission = 0; +#endif } ParlioDma::~ParlioDma() { ParlioDma::shutdown(); } @@ -288,6 +303,17 @@ void ParlioDma::configure_parlio() { ESP_LOGI(TAG, " Clock gating: NOT SUPPORTED"); #endif ESP_LOGI(TAG, " Transaction queue depth: %zu", config.trans_queue_depth); + +#ifndef SOC_PARLIO_TX_SUPPORT_LOOP_TRANSMISSION + parlio_event_cbs.on_trans_done = parlio_trans_done_callback; + parlio_event_cbs.on_buffer_switched = nullptr; + err = parlio_tx_unit_register_event_callbacks(tx_unit_, &parlio_event_cbs, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register event callbacks: %s", esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "Event callbacks registered"); +#endif } HUB75_CONST uint32_t ParlioDma::resolve_actual_clock_speed(Hub75ClockSpeed clock_speed) const { @@ -436,14 +462,15 @@ bool ParlioDma::allocate_row_buffers() { total_buffer_bytes_ = total_bytes; // Cache for flush_cache_to_dma() and build_transaction_queue() // Always allocate first buffer (buffer 0) + +#if HUB75_EXTERNAL_FRAMEBUFFERS == 1 + static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_SPIRAM; + ESP_LOGI(TAG, "Allocating buffer [0]: %zu bytes for %d rows × %d bits (PSRAM)", total_bytes, num_rows_, bit_depth_); +#else // ESP32-C6 has no PSRAM, so use internal DMA-capable memory -#ifdef CONFIG_IDF_TARGET_ESP32C6 static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL; ESP_LOGI(TAG, "Allocating buffer [0]: %zu bytes for %d rows × %d bits (internal RAM)", total_bytes, num_rows_, bit_depth_); -#else - static constexpr uint32_t DMA_MEM_CAPS = MALLOC_CAP_DMA | MALLOC_CAP_SPIRAM; - ESP_LOGI(TAG, "Allocating buffer [0]: %zu bytes for %d rows × %d bits (PSRAM)", total_bytes, num_rows_, bit_depth_); #endif dma_buffers_[0] = (uint16_t *) heap_caps_calloc(total_words, sizeof(uint16_t), DMA_MEM_CAPS); @@ -775,6 +802,7 @@ void ParlioDma::set_brightness_oe() { } void ParlioDma::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). @@ -789,6 +817,7 @@ void ParlioDma::flush_cache_to_dma() { if (err != ESP_OK) { ESP_LOGW(TAG, "Cache sync failed: %s", esp_err_to_name(err)); } +#endif } bool ParlioDma::build_transaction_queue() { @@ -1087,12 +1116,14 @@ void ParlioDma::flip_buffer() { // Swap indices (front ↔ active) std::swap(front_idx_, active_idx_); +#ifdef SOC_PARLIO_TX_SUPPORT_LOOP_TRANSMISSION // Queue new front buffer (hardware switches seamlessly after current frame) size_t total_bits = total_buffer_bytes_ * 8; esp_err_t err = parlio_tx_unit_transmit(tx_unit_, dma_buffers_[front_idx_], total_bits, &transmit_config_); if (err != ESP_OK) { ESP_LOGW(TAG, "flip_buffer: Failed to queue buffer: %s", esp_err_to_name(err)); } +#endif } } // namespace hub75 diff --git a/components/hub75/src/platforms/parlio/parlio_dma.h b/components/hub75/src/platforms/parlio/parlio_dma.h index 3d64f01..879d8e9 100644 --- a/components/hub75/src/platforms/parlio/parlio_dma.h +++ b/components/hub75/src/platforms/parlio/parlio_dma.h @@ -117,6 +117,12 @@ class ParlioDma : public PlatformDma { uint8_t basis_brightness_; float intensity_; bool transfer_started_; + +#ifndef SOC_PARLIO_TX_SUPPORT_LOOP_TRANSMISSION + friend bool parlio_trans_done_callback(parlio_tx_unit_handle_t tx_unit, const parlio_tx_done_event_data_t *edata, + void *user_ctx); + parlio_tx_event_callbacks_t parlio_event_cbs; +#endif }; } // namespace hub75