diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f72540a54e..70aedcb4e7 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1162,24 +1162,65 @@ void WS2812FX::finalizeInit() { unsigned digitalCount = 0; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) - unsigned maxLedsOnBus = 0; - unsigned busType = 0; + // Determine if I2S/LCD should be used and whether parallel mode is possible + // Count I2S buses and check if they meet requirements + unsigned i2sBusCount = 0; + unsigned firstI2SBusType = 0; + unsigned maxI2SLedsOnBus = 0; + bool mixedI2SBusTypes = false; + for (const auto &bus : busConfigs) { if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { digitalCount++; - if (busType == 0) busType = bus.type; // remember first bus type - if (busType != bus.type) { - DEBUG_PRINTF_P(PSTR("Mixed digital bus types detected! Forcing single I2S output.\n")); - useParallelI2S = false; // mixed bus types, no parallel I2S + // Check if this bus will use I2S driver (driverType == 1) + if (bus.driverType == 1) { + i2sBusCount++; + if (firstI2SBusType == 0) firstI2SBusType = bus.type; // remember first I2S bus type + if (firstI2SBusType != bus.type) { + mixedI2SBusTypes = true; + } + if (bus.count > maxI2SLedsOnBus) maxI2SLedsOnBus = bus.count; + } + } + } + DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u, Max LEDs on I2S bus: %u\n"), digitalCount, i2sBusCount, maxI2SLedsOnBus); + + // Determine I2S usage automatically based on bus configuration + bool useI2S = (i2sBusCount > 0); // Use I2S if any buses have driverType == 1 + + // Determine parallel vs single I2S usage + bool useParallelI2S = false; + if (useI2S && i2sBusCount > 0) { + // Parallel I2S requirements: + // - All I2S buses must be same LED type + // - If multiple I2S buses OR ESP32-S3: all I2S buses must have ≤600 LEDs + // - Single I2S bus has no LED count restriction + bool ledCountValid = true; + #if defined(CONFIG_IDF_TARGET_ESP32S3) + // S3: all I2S buses (LCD driver) must have ≤600 LEDs + if (maxI2SLedsOnBus > 600) ledCountValid = false; + #else + // ESP32/S2: only restrict if multiple I2S buses + if (i2sBusCount > 1 && maxI2SLedsOnBus > 600) ledCountValid = false; + #endif + + if (!mixedI2SBusTypes && ledCountValid) { + useParallelI2S = true; + DEBUG_PRINTF_P(PSTR("Using parallel I2S/LCD output.\n")); + } else { + if (mixedI2SBusTypes) { + DEBUG_PRINTF_P(PSTR("Using single I2S output (mixed I2S bus types).\n")); + } else { + DEBUG_PRINTF_P(PSTR("Using single I2S output (I2S bus >600 LEDs).\n")); } - if (bus.count > maxLedsOnBus) maxLedsOnBus = bus.count; } } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); - // we may remove 600 LEDs per bus limit when NeoPixelBus is updated beyond 2.8.3 - if (maxLedsOnBus <= 600 && useParallelI2S) BusManager::useParallelOutput(); // must call before creating buses - else useParallelI2S = false; // enforce single I2S + + // Set the flags in PolyBus via BusManager + BusManager::useI2SOutput(useI2S); + if (useParallelI2S) { + BusManager::useParallelOutput(); // This sets parallel I2S flag - must call before creating buses + } digitalCount = 0; #endif @@ -1190,12 +1231,25 @@ void WS2812FX::finalizeInit() { for (const auto &bus : busConfigs) { unsigned memB = bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // does not include DMA/RMT buffer mem += memB; - // estimate maximum I2S memory usage (only relevant for digital non-2pin busses) + // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) - #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) - const bool usesI2S = ((useParallelI2S && digitalCount <= 8) || (!useParallelI2S && digitalCount == 1)); - #elif defined(CONFIG_IDF_TARGET_ESP32S2) - const bool usesI2S = (useParallelI2S && digitalCount <= 8); + #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) + bool usesI2S = false; + if (BusManager::hasI2SOutput()) { + if (BusManager::hasParallelOutput()) { + // Parallel I2S: first 8 buses use I2S + usesI2S = (digitalCount <= 8); + } else { + // Single I2S: only the last bus uses I2S + #if defined(CONFIG_IDF_TARGET_ESP32) + usesI2S = (digitalCount == 9); // bus 8 (9th bus, 0-indexed) + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + usesI2S = (digitalCount == 5); // bus 4 (5th bus, 0-indexed) + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + usesI2S = false; // S3 doesn't support single I2S + #endif + } + } #else const bool usesI2S = false; #endif @@ -1206,6 +1260,8 @@ void WS2812FX::finalizeInit() { constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit) #endif unsigned i2sCommonSize = stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1); + // Parallel I2S uses 8 channels, requiring 8x the DMA buffer size + if (BusManager::hasParallelOutput()) i2sCommonSize *= 8; if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize; } #endif diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 5cc0eb2c95..d2a84321f0 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -27,7 +27,7 @@ extern char cmDNS[]; extern bool cctICused; -extern bool useParallelI2S; +extern bool useI2S; // functions to get/set bits in an array - based on functions created by Brandon for GOL // toDo : make this a class that's completely defined in a header file @@ -123,6 +123,7 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) , _colorOrder(bc.colorOrder) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) +, _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } @@ -139,7 +140,9 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) _pins[1] = bc.pins[1]; _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined } - _iType = PolyBus::getI(bc.type, _pins, nr); + // Reuse the iType that was determined during memory estimation (memUsage) + // This avoids calling getI() twice which would double-count channels + _iType = bc.iType; if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); @@ -1111,7 +1114,10 @@ size_t BusConfig::memUsage(unsigned nr) const { return sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); } else if (Bus::isDigital(type)) { // if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here - return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr)); + // Call getI() to determine bus type and allocate channel (this is the single call) + // Store the result in iType for later reuse during bus creation + const_cast(this)->iType = PolyBus::getI(type, pins, nr, driverType); + return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType); } else if (Bus::isOnOff(type)) { return sizeof(BusOnOff); } else { @@ -1213,6 +1219,14 @@ bool BusManager::hasParallelOutput() { return PolyBus::isParallelI2S1Output(); } +void BusManager::useI2SOutput(bool enable) { + PolyBus::setI2SOutput(enable); +} + +bool BusManager::hasI2SOutput() { + return PolyBus::isI2SOutput(); +} + //do not call this method from system context (network callback) void BusManager::removeAll() { DEBUGBUS_PRINTLN(F("Removing all.")); @@ -1220,6 +1234,8 @@ void BusManager::removeAll() { while (!canAllShow()) yield(); busses.clear(); PolyBus::setParallelI2S1Output(false); + // Reset channel tracking for fresh allocation + PolyBus::resetChannelTracking(); } #ifdef ESP32_DATA_IDLE_HIGH @@ -1423,6 +1439,7 @@ ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } bool PolyBus::_useParallelI2S = false; +bool PolyBus::_useI2S = false; // Bus static member definition int16_t Bus::_cct = -1; @@ -1431,6 +1448,10 @@ uint8_t Bus::_gAWM = 255; uint16_t BusDigital::_milliAmpsTotal = 0; +// PolyBus channel tracking for dynamic allocation +uint8_t PolyBus::_rmtChannelsUsed = 0; +uint8_t PolyBus::_i2sChannelsUsed = 0; + std::vector> BusManager::busses; uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 87d39fe34b..4d77f14a0e 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -140,6 +140,7 @@ class Bus { virtual uint16_t getLEDCurrent() const { return 0; } virtual uint16_t getUsedCurrent() const { return 0; } virtual uint16_t getMaxCurrent() const { return 0; } + virtual uint8_t getDriverType() const { return 0; } // Default to RMT (0) for non-digital buses virtual size_t getBusSize() const { return sizeof(Bus); } virtual const String getCustomText() const { return String(); } @@ -258,6 +259,7 @@ class BusDigital : public Bus { uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + uint8_t getDriverType() const override { return _driverType; } void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; } void estimateCurrent(); // estimate used current from summed colors void applyBriLimit(uint8_t newBri); @@ -272,6 +274,7 @@ class BusDigital : public Bus { uint8_t _colorOrder; uint8_t _pins[2]; uint8_t _iType; + uint8_t _driverType; // 0=RMT (default), 1=I2S uint16_t _frequencykHz; uint16_t _milliAmpsMax; uint8_t _milliAmpsPerLed; @@ -421,9 +424,11 @@ struct BusConfig { uint16_t frequency; uint8_t milliAmpsPerLed; uint16_t milliAmpsMax; + uint8_t driverType; // 0=RMT (default), 1=I2S + uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation String text; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, String sometext = "") + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "") : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) @@ -433,13 +438,15 @@ struct BusConfig { , frequency(clock_kHz) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) + , driverType(driver) + , iType(I_NONE) // Initialize to I_NONE, will be set during memory estimation , text(sometext) { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) size_t nPins = Bus::getNumberOfPins(type); for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; - DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"), + DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%d)\n"), (int)start, (int)(start+len), (int)type, (int)colorOrder, @@ -447,7 +454,8 @@ struct BusConfig { (int)skipAmount, (int)autoWhite, (int)frequency, - (int)milliAmpsPerLed, (int)milliAmpsMax + (int)milliAmpsPerLed, (int)milliAmpsMax, + (int)driverType ); } @@ -504,6 +512,8 @@ namespace BusManager { void useParallelOutput(); // workaround for inaccessible PolyBus bool hasParallelOutput(); // workaround for inaccessible PolyBus + void useI2SOutput(bool enable); // set I2S/LCD usage flag + bool hasI2SOutput(); // check I2S/LCD usage flag //do not call this method from system context (network callback) void removeAll(); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index b2ff947418..e7a58e0716 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -339,11 +339,14 @@ //handles pointer type conversion for all possible bus types class PolyBus { private: - static bool _useParallelI2S; + static bool _useParallelI2S; // use parallel I2S/LCD (8 channels) + static bool _useI2S; // use I2S/LCD at all (could be parallel or single) public: static inline void setParallelI2S1Output(bool b = true) { _useParallelI2S = b; } static inline bool isParallelI2S1Output(void) { return _useParallelI2S; } + static inline void setI2SOutput(bool b = true) { _useI2S = b; } + static inline bool isI2SOutput(void) { return _useI2S; } // initialize SPI bus speed for DotStar methods template @@ -481,16 +484,11 @@ class PolyBus { #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) if (_useParallelI2S && (channel >= 8)) { - // Parallel I2S channels are to be used first, so subtract 8 to get the RMT channel number + // I2S/LCD channels are to be used first, so subtract 8 to get the RMT channel number channel -= 8; } #endif - #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) - // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation - if (!_useParallelI2S && channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 - #endif - void* busPtr = nullptr; switch (busType) { case I_NONE: break; @@ -1283,8 +1281,19 @@ class PolyBus { return size; } + // Channel tracking for dynamic allocation + static uint8_t _rmtChannelsUsed; + static uint8_t _i2sChannelsUsed; + + // Reset channel tracking (call before adding buses) + static void resetChannelTracking() { + _rmtChannelsUsed = 0; + _i2sChannelsUsed = 0; + } + //gives back the internal type index (I_XX_XXX_X above) for the input - static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t num = 0) { + // driverPreference: 0=RMT (default), 1=I2S/LCD + static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t num = 0, uint8_t driverPreference = 0) { if (!Bus::isDigital(busType)) return I_NONE; if (Bus::is2Pin(busType)) { //SPI LED chips bool isHSPI = false; @@ -1340,39 +1349,43 @@ class PolyBus { return I_8266_U0_SM16825_5 + offset; } #else //ESP32 - uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S1 [I2S0 is used by Audioreactive] + // Dynamic channel allocation based on driver preference + // Get platform-specific max channels #if defined(CONFIG_IDF_TARGET_ESP32S2) - // ESP32-S2 only has 4 RMT channels - if (_useParallelI2S) { - if (num > 11) return I_NONE; - if (num < 8) offset = 1; // use x8 parallel I2S0 channels followed by RMT - // Note: conflicts with AudioReactive if enabled - } else { - if (num > 4) return I_NONE; - if (num > 3) offset = 1; // only one I2S0 (use last to allow Audioreactive) - } + const uint8_t maxRMT = 4; + const uint8_t maxI2S = _useI2S ? (_useParallelI2S ? 8 : 1) : 0; #elif defined(CONFIG_IDF_TARGET_ESP32C3) - // On ESP32-C3 only the first 2 RMT channels are usable for transmitting - if (num > 1) return I_NONE; - //if (num > 1) offset = 1; // I2S not supported yet (only 1 I2S) + const uint8_t maxRMT = 2; + const uint8_t maxI2S = 0; // I2S not supported on C3 #elif defined(CONFIG_IDF_TARGET_ESP32S3) - // On ESP32-S3 only the first 4 RMT channels are usable for transmitting - if (_useParallelI2S) { - if (num > 11) return I_NONE; - if (num < 8) offset = 1; // use x8 parallel I2S LCD channels, followed by RMT - } else { - if (num > 3) return I_NONE; // do not use single I2S (as it is not supported) - } + const uint8_t maxRMT = 4; + const uint8_t maxI2S = _useI2S ? (_useParallelI2S ? 8 : 0) : 0; // LCD only, no single I2S #else - // standard ESP32 has 8 RMT and x1/x8 I2S1 channels - if (_useParallelI2S) { - if (num > 15) return I_NONE; - if (num < 8) offset = 1; // 8 I2S followed by 8 RMT + // Standard ESP32 + const uint8_t maxRMT = 8; + const uint8_t maxI2S = _useI2S ? (_useParallelI2S ? 8 : 1) : 0; + #endif + + // Determine which driver to use based on preference and availability + uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD + + if (driverPreference == 1 && _i2sChannelsUsed < maxI2S) { + // User wants I2S and we have I2S channels available + offset = 1; + _i2sChannelsUsed++; + } else if (_rmtChannelsUsed < maxRMT) { + // Use RMT (either user wants RMT, or I2S unavailable, or fallback) + _rmtChannelsUsed++; + } else if (_i2sChannelsUsed < maxI2S) { + // RMT full, fallback to I2S if available + offset = 1; + _i2sChannelsUsed++; } else { - if (num > 9) return I_NONE; - if (num == 0) offset = 1; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + // No channels available + return I_NONE; } - #endif + + // Now determine actual bus type with the chosen offset switch (busType) { case TYPE_WS2812_1CH_X3: case TYPE_WS2812_2CH_X3: diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index e30be759b6..082e8e8520 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -166,7 +166,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { Bus::setCCTBlend(cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - CJSON(useParallelI2S, hw_led[F("prl")]); + // useI2S no longer loaded from config - determined automatically based on bus configuration + // CJSON(useI2S, hw_led[F("prl")]); // Removed - PR checkbox eliminated #endif #ifndef WLED_DISABLE_2D @@ -234,9 +235,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { maMax = 0; } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh + uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S String host = elm[F("text")] | String(); - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, host); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -319,7 +321,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { unsigned start = 0; // analog always has length 1 if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; - busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0); + busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default) doInitBusses = true; // finalization done in beginStrip() } } @@ -984,6 +986,8 @@ void serializeConfig(JsonObject root) { ins[F("freq")] = bus->getFrequency(); ins[F("maxpwr")] = bus->getMaxCurrent(); ins[F("ledma")] = bus->getLEDCurrent(); + // Save driver preference directly from bus object (busConfigs is cleared after bus creation) + ins[F("drv")] = bus->getDriverType(); ins[F("text")] = bus->getCustomText(); } diff --git a/wled00/const.h b/wled00/const.h index 6d1825d574..4c2669a0ab 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -63,21 +63,22 @@ constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_C #endif #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_DIGITAL_CHANNELS 2 + #define WLED_MAX_DIGITAL_CHANNELS 2 // x2 RMT only (I2S not supported by NPB) //#define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) - #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x1/x8 I2S0 + // I2S is only used when explicitly enabled by user (Enable I2S checkbox) + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT (default), or x4 RMT + x8 I2S0 (when I2S enabled) //#define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 - #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD + // LCD driver is only used when explicitly enabled by user (Enable I2S checkbox) + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT (default), or x4 RMT + x8 LCD (when I2S enabled) //#define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI #else - // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning - #define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT + // RMT is used by default; I2S is only used when explicitly enabled by user (Enable I2S checkbox) + #define WLED_MAX_DIGITAL_CHANNELS 16 // x8 RMT (default), or x8 RMT + x8 I2S1 (when I2S enabled) //#define WLED_MAX_ANALOG_CHANNELS 16 #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI #endif diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index f1ed130f2e..806a6dfc81 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -49,10 +49,11 @@ d.Sf.addEventListener("submit", trySubmit); if (d.um_p[0]==-1) d.um_p.shift(); pinDropdowns(); + fixLegacyDriverConfig(); // Handle legacy configs without driver selection }); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/leds'); } - function bLimits(b,v,p,m,l,o=5,d=2,a=6,n=4) { + function bLimits(b,v,p,m,l,o=5,d=2,a=6,n=4,rmt=0,i2s=0) { maxB = b; // maxB - max physical (analog + digital) buses: 32 - ESP32, 14 - S3/S2, 6 - C3, 4 - 8266 maxV = v; // maxV - min virtual buses: 6 - ESP32/S3, 4 - S2/C3, 3 - ESP8266 (only used to distinguish S2/S3) maxPB = p; // maxPB - max LEDs per bus @@ -62,6 +63,8 @@ maxD = d; // maxD - max digital channels (can be changed if using ESP32 parallel I2S): 16 - ESP32, 12 - S3/S2, 2 - C3, 3 - 8266 maxA = a; // maxA - max analog channels: 16 - ESP32, 8 - S3/S2, 6 - C3, 5 - 8266 maxBT = n; // maxBT - max buttons + maxRMT = rmt; // maxRMT - max RMT channels: 8 - ESP32, 4 - S2/S3, 2 - C3, 0 - 8266 + maxI2S = i2s; // maxI2S - max I2S/LCD channels: 8 - ESP32/S2/S3, 0 - C3/8266 } function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h @@ -233,13 +236,13 @@ if (is8266() && d.Sf["L0"+n].value == 3) { //8266 DMA uses 5x the mem mul = 5; } - let parallelI2S = d.Sf.PR.checked && (is32() || isS2() || isS3()) && !isD2P(t); - if (isC3() || (isS3() && !parallelI2S)) { - mul = 2; // ESP32 RMT uses double buffer - } else if ((is32() || isS2() || isS3()) && toNum(n) > (parallelI2S ? 7 : 0)) { - mul = 2; // ESP32 RMT uses double buffer - } else if ((parallelI2S && toNum(n) < 8) || (n == 0 && is32())) { // I2S uses extra DMA buffer - dbl = len * ch * 3; // DMA buffer for parallel I2S (TODO: ony the bus with largst LED count should be used) + let enabledI2S = (is32() || isS2() || isS3()) && !isD2P(t); // I2S always available (not 2-pin LED) + if (isC3() || !enabledI2S) { + mul = 2; // RMT uses double buffer + } else if (enabledI2S && toNum(n) < 8) { // I2S/LCD uses extra DMA buffer + dbl = len * ch * 3; // DMA buffer for I2S/LCD (TODO: only the bus with largest LED count should be used) + // Parallel I2S uses 8 channels, requiring 8x the DMA buffer size + if (d.Sf.PI && d.Sf.PI.checked) dbl *= 8; } } return len * ch * mul + dbl + pbfr; @@ -294,14 +297,12 @@ // enable/disable LED fields updateTypeDropdowns(); // restrict bus types in dropdowns to max allowed digital/analog buses - let dC = 0; // count of digital buses (for parallel I2S) let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]"); LTs.forEach((s,i)=>{ // is the field a LED type? var n = s.name.substring(2,3); // bus number (0-Z) var t = parseInt(s.value); memu += getMem(t, n); // calc memory - dC += (isDig(t) && !isD2P(t)); setPinConfig(n,t); gId("abl"+n).style.display = (!abl || !isDig(t)) ? "none" : "inline"; // show/hide individual ABL settings if (change) { // did we change LED type? @@ -340,9 +341,8 @@ let nm = LC.name.substring(0,2); // field name : /L./ let n = LC.name.substring(2,3); // bus number (0-Z) let t = parseInt(d.Sf["LT"+n].value); // LED type SELECT - if (isDig(t)) { + if (isDig(t) && !isD2P(t)) { if (sameType == 0) sameType = t; // first bus type - else if (sameType != t) sameType = -1; // different bus type } // do we have a led count field if (nm=="LC") { @@ -421,12 +421,126 @@ } else LC.style.color = "#fff"; }); if (is32() || isS2() || isS3()) { - if (maxLC > 600 || dC < 2 || sameType <= 0) { - d.Sf["PR"].checked = false; - gId("prl").classList.add("hide"); - } else - gId("prl").classList.remove("hide"); - } else d.Sf["PR"].checked = false; + // Always show show I2S checkbox on ESP32/S2/S3 + gId("prl").classList.remove("hide"); + } else { + d.Sf["PR"].checked = false; + } + // Calculate max single-pin digital bus count based on configuration: + // - If I2S is enabled and ≤600 LEDs/bus: parallel I2S (8 I2S + RMT) + // - If I2S is enabled but >600 LEDs/bus: single I2S (RMT + 1 I2S) + // - If I2S is disabled: RMT only + let maxDigitalBusCount; + if (d.Sf["PR"].checked) { + if (maxLC <= 600) { + // Parallel I2S mode possible + maxDigitalBusCount = maxD; // ESP32: 16, S2: 12, S3: 12 + } else { + // Single I2S mode + // Uses maxD-7 to match bus_wrapper.h logic (ESP32: 16-7=9, S2: 12-7=5) + if (isS3()) maxDigitalBusCount = 4; // S3 doesn't support single I2S, only RMT + else maxDigitalBusCount = maxD - 7; + } + } else { + // RMT only + maxDigitalBusCount = (isC3() ? 2 : (isS2() || isS3()) ? 4 : is32() ? 8 : maxD); + } + + // Mark invalid buses and update driver info + track channel usage + let invalidBusCount = 0; + let digitalBusIndex = 0; + let maxRMT = (is32() ? 8 : (isS2() || isS3()) ? 4 : isC3() ? 2 : 0); + let maxI2S = (is32() ? 8 : (isS2() || isS3()) ? 8 : isS3() ? 8 : 0); + + // Use helper function to calculate channel usage + let usage = calculateChannelUsage(); + let rmtUsed = usage.rmtUsed; + let i2sUsed = usage.i2sUsed; + let firstI2SType = usage.firstI2SType; + let maxLEDsOnI2SBus = usage.maxLEDsOnI2SBus; + let i2sBusesOver600 = usage.i2sBusesOver600; + + d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{ + let n = s.name.substring(2,3); + let t = parseInt(s.value); + let drv = gId("drv"+n); // bus driver info (I2S/RMT) + let drvsel = gId("drvsel"+n); // driver selection dropdown + if (drv) drv.textContent = ""; // reset + if (drvsel) { + drvsel.style.display = "none"; // hide by default + drvsel.style.color = "#fff"; // reset color + } + s.style.color = "#fff"; // reset + + if (isDig(t) && !isD2P(t)) { + digitalBusIndex++; + let driverPref = d.Sf["LD"+n] ? parseInt(d.Sf["LD"+n].value || 0) : 0; + let ledCount = parseInt(d.Sf["LC"+n].value) || 0; + + // Update I2S/RMT driver info/dropdown for ESP32 digital buses + if (!is8266()) { + let useI2S = (is32() || isS2() || isS3()); // I2S always available on ESP32 variants + + // Show driver selection dropdown when I2S is available + if (useI2S && drvsel) { + drvsel.style.display = "inline"; + // Set default value if not already set (backward compatibility) + if (!d.Sf["LD"+n].value) { + d.Sf["LD"+n].value = "0"; // default to RMT + } + + // Mark driver selection red if this I2S bus violates parallel I2S rules + if (driverPref === 1) { + // This bus uses I2S - check if it violates rules + if (i2sBusesOver600 > 0 && i2sUsed > 1) { + // Multiple I2S buses and at least one is >600 LEDs - invalid + drvsel.style.color = "red"; + } else if (firstI2SType !== null && t !== firstI2SType) { + // I2S buses must all be same type + drvsel.style.color = "red"; + } + } + } else if (drv) { + // Show info text when I2S is disabled + drv.textContent = " (RMT)"; + } + + // Lock PR checkbox if disabling would result in invalid config + if (useI2S) { + let rmtCount = is32() ? 8 : (isS2() || isS3()) ? 4 : 0; + if (maxLEDsOnI2SBus <= 600) { + // Parallel I2S mode would be used + if (digitalBusIndex > rmtCount) d.Sf.PR.disabled = true; + } + } + } + } + }); + updateTypeDropdowns(); // update type dropdowns to disable unavailable digital/analog types (I2S/RMT bus count may have changed) + + // Show channel usage warning + let channelWarning = gId('channelwarning'); + let channelMsg = gId('channelmsg'); + if (channelWarning && channelMsg && !is8266()) { + if (rmtUsed > maxRMT || i2sUsed > maxI2S) { + channelWarning.style.display = 'inline'; + channelWarning.style.color = 'red'; + let msg = ''; + if (rmtUsed > maxRMT) msg += `RMT: ${rmtUsed}/${maxRMT}`; + if (i2sUsed > maxI2S) { + if (msg) msg += ', '; + msg += `I2S: ${i2sUsed}/${maxI2S}`; + } + channelMsg.textContent = 'Channel limit exceeded! ' + msg; + } else if ((is32() || isS2() || isS3()) && (rmtUsed >= maxRMT - 1 || i2sUsed >= maxI2S - 1)) { + channelWarning.style.display = 'inline'; + channelWarning.style.color = 'orange'; + channelMsg.textContent = `Channel usage: RMT ${rmtUsed}/${maxRMT}, I2S ${i2sUsed}/${maxI2S}`; + } else { + channelWarning.style.display = 'none'; + } + } + // distribute ABL current if not using PPL enPPL(sDI); @@ -462,6 +576,7 @@ gId('fpsWarn').style.display = (d.Sf.FR.value == 0) || (d.Sf.FR.value >= 80) ? 'block':'none'; gId('fpsHigh').style.display = (d.Sf.FR.value >= 80) ? 'block':'none'; } + function lastEnd(i) { if (i-- < 1) return 0; var s = chrID(i); @@ -470,6 +585,7 @@ if (isPWM(t)) v = 1; //PWM busses return isNaN(v) ? 0 : v; } + function addLEDs(n,init=true) { var o = gEBCN("iST"); @@ -511,13 +627,20 @@
Start:   -
Length:

