diff --git a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h deleted file mode 100644 index 02e066f741..0000000000 --- a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h +++ /dev/null @@ -1,469 +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) - -// 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 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 8353201f08..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) - -#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 \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index f58c1a8c5a..63be201caa 100644 --- a/platformio.ini +++ b/platformio.ini @@ -130,7 +130,6 @@ upload_speed = 115200 lib_compat_mode = strict lib_deps = IRremoteESP8266 @ 2.8.2 - https://github.com/Makuna/NeoPixelBus.git#1d7ff38f14d04a976f9c6e365c83a232bcba04fc https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2 marvinroger/AsyncMqttClient @ 0.9.0 # for I2C interface @@ -256,7 +255,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 a8f6e1cd76..5030aa080b 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -88,11 +88,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 @@ -100,8 +96,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) @@ -897,7 +891,12 @@ 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 ESP8266 + if (!_pixels) return; // direct-to-bus mode: use Segment::setPixelColor() for effect output + #endif + if (n < getLengthTotal()) _pixels[n] = c; + } // 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)); } @@ -955,8 +954,18 @@ 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 ESP8266 + if (!_pixels) return (getMappedPixelIndex(n) < getLengthTotal()) ? BusManager::getPixelColor(getMappedPixelIndex(n)) : 0; + #endif + 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 { + #ifdef ESP8266 + if (!_pixels) return (n < getLengthTotal()) ? BusManager::getPixelColor(n) : 0; + #endif + return (n < getLengthTotal()) ? _pixels[n] : 0; + } // 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 34fde40058..91ac076906 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" +#endif /* Custom per-LED mapping has moved! @@ -1206,79 +1209,59 @@ void WS2812FX::finalizeInit() { 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(CONFIG_IDF_TARGET_ESP32C3) + unsigned rmtBusCount = 0; + //#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(ARDUINO_ARCH_ESP32) // validate the bus config: count I2S buses and check if they meet requirements unsigned i2sBusCount = 0; + #endif for (const auto &bus : busConfigs) { if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { digitalCount++; - if (bus.driverType == 1) + //#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(ARDUINO_ARCH_ESP32) + if (bus.driverType == BUSDRV_I2S) i2sBusCount++; + else + rmtBusCount++; + #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; - } + //#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(ARDUINO_ARCH_ESP32) + DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount); #endif + + #if defined(ARDUINO_ARCH_ESP32) + WLEDpixelBus::RmtBus::resetAutoChannel(); + 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; + // Pass 1: assign driver types (RMT/I2S channels) for all buses before any are constructed, + // because parallel I2S buses interact during channel assignment. 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::allocateHardware(bus.type, bus.pins, bus.driverType); } + // Pass 2: construct each bus. BusManager::add() automatically falls back to a BusPlaceholder + // (and sets errorFlag) if a bus fails to initialize (OOM, DMA failure, pin conflict, etc.). + // The placeholder preserves the full config so the bus is retried on the next reboot. 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(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) - 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 + if (BusManager::add(bus, false) != -1) { + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) + digitalCount--; // placeholder does not consume a digital channel slot } } - 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. @@ -1308,11 +1291,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 ESP8266 + // On ESP8266, 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() { @@ -1452,11 +1441,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 ESP8266 + // 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) { @@ -1473,12 +1483,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 ESP8266 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 ESP8266 + 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 @@ -1488,7 +1504,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 ESP8266 + blendPixelIntoFrame(idx, c_a, opacity); + #else _pixels[idx] = color_blend(_pixels[idx], segblend(c_a, _pixels[idx]), opacity); + #endif } } } @@ -1496,7 +1516,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 ESP8266 uint32_t* strip = _pixels; + #endif int start = topSegment.start; int off = topSegment.offset; for (int i = 0; i < length; i++) { @@ -1504,8 +1526,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 ESP8266 + 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; } @@ -1582,8 +1608,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 ESP8266 + 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; @@ -1591,6 +1621,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 ESP8266 + 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); @@ -1599,6 +1634,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 } }; @@ -1676,6 +1712,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 ESP8266 + // 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; @@ -1688,6 +1736,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/ @@ -1732,47 +1781,78 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } void WS2812FX::show() { + #ifndef ESP8266 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 ESP8266 + 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 ESP8266 + 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 ESP8266 + 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); } @@ -1785,10 +1865,24 @@ void WS2812FX::show() { p_free(_pixelCCT); _pixelCCT = nullptr; + #else + /* + // TODO: this is not working, something wrong with color encoding/color order and it should use the raw pixel color, maybe move this down to bus level? + if (useGammaCorrection) { + for (auto &bus : BusManager::busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + for (int i = 0; i < busd.getLength(); i++) { + uint32_t c = busd.getPixelColor(i); // TODO: this uses restore color lossy, which is not needed. + if (c > 0) c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss + busd.setPixelColor(i, c); + } + } + } + }*/ + #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 @@ -1854,7 +1948,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 @@ -2221,3 +2318,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 d0257eb735..d0fbc22c5f 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 1ba808d8b0..eea5403519 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 type == TYPE_CUSTOM_BUS. +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)) { @@ -157,23 +162,51 @@ BusDigital::BusDigital(const BusConfig &bc) _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); + + // For TYPE_CUSTOM_BUS: store custom config, use per-instance numChannels and timing + if (bc.type == TYPE_CUSTOM_BUS) { + _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, 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; + 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); + } _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->setPixel(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->setPixel(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,13 @@ 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_CUSTOM_BUS, "D", PSTR("Custom Digital")}, // fully configurable channel map {TYPE_WS2801, "2P", PSTR("WS2801 RGB")}, {TYPE_APA102, "2P", PSTR("APA102 RGB")}, {TYPE_LPD8806, "2P", PSTR("LPD8806 RGB")}, @@ -374,20 +426,27 @@ std::vector BusDigital::getLEDTypes() { } bool BusDigital::isI2S() { - return (_iType & 0x01) == 0; // I2S types have even iType values + return _driverType == BUSDRV_I2S; // I2S/LCD/ParallelSPI } 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); } @@ -783,7 +842,7 @@ void BusNetwork::cleanup() { DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); d_free(_data); _data = nullptr; - _type = I_NONE; + _type = TYPE_NONE; _valid = false; } @@ -1230,7 +1289,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.type == TYPE_CUSTOM_BUS) _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 { @@ -1241,22 +1331,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; @@ -1269,7 +1343,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)) { @@ -1285,6 +1359,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,19 +1400,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 @@ -1411,14 +1494,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; + } } } @@ -1439,6 +1532,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; @@ -1498,17 +1602,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); @@ -1525,15 +1625,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::_i2sChannelsAssigned = 0; +uint8_t PixelBusAllocator::_parallelI2sBusType = 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 @@ -1545,3 +1648,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..7e8da8c314 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,30 @@ typedef struct { const char *name; } LEDType; +// Configuration for TYPE_CUSTOM_BUS: per-channel color source mapping and timing. +// Must be defined before Bus so BusDigital and BusPlaceholder can use it as a member field. +struct CustomBusConfig { + // channelColors[i]: 0=Unused, 1=R, 2=G, 3=B, 4=W, 5=WW, 6=CW (7 reserved for amber) + uint8_t numChannels = 3; + uint8_t channelColors[6] = {2, 1, 3, 0, 0, 0}; // default GRB (matches UI default) + 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 + // Per-instance signal timing. Defaults match WS2812 800kHz with a 300µs reset. + 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 +}; + + +// 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_I2S = 1, // I2S / LCD / parallel-SPI (chip-dependent) + BUSDRV_BITBANG = 2, // parallel bit-bang GPIO (ESP32 only) +}; //parent class of BusDigital, BusPwm, and BusNetwork class Bus { @@ -115,7 +133,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 +148,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 +162,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 +183,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 when getType() == TYPE_CUSTOM_BUS; returns a static default 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 +217,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 +237,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 +262,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 +275,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 +289,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 void begin() override; void cleanup(); + const CustomBusConfig& getCustomBusConfig() const override { return *_pCustomConfig; } // valid only when getType() == TYPE_CUSTOM_BUS 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_I2S / 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; // heap-allocated only when _type == TYPE_CUSTOM_BUS static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -345,8 +371,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 +412,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 +429,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; } size_t getBusSize() const override { return sizeof(BusPlaceholder); } @@ -413,6 +442,7 @@ class BusPlaceholder : public Bus { uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; String _text; + CustomBusConfig* _pCustomConfig = nullptr; // heap-allocated only when type == TYPE_CUSTOM_BUS }; #ifdef WLED_ENABLE_HUB75MATRIX @@ -465,11 +495,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_I2S / BUSDRV_BITBANG String text; + uint8_t busSpeedFactor; // percent (100 = default) + CustomBusConfig custom; // used only when type == TYPE_CUSTOM_BUS - 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 +511,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 +527,7 @@ struct BusConfig { (int)autoWhite, (int)frequency, (int)milliAmpsPerLed, (int)milliAmpsMax, - driverType == 0 ? "RMT" : "I2S" + driverType == BUSDRV_RMT ? "RMT" : driverType == BUSDRV_I2S ? "I2S" : "BitBang" ); } @@ -511,8 +542,6 @@ struct BusConfig { if (start + count > total) total = start + count; return true; } - - size_t memUsage() const; }; @@ -533,6 +562,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 +580,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 +591,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 15b5b0a3e1..e68d9f6b0e 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -2,1402 +2,177 @@ #ifndef BusWrapper_h #define BusWrapper_h -//#define NPB_CONF_4STEP_CADENCE -#include "NeoPixelBus.h" +#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" +#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 - - -// 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 -// 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; -#elif !defined(CONFIG_IDF_TARGET_ESP32C3) - // 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 - -// RMT driver selection -#if !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 +// Use single RMT memory block per channel — allows RMT RX channels alongside TX. +//#define RMT_USE_SINGLE_MEM_BLOCK -//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 _i2sChannelsAssigned; + static uint8_t _parallelI2sBusType; // Track first I2S 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; + _i2sChannelsAssigned = 0; + _parallelI2sBusType = 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; + #ifndef ESP8266 + if (driverType == BUSDRV_RMT && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { + _rmtChannelsAssigned++; + } else if (driverType == BUSDRV_BITBANG) { + // BitBang: no hardware channel limit, but enforce single LED type for parallel output + 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++; + } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { + driverType = BUSDRV_I2S; + // If first I2S channel request, lock the type to ensure parallel timings match + if (_i2sChannelsAssigned == 0) { + _parallelI2sBusType = busType; + #ifdef CONFIG_IDF_TARGET_ESP32C3 + _hardwareSPIused++; // reserve SPI: C3 uses prallel SPI output for "I2S" and takes priority over 2pin buses + #endif + } + _i2sChannelsAssigned++; + } else { + return false; // No channels available } - - 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; + #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_I2S; // 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 + } } - } - - 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; - } + 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; +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, uint8_t customNumChannels = 0, const WLEDpixelBus::LedTiming* customTiming = nullptr) { + if (!Bus::isDigital(busType)) return nullptr; - // 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 + #ifndef ESP8266 + if (driverType == BUSDRV_I2S && _parallelI2sBusType != 0) { + busType = _parallelI2sBusType; // Lock type for hardware timing sync } - // 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 + if (driverType == BUSDRV_BITBANG && _bitBangBusType != 0) { + busType = _bitBangBusType; // Lock type for BitBang parallel 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; - } - } - - [[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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; - } - // 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 + // getProtocol() reads from a PROGMEM table (flash on ESP8266, .rodata on ESP32). + // The timing is a one-time read at bus creation; scale to a local if needed. + WLEDpixelBus::LedTiming timing = customTiming ? *customTiming : WLEDpixelBus::getProtocol(busType); + if (busSpeedFactor != 100) { + float factor = (float)busSpeedFactor / 100.0f; + timing = WLEDpixelBus::scaleTiming(timing, factor); } - 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; - } - } + const uint8_t numChannels = customNumChannels ? customNumChannels : (uint8_t)Bus::getNumberOfChannels(busType); - 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 - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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; - } - - 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) - #ifndef CONFIG_IDF_TARGET_ESP32C3 - 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], timing, 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 + // uint8_t offset = pins[0] - 1; + if (pins[0] == 1 || pins[0] == 2) driver = WLEDpixelBus::BusDriver::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled TODO: there is a bug, TX1 only works after saving, not after reboot, needs investigation, use bitbanging for now + else if (pins[0] == 3) driver = WLEDpixelBus::BusDriver::DMA; // DMA method uses a lot of RAM! + else driver = WLEDpixelBus::BusDriver::BitBang; + //driver = WLEDpixelBus::BusDriver::BitBang; // bit banging works on all pins (debug) + #else + switch (driverType) { + case BUSDRV_RMT: driver = WLEDpixelBus::BusDriver::RMT; break; + case BUSDRV_I2S: + #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + driver = WLEDpixelBus::BusDriver::I2S; // uses LCD hardware on S3 no actually I2S + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + driver = WLEDpixelBus::BusDriver::SPI; // parallel SPI + #else + driver = WLEDpixelBus::BusDriver::RMT; #endif - } - else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag - _useParallelI2S = true; - t = _parallelBusItype; - } - #endif + break; + case BUSDRV_BITBANG: driver = WLEDpixelBus::BusDriver::BitBang; break; + default: driver = WLEDpixelBus::BusDriver::RMT; break; } - return t; + #endif + + // 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 2e458e7da9..1369df6bc3 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -234,6 +234,24 @@ 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; + // Migrate legacy types to TYPE_CUSTOM_BUS + CustomBusConfig migratedCustom; + bool isMigrated = false; + if (ledType == TYPE_WS2812_1CH_X3) { + ledType = TYPE_CUSTOM_BUS; + isMigrated = true; + migratedCustom.numChannels = 3; + migratedCustom.channelColors[0] = migratedCustom.channelColors[1] = migratedCustom.channelColors[2] = 4; // W,W,W + // timing left at struct defaults (800kHz WS2812) + } else if (ledType == TYPE_WS2812_WWA) { + ledType = TYPE_CUSTOM_BUS; + isMigrated = true; + migratedCustom.numChannels = 3; + migratedCustom.channelColors[0] = 0; // Unused (was amber) + migratedCustom.channelColors[1] = 5; // WW + migratedCustom.channelColors[2] = 6; // CW + // timing left at struct defaults (800kHz WS2812) + } 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 @@ -249,7 +267,29 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S 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 config (migration or loaded from JSON) + BusConfig& bc_back = busConfigs.back(); + if (bc_back.type == TYPE_CUSTOM_BUS) { + if (isMigrated) { + bc_back.custom = migratedCustom; + } else { + bc_back.custom.numChannels = elm["cch"] | 3; + 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; + } + } doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -530,7 +570,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; @@ -1004,6 +1044,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->getType() & 0x7F) == TYPE_CUSTOM_BUS) { + 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 04ff8ded61..56645ba024 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_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 @@ -76,27 +77,39 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_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_I2S_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_I2S_CHANNELS 16 + #else + #define WLED_MAX_I2S_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_I2S_CHANNELS 16 // uses LCD parallel output not I2S and supports up to 16 parallel channels + #else + #define WLED_MAX_I2S_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! #else #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! + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_I2S_CHANNELS 16 + #else + #define WLED_MAX_I2S_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 - #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS) + #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS + WLED_MAX_BB_CHANNELS) // total number of digital channels (RMT + I2S + BitBang) #endif // WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed // instead it will help determine max number of buses that can be defined at compile time @@ -104,7 +117,6 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #undef WLED_MAX_BUSSES #endif #define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS) -static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); // Maximum number of pins per output. 5 for RGBCCT analog LEDs. #define OUTPUT_MAX_PINS 5 @@ -330,10 +342,13 @@ 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_CUSTOM_BUS 39 // fully configurable single-wire digital type (1-6 channels, custom per-channel color mapping) #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 09207f06d6..f2f0cf32f5 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -386,4 +386,4 @@ - + \ No newline at end of file diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 03fae259c4..c102e6d7cc 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -6,7 +6,7 @@ LED Settings @@ -1167,4 +1395,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 fb516ac7d6..7971201ee2 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -207,6 +207,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) @@ -265,7 +266,35 @@ 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); + // For TYPE_CUSTOM_BUS: parse per-channel custom config from the submitted form + if ((type & 0x7F) == TYPE_CUSTOM_BUS) { // strip bit 7 (off-refresh flag) + 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 = (uint8_t)constrain(request->arg(cbch).toInt(), 1, 6); + bc_back.custom.invertOutput = request->hasArg(cbio); + bc_back.custom.is16bit = request->hasArg(cbb); + bc_back.custom.t0h = (uint16_t)constrain(request->arg(cbt0h).toInt(), 50, 9999); + bc_back.custom.t0l = (uint16_t)constrain(request->arg(cbt0l).toInt(), 50, 9999); + bc_back.custom.t1h = (uint16_t)constrain(request->arg(cbt1h).toInt(), 50, 9999); + bc_back.custom.t1l = (uint16_t)constrain(request->arg(cbt1l).toInt(), 50, 9999); + bc_back.custom.trst = (uint16_t)constrain(request->arg(cbrst).toInt(), 1, 9999); + 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 0c60d2ebc9..126fd3023e 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) && !defined(CONFIG_BTDM_CTRL_HLI) diff --git a/wled00/src/WLEDpixelBus/RmtHIDriver.h b/wled00/src/WLEDpixelBus/RmtHIDriver.h new file mode 100644 index 0000000000..06fee873d0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/RmtHIDriver.h @@ -0,0 +1,30 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +-------------------------------------------------------------------------*/ +#pragma once + +#ifdef ARDUINO_ARCH_ESP32 + +#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 diff --git a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp new file mode 100644 index 0000000000..69f876f3c8 --- /dev/null +++ b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp @@ -0,0 +1,490 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +-------------------------------------------------------------------------*/ + +#include + +#if defined(ARDUINO_ARCH_ESP32) + +#include +#include "esp_idf_version.h" +#include "RmtHIDriver.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; +} + +__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..51e9b9e1e0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -0,0 +1,284 @@ +/*------------------------------------------------------------------------- +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 and ESP8266 +- RMT, I2S parallel, LCD parallel, SPI parallel, BitBang 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 + +Currently based on IDF v4.x API functions and low-level HAL + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" + +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_ParallelSpi.h" +#endif + +#if defined(ESP8266) +#include "WLEDpixelBus_ESP8266.h" +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// 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 (0=GRB,1=RGB,2=BRG,3=RBG,4=BGR,5=GBR) + uint8_t rPos, gPos, bPos; + switch (co & 0x0F) { + case 1: rPos=0; gPos=1; bPos=2; break; // RGB + case 2: rPos=1; gPos=2; bPos=0; break; // BRG + case 3: rPos=0; gPos=2; bPos=1; break; // RBG + case 4: rPos=2; gPos=1; bPos=0; break; // BGR + case 5: rPos=2; gPos=0; bPos=1; break; // GBR + default: rPos=1; gPos=0; bPos=2; break; // GRB (case 0) + } + _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 == 4) { + // 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 == 1) { std::swap(_idxW, _idxB); } // swap W & B + if (wSwap == 2) { std::swap(_idxW, _idxG); } // swap W & G + if (wSwap == 3) { std::swap(_idxW, _idxR); } // swap W & R + } else if (numChannels >= 5) { + const uint8_t wSwap = co >> 4; + if (wSwap == 4) { 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; + +} + +// Custom channel map constructor for TYPE_CUSTOM_BUS. +// channelMap[i]: 0=Unused, 1=R, 2=G, 3=B, 4=W, 5=WW, 6=CW +ColorEncoder::ColorEncoder(const uint8_t channelMap[6], uint8_t numChannels, uint8_t invertMask, bool is16bit) +{ + memcpy(_channelMap, channelMap, 6); + _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 1: val = getR(c); break; + case 2: val = getG(c); break; + case 3: val = getB(c); break; + case 4: val = getW(c); break; + case 5: val = cct.ww; break; + case 6: val = cct.cw; break; + default: val = 0; break; // 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 6: encodeRGB16(c, out, bri); break; + case 8: encodeRGBW16(c, out, bri); break; + default: encodeCCT16(c, cct, out, bri); break; // 10 + } + } else { + switch (logCh) { + case 3: encodeRGB(c, out); break; + case 4: encodeRGBW(c, out); break; + default: encodeCCT(c, cct, out); break; // 5 + } + } + // 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 1: r = val; break; + case 2: g = val; break; + case 3: b_ = val; break; + case 4: case 5: case 6: // 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 >= 8) w = readU16Hi(in, _idxW); // 8=RGBW16, 10=CCT16 + } else { + r = in[_idxR]; g = in[_idxG]; b = in[_idxB]; + if (logCh >= 4) w = in[_idxW]; // 4=RGBW, 5=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 >= 8) : (logCh >= 4); + 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(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) + 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 +#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; +#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(8); // 8-byte C1+C2 current-config prefix; updated per-frame in BusDigital::setBrightness() + bus->setInverted(true); + break; + case TYPE_TM1914: + bus->setPrefixLen(6); // 6-byte mode-setting prefix; written once in BusDigital::begin() + bus->setInverted(true); + break; + case TYPE_SM16825: + bus->setSuffixLen(4); // 4-byte 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..70296bd618 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -0,0 +1,561 @@ +/*------------------------------------------------------------------------- +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 and ESP8266 +- RMT, I2S parallel, LCD parallel, SPI parallel, BitBang 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 + +Currently based on IDF v4.x API functions and low-level HAL + +-------------------------------------------------------------------------*/ + +#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" +#include "driver/rmt.h" + +// I2S support: targets where the I2S peripheral has the LCD Intel 8080 mode +// used for parallel LED output. SOC_I2S_LCD_I80_VARIANT is defined on ESP32 and ESP32-S2 only +// SOC_LCDCAM_SUPPORTED is defined on ESP32-S3 only (uses LCD peripheral, not actually I2S) +#if (SOC_I2S_LCD_I80_VARIANT || SOC_LCDCAM_SUPPORTED) + #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). +// SOC_GPIO_PIN_COUNT is a preprocessor #define from soc/soc_caps.h (included +// transitively via driver/gpio.h): 40 for ESP32, 47 for S2, 49 for S3, +// 22 for C3, 31 for C6, 28 for H2. +#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 + * Passed as separate buffer from main RGBW data + */ + /* +struct CctPixel { + uint8_t ww; // Warm white + uint8_t cw; // Cool white +};*/ +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 + 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; +#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, need extra space for S3 and ESP32 classic, also requires triple buffering +#else +constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*3); +#endif +// TODO: need to assert the default is not larger than the max allowed, which is 4092 (12bit in DMA descriptor) + +//============================================================================== +// 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 (TYPE_CUSTOM_BUS): _channelMap[] drives encoding +// 0x80 reserved + +/** + * 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 TYPE_CUSTOM_BUS + // channelMap[i]: 0=Unused, 1=R, 2=G, 3=B, 4=W, 5=WW, 6=CW + ColorEncoder(const uint8_t channelMap[6], 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[6]; // custom channel map (TYPE_CUSTOM_BUS): _channelMap[i] = color source for wire byte i + // MUST remain last — keeps _idxR/_idxG/_idxB at same offsets as before, preserving IRAM code size + + // Helpers + 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 or moving + 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 TYPE_CUSTOM_BUS 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(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) = 0; // TODO: remove the parameters, they are unused + 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 + virtual bool setPixel(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 + case (4*2) | NCHF_16BIT: _encoder.encodeRGBW16(c, out, _encBri); break; // 16-bit RGBW + case (5*2) | NCHF_16BIT: _encoder.encodeCCT16(c, cct, out, _encBri); break; // 16-bit CCT + 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). + */ + // TODO: on ESP32, do not put this in IRAM + 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 = (size_t)numPixels * numChannels; + 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 + +//============================================================================== +// 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 + UART = 3, + DMA = 4, + BitBang = 5 +}; + +/** + * 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) + return 2; // ESP32-C3: 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 type, 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(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_ParallelSpi.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..8a831117b8 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -0,0 +1,399 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +works on all ESP32 variants + +Interrupts are fully disabled during output to avoid any glitches +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) +#include "esp_clk.h" // esp_clk_cpu_freq() +#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; + +// --------------------------------------------------------------------------- +// getCycleCount() — read the CPU cycle counter. +// Must be IRAM_ATTR because it is called from outputParallel() which is IRAM. +// --------------------------------------------------------------------------- +#if defined(ESP8266) + static inline uint32_t IRAM_ATTR getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("rsr %0,ccount" : "=a"(ccount)); + return ccount; + } + static constexpr uint32_t LOOPTEST_CYCLES = 1; +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || \ + defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32P4) + // RISC-V: machine cycle counter CSR 0x7e2 (vendor extension, same as NeoPixelBus) + static inline uint32_t IRAM_ATTR getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("csrr %0, 0x7e2" : "=r"(ccount)); + return ccount; + } + static constexpr uint32_t LOOPTEST_CYCLES = 1; +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + // Xtensa LX7 — same rsr instruction, tighter pipeline + static inline uint32_t IRAM_ATTR getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("rsr %0,ccount" : "=a"(ccount)); + return ccount; + } + static constexpr uint32_t LOOPTEST_CYCLES = 2; +#else + // Xtensa LX6 — ESP32, ESP32-S2 + static inline uint32_t IRAM_ATTR getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("rsr %0,ccount" : "=a"(ccount)); + return ccount; + } + static constexpr uint32_t LOOPTEST_CYCLES = 4; +#endif + +// --------------------------------------------------------------------------- +// Constructor / Destructor +// --------------------------------------------------------------------------- +BitBangBus::BitBangBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) + : _pin(pin), _rawTiming(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 = (_rawTiming.t0h_ns * cpuMHz) / 1000u; + uint32_t t1h = (_rawTiming.t1h_ns * cpuMHz) / 1000u; + t0h = (t0h > lo) ? t0h - lo : 0u; + t1h = (t1h > lo) ? t1h - lo : 0u; + uint32_t period = (_rawTiming.bitPeriod() * cpuMHz) / 1000u; + period = (period > lo) ? period - lo : 0u; + const uint32_t latchCycles = (uint32_t)_rawTiming.reset_us * cpuMHz; + + // 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() +// Stage this channel's data. When the last channel stages, run outputParallel(). +// --------------------------------------------------------------------------- +bool BitBangBus::show(const uint32_t*, uint16_t, const CctPixel*) { + if (!_initialized || !_pixelData) return false; + + _BBs->stagedCount++; + if (_BBs->stagedCount < _BBs->channelCount) { + return true; // not all channels ready yet + } + + _BBs->stagedCount = 0; // reset for next frame + return outputParallel(); // send data to LEDs (blocking) + +} + +// --------------------------------------------------------------------------- +// resetChannels() — called by PixelBusAllocator::resetChannelTracking() +// --------------------------------------------------------------------------- +void BitBangBus::resetChannels() { + if (_BBs) memset(_BBs, 0, sizeof(BBstate)); +} + +// --------------------------------------------------------------------------- +// outputParallel() — the hot path, must live 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. At each pixel boundary (after the first), release the ISR lock 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. +// 3. Wait for the full bit period since the previous HIGH edge. +// 4. Set all output pins HIGH simultaneously (setOutputMask). +// 5. After T0H cycles: pull the '0' outputs LOW (GPIO.out_w1tc = zeroMask). +// 6. After T1H cycles: pull all remaining outputs LOW (GPIO.out_w1tc = setOutputMask). +// +// 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 +// --------------------------------------------------------------------------- +bool IRAM_ATTR BitBangBus::outputParallel() { + if (!_BBs || _BBs->channelCount == 0) return true; + + 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; + // 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; + + const uint32_t totalBits = (uint32_t)maxPixels * pixelBytes * 8u; + const uint32_t bitsPerPixel = (uint32_t)pixelBytes * 8u; + + // Per-channel pin masks and data extents — pre-computed for fast inner-loop access. + // On classic ESP32, 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) { + 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) -> 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 + + // Period reference: initialise as already expired so the first pulse fires immediately. + uint32_t cyclesStart = getCycleCount() - period; + +#if defined(ARDUINO_ARCH_ESP32) + portENTER_CRITICAL(&_BBs->mux); +#elif defined(ESP8266) + os_intr_lock(); +#endif + + for (uint32_t bitIndex = 0; bitIndex < totalBits; 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 + + // Wait for the full bit period since the last HIGH edge + 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 + } + +#if defined(ARDUINO_ARCH_ESP32) + portEXIT_CRITICAL(&_BBs->mux); +#elif defined(ESP8266) + os_intr_unlock(); +#endif + + 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..4e845724d3 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -0,0 +1,98 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +works on all ESP32 variants + +Interrupts are fully disabled during output to avoid any glitches +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#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; + void setInverted(bool inv) override; // invert output signal + bool show(const uint32_t* = nullptr, uint16_t = 0, const CctPixel* = nullptr) 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 it currently is) + +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "BitBang"; } +#endif + + // Reset the shared static channel registryc, alled by PixelBusAllocator::resetChannelTracking() when all buses are destroyed + static void resetChannels(); + +private: + int8_t _pin = -1; + bool _inverted = false; // invert output signal + bool _initialized = false; + + LedTiming _rawTiming; // saved at construction, converted to cycles in begin() + + // ----------------------------------------------------------------------- + // 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) + }; + 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..c174952979 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -0,0 +1,597 @@ +/*------------------------------------------------------------------------- + +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), _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 + USC0(uartNum) |= (1 << UCTXI); // invert TX -> idle low TODO: need to handle inverted LED output for custom bus +} + +bool Esp8266UartBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + if (!canShow()) return false; // TODO: is this consistent accross all drivers? i.e. return instead of wait? + // 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); +} + +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), + _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 + const size_t pixelBytes = (size_t)numPixels * numChannels * 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; 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 LOW before any I2S activity (prevents startup glitch) + pinMode(3, OUTPUT); + digitalWrite(3, 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, 0, _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::setPixel(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; + 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 (((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 (((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; + 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; word |= (v & (1 << bit)) ? 0xEu : 0x8u; } + dst[i] = word; + } +} + +void Esp8266DmaBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +// --------------------------------------------------------------------------- +// 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(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { + 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..62aac77ded --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -0,0 +1,125 @@ +/*------------------------------------------------------------------------- + +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 + +-------------------------------------------------------------------------*/ +#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(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "ESP8266_UART"; } +#endif + void setColorOrder(uint8_t co); + + static void UartIsr(void* arg, void* exceptionFrame); + static Esp8266UartBus* s_instances[2]; + +private: + int8_t _pin; + LedTiming _timing; + 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(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "ESP8266_DMA"; } +#endif + + IRAM_ATTR bool setPixel(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; + void setColorOrder(uint8_t co); + + 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 _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..c3d9420913 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h @@ -0,0 +1,84 @@ +/*------------------------------------------------------------------------- + +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 +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp new file mode 100644 index 0000000000..fb80f0840a --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -0,0 +1,931 @@ +/*------------------------------------------------------------------------- + +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) +#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) + , _activeBuffer(0) + , _remainingDataBuffers(0) + , _resetBytesLeft(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; + + 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; + if (_dmaChannel) gdma_stop(_dmaChannel); +} + +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; + + _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->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } +} + +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 I2sBusContext::fillBuffer(uint8_t bufIdx) { + memset(_dmaBuffer[bufIdx], 0, _bufferSize); // clear the buffer, will be filled or left blank as reset signal (cant be skipped, some channels may have less data) + + 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 + } + + _activeBuffer = 0; // start with first buffer + _state = DriverState::Sending; + + 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)dma_chan; + (void)event_data; + I2sBusContext* ctx = (I2sBusContext*)user_data; + ctx->_processEof(); + 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_EOF_INT_ST)) return; + ctx->_processEof(); +} +#endif + +void IRAM_ATTR I2sBusContext::_processEof() { + uint8_t completedBuf = _activeBuffer; + _activeBuffer = (_activeBuffer + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; + + if (descGetNext(_dmaDesc[completedBuf]) == nullptr) { + hwStopTransfer(); + _state = DriverState::Idle; + return; + } + + if (_resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { // 3 means end of transfer (i.e. reset pulse) was encoded on last fillBuffer call + fillBuffer(completedBuf); // fill buffer, handle reset pulse and end of + //descSetOwnerDma(_dmaDesc[completedBuf]); // note: it looks like the owner is not reset upon eof, we do not need to hand it back + } +} + +// ============================================ +// 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) + , _busNum(busNum) + , _timing(timing) + , _initialized(false) + , _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 = (size_t)numPixels * numChannels; + 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(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + 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 I2sBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +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..326ffedbce --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -0,0 +1,241 @@ +/*------------------------------------------------------------------------- + +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 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#ifdef WLEDPB_I2S_SUPPORT +namespace WLEDpixelBus { + +//============================================================================== +// I2S Parallel Bus - ESP32, ESP32-S2, ESP32-S3 (LCD) +//============================================================================== + +#include "driver/periph_ctrl.h" +#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" +#else + #include "soc/i2s_struct.h" + #include "soc/i2s_reg.h" + #include "rom/lldesc.h" + #include "esp_intr_alloc.h" +#endif + +// SOC_LCD_I80_BUSES: number of I2S peripherals that support the LCD Intel 8080 +// TODO: support both buses on ESP32? (currently only bus 1 is used for LED output, one is for AR) +// mode. 2 on ESP32 (I2S0 + I2S1), 1 on ESP32-S2 (I2S0 only), 1 on ESP32-S3 (LCD_CAM). +#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, 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. +// TODO: this requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load +// TODO2: the buffer count and size need to be checked agains memory usage fomulas, they may be incorrect +#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 + #else + #define WLEDPB_I2S_DMA_BUFFER_COUNT 2 // 2 buffers is enough if not constantly interrupted by RMT + #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; } + + // 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(); + + // 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; +#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 _activeBuffer; + volatile uint8_t _remainingDataBuffers; + volatile uint16_t _resetBytesLeft; + + // 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(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct = nullptr) override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "I2S"; } +#endif + + // Override to use DMA-capable allocator for I2S + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; + + void setInverted(bool inv) override; + void setColorOrder(uint8_t co); + +private: + int8_t _pin; + uint8_t _busNum; + LedTiming _timing; + bool _inverted = false; + bool _initialized; + + int8_t _channelIdx; + I2sBusContext* _ctx; + + // _encodeBuffer and _encodeBufferSize are in PixelBus base (allocated DMA-capable via allocateEncodeBuffer override) + bool allocateBuffer(uint16_t numPixels); // legacy, calls allocateEncodeBuffer +}; + +} // namespace WLEDpixelBus +#endif // WLEDPB_I2S_SUPPORT + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp new file mode 100644 index 0000000000..e4b8dfd017 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -0,0 +1,718 @@ +/*------------------------------------------------------------------------- + +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 + +-------------------------------------------------------------------------*/ + +#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_private/gdma.h" +#include "esp_rom_gpio.h" +namespace WLEDpixelBus { + +//============================================= +// SPI Parallel Bus Implementation (ESP32-C3) +//============================================ + +// low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) +static inline void spi_ll_apply_config(spi_dev_t *hw) { + hw->cmd.update = 1; + while (hw->cmd.update); //waiting config applied +} + +static inline void spi_ll_user_start(spi_dev_t *hw) { + hw->cmd.usr = 1; +} + + +// 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) TODO: restrict in UI to 2048 source bytes + +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) + , _gdmaIsrHandle(nullptr) + , _spiIsrHandle(nullptr) + , _hw(&GPSPI2) + , _channelCount(0) + , _framePos(0) + , _numBytes(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(); +} + +//TODO: rename to isIdle() +bool SpiBusContext::isSpiDone() { + if (_state == SpiState::Idle) return true; + + // If we're in an error state, use a timeout to recover. + // This is a safety net for cases where the hardware got stuck + // (e.g. outfifo_empty_err fired but DMA didn't stop cleanly). + if (_state == SpiState::Error) { + if (millis() - _lastTransmitMs > 100) { + forceIdle(); + return true; + } + return false; + } + + // Normal sending states: poll hardware status + // On C3, SPI_TRANS_DONE_INT is the authoritative "transfer complete" signal. + // We also check if the SPI user command bit is still set. + if (_hw->cmd.usr == 0) { + // SPI has stopped. If we were still in a sending state, something + // went wrong (e.g. DMA underrun that didn't trigger the error ISR). + // Transition to error state for cleanup. + if (_state != SpiState::Idle) { + _state = SpiState::Error; + _lastTransmitMs = millis(); + } + return false; // will timeout above + } + + return false; +} + +// Recovery path for error conditions, cleanly stops DMA, SPI, and disconnects pins to prevent glitches +// TODO: never seeing this being called anymore. needs more testing but can probably be removed +void SpiBusContext::forceIdle() { +Serial.println("ERROR: forced idle"); + 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) { + //gpio_matrix_out(_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); // disconnect from SPI and use as regular GPIO, non inverted + esp_rom_gpio_connect_out_signal(_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); // TODO: this is the new command in IDF V5, works in V4 too? + 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[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + // 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 < (WLEDPB_SPI_DMA_BUFFER_SIZE / 4); i++) { + dst32[i] = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned + //memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); // TODO: memset is not ISR IRAM safe? + } + + if (_state == SpiState::WaitingReset) { + return; // No encoding needed; just return and send reset pulse (all zeros) + } + + size_t maxSrcThisChunk = WLEDPB_SPI_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 +// (FIFO underrun recovery). Both paths are synchronized with gdmaISR via _isrMux. +void IRAM_ATTR SpiBusContext::spiISR(void* arg) { + + 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) { + ctx->_state = SpiState::Idle; // Normal transfer completion. SPI has finished all bits. + } + else if (status & SPI_DMA_OUTFIFO_EMPTY_ERR_INT_ST) { + // SPI FIFO starved, ISR latency too high + 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 + ctx->_state = SpiState::Error; // set Error state so isSpiDone() will timeout and forceIdle() (forceIdle may not be ISR safe) + ctx->_lastTransmitMs = millis(); + + // disconnect pins from SPI to prevent garbage output (calling cmd.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.func_out_sel_cfg[ctx->_channels[i].pin].func_sel = SIG_GPIO_OUT_IDX; // disconnect from SPI using direct register write (ISR safe) + GPIO.out_w1tc.out_w1tc = (1 << ctx->_channels[i].pin); // set ouput low (clear) to avoid glitches note: if implementing this for other ESPs: need to also set the high register for pins >31 + } + } + ctx->_hw->cmd.usr = 0; // stop SPI user transfer + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + } +} + +void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { + SpiBusContext* ctx = (SpiBusContext*)arg; + gdma_dev_t* dma = &GDMA; + + // Check if this is our interrupt + if (!dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + return; + } + // make sure we are not disturbed filling the buffer to prevent underruns + portENTER_CRITICAL_ISR(&ctx->_isrMux); + // Clear interrupt immediately + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + + // 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; + } + + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; + + // If we're in WaitingReset, this EOF means the reset pulse buffer + // has been sent. The transfer is complete. + if (ctx->_state == SpiState::WaitingReset) { + // Stop DMA and SPI cleanly + ctx->hwStopTransfer(); + ctx->_state = SpiState::Idle; + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return; + } + + // Fill the completed buffer with next chunk of data (or zeroes for reset) + ctx->encodeSpiChunk(completedBuf); + + // If encodeSpiChunk transitioned to SendingLast, the NEXT buffer after this one is the reset pulse + // when tat EOF fires, we'll be in WaitingReset and stop the transfer + // TODO: this yields long reset pulses, which works but is slower than needed, check if this can be done like in I2S or if that breaks things (it did last time I tried) + + // Give ownership of the descriptor back to DMA so it can keep feeding SPI TODO: this may be unnecessary + ctx->_dmaDesc[completedBuf].eof = 1; + ctx->_dmaDesc[completedBuf].owner = 1; + portEXIT_CRITICAL_ISR(&ctx->_isrMux); +} + +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, WLEDPB_SPI_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, WLEDPB_SPI_DMA_BUFFER_SIZE); + } + + // Setup DMA descriptors - circular linked list + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].length = WLEDPB_SPI_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_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, SOC_GDMA_TRIG_PERIPH_SPI2); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + // gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + esp_err_t err = esp_intr_alloc(WLEDPB_SPI_GDMA_INTR_SOURCE, ESP_INTR_FLAG_LEVEL2, gdmaISR, this, &_gdmaIsrHandle); // note: saw no flickering even when using level1 + if (err != ESP_OK) { + //Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + deinit(); + return false; + } + + // 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_LEVEL2, 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[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + if (_hw) { + _hw->dma_int_ena.trans_done = 0; // Disable SPI trans_done interrupt + } + + if (_spiIsrHandle) { + esp_intr_free(_spiIsrHandle); + _spiIsrHandle = nullptr; + } + + if (_gdmaIsrHandle) { + esp_intr_free(_gdmaIsrHandle); + _gdmaIsrHandle = 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); + //gpio_matrix_out(pin, SPI_SIGNAL_INDICES[idx], inverted, false); // note: in IDF V5 this function is called esp_rom_gpio_connect_out_signal() + esp_rom_gpio_connect_out_signal(pin, SPI_SIGNAL_INDICES[idx], inverted, false); // TODO: this is the new command in IDF V5, works in V4 too? + + 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; + + // Total bits: 16 DMA bytes per source byte * 8 bits/byte = 128 bits per source byte + // Plus reset: extra zero bits at the end + uint32_t dataBits = _numBytes * 16 * 8; + uint32_t totalBits; + + if (dataBits + SPI_RESET_BITS > SPI_MAX_BITS) { + totalBits = SPI_MAX_BITS; // frame does not fit, truncate (cant send more than SPI_MAX_BITS, hardware limitation) + } else { + totalBits = dataBits + SPI_RESET_BITS; // frame fits into max transfer size + } + + // 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[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + spi_ll_clear_int_stat(_hw); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + + spi_ll_set_mosi_bitlen(_hw, totalBits); + + // 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 = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].length = WLEDPB_SPI_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, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].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; +} + +void SpiBusContext::hwStopTransfer() { + if (_hw) { + _hw->cmd.usr = 0; + _hw->dma_int_ena.val = 0; // Disable all SPI interrupts + } + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); +} + +void SpiBusContext::hwResetFifo() { + if (_hw) { + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + } +} + +/////////////////////////////////// +// 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 = (size_t)numPixels * numChannels; + 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(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { + 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..ea2014b6a4 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -0,0 +1,152 @@ +/*------------------------------------------------------------------------- + +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 +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_BUFFER_SIZE 2048 // 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) +#define WLEDPB_SPI_DMA_DESC_COUNT 2 // number of DMA buffers, 3 = tripple buffering to avoid SPI starvation under heavy load (TODO: 2 is probably more than enough, currently debugging 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) + +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 isSpiDone() 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 isSpiDone(); + bool isIdle() const { return _state == SpiState::Idle; } + void forceIdle(); // 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 void IRAM_ATTR gdmaISR(void* arg); + static void IRAM_ATTR spiISR(void* arg); + // Hardware control + void hwStopTransfer(); + void hwResetFifo(); + // State machine + 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]; + intr_handle_t _gdmaIsrHandle; + 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 + mutable uint32_t _lastTransmitMs; + // Staging: tracks which channels have provided data for the next frame + 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(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "SPI"; } +#endif + + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; + void setInverted(bool inv) override; + void setColorOrder(uint8_t co); + +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..27aee8bda3 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -0,0 +1,315 @@ +/*------------------------------------------------------------------------- + +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 and C3 +Supports auto-distribution of available RMT memory blocks to reduce interrupt frequency - needs to be refined if ever using RMT input +The glitch-free high priority interrupt implementation by @willmmiles is not available on the C3 + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" +#ifdef ARDUINO_ARCH_ESP32 +#include "WLEDpixelBus_RMT.h" + +namespace WLEDpixelBus { + +//============================================================================== +// RMT Bus Implementation +//============================================================================== + +// Per-channel context table - stored in DRAM, 4 byte aligned for ISR access +DMA_ATTR RmtBus::RmtContext RmtBus::s_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::s_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 +}; + +// Static auto-channel counter for RmtBus +uint8_t RmtBus::s_expectedChannels = 1; +uint8_t RmtBus::s_allocatedCount = 0; +uint8_t RmtBus::s_currentChannelIndex = 0; +uint8_t RmtBus::s_usedBlocks = 0; +uint8_t RmtBus::s_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) + , _usingRmtHi(false) + , _rmtChannel(RMT_CHANNEL_0) +{ + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +RmtBus::~RmtBus() { + end(); +} + +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); + + s_contexts[(int)_rmtChannel].bit0 = bit0.val; + s_contexts[(int)_rmtChannel].bit1 = bit1.val; + s_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 (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) + return false; + + uint8_t totalBlocks; +#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) // note: C6 RMT hardware is the same as C3 + totalBlocks = 4; // other supported ESP32 variants have 4 blocks +#else + totalBlocks = 4; // default to 4 if unknown, should be safe +#endif + + int left_channels = s_expectedChannels - s_allocatedCount - 1; + + if (left_channels == 0) { + _channel = s_currentChannelIndex; + blocksToUse = totalBlocks - s_usedBlocks; + } else { + int k = totalBlocks / s_expectedChannels; + int max_k_for_index = maxTxChannels - s_currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; + + _channel = s_currentChannelIndex; + blocksToUse = k; + } + #ifdef RMT_USE_SINGLE_MEM_BLOCK + blocksToUse = 1; + #endif + + s_currentChannelIndex += blocksToUse; + s_usedBlocks += blocksToUse; + s_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, s_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 + + // Try to use the High Priority RMT driver (Neo rmtHi) + // NOTE: rmtHi can deadlock on some cores (notably ESP32-C3). By default we disable it + // on C3 builds, but it can be enabled explicitly with -DWLEDPB_ENABLE_RMT_HI. + _usingRmtHi = false; +#if !defined(CONFIG_IDF_TARGET_ESP32C3) || defined(WLEDPB_ENABLE_RMT_HI) + esp_err_t hiErr = RmtHiDriver::Install(_rmtChannel, s_contexts[(int)_rmtChannel].bit0 , s_contexts[(int)_rmtChannel].bit1, s_contexts[(int)_rmtChannel].resetDuration, blocksToUse); + if (hiErr == ESP_OK) { + _usingRmtHi = true; + } else { + //DEBUG_PRINTF_P(PSTR("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n"), hiErr); + } +#endif + + if (!_usingRmtHi) { + // Fallback to 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, s_callbacks[(int)_rmtChannel]); + if (err != ESP_OK) { + rmt_driver_uninstall(_rmtChannel); + return false; + } + } + + // route the pin, use hardware signal inversion via GPIO matrix if _inverted + //gpio_matrix_out(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); // note: in IDF V5 this is called esp_rom_gpio_connect_out_signal() + esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); // TODO: this is the new command in IDF V5, works in V4 too? + + if (_initialized) esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); // TODO: this is the new command in IDF V5, works in V4 too? + + _initialized = true; + s_activeChannelMask |= (1 << _channel); + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } + return true; +} + +void RmtBus::end() { + if (!_initialized) return; + + s_activeChannelMask &= ~(1 << _channel); + if (_usingRmtHi) { + RmtHiDriver::Uninstall(_rmtChannel); + } else { + rmt_driver_uninstall(_rmtChannel); + } + 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; + _usingRmtHi = false; +} + +bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + // Encoding is done per-pixel in setPixel(); _encodeBuffer is ready to ship. + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + + const size_t dataLen = _encodeBufferSize; + esp_err_t err; + if (_usingRmtHi) { + 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 + } + return err; +} + +bool RmtBus::canShow() const { + if (!_initialized) return true; + if (_usingRmtHi) return (ESP_OK == RmtHiDriver::WaitForTxDone(_rmtChannel, 0)); // 0 timout means "poll and return immediately" + return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); +} + +void RmtBus::setColorOrder(uint8_t co) { + _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); +} + +//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 = s_contexts[channel].bit0; + const uint32_t bit1 = s_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 = s_contexts[channel].resetDuration; // set the last (low) pulse to reset duration + } + +// *translated_size = bytes_to_process; +// *item_num = bytes_to_process * 8; +} + +} // namespace WLEDpixelBus +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h new file mode 100644 index 0000000000..8778e6aafb --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -0,0 +1,122 @@ +/*------------------------------------------------------------------------- + +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 and C3 +Supports auto-distribution of available RMT memory blocks to reduce interrupt frequency - needs to be refined if ever using RMT input +The glitch-free high priority interrupt implementation by @willmmiles is not available on the C3 + +-------------------------------------------------------------------------*/ + +#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 "driver/rmt.h" +#include "RmtHIDriver.h" +#include "esp_rom_gpio.h" // for gpio routing to set inverted signal + +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(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "RMT"; } +#endif + + // Configuration + void setInverted(bool inv) override { + _inverted = inv; + } + void setColorOrder(uint8_t co); + + // Reset the auto-allocation counter (call before re-creating buses) + static void setExpectedChannels(uint8_t expected) { s_expectedChannels = (expected > 0) ? expected : 1; } + static void resetAutoChannel() { + s_allocatedCount = 0; + s_currentChannelIndex = 0; + s_usedBlocks = 0; + } + +private: + int8_t _pin; + int8_t _channel; + bool _inverted; + bool _initialized; + LedTiming _timing; + rmt_channel_t _rmtChannel; + bool _usingRmtHi; // TODO: use #ifdef only and remove this variable + + // _encodeBuffer and _encodeBufferSize are in PixelBus base + static uint8_t s_expectedChannels; // TODO: make none static + static uint8_t s_allocatedCount; + static uint8_t s_currentChannelIndex; + static uint8_t s_usedBlocks; + static uint8_t s_activeChannelMask; // bitmask of initialized channels + + 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 s_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 s_callbacks[WPB_RMT_CHANNELS]; +}; + +} // namespace WLEDpixelBus + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp new file mode 100644 index 0000000000..7e637f0c87 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -0,0 +1,255 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +supports ESP32, S3, S2 and C3 both hardware SPI and BitBanged output +supports hardware brightness on APA102 for improved color resolution + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" +#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 timing bitPeriod, clamped to [1 MHz, SPI_MAX_CLOCK_HZ]. +// bit timing is calculated in busmanager from requested _frequencykHz +// TODO: this could be done more efficiently by storing the value in kHz directly in t0h_ns +static uint32_t timingToClockHz(const LedTiming& t) { + const uint32_t periodNs = t.bitPeriod(); + if (periodNs == 0) return 2000000; + uint32_t hz = 1000000000UL / periodNs; + if (hz < 1000000) hz = 1000000; // minimum 1 MHz + if (hz > SPI_MAX_CLOCK_HZ) hz = SPI_MAX_CLOCK_HZ; + return hz; +} + +SpiBus::SpiBus(int8_t dataPin, int8_t clockPin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, bool useHardwareSpi, uint8_t ledType) + : _dataPin(dataPin), _clockPin(clockPin), _timing(timing), + _useHardware(useHardwareSpi), _initialized(false), _clockHz(0) { + _encoder = ColorEncoder(colorOrder, numChannels, ledType); + _ledType = ledType; +} + +SpiBus::~SpiBus() { + end(); +} + +bool SpiBus::begin() { + if (_initialized) return true; + + _clockHz = timingToClockHz(_timing); + + 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; + if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { 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 + sendByte(0x00); sendByte(0x00); sendByte(0x00); 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 uint16_t n = (numPixels + 15) / 16; + for (uint16_t i = 0; i < n; i++) sendByte(0x00); + } else if (_ledType == TYPE_LPD6803) { + const uint16_t n = (numPixels + 7) / 8; + for (uint16_t i = 0; i < n; i++) sendByte(0x00); + } else if (_ledType == TYPE_LPD8806) { + const uint16_t n = (numPixels + 31) / 32; + for (uint16_t i = 0; i < n; i++) sendByte(0xFF); + } else if (_ledType == TYPE_P9813) { + sendByte(0x00); sendByte(0x00); sendByte(0x00); sendByte(0x00); + } + // TYPE_WS2801: nothing to send +} + +bool SpiBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + + const uint8_t pixelBytes = _encoder.getPixelBytes(); + + if (_useHardware) { + // beginTransaction applies frequency, bit order, and SPI mode atomically. + // This is mandatory on ESP32 — settings applied outside a transaction are ignored by the hardware. + SPI.beginTransaction(SPISettings(_clockHz, MSBFIRST, SPI_MODE0)); + } + + sendStartFrame(_numPixels); + + if (_ledType == TYPE_APA102) { + // APA102 per-pixel wire format: [0xE0|brightness5bit, byte0, byte1, byte2] + // 0xE0|0x1F == 0xFF == full hardware brightness (5-bit field, 0x1F = max) + for (uint16_t i = 0; i < _numPixels; i++) { + sendByte(0xE0 | _apa102HwBri); + const uint8_t* src = _encodeBuffer + (size_t)i * pixelBytes; + for (uint8_t ch = 0; ch < pixelBytes; ch++) sendByte(src[ch]); + } + } else if (_ledType == TYPE_P9813) { + // P9813 per-pixel wire format: [flag, B, G, R] + // flag = 0xC0 | (~B[7:6]>>2) | (~G[7:6]>>4) | (~R[7:6]>>6) + // _encodeBuffer bytes are already in wire order (BGR = indices 0,1,2 when + // color order is configured as BGR, which is the P9813 native order). + for (uint16_t i = 0; i < _numPixels; i++) { + const uint8_t* src = _encodeBuffer + (size_t)i * pixelBytes; + const uint8_t b = src[0], g = src[1], r = src[2]; + const uint8_t flag = 0xC0 | ((~b & 0xC0) >> 2) | ((~g & 0xC0) >> 4) | ((~r & 0xC0) >> 6); + sendByte(flag); sendByte(b); sendByte(g); sendByte(r); + } + } else { + // WS2801 / LPD8806 / LPD6803: raw encoded bytes, no per-pixel framing byte + for (uint16_t i = 0; i < _numPixels; i++) { + const uint8_t* src = _encodeBuffer + (size_t)i * pixelBytes; + for (uint8_t ch = 0; ch < pixelBytes; ch++) sendByte(src[ch]); + } + } + + 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..4fbd415996 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -0,0 +1,84 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +supports ESP32, S3, S2 and C3 both hardware SPI and BitBanged output +supports hardware brightness on APA102 for improved color resolution + +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 LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, bool useHardwareSpi = true, uint8_t ledType = 0); + ~SpiBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) 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; + LedTiming _timing; + 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 + 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..adf2461e19 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -0,0 +1,107 @@ +/*------------------------------------------------------------------------- +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) +// it derives the total period from the given timings (0hi+0lo+1hi+1lo)/2) +// TODO: a better strategy might be to use 0hi*4 as the period as 0 timing is most critical + +/** + * 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 // TODO: globally limit this? RMT can do ~800us max (32767 ticks of 25ns) + + 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 as the average of all four pulse timings + constexpr uint32_t bitPeriod() const { + return (t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2; + } +}; + +/** + * 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 +// TYPE_CUSTOM_BUS (36) timing is passed directly as a LedTiming struct; no fixed entries here. +// TODO: could move these timing into the UI and always pass timings down to the firmware +}; + +// 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_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; // TODO: SPI types need testing + case TYPE_WS2801: return 12; + // TYPE_CUSTOM_BUS (36) timing is provided directly as a LedTiming* to create(). + default: return 0; // WS2812 fallback + } +} + +// Returns the LED timing for the given WLED bus type, read from flash. +// This is a one-time read at bus-creation; the bus constructor stores its own copy. +inline LedTiming getProtocol(uint8_t wledType) { + return s_ledTimings[getTimingIndex(wledType)]; +} + +} // namespace WLEDpixelBus diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 156a20f990..957fef67fc 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 03d4cd1101..be5d097703 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -334,8 +334,15 @@ void getSettingsJS(byte subPage, Print& settingsScript) settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str()); + #ifndef WLEDPB_LCD_DMA_BUFFER_SIZE + #define WLEDPB_LCD_DMA_BUFFER_SIZE 2048 + #endif + #ifndef WLEDPB_SPI_DMA_BUFFER_SIZE + #define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 + #endif + // 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, @@ -345,7 +352,11 @@ void getSettingsJS(byte subPage, Print& settingsScript) WLED_MAX_RMT_CHANNELS, WLED_MAX_I2S_CHANNELS, WLED_MAX_ANALOG_CHANNELS, - WLED_MAX_BUTTONS + WLED_MAX_BUTTONS, + WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, // TODO: use the same buffer size for all drivers? if possible do so and remove these additional defines + WLEDPB_LCD_DMA_BUFFER_SIZE, + WLEDPB_SPI_DMA_BUFFER_SIZE, + WLED_MAX_BB_CHANNELS ); printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments); @@ -373,6 +384,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) @@ -414,9 +426,36 @@ 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 + if ((bus->getType() & 0x7F) == TYPE_CUSTOM_BUS) { + const CustomBusConfig& cb = bus->getCustomBusConfig(); + 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; + 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);