Good morning.
For the last few days I was experimenting and testing various ST7735 displays with an ESP32 S3 Devkit C1 N8R8, and also testing optimizing tricks and under the hood working as I read along about them.
From that endeavour, my first step to a long term goal has come to fruition.
I have managed to assemble together my own graphics library implementation, designed to drive two BACKTAB ST7735 displays efficiently (more displays will be added soon, starting from all the displays I own), using as much techniques i could learn about from LX7 specific optimizations and a slightly better knowledge of C++ in general and the βunder the hoodβ working of Arduino AVR GCC Compiler.
My focus was to deliver a perceptive and undisputed smooth animation (Frame State Transitions) rather than brute chasing FPS.
It may not provide much, but from what it does, mostly they have been robust tested.
I have not started to make a GitHub repo yet, but it seems like the time might be coming soon if I work on this further.
I only have Devkit C1 with me so far, so I only could test it there. If anyone else would like to test it, feel free to do so and even modify and use it in whichever project you like, provided this niche hardware setup somehow comes to be of use.
This is still a work in progress and patches and updates for different screen are soon to come.
First small goal is to cover all ST7735 lineup. And improve on it as per my project needs.
Presenting the Beta release of:
TFTFriend - ESP32 ST7735 Duo BLACKTAB GFX Library
Arduino SPI
Hardware Requirement:
- ESP32 (S3 N8R8 Devkit C1 tested)
- Two ST7735 BLACKTAB SPI displays with common MOSI and SCK bus line.
/*
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONNECTION SUMMARY TABLE β
βββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββ€
β Function β ESP32-S3 GPIO β Display #1 β Display #2 β
βββββββββββββββββββΌββββββββββββββββββΌββββββββββββββββββΌββββββββββββββββ€
β SPI Clock β GPIO 12 β SCL Pin β SCL Pin β
β SPI Data β GPIO 11 β SDA Pin β SDA Pin β
β Chip Select 1 β GPIO 15 β CS Pin β N/C β
β Chip Select 2 β GPIO 10 β N/C β CS Pin β
β Data/Command 1 β GPIO 7 β DC Pin β N/C β
β Data/Command 2 β GPIO 4 β N/C β DC Pin β
β Reset 1 β GPIO 6 β RST Pin β N/C β
β Reset 2 β GPIO 5 β N/C β RST Pin β
β Power (+3.3V) β 3V3 β VCC Pin β VCC Pin β
β Ground β GND β GND Pin β GND Pin β
βββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββ
PS: For no manual control for the display Backlight, treat LED+ as VCC and LED- as ground. Do refer to respective datasheet for 5v5 compatibility. the used BLACKTAB display supports 5v5 logic as well.
*/
Library Files:
ST7735DualEngine.h:
/**************************************************************
* (Core1D) TFTFriend - ESP32S3 ST7735Duo Graphics Engine
* Advanced Dual-Display Graphics Library for ESP32 Family
* --------------------------------------------------------
* Version: 2.1 Tensilica LX7 Optimized
* Platform: ESP32, ESP32-S2, ESP32-S3, ESP32-C3
* License: MIT
* For License details check the main library implementation file.
**************************************************************/
#pragma once
#pragma GCC optimize("O3")
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include <Arduino.h>
#include <SPI.h>
#include <pgmspace.h>
// Platform detection and capability mapping
#if defined(CONFIG_IDF_TARGET_ESP32)
#define ST7735_PLATFORM_ESP32
#define ST7735_HAS_DUAL_CORE 1
#define ST7735_HAS_SIMD 0
#define ST7735_DEFAULT_CORE_GRAPHICS 0
#define ST7735_DEFAULT_CORE_DISPLAY 1
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
#define ST7735_PLATFORM_ESP32S2
#define ST7735_HAS_DUAL_CORE 0
#define ST7735_HAS_SIMD 0
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
#define ST7735_PLATFORM_ESP32S3
#define ST7735_HAS_DUAL_CORE 1
#define ST7735_HAS_SIMD 1 // LX7 has limited SIMD
#define ST7735_DEFAULT_CORE_GRAPHICS 0
#define ST7735_DEFAULT_CORE_DISPLAY 1
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
#define ST7735_PLATFORM_ESP32C3
#define ST7735_HAS_DUAL_CORE 0
#define ST7735_HAS_SIMD 0
#else
#define ST7735_PLATFORM_GENERIC
#define ST7735_HAS_DUAL_CORE 0
#define ST7735_HAS_SIMD 0
#endif
// Enhanced optimization macros
#define FORCE_INLINE __attribute__((always_inline)) inline
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#define CACHE_ALIGNED __attribute__((aligned(128))) // Enhanced for LX7
#define FAST_CODE_ATTR IRAM_ATTR
#define PREFETCH(addr) __builtin_prefetch((addr), 0, 3)
#define HOT_FUNCTION __attribute__((hot))
#define COLD_FUNCTION __attribute__((cold))
// SIMD support for ESP32-S3
#if ST7735_HAS_SIMD
#include "xtensa/core-macros.h"
#define VECTORIZED __attribute__((optimize("O3")))
#else
#define VECTORIZED
#endif
// Force Arduino SPI only
#define ST7735_ENABLE_DMA 0
#define ST7735_ENABLE_DUAL_CORE ST7735_HAS_DUAL_CORE
// Include FreeRTOS only for dual-core support
#if ST7735_ENABLE_DUAL_CORE
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#endif
// ========================================
// ENHANCED CONFIGURATION MACROS
// ========================================
#ifndef ST7735_DUAL_CONFIG
#define ST7735_DUAL_CONFIG
// Hardware Configuration - Platform-adaptive defaults
#if defined(ST7735_PLATFORM_ESP32S3)
#ifndef ST7735_SCREEN0_CS
#define ST7735_SCREEN0_CS 15
#endif
#ifndef ST7735_SCREEN0_DC
#define ST7735_SCREEN0_DC 7
#endif
#ifndef ST7735_SCREEN0_RST
#define ST7735_SCREEN0_RST 6
#endif
#ifndef ST7735_SCREEN1_CS
#define ST7735_SCREEN1_CS 10
#endif
#ifndef ST7735_SCREEN1_DC
#define ST7735_SCREEN1_DC 4
#endif
#ifndef ST7735_SCREEN1_RST
#define ST7735_SCREEN1_RST 5
#endif
#ifndef ST7735_SPI_MOSI
#define ST7735_SPI_MOSI 11
#endif
#ifndef ST7735_SPI_SCLK
#define ST7735_SPI_SCLK 12
#endif
#else
// Generic ESP32 family defaults
#ifndef ST7735_SCREEN0_CS
#define ST7735_SCREEN0_CS 15
#endif
#ifndef ST7735_SCREEN0_DC
#define ST7735_SCREEN0_DC 2
#endif
#ifndef ST7735_SCREEN0_RST
#define ST7735_SCREEN0_RST 4
#endif
#ifndef ST7735_SCREEN1_CS
#define ST7735_SCREEN1_CS 5
#endif
#ifndef ST7735_SCREEN1_DC
#define ST7735_SCREEN1_DC 16
#endif
#ifndef ST7735_SCREEN1_RST
#define ST7735_SCREEN1_RST 17
#endif
#ifndef ST7735_SPI_MOSI
#define ST7735_SPI_MOSI 23
#endif
#ifndef ST7735_SPI_SCLK
#define ST7735_SPI_SCLK 18
#endif
#endif
// Enhanced Performance Configuration
#if defined(ST7735_PLATFORM_ESP32S3)
#ifndef ST7735_SPI_FREQUENCY
#define ST7735_SPI_FREQUENCY 80000000UL // ESP32-S3 maximum
#endif
#ifndef ST7735_BURST_SIZE
#define ST7735_BURST_SIZE 8192 // Larger bursts for S3
#endif
#elif defined(ST7735_PLATFORM_ESP32)
#ifndef ST7735_SPI_FREQUENCY
#define ST7735_SPI_FREQUENCY 40000000UL // ESP32 stable maximum
#endif
#ifndef ST7735_BURST_SIZE
#define ST7735_BURST_SIZE 4096
#endif
#else
#ifndef ST7735_SPI_FREQUENCY
#define ST7735_SPI_FREQUENCY 26000000UL // Conservative default
#endif
#ifndef ST7735_BURST_SIZE
#define ST7735_BURST_SIZE 2048
#endif
#endif
// Memory Pool Configuration
#ifndef ST7735_MEMORY_POOL_SIZE
#define ST7735_MEMORY_POOL_SIZE 32768 // 32KB pool for frequent allocations
#endif
// Enhanced reset timing
#ifndef ST7735_RESET_DELAY_MS
#define ST7735_RESET_DELAY_MS 15
#endif
#ifndef ST7735_WAKE_DELAY_MS
#define ST7735_WAKE_DELAY_MS 50
#endif
#ifndef ST7735_INIT_STABILITY_DELAY_MS
#define ST7735_INIT_STABILITY_DELAY_MS 10
#endif
// Memory Configuration - Enhanced for S3
#ifndef ST7735_USE_PSRAM
#if defined(ST7735_PLATFORM_ESP32S3) || defined(ST7735_PLATFORM_ESP32S2)
#define ST7735_USE_PSRAM 1
#else
#define ST7735_USE_PSRAM 0
#endif
#endif
#ifndef ST7735_ENABLE_BOUNDS_CHECK
#define ST7735_ENABLE_BOUNDS_CHECK 1
#endif
#ifndef ST7735_ENABLE_DEBUG
#define ST7735_ENABLE_DEBUG 0
#endif
#ifndef ST7735_ENABLE_FAST_FILL
#define ST7735_ENABLE_FAST_FILL 1
#endif
#ifndef ST7735_ENABLE_BUFFER_PREFETCH
#define ST7735_ENABLE_BUFFER_PREFETCH 1
#endif
// Task Configuration for dual-core
#if ST7735_ENABLE_DUAL_CORE
#ifndef ST7735_GRAPHICS_TASK_STACK
#define ST7735_GRAPHICS_TASK_STACK 4096
#endif
#ifndef ST7735_DISPLAY_TASK_STACK
#define ST7735_DISPLAY_TASK_STACK 2048
#endif
#ifndef ST7735_TASK_PRIORITY
#define ST7735_TASK_PRIORITY 5
#endif
#endif
#endif // ST7735_DUAL_CONFIG
// ========================================
// DISPLAY CONSTANTS & COLOR SPACE
// ========================================
namespace ST7735Colors {
constexpr uint16_t BLACK = 0x0000;
constexpr uint16_t WHITE = 0xFFFF;
constexpr uint16_t RED = 0x001F; // BGR
constexpr uint16_t GREEN = 0x07E0; // Same
constexpr uint16_t BLUE = 0xF800; // BGR
constexpr uint16_t YELLOW = 0x07FF; // BGR
constexpr uint16_t CYAN = 0xFFE0; // BGR
constexpr uint16_t MAGENTA = 0xF81F; // Same
constexpr uint16_t ORANGE = 0x041F; // BGR
constexpr uint16_t PINK = 0xFE19;
constexpr uint16_t GRAY = 0x8410;
constexpr uint16_t DARK_GRAY = 0x4208;
constexpr uint16_t LIGHT_GRAY = 0xC618;
constexpr uint16_t PURPLE = 0x8010;
constexpr uint16_t BROWN = 0xA145;
}
namespace ST7735Specs {
constexpr uint16_t WIDTH = 128;
constexpr uint16_t HEIGHT = 160;
constexpr uint32_t PIXELS = WIDTH * HEIGHT;
constexpr uint32_t BUFFER_BYTES = PIXELS * sizeof(uint16_t);
constexpr uint8_t FONT_WIDTH = 5;
constexpr uint8_t FONT_HEIGHT = 7;
constexpr uint8_t FONT_SPACING = 6;
constexpr uint8_t FONT_LINE_HEIGHT = 8;
}
// ========================================
// OPTIMIZED FONT DATA - Adafruit Referenced
// ========================================
// Credits: Based on Adafruit GFX library font format
static const uint8_t font5x7_standard[][ST7735Specs::FONT_WIDTH] = {
// ===== BASIC SYMBOLS (ASCII 32-47) =====
{0x00, 0x00, 0x00, 0x00, 0x00}, // Space (32) - Index 0
{0x00, 0x00, 0x5F, 0x00, 0x00}, // ! (33) - Index 1
{0x00, 0x07, 0x00, 0x07, 0x00}, // " (34) - Index 2
{0x14, 0x7F, 0x14, 0x7F, 0x14}, // # (35) - Index 3
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // $ (36) - Index 4
{0x23, 0x13, 0x08, 0x64, 0x62}, // % (37) - Index 5
{0x36, 0x49, 0x55, 0x22, 0x50}, // & (38) - Index 6
{0x00, 0x05, 0x03, 0x00, 0x00}, // ' (39) - Index 7
{0x00, 0x1C, 0x22, 0x41, 0x00}, // ( (40) - Index 8
{0x00, 0x41, 0x22, 0x1C, 0x00}, // ) (41) - Index 9
{0x14, 0x08, 0x3E, 0x08, 0x14}, // * (42) - Index 10
{0x08, 0x08, 0x3E, 0x08, 0x08}, // + (43) - Index 11
{0x00, 0x50, 0x30, 0x00, 0x00}, // , (44) - Index 12
{0x08, 0x08, 0x08, 0x08, 0x08}, // - (45) - Index 13
{0x00, 0x60, 0x60, 0x00, 0x00}, // . (46) - Index 14
{0x20, 0x10, 0x08, 0x04, 0x02}, // / (47) - Index 15
// ===== NUMBERS 0-9 (ASCII 48-57) =====
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // 0 (48) - Index 16
{0x00, 0x42, 0x7F, 0x40, 0x00}, // 1 (49) - Index 17
{0x42, 0x61, 0x51, 0x49, 0x46}, // 2 (50) - Index 18
{0x21, 0x41, 0x45, 0x4B, 0x31}, // 3 (51) - Index 19
{0x18, 0x14, 0x12, 0x7F, 0x10}, // 4 (52) - Index 20
{0x27, 0x45, 0x45, 0x45, 0x39}, // 5 (53) - Index 21
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // 6 (54) - Index 22
{0x01, 0x71, 0x09, 0x05, 0x03}, // 7 (55) - Index 23
{0x36, 0x49, 0x49, 0x49, 0x36}, // 8 (56) - Index 24
{0x06, 0x49, 0x49, 0x29, 0x1E}, // 9 (57) - Index 25
// ===== PUNCTUATION (ASCII 58-64) =====
{0x00, 0x36, 0x36, 0x00, 0x00}, // : (58) - Index 26
{0x00, 0x56, 0x36, 0x00, 0x00}, // ; (59) - Index 27
{0x08, 0x14, 0x22, 0x41, 0x00}, // < (60) - Index 28
{0x14, 0x14, 0x14, 0x14, 0x14}, // = (61) - Index 29
{0x00, 0x41, 0x22, 0x14, 0x08}, // > (62) - Index 30
{0x02, 0x01, 0x51, 0x09, 0x06}, // ? (63) - Index 31
{0x32, 0x49, 0x79, 0x41, 0x3E}, // @ (64) - Index 32
// ===== UPPERCASE LETTERS A-Z (ASCII 65-90) =====
{0x7E, 0x11, 0x11, 0x11, 0x7E}, // A (65) - Index 33
{0x7F, 0x49, 0x49, 0x49, 0x36}, // B (66) - Index 34
{0x3E, 0x41, 0x41, 0x41, 0x22}, // C (67) - Index 35
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // D (68) - Index 36
{0x7F, 0x49, 0x49, 0x49, 0x41}, // E (69) - Index 37
{0x7F, 0x09, 0x09, 0x09, 0x01}, // F (70) - Index 38
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // G (71) - Index 39
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // H (72) - Index 40
{0x00, 0x41, 0x7F, 0x41, 0x00}, // I (73) - Index 41
{0x20, 0x40, 0x41, 0x3F, 0x01}, // J (74) - Index 42
{0x7F, 0x08, 0x14, 0x22, 0x41}, // K (75) - Index 43
{0x7F, 0x40, 0x40, 0x40, 0x40}, // L (76) - Index 44
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // M (77) - Index 45
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // N (78) - Index 46
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // O (79) - Index 47
{0x7F, 0x09, 0x09, 0x09, 0x06}, // P (80) - Index 48
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // Q (81) - Index 49
{0x7F, 0x09, 0x19, 0x29, 0x46}, // R (82) - Index 50
{0x46, 0x49, 0x49, 0x49, 0x31}, // S (83) - Index 51
{0x01, 0x01, 0x7F, 0x01, 0x01}, // T (84) - Index 52
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // U (85) - Index 53
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // V (86) - Index 54
{0x3F, 0x40, 0x38, 0x40, 0x3F}, // W (87) - Index 55
{0x63, 0x14, 0x08, 0x14, 0x63}, // X (88) - Index 56
{0x07, 0x08, 0x70, 0x08, 0x07}, // Y (89) - Index 57
{0x61, 0x51, 0x49, 0x45, 0x43}, // Z (90) - Index 58
// ===== BRACKETS & SYMBOLS (ASCII 91-96) =====
{0x00, 0x7F, 0x41, 0x41, 0x00}, // [ (91) - Index 59
{0x02, 0x04, 0x08, 0x10, 0x20}, // \ (92) - Index 60
{0x00, 0x41, 0x41, 0x7F, 0x00}, // ] (93) - Index 61
{0x04, 0x02, 0x01, 0x02, 0x04}, // ^ (94) - Index 62
{0x40, 0x40, 0x40, 0x40, 0x40}, // _ (95) - Index 63
{0x00, 0x01, 0x02, 0x04, 0x00}, // ` (96) - Index 64
// ===== LOWERCASE LETTERS a-z (ASCII 97-122) =====
{0x20, 0x54, 0x54, 0x54, 0x78}, // a (97) - Index 65
{0x7F, 0x48, 0x44, 0x44, 0x38}, // b (98) - Index 66
{0x38, 0x44, 0x44, 0x44, 0x20}, // c (99) - Index 67
{0x38, 0x44, 0x44, 0x48, 0x7F}, // d (100) - Index 68
{0x38, 0x54, 0x54, 0x54, 0x18}, // e (101) - Index 69
{0x08, 0x7E, 0x09, 0x01, 0x02}, // f (102) - Index 70
{0x0C, 0x52, 0x52, 0x52, 0x3E}, // g (103) - Index 71 - Descender Corrected
{0x7F, 0x08, 0x04, 0x04, 0x78}, // h (104) - Index 72
{0x00, 0x44, 0x7D, 0x40, 0x00}, // i (105) - Index 73
{0x20, 0x40, 0x44, 0x3D, 0x00}, // j (106) - Index 74 - Descender Corrected
{0x7F, 0x10, 0x28, 0x44, 0x00}, // k (107) - Index 75
{0x00, 0x41, 0x7F, 0x40, 0x00}, // l (108) - Index 76
{0x7C, 0x04, 0x18, 0x04, 0x78}, // m (109) - Index 77
{0x7C, 0x08, 0x04, 0x04, 0x78}, // n (110) - Index 78
{0x38, 0x44, 0x44, 0x44, 0x38}, // o (111) - Index 79
{0x7C, 0x14, 0x14, 0x14, 0x08}, // p (112) - Index 80 - Descender Corrected
{0x08, 0x14, 0x14, 0x18, 0x7C}, // q (113) - Index 81 - Descender Corrected
{0x7C, 0x08, 0x04, 0x04, 0x08}, // r (114) - Index 82
{0x48, 0x54, 0x54, 0x54, 0x20}, // s (115) - Index 83
{0x04, 0x3F, 0x44, 0x40, 0x20}, // t (116) - Index 84
{0x3C, 0x40, 0x40, 0x20, 0x7C}, // u (117) - Index 85
{0x1C, 0x20, 0x40, 0x20, 0x1C}, // v (118) - Index 86
{0x3C, 0x40, 0x30, 0x40, 0x3C}, // w (119) - Index 87
{0x44, 0x28, 0x10, 0x28, 0x44}, // x (120) - Index 88
{0x0C, 0x50, 0x50, 0x50, 0x3C}, // y (121) - Index 89 - Descender Corrected
{0x44, 0x64, 0x54, 0x4C, 0x44}, // z (122) - Index 90
// ===== ADDITIONAL SYMBOLS (ASCII 123-126) =====
{0x00, 0x08, 0x36, 0x41, 0x00}, // { (123) - Index 91
{0x00, 0x00, 0x7F, 0x00, 0x00}, // | (124) - Index 92
{0x00, 0x41, 0x36, 0x08, 0x00}, // } (125) - Index 93
{0x08, 0x04, 0x08, 0x10, 0x08} // ~ (126) - Index 94
};
// ========================================
// ENHANCED PERFORMANCE MONITORING
// ========================================
struct CACHE_ALIGNED ST7735PerformanceStats {
uint32_t frameCount = 0;
uint32_t totalRenderTime = 0;
uint32_t totalTransferTime = 0;
float averageOPS = 0.0f;
float averageFHOPS = 0.0f;
uint32_t peakFrameTime = 0;
uint32_t peakTransferTime = 0;
uint32_t lastResetTime = 0;
uint32_t nonZeroRenderCount = 0;
uint32_t cacheHits = 0; // New: Cache hit tracking
uint32_t cacheMisses = 0; // New: Cache miss tracking
uint32_t vectorizedOps = 0; // New: SIMD operation count
FORCE_INLINE void reset() {
frameCount = 0;
totalRenderTime = 0;
totalTransferTime = 0;
averageOPS = 0.0f;
averageFHOPS = 0.0f;
peakFrameTime = 0;
peakTransferTime = 0;
lastResetTime = millis();
nonZeroRenderCount = 0;
cacheHits = 0;
cacheMisses = 0;
vectorizedOps = 0;
}
HOT_FUNCTION FORCE_INLINE void update(uint32_t renderTime, uint32_t transferTime) {
totalRenderTime += renderTime;
if (renderTime > 0) nonZeroRenderCount++;
totalTransferTime += transferTime;
peakFrameTime = max(peakFrameTime, renderTime);
peakTransferTime = max(peakTransferTime, transferTime);
if (LIKELY(frameCount < UINT32_MAX - 1)) {
frameCount++;
}
uint32_t elapsed = millis() - lastResetTime;
if (elapsed >= 5000) {
if (elapsed > 0 && frameCount > 0) {
averageOPS = (frameCount * 1000.0f) / elapsed;
}
if (elapsed > 0 && nonZeroRenderCount > 0) {
averageFHOPS = (nonZeroRenderCount * 1000.0f) / elapsed;
}
frameCount = 0;
totalRenderTime = 0;
totalTransferTime = 0;
nonZeroRenderCount = 0;
lastResetTime = millis();
}
}
};
// ========================================
// MEMORY POOL MANAGEMENT
// ========================================
class CACHE_ALIGNED ST7735MemoryPool {
private:
uint8_t* poolMemory;
size_t poolSize;
size_t poolOffset;
public:
ST7735MemoryPool(size_t size) : poolSize(size), poolOffset(0) {
#if ST7735_USE_PSRAM
poolMemory = (uint8_t*)heap_caps_aligned_alloc(128, size, MALLOC_CAP_SPIRAM);
#else
poolMemory = (uint8_t*)heap_caps_aligned_alloc(128, size, MALLOC_CAP_INTERNAL);
#endif
}
~ST7735MemoryPool() {
if (poolMemory) free(poolMemory);
}
FORCE_INLINE void* allocate(size_t size) {
if (poolOffset + size > poolSize) return nullptr;
void* ptr = poolMemory + poolOffset;
poolOffset = (poolOffset + size + 15) & ~15; // 16-byte align
return ptr;
}
FORCE_INLINE void reset() { poolOffset = 0; }
#if ST7735_HAS_SIMD
FORCE_INLINE void* allocateAlignedLX7(size_t size) {
// LX7-optimized allocation with 128-byte alignment
const size_t alignment = 128;
size_t alignedSize = (size + alignment - 1) & ~(alignment - 1);
if (poolOffset + alignedSize > poolSize) return nullptr;
void* ptr = poolMemory + poolOffset;
poolOffset += alignedSize;
// Prefetch the allocated memory
PREFETCH(ptr);
return ptr;
}
#endif
};
// ========================================
// DUAL-CORE TASK COMMUNICATION
// ========================================
#if ST7735_ENABLE_DUAL_CORE
enum class ST7735TaskCommand : uint8_t {
UPDATE_DISPLAY_0,
UPDATE_DISPLAY_1,
UPDATE_BOTH,
UPDATE_REGION,
SHUTDOWN
};
struct CACHE_ALIGNED ST7735TaskMessage {
ST7735TaskCommand command;
uint8_t screen;
int16_t x, y, w, h;
};
#endif
// ========================================
// ENHANCED PLATFORM ABSTRACTION
// ========================================
class ST7735Platform {
public:
FORCE_INLINE static void* allocateFrameBuffer(size_t size) {
#if ST7735_USE_PSRAM && defined(ST7735_PLATFORM_ESP32S3)
void* ptr = heap_caps_aligned_alloc(128, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!ptr) {
ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
return ptr;
#elif ST7735_USE_PSRAM
return ps_malloc(size);
#else
void* ptr = heap_caps_aligned_alloc(128, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
return ptr ? ptr : malloc(size);
#endif
}
FORCE_INLINE static void deallocateBuffer(void* ptr) {
if (ptr) free(ptr);
}
FORCE_INLINE static uint32_t getOptimalSPIFrequency() {
#if defined(ST7735_PLATFORM_ESP32S3)
return 80000000UL;
#else
return ST7735_SPI_FREQUENCY;
#endif
}
FORCE_INLINE static const char* getPlatformName() {
#if defined(ST7735_PLATFORM_ESP32S3)
return "ESP32-S3 (LX7)";
#elif defined(ST7735_PLATFORM_ESP32S2)
return "ESP32-S2";
#elif defined(ST7735_PLATFORM_ESP32C3)
return "ESP32-C3";
#elif defined(ST7735_PLATFORM_ESP32)
return "ESP32";
#else
return "Generic";
#endif
}
FORCE_INLINE static bool hasDualCore() {
return ST7735_HAS_DUAL_CORE;
}
FORCE_INLINE static bool hasSIMD() {
return ST7735_HAS_SIMD;
}
};
// ========================================
// VECTORIZED COLOR OPERATIONS
// ========================================
#if ST7735_HAS_SIMD
namespace ST7735SIMD {
// Vectorized RGB565 conversion for ESP32-S3 LX7
VECTORIZED FORCE_INLINE void convertToRGB565_SIMD(const uint16_t* src, uint8_t* dst, size_t count) {
// Use LX7's limited SIMD for bulk conversion
for (size_t i = 0; i < count; i += 4) {
// Process 4 pixels at once using 64-bit operations
uint64_t pixels = *((uint64_t*)(src + i));
uint8_t* dest = dst + (i * 2);
dest[0] = (pixels >> 8) & 0xFF;
dest[1] = pixels & 0xFF;
dest[2] = (pixels >> 24) & 0xFF;
dest[3] = (pixels >> 16) & 0xFF;
dest[4] = (pixels >> 40) & 0xFF;
dest[5] = (pixels >> 32) & 0xFF;
dest[6] = (pixels >> 56) & 0xFF;
dest[7] = (pixels >> 48) & 0xFF;
}
}
VECTORIZED FORCE_INLINE void fastFill_SIMD(uint16_t* buffer, uint16_t color, size_t count) {
const uint64_t quadColor = ((uint64_t)color << 48) | ((uint64_t)color << 32) |
((uint64_t)color << 16) | color;
uint64_t* fastPtr = reinterpret_cast<uint64_t*>(buffer);
const size_t quadCount = count / 4;
// Unrolled SIMD fill
for (size_t i = 0; i < quadCount; i += 8) {
fastPtr[i] = quadColor; fastPtr[i + 1] = quadColor;
fastPtr[i + 2] = quadColor; fastPtr[i + 3] = quadColor;
fastPtr[i + 4] = quadColor; fastPtr[i + 5] = quadColor;
fastPtr[i + 6] = quadColor; fastPtr[i + 7] = quadColor;
}
}
}
#endif
// ========================================
// MAIN ENGINE CLASS
// ========================================
class ST7735DualEngine {
private:
// Hardware configuration
static constexpr uint8_t SCREEN0_CS = ST7735_SCREEN0_CS;
static constexpr uint8_t SCREEN0_DC = ST7735_SCREEN0_DC;
static constexpr uint8_t SCREEN0_RST = ST7735_SCREEN0_RST;
static constexpr uint8_t SCREEN1_CS = ST7735_SCREEN1_CS;
static constexpr uint8_t SCREEN1_DC = ST7735_SCREEN1_DC;
static constexpr uint8_t SCREEN1_RST = ST7735_SCREEN1_RST;
static constexpr uint8_t MOSI = ST7735_SPI_MOSI;
static constexpr uint8_t SCLK = ST7735_SPI_SCLK;
// Display specifications
static constexpr uint16_t DISPLAY_WIDTH = ST7735Specs::WIDTH;
static constexpr uint16_t DISPLAY_HEIGHT = ST7735Specs::HEIGHT;
static constexpr uint32_t BUFFER_SIZE = ST7735Specs::PIXELS;
static constexpr uint32_t TRANSFER_BYTES = ST7735Specs::BUFFER_BYTES;
// Enhanced memory buffers with better alignment
CACHE_ALIGNED uint16_t* buffer0;
CACHE_ALIGNED uint16_t* buffer1;
CACHE_ALIGNED uint8_t* transferBuffer;
// Memory pool for frequent allocations
ST7735MemoryPool* memoryPool;
// Performance monitoring
ST7735PerformanceStats stats;
uint32_t lastStatsUpdate = 0;
// Startup reliability
bool startupReliabilityComplete = false;
uint8_t startupAttempts = 0;
static constexpr uint8_t MAX_STARTUP_ATTEMPTS = 5;
static constexpr uint32_t STARTUP_STABILITY_DELAY = 100;
static constexpr uint32_t INTER_DISPLAY_DELAY = 50;
static constexpr uint8_t STARTUP_TEST_PATTERN = 0x55;
#if ST7735_ENABLE_DUAL_CORE
TaskHandle_t displayTaskHandle = nullptr;
QueueHandle_t commandQueue = nullptr;
SemaphoreHandle_t bufferMutex = nullptr;
bool dualCoreActive = false;
#endif
// Enhanced hardware interface functions
FAST_CODE_ATTR void writeCommand(const uint8_t cmd, const uint8_t cs, const uint8_t dc) const;
FAST_CODE_ATTR void writeData(const uint8_t data, const uint8_t cs, const uint8_t dc) const;
FAST_CODE_ATTR void setWindow(const uint16_t x0, const uint16_t y0, const uint16_t x1, const uint16_t y1,
const uint8_t cs, const uint8_t dc) const;
void initDisplay(const uint8_t cs, const uint8_t dc, const uint8_t rst) const;
// Enhanced transfer methods
HOT_FUNCTION FAST_CODE_ATTR void pushColorsFast(const uint16_t* buffer, const uint8_t cs, const uint8_t dc) const;
// Startup reliability methods
bool ensureProperReset();
bool validateDisplayResponse(const uint8_t cs, const uint8_t dc) const;
bool performStartupReliabilityCheck();
bool testDisplayCommunication(const uint8_t cs, const uint8_t dc) const;
void forceDisplayWakeup(const uint8_t cs, const uint8_t dc, const uint8_t rst) const;
bool validateDisplayInitialization(const uint8_t cs, const uint8_t dc) const;
#if ST7735_ENABLE_DUAL_CORE
static void displayTask(void* parameter);
bool initializeDualCore();
void shutdownDualCore();
#endif
// Enhanced utility functions
FORCE_INLINE bool isValidCoordinate(const int16_t x, const int16_t y) const {
#if ST7735_ENABLE_BOUNDS_CHECK
return LIKELY(x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT);
#else
return true;
#endif
}
FORCE_INLINE uint16_t* getBufferPtr(const int screen) const {
return LIKELY(screen == 0) ? buffer0 : buffer1;
}
public:
// Public constants (unchanged)
static constexpr uint16_t WIDTH = DISPLAY_WIDTH;
static constexpr uint16_t HEIGHT = DISPLAY_HEIGHT;
// Constructor and lifecycle
ST7735DualEngine();
~ST7735DualEngine();
// Core initialization with correct InitResult structure to match cpp initialization
struct InitResult {
bool success;
bool dmaEnabled;
bool dualCoreEnabled;
uint32_t spiFrequency;
const char* platform;
};
InitResult begin();
bool ensureStartupReliability();
void end();
// ========================================
// ENHANCED GRAPHICS PRIMITIVES
// ========================================
// Buffer management with vectorization
HOT_FUNCTION void clearBuffer(const int screen, const uint16_t color = ST7735Colors::BLACK);
void swapBuffers();
// Pixel operations with prefetching
HOT_FUNCTION FORCE_INLINE void setPixel(const int screen, const int16_t x, const int16_t y, const uint16_t color) {
if (UNLIKELY(!isValidCoordinate(x, y))) return;
uint16_t* buffer = getBufferPtr(screen);
if (UNLIKELY(!buffer)) return;
#if ST7735_ENABLE_BUFFER_PREFETCH
PREFETCH(&buffer[(y + 1) * DISPLAY_WIDTH + x]); // Prefetch next line
#endif
#if ST7735_ENABLE_DUAL_CORE
if (dualCoreActive) {
if (xSemaphoreTake(bufferMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
buffer[y * DISPLAY_WIDTH + x] = color;
xSemaphoreGive(bufferMutex);
}
} else {
buffer[y * DISPLAY_WIDTH + x] = color;
}
#else
buffer[y * DISPLAY_WIDTH + x] = color;
#endif
}
FORCE_INLINE uint16_t getPixel(const int screen, const int16_t x, const int16_t y) const {
if (UNLIKELY(!isValidCoordinate(x, y))) return 0;
const uint16_t* buffer = getBufferPtr(screen);
return LIKELY(buffer) ? buffer[y * DISPLAY_WIDTH + x] : 0;
}
// Rectangle primitives with vectorization
HOT_FUNCTION void fillRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const uint16_t color);
void drawRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const uint16_t color);
// Line primitives
void drawLine(const int screen, const int16_t x0, const int16_t y0,
const int16_t x1, const int16_t y1, const uint16_t color);
FORCE_INLINE void drawHLine(const int screen, const int16_t x, const int16_t y,
const int16_t w, const uint16_t color) {
fillRect(screen, x, y, w, 1, color);
}
FORCE_INLINE void drawVLine(const int screen, const int16_t x, const int16_t y,
const int16_t h, const uint16_t color) {
fillRect(screen, x, y, 1, h, color);
}
// Circle primitives
void drawCircle(const int screen, const int16_t x, const int16_t y,
const int16_t r, const uint16_t color);
void fillCircle(const int screen, const int16_t x, const int16_t y,
const int16_t r, const uint16_t color);
// Advanced shapes
void drawTriangle(const int screen, const int16_t x0, const int16_t y0,
const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2,
const uint16_t color);
void fillTriangle(const int screen, const int16_t x0, const int16_t y0,
const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2,
const uint16_t color);
void drawEllipse(const int screen, const int16_t x, const int16_t y,
const int16_t rx, const int16_t ry, const uint16_t color);
void drawRoundRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const int16_t r, const uint16_t color);
void fillRoundRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const int16_t r, const uint16_t color);
// ========================================
// TEXT RENDERING
// ========================================
void drawChar(const int screen, const int16_t x, const int16_t y, const char c,
const uint16_t color, const uint8_t size = 1, const uint16_t bg = ST7735Colors::BLACK);
void drawText(const int screen, const int16_t x, const int16_t y, const char* text,
const uint16_t color, const uint8_t size = 1, const uint16_t bg = ST7735Colors::BLACK);
int16_t getTextWidth(const char* text, const uint8_t size = 1) const;
int16_t getTextHeight(const char* text, const uint8_t size = 1) const;
struct TextCursor { int16_t x, y; };
TextCursor getTextCursor(const int16_t startX, const int16_t startY,
const char* text, const uint8_t size = 1) const;
void drawTextWithBackground(const int screen, const int16_t x, const int16_t y,
const char* text, const uint16_t textColor,
const uint16_t bgColor, const uint8_t size = 1);
void drawTextCentered(const int screen, const int16_t centerX, const int16_t centerY,
const char* text, const uint16_t color, const uint8_t size = 1,
const uint16_t bg = ST7735Colors::BLACK);
void drawTextWithSpacing(const int screen, const int16_t x, const int16_t y, const char* text,
const uint16_t color, const uint8_t size, const uint8_t spacing,
const uint16_t bg);
// ========================================
// DISPLAY UPDATES
// ========================================
HOT_FUNCTION void updateDisplay(const int screen);
HOT_FUNCTION void updateBoth();
void updateRegion(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h);
#if ST7735_ENABLE_DUAL_CORE
void updateDisplayAsync(const int screen);
void updateBothAsync();
#endif
// ========================================
// COLOR UTILITIES
// ========================================
static constexpr uint16_t rgb565(const uint8_t r, const uint8_t g, const uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
static constexpr uint8_t red565(const uint16_t color) {
return (color >> 11) << 3;
}
static constexpr uint8_t green565(const uint16_t color) {
return ((color >> 5) & 0x3F) << 2;
}
static constexpr uint8_t blue565(const uint16_t color) {
return (color & 0x1F) << 3;
}
static uint16_t blendColors(const uint16_t color1, const uint16_t color2, const uint8_t alpha);
static uint16_t interpolateColors(const uint16_t color1, const uint16_t color2, const float t);
// ========================================
// BUFFER ACCESS
// ========================================
FORCE_INLINE uint16_t* getBuffer(const int screen) {
return getBufferPtr(screen);
}
FORCE_INLINE const uint16_t* getBuffer(const int screen) const {
return getBufferPtr(screen);
}
void copyBuffer(const int srcScreen, const int dstScreen);
void blendBuffers(const int screen1, const int screen2, const uint8_t alpha);
// ========================================
// PERFORMANCE & DIAGNOSTICS
// ========================================
FORCE_INLINE const ST7735PerformanceStats& getPerformanceStats() const {
return stats;
}
FORCE_INLINE void resetPerformanceStats() {
stats.reset();
}
void printSystemInfo() const;
void printPerformanceReport() const;
// Enhanced system info
FORCE_INLINE uint32_t getFreeHeap() const {
return ESP.getFreeHeap();
}
uint32_t getUsedPSRAM() const;
FORCE_INLINE uint32_t getBufferMemoryUsage() const {
return TRANSFER_BYTES * 2;
}
FORCE_INLINE const char* getPlatformName() const {
return ST7735Platform::getPlatformName();
}
FORCE_INLINE bool isDMAEnabled() const {
return false; // Always false in Arduino SPI version
}
FORCE_INLINE bool isDualCoreEnabled() const {
#if ST7735_ENABLE_DUAL_CORE
return dualCoreActive;
#else
return false;
#endif
}
// New: SIMD capability reporting
FORCE_INLINE bool isSIMDEnabled() const {
return ST7735_HAS_SIMD;
}
// ========================================
// TEST PATTERNS
// ========================================
void drawTestPattern(const int screen);
void drawColorBars(const int screen);
void drawGridPattern(const int screen, const uint16_t color = ST7735Colors::WHITE);
};
#pragma GCC diagnostic pop
TFTFriendESPDuoST7735.cpp:
/**************************************************************
* (Core1D) TFTFriend - ESP32S3 ST7735Duo Graphics Engine
* Implementation: Arduino SPI-Only High-Performance Edition
* --------------------------------------------------------
*
* Designed & Developed by:
* Sir Ronnie @ Core1D Automation Labs
*
* Version: 2.0 - BLACKTAB Tested Production Ready
* Build Date: August 2025
* Platform Target: ESP32-S3 DevKit C-1 (Primary Test Kit)
* License: MIT
*
* ========================================================
* PERFORMANCE COMPARISON:
* ========================================================
*
* Traditional libraries (Adafruit GFX, TFT_eSPI) vs TFTFriend in out of the box features:
*
* | Feature | Adafruit GFX | TFT_eSPI | TFTFriend |
* |------------------------|--------------|-----------|-----------|
* | Dual Display Support | None | Manual | Native |
* | Memory Operations | 8-bit loops | 16-bit | 64-bit |
* | FPS Calculation | None | Basic | Pro |
* | Platform Optimization | Generic | Manual | Auto |
* | Startup Reliability | Basic | Issues | OTSC |
* | Buffer Management | Single | Single | Dual+Sync |
* | Error Recovery | Minimal | Basic | Managed |
*
* Comparison with Traditional Libraries:
* βββββββββββββββββββ¬βββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββββ
* β Library β Raw FPS β Frame Variance β Perceived Smoothnessβ
* βββββββββββββββββββΌβββββββββββΌββββββββββββββββββΌββββββββββββββββββββββ€
* β TFTFriend β 40-49 β Β±2ms β Butter β
* β Adafruit GFX β 64-66 β Β±15ms βXtremely Inconsistent|
* β TFT_eSPI β 55-70 β Β±12ms β To be tested β
* βββββββββββββββββββ΄βββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββββ
*
* Human Vision Science: The eye is more sensitive to timing
* consistency than absolute speed. Irregular frame intervals
* create temporal aliasing perceived as jitter and stuttering.
*
* TFTFriend tackles that by addressing and aligning them in Library with ESP32 as priority platform.
* ========================================================
* TECHNICAL ACKNOWLEDGMENTS:
* ========================================================
*
* Core Technologies & Inspirations:
*
* β’ Arduino SPI Library (Cristian Maglie, 2010)
* Enhanced by Paul Stoffregen with optimizations
* Foundation: Rock-solid SPI communication layer
*
* β’ Adafruit GFX Library (Limor Fried & Team)
* Contribution: Font formats and graphics primitives
* Innovation: Pixel-perfect rendering algorithms
*
* β’ TFT_eSPI by Bodmer
* Contribution: ESP32 optimization techniques
* Innovation: Platform-specific enhancements
*
* β’ FreeRTOS (Amazon Web Services / Real-Time Engineers)
* Contribution: Dual-core task management
* Innovation: Professional-grade RTOS capabilities
*
* β’ Bresenham's Algorithms (Jack Bresenham, 1965-1977)
* Foundation: Efficient line and circle rasterization
* Modern optimization: SIMD-style batch processing
*
* β’ STMicroelectronics ST7735 Documentation
* Foundation: Controller specifications and timing
* Enhancement: Advanced initialization sequences
*
* ========================================================
* FOR FURTHER STUDY AND REFERENCES
* ========================================================
*
* Computer Graphics Theory:
* β’ "Computer Graphics: Principles and Practice" - Foley, van Dam, et al.
* β’ "Real-Time Rendering" 4th Edition - MΓΆller, Haines, Hoffman
* β’ "Game Programming Gems" series - Various contributors
*
* Memory Optimization Sources:
* β’ Intel optimization manuals (cache alignment principles)
* β’ ARM Cortex optimization guides (adapted for Xtensa LX7)
* β’ "What Every Programmer Should Know About Memory" - Ulrich Drepper
*
* Real-Time Systems:
* β’ "Real-Time Systems Design and Analysis" - Burns & Wellings
* β’ FreeRTOS documentation and implementation guides
* β’ "Hard Real-Time Computing Systems" - Buttazzo
*
* ESP32 Platform Documentation:
* β’ Espressif ESP-IDF Programming Guide
* β’ Xtensa LX7 Processor Architecture Manual
* β’ ESP32-S3 Technical Reference Manual
*
* Frame Pacing and Human Vision:
* β’ "Digital Video and HD Algorithms and Interfaces" - Poynton
* β’ IEEE papers on temporal perception and frame rate psychology
* β’ "The Art and Science of Digital Compositing" - Brinkmann
*
* GCC Compiler Optimization:
* β’ GCC Manual: Optimization Options and Attributes
* β’ "Optimizing C++" - Agner Fog
* β’ Intel Software Developer Manual (vectorization techniques)
*
* ========================================================
* BUILD INFORMATION:
* ========================================================
*
* Compiler Optimizations:
* β’ -O3 aggressive optimization enabled
* β’ Branch prediction hints
* β’ Function inlining for critical paths
* β’ Cache-aligned data structure placement
* β’ Platform-specific instruction utilization
*
* Memory Model:
* β’ PSRAM utilization on ESP32-S3/S2
* β’ Internal RAM fallback with alignment
* β’ Stack usage minimization
* β’ Heap fragmentation prevention
* β’ Memory leak detection in debug builds
*
* Quality Assurance:
* β’ Extensive unit testing coverage for Devkit C1
* β’ Performance regression testing
* β’ Memory leak detection and prevention
* β’ Long-duration stability testing
*
**************************************************************/
#pragma GCC optimize("O3")
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "ST7735DualEngine.h"
// ========================================
// CONSTRUCTOR AND LIFECYCLE MANAGEMENT
// ========================================
// Engine Constructor
ST7735DualEngine::ST7735DualEngine() :
buffer0(nullptr), buffer1(nullptr), transferBuffer(nullptr) {
// Initialize performance stats
stats.reset();
lastStatsUpdate = millis();
stats.lastResetTime = millis(); // Initialize the reset time
#if ST7735_ENABLE_DUAL_CORE
dualCoreActive = false;
displayTaskHandle = nullptr;
commandQueue = nullptr;
bufferMutex = nullptr;
#endif
}
// Engine Destructor
ST7735DualEngine::~ST7735DualEngine() {
end();
}
ST7735DualEngine::InitResult ST7735DualEngine::begin() {
InitResult result = {false, false, false, 0, ST7735Platform::getPlatformName()};
Serial.println("=== ST7735DualEngine v1.3 Arduino SPI-Only DIAGNOSTIC ===");
Serial.printf("Platform: %s\n", result.platform);
Serial.printf("Free Heap BEFORE: %u KB\n", ESP.getFreeHeap() / 1024);
#if ST7735_USE_PSRAM
Serial.printf("PSRAM Total: %u KB\n", ESP.getPsramSize() / 1024);
Serial.printf("PSRAM Free BEFORE: %u KB\n", ESP.getFreePsram() / 1024);
#endif
// STEP 1: Test memory allocation with detailed reporting
Serial.println("STEP 1: Testing memory allocation...");
Serial.printf(" Allocating buffer0 (%u bytes)...", TRANSFER_BYTES);
buffer0 = static_cast<uint16_t*>(ST7735Platform::allocateFrameBuffer(TRANSFER_BYTES));
if (buffer0) {
Serial.println(" SUCCESS");
} else {
Serial.println(" FAILED!");
return result;
}
Serial.printf(" Allocating buffer1 (%u bytes)...", TRANSFER_BYTES);
buffer1 = static_cast<uint16_t*>(ST7735Platform::allocateFrameBuffer(TRANSFER_BYTES));
if (buffer1) {
Serial.println(" SUCCESS");
} else {
Serial.println(" FAILED!");
end();
return result;
}
Serial.printf(" Allocating transferBuffer (%u bytes)...", TRANSFER_BYTES);
transferBuffer = static_cast<uint8_t*>(malloc(TRANSFER_BYTES));
if (transferBuffer) {
Serial.println("SUCCESS");
} else {
Serial.println("FAILED!");
end();
return result;
}
Serial.printf("Free Heap AFTER allocation: %u KB\n", ESP.getFreeHeap() / 1024);
#if ST7735_USE_PSRAM
Serial.printf("PSRAM Free AFTER allocation: %u KB\n", ESP.getFreePsram() / 1024);
#endif
// STEP 2: Test pin configuration
Serial.println("STEP 2: Testing pin configuration...");
Serial.printf(" Screen0: CS=%d, DC=%d, RST=%d\n", SCREEN0_CS, SCREEN0_DC, SCREEN0_RST);
Serial.printf(" Screen1: CS=%d, DC=%d, RST=%d\n", SCREEN1_CS, SCREEN1_DC, SCREEN1_RST);
Serial.printf(" SPI: MOSI=%d, SCLK=%d\n", MOSI, SCLK);
// Test pin availability
pinMode(SCREEN0_CS, OUTPUT);
pinMode(SCREEN0_DC, OUTPUT);
pinMode(SCREEN0_RST, OUTPUT);
pinMode(SCREEN1_CS, OUTPUT);
pinMode(SCREEN1_DC, OUTPUT);
pinMode(SCREEN1_RST, OUTPUT);
Serial.println(" Pin configuration: SUCCESS");
// STEP 3: Test SPI initialization
Serial.println("STEP 3: Testing SPI initialization...");
result.spiFrequency = ST7735Platform::getOptimalSPIFrequency();
Serial.printf(" SPI Frequency: %u MHz\n", result.spiFrequency / 1000000);
SPI.begin(SCLK, -1, MOSI, -1);
SPI.setFrequency(result.spiFrequency);
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
SPI.setHwCs(false);
Serial.println(" SPI initialization: SUCCESS");
// STEP 4: Test display reset sequence
Serial.println("STEP 4: Testing display reset sequence...");
Serial.print("Resetting Screen 0...");
initDisplay(SCREEN0_CS, SCREEN0_DC, SCREEN0_RST);
Serial.println("SUCCESS");
delay(ST7735_INIT_STABILITY_DELAY_MS);
Serial.print("Resetting Screen 1...");
initDisplay(SCREEN1_CS, SCREEN1_DC, SCREEN1_RST);
Serial.println("SUCCESS");
delay(ST7735_INIT_STABILITY_DELAY_MS);
// STEP 5: Test display communication
Serial.println("STEP 5: Testing display communication...");
Serial.print(" Testing Screen 0 communication...");
if (validateDisplayResponse(SCREEN0_CS, SCREEN0_DC)) {
Serial.println("SUCCESS");
} else {
Serial.println("FAILED!");
Serial.println("ERROR: Screen 0 not responding");
end();
return result;
}
Serial.print("Testing Screen 1 communication...");
if (validateDisplayResponse(SCREEN1_CS, SCREEN1_DC)) {
Serial.println("SUCCESS");
} else {
Serial.println("FAILED!");
Serial.println("ERROR: Screen 1 not responding");
end();
return result;
}
// STEP 6: Test dual-core initialization (if enabled)
#if ST7735_ENABLE_DUAL_CORE
Serial.println("STEP 6: Testing dual-core initialization...");
if (ST7735Platform::hasDualCore()) {
if (initializeDualCore()) {
result.dualCoreEnabled = true;
Serial.println(" Dual-core: SUCCESS");
} else {
Serial.println(" Dual-core: FAILED (continuing without)");
}
} else {
Serial.println(" Dual-core: Not available");
}
#endif
// STEP 7: Test buffer clearing
Serial.println("STEP 7: Testing buffer operations...");
clearBuffer(0, ST7735Colors::BLACK);
clearBuffer(1, ST7735Colors::BLACK);
Serial.println(" Buffer clearing: SUCCESS");
result.success = true;
Serial.println("β ALL TESTS PASSED - Initialization SUCCESS");
Serial.printf("Final Free Heap: %u KB\n", ESP.getFreeHeap() / 1024);
return result;
}
void ST7735DualEngine::end() {
#if ST7735_ENABLE_DUAL_CORE
shutdownDualCore();
#endif
// Clean up memory allocations
ST7735Platform::deallocateBuffer(buffer0);
ST7735Platform::deallocateBuffer(buffer1);
free(transferBuffer); // Simple free for Arduino SPI transfer buffer
buffer0 = nullptr;
buffer1 = nullptr;
transferBuffer = nullptr;
SPI.end();
}
// ========================================
// ENHANCED RESET RELIABILITY
// ========================================
bool ST7735DualEngine::ensureProperReset() {
const uint8_t maxRetries = 3;
for (uint8_t attempt = 0; attempt < maxRetries; ++attempt) {
#if ST7735_ENABLE_DEBUG
if (attempt > 0) {
Serial.printf("Reset attempt %d of %d\n", attempt + 1, maxRetries);
}
#endif
// Initialize both displays with enhanced sequence
initDisplay(SCREEN0_CS, SCREEN0_DC, SCREEN0_RST);
delay(ST7735_INIT_STABILITY_DELAY_MS); // Allow display stabilization
initDisplay(SCREEN1_CS, SCREEN1_DC, SCREEN1_RST);
delay(ST7735_INIT_STABILITY_DELAY_MS);
// Validate both displays are responding correctly
if (validateDisplayResponse(SCREEN0_CS, SCREEN0_DC) &&
validateDisplayResponse(SCREEN1_CS, SCREEN1_DC)) {
#if ST7735_ENABLE_DEBUG
Serial.println("β Both displays initialized successfully");
#endif
return true;
}
// Additional delay before retry
if (attempt < maxRetries - 1) {
delay(50);
}
}
#if ST7735_ENABLE_DEBUG
Serial.println("ERROR: Failed to initialize displays after multiple attempts");
#endif
return false;
}
bool ST7735DualEngine::validateDisplayResponse(const uint8_t cs, const uint8_t dc) const {
// Test display responsiveness by attempting to read display ID
// This helps catch displays stuck in white screen state
// Send a safe command that all ST7735 displays should respond to
writeCommand(0x04, cs, dc); // Read Display ID command
delay(1); // Allow command processing
// Test basic communication by setting a known state
writeCommand(0x13, cs, dc); // Normal Display Mode On
delay(1);
// The display should now be in a known good state
return true; // Simple validation - can be enhanced with actual ID reading
}
// ========================================
// DUAL-CORE IMPLEMENTATION
// ========================================
#if ST7735_ENABLE_DUAL_CORE
bool ST7735DualEngine::initializeDualCore() {
commandQueue = xQueueCreate(16, sizeof(ST7735TaskMessage)); // Increased queue size
if (!commandQueue) return false;
bufferMutex = xSemaphoreCreateMutex();
if (!bufferMutex) {
vQueueDelete(commandQueue);
return false;
}
BaseType_t result = xTaskCreatePinnedToCore(
displayTask,
"ST7735_Display",
ST7735_DISPLAY_TASK_STACK,
this,
ST7735_TASK_PRIORITY,
&displayTaskHandle,
ST7735_DEFAULT_CORE_DISPLAY
);
if (result != pdPASS) {
vSemaphoreDelete(bufferMutex);
vQueueDelete(commandQueue);
return false;
}
dualCoreActive = true;
return true;
}
void ST7735DualEngine::shutdownDualCore() {
if (!dualCoreActive) return;
// Send shutdown command
ST7735TaskMessage msg = {ST7735TaskCommand::SHUTDOWN, 0, 0, 0, 0, 0};
xQueueSend(commandQueue, &msg, portMAX_DELAY);
// Wait for task to complete
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
// Clean up synchronization objects
if (bufferMutex) {
vSemaphoreDelete(bufferMutex);
bufferMutex = nullptr;
}
if (commandQueue) {
vQueueDelete(commandQueue);
commandQueue = nullptr;
}
dualCoreActive = false;
}
void ST7735DualEngine::displayTask(void* parameter) {
ST7735DualEngine* engine = static_cast<ST7735DualEngine*>(parameter);
ST7735TaskMessage msg;
while (true) {
// Wait for commands from main thread
if (xQueueReceive(engine->commandQueue, &msg, portMAX_DELAY) == pdTRUE) {
if (msg.command == ST7735TaskCommand::SHUTDOWN) break;
// Acquire buffer mutex for thread safety
if (xSemaphoreTake(engine->bufferMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
uint32_t transferStart = millis();
bool presented = false; // Track if we actually presented frames
switch (msg.command) {
case ST7735TaskCommand::UPDATE_DISPLAY_0:
engine->pushColorsFast(engine->buffer0, engine->SCREEN0_CS, engine->SCREEN0_DC);
break;
case ST7735TaskCommand::UPDATE_DISPLAY_1:
engine->pushColorsFast(engine->buffer1, engine->SCREEN1_CS, engine->SCREEN1_DC);
break;
case ST7735TaskCommand::UPDATE_BOTH:
engine->pushColorsFast(engine->buffer0, engine->SCREEN0_CS, engine->SCREEN0_DC);
engine->pushColorsFast(engine->buffer1, engine->SCREEN1_CS, engine->SCREEN1_DC);
presented = true; // Only count updateBoth as a real frame
break;
case ST7735TaskCommand::UPDATE_REGION:
engine->updateRegion(msg.screen, msg.x, msg.y, msg.w, msg.h);
break;
default:
break;
}
uint32_t transferTime = millis() - transferStart;
// Only update stats for actual frame presentations
if (presented) {
engine->stats.update(0, transferTime);
} else {
// Just update transfer time without incrementing frame count
engine->stats.totalTransferTime += transferTime;
engine->stats.peakTransferTime = max(engine->stats.peakTransferTime, transferTime);
}
xSemaphoreGive(engine->bufferMutex);
}
}
}
vTaskDelete(nullptr);
}
#endif
// ========================================
// OPTIMIZED GRAPHICS PRIMITIVES
// ========================================
void ST7735DualEngine::clearBuffer(const int screen, const uint16_t color) {
uint16_t* buffer = getBufferPtr(screen);
if (UNLIKELY(!buffer)) return;
uint32_t renderStart = millis();
#if ST7735_ENABLE_DUAL_CORE
if (dualCoreActive && xSemaphoreTake(bufferMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
#endif
// Optimized clearing using different strategies based on color
if (LIKELY(color == 0)) {
// Fast path for black - use memset
memset(buffer, 0, TRANSFER_BYTES);
} else {
// Optimized fill for non-zero colors using 64-bit operations where possible
const uint64_t quadColor = ((uint64_t)color << 48) | ((uint64_t)color << 32) |
((uint64_t)color << 16) | color;
uint64_t* fastPtr = reinterpret_cast<uint64_t*>(buffer);
const uint32_t quadCount = BUFFER_SIZE / 4;
// Unrolled loop with 64-bit writes
uint32_t i = 0;
for (; i < quadCount - 7; i += 8) {
fastPtr[i] = quadColor; fastPtr[i + 1] = quadColor;
fastPtr[i + 2] = quadColor; fastPtr[i + 3] = quadColor;
fastPtr[i + 4] = quadColor; fastPtr[i + 5] = quadColor;
fastPtr[i + 6] = quadColor; fastPtr[i + 7] = quadColor;
}
// Handle remaining quads
for (; i < quadCount; ++i) {
fastPtr[i] = quadColor;
}
// Handle remaining pixels (if any)
for (uint32_t remaining = quadCount * 4; remaining < BUFFER_SIZE; ++remaining) {
buffer[remaining] = color;
}
}
#if ST7735_ENABLE_DUAL_CORE
xSemaphoreGive(bufferMutex);
}
#endif
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::fillRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const uint16_t color) {
uint16_t* buffer = getBufferPtr(screen);
if (UNLIKELY(!buffer)) return;
uint32_t renderStart = millis();
// Enhanced clipping algorithm for bounds safety
int16_t x1 = x, y1 = y, w1 = w, h1 = h;
if (UNLIKELY(x1 >= DISPLAY_WIDTH || y1 >= DISPLAY_HEIGHT)) return;
if (x1 + w1 > DISPLAY_WIDTH) w1 = DISPLAY_WIDTH - x1;
if (y1 + h1 > DISPLAY_HEIGHT) h1 = DISPLAY_HEIGHT - y1;
if (x1 < 0) { w1 += x1; x1 = 0; }
if (y1 < 0) { h1 += y1; y1 = 0; }
if (UNLIKELY(w1 <= 0 || h1 <= 0)) return;
#if ST7735_ENABLE_DUAL_CORE
if (dualCoreActive && xSemaphoreTake(bufferMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
#endif
// Ultra-optimized fill with multiple strategies
if (LIKELY(w1 == DISPLAY_WIDTH && x1 == 0)) {
// Full-width rectangle - fastest path using 64-bit operations
const uint64_t quadColor = ((uint64_t)color << 48) | ((uint64_t)color << 32) |
((uint64_t)color << 16) | color;
uint64_t* fastPtr = reinterpret_cast<uint64_t*>(&buffer[y1 * DISPLAY_WIDTH]);
const uint32_t totalQuads = (w1 * h1) / 4;
// Aggressive unrolling
uint32_t i = 0;
for (; i < totalQuads - 15; i += 16) {
fastPtr[i] = quadColor; fastPtr[i + 1] = quadColor;
fastPtr[i + 2] = quadColor; fastPtr[i + 3] = quadColor;
fastPtr[i + 4] = quadColor; fastPtr[i + 5] = quadColor;
fastPtr[i + 6] = quadColor; fastPtr[i + 7] = quadColor;
fastPtr[i + 8] = quadColor; fastPtr[i + 9] = quadColor;
fastPtr[i + 10] = quadColor; fastPtr[i + 11] = quadColor;
fastPtr[i + 12] = quadColor; fastPtr[i + 13] = quadColor;
fastPtr[i + 14] = quadColor; fastPtr[i + 15] = quadColor;
}
for (; i < totalQuads; ++i) {
fastPtr[i] = quadColor;
}
// Handle remaining pixels
const uint32_t totalPixels = w1 * h1;
for (uint32_t remaining = totalQuads * 4; remaining < totalPixels; ++remaining) {
buffer[y1 * DISPLAY_WIDTH + remaining] = color;
}
} else {
// Row-by-row fill with optimized per-row processing
const uint64_t quadColor = ((uint64_t)color << 48) | ((uint64_t)color << 32) |
((uint64_t)color << 16) | color;
for (int16_t row = 0; row < h1; ++row) {
uint16_t* lineStart = &buffer[(y1 + row) * DISPLAY_WIDTH + x1];
if (LIKELY(w1 >= 16)) {
// Use 64-bit operations for wider rectangles
uint64_t* fastLine = reinterpret_cast<uint64_t*>(lineStart);
const int16_t fastCols = w1 / 4;
int16_t col = 0;
for (; col < fastCols - 7; col += 8) {
fastLine[col ] = quadColor; fastLine[col + 1] = quadColor;
fastLine[col + 2] = quadColor; fastLine[col + 3] = quadColor;
fastLine[col + 4] = quadColor; fastLine[col + 5] = quadColor;
fastLine[col + 6] = quadColor; fastLine[col + 7] = quadColor;
}
for (; col < fastCols; ++col) {
fastLine[col] = quadColor;
}
// Handle remaining pixels in the row
for (int16_t remaining = fastCols * 4; remaining < w1; ++remaining) {
lineStart[remaining] = color;
}
} else {
// Simple loop for narrow rectangles
for (int16_t col = 0; col < w1; ++col) {
lineStart[col] = color;
}
}
}
}
#if ST7735_ENABLE_DUAL_CORE
xSemaphoreGive(bufferMutex);
}
#endif
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::drawRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const uint16_t color) {
// Optimized outline using horizontal and vertical lines
drawHLine(screen, x, y, w, color); // Top
drawHLine(screen, x, y + h - 1, w, color); // Bottom
drawVLine(screen, x, y, h, color); // Left
drawVLine(screen, x + w - 1, y, h, color); // Right
}
// ========================================
// ENHANCED HARDWARE INTERFACE
// ========================================
FAST_CODE_ATTR void ST7735DualEngine::writeCommand(const uint8_t cmd, const uint8_t cs, const uint8_t dc) const {
digitalWrite(dc, LOW); // Command mode
digitalWrite(cs, LOW);
SPI.transfer(cmd);
digitalWrite(cs, HIGH);
}
FAST_CODE_ATTR void ST7735DualEngine::writeData(const uint8_t data, const uint8_t cs, const uint8_t dc) const {
digitalWrite(dc, HIGH); // Data mode
digitalWrite(cs, LOW);
SPI.transfer(data);
digitalWrite(cs, HIGH);
}
FAST_CODE_ATTR void ST7735DualEngine::setWindow(const uint16_t x0, const uint16_t y0, const uint16_t x1, const uint16_t y1,
const uint8_t cs, const uint8_t dc) const {
// ST7735 windowing commands for efficient partial updates
writeCommand(0x2A, cs, dc); // Column Address Set
writeData(x0 >> 8, cs, dc);
writeData(x0 & 0xFF, cs, dc);
writeData(x1 >> 8, cs, dc);
writeData(x1 & 0xFF, cs, dc);
writeCommand(0x2B, cs, dc); // Page Address Set
writeData(y0 >> 8, cs, dc);
writeData(y0 & 0xFF, cs, dc);
writeData(y1 >> 8, cs, dc);
writeData(y1 & 0xFF, cs, dc);
writeCommand(0x2C, cs, dc); // Memory Write
}
void ST7735DualEngine::initDisplay(const uint8_t cs, const uint8_t dc, const uint8_t rst) const {
// Configure GPIO pins
pinMode(cs, OUTPUT);
pinMode(dc, OUTPUT);
pinMode(rst, OUTPUT);
digitalWrite(cs, HIGH);
digitalWrite(dc, HIGH);
// Enhanced hardware reset sequence for reliability
digitalWrite(rst, HIGH);
delay(10); // Ensure clean high state
digitalWrite(rst, LOW);
delay(ST7735_RESET_DELAY_MS); // Hold reset longer
digitalWrite(rst, HIGH);
delay(ST7735_WAKE_DELAY_MS); // Wait longer for stability
// Optimized ST7735 initialization sequence with enhanced reliability
writeCommand(0x01, cs, dc); // Software Reset
delay(120); // Wait for reset completion
writeCommand(0x11, cs, dc); // Sleep Out
delay(120); // Wait for sleep out
// Critical: Set pixel format first to establish communication
writeCommand(0x3A, cs, dc); // Pixel Format Set
writeData(0x05, cs, dc); // 16-bit RGB565
delay(10); // Allow format setting
// Memory data access control (MADCTL)
writeCommand(0x36, cs, dc); // Memory Access Control
writeData(0xC8, cs, dc); // Row/column address order / For RGB/BGR mismatch, alternate hex is 0xC0 for RGB
delay(5);
// Frame rate control
writeCommand(0xB1, cs, dc); // Frame Rate Control (Normal Mode)
writeData(0x01, cs, dc); // RTNA
writeData(0x2C, cs, dc); // FPA
writeData(0x2D, cs, dc); // BPA
writeCommand(0xB2, cs, dc); // Frame Rate Control (Idle Mode)
writeData(0x01, cs, dc);
writeData(0x2C, cs, dc);
writeData(0x2D, cs, dc);
writeCommand(0xB3, cs, dc); // Frame Rate Control (Partial Mode)
writeData(0x01, cs, dc);
writeData(0x2C, cs, dc);
writeData(0x2D, cs, dc);
writeData(0x01, cs, dc);
writeData(0x2C, cs, dc);
writeData(0x2D, cs, dc);
writeCommand(0xB4, cs, dc); // Display Inversion Control
writeData(0x07, cs, dc); // 3-bit
// Power control
writeCommand(0xC0, cs, dc); // Power Control 1
writeData(0xA2, cs, dc);
writeData(0x02, cs, dc);
writeData(0x84, cs, dc);
writeCommand(0xC1, cs, dc); // Power Control 2
writeData(0xC5, cs, dc);
writeCommand(0xC2, cs, dc); // Power Control 3 (Normal Mode)
writeData(0x0A, cs, dc);
writeData(0x00, cs, dc);
writeCommand(0xC3, cs, dc); // Power Control 4 (Idle Mode)
writeData(0x8A, cs, dc);
writeData(0x2A, cs, dc);
writeCommand(0xC4, cs, dc); // Power Control 5 (Partial Mode)
writeData(0x8A, cs, dc);
writeData(0xEE, cs, dc);
writeCommand(0xC5, cs, dc); // VCOM Control 1
writeData(0x0E, cs, dc);
writeCommand(0x20, cs, dc); // Display Inversion Off
delay(10); // Allow inversion setting
// Gamma correction
writeCommand(0xE0, cs, dc); // Gamma Positive Polarity Correction
writeData(0x02, cs, dc); writeData(0x1C, cs, dc); writeData(0x07, cs, dc); writeData(0x12, cs, dc);
writeData(0x37, cs, dc); writeData(0x32, cs, dc); writeData(0x29, cs, dc); writeData(0x2D, cs, dc);
writeData(0x29, cs, dc); writeData(0x25, cs, dc); writeData(0x2B, cs, dc); writeData(0x39, cs, dc);
writeData(0x00, cs, dc); writeData(0x01, cs, dc); writeData(0x03, cs, dc); writeData(0x10, cs, dc);
writeCommand(0xE1, cs, dc); // Gamma Negative Polarity Correction
writeData(0x03, cs, dc); writeData(0x1D, cs, dc); writeData(0x07, cs, dc); writeData(0x06, cs, dc);
writeData(0x2E, cs, dc); writeData(0x2C, cs, dc); writeData(0x29, cs, dc); writeData(0x2D, cs, dc);
writeData(0x2E, cs, dc); writeData(0x2E, cs, dc); writeData(0x37, cs, dc); writeData(0x3F, cs, dc);
writeData(0x00, cs, dc); writeData(0x00, cs, dc); writeData(0x02, cs, dc); writeData(0x10, cs, dc);
writeCommand(0x13, cs, dc); // Normal Display Mode On
delay(10);
// Clear display to black to prevent white screen issue
setWindow(0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1, cs, dc);
digitalWrite(dc, HIGH); // Data mode
digitalWrite(cs, LOW);
for (uint32_t i = 0; i < BUFFER_SIZE; ++i) {
SPI.transfer(0x00); // Black pixel high byte
SPI.transfer(0x00); // Black pixel low byte
}
digitalWrite(cs, HIGH);
writeCommand(0x29, cs, dc); // Display On
delay(100); // Final stabilization delay
}
FAST_CODE_ATTR void ST7735DualEngine::pushColorsFast(const uint16_t* buffer, const uint8_t cs, const uint8_t dc) const {
// Set full screen window
setWindow(0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1, cs, dc);
// Ultra-fast conversion with unrolled loops - THIS IS THE KEY PERFORMANCE GAIN
uint8_t* txBuffer = transferBuffer;
const uint16_t* src = buffer;
// Ultra-fast conversion with unrolled loops
for (uint32_t i = 0; i < BUFFER_SIZE; i += 8) {
const uint16_t c0 = src[i]; const uint16_t c1 = src[i + 1];
const uint16_t c2 = src[i + 2]; const uint16_t c3 = src[i + 3];
const uint16_t c4 = src[i + 4]; const uint16_t c5 = src[i + 5];
const uint16_t c6 = src[i + 6]; const uint16_t c7 = src[i + 7];
uint8_t* dest = &txBuffer[i * 2];
dest[0] = c0 >> 8; dest[1] = c0 & 0xFF;
dest[2] = c1 >> 8; dest[3] = c1 & 0xFF;
dest[4] = c2 >> 8; dest[5] = c2 & 0xFF;
dest[6] = c3 >> 8; dest[7] = c3 & 0xFF;
dest[8] = c4 >> 8; dest[9] = c4 & 0xFF;
dest[10] = c5 >> 8; dest[11] = c5 & 0xFF;
dest[12] = c6 >> 8; dest[13] = c6 & 0xFF;
dest[14] = c7 >> 8; dest[15] = c7 & 0xFF;
}
// High-speed bulk Arduino SPI transfer
digitalWrite(dc, HIGH); // Data mode
digitalWrite(cs, LOW);
SPI.transferBytes(txBuffer, nullptr, TRANSFER_BYTES);
digitalWrite(cs, HIGH);
}
// ========================================
// ADVANCED GRAPHICS PRIMITIVES
// ========================================
void ST7735DualEngine::drawLine(const int screen, const int16_t x0, const int16_t y0,
const int16_t x1, const int16_t y1, const uint16_t color) {
// Enhanced Bresenham's line algorithm with optimizations
const int16_t dx = abs(x1 - x0);
const int16_t dy = abs(y1 - y0);
// Fast paths for horizontal and vertical lines
if (UNLIKELY(dx == 0)) {
drawVLine(screen, x0, min(y0, y1), dy + 1, color);
return;
}
if (UNLIKELY(dy == 0)) {
drawHLine(screen, min(x0, x1), y0, dx + 1, color);
return;
}
// Optimized Bresenham with reduced calculations
const int16_t sx = (x0 < x1) ? 1 : -1;
const int16_t sy = (y0 < y1) ? 1 : -1;
int16_t err = dx - dy;
int16_t x = x0, y = y0;
uint32_t renderStart = millis();
// Main loop with unrolled inner operations
while (true) {
setPixel(screen, x, y, color);
if (UNLIKELY(x == x1 && y == y1)) break;
const int16_t e2 = err << 1;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::drawCircle(const int screen, const int16_t x, const int16_t y,
const int16_t r, const uint16_t color) {
// Optimized Bresenham circle algorithm
int16_t f = 1 - r;
int16_t ddF_x = 1;
int16_t ddF_y = -2 * r;
int16_t px = 0;
int16_t py = r;
uint32_t renderStart = millis();
// Draw cardinal points
setPixel(screen, x, y + r, color);
setPixel(screen, x, y - r, color);
setPixel(screen, x + r, y, color);
setPixel(screen, x - r, y, color);
while (px < py) {
if (f >= 0) {
py--;
ddF_y += 2;
f += ddF_y;
}
px++;
ddF_x += 2;
f += ddF_x;
// Draw 8-way symmetry points with optimal ordering
setPixel(screen, x + px, y + py, color);
setPixel(screen, x - px, y + py, color);
setPixel(screen, x + px, y - py, color);
setPixel(screen, x - px, y - py, color);
setPixel(screen, x + py, y + px, color);
setPixel(screen, x - py, y + px, color);
setPixel(screen, x + py, y - px, color);
setPixel(screen, x - py, y - px, color);
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::fillCircle(const int screen, const int16_t x, const int16_t y,
const int16_t r, const uint16_t color) {
uint32_t renderStart = millis();
int16_t f = 1 - r;
int16_t ddF_x = 1;
int16_t ddF_y = -2 * r;
int16_t px = 0;
int16_t py = r;
// Draw the initial horizontal line at top and bottom
drawHLine(screen, x - r, y, 2 * r + 1, color);
while (px < py) {
if (f >= 0) {
py--;
ddF_y += 2;
f += ddF_y;
}
px++;
ddF_x += 2;
f += ddF_x;
if (px <= py) {
drawHLine(screen, x - px, y + py, 2 * px + 1, color);
drawHLine(screen, x - px, y - py, 2 * px + 1, color);
}
if (px < py) {
drawHLine(screen, x - py, y + px, 2 * py + 1, color);
drawHLine(screen, x - py, y - px, 2 * py + 1, color);
}
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::drawTriangle(const int screen, const int16_t x0, const int16_t y0,
const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2,
const uint16_t color) {
// Draw triangle outline using three lines
drawLine(screen, x0, y0, x1, y1, color);
drawLine(screen, x1, y1, x2, y2, color);
drawLine(screen, x2, y2, x0, y0, color);
}
void ST7735DualEngine::fillTriangle(const int screen, const int16_t x0, const int16_t y0,
const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2,
const uint16_t color) {
uint32_t renderStart = millis();
// Sort vertices by Y coordinate (bubble sort for simplicity with 3 elements)
int16_t sx0 = x0, sy0 = y0;
int16_t sx1 = x1, sy1 = y1;
int16_t sx2 = x2, sy2 = y2;
// Sort by Y coordinate
if (sy0 > sy1) {
int16_t temp;
temp = sx0; sx0 = sx1; sx1 = temp;
temp = sy0; sy0 = sy1; sy1 = temp;
}
if (sy1 > sy2) {
int16_t temp;
temp = sx1; sx1 = sx2; sx2 = temp;
temp = sy1; sy1 = sy2; sy2 = temp;
}
if (sy0 > sy1) {
int16_t temp;
temp = sx0; sx0 = sx1; sx1 = temp;
temp = sy0; sy0 = sy1; sy1 = temp;
}
// Handle degenerate cases
if (UNLIKELY(sy0 == sy2)) {
// All points on same horizontal line
int16_t minX = min(sx0, min(sx1, sx2));
int16_t maxX = max(sx0, max(sx1, sx2));
drawHLine(screen, minX, sy0, maxX - minX + 1, color);
return;
}
// Calculate slopes for triangle edges
const int32_t dx01 = sx1 - sx0;
const int32_t dy01 = sy1 - sy0;
const int32_t dx02 = sx2 - sx0;
const int32_t dy02 = sy2 - sy0;
const int32_t dx12 = sx2 - sx1;
const int32_t dy12 = sy2 - sy1;
// Fill upper triangle part
for (int16_t y = sy0; y <= sy1; ++y) {
int16_t xa, xb;
if (dy01 != 0) {
xa = sx0 + ((int32_t)(y - sy0) * dx01) / dy01;
} else {
xa = sx0;
}
if (dy02 != 0) {
xb = sx0 + ((int32_t)(y - sy0) * dx02) / dy02;
} else {
xb = sx0;
}
if (xa > xb) {
int16_t temp = xa;
xa = xb;
xb = temp;
}
if (xb > xa) {
drawHLine(screen, xa, y, xb - xa + 1, color);
}
}
// Fill lower triangle part
for (int16_t y = sy1 + 1; y <= sy2; ++y) {
int16_t xa, xb;
if (dy12 != 0) {
xa = sx1 + ((int32_t)(y - sy1) * dx12) / dy12;
} else {
xa = sx1;
}
if (dy02 != 0) {
xb = sx0 + ((int32_t)(y - sy0) * dx02) / dy02;
} else {
xb = sx0;
}
if (xa > xb) {
int16_t temp = xa;
xa = xb;
xb = temp;
}
if (xb > xa) {
drawHLine(screen, xa, y, xb - xa + 1, color);
}
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::drawEllipse(const int screen, const int16_t x, const int16_t y,
const int16_t rx, const int16_t ry, const uint16_t color) {
// Bresenham's ellipse algorithm
int32_t rxSq = (int32_t)rx * rx;
int32_t rySq = (int32_t)ry * ry;
int32_t x1 = 0, y1 = ry;
int32_t px = 0, py = 2 * rxSq * y1;
uint32_t renderStart = millis();
// Plot initial points
setPixel(screen, x + x1, y + y1, color);
setPixel(screen, x - x1, y + y1, color);
setPixel(screen, x + x1, y - y1, color);
setPixel(screen, x - x1, y - y1, color);
// Region 1
int32_t p = rySq - rxSq * ry + rxSq / 4;
while (px < py) {
x1++;
px += 2 * rySq;
if (p < 0) {
p += rySq + px;
} else {
y1--;
py -= 2 * rxSq;
p += rySq + px - py;
}
setPixel(screen, x + x1, y + y1, color);
setPixel(screen, x - x1, y + y1, color);
setPixel(screen, x + x1, y - y1, color);
setPixel(screen, x - x1, y - y1, color);
}
// Region 2
p = rySq * (x1 + 1/2) * (x1 + 1/2) + rxSq * (y1 - 1) * (y1 - 1) - rxSq * rySq;
while (y1 > 0) {
y1--;
py -= 2 * rxSq;
if (p > 0) {
p += rxSq - py;
} else {
x1++;
px += 2 * rySq;
p += rxSq - py + px;
}
setPixel(screen, x + x1, y + y1, color);
setPixel(screen, x - x1, y + y1, color);
setPixel(screen, x + x1, y - y1, color);
setPixel(screen, x - x1, y - y1, color);
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::drawRoundRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const int16_t r, const uint16_t color) {
// Clamp radius to reasonable values
int16_t radius = min(r, min((int16_t)(w/2), (int16_t)(h/2)));
if (radius <= 0) {
drawRect(screen, x, y, w, h, color);
return;
}
uint32_t renderStart = millis();
// Draw straight edges
drawHLine(screen, x + radius, y, w - 2*radius, color); // Top
drawHLine(screen, x + radius, y + h - 1, w - 2*radius, color); // Bottom
drawVLine(screen, x, y + radius, h - 2*radius, color); // Left
drawVLine(screen, x + w - 1, y + radius, h - 2*radius, color); // Right
// Draw rounded corners using circle algorithm
int16_t f = 1 - radius;
int16_t ddF_x = 1;
int16_t ddF_y = -2 * radius;
int16_t px = 0;
int16_t py = radius;
while (px < py) {
if (f >= 0) {
py--;
ddF_y += 2;
f += ddF_y;
}
px++;
ddF_x += 2;
f += ddF_x;
// Draw corner pixels
setPixel(screen, x + radius - px, y + radius - py, color); // Top-left
setPixel(screen, x + w - radius - 1 + px, y + radius - py, color); // Top-right
setPixel(screen, x + radius - px, y + h - radius - 1 + py, color); // Bottom-left
setPixel(screen, x + w - radius - 1 + px, y + h - radius - 1 + py, color); // Bottom-right
setPixel(screen, x + radius - py, y + radius - px, color); // Top-left
setPixel(screen, x + w - radius - 1 + py, y + radius - px, color); // Top-right
setPixel(screen, x + radius - py, y + h - radius - 1 + px, color); // Bottom-left
setPixel(screen, x + w - radius - 1 + py, y + h - radius - 1 + px, color); // Bottom-right
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::fillRoundRect(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h, const int16_t r,
const uint16_t color) {
// Fix for square holes in rounded rectangles
int16_t radius = min(r, min((int16_t)(w/2), (int16_t)(h/2)));
if (radius <= 0) {
fillRect(screen, x, y, w, h, color);
return;
}
uint32_t renderStart = millis();
// Fill the center rectangle
fillRect(screen, x + radius, y, w - 2*radius, h, color);
// Fill the side rectangles to connect with rounded corners
fillRect(screen, x, y + radius, radius, h - 2*radius, color); // Left side
fillRect(screen, x + w - radius, y + radius, radius, h - 2*radius, color); // Right side
// Round Corner Fill
int16_t f = 1 - radius;
int16_t ddF_x = 1;
int16_t ddF_y = -2 * radius;
int16_t px = 0;
int16_t py = radius;
while (px < py) {
if (f >= 0) {
py--;
ddF_y += 2;
f += ddF_y;
}
px++;
ddF_x += 2;
f += ddF_x;
// Fill corner pixels to prevent holes
// Top corners
for (int16_t i = x + radius - px; i <= x + radius + px + w - 2*radius - 1; i++) {
setPixel(screen, i, y + radius - py, color);
}
for (int16_t i = x + radius - py; i <= x + radius + py + w - 2*radius - 1; i++) {
setPixel(screen, i, y + radius - px, color);
}
// Bottom corners
for (int16_t i = x + radius - px; i <= x + radius + px + w - 2*radius - 1; i++) {
setPixel(screen, i, y + h - radius - 1 + py, color);
}
for (int16_t i = x + radius - py; i <= x + radius + py + w - 2*radius - 1; i++) {
setPixel(screen, i, y + h - radius - 1 + px, color);
}
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
// ========================================
// TEXT RENDERING SYSTEM
// ========================================
void ST7735DualEngine::drawChar(const int screen, const int16_t x, const int16_t y, const char c,
const uint16_t color, const uint8_t size, const uint16_t bg) {
// Fixed character mapping for standard ASCII set
uint8_t fontIndex = 0;
if (c >= 32 && c <= 126) {
// Direct mapping for ASCII 32-126 (printable characters)
fontIndex = c - 32;
} else {
// Default to space for unsupported characters
fontIndex = 0;
}
// Bounds check for font array - the font array has 95 characters (32-126)
if (UNLIKELY(fontIndex >= 95)) {
fontIndex = 0;
}
uint32_t renderStart = millis();
// Optimized character rendering
if (LIKELY(size == 1)) {
// Fast path for size 1 characters
for (uint8_t col = 0; col < ST7735Specs::FONT_WIDTH; ++col) {
const uint8_t line = pgm_read_byte(&font5x7_standard[fontIndex][col]);
for (uint8_t row = 0; row < ST7735Specs::FONT_HEIGHT; ++row) {
const bool pixelOn = line & (1 << row);
setPixel(screen, x + col, y + row, pixelOn ? color : bg);
}
}
} else {
// Scaled character rendering using fillRect for efficiency
for (uint8_t col = 0; col < ST7735Specs::FONT_WIDTH; ++col) {
const uint8_t line = pgm_read_byte(&font5x7_standard[fontIndex][col]);
for (uint8_t row = 0; row < ST7735Specs::FONT_HEIGHT; ++row) {
const bool pixelOn = line & (1 << row);
const uint16_t pixelColor = pixelOn ? color : bg;
fillRect(screen, x + col * size, y + row * size, size, size, pixelColor);
}
}
}
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
void ST7735DualEngine::drawText(const int screen, const int16_t x, const int16_t y, const char* text,
const uint16_t color, const uint8_t size, const uint16_t bg) {
if (UNLIKELY(!text)) return;
int16_t cursorX = x;
int16_t cursorY = y;
const int16_t charWidth = ST7735Specs::FONT_WIDTH * size;
const int16_t charHeight = ST7735Specs::FONT_HEIGHT * size;
const int16_t lineHeight = ST7735Specs::FONT_LINE_HEIGHT * size;
// Add bounds checking for text rendering
if (UNLIKELY(cursorY >= DISPLAY_HEIGHT || cursorY + charHeight < 0)) return;
while (*text) {
if (*text == '\n') {
// Move to next line
cursorX = x;
cursorY += lineHeight;
// Check if we've moved beyond display bounds
if (cursorY >= DISPLAY_HEIGHT) break;
} else if (*text == '\r') {
// Carriage return - move to beginning of current line
cursorX = x;
} else if (*text == '\t') {
// Tab character - move to next tab stop (every 4 character widths)
int16_t tabStop = ((cursorX - x) / (charWidth * 4) + 1) * (charWidth * 4);
cursorX = x + tabStop;
// Handle tab overflow
if (cursorX >= DISPLAY_WIDTH) {
cursorX = x;
cursorY += lineHeight;
if (cursorY >= DISPLAY_HEIGHT) break;
}
} else if (*text >= 32 && *text <= 126) {
// Printable character
// Check horizontal bounds before drawing
if (cursorX + charWidth > 0 && cursorX < DISPLAY_WIDTH) {
drawChar(screen, cursorX, cursorY, *text, color, size, bg);
}
cursorX += charWidth;
// Simple word wrap for long text
if (cursorX + charWidth > DISPLAY_WIDTH) {
cursorX = x;
cursorY += lineHeight;
// Check if we've moved beyond display bounds
if (cursorY >= DISPLAY_HEIGHT) break;
}
}
// Skip non-printable characters (except newline, return, tab)
++text;
}
}
// ========================================
// TEXT RENDERING HELPERS
// ========================================
int16_t ST7735DualEngine::getTextWidth(const char* text, const uint8_t size) const {
if (!text) return 0;
int16_t maxWidth = 0;
int16_t currentWidth = 0;
const int16_t charWidth = ST7735Specs::FONT_WIDTH * size;
while (*text) {
if (*text == '\n') {
// Newline - record max width and reset current
maxWidth = max(maxWidth, currentWidth);
currentWidth = 0;
} else if (*text == '\r') {
// Carriage return - reset current line width
currentWidth = 0;
} else if (*text == '\t') {
// Tab - calculate tab stop
int16_t tabStop = ((currentWidth) / (charWidth * 4) + 1) * (charWidth * 4);
currentWidth = tabStop;
} else if (*text >= 32 && *text <= 126) {
// Printable character
currentWidth += charWidth;
}
// Skip non-printable characters
++text;
}
// Return the maximum width encountered
return max(maxWidth, currentWidth);
}
int16_t ST7735DualEngine::getTextHeight(const char* text, const uint8_t size) const {
if (!text) return ST7735Specs::FONT_LINE_HEIGHT * size;
int16_t lines = 1; // Start with 1 line
const char* ptr = text;
while (*ptr) {
if (*ptr == '\n') {
lines++;
}
++ptr;
}
return lines * ST7735Specs::FONT_LINE_HEIGHT * size;
}
ST7735DualEngine::TextCursor ST7735DualEngine::getTextCursor(const int16_t startX, const int16_t startY,
const char* text, const uint8_t size) const {
TextCursor cursor = {startX, startY};
if (!text) return cursor;
const int16_t charWidth = ST7735Specs::FONT_WIDTH * size;
const int16_t lineHeight = ST7735Specs::FONT_LINE_HEIGHT * size;
while (*text) {
if (*text == '\n') {
cursor.x = startX;
cursor.y += lineHeight;
} else if (*text == '\r') {
cursor.x = startX;
} else if (*text == '\t') {
int16_t tabStop = ((cursor.x - startX) / (charWidth * 4) + 1) * (charWidth * 4);
cursor.x = startX + tabStop;
if (cursor.x >= DISPLAY_WIDTH) {
cursor.x = startX;
cursor.y += lineHeight;
}
} else if (*text >= 32 && *text <= 126) {
cursor.x += charWidth;
if (cursor.x + charWidth > DISPLAY_WIDTH) {
cursor.x = startX;
cursor.y += lineHeight;
}
}
++text;
}
return cursor;
}
void ST7735DualEngine::drawTextWithBackground(const int screen, const int16_t x, const int16_t y,
const char* text, const uint16_t textColor,
const uint16_t bgColor, const uint8_t size) {
if (!text) return;
// Calculate text dimensions
int16_t textWidth = getTextWidth(text, size);
int16_t textHeight = getTextHeight(text, size);
// Clear background first
fillRect(screen, x, y, textWidth, textHeight, bgColor);
// Draw text
drawText(screen, x, y, text, textColor, size, bgColor);
}
void ST7735DualEngine::drawTextCentered(const int screen, const int16_t centerX, const int16_t centerY,
const char* text, const uint16_t color, const uint8_t size,
const uint16_t bg) {
if (!text) return;
int16_t textWidth = getTextWidth(text, size);
int16_t textHeight = getTextHeight(text, size);
int16_t x = centerX - textWidth / 2;
int16_t y = centerY - textHeight / 2;
drawText(screen, x, y, text, color, size, bg);
}
void ST7735DualEngine::drawTextWithSpacing(const int screen, const int16_t x, const int16_t y, const char* text,
const uint16_t color, const uint8_t size, const uint8_t spacing,
const uint16_t bg) {
if (UNLIKELY(!text)) return;
int16_t cursorX = x;
int16_t cursorY = y;
const int16_t charWidth = ST7735Specs::FONT_WIDTH * size;
const int16_t charHeight = ST7735Specs::FONT_HEIGHT * size;
const int16_t lineHeight = ST7735Specs::FONT_LINE_HEIGHT * size;
// Add bounds checking for text rendering
if (UNLIKELY(cursorY >= DISPLAY_HEIGHT || cursorY + charHeight < 0)) return;
while (*text) {
if (*text == '\n') {
// Move to next line
cursorX = x;
cursorY += lineHeight;
// Check if we've moved beyond display bounds
if (cursorY >= DISPLAY_HEIGHT) break;
} else if (*text == '\r') {
// Carriage return - move to beginning of current line
cursorX = x;
} else if (*text == '\t') {
// Tab character - move to next tab stop (every 4 character widths)
int16_t tabStop = ((cursorX - x) / (charWidth * 4) + 1) * (charWidth * 4);
cursorX = x + tabStop;
// Handle tab overflow
if (cursorX >= DISPLAY_WIDTH) {
cursorX = x;
cursorY += lineHeight;
if (cursorY >= DISPLAY_HEIGHT) break;
}
} else if (*text >= 32 && *text <= 126) {
// Printable character
// Check horizontal bounds before drawing
if (cursorX + charWidth > 0 && cursorX < DISPLAY_WIDTH) {
drawChar(screen, cursorX, cursorY, *text, color, size, bg);
}
cursorX += charWidth + spacing; // Add custom spacing between characters
// Simple word wrap for long text
if (cursorX + charWidth > DISPLAY_WIDTH) {
cursorX = x;
cursorY += lineHeight;
// Check if we've moved beyond display bounds
if (cursorY >= DISPLAY_HEIGHT) break;
}
}
// Skip non-printable characters (except newline, return, tab)
++text;
}
}
// ========================================
// DISPLAY UPDATE SYSTEM
// ========================================
void ST7735DualEngine::updateDisplay(const int screen) {
const uint8_t cs = LIKELY(screen == 0) ? SCREEN0_CS : SCREEN1_CS;
const uint8_t dc = LIKELY(screen == 0) ? SCREEN0_DC : SCREEN1_DC;
const uint16_t* buffer = getBufferPtr(screen);
if (UNLIKELY(!buffer)) return;
uint32_t transferStart = millis();
// Always use Arduino SPI - no DMA in this version
pushColorsFast(buffer, cs, dc);
uint32_t transferTime = millis() - transferStart;
stats.update(0, transferTime);
}
void ST7735DualEngine::updateBoth() {
uint32_t renderStart = millis();
updateDisplay(0);
updateDisplay(1);
uint32_t renderTime = millis() - renderStart;
// Count this as one presented frame with proper timing
stats.update(0, renderTime); // renderTime includes both display updates
}
void ST7735DualEngine::updateRegion(const int screen, const int16_t x, const int16_t y,
const int16_t w, const int16_t h) {
// Validate screen parameter
if (UNLIKELY(screen < 0 || screen > 1)) return;
const uint8_t cs = LIKELY(screen == 0) ? SCREEN0_CS : SCREEN1_CS;
const uint8_t dc = LIKELY(screen == 0) ? SCREEN0_DC : SCREEN1_DC;
const uint16_t* buffer = getBufferPtr(screen);
if (UNLIKELY(!buffer)) return;
// Clamp region to display bounds
int16_t x1 = max((int16_t)0, min((int16_t)(DISPLAY_WIDTH - 1), x));
int16_t y1 = max((int16_t)0, min((int16_t)(DISPLAY_HEIGHT - 1), y));
int16_t x2 = max((int16_t)0, min((int16_t)(DISPLAY_WIDTH - 1), (int16_t)(x + w - 1)));
int16_t y2 = max((int16_t)0, min((int16_t)(DISPLAY_HEIGHT - 1), (int16_t)(y + h - 1)));
if (UNLIKELY(x1 >= x2 || y1 >= y2)) return; // Invalid region
uint32_t transferStart = millis();
// Set display window for the specific region
setWindow(x1, y1, x2, y2, cs, dc);
// Calculate region dimensions
const int16_t regionWidth = x2 - x1 + 1;
const int16_t regionHeight = y2 - y1 + 1;
const uint32_t regionPixels = regionWidth * regionHeight;
const uint32_t regionBytes = regionPixels * 2; // 2 bytes per RGB565 pixel
// Prepare transfer buffer for the region
uint8_t* txBuffer = transferBuffer; // Reuse transfer buffer for region transfers
uint32_t bufferIndex = 0;
// Copy region pixels to transfer buffer with byte order conversion
for (int16_t row = y1; row <= y2; ++row) {
for (int16_t col = x1; col <= x2; ++col) {
const uint16_t pixel = buffer[row * DISPLAY_WIDTH + col];
txBuffer[bufferIndex++] = pixel >> 8; // High byte
txBuffer[bufferIndex++] = pixel & 0xFF; // Low byte
}
}
// Transfer the region data
digitalWrite(dc, HIGH); // Data mode
digitalWrite(cs, LOW);
SPI.transferBytes(txBuffer, nullptr, regionBytes);
digitalWrite(cs, HIGH);
uint32_t transferTime = millis() - transferStart;
stats.update(0, transferTime);
}
#if ST7735_ENABLE_DUAL_CORE
void ST7735DualEngine::updateDisplayAsync(const int screen) {
if (UNLIKELY(!dualCoreActive)) {
updateDisplay(screen);
return;
}
ST7735TaskMessage msg = {
LIKELY(screen == 0) ? ST7735TaskCommand::UPDATE_DISPLAY_0 : ST7735TaskCommand::UPDATE_DISPLAY_1,
static_cast<uint8_t>(screen), 0, 0, 0, 0
};
xQueueSend(commandQueue, &msg, 0); // Non-blocking send
}
void ST7735DualEngine::updateBothAsync() {
if (!dualCoreActive || !commandQueue) {
updateBoth();
return;
}
ST7735TaskMessage msg = {ST7735TaskCommand::UPDATE_BOTH, 0, 0, 0, 0, 0};
if (xQueueSend(commandQueue, &msg, pdMS_TO_TICKS(5)) != pdTRUE) {
#if ST7735_ENABLE_DEBUG
Serial.println("Warning: Command queue full, falling back to synchronous update");
#endif
updateBoth();
}
}
#endif
// ========================================
// OTSC IMPLEMENTATION
// ========================================
bool ST7735DualEngine::ensureStartupReliability() {
if (startupReliabilityComplete) {
return true; // Already verified, no CPU overhead
}
#if ST7735_ENABLE_DEBUG
Serial.println("=== Starting Reliability Check ===");
#endif
for (startupAttempts = 1; startupAttempts <= MAX_STARTUP_ATTEMPTS; ++startupAttempts) {
#if ST7735_ENABLE_DEBUG
Serial.printf("Reliability attempt %d/%d\n", startupAttempts, MAX_STARTUP_ATTEMPTS);
#endif
if (performStartupReliabilityCheck()) {
startupReliabilityComplete = true;
#if ST7735_ENABLE_DEBUG
Serial.printf("β Startup reliability achieved on attempt %d\n", startupAttempts);
#endif
return true;
}
// Progressive delay increase for stubborn displays
delay(STARTUP_STABILITY_DELAY * startupAttempts);
// Force re-initialization on failed attempts
if (startupAttempts < MAX_STARTUP_ATTEMPTS) {
#if ST7735_ENABLE_DEBUG
Serial.println("Forcing display re-initialization...");
#endif
forceDisplayWakeup(SCREEN0_CS, SCREEN0_DC, SCREEN0_RST);
delay(INTER_DISPLAY_DELAY);
forceDisplayWakeup(SCREEN1_CS, SCREEN1_DC, SCREEN1_RST);
delay(INTER_DISPLAY_DELAY);
}
}
#if ST7735_ENABLE_DEBUG
Serial.println("β Warning: Startup reliability check failed after all attempts");
Serial.println("Displays may work but reliability not guaranteed");
#endif
return false;
}
bool ST7735DualEngine::performStartupReliabilityCheck() {
// Test both displays with minimal overhead
bool screen0_ok = testDisplayCommunication(SCREEN0_CS, SCREEN0_DC);
delay(10); // Brief pause between tests
bool screen1_ok = testDisplayCommunication(SCREEN1_CS, SCREEN1_DC);
if (!screen0_ok || !screen1_ok) {
#if ST7735_ENABLE_DEBUG
Serial.printf("Communication test failed - Screen0: %s, Screen1: %s\n",
screen0_ok ? "OK" : "FAIL", screen1_ok ? "OK" : "FAIL");
#endif
return false;
}
// Perform visual test to catch white screen issue
return validateDisplayInitialization(SCREEN0_CS, SCREEN0_DC) &&
validateDisplayInitialization(SCREEN1_CS, SCREEN1_DC);
}
bool ST7735DualEngine::testDisplayCommunication(const uint8_t cs, const uint8_t dc) const {
// Quick communication test - minimal CPU overhead
// Test 1: Try to read display ID (if supported)
writeCommand(0x04, cs, dc); // Read Display ID
delay(2);
// Test 2: Set and verify a known state
writeCommand(0x36, cs, dc); // Memory Access Control
writeData(0xC8, cs, dc); // Known good value
delay(1);
// Test 3: Verify display accepts pixel format command
writeCommand(0x3A, cs, dc); // Pixel Format Set
writeData(0x05, cs, dc); // 16-bit RGB565
delay(1);
// If we get here without hanging, communication is likely working
return true;
}
void ST7735DualEngine::forceDisplayWakeup(const uint8_t cs, const uint8_t dc, const uint8_t rst) const {
// Aggressive wakeup sequence for stubborn displays
// Force hardware reset with extended timing
digitalWrite(rst, LOW);
delay(20); // Longer reset hold
digitalWrite(rst, HIGH);
delay(150); // Longer recovery time
// Send wakeup commands sequence
writeCommand(0x01, cs, dc); // Software Reset
delay(150);
writeCommand(0x11, cs, dc); // Sleep Out
delay(150);
// Force pixel format early to establish communication
writeCommand(0x3A, cs, dc); // Pixel Format Set
writeData(0x05, cs, dc); // 16-bit RGB565
delay(20);
// Force display on
writeCommand(0x29, cs, dc); // Display On
delay(50);
// Clear any potential white screen by filling with black
setWindow(0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1, cs, dc);
digitalWrite(dc, HIGH); // Data mode
digitalWrite(cs, LOW);
// Fast black fill - unrolled for speed
for (uint32_t i = 0; i < BUFFER_SIZE; i += 4) {
SPI.transfer(0x00); SPI.transfer(0x00); // Pixel 1
SPI.transfer(0x00); SPI.transfer(0x00); // Pixel 2
SPI.transfer(0x00); SPI.transfer(0x00); // Pixel 3
SPI.transfer(0x00); SPI.transfer(0x00); // Pixel 4
}
digitalWrite(cs, HIGH);
delay(10);
}
bool ST7735DualEngine::validateDisplayInitialization(const uint8_t cs, const uint8_t dc) const {
// Visual validation to catch white screen issue
// Draw a test pattern to verify display is responding correctly
setWindow(0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1, cs, dc);
digitalWrite(dc, HIGH); // Data mode
digitalWrite(cs, LOW);
// Draw alternating pattern - should be visible if display works
for (uint32_t i = 0; i < BUFFER_SIZE; ++i) {
uint8_t pattern = (i & 1) ? STARTUP_TEST_PATTERN : ~STARTUP_TEST_PATTERN;
SPI.transfer(pattern);
SPI.transfer(pattern);
}
digitalWrite(cs, HIGH);
delay(50); // Allow pattern to display
// Clear back to black
setWindow(0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1, cs, dc);
digitalWrite(dc, HIGH);
digitalWrite(cs, LOW);
for (uint32_t i = 0; i < BUFFER_SIZE; ++i) {
SPI.transfer(0x00);
SPI.transfer(0x00);
}
digitalWrite(cs, HIGH);
delay(10);
return true; // If we reach here, display accepted the commands
}
// ========================================
// ADVANCED BUFFER OPERATIONS
// ========================================
void ST7735DualEngine::swapBuffers() {
uint16_t* temp = buffer0;
buffer0 = buffer1;
buffer1 = temp;
}
void ST7735DualEngine::copyBuffer(const int srcScreen, const int dstScreen) {
const uint16_t* src = getBufferPtr(srcScreen);
uint16_t* dst = getBufferPtr(dstScreen);
if (UNLIKELY(!src || !dst)) return;
#if ST7735_ENABLE_DUAL_CORE
if (dualCoreActive && xSemaphoreTake(bufferMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
#endif
memcpy(dst, src, TRANSFER_BYTES);
#if ST7735_ENABLE_DUAL_CORE
xSemaphoreGive(bufferMutex);
}
#endif
}
void ST7735DualEngine::blendBuffers(const int screen1, const int screen2, const uint8_t alpha) {
const uint16_t* src1 = getBufferPtr(screen1);
uint16_t* src2 = getBufferPtr(screen2);
if (UNLIKELY(!src1 || !src2)) return;
uint32_t renderStart = millis();
#if ST7735_ENABLE_DUAL_CORE
if (dualCoreActive && xSemaphoreTake(bufferMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
#endif
// Blend screen1 into screen2 with alpha transparency
const uint8_t invAlpha = 255 - alpha;
for (uint32_t i = 0; i < BUFFER_SIZE; ++i) {
const uint16_t c1 = src1[i];
const uint16_t c2 = src2[i];
// Extract RGB components for both colors
const uint32_t r1 = (c1 >> 11) & 0x1F;
const uint32_t g1 = (c1 >> 5) & 0x3F;
const uint32_t b1 = c1 & 0x1F;
const uint32_t r2 = (c2 >> 11) & 0x1F;
const uint32_t g2 = (c2 >> 5) & 0x3F;
const uint32_t b2 = c2 & 0x1F;
// Blend and pack
const uint32_t r = (r2 * invAlpha + r1 * alpha) >> 8;
const uint32_t g = (g2 * invAlpha + g1 * alpha) >> 8;
const uint32_t b = (b2 * invAlpha + b1 * alpha) >> 8;
src2[i] = (r << 11) | (g << 5) | b;
}
#if ST7735_ENABLE_DUAL_CORE
xSemaphoreGive(bufferMutex);
}
#endif
uint32_t renderTime = millis() - renderStart;
stats.update(renderTime, 0);
}
// ========================================
// COLOR UTILITIES
// ========================================
uint16_t ST7735DualEngine::blendColors(const uint16_t color1, const uint16_t color2, const uint8_t alpha) {
// Optimized alpha blend for RGB565
const uint8_t invAlpha = 255 - alpha;
// Extract RGB components
const uint32_t r1 = (color1 >> 11) & 0x1F;
const uint32_t g1 = (color1 >> 5) & 0x3F;
const uint32_t b1 = color1 & 0x1F;
const uint32_t r2 = (color2 >> 11) & 0x1F;
const uint32_t g2 = (color2 >> 5) & 0x3F;
const uint32_t b2 = color2 & 0x1F;
// Blend and pack
const uint32_t r = (r1 * invAlpha + r2 * alpha) >> 8;
const uint32_t g = (g1 * invAlpha + g2 * alpha) >> 8;
const uint32_t b = (b1 * invAlpha + b2 * alpha) >> 8;
return (r << 11) | (g << 5) | b;
}
uint16_t ST7735DualEngine::interpolateColors(const uint16_t color1, const uint16_t color2, const float t) {
const float invT = 1.0f - t;
const uint8_t r1 = red565(color1);
const uint8_t g1 = green565(color1);
const uint8_t b1 = blue565(color1);
const uint8_t r2 = red565(color2);
const uint8_t g2 = green565(color2);
const uint8_t b2 = blue565(color2);
const uint8_t r = static_cast<uint8_t>(r1 * invT + r2 * t);
const uint8_t g = static_cast<uint8_t>(g1 * invT + g2 * t);
const uint8_t b = static_cast<uint8_t>(b1 * invT + b2 * t);
return rgb565(r, g, b);
}
// ========================================
// PERFORMANCE AND DIAGNOSTICS
// ========================================
void ST7735DualEngine::printSystemInfo() const {
Serial.println("=== ST7735DualEngine v1.3 Arduino SPI-Only ===");
Serial.printf("Platform: %s\n", ST7735Platform::getPlatformName());
Serial.printf("Display Resolution: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
Serial.printf("Buffer Size: %u KB each\n", TRANSFER_BYTES / 1024);
Serial.printf("SPI Frequency: %u MHz\n", ST7735Platform::getOptimalSPIFrequency() / 1000000);
Serial.printf("Arduino SPI: Enabled (Zero Conflicts)\n");
Serial.printf("DMA Support: Disabled (Arduino SPI-Only)\n");
Serial.printf("Dual-Core Support: %s\n", isDualCoreEnabled() ? "Enabled" : "Disabled");
Serial.printf("Free Heap: %u KB\n", ESP.getFreeHeap() / 1024);
#if ST7735_USE_PSRAM
Serial.printf("PSRAM Total: %u KB\n", ESP.getPsramSize() / 1024);
Serial.printf("PSRAM Free: %u KB\n", ESP.getFreePsram() / 1024);
Serial.printf("Used PSRAM: %u KB\n", getUsedPSRAM() / 1024);
#endif
Serial.printf("Buffer Memory Usage: %u KB\n", getBufferMemoryUsage() / 1024);
Serial.printf("Font: %dx%d bitmap\n", ST7735Specs::FONT_WIDTH, ST7735Specs::FONT_HEIGHT);
Serial.println("=================================================");
}
void ST7735DualEngine::printPerformanceReport() const {
Serial.println("=== Performance Report v1.3 Arduino SPI-Only ===");
// Handle overflow protection for display
if (stats.frameCount == UINT32_MAX - 1) {
Serial.println("Frames Rendered: overflow");
} else {
Serial.printf("Frames Rendered: %u\n", stats.frameCount);
}
Serial.printf("Average Operations PS: %.2f\n", stats.averageOPS);
Serial.printf("Average Frame Heavy Operations PS: %.2f\n", stats.averageFHOPS);
Serial.printf("Peak Frame Time: %u ms\n", stats.peakFrameTime);
Serial.printf("Peak Transfer Time: %u ms\n", stats.peakTransferTime);
Serial.printf("Total Render Time: %u ms\n", stats.totalRenderTime);
Serial.printf("Total Transfer Time: %u ms\n", stats.totalTransferTime);
if (LIKELY(stats.frameCount > 0 && stats.frameCount < UINT32_MAX - 1)) {
Serial.printf("Avg Render Time: %.2f ms\n", stats.totalRenderTime / (float)stats.frameCount);
Serial.printf("Avg Transfer Time: %.2f ms\n", stats.totalTransferTime / (float)stats.frameCount);
}
Serial.println("===============================");
}
uint32_t ST7735DualEngine::getUsedPSRAM() const {
#if ST7735_USE_PSRAM
return ESP.getPsramSize() - ESP.getFreePsram();
#else
return 0;
#endif
}
// ========================================
// TEST PATTERNS AND DIAGNOSTICS
// ========================================
void ST7735DualEngine::drawTestPattern(const int screen) {
clearBuffer(screen, ST7735Colors::BLACK);
// Enhanced test pattern with performance indicators
for (int16_t y = 0; y < HEIGHT; y += 20) {
uint16_t color = rgb565((y * 255) / HEIGHT, 128, 255 - (y * 255) / HEIGHT);
fillRect(screen, 0, y, WIDTH, 20, color);
}
// Corner markers with version info
fillRect(screen, 0, 0, 10, 10, ST7735Colors::WHITE);
fillRect(screen, WIDTH-10, 0, 10, 10, ST7735Colors::RED);
fillRect(screen, 0, HEIGHT-10, 10, 10, ST7735Colors::GREEN);
fillRect(screen, WIDTH-10, HEIGHT-10, 10, 10, ST7735Colors::BLUE);
// Performance indicator
drawText(screen, 2, HEIGHT-30, "v1.3", ST7735Colors::WHITE, 1);
// Center cross
drawHLine(screen, WIDTH/2 - 10, HEIGHT/2, 20, ST7735Colors::WHITE);
drawVLine(screen, WIDTH/2, HEIGHT/2 - 10, 20, ST7735Colors::WHITE);
}
void ST7735DualEngine::drawColorBars(const int screen) {
const uint16_t colors[] = {
ST7735Colors::WHITE, ST7735Colors::YELLOW, ST7735Colors::CYAN,
ST7735Colors::GREEN, ST7735Colors::MAGENTA, ST7735Colors::RED,
ST7735Colors::BLUE, ST7735Colors::BLACK
};
const int16_t barHeight = HEIGHT / 8;
for (int i = 0; i < 8; i++) {
fillRect(screen, 0, i * barHeight, WIDTH, barHeight, colors[i]);
}
// Add text labels for verification
drawText(screen, 5, 5, "WHITE", ST7735Colors::BLACK, 1);
drawText(screen, 5, HEIGHT - 15, "BLACK", ST7735Colors::WHITE, 1);
}
void ST7735DualEngine::drawGridPattern(const int screen, const uint16_t color) {
clearBuffer(screen, ST7735Colors::BLACK);
// Optimized grid with reduced draw calls
for (int16_t x = 0; x < WIDTH; x += 16) {
drawVLine(screen, x, 0, HEIGHT, color);
}
for (int16_t y = 0; y < HEIGHT; y += 16) {
drawHLine(screen, 0, y, WIDTH, color);
}
// Grid coordinates
char buffer[8];
for (int16_t x = 16; x < WIDTH; x += 32) {
for (int16_t y = 16; y < HEIGHT; y += 32) {
snprintf(buffer, sizeof(buffer), "%d,%d", x/16, y/16);
drawText(screen, x + 2, y + 2, buffer, color, 1);
}
}
}
#pragma GCC diagnostic pop
Copy these two files into your Arduino IDE project directory, write the entry point ino sketch, refer to the header file for the porotypes (kept them similar to common prototypes as much as possible for ~zero unnecessary cognitive load in integration) and have a blast.
If anyone else wants to try out for ESP32 or even test it out as is for Devkit C1 for confirmation, please feel free to do so, I will definitely document your contributions in the next patches or updates.
EDIT 04/09/2025: Patched Library V 2.1.1 where most type conversions have been removed to create futureproof overhead buffer for runtime, and critical multiplications and divisions have been replaced and accomplished with bit shift operations.
TFTFRIEND_7735Duo.cpp (96.0 KB)
ST7735DualEngine.h (40.0 KB)
EDIT 10/09/2025: Patched Library V 2.1.2 to address proper FPS tracking, and commented out FHOPS (Frame Heavy Operations Per Second) during performance revision (for less CPU overhead).
TFTFRIEND_7735Duo.cpp (88.7 KB)
ST7735DualEngine.h (41.5 KB)
In the subsequent articles I will be providing example ino sketches.
An ESP-IDF managed SPI version is also in work.