+
Length:

GPIO: + +
Host: .local

Reversed:

Skip first LEDs:
@@ -540,11 +663,38 @@ } }); enLA(d.Sf["LAsel"+s],s); // update LED mA + // Check channel availability before selecting default type + let usage = calculateChannelUsage(); + let maxRMT = (is32() ? 8 : (isS2() || isS3()) ? 4 : isC3() ? 2 : 0); + let maxI2S = (is32() ? 8 : (isS2() || isS3()) ? 8 : 0); + let rmtFull = (usage.rmtUsed >= maxRMT); + let i2sFull = (usage.i2sUsed >= maxI2S); + + // Set driver preference based on channel availability + let drvSelect = d.Sf["LD"+s]; + if (drvSelect) { + if (rmtFull && !i2sFull && (is32() || isS2() || isS3())) { + // RMT is full but I2S available - default to I2S + drvSelect.value = "1"; + } else { + // Default to RMT (backward compatible) + drvSelect.value = "0"; + } + } + // temporarily set to virtual (network) type to avoid "same type" exception during dropdown update let sel = d.getElementsByName("LT"+s)[0]; sel.value = sel.querySelector('option[data-type="N"]').value; updateTypeDropdowns(); // update valid bus options including this new one - sel.selectedIndex = sel.querySelector('option:not(:disabled)').index; + + // Select first non-disabled option + let firstEnabled = sel.querySelector('option:not(:disabled)'); + if (firstEnabled) { + sel.value = firstEnabled.value; + } else { + // All digital types disabled - keep as network type + sel.value = sel.querySelector('option[data-type="N"]').value; + } updateTypeDropdowns(); // update again for the newly selected type } if (n==-1) { @@ -665,6 +815,67 @@ gId("si").checked = cs; tglSi(cs); } + function fixLegacyDriverConfig() { //on load, handle legacy configs without driver type (LD) field + // Check if this is a legacy config by seeing if all LD fields are unset or all RMT + if (!is32() && !isS2() && !isS3()) return; // Only applies to ESP32 variants + + let drvSelects = d.Sf.querySelectorAll("select[name^=LD]"); + if (drvSelects.length === 0) return; // No driver selects found + + // Check if any LD field has a non-default value (indicating it was set by backend) + let hasDriverConfig = false; + let allRMT = true; + let digitalBusCount = 0; + + drvSelects.forEach((sel) => { + let n = sel.name.substring(2, 3); + let t = parseInt(d.Sf["LT"+n].value); + + // Only check digital single-pin buses + if (isDig(t) && !isD2P(t)) { + digitalBusCount++; + if (sel.value && sel.value !== "" && sel.value !== "0") { + hasDriverConfig = true; + allRMT = false; + } + } + }); + + // If all drivers are RMT and we have digital buses, this might be a legacy config + // Apply intelligent driver assignment: fill RMT first, then fallback to I2S + if (allRMT && digitalBusCount > 0) { + let maxRMT = (is32() ? 8 : (isS2() || isS3()) ? 4 : 0); + let maxI2S = (is32() ? 8 : (isS2() || isS3()) ? 8 : 0); + let rmtAssigned = 0; + let i2sAssigned = 0; + + // First pass: assign drivers intelligently + drvSelects.forEach((sel) => { + let n = sel.name.substring(2, 3); + let t = parseInt(d.Sf["LT"+n].value); + + // Only process digital single-pin buses + if (isDig(t) && !isD2P(t)) { + if (rmtAssigned < maxRMT) { + // RMT channel available - use it + sel.value = "0"; + rmtAssigned++; + } else if (i2sAssigned < maxI2S) { + // RMT full, but I2S available - use I2S + sel.value = "1"; + i2sAssigned++; + } else { + // Both full - leave as RMT (will show validation error) + sel.value = "0"; + } + } + }); + + // Update UI to reflect the changes + updateTypeDropdowns(); + UI(); + } + } // https://stackoverflow.com/questions/7346563/loading-local-json-file function loadCfg(o) { var f, fr; @@ -709,6 +920,10 @@ d.getElementsByName("SP"+i)[0].value = v.freq; d.getElementsByName("LA"+i)[0].value = v.ledma; d.getElementsByName("MA"+i)[0].value = v.maxpwr; + // Handle driver type (LD field) - load from JSON if present + if (v.drv !== undefined && d.getElementsByName("LD"+i)[0]) { + d.getElementsByName("LD"+i)[0].value = v.drv; + } }); d.getElementsByName("PR")[0].checked = l.prl | 0; d.getElementsByName("MA")[0].value = l.maxpwr; @@ -743,6 +958,7 @@ if (li) { d.getElementsByName("MS")[0].checked = li.aseg; } + fixLegacyDriverConfig(); // Handle legacy configs without driver selection UI(); } } @@ -843,11 +1059,50 @@ } return opt; } + // Helper function to calculate channel usage across all buses + function calculateChannelUsage() { + let rmtUsed = 0, i2sUsed = 0; + let firstI2SType = null; + let maxLEDsOnI2SBus = 0; + let i2sBusesOver600 = 0; + + d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach(sel => { + let n = sel.name.substring(2,3); + let t = parseInt(sel.value); + let driverPref = d.Sf["LD"+n] ? parseInt(d.Sf["LD"+n].value || 0) : 0; + let ledCount = parseInt(d.Sf["LC"+n].value) || 0; + + if (isDig(t) && !isD2P(t)) { + if (driverPref === 1) { + i2sUsed++; + if (!firstI2SType) firstI2SType = t; + if (ledCount > maxLEDsOnI2SBus) maxLEDsOnI2SBus = ledCount; + if (ledCount > 600) i2sBusesOver600++; + } else { + rmtUsed++; + } + } + }); + + let parallelI2SAllowed = (maxLEDsOnI2SBus <= 600); + return { rmtUsed, i2sUsed, firstI2SType, maxLEDsOnI2SBus, i2sBusesOver600, parallelI2SAllowed }; + } + // dynamically enforce bus type availability based on current usage function updateTypeDropdowns() { let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]"); let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0; - // count currently used buses + let maxRMT = (is32() ? 8 : (isS2() || isS3()) ? 4 : isC3() ? 2 : 0); + let maxI2S = (is32() ? 8 : (isS2() || isS3()) ? 8 : isS3() ? 8 : 0); + + // Use helper function to calculate channel usage + let usage = calculateChannelUsage(); + let rmtUsed = usage.rmtUsed; + let i2sUsed = usage.i2sUsed; + let firstI2SType = usage.firstI2SType; + let maxLEDsOnI2SBus = usage.maxLEDsOnI2SBus; + + // Count all bus types LTs.forEach(sel => { let t = parseInt(sel.value); if (isDig(t) && !isD2P(t)) digitalB++; @@ -855,21 +1110,90 @@ if (isD2P(t)) twopinB++; if (isVir(t)) virtB++; }); - // enable/disable type options according to limits in dropdowns + + // Determine if parallel I2S is allowed (all I2S buses ≤600 LEDs) + let parallelI2SOK = (maxLEDsOnI2SBus <= 600); + let rmtFull = (rmtUsed >= maxRMT); + let i2sFull = (i2sUsed >= maxI2S); + + // Second pass: update each dropdown with appropriate constraints LTs.forEach(sel => { + let n = sel.name.substring(2,3); const curType = parseInt(sel.value); + const curDriver = d.Sf["LD"+n] ? parseInt(d.Sf["LD"+n].value || 0) : 0; const disable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = true); const enable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = false); enable('option'); // reset all first - // max digital buses: ESP32 & S2 support mono I2S as well as parallel so we need to take that into account; S3 only supports parallel - // supported outputs using parallel I2S/mono I2S: S2: 12/5, S3: 12/4, ESP32: 16/9 - let maxDB = maxD - ((is32() || isS2() || isS3()) ? (!d.Sf["PR"].checked) * 8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used - // disallow adding more of a type that has reached its limit but allow changing the current type - if (digitalB >= maxDB && !(isDig(curType) && !isD2P(curType))) disable('option[data-type="D"]'); + + // Update LED type constraints for digital buses + if (isDig(curType) && !isD2P(curType)) { + // If this bus uses I2S and other I2S buses exist, restrict to same type + // First I2S bus can select any type, subsequent I2S buses restricted to first type + let isFirstI2SBus = (curDriver === 1 && firstI2SType === null); + if (curDriver === 1 && firstI2SType !== null && !isFirstI2SBus) { + sel.querySelectorAll('option[data-type="D"]').forEach(o => { + if (parseInt(o.value) !== firstI2SType) o.disabled = true; + }); + } + } else { + // For non-digital current types, check if we can add digital buses + let canAddRMT = (rmtUsed < maxRMT || (isDig(curType) && !isD2P(curType) && curDriver === 0)); + let canAddI2S = ((is32() || isS2() || isS3()) && i2sUsed < maxI2S) || (isDig(curType) && !isD2P(curType) && curDriver === 1); + + // If both RMT and I2S are full, disable all digital types + if (!canAddRMT && !canAddI2S) { + disable('option[data-type="D"]'); + } + } + + // 2-pin digital buses limited to 2 if (twopinB >= 2 && !isD2P(curType)) disable('option[data-type="2P"]'); - // Disable PWM types that need more pins than available (accounting for current type's pins if PWM) + + // PWM analog types limited by pin count disable(`option[data-type^="${'A'.repeat(maxA - analogB + (isPWM(curType)?numPins(curType):0) + 1)}"]`); }); + + // Third pass: update driver selection dropdowns + if (is32() || isS2() || isS3()) { + LTs.forEach(sel => { + let n = sel.name.substring(2,3); + let t = parseInt(sel.value); + let drvsel = gId("drvsel"+n); + + if (drvsel && isDig(t) && !isD2P(t)) { + let rmtOpt = drvsel.querySelector('option[value="0"]'); + let i2sOpt = drvsel.querySelector('option[value="1"]'); + let curDriver = parseInt(d.Sf["LD"+n].value || 0); + + // Enable both options by default + if (rmtOpt) rmtOpt.disabled = false; + if (i2sOpt) i2sOpt.disabled = false; + + // Disable RMT if full (unless this bus already uses RMT) + if (rmtFull && curDriver !== 0) { + if (rmtOpt) rmtOpt.disabled = true; + } + + // Disable I2S if full (unless this bus already uses I2S) + if (i2sFull && curDriver !== 1) { + if (i2sOpt) i2sOpt.disabled = true; + } + + // For I2S buses >600 LEDs or when mixing with other I2S buses, validate + let ledCount = parseInt(d.Sf["LC"+n].value) || 0; + if (curDriver === 1 && !parallelI2SOK && ledCount > 600) { + // This I2S bus has >600 LEDs - mark as invalid if other I2S buses exist + if (i2sUsed > 1) { + drvsel.style.color = "red"; + } else { + drvsel.style.color = "#fff"; + } + } else { + drvsel.style.color = "#fff"; + } + } + }); + } } @@ -915,8 +1239,11 @@

