diff --git a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h deleted file mode 100644 index 273933e3b8..0000000000 --- a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h +++ /dev/null @@ -1,471 +0,0 @@ -/*------------------------------------------------------------------------- -NeoPixel driver for ESP32 RMTs using High-priority Interrupt - -(NB. This cannot be mixed with the non-HI driver.) - -Written by Will M. Miles. - -I invest time and resources providing this open source code, -please support me by donating (see https://github.com/Makuna/NeoPixelBus) - -------------------------------------------------------------------------- -This file is part of the Makuna/NeoPixelBus library. - -NeoPixelBus is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as -published by the Free Software Foundation, either version 3 of -the License, or (at your option) any later version. - -NeoPixelBus is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with NeoPixel. If not, see -. --------------------------------------------------------------------------*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP32) -#if !defined(WLED_USE_SHARED_RMT) // V5 fix: don't compile this file on unsupported platforms - -// Use the NeoEspRmtSpeed types from the driver-based implementation -#include - - -namespace NeoEsp32RmtHiMethodDriver { - // Install the driver for a specific channel, specifying timing properties - esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration); - - // Remove the driver on a specific channel - esp_err_t Uninstall(rmt_channel_t channel); - - // Write a buffer of data to a specific channel. - // Buffer reference is held until write completes. - esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size); - - // Wait until transaction is complete. - esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time); -}; - -template class NeoEsp32RmtHIMethodBase -{ -public: - typedef NeoNoSettings SettingsObject; - - NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize) : - _sizeData(pixelCount * elementSize + settingsSize), - _pin(pin) - { - construct(); - } - - NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize, NeoBusChannel channel) : - _sizeData(pixelCount* elementSize + settingsSize), - _pin(pin), - _channel(channel) - { - construct(); - } - - ~NeoEsp32RmtHIMethodBase() - { - // wait until the last send finishes before destructing everything - // arbitrary time out of 10 seconds - ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS)); - - ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Uninstall(_channel.RmtChannelNumber)); - - gpio_matrix_out(_pin, SIG_GPIO_OUT_IDX, false, false); - pinMode(_pin, INPUT); - - free(_dataEditing); - free(_dataSending); - } - - bool IsReadyToUpdate() const - { - return (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT_SILENT_TIMEOUT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 0))); - } - - void Initialize() - { - rmt_config_t config = {}; - - config.rmt_mode = RMT_MODE_TX; - config.channel = _channel.RmtChannelNumber; - config.gpio_num = static_cast(_pin); - config.mem_block_num = 1; - config.tx_config.loop_en = false; - - config.tx_config.idle_output_en = true; - config.tx_config.idle_level = T_SPEED::IdleLevel; - - config.tx_config.carrier_en = false; - config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - - config.clk_div = T_SPEED::RmtClockDivider; - - ESP_ERROR_CHECK(rmt_config(&config)); // Uses ESP library - ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Install(_channel.RmtChannelNumber, T_SPEED::RmtBit0, T_SPEED::RmtBit1, T_SPEED::RmtDurationReset)); - } - - void Update(bool maintainBufferConsistency) - { - // wait for not actively sending data - // this will time out at 10 seconds, an arbitrarily long period of time - // and do nothing if this happens - if (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS))) - { - // now start the RMT transmit with the editing buffer before we swap - ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::Write(_channel.RmtChannelNumber, _dataEditing, _sizeData)); - - if (maintainBufferConsistency) - { - // copy editing to sending, - // this maintains the contract that "colors present before will - // be the same after", otherwise GetPixelColor will be inconsistent - memcpy(_dataSending, _dataEditing, _sizeData); - } - - // swap so the user can modify without affecting the async operation - std::swap(_dataSending, _dataEditing); - } - } - - bool AlwaysUpdate() - { - // this method requires update to be called only if changes to buffer - return false; - } - - bool SwapBuffers() - { - std::swap(_dataSending, _dataEditing); - return true; - } - - uint8_t* getData() const - { - return _dataEditing; - }; - - size_t getDataSize() const - { - return _sizeData; - } - - void applySettings([[maybe_unused]] const SettingsObject& settings) - { - } - -private: - const size_t _sizeData; // Size of '_data*' buffers - const uint8_t _pin; // output pin number - const T_CHANNEL _channel; // holds instance for multi channel support - - // Holds data stream which include LED color values and other settings as needed - uint8_t* _dataEditing; // exposed for get and set - uint8_t* _dataSending; // used for async send using RMT - - - void construct() - { - _dataEditing = static_cast(malloc(_sizeData)); - // data cleared later in Begin() - - _dataSending = static_cast(malloc(_sizeData)); - // no need to initialize it, it gets overwritten on every send - } -}; - -// normal -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsMethod; -typedef NeoEsp32RmtHINWs2805Method NeoEsp32RmtHINWs2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsMethod; -typedef NeoEsp32RmtHI0Ws2805Method NeoEsp32RmtHI0Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsMethod; -typedef NeoEsp32RmtHI1Ws2805Method NeoEsp32RmtHI1Ws2814Method; - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsMethod; -typedef NeoEsp32RmtHI2Ws2805Method NeoEsp32RmtHI2Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsMethod; -typedef NeoEsp32RmtHI3Ws2805Method NeoEsp32RmtHI3Ws2814Method; - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsMethod; -typedef NeoEsp32RmtHI4Ws2805Method NeoEsp32RmtHI4Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsMethod; -typedef NeoEsp32RmtHI5Ws2805Method NeoEsp32RmtHI5Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsMethod; -typedef NeoEsp32RmtHI6Ws2805Method NeoEsp32RmtHI6Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsMethod; -typedef NeoEsp32RmtHI7Ws2805Method NeoEsp32RmtHI7Ws2814Method; - -#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) -#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) - -// inverted -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsInvertedMethod; -typedef NeoEsp32RmtHINWs2805InvertedMethod NeoEsp32RmtHINWs2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsInvertedMethod; -typedef NeoEsp32RmtHI0Ws2805InvertedMethod NeoEsp32RmtHI0Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsInvertedMethod; -typedef NeoEsp32RmtHI1Ws2805InvertedMethod NeoEsp32RmtHI1Ws2814InvertedMethod; - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsInvertedMethod; -typedef NeoEsp32RmtHI2Ws2805InvertedMethod NeoEsp32RmtHI2Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsInvertedMethod; -typedef NeoEsp32RmtHI3Ws2805InvertedMethod NeoEsp32RmtHI3Ws2814InvertedMethod; - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsInvertedMethod; -typedef NeoEsp32RmtHI4Ws2805InvertedMethod NeoEsp32RmtHI4Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsInvertedMethod; -typedef NeoEsp32RmtHI5Ws2805InvertedMethod NeoEsp32RmtHI5Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsInvertedMethod; -typedef NeoEsp32RmtHI6Ws2805InvertedMethod NeoEsp32RmtHI6Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsInvertedMethod; -typedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod; - -#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) -#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) - -#endif -#endif diff --git a/lib/NeoESP32RmtHI/library.json b/lib/NeoESP32RmtHI/library.json deleted file mode 100644 index 0608e59e12..0000000000 --- a/lib/NeoESP32RmtHI/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "NeoESP32RmtHI", - "build": { "libArchive": false }, - "platforms": ["espressif32"], - "dependencies": [ - { - "owner": "makuna", - "name": "NeoPixelBus", - "version": "^2.8.3" - } - ] -} diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp b/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp deleted file mode 100644 index 6c046943b5..0000000000 --- a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp +++ /dev/null @@ -1,507 +0,0 @@ -/*------------------------------------------------------------------------- -NeoPixel library helper functions for Esp32. - -A BIG thanks to Andreas Merkle for the investigation and implementation of -a workaround to the GCC bug that drops method attributes from template methods - -Written by Michael C. Miller. - -I invest time and resources providing this open source code, -please support me by donating (see https://github.com/Makuna/NeoPixelBus) - -------------------------------------------------------------------------- -This file is part of the Makuna/NeoPixelBus library. - -NeoPixelBus is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as -published by the Free Software Foundation, either version 3 of -the License, or (at your option) any later version. - -NeoPixelBus is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with NeoPixel. If not, see -. --------------------------------------------------------------------------*/ - -#include - -#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) - -#include -#include "esp_idf_version.h" -#include "NeoEsp32RmtHIMethod.h" -#include "soc/soc.h" -#include "soc/rmt_reg.h" - -#ifdef __riscv -#include "riscv/interrupt.h" -#endif - - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) -#include "hal/rmt_ll.h" -#else -/* Shims for older ESP-IDF v3; we can safely assume original ESP32 */ -#include "soc/rmt_struct.h" - -// Selected RMT API functions borrowed from ESP-IDF v4.4.8 -// components/hal/esp32/include/hal/rmt_ll.h -// Copyright 2019 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -__attribute__((always_inline)) -static inline void rmt_ll_tx_reset_pointer(rmt_dev_t *dev, uint32_t channel) -{ - dev->conf_ch[channel].conf1.mem_rd_rst = 1; - dev->conf_ch[channel].conf1.mem_rd_rst = 0; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_start(rmt_dev_t *dev, uint32_t channel) -{ - dev->conf_ch[channel].conf1.tx_start = 1; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel) -{ - RMTMEM.chan[channel].data32[0].val = 0; - dev->conf_ch[channel].conf1.tx_start = 0; - dev->conf_ch[channel].conf1.mem_rd_rst = 1; - dev->conf_ch[channel].conf1.mem_rd_rst = 0; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->apb_conf.mem_tx_wrap_en = enable; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_enable_loop(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->conf_ch[channel].conf1.tx_conti_mode = enable; -} - -__attribute__((always_inline)) -static inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel) -{ - return dev->status_ch[channel]; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_set_limit(rmt_dev_t *dev, uint32_t channel, uint32_t limit) -{ - dev->tx_lim_ch[channel].limit = limit; -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_interrupt(rmt_dev_t *dev, uint32_t mask, bool enable) -{ - if (enable) { - dev->int_ena.val |= mask; - } else { - dev->int_ena.val &= ~mask; - } -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->int_ena.val &= ~(1 << (channel * 3)); - dev->int_ena.val |= (enable << (channel * 3)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->int_ena.val &= ~(1 << (channel * 3 + 2)); - dev->int_ena.val |= (enable << (channel * 3 + 2)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->int_ena.val &= ~(1 << (channel + 24)); - dev->int_ena.val |= (enable << (channel + 24)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel) -{ - dev->int_clr.val = (1 << (channel * 3)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel) -{ - dev->int_clr.val = (1 << (channel * 3 + 2)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel) -{ - dev->int_clr.val = (1 << (channel + 24)); -} - - -__attribute__((always_inline)) -static inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev) -{ - uint32_t status = dev->int_st.val; - return (status & 0xFF000000) >> 24; -} -#endif - - -// ********************************* -// Select method for binding interrupt -// -// - If the Bluetooth driver has registered a high-level interrupt, piggyback on that API -// - If we're on a modern core, allocate the interrupt with the API (old cores are bugged) -// - Otherwise use the low-level hardware API to manually bind the interrupt - - -#if defined(CONFIG_BTDM_CTRL_HLI) -// Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls -#include "hal/interrupt_controller_hal.h" -extern "C" esp_err_t hli_intr_register(intr_handler_t handler, void* arg, uint32_t intr_reg, uint32_t intr_mask); - -#else /* !CONFIG_BTDM_CTRL_HLI*/ - -// Declare the our high-priority ISR handler -extern "C" void ld_include_hli_vectors_rmt(); // an object with an address, but no space - -#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) -#include "soc/periph_defs.h" -#endif - -// Select level flag -#if defined(__riscv) -// RISCV chips don't block interrupts while scheduling; all we need to do is be higher than the WiFi ISR -#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL3 -#elif defined(CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5) -#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL4 -#else -#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL5 -#endif - -// ESP-IDF v3 cannot enable high priority interrupts through the API at all; -// and ESP-IDF v4 on XTensa cannot enable Level 5 due to incorrect interrupt descriptor tables -#if !defined(__XTENSA__) || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) || ((ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) && CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5)) -#define NEOESP32_RMT_CAN_USE_INTR_ALLOC - -// XTensa cores require the assembly bridge -#ifdef __XTENSA__ -#define HI_IRQ_HANDLER nullptr -#define HI_IRQ_HANDLER_ARG ld_include_hli_vectors_rmt -#else -#define HI_IRQ_HANDLER NeoEsp32RmtMethodIsr -#define HI_IRQ_HANDLER_ARG nullptr -#endif - -#else -/* !CONFIG_BTDM_CTRL_HLI && !NEOESP32_RMT_CAN_USE_INTR_ALLOC */ -// This is the index of the LV5 interrupt vector - see interrupt descriptor table in idf components/hal/esp32/interrupt_descriptor_table.c -#define ESP32_LV5_IRQ_INDEX 26 - -#endif /* NEOESP32_RMT_CAN_USE_INTR_ALLOC */ -#endif /* CONFIG_BTDM_CTRL_HLI */ - - -// RMT driver implementation -struct NeoEsp32RmtHIChannelState { - uint32_t rmtBit0, rmtBit1; - uint32_t resetDuration; - - const byte* txDataStart; // data array - const byte* txDataEnd; // one past end - const byte* txDataCurrent; // current location - size_t rmtOffset; -}; - -// Global variables -#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) -static intr_handle_t isrHandle = nullptr; -#endif - -static NeoEsp32RmtHIChannelState** driverState = nullptr; -constexpr size_t rmtBatchSize = RMT_MEM_ITEM_NUM / 2; - -// Fill the RMT buffer memory -// This is implemented using many arguments instead of passing the structure object to ensure we do only one lookup -// All the arguments are passed in registers, so they don't need to be looked up again -static void IRAM_ATTR RmtFillBuffer(uint8_t channel, const byte** src_ptr, const byte* end, uint32_t bit0, uint32_t bit1, size_t* offset_ptr, size_t reserve) { - // We assume that (rmtToWrite % 8) == 0 - size_t rmtToWrite = rmtBatchSize - reserve; - rmt_item32_t* dest =(rmt_item32_t*) &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory - const byte* psrc = *src_ptr; - - *offset_ptr ^= rmtBatchSize; - - if (psrc != end) { - while (rmtToWrite > 0) { - uint8_t data = *psrc; - for (uint8_t bit = 0; bit < 8; bit++) - { - dest->val = (data & 0x80) ? bit1 : bit0; - dest++; - data <<= 1; - } - rmtToWrite -= 8; - psrc++; - - if (psrc == end) { - break; - } - } - - *src_ptr = psrc; - } - - if (rmtToWrite > 0) { - // Add end event - rmt_item32_t bit0_val = {{.val = bit0 }}; - *dest = rmt_item32_t {{{ .duration0 = 0, .level0 = bit0_val.level1, .duration1 = 0, .level1 = bit0_val.level1 }}}; - } -} - -static void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& state) { - // Reset context state - state.rmtOffset = 0; - - // Fill the first part of the buffer with a reset event - // FUTURE: we could do timing analysis with the last interrupt on this channel - // Use 8 words to stay aligned with the buffer fill logic - rmt_item32_t bit0_val = {{.val = state.rmtBit0 }}; - rmt_item32_t fill = {{{ .duration0 = 100, .level0 = bit0_val.level1, .duration1 = 100, .level1 = bit0_val.level1 }}}; - rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM.chan[channel].data32[0]; - for (auto i = 0; i < 7; ++i) dest[i] = fill; - fill.duration1 = state.resetDuration > 1400 ? (state.resetDuration - 1400) : 100; - dest[7] = fill; - - // Fill the remaining buffer with real data - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 8); - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); - - // Start operation - rmt_ll_clear_tx_thres_interrupt(&RMT, channel); - rmt_ll_tx_reset_pointer(&RMT, channel); - rmt_ll_tx_start(&RMT, channel); -} - -extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { - // Tx threshold interrupt - uint32_t status = rmt_ll_get_tx_thres_interrupt_status(&RMT); - while (status) { - uint8_t channel = __builtin_ffs(status) - 1; - if (driverState[channel]) { - // Normal case - NeoEsp32RmtHIChannelState& state = *driverState[channel]; - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); - } else { - // Danger - another driver got invoked? - rmt_ll_tx_stop(&RMT, channel); - } - rmt_ll_clear_tx_thres_interrupt(&RMT, channel); - status = rmt_ll_get_tx_thres_interrupt_status(&RMT); - } -}; - -// Wrapper around the register analysis defines -// For all currently supported chips, this is constant for all channels; but this is not true of *all* ESP32 -static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) { - uint32_t v; - switch(channel) { -#ifdef RMT_STATE_CH0 - case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break; -#endif -#ifdef RMT_STATE_CH1 - case 1: v = (status >> RMT_STATE_CH1_S) & RMT_STATE_CH1_V; break; -#endif -#ifdef RMT_STATE_CH2 - case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break; -#endif -#ifdef RMT_STATE_CH3 - case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break; -#endif -#ifdef RMT_STATE_CH4 - case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break; -#endif -#ifdef RMT_STATE_CH5 - case 5: v = (status >> RMT_STATE_CH5_S) & RMT_STATE_CH5_V; break; -#endif -#ifdef RMT_STATE_CH6 - case 6: v = (status >> RMT_STATE_CH6_S) & RMT_STATE_CH6_V; break; -#endif -#ifdef RMT_STATE_CH7 - case 7: v = (status >> RMT_STATE_CH7_S) & RMT_STATE_CH7_V; break; -#endif - default: v = 0; - } - - return v != 0; -} - - -esp_err_t NeoEsp32RmtHiMethodDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset) { - // Validate channel number - if (channel >= RMT_CHANNEL_MAX) { - return ESP_ERR_INVALID_ARG; - } - - esp_err_t err = ESP_OK; - if (!driverState) { - // First time init - driverState = reinterpret_cast(heap_caps_calloc(RMT_CHANNEL_MAX, sizeof(NeoEsp32RmtHIChannelState*), MALLOC_CAP_INTERNAL)); - if (!driverState) return ESP_ERR_NO_MEM; - - // Ensure all interrupts are cleared before binding - RMT.int_ena.val = 0; - RMT.int_clr.val = 0xFFFFFFFF; - - // Bind interrupt handler -#if defined(CONFIG_BTDM_CTRL_HLI) - // Bluetooth driver has taken the empty high-priority interrupt. Fortunately, it allows us to - // hook up another handler. - err = hli_intr_register(NeoEsp32RmtMethodIsr, nullptr, (uintptr_t) &RMT.int_st, 0xFF000000); - // 25 is the magic number of the bluetooth ISR on ESP32 - see soc/soc.h. - intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25); - intr_cntrl_ll_enable_interrupts(1<<25); -#elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) - // Use the platform code to allocate the interrupt - // If we need the additional assembly bridge, we pass it as the "arg" to the IDF so it gets linked in - err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, HI_IRQ_HANDLER, (void*) HI_IRQ_HANDLER_ARG, &isrHandle); - //err = ESP_ERR_NOT_FINISHED; -#else - // Broken IDF API does not allow us to reserve the interrupt; do it manually - static volatile const void* __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt; - intr_matrix_set(xPortGetCoreID(), ETS_RMT_INTR_SOURCE, ESP32_LV5_IRQ_INDEX); - ESP_INTR_ENABLE(ESP32_LV5_IRQ_INDEX); -#endif - - if (err != ESP_OK) { - heap_caps_free(driverState); - driverState = nullptr; - return err; - } - } - - if (driverState[channel] != nullptr) { - return ESP_ERR_INVALID_STATE; // already in use - } - - NeoEsp32RmtHIChannelState* state = reinterpret_cast(heap_caps_calloc(1, sizeof(NeoEsp32RmtHIChannelState), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); - if (state == nullptr) { - return ESP_ERR_NO_MEM; - } - - // Store timing information - state->rmtBit0 = rmtBit0; - state->rmtBit1 = rmtBit1; - state->resetDuration = reset; - - // Initialize hardware - rmt_ll_tx_stop(&RMT, channel); - rmt_ll_tx_reset_pointer(&RMT, channel); - rmt_ll_enable_tx_err_interrupt(&RMT, channel, false); - rmt_ll_enable_tx_end_interrupt(&RMT, channel, false); - rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); - rmt_ll_clear_tx_err_interrupt(&RMT, channel); - rmt_ll_clear_tx_end_interrupt(&RMT, channel); - rmt_ll_clear_tx_thres_interrupt(&RMT, channel); - - rmt_ll_tx_enable_loop(&RMT, channel, false); - rmt_ll_tx_enable_pingpong(&RMT, channel, true); - rmt_ll_tx_set_limit(&RMT, channel, rmtBatchSize); - - driverState[channel] = state; - - rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true); - - return err; -} - -esp_err_t NeoEsp32RmtHiMethodDriver::Uninstall(rmt_channel_t channel) { - if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; - - NeoEsp32RmtHIChannelState* state = driverState[channel]; - - WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); - - // Done or not, we're out of here - rmt_ll_tx_stop(&RMT, channel); - rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); - driverState[channel] = nullptr; - heap_caps_free(state); - -#if !defined(CONFIG_BTDM_CTRL_HLI) /* Cannot unbind from bluetooth ISR */ - // Turn off the driver ISR and release global state if none are left - for (uint8_t channelIndex = 0; channelIndex < RMT_CHANNEL_MAX; ++channelIndex) { - if (driverState[channelIndex]) return ESP_OK; // done - } - -#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) - esp_intr_free(isrHandle); -#else - ESP_INTR_DISABLE(ESP32_LV5_IRQ_INDEX); -#endif - - heap_caps_free(driverState); - driverState = nullptr; -#endif /* !defined(CONFIG_BTDM_CTRL_HLI) */ - - return ESP_OK; -} - -esp_err_t NeoEsp32RmtHiMethodDriver::Write(rmt_channel_t channel, const uint8_t *src, size_t src_size) { - if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; - - NeoEsp32RmtHIChannelState& state = *driverState[channel]; - esp_err_t result = WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); - - if (result == ESP_OK) { - state.txDataStart = src; - state.txDataCurrent = src; - state.txDataEnd = src + src_size; - RmtStartWrite(channel, state); - } - return result; -} - -esp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickType_t wait_time) { - if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; - - NeoEsp32RmtHIChannelState& state = *driverState[channel]; - // yield-wait until wait_time - esp_err_t rv = ESP_OK; - uint32_t status; - while(1) { - status = rmt_ll_tx_get_channel_status(&RMT, channel); - if (!_RmtStatusIsTransmitting(channel, status)) break; - if (wait_time == 0) { rv = ESP_ERR_TIMEOUT; break; }; - - TickType_t sleep = std::min(wait_time, (TickType_t) 5); - vTaskDelay(sleep); - wait_time -= sleep; - }; - - return rv; -} - -#endif diff --git a/platformio.ini b/platformio.ini index 9f69bf67bd..b074af8296 100644 --- a/platformio.ini +++ b/platformio.ini @@ -199,6 +199,7 @@ build_unflags = ${common.build_unflags} custom_usermods = build_flags = -DESP8266 + -DWLED_DISABLE_GLOBAL_PIXELBUFFER -DFP_IN_IROM ;-Wno-deprecated-declarations ;-Wno-register ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C @@ -233,6 +234,7 @@ monitor_filters = esp8266_exception_decoder ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 build_flags_compat = -DESP8266 + -DWLED_DISABLE_GLOBAL_PIXELBUFFER -DFP_IN_IROM ;;-Wno-deprecated-declarations -Wno-misleading-indentation @@ -256,7 +258,6 @@ lib_deps_compat = ESPAsyncUDP ESP8266PWM IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp index 4e4742ad1d..d2230171d2 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp @@ -61,7 +61,7 @@ class RgbRotaryEncoderUsermod : public Usermod // …then set only the LED pin _pins[0] = static_cast(ledIo); BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0); - busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver + BusManager::allocateHardware(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver ledBus = new BusDigital(busCfg); if (!ledBus->isOk()) { cleanup(); diff --git a/wled00/FX.h b/wled00/FX.h index c874c57209..64083ef3eb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -90,11 +90,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define MAX_NUM_SEGMENTS 32 #define MAX_SEGMENT_DATA (20*1024) // 20k by default (S2 is short on free RAM), limit does not apply if PSRAM is available #else - #ifdef BOARD_HAS_PSRAM - #define MAX_NUM_SEGMENTS 64 - #else - #define MAX_NUM_SEGMENTS 32 - #endif + #define MAX_NUM_SEGMENTS 64 #define MAX_SEGMENT_DATA (64*1024) // 64k by default, limit does not apply if PSRAM is available #endif @@ -102,8 +98,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex() assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) -#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) - #define NUM_COLORS 3 /* number of colors per segment */ #define SEGMENT (*strip._currentSegment) #define SEGENV (*strip._currentSegment) @@ -895,7 +889,13 @@ class WS2812FX { waitForIt(); // wait until frame is over (service() has finished or time for 1 frame has passed) void setRealtimePixelColor(unsigned i, uint32_t c); - inline void setPixelColor(unsigned n, uint32_t c) const { if (n < getLengthTotal()) _pixels[n] = c; } // paints absolute strip pixel with index n and color c + inline void setPixelColor(unsigned n, uint32_t c) const { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + n = getMappedPixelIndex(n); if (n < getLengthTotal()) BusManager::setPixelColor(n, c); + #else + if (n < getLengthTotal()) _pixels[n] = c; + #endif + } // paints absolute strip pixel with index n and color c inline void resetTimebase() { timebase = 0UL - millis(); } inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const { setPixelColor(n, RGBW32(r,g,b,w)); } @@ -953,8 +953,20 @@ class WS2812FX { }; unsigned long now, timebase; - inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds - inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table + inline uint32_t getPixelColor(unsigned n) const { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + n = getMappedPixelIndex(n); return n < getLengthTotal() ? BusManager::getPixelColor(n) : 0; + #else + return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; + #endif + } // returns color of pixel n, black if out of (mapped) bounds + inline uint32_t getPixelColorNoMap(unsigned n) const { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + return (n < getLengthTotal()) ? BusManager::getPixelColor(n) : 0; + #else + return (n < getLengthTotal()) ? _pixels[n] : 0; + #endif + } // ignores mapping table inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 916d324f17..7d2788aa60 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -12,6 +12,9 @@ #include "wled.h" #include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "colors.h" +#if defined(ARDUINO_ARCH_ESP32) +#include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" // needed for setExpectedChannels() +#endif /* Custom per-LED mapping has moved! @@ -1193,80 +1196,44 @@ void WS2812FX::finalizeInit() { _hasWhiteChannel = _isOffRefreshRequired = false; BusManager::removeAll(); // TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers. - unsigned digitalCount = 0; - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_HAS_PARALLEL_I2S) - // validate the bus config: count I2S buses and check if they meet requirements - unsigned i2sBusCount = 0; - - for (const auto &bus : busConfigs) { + // First assign driver types (RMT/I2S/... channels) for all buses before any are constructed, + // because parallel buses interact during channel assignment. If hardware allocation fails + // (no free channels), the bus is still constructed below and will fail initialization; + // BusManager::add() then replaces it with a BusPlaceholder. + #if defined(ARDUINO_ARCH_ESP32) + unsigned rmtBusCount = 0; + unsigned parHwBusCount = 0; // used for debug print only + #endif + for (auto &bus : busConfigs) { + BusManager::allocateHardware(bus.type, bus.pins, bus.driverType); // TODO: if no bus type can be allocated, this returns false, could fallback to placeholder + #if defined(ARDUINO_ARCH_ESP32) + // Count buses that will occupy an RMT channel (driverType is final after allocateHardware()) if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { - digitalCount++; - if (bus.driverType == 1) - i2sBusCount++; + if (bus.driverType == BUSDRV_RMT) rmtBusCount++; + else if (bus.driverType == BUSDRV_PARHW) parHwBusCount++; // parallel bus: I2S/LCD/SPI/PARLIO depending on chip (hardware driven, not BB) } + #endif } - DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount); - // Determine parallel vs single I2S usage (used for memory calculation only) - bool useParallelI2S = false; - #if defined(CONFIG_IDF_TARGET_ESP32S3) - // ESP32-S3 always uses parallel LCD driver for I2S - if (i2sBusCount > 0) { - useParallelI2S = true; - } - #else - if (i2sBusCount > 1) { - useParallelI2S = true; - } - #endif + #if defined(ARDUINO_ARCH_ESP32) + DEBUG_PRINTF_P(PSTR("Digital RMT buses: %u, parallel buses: %u\n"), rmtBusCount, parHwBusCount); + // RMT channel tracking was already reset by BusManager::removeAll() -> resetChannelTracking() + WLEDpixelBus::RmtBus::setExpectedChannels((uint8_t)rmtBusCount); #endif DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); - // create buses/outputs - unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers - unsigned I2SdmaMem = 0; + // Now construct each bus. BusManager::add() automatically falls back to a BusPlaceholder if it fails for (auto &bus : busConfigs) { - // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels - // store the result in iType for later use during bus creation (getI() must only be called once per BusConfig) - // note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage - bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType); + BusManager::add(bus, false); } - for (auto &bus : busConfigs) { - bool use_placeholder = false; - unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer) - mem += busMemUsage; - // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) - #if defined(WLED_HAS_PARALLEL_I2S) - bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT - if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) { - #ifdef NPB_CONF_4STEP_CADENCE - constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit) - #else - constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit) - #endif - unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1)); - if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses) - if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem; - } - #endif - if (mem + I2SdmaMem > MAX_LED_MEMORY + 1024) { // +1k to allow some margin to not drop buses that are allowed in UI (calculation here includes bus overhead) - DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count); - errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus - use_placeholder = true; - } - if (BusManager::add(bus, use_placeholder) != -1) { - mem += BusManager::busses.back()->getBusSize(); - if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count - } - } - DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem + I2SdmaMem); busConfigs.clear(); busConfigs.shrink_to_fit(); _length = 0; for (size_t i=0; iisOk() || bus->getStart() + bus->getLength() > MAX_LEDS) break; + if (!bus || bus->getStart() + bus->getLength() > MAX_LEDS) break; + if (!bus->isOk()) continue; // placeholder bus (failed init) — skip but keep initializing remaining buses //RGBW mode is enabled if at least one of the strips is RGBW _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. @@ -1296,11 +1263,17 @@ void WS2812FX::finalizeInit() { // update global _pixels[] buffer to match getLengthTotal() note: if allocation fails, WLED will not render anything void WS2812FX::updatePixelBuffer() { - uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t); p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it + _pixels = nullptr; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // Skip the global framebuffer: blendSegment() encodes directly into bus encode buffers. + // This saves N*4 bytes of heap (e.g. 1200B for 300 LEDs, 2000B for 500 LEDs). + #else + uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t); // use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer _pixels = static_cast(allocate_buffer(requiredMem, BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR)); DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), requiredMem); + #endif } void WS2812FX::service() { @@ -1440,11 +1413,32 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const size_t stopIndx = startIndx + length; uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE uint8_t cct = topSegment.currentCCT(); - if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) + opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) const Segment *segO = topSegment.getOldSegment(); const bool hasGrouping = topSegment.groupLength() != 1; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // Direct-to-bus mode: read/write pixels through the bus encode buffer instead of _pixels[]. + // CCT is set per-segment (not per-pixel) — acceptable trade-off for the RAM saved. + // Gamma is applied inline when writing; multi-segment blending occurs in gamma-compressed space. + if (!cctFromRgb) BusManager::setSegmentCCT(cct, correctWB); + const auto blendPixelIntoFrame = [&](size_t idx, uint32_t newC, uint8_t o) { + unsigned physIdx = getMappedPixelIndex(idx); + uint32_t result; + // Fast path: fully-opaque top layer — result is just newC, no readback needed. + // This covers the dominant case (single segment, full brightness, default blend mode) + // and avoids an expensive bus decode call per pixel for fast effects like Palette. + if (o == 255 && blendMode == 0) { + result = newC; + } else { + uint32_t existing = BusManager::getPixelColor(physIdx); + result = color_blend(existing, segblend(newC, existing), o); + } + BusManager::setPixelColor(physIdx, result); + }; + #endif + // fast path: handle the default case - no transitions, no grouping/spacing, no mirroring, no CCT if (!segO && blendingStyle == TRANSITION_FADE && !hasGrouping && !topSegment.mirror && !topSegment.mirror_y) { if (isMatrix && stopIndx <= matrixSize && !_pixelCCT) { @@ -1461,12 +1455,18 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { if (topSegment.reverse_y) { start_offset += (height - 1) * Segment::maxWidth; y_inc = -Segment::maxWidth; } for (int y = 0; y < height; y++) { + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER uint32_t* pRow = &_pixels[start_offset + y * y_inc]; + #endif const int y_width = y * width; for (int x = 0; x < width; x++) { - uint32_t* p = pRow + x * x_inc; uint32_t c_a = topSegment.getPixelColorRaw(x + y_width); + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame((size_t)(start_offset + y * y_inc + x * x_inc), c_a, opacity); + #else + uint32_t* p = pRow + x * x_inc; *p = color_blend(*p, segblend(c_a, *p), opacity); + #endif } } } else { // transposed @@ -1476,7 +1476,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const int py = topSegment.reverse_y ? (width - x - 1) : x; // source pixel: swap x into y, reverse if needed const uint32_t c_a = topSegment.getPixelColorRaw(px + py * height); // height = virtual width const size_t idx = XY(topSegment.start + x, topSegment.startY + y); // write logical (non swapped) pixel coordinate + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame(idx, c_a, opacity); + #else _pixels[idx] = color_blend(_pixels[idx], segblend(c_a, _pixels[idx]), opacity); + #endif } } } @@ -1484,7 +1488,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { #endif } else if (!isMatrix) { // 1D fast path, include CCT as it is more common on 1D setups + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER uint32_t* strip = _pixels; + #endif int start = topSegment.start; int off = topSegment.offset; for (int i = 0; i < length; i++) { @@ -1492,8 +1498,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { int p = topSegment.reverse ? (length - i - 1) : i; int idx = start + p + off; if (idx >= topSegment.stop) idx -= length; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame((size_t)idx, c_a, opacity); + #else strip[idx] = color_blend(strip[idx], segblend(c_a, strip[idx]), opacity); if (_pixelCCT) _pixelCCT[idx] = cct; + #endif } return; } @@ -1570,8 +1580,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const int baseX = topSegment.start + x; const int baseY = topSegment.startY + y; size_t indx = XY(baseX, baseY); // absolute address on strip + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame(indx, c, o); + #else _pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o); if (_pixelCCT) _pixelCCT[indx] = cct; + #endif // Apply mirroring if enabled if (topSegment.mirror || topSegment.mirror_y) { const int mirrorX = topSegment.start + width - x - 1; @@ -1579,6 +1593,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY); const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY); const size_t idxMM = XY(mirrorX, mirrorY); + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + if (topSegment.mirror) blendPixelIntoFrame(idxMX, c, o); + if (topSegment.mirror_y) blendPixelIntoFrame(idxMY, c, o); + if (topSegment.mirror && topSegment.mirror_y) blendPixelIntoFrame(idxMM, c, o); + #else if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], segblend(c, _pixels[idxMX]), o); if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], segblend(c, _pixels[idxMY]), o); if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], segblend(c, _pixels[idxMM]), o); @@ -1587,6 +1606,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { if (topSegment.mirror_y) _pixelCCT[idxMY] = cct; if (topSegment.mirror && topSegment.mirror_y) _pixelCCT[idxMM] = cct; } + #endif } }; @@ -1666,6 +1686,18 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) { int indx = topSegment.start + i; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // direct-to-bus: apply mirroring then encode + if (topSegment.mirror) { + unsigned indxM = topSegment.stop - i - 1; + indxM += topSegment.offset; // offset/phase + if (indxM >= topSegment.stop) indxM -= length; // wrap + blendPixelIntoFrame(indxM, c, o); + } + indx += topSegment.offset; // offset/phase + if (indx >= topSegment.stop) indx -= length; // wrap + blendPixelIntoFrame((size_t)indx, c, o); + #else // Apply mirroring if (topSegment.mirror) { unsigned indxM = topSegment.stop - i - 1; @@ -1678,6 +1710,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { if (indx >= topSegment.stop) indx -= length; // wrap _pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o); if (_pixelCCT) _pixelCCT[indx] = cct; + #endif }; // if we blend using "push" style we need to "shift" canvas to left/right/ @@ -1724,47 +1757,78 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } void WS2812FX::show() { + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER if (!_pixels) { DEBUGFX_PRINTLN(F("Error: no _pixels!")); errorFlag = ERR_NORAM; return; // no pixels allocated, nothing to show } - + #endif unsigned long showNow = millis(); size_t diff = showNow - _lastShow; + // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode + bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); size_t totalLen = getLengthTotal(); - // WARNING: as WLED doesn't handle CCT on pixel level but on Segment level instead - // we need to keep track of each pixel's CCT when blending segments (if CCT is present) - // and then set appropriate CCT from that pixel during paint (see below). - if ((hasCCTBus() || correctWB) && !cctFromRgb) - _pixelCCT = static_cast(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); // allocate CCT buffer if necessary, prefer PSRAM - if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT + // CCT per-pixel tracking: only needed when multiple active segments have *different* CCT values. + // When all segments share the same CCT we skip the allocation entirely and set Bus::_cct once + // before the paint loop — identical to the original per-pixel-change path but without the buffer. + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER + uint8_t uniformCCT = 127; // neutral 50:50 CCT; used when no buffer is allocated + if ((hasCCTBus() || correctWB) && !cctFromRgb) { + int16_t firstCCT = -1; // -1 = "no active segment seen yet" + for (const Segment &seg : _segments) { + if (!seg.isActive() || (!seg.on && !seg.isInTransition())) continue; + uint8_t segCCT = seg.currentCCT(); + if (firstCCT < 0) { + firstCCT = segCCT; + uniformCCT = segCCT; + } else if ((uint8_t)firstCCT != segCCT) { + // segments have different CCT values — need per-pixel buffer + _pixelCCT = static_cast(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); + if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT + break; + } + } + } + #endif if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + while (!BusManager::canAllShow()) yield(); // wait for all buses to finish sending the previous frame + // Direct-to-bus mode: clear bus encode buffers then blend segments directly into them. + // Per-pixel CCT tracking is not supported; CCT is set per-segment in blendSegment(). + if (cctFromRgb) BusManager::setSegmentCCT(-1); // set once; blendSegment skips CCT if cctFromRgb + BusManager::clearPixels(totalLen); + for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { + blendSegment(seg); + } + #else // clear frame buffer memset(_pixels, 0, sizeof(uint32_t) * totalLen); // blend all segments into (cleared) buffer for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { blendSegment(seg); // blend segment's buffer into frame buffer } + #endif } // avoid race condition, capture _callback value show_callback callback = _callback; if (callback) callback(); // will call setPixelColor or setRealtimePixelColor + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER + while (!BusManager::canAllShow()) yield(); // wait for all buses to finish sending the previous frame // paint actual pixels int oldCCT = Bus::getCCT(); // store original CCT value (since it is global) // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) if (cctFromRgb) BusManager::setSegmentCCT(-1); - // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode - bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); + else if (!_pixelCCT) BusManager::setSegmentCCT(uniformCCT, correctWB); // uniform CCT: set once before loop for (size_t i = 0; i < totalLen; i++) { // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then // correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio - if (_pixelCCT) { // cctFromRgb already exluded at allocation + if (_pixelCCT) { // cctFromRgb already excluded at allocation if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB); } @@ -1777,10 +1841,11 @@ void WS2812FX::show() { p_free(_pixelCCT); _pixelCCT = nullptr; + #else + // TODO: implement bus level gamma, see PR #5722 + #endif - // some buses send asynchronously and this method will return before - // all of the data has been sent. - // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods + // pass the pixels on to the buses and start sending them out BusManager::show(); if (diff > 0) { // skip calculation if no time has passed @@ -1846,7 +1911,10 @@ void WS2812FX::setCCT(uint16_t k) { // direct=true either expects the caller to call show() themselves (realtime modes) or be ok waiting for the next frame for the change to apply // direct=false immediately triggers an effect redraw void WS2812FX::setBrightness(uint8_t b, bool direct) { - if (gammaCorrectBri) b = gamma8(b); + if (gammaCorrectBri && b > 0) { + (int)(powf((float)b / 255.0f, gammaCorrectVal) * 255.0f + 0.5f); // use gamma formula instead of gamma table: gamma table is only for color correction (has unity mapping if color correction is diabled) + if (b == 0) b = 1; // dont go to 0 brigthness if input was non zero + } if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off @@ -2213,3 +2281,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", "Candy2","Traffic Light" ])====="; + diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 30f613bdbc..9f8051eeec 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1942,4 +1942,4 @@ static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const return rb | g; } -#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) \ No newline at end of file diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index f8cacc7b7b..5ba446fa9a 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -57,7 +57,7 @@ bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { return true; } -uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { +uint8_t ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { // upper nibble contains W swap information // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used for (const auto& map : _mappings) { @@ -125,23 +125,28 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { return c; } +// Default implementation for Bus::getCustomBusConfig() — returns a static default. +// BusDigital and BusPlaceholder override this when custom.active() == true. +const CustomBusConfig& Bus::getCustomBusConfig() const { + static const CustomBusConfig _defaultCustom; + return _defaultCustom; +} BusDigital::BusDigital(const BusConfig &bc) -: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) +: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814 || bc.type == TYPE_TM1815)) , _skip(bc.skipAmount) //sacrificial pixels , _colorOrder(bc.colorOrder) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) +, _milliAmpsLimit(0) , _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) +, _busPtr(nullptr) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); + _busSpeedFactor = bc.busSpeedFactor; if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } - _iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit() - if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } - if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; - _colorSum = 0; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { @@ -150,30 +155,58 @@ BusDigital::BusDigital(const BusConfig &bc) return; } _pins[1] = bc.pins[1]; - _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined + _frequencykHz = bc.frequency ? bc.frequency : 1000U; // 1MHz clock if undefined TODO: this is not passed to bus correctly, got lost somewhere in the refacotring } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); uint16_t lenToCreate = bc.count; - if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus - _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip); + + // Customized channel map/timing override + if (bc.custom.active()) { + _pCustomConfig = new CustomBusConfig(bc.custom); + const uint8_t nch = bc.custom.is16bit ? bc.custom.numChannels * 2 : bc.custom.numChannels; + const WLEDpixelBus::LedTiming customTiming(bc.custom.t0h, bc.custom.t0l, bc.custom.t1h, bc.custom.t1l, bc.custom.trst); + _busPtr = PixelBusAllocator::create(bc.type, _pins, lenToCreate + _skip, bc.colorOrder, _driverType, bc.busSpeedFactor, _frequencykHz, nch, &customTiming); + if (_busPtr) { + _busPtr->setEncoder(WLEDpixelBus::ColorEncoder(bc.custom.channelColors, bc.custom.numChannels, bc.custom.invertMask, bc.custom.is16bit)); + if (bc.custom.invertOutput) _busPtr->setInverted(true); // invert output, needs to be set before bus->begin() (uses native hardware inversion capability) + // TODO: should inverted be supported for normal buses too? probably better not as it complicates things for normal users, one more option to screw up the settings. + } + // Derive instance capabilities from actual channel map + _hasRgb = false; _hasWhite = false; _hasCCT = false; + bool hasWW = false, hasCW = false; + for (uint8_t i = 0; i < bc.custom.numChannels; i++) { + switch (bc.custom.channelColors[i]) { + case 1: case 2: case 3: _hasRgb = true; break; // at least one color channel is used + case 4: _hasWhite = true; break; + case 5: hasWW = true; break; + case 6: hasCW = true; break; + } + } + if (hasWW || hasCW) _hasWhite = true; + if (hasWW && hasCW) _hasCCT = true; + } else { + // create bus via PixelBusAllocator wrapper which will return a WLEDpixelBus::PixelBus + _busPtr = PixelBusAllocator::create(bc.type, _pins, lenToCreate + _skip, bc.colorOrder, _driverType, bc.busSpeedFactor, _frequencykHz); + } _valid = (_busPtr != nullptr) && bc.count > 0; + // fix for wled#4759 - if (_valid) for (unsigned i = 0; i < _skip; i++) { - PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here) + if (_valid) { + _busPtr->setNumPixels(lenToCreate + _skip); } - else { + + if (!_valid) cleanup(); - } - DEBUGBUS_PRINTF_P(PSTR("Bus len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u, driver:%s] mA=%d/%d %s\n"), + + DEBUGBUS_PRINTF_P(PSTR("Bus len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [driver:%s] mA=%d/%d %s\n"), (int)bc.count, (int)bc.type, (int)_hasRgb, (int)_hasWhite, (int)_hasCCT, (unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U, - (unsigned)_iType, - isI2S() ? "I2S" : "RMT", + (_valid && _busPtr) ? _busPtr->getTypeStr() : "none", (int)_milliAmpsPerLed, (int)_milliAmpsMax, _valid ? " " : "FAILED" ); @@ -193,21 +226,60 @@ BusDigital::BusDigital(const BusConfig &bc) // if limit is set too low, brightness is limited to 1 to at least show some light // to disable brightness limiter for a bus, set LED current to 0 +void BusDigital::setBrightness(uint8_t b) { + _bri = b; + if (!_busPtr) return; + if (_type == TYPE_TM1814 || _type == TYPE_TM1815) { + // TM1814/TM1815: coarse brightness via hardware drive current (64 steps), + // fine residual applied via color_fade() in setPixelColor(). + uint8_t currentStep, residualBri; + WLEDpixelBus::mapBrightnessToCurrentStep(b, 64, 44, currentStep, residualBri); + uint8_t prefix[8]; + memset(prefix, currentStep, 4); // C1: W R G B + memset(prefix + 4, ~currentStep, 4); // C2: ~W ~R ~G ~B + _busPtr->updatePrefix(prefix, 8); + _busPtr->setBusBri(residualBri); // used by color_fade() in setPixelColor() + } else if (_type == TYPE_APA102) { + // APA102: two-stage brightness. Hardware 5-bit brightness byte (0..31) for coarse + // control, color_fade() residual for fine interpolation between steps. + // minBri=8 reflects step-0 = 1/31 of max current (~3.2% of full brightness). + uint8_t hwStep, residualBri; + WLEDpixelBus::mapBrightnessToCurrentStep(b, 32, 8, hwStep, residualBri); + _busPtr->setApa102HwBri(hwStep); + _busPtr->setBusBri(residualBri); // used by color_fade() in setPixelColor() + } else if (is16bit()) { + // 16-bit LED types (SM16825, UCS8903, UCS8904): color_fade() is a no-op (_busBri=255); + // encoder applies full 16-bit precision via channel*_encBri. + _busPtr->setBusBri(255); + _busPtr->setEncBri(b); + } else { + _busPtr->setBusBri(b); // used by color_fade() in setPixelColor() + } +} + void BusDigital::estimateCurrent() { + uint32_t colorSum = _colorSum; uint32_t actualMilliampsPerLed = _milliAmpsPerLed; if (_milliAmpsPerLed == 255) { // use wacky WS2815 power model, see WLED issue #549 - _colorSum *= 3; // sum is sum of max value for each color, need to multiply by three to account for clrUnitsPerChannel being 3*255 + // WS2815 power model: _colorSum accumulated max(R,G,B) per pixel; multiply by 3 to match clrUnitsPerChannel + colorSum *= 3; actualMilliampsPerLed = 12; // from testing an actual strip } - // _colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps + // TM1814/TM1815/APA102: colors were accumulated with the fine residual brightness (getBusBri()). + // Scale colorSum back to the full _bri level so ABL sees the correct hardware power draw. + if (_type == TYPE_TM1814 || _type == TYPE_TM1815 || _type == TYPE_APA102) { + const uint8_t busBri = _busPtr->getBusBri(); + if (busBri > 0) colorSum = ((uint64_t)colorSum * _bri) / busBri; + } + // colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps uint32_t clrUnitsPerChannel = hasWhite() ? 4*255 : 3*255; - _milliAmpsTotal = ((uint64_t)_colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED to total (WS2812: ~0.7mA, WS2815: ~2mA) + _milliAmpsTotal = ((uint64_t)colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED } void BusDigital::applyBriLimit(uint8_t newBri) { // a newBri of 0 means calculate per-bus brightness limit - _NPBbri = 255; // reset, intermediate value is set below, final value is calculated in bus::show() + _totalBusBri = 255; // reset, intermediate value is set below, final value is calculated in bus::show() if (newBri == 0) { if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus newBri = 255; @@ -225,22 +297,10 @@ void BusDigital::applyBriLimit(uint8_t newBri) { } if (newBri < 255) { - _NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used) - uint16_t wwcw = 0; - unsigned hwLen = _len; - if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus - for (unsigned i = 0; i < hwLen; i++) { - uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); // need to revert color order for correct color scaling and CCT calc in case white is swapped - uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co); // Note: if ABL would be calculated as a seperate loop (as it was before) it is slower but could use original color, making it more color-accurate - if (hasCCT()) { - uint8_t cctWW, cctCW; - Bus::calculateCCT(c, cctWW, cctCW); // calculate CCT before fade (more accurate) | Note: if using "accurate" white calculation mode, approximateKelvinFromRGB can be very inaccurate (white is subtracted) - wwcw = ((cctCW + 1) * newBri) & 0xFF00; // apply brightness to CCT (leave it in upper byte for 16bit NeoPixelBus value) - wwcw |= ((cctWW + 1) * newBri) >> 8; - } - c = color_fade(c, newBri, true); // apply additional dimming note: using inline version is a bit faster but overhead of getPixelColor() dominates the speed impact by far - PolyBus::setPixelColor(_busPtr, _iType, i, c, co, wwcw); // repaint all pixels with new brightness - } + _totalBusBri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used) + // scaleAll() simply scales every channel byte proportionally, i.e. RGB and if available W (WW and CW) + // in order to enforce the brightness limit, this does not use video scaling and can scale to black if ABL limit is set too low + _busPtr->scaleAll(newBri); } _colorSum = 0; // reset for next frame @@ -248,64 +308,67 @@ void BusDigital::applyBriLimit(uint8_t newBri) { void BusDigital::show() { if (!_valid) return; - _NPBbri = (_NPBbri * _bri) / 255; // total applied brightness for use in restoreColorLossy (see applyBriLimit()) - PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs) + if (BusManager::_useABL) + _totalBusBri = (_totalBusBri * _bri) / 255; // total applied brightness for use in restoreColorLossy (see applyBriLimit()) + else + _totalBusBri = _bri; // if not using ABL, total bus brightness is just the bus brightness + _busPtr->show(); } bool BusDigital::canShow() const { if (!_valid) return true; - return PolyBus::canShow(_busPtr, _iType); + return _busPtr->canShow(); +} + +void BusDigital::clearPixels() { + if (_valid && _busPtr) _busPtr->clearEncodeBuffer(); } //If LEDs are skipped, it is possible to use the first as a status LED. //TODO only show if no new show due in the next 50ms void BusDigital::setStatusPixel(uint32_t c) { if (_valid && _skip) { - PolyBus::setPixelColor(_busPtr, _iType, 0, c, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); - if (canShow()) PolyBus::show(_busPtr, _iType); + _busPtr->setPixelColor(0, c, 0); + if (canShow()) _busPtr->show(); } } -// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash -void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { +// TODO: this function may still need some optimization, making better use of the new bus architecture +// Note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash +// Note: there is no benefit of putting this in IRAM: IRAM: 34.2fps, no IRAM: 34.3fps +void BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; - if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + if (_reversed) pix = _len - pix -1; + pix += _skip; uint8_t cctWW = 0, cctCW = 0; uint16_t wwcw = 0; - if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW); - c = color_fade(c, _bri, true); // apply brightness - - if (hasCCT()) { - wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte) - wwcw |= ((cctWW + 1) * _bri) >> 8; - if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w - } - - if (BusManager::_useABL) { - // if using ABL, sum all color channels to estimate current and limit brightness in show() - uint8_t r = R(c), g = G(c), b = B(c); - if (_milliAmpsPerLed < 255) { // normal ABL - _colorSum += r + g + b + W(c); - } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) - _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); + if (c > 0) { + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + if (hasWhite()) { + c = autoWhiteCalc(c, cctWW, cctCW); } - } - - if (_reversed) pix = _len - pix -1; - pix += _skip; - const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned pOld = pix; - pix = IC_INDEX_WS2812_1CH_3X(pix); - uint32_t cOld = PolyBus::getPixelColor(_busPtr, _iType, pix, co); - switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) - case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; - case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; - case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; + // Apply brightness via color_fade() on the full uint32_t — hue-preserving video scale. + // _busBri is the correct value for each type: + // normal 8-bit: _bri | TM1814/15: fine residual | 16-bit: 255 (no-op, encoder uses _encBri) + const uint8_t bri = _busPtr->getBusBri(); + c = color_fade(c, bri, true); + if (hasCCT()) { + wwcw = ((uint16_t)(cctCW + 1) * bri) & 0xFF00; // scale CW, store in high byte + wwcw |= ((uint16_t)(cctWW + 1) * bri) >> 8; // scale WW, store in low byte + } + if (BusManager::_useABL) { + // Accumulate brightness-scaled channel values for current estimation. + // For 16-bit types c is unscaled (bri=255 above); ABL slightly over-estimates at low brightness. + uint8_t r = R(c), g = G(c), b = B(c); + if (_milliAmpsPerLed < 255) { // normal ABL + _colorSum += r + g + b + W(c); + } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) + _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); + } } } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); + _busPtr->setPixelColor(pix, c, wwcw); } // returns lossly restored color from bus @@ -313,22 +376,9 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; if (_reversed) pix = _len - pix -1; pix += _skip; - const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_NPBbri); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - uint8_t r = R(c); - uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - uint8_t b = _reversed ? G(c) : B(c); - switch (pix % 3) { // get only the single channel - case 0: c = RGBW32(g, g, g, g); break; - case 1: c = RGBW32(r, r, r, r); break; - case 2: c = RGBW32(b, b, b, b); break; - } - } - if (_type == TYPE_WS2812_WWA) { - uint8_t w = R(c) | G(c); - c = RGBW32(w, w, 0, w); - } + //const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); // TODO: do we need the color order? where is getpixelcolor used? + uint32_t rawC = _busPtr->getPixelColor(pix); + uint32_t c = restoreColorLossy(rawC, _totalBusBri); return c; } @@ -339,7 +389,7 @@ size_t BusDigital::getPins(uint8_t* pinArray) const { } size_t BusDigital::getBusSize() const { - return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer + return sizeof(BusDigital) + (isOk() && _busPtr ? _busPtr->getEncodeBufferSize() : 0); // does not include common I2S DMA buffer } void BusDigital::setColorOrder(uint8_t colorOrder) { @@ -360,11 +410,12 @@ std::vector BusDigital::getLEDTypes() { {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, {TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")}, {TYPE_TM1814, "D", PSTR("TM1814 RGBW")}, + {TYPE_TM1815, "D", PSTR("TM1815 RGBW")}, {TYPE_FW1906, "D", PSTR("FW1906/WS2811 RGBCCT")}, {TYPE_WS2805, "D", PSTR("WS2805 RGBCCT")}, {TYPE_SM16825, "D", PSTR("SM16825 RGBCCT")}, {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, - {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored + {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber = WW {TYPE_WS2801, "2P", PSTR("WS2801 RGB")}, {TYPE_APA102, "2P", PSTR("APA102 RGB")}, {TYPE_LPD8806, "2P", PSTR("LPD8806 RGB")}, @@ -373,21 +424,28 @@ std::vector BusDigital::getLEDTypes() { }; } -bool BusDigital::isI2S() { - return (_iType & 0x01) == 0; // I2S types have even iType values +bool BusDigital::isParHw() { + return _driverType == BUSDRV_PARHW; // I2S/LCD/ParallelSPI/PARLIO are all parallel async drivers } void BusDigital::begin() { if (!_valid) return; - PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz); + if (!_busPtr->begin()) { cleanup(); return; } + // TM1914: write the static mode-setting prefix once after the encode buffer is allocated. + if (_type == TYPE_TM1914) + _busPtr->updatePrefix(TM1914_PREFIX, sizeof(TM1914_PREFIX)); } void BusDigital::cleanup() { DEBUGBUS_PRINTLN(F("Digital Cleanup.")); - PolyBus::cleanup(_busPtr, _iType); - _iType = I_NONE; + delete _pCustomConfig; + _pCustomConfig = nullptr; + if (_busPtr) { + _busPtr->end(); + delete _busPtr; + _busPtr = nullptr; + } _valid = false; - _busPtr = nullptr; PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -805,7 +863,7 @@ void BusNetwork::cleanup() { DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); d_free(_data); _data = nullptr; - _type = I_NONE; + _type = TYPE_NONE; _valid = false; } @@ -1252,7 +1310,38 @@ BusPlaceholder::BusPlaceholder(const BusConfig &bc) , _milliAmpsMax(bc.milliAmpsMax) , _text(bc.text) { + _busSpeedFactor = bc.busSpeedFactor; // preserve so config is restored faithfully on reboot + if (bc.custom.active()) _pCustomConfig = new CustomBusConfig(bc.custom); memcpy(_pins, bc.pins, sizeof(_pins)); + PinOwner pinOwner = PinOwner::None; + if (Bus::isDigital(bc.type) && bc.type != TYPE_ONOFF) pinOwner = PinOwner::BusDigital; + else if (Bus::isOnOff(bc.type)) pinOwner = PinOwner::BusOnOff; + else if (Bus::isPWM(bc.type)) pinOwner = PinOwner::BusPwm; + #ifdef WLED_ENABLE_HUB75MATRIX + else if (Bus::isHub75(bc.type)) pinOwner = PinOwner::HUB75; + #endif + + size_t nPins = Bus::getNumberOfPins(bc.type); + for (size_t i = 0; i < nPins; i++) { + if (_pins[i] != 255) PinManager::allocatePin(_pins[i], true, pinOwner); + } +} + +void BusPlaceholder::cleanup() { + delete _pCustomConfig; + _pCustomConfig = nullptr; + PinOwner pinOwner = PinOwner::None; + if (Bus::isDigital(_type) && _type != TYPE_ONOFF) pinOwner = PinOwner::BusDigital; + else if (Bus::isOnOff(_type)) pinOwner = PinOwner::BusOnOff; + else if (Bus::isPWM(_type)) pinOwner = PinOwner::BusPwm; + #ifdef WLED_ENABLE_HUB75MATRIX + else if (Bus::isHub75(_type)) pinOwner = PinOwner::HUB75; + #endif + + size_t nPins = Bus::getNumberOfPins(_type); + for (size_t i = 0; i < nPins; i++) { + if (_pins[i] != 255) PinManager::deallocatePin(_pins[i], pinOwner); + } } size_t BusPlaceholder::getPins(uint8_t* pinArray) const { @@ -1263,22 +1352,6 @@ size_t BusPlaceholder::getPins(uint8_t* pinArray) const { return nPins; } -//utility to get the approx. memory usage of a given BusConfig inclduding segmentbuffer and global buffer (4 bytes per pixel) -size_t BusConfig::memUsage() const { - size_t mem = (count + skipAmount) * 8; // 8 bytes per pixel for segment + global buffer - if (Bus::isVirtual(type)) { - mem += sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); // note: getNumberOfChannels() includes CCT channel if applicable but virtual buses do not use CCT channel buffer - } else if (Bus::isDigital(type)) { - // if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here - mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType); - } else if (Bus::isOnOff(type)) { - mem += sizeof(BusOnOff); - } else { - mem += sizeof(BusPwm); - } - return mem; -} - int BusManager::add(const BusConfig &bc, bool placeholder) { DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); unsigned digital = 0; @@ -1291,7 +1364,7 @@ int BusManager::add(const BusConfig &bc, bool placeholder) { } digital += (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type)); analog += (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0); - if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here + if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; if (placeholder) { busses.push_back(make_unique(bc)); } else if (Bus::isVirtual(bc.type)) { @@ -1307,6 +1380,15 @@ int BusManager::add(const BusConfig &bc, bool placeholder) { } else { busses.push_back(make_unique(bc)); } + // If the newly constructed bus failed to acquire resources (OOM, DMA failure, pin conflict, etc.) + // replace it with a BusPlaceholder so the configuration is preserved for the next reboot, + // where memory may be available and the bus can initialize successfully. + if (!placeholder && !busses.back()->isOk()) { + DEBUG_PRINTF_P(PSTR("Bus %u (type %u) failed initialization; replacing with placeholder.\n"), (unsigned)busses.size(), (unsigned)bc.type); + errorFlag = ERR_NORAM; + busses.back() = make_unique(bc); + } + _lastBusCache = busses[0].get(); // set cache to first bus, can be a placeholder but pointer must be valid (saves us pointer checking in hot path) return busses.size(); } @@ -1317,7 +1399,12 @@ static String LEDTypesToJson(const std::vector& types) { // capabilities follows similar pattern as JSON API int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4 | Bus::mustRefresh(type.id)<<5; char str[256]; - sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\"},"), type.id, capabilities, type.type, type.name); + WLEDpixelBus::LedTiming timing = WLEDpixelBus::getProtocol(type.id); + sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\",t0h:%u,t0l:%u,t1h:%u,t1l:%u,trst:%u},"), + type.id, capabilities, type.type, type.name, + (unsigned)timing.t0h_ns, (unsigned)timing.t0l_ns, + (unsigned)timing.t1h_ns, (unsigned)timing.t1l_ns, + (unsigned)timing.reset_us); json += str; } return json; @@ -1339,19 +1426,19 @@ String BusManager::getLEDTypesJSONString() { return json; } -uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { - return PolyBus::getI(busType, pins, driverPreference); +bool BusManager::allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { + return PixelBusAllocator::allocateHardware(busType, pins, driverType); } + //do not call this method from system context (network callback) void BusManager::removeAll() { DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); + _lastBusCache = nullptr; // Reset cache before destroying buses to avoid dangling pointer UB busses.clear(); - #ifndef ESP8266 // Reset channel tracking for fresh allocation - PolyBus::resetChannelTracking(); - #endif + PixelBusAllocator::resetChannelTracking(); } #ifdef ESP32_DATA_IDLE_HIGH @@ -1370,7 +1457,7 @@ void BusManager::esp32RMTInvertIdle() { unsigned u = 0; for (auto &bus : busses) { if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue; - if (static_cast(bus.get())->isI2S()) continue; + if (static_cast(bus.get())->isParHw()) continue; if (u >= WLED_MAX_RMT_CHANNELS) return; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(rmt); @@ -1438,14 +1525,24 @@ void BusManager::off() { void BusManager::show() { applyABL(); // apply brightness limit, updates _gMilliAmpsUsed for (auto &bus : busses) { - bus->show(); + if (bus->getDriverType() == BUSDRV_BITBANG) bus->show(); // run bit bang buses first, they do block interrupts and cant run in parallel with other types + } + for (auto &bus : busses) { + if (bus->getDriverType() != BUSDRV_BITBANG) bus->show(); } } void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { + if (_lastBusCache->containsPixel(pix)) { + _lastBusCache->setPixelColor(pix - _lastBusCache->getStart(), c); + return; + } for (auto &bus : busses) { - if (!bus->containsPixel(pix)) continue; - bus->setPixelColor(pix - bus->getStart(), c); + if (bus->containsPixel(pix)) { + bus->setPixelColor(pix - bus->getStart(), c); + _lastBusCache = bus.get(); + return; + } } } @@ -1466,6 +1563,17 @@ uint32_t BusManager::getPixelColor(unsigned pix) { return 0; } +void BusManager::clearPixels(size_t n) { + // memset each bus encode buffer directly — vastly faster than N encode calls. + // All-zero bytes encode as black for all supported LED protocols (RGB, RGBW, GRB, I2S 4-step, etc). + unsigned covered = 0; + for (auto &bus : busses) { + if (covered >= n) break; + bus->clearPixels(); + covered += bus->getLength(); + } +} + bool BusManager::canAllShow() { for (const auto &bus : busses) if (!bus->canShow()) return false; return true; @@ -1525,17 +1633,13 @@ void BusManager::applyABL() { if (_gMilliAmpsMax > 0) { uint8_t newBri = 255; uint32_t globalMax = _gMilliAmpsMax > MA_FOR_ESP ? _gMilliAmpsMax - MA_FOR_ESP : 1; // subtract ESP current consumption, fully limit if too low - if (globalMax > totalLEDs) { // check if budget is larger than standby current - if (milliAmpsSum > globalMax) { - newBri = globalMax * 255 / milliAmpsSum + 1; // scale brightness down to stay in current limit, +1 to avoid 0 brightness - milliAmpsSum = globalMax; // update total used current - } - } else { - newBri = 1; // limit too low, set brightness to minimum - milliAmpsSum = totalLEDs; // estimate total used current as minimum + if (milliAmpsSum > globalMax) { // check if global current limit is exceeded + newBri = globalMax * 255 / milliAmpsSum + 1; // scale brightness down to stay in current limit, +1 to avoid 0 brightness + if (newBri == 0) newBri = 1; // safety clamp + milliAmpsSum = globalMax; // update total used current } - // apply brightness limit to each bus, if its 255 it will only reset _colorSum + // apply brightness limit to each bus; resets bus _colorSum for next frame for (auto &bus : busses) { if (bus->isDigital() && bus->isOk()) { BusDigital &busd = static_cast(*bus); @@ -1552,15 +1656,18 @@ void BusManager::applyABL() { ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } +// PixelBusAllocator channel tracking for dynamic allocation #ifndef ESP8266 -// PolyBus channel tracking for dynamic allocation -bool PolyBus::_useParallelI2S = false; -uint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check -uint8_t PolyBus::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() -uint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check -uint8_t PolyBus::_parallelBusItype = 0; // type I_NONE -uint8_t PolyBus::_2PchannelsAssigned = 0; +uint8_t PixelBusAllocator::_rmtChannelsAssigned = 0; // number of RMT channels assigned during allocateHardware() +uint8_t PixelBusAllocator::_parHwChannelsAssigned = 0; // parallel output channels assigned (I2S/LCD/SPI/PARLIO depending on chip) +uint8_t PixelBusAllocator::_parHwBusType = 0; +uint8_t PixelBusAllocator::_bitBangChannelsAssigned = 0; +uint8_t PixelBusAllocator::_bitBangBusType = 0; +uint8_t PixelBusAllocator::_hardwareSPIused = 0; +#else +uint8_t PixelBusAllocator::_bitBangBusType = 0; #endif + // Bus static member definition int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin() int8_t Bus::_cctBlend = 0; // -128 to +127 @@ -1572,3 +1679,5 @@ std::vector> BusManager::busses; uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; bool BusManager::_useABL = false; +Bus* BusManager::_lastBusCache = nullptr; // cache for setPixelColor() fast path + diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index eeb10cff24..c0c6935f3f 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -14,6 +14,7 @@ */ #include "const.h" +#include "src/WLEDpixelBus/WLEDpixelBus.h" #include "pin_manager.h" #include #include @@ -63,13 +64,6 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb); #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) #define UNSET_BIT(var,bit) ((var)&=(~(uint16_t)(0x0001<<(bit)))) -#define NUM_ICS_WS2812_1CH_3X(len) (((len)+2)/3) // 1 WS2811 IC controls 3 zones (each zone has 1 LED, W) -#define IC_INDEX_WS2812_1CH_3X(i) ((i)/3) - -#define NUM_ICS_WS2812_2CH_3X(len) (((len)+1)*2/3) // 2 WS2811 ICs control 3 zones (each zone has 2 LEDs, CW and WW) -#define IC_INDEX_WS2812_2CH_3X(i) ((i)*2/3) -#define WS2812_2CH_3X_SPANS_2_ICS(i) ((i)&0x01) // every other LED zone is on two different ICs - struct BusConfig; // forward declaration // Defines an LED Strip and its color ordering. @@ -108,6 +102,29 @@ typedef struct { const char *name; } LEDType; +// Optional bus channel/timing override, attachable to ANY single-wire digital numChannels == 0 means not used +struct CustomBusConfig { + // channelColors[i]: 0=Unused, 1=R, 2=G, 3=B, 4=W, 5=WW, 6=CW TODO: add a 7th channel for amber? Also WW/CW are not treated differently if not both are set (see #5654 for reference) + uint8_t numChannels = 0; // 0 = not set (use native channel count/layout for the bus's LED type) + uint8_t channelColors[6] = {2, 1, 3, 0, 0, 0}; // default GRB (matches UI default), only used when numChannels != 0 + uint8_t invertMask = 0; // bitmask: bit i = invert channel i output level + bool is16bit = false; // true = 2 wire bytes per channel (like SM16825) + bool invertOutput = false; // invert the hardware output signal polarity + uint16_t t0h = 300; // '0' bit high time, ns + uint16_t t0l = 900; // '0' bit low time, ns + uint16_t t1h = 700; // '1' bit high time, ns + uint16_t t1l = 500; // '1' bit low time, ns + uint16_t trst = 300; // reset/latch time, µs + inline bool active() const { return numChannels != 0; } // true when this override should be applied instead of the native layout +}; + + +// Driver preference for digital LED buses: stored in BusConfig::driverType and BusDigital::_driverType +enum BusDriverType : uint8_t { + BUSDRV_RMT = 0, // RMT peripheral (default on most ESP32 variants) + BUSDRV_PARHW = 1, // parallel output (chip-dependent): ESP32/S2/S3 use I2S-LCD, C3 uses parallel SPI, C6/H2/C5/P4 use PARLIO + BUSDRV_BITBANG = 2, // parallel bit-bang GPIO (ESP32 only) +}; //parent class of BusDigital, BusPwm, and BusNetwork class Bus { @@ -115,7 +132,7 @@ class Bus { Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false) : _type(type) , _bri(255) - , _NPBbri(255) + , _totalBusBri(255) , _start(start) , _len(std::max(len,(uint16_t)1)) , _reversed(reversed) @@ -130,6 +147,7 @@ class Bus { virtual void begin() {}; virtual void show() = 0; virtual bool canShow() const { return true; } + virtual void clearPixels() {} // zero encode buffer to black (no-op for non-digital buses) virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(unsigned pix, uint32_t c) = 0; virtual void setBrightness(uint8_t b) { _bri = b; }; @@ -143,7 +161,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 uint8_t getDriverType() const { return BUSDRV_RMT; } // default to RMT (overridden by BusDigital) virtual size_t getBusSize() const { return sizeof(Bus); } // currently unused virtual const String getCustomText() const { return String(); } @@ -164,21 +182,23 @@ class Bus { inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } inline size_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } inline uint16_t getStart() const { return _start; } - inline uint8_t getType() const { return _type; } + inline uint8_t getType() const { return _type; } // 7-bit bus index, highest bit is "off refresh" inline bool isOk() const { return _valid; } inline bool isReversed() const { return _reversed; } inline bool isOffRefreshRequired() const { return _needsRefresh; } inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + inline uint8_t getBusSpeedFactor() const { return _busSpeedFactor; } + virtual const CustomBusConfig& getCustomBusConfig() const; // valid whenever result.active() == true; returns a static default (numChannels=0) otherwise static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK; for HUB75 the 5 slots store config params (panelW, panelH, chain, rows, cols), not GPIO pins - static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static constexpr size_t getNumberOfChannels(uint8_t type) { return (hasWhite(type) + 3*hasRGB(type) + hasCCT(type)); } static constexpr bool hasRGB(uint8_t type) { - return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); + return !((type >= TYPE_WS2812_1CH_X3 && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } static constexpr bool hasWhite(uint8_t type) { - return (type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || - type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || + return (type >= TYPE_WS2812_1CH_X3 && type <= TYPE_WS2812_WWA) || + type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_TM1815 || type == TYPE_UCS8904 || type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel @@ -196,7 +216,7 @@ class Bus { static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } static constexpr bool isHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } - static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } + static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814 || type == TYPE_TM1815; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } static inline int16_t getCCT() { return _cct; } @@ -216,7 +236,7 @@ class Bus { protected: uint8_t _type; uint8_t _bri; // bus brightness - uint8_t _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL) + uint8_t _totalBusBri; // total brightness applied to colors in bus buffers (_bri + ABL) uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; @@ -241,6 +261,8 @@ class Bus { // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) static int8_t _cctBlend; + uint8_t _busSpeedFactor = 100; // percent, default 100 = default timings + uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const; }; @@ -252,6 +274,7 @@ class BusDigital : public Bus { void show() override; bool canShow() const override; + void clearPixels() override; void setStatusPixel(uint32_t c) override; [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; @@ -265,27 +288,29 @@ class BusDigital : public Bus { uint16_t getMaxCurrent() const override { return _milliAmpsMax; } uint8_t getDriverType() const override { return _driverType; } void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; } + void setBrightness(uint8_t b) override; void estimateCurrent(); // estimate used current from summed colors void applyBriLimit(uint8_t newBri); size_t getBusSize() const override; - bool isI2S(); // true if this bus uses I2S driver + bool isParHw(); // true if this bus uses a parallel hardware driver (I2S/LCD/SPI/PARLIO) void begin() override; void cleanup(); + const CustomBusConfig& getCustomBusConfig() const override { return _pCustomConfig ? *_pCustomConfig : Bus::getCustomBusConfig(); } // valid whenever result.active() == true static std::vector getLEDTypes(); private: uint8_t _skip; - uint8_t _colorOrder; - uint8_t _pins[2]; - uint8_t _iType; - uint8_t _driverType; // 0=RMT (default), 1=I2S + uint8_t _colorOrder; // TODO: is this still used? color order is now done in bus + uint8_t _pins[2] = {255, 255}; + uint8_t _driverType; // BusDriverType: BUSDRV_RMT / BUSDRV_PARHW / BUSDRV_BITBANG uint16_t _frequencykHz; uint16_t _milliAmpsMax; uint8_t _milliAmpsPerLed; uint16_t _milliAmpsLimit; - uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current - void *_busPtr; + uint32_t _colorSum = 0; // sum of brightness-scaled channel bytes; updated in setPixelColor() when ABL active + WLEDpixelBus::PixelBus* _busPtr = nullptr; + CustomBusConfig* _pCustomConfig = nullptr; // allocated only when custom.active() == true static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -345,8 +370,8 @@ class BusOnOff : public Bus { static std::vector getLEDTypes(); private: - uint8_t _pin; - uint8_t _data; + uint8_t _pin = 255; + uint8_t _data = 0; }; @@ -386,6 +411,8 @@ class BusNetwork : public Bus { class BusPlaceholder : public Bus { public: BusPlaceholder(const BusConfig &bc); + ~BusPlaceholder() { cleanup(); } + void cleanup(); // Actual calls are stubbed out void setPixelColor(unsigned pix, uint32_t c) override {}; @@ -401,6 +428,7 @@ class BusPlaceholder : public Bus { uint8_t getDriverType() const override { return _driverType; } const String getCustomText() const override { return _text; } bool isPlaceholder() const override { return true; } + const CustomBusConfig& getCustomBusConfig() const override { return _pCustomConfig ? *_pCustomConfig : Bus::getCustomBusConfig(); } size_t getBusSize() const override { return sizeof(BusPlaceholder); } @@ -413,6 +441,7 @@ class BusPlaceholder : public Bus { uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; String _text; + CustomBusConfig* _pCustomConfig = nullptr; // allocated only when custom.active() == true }; #ifdef WLED_ENABLE_HUB75MATRIX @@ -465,11 +494,12 @@ 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 + uint8_t driverType; // BusDriverType: BUSDRV_RMT / BUSDRV_PARHW / BUSDRV_BITBANG String text; + uint8_t busSpeedFactor; // percent (100 = default) + CustomBusConfig custom; // optional override, active when custom.active() == true (1P digital LED types only) - 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 = "") + 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=BUSDRV_RMT, String sometext = "", uint8_t bsf = 100) : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) @@ -480,8 +510,8 @@ struct BusConfig { , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) , driverType(driver) - , iType(0) // default to I_NONE , text(sometext) + , busSpeedFactor(bsf) { 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) @@ -496,7 +526,7 @@ struct BusConfig { (int)autoWhite, (int)frequency, (int)milliAmpsPerLed, (int)milliAmpsMax, - driverType == 0 ? "RMT" : "I2S" + driverType == BUSDRV_RMT ? "RMT" : driverType == BUSDRV_PARHW ? "PAR" : "BitBang" ); } @@ -511,8 +541,6 @@ struct BusConfig { if (start + count > total) total = start + count; return true; } - - size_t memUsage() const; }; @@ -533,6 +561,7 @@ namespace BusManager { extern uint16_t _gMilliAmpsUsed; extern uint16_t _gMilliAmpsMax; extern bool _useABL; + extern Bus* _lastBusCache; #ifdef ESP32_DATA_IDLE_HIGH void esp32RMTInvertIdle() ; @@ -550,7 +579,7 @@ namespace BusManager { void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized void applyABL(); // apply automatic brightness limiter, global or per bus - uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp + bool allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType); // workaround to access PolyBus function //do not call this method from system context (network callback) void removeAll(); @@ -561,6 +590,7 @@ namespace BusManager { [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c); [[gnu::hot]] uint32_t getPixelColor(unsigned pix); + void clearPixels(size_t n); // zero all bus encode buffers for first n physical pixels void show(); bool canAllShow(); inline void setStatusPixel(uint32_t c) { for (auto &bus : busses) bus->setStatusPixel(c);} diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d1c9afd655..d8e8d8e77f 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,1415 +1,191 @@ -#pragma once +#pragma once #ifndef BusWrapper_h #define BusWrapper_h -//#define NPB_CONF_4STEP_CADENCE -#include "NeoPixelBus.h" - -#include "wled_boards.h" // pull in board-specific capability defines - -#ifdef CONFIG_IDF_TARGET_ESP32C5 -// see https://github.com/wled/WLED/pull/5048#issuecomment-3937888182 -#warning "second output on -C5 may fail, until NPB channel handling is fixed" +#include "src/WLEDpixelBus/WLEDpixelBus.h" +#include "src/WLEDpixelBus/WLEDpixelBus_SPI.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" +#include "src/WLEDpixelBus/WLEDpixelBus_I2S.h" +#include "src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h" +#include "src/WLEDpixelBus/WLEDpixelBus_PARLIO.h" +#include "src/WLEDpixelBus/WLEDpixelBus_BitBang.h" +#elif defined(ARDUINO_ARCH_ESP8266) +#include "src/WLEDpixelBus/WLEDpixelBus_ESP8266.h" +#include "src/WLEDpixelBus/WLEDpixelBus_BitBang.h" #endif -//Hardware SPI Pins +//Hardware SPI Pins (ESP8266 only; ESP32 uses bus allocation order to detect HSPI) #define P_8266_HS_MOSI 13 #define P_8266_HS_CLK 14 -#define P_32_HS_MOSI 13 -#define P_32_HS_CLK 14 -#define P_32_VS_MOSI 23 -#define P_32_VS_CLK 18 - -//The dirty list of possible bus types. Quite a lot... -#define I_NONE 0 -//ESP8266 RGB -#define I_8266_U0_NEO_3 1 -#define I_8266_U1_NEO_3 2 -#define I_8266_DM_NEO_3 3 -#define I_8266_BB_NEO_3 4 -//RGBW -#define I_8266_U0_NEO_4 5 -#define I_8266_U1_NEO_4 6 -#define I_8266_DM_NEO_4 7 -#define I_8266_BB_NEO_4 8 -//400Kbps -#define I_8266_U0_400_3 9 -#define I_8266_U1_400_3 10 -#define I_8266_DM_400_3 11 -#define I_8266_BB_400_3 12 -//TM1814 (RGBW) -#define I_8266_U0_TM1_4 13 -#define I_8266_U1_TM1_4 14 -#define I_8266_DM_TM1_4 15 -#define I_8266_BB_TM1_4 16 -//TM1829 (RGB) -#define I_8266_U0_TM2_3 17 -#define I_8266_U1_TM2_3 18 -#define I_8266_DM_TM2_3 19 -#define I_8266_BB_TM2_3 20 -//UCS8903 (RGB) -#define I_8266_U0_UCS_3 21 -#define I_8266_U1_UCS_3 22 -#define I_8266_DM_UCS_3 23 -#define I_8266_BB_UCS_3 24 -//UCS8904 (RGBW) -#define I_8266_U0_UCS_4 25 -#define I_8266_U1_UCS_4 26 -#define I_8266_DM_UCS_4 27 -#define I_8266_BB_UCS_4 28 -//FW1906 GRBCCT -#define I_8266_U0_FW6_5 29 -#define I_8266_U1_FW6_5 30 -#define I_8266_DM_FW6_5 31 -#define I_8266_BB_FW6_5 32 -//ESP8266 APA106 -#define I_8266_U0_APA106_3 33 -#define I_8266_U1_APA106_3 34 -#define I_8266_DM_APA106_3 35 -#define I_8266_BB_APA106_3 36 -//WS2805 (RGBCCT) -#define I_8266_U0_2805_5 37 -#define I_8266_U1_2805_5 38 -#define I_8266_DM_2805_5 39 -#define I_8266_BB_2805_5 40 -//TM1914 (RGB) -#define I_8266_U0_TM1914_3 41 -#define I_8266_U1_TM1914_3 42 -#define I_8266_DM_TM1914_3 43 -#define I_8266_BB_TM1914_3 44 -//SM16825 (RGBCCT) -#define I_8266_U0_SM16825_5 45 -#define I_8266_U1_SM16825_5 46 -#define I_8266_DM_SM16825_5 47 -#define I_8266_BB_SM16825_5 48 - -/*** ESP32 Neopixel methods ***/ -//RGB -#define I_32_RN_NEO_3 1 -#define I_32_I2_NEO_3 2 -//RGBW -#define I_32_RN_NEO_4 5 -#define I_32_I2_NEO_4 6 -//400Kbps -#define I_32_RN_400_3 9 -#define I_32_I2_400_3 10 -//TM1814 (RGBW) -#define I_32_RN_TM1_4 13 -#define I_32_I2_TM1_4 14 -//TM1829 (RGB) -#define I_32_RN_TM2_3 17 -#define I_32_I2_TM2_3 18 -//UCS8903 (RGB) -#define I_32_RN_UCS_3 21 -#define I_32_I2_UCS_3 22 -//UCS8904 (RGBW) -#define I_32_RN_UCS_4 25 -#define I_32_I2_UCS_4 26 -//FW1906 GRBCCT 6 color channels -#define I_32_RN_FW6_5 29 -#define I_32_I2_FW6_5 30 -//APA106 -#define I_32_RN_APA106_3 33 -#define I_32_I2_APA106_3 34 -//WS2805 (RGBCCT) -#define I_32_RN_2805_5 37 -#define I_32_I2_2805_5 38 -//TM1914 (RGB) -#define I_32_RN_TM1914_3 41 -#define I_32_I2_TM1914_3 42 -//SM16825 (RGBCCT) -#define I_32_RN_SM16825_5 45 -#define I_32_I2_SM16825_5 46 - -//APA102 -#define I_HS_DOT_3 101 //hardware SPI -#define I_SS_DOT_3 102 //soft SPI - -//LPD8806 -#define I_HS_LPD_3 103 -#define I_SS_LPD_3 104 - -//WS2801 -#define I_HS_WS1_3 105 -#define I_SS_WS1_3 106 - -//P9813 -#define I_HS_P98_3 107 -#define I_SS_P98_3 108 -//LPD6803 -#define I_HS_LPO_3 109 -#define I_SS_LPO_3 110 +// Use single RMT memory block per channel — allows RMT RX channels alongside TX. +//#define RMT_USE_SINGLE_MEM_BLOCK -// In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly -// unfortunately that may apply Gamma correction to pre-calculated palettes which is undesired - -/*** ESP8266 Neopixel methods ***/ -#ifdef ESP8266 -//RGB -#define B_8266_U0_NEO_3 NeoPixelBus //3 chan, esp8266, gpio1 -#define B_8266_U1_NEO_3 NeoPixelBus //3 chan, esp8266, gpio2 -#define B_8266_DM_NEO_3 NeoPixelBus //3 chan, esp8266, gpio3 -#define B_8266_BB_NEO_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) -//RGBW -#define B_8266_U0_NEO_4 NeoPixelBus //4 chan, esp8266, gpio1 -#define B_8266_U1_NEO_4 NeoPixelBus //4 chan, esp8266, gpio2 -#define B_8266_DM_NEO_4 NeoPixelBus //4 chan, esp8266, gpio3 -#define B_8266_BB_NEO_4 NeoPixelBus //4 chan, esp8266, bb (any pin) -//400Kbps -#define B_8266_U0_400_3 NeoPixelBus //3 chan, esp8266, gpio1 -#define B_8266_U1_400_3 NeoPixelBus //3 chan, esp8266, gpio2 -#define B_8266_DM_400_3 NeoPixelBus //3 chan, esp8266, gpio3 -#define B_8266_BB_400_3 NeoPixelBus //3 chan, esp8266, bb (any pin) -//TM1814 (RGBW) -#define B_8266_U0_TM1_4 NeoPixelBus -#define B_8266_U1_TM1_4 NeoPixelBus -#define B_8266_DM_TM1_4 NeoPixelBus -#define B_8266_BB_TM1_4 NeoPixelBus -//TM1829 (RGB) -#define B_8266_U0_TM2_3 NeoPixelBus -#define B_8266_U1_TM2_3 NeoPixelBus -#define B_8266_DM_TM2_3 NeoPixelBus -#define B_8266_BB_TM2_3 NeoPixelBus -//UCS8903 -#define B_8266_U0_UCS_3 NeoPixelBus //3 chan, esp8266, gpio1 -#define B_8266_U1_UCS_3 NeoPixelBus //3 chan, esp8266, gpio2 -#define B_8266_DM_UCS_3 NeoPixelBus //3 chan, esp8266, gpio3 -#define B_8266_BB_UCS_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) -//UCS8904 RGBW -#define B_8266_U0_UCS_4 NeoPixelBus //4 chan, esp8266, gpio1 -#define B_8266_U1_UCS_4 NeoPixelBus //4 chan, esp8266, gpio2 -#define B_8266_DM_UCS_4 NeoPixelBus //4 chan, esp8266, gpio3 -#define B_8266_BB_UCS_4 NeoPixelBus //4 chan, esp8266, bb (any pin) -//APA106 -#define B_8266_U0_APA106_3 NeoPixelBus //3 chan, esp8266, gpio1 -#define B_8266_U1_APA106_3 NeoPixelBus //3 chan, esp8266, gpio2 -#define B_8266_DM_APA106_3 NeoPixelBus //3 chan, esp8266, gpio3 -#define B_8266_BB_APA106_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) -//FW1906 GRBCCT -#define B_8266_U0_FW6_5 NeoPixelBus //esp8266, gpio1 -#define B_8266_U1_FW6_5 NeoPixelBus //esp8266, gpio2 -#define B_8266_DM_FW6_5 NeoPixelBus //esp8266, gpio3 -#define B_8266_BB_FW6_5 NeoPixelBus //esp8266, bb -//WS2805 GRBCCT -#define B_8266_U0_2805_5 NeoPixelBus //esp8266, gpio1 -#define B_8266_U1_2805_5 NeoPixelBus //esp8266, gpio2 -#define B_8266_DM_2805_5 NeoPixelBus //esp8266, gpio3 -#define B_8266_BB_2805_5 NeoPixelBus //esp8266, bb -//TM1914 (RGB) -#define B_8266_U0_TM1914_3 NeoPixelBus -#define B_8266_U1_TM1914_3 NeoPixelBus -#define B_8266_DM_TM1914_3 NeoPixelBus -#define B_8266_BB_TM1914_3 NeoPixelBus -//Sm16825 (RGBCCT) -#define B_8266_U0_SM16825_5 NeoPixelBus -#define B_8266_U1_SM16825_5 NeoPixelBus -#define B_8266_DM_SM16825_5 NeoPixelBus -#define B_8266_BB_SM16825_5 NeoPixelBus -#endif - -/*** ESP32 Neopixel methods ***/ -#ifdef ARDUINO_ARCH_ESP32 -#if defined(WLED_HAS_PARALLEL_I2S) -// C3: I2S0 and I2S1 methods not supported (has one I2S bus) -// S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus) -// S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (has two I2S buses) -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 -#if defined(CONFIG_IDF_TARGET_ESP32S3) - // S3 will always use LCD parallel output - typedef X8Ws2812xMethod X1Ws2812xMethod; - typedef X8Sk6812Method X1Sk6812Method; - typedef X8400KbpsMethod X1400KbpsMethod; - typedef X8800KbpsMethod X1800KbpsMethod; - typedef X8Tm1814Method X1Tm1814Method; - typedef X8Tm1829Method X1Tm1829Method; - typedef X8Apa106Method X1Apa106Method; - typedef X8Ws2805Method X1Ws2805Method; - typedef X8Tm1914Method X1Tm1914Method; -#elif defined(CONFIG_IDF_TARGET_ESP32S2) - // S2 will use I2S0 - typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod; - typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method; - typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod; - typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod; - typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method; - typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method; - typedef NeoEsp32I2s0Apa106Method X1Apa106Method; - typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method; - typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method; -#else - // regular ESP32 will use I2S1 - typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod; - typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method; - typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod; - typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod; - typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method; - typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method; - typedef NeoEsp32I2s1Apa106Method X1Apa106Method; - typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method; - typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method; -#endif -// RISC-V boards don't have I2S methods -#endif - -// RMT driver selection -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) -#define NeoEsp32RmtMethod(x) NeoEsp32RmtX ## x ## Method -#elif !defined(WLED_USE_SHARED_RMT) && !defined(__riscv) -#include -#define NeoEsp32RmtMethod(x) NeoEsp32RmtHIN ## x ## Method -#else -#define NeoEsp32RmtMethod(x) NeoEsp32RmtN ## x ## Method -#endif - -//RGB -#define B_32_RN_NEO_3 NeoPixelBus // ESP32, S2, S3, C3 -//#define B_32_IN_NEO_3 NeoPixelBus // ESP32 (dynamic I2S selection) -#define B_32_I2_NEO_3 NeoPixelBus // ESP32, S2, S3 (automatic I2S selection, see typedef above) -#define B_32_IP_NEO_3 NeoPixelBus // parallel I2S (ESP32, S2, S3) -//RGBW -#define B_32_RN_NEO_4 NeoPixelBus -#define B_32_I2_NEO_4 NeoPixelBus -#define B_32_IP_NEO_4 NeoPixelBus // parallel I2S -//400Kbps -#define B_32_RN_400_3 NeoPixelBus -#define B_32_I2_400_3 NeoPixelBus -#define B_32_IP_400_3 NeoPixelBus // parallel I2S -//TM1814 (RGBW) -#define B_32_RN_TM1_4 NeoPixelBus -#define B_32_I2_TM1_4 NeoPixelBus -#define B_32_IP_TM1_4 NeoPixelBus // parallel I2S -//TM1829 (RGB) -#define B_32_RN_TM2_3 NeoPixelBus -#define B_32_I2_TM2_3 NeoPixelBus -#define B_32_IP_TM2_3 NeoPixelBus // parallel I2S -//UCS8903 -#define B_32_RN_UCS_3 NeoPixelBus -#define B_32_I2_UCS_3 NeoPixelBus -#define B_32_IP_UCS_3 NeoPixelBus // parallel I2S -//UCS8904 -#define B_32_RN_UCS_4 NeoPixelBus -#define B_32_I2_UCS_4 NeoPixelBus -#define B_32_IP_UCS_4 NeoPixelBus// parallel I2S -//APA106 -#define B_32_RN_APA106_3 NeoPixelBus -#define B_32_I2_APA106_3 NeoPixelBus -#define B_32_IP_APA106_3 NeoPixelBus // parallel I2S -//FW1906 GRBCCT 6 color channels -#define B_32_RN_FW6_5 NeoPixelBus -#define B_32_I2_FW6_5 NeoPixelBus -#define B_32_IP_FW6_5 NeoPixelBus // parallel I2S -//WS2805 RGBCCT -#define B_32_RN_2805_5 NeoPixelBus -#define B_32_I2_2805_5 NeoPixelBus -#define B_32_IP_2805_5 NeoPixelBus // parallel I2S -//TM1914 (RGB) -#define B_32_RN_TM1914_3 NeoPixelBus -#define B_32_I2_TM1914_3 NeoPixelBus -#define B_32_IP_TM1914_3 NeoPixelBus // parallel I2S -//Sm16825 (RGBCCT) -#define B_32_RN_SM16825_5 NeoPixelBus -#define B_32_I2_SM16825_5 NeoPixelBus -#define B_32_IP_SM16825_5 NeoPixelBus // parallel I2S -#endif - -//APA102 -#ifdef WLED_USE_ETHERNET -// fix for #2542 (by @BlackBird77) -#define B_HS_DOT_3 NeoPixelBus //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) -#else -#define B_HS_DOT_3 NeoPixelBus //hardware VSPI -#endif -#define B_SS_DOT_3 NeoPixelBus //soft SPI - -//LPD8806 -#define B_HS_LPD_3 NeoPixelBus -#define B_SS_LPD_3 NeoPixelBus - -//LPD6803 -#define B_HS_LPO_3 NeoPixelBus -#define B_SS_LPO_3 NeoPixelBus - -//WS2801 -#ifdef WLED_USE_ETHERNET -#define B_HS_WS1_3 NeoPixelBus>> -#else -#define B_HS_WS1_3 NeoPixelBus -#endif -#define B_SS_WS1_3 NeoPixelBus - -//P9813 -#define B_HS_P98_3 NeoPixelBus -#define B_SS_P98_3 NeoPixelBus - -// 48bit & 64bit to 24bit & 32bit RGB(W) conversion -#define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF)) -#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) - -//handles pointer type conversion for all possible bus types -class PolyBus { +class PixelBusAllocator { private: #ifndef ESP8266 - static bool _useParallelI2S; // use parallel I2S/LCD (8 channels) - static uint8_t _rmtChannelsAssigned; // RMT channel tracking for dynamic allocation - static uint8_t _rmtChannel; // physical RMT channel to use during bus creation - static uint8_t _i2sChannelsAssigned; // I2S channel tracking for dynamic allocation - static uint8_t _parallelBusItype; // parallel output does not allow mixed LED types, track I_Type - static uint8_t _2PchannelsAssigned; // 2-Pin (SPI) channel assigned: first one gets the hardware SPI, others use bit-banged SPI - // note on 2-Pin Types: all supported types except WS2801 use start/stop or latch frames, speed is not critical. WS2801 uses a 500us timeout and is prone to flickering if bit-banged too slow. - // TODO: according to #4863 using more than one bit-banged output can cause glitches even in APA102. This needs investigation as from a hardware perspective all but WS2801 should be immune to timing issues. + static uint8_t _rmtChannelsAssigned; + static uint8_t _parHwChannelsAssigned; // parallel output channels: I2S/LCD (ESP32/S2/S3), parallel SPI (C3), PARLIO (C6/H2/C5/P4) + static uint8_t _parHwBusType; // Track first parallel bus type to enforce parallel timing + static uint8_t _bitBangChannelsAssigned; + static uint8_t _bitBangBusType; // Track first BitBang type to enforce parallel timing + static uint8_t _hardwareSPIused; // number of hardware SPI's used, currently only one SPI output is supported. On C3, parallel SPI output takes priority + #else + static uint8_t _bitBangBusType; // Track first ESP8266 BitBang type to enforce parallel timing #endif public: - // initialize SPI bus speed for DotStar methods - template - static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz /* 0 == use default */) { - T dotStar_strip = static_cast(busPtr); - #ifdef ESP8266 - dotStar_strip->Begin(); + static void resetChannelTracking() { + #ifndef ESP8266 + _rmtChannelsAssigned = 0; + _parHwChannelsAssigned = 0; + _parHwBusType = 0; // TYPE_NONE + _bitBangChannelsAssigned = 0; + _bitBangBusType = 0; // TYPE_NONE + _hardwareSPIused = 0; + WLEDpixelBus::RmtBus::resetAutoChannel(); + #if (WLED_MAX_BB_CHANNELS > 0) + WLEDpixelBus::BitBangBus::resetChannels(); + #endif #else - if (miso == -1) miso = 127; // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670 - if (sck == -1 && mosi == -1) dotStar_strip->Begin(); - else dotStar_strip->Begin(sck, miso, mosi, ss); + _bitBangBusType = 0; // TYPE_NONE + WLEDpixelBus::BitBangBus::resetChannels(); #endif - if (clock_kHz) dotStar_strip->SetMethodSettings(NeoSpiSettings((uint32_t)clock_kHz*1000)); } - // Begin & initialize the PixelSettings for TM1814 strips. - template - static void beginTM1814(void* busPtr) { - T tm1814_strip = static_cast(busPtr); - tm1814_strip->Begin(); - // Max current for each LED (22.5 mA). - tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); - } + static bool allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { + if (!Bus::isDigital(busType)) return false; - template - static void beginTM1914(void* busPtr) { - T tm1914_strip = static_cast(busPtr); - tm1914_strip->Begin(); - tm1914_strip->SetPixelSettings(NeoTm1914Settings()); //NeoTm1914_Mode_DinFdinAutoSwitch, NeoTm1914_Mode_DinOnly, NeoTm1914_Mode_FdinOnly - } - - static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz /* only used by DotStar */) { - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_TM1_4: beginTM1814(busPtr); break; - case I_8266_U1_TM1_4: beginTM1814(busPtr); break; - case I_8266_DM_TM1_4: beginTM1814(busPtr); break; - case I_8266_BB_TM1_4: beginTM1814(busPtr); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_WS1_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_P98_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_TM1914_3: beginTM1914(busPtr); break; - case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; - case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; - case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->Begin(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_TM1_4: beginTM1814(busPtr); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; - case I_32_RN_TM1914_3: beginTM1914(busPtr); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; - // I2S1 bus or parellel buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - #endif - // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() - case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_LPO_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_WS1_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_P98_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - #endif - case I_SS_DOT_3: (static_cast(busPtr))->Begin(); break; - case I_SS_LPD_3: (static_cast(busPtr))->Begin(); break; - case I_SS_LPO_3: (static_cast(busPtr))->Begin(); break; - case I_SS_WS1_3: (static_cast(busPtr))->Begin(); break; - case I_SS_P98_3: (static_cast(busPtr))->Begin(); break; + if (Bus::is2Pin(busType)) { + // TODO: could check if an SPI is still available and set _hardwareSPIused to 1 to prevent hardware collision + // note: SPI is intentionally not reserved here as only one is supported and first come first serve is used in create() + return true; // for now, allow as many SPI buses as the UI allows. First one uses hardware SPI if available (on C3, if a parallel SPI output is used it takes priority) } - } - - static void* create(uint8_t busType, uint8_t* pins, uint16_t len) { - void* busPtr = nullptr; - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: busPtr = new B_8266_U0_NEO_3(len, pins[0]); break; - case I_8266_U1_NEO_3: busPtr = new B_8266_U1_NEO_3(len, pins[0]); break; - case I_8266_DM_NEO_3: busPtr = new B_8266_DM_NEO_3(len, pins[0]); break; - case I_8266_BB_NEO_3: busPtr = new B_8266_BB_NEO_3(len, pins[0]); break; - case I_8266_U0_NEO_4: busPtr = new B_8266_U0_NEO_4(len, pins[0]); break; - case I_8266_U1_NEO_4: busPtr = new B_8266_U1_NEO_4(len, pins[0]); break; - case I_8266_DM_NEO_4: busPtr = new B_8266_DM_NEO_4(len, pins[0]); break; - case I_8266_BB_NEO_4: busPtr = new B_8266_BB_NEO_4(len, pins[0]); break; - case I_8266_U0_400_3: busPtr = new B_8266_U0_400_3(len, pins[0]); break; - case I_8266_U1_400_3: busPtr = new B_8266_U1_400_3(len, pins[0]); break; - case I_8266_DM_400_3: busPtr = new B_8266_DM_400_3(len, pins[0]); break; - case I_8266_BB_400_3: busPtr = new B_8266_BB_400_3(len, pins[0]); break; - case I_8266_U0_TM1_4: busPtr = new B_8266_U0_TM1_4(len, pins[0]); break; - case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; - case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; - case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; - case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break; - case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break; - case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break; - case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break; - case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; - case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; - case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; - case I_8266_BB_UCS_3: busPtr = new B_8266_BB_UCS_3(len, pins[0]); break; - case I_8266_U0_UCS_4: busPtr = new B_8266_U0_UCS_4(len, pins[0]); break; - case I_8266_U1_UCS_4: busPtr = new B_8266_U1_UCS_4(len, pins[0]); break; - case I_8266_DM_UCS_4: busPtr = new B_8266_DM_UCS_4(len, pins[0]); break; - case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break; - case I_8266_U0_APA106_3: busPtr = new B_8266_U0_APA106_3(len, pins[0]); break; - case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break; - case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break; - case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break; - case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break; - case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break; - case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break; - case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break; - case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break; - case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; - case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; - case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; - case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break; - case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; - case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; - case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; - case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break; - case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break; - case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break; - case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break; - case I_32_I2_NEO_4: if (_useParallelI2S) busPtr = new B_32_IP_NEO_4(len, pins[0]); else busPtr = new B_32_I2_NEO_4(len, pins[0]); break; - case I_32_I2_400_3: if (_useParallelI2S) busPtr = new B_32_IP_400_3(len, pins[0]); else busPtr = new B_32_I2_400_3(len, pins[0]); break; - case I_32_I2_TM1_4: if (_useParallelI2S) busPtr = new B_32_IP_TM1_4(len, pins[0]); else busPtr = new B_32_I2_TM1_4(len, pins[0]); break; - case I_32_I2_TM2_3: if (_useParallelI2S) busPtr = new B_32_IP_TM2_3(len, pins[0]); else busPtr = new B_32_I2_TM2_3(len, pins[0]); break; - case I_32_I2_UCS_3: if (_useParallelI2S) busPtr = new B_32_IP_UCS_3(len, pins[0]); else busPtr = new B_32_I2_UCS_3(len, pins[0]); break; - case I_32_I2_UCS_4: if (_useParallelI2S) busPtr = new B_32_IP_UCS_4(len, pins[0]); else busPtr = new B_32_I2_UCS_4(len, pins[0]); break; - case I_32_I2_APA106_3: if (_useParallelI2S) busPtr = new B_32_IP_APA106_3(len, pins[0]); else busPtr = new B_32_I2_APA106_3(len, pins[0]); break; - case I_32_I2_FW6_5: if (_useParallelI2S) busPtr = new B_32_IP_FW6_5(len, pins[0]); else busPtr = new B_32_I2_FW6_5(len, pins[0]); break; - case I_32_I2_2805_5: if (_useParallelI2S) busPtr = new B_32_IP_2805_5(len, pins[0]); else busPtr = new B_32_I2_2805_5(len, pins[0]); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) busPtr = new B_32_IP_TM1914_3(len, pins[0]); else busPtr = new B_32_I2_TM1914_3(len, pins[0]); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) busPtr = new B_32_IP_SM16825_5(len, pins[0]); else busPtr = new B_32_I2_SM16825_5(len, pins[0]); break; - #endif - #endif - // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) - case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; - case I_SS_DOT_3: busPtr = new B_SS_DOT_3(len, pins[1], pins[0]); break; - case I_HS_LPD_3: busPtr = new B_HS_LPD_3(len, pins[1], pins[0]); break; - case I_SS_LPD_3: busPtr = new B_SS_LPD_3(len, pins[1], pins[0]); break; - case I_HS_LPO_3: busPtr = new B_HS_LPO_3(len, pins[1], pins[0]); break; - case I_SS_LPO_3: busPtr = new B_SS_LPO_3(len, pins[1], pins[0]); break; - case I_HS_WS1_3: busPtr = new B_HS_WS1_3(len, pins[1], pins[0]); break; - case I_SS_WS1_3: busPtr = new B_SS_WS1_3(len, pins[1], pins[0]); break; - case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break; - case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break; - } - - return busPtr; - } - static void show(void* busPtr, uint8_t busType, bool consistent = true) { - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - #endif - #endif - case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_LPD_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_LPD_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_LPO_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_LPO_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_WS1_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_WS1_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_P98_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_P98_3: (static_cast(busPtr))->Show(consistent); break; + #ifndef ESP8266 + // Driver fallback order: requested driver -> parallel bus (I2S/LCD/SPI/PARLIO) -> BitBang. + // If the requested driver has no free channel (or is unsupported on this target), the next + // available driver is used instead. + if (driverType == BUSDRV_RMT && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { + _rmtChannelsAssigned++; + return true; } - } - - static bool canShow(void* busPtr, uint8_t busType) { - switch (busType) { - case I_NONE: return true; - #ifdef ESP8266 - case I_8266_U0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_400_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - #endif - #endif - case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_LPD_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_LPD_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_LPO_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_LPO_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_WS1_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_WS1_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_P98_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_P98_3: return (static_cast(busPtr))->CanShow(); break; + if (driverType != BUSDRV_BITBANG && _parHwChannelsAssigned < WLED_MAX_PARHW_CHANNELS) { + // BUSDRV_PARHW maps to the chip's parallel output peripheral: I2S on ESP32/S2, LCD on S3, parallel SPI on C3, PARLIO on C6/H2/C5/P4, all use 4-step cadence + driverType = BUSDRV_PARHW; + if (_parHwChannelsAssigned == 0) { + _parHwBusType = busType; // lock LED type to first parallel channel + #ifdef CONFIG_IDF_TARGET_ESP32C3 + _hardwareSPIused++; // reserve SPI: C3 uses parallel SPI output for "I2S" and takes priority over 2pin buses + #endif + } + _parHwChannelsAssigned++; + return true; } - return true; - } - - [[gnu::hot]] static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { - uint8_t r = c >> 16; - uint8_t g = c >> 8; - uint8_t b = c >> 0; - uint8_t w = c >> 24; - RgbwColor col; - uint8_t cctWW = wwcw & 0xFF, cctCW = (wwcw>>8) & 0xFF; - - // reorder channels to selected order - switch (co & 0x0F) { - default: col.G = g; col.R = r; col.B = b; break; //0 = GRB, default - case 1: col.G = r; col.R = g; col.B = b; break; //1 = RGB, common for WS2811 - case 2: col.G = b; col.R = r; col.B = g; break; //2 = BRG - case 3: col.G = r; col.R = b; col.B = g; break; //3 = RBG - case 4: col.G = b; col.R = g; col.B = r; break; //4 = BGR - case 5: col.G = g; col.R = b; col.B = r; break; //5 = GBR + // Last resort (or explicitly requested): parallel BitBang, all channels share one timing + if (_bitBangChannelsAssigned < WLED_MAX_BB_CHANNELS) { + driverType = BUSDRV_BITBANG; + if (_bitBangBusType == 0) { + _bitBangBusType = busType; // lock LED type to first BitBang channel + } else if (_bitBangBusType != busType) { + return false; // mismatched LED type — all BitBang channels must share timing + } + _bitBangChannelsAssigned++; + return true; } - // upper nibble contains W swap information - switch (co >> 4) { - default: col.W = w; break; // no swapping - case 1: col.W = col.B; col.B = w; break; // swap W & B - case 2: col.W = col.G; col.G = w; break; // swap W & G - case 3: col.W = col.R; col.R = w; break; // swap W & R - case 4: std::swap(cctWW, cctCW); break; // swap WW & CW + return false; // No channels available + #else + // ESP8266: assign driverType based on pin number so BusManager::show() can sequence correctly. + // GPIO1/2 → UART (async, fire-and-forget ISR) + // GPIO3 → DMA (async, I2S SLC DMA) + // others → BitBang (interrupt-blocking — must run before async buses) + if (pins[0] == 1 || pins[0] == 2) { + driverType = BUSDRV_RMT; // reuse BUSDRV_RMT as "async UART" sentinel on ESP8266 + } else if (pins[0] == 3) { + driverType = BUSDRV_PARHW; // async DMA + } else { + driverType = BUSDRV_BITBANG; + // Enforce single LED type for parallel timing + if (_bitBangBusType == 0) { + _bitBangBusType = busType; + } else if (_bitBangBusType != busType) { + return false; // mismatched LED type — all ESP8266 BitBang channels must share timing + } } - - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - } + + return true; } - [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { - RgbwColor col(0,0,0,0); - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_BB_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_U0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_U1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_DM_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_BB_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_U0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - #endif - #endif - case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - } +static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t colorOrder, uint8_t driverType = BUSDRV_RMT, uint8_t busSpeedFactor = 100, uint16_t frequencykHz = 0, uint8_t customNumChannels = 0, const WLEDpixelBus::LedTiming* customTiming = nullptr) { + if (!Bus::isDigital(busType)) return nullptr; - // upper nibble contains W swap information - uint8_t w = col.W; - switch (co >> 4) { - case 1: col.W = col.B; col.B = w; break; // swap W & B - case 2: col.W = col.G; col.G = w; break; // swap W & G - case 3: col.W = col.R; col.R = w; break; // swap W & R + #ifndef ESP8266 + if (driverType == BUSDRV_PARHW && _parHwBusType != 0) { + busType = _parHwBusType; // use the locked in bus type } - switch (co & 0x0F) { - // W G R B - default: return ((col.W << 24) | (col.G << 8) | (col.R << 16) | (col.B)); //0 = GRB, default - case 1: return ((col.W << 24) | (col.R << 8) | (col.G << 16) | (col.B)); //1 = RGB, common for WS2811 - case 2: return ((col.W << 24) | (col.B << 8) | (col.R << 16) | (col.G)); //2 = BRG - case 3: return ((col.W << 24) | (col.B << 8) | (col.G << 16) | (col.R)); //3 = RBG - case 4: return ((col.W << 24) | (col.R << 8) | (col.B << 16) | (col.G)); //4 = BGR - case 5: return ((col.W << 24) | (col.G << 8) | (col.B << 16) | (col.R)); //5 = GBR - } - return 0; - } - - static void cleanup(void* busPtr, uint8_t busType) { - if (busPtr == nullptr) return; - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_U1_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_DM_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_BB_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_U0_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_U1_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_DM_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_BB_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_U0_400_3: delete (static_cast(busPtr)); break; - case I_8266_U1_400_3: delete (static_cast(busPtr)); break; - case I_8266_DM_400_3: delete (static_cast(busPtr)); break; - case I_8266_BB_400_3: delete (static_cast(busPtr)); break; - case I_8266_U0_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_BB_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_U0_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_U1_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_DM_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_BB_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_U0_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_U1_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_DM_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_BB_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_U0_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_U1_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_DM_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_BB_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_U0_2805_5: delete (static_cast(busPtr)); break; - case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; - case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; - case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; - case I_8266_U0_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_U0_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_U1_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_DM_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_BB_SM16825_5: delete (static_cast(busPtr)); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; - case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; - case I_32_RN_400_3: delete (static_cast(busPtr)); break; - case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; - case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; - case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; - case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; - case I_32_RN_2805_5: delete (static_cast(busPtr)); break; - case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; - case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - #endif #endif - case I_HS_DOT_3: delete (static_cast(busPtr)); break; - case I_SS_DOT_3: delete (static_cast(busPtr)); break; - case I_HS_LPD_3: delete (static_cast(busPtr)); break; - case I_SS_LPD_3: delete (static_cast(busPtr)); break; - case I_HS_LPO_3: delete (static_cast(busPtr)); break; - case I_SS_LPO_3: delete (static_cast(busPtr)); break; - case I_HS_WS1_3: delete (static_cast(busPtr)); break; - case I_SS_WS1_3: delete (static_cast(busPtr)); break; - case I_HS_P98_3: delete (static_cast(busPtr)); break; - case I_SS_P98_3: delete (static_cast(busPtr)); break; + if (driverType == BUSDRV_BITBANG && _bitBangBusType != 0) { + busType = _bitBangBusType; // use the locked in bus type } - } - static unsigned getDataSize(void* busPtr, uint8_t busType) { - unsigned size = 0; - #ifdef ARDUINO_ARCH_ESP32 - size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266) - #endif - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_NEO_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_NEO_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_400_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM1_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM2_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_UCS_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_UCS_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_APA106_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_FW6_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_2805_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses (front + back + small system managed RMT) - case I_32_RN_NEO_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_NEO_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_400_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM1_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM2_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_UCS_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_UCS_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_APA106_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_FW6_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_2805_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM1914_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_SM16825_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - // I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - #endif - #endif - case I_HS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - } - return size; - } + const uint8_t numChannels = customNumChannels ? customNumChannels : (uint8_t)Bus::getNumberOfChannels(busType); - static unsigned memUsage(unsigned count, unsigned busType) { - unsigned size = count*3; // let's assume 3 channels, we will add count or 2*count below for 4 channels or 5 channels - switch (busType) { - case I_NONE: size = 0; break; - #ifdef ESP8266 - // UART methods have front + back buffers + small UART - case I_8266_U0_NEO_4 : // fallthrough - case I_8266_U1_NEO_4 : // fallthrough - case I_8266_BB_NEO_4 : // fallthrough - case I_8266_U0_TM1_4 : // fallthrough - case I_8266_U1_TM1_4 : // fallthrough - case I_8266_BB_TM1_4 : size = (size + count); break; // 4 channels - case I_8266_U0_UCS_3 : // fallthrough - case I_8266_U1_UCS_3 : // fallthrough - case I_8266_BB_UCS_3 : size *= 2; break; // 16 bit - case I_8266_U0_UCS_4 : // fallthrough - case I_8266_U1_UCS_4 : // fallthrough - case I_8266_BB_UCS_4 : size = (size + count)*2; break; // 16 bit 4 channels - case I_8266_U0_FW6_5 : // fallthrough - case I_8266_U1_FW6_5 : // fallthrough - case I_8266_BB_FW6_5 : // fallthrough - case I_8266_U0_2805_5 : // fallthrough - case I_8266_U1_2805_5 : // fallthrough - case I_8266_BB_2805_5 : size = (size + 2*count); break; // 5 channels - case I_8266_U0_SM16825_5: // fallthrough - case I_8266_U1_SM16825_5: // fallthrough - case I_8266_BB_SM16825_5: size = (size + 2*count)*2; break; // 16 bit 5 channels - // DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB) - case I_8266_DM_NEO_3 : // fallthrough - case I_8266_DM_400_3 : // fallthrough - case I_8266_DM_TM2_3 : // fallthrough - case I_8266_DM_APA106_3 : // fallthrough - case I_8266_DM_TM1914_3 : size *= 5; break; - case I_8266_DM_NEO_4 : // fallthrough - case I_8266_DM_TM1_4 : size = (size + count)*5; break; - case I_8266_DM_UCS_3 : size *= 2*5; break; - case I_8266_DM_UCS_4 : size = (size + count)*2*5; break; - case I_8266_DM_FW6_5 : // fallthrough - case I_8266_DM_2805_5 : size = (size + 2*count)*5; break; - case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break; - #else - // note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity - // RMT buses (1x front and 1x back buffer, does not include small RMT buffer) - case I_32_RN_NEO_4 : // fallthrough - case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels - case I_32_RN_UCS_3 : size *= 2*2; break; // 16bit - case I_32_RN_UCS_4 : size = (size + count)*2*2; break; // 16bit, 4 channels - case I_32_RN_FW6_5 : // fallthrough - case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels - case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels - // I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD) - #if defined(WLED_HAS_PARALLEL_I2S) || defined(CONFIG_IDF_TARGET_ESP32) - case I_32_I2_NEO_3 : // fallthrough - case I_32_I2_400_3 : // fallthrough - case I_32_I2_TM2_3 : // fallthrough - case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer - case I_32_I2_NEO_4 : // fallthrough - case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels - case I_32_I2_UCS_3 : size *= 2; break; // 16 bit - case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels - case I_32_I2_FW6_5 : // fallthrough - case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels - case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels - #endif - default : size *= 2; break; // everything else uses 2 buffers - #endif - } - return size; - } -#ifndef ESP8266 - // Reset channel tracking (call before adding buses) - static void resetChannelTracking() { - _useParallelI2S = false; - _rmtChannelsAssigned = 0; - _rmtChannel = 0; - _i2sChannelsAssigned = 0; - _parallelBusItype = I_NONE; - _2PchannelsAssigned = 0; - } -#endif - // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins - static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { - if (!Bus::isDigital(busType)) return I_NONE; - uint8_t t = I_NONE; - if (Bus::is2Pin(busType)) { //SPI LED chips + if (Bus::is2Pin(busType)) { bool isHSPI = false; #ifdef ESP8266 if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; #else - if (_2PchannelsAssigned == 0) isHSPI = true; // first 2-pin channel uses hardware SPI - _2PchannelsAssigned++; - #endif - switch (busType) { - case TYPE_APA102: t = I_SS_DOT_3; break; - case TYPE_LPD8806: t = I_SS_LPD_3; break; - case TYPE_LPD6803: t = I_SS_LPO_3; break; - case TYPE_WS2801: t = I_SS_WS1_3; break; - case TYPE_P9813: t = I_SS_P98_3; break; - } - if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software - } else { - #ifdef ESP8266 - uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang - if (offset > 3) offset = 3; - switch (busType) { - case TYPE_WS2812_1CH_X3: - case TYPE_WS2812_RGB: - case TYPE_WS2812_WWA: - t = I_8266_U0_NEO_3 + offset; break; - case TYPE_SK6812_RGBW: - t = I_8266_U0_NEO_4 + offset; break; - case TYPE_WS2811_400KHZ: - t = I_8266_U0_400_3 + offset; break; - case TYPE_TM1814: - t = I_8266_U0_TM1_4 + offset; break; - case TYPE_TM1829: - t = I_8266_U0_TM2_3 + offset; break; - case TYPE_UCS8903: - t = I_8266_U0_UCS_3 + offset; break; - case TYPE_UCS8904: - t = I_8266_U0_UCS_4 + offset; break; - case TYPE_APA106: - t = I_8266_U0_APA106_3 + offset; break; - case TYPE_FW1906: - t = I_8266_U0_FW6_5 + offset; break; - case TYPE_WS2805: - t = I_8266_U0_2805_5 + offset; break; - case TYPE_TM1914: - t = I_8266_U0_TM1914_3 + offset; break; - case TYPE_SM16825: - t = I_8266_U0_SM16825_5 + offset; break; - } - #else //ESP32 - // dynamic channel allocation based on driver preference - // determine which driver to use based on preference and availability. First I2S bus locks the I2S type, all subsequent I2S buses are assigned the same type (hardware restriction) - uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD - if (driverPreference == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { - _rmtChannelsAssigned++; - } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { - offset = 1; // I2S requested or RMT full - _i2sChannelsAssigned++; - } else { - return I_NONE; // No channels available + if (_hardwareSPIused == 0) { + isHSPI = true; + _hardwareSPIused++; // claim hardware SPI (currently only one is supported), on C3 this can also be claimed by parallel SPI (done so in allocateHardware) } + #endif + return new WLEDpixelBus::SpiBus(pins[0], pins[1], frequencykHz, colorOrder, numChannels, isHSPI, busType); // TODO: move this into createbus function? + } - // Now determine actual bus type with the chosen offset - switch (busType) { - case TYPE_WS2812_1CH_X3: - case TYPE_WS2812_RGB: - case TYPE_WS2812_WWA: - t = I_32_RN_NEO_3 + offset; break; - case TYPE_SK6812_RGBW: - t = I_32_RN_NEO_4 + offset; break; - case TYPE_WS2811_400KHZ: - t = I_32_RN_400_3 + offset; break; - case TYPE_TM1814: - t = I_32_RN_TM1_4 + offset; break; - case TYPE_TM1829: - t = I_32_RN_TM2_3 + offset; break; - case TYPE_UCS8903: - t = I_32_RN_UCS_3 + offset; break; - case TYPE_UCS8904: - t = I_32_RN_UCS_4 + offset; break; - case TYPE_APA106: - t = I_32_RN_APA106_3 + offset; break; - case TYPE_FW1906: - t = I_32_RN_FW6_5 + offset; break; - case TYPE_WS2805: - t = I_32_RN_2805_5 + offset; break; - case TYPE_TM1914: - t = I_32_RN_TM1914_3 + offset; break; - case TYPE_SM16825: - t = I_32_RN_SM16825_5 + offset; break; - } - // If using parallel I2S, set the type accordingly - if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type - _parallelBusItype = t; - #ifdef CONFIG_IDF_TARGET_ESP32S3 - _useParallelI2S = true; // ESP32-S3 always uses parallel I2S (LCD method) + WLEDpixelBus::BusDriver driver = WLEDpixelBus::BusDriver::RMT; // always overwritten below; initialised to avoid unused-variable warning + + #ifdef ESP8266 + if (pins[0] == 1 || pins[0] == 2) driver = WLEDpixelBus::BusDriver::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled + else if (pins[0] == 3) driver = WLEDpixelBus::BusDriver::DMA; // DMA method uses a lot of RAM! + else driver = WLEDpixelBus::BusDriver::BitBang; + #elif !defined(CONFIG_IDF_TARGET_ESP32C61) + switch (driverType) { + case BUSDRV_RMT: + driver = WLEDpixelBus::BusDriver::RMT; + break; + case BUSDRV_PARHW: + #if defined(CONFIG_IDF_TARGET_ESP32C3) //TODO: should use hardware capabilities instead of MCU types + driver = WLEDpixelBus::BusDriver::SPI; // parallel SPI on C3 + #elif defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32P4) + driver = WLEDpixelBus::BusDriver::PARLIO; // PARLIO on C5, C6, H2, P4 + #else + driver = WLEDpixelBus::BusDriver::I2S; // ESP32 & S2: use parallel I2S, S3: use LCD parallel out #endif - } - else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag - _useParallelI2S = true; - t = _parallelBusItype; - } - #endif + break; + default: + driver = WLEDpixelBus::BusDriver::BitBang; + break; } - return t; + #else + // C61 only supports BB for now (might be able to use parallel SPI), it has no RMT, no I2S, no PARLIO hardware + driver = WLEDpixelBus::BusDriver::BitBang; + #endif + + // getProtocol() reads the pulse-timing from a PROGMEM table, scales by speed factor if set then passes it to the bus + WLEDpixelBus::LedTiming timing = customTiming ? *customTiming : WLEDpixelBus::getProtocol(busType); + if (busSpeedFactor != 100) { + float factor = (float)busSpeedFactor / 100.0f; + timing = WLEDpixelBus::scaleTiming(timing, factor); + } + + // Chip-specific init (prefix/suffix/invert) is applied inside createBus() using ledType. + return WLEDpixelBus::createBus(driver, pins[0], timing, colorOrder, numChannels, busType, len); } }; #endif + diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 8ea967a26b..b703ab0c86 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -242,6 +242,22 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint16_t start = elm["start"] | 0; if (length==0 || start + length > MAX_LEDS) continue; // zero length or we reached max. number of LEDs, just stop uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; + // Special types that require a CustomBusConfig channel-map override. + // These legacy types default to a hardcoded channel map when no custom + // config is saved in cfg.json; otherwise the JSON values are used. + CustomBusConfig customBus; + bool isMappedType = false; + if (ledType == TYPE_WS2812_1CH_X3) { + isMappedType = true; + customBus.numChannels = 1; // one channel per LED, enables custom bus config + customBus.channelColors[0] = 4; // W,W,W TODO: get rid of magic numbers, using an enum + } else if (ledType == TYPE_WS2812_WWA) { + isMappedType = true; + customBus.numChannels = 3; // setting numChannels enables custom bus config + customBus.channelColors[0] = 6; // CW + customBus.channelColors[1] = 5; // WW + customBus.channelColors[2] = 5; // WW (amber, not supported so just set it to warm white) note: channels may need different order + } bool reversed = elm["rev"]; bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM @@ -254,10 +270,32 @@ 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 note: polybus may override this if driver is not available + uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=parallel HW (I2S/LCD/SPI/PARLIO, chip-dependent) note: polybus may override this if driver is not available String host = elm[F("text")] | String(); - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); + uint8_t bsf = (uint8_t)(elm[F("bsf")] | 100); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host, (uint8_t)bsf); + // Apply custom bus channel-map/timing override (loaded from JSON, with + // legacy fallback to the hardcoded defaults for special types). + BusConfig& bc_back = busConfigs.back(); + bc_back.custom.numChannels = elm["cch"] | 0; // 0 = not set, bus uses its native channel layout + if (bc_back.custom.active()) { + JsonArrayConst cmap = elm["cmap"]; + if (!cmap.isNull()) { + for (uint8_t ci = 0; ci < 6 && ci < cmap.size(); ci++) bc_back.custom.channelColors[ci] = (uint8_t)(int)cmap[ci]; + } + bc_back.custom.invertMask = elm["cinv"] | 0; + bc_back.custom.is16bit = elm["c16"] | false; + bc_back.custom.invertOutput = elm["cio"] | false; + bc_back.custom.t0h = elm["ct0h"] | 300; + bc_back.custom.t0l = elm["ct0l"] | 900; + bc_back.custom.t1h = elm["ct1h"] | 700; + bc_back.custom.t1l = elm["ct1l"] | 500; + bc_back.custom.trst = elm["crst"] | 300; + } + if (isMappedType && !bc_back.custom.active()) { + bc_back.custom = customBus; // use default custom config for special mapped types (i.e. 1 or 2 channel WS821x types), see above + } doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -538,7 +576,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { gammaCorrectBri = false; gammaCorrectCol = false; } - NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables (can be unity mapped if gamma=1.0) JsonObject light_tr = light["tr"]; int tdd = light_tr["dur"] | -1; @@ -1021,6 +1059,22 @@ void serializeConfig(JsonObject root) { ins[F("ledma")] = bus->getLEDCurrent(); ins[F("drv")] = bus->getDriverType(); ins[F("text")] = bus->getCustomText(); + ins[F("bsf")] = bus->getBusSpeedFactor(); + // Custom bus extra config + if (bus->getCustomBusConfig().active()) { + const CustomBusConfig& cb = bus->getCustomBusConfig(); + ins["cch"] = cb.numChannels; + JsonArray cmap = ins.createNestedArray("cmap"); + for (uint8_t i = 0; i < cb.numChannels; i++) cmap.add(cb.channelColors[i]); // emit only active channels; deserializer guards with cmap.size() + ins["cinv"] = cb.invertMask; + ins["c16"] = cb.is16bit; + ins["cio"] = cb.invertOutput; + ins["ct0h"] = cb.t0h; + ins["ct0l"] = cb.t0l; + ins["ct1h"] = cb.t1h; + ins["ct1l"] = cb.t1l; + ins["crst"] = cb.trst; + } } JsonArray hw_com = hw.createNestedArray(F("com")); diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 6ddc4ec892..c63f75f32b 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -663,8 +663,7 @@ void NeoGammaWLEDMethod::calcGammaTable(float gamma) uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) { - if (!gammaCorrectCol) return value; - return gammaT[value]; + return gammaT[value]; // gammaT[] uses unity mapping if gammaCorrectCol is false } uint32_t NeoGammaWLEDMethod::inverseGamma32(uint32_t color) diff --git a/wled00/colors.h b/wled00/colors.h index 00fe4fb498..00f1b8edf6 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -39,7 +39,6 @@ class NeoGammaWLEDMethod { static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) static inline uint32_t Correct32(uint32_t color) { // apply Gamma to RGBW32 color (WLED specific, not used by NPB) - if (!gammaCorrectCol) return color; // no gamma correction uint8_t w = byte(color>>24), r = byte(color>>16), g = byte(color>>8), b = byte(color); // extract r, g, b, w channels w = gammaT[w]; r = gammaT[r]; g = gammaT[g]; b = gammaT[b]; return (uint32_t(w) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b); diff --git a/wled00/const.h b/wled00/const.h index 00a6b4d226..95ed79eeef 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -63,12 +63,13 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #endif #ifdef ESP8266 - #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_DIGITAL_CHANNELS 8 #define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S - #define WLED_MAX_I2S_CHANNELS 0 + #define WLED_MAX_PARHW_CHANNELS 0 + #define WLED_MAX_BB_CHANNELS 4 // ESP8266 parallel bit-bang channels (all share same LED type/timing) can be set to 8 if more outputs are needed #define WLED_MAX_ANALOG_CHANNELS 5 #define WLED_MAX_TIMERS 16 // reduced limit for ESP8266 due to memory constraints - #define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix! + #define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix! #else #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX) #include "driver/ledc.h" // needed for analog/LEDC channel counts @@ -86,35 +87,50 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #if defined(CONFIG_IDF_TARGET_ESP32C3) #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! + #define WLED_MAX_PARHW_CHANNELS 4 // uses SPI hardware in 4x parallel output and not actual I2S + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels + #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels - #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_PARHW_CHANNELS 16 + #else + #define WLED_MAX_PARHW_CHANNELS 8 + #endif + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels + #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type 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_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels - #define WLED_MAX_I2S_CHANNELS 8 // uses LCD parallel output not I2S - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_PARHW_CHANNELS 16 // uses LCD parallel output not I2S and supports up to 16 parallel channels + #else + #define WLED_MAX_PARHW_CHANNELS 8 + #endif + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused (saves some flash and RAM) + #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32C6) + #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C6 has 2 RMT output channels + #define WLED_MAX_PARHW_CHANNELS 8 // ESP32-C6 uses PARLIO peripheral for 8 parallel LED channels (IDF >= 5.3) + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels (fallback if PARLIO unavailable) + #define WLED_PLATFORM_ID 5 // used in UI to distinguish ESP types, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32P4) + #define WLED_MAX_RMT_CHANNELS 0 // RMT driver not yet supported on these targets + #define WLED_MAX_PARHW_CHANNELS 8 // PARLIO peripheral provides 8 parallel LED output channels on these targets (IDF >= 5.3) + #define WLED_MAX_BB_CHANNELS 8 // BitBang fallback when PARLIO is unavailable + #define WLED_PLATFORM_ID 6 // used in UI to distinguish ESP type in UI, needs a proper fix! #else - #if defined(CONFIG_IDF_TARGET_ESP32) // classic esp32 - #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels - #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 16 - #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! - #else // all other risc-v based boards: same as C3 - #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types - falls back to "C3" until we have a proper fix! - #endif + #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_PARHW_CHANNELS 16 + #else + #define WLED_MAX_PARHW_CHANNELS 8 + #endif + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused (saves some flash and RAM) + #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_TIMERS 64 // maximum number of timers #ifndef WLED_MAX_DIGITAL_CHANNELS - #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS) + #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_PARHW_CHANNELS + WLED_MAX_BB_CHANNELS) // total number of digital channels (RMT + parallel + BitBang) #else #warning "buildenv overrides WLED_MAX_DIGITAL_CHANNELS - please check that the value is correct" #endif @@ -344,7 +360,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); //#define TYPE_WS2812_2CH_X3 20 // use FW1906 #define TYPE_WS2812_WWA 21 //amber + warm + cold white #define TYPE_WS2812_RGB 22 -#define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern) +//#define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern), unused just use WS2812 with off refresh #define TYPE_WS2811_400KHZ 24 //half-speed WS2812 protocol, used by very old WS2811 units #define TYPE_TM1829 25 #define TYPE_UCS8903 26 @@ -352,10 +368,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC) #define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp) #define TYPE_SK6812_RGBW 30 -#define TYPE_TM1814 31 +#define TYPE_TM1814 31 //RGBW #define TYPE_WS2805 32 //RGB + WW + CW #define TYPE_TM1914 33 //RGB #define TYPE_SM16825 34 //RGB + WW + CW +#define TYPE_TM1815 35 //RGBW (half speed TM1814) +//TODO add support for SM16714, see https://wled.discourse.group/t/sm16714-pixel-ic/9794 #define TYPE_DIGITAL_MAX 39 // last usable digital type //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 461aa9d387..f76df3c597 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -387,4 +387,4 @@ - + \ No newline at end of file diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 03fae259c4..1cf2a2a95f 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -6,10 +6,10 @@ LED Settings @@ -1034,7 +1276,6 @@

LED setup

LED outputs:

-

LED memory usage: 0 / ? B
@@ -1167,4 +1408,4 @@

Advanced

- + \ No newline at end of file diff --git a/wled00/file.cpp b/wled00/file.cpp index 5a169d6450..1a3a657121 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -437,6 +437,11 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ } } #endif + + #ifdef CONFIG_IDF_TARGET_ESP32C3 + while (!BusManager::canAllShow()) yield(); // accessing FS causes glitches due to RMT issue on C3 TODO: remove this when fixed + #endif + if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) { request->send(request->beginResponse(WLED_FS, path, {}, request->hasArg(F("download")), {})); return true; diff --git a/wled00/set.cpp b/wled00/set.cpp index dee8d00378..dc36df22d5 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -223,6 +223,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //channel swap char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) + char sf[4] = "SF"; sf[2] = offset+s; sf[3] = 0; //bus speed factor (Fast/Default/Slow) 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) @@ -281,7 +282,37 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) 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, driverType, text); + uint8_t bsf = request->hasArg(sf) ? (uint8_t)request->arg(sf).toInt() : 100; + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text, (uint8_t)bsf); + // Customized channel map/timing override: parsed for any digital LED type when the + // "customize" checkbox (CBen) is submitted; numChannels != 0 marks CustomBusConfig active. + char cben[7] = "CBen"; cben[4] = offset+s; cben[5] = 0; + if (request->hasArg(cben)) { + BusConfig& bc_back = busConfigs.back(); + char cbch[7] = "CBch"; cbch[4] = offset+s; cbch[5] = 0; + char cbio[7] = "CBio"; cbio[4] = offset+s; cbio[5] = 0; + char cbb[6] = "CBb"; cbb[3] = offset+s; cbb[4] = 0; + char cbt0h[7] = "CBt0h"; cbt0h[5] = offset+s; cbt0h[6] = 0; + char cbt0l[7] = "CBt0l"; cbt0l[5] = offset+s; cbt0l[6] = 0; + char cbt1h[7] = "CBt1h"; cbt1h[5] = offset+s; cbt1h[6] = 0; + char cbt1l[7] = "CBt1l"; cbt1l[5] = offset+s; cbt1l[6] = 0; + char cbrst[7] = "CBrst"; cbrst[5] = offset+s; cbrst[6] = 0; + bc_back.custom.numChannels = request->arg(cbch).toInt(); + bc_back.custom.invertOutput = request->hasArg(cbio); + bc_back.custom.is16bit = request->hasArg(cbb); + bc_back.custom.t0h = request->arg(cbt0h).toInt(); + bc_back.custom.t0l = request->arg(cbt0l).toInt(); + bc_back.custom.t1h = request->arg(cbt1h).toInt(); + bc_back.custom.t1l = request->arg(cbt1l).toInt(); + bc_back.custom.trst = request->arg(cbrst).toInt(); + bc_back.custom.invertMask = 0; + for (uint8_t ci = 0; ci < 6; ci++) { + char cbc[7] = "CBc"; cbc[3] = '0'+ci; cbc[4] = offset+s; cbc[5] = 0; + char cbi[7] = "CBi"; cbi[3] = '0'+ci; cbi[4] = offset+s; cbi[5] = 0; + bc_back.custom.channelColors[ci] = (uint8_t)constrain(request->arg(cbc).toInt(), 0, 6); + if (request->hasArg(cbi)) bc_back.custom.invertMask |= (1u << ci); + } + } busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S b/wled00/src/WLEDpixelBus/ESP32RmtHI.S similarity index 99% rename from lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S rename to wled00/src/WLEDpixelBus/ESP32RmtHI.S index bc28c1a840..2e260fcedd 100644 --- a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S +++ b/wled00/src/WLEDpixelBus/ESP32RmtHI.S @@ -2,7 +2,7 @@ * Bridges from a high-level interrupt to the C++ code. * * This code is largely derived from Espressif's 'hli_vector.S' Bluetooth ISR. - * + * implemented by @willmmiles */ #if defined(__XTENSA__) && defined(ESP32) diff --git a/wled00/src/WLEDpixelBus/RmtHIDriver.h b/wled00/src/WLEDpixelBus/RmtHIDriver.h new file mode 100644 index 0000000000..e6715b8a13 --- /dev/null +++ b/wled00/src/WLEDpixelBus/RmtHIDriver.h @@ -0,0 +1,42 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +-------------------------------------------------------------------------*/ +#pragma once + +#ifdef ARDUINO_ARCH_ESP32 + +#include "esp_idf_version.h" + +// The high priority RMT driver is only available on the Xtensa-based ESP32, S2 and S3 +// (not on the RISC-V C3) and requires ESP-IDF < 5.0 +#if (defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) \ + && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) +#define WPB_USE_RMTHI +#endif + +#ifdef WPB_USE_RMTHI + +#include +#include "driver/rmt.h" +#include "freertos/FreeRTOS.h" +#include +#include + +namespace RmtHiDriver { + // Install the driver for a specific channel, specifying timing properties + esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset, uint8_t blocksToUse = 1); + + // Remove the driver on a specific channel + esp_err_t Uninstall(rmt_channel_t channel); + + // Write a buffer of data to a specific channel. + // Buffer reference is held until write completes. + esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size); + + // Wait until transaction is complete. + esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time); +}; + +#endif // WPB_USE_RMTHI +#endif // ARDUINO_ARCH_ESP32 diff --git a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp new file mode 100644 index 0000000000..1d0240b79e --- /dev/null +++ b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp @@ -0,0 +1,490 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +-------------------------------------------------------------------------*/ + +#include +#include "RmtHIDriver.h" + +#if defined(WPB_USE_RMTHI) + +#include +#include "soc/soc.h" +#include "soc/rmt_reg.h" +#include "esp_idf_version.h" + +#ifdef __riscv +#include "riscv/interrupt.h" +#endif + + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) +#include "hal/rmt_ll.h" +#else +/* Shims for older ESP-IDF v3; we can safely assume original ESP32 */ +#include "soc/rmt_struct.h" + +// Selected RMT API functions borrowed from ESP-IDF v4.4.8 +// components/hal/esp32/include/hal/rmt_ll.h +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +__attribute__((always_inline)) +static inline void rmt_ll_tx_reset_pointer(rmt_dev_t *dev, uint32_t channel) +{ + dev->conf_ch[channel].conf1.mem_rd_rst = 1; + dev->conf_ch[channel].conf1.mem_rd_rst = 0; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_start(rmt_dev_t *dev, uint32_t channel) +{ + dev->conf_ch[channel].conf1.tx_start = 1; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel) +{ + RMTMEM.chan[channel].data32[0].val = 0; + dev->conf_ch[channel].conf1.tx_start = 0; + dev->conf_ch[channel].conf1.mem_rd_rst = 1; + dev->conf_ch[channel].conf1.mem_rd_rst = 0; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->apb_conf.mem_tx_wrap_en = enable; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_enable_loop(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->conf_ch[channel].conf1.tx_conti_mode = enable; +} + +__attribute__((always_inline)) +static inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel) +{ + return dev->status_ch[channel]; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_set_limit(rmt_dev_t *dev, uint32_t channel, uint32_t limit) +{ + dev->tx_lim_ch[channel].limit = limit; +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_interrupt(rmt_dev_t *dev, uint32_t mask, bool enable) +{ + if (enable) { + dev->int_ena.val |= mask; + } else { + dev->int_ena.val &= ~mask; + } +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->int_ena.val &= ~(1 << (channel * 3)); + dev->int_ena.val |= (enable << (channel * 3)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->int_ena.val &= ~(1 << (channel * 3 + 2)); + dev->int_ena.val |= (enable << (channel * 3 + 2)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) +{ + dev->int_ena.val &= ~(1 << (channel + 24)); + dev->int_ena.val |= (enable << (channel + 24)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel) +{ + dev->int_clr.val = (1 << (channel * 3)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel) +{ + dev->int_clr.val = (1 << (channel * 3 + 2)); +} + +__attribute__((always_inline)) +static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel) +{ + dev->int_clr.val = (1 << (channel + 24)); +} + +__attribute__((always_inline)) +static inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev) +{ + uint32_t status = dev->int_st.val; + return (status & 0xFF000000) >> 24; +} + +__attribute__((always_inline)) +static inline void rmt_ll_tx_set_mem_blocks(rmt_dev_t *dev, uint32_t channel, uint8_t block_num) +{ + dev->conf_ch[channel].conf0.mem_size = block_num; +} + +#endif + + +// ********************************* +// Select method for binding interrupt +// +// - If the Bluetooth driver has registered a high-level interrupt, piggyback on that API +// - If we're on a modern core, allocate the interrupt with the API (old cores are bugged) +// - Otherwise use the low-level hardware API to manually bind the interrupt + + +#if defined(CONFIG_BTDM_CTRL_HLI) +// Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls +#include "hal/interrupt_controller_hal.h" +extern "C" esp_err_t hli_intr_register(intr_handler_t handler, void* arg, uint32_t intr_reg, uint32_t intr_mask); + +#else /* !CONFIG_BTDM_CTRL_HLI*/ + +// Declare the our high-priority ISR handler +extern "C" void ld_include_hli_vectors_rmt(); // an object with an address, but no space + +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) +#include "soc/periph_defs.h" +#endif + +// Select level flag +#if defined(__riscv) +// RISCV chips don't block interrupts while scheduling; all we need to do is be higher than the WiFi ISR +#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL3 +#elif defined(CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5) +#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL4 +#else +#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL5 +#endif + +// ESP-IDF v3 cannot enable high priority interrupts through the API at all; +// and ESP-IDF v4 on XTensa cannot enable Level 5 due to incorrect interrupt descriptor tables +#if !defined(__XTENSA__) || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) || ((ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) && CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5)) +#define NEOESP32_RMT_CAN_USE_INTR_ALLOC + +// XTensa cores require the assembly bridge +#ifdef __XTENSA__ +#define HI_IRQ_HANDLER nullptr +#define HI_IRQ_HANDLER_ARG ld_include_hli_vectors_rmt +#else +#define HI_IRQ_HANDLER NeoEsp32RmtMethodIsr +#define HI_IRQ_HANDLER_ARG nullptr +#endif + +#else +/* !CONFIG_BTDM_CTRL_HLI && !NEOESP32_RMT_CAN_USE_INTR_ALLOC */ +// This is the index of the LV5 interrupt vector - see interrupt descriptor table in idf components/hal/esp32/interrupt_descriptor_table.c +#define ESP32_LV5_IRQ_INDEX 26 + +#endif /* NEOESP32_RMT_CAN_USE_INTR_ALLOC */ +#endif /* CONFIG_BTDM_CTRL_HLI */ + + +// RMT driver implementation +struct NeoEsp32RmtHIChannelState { + uint32_t rmtBit0, rmtBit1; + uint32_t resetDuration; + + const byte* txDataStart; // data array + const byte* txDataEnd; // one past end + const byte* txDataCurrent; // current location + size_t rmtOffset; + size_t batchSize; // memory blocks assigned +}; + +// Global variables +#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) +static intr_handle_t isrHandle = nullptr; +#endif + +static NeoEsp32RmtHIChannelState** driverState = nullptr; + +// Fill the RMT buffer memory +// This is implemented using many arguments instead of passing the structure object to ensure we do only one lookup +// All the arguments are passed in registers, so they don't need to be looked up again +static void IRAM_ATTR RmtFillBuffer(uint8_t channel, const byte** src_ptr, const byte* end, uint32_t bit0, uint32_t bit1, size_t* offset_ptr, size_t reserve, size_t batchSize) { + // We assume that (rmtToWrite % 8) == 0 + size_t rmtToWrite = batchSize - reserve; + rmt_item32_t* dest =(rmt_item32_t*) &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory + const byte* psrc = *src_ptr; + + *offset_ptr ^= batchSize; + + if (psrc != end) { + while (rmtToWrite > 0) { + uint8_t data = *psrc; + for (uint8_t bit = 0; bit < 8; bit++) + { + dest->val = (data & 0x80) ? bit1 : bit0; + dest++; + data <<= 1; + } + rmtToWrite -= 8; + psrc++; + if (psrc == end) { + break; + } + } + + *src_ptr = psrc; + } + + if (rmtToWrite > 0) { + // Add end event + rmt_item32_t bit0_val = {{.val = bit0 }}; + *dest = rmt_item32_t {{{ .duration0 = 0, .level0 = bit0_val.level1, .duration1 = 0, .level1 = bit0_val.level1 }}}; + } +} + +static void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& state) { + // Reset context state + state.rmtOffset = 0; + + // Fill the first part of the buffer with a reset event + // FUTURE: we could do timing analysis with the last interrupt on this channel + // Use 8 words to stay aligned with the buffer fill logic + rmt_item32_t bit0_val = {{.val = state.rmtBit0 }}; + rmt_item32_t fill = {{{ .duration0 = 100, .level0 = bit0_val.level1, .duration1 = 100, .level1 = bit0_val.level1 }}}; + rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM.chan[channel].data32[0]; + for (auto i = 0; i < 7; ++i) dest[i] = fill; + fill.duration1 = state.resetDuration > 1400 ? (state.resetDuration - 1400) : 100; + dest[7] = fill; + + // Fill the remaining buffer with real data + RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 8, state.batchSize); + RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0, state.batchSize); + + // Start operation + rmt_ll_clear_tx_thres_interrupt(&RMT, channel); + rmt_ll_tx_reset_pointer(&RMT, channel); + rmt_ll_tx_start(&RMT, channel); +} + +extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { + // Tx threshold interrupt + uint32_t status = rmt_ll_get_tx_thres_interrupt_status(&RMT); + while (status) { + uint8_t channel = __builtin_ffs(status) - 1; + if (driverState[channel]) { + // Normal case + NeoEsp32RmtHIChannelState& state = *driverState[channel]; + RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0, state.batchSize); + } else { + // Danger - another driver got invoked? + rmt_ll_tx_stop(&RMT, channel); + } + rmt_ll_clear_tx_thres_interrupt(&RMT, channel); + status = rmt_ll_get_tx_thres_interrupt_status(&RMT); + } +}; + +// Wrapper around the register analysis defines +// For all currently supported chips, this is constant for all channels; but this is not true of *all* ESP32 +static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) { + uint32_t v; + switch(channel) { +#ifdef RMT_STATE_CH0 + case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break; +#endif +#ifdef RMT_STATE_CH1 + case 1: v = (status >> RMT_STATE_CH1_S) & RMT_STATE_CH1_V; break; +#endif +#ifdef RMT_STATE_CH2 + case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break; +#endif +#ifdef RMT_STATE_CH3 + case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break; +#endif +#ifdef RMT_STATE_CH4 + case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break; +#endif +#ifdef RMT_STATE_CH5 + case 5: v = (status >> RMT_STATE_CH5_S) & RMT_STATE_CH5_V; break; +#endif +#ifdef RMT_STATE_CH6 + case 6: v = (status >> RMT_STATE_CH6_S) & RMT_STATE_CH6_V; break; +#endif +#ifdef RMT_STATE_CH7 + case 7: v = (status >> RMT_STATE_CH7_S) & RMT_STATE_CH7_V; break; +#endif + default: v = 0; + } + + return v != 0; +} + + +esp_err_t RmtHiDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset, uint8_t blocksToUse) { + // Validate channel number + if (channel >= RMT_CHANNEL_MAX) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = ESP_OK; + if (!driverState) { + // First time init + driverState = reinterpret_cast(heap_caps_calloc(RMT_CHANNEL_MAX, sizeof(NeoEsp32RmtHIChannelState*), MALLOC_CAP_INTERNAL)); + if (!driverState) return ESP_ERR_NO_MEM; + // Ensure all interrupts are cleared before binding + RMT.int_ena.val = 0; + RMT.int_clr.val = 0xFFFFFFFF; + + // Bind interrupt handler +#if defined(CONFIG_BTDM_CTRL_HLI) + // Bluetooth driver has taken the empty high-priority interrupt. Fortunately, it allows us to + // hook up another handler. + err = hli_intr_register(NeoEsp32RmtMethodIsr, nullptr, (uintptr_t) &RMT.int_st, 0xFF000000); + // 25 is the magic number of the bluetooth ISR on ESP32 - see soc/soc.h. + intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25); + intr_cntrl_ll_enable_interrupts(1<<25); +#elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) + // Use the platform code to allocate the interrupt + // If we need the additional assembly bridge, we pass it as the "arg" to the IDF so it gets linked in + err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, HI_IRQ_HANDLER, (void*) HI_IRQ_HANDLER_ARG, &isrHandle); + //err = ESP_ERR_NOT_FINISHED; +#else + // Broken IDF API does not allow us to reserve the interrupt; do it manually + static volatile const void* __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt; + intr_matrix_set(xPortGetCoreID(), ETS_RMT_INTR_SOURCE, ESP32_LV5_IRQ_INDEX); + ESP_INTR_ENABLE(ESP32_LV5_IRQ_INDEX); +#endif + + if (err != ESP_OK) { + heap_caps_free(driverState); + driverState = nullptr; + return err; + } + } + + if (driverState[channel] != nullptr) { + return ESP_ERR_INVALID_STATE; // already in use + } + + NeoEsp32RmtHIChannelState* state = reinterpret_cast(heap_caps_calloc(1, sizeof(NeoEsp32RmtHIChannelState), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); + if (state == nullptr) { + return ESP_ERR_NO_MEM; + } + + // Store timing information + state->rmtBit0 = rmtBit0; + state->rmtBit1 = rmtBit1; + state->resetDuration = reset; + + // Calculate batch size based on number of blocks (64 items per block) + state->batchSize = (RMT_MEM_ITEM_NUM * blocksToUse) / 2; + + // Initialize hardware + rmt_ll_tx_stop(&RMT, channel); + rmt_ll_tx_reset_pointer(&RMT, channel); + rmt_ll_tx_set_mem_blocks(&RMT, channel, blocksToUse); + rmt_ll_enable_tx_err_interrupt(&RMT, channel, false); + rmt_ll_enable_tx_end_interrupt(&RMT, channel, false); + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); + rmt_ll_clear_tx_err_interrupt(&RMT, channel); + rmt_ll_clear_tx_end_interrupt(&RMT, channel); + rmt_ll_clear_tx_thres_interrupt(&RMT, channel); + rmt_ll_tx_enable_loop(&RMT, channel, false); + rmt_ll_tx_enable_pingpong(&RMT, channel, true); + rmt_ll_tx_set_limit(&RMT, channel, state->batchSize); + + driverState[channel] = state; + + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true); + + return err; +} + +esp_err_t RmtHiDriver::Uninstall(rmt_channel_t channel) { + if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; + + NeoEsp32RmtHIChannelState* state = driverState[channel]; + + WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); + + // Done or not, we're out of here + rmt_ll_tx_stop(&RMT, channel); + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); + driverState[channel] = nullptr; + heap_caps_free(state); + +#if !defined(CONFIG_BTDM_CTRL_HLI) /* Cannot unbind from bluetooth ISR */ + // Turn off the driver ISR and release global state if none are left + for (uint8_t channelIndex = 0; channelIndex < RMT_CHANNEL_MAX; ++channelIndex) { + if (driverState[channelIndex]) return ESP_OK; // done + } + +#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) + esp_intr_free(isrHandle); +#else + ESP_INTR_DISABLE(ESP32_LV5_IRQ_INDEX); +#endif + + heap_caps_free(driverState); + driverState = nullptr; +#endif /* !defined(CONFIG_BTDM_CTRL_HLI) */ + + return ESP_OK; +} + +esp_err_t RmtHiDriver::Write(rmt_channel_t channel, const uint8_t *src, size_t src_size) { + if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; + + NeoEsp32RmtHIChannelState& state = *driverState[channel]; + esp_err_t result = WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); + + if (result == ESP_OK) { + state.txDataStart = src; + state.txDataCurrent = src; + state.txDataEnd = src + src_size; + RmtStartWrite(channel, state); + } + return result; +} + +esp_err_t RmtHiDriver::WaitForTxDone(rmt_channel_t channel, TickType_t wait_time) { + if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; + + NeoEsp32RmtHIChannelState& state = *driverState[channel]; + // yield-wait until wait_time + esp_err_t rv = ESP_OK; + uint32_t status; + while(1) { + status = rmt_ll_tx_get_channel_status(&RMT, channel); + if (!_RmtStatusIsTransmitting(channel, status)) break; + if (wait_time == 0) { rv = ESP_ERR_TIMEOUT; break; }; + + TickType_t sleep = std::min(wait_time, (TickType_t) 5); + vTaskDelay(sleep); + wait_time -= sleep; + }; + + return rv; +} + +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp new file mode 100644 index 0000000000..0a7d354a0f --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -0,0 +1,323 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna): +NeoPixelBus served me well as a reference to proper hardware initialisation +as well as figuring out the more exotic digital LED types + +Features: +- Runtime LED timing configuration +- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-C5 and ESP8266 (maybe other ESP32 flavours work too) +- RMT, I2S parallel, LCD parallel, SPI parallel, BitBang parallel, PARLIO parallel +- 2-Pin LED support (hardware SPI and BitBang) +- RGBW uint32_t pixel buffer format (WLED native) +- Support for up to 6 color channels, 8bit or 16bit +- Supports output inversion (ESP32 only) and individual color channel inversion +- Supports hardware LED brightness for improved color resolution if available + +Tested working on IDF V4 and V5 API functions and low-level HAL + +-------------------------------------------------------------------------*/ + +/* +TODO List +- size DMA buffers such that the buffer completes one LED +- need to check if the features "custom bus start indices" and "global color override" work (color override probably does not work) +- ESP32 C61 has no RMT and no PARLIO, either bit bang or check if parallel SPI can be used +- I2S/LCD requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load +- the DMA buffer count and size need to be checked agains memory usage formulas, they may be incorrect (we could do "worst case" in the UI as it wont matter much for a small amount of LEDs) +- I2S driver now has safe watchdog in case interrupts are missed. check if parallel spi driver and parlio driver can suffer from the same race condition (nullpointer / end of transfer overwritten or missed) +- LOOPTEST_CYCLES in BB bus need fine-tuning +- color orders of some strips may differ from NPB implementation, needs checking and reordering to match legacy behaviour or users will complain (see legacy bus_wrapper where this is defined in neoXXXtype) +- move gamma down to bus level, see pending PR +- brightness scaling for special buses with LED current (or 16bit) may need fine-tuning +- SPI 2-wire bus types need testing on all platforms (tested working in the past but not recently) +- SPI 2-wire types do not support signal inversion +- ESP32_DATA_IDLE_HIGH flag is a hack to fix bad hardware design and uses the legacy RMT driver, we should drop support for that +- parallel SPI driver: the reset pulse handling needs some improvement, currently SPI_RESET_BITS is fixed +- PARLIO bus needs an in depth review to check for any AI slop +- use chip capabilities instead of individual target ifdefs in buswrapper +- ESP8266 UART and I2S buses show() return false instead of waiting which is inconsistent +*/ + + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +// LED Timing Lookup +LedTiming getProtocol(uint8_t wledType) { + uint8_t idx = getTimingIndex(wledType); +#ifdef ESP8266 + return LedTiming( + (uint16_t)pgm_read_word(&s_ledTimings[idx].t0h_ns), + (uint16_t)pgm_read_word(&s_ledTimings[idx].t0l_ns), + (uint16_t)pgm_read_word(&s_ledTimings[idx].t1h_ns), + (uint16_t)pgm_read_word(&s_ledTimings[idx].t1l_ns), + (uint32_t)pgm_read_dword(&s_ledTimings[idx].reset_us) + ); +#else + return s_ledTimings[idx]; +#endif +} + +//============================================================================== +// Color Encoder Implementation +//============================================================================== + +ColorEncoder::ColorEncoder(uint8_t co, uint8_t numChannels, uint8_t ledType) +{ + _invertMask = 0; + memset(_channelMap, 0, sizeof(_channelMap)); + + // --- Standard types: fast path via _idxR/_idxG/_idxB/_idxW/_idxCW --- + + uint8_t flags = 0; + + // Decode RGB wire positions from color order lower nibble (see ColorOrder enum) + uint8_t rPos, gPos, bPos; + switch (co & 0x0F) { + case ORDER_RGB: rPos=0; gPos=1; bPos=2; break; + case ORDER_BRG: rPos=1; gPos=2; bPos=0; break; + case ORDER_RBG: rPos=0; gPos=2; bPos=1; break; + case ORDER_BGR: rPos=2; gPos=1; bPos=0; break; + case ORDER_GBR: rPos=2; gPos=0; bPos=1; break; + default: rPos=1; gPos=0; bPos=2; break; // ORDER_GRB + } + _idxR = rPos; _idxG = gPos; _idxB = bPos; + _idxW = 3; _idxCW = 4; // defaults, overridden below as needed + + // White/CCT channels: numChannels from bus type, W-swap from upper nibble + if (numChannels == CHANNELS_RGBW) { + // LED-type-specific native wire order: TM1814 and TM1815 send W first. + // Shift RGB indices up by 1 and place W at index 0 before applying user wSwap. + if (ledType == TYPE_TM1814 || ledType == TYPE_TM1815) { + _idxR++; _idxG++; _idxB++; + _idxW = 0; + } + const uint8_t wSwap = co >> 4; + if (wSwap == WSWAP_B) { std::swap(_idxW, _idxB); } // swap W & B + if (wSwap == WSWAP_G) { std::swap(_idxW, _idxG); } // swap W & G + if (wSwap == WSWAP_R) { std::swap(_idxW, _idxR); } // swap W & R + } else if (numChannels >= CHANNELS_CCT) { + const uint8_t wSwap = co >> 4; + if (wSwap == WSWAP_WWCW) { std::swap(_idxW, _idxCW); } // swap WW & CW + } + + // 16-bit chips: two wire bytes per logical channel; lower nibble stores wire bytes. + if (ledType == TYPE_UCS8903 || ledType == TYPE_UCS8904 || ledType == TYPE_SM16825) { + flags |= NCHF_16BIT; + numChannels *= 2; // 16bit buses use two bytes per color channel + } + _pixelFormat = numChannels | flags; + +} + +// channelMap[i]: ChannelSource value (CH_UNUSED/CH_R/CH_G/CH_B/CH_W/CH_WW/CH_CW) +ColorEncoder::ColorEncoder(const uint8_t channelMap[MAX_CUSTOM_CHANNELS], uint8_t numChannels, uint8_t invertMask, bool is16bit) +{ + memcpy(_channelMap, channelMap, MAX_CUSTOM_CHANNELS); + _invertMask = invertMask; + _idxR = _idxG = _idxB = _idxW = _idxCW = 0; // unused in custom path + uint8_t flags = NCHF_CUSTOM; + if (is16bit) { + flags |= NCHF_16BIT; + numChannels *= 2; // 2 wire bytes per logical channel in 16-bit mode + } + if (invertMask) flags |= NCHF_INVERT; // set invert flag so encodeGeneric is always called for custom + _pixelFormat = numChannels | flags; +} + +// Generic encoder for non-fast-path cases: NCHF_INVERT, NCHF_CUSTOM, +// and any 16-bit + invert combination. +void ColorEncoder::encodeGeneric(uint32_t c, const CctPixel& cct, uint8_t* out, uint8_t bri) const { + const uint8_t flags = _pixelFormat & 0xF0; + const uint8_t logCh = _pixelFormat & 0x0F; + + if (flags & NCHF_CUSTOM) { + // Custom channel map: each wire byte is assigned a color source from _channelMap + // logCh is wire bytes (numChannels * 2 for 16-bit, numChannels otherwise) + const bool b16 = (flags & NCHF_16BIT) != 0; + const uint8_t numLogi = b16 ? logCh / 2 : logCh; + for (uint8_t i = 0; i < numLogi; i++) { + uint8_t val; + switch (_channelMap[i]) { + case CH_R: val = getR(c); break; + case CH_G: val = getG(c); break; + case CH_B: val = getB(c); break; + case CH_W: val = getW(c); break; + case CH_WW: val = cct.ww; break; + case CH_CW: val = cct.cw; break; + default: val = 0; break; // CH_UNUSED + } + if (_invertMask & (1u << i)) val ^= 0xFF; + if (b16) { + const uint16_t v16 = (uint16_t)val * bri; + out[i*2] = v16 >> 8; + out[i*2+1] = v16 & 0xFF; + } else { + out[i] = val; + } + } + return; + } + + // NCHF_INVERT, optionally combined with NCHF_16BIT + // logCh is wire bytes: 6/8/10 for 16-bit, 3/4/5 for 8-bit + if (flags & NCHF_16BIT) { + switch (logCh) { + case WB_RGB16: encodeRGB16(c, out, bri); break; + case WB_RGBW16: encodeRGBW16(c, out, bri); break; + default: encodeCCT16(c, cct, out, bri); break; // WB_CCT16 + } + } else { + switch (logCh) { + case WB_RGB: encodeRGB(c, out); break; + case WB_RGBW: encodeRGBW(c, out); break; + default: encodeCCT(c, cct, out); break; // WB_CCT + } + } + // Apply invert mask; logCh already equals wire bytes + for (uint8_t i = 0; i < logCh; i++) { + if (_invertMask & (1u << i)) out[i] ^= 0xFF; + } +} + +uint32_t ColorEncoder::decodeGeneric(const uint8_t* in) const { + const uint8_t flags = _pixelFormat & 0xF0; + const uint8_t logCh = _pixelFormat & 0x0F; + + if (flags & NCHF_CUSTOM) { + // Custom channel map decode: reconstruct RGBW from wire bytes + const bool b16 = (flags & NCHF_16BIT) != 0; + const uint8_t numLogi = b16 ? logCh / 2 : logCh; + uint8_t r = 0, g = 0, b_ = 0, w = 0; + for (uint8_t i = 0; i < numLogi; i++) { + uint8_t val = b16 ? in[i*2] : in[i]; // take high byte for 16-bit + if (_invertMask & (1u << i)) val ^= 0xFF; + switch (_channelMap[i]) { + case CH_R: r = val; break; + case CH_G: g = val; break; + case CH_B: b_ = val; break; + case CH_W: case CH_WW: case CH_CW: // W, WW, CW → map to W (lossy) + if (val > w) w = val; break; + } + } + return makeColor(r, g, b_, w); + } + + // NCHF_INVERT, optionally combined with NCHF_16BIT + // logCh is wire bytes: 6/8/10 for 16-bit, 3/4/5 for 8-bit + uint8_t r, g, b, w = 0; + if (flags & NCHF_16BIT) { + r = readU16Hi(in, _idxR); g = readU16Hi(in, _idxG); b = readU16Hi(in, _idxB); + if (logCh >= WB_RGBW16) w = readU16Hi(in, _idxW); // WB_RGBW16=RGBW16, WB_CCT16=CCT16 + } else { + r = in[_idxR]; g = in[_idxG]; b = in[_idxB]; + if (logCh >= WB_RGBW) w = in[_idxW]; // WB_RGBW=RGBW, WB_CCT=CCT + } + if (flags & NCHF_INVERT) { + if (_invertMask & (1u << _idxR)) r ^= 0xFF; + if (_invertMask & (1u << _idxG)) g ^= 0xFF; + if (_invertMask & (1u << _idxB)) b ^= 0xFF; + const bool hasW = (flags & NCHF_16BIT) ? (logCh >= WB_RGBW16) : (logCh >= WB_RGBW); + if (hasW && (_invertMask & (1u << _idxW))) w ^= 0xFF; + } + return makeColor(r, g, b, w); +} + + +//============================================================================== +// Bus Factory Implementation +//============================================================================== + +PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType, size_t numPixels) { + + PixelBus* bus = nullptr; + // TODO: need to re-order the colorOrder here to match how NPB is mapping the colors or user configs will be wrong. NPB uses IC datasheet orders (to be confirmed) + // for example, SM16825 is RGBWY order on the chip. + switch (driver) { +#if defined(ESP32) + case BusDriver::RMT: + bus = new RmtBus(pin, timing, colorOrder, numChannels, ledType); + break; + +#ifdef WLEDPB_I2S_SUPPORT + case BusDriver::I2S: +#if defined(CONFIG_IDF_TARGET_ESP32C3) + #error C3 hardware does not support parallel I2S output and single channel output is not implemented, use WLEDPB_PARALLEL_SPI_SUPPORT instead +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + bus = new I2sBus(pin, timing, colorOrder, numChannels, 0, ledType, numPixels); +#else + bus = new I2sBus(pin, timing, colorOrder, numChannels, 1, ledType, numPixels); +#endif + break; +#endif + +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT + case BusDriver::SPI: + bus = new ParallelSpiBus(pin, timing, colorOrder, numChannels, ledType); + break; +#endif +#ifdef WLEDPB_PARLIO_SUPPORT + case BusDriver::PARLIO: + // ParlioBus registers itself with the shared ParlioBusContext singleton. + // numPixels is used for DMA buffer sizing. + bus = new ParlioBus(pin, timing, colorOrder, numChannels, 0, ledType, numPixels); + break; +#endif +#if (WLED_MAX_BB_CHANNELS > 0) + case BusDriver::BitBang: + bus = new BitBangBus(pin, timing, colorOrder, numChannels, ledType); + break; +#endif +#elif defined(ESP8266) + case BusDriver::UART: + bus = new Esp8266UartBus(pin, timing, colorOrder, numChannels, ledType); + break; + case BusDriver::DMA: + bus = new Esp8266DmaBus(pin, timing, colorOrder, numChannels, ledType); + break; + case BusDriver::BitBang: + bus = new BitBangBus(pin, timing, colorOrder, numChannels, ledType); + break; +#elif (WLED_MAX_BB_CHANNELS > 0) + // remaining ESP32 variants (C5/C6/C61/P4): BitBang is the only supported driver so far + case BusDriver::BitBang: + bus = new BitBangBus(pin, timing, colorOrder, numChannels, ledType); + break; +#endif + + default: + return nullptr; + } + + // Chip-specific post-creation configuration. + // Must be done before begin() so allocateEncodeBuffer() reserves the correct space. + if (bus) { + switch (ledType) { + case TYPE_TM1814: + case TYPE_TM1815: + bus->setPrefixLen(TM1814_PREFIX_LEN); // C1+C2 current-config prefix; updated per-frame in BusDigital::setBrightness() + bus->setInverted(true); + break; + case TYPE_TM1914: + bus->setPrefixLen(TM1914_PREFIX_LEN); // mode-setting prefix; written once in BusDigital::begin() + bus->setInverted(true); + break; + case TYPE_SM16825: + bus->setSuffixLen(SM16825_SUFFIX_LEN); // per-frame configuration suffix; defaults written by allocateEncodeBuffer() + break; + default: + break; + } + } + + return bus; +} // createBus + +} // namespace WLEDpixelBus + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h new file mode 100644 index 0000000000..806b26b98b --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -0,0 +1,606 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#if defined(ARDUINO_ARCH_ESP32) + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "esp_heap_caps.h" +#include "esp_attr.h" +#include "driver/gpio.h" +#include "esp_idf_version.h" + +// I2S support: targets where the LCD Intel 8080 bus is present +#if SOC_LCD_I80_BUSES + #define WLEDPB_I2S_SUPPORT +#endif + +// SPI parallel support (C3 - uses SPI quad mode with GDMA) +#if defined(CONFIG_IDF_TARGET_ESP32C3) + #define WLEDPB_PARALLEL_SPI_SUPPORT +#endif + +// ESP_HAS_HIGH_GPIO_BANK — defined when the target has GPIO > 31 and therefore +// needs the second GPIO output register bank (GPIO_OUT1_W1TS/TC_REG). +#if SOC_GPIO_PIN_COUNT > 32 +# define ESP_HAS_HIGH_GPIO_BANK 1 +#endif + +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT +#include "soc/spi_struct.h" +#include "soc/gdma_struct.h" +#include "hal/gdma_ll.h" +#include "soc/gdma_reg.h" +#include "rom/lldesc.h" +#endif + +#elif defined(ESP8266) +// nothing to do here + +#else +#error "WLEDpixelBus only supports ESP32 and ESP8266 platforms" +#endif + +#include "WLEDpixelBus_Timings.h" +#include "WLEDpixelBus_Features.h" + +namespace WLEDpixelBus { + + +//============================================================================== +// WLED Pixel Format - uint32_t RGBW +//============================================================================== + +/** + * Extract color components from WLED's uint32_t format + * Format: 0xWWRRGGBB (W in high byte, B in low byte) + */ +inline uint8_t getR(uint32_t color) { return (color >> 16) & 0xFF; } +inline uint8_t getG(uint32_t color) { return (color >> 8) & 0xFF; } +inline uint8_t getB(uint32_t color) { return color & 0xFF; } +inline uint8_t getW(uint32_t color) { return (color >> 24) & 0xFF; } + +/** + * Create uint32_t color from components + */ +inline uint32_t makeColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { + return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; +} + +//============================================================================== +// CCT (Warm White / Cool White) Support +//============================================================================== + +/** + * CCT data for a single pixel + */ +struct CctPixel { + union { + uint16_t wwcw; // Access as a 16-bit value (0xWWCW), default when setting 16-bit CCT types + struct { + uint8_t ww; // Warm white + uint8_t cw; // Cool white + }; + }; +}; + +//============================================================================== +// Driver State +//============================================================================== + +enum class DriverState : uint8_t { + Idle = 0, + Sending = 1, + SendingLast = 2, // Last data buffer was filled; wait for current buffer to finish so last-data buffer plays TODO: check if this is still used, otherwise, remove + WaitingReset = 3 // Last data buffer played; zero buffer playing as reset signal +}; + +//============================================================================== +// DMA Buffer Configuration +//============================================================================== + +constexpr size_t MIN_DMA_BUFFER_SIZE = 256; +constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; // 12bit in DMA descriptor +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT // C3 only + constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*2); // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (2k per buffer means about 1ms interrupt intervals with 4 step cadence) +#else + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT + constexpr size_t DEFAULT_DMA_BUFFER_SIZE = MAX_DMA_BUFFER_SIZE; // note: 3k is enough except if using 8RMT (ESP32 classic), also requires triple buffering + #else + constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*3); + #endif +#endif +static_assert(DEFAULT_DMA_BUFFER_SIZE <= 4092, "DEFAULT_DMA_BUFFER_SIZE exceeds the maximum possible size"); + +//============================================================================== +// Color Encoding Helpers +//============================================================================== + +// Upper-nibble flags packed into ColorEncoder::_pixelFormat. +// Lower nibble = logical channel count. Upper nibble = combination of these flags. +static constexpr uint8_t NCHF_16BIT = 0x10; // 16-bit chip (UCS8903/8904/SM16825): wire bytes = logCh * 2 +static constexpr uint8_t NCHF_INVERT = 0x20; // one or more channels polarity-inverted (_invertMask applies) +static constexpr uint8_t NCHF_CUSTOM = 0x40; // custom channel map: _channelMap[] drives encoding +// 0x80 reserved + +// Maximum number of wire-byte entries in a custom channel map (ColorEncoder::_channelMap[]). +static constexpr uint8_t MAX_CUSTOM_CHANNELS = 6; // note: changing this wont propaget to the UI, currently ICs with more than 6 channels are not supported + +// Chip-specific prefix/suffix byte lengths reserved in the encode buffer (see createBus()). +static constexpr uint8_t TM1814_PREFIX_LEN = 8; // C1+C2 current-config prefix +static constexpr uint8_t TM1914_PREFIX_LEN = 6; // mode-setting prefix +static constexpr uint8_t SM16825_SUFFIX_LEN = 4; // per-frame configuration suffix + +// Logical channel count thresholds used by the ColorEncoder(uint8_t co, ...) constructor +// to decide which per-channel wiring rules apply. +static constexpr uint8_t CHANNELS_RGBW = 4; // RGBW: W-swap applies, TM1814/TM1815 W-first quirk applies +static constexpr uint8_t CHANNELS_CCT = 5; // RGB + WW + CW: WW/CW-swap applies + +/** + * RGB wire-position permutation, decoded from the lower nibble of WLED's color-order byte (config value) + * Mirrors COL_ORDER_* in wled00/const.h (kept independent here to avoid dependency). + */ +enum ColorOrder : uint8_t { + ORDER_GRB = 0, // default + ORDER_RGB = 1, // common for WS2811 + ORDER_BRG = 2, + ORDER_RBG = 3, + ORDER_BGR = 4, + ORDER_GBR = 5, +}; + +/** + * White-channel swap selector, decoded from the upper nibble of WLED's color-order byte. + * For 4-channel (RGBW) buses, WSWAP_B/G/R swaps the W wire position with B/G/R. + * For 5+ channel (CCT) buses, WSWAP_WWCW swaps the warm-white and cool-white wire positions. + */ +enum WSwap : uint8_t { + WSWAP_NONE = 0, + WSWAP_B = 1, // swap W & B + WSWAP_G = 2, // swap W & G + WSWAP_R = 3, // swap W & R + WSWAP_WWCW = 4, // swap WW & CW +}; + +/** + * Custom channel map color source, used by ColorEncoder::_channelMap[] and the + * custom-channel-map ColorEncoder constructor. Selects which color source feeds a given wire byte. + */ +enum ChannelSource : uint8_t { + CH_UNUSED = 0, + CH_R = 1, + CH_G = 2, + CH_B = 3, + CH_W = 4, + CH_WW = 5, + CH_CW = 6, +}; + +/** + * Wire-byte counts per pixel for each pixel format (logical channels for 8-bit types; + * logical channels * 2 for 16-bit types). Used to dispatch encodeGeneric()/decodeGeneric(). + */ +enum WireBytes : uint8_t { + WB_RGB = 3, + WB_RGBW = 4, + WB_CCT = 5, + WB_RGB16 = 6, + WB_RGBW16 = 8, + WB_CCT16 = 10, +}; + +/** + * Pixel encoder: maps RGBW uint32_t to a per-LED byte stream according to color order. + * + * _pixelFormat packs two things: + * bits[3:0] wire bytes per pixel (= logical channels for 8-bit; logical channels * 2 for 16-bit) + * bits[7:4] NCHF_* flags (16BIT | INVERT | CUSTOM) + * + * setPixel / getPixelColor switch on _pixelFormat directly → single branch-free dispatch. + * 0x03/04/05 fast 8-bit RGB / RGBW / CCT paths (no inversion) + * 3|NCHF_16BIT etc. fast 16-bit paths (no inversion) + * all other values encodeGeneric / decodeGeneric (inverted, custom, special chips) + * + * getPixelBytes() returns wire bytes: logCh * 2 for 16-bit types, logCh otherwise. + */ +class ColorEncoder { +public: + ColorEncoder() : _pixelFormat(0x03), _invertMask(0), _idxR(0), _idxG(1), _idxB(2), _idxW(3), _idxCW(4) { memset(_channelMap, 0, sizeof(_channelMap)); } + ColorEncoder(uint8_t co, uint8_t numChannels, uint8_t ledType = 0); + // Custom channel map constructor for custom color orders + // channelMap[i]: ChannelSource value (CH_UNUSED/CH_R/CH_G/CH_B/CH_W/CH_WW/CH_CW) + ColorEncoder(const uint8_t channelMap[MAX_CUSTOM_CHANNELS], uint8_t numChannels, uint8_t invertMask, bool is16bit); + + // ------------------------------------------------------------------------- + // Fast encode — standard 8-bit types (no invert) + // ------------------------------------------------------------------------- + + inline void encodeRGB(uint32_t c, uint8_t* out) const { + out[_idxR] = getR(c); out[_idxG] = getG(c); out[_idxB] = getB(c); + } + inline void encodeRGBW(uint32_t c, uint8_t* out) const { + out[_idxR] = getR(c); out[_idxG] = getG(c); out[_idxB] = getB(c); out[_idxW] = getW(c); + } + inline void encodeCCT(uint32_t c, const CctPixel& cct, uint8_t* out) const { + out[_idxR] = getR(c); out[_idxG] = getG(c); out[_idxB] = getB(c); + out[_idxW] = cct.ww; out[_idxCW] = cct.cw; + } + + // ------------------------------------------------------------------------- + // Fast encode — 16-bit types (UCS8903 / UCS8904 / SM16825) + // ------------------------------------------------------------------------- + + inline void encodeRGB16(uint32_t c, uint8_t* out, uint8_t bri) const { + writeU16(out, _idxR, getR(c), bri); + writeU16(out, _idxG, getG(c), bri); + writeU16(out, _idxB, getB(c), bri); + } + inline void encodeRGBW16(uint32_t c, uint8_t* out, uint8_t bri) const { + writeU16(out, _idxR, getR(c), bri); + writeU16(out, _idxG, getG(c), bri); + writeU16(out, _idxB, getB(c), bri); + writeU16(out, _idxW, getW(c), bri); + } + inline void encodeCCT16(uint32_t c, const CctPixel& cct, uint8_t* out, uint8_t bri) const { + writeU16(out, _idxR, getR(c), bri); + writeU16(out, _idxG, getG(c), bri); + writeU16(out, _idxB, getB(c), bri); + writeU16(out, _idxW, cct.ww, bri); + writeU16(out, _idxCW, cct.cw, bri); + } + + // ------------------------------------------------------------------------- + // Fast decode — standard 8-bit types (no invert) + // ------------------------------------------------------------------------- + + inline uint32_t decodeRGB(const uint8_t* in) const { + return makeColor(in[_idxR], in[_idxG], in[_idxB]); + } + inline uint32_t decodeRGBW(const uint8_t* in) const { + return makeColor(in[_idxR], in[_idxG], in[_idxB], in[_idxW]); + } + inline uint32_t decodeCCT(const uint8_t* in) const { + return makeColor(in[_idxR], in[_idxG], in[_idxB], in[_idxW]); // WW→W, CW dropped (lossy) + } + + // ------------------------------------------------------------------------- + // Fast decode — 16-bit types + // ------------------------------------------------------------------------- + + inline uint32_t decodeRGB16(const uint8_t* in) const { + return makeColor(readU16Hi(in, _idxR), readU16Hi(in, _idxG), readU16Hi(in, _idxB)); + } + inline uint32_t decodeRGBW16(const uint8_t* in) const { + return makeColor(readU16Hi(in, _idxR), readU16Hi(in, _idxG), readU16Hi(in, _idxB), readU16Hi(in, _idxW)); + } + inline uint32_t decodeCCT16(const uint8_t* in) const { + return makeColor(readU16Hi(in, _idxR), readU16Hi(in, _idxG), readU16Hi(in, _idxB), readU16Hi(in, _idxW)); + } + + // ------------------------------------------------------------------------- + // Generic slow encode/decode — handles NCHF_INVERT, NCHF_SPEC1, NCHF_SPEC2 + // (and any 16-bit + invert combination); defined in WLEDpixelBus.cpp + // ------------------------------------------------------------------------- + + void encodeGeneric(uint32_t c, const CctPixel& cct, uint8_t* out, uint8_t bri) const; + uint32_t decodeGeneric(const uint8_t* in) const; + + // Accessors + uint8_t getPixelFormat() const { return _pixelFormat; } // packed dispatch key: lower nibble=wire bytes, upper=NCHF_* + uint8_t getColorChannels() const { return (_pixelFormat & NCHF_16BIT) ? (_pixelFormat & 0x0F) / 2 : (_pixelFormat & 0x0F); } // logical color channels + uint8_t getPixelBytes() const { return _pixelFormat & 0x0F; } // wire bytes per pixel (branch-free) + bool is16bit() const { return (_pixelFormat & NCHF_16BIT) != 0; } // true for UCS8903/8904/SM16825 + +private: + uint8_t _pixelFormat; // lower nibble = bytes per pixel, upper nibble = NCHF_ flags (invert, 16-bit, custom) + uint8_t _invertMask; // bitmask: bit i = invert color i (applies when NCHF_INVERT or NCHF_CUSTOM set) + uint8_t _idxR, _idxG, _idxB; // wire byte index for R, G, B + uint8_t _idxW, _idxCW; // wire byte index for W (or WW) and CW (CCT types) + uint8_t _channelMap[MAX_CUSTOM_CHANNELS]; // custom channel map: _channelMap[i] = ChannelSource for wire byte i + + // 16bit pixel helper functions + static inline void writeU16(uint8_t* out, uint8_t idx, uint8_t val8, uint8_t bri) { + const uint16_t v = (uint16_t)val8 * bri; + out[idx*2] = v >> 8; + out[idx*2+1] = v & 0xFF; + } + static inline uint8_t readU16Hi(const uint8_t* in, uint8_t idx) { return in[idx*2]; } +}; + +//============================================================================== +// Base Pixel Bus Interface +//============================================================================== + +class PixelBus { +protected: + uint8_t* _encodeBuffer = nullptr; // encoded pixel data ready for hardware transmission + size_t _encodeBufferSize = 0; // allocated size in bytes + uint16_t _numPixels = 0; + uint8_t _prefixLen = 0; // byte length of chip prefix at start of _encodeBuffer + ColorEncoder _encoder; // color encoder, set by derived class constructors + uint8_t _ledType = 0; // LED chip type (e.g. 31=TM1814); 0 = generic + uint8_t* _pixelData = nullptr; // _encodeBuffer + _prefixLen, cached to avoid per-call addition + uint8_t _suffixLen = 0; // byte length of chip suffix appended after pixel data + uint8_t _busBri = 255; // brightness for color_fade() in setPixelColor(): _bri for 8-bit types, + // fine residual for TM1814/TM1815, 255 (no-op) for 16-bit types + uint8_t _encBri = 255; // encoder brightness for 16-bit types (SM16825/UCS8903/UCS8904): + // applied as channel*_encBri for full 16-bit wire precision; 255 for 8-bit + +public: + virtual ~PixelBus() { + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; } + } + + /** + * Reserve prefix bytes before pixel data. Must be called BEFORE begin(). + * allocateEncodeBuffer() will zero the prefix region; updatePrefix() fills it per-frame. + * @param len Number of prefix bytes to reserve + */ + void setPrefixLen(uint8_t len) { + _prefixLen = len; + } + + /** + * Overwrite the prefix bytes in _encodeBuffer at runtime (e.g. per-frame for current control). + * Must be called AFTER begin() (i.e. after allocateEncodeBuffer()). No-op if buffer not ready. + * @param data New prefix bytes + * @param len Must be <= _prefixLen (will be clamped) + */ + void updatePrefix(const uint8_t* data, uint8_t len) { + if (!_encodeBuffer || len == 0) return; + if (len > _prefixLen) len = _prefixLen; + memcpy(_encodeBuffer, data, len); + } + + /** + * Set bus-level brightness applied during pixel encoding for all LED types. + * For 8-bit types: applied as video-scale fade on the full uint32_t pixel (hue-preserving). + * For 16-bit types (SM16825, UCS8903, UCS8904): applied as `channel * bri` → full 16-bit value. + * For TM1814/TM1815: set to the fine residual scale after hardware current-step selection. + * @param b brightness 0–255 + */ + void setBusBri(uint8_t b) { _busBri = b; } // TODO: brightness scaling/parameters may need some refinement + void setEncBri(uint8_t b) { _encBri = b; } + inline uint8_t getBusBri() const { return _busBri; } + + /** + * Set the APA102 5-bit per-pixel hardware brightness step (0–31). + * Default implementation is a no-op; overridden by SpiBus for TYPE_APA102. + * Called by BusDigital::setBrightness() as part of the two-stage brightness scheme: + * coarse control via hardware current step, fine control via color_fade() residual. + */ + virtual void setApa102HwBri(uint8_t /*v*/) {} + + /** + * physical output signal inversion (polarity). + * must be implemented on bus driver level + */ + virtual void setInverted(bool /*inv*/) { } + + /** + * Replace the color encoder (e.g. for curstom bus types after bus creation). + * Must be called after construction but before begin(). + */ + void setEncoder(const ColorEncoder& enc) { _encoder = enc; } + + bool hasPrefix() const { return _prefixLen > 0; } + uint8_t getPrefixLen() const { return _prefixLen; } + + /** + * Reserve suffix bytes after pixel data. Must be called BEFORE begin(). + * allocateEncodeBuffer() initialises the suffix region; updateSuffix() overwrites it. + * @param len Number of suffix bytes to reserve + */ + void setSuffixLen(uint8_t len) { + _suffixLen = len; + } + + /** + * Overwrite the suffix bytes in _encodeBuffer. + * Must be called AFTER begin() (i.e. after allocateEncodeBuffer()). No-op if buffer not ready. + * @param data New suffix bytes + * @param len Must be <= _suffixLen (will be clamped) + */ + virtual void updateSuffix(const uint8_t* data, uint8_t len) { + if (!_pixelData || _suffixLen == 0 || len == 0) return; + if (len > _suffixLen) len = _suffixLen; + memcpy(_pixelData + (size_t)_numPixels * _encoder.getPixelBytes(), data, len); + } + + bool hasSuffix() const { return _suffixLen > 0; } + uint8_t getSuffixLen() const { return _suffixLen; } + + virtual bool begin() = 0; + virtual void end() = 0; + // show() sends the pre-encoded _encodeBuffer to hardware. + virtual bool show() = 0; + virtual bool canShow() const = 0; +#ifdef WLED_DEBUG_BUS + virtual const char* getTypeStr() const = 0; +#endif + + /** + * Encode one pixel into _encodeBuffer at pos. + * c and ww/cw must already be brightness-scaled by the caller (BusDigital::setPixelColor + * applies color_fade() via _busBri before calling here). For 16-bit LED types the encoder + * multiplies each channel by _encBri for full 16-bit wire precision. + * @param pos pixel index (0-based, hardware index including skip) + * @param c RGBW color (brightness-scaled for 8-bit types; raw for 16-bit) + * @param wwcw warm-white/cool-white combined (CCT calculation result, brightness-scaled) + */ + // Preconditions (guaranteed by BusDigital calling path): + // _encodeBuffer != nullptr (_valid == true implies begin() succeeded) + // pos < _numPixels (setNumPixels = lenToCreate + _skip, pix bounded by both) + // note: using O2 optimization seems to make it slower + // TODO: on ESP32, do not put this in IRAM on C3 it works in IRAM, need to test if there is any speed benefit from IRAM (note: this may have all changed in IDF V5) + virtual bool setPixelColor(uint16_t pos, uint32_t c, uint16_t wwcw) { + const uint8_t pixelFormat = _encoder.getPixelFormat(); + uint8_t* out = _pixelData + (size_t)pos * _encoder.getPixelBytes(); + const CctPixel cct{wwcw}; + switch (pixelFormat) { + case 3: _encoder.encodeRGB(c, out); break; // 3ch RGB + case 4: _encoder.encodeRGBW(c, out); break; // 4ch RGBW + case 5: _encoder.encodeCCT(c, cct, out); break; // 5ch CCT + case (3*2) | NCHF_16BIT: _encoder.encodeRGB16(c, out, _encBri); break; // 16-bit RGB (6 bytes per pixel) + case (4*2) | NCHF_16BIT: _encoder.encodeRGBW16(c, out, _encBri); break; // 16-bit RGBW (8bytes per pixel) + case (5*2) | NCHF_16BIT: _encoder.encodeCCT16(c, cct, out, _encBri); break; // 16-bit CCT (10 bytes per pixel) + default: _encoder.encodeGeneric(c, cct, out, _encBri); break; // inverted / special cases + } + return true; + } + + /** + * Decode one pixel back from _encodeBuffer (used for read-modify-write and getPixelColor). + * Returns RGBW32 color. WW/CW are NOT encoded separately so CCT round-trips are lossy. + * Override only if the bus uses non-linear encoding (e.g. Esp8266DmaBus 4-step). + */ + virtual uint32_t getPixelColor(uint16_t pix) const { + const uint8_t pixelFormat = _encoder.getPixelFormat(); + const uint8_t* in = _pixelData + (size_t)pix * _encoder.getPixelBytes(); + switch (pixelFormat) { + case 3: return _encoder.decodeRGB(in); + case 4: return _encoder.decodeRGBW(in); + case 5: return _encoder.decodeCCT(in); + case (3*2) | NCHF_16BIT: return _encoder.decodeRGB16(in); + case (4*2) | NCHF_16BIT: return _encoder.decodeRGBW16(in); + case (5*2) | NCHF_16BIT: return _encoder.decodeCCT16(in); + default: return _encoder.decodeGeneric(in); + } + } + + /** + * Zero the encode buffer (set all pixels to black), preserving the prefix. + * Derived classes may override if their encoding is non-trivial (e.g. I2S 4-step). + * For all standard RGB/RGBW protocols, all-zero bytes encode as black. + */ + virtual void clearEncodeBuffer() { + if (_pixelData && _numPixels > 0) { + const size_t pixelBytes = (size_t)_numPixels * _encoder.getPixelBytes(); + memset(_pixelData, 0, pixelBytes); + } + } + + /** + * Proportionally scale all encoded channel bytes by scale/256 (video scale). + * Used by BusDigital::applyBriLimit() for ABL. No-op if scale == 255. + * Note: buses with non-linear encoding (e.g. Esp8266DmaBus 4-step) must override this. + */ + virtual void scaleAll(uint8_t scale) { + if (scale == 255 || !_pixelData || _numPixels == 0) return; + const size_t pixelBytes = (size_t)_numPixels * _encoder.getPixelBytes(); + for (size_t i = 0; i < pixelBytes; i++) { + _pixelData[i] = ((uint16_t)(_pixelData[i] + 1) * scale) >> 8; + } + } + + /** + * Allocate encode buffer. Called from begin() after hardware init. + * Default uses plain malloc; DMA buses override to use heap_caps_malloc. + * @param numPixels hardware pixel count (may include skipped pixels) + * @param numChannels bytes per pixel in the encoded stream + */ + virtual bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) { + const size_t pixelBytes = padPixelBytesForSuffix((size_t)numPixels * numChannels, _ledType); + size_t needed = _prefixLen + pixelBytes + _suffixLen; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; } + if (needed == 0) return true; + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { _encodeBufferSize = 0; return false; } + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + _prefixLen; + if (_suffixLen == sizeof(SM16825_SUFFIX) && _ledType == TYPE_SM16825) + memcpy(_pixelData + pixelBytes, SM16825_SUFFIX, sizeof(SM16825_SUFFIX)); + return true; + } + + size_t getEncodeBufferSize() const { return _encodeBufferSize; } + virtual uint16_t getNumPixels() const { return _numPixels; } + void setNumPixels(uint16_t n) { _numPixels = n; } +}; + +//============================================================================== +// Forward Declarations +//============================================================================== + +class RmtBus; + +#ifdef WLEDPB_I2S_SUPPORT +class I2sBus; +class I2sBusContext; +#endif + +#ifdef WLEDPB_PARLIO_SUPPORT +class ParlioBus; +class ParlioBusContext; +#endif + +//============================================================================== +// Bus Factory - Create appropriate bus for platform +//============================================================================== + +enum class BusDriver : uint8_t { + RMT = 0, + I2S = 1, // I2S on ESP32 and S2, LCD on S3 + SPI = 2, // parallel SPI output (C3) + UART = 3, + DMA = 4, + BitBang = 5, + PARLIO = 6 // PARLIO TX parallel output (C6, H2, C5, P4 — IDF >= 5.3) +}; + +/** + * Get the maximum number of RMT TX channels for the current platform + */ +constexpr uint8_t getRmtMaxChannels() { +#if defined(CONFIG_IDF_TARGET_ESP32) + return 8; // ESP32 original: 8 RMT channels +#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + return 4; // ESP32-S2/S3: 4 RMT TX channels +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32H2) + return 2; // ESP32-C3/C6/C5/C61/H2: 2 RMT TX channels +#else + return 0; +#endif +} + +/** + * Create a bus instance + * @param type Bus driver type + * @param pin GPIO pin + * @param timing LED timing + * @param colorOrder Color order byte + * @param numChannels Bytes per pixel in the encoded stream + * @param channel RMT channel to use (-1 for auto-allocate) + * @param ledType WLED LED type constant (TYPE_*), used for chip-specific init + * @param bufferSize DMA buffer size (for I2S/LCD) + * @return Bus instance (caller owns, delete when done) + */ +PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, + uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + +} // namespace WLEDpixelBus + +#include "WLEDpixelBus_SPI.h" + +#if defined(ESP32) +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_ParallelSpi.h" +#include "WLEDpixelBus_PARLIO.h" +#include "WLEDpixelBus_BitBang.h" +#elif defined(ESP8266) +#include "WLEDpixelBus_ESP8266.h" +#include "WLEDpixelBus_BitBang.h" +#endif + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp new file mode 100644 index 0000000000..eecae851dd --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -0,0 +1,417 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +works on all ESP32 variants and ESP8266 + +Uses software bit-bang timing and outputs data on all set-up pins in parallel. +Interrupts are are re-enabled after each LED data is written, if the return time is too long, output is aborted +The frame is then sent again to avoid effect tearing, this is repeated a few times before giving up +If reset period is set to zero, interrupts are disabled and the sendout is blocking +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_BitBang.h" +#if defined(ARDUINO_ARCH_ESP32) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + #include "esp_private/esp_clk.h" +#else + #include "clk.h" // esp_clk_cpu_freq() +#endif + + +#include "esp_cpu.h" +#elif defined(ESP8266) +#include +#define REG_WRITE(addr, val) GPIO_REG_WRITE(addr, val) +#define GPIO_OUT_W1TS_REG GPIO_OUT_W1TS_ADDRESS +#define GPIO_OUT_W1TC_REG GPIO_OUT_W1TC_ADDRESS +#endif + +namespace WLEDpixelBus { +// BB state, shared among all buses +BitBangBus::BBstate* BitBangBus::_BBs = nullptr; + +// critical section macros +#if defined(ESP8266) + #define WPB_BB_ENTERCRITICAL() os_intr_lock() + #define WPB_BB_EXITCRITICAL() os_intr_unlock() +#else + #define WPB_BB_ENTERCRITICAL() portENTER_CRITICAL(&_BBs->mux) + #define WPB_BB_EXITCRITICAL() portEXIT_CRITICAL(&_BBs->mux) +#endif + + +// getCycleCount() reads the CPU cycle counter for precise timing +#if defined(ESP8266) + static inline uint32_t getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("rsr %0,ccount" : "=a"(ccount)); + return ccount; + } + static constexpr uint32_t LOOPTEST_CYCLES = 16; +#else + + static inline __attribute__((always_inline)) uint32_t getCycleCount() { + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + return esp_cpu_get_cycle_count(); + #else + return esp_cpu_get_ccount(); + #endif + } +#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32P4) + static constexpr uint32_t LOOPTEST_CYCLES = 8; +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + static constexpr uint32_t LOOPTEST_CYCLES = 10; +#else // ESP32 and S2 + static constexpr uint32_t LOOPTEST_CYCLES = 12; // note: LOOPTEST_CYCLES values need fine-tuning +#endif +#endif + +// Constructor / Destructor +BitBangBus::BitBangBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _initialized(false) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +BitBangBus::~BitBangBus() { + end(); +} + +bool BitBangBus::begin() { + if (_initialized) return true; + + // GPIO pin range check +#if defined(ARDUINO_ARCH_ESP32) + if (_pin < 0 || _pin >= SOC_GPIO_PIN_COUNT || !((SOC_GPIO_VALID_OUTPUT_GPIO_MASK >> _pin) & 1ULL)) return false; +#elif defined(ESP8266) + if (_pin < 0 || _pin > 15) return false; +#endif + + if (!_BBs) { + _BBs = (BBstate*)calloc(1, sizeof(BBstate)); // Allocate shared state struct on first channel + if (!_BBs) return false; + } + if (_BBs->channelCount >= WLED_MAX_BB_CHANNELS) return false; + + // Configure GPIO as output, drive LOW (idle state for non-inverted LEDs) +#if defined(ARDUINO_ARCH_ESP32) + gpio_set_direction((gpio_num_t)_pin, GPIO_MODE_OUTPUT); + gpio_set_level((gpio_num_t)_pin, 0); +#elif defined(ESP8266) + pinMode(_pin, OUTPUT); + digitalWrite(_pin, LOW); +#endif + + // Convert nanosecond timings to CPU cycles +#if defined(ARDUINO_ARCH_ESP32) + const uint32_t cpuMHz = esp_clk_cpu_freq() / 1000000u; +#elif defined(ESP8266) + const uint32_t cpuMHz = ESP.getCpuFreqMHz(); +#endif + + // Subtract the while-loop test overhead (one extra iteration) to compensate for the latency between the condition passing and the actual GPIO write. + const uint32_t lo = LOOPTEST_CYCLES; + + uint32_t t0h = (_timing.t0h_ns * cpuMHz) / 1000u; + uint32_t t1h = (_timing.t1h_ns * cpuMHz) / 1000u; + t0h = (t0h > lo) ? t0h - lo : 0u; + t1h = (t1h > lo) ? t1h - lo : 0u; + uint32_t period = (_timing.bitPeriod() * cpuMHz) / 1000u; + period = (period > lo) ? period - lo : 0u; + // max time allowed before we assume a latch, use half the reset period. + // Note: there are strips that latch after just ~10us maybe even less, users can customize this period and set it to 10us if there are issues. + // if the reset period is set to 0, interrupts are disabled + uint32_t resetus = _timing.reset_us < 10 ? 10 : _timing.reset_us; // note: below 10us the output will starve due to short interrupts + const uint32_t latchCycles = resetus * cpuMHz; + if (_timing.reset_us > 0) _BBs->allowInterrupts = true; + else _BBs->allowInterrupts = false; // disable interrupt polling if set to zero + + // Register in the shared static table + const uint8_t idx = _BBs->channelCount; + _BBs->pins[idx] = _pin; +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_pin >= 32) _BBs->allMaskHigh |= (1u << (_pin - 32)); + else _BBs->allMask |= (1u << _pin); +#else + _BBs->allMask |= (1u << _pin); +#endif + _BBs->channelCount++; + + // Allocate the per-pixel encode buffer (via PixelBus helper) + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { + _BBs->channelCount--; +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_pin >= 32) _BBs->allMaskHigh &= ~(1u << (_pin - 32)); + else _BBs->allMask &= ~(1u << _pin); +#else + _BBs->allMask &= ~(1u << _pin); +#endif + return false; + } +#if defined(ARDUINO_ARCH_ESP32) + esp_rom_gpio_connect_out_signal(_pin, SIG_GPIO_OUT_IDX, _inverted, false); // route output pin, inverts signal in hardware if needed +#endif + + // Publish per-channel data pointers into the shared static arrays. + // Timing is set here (same for all channels; overwriting with same values is harmless). + _BBs->numPixels[idx] = _numPixels; + _BBs->pixelData[idx] = _pixelData; + _BBs->t0h = t0h; + _BBs->t1h = t1h; + _BBs->period = period; + _BBs->latchCycles = latchCycles; + _BBs->pixelBytes = _encoder.getPixelBytes(); + + _initialized = true; + return true; +} + +// invert output signal, must be set before begin() +void BitBangBus::setInverted(bool inv) { + _inverted = inv; +} + +void BitBangBus::end() { + if (_initialized) { + // Find our slot by scanning _BBs->pins (no stored index needed). + uint8_t slot = _BBs->channelCount; // sentinel: not found + for (uint8_t i = 0; i < _BBs->channelCount; i++) { + if (_BBs->pins[i] == _pin) { slot = i; break; } + } + if (slot < _BBs->channelCount) { + // Shift remaining entries down to fill the gap. + for (uint8_t i = slot; i + 1 < _BBs->channelCount; i++) { + _BBs->pins[i] = _BBs->pins[i + 1]; + _BBs->numPixels[i] = _BBs->numPixels[i + 1]; + _BBs->pixelData[i] = _BBs->pixelData[i + 1]; + } + _BBs->channelCount--; + _BBs->allMask = 0; +#ifdef ESP_HAS_HIGH_GPIO_BANK + _BBs->allMaskHigh = 0; + for (uint8_t i = 0; i < _BBs->channelCount; i++) { + if (_BBs->pins[i] >= 32) _BBs->allMaskHigh |= (1u << (_BBs->pins[i] - 32)); + else _BBs->allMask |= (1u << _BBs->pins[i]); + } +#else + for (uint8_t i = 0; i < _BBs->channelCount; i++) _BBs->allMask |= (1u << _BBs->pins[i]); +#endif + _BBs->stagedCount = 0; + } + } + _initialized = false; + +#if defined(ARDUINO_ARCH_ESP32) + if (_pin >= 0) gpio_reset_pin((gpio_num_t)_pin); // reset all pin settings, including inversion +#elif defined(ESP8266) + if (_pin >= 0) pinMode(_pin, INPUT); +#endif + + // Release the encode buffer via base class helper. + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _pixelData = nullptr; + } + + // If no channels left, free the shared state struct + if (_BBs && _BBs->channelCount == 0) { + free(_BBs); + _BBs = nullptr; + } +} + +// show() stages this channel's data. When the last channel stages, run outputParallel() +bool BitBangBus::show() { + if (!_initialized || !_pixelData) return false; + + _BBs->stagedCount++; + if (_BBs->stagedCount < _BBs->channelCount) { + return true; // not all channels have been staged, we need all of them triggered before sending + } + + _BBs->stagedCount = 0; // reset for next frame + int retryFrame = 3; // if sending is aborted, retry this frame + while (retryFrame-- > 0) { + if (outputParallel()) break; // send data to LEDs (blocking but ISRs are allowed if reset period is not set to 0) + // if sendout got interrupted, we need to wait for the LEDs to definitely latch + uint32_t resetStart = getCycleCount(); + while ((getCycleCount() - resetStart) < _BBs->latchCycles) yield(); + } + return true; // frame sent (or at least we tried) +} + +// resetChannels() — called by PixelBusAllocator::resetChannelTracking() +void BitBangBus::resetChannels() { + if (_BBs) memset(_BBs, 0, sizeof(BBstate)); +} + + +// --------------------------------------------------------------------------- +// outputParallel() is the hot path and MUST be in IRAM +// +// Per-bit sequence: +// 1. Compute zeroMask for the current bit (channels outputting a '0', or past +// their data end, contribute their pin to the mask). +// 2. Wait for the full bit period since the previous HIGH falling edge. +// 3. Set all output pins HIGH simultaneously (setOutputMask). +// 4. After T0H cycles: pull the '0' outputs LOW (GPIO.out_w1tc = zeroMask). +// 5. After T1H cycles: pull all remaining outputs LOW (GPIO.out_w1tc = setOutputMask). +// 6. Repeat until all bits for this LED are sent out +// 6. At each pixel boundary, release the ISR lock if enabled so the +// FreeRTOS scheduler and time-critical ISRs can run. Re-acquire and check +// whether the idle gap exceeded the LED latch threshold; abort if so. +// +// All channels are pulsed for maxPixels × pixelBytes × 8 bits. Channels that +// have fewer pixels than maxPixels simply output '0' bits once their data ends +// Note: checked for speed, this is pretty much optimal, no need to use O2 or optimize further +// --------------------------------------------------------------------------- +//note the noinline attribute is needed so the compiler is forced to put this into IRAM +bool IRAM_ATTR __attribute__((noinline)) BitBangBus::outputParallel() { + if (!_BBs || _BBs->channelCount == 0) return true; + // cache for speed + const uint32_t t0h = _BBs->t0h; + const uint32_t t1h = _BBs->t1h; + const uint32_t period = _BBs->period; + const uint32_t latchCycles = _BBs->latchCycles; + const uint8_t pixelBytes = _BBs->pixelBytes; + const uint8_t nCh = _BBs->channelCount; + const bool allowInterrupts = _BBs->allowInterrupts; + // GPIO output masks — split into low bank (pins 0–31, all variants) and + // high bank (pins 32+, ESP32/S2/S3 only via GPIO_OUT1_W1TS/TC_REG). +#ifdef ESP_HAS_HIGH_GPIO_BANK + const uint32_t setOutputMaskLow = _BBs->allMask; // pins 0–31 + const uint32_t setOutputMaskHigh = _BBs->allMaskHigh; // pins 32+ +#else + const uint32_t setOutputMask = _BBs->allMask; // GPIO bitmask of all active output pins (0–31) +#endif + + // Find the maximum pixel count across all channels (drives total loop length). + uint16_t maxPixels = 0; + for (uint8_t ch = 0; ch < nCh; ch++) { + if (_BBs->numPixels[ch] > maxPixels) maxPixels = _BBs->numPixels[ch]; + } + if (maxPixels == 0) return true; + + // Per-channel pin masks on ESP32 & S3, pins ≥32 go into the high-bank mask; others into the low-bank mask. + uint32_t chanPinMask[nCh]; +#ifdef ESP_HAS_HIGH_GPIO_BANK + uint32_t chanPinMaskHigh[nCh]; +#endif + uint32_t chanTotalBytes[nCh]; + for (uint8_t ch = 0; ch < nCh; ch++) { + #ifdef ESP_HAS_HIGH_GPIO_BANK + if (_BBs->pins[ch] >= 32) { + chanPinMask[ch] = 0; + chanPinMaskHigh[ch] = 1u << (_BBs->pins[ch] - 32); + } else { + chanPinMask[ch] = 1u << _BBs->pins[ch]; + chanPinMaskHigh[ch] = 0; + } + #else + chanPinMask[ch] = 1u << _BBs->pins[ch]; + #endif + chanTotalBytes[ch] = (uint32_t)_BBs->numPixels[ch] * pixelBytes; + } + + // Returns the GPIO mask(s) of all channels that should output a logical '0' for + // the given bit index. Channels that have exhausted their pixel data also + // output '0'. Bit order is MSB-first within each byte. +#ifdef ESP_HAS_HIGH_GPIO_BANK + auto computeZeroMasks = [&](uint32_t bitIndex, uint32_t& zmLow, uint32_t& zmHigh) __attribute__((always_inline)) { + const uint32_t byteIndex = bitIndex >> 3; + const uint8_t bitMask = 0x80u >> (bitIndex & 7u); + zmLow = 0; zmHigh = 0; + for (uint8_t ch = 0; ch < nCh; ch++) { + if (byteIndex >= chanTotalBytes[ch] || !(_BBs->pixelData[ch][byteIndex] & bitMask)) { + zmLow |= chanPinMask[ch]; + zmHigh |= chanPinMaskHigh[ch]; + } + } + }; +#else + auto computeZeroMask = [&](uint32_t bitIndex) __attribute__((always_inline)) -> uint32_t { + const uint32_t byteIndex = bitIndex >> 3; + const uint8_t bitMask = 0x80u >> (bitIndex & 7u); + uint32_t zm = 0; + for (uint8_t ch = 0; ch < nCh; ch++) { + if (byteIndex >= chanTotalBytes[ch] || !(_BBs->pixelData[ch][byteIndex] & bitMask)) { + zm |= chanPinMask[ch]; + } + } + return zm; + }; +#endif + + const uint32_t bitsPerPixel = (uint32_t)pixelBytes * 8u; // TODO: for custom bus with channels not observed on WS2812 but may be others + uint32_t idleStart = getCycleCount(); // used to track idle periods and abort if too long (LEDs may have latched) + WPB_BB_ENTERCRITICAL(); + for (uint16_t pixel = 0; pixel < maxPixels; pixel++) { + uint32_t bitStart = (uint32_t)pixel * bitsPerPixel; + uint32_t bitEnd = bitStart + bitsPerPixel; + uint32_t cyclesStart = getCycleCount() - period; // Period reference: initialise as already expired so the first pulse fires immediately + + for (uint32_t bitIndex = bitStart; bitIndex < bitEnd; bitIndex++) { + + // Compute zero mask(s) for this bit + #ifdef ESP_HAS_HIGH_GPIO_BANK + uint32_t zeroMaskLow = 0, zeroMaskHigh = 0; + computeZeroMasks(bitIndex, zeroMaskLow, zeroMaskHigh); + #else + uint32_t zeroMask = computeZeroMask(bitIndex); + #endif + + while ((getCycleCount() - cyclesStart) < period); + + // Set all outputs HIGH simultaneously + #ifdef ESP_HAS_HIGH_GPIO_BANK + REG_WRITE(GPIO_OUT_W1TS_REG, setOutputMaskLow); + REG_WRITE(GPIO_OUT1_W1TS_REG, setOutputMaskHigh); + #else + REG_WRITE(GPIO_OUT_W1TS_REG, setOutputMask); + #endif + cyclesStart = getCycleCount(); + + // After T0H — pull '0' outputs LOW + while ((getCycleCount() - cyclesStart) < t0h); + #ifdef ESP_HAS_HIGH_GPIO_BANK + REG_WRITE(GPIO_OUT_W1TC_REG, zeroMaskLow); + REG_WRITE(GPIO_OUT1_W1TC_REG, zeroMaskHigh); + #else + REG_WRITE(GPIO_OUT_W1TC_REG, zeroMask); + #endif + + // After T1H — pull all remaining outputs LOW + while ((getCycleCount() - cyclesStart) < t1h); + #ifdef ESP_HAS_HIGH_GPIO_BANK + REG_WRITE(GPIO_OUT_W1TC_REG, setOutputMaskLow); + REG_WRITE(GPIO_OUT1_W1TC_REG, setOutputMaskHigh); + #else + REG_WRITE(GPIO_OUT_W1TC_REG, setOutputMask); + #endif + } + + // capture time just before exiting critical so we can measure the gap + if (allowInterrupts) { + idleStart = getCycleCount(); + // allow ISRs to run between LEDs + WPB_BB_EXITCRITICAL(); + WPB_BB_ENTERCRITICAL(); + //check the idle gap: if it took longer than half of latchCycles abort to avoid overwriting from the start and causing flicker + if ((getCycleCount() - idleStart) > latchCycles) { + WPB_BB_EXITCRITICAL(); + return false; // abort: strip latched, do not send remaining pixels + } + } + } + WPB_BB_EXITCRITICAL(); // exit after we are done sending everything + return true; +} +} // namespace WLEDpixelBus \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h new file mode 100644 index 0000000000..b23ebfe7a0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -0,0 +1,95 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#if defined(ARDUINO_ARCH_ESP32) +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +//#include "soc/gpio_struct.h" // GPIO.out_w1ts / GPIO.out_w1tc -> better use REG_WRITE +#include "soc/gpio_reg.h" +#include "driver/gpio.h" +#elif defined(ESP8266) +#include +#include +#include +#include +#endif + +namespace WLEDpixelBus { + +// Note: maximum number of parallel BitBang channels WLED_MAX_BB_CHANNELS is defined in const.h + +class BitBangBus : public PixelBus { +public: + /** + * Construct a BitBang bus for one GPIO pin. + * @param pin GPIO pin number (must be set in SOC_GPIO_VALID_OUTPUT_GPIO_MASK for this target) + * @param timing LED protocol timing (from WLEDpixelBus_Timings) + * @param colorOrder WLED colour-order byte + * @param numChannels Number of colour channels per LED (3 or 4) + * @param ledType LED chip type constant (TYPE_*) + */ + BitBangBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, + uint8_t numChannels, uint8_t ledType = 0); + ~BitBangBus() override; + + bool begin() override; + void end() override; + + bool show() override; // Stage this channel's data. When all channels are staged, output all in parallel. + bool canShow() const override { return true; } // BitBang output is synchronous — always ready TODO: on multi-core systems with tasks running on different cores, this is not true (but currently there is no seperate task) + +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "BitBang"; } +#endif + + void setInverted(bool inv) override; // invert output signal + + // Reset the shared static channel registryc, alled by PixelBusAllocator::resetChannelTracking() when all buses are destroyed + static void resetChannels(); + +private: + int8_t _pin = -1; + LedTiming _timing; // saved at construction, converted to cycles in begin() + bool _inverted = false; // invert output signal + bool _initialized = false; + + // ----------------------------------------------------------------------- + // Shared state (one context for ALL BitBangBus instances) + // All timing fields must use the same LED type (enforced by PixelBusAllocator). + // ----------------------------------------------------------------------- + struct BBstate { + uint8_t* pixelData[WLED_MAX_BB_CHANNELS]; // encoded data pointer per channel + uint32_t t0h; // Timing in CPU cycles — identical across all channels + uint32_t t1h; + uint32_t period; + uint32_t latchCycles; + uint32_t allMask; // GPIO bitmask of registered pins 0–31 + #ifdef ESP_HAS_HIGH_GPIO_BANK + uint32_t allMaskHigh; // GPIO bitmask of registered pins 32+ (ESP32/S2/S3) + #endif + #if defined(ARDUINO_ARCH_ESP32) + portMUX_TYPE mux; // critical-section lock used inside outputParallel() + #endif + int8_t pins[WLED_MAX_BB_CHANNELS]; + uint16_t numPixels[WLED_MAX_BB_CHANNELS]; // pixel count per channel + uint8_t channelCount; + uint8_t stagedCount; // how many channels have called show() this frame + uint8_t pixelBytes; // bytes per encoded pixel (derived from LED type) + bool allowInterrupts; // if enabled, interrupts are allowed to run between LEDs, if not the output is fully blocking + }; + static BBstate* _BBs; + + // ----------------------------------------------------------------------- + // Core output routine — must run from IRAM for timing accuracy + // ----------------------------------------------------------------------- + static bool IRAM_ATTR outputParallel(); +}; + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp new file mode 100644 index 0000000000..d67c0fb8fc --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -0,0 +1,636 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - ESP8266 driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +Supports UART and I2S DMA output as well as parallel bit-banging + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_ESP8266.h" + +#if defined(ESP8266) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WLEDpixelBus { + +//============================================================================== +// ESP8266 UART Bus +//============================================================================== + +// Global static tracking for the shared UART ISR +Esp8266UartBus* Esp8266UartBus::s_instances[2] = {nullptr, nullptr}; + +Esp8266UartBus::Esp8266UartBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _inverted(false) + , _initialized(false) + , _asyncBuf(nullptr) + , _asyncBufEnd(nullptr) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +Esp8266UartBus::~Esp8266UartBus() { + end(); +} + +// Shared Interrupt Service Routine (must be in IRAM) +void IRAM_ATTR Esp8266UartBus::UartIsr(void* arg, void* exceptionFrame) { + for (uint8_t uartNum = 0; uartNum < 2; uartNum++) { + Esp8266UartBus* instance = s_instances[uartNum]; + + // Check if this UART triggered a TX FIFO Empty interrupt + if (instance && (USIS(uartNum) & (1 << UIFE))) { + // Logic for bit expansion (Replaces the LUT) + const uint8_t uartData[4] = {0b110111, 0b000111, 0b110100, 0b000100}; + + // Calculate remaining space in the 128-byte hardware FIFO + uint8_t avail = (128 - ((USS(uartNum) >> USTXC) & 0xff)) / 4; + + while (avail > 0 && instance->_asyncBuf < instance->_asyncBufEnd) { + uint8_t v = *instance->_asyncBuf++; + USF(uartNum) = uartData[(v >> 6) & 0x03]; + USF(uartNum) = uartData[(v >> 4) & 0x03]; + USF(uartNum) = uartData[(v >> 2) & 0x03]; + USF(uartNum) = uartData[v & 0x03]; + avail--; + } + + // If finished, disable interrupt for this UART + if (instance->_asyncBuf >= instance->_asyncBufEnd) { + USIE(uartNum) &= ~(1 << UIFE); + } + + // Clear all interrupt flags for this UART + USIC(uartNum) = 0xffff; + } + } +} + +bool Esp8266UartBus::begin() { + if (_initialized) return true; + if (_pin != 1 && _pin != 2) return false; + uint8_t uartNum = (_pin == 2) ? 1 : 0; + s_instances[uartNum] = this; + + // set LED timing and (re)init the serial + updateUartTiming(); + + ETS_UART_INTR_DISABLE(); + // Attach the shared ISR + ETS_UART_INTR_ATTACH(UartIsr, nullptr); + + // Set threshold: Interrupt when FIFO drops below 80 bytes + USC1(uartNum) = (80 << UCFET); + USIC(uartNum) = 0xffff; // Clear pending + USIE(uartNum) &= ~((1 << UIFF) | (1 << UIFE)); // Start with interrupts off + ETS_UART_INTR_ENABLE(); + + _initialized = true; + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + return true; +} + +void Esp8266UartBus::end() { + if (!_initialized) return; + uint8_t uartNum = (_pin == 2) ? 1 : 0; + + ETS_UART_INTR_DISABLE(); + USIE(uartNum) = 0; + s_instances[uartNum] = nullptr; + + // If no buses are left, detach ISR + if (s_instances[0] == nullptr && s_instances[1] == nullptr) { + ETS_UART_INTR_ATTACH(nullptr, nullptr); + } else { + ETS_UART_INTR_ENABLE(); + } + + if (_pin == 2) Serial1.end(); + else Serial.end(); + _initialized = false; +} + +void Esp8266UartBus::updateUartTiming() { + uint32_t periodNs = _timing.bitPeriod(); + if (periodNs < 200) periodNs = 1250; + uint32_t baud = 4000000000ULL / periodNs; + + uint8_t uartNum = (_pin == 2) ? 1 : 0; + if (uartNum == 1) { + Serial1.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); + } else { + Serial.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); + } + + const uint32_t fifoResetFlags = (1 << UCTXRST) | (1 << UCRXRST); + USC0(uartNum) |= fifoResetFlags; + USC0(uartNum) &= ~(fifoResetFlags); + USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI)); // clear invert bits + if (!_inverted) { + USC0(uartNum) |= (1 << UCTXI); // invert TX -> idle low + } +} + +bool Esp8266UartBus::show() { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + if (!canShow()) return false; // TODO: is this consistent accross all drivers? i.e. return instead of wait? -> no it should wait with a timout and fallback + // workaround for a bug: after bootup, something changes the pin mux AFTER the bus is initialized, breaking the UART output. it only starts working after a bus re-init. could not find out what causes it. + // since pinMode() is computationally cheap, it is an acceptable fix + pinMode(_pin, SPECIAL); + + uint8_t uartNum = (_pin == 2) ? 1 : 0; + _asyncBuf = _encodeBuffer; + _asyncBufEnd = _encodeBuffer + _encodeBufferSize; + + // note: no initial fill required, the ISR will fire and fill the FIFO + // Enable the "TX FIFO Empty" interrupt to trigger the ISR + USIE(uartNum) |= (1 << UIFE); + + return true; +} + +void Esp8266UartBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +void Esp8266UartBus::setInverted(bool inv) { + _inverted = inv; +} + +bool Esp8266UartBus::canShow() const { + if (!_initialized) return false; + // Ready if we have no more data to send + if (_asyncBuf < _asyncBufEnd) return false; + // also FIFO must have physically drained + uint8_t uartNum = (_pin == 2) ? 1 : 0; + if (((USS(uartNum) >> USTXC) & 0xff) > 0) return false; + return true; +} + + + + + +//============================================================================== +// ESP8266 DMA Bus (I2S + SLC linked-list DMA) +//============================================================================== +// +// Architecture overview: +// The SLC (streaming linked-list controller) drives I2S TX continuously. +// Two "state" descriptors loop on the shared idle buffer (all zeros → LOW). +// On show(), state[1].next is patched to the first pixel descriptor so the +// DMA seamlessly transitions: LOW idle → pixel data → reset zeros → LOW idle. +// An EOF ISR fires at the end of the last pixel descriptor and restores the +// idle loop without ever stopping I2S, so GPIO3 is ALWAYS driven and never +// floats HIGH. The "high pulse before first LED" problem is eliminated. +// +// Descriptor layout (during show): +// state[0] → state[1] → data[0] → ... → data[N-1,EOF] → reset[0..M-1] → state[0] +// Descriptor layout (idle): +// state[0] → state[1] → state[0] (infinite loop, outputs zeros) +// + +// ISR singleton +Esp8266DmaBus* Esp8266DmaBus::s_this = nullptr; + +Esp8266DmaBus::Esp8266DmaBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _inverted(false) + , _initialized(false) + , _sending(false) + , _dmaDesc(nullptr) + , _dmaDescCnt(0) + , _idleBuf(nullptr) + , _idleBufSize(0) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +Esp8266DmaBus::~Esp8266DmaBus() { + end(); +} + +// --------------------------------------------------------------------------- +// allocateEncodeBuffer +// Pixel buffer only — no lead-in, no appended reset. +// Idle/reset are handled by _idleBuf + descriptor chain. +// --------------------------------------------------------------------------- +bool Esp8266DmaBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) { + // 4-step I2S encoding: each source byte → 4 encoded bytes; pad in logical bytes before expansion + const size_t pixelBytes = padPixelBytesForSuffix((size_t)numPixels * numChannels, _ledType) * 4; + size_t needed = _prefixLen + pixelBytes + _suffixLen * 4; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; } + if (needed == 0) return true; + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { _encodeBufferSize = 0; return false; } + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + _prefixLen; + if (_suffixLen == sizeof(SM16825_SUFFIX) && _ledType == TYPE_SM16825) { + uint32_t* dst = (uint32_t*)(_pixelData + pixelBytes); + for (uint8_t i = 0; i < (uint8_t)sizeof(SM16825_SUFFIX); i++) { + uint32_t word = 0; + uint8_t v = SM16825_SUFFIX[i]; + for (int bit = 7; bit >= 0; bit--) { + word <<= 4; + if (_inverted) word |= (v & (1 << bit)) ? 0x1u : 0x7u; + else word |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + dst[i] = word; + } + } + return true; +} + +// --------------------------------------------------------------------------- +// buildDescriptorChain +// Builds the SLC descriptor linked list according to the layout above. +// Must be called (a) after allocateEncodeBuffer and (b) after _idleBuf +// has been set up. Descriptor memory must already be allocated. +// --------------------------------------------------------------------------- +void Esp8266DmaBus::buildDescriptorChain() { + // --- Layout constants --- + // reset duration: use timing.reset_us, minimum 300 µs for stubborn LEDs. + uint32_t bitPeriod = _timing.bitPeriod(); + if (bitPeriod == 0) bitPeriod = 1250; + uint32_t resetUs = (_timing.reset_us > 300) ? _timing.reset_us : 300; + // encoded bytes produced per LED bit period in 4-step cadence = 4 bits / 8 bits * 4 bytes = 0.5 bytes + // bytes per µs at bitPeriod ns/bit: bytesPerUs = 4 (i2s bytes/led-bit) / (bitPeriod/1000 µs/led-bit) + // = 4000.0 / bitPeriod bytes/µs + size_t resetBytes = (size_t)((4000.0f / (float)bitPeriod) * (float)resetUs + 0.5f); + // round up to 4-byte boundary + resetBytes = (resetBytes + 3) & ~3u; + if (resetBytes < 4) resetBytes = 4; + + // --- Count descriptors --- + size_t pixelBytes = _encodeBufferSize; + size_t dataBlocks = (pixelBytes > 0) ? ((pixelBytes + c_maxDmaBlockSize - 1) / c_maxDmaBlockSize) : 1; + size_t resetBlocks = (resetBytes + c_idleBufSize - 1) / c_idleBufSize; + _dmaDescCnt = (uint16_t)(c_stateBlockCount + dataBlocks + resetBlocks); + + // Free previous descriptor allocation + if (_dmaDesc) { free(_dmaDesc); _dmaDesc = nullptr; } + _dmaDesc = (SlcQueueItem*)malloc(_dmaDescCnt * sizeof(SlcQueueItem)); + if (!_dmaDesc) { _dmaDescCnt = 0; return; } + + uint16_t idx = 0; + + // --- State descriptors: loop on idle buf (4 bytes each to keep it tiny) --- + dmaItemInit(&_dmaDesc[0], _idleBuf, 4, &_dmaDesc[1]); + dmaItemInit(&_dmaDesc[1], _idleBuf, 4, &_dmaDesc[0]); // default: idle loop + + idx = c_stateBlockCount; + + // --- Pixel data descriptors --- + uint8_t* ptr = _encodeBuffer; + size_t left = pixelBytes; + size_t firstDataIdx = idx; + while (left > 0) { + size_t chunk = (left > c_maxDmaBlockSize) ? c_maxDmaBlockSize : left; + dmaItemInit(&_dmaDesc[idx], ptr, chunk, &_dmaDesc[idx + 1]); + ptr += chunk; + left -= chunk; + idx++; + } + // Mark the last data descriptor as EOF → ISR fires here + _dmaDesc[idx - 1].eof = 1; + + // --- Reset (idle) descriptors --- + size_t firstResetIdx = idx; + size_t resetLeft = resetBytes; + while (resetLeft > 0) { + size_t chunk = (resetLeft > c_idleBufSize) ? c_idleBufSize : resetLeft; + dmaItemInit(&_dmaDesc[idx], _idleBuf, (uint16_t)chunk, &_dmaDesc[idx + 1]); + resetLeft -= chunk; + idx++; + } + // Last reset descriptor loops back to state[0] + _dmaDesc[idx - 1].next_link_ptr = &_dmaDesc[0]; + (void)firstDataIdx; (void)firstResetIdx; // used only for clarity +} + +// --------------------------------------------------------------------------- +// slcIsr — fires on SLCIRXEOF (end of last pixel descriptor) +// Restore state[1] → state[0] idle loop. Mark as no longer sending. +// --------------------------------------------------------------------------- +void IRAM_ATTR Esp8266DmaBus::slcIsr() { + ETS_SLC_INTR_DISABLE(); + uint32_t status = SLCIS; + SLCIC = 0xFFFFFFFF; + if ((status & SLCIRXEOF) && s_this) { + // Re-close the idle loop so state[0]→state[1]→state[0] + s_this->_dmaDesc[1].next_link_ptr = &s_this->_dmaDesc[0]; + s_this->_sending = false; + } + ETS_SLC_INTR_ENABLE(); +} + +// --------------------------------------------------------------------------- +// startI2s — configure SLC + I2S registers and kick off continuous DMA +// --------------------------------------------------------------------------- + +// TODO: just like in uart, the output pin matrix is somehow overwritten, maybe by pinmanager? +void Esp8266DmaBus::startI2s(uint8_t bckDiv, uint8_t clkDiv) { + ETS_SLC_INTR_DISABLE(); // disable ISR while configuring (just in case) + // Reset SLC + SLCC0 |= SLCRXLR | SLCTXLR; + SLCC0 &= ~(SLCRXLR | SLCTXLR); + SLCIC = 0xFFFFFFFF; + + // Configure SLC in DMA mode 1 + SLCC0 &= ~(SLCMM << SLCM); + SLCC0 |= (1 << SLCM); + SLCRXDC |= SLCBINR | SLCBTNR; + SLCRXDC &= ~(SLCBRXFE | SLCBRXEM | SLCBRXFM); + + // TXLINK: needs a valid descriptor (we reuse the last one; TX not actually used) + SLCTXL &= ~(SLCTXLAM << SLCTXLA); + SLCTXL |= (uint32_t)(&_dmaDesc[_dmaDescCnt - 1]) << SLCTXLA; + + // RXLINK: start from state[0] (idle loop) + SLCRXL &= ~(SLCRXLAM << SLCRXLA); + SLCRXL |= (uint32_t)(&_dmaDesc[0]) << SLCRXLA; + + // Attach ISR + ETS_SLC_INTR_ATTACH(slcIsr, nullptr); + SLCIE = SLCIRXEOF; + ETS_SLC_INTR_ENABLE(); + + // Start SLC + SLCTXL |= SLCTXLS; + SLCRXL |= SLCRXLS; + + // Enable I2S clock + I2S_CLK_ENABLE(); + I2SIC = 0x3F; + I2SIE = 0; + + // Reset I2S + I2SC &= ~(I2SRST); + I2SC |= I2SRST; + I2SC &= ~(I2SRST); + + // Set RX/TX FIFO_MOD=0 (16-bit stereo) and re-enable DMA. + I2SFC &= ~(I2SDE | (I2STXFMM << I2STXFM) | (I2SRXFMM << I2SRXFM)); + I2SFC |= I2SDE; // re-enable DMA + // Set RX/TX CHAN_MOD=0 (stereo, normal). + I2SCC &= ~((I2STXCMM << I2STXCM) | (I2SRXCMM << I2SRXCM)); + + // I2S config: right-first, MSB-right, slave mode off + I2SC &= ~(I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD)); + I2SC |= I2SRF | I2SMR | I2SRSM | I2SRMS | ((uint32_t)bckDiv << I2SBD) | ((uint32_t)clkDiv << I2SCD); + + // Start I2S TX + I2SC |= I2STXS; +} + +// --------------------------------------------------------------------------- +// stopI2s — disable SLC + I2S +// --------------------------------------------------------------------------- +void Esp8266DmaBus::stopI2s() { + ETS_SLC_INTR_DISABLE(); + SLCIC = 0xFFFFFFFF; // clear pending interrupt flags + I2SC &= ~(I2STXS | I2SRXS); + I2SC &= ~I2SRST; + I2SC |= I2SRST; + I2SC &= ~I2SRST; +} + +// --------------------------------------------------------------------------- +// begin +// --------------------------------------------------------------------------- +bool Esp8266DmaBus::begin() { + if (_initialized) return true; + if (_pin != 3) return false; + + // Hold GPIO3 appropriately before any I2S activity (prevents startup glitch) + pinMode(3, OUTPUT); + digitalWrite(3, _inverted ? HIGH : LOW); + + // Compute I2S clock divisors for 4-step cadence + // I2S base clock = 160 MHz (even with 80 MHz CPU freq, tested) + // I2S clock is baseclk / bckDiv / clkDiv. We want to get as close as possible to 4 / bitPeriod + uint8_t best_clkdiv = 1; + uint8_t best_baseclkdiv = 1; + uint64_t best_error = UINT64_MAX; + // target I2S bit clock = 4 / bitPeriod_ns * 1e9 Hz + // divisor = I2SBASEFREQ / rate = I2SBASEFREQ * bitPeriod / 4e9 + uint32_t bitPeriod = _timing.bitPeriod(); // in nanoseconds, for example 1250=1.25us for WS2812 + uint64_t target = (uint64_t)I2SBASEFREQ * bitPeriod; + + for (uint8_t bck = 2; bck <= 64; bck += 2) { + uint64_t den = (uint64_t)4000000000ULL * bck; // denominator of clkdiv = I2SBASEFREQ * bitPeriod / (4e9 * baseclkdiv) + + uint64_t clk = (target + den / 2) / den; + if (clk < 1 || clk > 63) continue; + + uint64_t actual = clk * den; + uint64_t error = (actual > target) ? (actual - target) : (target - actual); + + if (error < best_error) { + best_error = error; + best_clkdiv = clk; + best_baseclkdiv = bck; + } + } + + uint8_t bckDiv = best_baseclkdiv; + uint8_t clkDiv = best_clkdiv; + + // Allocate idle/reset zero buffer + _idleBufSize = c_idleBufSize; + _idleBuf = (uint8_t*)malloc(_idleBufSize); + if (!_idleBuf) return false; + memset(_idleBuf, _inverted ? 0xFF : 0x00, _idleBufSize); + + // Allocate pixel encode buffer and build initial descriptor chain + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + buildDescriptorChain(); + if (!_dmaDesc) { end(); return false; } + + s_this = this; + _sending = false; + + // Switch GPIO3 MUX to I2S (transitions LOW→LOW because I2S starts in idle) + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_I2SO_DATA); + + startI2s(bckDiv, clkDiv); + + _initialized = true; + return true; +} + +// --------------------------------------------------------------------------- +// end +// --------------------------------------------------------------------------- +void Esp8266DmaBus::end() { + if (!_initialized && !_idleBuf && !_dmaDesc) return; + stopI2s(); + ETS_SLC_INTR_ATTACH(nullptr, nullptr); + s_this = nullptr; + if (_dmaDesc) { free(_dmaDesc); _dmaDesc = nullptr; _dmaDescCnt = 0; } + if (_idleBuf) { free(_idleBuf); _idleBuf = nullptr; _idleBufSize = 0; } + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } + pinMode(_pin, INPUT); + _initialized = false; + _sending = false; +} + +// --------------------------------------------------------------------------- +// setPixel / getPixelColor / scaleAll +// Pixel data starts at offset 0 in _encodeBuffer (no lead-in needed; +// idle state ensures GPIO3 is LOW before the first pixel bit arrives). +// --------------------------------------------------------------------------- +IRAM_ATTR bool Esp8266DmaBus::setPixelColor(uint16_t pos, uint32_t c, uint16_t wwcw) { + const uint8_t pixelBytes = _encoder.getPixelBytes(); + uint8_t src[12]; + const CctPixel cct{wwcw}; + switch (_encoder.getPixelFormat()) { + case 3: _encoder.encodeRGB(c, src); break; + case 4: _encoder.encodeRGBW(c, src); break; + case 5: _encoder.encodeCCT(c, cct, src); break; + case (3*2) | NCHF_16BIT: _encoder.encodeRGB16(c, src, _encBri); break; + case (4*2) | NCHF_16BIT: _encoder.encodeRGBW16(c, src, _encBri); break; + case (5*2) | NCHF_16BIT: _encoder.encodeCCT16(c, cct, src, _encBri); break; + default: _encoder.encodeGeneric(c, cct, src, _encBri); break; + } + uint32_t* dst = (uint32_t*)(_pixelData + (size_t)pos * pixelBytes * 4); + for (uint8_t b = 0; b < pixelBytes; b++) { + uint32_t word = 0; + uint8_t v = src[b]; + for (int bit = 7; bit >= 0; bit--) { + word <<= 4; + if (_inverted) word |= (v & (1 << bit)) ? 0x1u : 0x7u; + else word |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + dst[b] = word; + } + return true; +} + +IRAM_ATTR uint32_t Esp8266DmaBus::getPixelColor(uint16_t pix) const { + const uint8_t pixelBytes = _encoder.getPixelBytes(); + const uint32_t* src = (const uint32_t*)(_pixelData + (size_t)pix * pixelBytes * 4); + uint8_t decoded[12]; + for (uint8_t b = 0; b < pixelBytes; b++) { + uint32_t word = src[b]; + uint8_t v = 0; + for (int nib = 7; nib >= 0; nib--) { + v <<= 1; + if (_inverted) { + if (((word >> (nib * 4)) & 0xF) == 0x1) v |= 1; + } else { + if (((word >> (nib * 4)) & 0xF) == 0xE) v |= 1; + } + } + decoded[b] = v; + } + switch (_encoder.getPixelFormat()) { + case 3: return _encoder.decodeRGB(decoded); + case 4: return _encoder.decodeRGBW(decoded); + case 5: return _encoder.decodeCCT(decoded); + case (3*2) | NCHF_16BIT: return _encoder.decodeRGB16(decoded); + case (4*2) | NCHF_16BIT: return _encoder.decodeRGBW16(decoded); + case (5*2) | NCHF_16BIT: return _encoder.decodeCCT16(decoded); + default: return _encoder.decodeGeneric(decoded); + } +} + +// Since the colors are already 4-step encoded, we need to decode first, scale then re-encode. +void Esp8266DmaBus::scaleAll(uint8_t scale) { + if (!_pixelData || scale == 255) return; + uint8_t pixelBytes = _encoder.getPixelBytes(); + uint32_t* buf = (uint32_t*)_pixelData; + size_t numWords = (size_t)_numPixels * pixelBytes; + for (size_t w = 0; w < numWords; w++) { + uint32_t word = buf[w]; + uint8_t v = 0; + for (int nib = 7; nib >= 0; nib--) { + v <<= 1; + if (_inverted) { + if (((word >> (nib * 4)) & 0xF) == 0x1) v |= 1; + } else { + if (((word >> (nib * 4)) & 0xF) == 0xE) v |= 1; + } + } + v = ((uint16_t)(v + 1) * scale) >> 8; + uint32_t newWord = 0; + for (int bit = 7; bit >= 0; bit--) { + newWord <<= 4; + if (_inverted) newWord |= (v & (1 << bit)) ? 0x1u : 0x7u; + else newWord |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + buf[w] = newWord; + } +} + +void Esp8266DmaBus::updateSuffix(const uint8_t* data, uint8_t len) { + if (!_pixelData || _suffixLen == 0 || len == 0) return; + if (len > _suffixLen) len = _suffixLen; + const size_t pixelWords = (size_t)_numPixels * _encoder.getPixelBytes(); + uint32_t* dst = (uint32_t*)(_pixelData + pixelWords * 4); + for (uint8_t i = 0; i < len; i++) { + uint32_t word = 0; + uint8_t v = data[i]; + for (int bit = 7; bit >= 0; bit--) { + word <<= 4; + if (_inverted) word |= (v & (1 << bit)) ? 0x1u : 0x7u; + else word |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + dst[i] = word; + } +} + +void Esp8266DmaBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +void Esp8266DmaBus::setInverted(bool inv) { + _inverted = inv; +} + +// --------------------------------------------------------------------------- +// show +// Patches state[1] to break out of the idle loop into the pixel data, +// then returns immediately. The ISR restores the idle loop when done. +// --------------------------------------------------------------------------- +bool Esp8266DmaBus::show() { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + if (_sending) return false; // previous frame still running + + // Break the idle loop: state[1] now points to first pixel descriptor + _dmaDesc[1].next_link_ptr = &_dmaDesc[c_stateBlockCount]; + _sending = true; + + return true; +} + +// --------------------------------------------------------------------------- +// canShow — ready when the previous frame's ISR has restored idle loop +// --------------------------------------------------------------------------- +bool Esp8266DmaBus::canShow() const { + return _initialized && !_sending; +} + +} // namespace WLEDpixelBus + +#endif // ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h new file mode 100644 index 0000000000..ff1f2d55bc --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -0,0 +1,125 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - ESP8266 driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ +#pragma once + +#include "WLEDpixelBus.h" + +#if defined(ESP8266) + +namespace WLEDpixelBus { + +//============================================================================== +// ESP8266 UART Bus (Asynchronous via UART1/UART0) +//============================================================================== + +class Esp8266UartBus : public PixelBus { +public: + Esp8266UartBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0); + ~Esp8266UartBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "ESP8266_UART"; } +#endif + void setColorOrder(uint8_t co); + void setInverted(bool inv) override; + + static void UartIsr(void* arg, void* exceptionFrame); + static Esp8266UartBus* s_instances[2]; + +private: + int8_t _pin; + LedTiming _timing; + bool _inverted; // invert output signal + bool _initialized; + volatile uint8_t* _asyncBuf = nullptr; + volatile uint8_t* _asyncBufEnd = nullptr; + + void updateUartTiming(); +}; + +//============================================================================== +// ESP8266 DMA Bus (Via I2S + SLC linked-list DMA) +//============================================================================== + +// SLC DMA descriptor (matches SDK slc_queue_item layout) +struct SlcQueueItem { + uint32_t blocksize : 12; + uint32_t datalen : 12; + uint32_t unused : 5; + uint32_t sub_sof : 1; + uint32_t eof : 1; + uint32_t owner : 1; + uint8_t* buf_ptr; + struct SlcQueueItem* next_link_ptr; +}; + +class Esp8266DmaBus : public PixelBus { +public: + Esp8266DmaBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0); + ~Esp8266DmaBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "ESP8266_DMA"; } +#endif + + void setColorOrder(uint8_t co); + void setInverted(bool inv) override; + IRAM_ATTR bool setPixelColor(uint16_t pos, uint32_t c, uint16_t wwcw) override; + IRAM_ATTR uint32_t getPixelColor(uint16_t pix) const override; + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; + void updateSuffix(const uint8_t* data, uint8_t len) override; + void scaleAll(uint8_t scale) override; + + static Esp8266DmaBus* s_this; + +private: + static const uint16_t c_maxDmaBlockSize = 4095; + static const uint8_t c_stateBlockCount = 2; + static const uint16_t c_idleBufSize = 256; // size of idle/reset zero buffer + + int8_t _pin; // Only GPIO3 supported for I2S DMA on ESP8266 + LedTiming _timing; + bool _inverted; // invert output signal + bool _initialized; + volatile bool _sending; + + // SLC DMA linked-list + SlcQueueItem* _dmaDesc; // allocated array of all descriptors + uint16_t _dmaDescCnt; // total count of descriptors + uint8_t* _idleBuf; // zero-filled buffer shared by state + reset descriptors + size_t _idleBufSize; + + void buildDescriptorChain(); + void startI2s(uint8_t bckDiv, uint8_t clkDiv); + void stopI2s(); + static void IRAM_ATTR slcIsr(); + + // DmaItemInit helper + static void dmaItemInit(SlcQueueItem* item, uint8_t* data, size_t sz, SlcQueueItem* next) { + item->owner = 1; item->eof = 0; item->sub_sof = 0; item->unused = 0; + item->datalen = sz; item->blocksize = sz; + item->buf_ptr = data; item->next_link_ptr = next; + } +}; + + + +} // namespace WLEDpixelBus + +#endif // ESP8266 + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h new file mode 100644 index 0000000000..c8cc633af3 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - special features + +written by Damian Schneider @dedehai 2026 + +prefix data (TM1914), suffix data (SM16825) and brigthness to LED hardware current mapping (TM1814, TM1815 and APA102) + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "../../const.h" + +// SM16825E 32-bit per-frame suffix (appended once after all pixel data): +// bits 31..7 (25 bits): current gain OUT R,G,B,W,Y — 5 bits each, 0x1F = 310mA, 0x00 = 10.2mA, step ~10.1mA +// bits 6..5 ( 2 bits): standby enable — 0b00 = normal op (0b10 = standby) +// bits 4..0 ( 5 bits): reserved — all 1 recommended +static constexpr uint8_t SM16825_SUFFIX[4] = { 0xFF, 0xFF, 0xFF, 0x9F }; // set max current (not really safe but always was the default) +//static constexpr uint8_t SM16825_SUFFIX[4] = { 0x08, 0x42, 0x10, 0x9F }; // 20.3mA for all channels as a safe default - TODO: make this configurable or use a safe default? also add standby mode support if off? + +// mapBrightnessToCurrentStep() is used by BusDigital for current-based dimming of chips +// with discrete current levels (e.g. TM1814/TM1815). + +#include + +// TM1914 mode-setting prefix: 6 bytes (3 data + 3 inverted). +// DIN/FDIN auto-switch mode (0xFF). Written once after begin(); never changes at runtime. +// Other modes: DIN-only 0xFA, FDIN-only 0xF5. +//static constexpr uint8_t TM1914_PREFIX[6] = { 0xFF, 0xFF, 0xFA, 0x00, 0x00, 0x05 }; DIN only +//static constexpr uint8_t TM1914_PREFIX[6] = { 0xFF, 0xFF, 0xF5, 0x00, 0x00, 0x0A }; FDIN only +static constexpr uint8_t TM1914_PREFIX[6] = { 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00 }; + + +namespace WLEDpixelBus { +/** + * Map a WLED brightness value (0..255) to a hardware current step and a residual + * color scale, maximising effective resolution for chips with discrete current levels. + * + * The strategy: pick the smallest current step whose brightness is >= the target, + * then scale pixel colors down to close the gap. Current handles coarse dimming; + * color scale provides sub-step interpolation without wasting hardware range. + * + * All arithmetic is integer (Q16.8 fixed-point) + * + * @param brightness Target brightness 0..255. + * @param numSteps Number of discrete current levels the chip supports (e.g. 64). + * @param minBri Brightness equivalent of the minimum current step (e.g. 44 for + * TM1814: floor(6.5/38 * 255)). Below this floor, step 0 is used + * and color scaling brings brightness down further. + * @param stepOut Output: current step to program into the chip (0..numSteps-1). + * @param scaleOut Output: color scale to apply to pixel data (0..255; 255 = no change). + */ +inline void mapBrightnessToCurrentStep(uint8_t brightness, uint8_t numSteps, uint8_t minBri, uint8_t& stepOut, uint8_t& scaleOut) { + if (brightness == 0 || numSteps == 0) { + stepOut = 0; scaleOut = 0; + return; + } + + const uint8_t maxStep = numSteps - 1; + // Q16.8 step size: (255 - minBri) / maxStep + const uint32_t range = 255 - minBri; + const uint32_t stepFP = (range << 8) / maxStep; // Q16.8 + + if (brightness <= minBri) { + // Below minimum current floor: use step 0, scale colors down proportionally. + stepOut = 0; + scaleOut = ((uint16_t)brightness * 255) / minBri; + return; + } + + // Compute ceiling step: smallest step whose brightness >= target. + const uint32_t diffFP = (uint32_t)(brightness - minBri) << 8; // Q16.8 + uint32_t step = (diffFP + stepFP - 1) / stepFP; // ceiling division + if (step > maxStep) step = maxStep; + + // Actual brightness at this step (integer floor, guaranteed >= brightness). + const uint32_t curBri = minBri + (step * range) / maxStep; + + stepOut = (uint8_t)step; + scaleOut = (uint8_t)(((uint16_t)brightness * 255) / curBri); // always <= 255 +} + +// Pad pixelBytes up to a whole number of native LED-IC channels if the bus has a suffix to make sure it alignes +inline size_t padPixelBytesForSuffix(size_t pixelBytes, uint8_t ledType) { + uint8_t nativeBytes = 0; + switch (ledType) { + case TYPE_SM16825: nativeBytes = 10; break; // 5 channels (R,G,B,WW,CW) x 2 bytes (16-bit) + default: break; + } + if (nativeBytes == 0) return pixelBytes; + const size_t rem = pixelBytes % nativeBytes; + return rem ? pixelBytes + (nativeBytes - rem) : pixelBytes; +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp new file mode 100644 index 0000000000..f7c7c6d362 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -0,0 +1,991 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel I2S/LCD output driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +supports ESP32, ESP32 S2 and ESP32 S3 (via LCD peripheral) +Default is 8 parallel outputs and double DMA buffering but it also supports 16 parallel outputs if needed +For 16 parallel output, triple buffering is required for glitch-free output. +Data is output in 4-step cadence meaning each LED bit is encoded into 4 I2S bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +3k per DMA buffer works well, enough for 32 RGB LEDs in 8x parallel output or roughly 0.9ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_I2S.h" + +#ifdef WLEDPB_I2S_SUPPORT +namespace WLEDpixelBus { + +I2sBusContext* I2sBusContext::_instances[WLEDPB_I2S_BUS_COUNT] = {nullptr}; +uint8_t I2sBusContext::_refCount[WLEDPB_I2S_BUS_COUNT] = {0}; + +I2sBusContext* I2sBusContext::get(uint8_t busNum) { + if (busNum >= WLEDPB_I2S_BUS_COUNT) return nullptr; + + if (_instances[busNum] == nullptr) { + _instances[busNum] = new I2sBusContext(busNum); + } + if (_instances[busNum] != nullptr) + _refCount[busNum]++; + return _instances[busNum]; +} + +void I2sBusContext::release(uint8_t busNum) { + if (busNum >= WLEDPB_I2S_BUS_COUNT) return; + if (_refCount[busNum] == 0) return; + + _refCount[busNum]--; + if (_refCount[busNum] == 0 && _instances[busNum]) { + delete _instances[busNum]; + _instances[busNum] = nullptr; + } +} + +#ifdef CONFIG_IDF_TARGET_ESP32S3 +I2sBusContext::I2sBusContext(uint8_t /*busNum*/) + : _dmaChannel(nullptr) + , _dmaChanId(-1) +#else +I2sBusContext::I2sBusContext(uint8_t busNum) + : _busNum(busNum) + , _i2sDev( +#if defined(CONFIG_IDF_TARGET_ESP32) + (busNum == 0) ? &I2S0 : &I2S1 +#else + &I2S0 +#endif + ) + , _isrHandle(nullptr) +#endif + , _state(DriverState::Idle) + , _initialized(false) + , _maxSrcBytes(0) + , _bufferSize(0) + , _dmaAllocated(false) + , _lastFilled(0) + , _resetBytesLeft(0) + , _txStartMillis(0) + , _timing{0, 0, 0, 0, 0} + , _clockDiv(1) + , _channelCount(0) + , _channelMask(0) + , _stagedMask(0) + , _maxDataLen(0) +{ + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + _dmaDesc[i] = nullptr; + _dmaBuffer[i] = nullptr; + } +} + +I2sBusContext::~I2sBusContext() { + deinit(); +} + +bool I2sBusContext::init(const LedTiming& timing) { + if (_initialized) return true; + + _timing = timing; + if (!hwInit(timing)) { + deinit(); + return false; + } + + _initialized = true; + return true; +} + +void I2sBusContext::deinit() { + int timeout = 100; + while (!isIdle() && timeout--) { vTaskDelay(1); } + + hwStopTransfer(); + + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; + } + } + + hwDeinit(); + + _dmaAllocated = false; + _initialized = false; +} + +//============================================================================== +// Hardware abstraction +//============================================================================== + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + +bool I2sBusContext::hwInit(const LedTiming& timing) { + uint32_t bitPeriodNs = timing.bitPeriod(); + + // Enable LCD_CAM peripheral + periph_module_enable(PERIPH_LCD_CAM_MODULE); + periph_module_reset(PERIPH_LCD_CAM_MODULE); + + // Reset LCD + LCD_CAM.lcd_user.lcd_reset = 1; + esp_rom_delay_us(100); + LCD_CAM.lcd_user.lcd_reset = 0; + esp_rom_delay_us(100); + + // Calculate clock divider for 4-step cadence + double clkm_div = (double)bitPeriodNs / 4.0 / 1000.0 * 240.0; + if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { + return false; + } + + uint8_t clkm_div_int = (uint8_t)clkm_div; + double clkm_frac = clkm_div - clkm_div_int; + uint8_t divB = 0; + uint8_t divA = 0; + if (clkm_frac > 0.001) { + divA = 63; + divB = (uint8_t)(clkm_frac * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + // Configure LCD clock + LCD_CAM.lcd_clock.clk_en = 1; + LCD_CAM.lcd_clock.lcd_clk_sel = 2; + LCD_CAM.lcd_clock.lcd_clkm_div_a = divA; + LCD_CAM.lcd_clock.lcd_clkm_div_b = divB; + LCD_CAM.lcd_clock.lcd_clkm_div_num = clkm_div_int; + LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; + LCD_CAM.lcd_clock.lcd_ck_idle_edge = 0; + LCD_CAM.lcd_clock.lcd_clk_equ_sysclk = 1; + + // Configure frame format + LCD_CAM.lcd_ctrl.lcd_rgb_mode_en = 0; + LCD_CAM.lcd_rgb_yuv.lcd_conv_bypass = 0; + LCD_CAM.lcd_misc.lcd_next_frame_en = 0; + LCD_CAM.lcd_data_dout_mode.val = 0; + LCD_CAM.lcd_user.lcd_always_out_en = 1; + LCD_CAM.lcd_user.lcd_8bits_order = 0; + LCD_CAM.lcd_user.lcd_bit_order = 0; +#ifdef WLED_PIXELBUS_16PARALLEL + LCD_CAM.lcd_user.lcd_2byte_en = 1; +#else + LCD_CAM.lcd_user.lcd_2byte_en = 0; +#endif + LCD_CAM.lcd_user.lcd_dummy = 1; + LCD_CAM.lcd_user.lcd_dummy_cyclelen = 0; + LCD_CAM.lcd_user.lcd_cmd = 0; + + // Allocate GDMA channel + gdma_channel_alloc_config_t dma_chan_config = { + .sibling_chan = NULL, + .direction = GDMA_CHANNEL_DIRECTION_TX, + .flags = {.reserve_sibling = 0} + }; + + esp_err_t err = gdma_new_channel(&dma_chan_config, &_dmaChannel); + if (err != ESP_OK) return false; + int chanId = -1; + if (gdma_get_channel_id(_dmaChannel, &chanId) != ESP_OK) return false; + _dmaChanId = (int8_t)chanId; + + err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + if (err != ESP_OK) return false; + + gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(_dmaChannel, &strategy_config); + + // Register DMA callback + gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; + gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); + + return true; +} + +void I2sBusContext::hwDeinit() { + if (_dmaChannel) { + gdma_disconnect(_dmaChannel); + gdma_del_channel(_dmaChannel); + _dmaChannel = nullptr; + } + periph_module_disable(PERIPH_LCD_CAM_MODULE); +} + +void I2sBusContext::hwStartTransfer() { + gdma_reset(_dmaChannel); + LCD_CAM.lcd_user.lcd_dout = 1; + LCD_CAM.lcd_user.lcd_update = 1; + LCD_CAM.lcd_misc.lcd_afifo_reset = 1; + LCD_CAM.lcd_misc.lcd_afifo_reset = 0; + gdma_start(_dmaChannel, (intptr_t)_dmaDesc[0]); + esp_rom_delay_us(1); + LCD_CAM.lcd_user.lcd_start = 1; +} + +void IRAM_ATTR I2sBusContext::hwStopTransfer() { + LCD_CAM.lcd_user.lcd_start = 0; + // Register-level GDMA stop: gdma_stop() lives in flash and must not be called from ISR + if (_dmaChanId >= 0) GDMA.channel[_dmaChanId].out.link.stop = 1; + _state = DriverState::Idle; +} + +void I2sBusContext::hwRoutePin(int8_t pin, int8_t idx, bool inverted) { + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, inverted, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); +} + +#else // !CONFIG_IDF_TARGET_ESP32S3 + +bool I2sBusContext::hwInit(const LedTiming& timing) { + // Enable I2S peripheral +#if defined(CONFIG_IDF_TARGET_ESP32) + periph_module_enable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); +#else + periph_module_enable(PERIPH_I2S0_MODULE); +#endif + + // Stop any existing transmission + _i2sDev->out_link.stop = 1; + _i2sDev->conf.tx_start = 0; + _i2sDev->int_ena.val = 0; + _i2sDev->int_clr.val = 0xFFFFFFFF; + _i2sDev->fifo_conf.dscr_en = 0; + + // Reset I2S + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.rx_reset = 1; + _i2sDev->conf.rx_reset = 0; + + // Reset DMA + _i2sDev->lc_conf.in_rst = 1; + _i2sDev->lc_conf.in_rst = 0; + _i2sDev->lc_conf.out_rst = 1; + _i2sDev->lc_conf.out_rst = 0; + _i2sDev->lc_conf.ahbm_rst = 1; + _i2sDev->lc_conf.ahbm_rst = 0; + _i2sDev->lc_conf.ahbm_fifo_rst = 1; + _i2sDev->lc_conf.ahbm_fifo_rst = 0; + + // Reset FIFO + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_fifo_reset = 0; + _i2sDev->conf.rx_fifo_reset = 1; + _i2sDev->conf.rx_fifo_reset = 0; + + // Configure for parallel LCD mode + _i2sDev->conf2.val = 0; + _i2sDev->conf2.lcd_en = 1; +#ifdef WLED_PIXELBUS_16PARALLEL + _i2sDev->conf2.lcd_tx_wrx2_en = 0; // 16-bit mode: disable 8-bit double-write swap path +#else + _i2sDev->conf2.lcd_tx_wrx2_en = 1; // 8-bit mode: required for 8-bit parallel output +#endif + _i2sDev->conf2.lcd_tx_sdx2_en = 0; + + // DMA config + _i2sDev->lc_conf.val = 0; + _i2sDev->lc_conf.out_eof_mode = 1; + + // Disable PDM +#if defined(CONFIG_IDF_TARGET_ESP32) + _i2sDev->pdm_conf.tx_pdm_en = 0; + _i2sDev->pdm_conf.pcm2pdm_conv_en = 0; +#endif + + // FIFO configuration + _i2sDev->fifo_conf.val = 0; + _i2sDev->fifo_conf.tx_fifo_mod_force_en = 1; + //_i2sDev->fifo_conf.tx_fifo_mod = 3; // 0=16bit dual, 1=16bit single, 2=32bit dual, 3=32bit single (32-bit linked for 16-bit samples) + // For ESP32 Classic, use 16-bit FIFO mode +#if !defined(CONFIG_IDF_TARGET_ESP32S2) + _i2sDev->fifo_conf.tx_fifo_mod = 1; +#else + _i2sDev->fifo_conf.tx_fifo_mod = 3; +#endif + _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold + + // PCM bypass + _i2sDev->conf1.val = 0; + _i2sDev->conf1.tx_stop_en = 0; + _i2sDev->conf1.tx_pcm_bypass = 1; + + // Channel config + _i2sDev->conf_chan.val = 0; + _i2sDev->conf_chan.tx_chan_mod = 1; // 0=stereo, 1=right-left, 2=left-right, 3=right only, 4=left only + + // I2S conf + _i2sDev->conf.val = 0; + _i2sDev->conf.tx_msb_shift = 0; // No shift in parallel mode + _i2sDev->conf.tx_right_first = 1; + #if defined(CONFIG_IDF_TARGET_ESP32S2) + _i2sDev->conf.tx_dma_equal = 1; // seems required for S2 + #endif + + // Clear timing register + _i2sDev->timing.val = 0; + + // Calculate clock divider for 4-step cadence + // bck_div_num must be >= 2 on ESP32 hardware + // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns + // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) + const uint8_t bckDiv = 4; // must be >= 2 + uint32_t bitPeriodNs = timing.bitPeriod(); + +#if defined(CONFIG_IDF_TARGET_ESP32) + #ifndef WLED_PIXELBUS_16PARALLEL + // 8-bit mode: lcd_tx_wrx2_en=1 halves the effective output rate (WR pulses at BCK/2). + // Use 2x clock constant so the divider is doubled, yielding the correct BCK after the factor-of-2. + const double baseClockMhz = 160.0; + #else + const double baseClockMhz = 80.0; // 16-bit mode: APB clock, lcd_tx_wrx2_en=0 has no rate halving + #endif +#else + const double baseClockMhz = 80.0; // S2: 80MHz I2S base clock (wrx2 on S2 does not halve the rate) +#endif + + // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 + double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; + if (clkmdiv < 2.0) clkmdiv = 2.0; + if (clkmdiv > 255.0) clkmdiv = 255.0; + + uint8_t clkmInteger = (uint8_t)clkmdiv; + double clkmFraction = clkmdiv - clkmInteger; + + // Convert fraction to divB/divA (fraction = divB/divA) + uint8_t divB = 0; + uint8_t divA = 0; + if (clkmFraction > 0.001) { + divA = 63; // use max denominator for best precision + divB = (uint8_t)(clkmFraction * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + _clockDiv = clkmInteger; + + // Set clock (with fractional divider for accurate timing) + _i2sDev->clkm_conf.val = 0; + + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + _i2sDev->clkm_conf.clk_sel = 2; // APPL = 1 APB = 2 + _i2sDev->clkm_conf.clk_en = 1; // examples of i2s show this being set if sel is set to 2 + #else + _i2sDev->clkm_conf.clk_en = 1; + _i2sDev->clkm_conf.clka_en = 0; + #endif + + _i2sDev->clkm_conf.clkm_div_a = divA; + _i2sDev->clkm_conf.clkm_div_b = divB; + _i2sDev->clkm_conf.clkm_div_num = clkmInteger; + + _i2sDev->sample_rate_conf.val = 0; + _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; +#ifdef WLED_PIXELBUS_16PARALLEL + _i2sDev->sample_rate_conf.tx_bits_mod = 16; // 16-bit samples for up to 16 parallel channels +#else + _i2sDev->sample_rate_conf.tx_bits_mod = 8; // 8-bit samples for up to 8 parallel channels +#endif + + // Final reset before ISR install + _i2sDev->lc_conf.in_rst = 1; + _i2sDev->lc_conf.out_rst = 1; + _i2sDev->lc_conf.ahbm_rst = 1; + _i2sDev->lc_conf.ahbm_fifo_rst = 1; + _i2sDev->lc_conf.in_rst = 0; + _i2sDev->lc_conf.out_rst = 0; + _i2sDev->lc_conf.ahbm_rst = 0; + _i2sDev->lc_conf.ahbm_fifo_rst = 0; + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.tx_fifo_reset = 0; + + // Install ISR + int intSource; + #if defined(CONFIG_IDF_TARGET_ESP32) + intSource = (_busNum == 0) ? ETS_I2S0_INTR_SOURCE : ETS_I2S1_INTR_SOURCE; + #else + intSource = ETS_I2S0_INTR_SOURCE; + #endif + + esp_err_t err = esp_intr_alloc(intSource, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3, dmaISR, this, &_isrHandle); // note: level 3 is the maximum supported without resorting to assembly + if (err != ESP_OK) { + deinit(); + return false; + } + + return true; +} + +void I2sBusContext::hwDeinit() { + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } +#if defined(CONFIG_IDF_TARGET_ESP32) + periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); +#else + periph_module_disable(PERIPH_I2S0_MODULE); +#endif +} + +void I2sBusContext::hwStartTransfer() { + _i2sDev->lc_conf.in_rst = 1; + _i2sDev->lc_conf.out_rst = 1; + _i2sDev->lc_conf.ahbm_rst = 1; + _i2sDev->lc_conf.ahbm_fifo_rst = 1; + _i2sDev->lc_conf.in_rst = 0; + _i2sDev->lc_conf.out_rst = 0; + _i2sDev->lc_conf.ahbm_rst = 0; + _i2sDev->lc_conf.ahbm_fifo_rst = 0; + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.tx_fifo_reset = 0; + + _i2sDev->int_clr.val = 0xFFFFFFFF; + _i2sDev->int_ena.out_eof = 1; // single descriptor transfer finished + _i2sDev->int_ena.out_total_eof = 1; // full transfer finished + + _i2sDev->fifo_conf.dscr_en = 1; + _i2sDev->out_link.start = 0; + _i2sDev->out_link.addr = (uint32_t)_dmaDesc[0]; + _i2sDev->out_link.start = 1; + _i2sDev->conf.tx_start = 1; +} + +void IRAM_ATTR I2sBusContext::hwStopTransfer() { + if (_i2sDev) { + _i2sDev->int_ena.out_eof = 0; + _i2sDev->int_ena.out_total_eof = 0; + _i2sDev->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } + _state = DriverState::Idle; +} + +void I2sBusContext::hwRoutePin(int8_t pin, int8_t idx, bool inverted) { + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); // Configure GPIO + int sigIdx; +#ifdef WLED_PIXELBUS_16PARALLEL + #if defined(CONFIG_IDF_TARGET_ESP32) + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT8_IDX : I2S1O_DATA_OUT8_IDX; + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + sigIdx = I2S0O_DATA_OUT8_IDX; // 16-bit mode: mapping starts at DATA_OUT8_IDX for the wide 16-bit window + #else + sigIdx = I2S0O_DATA_OUT0_IDX; + #endif +#else + #if defined(CONFIG_IDF_TARGET_ESP32) + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes on S2 + #else + sigIdx = I2S0O_DATA_OUT0_IDX; + #endif +#endif + sigIdx += idx; + esp_rom_gpio_connect_out_signal(pin, sigIdx, inverted, false); +} + +#endif // !CONFIG_IDF_TARGET_ESP32S3 + +//============================================================================== +// Buffer management & encoding +//============================================================================== + +bool I2sBusContext::_allocDmaBuffers() { + if (_dmaBuffer[0] != nullptr) return true; + + _bufferSize = (WLEDPB_I2S_DMABYTES * _maxSrcBytes) / WLEDPB_I2S_DMA_BUFFER_COUNT; + _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes + if (_bufferSize > DEFAULT_DMA_BUFFER_SIZE) _bufferSize = DEFAULT_DMA_BUFFER_SIZE; + if (_bufferSize < MIN_DMA_BUFFER_SIZE) _bufferSize = MIN_DMA_BUFFER_SIZE; + + // allocate DMA-capable buffers (4-byte aligned for hardware DMA engine) + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) return false; + memset(_dmaBuffer[i], 0, _bufferSize); + + _dmaDesc[i] = (DmaDesc_t*)heap_caps_aligned_alloc(4, sizeof(DmaDesc_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) return false; + } + + // set up DMA descriptors as a circular linked list + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + descSetSizeAndLen(_dmaDesc[i], _bufferSize); + descSetBuf(_dmaDesc[i], _dmaBuffer[i]); + descSetEof(_dmaDesc[i]); + descSetOwnerDma(_dmaDesc[i]); + descSetNext(_dmaDesc[i], _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]); + } + + _dmaAllocated = true; + //DEBUG_PRINTF_P(PSTR("[I2S] DMA buffers allocated: bufSize=%u x%u\n"), _bufferSize, WLEDPB_I2S_DMA_BUFFER_COUNT); + return true; +} + +int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, size_t srcBytes, bool inverted) { + // Find free slot + int8_t idx = -1; + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + + // track the largest source byte count across channels; used in _allocDmaBuffers() to size DMA buffers + if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; + + hwRoutePin(pin, idx, inverted); + + return idx; +} + +void I2sBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + if (len > _maxDataLen) { + _maxDataLen = len; + } + + // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() + if (_stagedMask & (1 << channelIdx)) { + _stagedMask = 0; + } + _stagedMask |= (1 << channelIdx); +} + +// encode4Step: 4-step cadence, converts per-channel byte streams to parallel DMA words. + +#ifdef WLED_PIXELBUS_16PARALLEL +// 16-bit parallel encode: branchless gather + scatter, 64 bytes per source byte (16 channels) +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { + for (size_t pos = 0; pos + 64 <= destLen; pos += 64) { + // alwaysMask: channels with active data (HIGH step); bN: channels with bit N set + uint16_t alwaysMask = 0; + uint16_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; // named regs: compiler should keep in regs + uint16_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; + + for (int ch = 0; ch < maxChannel; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + const uint16_t m = (uint16_t)(1u << ch); + alwaysMask |= m; + const uint8_t b = _channels[ch].srcData[_channels[ch].srcPos++]; + // extract bits, unrolled for speed + b0 |= m & (uint16_t)(0u - ((b >> 7) & 1u)); + b1 |= m & (uint16_t)(0u - ((b >> 6) & 1u)); + b2 |= m & (uint16_t)(0u - ((b >> 5) & 1u)); + b3 |= m & (uint16_t)(0u - ((b >> 4) & 1u)); + b4 |= m & (uint16_t)(0u - ((b >> 3) & 1u)); + b5 |= m & (uint16_t)(0u - ((b >> 2) & 1u)); + b6 |= m & (uint16_t)(0u - ((b >> 1) & 1u)); + b7 |= m & (uint16_t)(0u - ((b >> 0) & 1u)); + } + + if (!alwaysMask) break; // no active channels produced data + + uint32_t* p = (uint32_t*)(dest + pos); +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + // S2/S3 layout: [step0, step1, step2, step3] (no half-word swap) + // step0=HIGH, step1=data, step2=data, step3=LOW (or 0b1000, 0b1110) + // 32-bit pair: p[0]=(bN<<16)|alwaysMask, p[1]=(0<<16)|bN + #define EMIT(bN, OFF) \ + p[OFF] = ((uint32_t)(bN) << 16) | alwaysMask; \ + p[OFF+1] = (bN); +#else + // Classic ESP32 layout: [S1, S0, S3, S2] (half-words swapped) + // Output order: S0=HIGH, S1=data, S2=data, S3=LOW + // 32-bit pair: p[0]=(alwaysMask<<16)|bN, p[1]=(bN<<16)|0 + const uint32_t AH = (uint32_t)alwaysMask << 16; + #define EMIT(bN, OFF) \ + p[OFF] = AH | (bN); \ + p[OFF+1] = (uint32_t)(bN) << 16; +#endif + EMIT(b0, 0) EMIT(b1, 2) EMIT(b2, 4) EMIT(b3, 6) + EMIT(b4, 8) EMIT(b5, 10) EMIT(b6, 12) EMIT(b7, 14) + #undef EMIT + } +} + +#else +// 8-bit parallel encode: branchless gather + scatter, 32 bytes per source byte (8 channels) +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { + for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { + uint8_t alwaysMask = 0; + uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; + uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; + + for (int ch = 0; ch < maxChannel; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + const uint8_t m = (uint8_t)(1u << ch); + alwaysMask |= m; + const uint8_t b = _channels[ch].srcData[_channels[ch].srcPos++]; + b0 |= m & (uint8_t)(0u - ((b >> 7) & 1u)); + b1 |= m & (uint8_t)(0u - ((b >> 6) & 1u)); + b2 |= m & (uint8_t)(0u - ((b >> 5) & 1u)); + b3 |= m & (uint8_t)(0u - ((b >> 4) & 1u)); + b4 |= m & (uint8_t)(0u - ((b >> 3) & 1u)); + b5 |= m & (uint8_t)(0u - ((b >> 2) & 1u)); + b6 |= m & (uint8_t)(0u - ((b >> 1) & 1u)); + b7 |= m & (uint8_t)(0u - ((b >> 0) & 1u)); + } + if (!alwaysMask) break; + + uint32_t* p = (uint32_t*)(dest + pos); +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + // S2/S3: no swap, layout [S0,S1,S2,S3] = [HIGH,data,data,LOW] + #define EMIT8(bN, OFF) \ + p[OFF] = (uint32_t)(alwaysMask) | ((uint32_t)(bN) << 8) | ((uint32_t)(bN) << 16); +#else + // Classic ESP32: half-word swap, memory [S2,S3,S0,S1] = [data,0,HIGH,data] + const uint32_t AH8 = (uint32_t)alwaysMask << 16; + #define EMIT8(bN, OFF) \ + p[OFF] = AH8 | (uint32_t)(bN) | ((uint32_t)(bN) << 24); +#endif + EMIT8(b0, 0) EMIT8(b1, 1) EMIT8(b2, 2) EMIT8(b3, 3) + EMIT8(b4, 4) EMIT8(b5, 5) EMIT8(b6, 6) EMIT8(b7, 7) + #undef EMIT8 + } +} +#endif // WLED_PIXELBUS_16PARALLEL + +void IRAM_ATTR __attribute__((noinline)) I2sBusContext::fillBuffer(uint8_t bufIdx) { + uint32_t* w = (uint32_t*)_dmaBuffer[bufIdx]; + const uint32_t* end = w + (_bufferSize >> 2); + while (w < end) *w++ = 0; // clear buffer: do not use memset, it is not necesarily IARM safe. _bufferSize is 4-byte aligned (could clear just the reset part but it adds complexity and this is fast) + + if (_resetBytesLeft > 0) { + descSetLength(_dmaDesc[bufIdx], _resetBytesLeft); + descSetNext(_dmaDesc[bufIdx], nullptr); + _resetBytesLeft = WLEDPB_I2S_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers (is a multiple of 4 if set "naturally" below) + return; // nothing to encode, this is a reset pulse, keep output low (all zeroes) + } + + uint32_t bytesToEncode = 0; + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + maxCh = ch + 1; + uint32_t channelBytesLeft = _channels[ch].srcLen - _channels[ch].srcPos; + if (channelBytesLeft > bytesToEncode) bytesToEncode = channelBytesLeft; + } + } + + uint32_t translatedbytes = bytesToEncode * WLEDPB_I2S_DMABYTES; + translatedbytes = translatedbytes > _bufferSize ? _bufferSize : translatedbytes; + encode4Step(_dmaBuffer[bufIdx], translatedbytes, maxCh); + + if (translatedbytes < _bufferSize) { + // Data ran out before the buffer was full (i.e. we are done), compute the minimum reset period we must send as zero cycles + uint32_t resetNs = _timing.reset_us * 1000; + uint32_t bitPeriodNs = _timing.bitPeriod() + 1; // +1 to ensure no division by zero and slightly over-estimate the reset cycle + uint32_t zeroCycles = resetNs / bitPeriodNs; + size_t resetBytes = zeroCycles * (WLEDPB_I2S_DMABYTES / 8); // one cycle is 4 clocks, on each clock two/one buffer byte(s) sent out in parallel + + size_t newLen = translatedbytes + resetBytes; + if (newLen > _bufferSize) { + _resetBytesLeft = newLen - _bufferSize; // reset pulse does not fit into this buffer frame, send another one (see above) + descSetLength(_dmaDesc[bufIdx], _bufferSize); + } + else { + descSetLength(_dmaDesc[bufIdx], newLen); // send the rest (zeroes) as a reset + _resetBytesLeft = WLEDPB_I2S_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers or it will mess up the DMA + descSetNext(_dmaDesc[bufIdx], nullptr); // reset fit into this buffer, end transfer after this is sent + } + } +} + +bool I2sBusContext::startTransmit() { + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; + + // Only start transmission if ALL active channels have populated data + if (_stagedMask != _channelMask) return true; + _stagedMask = 0; // Reset for next frame + + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + _resetBytesLeft = 0; + + if (!_dmaAllocated) { + if (!_allocDmaBuffers()) return false; + } + + // Fill all buffers initially + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + descSetNext(_dmaDesc[i], _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]); // restore circular buffer chain + descSetLength(_dmaDesc[i], _bufferSize); + fillBuffer(i); + descSetEof(_dmaDesc[i]); // enable eof, just in case + descSetOwnerDma(_dmaDesc[i]); // hand ownership over to DMA after descriptor init + } + + _lastFilled = WLEDPB_I2S_DMA_BUFFER_COUNT - 1; // all buffers were just pre-filled, next to refill is buffer 0 + _state = DriverState::Sending; + _txStartMillis = millis(); // watchdog start + + hwStartTransfer(); + + return true; +} + +//============================================================================== +// ISR / DMA callback +//============================================================================== + +#ifdef CONFIG_IDF_TARGET_ESP32S3 +IRAM_ATTR bool I2sBusContext::dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data) { + (void)event_data; + I2sBusContext* ctx = (I2sBusContext*)user_data; + if (dma_chan != ctx->_dmaChannel || ctx->_dmaChanId < 0) return false; // sanity check, just in case + + uint32_t eofAddr = GDMA.channel[ctx->_dmaChanId].out.eof_des_addr; // descriptor that just finished + uint32_t curDesc = GDMA.channel[ctx->_dmaChanId].out.dscr; // note: at EOF this still points to the just-completed descriptor, not the next one + + int8_t completedBuf = -1; + for (uint8_t i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + if ((uint32_t)ctx->_dmaDesc[i] == eofAddr) { completedBuf = i; break; } + } + + ctx->_processEof(completedBuf, curDesc); + return false; +} +#else +void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { + I2sBusContext* ctx = (I2sBusContext*)arg; + i2s_dev_t* dev = ctx->_i2sDev; + uint32_t status = dev->int_st.val; + dev->int_clr.val = status; + + if (status & I2S_OUT_TOTAL_EOF_INT_ST) { // link terminated: frame fully sent + ctx->hwStopTransfer(); + return; + } + if (!(status & I2S_OUT_EOF_INT_ST)) return; + + // which descriptor actually produced this EOF (resyncs even if EOFs were coalesced) + uint32_t eofAddr = dev->out_eof_des_addr; + int8_t completedBuf = -1; + for (uint8_t i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + if ((uint32_t)ctx->_dmaDesc[i] == eofAddr) { completedBuf = i; break; } + } + + ctx->_processEof(completedBuf, dev->out_link_dscr); // note: out_link_dscr still points to the just-completed descriptor at EOF time +} +#endif + +// refill buffer(s) upon eof, completedBuf is the descriptor that just finished (from out_eof_des_addr / GDMA eof_des_addr) +// note: curDescAddr (out_link_dscr / GDMA out.dscr) is NOT reliable to find the in-flight descriptor: at EOF time it +// still points to the descriptor that just completed, not the next one (verified on ESP32 and ESP32-S2). Do not use it +// to decide what is safe to refill — completed buffers are always safe, the in-flight one is (completedBuf + 1). +void IRAM_ATTR I2sBusContext::_processEof(int8_t completedBuf, uint32_t curDescAddr) { + (void)curDescAddr; // unreliable, see above + if (completedBuf < 0) return; + + // end of frame? checks the just finished descriptor for the terminating nullptr + if (descGetNext(_dmaDesc[completedBuf]) == nullptr) { + hwStopTransfer(); + return; + } + +#if WLEDPB_I2S_DMA_BUFFER_COUNT > 2 + if (_lastFilled == (uint8_t)completedBuf) return; // duplicate eof, nothing pending (never refill the in-flight buffer) + // refill every completed-but-unprocessed buffer, oldest first, to catch up if a previous eof was missed. + uint8_t b = (_lastFilled + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; + while (_resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { // dont fill if reset pulse was queued + fillBuffer(b); // fill buffer, handles reset pulse and end of transfer + _lastFilled = b; + if (b == (uint8_t)completedBuf) break; + b = (b + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; + } +#else + // double buffering: only the completed buffer is safe to refill, the other one is in-flight + if (_resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { + fillBuffer(completedBuf); // fill buffer, handles reset pulse and end of transfer + _lastFilled = (uint8_t)completedBuf; + } +#endif +} + +void I2sBusContext::abortTransmit() { + hwStopTransfer(); +} + +// ============================================ +// I2sBus implementation +// ============================================ + +I2sBus::I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum, uint8_t ledType, size_t numPixels) + : _pin(pin) + , _timing(timing) + , _inverted(false) + , _initialized(false) + , _busNum(busNum) + , _channelIdx(-1) + , _ctx(nullptr) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; + _numPixels = numPixels; // stored so begin() can report srcBytes to the shared I2sBusContext for DMA sizing +} + +I2sBus::~I2sBus() { + end(); +} + +bool I2sBus::begin() { + if (_initialized) return true; + + _ctx = I2sBusContext::get(_busNum); + if (!_ctx) return false; + + if (!_ctx->init(_timing)) { + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + // pass our encoded byte count so the context can size DMA buffers for the largest bus + const size_t srcBytes = (size_t)_numPixels * _encoder.getPixelBytes(); + _channelIdx = _ctx->registerChannel(_pin, this, srcBytes, _inverted); + if (_channelIdx < 0) { + //DEBUG_PRINTF_P(PSTR("[I2S] registerChannel failed for pin %d\n"), _pin); + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _initialized = true; + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + //DEBUG_PRINTF_P(PSTR("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n"), _pin, _busNum, _channelIdx); + return true; +} + +// invert output signal, must be set before begin() +void I2sBus::setInverted(bool inv) { + _inverted = inv; +} + +void I2sBus::end() { + if (!_initialized) return; + + if (_ctx) { + // Wait for any active transmission to complete before cleanup + while (!_ctx->isIdle()) vTaskDelay(1); + _ctx->unregisterChannel(_channelIdx); + I2sBusContext::release(_busNum); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool I2sBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) { + const size_t pixelBytes = padPixelBytesForSuffix((size_t)numPixels * numChannels, _ledType); + size_t needed = _prefixLen + pixelBytes + _suffixLen; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + if (_encodeBuffer) { heap_caps_free(_encodeBuffer); _encodeBuffer = nullptr; } + if (needed == 0) return true; + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); + if (!_encodeBuffer) { _encodeBufferSize = 0; return false; } + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + _prefixLen; + if (_suffixLen == sizeof(SM16825_SUFFIX) && _ledType == TYPE_SM16825) + memcpy(_pixelData + pixelBytes, SM16825_SUFFIX, sizeof(SM16825_SUFFIX)); + return true; +} + +bool I2sBus::show() { + if (!_initialized || !_ctx || !_encodeBuffer || _numPixels == 0) return false; + + // Wait for previous transmission to complete, timeout should not happen, it is a fallback to guarantee driver wont get stuck + while (!_ctx->isIdle() && (millis() - _ctx->getTxStartMillis()) < 500) { + vTaskDelay(1); + } + + // Send already-encoded buffer directly + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodeBufferSize); + return _ctx->startTransmit(); +} + +bool I2sBus::canShow() const { + if (!_ctx) return true; + if (_ctx->isIdle()) return true; + // safety watchdog if the driver ever gets stuck (e.g. a missed/overwritten terminating descriptor) + if ((uint32_t)(millis() - _ctx->getTxStartMillis()) > 500) { + _ctx->abortTransmit(); + return true; + } + return false; +} + +void I2sBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +} // namespace WLEDpixelBus +#endif // WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h new file mode 100644 index 0000000000..dacf3f4258 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -0,0 +1,235 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel I2S/LCD output driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" + +#ifdef WLEDPB_I2S_SUPPORT + +#if SP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) +#include "driver/periph_ctrl.h" +#else +#include "esp_private/periph_ctrl.h" // TODO: this may get removed in future IDF (V6 still has it), need a rewrite if that ever happens +#endif + +#include "esp_rom_gpio.h" + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + #include "esp_private/gdma.h" + #include "hal/dma_types.h" + #include "hal/gpio_hal.h" + #include "hal/lcd_ll.h" + #include "soc/lcd_cam_struct.h" + #include "soc/gpio_sig_map.h" + #include "soc/gdma_struct.h" // for global GDMA +#else + #include "soc/i2s_struct.h" + #include "soc/i2s_reg.h" + #include "rom/lldesc.h" + #include "esp_intr_alloc.h" +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// I2S Parallel Bus - ESP32, ESP32-S2, ESP32-S3 (LCD) +//============================================================================== + +// SOC_LCD_I80_BUSES: number of (I2S) peripherals that support the LCD Intel 8080 i.e. parallel output mode (ESP32: two, S2: one, S3: one) +// TODO: support both buses on ESP32? (currently only I2S_NUM_1 is used for LED output, I2S_NUM_0 is for AR) + +#define WLEDPB_I2S_BUS_COUNT SOC_LCD_I80_BUSES + +// note: 4-step cadence with 16 parallel outs requires 8 bytes per source bit or 192bytes per RGB LED, i.e. a 1k buffer can hold ~5 LEDs, ISR will fire every 144us + +// I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering is required for 16-parallel output. +#ifndef WLEDPB_I2S_DMA_BUFFER_COUNT + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 // need 3 buffers in 16x parallel mode + #else + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT (ESP32 only) + #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 // need triple buffering as RMT eat up a lot of ISR time + #else + #define WLEDPB_I2S_DMA_BUFFER_COUNT 2 // 2 buffers is enough if not constantly interrupted by RMT (i.e. with 4 RMT outputs) + #endif + #endif +#endif + +// 16-bit parallel mode supports 16 channels; 8-bit supports 8 channels. +#ifdef WLED_PIXELBUS_16PARALLEL + #define WLEDPB_I2S_MAX_CHANNELS 16 + #define WLEDPB_I2S_DMABYTES 64 // 64 bytes per pixel byte (4 clocks per bit, 2 bytes per clock) +#else + #define WLEDPB_I2S_MAX_CHANNELS 8 + #define WLEDPB_I2S_DMABYTES 32 // 32 bytes per pixel byte (4 clocks per bit, 1 byte per clock) +#endif +#define WLEDPB_I2S_XFER_DONE_FLAG 3 // flag to indicate end of transfer, must NOT be a multiple of 4 +/** + * I2S bus context - manages shared I2S/LCD peripheral for parallel output + * Uses circular DMA buffers with ISR-driven buffer refill + */ +class I2sBusContext { +public: + static I2sBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); + + bool init(const LedTiming& timing); + void deinit(); + + // Channel management + int8_t registerChannel(int8_t pin, I2sBus* bus, size_t srcBytes, bool inverted = false); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + // Transmission + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } + uint32_t getTxStartMillis() const { return _txStartMillis; } + void abortTransmit(); // abort on timeout (just in case to avoid driver getting stuck) + + // Data access for channels + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + I2sBusContext(uint8_t busNum); + ~I2sBusContext(); + + void IRAM_ATTR fillBuffer(uint8_t bufIdx); + bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); // Encoding (4-step cadence) + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); +#else + static void IRAM_ATTR dmaISR(void* arg); +#endif + void IRAM_ATTR _processEof(int8_t completedBuf, uint32_t curDescAddr); + + // Hardware abstraction + bool hwInit(const LedTiming& timing); + void hwDeinit(); + void hwStartTransfer(); + void IRAM_ATTR hwStopTransfer(); + void hwRoutePin(int8_t pin, int8_t idx, bool inverted); + + // DMA descriptor abstraction +#ifdef CONFIG_IDF_TARGET_ESP32S3 + using DmaDesc_t = dma_descriptor_t; + static inline void descSetBuf(DmaDesc_t* d, uint8_t* b) { d->buffer = b; } + static inline void descSetSizeAndLen(DmaDesc_t* d, size_t len) { d->dw0.size = len; d->dw0.length = len; } + static inline void descSetLength(DmaDesc_t* d, size_t len) { d->dw0.length = len; } + static inline void descSetNext(DmaDesc_t* d, DmaDesc_t* n) { d->next = n; } + static inline DmaDesc_t* descGetNext(DmaDesc_t* d) { return d->next; } + static inline void descSetEof(DmaDesc_t* d) { d->dw0.suc_eof = 1; } + static inline void descSetOwnerDma(DmaDesc_t* d) { d->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; } + static inline bool descIsOwnerDma(DmaDesc_t* d) { return d->dw0.owner == DMA_DESCRIPTOR_BUFFER_OWNER_DMA; } +#else + using DmaDesc_t = lldesc_t; + static inline void descSetBuf(DmaDesc_t* d, uint8_t* b) { d->buf = b; } + static inline void descSetSizeAndLen(DmaDesc_t* d, size_t len) { d->size = len; d->length = len; } + static inline void descSetLength(DmaDesc_t* d, size_t len) { d->length = len; } + static inline void descSetNext(DmaDesc_t* d, DmaDesc_t* n) { d->qe.stqe_next = n; } + static inline DmaDesc_t* descGetNext(DmaDesc_t* d) { return d->qe.stqe_next; } + static inline void descSetEof(DmaDesc_t* d) { d->eof = 1; } + static inline void descSetOwnerDma(DmaDesc_t* d) { d->owner = 1; } + static inline bool descIsOwnerDma(DmaDesc_t* d) { return d->owner == 1; } +#endif + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + gdma_channel_handle_t _dmaChannel; + int8_t _dmaChanId; +#else + uint8_t _busNum; + i2s_dev_t* _i2sDev; + intr_handle_t _isrHandle; +#endif + + volatile DriverState _state; + bool _initialized; + + // DMA circular buffer chain + DmaDesc_t* _dmaDesc[WLEDPB_I2S_DMA_BUFFER_COUNT]; + uint8_t* _dmaBuffer[WLEDPB_I2S_DMA_BUFFER_COUNT]; + size_t _bufferSize; // actual allocated DMA buffer size (per buffer) + size_t _maxSrcBytes; // max source (encoded pixel) bytes across all registered channels; drives DMA sizing + bool _dmaAllocated; // true when DMA buffers are allocated and reflect current _maxSrcBytes + volatile uint8_t _lastFilled; // last DMA buffer that was refilled; next to refill is (_lastFilled + 1) % COUNT + volatile uint16_t _resetBytesLeft; + volatile uint32_t _txStartMillis; // millis() when the current transfer started, watchdog for canShow() and show() + + // Timing + LedTiming _timing; + uint32_t _clockDiv; + + // Channel data + struct ChannelData { + I2sBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; + uint8_t _channelCount; + uint16_t _channelMask; + uint16_t _stagedMask; + size_t _maxDataLen; + + // Singleton instances + static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; +}; + +/** + * I2S parallel output bus + */ +class I2sBus : public PixelBus { +public: + /** + * Create I2S bus + * @param pin GPIO pin + * @param timing LED timing + * @param colorOrder Color order + * @param numChannels Bytes per pixel + * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2/S3) + * @param ledType LED chip type constant + * @param numPixels Number of pixels; stored for DMA buffer sizing in I2sBusContext + */ + I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum = 1, uint8_t ledType = 0, size_t numPixels = 0); + ~I2sBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "I2S"; } +#endif + + void setInverted(bool inv) override; + void setColorOrder(uint8_t co); + + // Override to use DMA-capable allocator for I2S + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; + +private: + int8_t _pin; + LedTiming _timing; + bool _inverted; + bool _initialized; + uint8_t _busNum; + int8_t _channelIdx; + I2sBusContext* _ctx; +}; + +} // namespace WLEDpixelBus +#endif // WLEDPB_I2S_SUPPORT + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.cpp new file mode 100644 index 0000000000..2657dc8dc3 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.cpp @@ -0,0 +1,813 @@ +// WLEDpixelBus_PARLIO.cpp +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel PARLIO output driver implementation + +written by Damian Schneider @dedehai 2026 + +Note: PARLIO driver was generated with heavy help of an AI with lots of refinement and testing (works but needs a thorough review) +For a detailed escription on how it works see header file + +TODO: need to do an in-depth review and harden edge cases, maybe also add a watchdog timeout in case things can go wrong + as we use low level stuff to get around the gaps the "pure API" espressif driver has when using linked DMA lists + + Also C5, H2, C61 and P4 are untested (only tested working on C6) + P4 does not use the seamless-DMA mode (not supported by API); the fallback path now + encodes the whole frame into one buffer and sends it with a single + parlio_tx_unit_transmit() call instead of ping-ponging several small buffers + (see _allocDmaBuffers()) - still needs testing on real P4 hardware + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_PARLIO.h" + +#ifdef WLEDPB_PARLIO_SUPPORT + +#if WLEDPB_PARLIO_SEAMLESS_DMA + #include "hal/parlio_ll.h" // TX engine low-level control (tx_bytelen, start, clock, idle value) + #include "hal/gdma_ll.h" // gdma_ll_tx_restart() - always-inline register write (includes soc/gdma_struct.h for the GDMA instance) + #include "soc/parl_io_struct.h" // PARL_IO register struct instance + #define PARLIO_HW (&PARL_IO) // single PARLIO group/unit on all supported targets + // C6's parlio_ll.h defines PARLIO_LL_TX_MAX_BYTES_PER_FRAME (0xFFFF), the other targets + // only define the BITS variant (0x7FFFF) - the byte limit is the same 65535 everywhere + #ifndef PARLIO_LL_TX_MAX_BYTES_PER_FRAME + #define PARLIO_LL_TX_MAX_BYTES_PER_FRAME (PARLIO_LL_TX_MAX_BITS_PER_FRAME / 8) + #endif + + // C5 MP (and future chips with AHB GDMA v2+) expose the DMA controller as AHB_DMA + // and use ahb_dma_ll_* helpers, while C6/H2 keep the legacy GDMA naming. + #if defined(SOC_AHB_GDMA_VERSION) && (SOC_AHB_GDMA_VERSION >= 2) + #define WLEDPB_PARLIO_GDMA_DEV (&AHB_DMA) + #define WLEDPB_PARLIO_GDMA_LL_TX_RESTART ahb_dma_ll_tx_restart + #else + #define WLEDPB_PARLIO_GDMA_DEV (&GDMA) + #define WLEDPB_PARLIO_GDMA_LL_TX_RESTART gdma_ll_tx_restart + #endif +#endif + +namespace WLEDpixelBus { + +ParlioBusContext* ParlioBusContext::_instances[WLEDPB_PARLIO_BUS_COUNT] = {nullptr}; +uint8_t ParlioBusContext::_refCount[WLEDPB_PARLIO_BUS_COUNT] = {0}; + +ParlioBusContext* ParlioBusContext::get(uint8_t busNum) { + if (busNum >= WLEDPB_PARLIO_BUS_COUNT) return nullptr; + + if (_instances[busNum] == nullptr) { + _instances[busNum] = new ParlioBusContext(busNum); + } + if (_instances[busNum] != nullptr) + _refCount[busNum]++; + return _instances[busNum]; +} + +void ParlioBusContext::release(uint8_t busNum) { + if (busNum >= WLEDPB_PARLIO_BUS_COUNT) return; + if (_refCount[busNum] == 0) return; + + _refCount[busNum]--; + if (_refCount[busNum] == 0 && _instances[busNum]) { + delete _instances[busNum]; + _instances[busNum] = nullptr; + } +} + +ParlioBusContext::ParlioBusContext(uint8_t /*busNum*/) + : _txUnit(nullptr) + , _unitEnabled(false) + , _unitStale(false) + , _state(DriverState::Idle) + , _initialized(false) + , _bufferSize(0) + , _maxSrcBytes(0) + , _dmaAllocated(false) + , _resetBytesLeft(0) +#if WLEDPB_PARLIO_SEAMLESS_DMA + , _dmaChan(nullptr) + , _dmaChanIdx(-1) + , _desc(nullptr) + , _fillHead(0) + , _chunkBytesLeft(0) +#endif + , _timing{0, 0, 0, 0, 0} + , _outClockHz(0) + , _channelCount(0) + , _channelMask(0) + , _stagedMask(0) + , _invertMask(0) + , _maxDataLen(0) +{ + for (int i = 0; i < WLEDPB_PARLIO_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = nullptr; + _txLen[i] = 0; + _endOfFrame[i] = false; + } +} + +ParlioBusContext::~ParlioBusContext() { + deinit(); +} + +bool ParlioBusContext::init(const LedTiming& timing) { + if (_initialized) return true; + + _timing = timing; + + // PARLIO output clock: 4 clocks per LED bit (4-step cadence). + // Note: the PARLIO driver only has an INTEGER clock divider from the source clock, + // so arbitrary custom timings are less accurate than with the fractional I2S divider. + // Standard timings are fine: 1.25us bit period -> 3.2MHz, exact integer division. + uint32_t bitPeriodNs = timing.bitPeriod(); + if (bitPeriodNs == 0) return false; + uint64_t clkHz = (4ULL * 1000000000ULL) / bitPeriodNs; + if (clkHz > 40000000ULL) return false; // above PARLIO TX max clock on C6/H2 + _outClockHz = (uint32_t)clkHz; + + // NOTE: the PARLIO unit is NOT created here. Pins are fixed at unit creation time and + // channels register after init(), so creation is deferred to the first startTransmit(). + + _initialized = true; + return true; +} + +void ParlioBusContext::deinit() { + int timeout = 100; + while (!isIdle() && timeout--) { vTaskDelay(1); } + + hwStopTransfer(); + +#if WLEDPB_PARLIO_SEAMLESS_DMA + // note: _dmaChan is owned by the PARLIO unit, it is deleted in hwDeinit() + if (_desc) { + heap_caps_free(_desc); + _desc = nullptr; + } +#endif + + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + } + + hwDeinit(); + + _dmaAllocated = false; + _initialized = false; +} + +//============================================================================== +// Hardware abstraction +//============================================================================== + +bool ParlioBusContext::hwInit() { + // data GPIOs are fixed at creation; use GPIO_NUM_NC for unused data lines + parlio_tx_unit_config_t config; + memset(&config, 0, sizeof(config)); + config.clk_src = PARLIO_CLK_SRC_DEFAULT; // do NOT select XTAL explicitly, known C6 issue + config.data_width = WLEDPB_PARLIO_MAX_CHANNELS; + config.clk_in_gpio_num = GPIO_NUM_NC; // not used, TX only + config.valid_gpio_num = GPIO_NUM_NC; // valid signal would occupy the MSB data line + config.clk_out_gpio_num = GPIO_NUM_NC; // no external clock needed + for (int i = 0; i < WLEDPB_PARLIO_MAX_CHANNELS; i++) { + config.data_gpio_nums[i] = (_channels[i].active && _channels[i].pin >= 0) + ? (gpio_num_t)_channels[i].pin : GPIO_NUM_NC; + } + config.output_clk_freq_hz = _outClockHz; + config.trans_queue_depth = WLEDPB_PARLIO_DMA_BUFFER_COUNT; + config.max_transfer_size = _bufferSize; // note: _allocDmaBuffers() must run before hwInit() + // the following two fields exist in recent IDF versions, remove them if your IDF does not have them + config.sample_edge = PARLIO_SAMPLE_EDGE_POS; + config.bit_pack_order = PARLIO_BIT_PACK_ORDER_MSB; + + esp_err_t err = parlio_new_tx_unit(&config, &_txUnit); + if (err != ESP_OK) { + _txUnit = nullptr; + return false; + } + + parlio_tx_event_callbacks_t cbs; + memset(&cbs, 0, sizeof(cbs)); + cbs.on_trans_done = dmaCallback; + err = parlio_tx_unit_register_event_callbacks(_txUnit, &cbs, this); + if (err != ESP_OK) { + parlio_del_tx_unit(_txUnit); + _txUnit = nullptr; + return false; + } + +#if WLEDPB_PARLIO_SEAMLESS_DMA + // descriptor ring (once) + borrow this unit's GDMA channel and register the refill callback + if (!_initDmaRing()) { + parlio_del_tx_unit(_txUnit); + _txUnit = nullptr; + return false; + } +#endif + + _unitStale = false; + _unitEnabled = false; + return true; +} + +void ParlioBusContext::hwDeinit() { + if (_txUnit) { + if (_unitEnabled) { + parlio_tx_unit_disable(_txUnit); + _unitEnabled = false; + } + parlio_del_tx_unit(_txUnit); + _txUnit = nullptr; +#if WLEDPB_PARLIO_SEAMLESS_DMA + _dmaChan = nullptr; // channel was owned by the unit, now gone +#endif + } +} + +void IRAM_ATTR ParlioBusContext::hwStopTransfer() { + // only called from deinit() (waits for idle first) +#if WLEDPB_PARLIO_SEAMLESS_DMA + if (_dmaChan) gdma_stop(_dmaChan); +#endif + if (_txUnit && _unitEnabled) { + parlio_tx_unit_disable(_txUnit); + _unitEnabled = false; + } +} + +#if WLEDPB_PARLIO_SEAMLESS_DMA +//============================================================================== +// Seamless streaming: unit's GDMA channel + our circular descriptor ring +//============================================================================== + +bool ParlioBusContext::_initDmaRing() { + // descriptor ring in DMA-capable internal RAM, statically closed into a circle: + // the hardware follows the next pointers on its own, so the DMA never stops mid-frame. + // Allocated once (survives unit recreation); the channel is re-borrowed per unit. + if (!_desc) { + _desc = (dma_descriptor_t*)heap_caps_aligned_alloc(4, WLEDPB_PARLIO_DMA_BUFFER_COUNT * sizeof(dma_descriptor_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); + if (!_desc) return false; + memset(_desc, 0, WLEDPB_PARLIO_DMA_BUFFER_COUNT * sizeof(dma_descriptor_t)); + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _desc[i].dw0.suc_eof = 1; // per-descriptor EOF interrupt (refill heartbeat); PARLIO ignores it (EOF is by byte counter) + _desc[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU; + _desc[i].buffer = _dmaBuffer[i]; + _desc[i].next = (i == WLEDPB_PARLIO_DMA_BUFFER_COUNT - 1) ? &_desc[0] : &_desc[i + 1]; + } + } + + // borrow the unit's GDMA channel (see WledpbParlioTxUnitHead): already connected to the + // PARLIO trigger, with owner-check + auto-write-back strategy applied by the driver + _dmaChan = WLEDPB_PARLIO_TX_DMA_CHAN(_txUnit); + if (!_dmaChan) return false; + + // Hardware channel index for direct LL register access from the ISR. On AHB GDMA + // (C6/H2/C5) the channel ID equals the register struct channel index. Queried via the + // public (esp_private) API so no struct mirroring is needed. If it fails we fall back + // to gdma_append() in the ISR (fine as long as CONFIG_GDMA_CTRL_FUNC_IN_IRAM=y or the + // callback never runs with the flash cache off). + _dmaChanIdx = -1; + int chanIdx = -1; + if (gdma_get_channel_id(_dmaChan, &chanIdx) == ESP_OK && + chanIdx >= 0 && chanIdx < SOC_GDMA_PAIRS_PER_GROUP_MAX) { + _dmaChanIdx = (int8_t)chanIdx; + } + + // the driver (v5.3-v5.5) never registers GDMA callbacks on this channel, they are ours + gdma_tx_event_callbacks_t cbs = {}; + cbs.on_trans_eof = gdmaEofCallback; + if (gdma_register_tx_event_callbacks(_dmaChan, &cbs, this) != ESP_OK) { + _dmaChan = nullptr; + return false; + } + return true; +} + +void IRAM_ATTR ParlioBusContext::_armDescriptor(uint8_t idx) { + const uint32_t len = (uint32_t)_txLen[idx]; + _desc[idx].dw0.size = len; + _desc[idx].dw0.length = len; + _desc[idx].buffer = _dmaBuffer[idx]; + _desc[idx].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; // owner write last: hands the descriptor to the DMA +} + +IRAM_ATTR bool ParlioBusContext::gdmaEofCallback(gdma_channel_handle_t /*dma_chan*/, + gdma_event_data_t* /*edata*/, + void* user_ctx) { + ParlioBusContext* ctx = (ParlioBusContext*)user_ctx; + if (ctx->_state != DriverState::Sending) return false; // stale wake-up between frames + + // Walk the ring from the fill head and refill every descriptor the DMA has handed back + // to the CPU: auto write-back clears the owner bit exactly when a descriptor has been + // consumed (descriptors never armed this frame are CPU-owned as well and simply get + // filled next). Stops at the first DMA-owned descriptor. This makes the callback + // idempotent: coalesced interrupts are caught up, and a stale wake-up between frames + // either finds nothing to do or accidentally does the correct work. + while (ctx->_state == DriverState::Sending) { + const uint8_t idx = ctx->_fillHead; + if (ctx->_desc[idx].dw0.owner != DMA_DESCRIPTOR_BUFFER_OWNER_CPU) break; // not consumed yet + if (ctx->_resetBytesLeft == WLEDPB_PARLIO_XFER_DONE_FLAG) break; // final buffer already queued, leave DMA stalled behind it + ctx->fillBuffer(idx); + ctx->_armDescriptor(idx); + ctx->_fillHead = (uint8_t)((idx + 1) % WLEDPB_PARLIO_DMA_BUFFER_COUNT); + } + + // If the DMA caught up with the CPU and paused on a CPU-owned descriptor, restart the + // outlink. Use the always-inline LL register write instead of gdma_append(): gdma_append() + // lives in flash unless CONFIG_GDMA_CTRL_FUNC_IN_IRAM=y (not the default, and unchangeable + // on precompiled Arduino/Tasmota frameworks), while the GDMA ISR itself is IRAM-resident + // and IRAM-flagged in those builds - calling flash from it panics with "Cache error" + // during NVS writes. The LL TX restart helper compiles to a single MMIO write. + if (ctx->_dmaChanIdx >= 0) { + WLEDPB_PARLIO_GDMA_LL_TX_RESTART(WLEDPB_PARLIO_GDMA_DEV, (uint32_t)ctx->_dmaChanIdx); + } else { + gdma_append(ctx->_dmaChan); // channel index unknown: last resort + } + return false; +} + +void IRAM_ATTR ParlioBusContext::_processChunkEof() { + // Invoked from the IDF driver's PARLIO TX EOF ISR (via on_trans_done). TX EOF is + // wire-accurate: tx_bytelen bytes have been clocked out. The driver ISR has already + // stopped the TX engine and clock when we get here. + if (_state != DriverState::Sending) return; // stale TX EOF from a previous frame + if (_chunkBytesLeft == 0) { + _state = DriverState::Idle; // whole frame clocked out, engine stays off until the next frame + return; + } + + // Frame longer than the 16-bit tx_bytelen register: seamlessly chain the next chunk. + // The DMA ring kept feeding the FIFO across this boundary, so unlike the IDF driver's + // transaction model there is no FIFO reset, no descriptor remount and no GDMA restart + // here - just a few register writes. The outputs sit at the idle level only for the + // ISR latency (~1-2us), far below the LED reset/latch threshold. + const uint32_t chunk = _chunkBytesLeft > PARLIO_LL_TX_MAX_BYTES_PER_FRAME ? PARLIO_LL_TX_MAX_BYTES_PER_FRAME : _chunkBytesLeft; + _chunkBytesLeft -= chunk; + parlio_ll_tx_set_trans_bit_len(PARLIO_HW, chunk * 8); + uint32_t wait = 100000; // FIFO still holds DMA'd data; bounded wait for safety + while (!parlio_ll_tx_is_ready(PARLIO_HW) && --wait) {} + parlio_ll_tx_start(PARLIO_HW, true); + parlio_ll_tx_enable_clock(PARLIO_HW, true); +} + +#else +//============================================================================== +// Fallback: single full-frame buffer, one transaction per frame +//============================================================================== + +bool IRAM_ATTR ParlioBusContext::_queueBuffer(uint8_t bufIdx) { + parlio_transmit_config_t trans_config = { + .idle_value = _invertMask // hold lines at the reset level after the transaction (remove field if your IDF lacks it) + }; + // note: payload size is in BITS, not bytes + return parlio_tx_unit_transmit(_txUnit, _dmaBuffer[bufIdx], _txLen[bufIdx] * 8, &trans_config) == ESP_OK; +} +#endif // WLEDPB_PARLIO_SEAMLESS_DMA + +//============================================================================== +// Buffer management & encoding +//============================================================================== + +bool ParlioBusContext::_allocDmaBuffers() { + if (_dmaBuffer[0] != nullptr) return true; + +#if WLEDPB_PARLIO_SEAMLESS_DMA + _bufferSize = (WLEDPB_PARLIO_DMABYTES * _maxSrcBytes) / WLEDPB_PARLIO_DMA_BUFFER_COUNT; + _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes + if (_bufferSize > DEFAULT_DMA_BUFFER_SIZE) _bufferSize = DEFAULT_DMA_BUFFER_SIZE; + // GDMA descriptors have 12-bit size/length fields (max 4095, 4-byte aligned -> 4092) + if (_bufferSize > DMA_DESCRIPTOR_BUFFER_MAX_SIZE_4B_ALIGNED) _bufferSize = DMA_DESCRIPTOR_BUFFER_MAX_SIZE_4B_ALIGNED; + if (_bufferSize < MIN_DMA_BUFFER_SIZE) _bufferSize = MIN_DMA_BUFFER_SIZE; + + // allocate the descriptor-ring buffers (4-byte aligned for the GDMA engine) + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) return false; + memset(_dmaBuffer[i], 0, _bufferSize); + } +#else + // Fallback (P4): there is no byte-counter EOF to chain off, so don't stream several + // small buffers through the transaction queue - each transaction boundary there is a + // full pipeline restart (engine/clock off, FIFO reset), the exact cost the seamless + // path exists to avoid on C6/H2/C5. Instead encode the whole frame (pixel data + the + // trailing reset period) into one buffer up front and hand it to + // parlio_tx_unit_transmit() in a single call: the IDF driver builds its own linked DMA + // descriptor chain to cover it (sized via max_transfer_size in hwInit(), below), so the + // transfer streams gap-free start to finish and completes exactly once, at the true end + // of the frame. With the buffer sized to hold data+reset, fillBuffer() naturally takes + // the "last buffer of the frame" path on the first (only) call, so hwStartTransfer() + // queues exactly one transaction and the transmit-done callback just marks the frame + // complete (no refill/re-queue logic in the fallback at all). + // NOTE: this trades the small rotating buffers for one buffer sized for the entire + // frame - for long strips this can be a lot of DMA-capable RAM (32x the source bytes). + // If that doesn't fit in internal RAM, consider MALLOC_CAP_SPIRAM here (verify your IDF + // version/target's GDMA can reach PSRAM before relying on it). + _bufferSize = (size_t)WLEDPB_PARLIO_DMABYTES * _maxSrcBytes + _calcResetBytes(); + _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes + + _dmaBuffer[0] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[0]) return false; + memset(_dmaBuffer[0], 0, _bufferSize); +#endif + + _dmaAllocated = true; + return true; +} + +int8_t ParlioBusContext::registerChannel(int8_t pin, ParlioBus* bus, size_t srcBytes, bool inverted) { + // Find free slot + int8_t idx = -1; + for (int i = 0; i < WLEDPB_PARLIO_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + if (inverted) _invertMask |= (1 << idx); + + // track the largest source byte count across channels; used in _allocDmaBuffers() to size DMA buffers + if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; + + // pins are fixed at PARLIO unit creation: no pin routing here, just mark the unit for recreation + if (_txUnit) _unitStale = true; + + return idx; +} + +void ParlioBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_PARLIO_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); + _invertMask &= ~(1 << channelIdx); + + if (_txUnit) _unitStale = true; // recreate unit without this pin on next transmit +} + +void ParlioBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_PARLIO_MAX_CHANNELS) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + if (len > _maxDataLen) { + _maxDataLen = len; + } + + // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() + if (_stagedMask & (1 << channelIdx)) { + _stagedMask = 0; + } + _stagedMask |= (1 << channelIdx); +} + +// encode4Step: 4-step cadence, converts per-channel byte streams to parallel DMA words. +// PARLIO outputs one byte across the 8 data lines per clock, in linear memory order: +// step0=HIGH, step1=data, step2=data, step3=LOW ('0' is 0b1000, '1' is 0b1110). +// Inversion is applied by XORing every step with _invertMask, replicating GPIO-matrix +// inversion: inverted channels emit the complementary waveform (and a HIGH reset period, +// since fillBuffer pre-fills the buffer with _invertMask instead of 0). +void IRAM_ATTR ParlioBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { + const uint8_t inv = _invertMask; + for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { + // alwaysMask: channels with active data (HIGH step); bN: channels with bit N set + uint8_t alwaysMask = 0; + uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; + uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; + + for (int ch = 0; ch < maxChannel; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + const uint8_t m = (uint8_t)(1u << ch); + alwaysMask |= m; + const uint8_t b = _channels[ch].srcData[_channels[ch].srcPos++]; + // extract bits, unrolled for speed + b0 |= m & (uint8_t)(0u - ((b >> 7) & 1u)); + b1 |= m & (uint8_t)(0u - ((b >> 6) & 1u)); + b2 |= m & (uint8_t)(0u - ((b >> 5) & 1u)); + b3 |= m & (uint8_t)(0u - ((b >> 4) & 1u)); + b4 |= m & (uint8_t)(0u - ((b >> 3) & 1u)); + b5 |= m & (uint8_t)(0u - ((b >> 2) & 1u)); + b6 |= m & (uint8_t)(0u - ((b >> 1) & 1u)); + b7 |= m & (uint8_t)(0u - ((b >> 0) & 1u)); + } + if (!alwaysMask) break; // no active channels produced data + + uint32_t* p = (uint32_t*)(dest + pos); + // little-endian 32-bit store = memory bytes [step0, step1, step2, step3] + const uint32_t s0 = (uint32_t)(alwaysMask ^ inv); + const uint32_t s3 = (uint32_t)inv << 24; + #define EMIT(bN, OFF) { \ + const uint32_t d = (uint32_t)((bN) ^ inv); \ + p[OFF] = s0 | (d << 8) | (d << 16) | s3; \ + } + EMIT(b0, 0) EMIT(b1, 1) EMIT(b2, 2) EMIT(b3, 3) + EMIT(b4, 4) EMIT(b5, 5) EMIT(b6, 6) EMIT(b7, 7) + #undef EMIT + } +} + +// bytes of reset period appended at the end of a frame +uint32_t IRAM_ATTR ParlioBusContext::_calcResetBytes() const { + uint32_t resetNs = _timing.reset_us * 1000; + uint32_t bitPeriodNs = _timing.bitPeriod() + 1; // +1 to ensure no division by zero and slightly over-estimate the reset cycle + uint32_t zeroCycles = resetNs / bitPeriodNs; + return zeroCycles * (WLEDPB_PARLIO_DMABYTES / 8); // one LED bit cycle is 4 clocks, 1 buffer byte per clock +} + +void IRAM_ATTR ParlioBusContext::fillBuffer(uint8_t bufIdx) { + // pre-fill with _invertMask instead of 0: this is the "all lines idle" level, i.e. the reset + // level (LOW for normal channels, HIGH for inverted channels, matching matrix inversion) + memset(_dmaBuffer[bufIdx], _invertMask, _bufferSize); + _endOfFrame[bufIdx] = false; + + if (_resetBytesLeft > 0) { + // reset pulse continuation: this buffer is pure reset level. Long reset periods span + // several buffers; only the last one is flagged as end of frame. Never let _txLen + // exceed _bufferSize: the DMA would read past the buffer (can fault on C6's PMA). + if (_resetBytesLeft <= _bufferSize) { + _txLen[bufIdx] = _resetBytesLeft; + _resetBytesLeft = WLEDPB_PARLIO_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers + _endOfFrame[bufIdx] = true; + } else { + _txLen[bufIdx] = _bufferSize; + _resetBytesLeft -= _bufferSize; // more pure-reset buffers to come (stays a multiple of 4) + } + return; // nothing to encode, keep lines at reset level + } + + uint32_t bytesToEncode = 0; + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_PARLIO_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + maxCh = ch + 1; + uint32_t channelBytesLeft = _channels[ch].srcLen - _channels[ch].srcPos; + if (channelBytesLeft > bytesToEncode) bytesToEncode = channelBytesLeft; + } + } + + uint32_t translatedbytes = bytesToEncode * WLEDPB_PARLIO_DMABYTES; + translatedbytes = translatedbytes > _bufferSize ? _bufferSize : translatedbytes; + encode4Step(_dmaBuffer[bufIdx], translatedbytes, maxCh); + _txLen[bufIdx] = translatedbytes; + + if (translatedbytes < _bufferSize) { + // Data ran out before the buffer was full (i.e. we are done), compute the minimum reset period we must send + size_t resetBytes = _calcResetBytes(); + + size_t newLen = translatedbytes + resetBytes; + if (newLen > _bufferSize) { + _resetBytesLeft = newLen - _bufferSize; // reset pulse does not fit into this buffer frame, send another one (see above) + _txLen[bufIdx] = _bufferSize; + } + else { + _txLen[bufIdx] = newLen; // send the rest (reset level) as a reset + _endOfFrame[bufIdx] = true; + _resetBytesLeft = WLEDPB_PARLIO_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers + } + } +} + +bool ParlioBusContext::startTransmit() { + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; + + // Only start transmission if ALL active channels have populated data + if (_stagedMask != _channelMask) return true; + _stagedMask = 0; // Reset for next frame + + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_PARLIO_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + _resetBytesLeft = 0; + + if (!_dmaAllocated) { + if (!_allocDmaBuffers()) return false; + } + + // (re)create the PARLIO unit: lazily on first transmit, or when channels changed + if (!_txUnit || _unitStale) { + hwDeinit(); + if (!hwInit()) return false; + } + +#if !WLEDPB_PARLIO_SEAMLESS_DMA + _state = DriverState::Sending; // fallback: EOF callbacks do not gate on the state +#endif + // seamless: _state is set to Sending at the end of hwStartTransfer(), after the ring + // is fully built and the engine is running - until then EOF callbacks stay disabled + + if (!hwStartTransfer()) { + _state = DriverState::Idle; + return false; + } + + return true; +} + +bool ParlioBusContext::hwStartTransfer() { + if (!_unitEnabled) { + if (parlio_tx_unit_enable(_txUnit) != ESP_OK) return false; + _unitEnabled = true; + } + +#if WLEDPB_PARLIO_SEAMLESS_DMA + // one hardware transaction for the whole frame (data + reset period) + const uint32_t totalBytes = (uint32_t)_maxDataLen * WLEDPB_PARLIO_DMABYTES + _calcResetBytes(); + const uint32_t firstChunk = totalBytes > PARLIO_LL_TX_MAX_BYTES_PER_FRAME ? PARLIO_LL_TX_MAX_BYTES_PER_FRAME : totalBytes; + _chunkBytesLeft = totalBytes - firstChunk; + + // reset the ring: DMA stopped, all descriptors back to CPU, refill from scratch. + // note: _state is still Idle here, so a stale GDMA/PARLIO EOF interrupt pending from + // the previous frame is ignored by the callbacks while we rebuild (see gdmaEofCallback) + gdma_stop(_dmaChan); + _fillHead = 0; + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _desc[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU; + fillBuffer(i); + _armDescriptor(i); + _fillHead = (uint8_t)((i + 1) % WLEDPB_PARLIO_DMA_BUFFER_COUNT); + if (_endOfFrame[i]) break; // frame fits into the first buffers, rest of the ring stays disarmed + } + + // mirror the IDF driver's transaction start sequence, minus the descriptor remount: + // the ring stays linked, so the DMA streams across buffers without any CPU involvement + parlio_ll_tx_set_idle_data_value(PARLIO_HW, _invertMask); // park lines at the reset level when idle + parlio_ll_tx_reset_fifo(PARLIO_HW); + parlio_ll_tx_reset_clock(PARLIO_HW); + parlio_ll_tx_set_trans_bit_len(PARLIO_HW, firstChunk * 8); + gdma_start(_dmaChan, (intptr_t)&_desc[0]); + // wait until the first DMA data reached the TX FIFO (same handshake as the IDF driver) + uint32_t wait = 1000000; + while (!parlio_ll_tx_is_ready(PARLIO_HW) && --wait) {} + if (!wait) return false; + parlio_ll_tx_start(PARLIO_HW, true); + parlio_ll_tx_enable_clock(PARLIO_HW, true); + // open the refill window only now: the first EOF can raise one buffer-duration from + // here at the earliest, and the ring is fully built + _state = DriverState::Sending; + return true; +#else + // Single-buffer fallback: fill buffer 0 with the whole frame (data + reset period, + // the buffer is sized to hold both) and send it as one transaction. The IDF driver + // builds the DMA descriptor chain for it and raises transmit-done once, at the end. + fillBuffer(0); + return _queueBuffer(0); +#endif +} + +//============================================================================== +// ISR / transmit-done callback +//============================================================================== + +IRAM_ATTR bool ParlioBusContext::dmaCallback(parlio_tx_unit_handle_t tx_unit, + const parlio_tx_done_event_data_t* edata, + void* user_ctx) { + (void)tx_unit; + (void)edata; + ParlioBusContext* ctx = (ParlioBusContext*)user_ctx; +#if WLEDPB_PARLIO_SEAMLESS_DMA + ctx->_processChunkEof(); +#else + ctx->_state = DriverState::Idle; // single transaction per frame: transmit-done = frame complete +#endif + return false; +} + +// ============================================ +// ParlioBus implementation +// ============================================ + +ParlioBus::ParlioBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum, uint8_t ledType, size_t numPixels) + : _pin(pin) + , _timing(timing) + , _inverted(false) + , _initialized(false) + , _busNum(busNum) + , _channelIdx(-1) + , _ctx(nullptr) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; + _numPixels = numPixels; // stored so begin() can report srcBytes to the shared ParlioBusContext for DMA sizing +} + +ParlioBus::~ParlioBus() { + end(); +} + +bool ParlioBus::begin() { + if (_initialized) return true; + + _ctx = ParlioBusContext::get(_busNum); + if (!_ctx) return false; + + if (!_ctx->init(_timing)) { + ParlioBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + // pass our encoded byte count so the context can size DMA buffers for the largest bus + const size_t srcBytes = (size_t)_numPixels * _encoder.getPixelBytes(); + _channelIdx = _ctx->registerChannel(_pin, this, srcBytes, _inverted); + if (_channelIdx < 0) { + ParlioBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _initialized = true; + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + return true; +} + +// invert output signal, must be set before begin() +void ParlioBus::setInverted(bool inv) { + _inverted = inv; +} + +void ParlioBus::end() { + if (!_initialized) return; + + if (_ctx) { + // Wait for any active transmission to complete before cleanup + while (!_ctx->isIdle()) vTaskDelay(1); + _ctx->unregisterChannel(_channelIdx); + ParlioBusContext::release(_busNum); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool ParlioBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) { + const size_t pixelBytes = padPixelBytesForSuffix((size_t)numPixels * numChannels, _ledType); + size_t needed = _prefixLen + pixelBytes + _suffixLen; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + if (_encodeBuffer) { heap_caps_free(_encodeBuffer); _encodeBuffer = nullptr; } + if (needed == 0) return true; + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); + if (!_encodeBuffer) { _encodeBufferSize = 0; return false; } + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + _prefixLen; + if (_suffixLen == sizeof(SM16825_SUFFIX) && _ledType == TYPE_SM16825) + memcpy(_pixelData + pixelBytes, SM16825_SUFFIX, sizeof(SM16825_SUFFIX)); + return true; +} + +bool ParlioBus::show() { + if (!_initialized || !_ctx || !_encodeBuffer || _numPixels == 0) return false; + + // Wait for previous transmission to complete + while (!_ctx->isIdle()) { + vTaskDelay(1); + } + + // Send already-encoded buffer directly + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodeBufferSize); + return _ctx->startTransmit(); +} + +bool ParlioBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void ParlioBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +} // namespace WLEDpixelBus +#endif // WLEDPB_PARLIO_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.h new file mode 100644 index 0000000000..8016b83a3e --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.h @@ -0,0 +1,311 @@ +// WLEDpixelBus_PARLIO.h +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel PARLIO output driver implementation + +written by Damian Schneider @dedehai 2026 + +Note: PARLIO driver was generated with heavy help of an AI with lots of refinement and testing (works but needs a thorough review) + +supports ESP32-C6, ESP32-H2 and ESP32-C5 via the PARLIO TX peripheral (8 parallel outputs). +ESP32-P4 has a 16-bit wide PARLIO unit, this driver uses 8 bits of it (could be extended). +Requires IDF >= 5.3 (driver/parlio_tx.h). Not available on ESP32, S2, S3, C3, C2. + +Architecture mirrors the I2S driver (WLEDpixelBus_I2S): data is streamed from a circular +GDMA descriptor ring that we own, refilled on the fly from per-descriptor EOF interrupts. + +Why not the IDF driver's transaction queue (parlio_tx_unit_transmit): +each queued transaction is ended by the driver's EOF ISR with a full pipeline restart - +output clock off, TX engine off, FIFO reset, descriptor remount, GDMA restart, busy-wait. +During that whole sequence the outputs sit at the idle level (LOW); under load this takes +up to ~100us, which is long enough to latch addressable LEDs mid-frame. + +Seamless streaming model (WLEDPB_PARLIO_SEAMLESS_DMA, used on C6/H2/C5): +- One hardware transaction per frame. The frame length (data + reset period) is programmed + into the PARLIO TX byte counter (tx_bytelen); the hardware clocks out exactly that many + bytes and raises TX EOF wire-accurately at the end of the frame. No descriptor remount, + no FIFO reset and no TX engine stop ever happens mid-frame -> no output gaps. +- The TX unit's GDMA channel (borrowed via WLEDPB_PARLIO_TX_DMA_CHAN) streams a circular + descriptor ring (owner check + auto write-back, applied by the driver). Every + descriptor has suc_eof set, which is safe here because TX EOF on these chips is derived + from the byte counter, not from DMA EOF. Each completed descriptor raises a GDMA EOF + interrupt that refills and re-arms it. +- The 16-bit tx_bytelen register limits one hardware transaction to 65535 bytes + (~680 RGB pixels). Larger frames are chained at TX EOF by simply reprogramming the byte + counter and re-starting TX: a few register writes (~1-2us at idle level, no FIFO reset, + no GDMA restart), far below the LED reset/latch threshold. +- End of frame = final TX EOF. The IDF driver's EOF ISR (which we still use) stops the TX + engine and clock; the lines park at idle_value = _invertMask, i.e. the correct reset + level (HIGH for inverted buses - the old transaction model parked them LOW). + +The IDF driver is still used for unit creation (clock setup, GPIO matrix routing, interrupt +install) and for its EOF ISR, which invokes our on_trans_done callback. We never call +parlio_tx_unit_transmit(). + +Fallback (WLEDPB_PARLIO_SEAMLESS_DMA == 0, e.g. ESP32-P4 with SOC_PARLIO_TX_SIZE_BY_DMA +or non-AHB GDMA): the whole frame (data + reset period) is encoded into a single buffer +up front and sent with one parlio_tx_unit_transmit() call per frame. The IDF driver +builds the DMA descriptor chain internally, so the transfer is gap-free and the +transmit-done callback fires exactly once, at the end of the frame. + +Data is output in 4-step cadence meaning each LED bit is encoded into 4 PARLIO clocks. +'0' is 0b1000 and '1' is 0b1110 (one byte across the 8 data lines per clock). +Encoding is done "on the fly" in the refill path while queued buffers are sent by DMA. +Signal inversion is done in the encoder via a per-channel XOR mask (PARLIO has no hardware +inversion), replicating the GPIO-matrix inversion semantics of the I2S driver: +inverted channels idle HIGH and their reset period is HIGH. + +Each bus can have individual configuration of color channels but all must share the same timing. + +NOTE: pins are fixed at PARLIO unit creation (unlike I2S where pins are routed lazily via the +GPIO matrix). The unit is therefore created lazily on first startTransmit() and recreated if +channels change afterwards. + +NOTE: the seamless path is flash-safe WITHOUT any special sdkconfig options, so it works +on precompiled frameworks (Arduino/Tasmota) where sdkconfig cannot be changed: +- the IDF GDMA TX ISR is linked into IRAM unconditionally (linkerscript, all configs) +- the IDF PARLIO TX ISR is IRAM_ATTR unconditionally +- everything our callbacks call is IRAM_ATTR or always-inline LL code: the DMA outlink + restart uses a direct gdma_ll_tx_restart() register write (indexed via the channel ID + obtained from gdma_get_channel_id() at init), not gdma_append() which may live in flash +- pixel data and descriptors live in internal SRAM +If a flash write (e.g. saving settings to NVS) overlaps a frame WITHOUT +CONFIG_GDMA_ISR_IRAM_SAFE=y / CONFIG_PARLIO_ISR_IRAM_SAFE=y, the EOF interrupts are +simply deferred until the flash cache is re-enabled: refills/chunk re-arms run late but +nothing crashes. With those options set (pure-IDF projects) the frame is serviced even +during the flash write. A very long cache-off window can still starve the refill and +latch the LEDs mid-frame - unavoidable on any driver. + +NOTE: the PARLIO register struct instance is `PARL_IO` on C6 and H2 (soc/parl_io_struct.h). +If a future target names it differently, adjust the PARLIO_HW macro below. + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#ifdef ARDUINO_ARCH_ESP32 +#include +#include "soc/soc_caps.h" +#endif + +#if defined(SOC_PARLIO_SUPPORTED) + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + #define WLEDPB_PARLIO_SUPPORT 1 + #endif +#endif + +#ifdef WLEDPB_PARLIO_SUPPORT + +#include "driver/parlio_tx.h" +#include "driver/gpio.h" + +// Seamless single-transaction streaming is possible when the chip has AHB GDMA and the +// PARLIO TX end-of-frame is derived from the byte counter (not from DMA EOF). Limited to +// IDF 5.x: the driver's internals changed on 6.x (struct layout, driver-owned GDMA +// callbacks), the fallback below is used there until verified. +// ESP32-P4 is excluded: on current IDF versions it uses DMA EOF for frame sizing and the +// PARLIO LL helpers require an RCC atomic environment, so keep it on the transaction queue. +#if defined(SOC_AHB_GDMA_SUPPORTED) && SOC_AHB_GDMA_SUPPORTED && !(defined(SOC_PARLIO_TX_SIZE_BY_DMA) && SOC_PARLIO_TX_SIZE_BY_DMA) && !defined(CONFIG_IDF_TARGET_ESP32P4) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + #define WLEDPB_PARLIO_SEAMLESS_DMA 1 +#else + #define WLEDPB_PARLIO_SEAMLESS_DMA 0 // fall back to "single buffer transfer" mode (uses more RAM) +#endif + +#if WLEDPB_PARLIO_SEAMLESS_DMA + #include "hal/dma_types.h" // dma_descriptor_t, DMA_DESCRIPTOR_BUFFER_* + #include "esp_private/gdma.h" // gdma_start/stop/append, gdma_register_tx_event_callbacks + +// Mirror of the head of the IDF driver's private struct parlio_tx_unit_t (parlio_tx.c, +// parlio_priv.h since v5.5). Lets us reach the TX unit's GDMA channel: that channel is +// already connected to the PARLIO trigger by the driver and has the owner-check + +// auto-write-back strategy applied - and the driver never registers GDMA callbacks on +// it (v5.3-v5.5), so we can use it for our descriptor ring instead of allocating a +// second channel. Note: GDMA allows only ONE channel connected to a peripheral trigger; +// a second gdma_connect() to PARLIO fails with ESP_ERR_INVALID_STATE (and the driver's +// own connect failing instead would break parlio_del_tx_unit's cleanup path). +// Layout verified identical for IDF v5.3, v5.4, v5.5. Only fields up to dma_chan must +// match; if a future IDF changes this, the handle reads wrong and GDMA calls fail +// safely (no output) - update the mirror then. +typedef struct { + int unit_id; // parlio_unit_t.unit_id + int dir; // parlio_unit_t.dir (parlio_dir_t) + void* group; // parlio_unit_t.group + size_t data_width; + void* intr; // intr_handle_t + void* pm_lock; // esp_pm_lock_handle_t + gdma_channel_handle_t dma_chan; +} WledpbParlioTxUnitHead; +#define WLEDPB_PARLIO_TX_DMA_CHAN(unit) (((const WledpbParlioTxUnitHead*)(unit))->dma_chan) +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// PARLIO Parallel Bus - ESP32-C6, ESP32-H2, ESP32-C5 (8 lines), ESP32-P4 (subset) +//============================================================================== + +// only one PARLIO TX unit per chip +#define WLEDPB_PARLIO_BUS_COUNT 1 + +// DMA buffer count. In seamless mode each buffer is one descriptor in the ring; refill +// happens from the GDMA EOF interrupt (LOWMED priority), so 3-4 buffers of a few KB give +// ample refill slack (one buffer duration is ~1ms per 4KB at 3.2MHz output clock). +#ifndef WLEDPB_PARLIO_DMA_BUFFER_COUNT + #define WLEDPB_PARLIO_DMA_BUFFER_COUNT 3 +#endif + +// PARLIO TX on C6/H2/C5 supports up to 8 data lines (SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH) +#define WLEDPB_PARLIO_MAX_CHANNELS 8 +#define WLEDPB_PARLIO_DMABYTES 32 // 32 bytes per pixel byte (4 clocks per bit, 1 byte per clock) +#define WLEDPB_PARLIO_XFER_DONE_FLAG 3 // flag to indicate end of transfer, must NOT be a multiple of 4 + +class ParlioBus; + +/** + * PARLIO bus context - manages the shared PARLIO TX unit for parallel output + * Seamless mode: one transaction per frame over our own GDMA descriptor ring. + * Fallback mode: one parlio_tx_unit_transmit() per frame from a single full-frame buffer. + */ +class ParlioBusContext { +public: + static ParlioBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); + + bool init(const LedTiming& timing); + void deinit(); + + // Channel management + int8_t registerChannel(int8_t pin, ParlioBus* bus, size_t srcBytes, bool inverted = false); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + // Transmission + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } + + // Data access for channels + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + ParlioBusContext(uint8_t busNum); + ~ParlioBusContext(); + + void IRAM_ATTR fillBuffer(uint8_t bufIdx); + bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); // 4-step cadence encoding, applies inversion via XOR + uint32_t IRAM_ATTR _calcResetBytes() const; // bytes of reset period appended at the end of a frame (called from ISR context) + + static IRAM_ATTR bool dmaCallback(parlio_tx_unit_handle_t tx_unit, const parlio_tx_done_event_data_t* edata, void* user_ctx); + +#if WLEDPB_PARLIO_SEAMLESS_DMA + bool _initDmaRing(); // descriptor ring (once) + borrow the unit's GDMA channel + refill callback + void IRAM_ATTR _armDescriptor(uint8_t idx); // hand a filled buffer's descriptor to the DMA + void IRAM_ATTR _processChunkEof(); // PARLIO TX EOF: chain next byte-count chunk or finish frame + static IRAM_ATTR bool gdmaEofCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* edata, void* user_ctx); +#else + bool IRAM_ATTR _queueBuffer(uint8_t bufIdx); // queue the (single) full-frame buffer as one PARLIO transaction +#endif + + // Hardware abstraction + bool hwInit(); // creates the PARLIO TX unit with the currently registered pins + void hwDeinit(); + bool hwStartTransfer(); // enables the unit and starts the frame + void IRAM_ATTR hwStopTransfer(); + + parlio_tx_unit_handle_t _txUnit; + bool _unitEnabled; + bool _unitStale; // channels changed after unit creation, recreate on next transmit + + volatile DriverState _state; + bool _initialized; + + // DMA buffers + uint8_t* _dmaBuffer[WLEDPB_PARLIO_DMA_BUFFER_COUNT]; + volatile size_t _txLen[WLEDPB_PARLIO_DMA_BUFFER_COUNT]; // bytes per buffer + volatile bool _endOfFrame[WLEDPB_PARLIO_DMA_BUFFER_COUNT]; // last buffer of the frame + size_t _bufferSize; // actual allocated DMA buffer size (per buffer) + size_t _maxSrcBytes; // max source (encoded pixel) bytes across all registered channels; drives DMA sizing + bool _dmaAllocated; + volatile uint16_t _resetBytesLeft; + +#if WLEDPB_PARLIO_SEAMLESS_DMA + // GDMA channel borrowed from the PARLIO unit (owned and deleted by the IDF driver) + // + our circular descriptor ring + gdma_channel_handle_t _dmaChan; + int8_t _dmaChanIdx; // hardware channel index of _dmaChan for direct LL register access (-1 = unknown) + dma_descriptor_t* _desc; + volatile uint8_t _fillHead; // ring index of the next descriptor to refill + volatile uint32_t _chunkBytesLeft; // frame bytes not yet covered by a programmed tx_bytelen chunk +#endif + + // Timing + LedTiming _timing; + uint32_t _outClockHz; // PARLIO output clock = 4 clocks per LED bit + + // Channel data + struct ChannelData { + ParlioBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_PARLIO_MAX_CHANNELS]; + uint8_t _channelCount; + uint16_t _channelMask; + uint16_t _stagedMask; + uint8_t _invertMask; // bit per channel: XOR mask for inverted outputs + size_t _maxDataLen; + + // Singleton instances + static ParlioBusContext* _instances[WLEDPB_PARLIO_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_PARLIO_BUS_COUNT]; +}; + +/** + * PARLIO parallel output bus + */ +class ParlioBus : public PixelBus { +public: + /** + * Create PARLIO bus + * @param pin GPIO pin + * @param timing LED timing + * @param colorOrder Color order + * @param numChannels Bytes per pixel + * @param busNum PARLIO bus number (only bus 0 exists, one PARLIO TX unit per chip) + * @param ledType LED chip type constant + * @param numPixels Number of pixels; stored for DMA buffer sizing in ParlioBusContext + */ + ParlioBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum = 0, uint8_t ledType = 0, size_t numPixels = 0); + ~ParlioBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "PARLIO"; } +#endif + + void setInverted(bool inv) override; + void setColorOrder(uint8_t co); + + // Override to use DMA-capable allocator for PARLIO + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; + +private: + int8_t _pin; + LedTiming _timing; + bool _inverted; + bool _initialized; + uint8_t _busNum; + int8_t _channelIdx; + ParlioBusContext* _ctx; +}; + +} // namespace WLEDpixelBus +#endif // WLEDPB_PARLIO_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp new file mode 100644 index 0000000000..f23c9e6803 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -0,0 +1,709 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel SPI output driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32 C3, other chips may work but are untested +uses 4 parallel outputs and double DMA buffering on SPI2 +Data is output in 4-step cadence meaning each LED bit is encoded into 4 bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +2k per DMA buffer works well, enough for 42 RGB LEDs or roughly 1.2ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT +#include "WLEDpixelBus_ParallelSpi.h" + +#undef FLAG_ATTR +#define FLAG_ATTR(TYPE) +#include "hal/spi_ll.h" +#include "driver/periph_ctrl.h" +#include "esp_rom_gpio.h" +#include "esp_private/gdma.h" +namespace WLEDpixelBus { + +//============================================= +// SPI Parallel Bus Implementation (ESP32-C3) +//============================================ + + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) +// low level functions not available in IDF V4 +static inline void IRAM_ATTR spi_ll_apply_config(spi_dev_t *hw) { + hw->cmd.update = 1; + while (hw->cmd.update); //waiting config applied +} + +static inline void IRAM_ATTR spi_ll_user_start(spi_dev_t *hw) { + hw->cmd.usr = 1; +} +#define GPIO_HW &GPIO +#define gpio_ll_set_func_sel(pin, sig) GPIO_HW->func_out_sel_cfg[pin].func_sel = sig +#else +#include "hal/gpio_ll.h" // need low level GPIO register access to disconnect pin inside the ISR +#define GPIO_HW GPIO_LL_GET_HW(0) +#define gpio_ll_set_func_sel(pin, sig) GPIO_HW->func_out_sel_cfg[pin].out_sel = SIG_GPIO_OUT_IDX; +#endif + + + +// Pin assignments for SPI2 quad mode on C3 +// SPI2 signals: FSPID (MOSI/D0), FSPIQ (MISO/D1), FSPIWP (D2), FSPIHD (D3) +static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_OUT_IDX, FSPIHD_OUT_IDX }; + +// Encoding patterns for SPI quad mode (4-step cadence, LSB first) +// Each lane is one bit position in a nibble, one byte = two clock cycles, 2 bytes = one 4-step bit +static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high (0000 0000 0000 0001 in binary, output LSB first) +static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high (0000 0001 0001 0001 in binary, output LSB first) + +// Reset pulse: ~300us at ~2.6MHz (4-step cadence). +// 300us * 2.6MHz = 780 bits. We use 1024 bits (~400us) to be safe for all LED types. TODO: is this really safe for all led types? +static constexpr uint32_t SPI_RESET_BITS = 1024; + +// Maximum bits per SPI user transfer (18-bit length register on C3 SPI_MS_DLEN_REG) +static constexpr uint32_t SPI_MAX_BITS = 262143; // note: 4x parallel, 4 steps -> 16bits per source bit, 2kbyte or 680 RGB LEDs max (tested, confirmed) +// Chained segment size in whole source bytes (128 SPI bits each), just under SPI_MAX_BITS. +// Longer frames are sent as multiple back-to-back transfers, chained in the trans_done ISR. +// The DMA stream is not affected by segment boundaries - only the SPI bit length is. +static constexpr uint32_t SPI_SEG_BITS = 2047 * 128; // 262016 bits = 2047 source bytes +// Maximum chained segments per frame (2 segments ~= 1364 RGB LEDs) +#define WLEDPB_SPI_MAX_SEGMENTS 2 + +SpiBusContext* SpiBusContext::_instance = nullptr; +uint8_t SpiBusContext::_refCount = 0; + +SpiBusContext* SpiBusContext::get() { + if (_instance == nullptr) { + _instance = new SpiBusContext(); + } + _refCount++; + return _instance; +} + +void SpiBusContext::release() { + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } +} + +SpiBusContext::SpiBusContext() + : _state(SpiState::Idle) + , _initialized(false) + , _activeBuffer(0) + , _gdmaChan(nullptr) + , _dmaChan(-1) + , _spiIsrHandle(nullptr) + , _hw(&GPSPI2) + , _channelCount(0) + , _framePos(0) + , _numBytes(0) + , _bitsLeft(0) + , _lastTransmitMs(0) + , _stagedMask(0) + , _channelMask(0) +{ + _isrMux = portMUX_INITIALIZER_UNLOCKED; + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaBuffer[i] = nullptr; + } + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, nullptr, 0, -1, false, false}; + } +} + +SpiBusContext::~SpiBusContext() { + deinit(); +} + +bool SpiBusContext::isIdle() const { + if (_state == SpiState::Idle) return true; + + // If we're in an error state, clean up the SPI state, then we are ready transmit again + if (_state == SpiState::Error) { + forceIdle(); + return true; + } + + if (_hw->cmd.usr == 0) { + for (int i = 0; i < 200; i++) { + if (_hw->cmd.usr != 0) return false; // chained segment restarted, all good + if (_state == SpiState::Idle) return true; // trans_done ISR completed normally + } + // SPI genuinely stopped without completing: trans_done ISR was lost. + forceIdle(); + return true; + } + + return false; +} + +// Recovery path for error conditions, cleanly stops DMA, SPI, and disconnects pins to prevent glitches +void SpiBusContext::forceIdle() const { + portENTER_CRITICAL(&_isrMux); // make sure no ISR will disturb the sequence + // disconnect pins from SPI and set low + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0) { + esp_rom_gpio_connect_out_signal(_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); + gpio_set_level((gpio_num_t)_channels[i].pin, 0); + } + } + if (_hw) { + _hw->cmd.usr = 0; + _hw->dma_int_ena.val = 0; // disable all SPI interrupts + _hw->dma_int_clr.val = 0xFFFFFFFF; + } + + // Stop DMA + gdma_dev_t* dma = &GDMA; + dma->intr[_dmaChan].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, _dmaChan); + + // Reset FIFOs + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + + _state = SpiState::Idle; + _stagedMask = 0; + portEXIT_CRITICAL(&_isrMux); +} + +//note: using O2 optimization has little to no effect on FPS +void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { + uint8_t* dst = _dmaBuffer[bufIdx]; + uint32_t* dst32 = reinterpret_cast(dst); + for (size_t i = 0; i < (DEFAULT_DMA_BUFFER_SIZE / 4); i++) { + dst32[i] = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned. Note: memset is not IRAM safe and may crash + } + + size_t maxSrcThisChunk = DEFAULT_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte + size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; + size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; + + if (srcThisChunk == 0) { + // All pixel data has been encoded. Transition to SendingLast state. + // The next buffer fill will be zeroed (reset pulse). + if (_state == SpiState::Sending) { + _state = SpiState::SendingLast; + } + return; + } + + for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { + if (!_channels[lane].active || !_channels[lane].srcData) continue; + + size_t srcLen = _channels[lane].srcLen; + if (_framePos >= srcLen) continue; // Past the end of this lane's data, leave buffer 0 (low/no pulse) + + size_t validBytes = srcLen - _framePos; + if (validBytes > srcThisChunk) validBytes = srcThisChunk; + + const uint16_t zerobit = SPI_ZERO_BIT << lane; + const uint16_t onebit = SPI_ONE_BIT << lane; + const uint8_t* src = _channels[lane].srcData; + uint16_t* pOut = reinterpret_cast(dst); + + for (size_t i = 0; i < validBytes; i++) { + uint8_t v = src[_framePos + i]; + *pOut++ |= (v & 0x80) ? onebit : zerobit; + *pOut++ |= (v & 0x40) ? onebit : zerobit; + *pOut++ |= (v & 0x20) ? onebit : zerobit; + *pOut++ |= (v & 0x10) ? onebit : zerobit; + *pOut++ |= (v & 0x08) ? onebit : zerobit; + *pOut++ |= (v & 0x04) ? onebit : zerobit; + *pOut++ |= (v & 0x02) ? onebit : zerobit; + *pOut++ |= (v & 0x01) ? onebit : zerobit; + } + } + _framePos += srcThisChunk; +} + +// SPI ISR: handles trans_done (normal completion) and outfifo_empty_err +void IRAM_ATTR SpiBusContext::spiISR(void* arg) { +// (FIFO underrun recovery). Both paths are synchronized with gdmaISR via _isrMux. + SpiBusContext* ctx = (SpiBusContext*)arg; + uint32_t status = ctx->_hw->dma_int_st.val; + ctx->_hw->dma_int_clr.val = status; // Clear all flags immediately + if (status & SPI_TRANS_DONE_INT_ST) { + if (ctx->_bitsLeft > 0) { + // Chain the next segment. The circular DMA never stopped, so the TX FIFO already + // holds the next bits: only re-arm the bit length and restart. Do NOT touch the + // FIFO or DMA here - that would break bit-stream continuity and stall the restart. + uint32_t bits = (ctx->_bitsLeft > (int32_t)SPI_SEG_BITS) ? SPI_SEG_BITS : (uint32_t)ctx->_bitsLeft; + ctx->_bitsLeft -= (int32_t)bits; + spi_ll_set_mosi_bitlen(ctx->_hw, bits); + spi_ll_apply_config(ctx->_hw); // fast handshake, sub-microsecond + spi_ll_user_start(ctx->_hw); // clock resumes ~1-2us after the last bit + // state stays Sending; encode/DMA are unaffected by segment boundaries + } else { + ctx->_state = SpiState::Idle; // last segment finished (includes the reset tail) + } + } + else if (status & SPI_DMA_OUTFIFO_EMPTY_ERR_INT_ST) { + if (ctx->_state == SpiState::Idle) return; // state machine finished cleanly, ignore + // SPI FIFO starved (ISR latency too high). The frame is lost, abort and recover immediately + portENTER_CRITICAL_ISR(&ctx->_isrMux); // note: on C3 this is not really needed as GDMA interrupt has the same priority, keep it just in case + // disconnect pins from SPI to prevent garbage output (usr=0 outputs a fast clock) + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { + gpio_ll_set_func_sel(ctx->_channels[i].pin, SIG_GPIO_OUT_IDX); // disconnect from SPI using direct register write (ISR safe) + // set the pin to static level immediately, note: if implementing this for other ESPs: need to also set the high register for pins >31 + if (ctx->_channels[i].inverted) + GPIO_HW->out_w1ts.out_w1ts = (1 << ctx->_channels[i].pin); // set ouput high (set) to avoid glitches + else + GPIO_HW->out_w1tc.out_w1tc = (1 << ctx->_channels[i].pin); // set ouput low (clear) to avoid glitches + } + } + ctx->_hw->cmd.usr = 0; // stop SPI user transfer + ctx->_hw->dma_int_ena.val = 0; // startTransmit() re-arms these + gdma_dev_t* dma = &GDMA; + dma->intr[ctx->_dmaChan].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, ctx->_dmaChan); + spi_ll_dma_tx_fifo_reset(ctx->_hw); + spi_ll_outfifo_empty_clr(ctx->_hw); + ctx->_stagedMask = 0; + ctx->_state = SpiState::Idle; // recovered: next show() can send immediately + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + } +} + +bool IRAM_ATTR SpiBusContext::gdmaISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data) { + SpiBusContext* ctx = (SpiBusContext*)user_data; + gdma_dev_t* dma = &GDMA; + portENTER_CRITICAL_ISR(&ctx->_isrMux); // make sure we are not disturbed filling the buffer to prevent underruns + dma->intr[ctx->_dmaChan].clr.out_eof = 1; // clear interrupt immediately, harmless if driver already cleared it + + // If we're idle or in error, ignore spurious interrupts + if (ctx->_state == SpiState::Idle || ctx->_state == SpiState::Error) { + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return false; + } + + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; + ctx->encodeSpiChunk(completedBuf); // fill the completed buffer with next chunk of data (or zeroes for reset) + ctx->_dmaDesc[completedBuf].eof = 1; // Give ownership of the descriptor back to DMA so it can keep feeding SPI TODO: this may be unnecessary + ctx->_dmaDesc[completedBuf].owner = 1; + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return false; // no higher-priority task woken +} + +bool SpiBusContext::init(const LedTiming& timing) { + if (_initialized) return true; + + // Allocate DMA buffers + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, DEFAULT_DMA_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (!_dmaBuffer[i]) { + //Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, DEFAULT_DMA_BUFFER_SIZE); + } + + // Setup DMA descriptors - circular linked list + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaDesc[i].size = DEFAULT_DMA_BUFFER_SIZE; + _dmaDesc[i].length = DEFAULT_DMA_BUFFER_SIZE; + _dmaDesc[i].owner = 1; + _dmaDesc[i].sosf = 0; + _dmaDesc[i].eof = 1; + _dmaDesc[i].buf = _dmaBuffer[i]; + } + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaDesc[i].qe.stqe_next = &_dmaDesc[(i + 1) % WLEDPB_SPI_DMA_DESC_COUNT]; + } + + // Enable peripheral clocks and force-reset (SPI2 + DMA) + // periph_module_enable() uses ref counting and may be a no-op if the + // peripheral was already enabled. Explicit reset ensures clean state. + periph_module_enable(PERIPH_SPI2_MODULE); + periph_module_reset(PERIPH_SPI2_MODULE); + periph_module_enable(PERIPH_GDMA_MODULE); + periph_module_reset(PERIPH_GDMA_MODULE); + + // Configure SPI2 master + spi_ll_master_init(_hw); + spi_ll_master_set_mode(_hw, 0); + spi_ll_set_tx_lsbfirst(_hw, true); + + // skip all SPI phases and jump directly to user mosi phase + _hw->user.usr_command = 0; + _hw->user.usr_addr = 0; + _hw->user.usr_dummy = 0; + _hw->user.usr_miso = 0; + _hw->user.usr_mosi = 1; + + // Clear idle output polarities for D2/D3 + _hw->ctrl.q_pol = 0; + _hw->ctrl.d_pol = 0; + _hw->ctrl.hold_pol = 0; + _hw->ctrl.wp_pol = 0; + + spi_line_mode_t linemode = {}; + linemode.data_lines = 4; // quad mode + spi_ll_master_set_line_mode(_hw, linemode); + + // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config + // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) + // Clock: 4 steps per bit → 4 SPI clock cycles per bit period. + // targetFreq = 4 / (bitPeriod_ns * 1e-9) = 4,000,000,000 / bitPeriod_ns + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t targetFreq = 4000000000UL / bitPeriodNs; + if (targetFreq < 2000000) targetFreq = 2000000; + if (targetFreq > 5000000) targetFreq = 5000000; + + spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); + + // Route SPI clock to a dummy pin (needed for DMA to work) -> seems to work fine without this (maybe an IDF V5 issue?) + //pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + + // spi_ll_set_mosi_bitlen(_hw, 16384); // dummy init value, not required (set properly when transfer starts) + spi_ll_enable_mosi(_hw, true); + + spi_ll_dma_tx_enable(_hw, true); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + // Configure GDMA + gdma_channel_alloc_config_t allocCfg = {}; + allocCfg.direction = GDMA_CHANNEL_DIRECTION_TX; + esp_err_t err = gdma_new_ahb_channel(&allocCfg, &_gdmaChan); // get a DMA channel + if (err != ESP_OK) { + deinit(); + return false; // no free TX channel -> clean failure instead of silent corruption + } + gdma_get_channel_id(_gdmaChan, &_dmaChan); + + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, _dmaChan); + + err = gdma_connect(_gdmaChan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_SPI, 2)); + if (err != ESP_OK) { deinit(); return false; } + + gdma_ll_tx_set_desc_addr(dma, _dmaChan, (uint32_t)&_dmaDesc[0]); + + gdma_tx_event_callbacks_t cbs = {}; + cbs.on_trans_eof = gdmaISR; // signature changes, see below + err = gdma_register_tx_event_callbacks(_gdmaChan, &cbs, this); + if (err != ESP_OK) { + //Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + deinit(); + return false; + } + gdma_ll_tx_reset_channel(dma, _dmaChan); + gdma_ll_tx_set_desc_addr(dma, _dmaChan, (uint32_t)&_dmaDesc[0]); + // gdma_ll_tx_start(dma, _dmaChan); // note: do not start yet, done in startTransmit() + + // Install SPI ISR for trans_done and outfifo_empty_err recovery + _hw->dma_int_clr.val = 0xFFFFFFFF; + _hw->dma_int_ena.trans_done = 1; + _hw->dma_int_ena.outfifo_empty_err = 1; + // _hw->dma_int_ena.val = 0xFFFFFFFF; // REMOVED: Do not enable all interrupts, they trigger false aborts! + err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM, spiISR, this, &_spiIsrHandle); + if (err != ESP_OK) { + //Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); + deinit(); + return false; + } + + _initialized = true; + return true; +} + +void SpiBusContext::deinit() { + // Ensure we're in a clean state before freeing resources + forceIdle(); + + // Stop SPI and DMA before freeing resources + if (_hw) { + _hw->cmd.usr = 0; // Stop SPI transfer + } + + gdma_dev_t* dma = &GDMA; + dma->intr[_dmaChan].ena.out_eof = 0; // Disable interrupt + gdma_ll_tx_reset_channel(dma, _dmaChan); + + if (_gdmaChan) { + gdma_del_channel(_gdmaChan); // also tears down the callback/interrupt it installed + _gdmaChan = nullptr; + _dmaChan = -1; +} + + if (_spiIsrHandle) { + esp_intr_free(_spiIsrHandle); + _spiIsrHandle = nullptr; + } + + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + } + + periph_module_disable(PERIPH_SPI2_MODULE); + periph_module_disable(PERIPH_GDMA_MODULE); + _initialized = false; +} + +int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus, bool inverted) { + int8_t idx = -1; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channels[idx].inverted = inverted; + _channelCount++; + _channelMask |= (1 << idx); + + // Route SPI data signal to GPIO + pinMode(pin, OUTPUT); + esp_rom_gpio_connect_out_signal(pin, SPI_SIGNAL_INDICES[idx], inverted, false); + + return idx; +} + +void SpiBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } + + _channels[channelIdx] = {nullptr, nullptr, 0, -1, false, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + // Mark this channel as staged + _stagedMask |= (1 << channelIdx); +} + +bool SpiBusContext::startTransmit() { + if (_state != SpiState::Idle) return false; // must be idle to start a new frame, skip frame + if (_channelCount == 0) return false; + + // Only start transmission if ALL active channels have populated data + if (_stagedMask != _channelMask) return false; // not all channels staged, something went wrong, skip frame + _stagedMask = 0; // Reset for next frame + + // Calculate actual data length from staged channels + size_t newBytes = 0; + for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcLen > newBytes) { + newBytes = _channels[ch].srcLen; + } + } + _numBytes = newBytes; +// clamp to chain capacity (tail pixels simply keep their previous values) + const size_t maxBytes = 2047 * WLEDPB_SPI_MAX_SEGMENTS; + if (_numBytes > maxBytes) _numBytes = maxBytes; + + // Total bits: 16 DMA bytes per source byte * 8 bits/byte = 128 bits per source byte + // Plus reset: extra zero bits at the end (in the last segment). + // Frames longer than one transfer are chained in the trans_done ISR. + uint32_t dataBits = _numBytes * 16 * 8; + uint32_t totalBits = dataBits + SPI_RESET_BITS; + + uint32_t firstBits = (totalBits > SPI_SEG_BITS) ? SPI_SEG_BITS : totalBits; + _bitsLeft = (int32_t)(totalBits - firstBits); // remaining bits are chained by the ISR + + // Wait for SPI to be idle + uint32_t timeout = 100; + while (_hw->cmd.usr && timeout--) { + delay(1); + } + if (_hw->cmd.usr) { + forceIdle(); // SPI is still busy after timeout. Force it idle. + } + + // init hardware, must not be interrupted, otherwise it breaks for some reason + portENTER_CRITICAL(&_isrMux); + _hw->cmd.usr = 0; + gdma_dev_t* dma = &GDMA; + dma->intr[_dmaChan].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, _dmaChan); + + spi_ll_clear_int_stat(_hw); + _hw->dma_int_ena.trans_done = 1; // re-enable interrupts in case they got disabled due to error + _hw->dma_int_ena.outfifo_empty_err = 1; + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + + spi_ll_set_mosi_bitlen(_hw, firstBits); + + // Re-initialize DMA descriptors and encode initial buffers + _framePos = 0; + _activeBuffer = 0; + _state = SpiState::Sending; + + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + // Restore circular linked list + _dmaDesc[i].qe.stqe_next = &_dmaDesc[(i + 1) % WLEDPB_SPI_DMA_DESC_COUNT]; + _dmaDesc[i].size = DEFAULT_DMA_BUFFER_SIZE; + _dmaDesc[i].length = DEFAULT_DMA_BUFFER_SIZE; + _dmaDesc[i].owner = 1; + _dmaDesc[i].eof = 1; + encodeSpiChunk(i); + } + + // Phase 4: Brief critical section to start DMA and SPI atomically. + gdma_ll_tx_set_desc_addr(dma, _dmaChan, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, _dmaChan); + dma->intr[_dmaChan].clr.out_eof = 1; + dma->intr[_dmaChan].ena.out_eof = 1; + + // Re-attach pins to SPI signals + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0) { + pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], _channels[i].inverted, false); + } + } + spi_ll_dma_tx_enable(_hw, true); + spi_ll_apply_config(_hw); // apply SPI config AFTER starting DMA to make sure they are in sync + + // Short hardware handshake sync: adding a few nops is enough to ensure there is DMA data in the SPI buffer (without this, SPI can immediately quit its duty due to FIFO unterrun) + //asm volatile("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"); // might not be needed but just in case + spi_ll_user_start(_hw); // start SPI user transfer + _lastTransmitMs = millis(); + portEXIT_CRITICAL(&_isrMux); + return true; +} + +/////////////////////////////////// +// ParallelSpiBus implementation // +/////////////////////////////////// +ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +ParallelSpiBus::~ParallelSpiBus() { + end(); +} + +bool ParallelSpiBus::begin() { + if (_initialized) return true; + + _ctx = SpiBusContext::get(); + if (!_ctx) return false; + + if (!_ctx->init(_timing)) { + SpiBusContext::release(); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this, _inverted); + if (_channelIdx < 0) { + //Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); + SpiBusContext::release(); + _ctx = nullptr; + return false; + } + + _initialized = true; + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + return true; +} + +// invert output signal, must be set before begin() +void ParallelSpiBus::setInverted(bool inv) { + _inverted = inv; +} + +void ParallelSpiBus::end() { + if (!_initialized) return; + + if (_ctx) { + uint32_t startWait = millis(); + while (!_ctx->isIdle()) { + if (millis() - startWait > 200) { + break; // Timeout: proceed with cleanup anyway + } + vTaskDelay(1); + } + _ctx->unregisterChannel(_channelIdx); + SpiBusContext::release(); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool ParallelSpiBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) { + const size_t pixelBytes = padPixelBytesForSuffix((size_t)numPixels * numChannels, _ledType); + size_t needed = _prefixLen + pixelBytes + _suffixLen; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + if (_encodeBuffer) { heap_caps_free(_encodeBuffer); _encodeBuffer = nullptr; } + if (needed == 0) return true; + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL); + if (!_encodeBuffer) { _encodeBufferSize = 0; return false; } + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + _prefixLen; + if (_suffixLen == sizeof(SM16825_SUFFIX) && _ledType == TYPE_SM16825) + memcpy(_pixelData + pixelBytes, SM16825_SUFFIX, sizeof(SM16825_SUFFIX)); + return true; +} + +bool ParallelSpiBus::show() { + if (!_initialized || !_ctx || !_encodeBuffer) return false; + + // Wait for previous transmission to complete with timeout (should not happen, BusManager already waits for canShow()) + uint32_t waitStart = millis(); + while (!_ctx->isIdle()) { + if (millis() - waitStart > 200) { + return false; // Timeout: don't start a new frame on a stuck driver + } + vTaskDelay(1); + } + + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodeBufferSize); + return _ctx->startTransmit(); +} + +bool ParallelSpiBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void ParallelSpiBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +} // namespace WLEDpixelBus +#endif // WLEDPB_PARALLEL_SPI_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h new file mode 100644 index 0000000000..652423af95 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -0,0 +1,167 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel SPI output driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32 C3 +uses 4 parallel outputs and double DMA buffering +Data is output in 4-step cadence meaning each LED bit is encoded into 4 bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +2k per DMA buffer works well, enough for 42 RGB LEDs or roughly 1.2ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT +#include "esp_private/gdma.h" // for gdma_channel_handle_t +namespace WLEDpixelBus { + +//============================================================================== +// SPI Parallel Bus - ESP32-C3 (uses SPI2 quad mode + GDMA) +//============================================================================== + +#define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines +#define WLEDPB_SPI_DMA_DESC_COUNT 2 // number of DMA buffers, increase to 3 if there are flickering issues +//#define WLEDPB_SPI_GDMA_CHANNEL 1 // TODO: how to manage the DMA channels to avoid conflicts with other peripherals? for now we just assume channel 1 is free and used exclusively by this driver +#define WLEDPB_SPI_GDMA_INTR_SOURCE ETS_DMA_CH1_INTR_SOURCE // must match dma channel (otherwise it just loops the two DMA descriptors and will eventually time-out) + +// TODO: use more modern GMDA channel reservation to get rid of hard ceded GDMA_CHANNEL, something like this: +/* +// in init(), replacing the hardcoded WLEDPB_SPI_GDMA_CHANNEL +gdma_channel_alloc_config_t allocCfg = {}; +allocCfg.direction = GDMA_CHANNEL_DIRECTION_TX; +esp_err_t err = gdma_new_ahb_channel(&allocCfg, &_gdmaChan); // new member: gdma_channel_handle_t _gdmaChan +if (err != ESP_OK) { + deinit(); + return false; // no free TX channel (RMT/I2S/... took them all) -> clean failure instead of silent corruption +} +gdma_get_channel_id(_gdmaChan, &_dmaChan); // new member: int _dmaChan +*/ + + +class ParallelSpiBus; + +/** + * SPI driver state machine states. + * Error state is used for recovery from FIFO underrun or other hardware errors. + */ +enum class SpiState : uint8_t { + Idle = 0, // Ready for new frame + Sending = 1, // Data phase active, DMA running + SendingLast = 2, // Last data buffer was filled; waiting for current buffer to finish + WaitingReset = 3, // Reset pulse being sent (zero-filled buffers) + Error = 4 // Error recovery needed (FIFO underrun, timeout, etc.) +}; + +/** + * SPI bus context - manages SPI2 quad mode for parallel LED output on C3 + * Uses GDMA with circular linked-list and ISR-driven buffer refill + + * Error handling: + * If outfifo_empty_err fires, transition to Error state. + * Pins are disconnected and driven low to prevent glitches. + * A 100ms timeout in isIdle() will eventually clear Error state. + */ +class SpiBusContext { +public: + static SpiBusContext* get(); + static void release(); + + bool init(const LedTiming& timing); + void deinit(); + + int8_t registerChannel(int8_t pin, ParallelSpiBus* bus, bool inverted = false); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + bool startTransmit(); + bool isIdle() const; // returns true when idle, also handles error timeout recovery + void forceIdle() const; // emergency stop, disconnects pins, resets hardware + + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + SpiBusContext(); + ~SpiBusContext(); + void IRAM_ATTR encodeSpiChunk(uint8_t bufIdx); + static bool IRAM_ATTR gdmaISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); + static void IRAM_ATTR spiISR(void* arg); + // Hardware control + void hwStopTransfer(); + void hwResetFifo(); + // State machine + mutable volatile SpiState _state; + bool _initialized; + volatile uint8_t _activeBuffer; // buffer currently being sent by DMA (like I2S _activeBuffer) + // DMA + uint8_t* _dmaBuffer[WLEDPB_SPI_DMA_DESC_COUNT]; + lldesc_t _dmaDesc[WLEDPB_SPI_DMA_DESC_COUNT]; + gdma_channel_handle_t _gdmaChan; + int _dmaChan; + intr_handle_t _spiIsrHandle; + portMUX_TYPE _isrMux; + spi_dev_t* _hw; // SPI device + // Source data per channel + struct ChannelData { + ParallelSpiBus* bus; + const uint8_t* srcData; + size_t srcLen; + int8_t pin; + bool active; + bool inverted; + }; + ChannelData _channels[WLEDPB_SPI_MAX_CHANNELS]; + uint8_t _channelCount; + volatile size_t _framePos; // current source byte position + volatile size_t _numBytes; // total source bytes to send + volatile int32_t _bitsLeft; // bits still to send in chained segments (0 = last segment) + mutable uint32_t _lastTransmitMs; + // Staging: tracks which channels have provided data for the next frame + mutable uint8_t _stagedMask; + uint8_t _channelMask; + + static SpiBusContext* _instance; + static uint8_t _refCount; +}; + +/** + * SPI parallel output bus (for ESP32-C3) + */ +class ParallelSpiBus : public PixelBus { +public: + ParallelSpiBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0); + ~ParallelSpiBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "SPI"; } +#endif + + void setInverted(bool inv) override; + void setColorOrder(uint8_t co); + + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; + +private: + int8_t _pin; + LedTiming _timing; + bool _inverted = false; + bool _initialized; + + int8_t _channelIdx; + SpiBusContext* _ctx; +}; + +} // namespace WLEDpixelBus + +#endif // WLEDPB_PARALLEL_SPI_SUPPORT + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp new file mode 100644 index 0000000000..b3e7ac1f5f --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -0,0 +1,505 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - RMT output driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +RMT bus works on ESP32, S3, S2, C3 and C6 (tested) and should work on C5 (untested) +Supports auto-distribution of available RMT memory blocks to reduce interrupt frequency - needs to be refined if ever using RMT input +IDF V5: new rmt_tx driver + bytes encoder; reset/latch gap enforced via TX-done callback timestamp in show() +The glitch-free high priority interrupt implementation by @willmmiles is not yet available in V5 builds + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" +#ifdef ARDUINO_ARCH_ESP32 +#include "WLEDpixelBus_RMT.h" + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) +#include "esp_timer.h" // for esp_timer_get_time() (ISR-safe microsecond timestamp) +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// Shared: auto-channel allocator state and constructor +//============================================================================== + +// Static auto-channel counter for RmtBus +uint8_t RmtBus::expectedChannels = 1; +uint8_t RmtBus::allocatedCount = 0; +uint8_t RmtBus::currentChannelIndex = 0; +uint8_t RmtBus::usedBlocks = 0; +uint8_t RmtBus::activeChannelMask = 0; + +RmtBus::RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _inverted(false) + , _initialized(false) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5,0,0) + , _rmtChannel(RMT_CHANNEL_0) +#endif +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +RmtBus::~RmtBus() { + end(); +} + +void RmtBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +// AI: below section was generated by an AI (works but needs a thorough review) + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) +//============================================================================== +// IDF V5 implementation (new RMT driver: rmt_tx + bytes encoder) +//============================================================================== + +bool RmtBus::begin() { + if (_initialized) return true; + uint8_t blocksToUse = 1; + uint8_t maxTxChannels = getRmtMaxChannels(); + + // Auto-channel select with optimized memory block allocation (assumes no RMT RX usage) + // note on channel allocation: channels are assigned such as to maximize the number of memory blocks + // available to the channel to minimize the number of interrupts needed for buffer re-fills (less context switching overhead) + // in V5 the request is passed as mem_block_symbols and the driver picks a channel with enough contiguous free blocks; + // memory-block ownership (the V4 rmt_set_memory_owner hack on S3/C3) is handled internally by the driver + // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks + // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 block, and CH3 with 5 blocks + if (allocatedCount >= expectedChannels || allocatedCount >= maxTxChannels) + return false; + + // total RMT memory blocks per group equals the total channel count (each channel owns one block, + // TX and RX channels share the same memory pool) + const uint8_t totalBlocks = SOC_RMT_CHANNELS_PER_GROUP; + + int left_channels = expectedChannels - allocatedCount - 1; + + if (left_channels == 0) { + _channel = currentChannelIndex; + blocksToUse = totalBlocks - usedBlocks; + } else { + int k = totalBlocks / expectedChannels; + int max_k_for_index = maxTxChannels - currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; + + _channel = currentChannelIndex; + blocksToUse = k; + } + #ifdef RMT_USE_SINGLE_MEM_BLOCK + blocksToUse = 1; + #endif + + currentChannelIndex += blocksToUse; + usedBlocks += blocksToUse; + allocatedCount++; + + if (_channel >= (int8_t)maxTxChannels) { + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d >= max %u, FAIL\n"), _channel, maxTxChannels); + return false; + } + + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n"), _channel, blocksToUse, allocatedCount, maxTxChannels); + + // RMT clock: 40MHz -> 25ns per tick + const float tickNs = 25.0f; + + auto nsToTicks = [tickNs](uint16_t ns) -> uint16_t { + uint16_t ticks = (uint16_t)((ns + tickNs / 2) / tickNs); + return ticks > 0 ? ticks : 1; + }; + + rmt_symbol_word_t bit0 = {}, bit1 = {}; + bit0.level0 = 1; bit0.duration0 = nsToTicks(_timing.t0h_ns); + bit0.level1 = 0; bit0.duration1 = nsToTicks(_timing.t0l_ns); + bit1.level0 = 1; bit1.duration0 = nsToTicks(_timing.t1h_ns); + bit1.level1 = 0; bit1.duration1 = nsToTicks(_timing.t1l_ns); + + rmt_tx_channel_config_t config = {}; + config.gpio_num = (gpio_num_t)_pin; + config.clk_src = RMT_CLK_SRC_DEFAULT; + config.resolution_hz = 40000000; // 40MHz, 25ns per tick + config.mem_block_symbols = (size_t)blocksToUse * SOC_RMT_MEM_WORDS_PER_CHANNEL; // multi-block allocation: fewer refill interrupts + config.trans_queue_depth = 2; // we always wait for tx done before transmitting, 1 would suffice + config.intr_priority = 3; // highest user priority in V5: refill ISR preempts other low/med ISRs, fewer underruns + config.flags.invert_out = _inverted ? 1 : 0; // hardware signal inversion via GPIO matrix (replaces esp_rom_gpio hack) + + esp_err_t err = rmt_new_tx_channel(&config, &_rmtChannel); + if (err != ESP_OK) { + return false; + } + + // bytes encoder: streams _encodeBuffer straight into RMT symbols, MSB first (replaces the V4 translator callbacks) + rmt_bytes_encoder_config_t encCfg = {}; + encCfg.bit0 = bit0; + encCfg.bit1 = bit1; + encCfg.flags.msb_first = 1; + err = rmt_new_bytes_encoder(&encCfg, &_bytesEncoder); + if (err != ESP_OK) { + rmt_del_channel(_rmtChannel); _rmtChannel = nullptr; + return false; + } + + // done callback stamps the end of each frame (used for the reset/latch gap enforcement in show()) + rmt_tx_event_callbacks_t cbs = {}; + cbs.on_trans_done = RmtBus::onTxDone; + err = rmt_tx_register_event_callbacks(_rmtChannel, &cbs, this); + if (err != ESP_OK) { + rmt_del_encoder(_bytesEncoder); _bytesEncoder = nullptr; + rmt_del_channel(_rmtChannel); _rmtChannel = nullptr; + return false; + } + + _txConfig = {}; + _txConfig.loop_count = 0; // no looping + _txConfig.flags.eot_level = 0; // keep line low after transmission (replaces idle_output_en / idle_level LOW) + + _resetUs = (_timing.reset_us > 0) ? _timing.reset_us : 300; + // pretend the last transmission ended long ago so the first frame never waits (unsigned wrap-safe) + _lastTxEndUs = (uint32_t)esp_timer_get_time() - _resetUs; + + err = rmt_enable(_rmtChannel); + if (err != ESP_OK) { + rmt_del_encoder(_bytesEncoder); _bytesEncoder = nullptr; + rmt_del_channel(_rmtChannel); _rmtChannel = nullptr; + return false; + } + + _initialized = true; + activeChannelMask |= (1 << _channel); + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + return true; +} + +void RmtBus::end() { + if (!_initialized) return; + + activeChannelMask &= ~(1 << _channel); + + rmt_tx_wait_all_done(_rmtChannel, 100); + rmt_disable(_rmtChannel); + if (_bytesEncoder) { rmt_del_encoder(_bytesEncoder); _bytesEncoder = nullptr; } + if (_rmtChannel) { rmt_del_channel(_rmtChannel); _rmtChannel = nullptr; } + + if (_pin >= 0) { + gpio_reset_pin((gpio_num_t)_pin); // reset all pin settings + } + + // Free encode buffer with the same allocator used in allocateEncodeBuffer() (plain malloc) + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } + _initialized = false; +} + +bool RmtBus::show() { + // Encoding is done per-pixel in setPixelColor(); _encodeBuffer is ready to ship. + if (!_initialized || !_encodeBuffer || _numPixels == 0 || !_rmtChannel) return false; + + esp_err_t err = rmt_tx_wait_all_done(_rmtChannel, 1000); // wait 1s max + if (err != ESP_OK) return false; + + // enforce the reset/latch gap: onTxDone() stamped the end of the previous frame, wait out the remainder + uint32_t elapsedUs = (uint32_t)esp_timer_get_time() - _lastTxEndUs; // unsigned subtraction is wrap-safe + if (elapsedUs < _resetUs) delayMicroseconds(_resetUs - elapsedUs); + + err = rmt_transmit(_rmtChannel, _bytesEncoder, _encodeBuffer, _encodeBufferSize, &_txConfig); + return (err == ESP_OK); // note: the V4 legacy path returned false on success (esp_err_t as bool), fixed here +} + +bool RmtBus::canShow() const { + if (!_initialized) return true; + return (ESP_OK == rmt_tx_wait_all_done(_rmtChannel, 0)); // 0 timeout means "poll and return immediately" +} + +void RmtBus::setInverted(bool inv) { + _inverted = inv; + // note: in V5 inversion is applied at channel creation (flags.invert_out); + // if changed after begin() it takes effect on the next begin() only +} + +// TX-done event callback (ISR context): stamp the end time of the frame. +// esp_timer_get_time() is safe to call from an ISR; keep this function short and IRAM resident. +bool IRAM_ATTR RmtBus::onTxDone(rmt_channel_handle_t channel, const rmt_tx_done_event_data_t *edata, void *user_ctx) { + RmtBus *self = static_cast(user_ctx); + self->_lastTxEndUs = (uint32_t)esp_timer_get_time(); + return false; // no higher priority task woken +} + +// AI: end + +#else +//============================================================================== +// IDF V4 implementation (legacy RMT driver + translator callbacks) +//============================================================================== + +// Per-channel context table - stored in DRAM, 4 byte aligned for ISR access +DMA_ATTR RmtBus::RmtContext RmtBus::contexts[WPB_RMT_CHANNELS] = {}; + +// Explicit IRAM tranlator callback wrappers for each channel (ensures the function is placed in IRAM which is dropped when using templates) +void IRAM_ATTR RmtBus::translator_ch0(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(0, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch1(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(1, s, d, ss, w, ts, in); } +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 +void IRAM_ATTR RmtBus::translator_ch2(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(2, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch3(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(3, s, d, ss, w, ts, in); } +#endif +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 +void IRAM_ATTR RmtBus::translator_ch4(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(4, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch5(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(5, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch6(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(6, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch7(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(7, s, d, ss, w, ts, in); } +#endif +// Jump table stored in DRAM, 4 byte aligned so ISR code can quickly find the correct wrapper +DMA_ATTR const sample_to_rmt_t RmtBus::callbacks[WPB_RMT_CHANNELS] = { + RmtBus::translator_ch0, RmtBus::translator_ch1 +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 + ,RmtBus::translator_ch2, RmtBus::translator_ch3 +#endif +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 + ,RmtBus::translator_ch4, RmtBus::translator_ch5, + RmtBus::translator_ch6, RmtBus::translator_ch7 +#endif +}; + +void RmtBus::updateRmtTiming() { + // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick + const float tickNs = 25.0f; + + auto nsToTicks = [tickNs](uint16_t ns) -> uint16_t { + uint16_t ticks = (uint16_t)((ns + tickNs / 2) / tickNs); + return ticks > 0 ? ticks : 1; + }; + + rmt_item32_t bit0, bit1; + bit0.level0 = 1; bit0.duration0 = nsToTicks(_timing.t0h_ns); + bit0.level1 = 0; bit0.duration1 = nsToTicks(_timing.t0l_ns); + bit1.level0 = 1; bit1.duration0 = nsToTicks(_timing.t1h_ns); + bit1.level1 = 0; bit1.duration1 = nsToTicks(_timing.t1l_ns); + + contexts[(int)_rmtChannel].bit0 = bit0.val; + contexts[(int)_rmtChannel].bit1 = bit1.val; + contexts[(int)_rmtChannel].resetDuration = nsToTicks(_timing.reset_us * 1000); + +} + +bool RmtBus::begin() { + if (_initialized) return true; + uint8_t blocksToUse = 1; + uint8_t maxTxChannels = getRmtMaxChannels(); + + // Auto-channel select with optimized memory block allocation (assumes no RMT RX usage) + // note on channel allocation: channels are assigned such as to maximize the number of memory blocks + // available to the channel to minimize the number of interrupts needed for buffer re-fills (less context switching overhead) + // C3 and S3 have less channels than blocks, so the last channel can always use additional blocks + // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks + // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 block, and CH3 with 5 blocks + if (allocatedCount >= expectedChannels || allocatedCount >= maxTxChannels) + return false; + + uint8_t totalBlocks = 4; // default to 4 if unknown, should be safe +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) + totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory +#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)// note: C6 RMT hardware is the same as C3 + totalBlocks = 4; // other supported ESP32 variants have 4 blocks +#endif + + int left_channels = expectedChannels - allocatedCount - 1; + + if (left_channels == 0) { + _channel = currentChannelIndex; + blocksToUse = totalBlocks - usedBlocks; + } else { + int k = totalBlocks / expectedChannels; + int max_k_for_index = maxTxChannels - currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; + + _channel = currentChannelIndex; + blocksToUse = k; + } + #ifdef RMT_USE_SINGLE_MEM_BLOCK + blocksToUse = 1; + #endif + + currentChannelIndex += blocksToUse; + usedBlocks += blocksToUse; + allocatedCount++; + + if (_channel >= (int8_t)maxTxChannels) { + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d >= max %u, FAIL\n"), _channel, maxTxChannels); + return false; + } + _rmtChannel = (rmt_channel_t)_channel; + + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n"), _channel, blocksToUse, allocatedCount, maxTxChannels); + + updateRmtTiming(); + + rmt_config_t config = {}; + + config.rmt_mode = RMT_MODE_TX; + config.channel = _rmtChannel; + config.gpio_num = (gpio_num_t)_pin; + config.mem_block_num = blocksToUse; + config.clk_div = 2; // 40MHz + + config.tx_config.loop_en = false; + config.tx_config.carrier_en = false; + config.tx_config.idle_output_en = true; + config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; + + esp_err_t err = rmt_config(&config); + if (err != ESP_OK) { + return false; + } + + // Register hack for memory blocks normally assigned to RX (S2 / S3 / C3) TODO: need this for ESP32 as well? + // set owner to TX if blocks are shared with RX unit +#ifndef RMT_USE_SINGLE_MEM_BLOCK + #if defined(CONFIG_IDF_TARGET_ESP32S3) + for (int i = 4; i < 8; i++) { + rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); + } + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + for (int i = 2; i < 4; i++) { + rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); + } + #endif +#endif + +#ifdef WPB_USE_RMTHI + // Use the glitch-free high priority RMT driver (not available on C3 and IDF >= 5.0) + err = RmtHiDriver::Install(_rmtChannel, contexts[(int)_rmtChannel].bit0, contexts[(int)_rmtChannel].bit1, contexts[(int)_rmtChannel].resetDuration, blocksToUse); + if (err != ESP_OK) { + //DEBUG_PRINTF_P(PSTR("[WPB] rmtHi Install failed: %d\n"), err); + return false; + } +#else + // Use the IDF rmt driver + translator + err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LOWMED)); + if (err != ESP_OK) { + return false; + } + + err = rmt_translator_init(_rmtChannel, callbacks[(int)_rmtChannel]); + if (err != ESP_OK) { + rmt_driver_uninstall(_rmtChannel); + return false; + } +#endif + + // route the pin, use hardware signal inversion via GPIO matrix if _inverted + esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); + + if (_initialized) esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); + + _initialized = true; + activeChannelMask |= (1 << _channel); + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + return true; +} + +void RmtBus::end() { + if (!_initialized) return; + + activeChannelMask &= ~(1 << _channel); +#ifdef WPB_USE_RMTHI + RmtHiDriver::Uninstall(_rmtChannel); +#else + rmt_driver_uninstall(_rmtChannel); +#endif + if (_pin >= 0) { + gpio_reset_pin((gpio_num_t)_pin); // reset all pin settings + } + + // Free encode buffer with the same allocator used in allocateEncodeBuffer() (plain malloc) + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } + _initialized = false; +} + +bool RmtBus::show() { + // Encoding is done per-pixel in setPixelColor(); _encodeBuffer is ready to ship. + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + + const size_t dataLen = _encodeBufferSize; + esp_err_t err; +#ifdef WPB_USE_RMTHI + err = (RmtHiDriver::Write(_rmtChannel, _encodeBuffer, dataLen) == ESP_OK); +#else + err = rmt_wait_tx_done(_rmtChannel, 1000 / portTICK_PERIOD_MS); // wait 1s max + if (err == ESP_OK) + err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); // CRITICAL BUG: crashes on C3 under heavy UI refresh, this line is reached, then it stalls for some unknown reason +#endif + return err; +} + +bool RmtBus::canShow() const { + if (!_initialized) return true; +#ifdef WPB_USE_RMTHI + return (ESP_OK == RmtHiDriver::WaitForTxDone(_rmtChannel, 0)); // 0 timout means "poll and return immediately" +#else + return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); +#endif +} + +void RmtBus::setInverted(bool inv) { + _inverted = inv; +} + +//note: using O2 optimization has little to no effect on FPS +void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, size_t src_size, size_t wanted_num, size_t* translated_size, size_t* item_num) { + + // safety check - should never happen + if (src == nullptr || dest == nullptr) { + *translated_size = 0; + *item_num = 0; + return; + } + + const uint8_t* psrc = (const uint8_t*)src; + + // Cache instance timings in registers for maximum ISR speed + const uint32_t bit0 = contexts[channel].bit0; + const uint32_t bit1 = contexts[channel].bit1; + + // Calculate how many full bytes we can translate based on RMT buffer space (wanted_num) + const uint32_t items_limit = wanted_num / 8; + const uint32_t bytes_to_process = (src_size > items_limit) ? items_limit : src_size; + *translated_size = bytes_to_process; + *item_num = bytes_to_process * 8; + + for (uint32_t i = 0; i < bytes_to_process; i++) { + const uint8_t data = psrc[i]; + rmt_item32_t* pdest = &dest[i * 8]; + + // using loop unrolling makes this faster avoiding bit shifts, loop overhead and lets the compiler optimize more + pdest[0].val = (data & 0x80) ? bit1 : bit0; + pdest[1].val = (data & 0x40) ? bit1 : bit0; + pdest[2].val = (data & 0x20) ? bit1 : bit0; + pdest[3].val = (data & 0x10) ? bit1 : bit0; + pdest[4].val = (data & 0x08) ? bit1 : bit0; + pdest[5].val = (data & 0x04) ? bit1 : bit0; + pdest[6].val = (data & 0x02) ? bit1 : bit0; + pdest[7].val = (data & 0x01) ? bit1 : bit0; + } + + // If max_bytes == src_size, it means we've reached the end of the LED strip. + if (bytes_to_process > 0 && bytes_to_process == src_size) { + dest[(bytes_to_process * 8) - 1].duration1 = contexts[channel].resetDuration; // set the last (low) pulse to reset duration + } + +// *translated_size = bytes_to_process; +// *item_num = bytes_to_process * 8; +} + +#endif // ESP_IDF_VERSION + +} // namespace WLEDpixelBus +#endif // ARDUINO_ARCH_ESP32 \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h new file mode 100644 index 0000000000..9c324f13de --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -0,0 +1,131 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - RMT output driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 +# define WPB_RMT_CHANNELS 8 +#elif SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 +# define WPB_RMT_CHANNELS 4 +#else +# define WPB_RMT_CHANNELS 2 +#endif + +#include "WLEDpixelBus.h" +#include "esp_idf_version.h" + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) + #include "driver/rmt_tx.h" + #include "driver/rmt_encoder.h" +#else + #include "driver/rmt.h" + #include "RmtHIDriver.h" // high interrupt priority driver, only on ESP32, S2, S3 using IDF V4 + #include "esp_rom_gpio.h" // for gpio routing to set inverted signal +#endif + +namespace WLEDpixelBus { + +//======================================= +// RMT Bus +//======================================= + +class RmtBus : public PixelBus { +public: + /** + * Create RMT bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + */ + RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0); + ~RmtBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "RMT"; } +#endif + + void setInverted(bool inv) override; + void setColorOrder(uint8_t co); + + // Reset the auto-allocation counter (call before re-creating buses) + static void setExpectedChannels(uint8_t expected) { expectedChannels = (expected > 0) ? expected : 1; } + static void resetAutoChannel() { + allocatedCount = 0; + currentChannelIndex = 0; + usedBlocks = 0; + } + +private: + int8_t _pin; + int8_t _channel; + bool _inverted; + bool _initialized; + LedTiming _timing; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) + // ---- IDF V5: new RMT driver (rmt_tx + bytes encoder) ---- + rmt_channel_handle_t _rmtChannel = nullptr; + rmt_encoder_handle_t _bytesEncoder = nullptr; + rmt_transmit_config_t _txConfig = {}; + uint32_t _resetUs = 50; + volatile uint32_t _lastTxEndUs = 0; // written from the TX-done ISR callback + + // TX-done event callback: stamps the end of each frame so show() can enforce the reset/latch gap + static bool IRAM_ATTR onTxDone(rmt_channel_handle_t channel, const rmt_tx_done_event_data_t *edata, void *user_ctx); +#else + // ---- IDF V4: legacy RMT driver ---- + rmt_channel_t _rmtChannel; + + void updateRmtTiming(); + + // Per-channel translator context and helpers + struct RmtContext { + uint32_t bit0; + uint32_t bit1; + uint16_t resetDuration; + }; + + // Static lookup table for timing speeds + static RmtContext contexts[WPB_RMT_CHANNELS]; + + // Explicit wrappers: implemented in .cpp file to ensure they are placed in IRAM + static void IRAM_ATTR translator_ch0(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch1(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 + static void IRAM_ATTR translator_ch2(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch3(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + #endif + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 + static void IRAM_ATTR translator_ch4(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch5(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch6(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch7(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + #endif + // Actual translator implementation (defined in .cpp) + static void IRAM_ATTR translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num); + + // Jump table of callbacks (defined in .cpp). Use 8 entries to match max RMT channels. + static const sample_to_rmt_t callbacks[WPB_RMT_CHANNELS]; +#endif + + // _encodeBuffer and _encodeBufferSize are in PixelBus base + static uint8_t expectedChannels; // TODO: make none static? would save a few bytes of ram but use more heap + static uint8_t allocatedCount; + static uint8_t currentChannelIndex; + static uint8_t usedBlocks; + static uint8_t activeChannelMask; // bitmask of initialized channels +}; + +} // namespace WLEDpixelBus \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp new file mode 100644 index 0000000000..d0c79a2183 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -0,0 +1,271 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32, S3, S2 and C3 both hardware SPI and BitBanged output +supports hardware brightness on APA102 for improved color resolution +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_SPI.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include "soc/gpio_reg.h" +#endif + +#define SPI_MAX_CLOCK_HZ 20000000UL // maximum SPI clock supported by all target platforms (ESP8266 max is 20 MHz, ESP32 can do 80 MHz but we clamp to 20 MHz for compatibility with ESP8266 and timing accuracy) + +namespace WLEDpixelBus { + +// Derive SPI clock Hz from kHz timing set through busconfig (see busmanager) +static uint32_t timingToClockHz(uint16_t frequencykHz) { + uint32_t frequencyHz = frequencykHz*1000; + if (frequencyHz < 1000000) frequencyHz = 1000000; // minimum 1 MHz + if (frequencyHz > SPI_MAX_CLOCK_HZ) frequencyHz = SPI_MAX_CLOCK_HZ; + return frequencyHz; +} + +SpiBus::SpiBus(int8_t dataPin, int8_t clockPin, uint16_t frequencykHz, uint8_t colorOrder, uint8_t numChannels, bool useHardwareSpi, uint8_t ledType) + : _dataPin(dataPin) + , _clockPin(clockPin) + , _useHardware(useHardwareSpi) + , _initialized(false) + , _clockHz(timingToClockHz(frequencykHz)) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +SpiBus::~SpiBus() { + end(); +} + +bool SpiBus::begin() { + if (_initialized) return true; + + if (_useHardware) { + // On ESP32 SPI.begin(sck, miso, mosi, ss) must be called with the actual pins so the IO matrix routes the SPI peripheral to the right GPIOs. + // On ESP8266 the hardware SPI uses fixed pins (MOSI=GPIO13, SCK=GPIO14) so no pin args are needed. +#if defined(ARDUINO_ARCH_ESP32) + SPI.begin(_clockPin, 127, _dataPin, -1); // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670 +#else + SPI.begin(); +#endif + // Frequency and mode are applied per-frame via beginTransaction(); do NOT call the deprecated SPI.setFrequency / SPI.setDataMode here. + } else { + // bit banged output + pinMode(_dataPin, OUTPUT); + pinMode(_clockPin, OUTPUT); + // Pre-compute bitmasks so the hot-path bit-bang loop does register writes not the ~10x-slower digitalWrite(). +#if defined(ARDUINO_ARCH_ESP32) + _dataHigh = (_dataPin >= 32); + _clkHigh = (_clockPin >= 32); + _dataMask = 1UL << (_dataHigh ? (_dataPin - 32) : _dataPin); + _clkMask = 1UL << (_clkHigh ? (_clockPin - 32) : _clockPin); + // Drive both lines LOW initially +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_dataHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _dataMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); + if (_clkHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _clkMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); +#else + REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); + REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); +#endif +#elif defined(ARDUINO_ARCH_ESP8266) + _dataMask = 1UL << _dataPin; + _clkMask = 1UL << _clockPin; + GPOC = _dataMask; + GPOC = _clkMask; +#endif + } + + _initialized = true; + uint8_t allocBytes = _encoder.getPixelBytes(); + if (_ledType == TYPE_APA102 || _ledType == TYPE_P9813) allocBytes++; // need more space for per-pixel header byte, see show() + if (!allocateEncodeBuffer(_numPixels, allocBytes)) { end(); return false; } + return true; +} + +void SpiBus::end() { + if (!_initialized) return; + if (_useHardware) { + SPI.end(); + } + #if defined(ARDUINO_ARCH_ESP32) + if (_dataPin >= 0) gpio_reset_pin((gpio_num_t)_dataPin); + if (_clockPin >= 0) gpio_reset_pin((gpio_num_t)_clockPin); + #else + pinMode(_dataPin, INPUT); + pinMode(_clockPin, INPUT); + #endif + if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } + _initialized = false; +} + +// Fast inline helpers for bit-bang GPIO register access. +// Using W1TS/W1TC (write-1-to-set/clear) registers avoids read-modify-write race conditions and is faster than GPIO_OUT read-modify-write. +inline void SpiBus::bbSetData(bool high) const { +#if defined(ARDUINO_ARCH_ESP32) +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (high) { + if (_dataHigh) REG_WRITE(GPIO_OUT1_W1TS_REG, _dataMask); else REG_WRITE(GPIO_OUT_W1TS_REG, _dataMask); + } else { + if (_dataHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _dataMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); + } +#else + if (high) { + REG_WRITE(GPIO_OUT_W1TS_REG, _dataMask); + } else { + REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); + } + // TODO: on the C3 BB output is much faster when using direct CPU access through cpu pin groups (see espressif documentation), could potentially achieve several MHz clockspeed +#endif +#elif defined(ARDUINO_ARCH_ESP8266) + if (high) GPOS = _dataMask; else GPOC = _dataMask; +#endif +} + +inline void SpiBus::bbSetClk(bool high) const { +#if defined(ARDUINO_ARCH_ESP32) +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (high) { + if (_clkHigh) REG_WRITE(GPIO_OUT1_W1TS_REG, _clkMask); else REG_WRITE(GPIO_OUT_W1TS_REG, _clkMask); + } else { + if (_clkHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _clkMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); + } +#else + if (high) { + REG_WRITE(GPIO_OUT_W1TS_REG, _clkMask); + } else { + REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); + } +#endif +#elif defined(ARDUINO_ARCH_ESP8266) + if (high) GPOS = _clkMask; else GPOC = _clkMask; +#endif +} + +void SpiBus::sendByte(uint8_t d) { + if (_useHardware) { + SPI.transfer(d); + } else { + // MSB-first bit-bang, SPI mode 0 (CPOL=0, CPHA=0): data is set up while clock is low, sampled on rising edge. + for (uint8_t i = 0; i < 8; i++) { + bbSetData(d & 0x80); + bbSetClk(true); + d <<= 1; + bbSetClk(false); + } + } +} + +void SpiBus::sendStartFrame(uint16_t numPixels) { + if (_ledType == TYPE_LPD8806) { + // LPD8806: start frame is ceil(N/32) zero bytes to clock in the initial latch + const uint16_t n = (numPixels + 31) / 32; + for (uint16_t i = 0; i < n; i++) sendByte(0x00); + } else if (_ledType != TYPE_WS2801) { + // APA102 / LPD6803 / P9813: fixed 4-byte zero start frame + // WS2801: no start frame — latch is the reset-time gap between frames + for (uint32_t i = 0; i < 4; i++) sendByte(0x00); + } +} + +void SpiBus::sendEndFrame(uint16_t numPixels) { + // APA102: ceil(N/16) zero bytes. TODO: NPB seems to send zero bytes, datasheet states four 0xFF bytes is the end frame + // Each APA102 delays the clock by one half-cycle; N LEDs need N/2 extra clock pulses to ensure the last pixel latches. One byte = 8 clocks, + // so ceil(N/16) bytes provide the required ceil(N/2) pulses. The APA102 datasheet's "4 zero bytes" claim is only valid for N ≤ 64. + // LPD6803: ceil(N/8) zero bytes (one clock per pixel required). + // LPD8806: ceil(N/32) 0xFF bytes (high level = latch for MSB-set pixel data). + // P9813: fixed 4 zero bytes. + // WS2801: nothing — latch is a timing gap, not a byte sequence. + if (_ledType == TYPE_APA102) { + const uint32_t n = (numPixels + 15) / 16; + for (uint32_t i = 0; i < n; i++) sendByte(0x00); + } else if (_ledType == TYPE_LPD6803) { + const uint32_t n = (numPixels + 7) / 8; + for (uint32_t i = 0; i < n; i++) sendByte(0x00); + } else if (_ledType == TYPE_LPD8806) { + const uint32_t n = (numPixels + 31) / 32; + for (uint32_t i = 0; i < n; i++) sendByte(0xFF); + } else if (_ledType == TYPE_P9813) { + for (uint32_t i = 0; i < 4; i++) sendByte(0x00); + } + // TYPE_WS2801: nothing to send +} + +bool SpiBus::show() { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + + const uint8_t pixelBytes = _encoder.getPixelBytes(); + + if (_useHardware) { + SPI.beginTransaction(SPISettings(_clockHz, MSBFIRST, SPI_MODE0)); // beginTransaction applies frequency, bit order, and SPI mode + } + + if (_ledType == TYPE_APA102 || _ledType == TYPE_P9813) { + // These chips need a per-pixel header byte (brightness/flag) in front of the RGB data + // Expand the compact encoded buffer in-place, starting from the last pixel (no data is overwritten) + // APA102 wire format: [0xE0|bri, B, G, R], P9813 wire format: [flag, B, G, R] + // note: this "extension" is intentionally not done when encoding the buffer so we can keep the encoding branch-free and fast for all types. + const uint8_t wireBytes = pixelBytes + 1; + for (uint32_t i = _numPixels; i > 0; i--) { + uint8_t* src = _encodeBuffer + (size_t)(i - 1) * pixelBytes; + uint8_t* dst = _encodeBuffer + (size_t)(i - 1) * wireBytes; + for (int8_t b = pixelBytes - 1; b >= 0; b--) dst[b + 1] = src[b]; + if (_ledType == TYPE_APA102) { + dst[0] = 0xE0 | _apa102HwBri; + } else { // TYPE_P9813 + const uint8_t b = dst[1], g = dst[2], r = dst[3]; + dst[0] = 0xC0 | ((~b & 0xC0) >> 2) | ((~g & 0xC0) >> 4) | ((~r & 0xC0) >> 6); + } + } + sendStartFrame(_numPixels); + uint8_t* src = _encodeBuffer; + if (_useHardware) { + SPI.writeBytes(src, (size_t)_numPixels * wireBytes); + } else { + const size_t total = (size_t)_numPixels * wireBytes; + for (size_t i = 0; i < total; i++) sendByte(src[i]); + } + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // Restore the compact layout so getPixelColor() and the next frame's getPixelColor() see the buffer in its normal encoded format. + // When the global _pixels[] buffer is disabled, this is necessary for all subsequent getPixelColor() calls. Still way faster than not doing full buffer encoding. + src = _encodeBuffer + 1; // skip header + for (uint32_t i = 0; i < _numPixels; ++i) { + uint8_t* dst = _encodeBuffer + (size_t)i * pixelBytes; + for (uint8_t b = 0; b < pixelBytes; b++) dst[b] = src[b]; + src += wireBytes; // move to next pixel + } + #endif + } else { + // WS2801 / LPD8806 / LPD6803: raw encoded bytes, no per-pixel framing byte + uint8_t* src = _encodeBuffer; + if (_useHardware) { + SPI.writeBytes(src, pixelBytes*_numPixels); + } + else { + for (uint16_t i = 0; i < _numPixels; i++) { + for (uint8_t ch = 0; ch < pixelBytes; ch++) sendByte(src[ch]); + src += pixelBytes; + } + } + } + + sendEndFrame(_numPixels); + + if (_useHardware) { + SPI.endTransaction(); + } + + return true; +} + +void SpiBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +bool SpiBus::canShow() const { + return true; +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h new file mode 100644 index 0000000000..d2a7cfeb4c --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -0,0 +1,80 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#include + +namespace WLEDpixelBus { + +//========================================================================== +// SPI Bus: Hardware and Software SPI for 2-wire LEDs like APA102, WS2801 +//========================================================================== + +class SpiBus : public PixelBus { +public: + /** + * Create SPI bus + * @param dataPin MOSI pin + * @param clockPin SCLK pin + * @param timing LED timing (bitPeriod() determines SPI clock, clamped to 1–20 MHz) + * @param colorOrder Color order + * @param numChannels Number of color channels + * @param useHardwareSpi True = hardware SPI peripheral, false = GPIO bit-bang + * @param ledType WLED LED type constant (TYPE_APA102, TYPE_WS2801, …) + */ + SpiBus(int8_t dataPin, int8_t clockPin, const uint16_t frequencykHz, uint8_t colorOrder, uint8_t numChannels, bool useHardwareSpi = true, uint8_t ledType = 0); + ~SpiBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return _useHardware ? "HW_SPI" : "SW_SPI"; } +#endif + void setColorOrder(uint8_t co); + + /** + * Set the APA102 per-pixel 5-bit hardware brightness step (0–31). + * Stored and sent in the brightness byte: 0xE0 | step. + * Overrides the PixelBus no-op base implementation. + */ + void setApa102HwBri(uint8_t v) override { _apa102HwBri = v & 0x1F; } + +private: + uint8_t _apa102HwBri = 31; // APA102 5-bit hardware brightness step (0–31); 31 = max (default) + int8_t _dataPin; + int8_t _clockPin; + bool _useHardware; + bool _initialized; + uint32_t _clockHz; // SPI clock in Hz, derived from timing at begin() time + // TODO: need _inverted flag here as well? output inversion is currently not supported on SPI type bus + + // Pre-computed GPIO bitmasks for fast bit-bang output (set in begin()) +#if defined(ARDUINO_ARCH_ESP32) + uint32_t _dataMask; // GPIO set/clear mask for data pin + uint32_t _clkMask; // GPIO set/clear mask for clock pin + bool _dataHigh; // true when data pin >= 32 (use GPIO1 register) TODO: this is only for ESP32's with a high data bank (ESP32, S3, maybe C6,C5) + bool _clkHigh; // true when clock pin >= 32 (use GPIO1 register) +#elif defined(ARDUINO_ARCH_ESP8266) + uint32_t _dataMask; + uint32_t _clkMask; +#endif + + inline void bbSetData(bool high) const; + inline void bbSetClk(bool high) const; + + void sendByte(uint8_t d); + void sendStartFrame(uint16_t numPixels); + void sendEndFrame(uint16_t numPixels); +}; + +} // namespace WLEDpixelBus + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h new file mode 100644 index 0000000000..4226a3a9e1 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -0,0 +1,103 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 +-------------------------------------------------------------------------*/ + +#pragma once + +#include +#include "../../const.h" + +namespace WLEDpixelBus { + +// Note on timings regarding I2S/LCD/SPI buses (4 step cadence): +// the drivers will always use a 1/4 - 3/4 duty cycle for '0' and '1' bits (e.g. 250ns high + 750ns low for a 1us period) +// since the t0h timing is the most critical (according to my testing) make it accurate and derive the rest + +/** + * LED timing parameters in nanoseconds + */ +struct LedTiming { + uint16_t t0h_ns; // '0' bit high time + uint16_t t0l_ns; // '0' bit low time + uint16_t t1h_ns; // '1' bit high time + uint16_t t1l_ns; // '1' bit low time + uint32_t reset_us; // Reset/latch time in microseconds + + constexpr LedTiming(uint16_t t0h, uint16_t t0l, uint16_t t1h, uint16_t t1l, uint32_t reset) + : t0h_ns(t0h), t0l_ns(t0l), t1h_ns(t1h), t1l_ns(t1l), reset_us(reset) {} + + // Calculate bit period in ns: used in hw-parallel buses only. since t0h is most critical and we do 4-step encoding, make the period 4*t0h + // we do not have the flexibility of RMT or BB outputs: t1h is fixed at t0h*3 + constexpr uint32_t bitPeriod() const { + return t0h_ns * 4; + } +}; + +/** + * Scale LED timing parameters by a floating point factor (percent expressed as factor, e.g. 1.2 for +20%) + * Only t* timings (ns) are scaled; reset_us is preserved. + */ +inline LedTiming scaleTiming(const LedTiming& timing, float factor) { + auto s = [&](uint32_t v)->uint16_t { + uint32_t r = (uint32_t)(v * factor + 0.5f); + if (r < 1) r = 1; + if (r > 0xFFFF) r = 0xFFFF; + return (uint16_t)r; + }; + return LedTiming(s(timing.t0h_ns), s(timing.t0l_ns), s(timing.t1h_ns), s(timing.t1l_ns), timing.reset_us); +} + +// LED timing lookup table in flash (PROGMEM on ESP8266, .rodata on ESP32) +// Indexed by getTimingIndex() below. +static const PROGMEM LedTiming s_ledTimings[] = { +// t0h t0l t1h t1l reset_us + { 300, 900, 700, 500, 100 }, // WS2812B (and 1CH_X3, 2CH_X3, WWA) + { 800, 1700, 1600, 900, 300 }, // Generic 400Kbps + { 300, 900, 800, 400, 200 }, // TM1829 + { 400, 850, 800, 450, 500 }, // UCS8903 / UCS8904 (16-bit) + { 350, 1350, 1350, 350, 50 }, // APA106 / PL9823 + { 360, 890, 720, 530, 200 }, // TM1914 / TM1814 (same timing) + { 300, 900, 800, 450, 200 }, // SK6812 / SK6812 RGBW + { 740, 1780, 1440, 1060, 200 }, // TM1815 + { 400, 850, 800, 450, 300 }, // FW1906 GRBCW + { 300, 790, 790, 300, 300 }, // WS2805 RGBCW + { 300, 900, 900, 300, 80 }, // SM16825 (16-bit) + { 250, 250, 250, 250, 0 }, // APA102 / LPD8806 / P9813 / LPD6803 -> SPI LEDs, timing is not really used + { 500, 500, 500, 500, 1000 }, // WS2801 +// TODO: could move these timing into the UI and always pass timings down to the firmware or better: make a LED type a struct containing all info including the string and send to UI as json +}; + +// Maps WLED TYPE_ to an index into s_ledTimings +static inline uint8_t getTimingIndex(uint8_t wledType) { + switch (wledType) { + case TYPE_WS2812_RGB: + case TYPE_WS2812_1CH_X3: + case TYPE_WS2812_1CH: + case TYPE_WS2812_WWA: return 0; // migration: WWA kept for timing selection only + case TYPE_WS2811_400KHZ: return 1; + case TYPE_TM1829: return 2; + case TYPE_UCS8903: + case TYPE_UCS8904: return 3; // identical timing + case TYPE_APA106: return 4; + case TYPE_TM1914: + case TYPE_TM1814: return 5; // identical timing + case TYPE_SK6812_RGBW: return 6; + case TYPE_TM1815: return 7; + case TYPE_FW1906: return 8; + case TYPE_WS2805: return 9; + case TYPE_SM16825: return 10; + case TYPE_APA102: + case TYPE_LPD8806: + case TYPE_P9813: + case TYPE_LPD6803: return 11; + case TYPE_WS2801: return 12; + default: return 0; // WS2812 fallback + } +} + +// Returns the LED timing for the given WLED bus type, read from flash. note: the bus constructor stores its own copy +LedTiming getProtocol(uint8_t wledType); + +} // namespace WLEDpixelBus diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 077c077f7d..a43bad97ec 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -433,6 +433,11 @@ void realtimeLock(uint32_t timeoutMs, byte md) realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs; } realtimeMode = md; + // switch gamma table to unity when realtime mode disables gamma correction, restore otherwise + if (arlsDisableGammaCorrection && md != REALTIME_MODE_INACTIVE) + NeoGammaWLEDMethod::calcGammaTable(1.0f); + else + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); if (realtimeOverride) return; if (arlsForceMaxBri) strip.setBrightness(255, true); @@ -445,6 +450,7 @@ void exitRealtime() { strip.setBrightness(bri, true); realtimeTimeout = 0; // cancel realtime mode immediately realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // restore gamma table after realtime mode realtimeIP[0] = 0; if (useMainSegmentOnly) { // unfreeze live segment again strip.getMainSegment().freeze = false; diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 04d57ebeb7..dd15da9584 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -340,7 +340,7 @@ 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,%d);"), + settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);"), WLED_PLATFORM_ID, // TODO: replace with a info json lookup MAX_LEDS_PER_BUS, MAX_LED_MEMORY, @@ -348,9 +348,11 @@ void getSettingsJS(byte subPage, Print& settingsScript) WLED_MAX_COLOR_ORDER_MAPPINGS, WLED_MAX_DIGITAL_CHANNELS, WLED_MAX_RMT_CHANNELS, - WLED_MAX_I2S_CHANNELS, + WLED_MAX_PARHW_CHANNELS, + WLED_MAX_BB_CHANNELS, WLED_MAX_ANALOG_CHANNELS, - WLED_MAX_BUTTONS + WLED_MAX_BUTTONS, + WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE ); printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments); @@ -378,6 +380,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //swap channels char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed + char sf[4] = "SF"; sf[2] = offset+s; sf[3] = 0; //bus speed factor (percent) 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 hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) @@ -419,9 +422,38 @@ void getSettingsJS(byte subPage, Print& settingsScript) } } printSetFormValue(settingsScript,sp,speed); + printSetFormValue(settingsScript,sf,bus->getBusSpeedFactor()); printSetFormValue(settingsScript,la,bus->getLEDCurrent()); printSetFormValue(settingsScript,ma,bus->getMaxCurrent()); printSetFormValue(settingsScript,hs,bus->getCustomText().c_str()); + // Custom bus: send per-channel config fields (active for any digital type when customized) + if (bus->getCustomBusConfig().active()) { + const CustomBusConfig& cb = bus->getCustomBusConfig(); + char cben[7] = "CBen"; cben[4] = offset+s; cben[5] = 0; // customize enabled + char cbch[7] = "CBch"; cbch[4] = offset+s; cbch[5] = 0; // channel count + char cbio[7] = "CBio"; cbio[4] = offset+s; cbio[5] = 0; // invert output + char cbb[6] = "CBb"; cbb[3] = offset+s; cbb[4] = 0; // is16bit + char cbt0h[7] = "CBt0h"; cbt0h[5] = offset+s; cbt0h[6] = 0; + char cbt0l[7] = "CBt0l"; cbt0l[5] = offset+s; cbt0l[6] = 0; + char cbt1h[7] = "CBt1h"; cbt1h[5] = offset+s; cbt1h[6] = 0; + char cbt1l[7] = "CBt1l"; cbt1l[5] = offset+s; cbt1l[6] = 0; + char cbrst[7] = "CBrst"; cbrst[5] = offset+s; cbrst[6] = 0; + printSetFormCheckbox(settingsScript, cben, true); + printSetFormValue(settingsScript, cbch, cb.numChannels); + printSetFormCheckbox(settingsScript, cbio, cb.invertOutput); + printSetFormCheckbox(settingsScript, cbb, cb.is16bit); + printSetFormValue(settingsScript, cbt0h, cb.t0h); + printSetFormValue(settingsScript, cbt0l, cb.t0l); + printSetFormValue(settingsScript, cbt1h, cb.t1h); + printSetFormValue(settingsScript, cbt1l, cb.t1l); + printSetFormValue(settingsScript, cbrst, cb.trst); + for (uint8_t ci = 0; ci < 6; ci++) { + char cbc[7] = "CBc"; cbc[3] = '0'+ci; cbc[4] = offset+s; cbc[5] = 0; // channel color + char cbi[7] = "CBi"; cbi[3] = '0'+ci; cbi[4] = offset+s; cbi[5] = 0; // invert bit + printSetFormValue(settingsScript, cbc, cb.channelColors[ci]); + printSetFormCheckbox(settingsScript, cbi, (cb.invertMask >> ci) & 1); + } + } sumMa += bus->getMaxCurrent(); } printSetFormValue(settingsScript,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa);