feat(display): Implement singleton pattern for EInkDisplayHandler and enhance buffer allocation checks

This commit is contained in:
GW_MC
2026-04-20 15:11:48 +08:00
parent 05a65988dd
commit c5d6cfcd22
3 changed files with 72 additions and 40 deletions

View File

@@ -13,22 +13,31 @@
#define MINIMUM_POWER_ON_DELAY_MS 100 #define MINIMUM_POWER_ON_DELAY_MS 100
#define PARTIAL_REFRESH_THRESHOLD 5 // Full refresh every N partial refreshes #define PARTIAL_REFRESH_THRESHOLD 5 // Full refresh every N partial refreshes
static uint8_t* DRAW_BUFFER; // 1 bit per pixel // Static flag to prevent multiple instances (these buffers are large, only one display allowed)
static uint8_t* OLD_DRAW_BUFFER; // 1 bit per pixel static bool display_instance_exists = false;
static uint8_t* black_data;
static uint8_t* white_data;
EInkDisplayHandler::EInkDisplayHandler() { EInkDisplayHandler::EInkDisplayHandler() {
black_data = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); if (display_instance_exists) {
white_data = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); ESP_LOGE(TAG, "Only one EInkDisplayHandler instance allowed!");
DRAW_BUFFER = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); return;
OLD_DRAW_BUFFER = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); }
memset(black_data, 0xFF, DISPLAY_BUFFER_SIZE); // eink uses 1 for black display_instance_exists = true;
memset(white_data, 0x00, DISPLAY_BUFFER_SIZE);
memset(DRAW_BUFFER, 0x00, DISPLAY_BUFFER_SIZE); // start with all white (0 = white in e-ink) black_data_ = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM));
memset(OLD_DRAW_BUFFER, 0x00, DISPLAY_BUFFER_SIZE); // start with all white (0 = white in e-ink) white_data_ = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM));
draw_buffer_ = DRAW_BUFFER; draw_buffer_ = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM));
old_buffer_ = OLD_DRAW_BUFFER; old_buffer_ = static_cast<uint8_t*>(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM));
// Check for allocation failures
if (!black_data_ || !white_data_ || !draw_buffer_ || !old_buffer_) {
ESP_LOGE(TAG, "Failed to allocate display buffers!");
return;
}
memset(black_data_, 0xFF, DISPLAY_BUFFER_SIZE); // eink uses 1 for black
memset(white_data_, 0x00, DISPLAY_BUFFER_SIZE);
memset(draw_buffer_, 0x00, DISPLAY_BUFFER_SIZE); // start with all white (0 = white in e-ink)
memset(old_buffer_, 0x00, DISPLAY_BUFFER_SIZE); // start with all white (0 = white in e-ink)
refresh_mutex_ = xSemaphoreCreateMutex(); refresh_mutex_ = xSemaphoreCreateMutex();
if (refresh_mutex_ == nullptr) { if (refresh_mutex_ == nullptr) {
@@ -46,18 +55,23 @@ EInkDisplayHandler::~EInkDisplayHandler() {
if (tp_io_handle_ != nullptr) { if (tp_io_handle_ != nullptr) {
esp_lcd_panel_io_del(tp_io_handle_); esp_lcd_panel_io_del(tp_io_handle_);
} }
if (black_data != nullptr) { if (black_data_ != nullptr) {
heap_caps_free(black_data); heap_caps_free(black_data_);
black_data_ = nullptr;
} }
if (white_data != nullptr) { if (white_data_ != nullptr) {
heap_caps_free(white_data); heap_caps_free(white_data_);
white_data_ = nullptr;
} }
if (DRAW_BUFFER != nullptr) { if (draw_buffer_ != nullptr) {
heap_caps_free(DRAW_BUFFER); heap_caps_free(draw_buffer_);
draw_buffer_ = nullptr;
} }
if (OLD_DRAW_BUFFER != nullptr) { if (old_buffer_ != nullptr) {
heap_caps_free(OLD_DRAW_BUFFER); heap_caps_free(old_buffer_);
old_buffer_ = nullptr;
} }
display_instance_exists = false;
} }
esp_err_t EInkDisplayHandler::deep_sleep_display(void) { esp_err_t EInkDisplayHandler::deep_sleep_display(void) {
@@ -185,7 +199,7 @@ esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer, const bool
ESP_LOGE(TAG, "Failed to send old data command: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to send old data command: %s", esp_err_to_name(err));
return err; return err;
} }
err = epd_handler_.transfer_spi_data(white_basemap ? black_data : white_data, DISPLAY_BUFFER_SIZE, transaction_guard.transaction_id()); // Send all white data (0xFF) err = epd_handler_.transfer_spi_data(white_basemap ? black_data_ : white_data_, DISPLAY_BUFFER_SIZE, transaction_guard.transaction_id()); // Send all white data (0xFF)
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send all white data for old data: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to send all white data for old data: %s", esp_err_to_name(err));
return err; return err;
@@ -449,7 +463,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
esp_err_t EInkDisplayHandler::clear_display(void) { esp_err_t EInkDisplayHandler::clear_display(void) {
ESP_LOGV(TAG, "Clearing display to all white..."); ESP_LOGV(TAG, "Clearing display to all white...");
esp_err_t err = full_write(white_data, false); esp_err_t err = full_write(white_data_, false);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to clear display: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to clear display: %s", esp_err_to_name(err));
return err; return err;

View File

@@ -90,9 +90,11 @@ private:
esp_lcd_panel_io_handle_t tp_io_handle_ = nullptr; esp_lcd_panel_io_handle_t tp_io_handle_ = nullptr;
esp_lcd_touch_handle_t tp_handle_ = nullptr; esp_lcd_touch_handle_t tp_handle_ = nullptr;
// this buffer reflects the current display state (1=black, 0=white) // Display buffers (1=black, 0=white)
uint8_t* draw_buffer_ = nullptr; uint8_t* draw_buffer_ = nullptr;
uint8_t* old_buffer_ = nullptr; uint8_t* old_buffer_ = nullptr;
uint8_t* black_data_ = nullptr; // All 0xFF (black pattern)
uint8_t* white_data_ = nullptr; // All 0x00 (white pattern)
RefreshArea refresh_area_ = { 0, 0, 0, 0 }; RefreshArea refresh_area_ = { 0, 0, 0, 0 };
}; };

View File

@@ -4,6 +4,7 @@
#include "common/constants.h" #include "common/constants.h"
#include "esp_lcd_touch_gt911.h" #include "esp_lcd_touch_gt911.h"
#include <driver/i2c.h> #include <driver/i2c.h>
#include <esp_cache.h>
#define TAG "EPDHandler" #define TAG "EPDHandler"
#define BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low #define BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low
@@ -213,10 +214,29 @@ esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& lengt
size_t remaining = length; size_t remaining = length;
gpio_set_level(PIN_DC, 1); // Data mode gpio_set_level(PIN_DC, 1); // Data mode
// Allocate a temporary buffer for inverted data (only if inverted) // Check if data is in PSRAM (needs cache sync and staging buffer)
bool data_in_psram = (esp_ptr_external_ram((void*)data) != 0);
if (data_in_psram) {
// Flush cache to ensure DMA sees the latest data in PSRAM
esp_err_t cache_err = esp_cache_msync((void*)data, length, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
if (cache_err != ESP_OK) {
ESP_LOGW(TAG, "Cache sync failed: %s", esp_err_to_name(cache_err));
}
}
// Use staging buffer in internal DMA-capable RAM
// PSRAM cannot be allocated with MALLOC_CAP_DMA, so we always use a staging buffer
uint8_t* staging_buffer = (uint8_t*)heap_caps_malloc(DMA_TRANSFER_CHUNK_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
if (staging_buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate DMA staging buffer");
return ESP_ERR_NO_MEM;
}
// Additional buffer needed only for inverted data
uint8_t* temp_transfer_buffer = nullptr; uint8_t* temp_transfer_buffer = nullptr;
if (inverted) { if (inverted) {
temp_transfer_buffer = (uint8_t*)heap_caps_malloc(DMA_TRANSFER_CHUNK_SIZE, MALLOC_CAP_DMA); temp_transfer_buffer = (uint8_t*)heap_caps_malloc(DMA_TRANSFER_CHUNK_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
if (temp_transfer_buffer == nullptr) { if (temp_transfer_buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for inverted data transfer buffer"); ESP_LOGE(TAG, "Failed to allocate memory for inverted data transfer buffer");
ESP_LOGI(TAG, "Current free heap size: %u bytes", esp_get_free_heap_size()); ESP_LOGI(TAG, "Current free heap size: %u bytes", esp_get_free_heap_size());
@@ -229,31 +249,27 @@ esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& lengt
while (remaining > 0) { while (remaining > 0) {
size_t transfer_size = (remaining < DMA_TRANSFER_CHUNK_SIZE) ? remaining : DMA_TRANSFER_CHUNK_SIZE; size_t transfer_size = (remaining < DMA_TRANSFER_CHUNK_SIZE) ? remaining : DMA_TRANSFER_CHUNK_SIZE;
const uint8_t* transfer_buffer = nullptr; // Copy data to DMA-capable staging buffer
// Required because PSRAM cannot be allocated with MALLOC_CAP_DMA
if (inverted) { if (inverted) {
// Invert only the current chunk into the temporary buffer // Invert while copying
for (size_t i = 0; i < transfer_size; ++i) { for (size_t i = 0; i < transfer_size; ++i) {
temp_transfer_buffer[i] = ~data[offset + i]; staging_buffer[i] = ~data[offset + i];
} }
transfer_buffer = temp_transfer_buffer;
} else { } else {
transfer_buffer = data + offset; // Straight copy from PSRAM to internal DMA buffer
memcpy(staging_buffer, data + offset, transfer_size);
} }
spi_transaction_t t = {}; spi_transaction_t t = {};
t.length = transfer_size * 8; // Length in bits t.length = transfer_size * 8; // Length in bits
t.tx_buffer = transfer_buffer; t.tx_buffer = staging_buffer;
esp_err_t ret = spi_device_polling_transmit(spi_, &t); esp_err_t ret = spi_device_polling_transmit(spi_, &t);
if (ret != ESP_OK) { if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret)); ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret));
if (ret == ESP_ERR_NO_MEM) { heap_caps_free(staging_buffer);
ESP_LOGE(TAG, "Current free heap size: %u bytes", esp_get_free_heap_size());
ESP_LOGE(TAG, "Current free DMA-capable memory size: %u bytes",
heap_caps_get_free_size(MALLOC_CAP_DMA));
}
if (inverted && temp_transfer_buffer != nullptr) { if (inverted && temp_transfer_buffer != nullptr) {
// Free the temporary inverted buffer
heap_caps_free(temp_transfer_buffer); heap_caps_free(temp_transfer_buffer);
} }
return ret; return ret;
@@ -269,8 +285,8 @@ esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& lengt
} }
} }
heap_caps_free(staging_buffer);
if (inverted && temp_transfer_buffer != nullptr) { if (inverted && temp_transfer_buffer != nullptr) {
// Free the temporary inverted buffer
heap_caps_free(temp_transfer_buffer); heap_caps_free(temp_transfer_buffer);
} }