Hardware setup

⚠ You might run into stability or lag issues.
Use less than 800 LEDs per output for the best experience!
+
-
Use parallel I2S:
+ Make a segment for each output:
Custom bus start indices:

diff --git a/wled00/set.cpp b/wled00/set.cpp index db8b30bac8..c254e07cf0 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -138,7 +138,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } } - unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed; + unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed, driverType; unsigned length, start, maMax; uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; String text; @@ -156,7 +156,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); strip.setTargetFps(request->arg(F("FR")).toInt()); #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - useParallelI2S = request->hasArg(F("PR")); + // useI2S is now always determined based on actual bus configuration, no longer user-controlled via PR checkbox #endif bool busesChanged = false; @@ -175,6 +175,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA + char ld[4] = "LD"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1) char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) if (!request->hasArg(lp)) { DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s+1); @@ -226,10 +227,11 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } type |= request->hasArg(rf) << 7; // off refresh override + driverType = request->arg(ld).toInt(); // 0=RMT (default), 1=I2S text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, text); + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed diff --git a/wled00/wled.h b/wled00/wled.h index 66b33740d6..5f2313858b 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -401,7 +401,7 @@ WLED_GLOBAL bool useGlobalLedBuffer _INIT(false); // double buffering disabled o #else WLED_GLOBAL bool useGlobalLedBuffer _INIT(true); // double buffering enabled on ESP32 #ifndef CONFIG_IDF_TARGET_ESP32C3 -WLED_GLOBAL bool useParallelI2S _INIT(false); // parallel I2S for ESP32 +WLED_GLOBAL bool useI2S _INIT(false); // I2S/LCD for ESP32 (parallel or single) #endif #endif #ifdef WLED_USE_IC_CCT diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 194256d82e..dab209ee8a 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -291,7 +291,23 @@ void getSettingsJS(byte subPage, Print& settingsScript) settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str()); // set limits - settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d);"), + // Calculate max RMT and I2S channels based on platform + uint8_t maxRMT = 0, maxI2S = 0; + #if defined(CONFIG_IDF_TARGET_ESP32) + maxRMT = 8; // ESP32 has 8 RMT channels + maxI2S = 8; // Can use 8 parallel I2S or 1 single I2S + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + maxRMT = 4; // ESP32-S2 has 4 RMT channels + maxI2S = 8; // Can use 8 parallel I2S or 1 single I2S + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + maxRMT = 4; // ESP32-S3 has 4 RMT channels + maxI2S = 8; // Can use 8 parallel LCD (no single I2S support) + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + maxRMT = 2; // ESP32-C3 has 2 RMT channels + maxI2S = 0; // No I2S support + #endif + + settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);"), WLED_MAX_BUSSES, WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI MAX_LEDS_PER_BUS, @@ -300,7 +316,9 @@ void getSettingsJS(byte subPage, Print& settingsScript) WLED_MAX_COLOR_ORDER_MAPPINGS, WLED_MAX_DIGITAL_CHANNELS, WLED_MAX_ANALOG_CHANNELS, - WLED_MAX_BUTTONS + WLED_MAX_BUTTONS, + maxRMT, + maxI2S ); printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments); @@ -330,6 +348,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current + char ld[4] = "LD"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1) char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) settingsScript.print(F("addLEDs(1);")); uint8_t pins[OUTPUT_MAX_PINS]; @@ -370,6 +389,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,sp,speed); printSetFormValue(settingsScript,la,bus->getLEDCurrent()); printSetFormValue(settingsScript,ma,bus->getMaxCurrent()); + printSetFormValue(settingsScript,ld,bus->getDriverType()); printSetFormValue(settingsScript,hs,bus->getCustomText().c_str()); sumMa += bus->getMaxCurrent(); }