From 8006cb50f249b131db0df6cc4c4b9c39dabc59e3 Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:09:18 -0400 Subject: [PATCH 1/7] S3 LTO Script --- pio-scripts/enable_lto_s3.py | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 pio-scripts/enable_lto_s3.py diff --git a/pio-scripts/enable_lto_s3.py b/pio-scripts/enable_lto_s3.py new file mode 100644 index 0000000000..e15256fe7e --- /dev/null +++ b/pio-scripts/enable_lto_s3.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +Enable Link Time Optimization (LTO) for PlatformIO / Xtensa ESP32-S3 builds. + +Simply adding -flto to build_flags is not sufficient: LTO also requires the +linker to receive -flto and the archiver/ranlib to be the GCC-LTO-aware +variants (gcc-ar / gcc-ranlib). This script wires all three up so that the +flag is consistently applied and the static-library archives are built in a +way that LTO can see through them. +""" +## This script was created with the help of an AI, reviewed by @troyhacks + +Import("env") +import os +import shutil + +def _find_tool(name, cc_dir): + """Locate a toolchain binary: check cc_dir first, then PATH.""" + if cc_dir: + for suffix in ("", ".exe"): + candidate = os.path.join(cc_dir, name + suffix) + if os.path.isfile(candidate): + return candidate + # Fall back to searching PATH + return shutil.which(name) + +def enable_lto(env): + # -flto: LTO itself. + # -fipa-pta: interprocedural pointer analysis — requires whole-program IR, only useful with LTO. + # -ffunction-sections / -fdata-sections / -Wl,--gc-sections: linker dead-code elimination; + # far more effective with LTO because the linker has cross-TU visibility. + LTO_CCFLAGS = ["-flto", "-fipa-pta", "-ffunction-sections", "-fdata-sections"] + LTO_LDFLAGS = ["-flto", "-Wl,--gc-sections"] + + for flaglist, new_flags in (("CCFLAGS", LTO_CCFLAGS), + ("CXXFLAGS", LTO_CCFLAGS), + ("LINKFLAGS", LTO_LDFLAGS)): + existing = env.get(flaglist, []) + to_add = [f for f in new_flags if f not in existing] + if to_add: + env.Append(**{flaglist: to_add}) + + # Swap ar / ranlib for the LTO-aware GCC wrappers so that static + # library archives carry IR that the linker can optimise across. + cc = str(env.get("CC", "")) + if cc: + toolchain_prefix = cc.replace("gcc", "").replace("g++", "") + # toolchain_prefix is something like "xtensa-esp32s3-elf-" + new_ar = toolchain_prefix + "gcc-ar" + new_ranlib = toolchain_prefix + "gcc-ranlib" + + # Resolve CC to its real path so we can search the same directory + cc_resolved = shutil.which(cc) or cc + cc_dir = os.path.dirname(cc_resolved) + + ar_path = _find_tool(new_ar, cc_dir) + if ar_path: + env.Replace(AR=ar_path) + print(f"enable_lto: AR -> {ar_path}") + else: + print(f"enable_lto: gcc-ar '{new_ar}' not found, keeping default AR") + + ranlib_path = _find_tool(new_ranlib, cc_dir) + if ranlib_path: + env.Replace(RANLIB=ranlib_path) + print(f"enable_lto: RANLIB -> {ranlib_path}") + else: + print(f"enable_lto: gcc-ranlib '{new_ranlib}' not found, keeping default RANLIB") + + print("enable_lto: -flto added to CCFLAGS / CXXFLAGS / LINKFLAGS") + +enable_lto(env) From 4bd59600933414203ee4dbb5d3ce3a7032c65cfc Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 20 May 2026 15:51:41 -0400 Subject: [PATCH 2/7] Initial version for submission - needs fixes. --- usermods/M5Stack_CoreS3_Display/readme.md | 42 ++ .../usermod_m5stack_s3_display.h | 404 ++++++++++++++++++ wled00/const.h | 1 + wled00/usermods_list.cpp | 8 + wled00/wled.cpp | 4 +- wled00/xml.cpp | 4 +- 6 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 usermods/M5Stack_CoreS3_Display/readme.md create mode 100644 usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h diff --git a/usermods/M5Stack_CoreS3_Display/readme.md b/usermods/M5Stack_CoreS3_Display/readme.md new file mode 100644 index 0000000000..1fbf4c26a4 --- /dev/null +++ b/usermods/M5Stack_CoreS3_Display/readme.md @@ -0,0 +1,42 @@ +# M5Stack Core S3 Display Usermod + +Display usermod for the ILI9342C 320x240 TFT display on the M5Stack Core S3, using LovyanGFX. + +## Pin Mapping (M5Stack Core S3) + +| ESP32-S3 | ILI9342C | Description | +|----------|----------|-----------------| +| G37 | MOSI | SPI Data | +| G36 | SCLK | SPI Clock | +| G3 | CS | Chip Select | +| G35 | DC | Data/Command | + +Reset is controlled via the AW9523B GPIO expander (P1_1). Backlight is powered via AXP2101 PMU (DLDO1). + +## Building + +In `platformio_override.ini` for your M5Stack Core S3 environment: + +```ini +build_flags = + -D USERMOD_M5STACK_CORE_S3_DISPLAY + +lib_deps = + https://github.com/lovyan03/LovyanGFX +``` + +## Features + +- SSID and IP address in header bar +- 16-band graphic equalizer bars (differential drawing) +- Real audio reactive data when Audioreactive usermod is enabled +- Simulated bouncing bars when no audio data +- Rainbow color per bar (red → violet) +- Auto sleep after 5 minutes of inactivity + +## Display Notes + +- Uses LovyanGFX with `SPI3_HOST` (HSPI) +- Native landscape 320x240 resolution +- BGR color order, display inversion enabled +- Backlight always on (controlled by AXP2101 DLDO1) diff --git a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h new file mode 100644 index 0000000000..e63daf1bfa --- /dev/null +++ b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h @@ -0,0 +1,404 @@ +#pragma once + +#include "wled.h" +#include +#include + +// LovyanGFX — no compile-time TFT_eSPI macros needed +#include + +#ifndef USERMOD_ID_M5STACK_CORE_S3_DISPLAY + #define USERMOD_ID_M5STACK_CORE_S3_DISPLAY 95 +#endif + +// AXP2101 PMU I2C address +#define AXP2101_ADDR 0x34 +// AW9523B GPIO Expander I2C address +#define AW9523B_ADDR 0x58 + +// M5Stack Core S3 — SPI3 (HSPI) on these pins +#define TFT_CS 3 +#define TFT_DC 35 +#define TFT_MOSI 37 +#define TFT_SCLK 36 + +// LovyanGFX panel definition for ILI9342 on ESP32-S3 +class LGFX_ILI9342_M5StackS3 : public lgfx::LGFX_Device { +private: + lgfx::Bus_SPI _bus_instance; + lgfx::Panel_ILI9342 _panel_instance; + +public: + LGFX_ILI9342_M5StackS3(void) { + // SPI bus configuration — ESP32-S3 HSPI (SPI3) + { + auto cfg = _bus_instance.config(); + cfg.spi_host = SPI3_HOST; + cfg.spi_mode = 0; + cfg.freq_write = 10000000; + cfg.freq_read = 6000000; + cfg.pin_mosi = TFT_MOSI; + cfg.pin_sclk = TFT_SCLK; + cfg.pin_miso = -1; + cfg.pin_dc = TFT_DC; + cfg.spi_3wire = false; + cfg.use_lock = true; + cfg.dma_channel = SPI_DMA_CH_AUTO; + _bus_instance.config(cfg); + _panel_instance.setBus(&_bus_instance); + } + + // Panel configuration + { + auto cfg = _panel_instance.config(); + cfg.pin_cs = TFT_CS; + cfg.pin_rst = -1; // managed externally (AW9523) + cfg.bus_shared = false; + cfg.panel_width = 320; + cfg.panel_height = 240; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.rgb_order = false; // true=BGR (swap R/B), false=RGB + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.offset_rotation = 0; // 0=landscape (native 320x240), 1=+90°, 2=+180° + cfg.invert = true; + _panel_instance.config(cfg); + } + + setPanel(&_panel_instance); + } +}; + +static LGFX_ILI9342_M5StackS3 tft; + +extern int getSignalQuality(int rssi); + + +//class name. Use something descriptive and leave the ": public Usermod" part :) +class M5StackCoreS3DisplayUsermod : public Usermod { + private: + bool enabled = true; + + bool displayTurnedOff = false; + long lastRedraw = 0; + // needRedraw marks if redraw is required to prevent often redrawing. + bool needRedraw = true; + // Next variables hold the previous known values to determine if redraw is required. + String knownSsid = ""; + IPAddress knownIp; + + long lastUpdate = 0; + + // GEQ display constants + static constexpr uint8_t NUM_GEQ_BANDS = 16; + uint8_t HEADER_HEIGHT; // Set dynamically in setup() + uint8_t BAR_WIDTH; // Set dynamically in setup() + uint16_t prevBarHeight[NUM_GEQ_BANDS] = {0}; // Differential drawing: previous frame heights + + // Static vibrant color per bar based on position (rainbow: red → violet) + uint16_t geqColor(uint8_t bandIndex) { + switch (bandIndex % 16) { + case 0: return 0xF800; // red + case 1: return 0xFC00; // orange + case 2: return 0xFC40; // amber + case 3: return 0xFCC0; // yellow-orange + case 4: return 0xFFE0; // yellow + case 5: return 0xC700; // yellow-green + case 6: return 0x07E0; // green + case 7: return 0x07EF; // green-cyan + case 8: return 0x07FF; // cyan + case 9: return 0x041F; // cyan-blue + case 10: return 0x001F; // blue + case 11: return 0x281F; // blue-indigo + case 12: return 0x601F; // indigo + case 13: return 0x781F; // violet + case 14: return 0xF81F; // magenta + case 15: return 0xF81F; // magenta + default: return 0xF800; + } + } + + public: + //Functions called by WLED + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + Serial.println("M5StackS3Display: starting setup"); + + // Ensure Wire is initialized + if (i2c_sda >= 0 && i2c_scl >= 0) { + Wire.begin(i2c_sda, i2c_scl); + Wire.setTimeout(50); + } + + // AXP2101 - enable DLDO1 (VCC_BL per user) + // Read current state first + Wire.beginTransmission(AXP2101_ADDR); + Wire.write(0x90); + Wire.endTransmission(false); + Wire.requestFrom((uint8_t)AXP2101_ADDR, (uint8_t)1); + byte pwr_orig = Wire.read(); + Serial.printf("M5StackS3Display: 0x90 orig: 0x%02X\n", pwr_orig); + + // Set DLDO1 voltage to 3.3V (voltage reg 0x99) + // DLDO uses 100mV steps: (3300-500)/100 = 28 = 0x1C + Wire.beginTransmission(AXP2101_ADDR); + Wire.write(0x99); // DLDO1 voltage register + Wire.write(0x1C); // 3.3V + Wire.endTransmission(); + + // Read DLDO1 voltage back + Wire.beginTransmission(AXP2101_ADDR); + Wire.write(0x99); + Wire.endTransmission(false); + Wire.requestFrom((uint8_t)AXP2101_ADDR, (uint8_t)1); + byte dldo1_v = Wire.read(); + Serial.printf("M5StackS3Display: DLDO1 0x99: 0x%02X\n", dldo1_v); + + // Enable DLDO1 via bit 7 of 0x90 + byte pwr_new = pwr_orig | 0x80; // set bit 7 (DLDO1 enable) + Wire.beginTransmission(AXP2101_ADDR); + Wire.write(0x90); + Wire.write(pwr_new); + Wire.endTransmission(); + + delay(1); + + // Verify + Wire.beginTransmission(AXP2101_ADDR); + Wire.write(0x90); + Wire.endTransmission(false); + Wire.requestFrom((uint8_t)AXP2101_ADDR, (uint8_t)1); + byte pwr_after = Wire.read(); + Serial.printf("M5StackS3Display: 0x90 after: 0x%02X\n", pwr_after); + + // AW9523 GPIO expander for LCD reset + // P1_1 = LCD_RST + Wire.beginTransmission(AW9523B_ADDR); + Wire.write(0x00); // GCR - push-pull for port 0 + Wire.write(0xFF); + Wire.endTransmission(); + + Wire.beginTransmission(AW9523B_ADDR); + Wire.write(0x01); // GCR - push-pull for port 1 + Wire.write(0xFF); + Wire.endTransmission(); + + Wire.beginTransmission(AW9523B_ADDR); + Wire.write(0x07); // P1 direction (0=output, 1=input) + Wire.write(0xFD); // P1_1 output, rest input + Wire.endTransmission(); + + // Toggle P1_1 for reset + Wire.beginTransmission(AW9523B_ADDR); + Wire.write(0x03); // P1 output register + Wire.write(0x00); // P1_1 LOW (reset) + Wire.endTransmission(); + delay(10); + Wire.beginTransmission(AW9523B_ADDR); + Wire.write(0x03); // P1 output register + Wire.write(0x02); // P1_1 HIGH (release) + Wire.endTransmission(); + delay(10); + + // Verify AW9523 P1 register access + Wire.beginTransmission(AW9523B_ADDR); + Wire.write(0x03); // P1 output register + Wire.endTransmission(false); + Wire.requestFrom((uint8_t)AW9523B_ADDR, (uint8_t)1); + byte p1_state = Wire.read(); + Serial.printf("M5StackS3Display: AW9523B P1 state: 0x%02X\n", p1_state); + + Serial.println("M5StackS3Display: init TFT"); + tft.init(); + + Serial.printf("M5StackS3Display: TFT width: %d, height: %d\n", tft.width(), tft.height()); + + // Dynamic header sizing + tft.setTextSize(2); + HEADER_HEIGHT = tft.fontHeight() + 10; // 5px padding top and bottom + BAR_WIDTH = tft.width() / NUM_GEQ_BANDS; // 320 / 16 = 20 + + // Show loading screen + tft.fillScreen(TFT_BLACK); + + // Header background + tft.fillRect(0, 0, tft.width(), HEADER_HEIGHT, TFT_DARKGREY); + int16_t textY = (HEADER_HEIGHT - tft.fontHeight()) / 2; + tft.setTextColor(TFT_WHITE); + tft.setTextDatum(TL_DATUM); + tft.drawString("WLED", 10, textY); + tft.setTextDatum(TR_DATUM); + tft.drawString("Loading...", tft.width() - 10, textY); + + Serial.println("M5StackS3Display: setup complete"); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() { + // Faster refresh for bouncing bars + if (millis() - lastUpdate < 100) // 50ms = 20fps + { + return; + } + lastUpdate = millis(); + + // Turn off display after 5 minutes with no change. + if (!displayTurnedOff && millis() - lastRedraw > 5*60*1000) + { + displayTurnedOff = true; + } + + // Check if values which are shown on display changed from the last time. + #if defined(ESP8266) + String currentSsid = apActive ? String(apSSID) : WiFi.SSID(); + #else + String currentSsid = WiFi.SSID(); + #endif + IPAddress currentIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); + + if (currentSsid != knownSsid || currentIp != knownIp) + { + needRedraw = true; + knownSsid = currentSsid; + knownIp = currentIp; + } + + if (displayTurnedOff) + { + displayTurnedOff = false; + } + lastRedraw = millis(); + + // === HEADER: Only redraw when needed === + if (needRedraw) { + // Gray background + tft.fillRect(0, 0, tft.width(), HEADER_HEIGHT, TFT_DARKGREY); + + // Text vertically centered in header + int16_t textY = (HEADER_HEIGHT - tft.fontHeight()) / 2; + tft.setTextSize(2); + tft.setTextColor(TFT_WHITE); + + // SSID on left + tft.setTextDatum(TL_DATUM); + tft.drawString(knownSsid.c_str(), 10, textY); + + // IP on right + tft.setTextDatum(TR_DATUM); + tft.drawString(knownIp.toString().c_str(), tft.width() - 10, textY); + + needRedraw = false; + } + + // === 16 BOUNCING BARS - differential drawing === + uint8_t geq[NUM_GEQ_BANDS]; + + // Get audio data if available + um_data_t *um_data = nullptr; + if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // Real audio data + memcpy(geq, um_data->u_data[2], NUM_GEQ_BANDS); + } else { + // Simulated bouncing bars + unsigned long t = millis() / 100; + for (uint8_t i = 0; i < NUM_GEQ_BANDS; i++) { + geq[i] = ((t + i * 5) % 20) < 10 + ? ((t + i * 5) % 20) * 25 + : (20 - ((t + i * 5) % 20)) * 25; + } + } + + // Differential bar drawing - only touch pixels that changed + uint16_t canvasBottom = tft.height() - 1; + uint16_t availableHeight = tft.height() - HEADER_HEIGHT; + + for (uint8_t i = 0; i < NUM_GEQ_BANDS; i++) { + uint16_t x = i * BAR_WIDTH; + + // barHeight: 0-255 maps to 0-availableHeight pixels + uint16_t newBarHeight = (geq[i] * availableHeight) / 255; + if (newBarHeight < 1) newBarHeight = 1; + + int16_t diff = newBarHeight - prevBarHeight[i]; + + if (diff > 0) { + // Bar grew: draw new colored pixels at bottom + uint16_t bottomY = canvasBottom + 1; + tft.fillRect(x, bottomY - newBarHeight, BAR_WIDTH - 1, diff, geqColor(i)); + } else if (diff < 0) { + // Bar shrank: erase excess pixels starting from the OLD top down to the NEW top + uint16_t oldTopY = canvasBottom + 1 - prevBarHeight[i]; + tft.fillRect(x, oldTopY, BAR_WIDTH - 1, -diff, TFT_BLACK); + } + // diff == 0: no change, skip entirely + + prevBarHeight[i] = newBarHeight; + } + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + */ + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("M5StackS3Display"); //name + lightArr.add(enabled?F("installed"):F("disabled")); //unit + } + + void addToJsonState(JsonObject& root) { + root["M5S3_enabled"] = enabled; + } + void readFromJsonState(JsonObject& root) { + enabled = root["M5S3_enabled"] | enabled; + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + */ + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject("M5StackS3Display"); + top["CS"] = TFT_CS; + top["DC"] = TFT_DC; + top["MOSI"] = TFT_MOSI; + top["SCLK"] = TFT_SCLK; + top["enabled"] = enabled; + } + + void appendConfigData() + { + oappend(SET_F("addHB('M5StackS3Display');")); + oappend(SET_F("addInfo('M5StackS3Display:CS',0,'SPI Chip Select (GPIO3)');")); + oappend(SET_F("addInfo('M5StackS3Display:DC',0,'Data/Command (GPIO35)');")); + oappend(SET_F("addInfo('M5StackS3Display:MOSI',0,'SPI MOSI (GPIO37)');")); + oappend(SET_F("addInfo('M5StackS3Display:SCLK',0,'SPI Clock (GPIO36)');")); + } + + /* + * readFromConfig() is called BEFORE setup(). This is called by WLED when settings are loaded. + */ + bool readFromConfig(JsonObject& root) { + JsonObject top = root["M5StackS3Display"]; + if (!top.isNull()) enabled = top["enabled"] | true; + return true; + } + + /* + * getId() allows you to optionally give your v2 usermod an unique ID (please define it in const.h!). + */ + uint16_t getId() + { + return USERMOD_ID_M5STACK_CORE_S3_DISPLAY; + } +}; diff --git a/wled00/const.h b/wled00/const.h index a422dfddef..87f3e88ecb 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -159,6 +159,7 @@ #define USERMOD_ID_GAMES 92 //Usermod "usermod_v2_games.h" #define USERMOD_ID_ANIMARTRIX 93 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_AUTOPLAYLIST 94 // Usermod usermod_v2_auto_playlist.h +#define USERMOD_ID_M5STACK_CORE_S3_DISPLAY 95 // Usermod "M5Stack_CoreS3_Display" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index a6e7ba7a85..7f59e43e58 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -101,6 +101,10 @@ #include "../usermods/ST7789_display/ST7789_Display.h" #endif +#ifdef USERMOD_M5STACK_CORE_S3_DISPLAY +#include "../usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h" +#endif + #ifdef USERMOD_SEVEN_SEGMENT #include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" #endif @@ -299,6 +303,10 @@ void registerUsermods() usermods.add(new St7789DisplayUsermod()); #endif +#ifdef USERMOD_M5STACK_CORE_S3_DISPLAY + usermods.add(new M5StackCoreS3DisplayUsermod()); +#endif + #ifdef USERMOD_SEVEN_SEGMENT usermods.add(new SevenSegmentDisplay()); #endif diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 8015d58a06..9ef47be6b8 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -491,7 +491,7 @@ void WLED::setup() #if ARDUINO_USB_CDC_ON_BOOT && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)) Serial.begin(115200); // WLEDMM avoid "hung devices" when USB_CDC is enabled; see https://github.com/espressif/arduino-esp32/issues/9043 - Serial.setTxTimeoutMs(0); // potential side-effect: incomplete debug output, with missing characters whenever TX buffer is full. + Serial.setTxTimeoutMs(1); // potential side-effect: incomplete debug output, with missing characters whenever TX buffer is full. #else Serial.begin(115200); #endif @@ -525,7 +525,7 @@ void WLED::setup() #if ARDUINO_USB_CDC_ON_BOOT || ARDUINO_USB_MODE #if ARDUINO_USB_CDC_ON_BOOT && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)) // WLEDMM avoid "hung devices" when USB_CDC is enabled; see https://github.com/espressif/arduino-esp32/issues/9043 - Serial.setTxTimeoutMs(0); + Serial.setTxTimeoutMs(1); #endif #if !defined(WLEDMM_NO_SERIAL_WAIT) || defined(WLED_DEBUG) if (!Serial) delay(2500); // WLEDMM: always allow CDC USB serial to initialise diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 13ac2247cd..bd0c1aa029 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -239,7 +239,9 @@ void appendGPIOinfo() { if (psramFound()) oappend(SET_F(",16,17")); // GPIO16 & GPIO17 reserved for SPI RAM on ESP32 (not on S2, S3 or C3) } #elif defined(CONFIG_IDF_TARGET_ESP32S3) - if (psramFound()) oappend(SET_F(",33,34,35,36,37")); // in use for "octal" PSRAM or "octal" FLASH -seems that octal PSRAM is very common on S3. + #if CONFIG_SPIRAM_MODE_OCT || CONFIG_ESPTOOLPY_FLASHMODE_OPI + if (psramFound()) oappend(SET_F(",33,34,35,36,37")); // in use for "octal" PSRAM or "octal" FLASH + #endif #endif #endif From 4ae70a4f5f844950034c74ecb635f6d368f3a8ac Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 20 May 2026 17:50:24 -0400 Subject: [PATCH 3/7] Fixes after audit, improved docs. --- usermods/M5Stack_CoreS3_Display/readme.md | 31 +++- .../usermod_m5stack_s3_display.h | 138 +++++++++--------- wled00/wled.cpp | 4 +- 3 files changed, 96 insertions(+), 77 deletions(-) diff --git a/usermods/M5Stack_CoreS3_Display/readme.md b/usermods/M5Stack_CoreS3_Display/readme.md index 1fbf4c26a4..8121805c84 100644 --- a/usermods/M5Stack_CoreS3_Display/readme.md +++ b/usermods/M5Stack_CoreS3_Display/readme.md @@ -17,13 +17,20 @@ Reset is controlled via the AW9523B GPIO expander (P1_1). Backlight is powered v In `platformio_override.ini` for your M5Stack Core S3 environment: -```ini -build_flags = +```build_flags = -D USERMOD_M5STACK_CORE_S3_DISPLAY + ;; For the M5Stack ModuleAudio: + -D SR_ENABLE_DEFAULT + -D SR_DMTYPE=6 + -D I2S_SDPIN=13 + -D I2S_WSPIN=6 + -D I2S_CKPIN=0 + -D MCLK_PIN=7 + -D HW_SDA_PIN=12 + -D HW_SCL_PIN=11 lib_deps = - https://github.com/lovyan03/LovyanGFX -``` + https://github.com/lovyan03/LovyanGFX``` ## Features @@ -32,7 +39,8 @@ lib_deps = - Real audio reactive data when Audioreactive usermod is enabled - Simulated bouncing bars when no audio data - Rainbow color per bar (red → violet) -- Auto sleep after 5 minutes of inactivity +- Maximum of 100 FPS to match AudioReactive +- Minimum of 5 FPS so it updates even if you use unlimited FPS mode. ## Display Notes @@ -40,3 +48,16 @@ lib_deps = - Native landscape 320x240 resolution - BGR color order, display inversion enabled - Backlight always on (controlled by AXP2101 DLDO1) + +## TroyHacks Recommended AudioReactive Settings + +For the M5Stack Core S3 + ModuleAudio line-in: + +- The ModuleAudio uses an ES8388 +- Squelch & Gain at 1 +- AGC is Normal +- MicLev is Freeze +- Mic Quality is Perfect +- FFT Window is Nutall (or whatever you prefer) +- Profile is Generic Line-In +- Limiter is Enabled, with a rise of 1 and a fall of 250 (to 500). diff --git a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h index e63daf1bfa..8f4ddb6d5c 100644 --- a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h +++ b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h @@ -72,23 +72,19 @@ class LGFX_ILI9342_M5StackS3 : public lgfx::LGFX_Device { static LGFX_ILI9342_M5StackS3 tft; -extern int getSignalQuality(int rssi); - //class name. Use something descriptive and leave the ": public Usermod" part :) class M5StackCoreS3DisplayUsermod : public Usermod { private: bool enabled = true; - bool displayTurnedOff = false; - long lastRedraw = 0; // needRedraw marks if redraw is required to prevent often redrawing. bool needRedraw = true; // Next variables hold the previous known values to determine if redraw is required. String knownSsid = ""; IPAddress knownIp; - long lastUpdate = 0; + unsigned long lastUpdate = 0; // GEQ display constants static constexpr uint8_t NUM_GEQ_BANDS = 16; @@ -102,19 +98,19 @@ class M5StackCoreS3DisplayUsermod : public Usermod { case 0: return 0xF800; // red case 1: return 0xFC00; // orange case 2: return 0xFC40; // amber - case 3: return 0xFCC0; // yellow-orange - case 4: return 0xFFE0; // yellow - case 5: return 0xC700; // yellow-green - case 6: return 0x07E0; // green - case 7: return 0x07EF; // green-cyan - case 8: return 0x07FF; // cyan - case 9: return 0x041F; // cyan-blue - case 10: return 0x001F; // blue - case 11: return 0x281F; // blue-indigo - case 12: return 0x601F; // indigo - case 13: return 0x781F; // violet - case 14: return 0xF81F; // magenta - case 15: return 0xF81F; // magenta + case 3: return 0xFFC0; // yellow + case 4: return 0x07E0; // green + case 5: return 0x07EF; // green-cyan + case 6: return 0x07FF; // cyan + case 7: return 0x001F; // blue + case 8: return 0x281F; // blue-indigo + case 9: return 0x481F; // indigo + case 10: return 0x701F; // violet + case 11: return 0x881F; // violet-magenta + case 12: return 0xF81F; // magenta + case 13: return 0xF81C; // magenta-red + case 14: return 0xF800; // red + case 15: return 0xF808; // red-orange default: return 0xF800; } } @@ -130,11 +126,13 @@ class M5StackCoreS3DisplayUsermod : public Usermod { { Serial.println("M5StackS3Display: starting setup"); - // Ensure Wire is initialized + // Ensure Wire is initialized (safe to call even if WLED already started it) if (i2c_sda >= 0 && i2c_scl >= 0) { Wire.begin(i2c_sda, i2c_scl); - Wire.setTimeout(50); + } else { + Wire.begin(); } + Wire.setTimeout(50); // AXP2101 - enable DLDO1 (VCC_BL per user) // Read current state first @@ -217,6 +215,19 @@ class M5StackCoreS3DisplayUsermod : public Usermod { Serial.println("M5StackS3Display: init TFT"); tft.init(); + // Lock SPI pins so they can't be reassigned to other usermods + PinManagerPinType pins[] = { + { (gpio_num_t)TFT_MOSI, true }, + { (gpio_num_t)TFT_SCLK, true }, + { (gpio_num_t)TFT_CS, true }, + { (gpio_num_t)TFT_DC, true } + }; + if (pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_Unspecified)) { + Serial.println("M5StackS3Display: SPI pins allocated"); + } else { + Serial.println("M5StackS3Display: SPI pin allocation FAILED"); + } + Serial.printf("M5StackS3Display: TFT width: %d, height: %d\n", tft.width(), tft.height()); // Dynamic header sizing @@ -243,59 +254,54 @@ class M5StackCoreS3DisplayUsermod : public Usermod { * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { - // Faster refresh for bouncing bars - if (millis() - lastUpdate < 100) // 50ms = 20fps - { + if (!enabled) return; + + unsigned long now = millis(); + + // Standard max framerate cap (~100 FPS, matches audio reactive update rate) + if (now - lastUpdate < 10) return; + + // Fallback: if LEDs are hogging CPU, only block if we refreshed recently enough + // Otherwise punch through to guarantee minimum 5 FPS on display + if (strip.isUpdating() && (now - lastUpdate < 200)) { return; } - lastUpdate = millis(); - // Turn off display after 5 minutes with no change. - if (!displayTurnedOff && millis() - lastRedraw > 5*60*1000) - { - displayTurnedOff = true; - } + lastUpdate = now; - // Check if values which are shown on display changed from the last time. - #if defined(ESP8266) + // Skip content/header updates while LEDs are being updated + if (!strip.isUpdating()) { + // Check if values which are shown on display changed from the last time. String currentSsid = apActive ? String(apSSID) : WiFi.SSID(); - #else - String currentSsid = WiFi.SSID(); - #endif - IPAddress currentIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - - if (currentSsid != knownSsid || currentIp != knownIp) - { - needRedraw = true; - knownSsid = currentSsid; - knownIp = currentIp; - } + IPAddress currentIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - if (displayTurnedOff) - { - displayTurnedOff = false; - } - lastRedraw = millis(); + if (currentSsid != knownSsid || currentIp != knownIp) + { + needRedraw = true; + knownSsid = currentSsid; + knownIp = currentIp; + } - // === HEADER: Only redraw when needed === - if (needRedraw) { - // Gray background - tft.fillRect(0, 0, tft.width(), HEADER_HEIGHT, TFT_DARKGREY); + // === HEADER: Only redraw when needed === + if (needRedraw) { + // Gray background + tft.fillRect(0, 0, tft.width(), HEADER_HEIGHT, TFT_DARKGREY); - // Text vertically centered in header - int16_t textY = (HEADER_HEIGHT - tft.fontHeight()) / 2; - tft.setTextSize(2); - tft.setTextColor(TFT_WHITE); + // Text vertically centered in header + int16_t textY = (HEADER_HEIGHT - tft.fontHeight()) / 2; + tft.setTextSize(2); + tft.setTextColor(TFT_WHITE); - // SSID on left - tft.setTextDatum(TL_DATUM); - tft.drawString(knownSsid.c_str(), 10, textY); + // SSID on left + tft.setTextDatum(TL_DATUM); + tft.drawString(knownSsid.c_str(), 10, textY); - // IP on right - tft.setTextDatum(TR_DATUM); - tft.drawString(knownIp.toString().c_str(), tft.width() - 10, textY); + // IP on right + tft.setTextDatum(TR_DATUM); + tft.drawString(knownIp.toString().c_str(), tft.width() - 10, textY); - needRedraw = false; + needRedraw = false; + } } // === 16 BOUNCING BARS - differential drawing === @@ -369,20 +375,12 @@ class M5StackCoreS3DisplayUsermod : public Usermod { void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject("M5StackS3Display"); - top["CS"] = TFT_CS; - top["DC"] = TFT_DC; - top["MOSI"] = TFT_MOSI; - top["SCLK"] = TFT_SCLK; top["enabled"] = enabled; } void appendConfigData() { oappend(SET_F("addHB('M5StackS3Display');")); - oappend(SET_F("addInfo('M5StackS3Display:CS',0,'SPI Chip Select (GPIO3)');")); - oappend(SET_F("addInfo('M5StackS3Display:DC',0,'Data/Command (GPIO35)');")); - oappend(SET_F("addInfo('M5StackS3Display:MOSI',0,'SPI MOSI (GPIO37)');")); - oappend(SET_F("addInfo('M5StackS3Display:SCLK',0,'SPI Clock (GPIO36)');")); } /* diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 9ef47be6b8..8015d58a06 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -491,7 +491,7 @@ void WLED::setup() #if ARDUINO_USB_CDC_ON_BOOT && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)) Serial.begin(115200); // WLEDMM avoid "hung devices" when USB_CDC is enabled; see https://github.com/espressif/arduino-esp32/issues/9043 - Serial.setTxTimeoutMs(1); // potential side-effect: incomplete debug output, with missing characters whenever TX buffer is full. + Serial.setTxTimeoutMs(0); // potential side-effect: incomplete debug output, with missing characters whenever TX buffer is full. #else Serial.begin(115200); #endif @@ -525,7 +525,7 @@ void WLED::setup() #if ARDUINO_USB_CDC_ON_BOOT || ARDUINO_USB_MODE #if ARDUINO_USB_CDC_ON_BOOT && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)) // WLEDMM avoid "hung devices" when USB_CDC is enabled; see https://github.com/espressif/arduino-esp32/issues/9043 - Serial.setTxTimeoutMs(1); + Serial.setTxTimeoutMs(0); #endif #if !defined(WLEDMM_NO_SERIAL_WAIT) || defined(WLED_DEBUG) if (!Serial) delay(2500); // WLEDMM: always allow CDC USB serial to initialise From b1feafacc6a272eaccc659217f87ca7206c79939 Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 20 May 2026 18:01:47 -0400 Subject: [PATCH 4/7] Docs update --- usermods/M5Stack_CoreS3_Display/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usermods/M5Stack_CoreS3_Display/readme.md b/usermods/M5Stack_CoreS3_Display/readme.md index 8121805c84..a8e1cfffed 100644 --- a/usermods/M5Stack_CoreS3_Display/readme.md +++ b/usermods/M5Stack_CoreS3_Display/readme.md @@ -2,6 +2,8 @@ Display usermod for the ILI9342C 320x240 TFT display on the M5Stack Core S3, using LovyanGFX. +**NOTE:** The M5Stack Core S3 has 8MB of PSRAM, but it's uncommonly **QSPI PSRAM**. This was likely a design tradeoff to free up more pins for add-on modules over using octal PSRAM which blocks off more pins. + ## Pin Mapping (M5Stack Core S3) | ESP32-S3 | ILI9342C | Description | From 208c1e9bb7a3b510b930873d46372bed69daea38 Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 20 May 2026 19:17:50 -0400 Subject: [PATCH 5/7] Rabbit Droppings --- pio-scripts/enable_lto_s3.py | 15 +++++++++---- usermods/M5Stack_CoreS3_Display/readme.md | 8 ++++--- .../usermod_m5stack_s3_display.h | 22 +++++++++++-------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/pio-scripts/enable_lto_s3.py b/pio-scripts/enable_lto_s3.py index e15256fe7e..aff977e865 100644 --- a/pio-scripts/enable_lto_s3.py +++ b/pio-scripts/enable_lto_s3.py @@ -44,10 +44,17 @@ def enable_lto(env): # library archives carry IR that the linker can optimise across. cc = str(env.get("CC", "")) if cc: - toolchain_prefix = cc.replace("gcc", "").replace("g++", "") - # toolchain_prefix is something like "xtensa-esp32s3-elf-" - new_ar = toolchain_prefix + "gcc-ar" - new_ranlib = toolchain_prefix + "gcc-ranlib" + cc_basename = os.path.basename(cc) + # Strip trailing "gcc" or "g++" (with optional .exe suffix) from basename only + if cc_basename.endswith(".exe"): + cc_basename = cc_basename[:-4] + if cc_basename.endswith("gcc"): + cc_basename = cc_basename[:-3] + elif cc_basename.endswith("g++"): + cc_basename = cc_basename[:-3] + # cc_basename is now something like "xtensa-esp32s3-elf" + new_ar = cc_basename + "-gcc-ar" + new_ranlib = cc_basename + "-gcc-ranlib" # Resolve CC to its real path so we can search the same directory cc_resolved = shutil.which(cc) or cc diff --git a/usermods/M5Stack_CoreS3_Display/readme.md b/usermods/M5Stack_CoreS3_Display/readme.md index a8e1cfffed..edc0c2b228 100644 --- a/usermods/M5Stack_CoreS3_Display/readme.md +++ b/usermods/M5Stack_CoreS3_Display/readme.md @@ -19,7 +19,8 @@ Reset is controlled via the AW9523B GPIO expander (P1_1). Backlight is powered v In `platformio_override.ini` for your M5Stack Core S3 environment: -```build_flags = +```ini +build_flags = -D USERMOD_M5STACK_CORE_S3_DISPLAY ;; For the M5Stack ModuleAudio: -D SR_ENABLE_DEFAULT @@ -32,7 +33,8 @@ In `platformio_override.ini` for your M5Stack Core S3 environment: -D HW_SCL_PIN=11 lib_deps = - https://github.com/lovyan03/LovyanGFX``` + https://github.com/lovyan03/LovyanGFX +``` ## Features @@ -48,7 +50,7 @@ lib_deps = - Uses LovyanGFX with `SPI3_HOST` (HSPI) - Native landscape 320x240 resolution -- BGR color order, display inversion enabled +- RGB color order, display inversion enabled - Backlight always on (controlled by AXP2101 DLDO1) ## TroyHacks Recommended AudioReactive Settings diff --git a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h index 8f4ddb6d5c..0f30d9b4fb 100644 --- a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h +++ b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h @@ -77,6 +77,7 @@ static LGFX_ILI9342_M5StackS3 tft; class M5StackCoreS3DisplayUsermod : public Usermod { private: bool enabled = true; + bool _initOk = false; // needRedraw marks if redraw is required to prevent often redrawing. bool needRedraw = true; @@ -140,7 +141,7 @@ class M5StackCoreS3DisplayUsermod : public Usermod { Wire.write(0x90); Wire.endTransmission(false); Wire.requestFrom((uint8_t)AXP2101_ADDR, (uint8_t)1); - byte pwr_orig = Wire.read(); + byte pwr_orig = Wire.available() ? Wire.read() : 0x00; Serial.printf("M5StackS3Display: 0x90 orig: 0x%02X\n", pwr_orig); // Set DLDO1 voltage to 3.3V (voltage reg 0x99) @@ -155,7 +156,7 @@ class M5StackCoreS3DisplayUsermod : public Usermod { Wire.write(0x99); Wire.endTransmission(false); Wire.requestFrom((uint8_t)AXP2101_ADDR, (uint8_t)1); - byte dldo1_v = Wire.read(); + byte dldo1_v = Wire.available() ? Wire.read() : 0x00; Serial.printf("M5StackS3Display: DLDO1 0x99: 0x%02X\n", dldo1_v); // Enable DLDO1 via bit 7 of 0x90 @@ -172,7 +173,7 @@ class M5StackCoreS3DisplayUsermod : public Usermod { Wire.write(0x90); Wire.endTransmission(false); Wire.requestFrom((uint8_t)AXP2101_ADDR, (uint8_t)1); - byte pwr_after = Wire.read(); + byte pwr_after = Wire.available() ? Wire.read() : 0x00; Serial.printf("M5StackS3Display: 0x90 after: 0x%02X\n", pwr_after); // AW9523 GPIO expander for LCD reset @@ -209,7 +210,7 @@ class M5StackCoreS3DisplayUsermod : public Usermod { Wire.write(0x03); // P1 output register Wire.endTransmission(false); Wire.requestFrom((uint8_t)AW9523B_ADDR, (uint8_t)1); - byte p1_state = Wire.read(); + byte p1_state = Wire.available() ? Wire.read() : 0x00; Serial.printf("M5StackS3Display: AW9523B P1 state: 0x%02X\n", p1_state); Serial.println("M5StackS3Display: init TFT"); @@ -222,11 +223,13 @@ class M5StackCoreS3DisplayUsermod : public Usermod { { (gpio_num_t)TFT_CS, true }, { (gpio_num_t)TFT_DC, true } }; - if (pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_Unspecified)) { - Serial.println("M5StackS3Display: SPI pins allocated"); - } else { - Serial.println("M5StackS3Display: SPI pin allocation FAILED"); + if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_Unspecified)) { + Serial.println("M5StackS3Display: SPI pin allocation FAILED — disabling usermod"); + enabled = false; + _initOk = false; + return; } + Serial.println("M5StackS3Display: SPI pins allocated"); Serial.printf("M5StackS3Display: TFT width: %d, height: %d\n", tft.width(), tft.height()); @@ -248,13 +251,14 @@ class M5StackCoreS3DisplayUsermod : public Usermod { tft.drawString("Loading...", tft.width() - 10, textY); Serial.println("M5StackS3Display: setup complete"); + _initOk = true; } /* * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { - if (!enabled) return; + if (!enabled || !_initOk) return; unsigned long now = millis(); From 365a7b1cee2cbe2a19d0000cc5b604e579b32a5d Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 20 May 2026 19:25:05 -0400 Subject: [PATCH 6/7] Fixes to LTO script, not really related to the overall PR --- pio-scripts/enable_lto_s3.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pio-scripts/enable_lto_s3.py b/pio-scripts/enable_lto_s3.py index aff977e865..b373e8ed68 100644 --- a/pio-scripts/enable_lto_s3.py +++ b/pio-scripts/enable_lto_s3.py @@ -48,10 +48,11 @@ def enable_lto(env): # Strip trailing "gcc" or "g++" (with optional .exe suffix) from basename only if cc_basename.endswith(".exe"): cc_basename = cc_basename[:-4] - if cc_basename.endswith("gcc"): - cc_basename = cc_basename[:-3] - elif cc_basename.endswith("g++"): - cc_basename = cc_basename[:-3] + # Strip trailing "-gcc" or "-g++" including the dash + if cc_basename.endswith("-gcc"): + cc_basename = cc_basename[:-4] + elif cc_basename.endswith("-g++"): + cc_basename = cc_basename[:-4] # cc_basename is now something like "xtensa-esp32s3-elf" new_ar = cc_basename + "-gcc-ar" new_ranlib = cc_basename + "-gcc-ranlib" From b9ea1b1e8694a40d6cbebecf1db5abac29d45e1f Mon Sep 17 00:00:00 2001 From: Troy <5659019+troyhacks@users.noreply.github.com> Date: Wed, 20 May 2026 19:42:50 -0400 Subject: [PATCH 7/7] null fixes --- usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h index 0f30d9b4fb..60b7765b6c 100644 --- a/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h +++ b/usermods/M5Stack_CoreS3_Display/usermod_m5stack_s3_display.h @@ -313,7 +313,9 @@ class M5StackCoreS3DisplayUsermod : public Usermod { // Get audio data if available um_data_t *um_data = nullptr; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE) + && um_data != nullptr + && um_data->u_data[2] != nullptr) { // Real audio data memcpy(geq, um_data->u_data[2], NUM_GEQ_BANDS); } else {