diff --git a/.gitignore.optional b/.gitignore.optional new file mode 100644 index 0000000000..3d5ef497ee --- /dev/null +++ b/.gitignore.optional @@ -0,0 +1,8 @@ +.pio +.cache +.dummy +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +compile_commands.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5c5735c63b..411655e24d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format "recommendations": [ + "Jason2866.esp-decoder", "pioarduino.pioarduino-ide", "platformio.platformio-ide" ], diff --git a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h deleted file mode 100644 index 273933e3b8..0000000000 --- a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h +++ /dev/null @@ -1,471 +0,0 @@ -/*------------------------------------------------------------------------- -NeoPixel driver for ESP32 RMTs using High-priority Interrupt - -(NB. This cannot be mixed with the non-HI driver.) - -Written by Will M. Miles. - -I invest time and resources providing this open source code, -please support me by donating (see https://github.com/Makuna/NeoPixelBus) - -------------------------------------------------------------------------- -This file is part of the Makuna/NeoPixelBus library. - -NeoPixelBus is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as -published by the Free Software Foundation, either version 3 of -the License, or (at your option) any later version. - -NeoPixelBus is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with NeoPixel. If not, see -. --------------------------------------------------------------------------*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP32) -#if !defined(WLED_USE_SHARED_RMT) // V5 fix: don't compile this file on unsupported platforms - -// Use the NeoEspRmtSpeed types from the driver-based implementation -#include - - -namespace NeoEsp32RmtHiMethodDriver { - // Install the driver for a specific channel, specifying timing properties - esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration); - - // Remove the driver on a specific channel - esp_err_t Uninstall(rmt_channel_t channel); - - // Write a buffer of data to a specific channel. - // Buffer reference is held until write completes. - esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size); - - // Wait until transaction is complete. - esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time); -}; - -template class NeoEsp32RmtHIMethodBase -{ -public: - typedef NeoNoSettings SettingsObject; - - NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize) : - _sizeData(pixelCount * elementSize + settingsSize), - _pin(pin) - { - construct(); - } - - NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize, NeoBusChannel channel) : - _sizeData(pixelCount* elementSize + settingsSize), - _pin(pin), - _channel(channel) - { - construct(); - } - - ~NeoEsp32RmtHIMethodBase() - { - // wait until the last send finishes before destructing everything - // arbitrary time out of 10 seconds - ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS)); - - ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Uninstall(_channel.RmtChannelNumber)); - - gpio_matrix_out(_pin, SIG_GPIO_OUT_IDX, false, false); - pinMode(_pin, INPUT); - - free(_dataEditing); - free(_dataSending); - } - - bool IsReadyToUpdate() const - { - return (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT_SILENT_TIMEOUT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 0))); - } - - void Initialize() - { - rmt_config_t config = {}; - - config.rmt_mode = RMT_MODE_TX; - config.channel = _channel.RmtChannelNumber; - config.gpio_num = static_cast(_pin); - config.mem_block_num = 1; - config.tx_config.loop_en = false; - - config.tx_config.idle_output_en = true; - config.tx_config.idle_level = T_SPEED::IdleLevel; - - config.tx_config.carrier_en = false; - config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - - config.clk_div = T_SPEED::RmtClockDivider; - - ESP_ERROR_CHECK(rmt_config(&config)); // Uses ESP library - ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Install(_channel.RmtChannelNumber, T_SPEED::RmtBit0, T_SPEED::RmtBit1, T_SPEED::RmtDurationReset)); - } - - void Update(bool maintainBufferConsistency) - { - // wait for not actively sending data - // this will time out at 10 seconds, an arbitrarily long period of time - // and do nothing if this happens - if (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS))) - { - // now start the RMT transmit with the editing buffer before we swap - ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::Write(_channel.RmtChannelNumber, _dataEditing, _sizeData)); - - if (maintainBufferConsistency) - { - // copy editing to sending, - // this maintains the contract that "colors present before will - // be the same after", otherwise GetPixelColor will be inconsistent - memcpy(_dataSending, _dataEditing, _sizeData); - } - - // swap so the user can modify without affecting the async operation - std::swap(_dataSending, _dataEditing); - } - } - - bool AlwaysUpdate() - { - // this method requires update to be called only if changes to buffer - return false; - } - - bool SwapBuffers() - { - std::swap(_dataSending, _dataEditing); - return true; - } - - uint8_t* getData() const - { - return _dataEditing; - }; - - size_t getDataSize() const - { - return _sizeData; - } - - void applySettings([[maybe_unused]] const SettingsObject& settings) - { - } - -private: - const size_t _sizeData; // Size of '_data*' buffers - const uint8_t _pin; // output pin number - const T_CHANNEL _channel; // holds instance for multi channel support - - // Holds data stream which include LED color values and other settings as needed - uint8_t* _dataEditing; // exposed for get and set - uint8_t* _dataSending; // used for async send using RMT - - - void construct() - { - _dataEditing = static_cast(malloc(_sizeData)); - // data cleared later in Begin() - - _dataSending = static_cast(malloc(_sizeData)); - // no need to initialize it, it gets overwritten on every send - } -}; - -// normal -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsMethod; -typedef NeoEsp32RmtHINWs2805Method NeoEsp32RmtHINWs2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsMethod; -typedef NeoEsp32RmtHI0Ws2805Method NeoEsp32RmtHI0Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsMethod; -typedef NeoEsp32RmtHI1Ws2805Method NeoEsp32RmtHI1Ws2814Method; - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsMethod; -typedef NeoEsp32RmtHI2Ws2805Method NeoEsp32RmtHI2Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsMethod; -typedef NeoEsp32RmtHI3Ws2805Method NeoEsp32RmtHI3Ws2814Method; - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsMethod; -typedef NeoEsp32RmtHI4Ws2805Method NeoEsp32RmtHI4Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsMethod; -typedef NeoEsp32RmtHI5Ws2805Method NeoEsp32RmtHI5Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsMethod; -typedef NeoEsp32RmtHI6Ws2805Method NeoEsp32RmtHI6Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsMethod; -typedef NeoEsp32RmtHI7Ws2805Method NeoEsp32RmtHI7Ws2814Method; - -#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) -#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) - -// inverted -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsInvertedMethod; -typedef NeoEsp32RmtHINWs2805InvertedMethod NeoEsp32RmtHINWs2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsInvertedMethod; -typedef NeoEsp32RmtHI0Ws2805InvertedMethod NeoEsp32RmtHI0Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsInvertedMethod; -typedef NeoEsp32RmtHI1Ws2805InvertedMethod NeoEsp32RmtHI1Ws2814InvertedMethod; - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsInvertedMethod; -typedef NeoEsp32RmtHI2Ws2805InvertedMethod NeoEsp32RmtHI2Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsInvertedMethod; -typedef NeoEsp32RmtHI3Ws2805InvertedMethod NeoEsp32RmtHI3Ws2814InvertedMethod; - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsInvertedMethod; -typedef NeoEsp32RmtHI4Ws2805InvertedMethod NeoEsp32RmtHI4Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsInvertedMethod; -typedef NeoEsp32RmtHI5Ws2805InvertedMethod NeoEsp32RmtHI5Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsInvertedMethod; -typedef NeoEsp32RmtHI6Ws2805InvertedMethod NeoEsp32RmtHI6Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsInvertedMethod; -typedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod; - -#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) -#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) - -#endif -#endif diff --git a/lib/NeoESP32RmtHI/library.json b/lib/NeoESP32RmtHI/library.json deleted file mode 100644 index 0608e59e12..0000000000 --- a/lib/NeoESP32RmtHI/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "NeoESP32RmtHI", - "build": { "libArchive": false }, - "platforms": ["espressif32"], - "dependencies": [ - { - "owner": "makuna", - "name": "NeoPixelBus", - "version": "^2.8.3" - } - ] -} diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp b/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp deleted file mode 100644 index 6c046943b5..0000000000 --- a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp +++ /dev/null @@ -1,507 +0,0 @@ -/*------------------------------------------------------------------------- -NeoPixel library helper functions for Esp32. - -A BIG thanks to Andreas Merkle for the investigation and implementation of -a workaround to the GCC bug that drops method attributes from template methods - -Written by Michael C. Miller. - -I invest time and resources providing this open source code, -please support me by donating (see https://github.com/Makuna/NeoPixelBus) - -------------------------------------------------------------------------- -This file is part of the Makuna/NeoPixelBus library. - -NeoPixelBus is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as -published by the Free Software Foundation, either version 3 of -the License, or (at your option) any later version. - -NeoPixelBus is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with NeoPixel. If not, see -. --------------------------------------------------------------------------*/ - -#include - -#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) - -#include -#include "esp_idf_version.h" -#include "NeoEsp32RmtHIMethod.h" -#include "soc/soc.h" -#include "soc/rmt_reg.h" - -#ifdef __riscv -#include "riscv/interrupt.h" -#endif - - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) -#include "hal/rmt_ll.h" -#else -/* Shims for older ESP-IDF v3; we can safely assume original ESP32 */ -#include "soc/rmt_struct.h" - -// Selected RMT API functions borrowed from ESP-IDF v4.4.8 -// components/hal/esp32/include/hal/rmt_ll.h -// Copyright 2019 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -__attribute__((always_inline)) -static inline void rmt_ll_tx_reset_pointer(rmt_dev_t *dev, uint32_t channel) -{ - dev->conf_ch[channel].conf1.mem_rd_rst = 1; - dev->conf_ch[channel].conf1.mem_rd_rst = 0; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_start(rmt_dev_t *dev, uint32_t channel) -{ - dev->conf_ch[channel].conf1.tx_start = 1; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel) -{ - RMTMEM.chan[channel].data32[0].val = 0; - dev->conf_ch[channel].conf1.tx_start = 0; - dev->conf_ch[channel].conf1.mem_rd_rst = 1; - dev->conf_ch[channel].conf1.mem_rd_rst = 0; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->apb_conf.mem_tx_wrap_en = enable; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_enable_loop(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->conf_ch[channel].conf1.tx_conti_mode = enable; -} - -__attribute__((always_inline)) -static inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel) -{ - return dev->status_ch[channel]; -} - -__attribute__((always_inline)) -static inline void rmt_ll_tx_set_limit(rmt_dev_t *dev, uint32_t channel, uint32_t limit) -{ - dev->tx_lim_ch[channel].limit = limit; -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_interrupt(rmt_dev_t *dev, uint32_t mask, bool enable) -{ - if (enable) { - dev->int_ena.val |= mask; - } else { - dev->int_ena.val &= ~mask; - } -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->int_ena.val &= ~(1 << (channel * 3)); - dev->int_ena.val |= (enable << (channel * 3)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->int_ena.val &= ~(1 << (channel * 3 + 2)); - dev->int_ena.val |= (enable << (channel * 3 + 2)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) -{ - dev->int_ena.val &= ~(1 << (channel + 24)); - dev->int_ena.val |= (enable << (channel + 24)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel) -{ - dev->int_clr.val = (1 << (channel * 3)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel) -{ - dev->int_clr.val = (1 << (channel * 3 + 2)); -} - -__attribute__((always_inline)) -static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel) -{ - dev->int_clr.val = (1 << (channel + 24)); -} - - -__attribute__((always_inline)) -static inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev) -{ - uint32_t status = dev->int_st.val; - return (status & 0xFF000000) >> 24; -} -#endif - - -// ********************************* -// Select method for binding interrupt -// -// - If the Bluetooth driver has registered a high-level interrupt, piggyback on that API -// - If we're on a modern core, allocate the interrupt with the API (old cores are bugged) -// - Otherwise use the low-level hardware API to manually bind the interrupt - - -#if defined(CONFIG_BTDM_CTRL_HLI) -// Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls -#include "hal/interrupt_controller_hal.h" -extern "C" esp_err_t hli_intr_register(intr_handler_t handler, void* arg, uint32_t intr_reg, uint32_t intr_mask); - -#else /* !CONFIG_BTDM_CTRL_HLI*/ - -// Declare the our high-priority ISR handler -extern "C" void ld_include_hli_vectors_rmt(); // an object with an address, but no space - -#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) -#include "soc/periph_defs.h" -#endif - -// Select level flag -#if defined(__riscv) -// RISCV chips don't block interrupts while scheduling; all we need to do is be higher than the WiFi ISR -#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL3 -#elif defined(CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5) -#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL4 -#else -#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL5 -#endif - -// ESP-IDF v3 cannot enable high priority interrupts through the API at all; -// and ESP-IDF v4 on XTensa cannot enable Level 5 due to incorrect interrupt descriptor tables -#if !defined(__XTENSA__) || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) || ((ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) && CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5)) -#define NEOESP32_RMT_CAN_USE_INTR_ALLOC - -// XTensa cores require the assembly bridge -#ifdef __XTENSA__ -#define HI_IRQ_HANDLER nullptr -#define HI_IRQ_HANDLER_ARG ld_include_hli_vectors_rmt -#else -#define HI_IRQ_HANDLER NeoEsp32RmtMethodIsr -#define HI_IRQ_HANDLER_ARG nullptr -#endif - -#else -/* !CONFIG_BTDM_CTRL_HLI && !NEOESP32_RMT_CAN_USE_INTR_ALLOC */ -// This is the index of the LV5 interrupt vector - see interrupt descriptor table in idf components/hal/esp32/interrupt_descriptor_table.c -#define ESP32_LV5_IRQ_INDEX 26 - -#endif /* NEOESP32_RMT_CAN_USE_INTR_ALLOC */ -#endif /* CONFIG_BTDM_CTRL_HLI */ - - -// RMT driver implementation -struct NeoEsp32RmtHIChannelState { - uint32_t rmtBit0, rmtBit1; - uint32_t resetDuration; - - const byte* txDataStart; // data array - const byte* txDataEnd; // one past end - const byte* txDataCurrent; // current location - size_t rmtOffset; -}; - -// Global variables -#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) -static intr_handle_t isrHandle = nullptr; -#endif - -static NeoEsp32RmtHIChannelState** driverState = nullptr; -constexpr size_t rmtBatchSize = RMT_MEM_ITEM_NUM / 2; - -// Fill the RMT buffer memory -// This is implemented using many arguments instead of passing the structure object to ensure we do only one lookup -// All the arguments are passed in registers, so they don't need to be looked up again -static void IRAM_ATTR RmtFillBuffer(uint8_t channel, const byte** src_ptr, const byte* end, uint32_t bit0, uint32_t bit1, size_t* offset_ptr, size_t reserve) { - // We assume that (rmtToWrite % 8) == 0 - size_t rmtToWrite = rmtBatchSize - reserve; - rmt_item32_t* dest =(rmt_item32_t*) &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory - const byte* psrc = *src_ptr; - - *offset_ptr ^= rmtBatchSize; - - if (psrc != end) { - while (rmtToWrite > 0) { - uint8_t data = *psrc; - for (uint8_t bit = 0; bit < 8; bit++) - { - dest->val = (data & 0x80) ? bit1 : bit0; - dest++; - data <<= 1; - } - rmtToWrite -= 8; - psrc++; - - if (psrc == end) { - break; - } - } - - *src_ptr = psrc; - } - - if (rmtToWrite > 0) { - // Add end event - rmt_item32_t bit0_val = {{.val = bit0 }}; - *dest = rmt_item32_t {{{ .duration0 = 0, .level0 = bit0_val.level1, .duration1 = 0, .level1 = bit0_val.level1 }}}; - } -} - -static void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& state) { - // Reset context state - state.rmtOffset = 0; - - // Fill the first part of the buffer with a reset event - // FUTURE: we could do timing analysis with the last interrupt on this channel - // Use 8 words to stay aligned with the buffer fill logic - rmt_item32_t bit0_val = {{.val = state.rmtBit0 }}; - rmt_item32_t fill = {{{ .duration0 = 100, .level0 = bit0_val.level1, .duration1 = 100, .level1 = bit0_val.level1 }}}; - rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM.chan[channel].data32[0]; - for (auto i = 0; i < 7; ++i) dest[i] = fill; - fill.duration1 = state.resetDuration > 1400 ? (state.resetDuration - 1400) : 100; - dest[7] = fill; - - // Fill the remaining buffer with real data - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 8); - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); - - // Start operation - rmt_ll_clear_tx_thres_interrupt(&RMT, channel); - rmt_ll_tx_reset_pointer(&RMT, channel); - rmt_ll_tx_start(&RMT, channel); -} - -extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { - // Tx threshold interrupt - uint32_t status = rmt_ll_get_tx_thres_interrupt_status(&RMT); - while (status) { - uint8_t channel = __builtin_ffs(status) - 1; - if (driverState[channel]) { - // Normal case - NeoEsp32RmtHIChannelState& state = *driverState[channel]; - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); - } else { - // Danger - another driver got invoked? - rmt_ll_tx_stop(&RMT, channel); - } - rmt_ll_clear_tx_thres_interrupt(&RMT, channel); - status = rmt_ll_get_tx_thres_interrupt_status(&RMT); - } -}; - -// Wrapper around the register analysis defines -// For all currently supported chips, this is constant for all channels; but this is not true of *all* ESP32 -static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) { - uint32_t v; - switch(channel) { -#ifdef RMT_STATE_CH0 - case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break; -#endif -#ifdef RMT_STATE_CH1 - case 1: v = (status >> RMT_STATE_CH1_S) & RMT_STATE_CH1_V; break; -#endif -#ifdef RMT_STATE_CH2 - case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break; -#endif -#ifdef RMT_STATE_CH3 - case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break; -#endif -#ifdef RMT_STATE_CH4 - case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break; -#endif -#ifdef RMT_STATE_CH5 - case 5: v = (status >> RMT_STATE_CH5_S) & RMT_STATE_CH5_V; break; -#endif -#ifdef RMT_STATE_CH6 - case 6: v = (status >> RMT_STATE_CH6_S) & RMT_STATE_CH6_V; break; -#endif -#ifdef RMT_STATE_CH7 - case 7: v = (status >> RMT_STATE_CH7_S) & RMT_STATE_CH7_V; break; -#endif - default: v = 0; - } - - return v != 0; -} - - -esp_err_t NeoEsp32RmtHiMethodDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset) { - // Validate channel number - if (channel >= RMT_CHANNEL_MAX) { - return ESP_ERR_INVALID_ARG; - } - - esp_err_t err = ESP_OK; - if (!driverState) { - // First time init - driverState = reinterpret_cast(heap_caps_calloc(RMT_CHANNEL_MAX, sizeof(NeoEsp32RmtHIChannelState*), MALLOC_CAP_INTERNAL)); - if (!driverState) return ESP_ERR_NO_MEM; - - // Ensure all interrupts are cleared before binding - RMT.int_ena.val = 0; - RMT.int_clr.val = 0xFFFFFFFF; - - // Bind interrupt handler -#if defined(CONFIG_BTDM_CTRL_HLI) - // Bluetooth driver has taken the empty high-priority interrupt. Fortunately, it allows us to - // hook up another handler. - err = hli_intr_register(NeoEsp32RmtMethodIsr, nullptr, (uintptr_t) &RMT.int_st, 0xFF000000); - // 25 is the magic number of the bluetooth ISR on ESP32 - see soc/soc.h. - intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25); - intr_cntrl_ll_enable_interrupts(1<<25); -#elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) - // Use the platform code to allocate the interrupt - // If we need the additional assembly bridge, we pass it as the "arg" to the IDF so it gets linked in - err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, HI_IRQ_HANDLER, (void*) HI_IRQ_HANDLER_ARG, &isrHandle); - //err = ESP_ERR_NOT_FINISHED; -#else - // Broken IDF API does not allow us to reserve the interrupt; do it manually - static volatile const void* __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt; - intr_matrix_set(xPortGetCoreID(), ETS_RMT_INTR_SOURCE, ESP32_LV5_IRQ_INDEX); - ESP_INTR_ENABLE(ESP32_LV5_IRQ_INDEX); -#endif - - if (err != ESP_OK) { - heap_caps_free(driverState); - driverState = nullptr; - return err; - } - } - - if (driverState[channel] != nullptr) { - return ESP_ERR_INVALID_STATE; // already in use - } - - NeoEsp32RmtHIChannelState* state = reinterpret_cast(heap_caps_calloc(1, sizeof(NeoEsp32RmtHIChannelState), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); - if (state == nullptr) { - return ESP_ERR_NO_MEM; - } - - // Store timing information - state->rmtBit0 = rmtBit0; - state->rmtBit1 = rmtBit1; - state->resetDuration = reset; - - // Initialize hardware - rmt_ll_tx_stop(&RMT, channel); - rmt_ll_tx_reset_pointer(&RMT, channel); - rmt_ll_enable_tx_err_interrupt(&RMT, channel, false); - rmt_ll_enable_tx_end_interrupt(&RMT, channel, false); - rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); - rmt_ll_clear_tx_err_interrupt(&RMT, channel); - rmt_ll_clear_tx_end_interrupt(&RMT, channel); - rmt_ll_clear_tx_thres_interrupt(&RMT, channel); - - rmt_ll_tx_enable_loop(&RMT, channel, false); - rmt_ll_tx_enable_pingpong(&RMT, channel, true); - rmt_ll_tx_set_limit(&RMT, channel, rmtBatchSize); - - driverState[channel] = state; - - rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true); - - return err; -} - -esp_err_t NeoEsp32RmtHiMethodDriver::Uninstall(rmt_channel_t channel) { - if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; - - NeoEsp32RmtHIChannelState* state = driverState[channel]; - - WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); - - // Done or not, we're out of here - rmt_ll_tx_stop(&RMT, channel); - rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); - driverState[channel] = nullptr; - heap_caps_free(state); - -#if !defined(CONFIG_BTDM_CTRL_HLI) /* Cannot unbind from bluetooth ISR */ - // Turn off the driver ISR and release global state if none are left - for (uint8_t channelIndex = 0; channelIndex < RMT_CHANNEL_MAX; ++channelIndex) { - if (driverState[channelIndex]) return ESP_OK; // done - } - -#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) - esp_intr_free(isrHandle); -#else - ESP_INTR_DISABLE(ESP32_LV5_IRQ_INDEX); -#endif - - heap_caps_free(driverState); - driverState = nullptr; -#endif /* !defined(CONFIG_BTDM_CTRL_HLI) */ - - return ESP_OK; -} - -esp_err_t NeoEsp32RmtHiMethodDriver::Write(rmt_channel_t channel, const uint8_t *src, size_t src_size) { - if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; - - NeoEsp32RmtHIChannelState& state = *driverState[channel]; - esp_err_t result = WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); - - if (result == ESP_OK) { - state.txDataStart = src; - state.txDataCurrent = src; - state.txDataEnd = src + src_size; - RmtStartWrite(channel, state); - } - return result; -} - -esp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickType_t wait_time) { - if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; - - NeoEsp32RmtHIChannelState& state = *driverState[channel]; - // yield-wait until wait_time - esp_err_t rv = ESP_OK; - uint32_t status; - while(1) { - status = rmt_ll_tx_get_channel_status(&RMT, channel); - if (!_RmtStatusIsTransmitting(channel, status)) break; - if (wait_time == 0) { rv = ESP_ERR_TIMEOUT; break; }; - - TickType_t sleep = std::min(wait_time, (TickType_t) 5); - vTaskDelay(sleep); - wait_time -= sleep; - }; - - return rv; -} - -#endif diff --git a/platformio.ini b/platformio.ini index e7444d3038..bda416aa00 100644 --- a/platformio.ini +++ b/platformio.ini @@ -197,6 +197,7 @@ build_unflags = ${common.build_unflags} custom_usermods = build_flags = -DESP8266 + -DWLED_DISABLE_GLOBAL_PIXELBUFFER -DFP_IN_IROM ;-Wno-deprecated-declarations ;-Wno-register ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C @@ -230,6 +231,7 @@ monitor_filters = esp8266_exception_decoder ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 build_flags_compat = -DESP8266 + -DWLED_DISABLE_GLOBAL_PIXELBUFFER -DFP_IN_IROM ;;-Wno-deprecated-declarations -Wno-misleading-indentation @@ -253,7 +255,7 @@ lib_deps_compat = https://github.com/me-no-dev/ESPAsyncUDP.git#697c75a0255e5dccf91b4ef8b5cc28c569b3bac9 ESP8266PWM IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.9 + https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f https://github.com/Aircoookie/ESPAsyncWebServer.git#2d3af2c32b1dd1a8fd9fb01d7b51fa3653221a89 ;; v2.4.0 (pinned by commit to avoid PIO annotated-tag checkout warning) @@ -346,7 +348,6 @@ lib_deps = ${esp32_all_variants.lib_deps} https://github.com/netmindz/esp_dmx/#ed12a2909960c92d7f144d368564e2473bd38c63 ;; esp-idf-v5-fixes branch, pinned; TODO: need an upstream update for V5 ${env.lib_deps} - NeoPixelBus = git+https://github.com/Makuna/NeoPixelBus#76afe832f74b0738a3fa1bba0caf389ade9e7693 ;; CORE3 branch ;; AsyncMqttClient = git+https://github.com/marvinroger/async-mqtt-client#3d93fc7f662e65366f8e2b0d88b108f874f035b9 ;; override to use latest commmit from Sep 25, 2024 lib_ignore = NeoESP32RmtHI @@ -1069,8 +1070,8 @@ lib_deps = ${esp32s2.lib_deps} lib_ignore = ${esp32s2.lib_ignore} [env:usermods] -extends = env:esp32dev_V4 ; ToDO: fix usermods build once the main V5 build works without errors and warnings -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" +extends = env:esp32dev +build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" -DTOUCH_CS=9 custom_usermods = * ; Expands to all usermods in usermods folder board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp index 4e4742ad1d..218dd0af5d 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp @@ -60,8 +60,8 @@ class RgbRotaryEncoderUsermod : public Usermod std::fill(std::begin(_pins), std::end(_pins), 255); // …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 + BusConfig busCfg(TYPE_WS2812_RGB, _pins, 0, numLeds); + 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 9eca16dfef..f77b25ab01 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -90,11 +90,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define MAX_NUM_SEGMENTS 32 #define MAX_SEGMENT_DATA (20*1024) // 20k by default (S2 is short on free RAM), limit does not apply if PSRAM is available #else - #ifdef BOARD_HAS_PSRAM - #define MAX_NUM_SEGMENTS 64 - #else - #define MAX_NUM_SEGMENTS 32 - #endif + #define MAX_NUM_SEGMENTS 64 #define MAX_SEGMENT_DATA (64*1024) // 64k by default, limit does not apply if PSRAM is available #endif @@ -102,8 +98,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex() assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) -#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) - #define NUM_COLORS 3 /* number of colors per segment */ #define SEGMENT (*strip._currentSegment) #define SEGENV (*strip._currentSegment) @@ -874,7 +868,7 @@ class WS2812FX { _pixels(nullptr), _pixelCCT(nullptr), _suspend(false), - _brightness(DEFAULT_BRIGHTNESS), + _brightness16(DEFAULT_BRIGHTNESS << 8), _length(DEFAULT_LED_COUNT), _transitionDur(750), _frametime(FRAMETIME_FIXED), @@ -920,6 +914,7 @@ class WS2812FX { service(), // executes effect functions when due and calls strip.show() setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K) setBrightness(uint8_t b, bool direct = false), // sets strip brightness + setBrightness16(uint16_t b, bool direct = false), // sets Q8.8 strip brightness setRange(uint16_t i, uint16_t i2, uint32_t col), // used for clock overlay purgeSegments(), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) setMainSegmentId(unsigned n = 0), @@ -933,7 +928,13 @@ class WS2812FX { waitForIt(); // wait until frame is over (service() has finished or time for 1 frame has passed) void setRealtimePixelColor(unsigned i, uint32_t c); - inline void setPixelColor(unsigned n, uint32_t c) const { if (n < getLengthTotal()) _pixels[n] = c; } // paints absolute strip pixel with index n and color c + inline void setPixelColor(unsigned n, uint32_t c) const { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + n = getMappedPixelIndex(n); if (n < getLengthTotal()) BusManager::setPixelColor(n, c); + #else + if (n < getLengthTotal()) _pixels[n] = c; + #endif + } // paints absolute strip pixel with index n and color c inline void resetTimebase() { timebase = 0UL - millis(); } inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const { setPixelColor(n, RGBW32(r,g,b,w)); } @@ -973,7 +974,8 @@ class WS2812FX { uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const; uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list - mode_name must point to PROGMEM; defined in FX.cpp; - inline uint8_t getBrightness() const { return _brightness; } // returns current strip brightness + inline uint8_t getBrightness() const { return _brightness16 >> 8; } // returns current strip brightness + inline uint16_t getBrightness16() const { return _brightness16; } inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) @@ -995,8 +997,20 @@ class WS2812FX { }; unsigned long now, timebase; - inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds - inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table + inline uint32_t getPixelColor(unsigned n) const { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + n = getMappedPixelIndex(n); return n < getLengthTotal() ? BusManager::getPixelColor(n) : 0; + #else + return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; + #endif + } // returns color of pixel n, black if out of (mapped) bounds + inline uint32_t getPixelColorNoMap(unsigned n) const { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + return (n < getLengthTotal()) ? BusManager::getPixelColor(n) : 0; + #else + return (n < getLengthTotal()) ? _pixels[n] : 0; + #endif + } // ignores mapping table inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } @@ -1058,7 +1072,7 @@ class WS2812FX { volatile bool _suspend; - uint8_t _brightness; + uint16_t _brightness16; uint16_t _length; uint16_t _transitionDur; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 8e09bd9fa5..9c869e3ab4 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1331,89 +1331,60 @@ void WS2812FX::finalizeInit() { _hasWhiteChannel = _isOffRefreshRequired = false; BusManager::removeAll(); // TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers. - unsigned digitalCount = 0; - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_HAS_PARALLEL_I2S) - // validate the bus config: count I2S buses and check if they meet requirements - unsigned i2sBusCount = 0; - - for (const auto &bus : busConfigs) { - if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { - digitalCount++; - if (bus.driverType == 1) - i2sBusCount++; + // First assign driver types (RMT/I2S/... channels) for all buses before any are constructed. If hardware allocation fails + // (no free channels, no fallback) the bus is still constructed below and might fail construction. + // BusManager::add() replaces failed buses with a BusPlaceholder, edge cases of bad configs can slip through. + #if defined(ARDUINO_ARCH_ESP32) + unsigned rmtBusCount = 0; + unsigned parHwBusCount = 0; // used for debug print only + #endif + for (auto &bus : busConfigs) { + if (!Bus::isDigital(bus.type)) continue; + if (!BusManager::allocateHardware(bus.type, bus.pins, bus.driverType)) { + DEBUG_PRINTLN(F("Bus driver allocation failed.")); + continue; // TODO: avoid constructing this bus when no driver could be allocated? } + #if defined(ARDUINO_ARCH_ESP32) + // Count buses that will occupy an RMT channel (driverType is final after allocateHardware()) + if (!Bus::is2Pin(bus.type)) { + if (bus.driverType == BUSDRV_RMT) rmtBusCount++; + else if (bus.driverType == BUSDRV_PARHW) parHwBusCount++; // parallel bus: I2S/LCD/SPI/PARLIO depending on chip (hardware driven, not BB) + } + #endif } - DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount); - // Determine parallel vs single I2S usage (used for memory calculation only) - bool useParallelI2S = false; - #if defined(CONFIG_IDF_TARGET_ESP32S3) - // ESP32-S3 always uses parallel LCD driver for I2S - if (i2sBusCount > 0) { - useParallelI2S = true; - } - #else - if (i2sBusCount > 1) { - useParallelI2S = true; - } - #endif + #if defined(ARDUINO_ARCH_ESP32) + DEBUG_PRINTF_P(PSTR("Digital RMT buses: %u, parallel buses: %u\n"), rmtBusCount, parHwBusCount); + // RMT channel tracking was already reset by BusManager::removeAll() -> resetChannelTracking() + BusManager::setExpectedRmtChannelCount((uint8_t)rmtBusCount); #endif DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); - // create buses/outputs - unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers - unsigned I2SdmaMem = 0; + // Now construct each bus. BusManager::add() automatically falls back to a BusPlaceholder if it fails for (auto &bus : busConfigs) { - // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels - // store the result in iType for later use during bus creation (getI() must only be called once per BusConfig) - // note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage - bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType); - } - for (auto &bus : busConfigs) { - bool use_placeholder = false; - unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer) - mem += busMemUsage; - // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) - #if defined(WLED_HAS_PARALLEL_I2S) - bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT - if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) { - #ifdef NPB_CONF_4STEP_CADENCE - constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit) - #else - constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit) - #endif - unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1)); - if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses) - if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem; - } - #endif - if (mem + I2SdmaMem > MAX_LED_MEMORY + 1024) { // +1k to allow some margin to not drop buses that are allowed in UI (calculation here includes bus overhead) - DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count); - errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus - use_placeholder = true; - } - if (BusManager::add(bus, use_placeholder) != -1) { - mem += BusManager::busses.back()->getBusSize(); - if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count - } + BusManager::add(bus, false); } - 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 + // This must be done after all buses have been created, as parallel buses interact. + bus->begin(); + if (!bus->isOk()) { + DEBUG_PRINTF_P(PSTR("Bus %u failed hardware initialization.\n"), (unsigned)i); + continue; // TODO: preserve the original configuration when late initialization fails? + } //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. _isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; - // This must be done after all buses have been created, as some kinds (parallel I2S) interact - bus->begin(); - bus->setBrightness(scaledBri(bri)); + bus->setBrightness16(scaledBri16(bri<<8)); } BusManager::initializeABL(); // init brightness limiter DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); @@ -1434,11 +1405,17 @@ void WS2812FX::finalizeInit() { // update global _pixels[] buffer to match getLengthTotal() note: if allocation fails, WLED will not render anything void WS2812FX::updatePixelBuffer() { - uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t); p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it + _pixels = nullptr; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // Skip the global framebuffer: blendSegment() encodes directly into bus encode buffers. + // This saves N*4 bytes of heap (e.g. 1200B for 300 LEDs, 2000B for 500 LEDs). + #else + uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t); // use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer _pixels = static_cast(allocate_buffer(requiredMem, BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR)); DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), requiredMem); + #endif } void WS2812FX::service() { @@ -1589,6 +1566,27 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } const bool hasGrouping = topSegment.groupLength() != 1; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // Direct-to-bus mode: read/write pixels through the bus encode buffer instead of _pixels[]. + // CCT is set per-segment (not per-pixel) — acceptable trade-off for the RAM saved. + // Gamma is applied inline when writing; multi-segment blending occurs in gamma-compressed space. + if (!cctFromRgb) BusManager::setSegmentCCT(cct, correctWB); + const auto blendPixelIntoFrame = [&](size_t idx, uint32_t newC, uint8_t o) { + unsigned physIdx = getMappedPixelIndex(idx); + uint32_t result; + // Fast path: fully-opaque top layer — result is just newC, no readback needed. + // This covers the dominant case (single segment, full brightness, default blend mode) + // and avoids an expensive bus decode call per pixel for fast effects like Palette. + if (o == 255 && blendMode == 0) { + result = newC; + } else { + uint32_t existing = BusManager::getPixelColor(physIdx); + result = color_blend(existing, segblend(newC, existing), o); + } + BusManager::setPixelColor(physIdx, result); + }; + #endif + // fast path: handle the default case - no transitions, no grouping/spacing, no mirroring, no CCT if (!segO && style == TRANSITION_FADE && !hasGrouping && !topSegment.mirror && !topSegment.mirror_y) { if (isMatrix && stopIndx <= matrixSize && !_pixelCCT) { @@ -1605,12 +1603,18 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { if (topSegment.reverse_y) { start_offset += (height - 1) * Segment::maxWidth; y_inc = -Segment::maxWidth; } for (int y = 0; y < height; y++) { + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER uint32_t* pRow = &_pixels[start_offset + y * y_inc]; + #endif const int y_width = y * width; for (int x = 0; x < width; x++) { - uint32_t* p = pRow + x * x_inc; uint32_t c_a = topSegment.getPixelColorRaw(x + y_width); + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame((size_t)(start_offset + y * y_inc + x * x_inc), c_a, opacity); + #else + uint32_t* p = pRow + x * x_inc; *p = color_blend(*p, segblend(c_a, *p), opacity); + #endif } } } else { // transposed @@ -1620,7 +1624,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const int py = topSegment.reverse_y ? (width - x - 1) : x; // source pixel: swap x into y, reverse if needed const uint32_t c_a = topSegment.getPixelColorRaw(px + py * height); // height = virtual width const size_t idx = XY(topSegment.start + x, topSegment.startY + y); // write logical (non swapped) pixel coordinate + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame(idx, c_a, opacity); + #else _pixels[idx] = color_blend(_pixels[idx], segblend(c_a, _pixels[idx]), opacity); + #endif } } } @@ -1628,7 +1636,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { #endif } else if (!isMatrix) { // 1D fast path, include CCT as it is more common on 1D setups + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER uint32_t* strip = _pixels; + #endif int start = topSegment.start; int off = topSegment.offset; for (int i = 0; i < length; i++) { @@ -1636,8 +1646,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { int p = topSegment.reverse ? (length - i - 1) : i; int idx = start + p + off; if (idx >= topSegment.stop) idx -= length; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame((size_t)idx, c_a, opacity); + #else strip[idx] = color_blend(strip[idx], segblend(c_a, strip[idx]), opacity); if (_pixelCCT) _pixelCCT[idx] = cct; + #endif } return; } @@ -1716,8 +1730,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const int baseX = topSegment.start + x; const int baseY = topSegment.startY + y; size_t indx = XY(baseX, baseY); // absolute address on strip + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + blendPixelIntoFrame(indx, c, o); + #else _pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx]), o); if (_pixelCCT) _pixelCCT[indx] = cct; + #endif // Apply mirroring if enabled if (topSegment.mirror || topSegment.mirror_y) { const int mirrorX = topSegment.start + width - x - 1; @@ -1725,6 +1743,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY); const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY); const size_t idxMM = XY(mirrorX, mirrorY); + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + if (topSegment.mirror) blendPixelIntoFrame(idxMX, c, o); + if (topSegment.mirror_y) blendPixelIntoFrame(idxMY, c, o); + if (topSegment.mirror && topSegment.mirror_y) blendPixelIntoFrame(idxMM, c, o); + #else if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], segblend(c, _pixels[idxMX]), o); if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], segblend(c, _pixels[idxMY]), o); if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], segblend(c, _pixels[idxMM]), o); @@ -1733,6 +1756,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 } }; @@ -1808,6 +1832,18 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) { int indx = topSegment.start + i; + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + // direct-to-bus: apply mirroring then encode + if (topSegment.mirror) { + unsigned indxM = topSegment.stop - i - 1; + indxM += topSegment.offset; // offset/phase + if (indxM >= topSegment.stop) indxM -= length; // wrap + blendPixelIntoFrame(indxM, c, o); + } + indx += topSegment.offset; // offset/phase + if (indx >= topSegment.stop) indx -= length; // wrap + blendPixelIntoFrame((size_t)indx, c, o); + #else // Apply mirroring if (topSegment.mirror) { unsigned indxM = topSegment.stop - i - 1; @@ -1820,6 +1856,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/ @@ -1861,46 +1898,77 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } void WS2812FX::show() { + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER if (!_pixels) { DEBUGFX_PRINTLN(F("Error: no _pixels!")); errorFlag = ERR_NORAM; return; // no pixels allocated, nothing to show } - + #endif unsigned long showNow = millis(); size_t diff = showNow - _lastShow; + // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode + bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); size_t totalLen = getLengthTotal(); - // WARNING: as WLED doesn't handle CCT on pixel level but on Segment level instead - // we need to keep track of each pixel's CCT when blending segments (if CCT is present) - // and then set appropriate CCT from that pixel during paint (see below). - if ((hasCCTBus() || correctWB) && !cctFromRgb) - _pixelCCT = static_cast(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); // allocate CCT buffer if necessary, prefer PSRAM - if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT + // CCT per-pixel tracking: only needed when multiple active segments have *different* CCT values. + // When all segments share the same CCT we skip the allocation entirely and set Bus::_cct once + // before the paint loop — identical to the original per-pixel-change path but without the buffer. + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER + uint8_t uniformCCT = 127; // neutral 50:50 CCT; used when no buffer is allocated + if ((hasCCTBus() || correctWB) && !cctFromRgb) { + int16_t firstCCT = -1; // -1 = "no active segment seen yet" + for (const Segment &seg : _segments) { + if (!seg.isActive() || (!seg.on && !seg.isInTransition())) continue; + uint8_t segCCT = seg.currentCCT(); + if (firstCCT < 0) { + firstCCT = segCCT; + uniformCCT = segCCT; + } else if ((uint8_t)firstCCT != segCCT) { + // segments have different CCT values — need per-pixel buffer + _pixelCCT = static_cast(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); + if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT + break; + } + } + } + #endif if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) { + #ifdef WLED_DISABLE_GLOBAL_PIXELBUFFER + while (!BusManager::canAllShow()) yield(); // wait for all buses to finish sending the previous frame + // Direct-to-bus mode: clear bus encode buffers then blend segments directly into them. + // Per-pixel CCT tracking is not supported; CCT is set per-segment in blendSegment(). + if (cctFromRgb) BusManager::setSegmentCCT(-1); // set once; blendSegment skips CCT if cctFromRgb + BusManager::clearPixels(totalLen); + for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { + blendSegment(seg); + } + #else // clear frame buffer memset(_pixels, 0, sizeof(uint32_t) * totalLen); // blend all segments into (cleared) buffer for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { blendSegment(seg); // blend segment's buffer into frame buffer } + #endif } // Paint overlays in to pixel buffer handleOverlayDraw(); + #ifndef WLED_DISABLE_GLOBAL_PIXELBUFFER + while (!BusManager::canAllShow()) yield(); // wait for all buses to finish sending the previous frame // paint actual pixels int oldCCT = Bus::getCCT(); // store original CCT value (since it is global) // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) if (cctFromRgb) BusManager::setSegmentCCT(-1); - // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode - bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); + else if (!_pixelCCT) BusManager::setSegmentCCT(uniformCCT, correctWB); // uniform CCT: set once before loop for (size_t i = 0; i < totalLen; i++) { // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then // correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio - if (_pixelCCT) { // cctFromRgb already exluded at allocation + if (_pixelCCT) { // cctFromRgb already excluded at allocation if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB); } @@ -1913,10 +1981,11 @@ void WS2812FX::show() { p_free(_pixelCCT); _pixelCCT = nullptr; + #else + // TODO: implement bus level gamma, see PR #5722 + #endif - // some buses send asynchronously and this method will return before - // all of the data has been sent. - // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods + // pass the pixels on to the buses and start sending them out BusManager::show(); if (diff > 0) { // skip calculation if no time has passed @@ -1985,16 +2054,25 @@ 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 (_brightness == b) return; - _brightness = b; - if (_brightness == 0) { // unfreeze all segments on power off and stop all ongoing segment transitions + setBrightness16((uint16_t)b << 8, direct); +} + +void WS2812FX::setBrightness16(uint16_t b, bool direct) { + const uint16_t maxBri16 = 255U << 8; + if (b > maxBri16) b = maxBri16; // safety clamp just in case + if (gammaCorrectBri && b > 0) { + b = (int)(powf((float)b / maxBri16, gammaCorrectVal) * maxBri16 + 0.5f); // use float math not the gamma LUT to preserve 16bit precision + if (b < (1U << 8)) b = 1U << 8; // prevent 0 brightness (after shifting) so LEDs do not turn off + } + if (_brightness16 == b) return; + _brightness16 = b; + if (_brightness16 == 0) { // unfreeze all segments on power off and stop all ongoing segment transitions for (Segment &seg : _segments) { seg.freeze = false; // freeze is mutable seg.stopTransition(); // stop transition, nothing to display anymore } } - BusManager::setBrightness(scaledBri(b)); + BusManager::setBrightness16(scaledBri16(b)); if (!direct) { unsigned long t = millis(); if (t - _lastShow > min(_frametime, uint16_t(FRAMETIME_FIXED))) trigger(); //apply brightness change immediately if no refresh soon, but don't speed up above 42fps diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 30f613bdbc..9f8051eeec 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1942,4 +1942,4 @@ static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const return rb | g; } -#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) \ No newline at end of file diff --git a/wled00/bus_config.h b/wled00/bus_config.h new file mode 100644 index 0000000000..6db178c7e7 --- /dev/null +++ b/wled00/bus_config.h @@ -0,0 +1,118 @@ +#pragma once +#ifndef BusConfig_h +#define BusConfig_h + +#include "const.h" +#include "src/WLEDpixelBus/WLEDpixelBus.h" // MAX_CUSTOM_CHANNELS, CH_*, LedTiming + +// Bus configuration structures and helpers (formerly part of bus_manager) +//========================================================================== + +constexpr bool busTypeHasRGB(uint8_t type) { + return !((type >= TYPE_WS2812_1CH_X3 && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); +} +constexpr bool busTypeHasWhite(uint8_t type) { + 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 +} +constexpr bool busTypeHasCCT(uint8_t type) { + return type == TYPE_WS2812_WWA || type == TYPE_SM16825 || + type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || + type == TYPE_FW1906 || type == TYPE_WS2805; +} +constexpr bool busTypeIsValid(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type < 128); } +constexpr bool busTypeIs2Pin(uint8_t type) { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); } +constexpr bool busTypeIsDigital(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || busTypeIs2Pin(type); } +constexpr bool busTypeIsOnOff(uint8_t type) { return (type == TYPE_ONOFF); } +constexpr bool busTypeIsPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } +constexpr bool busTypeIsVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } +constexpr bool busTypeIsHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); } +constexpr bool busTypeIs16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } +constexpr bool busTypeMustRefresh(uint8_t type) { return type == TYPE_TM1814 || type == TYPE_TM1815; } +constexpr int busTypeNumPWMPins(uint8_t type) { return (type - 40); } + +constexpr size_t busTypeGetNumberOfPins(uint8_t type) { + // credit @PaoloTK; for HUB75 the 5 slots store config params (panelW, panelH, chain, rows, cols), not GPIO pins + return busTypeIsVirtual(type) ? 4 : busTypeIsPWM(type) ? busTypeNumPWMPins(type) : busTypeIsHub75(type) ? 5 : busTypeIs2Pin(type) + 1; +} + +constexpr size_t busTypeGetNumberOfChannels(uint8_t type) { + // fixed-map legacy types (1CH_X3, WWA) always drive 3 wire channels, see resolveChannelLayout() + return (type == TYPE_WS2812_1CH_X3 || type == TYPE_WS2812_WWA) ? 3 : (busTypeHasWhite(type) + 3*busTypeHasRGB(type) + busTypeHasCCT(type)); +} + +// Driver preference for digital LED buses: stored in BusConfig::driverType and BusDigital::_driverType +enum BusDriverType : uint8_t { + BUSDRV_RMT = 0, // RMT peripheral (default on most ESP32 variants) + BUSDRV_PARHW = 1, // parallel output (chip-dependent): ESP32/S2/S3 use I2S-LCD, C3 uses parallel SPI, C6/H2/C5/P4 use PARLIO + BUSDRV_BITBANG = 2, // parallel bit-bang GPIO (ESP32 only) +}; + +// Channel layout & protocol timing for digital LED buses +// WW/CW are not treated differently if not both are set (see #5654 for reference). +struct BusChannelLayout { + uint8_t numChannels = 0; // wire channels per pixel; 0 = not resolved yet + uint8_t channelMap[WLEDpixelBus::MAX_CUSTOM_CHANNELS] = {WLEDpixelBus::CH_G, WLEDpixelBus::CH_R, WLEDpixelBus::CH_B, WLEDpixelBus::CH_UNUSED, WLEDpixelBus::CH_UNUSED, WLEDpixelBus::CH_UNUSED}; // GRB default + uint8_t invertMask = 0; // bitmask: bit i = invert channel i output level + bool is16bit = false; // 2 wire bytes per channel (UCS8903/04, SM16825) + bool invertOutput = false; // invert the hardware output signal polarity + bool custom = false; // user-entered override active (replaces CustomBusConfig::active()) + 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 +}; + +// Transient bus configuration, filled by JSON/HTTP parsing and consumed by the +// Bus constructors (busConfigs vector is cleared after finalizeInit). +struct BusConfig { + // identity & geometry + uint8_t type = TYPE_NONE; // 7-bit bus type; the off-refresh flag lives in refreshReq (bit-7 hack ends at the JSON/form boundary) + uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; + uint16_t start = 0; + uint16_t count = 1; + // color + uint8_t colorOrder = COL_ORDER_GRB; // lower nibble: RGB permutation, upper nibble: W/WWCW swap + uint8_t skipAmount = 0; + BusChannelLayout channels; // wire layout: resolved from type+colorOrder, or user override when channels.custom + uint8_t autoWhite = RGBW_MODE_MANUAL_ONLY; + // driver & electrical + uint8_t driverType = BUSDRV_RMT; + uint8_t busSpeedFactor = 100; // percent (100 = default timings) + uint8_t milliAmpsPerLed = LED_MILLIAMPS_DEFAULT; + uint16_t milliAmpsMax = ABL_MILLIAMPS_DEFAULT; + uint16_t frequency = 0; // kHz (2-pin) / Hz (PWM) + // flags + bool reversed = false; + bool refreshReq = false; // requires refresh while off + String text; // network hostname / custom text (config-time only) + + BusConfig() = default; + // Slim ctor for programmatic construction (default buses, usermods); all other fields keep their defaults. + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1) + : type(busType) + , start(pstart) + , count(std::max(len, (uint16_t)1)) + { + const size_t nPins = busTypeGetNumberOfPins(type); + for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + } + + // validates start and length and extends total if needed + bool adjustBounds(uint16_t& total) { + if (!count) count = 1; + if (!busTypeIsVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; + if (start >= MAX_LEDS) return false; + // limit length of strip if it would exceed total permissible LEDs + if (start + count > MAX_LEDS) count = MAX_LEDS - start; + // extend total count accordingly + if (start + count > total) total = start + count; + return true; + } +}; + +#endif diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 0f5e2d0936..e5f3843129 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -50,6 +50,7 @@ static inline void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { static ColorOrderMap _colorOrderMap = {}; +// Convert between two color orders to reproduce legacy color order on the wire (legacy color order is not "absolute" but relative to wire order from NPB methods) bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information _mappings.push_back({start,len,colorOrder}); @@ -57,7 +58,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 +126,30 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { return c; } +// Default implementation for Bus::getChannelLayout() — returns a static default. +// BusDigital and BusPlaceholder return their stored layout instead. +const BusChannelLayout& Bus::getChannelLayout() const { + static const BusChannelLayout _defaultLayout; + return _defaultLayout; +} 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) +, _protocolType(bc.type) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) +, _milliAmpsLimit(0) , _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) +, _busPtr(nullptr) +, _channels(bc.channels) // configured layout as passed (user-entered values when custom) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); - 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; } - + _busSpeedFactor = bc.busSpeedFactor; + if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digital or empty bus!")); return; } if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; - _colorSum = 0; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { @@ -150,30 +158,79 @@ BusDigital::BusDigital(const BusConfig &bc) return; } _pins[1] = bc.pins[1]; - _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined + _frequencykHz = bc.frequency ? bc.frequency : 1000U; // 1MHz clock if undefined } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); uint16_t lenToCreate = bc.count; - if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus - _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip); + + // Channel-map capabilities for one-wire bus layouts. + auto applyChannelCapabilities = [&](const uint8_t* colors, uint8_t nch) { + _hasRgb = false; _hasWhite = false; _hasCCT = false; + bool hasWW = false, hasCW = false; + for (uint8_t i = 0; i < nch; i++) { + switch (colors[i]) { + case WLEDpixelBus::CH_R: + case WLEDpixelBus::CH_G: + case WLEDpixelBus::CH_B: _hasRgb = true; break; + case WLEDpixelBus::CH_W: _hasWhite = true; break; + case WLEDpixelBus::CH_WW: hasWW = true; break; + case WLEDpixelBus::CH_CW: hasCW = true; break; + } + } + if (hasWW || hasCW) _hasWhite = true; + if (hasWW && hasCW) _hasCCT = true; + }; + + bool colorOrderMapApplies = true; + if (!is2Pin(bc.type)) { + _protocolType = bc.type; // parallel drivers share the first bus's timing, not its complete wire protocol + BusChannelLayout wireLayout = bc.channels; + if (!wireLayout.custom && wireLayout.numChannels == 0) + resolveChannelLayout(wireLayout, _protocolType, bc.colorOrder); + _busProperties = makeBusProperties(wireLayout, _protocolType); // single assembly point for the wire descriptor + if (_busProperties.numChannels == 0 || _busProperties.numChannels > WLEDpixelBus::MAX_CUSTOM_CHANNELS) { + cleanup(); + return; + } + _busPtr = PixelBusAllocator::create(_protocolType, _pins, lenToCreate + _skip, bc.colorOrder, _driverType, bc.busSpeedFactor, _frequencykHz, &_busProperties); + applyChannelCapabilities(_busProperties.channelMap, _busProperties.numChannels); + if (_hasWhite) _autoWhiteMode = bc.autoWhite; + // A custom map can still use ColorOrderMap when it is the saved color-order permutation. Anything else cannot. + colorOrderMapApplies = !bc.channels.custom ? !legacyHasFixedChannelMap(_protocolType) : legacyChannelMapMatchesColorOrder(_busProperties, _colorOrder, _protocolType); + } else { + _busPtr = PixelBusAllocator::create(bc.type, _pins, lenToCreate + _skip, bc.colorOrder, _driverType, bc.busSpeedFactor, _frequencykHz); + // note: bc.channels.custom ignored for 2-pin buses as it is not supported, only permutations of RGB/W are used + } _valid = (_busPtr != nullptr) && bc.count > 0; + + if (colorOrderMapApplies) { + const ColorOrderMap& colorOrderMap = BusManager::getColorOrderMap(); + for (uint8_t i = 0; i < colorOrderMap.count(); i++) { + const ColorOrderMapEntry* map = colorOrderMap.get(i); + if ((map->start < (_start + _len)) && ((map->start + map->len) > _start)) { + _hasColorOrderMap = true; + break; + } + } + } + // 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 +250,63 @@ 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) { + setBrightness16((uint16_t)b << 8); +} + +void BusDigital::setBrightness16(uint16_t b) { + _bri16 = b; + if (!_busPtr) return; + if (_type == TYPE_TM1814 || _type == TYPE_TM1815) { + // coarse brightness via per-strip hardware drive current (64 steps), 0 is 6.5mA, 63 is 38mA + // 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) { + // coarse brightness via per-pixel hardware drive current (32 steps), 0 is 0mA, 31 is 24mA + // residual applied via color_fade() in setPixelColor(). + uint8_t hwStep, residualBri; + WLEDpixelBus::mapBrightnessToCurrentStep(b, 31, 8, hwStep, residualBri); + _busPtr->setApa102HwBri(hwStep + 1); // 0 is off so use 1..31 + _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 >> 8); // 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 full brightness 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 * _bri16) / ((uint32_t)busBri << 8); + } + // 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 +324,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) + // Scale complete logical channels, preserving inversion and 16-bit values. + // 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->scaleEncodeBuffer(newBri); } _colorSum = 0; // reset for next frame @@ -248,86 +335,88 @@ 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 = ((uint32_t)_totalBusBri * _bri16) / (255U << 8); // total applied brightness for use in restoreColorLossy (see applyBriLimit()) + else + _totalBusBri = _bri16 >> 8; // if not using ABL, total bus brightness is just the bus brightness + _busPtr->show(); } bool BusDigital::canShow() const { if (!_valid) return true; - return PolyBus::canShow(_busPtr, _iType); + return _busPtr->canShow(); +} + +void BusDigital::clearPixels() { + if (_valid && _busPtr) _busPtr->clearEncodeBuffer(); } //If LEDs are skipped, it is possible to use the first as a status LED. //TODO only show if no new show due in the next 50ms void BusDigital::setStatusPixel(uint32_t c) { if (_valid && _skip) { - PolyBus::setPixelColor(_busPtr, _iType, 0, c, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); - if (canShow()) PolyBus::show(_busPtr, _iType); + _busPtr->setPixelColor(0, c, 0); + if (canShow()) _busPtr->show(); } } -// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash -void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { +// TODO: this function may still need some optimization, making better use of the new bus architecture +// Note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash +// Note: there is no benefit of putting this in IRAM: IRAM: 34.2fps, no IRAM: 34.3fps +void BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; - if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT - 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)); - } - } - + const unsigned globalPix = pix + _start; 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; + CctPixel cct{}; + if (c > 0) { + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + if (hasWhite()) { + c = autoWhiteCalc(c, cct.ww, cct.cw); } + const uint8_t bri = _busPtr->getBusBri(); + c = color_fade(c, bri, true); // apply software brightness scaling (no op for 16bit types, residual scale if HW current scaling is used) + if (hasCCT()) { + cct.cw = ((uint16_t)(cct.cw + 1) * bri) >> 8; + cct.ww = ((uint16_t)(cct.ww + 1) * bri) >> 8; + } + 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)); + } + } + } + + if (__builtin_expect(_hasColorOrderMap, 0)) { + const uint8_t colorOrder = _colorOrderMap.getPixelColorOrder(globalPix, _colorOrder); + const uint8_t numChannels = hasCCT() ? 5 : (hasWhite() ? 4 : 3); + translateLegacyColorOrder(c, cct, colorOrder, _colorOrder, numChannels, _protocolType); } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); + _busPtr->setPixelColor(pix, c, cct.wwcw); } // returns lossly restored color from bus uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; + const unsigned globalPix = pix + _start; 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); + uint32_t rawC = _busPtr->getPixelColor(pix); + uint32_t c = restoreColorLossy(rawC, _totalBusBri); + if (__builtin_expect(_hasColorOrderMap, 0)) { + const uint8_t colorOrder = _colorOrderMap.getPixelColorOrder(globalPix, _colorOrder); + // for CCT buses the bus decode returns WW in W(c) (CW is dropped, lossy); pass it via wwcw + // so translateColorOrder() can move it to the canonical WW position + CctPixel cct{}; + cct.ww = W(c); + const uint8_t numChannels = hasCCT() ? 5 : (hasWhite() ? 4 : 3); + translateLegacyColorOrder(c, cct, _colorOrder, colorOrder, numChannels, _protocolType); } return c; } @@ -339,7 +428,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 +449,12 @@ std::vector BusDigital::getLEDTypes() { {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, {TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")}, {TYPE_TM1814, "D", PSTR("TM1814 RGBW")}, + {TYPE_TM1815, "D", PSTR("TM1815 RGBW")}, {TYPE_FW1906, "D", PSTR("FW1906/WS2811 RGBCCT")}, {TYPE_WS2805, "D", PSTR("WS2805 RGBCCT")}, {TYPE_SM16825, "D", PSTR("SM16825 RGBCCT")}, {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, - {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored + {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber = WW {TYPE_WS2801, "2P", PSTR("WS2801 RGB")}, {TYPE_APA102, "2P", PSTR("APA102 RGB")}, {TYPE_LPD8806, "2P", PSTR("LPD8806 RGB")}, @@ -373,21 +463,26 @@ std::vector BusDigital::getLEDTypes() { }; } -bool BusDigital::isI2S() { - return (_iType & 0x01) == 0; // I2S types have even iType values +bool BusDigital::isParHw() { + return _driverType == BUSDRV_PARHW; // I2S/LCD/ParallelSPI/PARLIO are all parallel async drivers } void BusDigital::begin() { if (!_valid) return; - PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz); + if (!_busPtr->begin()) { cleanup(); return; } + // TM1914: write the static mode-setting prefix once after the encode buffer is allocated. + if (_busProperties.prefixLen > 0) + _busPtr->updatePrefix(_busProperties.prefixData, _busProperties.prefixLen); } void BusDigital::cleanup() { DEBUGBUS_PRINTLN(F("Digital Cleanup.")); - PolyBus::cleanup(_busPtr, _iType); - _iType = I_NONE; + if (_busPtr) { + _busPtr->end(); + delete _busPtr; + _busPtr = nullptr; + } _valid = false; - _busPtr = nullptr; PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -460,7 +555,7 @@ BusPwm::BusPwm(const BusConfig &bc) // LEDC timer reset credit @dedehai #endif // LEDC timer reset credit @dedehai - uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() + uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same formula as in ledcSetup() ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) #endif } @@ -549,13 +644,14 @@ void BusPwm::show() { #endif // use CIE brightness formula (linear + cubic) to approximate human eye perceived brightness // see: https://en.wikipedia.org/wiki/Lightness - unsigned pwmBri = _bri; - if (pwmBri < 21) { // linear response for values [0-20] - pwmBri = (pwmBri * maxBri + 2300 / 2) / 2300 ; // adding '0.5' before division for correct rounding, 2300 gives a good match to CIE curve - } else { // cubic response for values [21-255] - float temp = float(pwmBri + 41) / float(255 + 41); // 41 is to match offset & slope to linear part - temp = temp * temp * temp * (float)maxBri; - pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] C + float briFloat = float(_bri16) / 256.0f; + if (briFloat > 255.0f) briFloat = 255.0f; // safety clamp in case _bri16 is larger than 255<<8 + float pwmBri = briFloat; + if (pwmBri < 21.0f) { + pwmBri = pwmBri * maxBri / 2300.0f; + } else { + float temp = (pwmBri + 41.0f) / (255.0f + 41.0f); + pwmBri = temp * temp * temp * maxBri; } [[maybe_unused]] unsigned hPoint = 0; // phase shift (0 - maxBri) @@ -566,14 +662,14 @@ void BusPwm::show() { // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { - unsigned duty = (_data[i] * pwmBri) / 255; + unsigned duty = (_data[i] * pwmBri) / 255.0f; unsigned deadTime = 0; if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) { // add dead time between signals (when using dithering, two full 8bit pulses are required) deadTime = (1+dithering) << bitShift; // we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap - if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) { + if (briFloat >= 254.0f && duty >= maxBri / 2 && duty < maxBri) { duty -= deadTime << 1; // shorten duty of larger signal except if full on } } @@ -675,7 +771,7 @@ BusOnOff::BusOnOff(const BusConfig &bc) void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel - _data = (c > 0) && bool(_bri) ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on + _data = (c > 0) && _bri16 ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on } uint32_t BusOnOff::getPixelColor(unsigned pix) const { @@ -754,7 +850,7 @@ uint32_t BusNetwork::getPixelColor(unsigned pix) const { void BusNetwork::show() { if (!_valid || !canShow()) return; _broadcastLock = true; - realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, hasWhite()); + realtimeBroadcast(_UDPtype, _client, _len, _data, _bri16 >> 8, hasWhite()); _broadcastLock = false; } @@ -805,7 +901,7 @@ void BusNetwork::cleanup() { DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); d_free(_data); _data = nullptr; - _type = I_NONE; + _type = TYPE_NONE; _valid = false; } @@ -1195,9 +1291,13 @@ uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const { } void BusHub75Matrix::setBrightness(uint8_t b) { - _bri = b; + setBrightness16((uint16_t)b << 8); +} + +void BusHub75Matrix::setBrightness16(uint16_t b) { + _bri16 = b; if (!_valid || !display) return; - display->setBrightness(_bri); + display->setBrightness(_bri16 >> 8); } void BusHub75Matrix::show(void) { @@ -1213,7 +1313,7 @@ void BusHub75Matrix::show(void) { for (int y=0; y> 8); // apply brightness if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b); else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b); } @@ -1283,8 +1383,37 @@ BusPlaceholder::BusPlaceholder(const BusConfig &bc) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) , _text(bc.text) +, _channels(bc.channels) // configured layout as passed (user-entered values when custom) { + _busSpeedFactor = bc.busSpeedFactor; // preserve so config is restored faithfully on reboot 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() { + 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 { @@ -1295,23 +1424,18 @@ 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: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%s)\n"), + (int)bc.start, (int)(bc.start+bc.count), + (int)bc.type, + (int)bc.colorOrder, + (int)bc.reversed, + (int)bc.skipAmount, + (int)bc.autoWhite, + (int)bc.frequency, + (int)bc.milliAmpsPerLed, (int)bc.milliAmpsMax, + bc.driverType == BUSDRV_RMT ? "RMT" : bc.driverType == BUSDRV_PARHW ? "PAR" : "BitBang" + ); DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); unsigned digital = 0; unsigned analog = 0; @@ -1323,7 +1447,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)) { @@ -1339,6 +1463,14 @@ 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); + } return busses.size(); } @@ -1349,8 +1481,21 @@ static String LEDTypesToJson(const std::vector& types) { // capabilities follows similar pattern as JSON API int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4 | Bus::mustRefresh(type.id)<<5; char str[256]; - sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\"},"), type.id, capabilities, type.type, type.name); + WLEDpixelBus::LedTiming timing = WLEDpixelBus::getLedTiming(type.id); + sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\",t0h:%u,t0l:%u,t1h:%u,t1l:%u,trst:%u"), + type.id, capabilities, type.type, type.name, + (unsigned)timing.t0h_ns, (unsigned)timing.t0l_ns, + (unsigned)timing.t1h_ns, (unsigned)timing.t1l_ns, + (unsigned)timing.reset_us); json += str; + if (Bus::isDigital(type.id) && !Bus::is2Pin(type.id)) { + BusChannelLayout layout; + resolveChannelLayout(layout, type.id, COL_ORDER_GRB); + json += F(",cm:\""); + for (uint8_t i = 0; i < layout.numChannels; i++) json += char('0' + layout.channelMap[i]); + json += layout.invertOutput ? F("\",io:1") : F("\",io:0"); + } + json += F("},"); } return json; } @@ -1371,19 +1516,22 @@ 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); +} + +void BusManager::setExpectedRmtChannelCount(uint8_t count) { + PixelBusAllocator::setExpectedRmtChannelCount(count); } + //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(); busses.clear(); - #ifndef ESP8266 // Reset channel tracking for fresh allocation - PolyBus::resetChannelTracking(); - #endif + PixelBusAllocator::resetChannelTracking(); } #ifdef ESP32_DATA_IDLE_HIGH @@ -1402,7 +1550,7 @@ void BusManager::esp32RMTInvertIdle() { unsigned u = 0; for (auto &bus : busses) { if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue; - if (static_cast(bus.get())->isI2S()) continue; + if (static_cast(bus.get())->isParHw()) continue; if (u >= WLED_MAX_RMT_CHANNELS) return; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(rmt); @@ -1470,7 +1618,10 @@ 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(); } } @@ -1507,6 +1658,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; @@ -1566,17 +1728,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); @@ -1593,15 +1751,18 @@ void BusManager::applyABL() { ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } +// PixelBusAllocator channel tracking for dynamic allocation #ifndef ESP8266 -// PolyBus channel tracking for dynamic allocation -bool PolyBus::_useParallelI2S = false; -uint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check -uint8_t PolyBus::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() -uint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check -uint8_t PolyBus::_parallelBusItype = 0; // type I_NONE -uint8_t PolyBus::_2PchannelsAssigned = 0; +uint8_t PixelBusAllocator::_rmtChannelsAssigned = 0; // number of RMT channels assigned during allocateHardware() +uint8_t PixelBusAllocator::_parHwChannelsAssigned = 0; // parallel output channels assigned (I2S/LCD/SPI/PARLIO depending on chip) +uint8_t PixelBusAllocator::_parHwBusType = 0; +uint8_t PixelBusAllocator::_bitBangChannelsAssigned = 0; +uint8_t PixelBusAllocator::_bitBangBusType = 0; +uint8_t PixelBusAllocator::_hardwareSPIused = 0; +#else +uint8_t PixelBusAllocator::_bitBangBusType = 0; #endif + // Bus static member definition int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin() int8_t Bus::_cctBlend = 0; // -128 to +127 diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 6c9c3c8c1c..49040f46f8 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -14,6 +14,7 @@ */ #include "const.h" +#include "bus_config.h" #include "pin_manager.h" #include #include @@ -63,15 +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. typedef struct { uint16_t start; @@ -114,8 +106,8 @@ class Bus { public: 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) + , _bri16(255U << 8) + , _totalBusBri(255) , _start(start) , _len(std::max(len,(uint16_t)1)) , _reversed(reversed) @@ -130,9 +122,11 @@ 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; }; + virtual void setBrightness(uint8_t b) { setBrightness16((uint16_t)b << 8); }; + virtual void setBrightness16(uint16_t b) { _bri16 = b; }; virtual void setColorOrder(uint8_t co) {} virtual uint32_t getPixelColor(unsigned pix) const { return 0; } virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; } @@ -143,7 +137,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(); } @@ -162,42 +156,32 @@ class Bus { inline void setStart(uint16_t start) { _start = start; } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } 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 BusChannelLayout& getChannelLayout() const; // returns default layout for non-digital buses (placeholder stores it too) 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 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); - } - 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 || - 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 - } - static constexpr bool hasCCT(uint8_t type) { - return type == TYPE_WS2812_WWA || type == TYPE_SM16825 || - type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || - type == TYPE_FW1906 || type == TYPE_WS2805; - } - static constexpr bool isTypeValid(uint8_t type) { return (type > 15 && type < 128); } - static constexpr bool isDigital(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || is2Pin(type); } - static constexpr bool is2Pin(uint8_t type) { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); } - static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); } - static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } - 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 int numPWMPins(uint8_t type) { return (type - 40); } + // Type traits delegate to the free functions in bus_config.h (single source of truth). + static constexpr size_t getNumberOfPins(uint8_t type) { return busTypeGetNumberOfPins(type); } + static constexpr size_t getNumberOfChannels(uint8_t type) { return busTypeGetNumberOfChannels(type); } + static constexpr bool hasRGB(uint8_t type) { return busTypeHasRGB(type); } + static constexpr bool hasWhite(uint8_t type) { return busTypeHasWhite(type); } + static constexpr bool hasCCT(uint8_t type) { return busTypeHasCCT(type); } + static constexpr bool isTypeValid(uint8_t type) { return busTypeIsValid(type); } + static constexpr bool isDigital(uint8_t type) { return busTypeIsDigital(type); } + static constexpr bool is2Pin(uint8_t type) { return busTypeIs2Pin(type); } + static constexpr bool isOnOff(uint8_t type) { return busTypeIsOnOff(type); } + static constexpr bool isPWM(uint8_t type) { return busTypeIsPWM(type); } + static constexpr bool isVirtual(uint8_t type) { return busTypeIsVirtual(type); } + static constexpr bool isHub75(uint8_t type) { return busTypeIsHub75(type); } + static constexpr bool is16bit(uint8_t type) { return busTypeIs16bit(type); } + static constexpr bool mustRefresh(uint8_t type) { return busTypeMustRefresh(type); } + static constexpr int numPWMPins(uint8_t type) { return busTypeNumPWMPins(type); } static inline int16_t getCCT() { return _cct; } static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } @@ -215,8 +199,8 @@ class Bus { protected: uint8_t _type; - uint8_t _bri; // bus brightness - uint8_t _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL) + uint16_t _bri16; // Q8.8 bus brightness + uint8_t _totalBusBri; // total brightness applied to colors in bus buffers (_bri16>>8 + ABL) uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; @@ -230,7 +214,7 @@ class Bus { //} __attribute__ ((packed)); static uint8_t _gAWM; // _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()): - // -1 means to extract approximate CCT value in K from RGB (in calcualteCCT()) + // -1 means to extract approximate CCT value in K from RGB (in calculateCCT()) // [0,255] is the exact CCT value where 0 means warm and 255 cold // [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin()) static int16_t _cct; @@ -241,6 +225,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 +238,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 +252,33 @@ 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 setBrightness16(uint16_t b) override; void estimateCurrent(); // estimate used current from summed colors void applyBriLimit(uint8_t newBri); size_t getBusSize() const override; - bool isI2S(); // true if this bus uses I2S driver + bool isParHw(); // true if this bus uses a parallel hardware driver (I2S/LCD/SPI/PARLIO) void begin() override; void cleanup(); + const BusChannelLayout& getChannelLayout() const override { return _channels; } 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 - uint16_t _frequencykHz; - uint16_t _milliAmpsMax; + uint8_t _colorOrder; // saved color-order byte. Encoder indices are built from it once, at creation. + uint8_t _protocolType; // used wire protocol: timing is shared in parallel buses, color channels can differ + bool _hasColorOrderMap = false; + uint8_t _pins[2] = {255, 255}; + uint8_t _driverType; // BusDriverType: BUSDRV_RMT / BUSDRV_PARHW / BUSDRV_BITBANG uint8_t _milliAmpsPerLed; + uint16_t _milliAmpsMax; uint16_t _milliAmpsLimit; - uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current - void *_busPtr; + uint16_t _frequencykHz; + uint32_t _colorSum = 0; // sum of brightness-scaled channel bytes; updated in setPixelColor() when ABL active + WLEDpixelBus::BusProperties _busProperties; // assembled wire descriptor (from BusConfig), also used by begin() for the prefix upload + WLEDpixelBus::PixelBus* _busPtr = nullptr; + BusChannelLayout _channels; // configured channel layout as passed in BusConfig (user-entered when custom) static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -294,7 +287,7 @@ class BusDigital : public Bus { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { uint_fast16_t val = chan[i]; - chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding _bri slightly improves recovery / stops degradation on re-scale + chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding brightness slightly improves recovery / stops degradation on re-scale } } return c; @@ -345,8 +338,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 +379,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 +396,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 BusChannelLayout& getChannelLayout() const override { return _channels; } size_t getBusSize() const override { return sizeof(BusPlaceholder); } @@ -413,6 +409,7 @@ class BusPlaceholder : public Bus { uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; String _text; + BusChannelLayout _channels; // configured channel layout as passed in BusConfig (user-entered when custom) }; #ifdef WLED_ENABLE_HUB75MATRIX @@ -423,6 +420,7 @@ class BusHub75Matrix : public Bus { [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; void show() override; void setBrightness(uint8_t b) override; + void setBrightness16(uint16_t b) override; size_t getPins(uint8_t* pinArray = nullptr) const override; void deallocatePins(); void cleanup(); @@ -451,71 +449,6 @@ class BusHub75Matrix : public Bus { }; #endif -//temporary struct for passing bus configuration to bus -struct BusConfig { - uint8_t type; - uint16_t count; - uint16_t start; - uint8_t colorOrder; - bool reversed; - uint8_t skipAmount; - bool refreshReq; - uint8_t autoWhite; - uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; - uint16_t frequency; - uint8_t milliAmpsPerLed; - uint16_t milliAmpsMax; - uint8_t driverType; // 0=RMT (default), 1=I2S - uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation - String text; - - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "") - : count(std::max(len,(uint16_t)1)) - , start(pstart) - , colorOrder(pcolorOrder) - , reversed(rev) - , skipAmount(skip) - , autoWhite(aw) - , frequency(clock_kHz) - , milliAmpsPerLed(maPerLed) - , milliAmpsMax(maMax) - , driverType(driver) - , iType(0) // default to I_NONE - , text(sometext) - { - refreshReq = (bool) GET_BIT(busType,7); - type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) - size_t nPins = Bus::getNumberOfPins(type); - for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; - DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%s)\n"), - (int)start, (int)(start+len), - (int)type, - (int)colorOrder, - (int)reversed, - (int)skipAmount, - (int)autoWhite, - (int)frequency, - (int)milliAmpsPerLed, (int)milliAmpsMax, - driverType == 0 ? "RMT" : "I2S" - ); - } - - //validates start and length and extends total if needed - bool adjustBounds(uint16_t& total) { - if (!count) count = 1; - if (!Bus::isVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; - if (start >= MAX_LEDS) return false; - //limit length of strip if it would exceed total permissible LEDs - if (start + count > MAX_LEDS) count = MAX_LEDS - start; - //extend total count accordingly - if (start + count > total) total = start + count; - return true; - } - - size_t memUsage() const; -}; - - // milliamps used by ESP (for power estimation) // you can set it to 0 if the ESP is powered by USB and the LEDs by external #ifndef MA_FOR_ESP @@ -550,7 +483,8 @@ 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 + void setExpectedRmtChannelCount(uint8_t count); //do not call this method from system context (network callback) void removeAll(); @@ -561,11 +495,13 @@ 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(); void blank(); // Set all pixels to black inline void setStatusPixel(uint32_t c) { for (auto &bus : busses) bus->setStatusPixel(c);} - inline void setBrightness(uint8_t b) { for (auto &bus : busses) bus->setBrightness(b); } + inline void setBrightness(uint8_t b) { for (auto &bus : busses) bus->setBrightness(b); } // set 8-bit brightness (legacy, not used. For UM compatibility only) + inline void setBrightness16(uint16_t b) { for (auto &bus : busses) bus->setBrightness16(b); } // set 16-bit brightness // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d1c9afd655..1785eb82ee 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,1415 +1,436 @@ -#pragma once +#pragma once #ifndef BusWrapper_h #define BusWrapper_h -//#define NPB_CONF_4STEP_CADENCE -#include "NeoPixelBus.h" - -#include "wled_boards.h" // pull in board-specific capability defines - -#ifdef CONFIG_IDF_TARGET_ESP32C5 -// see https://github.com/wled/WLED/pull/5048#issuecomment-3937888182 -#warning "second output on -C5 may fail, until NPB channel handling is fixed" +#include "src/WLEDpixelBus/WLEDpixelBus.h" +#include "src/WLEDpixelBus/WLEDpixelBus_SPI.h" +#include "bus_config.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" +#include "src/WLEDpixelBus/WLEDpixelBus_I2S.h" +#include "src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h" +#include "src/WLEDpixelBus/WLEDpixelBus_PARLIO.h" +#include "src/WLEDpixelBus/WLEDpixelBus_BitBang.h" +#elif defined(ARDUINO_ARCH_ESP8266) +#include "src/WLEDpixelBus/WLEDpixelBus_ESP8266.h" +#include "src/WLEDpixelBus/WLEDpixelBus_BitBang.h" #endif -//Hardware SPI Pins +//Hardware SPI Pins (ESP8266 only; ESP32 uses bus allocation order to detect HSPI) #define P_8266_HS_MOSI 13 #define P_8266_HS_CLK 14 -#define P_32_HS_MOSI 13 -#define P_32_HS_CLK 14 -#define P_32_VS_MOSI 23 -#define P_32_VS_CLK 18 - -//The dirty list of possible bus types. Quite a lot... -#define I_NONE 0 -//ESP8266 RGB -#define I_8266_U0_NEO_3 1 -#define I_8266_U1_NEO_3 2 -#define I_8266_DM_NEO_3 3 -#define I_8266_BB_NEO_3 4 -//RGBW -#define I_8266_U0_NEO_4 5 -#define I_8266_U1_NEO_4 6 -#define I_8266_DM_NEO_4 7 -#define I_8266_BB_NEO_4 8 -//400Kbps -#define I_8266_U0_400_3 9 -#define I_8266_U1_400_3 10 -#define I_8266_DM_400_3 11 -#define I_8266_BB_400_3 12 -//TM1814 (RGBW) -#define I_8266_U0_TM1_4 13 -#define I_8266_U1_TM1_4 14 -#define I_8266_DM_TM1_4 15 -#define I_8266_BB_TM1_4 16 -//TM1829 (RGB) -#define I_8266_U0_TM2_3 17 -#define I_8266_U1_TM2_3 18 -#define I_8266_DM_TM2_3 19 -#define I_8266_BB_TM2_3 20 -//UCS8903 (RGB) -#define I_8266_U0_UCS_3 21 -#define I_8266_U1_UCS_3 22 -#define I_8266_DM_UCS_3 23 -#define I_8266_BB_UCS_3 24 -//UCS8904 (RGBW) -#define I_8266_U0_UCS_4 25 -#define I_8266_U1_UCS_4 26 -#define I_8266_DM_UCS_4 27 -#define I_8266_BB_UCS_4 28 -//FW1906 GRBCCT -#define I_8266_U0_FW6_5 29 -#define I_8266_U1_FW6_5 30 -#define I_8266_DM_FW6_5 31 -#define I_8266_BB_FW6_5 32 -//ESP8266 APA106 -#define I_8266_U0_APA106_3 33 -#define I_8266_U1_APA106_3 34 -#define I_8266_DM_APA106_3 35 -#define I_8266_BB_APA106_3 36 -//WS2805 (RGBCCT) -#define I_8266_U0_2805_5 37 -#define I_8266_U1_2805_5 38 -#define I_8266_DM_2805_5 39 -#define I_8266_BB_2805_5 40 -//TM1914 (RGB) -#define I_8266_U0_TM1914_3 41 -#define I_8266_U1_TM1914_3 42 -#define I_8266_DM_TM1914_3 43 -#define I_8266_BB_TM1914_3 44 -//SM16825 (RGBCCT) -#define I_8266_U0_SM16825_5 45 -#define I_8266_U1_SM16825_5 46 -#define I_8266_DM_SM16825_5 47 -#define I_8266_BB_SM16825_5 48 - -/*** ESP32 Neopixel methods ***/ -//RGB -#define I_32_RN_NEO_3 1 -#define I_32_I2_NEO_3 2 -//RGBW -#define I_32_RN_NEO_4 5 -#define I_32_I2_NEO_4 6 -//400Kbps -#define I_32_RN_400_3 9 -#define I_32_I2_400_3 10 -//TM1814 (RGBW) -#define I_32_RN_TM1_4 13 -#define I_32_I2_TM1_4 14 -//TM1829 (RGB) -#define I_32_RN_TM2_3 17 -#define I_32_I2_TM2_3 18 -//UCS8903 (RGB) -#define I_32_RN_UCS_3 21 -#define I_32_I2_UCS_3 22 -//UCS8904 (RGBW) -#define I_32_RN_UCS_4 25 -#define I_32_I2_UCS_4 26 -//FW1906 GRBCCT 6 color channels -#define I_32_RN_FW6_5 29 -#define I_32_I2_FW6_5 30 -//APA106 -#define I_32_RN_APA106_3 33 -#define I_32_I2_APA106_3 34 -//WS2805 (RGBCCT) -#define I_32_RN_2805_5 37 -#define I_32_I2_2805_5 38 -//TM1914 (RGB) -#define I_32_RN_TM1914_3 41 -#define I_32_I2_TM1914_3 42 -//SM16825 (RGBCCT) -#define I_32_RN_SM16825_5 45 -#define I_32_I2_SM16825_5 46 - -//APA102 -#define I_HS_DOT_3 101 //hardware SPI -#define I_SS_DOT_3 102 //soft SPI -//LPD8806 -#define I_HS_LPD_3 103 -#define I_SS_LPD_3 104 +// Use single RMT memory block per channel — allows RMT RX channels alongside TX (see WLEDpixelBus_RMT) +//#define RMT_USE_SINGLE_MEM_BLOCK -//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 +// Legacy color-order compatibility shim +// +// Backwards compatibility with the NeoPixelBus-based driver: each bus type used a fixed NPB "feature" +// that defined the physical wire color-order on top of which the color order was applied (permutations) +// The features were not used consistently but "by strip defaults". Below table was extracted from legacy bus_wrapper.h using AI. -//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 +enum LegacyField : uint8_t { LF_R, LF_G, LF_B, LF_W, LF_CW, LF_PAD }; -/*** ESP32 Neopixel methods ***/ -#ifdef ARDUINO_ARCH_ESP32 -#if defined(WLED_HAS_PARALLEL_I2S) -// C3: I2S0 and I2S1 methods not supported (has one I2S bus) -// S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus) -// S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (has two I2S buses) -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 -#if defined(CONFIG_IDF_TARGET_ESP32S3) - // S3 will always use LCD parallel output - typedef X8Ws2812xMethod X1Ws2812xMethod; - typedef X8Sk6812Method X1Sk6812Method; - typedef X8400KbpsMethod X1400KbpsMethod; - typedef X8800KbpsMethod X1800KbpsMethod; - typedef X8Tm1814Method X1Tm1814Method; - typedef X8Tm1829Method X1Tm1829Method; - typedef X8Apa106Method X1Apa106Method; - typedef X8Ws2805Method X1Ws2805Method; - typedef X8Tm1914Method X1Tm1914Method; -#elif defined(CONFIG_IDF_TARGET_ESP32S2) - // S2 will use I2S0 - typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod; - typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method; - typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod; - typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod; - typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method; - typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method; - typedef NeoEsp32I2s0Apa106Method X1Apa106Method; - typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method; - typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method; -#else - // regular ESP32 will use I2S1 - typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod; - typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method; - typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod; - typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod; - typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method; - typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method; - typedef NeoEsp32I2s1Apa106Method X1Apa106Method; - typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method; - typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method; -#endif -// RISC-V boards don't have I2S methods -#endif - -// RMT driver selection -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) -#define NeoEsp32RmtMethod(x) NeoEsp32RmtX ## x ## Method -#elif !defined(WLED_USE_SHARED_RMT) && !defined(__riscv) -#include -#define NeoEsp32RmtMethod(x) NeoEsp32RmtHIN ## x ## Method -#else -#define NeoEsp32RmtMethod(x) NeoEsp32RmtN ## x ## Method -#endif - -//RGB -#define B_32_RN_NEO_3 NeoPixelBus // ESP32, S2, S3, C3 -//#define B_32_IN_NEO_3 NeoPixelBus // ESP32 (dynamic I2S selection) -#define B_32_I2_NEO_3 NeoPixelBus // ESP32, S2, S3 (automatic I2S selection, see typedef above) -#define B_32_IP_NEO_3 NeoPixelBus // parallel I2S (ESP32, S2, S3) -//RGBW -#define B_32_RN_NEO_4 NeoPixelBus -#define B_32_I2_NEO_4 NeoPixelBus -#define B_32_IP_NEO_4 NeoPixelBus // parallel I2S -//400Kbps -#define B_32_RN_400_3 NeoPixelBus -#define B_32_I2_400_3 NeoPixelBus -#define B_32_IP_400_3 NeoPixelBus // parallel I2S -//TM1814 (RGBW) -#define B_32_RN_TM1_4 NeoPixelBus -#define B_32_I2_TM1_4 NeoPixelBus -#define B_32_IP_TM1_4 NeoPixelBus // parallel I2S -//TM1829 (RGB) -#define B_32_RN_TM2_3 NeoPixelBus -#define B_32_I2_TM2_3 NeoPixelBus -#define B_32_IP_TM2_3 NeoPixelBus // parallel I2S -//UCS8903 -#define B_32_RN_UCS_3 NeoPixelBus -#define B_32_I2_UCS_3 NeoPixelBus -#define B_32_IP_UCS_3 NeoPixelBus // parallel I2S -//UCS8904 -#define B_32_RN_UCS_4 NeoPixelBus -#define B_32_I2_UCS_4 NeoPixelBus -#define B_32_IP_UCS_4 NeoPixelBus// parallel I2S -//APA106 -#define B_32_RN_APA106_3 NeoPixelBus -#define B_32_I2_APA106_3 NeoPixelBus -#define B_32_IP_APA106_3 NeoPixelBus // parallel I2S -//FW1906 GRBCCT 6 color channels -#define B_32_RN_FW6_5 NeoPixelBus -#define B_32_I2_FW6_5 NeoPixelBus -#define B_32_IP_FW6_5 NeoPixelBus // parallel I2S -//WS2805 RGBCCT -#define B_32_RN_2805_5 NeoPixelBus -#define B_32_I2_2805_5 NeoPixelBus -#define B_32_IP_2805_5 NeoPixelBus // parallel I2S -//TM1914 (RGB) -#define B_32_RN_TM1914_3 NeoPixelBus -#define B_32_I2_TM1914_3 NeoPixelBus -#define B_32_IP_TM1914_3 NeoPixelBus // parallel I2S -//Sm16825 (RGBCCT) -#define B_32_RN_SM16825_5 NeoPixelBus -#define B_32_I2_SM16825_5 NeoPixelBus -#define B_32_IP_SM16825_5 NeoPixelBus // parallel I2S -#endif - -//APA102 -#ifdef WLED_USE_ETHERNET -// fix for #2542 (by @BlackBird77) -#define B_HS_DOT_3 NeoPixelBus //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) -#else -#define B_HS_DOT_3 NeoPixelBus //hardware VSPI -#endif -#define B_SS_DOT_3 NeoPixelBus //soft SPI - -//LPD8806 -#define B_HS_LPD_3 NeoPixelBus -#define B_SS_LPD_3 NeoPixelBus - -//LPD6803 -#define B_HS_LPO_3 NeoPixelBus -#define B_SS_LPO_3 NeoPixelBus - -//WS2801 -#ifdef WLED_USE_ETHERNET -#define B_HS_WS1_3 NeoPixelBus>> -#else -#define B_HS_WS1_3 NeoPixelBus -#endif -#define B_SS_WS1_3 NeoPixelBus - -//P9813 -#define B_HS_P98_3 NeoPixelBus -#define B_SS_P98_3 NeoPixelBus - -// 48bit & 64bit to 24bit & 32bit RGB(W) conversion -#define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF)) -#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) - -//handles pointer type conversion for all possible bus types -class PolyBus { - 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. - #endif - public: +struct LegacyLayout { + uint8_t n; // wire bytes per pixel + LegacyField f[6]; // color order (WLEDpixelBus has up to 6 colors, TYPE_FW1906 uses all 6 in hardware) +}; - // 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(); - #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); - #endif - if (clock_kHz) dotStar_strip->SetMethodSettings(NeoSpiSettings((uint32_t)clock_kHz*1000)); +// LED Type wire layout as instantiated by the legacy NPB wrapper (except diverging ESP8266 corner casses that were probably bugs). +inline const LegacyLayout& getLegacyChannelLayout(uint8_t busType) { + static const LegacyLayout grb = {3, {LF_G, LF_R, LF_B, LF_PAD, LF_PAD, LF_PAD}}; // WS2812/13, WS2811_400KHZ, LPD8806, LPD6803, TM1914, APA106 + static const LegacyLayout grbw = {4, {LF_G, LF_R, LF_B, LF_W, LF_PAD, LF_PAD}}; // SK6812_RGBW + static const LegacyLayout grbwwcw = {5, {LF_G, LF_R, LF_B, LF_W, LF_CW, LF_PAD}}; // WS2805 (NeoGrbwwFeature) + static const LegacyLayout brg = {3, {LF_B, LF_R, LF_G, LF_PAD, LF_PAD, LF_PAD}}; // TM1829 (NeoBrgFeature) + static const LegacyLayout rgb = {3, {LF_R, LF_G, LF_B, LF_PAD, LF_PAD, LF_PAD}}; // UCS8903 (16-bit) + static const LegacyLayout rgbw = {4, {LF_R, LF_G, LF_B, LF_W, LF_PAD, LF_PAD}}; // UCS8904 (16-bit) + static const LegacyLayout wrgb = {4, {LF_W, LF_R, LF_G, LF_B, LF_PAD, LF_PAD}}; // TM1814/TM1815 (NeoWrgbTm1814Feature) + static const LegacyLayout grbcwx = {6, {LF_G, LF_R, LF_B, LF_CW, LF_W, LF_PAD}}; // FW1906 (NeoGrbcwxFeature, 6th byte unused) + static const LegacyLayout rgbcww = {5, {LF_R, LF_G, LF_B, LF_CW, LF_W, LF_PAD}}; // SM16825 (NeoRgbcwSm16825eFeature, 16-bit) + static const LegacyLayout rbg = {3, {LF_R, LF_B, LF_G, LF_PAD, LF_PAD, LF_PAD}}; // WS2801 (NeoRbgFeature) + static const LegacyLayout bgr = {3, {LF_B, LF_G, LF_R, LF_PAD, LF_PAD, LF_PAD}}; // APA102 (DotStarBgrFeature), P9813 (P9813BgrFeature) + switch (busType) { + case TYPE_TM1829: return brg; + case TYPE_UCS8903: return rgb; + case TYPE_UCS8904: return rgbw; + case TYPE_TM1814: // fallthrough + case TYPE_TM1815: return wrgb; + case TYPE_FW1906: return grbcwx; + case TYPE_WS2805: return grbwwcw; + case TYPE_SM16825: return rgbcww; + case TYPE_WS2801: return rbg; + case TYPE_APA102: // fallthrough + case TYPE_P9813: return bgr; + default: // all other one-wire chips used GRB-based NPB features + if (busTypeHasCCT(busType)) return grbwwcw; + if (busTypeHasWhite(busType)) return grbw; + return grb; } - - // 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)); +} + +// Compose a legacy wire layout with color order into a WLEDpixelBus channel map (which puts colors on the wire in the order of channelMap[], see color encoders) +inline uint8_t legacyColorOrderToChannelMap(uint8_t busType, uint8_t colorOrder, uint8_t* channelMap) { + // permutations of how "colorOrder" was applied, 0 is "GRB", i.e. no permuation but the underlying bus layout was GRB for common types (but not all see getLegacyChannelLayout()) + static constexpr uint8_t kLegacyPerm[6][3] = { + {LF_R, LF_G, LF_B}, // 0 = "GRB" (default) + {LF_G, LF_R, LF_B}, // 1 = "RGB" + {LF_R, LF_B, LF_G}, // 2 = "BRG" + {LF_B, LF_R, LF_G}, // 3 = "RBG" + {LF_G, LF_B, LF_R}, // 4 = "BGR" + {LF_B, LF_G, LF_R}, // 5 = "GBR" + }; + const LegacyLayout& layout = getLegacyChannelLayout(busType); + + // Logical content carried by each field after the permutation + white swap. + uint8_t content[LF_PAD + 1]; + const uint8_t perm = colorOrder & 0x0F; + const uint8_t* p = kLegacyPerm[perm <= COL_ORDER_MAX ? perm : 0]; + content[LF_R] = p[0]; content[LF_G] = p[1]; content[LF_B] = p[2]; + content[LF_W] = LF_W; content[LF_CW] = LF_CW; content[LF_PAD] = LF_PAD; + + const bool isCct = layout.n >= 5; // 5+ channel layouts carry WW/CW + const uint8_t wSwap = colorOrder >> 4; + if (!isCct) { + // RGBW: swap contents of W with B/G/R (upper nibble 1/2/3) + if (wSwap >= WLEDpixelBus::WSWAP_B && wSwap <= WLEDpixelBus::WSWAP_R) { + const uint8_t other = (wSwap == WLEDpixelBus::WSWAP_B) ? LF_B : (wSwap == WLEDpixelBus::WSWAP_G) ? LF_G : LF_R; + std::swap(content[LF_W], content[other]); + } + } else if (wSwap == WLEDpixelBus::WSWAP_WWCW) { + std::swap(content[LF_W], content[LF_CW]); // RGBCCT: swap WW & CW contents } - 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 + for (uint8_t i = 0; i < WLEDpixelBus::MAX_CUSTOM_CHANNELS; i++) { + if (i >= layout.n) { channelMap[i] = WLEDpixelBus::CH_UNUSED; continue; } + switch (content[layout.f[i]]) { + case LF_R: channelMap[i] = WLEDpixelBus::CH_R; break; + case LF_G: channelMap[i] = WLEDpixelBus::CH_G; break; + case LF_B: channelMap[i] = WLEDpixelBus::CH_B; break; + case LF_W: channelMap[i] = isCct ? WLEDpixelBus::CH_WW : WLEDpixelBus::CH_W; break; + case LF_CW: channelMap[i] = WLEDpixelBus::CH_CW; break; + default: channelMap[i] = WLEDpixelBus::CH_UNUSED; break; // LF_PAD + } } - - static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz /* only used by DotStar */) { - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_TM1_4: beginTM1814(busPtr); break; - case I_8266_U1_TM1_4: beginTM1814(busPtr); break; - case I_8266_DM_TM1_4: beginTM1814(busPtr); break; - case I_8266_BB_TM1_4: beginTM1814(busPtr); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_WS1_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_P98_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_TM1914_3: beginTM1914(busPtr); break; - case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; - case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; - case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->Begin(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_TM1_4: beginTM1814(busPtr); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; - case I_32_RN_TM1914_3: beginTM1914(busPtr); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; - // I2S1 bus or parellel buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - #endif - // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() - case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_LPO_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_WS1_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_P98_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - #endif - case I_SS_DOT_3: (static_cast(busPtr))->Begin(); break; - case I_SS_LPD_3: (static_cast(busPtr))->Begin(); break; - case I_SS_LPO_3: (static_cast(busPtr))->Begin(); break; - case I_SS_WS1_3: (static_cast(busPtr))->Begin(); break; - case I_SS_P98_3: (static_cast(busPtr))->Begin(); break; + return layout.n; +} + +// Legacy LED types that do not have a color-order permutation. Their wire map is fixed. +inline bool legacyHasFixedChannelMap(uint8_t ledType) { + return ledType == TYPE_WS2812_1CH_X3 || ledType == TYPE_WS2812_WWA; +} + +// True when a custom channel map matches the permutation supported by ColorOrderMap. +inline bool legacyChannelMapMatchesColorOrder(const WLEDpixelBus::BusProperties& properties, uint8_t colorOrder, uint8_t ledType) { + if (legacyHasFixedChannelMap(ledType) || properties.numChannels < 3 || properties.numChannels > WLEDpixelBus::MAX_CUSTOM_CHANNELS) return false; + uint8_t expected[WLEDpixelBus::MAX_CUSTOM_CHANNELS]; + const uint8_t n = legacyColorOrderToChannelMap(ledType, colorOrder, expected); + if (n != properties.numChannels) return false; + for (uint8_t i = 0; i < n; i++) { + if (properties.channelMap[i] != expected[i]) return false; + } + return true; +} + +// Wire position of each logical channel (R,G,B,W/WW,CW) for a color order. +inline void getLegacyColorOrderPositions(uint8_t colorOrder, uint8_t numChannels, uint8_t ledType, uint8_t positions[5]) { + uint8_t map[WLEDpixelBus::MAX_CUSTOM_CHANNELS]; + legacyColorOrderToChannelMap(ledType, colorOrder, map); + positions[0] = 0; positions[1] = 1; positions[2] = 2; positions[3] = 3; positions[4] = 4; + for (uint8_t i = 0; i < numChannels; i++) { + switch (map[i]) { + case WLEDpixelBus::CH_R: positions[0] = i; break; + case WLEDpixelBus::CH_G: positions[1] = i; break; + case WLEDpixelBus::CH_B: positions[2] = i; break; + case WLEDpixelBus::CH_W: + case WLEDpixelBus::CH_WW: positions[3] = i; break; + case WLEDpixelBus::CH_CW: positions[4] = i; break; + default: break; } } - - static void* create(uint8_t busType, uint8_t* pins, uint16_t len) { - void* busPtr = nullptr; - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: busPtr = new B_8266_U0_NEO_3(len, pins[0]); break; - case I_8266_U1_NEO_3: busPtr = new B_8266_U1_NEO_3(len, pins[0]); break; - case I_8266_DM_NEO_3: busPtr = new B_8266_DM_NEO_3(len, pins[0]); break; - case I_8266_BB_NEO_3: busPtr = new B_8266_BB_NEO_3(len, pins[0]); break; - case I_8266_U0_NEO_4: busPtr = new B_8266_U0_NEO_4(len, pins[0]); break; - case I_8266_U1_NEO_4: busPtr = new B_8266_U1_NEO_4(len, pins[0]); break; - case I_8266_DM_NEO_4: busPtr = new B_8266_DM_NEO_4(len, pins[0]); break; - case I_8266_BB_NEO_4: busPtr = new B_8266_BB_NEO_4(len, pins[0]); break; - case I_8266_U0_400_3: busPtr = new B_8266_U0_400_3(len, pins[0]); break; - case I_8266_U1_400_3: busPtr = new B_8266_U1_400_3(len, pins[0]); break; - case I_8266_DM_400_3: busPtr = new B_8266_DM_400_3(len, pins[0]); break; - case I_8266_BB_400_3: busPtr = new B_8266_BB_400_3(len, pins[0]); break; - case I_8266_U0_TM1_4: busPtr = new B_8266_U0_TM1_4(len, pins[0]); break; - case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; - case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; - case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; - case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break; - case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break; - case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break; - case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break; - case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; - case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; - case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; - case I_8266_BB_UCS_3: busPtr = new B_8266_BB_UCS_3(len, pins[0]); break; - case I_8266_U0_UCS_4: busPtr = new B_8266_U0_UCS_4(len, pins[0]); break; - case I_8266_U1_UCS_4: busPtr = new B_8266_U1_UCS_4(len, pins[0]); break; - case I_8266_DM_UCS_4: busPtr = new B_8266_DM_UCS_4(len, pins[0]); break; - case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break; - case I_8266_U0_APA106_3: busPtr = new B_8266_U0_APA106_3(len, pins[0]); break; - case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break; - case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break; - case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break; - case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break; - case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break; - case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break; - case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break; - case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break; - case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; - case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; - case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; - case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break; - case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; - case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; - case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; - case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break; - case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break; - case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break; - case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break; - case I_32_I2_NEO_4: if (_useParallelI2S) busPtr = new B_32_IP_NEO_4(len, pins[0]); else busPtr = new B_32_I2_NEO_4(len, pins[0]); break; - case I_32_I2_400_3: if (_useParallelI2S) busPtr = new B_32_IP_400_3(len, pins[0]); else busPtr = new B_32_I2_400_3(len, pins[0]); break; - case I_32_I2_TM1_4: if (_useParallelI2S) busPtr = new B_32_IP_TM1_4(len, pins[0]); else busPtr = new B_32_I2_TM1_4(len, pins[0]); break; - case I_32_I2_TM2_3: if (_useParallelI2S) busPtr = new B_32_IP_TM2_3(len, pins[0]); else busPtr = new B_32_I2_TM2_3(len, pins[0]); break; - case I_32_I2_UCS_3: if (_useParallelI2S) busPtr = new B_32_IP_UCS_3(len, pins[0]); else busPtr = new B_32_I2_UCS_3(len, pins[0]); break; - case I_32_I2_UCS_4: if (_useParallelI2S) busPtr = new B_32_IP_UCS_4(len, pins[0]); else busPtr = new B_32_I2_UCS_4(len, pins[0]); break; - case I_32_I2_APA106_3: if (_useParallelI2S) busPtr = new B_32_IP_APA106_3(len, pins[0]); else busPtr = new B_32_I2_APA106_3(len, pins[0]); break; - case I_32_I2_FW6_5: if (_useParallelI2S) busPtr = new B_32_IP_FW6_5(len, pins[0]); else busPtr = new B_32_I2_FW6_5(len, pins[0]); break; - case I_32_I2_2805_5: if (_useParallelI2S) busPtr = new B_32_IP_2805_5(len, pins[0]); else busPtr = new B_32_I2_2805_5(len, pins[0]); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) busPtr = new B_32_IP_TM1914_3(len, pins[0]); else busPtr = new B_32_I2_TM1914_3(len, pins[0]); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) busPtr = new B_32_IP_SM16825_5(len, pins[0]); else busPtr = new B_32_I2_SM16825_5(len, pins[0]); break; - #endif - #endif - // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) - case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; - case I_SS_DOT_3: busPtr = new B_SS_DOT_3(len, pins[1], pins[0]); break; - case I_HS_LPD_3: busPtr = new B_HS_LPD_3(len, pins[1], pins[0]); break; - case I_SS_LPD_3: busPtr = new B_SS_LPD_3(len, pins[1], pins[0]); break; - case I_HS_LPO_3: busPtr = new B_HS_LPO_3(len, pins[1], pins[0]); break; - case I_SS_LPO_3: busPtr = new B_SS_LPO_3(len, pins[1], pins[0]); break; - case I_HS_WS1_3: busPtr = new B_HS_WS1_3(len, pins[1], pins[0]); break; - case I_SS_WS1_3: busPtr = new B_SS_WS1_3(len, pins[1], pins[0]); break; - case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break; - case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break; +} + +// Translate between legacy color orders while preserving the chip's physical wire layout. +inline void translateLegacyColorOrder(uint32_t &c, CctPixel& cct, uint8_t sourceOrder, uint8_t targetOrder, uint8_t numChannels, uint8_t ledType) { + if (sourceOrder == targetOrder) return; + + uint8_t sourcePositions[5], targetPositions[5]; + getLegacyColorOrderPositions(sourceOrder, numChannels, ledType, sourcePositions); + getLegacyColorOrderPositions(targetOrder, numChannels, ledType, targetPositions); + uint8_t sourceByWirePosition[5] = {}; + sourceByWirePosition[sourcePositions[0]] = R(c); + sourceByWirePosition[sourcePositions[1]] = G(c); + sourceByWirePosition[sourcePositions[2]] = B(c); + if (numChannels >= 4) + sourceByWirePosition[sourcePositions[3]] = numChannels == 5 ? cct.ww : W(c); + if (numChannels == 5) + sourceByWirePosition[sourcePositions[4]] = cct.cw; + + const uint8_t targetR = sourceByWirePosition[targetPositions[0]]; + const uint8_t targetG = sourceByWirePosition[targetPositions[1]]; + const uint8_t targetB = sourceByWirePosition[targetPositions[2]]; + const uint8_t targetW = numChannels >= 4 ? sourceByWirePosition[targetPositions[3]] : W(c); + const uint8_t targetCW = numChannels == 5 ? sourceByWirePosition[targetPositions[4]] : 0; + // for 5ch (CCT) also write WW into W(c): the encoder ignores W(c) and uses wwcw, but + // getPixelColor() can only recover the WW channel through W(c) (CW stays lost, decode is lossy) + c = RGBW32(targetR, targetG, targetB, targetW); + if (numChannels == 5) { + cct.ww = targetW; + cct.cw = targetCW; + } +} + +// Resolve the wire channel layout for a bus type + color order. No-op for custom (user-entered) layouts. +// Handles the fixed-map types WS2812_1CH_X3 / WS2812_WWA for config backwards compatibilty (they could now be handled as custom bus layouts) +inline void resolveChannelLayout(BusChannelLayout& ch, uint8_t busType, uint8_t colorOrder) { + if (ch.custom) return; + if (busType == TYPE_WS2812_1CH_X3) { + ch.numChannels = 3; + ch.channelMap[0] = ch.channelMap[1] = ch.channelMap[2] = WLEDpixelBus::CH_W; + } else if (busType == TYPE_WS2812_WWA) { + ch.numChannels = 3; + ch.channelMap[0] = WLEDpixelBus::CH_CW; + ch.channelMap[1] = WLEDpixelBus::CH_WW; + ch.channelMap[2] = WLEDpixelBus::CH_WW; // separate amber channel is not yet supported + } else { + ch.numChannels = legacyColorOrderToChannelMap(busType, colorOrder, ch.channelMap); + } + ch.invertMask = 0; + ch.is16bit = busTypeIs16bit(busType); + ch.invertOutput = busType == TYPE_TM1814 || busType == TYPE_TM1815 || busType == TYPE_TM1914 || busType == TYPE_TM1829; + const WLEDpixelBus::LedTiming timing = WLEDpixelBus::getLedTiming(busType); + ch.t0h = timing.t0h_ns; ch.t0l = timing.t0l_ns; + ch.t1h = timing.t1h_ns; ch.t1l = timing.t1l_ns; + ch.trst = timing.reset_us; +} + +// Resolve bc.channels from type + colorOrder (no-op when bc.channels.custom). +// 2-pin (clocked) buses have a fixed wire format, so custom channel maps are cleared for them. +inline void resolveBusConfigLayout(BusConfig& bc) { + if (busTypeIs2Pin(bc.type)) bc.channels.custom = false; + resolveChannelLayout(bc.channels, bc.type, bc.colorOrder); +} + +// Type-derived hardware properties: prefix/suffix bytes and output inversion (special chip types only) +inline void applyTypeFraming(WLEDpixelBus::BusProperties& props, uint8_t busType) { + if (busType == TYPE_TM1814 || busType == TYPE_TM1815) { + props.prefixLen = WLEDpixelBus::TM1814_PREFIX_LEN; // prefix data is calculated per frame and contains brightness scaling + props.invertOutput = true; + } else if (busType == TYPE_TM1914) { + props.prefixLen = WLEDpixelBus::TM1914_PREFIX_LEN; + memcpy(props.prefixData, WLEDpixelBus::TM1914_PREFIX, sizeof(WLEDpixelBus::TM1914_PREFIX)); + props.invertOutput = true; + } else if (busType == TYPE_SM16825) { + props.suffixLen = WLEDpixelBus::SM16825_SUFFIX_LEN; + memcpy(props.suffixData, WLEDpixelBus::SM16825_SUFFIX, sizeof(WLEDpixelBus::SM16825_SUFFIX)); + } +} + +// Assemble the complete one-wire wire descriptor from a resolved channel layout (this is then used to create the actual bus) +// Note: all (or most) of the above legacy handling could be removed if the full bus properties were stored in config (and also handed down from the UI) +inline WLEDpixelBus::BusProperties makeBusProperties(const BusChannelLayout& layout, uint8_t protocolType) { + WLEDpixelBus::BusProperties props; + props.numChannels = layout.numChannels <= WLEDpixelBus::MAX_CUSTOM_CHANNELS ? layout.numChannels : WLEDpixelBus::MAX_CUSTOM_CHANNELS; + memcpy(props.channelMap, layout.channelMap, sizeof(props.channelMap)); + // convert WW/CW to W if only one is present (WW or CW are only calculated for CCT, a single WW channel is never set) + bool hasWW = false, hasCW = false; + for (uint8_t i = 0; i < props.numChannels; i++) { + hasWW |= props.channelMap[i] == WLEDpixelBus::CH_WW; + hasCW |= props.channelMap[i] == WLEDpixelBus::CH_CW; + } + if (hasWW != hasCW) { + for (uint8_t i = 0; i < props.numChannels; i++) { + if (props.channelMap[i] == WLEDpixelBus::CH_WW || props.channelMap[i] == WLEDpixelBus::CH_CW) + props.channelMap[i] = WLEDpixelBus::CH_W; } + } + props.invertMask = layout.invertMask; + props.is16bit = layout.is16bit; + props.invertOutput = layout.invertOutput; + props.timing = WLEDpixelBus::LedTiming(layout.t0h, layout.t0l, layout.t1h, layout.t1l, layout.trst); + applyTypeFraming(props, protocolType); + return props; +} + +class PixelBusAllocator { + private: + #ifndef ESP8266 + static uint8_t _rmtChannelsAssigned; + static uint8_t _parHwChannelsAssigned; // parallel output channels: I2S/LCD (ESP32/S2/S3), parallel SPI (C3), PARLIO (C6/H2/C5/P4) + static uint8_t _parHwBusType; // Track first parallel bus type to enforce parallel timing + static uint8_t _bitBangChannelsAssigned; + static uint8_t _bitBangBusType; // Track first BitBang type to enforce parallel timing + static uint8_t _hardwareSPIused; // number of hardware SPI's used, currently only one SPI output is supported. On C3, parallel SPI output takes priority + #else + static uint8_t _bitBangBusType; // Track first ESP8266 BitBang type to enforce parallel timing + #endif + public: - return busPtr; + // Shared-clock drivers (parallel HW, bit-bang) lock every channel to the first bus type. + static uint8_t checkParallelType(uint8_t busType, uint8_t driverType) { + #ifndef ESP8266 + if (driverType == BUSDRV_PARHW && _parHwBusType != 0) return _parHwBusType; + #endif + if (driverType == BUSDRV_BITBANG && _bitBangBusType != 0) return _bitBangBusType; + return busType; } - 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; + static void resetChannelTracking() { + #ifndef ESP8266 + _rmtChannelsAssigned = 0; + _parHwChannelsAssigned = 0; + _parHwBusType = 0; // TYPE_NONE + _bitBangChannelsAssigned = 0; + _bitBangBusType = 0; // TYPE_NONE + _hardwareSPIused = 0; + WLEDpixelBus::RmtBus::resetChannelTracking(); + #if (WLED_MAX_BB_CHANNELS > 0) + WLEDpixelBus::BitBangBus::resetChannelTracking(); #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - #endif + #else + _bitBangBusType = 0; // TYPE_NONE + WLEDpixelBus::BitBangBus::resetChannelTracking(); #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; - } } - static bool canShow(void* busPtr, uint8_t busType) { - switch (busType) { - case I_NONE: return true; - #ifdef ESP8266 - case I_8266_U0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_400_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - #endif + // RMT driver sizes its channel pool from this count, so it must be set before the RMT buses are constructed + static void setExpectedRmtChannelCount(uint8_t count) { + #ifndef ESP8266 + WLEDpixelBus::RmtBus::setExpectedChannelCount(count); #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 bool allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { + if (!busTypeIsDigital(busType)) return false; - // 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 - } - // 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 (busTypeIs2Pin(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) } - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #ifndef ESP8266 + // Driver fallback order: requested driver -> parallel bus (I2S/LCD/SPI/PARLIO) -> BitBang. + // If the requested driver has no free channel (or is unsupported on this target), the next + // available driver is used instead. + if (driverType == BUSDRV_RMT && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { + _rmtChannelsAssigned++; + return true; } - } - - [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { - RgbwColor col(0,0,0,0); - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_BB_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_U0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_U1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_DM_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_BB_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_U0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - #endif - #endif - case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + if (driverType != BUSDRV_BITBANG && _parHwChannelsAssigned < WLED_MAX_PARHW_CHANNELS) { + // BUSDRV_PARHW maps to the chip's parallel output peripheral: I2S on ESP32/S2, LCD on S3, parallel SPI on C3, PARLIO on C6/H2/C5/P4, all use 4-step cadence + driverType = BUSDRV_PARHW; + if (_parHwChannelsAssigned == 0) { + _parHwBusType = busType; // lock LED type to first parallel channel + #ifdef CONFIG_IDF_TARGET_ESP32C3 + _hardwareSPIused++; // reserve SPI: C3 uses parallel SPI output for "I2S" and takes priority over 2pin buses + #endif + } + _parHwChannelsAssigned++; + return true; } - - // 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 + // Last resort (or explicitly requested): parallel BitBang, all channels share one timing + if (_bitBangChannelsAssigned < WLED_MAX_BB_CHANNELS) { + driverType = BUSDRV_BITBANG; + if (_bitBangBusType == 0) { + _bitBangBusType = busType; // lock LED type to first BitBang channel + } else if (_bitBangBusType != busType) { + return false; // mismatched LED type — all BitBang channels must share timing + } + _bitBangChannelsAssigned++; + return true; } - 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 false; // No channels available + #else + // ESP8266: assign driverType based on pin number so BusManager::show() can sequence correctly. + // GPIO1/2 → UART (async, fire-and-forget ISR) + // GPIO3 → DMA (async, I2S SLC DMA) + // others → BitBang (interrupt-blocking — must run before async buses) + if (pins[0] == 1 || pins[0] == 2) { + driverType = BUSDRV_RMT; // reuse BUSDRV_RMT as "async UART" sentinel on ESP8266 + } else if (pins[0] == 3) { + driverType = BUSDRV_PARHW; // async DMA + } else { + driverType = BUSDRV_BITBANG; + // Enforce single LED type for parallel timing + if (_bitBangBusType == 0) { + _bitBangBusType = busType; + } else if (_bitBangBusType != busType) { + return false; // mismatched LED type — all ESP8266 BitBang channels must share timing + } } - return 0; - } - - static void cleanup(void* busPtr, uint8_t busType) { - if (busPtr == nullptr) return; - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_U1_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_DM_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_BB_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_U0_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_U1_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_DM_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_BB_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_U0_400_3: delete (static_cast(busPtr)); break; - case I_8266_U1_400_3: delete (static_cast(busPtr)); break; - case I_8266_DM_400_3: delete (static_cast(busPtr)); break; - case I_8266_BB_400_3: delete (static_cast(busPtr)); break; - case I_8266_U0_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_BB_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_U0_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_U1_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_DM_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_BB_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_U0_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_U1_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_DM_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_BB_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_U0_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_U1_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_DM_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_BB_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_U0_2805_5: delete (static_cast(busPtr)); break; - case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; - case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; - case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; - case I_8266_U0_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_U0_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_U1_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_DM_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_BB_SM16825_5: delete (static_cast(busPtr)); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; - case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; - case I_32_RN_400_3: delete (static_cast(busPtr)); break; - case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; - case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; - case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; - case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; - case I_32_RN_2805_5: delete (static_cast(busPtr)); break; - case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; - case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; - // I2S1 bus or paralell buses - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - #endif #endif - case I_HS_DOT_3: delete (static_cast(busPtr)); break; - case I_SS_DOT_3: delete (static_cast(busPtr)); break; - case I_HS_LPD_3: delete (static_cast(busPtr)); break; - case I_SS_LPD_3: delete (static_cast(busPtr)); break; - case I_HS_LPO_3: delete (static_cast(busPtr)); break; - case I_SS_LPO_3: delete (static_cast(busPtr)); break; - case I_HS_WS1_3: delete (static_cast(busPtr)); break; - case I_SS_WS1_3: delete (static_cast(busPtr)); break; - case I_HS_P98_3: delete (static_cast(busPtr)); break; - case I_SS_P98_3: delete (static_cast(busPtr)); break; - } - } - static unsigned getDataSize(void* busPtr, uint8_t busType) { - unsigned size = 0; - #ifdef ARDUINO_ARCH_ESP32 - size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266) - #endif - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_NEO_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_NEO_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_400_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM1_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM2_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_UCS_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_UCS_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_APA106_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_FW6_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_2805_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses (front + back + small system managed RMT) - case I_32_RN_NEO_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_NEO_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_400_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM1_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM2_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_UCS_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_UCS_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_APA106_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_FW6_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_2805_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM1914_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_SM16825_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - // I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence - #if defined(WLED_HAS_PARALLEL_I2S) - case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - #endif - #endif - case I_HS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_HS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - case I_SS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; - } - return size; + return true; } - static unsigned memUsage(unsigned count, unsigned busType) { - unsigned size = count*3; // let's assume 3 channels, we will add count or 2*count below for 4 channels or 5 channels - switch (busType) { - case I_NONE: size = 0; break; - #ifdef ESP8266 - // UART methods have front + back buffers + small UART - case I_8266_U0_NEO_4 : // fallthrough - case I_8266_U1_NEO_4 : // fallthrough - case I_8266_BB_NEO_4 : // fallthrough - case I_8266_U0_TM1_4 : // fallthrough - case I_8266_U1_TM1_4 : // fallthrough - case I_8266_BB_TM1_4 : size = (size + count); break; // 4 channels - case I_8266_U0_UCS_3 : // fallthrough - case I_8266_U1_UCS_3 : // fallthrough - case I_8266_BB_UCS_3 : size *= 2; break; // 16 bit - case I_8266_U0_UCS_4 : // fallthrough - case I_8266_U1_UCS_4 : // fallthrough - case I_8266_BB_UCS_4 : size = (size + count)*2; break; // 16 bit 4 channels - case I_8266_U0_FW6_5 : // fallthrough - case I_8266_U1_FW6_5 : // fallthrough - case I_8266_BB_FW6_5 : // fallthrough - case I_8266_U0_2805_5 : // fallthrough - case I_8266_U1_2805_5 : // fallthrough - case I_8266_BB_2805_5 : size = (size + 2*count); break; // 5 channels - case I_8266_U0_SM16825_5: // fallthrough - case I_8266_U1_SM16825_5: // fallthrough - case I_8266_BB_SM16825_5: size = (size + 2*count)*2; break; // 16 bit 5 channels - // DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB) - case I_8266_DM_NEO_3 : // fallthrough - case I_8266_DM_400_3 : // fallthrough - case I_8266_DM_TM2_3 : // fallthrough - case I_8266_DM_APA106_3 : // fallthrough - case I_8266_DM_TM1914_3 : size *= 5; break; - case I_8266_DM_NEO_4 : // fallthrough - case I_8266_DM_TM1_4 : size = (size + count)*5; break; - case I_8266_DM_UCS_3 : size *= 2*5; break; - case I_8266_DM_UCS_4 : size = (size + count)*2*5; break; - case I_8266_DM_FW6_5 : // fallthrough - case I_8266_DM_2805_5 : size = (size + 2*count)*5; break; - case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break; - #else - // note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity - // RMT buses (1x front and 1x back buffer, does not include small RMT buffer) - case I_32_RN_NEO_4 : // fallthrough - case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels - case I_32_RN_UCS_3 : size *= 2*2; break; // 16bit - case I_32_RN_UCS_4 : size = (size + count)*2*2; break; // 16bit, 4 channels - case I_32_RN_FW6_5 : // fallthrough - case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels - case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels - // I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD) - #if defined(WLED_HAS_PARALLEL_I2S) || defined(CONFIG_IDF_TARGET_ESP32) - case I_32_I2_NEO_3 : // fallthrough - case I_32_I2_400_3 : // fallthrough - case I_32_I2_TM2_3 : // fallthrough - case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer - case I_32_I2_NEO_4 : // fallthrough - case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels - case I_32_I2_UCS_3 : size *= 2; break; // 16 bit - case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels - case I_32_I2_FW6_5 : // fallthrough - case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels - case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels - #endif - default : size *= 2; break; // everything else uses 2 buffers - #endif - } - return size; - } -#ifndef ESP8266 - // Reset channel tracking (call before adding buses) - static void resetChannelTracking() { - _useParallelI2S = false; - _rmtChannelsAssigned = 0; - _rmtChannel = 0; - _i2sChannelsAssigned = 0; - _parallelBusItype = I_NONE; - _2PchannelsAssigned = 0; - } -#endif - // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins - static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { - if (!Bus::isDigital(busType)) return I_NONE; - uint8_t t = I_NONE; - if (Bus::is2Pin(busType)) { //SPI LED chips +// One-wire buses require properties (built from the resolved LED type). Clocked 2-pin buses ignore them. +static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t colorOrder, uint8_t driverType = BUSDRV_RMT, uint8_t busSpeedFactor = 100, uint16_t frequencykHz = 0, const WLEDpixelBus::BusProperties* properties = nullptr) { + if (!busTypeIsDigital(busType)) return nullptr; + + // Clocked chips are not part of the one-wire property path and do not share the parallel-type lock. + if (busTypeIs2Pin(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 + WLEDpixelBus::SpiBus* bus = new WLEDpixelBus::SpiBus(pins[0], pins[1], frequencykHz, isHSPI, busType); // TODO: move this into createbus function? + // Override the generic co-based encoder with the legacy chip-specific wire layout + // (WS2801 = R,B,G; APA102/P9813 = B,G,R; LPD8806/LPD6803 = G,R,B). + uint8_t channelMap[WLEDpixelBus::MAX_CUSTOM_CHANNELS]; + const uint8_t wireCh = legacyColorOrderToChannelMap(busType, colorOrder, channelMap); + bus->setEncoder(WLEDpixelBus::ColorEncoder(channelMap, wireCh, 0, false)); + return bus; + } - // 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) + const uint8_t requestedType = busType; + const uint8_t sharedTimingType = checkParallelType(busType, driverType); + + WLEDpixelBus::BusDriver driver = WLEDpixelBus::BusDriver::RMT; // always overwritten below; initialised to avoid unused-variable warning + + #ifdef ESP8266 + if (pins[0] == 1 || pins[0] == 2) driver = WLEDpixelBus::BusDriver::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled + else if (pins[0] == 3) driver = WLEDpixelBus::BusDriver::DMA; // DMA method uses a lot of RAM! + else driver = WLEDpixelBus::BusDriver::BitBang; + #elif !defined(CONFIG_IDF_TARGET_ESP32C61) + switch (driverType) { + case BUSDRV_RMT: + driver = WLEDpixelBus::BusDriver::RMT; + break; + case BUSDRV_PARHW: + #if defined(CONFIG_IDF_TARGET_ESP32C3) //TODO: should use hardware capabilities instead of MCU types + driver = WLEDpixelBus::BusDriver::PARSPI; // parallel SPI on C3 + #elif defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32P4) + driver = WLEDpixelBus::BusDriver::PARLIO; // PARLIO on C5, C6, H2, P4 + #else + driver = WLEDpixelBus::BusDriver::I2S; // ESP32 & S2: use parallel I2S, S3: use LCD parallel out #endif - } - else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag - _useParallelI2S = true; - t = _parallelBusItype; - } - #endif + break; + default: + driver = WLEDpixelBus::BusDriver::BitBang; + break; + } + #else + // C61 only supports BB for now (might be able to use parallel SPI), it has no RMT, no I2S, no PARLIO hardware + driver = WLEDpixelBus::BusDriver::BitBang; + #endif + + if (!properties) return nullptr; // one-wire buses are created from an explicit property set + WLEDpixelBus::BusProperties wire = *properties; + // Caller should already have used checkParallelType(). If it did not, keep the shared clock consistent. + if (sharedTimingType != requestedType) wire.timing = WLEDpixelBus::getLedTiming(sharedTimingType); + if (busSpeedFactor != 100) { + float factor = (float)busSpeedFactor / 100.0f; + wire.timing = WLEDpixelBus::scaleTiming(wire.timing, factor); } - return t; + + return WLEDpixelBus::createBus(driver, pins[0], wire, requestedType, len); } }; #endif diff --git a/wled00/button.cpp b/wled00/button.cpp index 7171c7e668..1644c6ff8e 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -362,7 +362,7 @@ void handleButton() } } -// handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT but *before* strip.service() +// handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT16 but *before* strip.service() // where actual LED painting occurrs // this is important for relay control and in the event of turning off on-board LED void handleIO() @@ -376,7 +376,7 @@ void handleIO() void handleOnOff(bool forceOff) { - if (strip.getBrightness() && !forceOff) { + if (strip.getBrightness16() && !forceOff) { lastOnTime = millis(); if (offMode) { BusManager::on(); diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index d35f8a56ef..304e1a4c50 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -1,5 +1,6 @@ #include "wled.h" #include "wled_ethernet.h" +#include "bus_wrapper.h" /* * Serializes and parses the cfg.json and wsec.json settings files, stored in internal FS. @@ -253,11 +254,42 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { maPerLed = 0; maMax = 0; } - ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available + uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=parallel HW (I2S/LCD/SPI/PARLIO, chip-dependent) note: polybus may override this if driver is not available String host = elm[F("text")] | String(); - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); + uint8_t bsf = (uint8_t)(elm[F("bsf")] | 100); + busConfigs.emplace_back(ledType, pins, start, length); + BusConfig& bc = busConfigs.back(); + bc.colorOrder = colorOrder; + bc.reversed = reversed; + bc.skipAmount = skipFirst; + bc.refreshReq = refresh; + bc.autoWhite = AWmode; + bc.frequency = freqkHz; + bc.milliAmpsPerLed = maPerLed; + bc.milliAmpsMax = maMax; + bc.driverType = driverType; + bc.text = host; + bc.busSpeedFactor = bsf; + // Explicit channel-map/timing override. Absent or cch==0 means the type defaults build the bus. + bc.channels.numChannels = (uint8_t)constrain((int)(elm["cch"] | 0), 0, WLEDpixelBus::MAX_CUSTOM_CHANNELS); + bc.channels.custom = bc.channels.numChannels != 0; + if (bc.channels.custom) { + JsonArrayConst cmap = elm["cmap"]; + if (!cmap.isNull()) { + for (uint8_t ci = 0; ci < WLEDpixelBus::MAX_CUSTOM_CHANNELS && ci < cmap.size(); ci++) + bc.channels.channelMap[ci] = (uint8_t)constrain((int)cmap[ci], 0, 6); + } + bc.channels.invertMask = elm["cinv"] | 0; + bc.channels.is16bit = elm["c16"] | false; + bc.channels.invertOutput = elm["cio"] | false; + bc.channels.t0h = elm["ct0h"] | 300; + bc.channels.t0l = elm["ct0l"] | 900; + bc.channels.t1h = elm["ct1h"] | 700; + bc.channels.t1l = elm["ct1l"] | 500; + bc.channels.trst = elm["crst"] | 300; + } + resolveBusConfigLayout(bc); // fill the wire layout from type + color order unless custom doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -340,13 +372,16 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { unsigned start = 0; // analog always has length 1 if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; - busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default) + busConfigs.emplace_back(dataType, defPin, start, count); + busConfigs.back().colorOrder = DEFAULT_LED_COLOR_ORDER; + resolveBusConfigLayout(busConfigs.back()); doInitBusses = true; // finalization done in beginStrip() } } if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration + BusManager::getColorOrderMap().reset(); // clear stale entries in case config is re-loaded without reboot JsonArray hw_com = hw[F("com")]; if (!hw_com.isNull()) { BusManager::getColorOrderMap().reserve(std::min(hw_com.size(), (size_t)WLED_MAX_COLOR_ORDER_MAPPINGS)); @@ -540,7 +575,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; @@ -1029,6 +1064,21 @@ void serializeConfig(JsonObject root) { setConfigValue(ins, F("ledma"), bus->getLEDCurrent()); setConfigValue(ins, F("drv"), bus->getDriverType()); setConfigValue(ins, F("text"), bus->getCustomText()); + setConfigValue(ins, F("bsf"), bus->getBusSpeedFactor()); + if (bus->getChannelLayout().custom) { + const BusChannelLayout& cb = bus->getChannelLayout(); + setConfigValue(ins, "cch", cb.numChannels); + JsonArray cmap = ins.createNestedArray("cmap"); + for (uint8_t i = 0; i < cb.numChannels; i++) cmap.add(cb.channelMap[i]); // emit only active channels; deserializer guards with cmap.size() + setConfigValue(ins, "cinv", cb.invertMask); + setConfigValue(ins, "c16", cb.is16bit); + setConfigValue(ins, "cio", cb.invertOutput); + setConfigValue(ins, "ct0h", cb.t0h); + setConfigValue(ins, "ct0l", cb.t0l); + setConfigValue(ins, "ct1h", cb.t1h); + setConfigValue(ins, "ct1l", cb.t1l); + setConfigValue(ins, "crst", cb.trst); + } } JsonArray hw_com = hw.createNestedArray(F("com")); diff --git a/wled00/colors.cpp b/wled00/colors.cpp index e45b7c2700..af3e6f82ac 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -668,8 +668,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..2902ed3cbc 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -26,6 +26,16 @@ #define B(c) (byte(c)) #define W(c) (byte((c) >> 24)) +struct CctPixel { + union { + uint16_t wwcw; // Packed as 0xCWWW: cool white in the high byte, warm white in the low byte + struct { + uint8_t ww; + uint8_t cw; + }; + }; +}; + struct CRGBW; // forward declations struct CHSV32; @@ -39,7 +49,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 9b22a23ada..458a946527 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -63,12 +63,13 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #endif #ifdef ESP8266 - #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_DIGITAL_CHANNELS 8 #define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S - #define WLED_MAX_I2S_CHANNELS 0 + #define WLED_MAX_PARHW_CHANNELS 0 + #define WLED_MAX_BB_CHANNELS 4 // ESP8266 parallel bit-bang channels (all share same LED type/timing) can be set to 8 if more outputs are needed #define WLED_MAX_ANALOG_CHANNELS 5 #define WLED_MAX_TIMERS 16 // reduced limit for ESP8266 due to memory constraints - #define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix! + #define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix! #else #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX) #include "driver/ledc.h" // needed for analog/LEDC channel counts @@ -84,37 +85,62 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - // SOC_I2S_NUM, SOC_RMT_CHANNELS_PER_GROUP, etc // see https://github.com/wled/WLED/pull/5048#issuecomment-3868678248 - #if defined(CONFIG_IDF_TARGET_ESP32C3) - #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! + #if defined(CONFIG_IDF_TARGET_ESP32) + #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_PARHW_CHANNELS 16 + #else + #define WLED_MAX_PARHW_CHANNELS 8 + #endif + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused (saves some flash and RAM) + #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP type in UI, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + #define WLED_MAX_RMT_CHANNELS 1 // hardware has 2 RMT output channels, using both causes flickering (ISR latency) so we only allow one + #define WLED_MAX_PARHW_CHANNELS 4 // uses SPI hardware in 4x parallel output + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels + #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels - #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_PARHW_CHANNELS 16 + #else + #define WLED_MAX_PARHW_CHANNELS 8 + #endif + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels + #define WLED_PLATFORM_ID 3 // 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! - #else - #if defined(CONFIG_IDF_TARGET_ESP32) // classic esp32 - #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels - #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 16 - #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! - #else // all other risc-v based boards: same as C3 - #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types - falls back to "C3" until we have a proper fix! - #endif + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLED_MAX_PARHW_CHANNELS 16 // uses LCD parallel output and supports up to 16 parallel channels + #else + #define WLED_MAX_PARHW_CHANNELS 8 + #endif + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused (saves some flash and RAM) + #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32C5) + #define WLED_MAX_RMT_CHANNELS 1 // hardware has 2 RMT output channels, using both causes flickering (ISR latency) so we only allow one + #define WLED_MAX_PARHW_CHANNELS 8 // PARLIO peripheral provides 8 parallel LED output channels on these targets (IDF >= 5.3) + #define WLED_MAX_BB_CHANNELS 8 // BitBang fallback when PARLIO is unavailable + #define WLED_PLATFORM_ID 5 // used in UI to distinguish ESP type in UI, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32C6) + #define WLED_MAX_RMT_CHANNELS 1 // hardware has 2 RMT output channels, using both causes flickering (ISR latency) so we only allow one + #define WLED_MAX_PARHW_CHANNELS 8 // ESP32-C6 uses PARLIO peripheral for 8 parallel LED channels (IDF >= 5.3) + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels (fallback if PARLIO unavailable) + #define WLED_PLATFORM_ID 6 // used in UI to distinguish ESP types, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32C61) + #define WLED_MAX_RMT_CHANNELS 0 // BitBang only: every digital bus is created as BitBang on C61 (see PixelBusAllocator::create) + #define WLED_MAX_PARHW_CHANNELS 0 + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels + #define WLED_PLATFORM_ID 7 // used in UI to distinguish ESP type in UI, needs a proper fix! + #elif defined(CONFIG_IDF_TARGET_ESP32P4) + #define WLED_MAX_RMT_CHANNELS 0 // RMT output is untested on P4 + #define WLED_MAX_PARHW_CHANNELS 8 // PARLIO peripheral provides 8 parallel LED output channels (IDF >= 5.3) + #define WLED_MAX_BB_CHANNELS 8 // BitBang fallback when PARLIO is unavailable + #define WLED_PLATFORM_ID 8 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_TIMERS 64 // maximum number of timers #ifndef WLED_MAX_DIGITAL_CHANNELS - #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS) + #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_PARHW_CHANNELS + WLED_MAX_BB_CHANNELS) // total number of digital channels (RMT + parallel + BitBang) #else #warning "buildenv overrides WLED_MAX_DIGITAL_CHANNELS - please check that the value is correct" #endif @@ -346,7 +372,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); //#define TYPE_WS2812_2CH_X3 20 // use FW1906 #define TYPE_WS2812_WWA 21 //amber + warm + cold white #define TYPE_WS2812_RGB 22 -#define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern) +//#define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern), unused just use WS2812 with off refresh #define TYPE_WS2811_400KHZ 24 //half-speed WS2812 protocol, used by very old WS2811 units #define TYPE_TM1829 25 #define TYPE_UCS8903 26 @@ -354,10 +380,12 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC) #define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp) #define TYPE_SK6812_RGBW 30 -#define TYPE_TM1814 31 +#define TYPE_TM1814 31 //RGBW #define TYPE_WS2805 32 //RGB + WW + CW #define TYPE_TM1914 33 //RGB #define TYPE_SM16825 34 //RGB + WW + CW +#define TYPE_TM1815 35 //RGBW (half speed TM1814) +//TODO add support for SM16714, see https://wled.discourse.group/t/sm16714-pixel-ic/9794 #define TYPE_DIGITAL_MAX 39 // last usable digital type //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 461aa9d387..f76df3c597 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -387,4 +387,4 @@ - + \ No newline at end of file diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 446282777e..68e67f9b40 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -6,10 +6,10 @@ LED Settings @@ -1035,7 +1276,6 @@

LED setup

LED outputs:

-

LED memory usage: 0 / ? B
@@ -1169,4 +1409,4 @@

Advanced

- + \ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 4566ef33e8..351de7981e 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -207,7 +207,7 @@ void stateUpdated(byte callMode); void updateInterfaces(uint8_t callMode); void handleTransitions(); void handleNightlight(); -byte scaledBri(byte in); +uint16_t scaledBri16(uint16_t in); #ifdef WLED_ENABLE_LOXONE //lx_parser.cpp diff --git a/wled00/file.cpp b/wled00/file.cpp index 361f991491..c05f6299d7 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -490,6 +490,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/led.cpp b/wled00/led.cpp index be3e3d492a..6699161be1 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -41,7 +41,7 @@ void applyValuesToSelectedSegs() { void toggleOnOff() { - briOld = briT; // briT = 0 when off, briT = bri when on or in between while transitioning, store current value so brightness does not jump when toggling on/off during a transition + briOld16 = briT16; // adopt current brightness so toggling during a transition does not jump if (bri == 0) { bri = briLast; strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); @@ -55,32 +55,32 @@ void toggleOnOff() //scales the brightness with the briMultiplier factor -byte scaledBri(byte in) +uint16_t scaledBri16(uint16_t in) { - unsigned val = ((unsigned)in*briMultiplier)/100; - if (val > 255) val = 255; - return (byte)val; + uint32_t val = ((uint32_t)in * briMultiplier) / 100; + if (val > (255U << 8)) val = 255U << 8; + return (uint16_t)val; } -//applies global temporary brightness (briT) to strip +//applies global temporary brightness (briT16) to strip void applyBri() { if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) - strip.setBrightness(briT); + strip.setBrightness16(briT16); } //applies global brightness and sets it as the "current" brightness (no transition) void applyFinalBri() { - briOld = bri; - briT = bri; + briOld16 = (uint16_t)bri << 8; + briT16 = briOld16; applyBri(); strip.trigger(); // force one last update } // local function to handle global brightness transition, called from stateUpdated(). Note: power flags are set in toggleOnOff() void handleBriChange() { - //DEBUG_PRINTF_P(PSTR("state update: briT: %d bri: %d briOld: %d, isPoweron: %d , isPoweroff %d, trigger: %d\n"), (int)briT, (int)bri, (int)briOld, (int)strip.isPoweringOn(), (int)strip.isPoweringOff(), (int)strip.isPowerTrigger()); + //DEBUG_PRINTF_P(PSTR("state update: briT16: %d bri: %d briOld16: %d, isPoweron: %d , isPoweroff %d, trigger: %d\n"), (int)briT16, (int)bri, (int)briOld16, (int)strip.isPoweringOn(), (int)strip.isPoweringOff(), (int)strip.isPowerTrigger()); if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; @@ -89,10 +89,10 @@ void handleBriChange() { } else { uint32_t now = millis(); - if ((!strip.isPoweringOn() && bri > 0 && (briOld == 0 || strip.isPoweringOff()))) { + if ((!strip.isPoweringOn() && bri > 0 && (briOld16 == 0 || strip.isPoweringOff()))) { // Brightness was increased from zero; treat as power on if we're not already strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); - } else if (!strip.isPoweringOff() && bri == 0 && (briOld > 0 || strip.isPoweringOn())) { + } else if (!strip.isPoweringOff() && bri == 0 && (briOld16 > 0 || strip.isPoweringOn())) { // Brightness was decreased to zero; treat as power off if we're not already strip.setPowerFlag(TRANSITION_POWER_OFF | TRANSITION_POWER_TRIGGER); } @@ -105,13 +105,15 @@ void handleBriChange() { int duration = strip.getTransition(); int elapsed = constrain((int)(now - transitionStartTime), 0, duration); // duration may have changed mid-flight int progress = duration - elapsed; - int delta = (bri > 0 || blendingStyle == TRANSITION_FADE) ? (int)briT - (int)bri : 0; // spatial power-off doesn't fade + const int32_t targetBri = (uint16_t)bri << 8; + int32_t delta = (bri > 0 || blendingStyle == TRANSITION_FADE) ? (int32_t)briT16 - targetBri : 0; // spatial power-off doesn't fade if (delta) { - int room = (delta > 0) ? 255 - bri : bri; // headroom for the fade start on briT's side of bri - elapsed = max(elapsed, (duration * abs(delta) + room - 1) / room); // target moved: stretch so the start stays in range + int32_t room = (delta > 0) ? (255U << 8) - targetBri : targetBri; // headroom for the fade start on briT's side of bri + int32_t absDelta = delta > 0 ? delta : -delta; + elapsed = max(elapsed, (int)((duration * (int64_t)absDelta + room - 1) / room)); // target moved: stretch so the start stays in range } - if (elapsed) briOld = bri + delta * duration / elapsed; // extrapolate from bri back through briT - transitionStartTime = now - (duration - elapsed); // invert progress + if (elapsed) briOld16 = targetBri + (int32_t)((int64_t)delta * duration / elapsed); // extrapolate from bri back through briT + transitionStartTime = now - (duration - elapsed); // invert progress strip.clearPowerFlag((bri > 0) ? TRANSITION_POWER_OFF : TRANSITION_POWER_ON); // bri carries the target state } else @@ -123,7 +125,7 @@ void handleBriChange() { strip.restartRuntime(); // and restart any running effect when powering on if (blendingStyle != TRANSITION_FADE) applyFinalBri(); // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? } - briOld = briT; // Adopt the current state of any existing fade transition + briOld16 = briT16; // Adopt the current state of any existing fade transition transitionStartTime = now; // (Re)start global transition timer } @@ -133,13 +135,13 @@ void handleBriChange() { strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); } else { - if (bri == briOld) return; // no change found + if (((uint16_t)bri << 8) == briOld16) return; // no change found // Purely a brightness change. // Strictly speaking someone might have set brightness 0->0 so we have to ignore that case during power off. if (!strip.isPoweringOff()) { // Any existing transition re-starts from where we are - briOld = briT; + briOld16 = briT16; transitionStartTime = now; } } @@ -156,11 +158,11 @@ void stateUpdated(byte callMode) { // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa 11: ws send only 12: button preset setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() - if (bri != briOld || stateChanged) { + if (((uint16_t)bri << 8) != briOld16 || stateChanged) { if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset if (callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) notify(callMode); - if (bri != briOld && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state + if (((uint16_t)bri << 8) != briOld16 && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state //set flag to update ws and mqtt interfaceUpdateCallMode = callMode; @@ -177,7 +179,7 @@ void stateUpdated(byte callMode) { nightlightDelayMs -= (now - nightlightStartTime); nightlightStartTime = now; } - if (briT == 0) { + if (briT16 == 0) { if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning } @@ -234,13 +236,13 @@ void handleTransitions() { applyFinalBri(); return; } - // fade global brightness from briOld to bri, skip if powering off using spatial transition (avoid fading in parallel) - // note: power on sets briOld = bri so it wont fade but still allows global brightness change during that transition which then will fade + // fade global brightness from briOld16 to bri, skip if powering off using spatial transition (avoid fading in parallel) + // note: power on starts at the target brightness, but a later brightness change can still fade if (!strip.isPoweringOff() || blendingStyle == TRANSITION_FADE) { - byte briTO = briT; - int deltaBri = (int)bri - (int)briOld; - briT = briOld + (deltaBri * progress / duration); - if (briTO != briT) applyBri(); + uint16_t briTO = briT16; + int32_t deltaBri = (int32_t)((uint16_t)bri << 8) - (int32_t)briOld16; + briT16 = (uint16_t)((int32_t)briOld16 + ((int64_t)deltaBri * progress / duration)); + if (briTO != briT16) applyBri(); } } } diff --git a/wled00/set.cpp b/wled00/set.cpp index 21885b0f02..25654c3381 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -1,4 +1,5 @@ #include "wled.h" +#include "bus_wrapper.h" // resolveBusConfigLayout() /* * Receives client input @@ -227,6 +228,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) @@ -280,12 +282,54 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) maPerLed = request->arg(la).toInt(); maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } - type |= request->hasArg(rf) << 7; // off refresh override driverType = request->arg(ld).toInt(); // 0=RMT (default), 1=I2S text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text); + uint8_t bsf = request->hasArg(sf) ? (uint8_t)request->arg(sf).toInt() : 100; + busConfigs.emplace_back(type, pins, start, length); + BusConfig& bc = busConfigs.back(); + bc.colorOrder = colorOrder | (channelSwap<<4); + bc.reversed = request->hasArg(cv); + bc.skipAmount = skip; + bc.refreshReq = request->hasArg(rf); // off refresh override + bc.autoWhite = awmode; + bc.frequency = freq; + bc.milliAmpsPerLed = maPerLed; + bc.milliAmpsMax = maMax; + bc.driverType = driverType; + bc.text = text; + bc.busSpeedFactor = bsf; + // Customized channel map/timing override: parsed for any digital LED type when the "customize" checkbox (CBen) is submitted. + char cben[7] = "CBen"; cben[4] = offset+s; cben[5] = 0; + bc.channels.custom = request->hasArg(cben); + if (bc.channels.custom) { + 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.channels.numChannels = (uint8_t)constrain(request->arg(cbch).toInt(), 0, WLEDpixelBus::MAX_CUSTOM_CHANNELS); + bc.channels.invertOutput = request->hasArg(cbio); + bc.channels.is16bit = request->hasArg(cbb); + bc.channels.t0h = request->arg(cbt0h).toInt(); + bc.channels.t0l = request->arg(cbt0l).toInt(); + bc.channels.t1h = request->arg(cbt1h).toInt(); + bc.channels.t1l = request->arg(cbt1l).toInt(); + bc.channels.trst = request->arg(cbrst).toInt(); + bc.channels.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.channels.channelMap[ci] = (uint8_t)constrain(request->arg(cbc).toInt(), 0, 6); + if (request->hasArg(cbi)) bc.channels.invertMask |= (1u << ci); + } + } + if (bc.channels.numChannels == 0) bc.channels.custom = false; // no channels entered: fall back to type defaults + resolveBusConfigLayout(bc); // fill the wire layout from type + color order unless custom 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 97% rename from lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S rename to wled00/src/WLEDpixelBus/ESP32RmtHI.S index bc28c1a840..c7f6900f5b 100644 --- a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S +++ b/wled00/src/WLEDpixelBus/ESP32RmtHI.S @@ -2,16 +2,16 @@ * 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) +#if defined(__XTENSA__) && defined(ARDUINO_ARCH_ESP32) #include "esp_idf_version.h" #include "sdkconfig.h" /* If the Bluetooth driver has hooked the high-priority interrupt, we piggyback on it and don't need this. */ -#if !defined(CONFIG_BTDM_CTRL_HLI) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) +#if !defined(CONFIG_BTDM_CTRL_HLI) #include #include "soc/soc.h" @@ -167,7 +167,7 @@ _highint_stack_switch: /* Call C handler */ mov a6, sp - call4 NeoEsp32RmtMethodIsr + call4 NeoEsp32RmtMethodISR l32e sp, sp, -12 /* switch back to the original stack */ diff --git a/wled00/src/WLEDpixelBus/RmtHIDriver.h b/wled00/src/WLEDpixelBus/RmtHIDriver.h new file mode 100644 index 0000000000..60e0effa65 --- /dev/null +++ b/wled00/src/WLEDpixelBus/RmtHIDriver.h @@ -0,0 +1,44 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +-------------------------------------------------------------------------*/ +#pragma once + +#ifdef ARDUINO_ARCH_ESP32 + +#include "esp_idf_version.h" + +// Use the fast refill driver on Xtensa targets (ESP32/S2/S3, assembly-bridged high-level ISR) +// and the normal-priority ISR path on RISC-V targets (C3/C5/C6/H2). +// C5 and H2 share the C6 RMT register layout; P4 uses a newer RMT IP and is not supported. +#if (defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || (defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2))) +#define WLEDPB_USE_RMTHI +#endif + +#ifdef WLEDPB_USE_RMTHI + +#include +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) +#include "driver/rmt.h" // legacy driver, only used on IDF V4 +#endif +#include "freertos/FreeRTOS.h" +#include +#include + +namespace RmtHiDriver { + // Install the driver for a specific channel, specifying timing properties + esp_err_t Install(uint8_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(uint8_t channel); + + // Write a buffer of data to a specific channel. + // Buffer reference is held until write completes. + esp_err_t Write(uint8_t channel, const uint8_t *src, size_t srcSize); + + // Wait until transaction is complete. + esp_err_t WaitForTxDone(uint8_t channel, TickType_t wait_time); +}; + +#endif // WLEDPB_USE_RMTHI +#endif // ARDUINO_ARCH_ESP32 diff --git a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp new file mode 100644 index 0000000000..dac773aea1 --- /dev/null +++ b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp @@ -0,0 +1,603 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +IDF V5 support: rmt_ll compatibility layer, no legacy driver dependency +-------------------------------------------------------------------------*/ + +#include +#include "RmtHIDriver.h" + +#if defined(WLEDPB_USE_RMTHI) + +#include +#include "soc/soc.h" +#include "soc/rmt_reg.h" +#include "esp_idf_version.h" + +#ifdef __riscv +#include "riscv/interrupt.h" +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +// ---- IDF V5 compatibility layer ---- +// The legacy RMT driver (driver/rmt.h) is deprecated on IDF5; the few legacy types, constants and rmt_ll helpers this driver relies on are re-declared here +#include "soc/rmt_struct.h" +#include "hal/rmt_ll.h" +#include "hal/rmt_types.h" +#include "esp_intr_alloc.h" + +#define RMT_CHANNEL_MAX SOC_RMT_CHANNELS_PER_GROUP +#define RMT_MEM_ITEM_NUM SOC_RMT_MEM_WORDS_PER_CHANNEL + +typedef rmt_symbol_word_t rmt_item32_t; // same 32-bit layout as the legacy rmt_item32_t + +// RMTMEM: the channel memory view struct moved to the private RMT driver headers in IDF5, but the symbol is still provided by the linker (.peripherals.ld). +extern "C" volatile rmt_item32_t RMTMEM[RMT_CHANNEL_MAX * RMT_MEM_ITEM_NUM]; + +#define RMTMEM_WORD(chan, idx) RMTMEM[(chan) * RMT_MEM_ITEM_NUM + (idx)] + +// the per-event interrupt helpers were removed in IDF5, map them to the generic mask-based functions +static inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) { rmt_ll_enable_interrupt(dev, RMT_LL_EVENT_TX_DONE(channel), enable); } +static inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) { rmt_ll_enable_interrupt(dev, RMT_LL_EVENT_TX_ERROR(channel), enable); } +static inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) { rmt_ll_enable_interrupt(dev, RMT_LL_EVENT_TX_THRES(channel), enable); } +static inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel) { rmt_ll_clear_interrupt_status(dev, RMT_LL_EVENT_TX_DONE(channel)); } +static inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel) { rmt_ll_clear_interrupt_status(dev, RMT_LL_EVENT_TX_ERROR(channel)); } +static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel) { rmt_ll_clear_interrupt_status(dev, RMT_LL_EVENT_TX_THRES(channel)); } + +// renamed in IDF5 +static inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable) { rmt_ll_tx_enable_wrap(dev, channel, enable); } + +// rmt_ll_tx_stop() is missing in the esp32 (classic) IDF5 hal +#if defined(CONFIG_IDF_TARGET_ESP32) +static inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel) +{ + RMTMEM_WORD(channel, 0).val = 0; // append end marker, stops the transmitter + 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; +} +#endif + +// channel status word: holds the RMT_STATE_CHx transmitter state field (see soc/rmt_reg.h) +__attribute__((always_inline)) +static inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel) { +#if defined(CONFIG_IDF_TARGET_ESP32) + return dev->status_ch[channel]; +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + return dev->tx_status[channel].state; +#elif defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) + // C5, C6 and H2 share the same RMT register layout; only the state field may be returned here + // (the RISC-V _RmtStatusIsTransmitting() tests status != 0, and chnstatus.val also contains + // the memory address counters, which stay non-zero when idle) + return dev->chnstatus[channel].state_chn; +#else // S2/S3: the state field lives in the chnstatus register + return dev->chnstatus[channel].val; +#endif +} + +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) +#include "hal/rmt_ll.h" +#define RMTMEM_WORD(chan, idx) RMTMEM.chan[chan].data32[idx] +#else +/* Shims for older ESP-IDF v3; we can safely assume original ESP32 */ +#include "soc/rmt_struct.h" + +#define RMTMEM_WORD(chan, idx) RMTMEM.chan[chan].data32[idx] + +// 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; // TODO: this is probably wrong for C5 and C6, needs investigation +} + +__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) && !defined(__riscv) +// Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#include "esp_cpu.h" // esp_cpu_get_core_id() +#include "rom/ets_sys.h" // intr_matrix_set() +#else +#include "hal/interrupt_controller_hal.h" +#endif +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) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) +#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 // on RISC-V, higher-level interrupts are not RTOS-safe TODO: may need revisiting +#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_WORD(channel, *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; + // unrolled loop for speed: do not depend on compiler to do it for us + dest[0].val = (data & 0x80) ? bit1 : bit0; + dest[1].val = (data & 0x40) ? bit1 : bit0; + dest[2].val = (data & 0x20) ? bit1 : bit0; + dest[3].val = (data & 0x10) ? bit1 : bit0; + dest[4].val = (data & 0x08) ? bit1 : bit0; + dest[5].val = (data & 0x04) ? bit1 : bit0; + dest[6].val = (data & 0x02) ? bit1 : bit0; + dest[7].val = (data & 0x01) ? bit1 : bit0; + dest += 8; // Advance destination pointer by 8 items at once + rmtToWrite -= 8; + psrc++; + if (psrc == end) { + break; + } + } + + *src_ptr = psrc; + } + + if (rmtToWrite > 0) { + // Add end event (zero durations = EOF marker); the output keeps the level of bit0's second phase (low) + // note: the bitfield types differ between IDF4 (uint32_t) and IDF5 (uint16_t) + rmt_item32_t bit0Word; + bit0Word.val = bit0; + rmt_item32_t endMark = {}; + endMark.duration0 = 0; + endMark.level0 = bit0Word.level1; + endMark.duration1 = 0; + endMark.level1 = bit0Word.level1; + *dest = endMark; + } +} + +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 + // note: the bitfield types differ between IDF4 (uint32_t) and IDF5 (uint16_t) + + rmt_item32_t bit0Word; + bit0Word.val = state.rmtBit0; + rmt_item32_t fill = {}; + fill.duration0 = 100; + fill.level0 = bit0Word.level1; + fill.duration1 = 100; + fill.level1 = bit0Word.level1; + rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM_WORD(channel, 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); + #if defined(CONFIG_IDF_TARGET_ESP32C5) + RMT.chnconf0[channel].tx_stop_chn = 0; // C5: tx_stop_chn has been observed to remain asserted after rmt_ll_tx_stop(), preventing subsequent transmissions. + #endif + 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(uint8_t channel, uint32_t status) { +#if defined(__riscv) + return status != 0; +#else + 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; +#endif +} + + +esp_err_t RmtHiDriver::Install(uint8_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. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + intr_matrix_set(esp_cpu_get_core_id(), ETS_RMT_INTR_SOURCE, 25); + ESP_INTR_ENABLE(25); +#else + intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25); + intr_cntrl_ll_enable_interrupts(1<<25); +#endif +#elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) + // Use the platform code to allocate the interrupt +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && defined(__XTENSA__) + // IDF5 requires handler and arg to be NULL for high level (assembly vectored) interrupts; + // reference the bridge's guard symbol so the linker keeps the .S file + static volatile const void* __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt; + err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, nullptr, nullptr, &isrHandle); +#else + // 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); +#endif + //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); + #if defined(CONFIG_IDF_TARGET_ESP32C5) + RMT.chnconf0[channel].tx_stop_chn = 0; // The peripheral/channel reset has already made TX idle. Avoid leaving tx_stop_chn asserted on C5. + #else + //rmt_ll_tx_stop(&RMT, channel); + #endif + 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); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + rmt_ll_tx_enable_carrier_modulation(&RMT, channel, false); // disable carrier modulation, its enabled by default in V5 + rmt_ll_tx_fix_idle_level(&RMT, channel, 0, true); // just in case, set idle low (TODO: need to invert if inverted output) +#endif + + driverState[channel] = state; + + rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true); + + return err; +} + +esp_err_t RmtHiDriver::Uninstall(uint8_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(uint8_t channel, const uint8_t *src, size_t srcSize) { + 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 + srcSize; + RmtStartWrite(channel, state); + } + return result; +} + +esp_err_t RmtHiDriver::WaitForTxDone(uint8_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..5a9856b1cc --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -0,0 +1,323 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna): +NeoPixelBus served me well as a reference to proper hardware initialisation +as well as figuring out the more exotic digital LED types + +Features: +- Runtime LED timing configuration +- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-C5 and ESP8266 (maybe other ESP32 flavours work too) +- RMT, I2S parallel, LCD parallel, SPI parallel, BitBang parallel, PARLIO parallel +- 2-Pin LED support (hardware SPI and BitBang) +- RGBW uint32_t pixel buffer format (WLED native) +- Support for up to 6 color channels, 8bit or 16bit +- Supports output inversion (ESP32 only) and individual color channel inversion +- Supports hardware LED brightness for improved color resolution if available + +Tested working on IDF V4 and V5 API functions and low-level HAL + +-------------------------------------------------------------------------*/ + +/* +TODO List +- need to verify custom bus start indices and color-order overrides on hardware (both should now work) +- ESP32 C61 has no RMT and no PARLIO, uses bit bang. check if parallel SPI can be used +- I2S/LCD requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load +- the DMA buffer count and size need to be checked against memory usage formulas, they may be incorrect (we could do "worst case" in the UI as it wont matter much for a small amount of LEDs) +- stress-test all parallel drivers for missed interrupts, descriptor races, and watchdog recovery +- color orders of some strips may differ from NPB implementation, needs checking and reordering to match legacy behaviour or users will complain (see legacy bus_wrapper where this is defined in neoXXXtype) +- move gamma down to bus level, see pending PR +- brightness scaling for special buses with LED current (or 16bit) may need fine-tuning +- SPI 2-wire bus types need testing on all platforms (tested working in the past but not recently) +- SPI 2-wire types do not support signal inversion (_inverted flag) +- SPI-2-wire Optimisation: C3 (maybe also C5, C6?) can output much faster if using CPU pin groups i.e. direct CPU to GPIO access, would speed up SPI BB outputs +- ESP32_DATA_IDLE_HIGH flag is a hack to fix bad hardware design and uses the legacy RMT driver, we should drop support for that +- parallel SPI driver: the reset pulse is now derived from the LED timing (calc4StepResetDmaBytes); needs testing on hardware +- PARLIO bus needs further target-hardware testing +- use chip capabilities instead of individual target ifdefs in buswrapper +- SM16825_SUFFIX should be made dynamic to increase color range at lower brightness (scale with global brightness) +- The config rework is unfinished: with the updated workflow the goal is to fully drop "custom" bus handling and tread every bus the same (UI needs rework to do that) -> partially done +- RMT driver has no canShow() timeout yet, could adopt the "ensureIdle()" approach from I2S/PARLIO +- the bus capability definitions in const.h should probably be moved to driver level or use driver level constants instead of "overriding" low level hardware capabilities +- LPD6803 and LPD8806 encoding is done on RGB only, need to add support for more color channels if such stripes exist +- ESP8266 DMA output uses a slow path to scale and reset buffers, needs optimisation +- the "non global buffer" path is not optimised, check if this path should not color-encode on the fly so reading buffer back is faster, then do an encode pass before sending +- I2S output on the S2 has very strange glitching behaviour exactly at every 32nd LED (=buffer boundry), depends on number of LEDs, anything below 256 is fine. +*/ + + +#include "WLEDpixelBus.h" +#include "../../const.h" // needed for color orders + +#if defined(ARDUINO_ARCH_ESP32) + #include "WLEDpixelBus_RMT.h" + #include "WLEDpixelBus_BitBang.h" + #include "WLEDpixelBus_I2S.h" + #include "WLEDpixelBus_ParallelSpi.h" + #include "WLEDpixelBus_PARLIO.h" +#elif defined(ESP8266) + #include "WLEDpixelBus_ESP8266.h" + #include "WLEDpixelBus_BitBang.h" +#endif + +namespace WLEDpixelBus { + +// LED Timing Lookup +LedTiming getLedTiming(uint8_t wledType) { + const uint8_t idx = getTimingIndex(wledType); + #ifdef ESP8266 + return LedTiming( + (uint16_t)pgm_read_word(&_ledTimings[idx].t0h_ns), + (uint16_t)pgm_read_word(&_ledTimings[idx].t0l_ns), + (uint16_t)pgm_read_word(&_ledTimings[idx].t1h_ns), + (uint16_t)pgm_read_word(&_ledTimings[idx].t1l_ns), + (uint32_t)pgm_read_dword(&_ledTimings[idx].reset_us) + ); + #else + return _ledTimings[idx]; + #endif +} + +// Color Encoder +//-------------- + +// custom encoder from channel map, if a standard permutation is detected and no inversion it uses fast path +ColorEncoder::ColorEncoder(const uint8_t channelMap[MAX_CUSTOM_CHANNELS], uint8_t numChannels, uint8_t invertMask, bool is16bit) +{ + if (numChannels > MAX_CUSTOM_CHANNELS) numChannels = MAX_CUSTOM_CHANNELS; + memcpy(_channelMap, channelMap, MAX_CUSTOM_CHANNELS); + _invertMask = invertMask; + + // Try to detect a standard permutation usable on the fast inline encode/decode path + // Requirements: no inversion, each color source appears at most once, channel count {3,4,5} + bool isPermutation = (invertMask == 0) && (numChannels >= 3) && (numChannels <= 5); + uint8_t rIdx = 0, gIdx = 1, bIdx = 2, wIdx = 3, cwIdx = 4; + bool hasR = false, hasG = false, hasB = false, hasW = false, hasCW = false; + + if (isPermutation) { + for (uint8_t i = 0; i < numChannels; i++) { + switch (channelMap[i]) { + case CH_R: if (hasR) isPermutation = false; else { hasR = true; rIdx = i; } break; + case CH_G: if (hasG) isPermutation = false; else { hasG = true; gIdx = i; } break; + case CH_B: if (hasB) isPermutation = false; else { hasB = true; bIdx = i; } break; + case CH_W: // fallthrough + case CH_WW: if (hasW) isPermutation = false; else { hasW = true; wIdx = i; } break; + case CH_CW: if (hasCW) isPermutation = false; else { hasCW = true; cwIdx = i; } break; + default: isPermutation = false; break; // CH_UNUSED or unknown: not a fast-path permutation + } + if (!isPermutation) break; + } + // Validate that the present sources match the expected fast-path dispatch case exactly + if (numChannels == 3) { + // encodeRGB: exactly R+G+B, no W/CW + if (!hasR || !hasG || !hasB || hasW || hasCW) isPermutation = false; + } else if (numChannels == 4) { + // encodeRGBW: R+G+B+W where W must be CH_W (not CH_WW — encodeRGBW uses W(c)) + if (!hasR || !hasG || !hasB || !hasW || hasCW) isPermutation = false; + else if (channelMap[wIdx] != CH_W) isPermutation = false; + } else if (numChannels == 5) { + // encodeCCT: R+G+B+WW+CW; _idxW stores the WW position, _idxCW the CW position + if (!hasR || !hasG || !hasB || !hasW || !hasCW) isPermutation = false; + else if (channelMap[wIdx] != CH_WW) isPermutation = false; + } else { + isPermutation = false; // unsupported count for fast path + } + } + + uint8_t flags = 0; + if (is16bit) { + flags |= NCHF_16BIT; + numChannels *= 2; // 2 wire bytes per logical channel in 16-bit mode + } + if (isPermutation) { + // Standard permutation: populate fast-path indices, skip NCHF_CUSTOM + _idxR = rIdx; _idxG = gIdx; _idxB = bIdx; _idxW = wIdx; _idxCW = cwIdx; + _pixelFormat = numChannels | flags; + } else { + // True custom map: use generic encodeGeneric() loop + _idxR = _idxG = _idxB = _idxW = _idxCW = 0; + flags |= NCHF_CUSTOM; + _pixelFormat = numChannels | flags; + } +} + +// Generic encoder for non-fast-path cases: NCHF_CUSTOM maps, and inverted channels (hasInvertedChannels()) in any bit mode +void ColorEncoder::encodeGeneric(uint32_t c, const CctPixel& cct, uint8_t* out, uint16_t bri) const { + const uint8_t flags = _pixelFormat & 0xF0; + const uint8_t wireBytes = _pixelFormat & 0x0F; // total bytes per pixel (numChannels * 2 for 16-bit, numChannels otherwise) + const bool is16bit = (flags & NCHF_16BIT) != 0; // cache for speed + + if (flags & NCHF_CUSTOM) { + // Custom channel map: each wire byte is assigned a color source from _channelMap (up to 6 channels supported) + const uint8_t numLogi = is16bit ? wireBytes / 2 : wireBytes; + for (uint8_t i = 0; i < numLogi; i++) { + uint8_t val; + switch (_channelMap[i]) { + case CH_R: val = R(c); break; + case CH_G: val = G(c); break; + case CH_B: val = B(c); break; + case CH_W: val = W(c); break; + case CH_WW: val = cct.ww; break; + case CH_CW: val = cct.cw; break; + default: val = 0; break; // CH_UNUSED + } + if (is16bit) { + uint16_t v16 = ((uint32_t)val * bri) >> 8; // Note: this can "only" reach 99% as "255×255" = 65025. We do not care. + if (_invertMask & (1u << i)) v16 ^= 0xFFFF; // invert all bits + out[i*2] = v16 >> 8; + out[i*2+1] = v16 & 0xFF; + } else { + if (_invertMask & (1u << i)) val ^= 0xFF; + out[i] = val; // note: for 8bit colors, scaling happens in BusDigital, only 16bit colors scale during encoding + } + } + return; + } + + // wire bytes: 6/8/10 for 16-bit, 3/4/5 for 8-bit (standard encoding supports up to 5 channels) + if (is16bit) { + switch (wireBytes) { + case WB_RGB16: encodeRGB16(c, out, bri); break; + case WB_RGBW16: encodeRGBW16(c, out, bri); break; + default: encodeCCT16(c, cct, out, bri); break; // WB_CCT16 + } + } else { + switch (wireBytes) { + case WB_RGB: encodeRGB(c, out); break; + case WB_RGBW: encodeRGBW(c, out); break; + default: encodeCCT(c, cct, out); break; // WB_CCT + } + } + + if (!hasInvertedChannels()) return; + // Handle color inversion (per color channel) + if (is16bit) { + for (uint8_t i = 0; i < wireBytes / 2; i++) { + if (_invertMask & (1u << i)) { + out[i*2] ^= 0xFF; + out[i*2+1] ^= 0xFF; + } + } + } else { + for (uint8_t i = 0; i < wireBytes; 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 wireBytes = _pixelFormat & 0x0F; // total bytes per pixel (numChannels * 2 for 16-bit, numChannels otherwise) + const bool is16bit = (flags & NCHF_16BIT) != 0; // cache for speed + + if (flags & NCHF_CUSTOM) { + // Custom channel map decode: reconstruct RGBW from wire bytes + const uint8_t numLogi = is16bit ? wireBytes / 2 : wireBytes; + uint8_t r = 0, g = 0, b_ = 0, w = 0; + for (uint8_t i = 0; i < numLogi; i++) { + uint8_t val = is16bit ? in[i*2] : in[i]; // take high byte for 16-bit + if (_invertMask & (1u << i)) val ^= 0xFF; + switch (_channelMap[i]) { + case CH_R: r = val; break; + case CH_G: g = val; break; + case CH_B: b_ = val; break; + case CH_W: case CH_WW: case CH_CW: // W, WW, CW → map to W (lossy) + if (val > w) w = val; break; + } + } + return RGBW32(r, g, b_, w); + } + + uint8_t r, g, b, w = 0; + if (is16bit) { + r = readU16Hi(in, _idxR); g = readU16Hi(in, _idxG); b = readU16Hi(in, _idxB); + if (wireBytes >= WB_RGBW16) w = readU16Hi(in, _idxW); // WB_RGBW16=RGBW16, WB_CCT16=CCT16 + } else { + r = in[_idxR]; g = in[_idxG]; b = in[_idxB]; + if (wireBytes >= WB_RGBW) w = in[_idxW]; // WB_RGBW=RGBW, WB_CCT=CCT + } + if (hasInvertedChannels()) { + if (_invertMask & (1u << _idxR)) r ^= 0xFF; + if (_invertMask & (1u << _idxG)) g ^= 0xFF; + if (_invertMask & (1u << _idxB)) b ^= 0xFF; + const bool hasW = (is16bit) ? (wireBytes >= WB_RGBW16) : (wireBytes >= WB_RGBW); + if (hasW && (_invertMask & (1u << _idxW))) w ^= 0xFF; + } + return RGBW32(r, g, b, w); +} + +// create a 1-wire digital bus (RMT, I2S, ...) +PixelBus* createBus(BusDriver driver, int8_t pin, const BusProperties& properties, uint8_t ledType, size_t numPixels) { + if (numPixels > UINT16_MAX) return nullptr; + PixelBus* bus = nullptr; + const LedTiming& timing = properties.timing; + switch (driver) { +#if defined(ARDUINO_ARCH_ESP32) + case BusDriver::RMT: + bus = new RmtBus(pin, timing, 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_PARALLELSPI_SUPPORT instead +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + bus = new I2sBus(pin, timing, 0, ledType); +#else + bus = new I2sBus(pin, timing, 1, ledType); +#endif + break; +#endif + +#ifdef WLEDPB_PARALLELSPI_SUPPORT + case BusDriver::PARSPI: + bus = new ParallelSpiBus(pin, timing, ledType); + break; +#endif +#ifdef WLEDPB_PARLIO_SUPPORT + case BusDriver::PARLIO: + // ParlioBus registers itself with the shared ParlioBusContext singleton. + // numPixels is used for DMA buffer sizing. + bus = new ParlioBus(pin, timing, 0, ledType); + break; +#endif +#if (WLED_MAX_BB_CHANNELS > 0) + case BusDriver::BitBang: + bus = new BitBangBus(pin, timing, ledType); + break; +#endif +#elif defined(ESP8266) + case BusDriver::UART: + bus = new Esp8266UartBus(pin, timing, ledType); + break; + case BusDriver::DMA: + bus = new Esp8266DmaBus(pin, timing, ledType); + break; + case BusDriver::BitBang: + bus = new BitBangBus(pin, timing, ledType); + break; +#endif + + default: + return nullptr; + } + +// we have a bus, set the parameters + if (bus) { + bus->setNumPixels((uint16_t)numPixels); + bus->setPrefixLen(properties.prefixLen); + bus->setPrefixData(properties.prefixData, properties.prefixLen); + bus->setSuffixLen(properties.suffixLen); + bus->setSuffixData(properties.suffixData, properties.suffixLen); + bus->setInverted(properties.invertOutput); + bus->setEncoder(ColorEncoder(properties.channelMap, properties.numChannels, properties.invertMask, properties.is16bit)); + } + + 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..c21589dfe0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -0,0 +1,509 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#if defined(ARDUINO_ARCH_ESP32) + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "esp_heap_caps.h" +#include "esp_attr.h" +#include "driver/gpio.h" +#include "esp_idf_version.h" + +// I2S support: targets where the LCD Intel 8080 bus is present (ESP32, ESP32-S2, ESP32-S3) +#if SOC_LCD_I80_BUSES + #define WLEDPB_I2S_SUPPORT +#endif + +// SPI parallel support (C3 - uses SPI quad mode with GDMA) +#if defined(CONFIG_IDF_TARGET_ESP32C3) + #define WLEDPB_PARALLELSPI_SUPPORT +#endif + +// ESP_HAS_HIGH_GPIO_BANK — defined when the target has GPIO > 31 and needs the second GPIO output register bank (GPIO_OUT1_W1TS/TC_REG). +#if SOC_GPIO_PIN_COUNT > 32 +# define ESP_HAS_HIGH_GPIO_BANK 1 +#endif + +#ifdef WLEDPB_PARALLELSPI_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 "../../colors.h" + + + +namespace WLEDpixelBus { +static constexpr uint32_t WLEDPB_TRANSFER_TIMEOUT_MS = 1000; // max allowed time for a full transfer, recovery is triggered if exceeded + +// Brightness to LED hardware current mapping (TM1814, TM1815 and APA102) +// maps Q8.8 brightness to a hardware current step and an 8-bit residual color scale +// pick the smallest current step whose brightness is >= the target, then scale pixel colors to match total scaling +inline void mapBrightnessToCurrentStep(uint16_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; + const uint32_t minBriFP = (uint32_t)minBri << 8; + + if (brightness <= minBriFP) { + // Below minimum current floor: use step 0, scale colors down proportionally. + stepOut = 0; + scaleOut = ((uint32_t)brightness * 255) / minBriFP; + return; + } + + const uint32_t range = (255U << 8) - minBriFP; + const uint32_t stepFP = range / maxStep; + // Compute ceiling step: smallest step whose brightness >= target. + const uint32_t diffFP = brightness - minBriFP; + uint32_t step = (diffFP + stepFP - 1) / stepFP; + if (step > maxStep) step = maxStep; + + // Actual brightness at this step (integer floor, guaranteed >= brightness). + const uint32_t curBri = minBriFP + (step * range) / maxStep; + stepOut = (uint8_t)step; + scaleOut = (uint8_t)(((uint32_t)brightness * 255) / curBri); +} + + +// DMA Buffer Configuration +//========================== +constexpr size_t MIN_DMA_BUFFER_SIZE = 256; +constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; // 12bit in DMA descriptor +#ifdef WLEDPB_PARALLELSPI_SUPPORT // C3 only + constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*2); // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (2k per buffer means about 1ms interrupt intervals with 4 step cadence) +#else + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT + constexpr size_t DEFAULT_DMA_BUFFER_SIZE = MAX_DMA_BUFFER_SIZE; // note: 3k is enough except if using 8RMT (ESP32 classic), also requires triple buffering + #else + constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*3); + #endif +#endif +static_assert(DEFAULT_DMA_BUFFER_SIZE <= 4092, "DEFAULT_DMA_BUFFER_SIZE exceeds the maximum possible size"); + +// packed into ColorEncoder::_pixelFormat: upper nibble = flags +static constexpr uint8_t NCHF_16BIT = 0x10; // 16-bit per color (UCS8903/8904/SM16825) +static constexpr uint8_t NCHF_CUSTOM = 0x40; // custom channel map: _channelMap[] drives encoding +// 0x20 and 0x80 reserved + +// Maximum number of color channels in custom channel map (ColorEncoder::_channelMap[]). +static constexpr uint8_t MAX_CUSTOM_CHANNELS = 6; // note: changing this wont propagate, currently ICs with more than 6 channels are not supported + +static constexpr uint8_t MAX_PREFIX_BYTES = 8; +static constexpr uint8_t MAX_SUFFIX_BYTES = 8; +// Chip-specific prefix/suffix bytes +static constexpr uint8_t TM1814_PREFIX_LEN = 8; // C1+C2 current-config prefix +static constexpr uint8_t TM1914_PREFIX_LEN = 6; // mode-setting prefix +static constexpr uint8_t SM16825_SUFFIX_LEN = 4; // per-frame configuration suffix +// TM1914 mode-setting prefix: 6 bytes (3 data + 3 inverted) +//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 }; // auto switch DIN / FDIN +// 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] = { 0x08, 0x42, 0x10, 0x9F }; // 20.3mA for all channels as a safe default - TODO: make this configurable, could use the led current from settings +static constexpr uint8_t SM16825_SUFFIX[4] = { 0xFF, 0xFF, 0xFF, 0x9F }; // set max current (not really safe but always was the default) + +// White-channel swap selector, decoded from the upper nibble of WLED's color-order byte. +enum WSwap : uint8_t { + WSWAP_NONE = 0, + WSWAP_B = 1, // swap W & B + WSWAP_G = 2, // swap W & G + WSWAP_R = 3, // swap W & R + WSWAP_WWCW = 4, // swap WW & CW +}; + +// Custom channel map color source to select which color source feeds a given wire byte. +enum ChannelSource : uint8_t { + CH_UNUSED = 0, + CH_R = 1, + CH_G = 2, + CH_B = 3, + CH_W = 4, + CH_WW = 5, + CH_CW = 6, +}; + +// Wire-byte counts per pixel for each pixel format +enum WireBytes : uint8_t { + WB_RGB = 3, + WB_RGBW = 4, + WB_CCT = 5, + WB_RGB16 = 6, + WB_RGBW16 = 8, + WB_CCT16 = 10, +}; + +// Hardware properties of a one-wire digital bus. Note: clocked 2-pin LEDs have no custom channel support (no known hardware exists that uses that) +struct BusProperties { + uint8_t numChannels = 3; // logical channels; is16bit doubles wire bytes + uint8_t channelMap[MAX_CUSTOM_CHANNELS] = {2, 1, 3, 0, 0, 0}; + uint8_t invertMask = 0; + bool is16bit = false; + bool invertOutput = false; + LedTiming timing = LedTiming(300, 900, 700, 500, 300); + uint8_t prefixLen = 0; + uint8_t prefixData[MAX_PREFIX_BYTES] = {}; + uint8_t suffixLen = 0; + uint8_t suffixData[MAX_SUFFIX_BYTES] = {}; +}; + +enum class DriverState : uint8_t { + Idle = 0, + Sending = 1 +}; + +using ::CctPixel; + +// Pixel color encoder: maps RGBW uint32_t to a per-LED byte stream according to color order (fast) or custom color map (slow, generic) +class ColorEncoder { +public: + ColorEncoder() : _pixelFormat(0x03), _invertMask(0), _idxR(0), _idxG(1), _idxB(2), _idxW(3), _idxCW(4) { memset(_channelMap, 0, sizeof(_channelMap)); } + ColorEncoder(const uint8_t channelMap[MAX_CUSTOM_CHANNELS], uint8_t numChannels, uint8_t invertMask, bool is16bit); + + // Fast encode — standard 8-bit + inline void encodeRGB(uint32_t c, uint8_t* out) const { + out[_idxR] = R(c); out[_idxG] = G(c); out[_idxB] = B(c); + } + inline void encodeRGBW(uint32_t c, uint8_t* out) const { + out[_idxR] = R(c); out[_idxG] = G(c); out[_idxB] = B(c); out[_idxW] = W(c); + } + inline void encodeCCT(uint32_t c, const CctPixel& cct, uint8_t* out) const { + out[_idxR] = R(c); out[_idxG] = G(c); out[_idxB] = B(c); + out[_idxW] = cct.ww; out[_idxCW] = cct.cw; + } + // Fast decode — standard 8-bit + inline uint32_t decodeRGB(const uint8_t* in) const { + return RGBW32(in[_idxR], in[_idxG], in[_idxB], 0); + } + inline uint32_t decodeRGBW(const uint8_t* in) const { + return RGBW32(in[_idxR], in[_idxG], in[_idxB], in[_idxW]); + } + inline uint32_t decodeCCT(const uint8_t* in) const { + return RGBW32(in[_idxR], in[_idxG], in[_idxB], in[_idxW]); // WW→W, CW dropped (lossy) + } + + // Fast encode — 16-bit. Note: only 16bit colors scale during encoding, for 8bit colors, scaling happens in BusDigital + inline void encodeRGB16(uint32_t c, uint8_t* out, uint16_t bri) const { + writeU16(out, _idxR, R(c), bri); + writeU16(out, _idxG, G(c), bri); + writeU16(out, _idxB, B(c), bri); + } + inline void encodeRGBW16(uint32_t c, uint8_t* out, uint16_t bri) const { + writeU16(out, _idxR, R(c), bri); + writeU16(out, _idxG, G(c), bri); + writeU16(out, _idxB, B(c), bri); + writeU16(out, _idxW, W(c), bri); + } + inline void encodeCCT16(uint32_t c, const CctPixel& cct, uint8_t* out, uint16_t bri) const { + writeU16(out, _idxR, R(c), bri); + writeU16(out, _idxG, G(c), bri); + writeU16(out, _idxB, B(c), bri); + writeU16(out, _idxW, cct.ww, bri); + writeU16(out, _idxCW, cct.cw, bri); + } + + // Fast decode — 16-bit + inline uint32_t decodeRGB16(const uint8_t* in) const { + return RGBW32(readU16Hi(in, _idxR), readU16Hi(in, _idxG), readU16Hi(in, _idxB), 0); + } + inline uint32_t decodeRGBW16(const uint8_t* in) const { + return RGBW32(readU16Hi(in, _idxR), readU16Hi(in, _idxG), readU16Hi(in, _idxB), readU16Hi(in, _idxW)); + } + inline uint32_t decodeCCT16(const uint8_t* in) const { + return RGBW32(readU16Hi(in, _idxR), readU16Hi(in, _idxG), readU16Hi(in, _idxB), readU16Hi(in, _idxW)); + } + + // Generic slow encode/decode — handles custom and inverted color channels + void encodeGeneric(uint32_t c, const CctPixel& cct, uint8_t* out, uint16_t bri) const; + uint32_t decodeGeneric(const uint8_t* in) const; + + // pixel color scaling functions, respect color inverted channels, split 8/16 bit for faster for-loops + inline void scalePixel8Bit(uint8_t* pixel, uint8_t scale) const { + const uint8_t logicalChannels = _pixelFormat & 0x0F; + for (uint8_t i = 0; i < logicalChannels; ++i) { + const uint8_t inverted = (uint8_t)(0u - ((_invertMask >> i) & 1u)); + uint8_t value = pixel[i] ^ inverted; + value = (uint8_t)((((uint16_t)value) * scale) >> 8); + pixel[i] = value ^ inverted; + } + } + + inline void scalePixel16Bit(uint8_t* pixel, uint8_t scale) const { + const uint8_t logicalChannels = (_pixelFormat & 0x0F) / 2; + for (uint8_t i = 0; i < logicalChannels; ++i) { + uint8_t* channel = pixel + (size_t)i * 2; + const uint16_t inverted = (uint16_t)(0u - ((_invertMask >> i) & 1u)); + uint16_t value = (((uint16_t)channel[0] << 8) | channel[1]) ^ inverted; + value = (uint16_t)((((uint32_t)value) * scale) >> 8); + value ^= inverted; + channel[0] = value >> 8; + channel[1] = value & 0xFF; + } + } + + void scalePixel(uint8_t* pixel, uint8_t scale) const { + if (scale == 255) return; + scale++; // add 1 for rounding using bitshift + if (is16Bit()) scalePixel16Bit(pixel, scale); // TODO: we do not really need the functions inlined here but we do in scaleEncodeBuffer(), may need some refinement, also check ESP8266 DMA driver + else scalePixel8Bit(pixel, scale); + } + + // Accessors + uint8_t getPixelFormat() const { return _pixelFormat; } // lower nibble=wire bytes, upper=NCHF_* flags + uint8_t getLogicalChannels() const { return (_pixelFormat & NCHF_16BIT) ? (_pixelFormat & 0x0F) / 2 : (_pixelFormat & 0x0F); } // logical color channels + uint8_t getBytesPerPixel() const { return _pixelFormat & 0x0F; } // wire bytes per pixel + bool is16Bit() const { return (_pixelFormat & NCHF_16BIT) != 0; } // true for UCS8903/8904/SM16825 + bool hasInvertedChannels() const { return _invertMask != 0; } // bus with at least one inverted color channel + bool isCustom() const { return (_pixelFormat & NCHF_CUSTOM) != 0; } // true for non-permutation custom channel maps + +private: + uint8_t _pixelFormat; // lower nibble = bytes per pixel, upper nibble = NCHF_ flags (16-bit, custom) + uint8_t _invertMask; // bitmask: bit i = invert color channel i (applies when hasInvertedChannels()) + uint8_t _idxR, _idxG, _idxB; // wire byte index for R, G, B + uint8_t _idxW, _idxCW; // wire byte index for W (or WW) and CW (CCT types) + uint8_t _channelMap[MAX_CUSTOM_CHANNELS]; // custom channel map: _channelMap[i] = ChannelSource for wire byte i + + // 16bit pixel helper functions + static inline void writeU16(uint8_t* out, uint8_t idx, uint8_t val8, uint16_t bri) { + const uint16_t v = ((uint32_t)val8 * bri) >> 8; // Note: this can "only" reach 99% as "255×255" = 65025. We do not care. + 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]; } +}; + + +// PixelBus base class +//============================================================================== + +class PixelBus { +protected: + uint8_t* _encodeBuffer = nullptr; // color data sent to the LEDs (includes prefix and suffix) + uint8_t* _pixelData = nullptr; // _encodeBuffer + _prefixLen, cached to avoid per-call addition + size_t _encodeBufferSize = 0; // allocated size in bytes + uint16_t _numPixels = 0; // number of encoded pixels (bytes per pixel depends on _encoder) + uint8_t _prefixLen = 0; // byte length of chip prefix at start of _encodeBuffer i.e. sent out first + uint8_t _suffixLen = 0; // byte length of chip suffix appended after pixel data i.e. sent out after pixel data + uint8_t _prefixData[MAX_PREFIX_BYTES] = {}; + uint8_t _suffixData[MAX_SUFFIX_BYTES] = {}; + uint8_t _ledType = 0; // LED chip type (e.g. 31=TM1814); 0 = generic + bool _inverted = false; // physical output signal inversion (per bus), not to be confused by _invertMask which is per-color channel inversion + uint8_t _busBri = 255; // brightness for color_fade() in setPixelColor() (software scaling, can be on top of hardware scaling) + uint16_t _encBri = 255U << 8; // Q8.8 encoder brightness for 16-bit types uses _encBri * colorchannel + ColorEncoder _encoder; // color encoder, set by derived class constructors + +public: + virtual ~PixelBus() { releaseEncodeBuffer(); } + + // color data prefix handling + void setPrefixLen(uint8_t len) { _prefixLen = len > MAX_PREFIX_BYTES ? MAX_PREFIX_BYTES : len; } + void setPrefixData(const uint8_t* data, uint8_t len) { + if (!data) return; + if (len > MAX_PREFIX_BYTES) len = MAX_PREFIX_BYTES; + memcpy(_prefixData, data, len); + } + virtual void updatePrefix(const uint8_t* data, uint8_t len) { + if (!_encodeBuffer || len == 0) return; + if (len > _prefixLen) len = _prefixLen; + memcpy(_encodeBuffer, data, len); + } + bool hasPrefix() const { return _prefixLen > 0; } + uint8_t getPrefixLen() const { return _prefixLen; } + // color data suffix handling + void setSuffixLen(uint8_t len) { _suffixLen = len > MAX_SUFFIX_BYTES ? MAX_SUFFIX_BYTES : len; } + void setSuffixData(const uint8_t* data, uint8_t len) { + if (!data) return; + if (len > MAX_SUFFIX_BYTES) len = MAX_SUFFIX_BYTES; + memcpy(_suffixData, data, len); + } + 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.getBytesPerPixel(), data, len); + } + bool hasSuffix() const { return _suffixLen > 0; } + uint8_t getSuffixLen() const { return _suffixLen; } + + void setBusBri(uint8_t b) { _busBri = b; } + void setEncBri(uint16_t b) { _encBri = b; } + inline uint8_t getBusBri() const { return _busBri; } + // Set APA102 5-bit per-pixel hardware brightness step (0–31), default implementation is a no-op; overridden by SpiBus for TYPE_APA102 + // coarse control via hardware step, fine via color_fade() - is set globally, same value is used for all pixels + // Note: APA102 would effectively have 13bit per-color resolution we could leverage for smoother dimming + virtual void setApa102HwBri(uint8_t /*v*/) {} + + virtual void setInverted(bool inv) { _inverted = inv; } + bool isInverted() const { return _inverted; } + void setEncoder(const ColorEncoder& enc) { _encoder = enc; } // must be called after construction but before begin(). + + virtual bool begin() = 0; + virtual void end() = 0; + virtual bool show() = 0; // show() sends the _encodeBuffer to hardware + virtual bool canShow() const = 0; // polled by busmanager, must never block and never return false permanently + #ifdef WLED_DEBUG_BUS + virtual const char* getTypeStr() const = 0; + #endif + + // Encode one pixel into _encodeBuffer at pos, c and ww/cw are already brightness-scaled for 8bit pixels (done by BusDigital) + // for 16-bit LED types the encoder multiplies each channel by _encBri for better resolution + // Note: _encodeBuffer != nullptr and pos < _numPixels must be checked by caller (speed optimisation) + // Note: using O2 optimization seems to make it slower + // TODO: on ESP32, do not put this in IRAM on C3 it works in IRAM, need to test if there is any speed benefit from IRAM (note: this may have all changed in IDF V5) + virtual bool setPixelColor(uint16_t pos, uint32_t c, uint16_t wwcw) { + const uint8_t pixelFormat = _encoder.getPixelFormat(); + uint8_t* out = _pixelData + (size_t)pos * _encoder.getBytesPerPixel(); + const CctPixel cct{wwcw}; + switch (pixelFormat) { + case 3: _encoder.encodeRGB(c, out); break; // 3ch RGB + case 4: _encoder.encodeRGBW(c, out); break; // 4ch RGBW + case 5: _encoder.encodeCCT(c, cct, out); break; // 5ch CCT + case (3*2) | NCHF_16BIT: _encoder.encodeRGB16(c, out, _encBri); break; // 16-bit RGB (6 bytes per pixel) + case (4*2) | NCHF_16BIT: _encoder.encodeRGBW16(c, out, _encBri); break; // 16-bit RGBW (8bytes per pixel) + case (5*2) | NCHF_16BIT: _encoder.encodeCCT16(c, cct, out, _encBri); break; // 16-bit CCT (10 bytes per pixel) + default: _encoder.encodeGeneric(c, cct, out, _encBri); break; // inverted / special cases up to 6 channels per physical pixel + } + return true; + } + + // Decode one pixel back from _encodeBuffer (lossy, used if no global buffer is available) + // Note: buses with non-linear encoding (e.g. Esp8266DmaBus 4-step) must override this. + virtual uint32_t getPixelColor(uint16_t pix) const { + const uint8_t pixelFormat = _encoder.getPixelFormat(); + const uint8_t* in = _pixelData + (size_t)pix * _encoder.getBytesPerPixel(); + 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); + } + } + + // Set every logical pixel to black, respecting color channel inversion + virtual void clearEncodeBuffer() { + if (!_pixelData || _numPixels == 0) return; + const size_t bytesPerPixel = _encoder.getBytesPerPixel(); + const size_t pixelBytes = (size_t)_numPixels * bytesPerPixel; + + // fast path if no inverted channels: just set entire color buffer to 0 + if (!_encoder.hasInvertedChannels()) { + memset(_pixelData, 0, pixelBytes); + return; + } + // slow path if there is at least one inverted color channel: fill each pixel with black + uint8_t blackPixel[MAX_CUSTOM_CHANNELS * 2] = {}; + _encoder.scalePixel(blackPixel, 0); + + // Mixed channel inversion needs a repeating per-pixel fill pattern. + for (uint16_t i = 0; i < _numPixels; ++i) { + memcpy(_pixelData + (size_t)i * bytesPerPixel, blackPixel, bytesPerPixel); + } + } + + // Proportionally scale all encoded colors, used by BusDigital::applyBriLimit() for ABL + // Note: buses with non-linear encoding (e.g. Esp8266DmaBus 4-step) must override this. + virtual void scaleEncodeBuffer(uint8_t scale) { + if (scale == 255 || !_pixelData) return; + scale++; // add 1 for rounding using bitshift + const size_t bytesPerPixel = _encoder.getBytesPerPixel(); + if (!_encoder.is16Bit()) { + // fast path: 8bit colors, no inversion: just scale each byte + if (!_encoder.hasInvertedChannels()) { + const size_t pixelBytes = (size_t)_numPixels * bytesPerPixel; + for (size_t i = 0; i < pixelBytes; i++) { + _pixelData[i] = ((uint16_t)(_pixelData[i]) * scale) >> 8; + } + return; + } + // slow path: respect inverted color channels + for (uint16_t i = 0; i < _numPixels; ++i) { + _encoder.scalePixel8Bit(_pixelData + (size_t)i * bytesPerPixel, scale); // TODO: could be optimised for speed + } + } else { + // no fast path for 16bit, could be optimised for speed if needed + for (uint16_t i = 0; i < _numPixels; ++i) { + _encoder.scalePixel16Bit(_pixelData + (size_t)i * bytesPerPixel, scale); // TODO: could be optimised for speed + } + } + } + + // Allocate encode buffer (data sent to the LEDs) + virtual bool allocateEncodeBuffer(uint16_t numPixels, uint8_t bytesPerPixel) { + const size_t pixelBytes = (size_t)numPixels * bytesPerPixel; + const size_t needed = _prefixLen + pixelBytes + _suffixLen; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + releaseEncodeBuffer(); + if (needed == 0) return true; + #if defined(ARDUINO_ARCH_ESP32) + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + #else + _encodeBuffer = (uint8_t*)malloc(needed); + #endif + if (!_encodeBuffer) return false; + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + _prefixLen; + if (_prefixLen > 0) memcpy(_encodeBuffer, _prefixData, _prefixLen); + if (_suffixLen > 0) memcpy(_pixelData + pixelBytes, _suffixData, _suffixLen); + return true; + } + + void releaseEncodeBuffer() { + if (_encodeBuffer) { + #if defined(ARDUINO_ARCH_ESP32) + heap_caps_free(_encodeBuffer); + #else + free(_encodeBuffer); + #endif + } + _encodeBuffer = nullptr; + _pixelData = nullptr; + _encodeBufferSize = 0; + } + + size_t getEncodeBufferSize() const { return _encodeBufferSize; } + virtual uint16_t getNumPixels() const { return _numPixels; } + void setNumPixels(uint16_t n) { _numPixels = n; } +}; + +enum class BusDriver : uint8_t { + RMT = 0, + I2S = 1, // I2S on ESP32 and S2, LCD on S3 + PARSPI = 2, // parallel SPI output (C3) + UART = 3, + DMA = 4, + BitBang = 5, + PARLIO = 6 // PARLIO TX parallel output (C6, H2, C5, P4 — IDF >= 5.3) +}; + +PixelBus* createBus(BusDriver driver, int8_t pin, const BusProperties& properties, uint8_t ledType = 0, size_t numPixels = 0); + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp new file mode 100644 index 0000000000..ff02caae1c --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -0,0 +1,347 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +works on all ESP32 variants and ESP8266 + +Uses software bit-bang timing and outputs data on all set-up pins in parallel. +Interrupts are re-enabled after each LED data is written, if the return time is too long, output is aborted +The frame is then sent again to avoid effect tearing, this is repeated a few times before giving up +If reset period is set to zero, interrupts are disabled and the sendout is blocking (flicker free but conflicts with anything that requires timed ISRs, like IR receiver) +Each bus can have individual configuration of color channels but all must share the same timing +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_BitBang.h" +#if defined(ARDUINO_ARCH_ESP32) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + #include "esp_private/esp_clk.h" +#else + #include "clk.h" // esp_clk_cpu_freq() +#endif + + +#include "esp_cpu.h" +#elif defined(ESP8266) +#include +#define REG_WRITE(addr, val) GPIO_REG_WRITE(addr, val) +#define GPIO_OUT_W1TS_REG GPIO_OUT_W1TS_ADDRESS +#define GPIO_OUT_W1TC_REG GPIO_OUT_W1TC_ADDRESS +#endif + +namespace WLEDpixelBus { +// BB state, shared among all buses +BitBangBus::BBstate* BitBangBus::_bitBangState = nullptr; + +// critical section macros +#if defined(ESP8266) + #define WLEDPB_BB_ENTERCRITICAL() os_intr_lock() + #define WLEDPB_BB_EXITCRITICAL() os_intr_unlock() +#else + #define WLEDPB_BB_ENTERCRITICAL() portENTER_CRITICAL(&_bitBangState->mux) + #define WLEDPB_BB_EXITCRITICAL() portEXIT_CRITICAL(&_bitBangState->mux) +#endif + + +// getCycleCount() reads the CPU cycle counter for precise timing +#if defined(ESP8266) + static inline uint32_t getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("rsr %0,ccount" : "=a"(ccount)); + return ccount; + } + static constexpr uint32_t LATENCY_CYCLES = 16; +#else + + static inline __attribute__((always_inline)) uint32_t getCycleCount() { + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + return esp_cpu_get_cycle_count(); + #else + return esp_cpu_get_ccount(); + #endif + } +#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32P4) + static constexpr uint32_t LATENCY_CYCLES = 8; +#elif defined(CONFIG_IDF_TARGET_ESP32C5) + static constexpr uint32_t LATENCY_CYCLES = 56; // C5 has high latency from CPU to AHB bus for some reason TODO: needs investigation +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + static constexpr uint32_t LATENCY_CYCLES = 24; +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + static constexpr uint32_t LATENCY_CYCLES = 10; +#else // ESP32 classic + static constexpr uint32_t LATENCY_CYCLES = 12; +#endif +#endif + +// Constructor / Destructor +BitBangBus::BitBangBus(int8_t pin, const LedTiming& timing, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _initialized(false) +{ + _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 (!_bitBangState) { + _bitBangState = (BBstate*)calloc(1, sizeof(BBstate)); // Allocate shared state struct on first channel + if (!_bitBangState) return false; + } + if (_bitBangState->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 = LATENCY_CYCLES; + + uint32_t t0h = (_timing.t0h_ns * cpuMHz) / 1000u; + uint32_t t1h = (_timing.t1h_ns * cpuMHz) / 1000u; + t0h = (t0h > lo) ? t0h - lo : 0u; + t1h = (t1h > lo) ? t1h - lo : 0u; + uint32_t period = (((uint32_t)_timing.t0h_ns + _timing.t0l_ns) * cpuMHz) / 1000u; // derive the period from the "0" bit + period = (period > lo) ? period - lo : 0u; + // max time allowed before we assume a latch, use a quarter of the reset period. + // Note: there are strips that latch after just ~10us maybe even less, users can customize this period and set it to 10us if there are issues. + // if the reset period is set to 0, interrupts are disabled + uint32_t latch_us = _timing.reset_us >> 2; + if (latch_us < 10) latch_us = 10; // note: below 10us the output will starve due to short interrupts + const uint32_t latchCycles = latch_us * cpuMHz; + + // Register in the shared static table + const uint8_t idx = _bitBangState->channelCount; + _bitBangState->pins[idx] = _pin; +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_pin >= 32) _bitBangState->allMaskHigh |= (1u << (_pin - 32)); + else _bitBangState->allMask |= (1u << _pin); +#else + _bitBangState->allMask |= (1u << _pin); +#endif + _bitBangState->channelCount++; + + // Allocate the per-pixel encode buffer (via PixelBus helper) + if (!allocateEncodeBuffer(_numPixels, _encoder.getBytesPerPixel())) { + _bitBangState->channelCount--; +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_pin >= 32) _bitBangState->allMaskHigh &= ~(1u << (_pin - 32)); + else _bitBangState->allMask &= ~(1u << _pin); +#else + _bitBangState->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. + _bitBangState->encodeBuffer[idx] = _encodeBuffer; + _bitBangState->encodeBufferSize[idx] = _encodeBufferSize; + // The first successfully initialized channel sets the timing for all channels. + if (idx == 0) { + _bitBangState->t0h = t0h; + _bitBangState->t1h = t1h; + _bitBangState->period = period; + _bitBangState->latchCycles = latchCycles; + _bitBangState->allowInterrupts = _timing.reset_us > 0; + } + + _initialized = true; + return true; +} + +void BitBangBus::end() { + if (_initialized) { + // Find our slot by scanning _bitBangState->pins (no stored index needed). + uint8_t slot = _bitBangState->channelCount; // sentinel: not found + for (uint8_t i = 0; i < _bitBangState->channelCount; i++) { + if (_bitBangState->pins[i] == _pin) { slot = i; break; } + } + if (slot < _bitBangState->channelCount) { + // Shift remaining entries down to fill the gap. + for (uint8_t i = slot; i + 1 < _bitBangState->channelCount; i++) { + _bitBangState->pins[i] = _bitBangState->pins[i + 1]; + _bitBangState->encodeBuffer[i] = _bitBangState->encodeBuffer[i + 1]; + _bitBangState->encodeBufferSize[i] = _bitBangState->encodeBufferSize[i + 1]; + } + _bitBangState->channelCount--; + _bitBangState->allMask = 0; +#ifdef ESP_HAS_HIGH_GPIO_BANK + _bitBangState->allMaskHigh = 0; + for (uint8_t i = 0; i < _bitBangState->channelCount; i++) { + if (_bitBangState->pins[i] >= 32) _bitBangState->allMaskHigh |= (1u << (_bitBangState->pins[i] - 32)); + else _bitBangState->allMask |= (1u << _bitBangState->pins[i]); + } +#else + for (uint8_t i = 0; i < _bitBangState->channelCount; i++) _bitBangState->allMask |= (1u << _bitBangState->pins[i]); +#endif + _bitBangState->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. + releaseEncodeBuffer(); + + // If no channels left, free the shared state struct + if (_bitBangState && _bitBangState->channelCount == 0) { + free(_bitBangState); + _bitBangState = nullptr; + } +} + +// show() stages this channel's data. When the last channel stages, run outputParallel() +bool BitBangBus::show() { + if (!_initialized || !_encodeBuffer) return false; + + _bitBangState->stagedCount++; + if (_bitBangState->stagedCount < _bitBangState->channelCount) { + return true; // not all channels have been staged, we need all of them triggered before sending + } + + _bitBangState->stagedCount = 0; // reset for next frame + for (uint8_t retry = 0; retry < 3; ++retry) { + if (outputParallel()) return true; + // Let every strip latch the partial frame before restarting from pixel zero. + const uint32_t resetStart = getCycleCount(); + while ((getCycleCount() - resetStart) < _bitBangState->latchCycles) yield(); + } + return false; +} + +// resetChannelTracking() — called by PixelBusAllocator::resetChannelTracking() +void BitBangBus::resetChannelTracking() { + if (_bitBangState) memset(_bitBangState, 0, sizeof(BBstate)); +} + +// outputParallel() is the hot path and MUST remain in IRAM. Completed channels emit zero bits. +// ISRs are enabled after every LED, an over-long gap aborts the frame (and retries) +// --------------------------------------------------------------------------- +bool IRAM_ATTR __attribute__((noinline)) BitBangBus::outputParallel() { + if (!_bitBangState || _bitBangState->channelCount == 0) return true; + const uint32_t t0h = _bitBangState->t0h; + const uint32_t t1h = _bitBangState->t1h; + const uint32_t period = _bitBangState->period; + const uint32_t latchCycles = _bitBangState->latchCycles; + const uint8_t nCh = _bitBangState->channelCount; + const bool allowInterrupts = _bitBangState->allowInterrupts; +#ifdef ESP_HAS_HIGH_GPIO_BANK + const uint32_t setOutputMaskLow = _bitBangState->allMask; + const uint32_t setOutputMaskHigh = _bitBangState->allMaskHigh; +#else + const uint32_t setOutputMask = _bitBangState->allMask; +#endif + + size_t maxBytes = 0; + uint32_t chanPinMask[nCh]; +#ifdef ESP_HAS_HIGH_GPIO_BANK + uint32_t chanPinMaskHigh[nCh]; +#endif + for (uint8_t ch = 0; ch < nCh; ++ch) { + if (_bitBangState->encodeBufferSize[ch] > maxBytes) maxBytes = _bitBangState->encodeBufferSize[ch]; +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_bitBangState->pins[ch] >= 32) { + chanPinMask[ch] = 0; + chanPinMaskHigh[ch] = 1u << (_bitBangState->pins[ch] - 32); + } else { + chanPinMask[ch] = 1u << _bitBangState->pins[ch]; + chanPinMaskHigh[ch] = 0; + } +#else + chanPinMask[ch] = 1u << _bitBangState->pins[ch]; +#endif + } + if (maxBytes == 0) return true; + const uint32_t totalBits = maxBytes * 8u; + uint32_t cyclesStart = getCycleCount() - period; + WLEDPB_BB_ENTERCRITICAL(); + + for (uint32_t bitIndex = 0; bitIndex < totalBits; ++bitIndex) { + const size_t byteIndex = bitIndex >> 3; + const uint8_t bitMask = 0x80u >> (bitIndex & 7u); +#ifdef ESP_HAS_HIGH_GPIO_BANK + uint32_t zeroMaskLow = 0, zeroMaskHigh = 0; + for (uint8_t ch = 0; ch < nCh; ++ch) { + if (byteIndex >= _bitBangState->encodeBufferSize[ch] || !(_bitBangState->encodeBuffer[ch][byteIndex] & bitMask)) { + zeroMaskLow |= chanPinMask[ch]; + zeroMaskHigh |= chanPinMaskHigh[ch]; + } + } +#else + uint32_t zeroMask = 0; + for (uint8_t ch = 0; ch < nCh; ++ch) { + if (byteIndex >= _bitBangState->encodeBufferSize[ch] || !(_bitBangState->encodeBuffer[ch][byteIndex] & bitMask)) { + zeroMask |= chanPinMask[ch]; + } + } +#endif + + while ((getCycleCount() - cyclesStart) < period) {} +#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(); + 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 + 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 (allowInterrupts && ((bitIndex + 1) % 24u == 0)) { + const uint32_t idleStart = getCycleCount(); + WLEDPB_BB_EXITCRITICAL(); + WLEDPB_BB_ENTERCRITICAL(); + if ((getCycleCount() - idleStart) > latchCycles) { + WLEDPB_BB_EXITCRITICAL(); + return false; + } + } + } + WLEDPB_BB_EXITCRITICAL(); + 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..1e61a01c94 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -0,0 +1,74 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#if defined(ARDUINO_ARCH_ESP32) +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +//#include "soc/gpio_struct.h" // GPIO.out_w1ts / GPIO.out_w1tc -> better use REG_WRITE +#include "soc/gpio_reg.h" +#include "driver/gpio.h" +#elif defined(ESP8266) +#include +#include +#include +#include +#endif + +// Note: maximum number of parallel BitBang channels WLED_MAX_BB_CHANNELS is defined in const.h + +namespace WLEDpixelBus { +class BitBangBus : public PixelBus { +public: + BitBangBus(int8_t pin, const LedTiming& timing, uint8_t ledType = 0); + ~BitBangBus() override; + + bool begin() override; + void end() override; + + bool show() override; // Stage this channel's data. When all channels are staged, output all in parallel. + bool canShow() const override { return true; } // BitBang output is synchronous — always ready TODO: on multi-core systems with tasks running on different cores, this is not true (but currently there is no separate task) + +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "BITBANG"; } +#endif + + static void resetChannelTracking(); // reset shared channel tracking, called by PixelBusAllocator + +private: + int8_t _pin = -1; + LedTiming _timing; + bool _initialized = false; + + // Shared state (one context for ALL BitBangBus instances); timing is set by the first channel. + struct BBstate { + const uint8_t* encodeBuffer[WLED_MAX_BB_CHANNELS]; // encoded color data to be sent out + size_t encodeBufferSize[WLED_MAX_BB_CHANNELS]; + 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]; + uint8_t channelCount; + uint8_t stagedCount; // how many channels have called show() this frame + bool allowInterrupts; // if enabled, interrupts are allowed to run between LEDs, if not the output is fully blocking + }; + static BBstate* _bitBangState; + + 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..c0bf99d508 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -0,0 +1,637 @@ +/*------------------------------------------------------------------------- + +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::_uartInstances[2] = {nullptr, nullptr}; + +Esp8266UartBus::Esp8266UartBus(int8_t pin, const LedTiming& timing, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _initialized(false) + , _asyncBuf(nullptr) + , _asyncBufEnd(nullptr) + , _txStartMillis(0) +{ + // encoder is installed by createBus() from BusProperties + _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 = _uartInstances[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; + _uartInstances[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.getBytesPerPixel())) { end(); return false; } + return true; +} + +void Esp8266UartBus::end() { + if (!_initialized) return; + waitForIdle(); + uint8_t uartNum = (_pin == 2) ? 1 : 0; + + ETS_UART_INTR_DISABLE(); + USIE(uartNum) = 0; + _uartInstances[uartNum] = nullptr; + + // If no buses are left, detach ISR + if (_uartInstances[0] == nullptr && _uartInstances[1] == nullptr) { + ETS_UART_INTR_ATTACH(nullptr, nullptr); + } else { + ETS_UART_INTR_ENABLE(); + } + + if (_pin == 2) Serial1.end(); + else Serial.end(); + _initialized = false; +} + +void Esp8266UartBus::updateUartTiming() { + uint32_t periodNs = _timing.bitPeriod(); + if (periodNs < 200) periodNs = 1250; + uint32_t baud = 4000000000ULL / periodNs; + + uint8_t uartNum = (_pin == 2) ? 1 : 0; + if (uartNum == 1) { + Serial1.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); + } else { + Serial.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); + } + + const uint32_t fifoResetFlags = (1 << UCTXRST) | (1 << UCRXRST); + USC0(uartNum) |= fifoResetFlags; + USC0(uartNum) &= ~(fifoResetFlags); + USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI)); // clear invert bits + if (!_inverted) { + USC0(uartNum) |= (1 << UCTXI); // invert TX -> idle low + } +} + +bool Esp8266UartBus::show() { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + waitForIdle(); + // 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; + _txStartMillis = millis(); + + // 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; +} + +bool Esp8266UartBus::isIdle() const { + if (_asyncBuf < _asyncBufEnd) return false; + const uint8_t uartNum = (_pin == 2) ? 1 : 0; + return ((USS(uartNum) >> USTXC) & 0xff) == 0; +} + +void Esp8266UartBus::abortTransmit() const { + const uint8_t uartNum = (_pin == 2) ? 1 : 0; + ETS_UART_INTR_DISABLE(); + USIE(uartNum) &= ~(1 << UIFE); + _asyncBuf = nullptr; + _asyncBufEnd = nullptr; + ETS_UART_INTR_ENABLE(); +} + +bool Esp8266UartBus::ensureIdle() const { + if (isIdle()) return true; + if ((uint32_t)(millis() - _txStartMillis) <= WLEDPB_TRANSFER_TIMEOUT_MS) return false; + abortTransmit(); + return true; +} + +void Esp8266UartBus::waitForIdle() const { + const uint32_t start = millis(); + while (!ensureIdle() && (uint32_t)(millis() - start) <= WLEDPB_TRANSFER_TIMEOUT_MS) delay(1); + if (!isIdle()) abortTransmit(); +} + +bool Esp8266UartBus::canShow() const { + if (!_initialized) return true; // do not block if init failed! busmanager waits until all buses can show + return ensureIdle(); +} + + +// ESP8266 DMA Bus (I2S + SLC linked-list DMA) +// -------------------------------------------- +Esp8266DmaBus* Esp8266DmaBus::_dmaInstance = nullptr; + +Esp8266DmaBus::Esp8266DmaBus(int8_t pin, const LedTiming& timing, uint8_t ledType) + : _pin(pin) + , _timing(timing) + , _initialized(false) + , _sending(false) + , _txStartMillis(0) + , _bckDiv(1) + , _clkDiv(1) + , _dmaDesc(nullptr) + , _dmaDescCnt(0) + , _idleBuf(nullptr) + , _idleBufSize(0) +{ + // encoder is installed by createBus() from BusProperties + _ledType = ledType; +} + +Esp8266DmaBus::~Esp8266DmaBus() { + end(); +} + +bool Esp8266DmaBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t bytesPerPixel) { + // 4-step I2S encoding: each source byte → 4 encoded bytes + const size_t sourcePixelBytes = (size_t)numPixels * bytesPerPixel; + const size_t prefixBytes = (size_t)_prefixLen * 4; + const size_t pixelBytes = sourcePixelBytes * 4; + const size_t needed = prefixBytes + pixelBytes + (size_t)_suffixLen * 4; + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + releaseEncodeBuffer(); + if (needed == 0) return true; + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { _encodeBufferSize = 0; return false; } + memset(_encodeBuffer, 0, needed); + _encodeBufferSize = needed; + _pixelData = _encodeBuffer + prefixBytes; + updatePrefix(_prefixData, _prefixLen); + if (_suffixLen > 0) { + uint32_t* dst = (uint32_t*)(_pixelData + pixelBytes); + for (uint8_t i = 0; i < _suffixLen; i++) { + uint32_t word = 0; + uint8_t v = _suffixData[i]; + for (int bit = 7; bit >= 0; bit--) { + word <<= 4; + if (_inverted) word |= (v & (1 << bit)) ? 0x1u : 0x7u; + else word |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + dst[i] = word; + } + } + return true; +} + +void Esp8266DmaBus::buildDescriptorChain() { + uint32_t bitPeriod = _timing.bitPeriod(); + if (bitPeriod == 0) bitPeriod = 1250; + uint32_t resetUs = (_timing.reset_us > 300) ? _timing.reset_us : 300; + size_t resetBytes = (size_t)((4000.0f / (float)bitPeriod) * (float)resetUs + 0.5f); + resetBytes = (resetBytes + 3) & ~3u; // round up to 4-byte boundary + if (resetBytes < 4) resetBytes = 4; + + 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; + + // 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; + + 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++; + } + _dmaDesc[idx - 1].eof = 1; // Mark the last data descriptor as EOF: ISR fires here + + 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) +void IRAM_ATTR Esp8266DmaBus::slcISR() { + ETS_SLC_INTR_DISABLE(); + uint32_t status = SLCIS; + SLCIC = 0xFFFFFFFF; + if ((status & SLCIRXEOF) && _dmaInstance) { + // Re-close the idle loop so state[0]→state[1]→state[0] + _dmaInstance->_dmaDesc[1].next_link_ptr = &_dmaInstance->_dmaDesc[0]; + _dmaInstance->_sending = false; + } + ETS_SLC_INTR_ENABLE(); +} + +// TODO: just like in uart, the output pin matrix is somehow overwritten, needs investigation and maybe a workaround like in UART +void Esp8266DmaBus::startI2s(uint8_t bckDiv, uint8_t clkDiv) const { + 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; +} + +void Esp8266DmaBus::stopI2s() const { + ETS_SLC_INTR_DISABLE(); + SLCIE = 0; + SLCRXL &= ~SLCRXLS; // stop both links before their descriptors can be freed + SLCTXL &= ~SLCTXLS; + SLCIC = 0xFFFFFFFF; + _sending = false; + I2SC &= ~(I2STXS | I2SRXS); + I2SC &= ~I2SRST; + I2SC |= I2SRST; + I2SC &= ~I2SRST; +} + +// --------------------------------------------------------------------------- +// begin +// --------------------------------------------------------------------------- +bool Esp8266DmaBus::begin() { + if (_initialized) return true; + if (_pin != 3) return false; + + // Hold GPIO3 appropriately before any I2S activity (prevents startup glitch) + pinMode(3, OUTPUT); + digitalWrite(3, _inverted ? HIGH : LOW); + + // Compute I2S clock divisors for 4-step cadence + // I2S base clock = 160 MHz (even with 80 MHz CPU freq, tested) + // I2S clock is baseclk / bckDiv / clkDiv. We want to get as close as possible to 4 / bitPeriod + uint8_t best_clkdiv = 1; + uint8_t best_baseclkdiv = 1; + uint64_t best_error = UINT64_MAX; + // target I2S bit clock = 4 / bitPeriod_ns * 1e9 Hz + // divisor = I2SBASEFREQ / rate = I2SBASEFREQ * bitPeriod / 4e9 + uint32_t bitPeriod = _timing.bitPeriod(); // in nanoseconds, for example 1250=1.25us for WS2812 + uint64_t target = (uint64_t)I2SBASEFREQ * bitPeriod; + + for (uint8_t bck = 2; bck <= 64; bck += 2) { + uint64_t den = (uint64_t)4000000000ULL * bck; // denominator of clkdiv = I2SBASEFREQ * bitPeriod / (4e9 * baseclkdiv) + + uint64_t clk = (target + den / 2) / den; + if (clk < 1 || clk > 63) continue; + + uint64_t actual = clk * den; + uint64_t error = (actual > target) ? (actual - target) : (target - actual); + + if (error < best_error) { + best_error = error; + best_clkdiv = clk; + best_baseclkdiv = bck; + } + } + + _bckDiv = best_baseclkdiv; + _clkDiv = best_clkdiv; + + // Allocate idle/reset zero buffer + _idleBufSize = c_idleBufSize; + _idleBuf = (uint8_t*)malloc(_idleBufSize); + if (!_idleBuf) return false; + memset(_idleBuf, _inverted ? 0xFF : 0x00, _idleBufSize); + + // Allocate pixel encode buffer and build initial descriptor chain + if (!allocateEncodeBuffer(_numPixels, _encoder.getBytesPerPixel())) { end(); return false; } + buildDescriptorChain(); + if (!_dmaDesc) { end(); return false; } + + _dmaInstance = 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); + _dmaInstance = nullptr; + if (_dmaDesc) { free(_dmaDesc); _dmaDesc = nullptr; _dmaDescCnt = 0; } + if (_idleBuf) { free(_idleBuf); _idleBuf = nullptr; _idleBufSize = 0; } + releaseEncodeBuffer(); + pinMode(_pin, INPUT); + _initialized = false; + _sending = false; +} + +// --------------------------------------------------------------------------- +// setPixel / getPixelColor / encoded-buffer scaling +// Pixel data starts at offset 0 in _encodeBuffer (no lead-in needed; +// idle state ensures GPIO3 is LOW before the first pixel bit arrives). +// --------------------------------------------------------------------------- +void IRAM_ATTR Esp8266DmaBus::encodeBytes(const uint8_t* src, uint32_t* dst) const { + const uint8_t bytes = _encoder.getBytesPerPixel(); + for (uint8_t b = 0; b < bytes; ++b) { + uint32_t word = 0; + const uint8_t value = src[b]; + for (int bit = 7; bit >= 0; --bit) { + word <<= 4; + if (_inverted) word |= (value & (1 << bit)) ? 0x1u : 0x7u; + else word |= (value & (1 << bit)) ? 0xEu : 0x8u; + } + dst[b] = word; + } +} + +void IRAM_ATTR Esp8266DmaBus::decodeBytes(const uint32_t* src, uint8_t* dst) const { + const uint8_t bytes = _encoder.getBytesPerPixel(); + for (uint8_t b = 0; b < bytes; ++b) { + const uint32_t word = src[b]; + uint8_t value = 0; + for (int nib = 7; nib >= 0; --nib) { + value <<= 1; + const uint8_t step = (word >> (nib * 4)) & 0xF; + if ((!_inverted && step == 0xE) || (_inverted && step == 0x1)) value |= 1; + } + dst[b] = value; + } +} + +IRAM_ATTR bool Esp8266DmaBus::setPixelColor(uint16_t pos, uint32_t c, uint16_t wwcw) { + const uint8_t pixelBytes = _encoder.getBytesPerPixel(); + 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); + encodeBytes(src, dst); + return true; +} + +IRAM_ATTR uint32_t Esp8266DmaBus::getPixelColor(uint16_t pix) const { + const uint8_t pixelBytes = _encoder.getBytesPerPixel(); + const uint32_t* src = (const uint32_t*)(_pixelData + (size_t)pix * pixelBytes * 4); + uint8_t decoded[12]; + decodeBytes(src, decoded); + 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); + } +} + +void Esp8266DmaBus::scaleEncodeBuffer(uint8_t scale) { + if (!_pixelData || scale == 255) return; + const uint8_t pixelBytes = _encoder.getBytesPerPixel(); + for (uint16_t pixel = 0; pixel < _numPixels; ++pixel) { + uint32_t* encoded = (uint32_t*)(_pixelData + (size_t)pixel * pixelBytes * 4); + uint8_t decoded[12]; + decodeBytes(encoded, decoded); + _encoder.scalePixel(decoded, scale); + encodeBytes(decoded, encoded); + } +} + +void Esp8266DmaBus::clearEncodeBuffer() { + scaleEncodeBuffer(0); // TODO: this needs optimisation, it is slow +} + +void Esp8266DmaBus::updatePrefix(const uint8_t* data, uint8_t len) { + if (!_encodeBuffer || !data || _prefixLen == 0 || len == 0) return; + if (len > _prefixLen) len = _prefixLen; + uint32_t* dst = (uint32_t*)_encodeBuffer; + for (uint8_t i = 0; i < len; i++) { + uint32_t word = 0; + uint8_t v = data[i]; + for (int bit = 7; bit >= 0; bit--) { + word <<= 4; + if (_inverted) word |= (v & (1 << bit)) ? 0x1u : 0x7u; + else word |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + dst[i] = word; + } +} + +void Esp8266DmaBus::updateSuffix(const uint8_t* data, uint8_t len) { + if (!_pixelData || !data || _suffixLen == 0 || len == 0) return; + if (len > _suffixLen) len = _suffixLen; + const size_t pixelBytes = (size_t)_numPixels * _encoder.getBytesPerPixel(); + uint32_t* dst = (uint32_t*)(_pixelData + pixelBytes * 4); + for (uint8_t i = 0; i < len; i++) { + uint32_t word = 0; + uint8_t v = data[i]; + for (int bit = 7; bit >= 0; bit--) { + word <<= 4; + if (_inverted) word |= (v & (1 << bit)) ? 0x1u : 0x7u; + else word |= (v & (1 << bit)) ? 0xEu : 0x8u; + } + dst[i] = word; + } +} + +// --------------------------------------------------------------------------- +// show +// Patches state[1] to break out of the idle loop into the pixel data, +// then returns immediately. The ISR restores the idle loop when done. +// --------------------------------------------------------------------------- +bool Esp8266DmaBus::show() { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + waitForIdle(); + + // Publish the active state before breaking the idle loop to avoid race condition on short frames + ETS_SLC_INTR_DISABLE(); + _sending = true; + _txStartMillis = millis(); + __asm__ __volatile__("" ::: "memory"); + _dmaDesc[1].next_link_ptr = &_dmaDesc[c_stateBlockCount]; // Break the idle loop: state[1] now points to first pixel descriptor + __asm__ __volatile__("" ::: "memory"); + ETS_SLC_INTR_ENABLE(); + + return true; +} + +void Esp8266DmaBus::abortTransmit() const { + _dmaDesc[1].next_link_ptr = &_dmaDesc[0]; + stopI2s(); + startI2s(_bckDiv, _clkDiv); // restart the idle loop with valid descriptors + _sending = false; +} + +bool Esp8266DmaBus::ensureIdle() const { + if (!_sending) return true; + if ((uint32_t)(millis() - _txStartMillis) <= WLEDPB_TRANSFER_TIMEOUT_MS) return false; + abortTransmit(); + return true; +} + +void Esp8266DmaBus::waitForIdle() const { + const uint32_t start = millis(); + while (!ensureIdle() && (uint32_t)(millis() - start) <= WLEDPB_TRANSFER_TIMEOUT_MS) delay(1); + if (_sending) abortTransmit(); +} + +bool Esp8266DmaBus::canShow() const { + if (!_initialized) return true; // do not block if init failed! busmanager waits until all buses can show + return ensureIdle(); +} + +} // namespace WLEDpixelBus + +#endif // ESP8266 \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h new file mode 100644 index 0000000000..201c1c9aa1 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -0,0 +1,129 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - ESP8266 driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ +#pragma once + +#include "WLEDpixelBus.h" + +#if defined(ESP8266) + +namespace WLEDpixelBus { + + +// ESP8266 UART Bus (Asynchronous via UART1/UART0) +class Esp8266UartBus : public PixelBus { +public: + Esp8266UartBus(int8_t pin, const LedTiming& timing, uint8_t ledType = 0); + ~Esp8266UartBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "UART"; } +#endif + static void uartISR(void* arg, void* exceptionFrame); + static Esp8266UartBus* _uartInstances[2]; + +private: + int8_t _pin; + LedTiming _timing; + bool _initialized; + mutable volatile uint8_t* _asyncBuf = nullptr; + mutable volatile uint8_t* _asyncBufEnd = nullptr; + mutable uint32_t _txStartMillis = 0; + + void updateUartTiming(); + bool isIdle() const; + bool ensureIdle() const; + void waitForIdle() const; + void abortTransmit() const; +}; + + +// 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 ledType = 0); + ~Esp8266DmaBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "DMA"; } +#endif + + IRAM_ATTR bool setPixelColor(uint16_t pos, uint32_t c, uint16_t wwcw) override; + IRAM_ATTR uint32_t getPixelColor(uint16_t pix) const override; + bool allocateEncodeBuffer(uint16_t numPixels, uint8_t bytesPerPixel) override; + void clearEncodeBuffer() override; + void updatePrefix(const uint8_t* data, uint8_t len) override; + void updateSuffix(const uint8_t* data, uint8_t len) override; + void scaleEncodeBuffer(uint8_t scale) override; + + static Esp8266DmaBus* _dmaInstance; + +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; + mutable volatile bool _sending; + mutable uint32_t _txStartMillis; + uint8_t _bckDiv; + uint8_t _clkDiv; + + // 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) const; + void stopI2s() const; + bool ensureIdle() const; + void waitForIdle() const; + void abortTransmit() const; + void IRAM_ATTR encodeBytes(const uint8_t* src, uint32_t* dst) const; + void IRAM_ATTR decodeBytes(const uint32_t* src, uint8_t* dst) const; + 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 \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp new file mode 100644 index 0000000000..2ec3025c32 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -0,0 +1,692 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel I2S/LCD output driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +supports ESP32, ESP32 S2 and ESP32 S3 (via LCD peripheral) +Default is 8 parallel outputs and double DMA buffering but it also supports 16 parallel outputs if needed +For 16 parallel output, triple buffering is required for glitch-free output. +Data is output in 4-step cadence meaning each LED bit is encoded into 4 I2S bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +3k per DMA buffer works well, enough for 32 RGB LEDs in 8x parallel output or roughly 0.9ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_I2S.h" + +#ifdef WLEDPB_I2S_SUPPORT +namespace WLEDpixelBus { + +I2sBusContext* I2sBusContext::_instances[WLEDPB_I2S_BUS_COUNT] = {nullptr}; +uint8_t I2sBusContext::_refCount[WLEDPB_I2S_BUS_COUNT] = {0}; + +I2sBusContext* I2sBusContext::get(uint8_t busNum) { + if (busNum >= WLEDPB_I2S_BUS_COUNT) return nullptr; + + if (_instances[busNum] == nullptr) { + _instances[busNum] = new I2sBusContext(busNum); + } + if (_instances[busNum] != nullptr) + _refCount[busNum]++; + return _instances[busNum]; +} + +void I2sBusContext::release(uint8_t busNum) { + if (busNum >= WLEDPB_I2S_BUS_COUNT) return; + if (_refCount[busNum] == 0) return; + + _refCount[busNum]--; + if (_refCount[busNum] == 0 && _instances[busNum]) { + delete _instances[busNum]; + _instances[busNum] = nullptr; + } +} + +#ifdef CONFIG_IDF_TARGET_ESP32S3 +I2sBusContext::I2sBusContext(uint8_t /*busNum*/) + : _dmaChannel(nullptr) + , _dmaChanId(-1) +#else +I2sBusContext::I2sBusContext(uint8_t busNum) + : _busNum(busNum) + , _i2sDev( +#if defined(CONFIG_IDF_TARGET_ESP32) + (busNum == 0) ? &I2S0 : &I2S1 +#else + &I2S0 +#endif + ) + , _isrHandle(nullptr) +#endif + , _lastFilled(0) +{ + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + _dmaDesc[i] = nullptr; + _dmaBuffer[i] = nullptr; + } +} + +I2sBusContext::~I2sBusContext() { + deinit(); +} + +#ifdef CONFIG_IDF_TARGET_ESP32S3 // S3 uses the LCD peripheral, not I2S + +bool I2sBusContext::hwInit() { + // 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 (240 MHz LCD base clock) + double clkm_div = 240000000.0 / (double)calc4StepClockHz(_timing); + if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { + return false; + } + + uint8_t clkm_div_int = (uint8_t)clkm_div; + double clkm_frac = clkm_div - clkm_div_int; + uint8_t divB = 0; + uint8_t divA = 0; + if (clkm_frac > 0.001) { + divA = 63; + divB = (uint8_t)(clkm_frac * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + // Configure LCD clock + LCD_CAM.lcd_clock.clk_en = 1; + LCD_CAM.lcd_clock.lcd_clk_sel = 2; + LCD_CAM.lcd_clock.lcd_clkm_div_a = divA; + LCD_CAM.lcd_clock.lcd_clkm_div_b = divB; + LCD_CAM.lcd_clock.lcd_clkm_div_num = clkm_div_int; + LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; + LCD_CAM.lcd_clock.lcd_ck_idle_edge = 0; + LCD_CAM.lcd_clock.lcd_clk_equ_sysclk = 1; + + // Configure frame format + LCD_CAM.lcd_ctrl.lcd_rgb_mode_en = 0; + LCD_CAM.lcd_rgb_yuv.lcd_conv_bypass = 0; + LCD_CAM.lcd_misc.lcd_next_frame_en = 0; + LCD_CAM.lcd_data_dout_mode.val = 0; + LCD_CAM.lcd_user.lcd_always_out_en = 1; + LCD_CAM.lcd_user.lcd_8bits_order = 0; + LCD_CAM.lcd_user.lcd_bit_order = 0; +#ifdef WLED_PIXELBUS_16PARALLEL + LCD_CAM.lcd_user.lcd_2byte_en = 1; +#else + LCD_CAM.lcd_user.lcd_2byte_en = 0; +#endif + LCD_CAM.lcd_user.lcd_dummy = 1; + LCD_CAM.lcd_user.lcd_dummy_cyclelen = 0; + LCD_CAM.lcd_user.lcd_cmd = 0; + + // Allocate GDMA channel + gdma_channel_alloc_config_t dma_chan_config = { + .sibling_chan = NULL, + .direction = GDMA_CHANNEL_DIRECTION_TX, + .flags = {.reserve_sibling = 0} + }; + + esp_err_t err = gdma_new_channel(&dma_chan_config, &_dmaChannel); + if (err != ESP_OK) return false; + int chanId = -1; + if (gdma_get_channel_id(_dmaChannel, &chanId) != ESP_OK) return false; + _dmaChanId = (int8_t)chanId; + + err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + if (err != ESP_OK) return false; + + gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(_dmaChannel, &strategy_config); + + // Register DMA callback + gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = gdmaTxDoneISR}; + 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::stopTransferHwIram() { + LCD_CAM.lcd_user.lcd_start = 0; + // Register-level GDMA stop: gdma_stop() lives in flash and must not be called from ISR + if (_dmaChanId >= 0) GDMA.channel[_dmaChanId].out.link.stop = 1; + _state = DriverState::Idle; +} + +void I2sBusContext::channelAttached(int8_t idx, int8_t pin, 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_context_t gpio_hal = { .dev = GPIO_HAL_GET_HW(GPIO_PORT_0) }; + gpio_hal_func_sel(&gpio_hal, 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() { + // 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 + + // note: PCM bypass is not needed for raw parallel output in LCD mode so we do not touch conf1 + + // 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 = base_clock_Hz / (4-step cadence clock * bck_div) + const uint8_t bckDiv = 4; // must be >= 2 + const uint32_t clkHz = calc4StepClockHz(_timing); // 4 clock cycles per LED bit period + +#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 baseClockHz = 160000000.0; + #else + const double baseClockHz = 80000000.0; // 16-bit mode: APB clock, lcd_tx_wrx2_en=0 has no rate halving + #endif +#else + const double baseClockHz = 80000000.0; // S2: 80MHz I2S base clock (wrx2 on S2 does not halve the rate) +#endif + + double clkmdiv = baseClockHz / (double)clkHz / (double)bckDiv; + 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; + } + + // Set clock (with fractional divider for accurate timing) + _i2sDev->clkm_conf.val = 0; + + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + _i2sDev->clkm_conf.clk_sel = 2; // APPL = 1 APB = 2 + _i2sDev->clkm_conf.clk_en = 1; // examples of i2s show this being set if sel is set to 2 + #else + _i2sDev->clkm_conf.clk_en = 1; + _i2sDev->clkm_conf.clka_en = 0; + #endif + + _i2sDev->clkm_conf.clkm_div_a = divA; + _i2sDev->clkm_conf.clkm_div_b = divB; + _i2sDev->clkm_conf.clkm_div_num = clkmInteger; + + _i2sDev->sample_rate_conf.val = 0; + _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; +#ifdef WLED_PIXELBUS_16PARALLEL + _i2sDev->sample_rate_conf.tx_bits_mod = 16; // 16-bit samples for up to 16 parallel channels +#else + _i2sDev->sample_rate_conf.tx_bits_mod = 8; // 8-bit samples for up to 8 parallel channels +#endif + + // Final reset before ISR install + _i2sDev->lc_conf.in_rst = 1; + _i2sDev->lc_conf.out_rst = 1; + _i2sDev->lc_conf.ahbm_rst = 1; + _i2sDev->lc_conf.ahbm_fifo_rst = 1; + _i2sDev->lc_conf.in_rst = 0; + _i2sDev->lc_conf.out_rst = 0; + _i2sDev->lc_conf.ahbm_rst = 0; + _i2sDev->lc_conf.ahbm_fifo_rst = 0; + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.tx_fifo_reset = 0; + + // Install ISR + int intSource; + #if defined(CONFIG_IDF_TARGET_ESP32) + intSource = (_busNum == 0) ? ETS_I2S0_INTR_SOURCE : ETS_I2S1_INTR_SOURCE; + #else + intSource = ETS_I2S0_INTR_SOURCE; + #endif + + esp_err_t err = esp_intr_alloc(intSource, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3, dmaISR, this, &_isrHandle); // note: level 3 is the maximum supported without resorting to assembly + if (err != ESP_OK) { + deinit(); + return false; + } + + return true; +} + +void I2sBusContext::hwDeinit() { + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } +#if defined(CONFIG_IDF_TARGET_ESP32) + periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); +#else + periph_module_disable(PERIPH_I2S0_MODULE); +#endif +} + +void I2sBusContext::hwStartTransfer() { + _i2sDev->lc_conf.in_rst = 1; + _i2sDev->lc_conf.out_rst = 1; + _i2sDev->lc_conf.ahbm_rst = 1; + _i2sDev->lc_conf.ahbm_fifo_rst = 1; + _i2sDev->lc_conf.in_rst = 0; + _i2sDev->lc_conf.out_rst = 0; + _i2sDev->lc_conf.ahbm_rst = 0; + _i2sDev->lc_conf.ahbm_fifo_rst = 0; + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.tx_fifo_reset = 0; + + _i2sDev->int_clr.val = 0xFFFFFFFF; + _i2sDev->int_ena.out_eof = 1; // single descriptor transfer finished + _i2sDev->int_ena.out_total_eof = 1; // full transfer finished + + _i2sDev->fifo_conf.dscr_en = 1; + _i2sDev->out_link.start = 0; + _i2sDev->out_link.addr = (uint32_t)_dmaDesc[0]; + _i2sDev->out_link.start = 1; + _i2sDev->conf.tx_start = 1; +} + +void IRAM_ATTR I2sBusContext::stopTransferHwIram() { + if (_i2sDev) { + _i2sDev->int_ena.out_eof = 0; + _i2sDev->int_ena.out_total_eof = 0; + _i2sDev->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } + _state = DriverState::Idle; +} + +void I2sBusContext::channelAttached(int8_t idx, int8_t pin, 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 + +void I2sBusContext::channelDetached(int8_t /*idx*/, int8_t pin) { + if (pin >= 0) gpio_reset_pin((gpio_num_t)pin); +} + +void I2sBusContext::releaseResources() { + 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; + } + } +} + +bool I2sBusContext::_allocDmaBuffers() { + if (_dmaAllocated) return true; + _bufferSize = calc4StepBufferSize(_maxSrcBytes, WLEDPB_I2S_DMABYTES, + WLEDPB_I2S_DMA_BUFFER_COUNT, MIN_DMA_BUFFER_SIZE, DEFAULT_DMA_BUFFER_SIZE); + if (_bufferSize == 0) return false; + + auto rollback = [&]() { + for (uint8_t i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; ++i) { + if (_dmaDesc[i]) heap_caps_free(_dmaDesc[i]); + if (_dmaBuffer[i]) heap_caps_free(_dmaBuffer[i]); + _dmaDesc[i] = nullptr; + _dmaBuffer[i] = nullptr; + } + _bufferSize = 0; + }; + + for (uint8_t 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]) { rollback(); 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]) { rollback(); return false; } + } + + for (uint8_t 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; +} + +// 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) { +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + encode4Step16(dest, destLen, maxChannel, _channels); +#else + encode4Step16(dest, destLen, maxChannel, _channels); +#endif +} + +#else +// 8-bit parallel encode: branchless gather + scatter, 32 bytes per source byte (8 channels) +// uses the shared 4-step encoder, only the scatter layout differs per target +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + encode4Step8(dest, destLen, maxChannel, _channels); +#else + // Classic ESP32: half-word swap, memory [S2,S3,S0,S1] = [data,0,HIGH,data] + encode4Step8(dest, destLen, maxChannel, _channels); +#endif +} +#endif // WLED_PIXELBUS_16PARALLEL + +void IRAM_ATTR __attribute__((noinline)) I2sBusContext::fillBuffer(uint8_t bufIdx) { + uint32_t* buf32 = (uint32_t*)_dmaBuffer[bufIdx]; + const uint32_t* end = buf32 + (_bufferSize >> 2); + while (buf32 < end) *buf32++ = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned. Note: memset is not IRAM safe and may crash + + if (_resetBytesLeft > 0 && _resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { + if (_resetBytesLeft <= _bufferSize) { + descSetLength(_dmaDesc[bufIdx], _resetBytesLeft); + descSetNext(_dmaDesc[bufIdx], nullptr); + _resetBytesLeft = WLEDPB_I2S_XFER_DONE_FLAG; + } else { + descSetLength(_dmaDesc[bufIdx], _bufferSize); + _resetBytesLeft -= _bufferSize; + } + return; + } + + 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; + } + } + + size_t translatedBytes = bytesToEncode * WLEDPB_I2S_DMABYTES; + if (translatedBytes > _bufferSize) translatedBytes = _bufferSize; + translatedBytes -= translatedBytes % WLEDPB_I2S_DMABYTES; + descSetLength(_dmaDesc[bufIdx], 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 + size_t resetBytes = calc4StepResetDmaBytes(_timing, WLEDPB_I2S_DMABYTES / 8); + + 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 + + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + } + } + + _resetBytesLeft = 0; + + if (!_dmaAllocated) { + if (!_allocDmaBuffers()) return false; + } + + // Fill all buffers initially + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + descSetNext(_dmaDesc[i], _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]); // restore circular buffer chain + descSetLength(_dmaDesc[i], _bufferSize); + fillBuffer(i); + descSetEof(_dmaDesc[i]); // enable eof, just in case + descSetOwnerDma(_dmaDesc[i]); // hand ownership over to DMA after descriptor init + } + + _lastFilled = WLEDPB_I2S_DMA_BUFFER_COUNT - 1; // all buffers were just pre-filled, next to refill is buffer 0 + _state = DriverState::Sending; + _txStartMillis = millis(); // watchdog start + + hwStartTransfer(); + + return true; +} + +#ifdef CONFIG_IDF_TARGET_ESP32S3 +IRAM_ATTR bool I2sBusContext::gdmaTxDoneISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data) { + (void)event_data; + I2sBusContext* ctx = (I2sBusContext*)user_data; + if (dma_chan != ctx->_dmaChannel || ctx->_dmaChanId < 0) return false; // sanity check, just in case + + uint32_t eofAddr = GDMA.channel[ctx->_dmaChanId].out.eof_des_addr; // descriptor that just finished + + int8_t completedBuf = -1; + for (uint8_t i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + if ((uint32_t)ctx->_dmaDesc[i] == eofAddr) { completedBuf = i; break; } + } + + ctx->_processEof(completedBuf); + return false; +} +#else +void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { + I2sBusContext* ctx = (I2sBusContext*)arg; + i2s_dev_t* dev = ctx->_i2sDev; + uint32_t status = dev->int_st.val; + dev->int_clr.val = status; + + if (status & I2S_OUT_TOTAL_EOF_INT_ST) { // link terminated: frame fully sent + ctx->stopTransferHwIram(); + return; + } + if (!(status & I2S_OUT_EOF_INT_ST)) return; + + // which descriptor actually produced this EOF (resyncs even if EOFs were coalesced) + uint32_t eofAddr = dev->out_eof_des_addr; + int8_t completedBuf = -1; + for (uint8_t i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + if ((uint32_t)ctx->_dmaDesc[i] == eofAddr) { completedBuf = i; break; } + } + + ctx->_processEof(completedBuf); +} +#endif + +// Refill buffer(s) upon EOF. The completed descriptor is safe to reuse, the following descriptor may still be in flight. +void IRAM_ATTR I2sBusContext::_processEof(int8_t completedBuf) { + if (completedBuf < 0) return; + + // end of frame? checks the just finished descriptor for the terminating nullptr + if (descGetNext(_dmaDesc[completedBuf]) == nullptr) { + stopTransferHwIram(); + return; + } + +#if WLEDPB_I2S_DMA_BUFFER_COUNT > 2 + if (_lastFilled == (uint8_t)completedBuf) return; // duplicate eof, nothing pending (never refill the in-flight buffer) + // refill every completed-but-unprocessed buffer, oldest first, to catch up if a previous eof was missed. + uint8_t b = (_lastFilled + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; + while (_resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { // dont fill if reset pulse was queued + fillBuffer(b); // fill buffer, handles reset pulse and end of transfer + _lastFilled = b; + if (b == (uint8_t)completedBuf) break; + b = (b + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; + } +#else + // double buffering: only the completed buffer is safe to refill, the other one is in-flight + if (_resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { + fillBuffer(completedBuf); // fill buffer, handles reset pulse and end of transfer + _lastFilled = (uint8_t)completedBuf; + } +#endif +} + +// I2sBus implementation +// the bus handling (begin/end/show/canShow) is provided by ParallelBusBase +I2sBus::I2sBus(int8_t pin, const LedTiming& timing, uint8_t busNum, uint8_t ledType) + : ParallelBusBase(pin, timing, busNum) +{ + // encoder is installed by createBus() from BusProperties + _ledType = 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..53eb6b55c0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -0,0 +1,155 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel I2S/LCD output driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" + +#ifdef WLEDPB_I2S_SUPPORT +#include "WLEDpixelBus_Parallel.h" // shared base classes for the parallel drivers + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) +#include "driver/periph_ctrl.h" +#else +#include "esp_private/periph_ctrl.h" // TODO: this may get removed in future IDF (V6 still has it), need a rewrite if that ever happens +#endif + +#include "esp_rom_gpio.h" + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + #include "esp_private/gdma.h" + #include "hal/dma_types.h" + #include "hal/gpio_hal.h" + #include "hal/lcd_ll.h" + #include "soc/lcd_cam_struct.h" + #include "soc/gpio_sig_map.h" + #include "soc/gdma_struct.h" // for global GDMA +#else + #include "soc/i2s_struct.h" + #include "soc/i2s_reg.h" + #include "rom/lldesc.h" + #include "esp_intr_alloc.h" +#endif + +namespace WLEDpixelBus { + +// I2S Parallel Bus - ESP32, ESP32-S2, ESP32-S3 + +// SOC_LCD_I80_BUSES: number of (I2S) peripherals that support the LCD Intel 8080 i.e. parallel output mode (ESP32: two, S2: one, S3: one) +// TODO: support both buses on ESP32? (currently only I2S_NUM_1 is used for LED output, I2S_NUM_0 is for AR) + +#define WLEDPB_I2S_BUS_COUNT SOC_LCD_I80_BUSES + +// note: 4-step cadence with 16 parallel outs requires 8 bytes per source bit or 192bytes per RGB LED, i.e. a 1k buffer can hold ~5 LEDs, ISR will fire every 144us +// I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is ok, saw some glitches on S2 so use 3 buffers for safety. +// Tripple buffering also works for 16-parallel output but may be tight under heavy traffic so using 4 is safer +#ifndef WLEDPB_I2S_DMA_BUFFER_COUNT + #ifdef WLED_PIXELBUS_16PARALLEL + #define WLEDPB_I2S_DMA_BUFFER_COUNT 4 // use 4 buffers in 16x parallel mode + #else + #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 // use triple buffering as RMT and WiFi eat up a lot of ISR time + #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 +class I2sBusContext : public ParallelBusContext { +public: + static I2sBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); + + bool startTransmit(); + +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); // 4-step cadence encoding + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + static IRAM_ATTR bool gdmaTxDoneISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); +#else + static void IRAM_ATTR dmaISR(void* arg); +#endif + void IRAM_ATTR _processEof(int8_t completedBuf); + + // Hardware abstraction + bool hwInit() override; + void hwDeinit() override; + void releaseResources() override; + void hwStartTransfer(); + void stopTransferHw() override { stopTransferHwIram(); } // base class hook, called from task context only + void IRAM_ATTR stopTransferHwIram(); // called from the DMA ISR + void channelAttached(int8_t idx, int8_t pin, bool inverted) override; + void channelDetached(int8_t idx, int8_t pin) override; + + // DMA descriptor abstraction +#ifdef CONFIG_IDF_TARGET_ESP32S3 + using DmaDesc_t = dma_descriptor_t; + static inline void descSetBuf(DmaDesc_t* d, uint8_t* b) { d->buffer = b; } + static inline void descSetSizeAndLen(DmaDesc_t* d, size_t len) { d->dw0.size = len; d->dw0.length = len; } + static inline void descSetLength(DmaDesc_t* d, size_t len) { d->dw0.length = len; } + static inline void descSetNext(DmaDesc_t* d, DmaDesc_t* n) { d->next = n; } + static inline DmaDesc_t* descGetNext(DmaDesc_t* d) { return d->next; } + static inline void descSetEof(DmaDesc_t* d) { d->dw0.suc_eof = 1; } + static inline void descSetOwnerDma(DmaDesc_t* d) { d->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; } + static inline bool descIsOwnerDma(DmaDesc_t* d) { return d->dw0.owner == DMA_DESCRIPTOR_BUFFER_OWNER_DMA; } +#else + using DmaDesc_t = lldesc_t; + static inline void descSetBuf(DmaDesc_t* d, uint8_t* b) { d->buf = b; } + static inline void descSetSizeAndLen(DmaDesc_t* d, size_t len) { d->size = len; d->length = len; } + static inline void descSetLength(DmaDesc_t* d, size_t len) { d->length = len; } + static inline void descSetNext(DmaDesc_t* d, DmaDesc_t* n) { d->qe.stqe_next = n; } + static inline DmaDesc_t* descGetNext(DmaDesc_t* d) { return d->qe.stqe_next; } + static inline void descSetEof(DmaDesc_t* d) { d->eof = 1; } + static inline void descSetOwnerDma(DmaDesc_t* d) { d->owner = 1; } + static inline bool descIsOwnerDma(DmaDesc_t* d) { return d->owner == 1; } +#endif + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + gdma_channel_handle_t _dmaChannel; + int8_t _dmaChanId; +#else + uint8_t _busNum; + i2s_dev_t* _i2sDev; + intr_handle_t _isrHandle; +#endif + + // DMA circular buffer chain + DmaDesc_t* _dmaDesc[WLEDPB_I2S_DMA_BUFFER_COUNT]; + uint8_t* _dmaBuffer[WLEDPB_I2S_DMA_BUFFER_COUNT]; + volatile uint8_t _lastFilled; // last DMA buffer that was refilled; next to refill is (_lastFilled + 1) % COUNT + + static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; +}; + +// I2S parallel output bus +// ------------------------- + +class I2sBus : public ParallelBusBase { +public: + I2sBus(int8_t pin, const LedTiming& timing, uint8_t busNum = 1, uint8_t ledType = 0); +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "I2S"; } +#endif +}; + +} // namespace WLEDpixelBus +#endif // WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.cpp new file mode 100644 index 0000000000..ae74eba17f --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.cpp @@ -0,0 +1,561 @@ +// WLEDpixelBus_PARLIO.cpp +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel PARLIO output driver implementation + +written by Damian Schneider @dedehai 2026 + +Note: PARLIO driver was generated with heavy help of AI with lots of refinement and testing +For a detailed description of how it works see the header file. + +H2, C61 and P4 are untested (only tested working on C5 and C6), P4 does not use the seamless-DMA mode (not supported by API) +P4 encodes the whole frame into one buffer and sends it with parlio_tx_unit_transmit() call - still needs testing on real P4 hardware + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_PARLIO.h" + +#ifdef WLEDPB_PARLIO_SUPPORT + +#include "soc/parlio_periph.h" // needed for hardware signal inversion using esp_rom_gpio_connect_out_signal() + +#if WLEDPB_PARLIO_SEAMLESS_DMA + #include "hal/parlio_ll.h" // TX engine low-level control (tx_bytelen, start, clock, idle value) + #include "hal/gdma_ll.h" // gdma_ll_tx_restart() - always-inline register write (includes soc/gdma_struct.h for the GDMA instance) + #include "soc/parl_io_struct.h" // PARL_IO register struct instance + //#include "esp_rom_gpio.h" + #define PARLIO_HW (&PARL_IO) // single PARLIO group/unit on all supported targets + // C6's parlio_ll.h defines PARLIO_LL_TX_MAX_BYTES_PER_FRAME (0xFFFF), the other targets only define the BITS variant (0x7FFFF) - the byte limit is the same 65535 everywhere + #ifndef PARLIO_LL_TX_MAX_BYTES_PER_FRAME + #define PARLIO_LL_TX_MAX_BYTES_PER_FRAME (PARLIO_LL_TX_MAX_BITS_PER_FRAME / 8) + #endif + + // C5 MP (and future chips with AHB GDMA v2+) expose the DMA controller as AHB_DMA and use ahb_dma_ll_* helpers, while C6/H2 keep the legacy GDMA naming + #if defined(SOC_AHB_GDMA_VERSION) && (SOC_AHB_GDMA_VERSION >= 2) + #define WLEDPB_PARLIO_GDMA_DEV (&AHB_DMA) + #define WLEDPB_PARLIO_GDMA_LL_TX_RESTART ahb_dma_ll_tx_restart + #else + #define WLEDPB_PARLIO_GDMA_DEV (&GDMA) + #define WLEDPB_PARLIO_GDMA_LL_TX_RESTART gdma_ll_tx_restart + #endif +#endif + +namespace WLEDpixelBus { + +ParlioBusContext* ParlioBusContext::_instances[WLEDPB_PARLIO_BUS_COUNT] = {nullptr}; +uint8_t ParlioBusContext::_refCount[WLEDPB_PARLIO_BUS_COUNT] = {0}; + +ParlioBusContext* ParlioBusContext::get(uint8_t busNum) { + if (busNum >= WLEDPB_PARLIO_BUS_COUNT) return nullptr; + + if (_instances[busNum] == nullptr) { + _instances[busNum] = new ParlioBusContext(busNum); + } + if (_instances[busNum] != nullptr) + _refCount[busNum]++; + return _instances[busNum]; +} + +void ParlioBusContext::release(uint8_t busNum) { + if (busNum >= WLEDPB_PARLIO_BUS_COUNT) return; + if (_refCount[busNum] == 0) return; + + _refCount[busNum]--; + if (_refCount[busNum] == 0 && _instances[busNum]) { + delete _instances[busNum]; + _instances[busNum] = nullptr; + } +} + +ParlioBusContext::ParlioBusContext(uint8_t /*busNum*/) + : _txUnit(nullptr) + , _unitEnabled(false) + , _unitStale(false) +#if WLEDPB_PARLIO_SEAMLESS_DMA + , _dmaChan(nullptr) + , _dmaChanIdx(-1) + , _desc(nullptr) + , _fillHead(0) + , _chunkBytesLeft(0) +#endif + , _outClockHz(0) + , _maxDataLen(0) +{ + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = nullptr; + _txLen[i] = 0; + _endOfFrame[i] = false; + } +} + +ParlioBusContext::~ParlioBusContext() { + deinit(); +} + +bool ParlioBusContext::hwInit() { + // PARLIO output clock: 4 clocks per LED bit (4-step cadence). + // Note: the PARLIO driver only has an INTEGER clock divider from the source clock (increases in steps of about 16ns) + uint32_t clkHz = calc4StepClockHz(_timing); + if (clkHz > 40000000UL) clkHz = 40000000UL; // limit output clock (40MHz max) TODO: check if this is the limit for all ESPs + _outClockHz = clkHz; + + // Channel sizes are not known until after init(), so defer unit creation until DMA buffers are sized. + if (_bufferSize == 0) return true; + + // Pins are fixed at unit creation; later channel changes mark the unit for recreation. + + // data GPIOs are fixed at creation; use GPIO_NUM_NC for unused data lines + parlio_tx_unit_config_t config; + memset(&config, 0, sizeof(config)); + config.clk_src = PARLIO_CLK_SRC_DEFAULT; // do NOT select XTAL explicitly, known C6 issue + config.data_width = WLEDPB_PARLIO_MAX_CHANNELS; + config.clk_in_gpio_num = GPIO_NUM_NC; // not used, TX only + config.valid_gpio_num = GPIO_NUM_NC; // valid signal would occupy the MSB data line + config.clk_out_gpio_num = GPIO_NUM_NC; // no external clock needed + for (int i = 0; i < WLEDPB_PARLIO_MAX_CHANNELS; i++) { + config.data_gpio_nums[i] = (_channels[i].active && _channels[i].pin >= 0) ? (gpio_num_t)_channels[i].pin : GPIO_NUM_NC; + } + config.output_clk_freq_hz = _outClockHz; + config.trans_queue_depth = WLEDPB_PARLIO_DMA_BUFFER_COUNT; + config.max_transfer_size = _bufferSize; // note: _allocDmaBuffers() must run before hwInit() + config.sample_edge = PARLIO_SAMPLE_EDGE_POS; + config.bit_pack_order = PARLIO_BIT_PACK_ORDER_MSB; + + esp_err_t err = parlio_new_tx_unit(&config, &_txUnit); + if (err != ESP_OK) { + _txUnit = nullptr; + return false; + } + + for (int i = 0; i < WLEDPB_PARLIO_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0 && _channels[i].inverted) { + esp_rom_gpio_connect_out_signal(_channels[i].pin, parlio_periph_signals.groups[0].tx_units[0].data_sigs[i], true, false); // invert the output using hardware + } + } + /* + // hardware inversion alternative, might work better on P4 (untested, needs gpio struct include for "GPIO") + for (int i = 0; i < WLEDPB_PARLIO_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0 && _channels[i].inverted) { + int pin = _channels[i].pin; + uint32_t sig = GPIO.func_out_sel_cfg[pin].func_sel; // signal the driver routed + esp_rom_gpio_connect_out_signal(pin, sig, true, false); // out_inv = true + } + }*/ + + parlio_tx_event_callbacks_t cbs; + memset(&cbs, 0, sizeof(cbs)); + cbs.on_trans_done = parlioTxDoneISR; + err = parlio_tx_unit_register_event_callbacks(_txUnit, &cbs, this); + if (err != ESP_OK) { + parlio_del_tx_unit(_txUnit); + _txUnit = nullptr; + return false; + } + +#if WLEDPB_PARLIO_SEAMLESS_DMA + if (!_initDmaRing()) { + parlio_del_tx_unit(_txUnit); + _txUnit = nullptr; + return false; + } +#endif + + _unitStale = false; + _unitEnabled = false; + return true; +} + +void ParlioBusContext::releaseResources() { +#if WLEDPB_PARLIO_SEAMLESS_DMA + if (_desc) { + heap_caps_free(_desc); // note: _dmaChan is owned by the PARLIO unit, it is deleted in hwDeinit() + _desc = nullptr; + } +#endif + + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + } +} + +void ParlioBusContext::hwDeinit() { + if (_txUnit) { + if (_unitEnabled) { + parlio_tx_unit_disable(_txUnit); + _unitEnabled = false; + } + parlio_del_tx_unit(_txUnit); + _txUnit = nullptr; +#if WLEDPB_PARLIO_SEAMLESS_DMA + _dmaChan = nullptr; // channel was owned by the unit +#endif + } +} + +// note: only called from deinit() (waits for idle first) and abortTransmit() (watchdog recovery) +void IRAM_ATTR ParlioBusContext::stopTransferHw() { +#if WLEDPB_PARLIO_SEAMLESS_DMA + if (_dmaChan) gdma_stop(_dmaChan); +#endif + if (_txUnit && _unitEnabled) { + parlio_tx_unit_disable(_txUnit); + _unitEnabled = false; + } +} + +// Watchdog recovery for a stalled transmission: stop the unit/DMA and return to idle, hardware is re-enabled by hwStartTransfer() +void ParlioBusContext::abortTransferHw() { + stopTransferHw(); +#if WLEDPB_PARLIO_SEAMLESS_DMA + _chunkBytesLeft = 0; +#endif +} + +// pins are fixed at PARLIO unit creation: no pin routing here, just mark the unit for recreation +void ParlioBusContext::channelAttached(int8_t /*idx*/, int8_t /*pin*/, bool /*inverted*/) { + if (_txUnit) _unitStale = true; +} + +void ParlioBusContext::channelDetached(int8_t /*idx*/, int8_t /*pin*/) { + if (_txUnit) _unitStale = true; // recreate unit without this pin on next transmit +} + +void ParlioBusContext::onChannelDataStaged(int8_t /*idx*/, size_t len) { + if (len > _maxDataLen) _maxDataLen = len; +} + +#if WLEDPB_PARLIO_SEAMLESS_DMA + +// Seamless streaming: unit's GDMA channel + our circular descriptor ring +bool ParlioBusContext::_initDmaRing() { + if (!_desc) { + _desc = (dma_descriptor_t*)heap_caps_aligned_alloc(4, WLEDPB_PARLIO_DMA_BUFFER_COUNT * sizeof(dma_descriptor_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); + if (!_desc) return false; + memset(_desc, 0, WLEDPB_PARLIO_DMA_BUFFER_COUNT * sizeof(dma_descriptor_t)); + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _desc[i].dw0.suc_eof = 1; // per-descriptor EOF interrupt (refill heartbeat); PARLIO ignores it (EOF is by byte counter) + _desc[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU; + _desc[i].buffer = _dmaBuffer[i]; + _desc[i].next = (i == WLEDPB_PARLIO_DMA_BUFFER_COUNT - 1) ? &_desc[0] : &_desc[i + 1]; + } + } + + // borrow the unit's GDMA channel (see WledPbParlioTxUnitHead): already connected to the PARLIO trigger + _dmaChan = WLEDPB_PARLIO_TX_DMA_CHAN(_txUnit); + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // IDF 5.5 configures the channel with owner_check=false / auto_update_desc=false, we need that changed for our seamless ring refill + gdma_strategy_config_t strategy = { + .owner_check = true, + .auto_update_desc = true, + .eof_till_data_popped = false, + }; + if (gdma_apply_strategy(_dmaChan, &strategy) != ESP_OK) { + _dmaChan = nullptr; + return false; + } + #endif + if (!_dmaChan) return false; + + _dmaChanIdx = -1; + int chanIdx = -1; + if (gdma_get_channel_id(_dmaChan, &chanIdx) == ESP_OK && + chanIdx >= 0 && chanIdx < SOC_GDMA_PAIRS_PER_GROUP_MAX) { + _dmaChanIdx = (int8_t)chanIdx; + } + + // the driver (v5.3-v5.5) never registers GDMA callbacks on this channel, they are ours + gdma_tx_event_callbacks_t cbs = {}; + cbs.on_trans_eof = gdmaEofISR; + if (gdma_register_tx_event_callbacks(_dmaChan, &cbs, this) != ESP_OK) { + _dmaChan = nullptr; + return false; + } + return true; +} + +void IRAM_ATTR ParlioBusContext::_armDescriptor(uint8_t idx) { + const uint32_t len = (uint32_t)_txLen[idx]; + _desc[idx].dw0.size = len; + _desc[idx].dw0.length = len; + _desc[idx].buffer = _dmaBuffer[idx]; + _desc[idx].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; // owner write last: hands the descriptor to the DMA +} + +IRAM_ATTR bool ParlioBusContext::gdmaEofISR(gdma_channel_handle_t /*dma_chan*/, gdma_event_data_t* /*edata*/, void* user_ctx) { + ParlioBusContext* ctx = (ParlioBusContext*)user_ctx; + if (ctx->_state != DriverState::Sending) return false; // stale wake-up between frames + + // refill every descriptor the DMA has handed back to the CPU + while (ctx->_state == DriverState::Sending) { + const uint8_t idx = ctx->_fillHead; + if (ctx->_desc[idx].dw0.owner != DMA_DESCRIPTOR_BUFFER_OWNER_CPU) break; // not consumed yet + if (ctx->_resetBytesLeft == WLEDPB_PARLIO_XFER_DONE_FLAG) break; // final buffer already queued, leave DMA stalled behind it + ctx->fillBuffer(idx); + ctx->_armDescriptor(idx); + ctx->_fillHead = (uint8_t)((idx + 1) % WLEDPB_PARLIO_DMA_BUFFER_COUNT); + } + + // If the DMA caught up with the CPU and paused on a CPU-owned descriptor, restart the outlink + if (ctx->_dmaChanIdx >= 0) { + WLEDPB_PARLIO_GDMA_LL_TX_RESTART(WLEDPB_PARLIO_GDMA_DEV, (uint32_t)ctx->_dmaChanIdx); + } + return false; +} + +void IRAM_ATTR ParlioBusContext::_processChunkEof() { + // Invoked from the IDF driver's PARLIO TX EOF ISR (via on_trans_done) + // at this point, tx_bytelen bytes have been clocked out. The driver ISR has already stopped the TX engine and clock + if (_state != DriverState::Sending) return; // stale TX EOF from a previous frame + if (_chunkBytesLeft == 0) { + _state = DriverState::Idle; // whole frame clocked out, engine stays off until the next frame + return; + } + + // Frame longer than the 16-bit tx_bytelen register: seamlessly chain the next chunk + const uint32_t chunk = _chunkBytesLeft > PARLIO_LL_TX_MAX_BYTES_PER_FRAME ? PARLIO_LL_TX_MAX_BYTES_PER_FRAME : _chunkBytesLeft; + _chunkBytesLeft -= chunk; + parlio_ll_tx_set_trans_bit_len(PARLIO_HW, chunk * 8); + uint32_t wait = 100000; // FIFO still holds DMA'd data; bounded wait for safety + while (!parlio_ll_tx_is_ready(PARLIO_HW) && --wait) {} + parlio_ll_tx_start(PARLIO_HW, true); + parlio_ll_tx_enable_clock(PARLIO_HW, true); +} + +#else + +// Fallback: single full-frame buffer, one transaction per frame +bool IRAM_ATTR ParlioBusContext::_queueBuffer(uint8_t bufIdx) { + parlio_transmit_config_t trans_config = { + .idle_value = 0 // GPIO matrix inversion applies to the idle level too + }; + // note: payload size is in BITS, not bytes + return parlio_tx_unit_transmit(_txUnit, _dmaBuffer[bufIdx], _txLen[bufIdx] * 8, &trans_config) == ESP_OK; +} +#endif // WLEDPB_PARLIO_SEAMLESS_DMA + +bool ParlioBusContext::_allocDmaBuffers() { + if (_dmaAllocated) return true; + auto rollback = [&]() { + for (uint8_t i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; ++i) { + if (_dmaBuffer[i]) heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + _bufferSize = 0; + }; + +#if WLEDPB_PARLIO_SEAMLESS_DMA + _bufferSize = calc4StepBufferSize(_maxSrcBytes, WLEDPB_PARLIO_DMABYTES, + WLEDPB_PARLIO_DMA_BUFFER_COUNT, MIN_DMA_BUFFER_SIZE, DEFAULT_DMA_BUFFER_SIZE); + if (_bufferSize == 0) return false; + for (uint8_t i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; ++i) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { rollback(); return false; } + memset(_dmaBuffer[i], 0, _bufferSize); + } +#else + // The transaction fallback needs one gap-free buffer for the complete frame. + _bufferSize = (size_t)WLEDPB_PARLIO_DMABYTES * _maxSrcBytes + _calcResetBytes(); + _bufferSize = (_bufferSize + 3) & ~size_t(3); + if (_bufferSize == 0) return false; + _dmaBuffer[0] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[0]) { rollback(); return false; } + memset(_dmaBuffer[0], 0, _bufferSize); +#endif + _dmaAllocated = true; + return true; +} + +// encode4Step: S2/S3 layout, with steps [HIGH,data,data,LOW]. +void IRAM_ATTR ParlioBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { + encode4Step8(dest, destLen, maxChannel, _channels); +} + +// bytes of reset period appended at the end of a frame +uint32_t IRAM_ATTR ParlioBusContext::_calcResetBytes() const { + // one LED bit cycle is 4 clocks, 1 buffer byte per clock + return (uint32_t)calc4StepResetDmaBytes(_timing, WLEDPB_PARLIO_DMABYTES / 8); +} + +void IRAM_ATTR __attribute__((noinline)) ParlioBusContext::fillBuffer(uint8_t bufIdx) { + uint32_t* buf32 = (uint32_t*)_dmaBuffer[bufIdx]; + const uint32_t* end = buf32 + (_bufferSize >> 2); + while (buf32 < end) *buf32++ = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned. Note: memset is not IRAM safe and may crash + + _endOfFrame[bufIdx] = false; + + if (_resetBytesLeft > 0 && _resetBytesLeft != WLEDPB_PARLIO_XFER_DONE_FLAG) { + // reset pulse continuation: this buffer is pure reset level. Long reset periods span several buffers; only the last one is flagged as end of frame. Never let _txLen + if (_resetBytesLeft <= _bufferSize) { + _txLen[bufIdx] = _resetBytesLeft; + _resetBytesLeft = WLEDPB_PARLIO_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers + _endOfFrame[bufIdx] = true; + } else { + _txLen[bufIdx] = _bufferSize; + _resetBytesLeft -= _bufferSize; // more pure-reset buffers to come (stays a multiple of 4) + } + return; // nothing to encode, keep lines at reset level + } + + uint32_t bytesToEncode = 0; + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_PARLIO_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + maxCh = ch + 1; + uint32_t channelBytesLeft = _channels[ch].srcLen - _channels[ch].srcPos; + if (channelBytesLeft > bytesToEncode) bytesToEncode = channelBytesLeft; + } + } + + size_t translatedBytes = bytesToEncode * WLEDPB_PARLIO_DMABYTES; + if (translatedBytes > _bufferSize) translatedBytes = _bufferSize; + translatedBytes -= translatedBytes % WLEDPB_PARLIO_DMABYTES; + encode4Step(_dmaBuffer[bufIdx], translatedBytes, maxCh); + _txLen[bufIdx] = translatedBytes; + + if (translatedBytes < _bufferSize) { + // Data ran out before the buffer was full (i.e. we are done), compute the minimum reset period we must send + size_t resetBytes = _calcResetBytes(); + + size_t newLen = translatedBytes + resetBytes; + if (newLen > _bufferSize) { + _resetBytesLeft = newLen - _bufferSize; // reset pulse does not fit into this buffer frame, send another one (see above) + _txLen[bufIdx] = _bufferSize; + } + else { + _txLen[bufIdx] = newLen; // send the rest (reset level) as a reset + _endOfFrame[bufIdx] = true; + _resetBytesLeft = WLEDPB_PARLIO_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers + } + } +} + +bool ParlioBusContext::startTransmit() { + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; + + // Only start transmission if ALL active channels have populated data + if (_stagedMask != _channelMask) return true; + _stagedMask = 0; // Reset for next frame + + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_PARLIO_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + _resetBytesLeft = 0; + _txStartMillis = millis(); + +#if !WLEDPB_PARLIO_SEAMLESS_DMA + // The fallback transmits one buffer + const size_t requiredBytes = (_maxDataLen * WLEDPB_PARLIO_DMABYTES + _calcResetBytes() + 3) & ~size_t(3); + if (_dmaAllocated && requiredBytes > _bufferSize) { + hwDeinit(); + heap_caps_free(_dmaBuffer[0]); + _dmaBuffer[0] = nullptr; + _bufferSize = 0; + _dmaAllocated = false; + } +#endif + if (!_dmaAllocated && !_allocDmaBuffers()) return false; + + // (re)create the PARLIO unit on first transmit, or when channels changed + if (!_txUnit || _unitStale) { + hwDeinit(); + if (!hwInit()) return false; + } + +#if !WLEDPB_PARLIO_SEAMLESS_DMA + _state = DriverState::Sending; // fallback: EOF callbacks do not gate on the state +#endif + // seamless: _state is set to Sending at the end of hwStartTransfer(), after the ring/ is fully built and the engine is running - until then EOF callbacks stay disabled + + if (!hwStartTransfer()) { + _state = DriverState::Idle; + return false; + } + + return true; +} + +bool ParlioBusContext::hwStartTransfer() { + if (!_unitEnabled) { + if (parlio_tx_unit_enable(_txUnit) != ESP_OK) return false; + _unitEnabled = true; + } + +#if WLEDPB_PARLIO_SEAMLESS_DMA + // one hardware transaction for the whole frame (data + reset period) + const uint32_t totalBytes = (uint32_t)_maxDataLen * WLEDPB_PARLIO_DMABYTES + _calcResetBytes(); + const uint32_t firstChunk = totalBytes > PARLIO_LL_TX_MAX_BYTES_PER_FRAME ? PARLIO_LL_TX_MAX_BYTES_PER_FRAME : totalBytes; + _chunkBytesLeft = totalBytes - firstChunk; + + // reset the ring: DMA stopped, all descriptors back to CPU, refill from scratch. + // note: _state is still Idle here, so a stale pending GDMA/PARLIO EOF interrupt is ignored by the callbacks while we rebuild (see gdmaEofISR) + gdma_stop(_dmaChan); + _fillHead = 0; + for (int i = 0; i < WLEDPB_PARLIO_DMA_BUFFER_COUNT; i++) { + _desc[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU; + fillBuffer(i); + _armDescriptor(i); + _fillHead = (uint8_t)((i + 1) % WLEDPB_PARLIO_DMA_BUFFER_COUNT); + if (_endOfFrame[i]) break; // frame fits into the first buffers, rest of the ring stays disarmed + } + + // mirror the IDF driver's transaction start sequence, minus the descriptor remount (the ring stays linked) + parlio_ll_tx_set_idle_data_value(PARLIO_HW, 0); // GPIO matrix inversion applies to the idle level too + parlio_ll_tx_reset_fifo(PARLIO_HW); + parlio_ll_tx_reset_clock(PARLIO_HW); + parlio_ll_tx_set_trans_bit_len(PARLIO_HW, firstChunk * 8); + gdma_start(_dmaChan, (intptr_t)&_desc[0]); + // wait until the first DMA data reached the TX FIFO (same handshake as the IDF driver) + uint32_t wait = 1000000; + while (!parlio_ll_tx_is_ready(PARLIO_HW) && --wait) {} + if (!wait) return false; + parlio_ll_tx_start(PARLIO_HW, true); + parlio_ll_tx_enable_clock(PARLIO_HW, true); + // open the refill window only now: the first EOF can raise one buffer-duration from here at the earliest, and the ring is fully built + _state = DriverState::Sending; + return true; +#else + // Single-buffer fallback: fill buffer 0 with the whole frame + fillBuffer(0); + return _queueBuffer(0); +#endif +} + +IRAM_ATTR bool ParlioBusContext::parlioTxDoneISR(parlio_tx_unit_handle_t tx_unit, const parlio_tx_done_event_data_t* edata, void* user_ctx) { + (void)tx_unit; + (void)edata; + ParlioBusContext* ctx = (ParlioBusContext*)user_ctx; +#if WLEDPB_PARLIO_SEAMLESS_DMA + ctx->_processChunkEof(); +#else + ctx->_state = DriverState::Idle; // single transaction per frame: transmit-done = frame complete +#endif + return false; +} + +// ParlioBus implementation +// ============================================ + +// ParlioBus implementation +// the bus handling (begin/end/show/canShow) is provided by ParallelBusBase +ParlioBus::ParlioBus(int8_t pin, const LedTiming& timing, uint8_t busNum, uint8_t ledType) + : ParallelBusBase(pin, timing, busNum) +{ + // encoder is installed by createBus() from BusProperties + _ledType = ledType; +} + +} // namespace WLEDpixelBus +#endif // WLEDPB_PARLIO_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.h new file mode 100644 index 0000000000..f22f777b8b --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_PARLIO.h @@ -0,0 +1,155 @@ +// WLEDpixelBus_PARLIO.h +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel PARLIO output driver implementation + +written by Damian Schneider @dedehai 2026 + +Note: PARLIO driver was generated with heavy help of an AI with lots of refinement and testing (works but needs a thorough review) + +Supports ESP32-C6, ESP32-H2 and ESP32-C5 via the PARLIO TX peripheral (8 parallel outputs). +Each bus can have individual configuration of color channels but all must share the same timing. +ESP32-P4 has a 16-bit wide PARLIO unit, this driver uses 8 bits of it (could be extended). +Requires IDF >= 5.3 (driver/parlio_tx.h). Not available on ESP32, S2, S3, C3, C2. + +Architecture mirrors the I2S driver (WLEDpixelBus_I2S): data is streamed from a circular +GDMA descriptor ring that we own, refilled on the fly from per-descriptor EOF interrupts. +This seamless transmission avoids the ~100us pauses that the stock IDF driver has. +The IDF driver is still used for unit creation (clock setup, GPIO matrix routing, interrupt install) + +Fallback (WLEDPB_PARLIO_SEAMLESS_DMA == 0, e.g. ESP32-P4 with SOC_PARLIO_TX_SIZE_BY_DMA or non-AHB GDMA): +the whole frame (data + reset period) is encoded into a single buffer up front and uses the IDF driver to send it. + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#ifdef ARDUINO_ARCH_ESP32 +#include +#include "soc/soc_caps.h" +#endif + +#ifdef SOC_PARLIO_SUPPORTED +#define WLEDPB_PARLIO_SUPPORT +#include "WLEDpixelBus_Parallel.h" // shared base classes for the parallel drivers + +#include "driver/parlio_tx.h" +#include "driver/gpio.h" + +#if defined(SOC_AHB_GDMA_SUPPORTED) && SOC_AHB_GDMA_SUPPORTED && !(defined(SOC_PARLIO_TX_SIZE_BY_DMA) && SOC_PARLIO_TX_SIZE_BY_DMA) && !defined(CONFIG_IDF_TARGET_ESP32P4) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + #define WLEDPB_PARLIO_SEAMLESS_DMA 1 +#else + #define WLEDPB_PARLIO_SEAMLESS_DMA 0 // fall back to "single buffer transfer" mode (uses more RAM) +#endif + +#if WLEDPB_PARLIO_SEAMLESS_DMA + #include "hal/dma_types.h" // dma_descriptor_t, DMA_DESCRIPTOR_BUFFER_* + #include "esp_private/gdma.h" // gdma_start/stop/append, gdma_register_tx_event_callbacks + +// Mirror of the beginning of the IDF driver's private struct parlio_tx_unit_t (parlio_tx.c, parlio_priv.h since v5.5) +// up to the dma_chan field which we need to reconfigure the DMA. In V6 it changed again but not before dma_chan, so the mirror is still valid. +typedef struct { + int unit_id; + int dir; + void* group; + size_t data_width; + gpio_num_t data_gpio_nums[SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH]; + gpio_num_t valid_gpio_num; + gpio_num_t clk_out_gpio_num; + gpio_num_t clk_in_gpio_num; + void* intr; + void* pm_lock; + gdma_channel_handle_t dma_chan; +} WledPbParlioTxUnitHead; + +#define WLEDPB_PARLIO_TX_DMA_CHAN(unit) (((const WledPbParlioTxUnitHead*)(unit))->dma_chan) +#endif + +namespace WLEDpixelBus { + +#define WLEDPB_PARLIO_BUS_COUNT 1 // only one PARLIO TX unit per chip (note: busNum in constructor is always 0 for now, could remove that parameter) + +#ifndef WLEDPB_PARLIO_DMA_BUFFER_COUNT + #define WLEDPB_PARLIO_DMA_BUFFER_COUNT 3 // 3 buffers in a ring configuration seems to work well even when using RMT +#endif + + +#define WLEDPB_PARLIO_MAX_CHANNELS 8 // PARLIO TX on C6/H2/C5 supports up to 8 data lines TODO: use SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH ? +#define WLEDPB_PARLIO_DMABYTES 32 // 32 bytes per pixel byte (4 clocks per bit, 1 byte per clock) +#define WLEDPB_PARLIO_XFER_DONE_FLAG 3 // flag to indicate end of transfer, must NOT be a multiple of 4 + +// PARLIO bus context - manages shared PARLIO unit for parallel output +class ParlioBusContext : public ParallelBusContext { +public: + static ParlioBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); + + bool startTransmit(); + +private: + ParlioBusContext(uint8_t busNum); + ~ParlioBusContext(); + + void IRAM_ATTR fillBuffer(uint8_t bufIdx); + bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); // 4-step cadence encoding, applies inversion via XOR + uint32_t IRAM_ATTR _calcResetBytes() const; // bytes of reset period appended at the end of a frame (called from ISR context) + + static IRAM_ATTR bool parlioTxDoneISR(parlio_tx_unit_handle_t tx_unit, const parlio_tx_done_event_data_t* edata, void* user_ctx); + +#if WLEDPB_PARLIO_SEAMLESS_DMA + bool _initDmaRing(); // descriptor ring (once) + borrow the unit's GDMA channel + refill callback + void IRAM_ATTR _armDescriptor(uint8_t idx); // hand a filled buffer's descriptor to the DMA + void IRAM_ATTR _processChunkEof(); // PARLIO TX EOF: chain next byte-count chunk or finish frame + static IRAM_ATTR bool gdmaEofISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* edata, void* user_ctx); +#else + bool IRAM_ATTR _queueBuffer(uint8_t bufIdx); // queue the (single) full-frame buffer as one PARLIO transaction +#endif + + bool hwInit() override; // creates the PARLIO TX unit with the currently registered pins + void hwDeinit() override; + void releaseResources() override; + bool hwStartTransfer(); // enables the unit and starts the frame + void IRAM_ATTR stopTransferHw() override; + void abortTransferHw() override; + void channelAttached(int8_t idx, int8_t pin, bool inverted) override; + void channelDetached(int8_t idx, int8_t pin) override; + void onChannelDataStaged(int8_t idx, size_t len) override; + + parlio_tx_unit_handle_t _txUnit; + bool _unitEnabled; + bool _unitStale; // channels changed after unit creation, recreate on next transmit + + uint8_t* _dmaBuffer[WLEDPB_PARLIO_DMA_BUFFER_COUNT]; + volatile size_t _txLen[WLEDPB_PARLIO_DMA_BUFFER_COUNT]; // bytes per buffer + volatile bool _endOfFrame[WLEDPB_PARLIO_DMA_BUFFER_COUNT]; // last buffer of the frame + +#if WLEDPB_PARLIO_SEAMLESS_DMA + // GDMA channel borrowed from the PARLIO unit (owned and deleted by the IDF driver) + our circular descriptor ring + gdma_channel_handle_t _dmaChan; + int8_t _dmaChanIdx; // hardware channel index of _dmaChan for direct LL register access (-1 = unknown) + dma_descriptor_t* _desc; + volatile uint8_t _fillHead; // ring index of the next descriptor to refill + volatile uint32_t _chunkBytesLeft; // frame bytes not yet covered by a programmed tx_bytelen chunk +#endif + + uint32_t _outClockHz; // PARLIO output clock = 4 clocks per LED bit + size_t _maxDataLen; // longest source byte count of this frame, drives the frame length + + static ParlioBusContext* _instances[WLEDPB_PARLIO_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_PARLIO_BUS_COUNT]; +}; + +// PARLIO parallel output bus + +class ParlioBus : public ParallelBusBase { +public: + ParlioBus(int8_t pin, const LedTiming& timing, uint8_t busNum = 0, uint8_t ledType = 0); +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "PARLIO"; } +#endif +}; + +} // namespace WLEDpixelBus +#endif // WLEDPB_PARLIO_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Parallel.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Parallel.h new file mode 100644 index 0000000000..aa3ef4d31b --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Parallel.h @@ -0,0 +1,443 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - shared base classes and helpers for the parallel output drivers + +written by Damian Schneider @dedehai 2026 + +The parallel output drivers (I2S/LCD, PARLIO and parallel SPI) all use the same +architecture: one shared peripheral context owns a ring of DMA buffers and one +"channel" per output pin, and every bus of the same kind shares that context. + +This file holds everything the three of them have in common: + + * ParallelChannel - the per-pin record (was a per-driver "ChannelData" with + three different field orders) + * ParallelBusContext - channel registry, staging mask, transfer state machine + and the stall watchdog + * ParallelBusBase - the PixelBus half (begin/end/show/canShow) + * the 4-step cadence encoder and the DMA buffer sizing helpers + +Everything peripheral specific stays in the driver: GPIO routing, the DMA +descriptor layout, the interrupt handlers and the actual transfer start. + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +// One output pin of a parallel bus. +// Initialize with ParallelChannel{} and set fields explicitly; do not rely on +// the field order. +struct ParallelChannel { + int8_t pin = -1; + const uint8_t* srcData = nullptr; + size_t srcLen = 0; + size_t srcPos = 0; + bool active = false; + bool inverted = false; +}; + +// Base class for the parallel output contexts. +// +// MaxChannels is a template parameter so that the channel array is sized +// exactly as the individual driver needs it (8/16 for I2S, 8 for PARLIO, +// 4 for parallel SPI) - no RAM is wasted and no runtime lookup is needed. +template +class ParallelBusContext { +public: + virtual ~ParallelBusContext() {} + + int8_t registerChannel(int8_t pin, size_t srcBytes, bool inverted = false); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + + bool isIdle() const { return _state == DriverState::Idle; } + virtual bool ensureIdle(); // non-blocking completion check with timeout recovery + void waitForIdle(); // bounded wait; aborts a stalled transfer before returning + void abortTransmit(); // abort on timeout (just in case to avoid driver getting stuck) + + bool init(const LedTiming& timing); + void deinit(); + +protected: + // --- hooks the driver has to provide ------------------------------------ + virtual bool hwInit() = 0; // set up the peripheral, _timing is already valid + virtual void hwDeinit() = 0; // release the peripheral + virtual void releaseResources() {} // free the driver owned DMA buffers/descriptors + virtual void stopTransferHw() {} // stop an in-flight transfer + virtual void abortTransferHw() { stopTransferHw(); } // stop an in-flight transfer + virtual void channelAttached(int8_t idx, int8_t pin, bool inverted) = 0; // route the pin + virtual void channelDetached(int8_t idx, int8_t pin) {} // release the pin + virtual void onChannelDataStaged(int8_t idx, size_t len) {} // driver bookkeeping + + // --- shared state -------------------------------------------------------- + ParallelChannel _channels[MaxChannels]; + uint8_t _channelCount = 0; + uint16_t _channelMask = 0; + uint16_t _stagedMask = 0; + size_t _maxSrcBytes = 0; // largest source byte count across all channels, drives DMA sizing + size_t _bufferSize = 0; // actual allocated DMA buffer size (per buffer) + bool _dmaAllocated = false; + bool _initialized = false; + volatile DriverState _state = DriverState::Idle; + volatile uint32_t _txStartMillis = 0; // millis() when the current transfer started, watchdog + volatile size_t _resetBytesLeft = 0; + LedTiming _timing = LedTiming(300, 900, 700, 500, 300); // overwritten by init() +}; + +// ---------------------------------------------------------------------------- +// ParallelBusContext implementation +// ---------------------------------------------------------------------------- + +template +int8_t ParallelBusContext::registerChannel(int8_t pin, size_t srcBytes, bool inverted) { + // find a free slot + int8_t idx = -1; + for (uint8_t i = 0; i < MaxChannels; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + if (idx < 0) return -1; + + _channels[idx].pin = pin; + _channels[idx].active = true; + _channels[idx].inverted = inverted; + _channelCount++; + _channelMask |= (1u << idx); + + // track the largest source byte count across channels; used to size the DMA buffers + if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; + + channelAttached(idx, pin, inverted); // driver specific pin routing + + return idx; +} + +template +void ParallelBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= MaxChannels) return; + if (!_channels[channelIdx].active) return; + + channelDetached(channelIdx, _channels[channelIdx].pin); // driver specific pin release + + _channels[channelIdx] = ParallelChannel{}; + _channelCount--; + _channelMask &= ~(1u << channelIdx); + _stagedMask &= ~(1u << channelIdx); +} + +template +void ParallelBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= MaxChannels) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + onChannelDataStaged(channelIdx, len); // driver specific bookkeeping + + // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() + if (_stagedMask & (1u << channelIdx)) _stagedMask = 0; // reset the stage mask to resync (should not happen) + _stagedMask |= (1u << channelIdx); +} + +template +bool ParallelBusContext::ensureIdle() { + if (isIdle()) return true; + if ((uint32_t)(millis() - _txStartMillis) <= WLEDPB_TRANSFER_TIMEOUT_MS) return false; + abortTransmit(); + return true; +} + +template +void ParallelBusContext::waitForIdle() { + const uint32_t start = millis(); + while (!ensureIdle() && (uint32_t)(millis() - start) <= WLEDPB_TRANSFER_TIMEOUT_MS) { + vTaskDelay(1); + } + if (!isIdle()) abortTransmit(); +} + +template +void ParallelBusContext::abortTransmit() { + abortTransferHw(); // driver specific: stop the DMA and release the pins + _state = DriverState::Idle; + _stagedMask = 0; + _resetBytesLeft = 0; +} + +template +bool ParallelBusContext::init(const LedTiming& timing) { + if (_initialized) return true; + + _timing = timing; + if (!hwInit()) { + deinit(); + return false; + } + _initialized = true; + return true; +} + +template +void ParallelBusContext::deinit() { + waitForIdle(); + abortTransmit(); // stops the hardware and releases the pins + releaseResources(); // frees the driver owned DMA buffers/descriptors + hwDeinit(); // releases the peripheral + _dmaAllocated = false; + _initialized = false; +} + +// ---------------------------------------------------------------------------- +// ParallelBusBase - the PixelBus half, shared by all parallel drivers +// ---------------------------------------------------------------------------- + +template +class ParallelBusBase : public PixelBus { +public: + ~ParallelBusBase() override { end(); } + + bool begin() override { + if (_initialized) return true; + + _ctx = Ctx::get(_busNum); + if (!_ctx) return false; + + if (!_ctx->init(_timing)) { + Ctx::release(_busNum); + _ctx = nullptr; + return false; + } + + // pass our encoded byte count so the context can size the DMA buffers for the largest bus + const size_t srcBytes = (size_t)_numPixels * _encoder.getBytesPerPixel() + _prefixLen + _suffixLen; + _channelIdx = _ctx->registerChannel(_pin, srcBytes, _inverted); + if (_channelIdx < 0) { + Ctx::release(_busNum); + _ctx = nullptr; + return false; + } + + _initialized = true; + if (!allocateEncodeBuffer(_numPixels, _encoder.getBytesPerPixel())) { end(); return false; } + return true; + } + + void end() override { + if (_ctx) { + _ctx->waitForIdle(); + _ctx->unregisterChannel(_channelIdx); + Ctx::release(_busNum); + _ctx = nullptr; + } + releaseEncodeBuffer(); + _initialized = false; + } + + bool show() override { + if (!_initialized || !_ctx || !_encodeBuffer || _numPixels == 0) return false; + _ctx->waitForIdle(); + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodeBufferSize); + return _ctx->startTransmit(); + } + + bool canShow() const override { + if (!_initialized) return true; // do not block if init failed! busmanager waits until all buses can show + return _ctx && _ctx->ensureIdle(); + } + +protected: + // note: a mem-initializer may not name a base class member, so the derived + // classes pass their configuration through this constructor + ParallelBusBase(int8_t pin, const LedTiming& timing, uint8_t busNum) + : _pin(pin) + , _timing(timing) + , _busNum(busNum) + {} + + int8_t _pin = -1; + LedTiming _timing = LedTiming(300, 900, 700, 500, 300); // overwritten by the constructor + bool _initialized = false; + uint8_t _busNum = 0; + int8_t _channelIdx = -1; + Ctx* _ctx = nullptr; +}; + +// ---------------------------------------------------------------------------- +// 4-step cadence helpers for the parallel buses (I2S, PARLIO, parallel SPI) +// ---------------------------------------------------------------------------- + +// Compute target clock frequency in Hz for 4-step cadence (4 clock cycles per bit period). +inline uint32_t calc4StepClockHz(const LedTiming& timing) { + uint32_t period = timing.bitPeriod(); + if (period == 0) period = 1250; + return 4000000000UL / period; +} + +// Compute number of DMA bytes for the reset period appended to a frame. +// dmaBytesPerLedBit = bytes consumed in DMA for one full 4-step LED bit i.e. one bitPeriod (called from ISR context) +inline __attribute__((always_inline)) size_t calc4StepResetDmaBytes(const LedTiming& timing, uint8_t dmaBytesPerLedBit) { + uint32_t resetNs = timing.reset_us * 1000; + uint32_t bitPeriodNs = timing.bitPeriod() + 1; // +1 to prevent div by zero + uint32_t zeroCycles = resetNs / bitPeriodNs; + return (size_t)zeroCycles * dmaBytesPerLedBit; +} + +// Compute a DMA buffer size aligned to 4 bytes, clamped to bounds and source-byte groups +inline size_t calc4StepBufferSize(size_t maxSrcBytes, uint8_t dmaBytesPerSrcByte, uint8_t bufCount, size_t minSize, size_t maxSize) { + if (bufCount == 0) bufCount = 1; + size_t sz = (dmaBytesPerSrcByte * maxSrcBytes) / bufCount; + sz = (sz + 3) & ~3; // 4-byte align + if (sz > maxSize) sz = maxSize; + if (sz < minSize) sz = minSize; + // note: dmaBytesPerSrcByte must be a multiple of 4 for proper alignment (true for current 4-step encoding: its 16, 32 or 64 bytes) + sz -= sz % dmaBytesPerSrcByte; // align to src byte groups (see xxx_DMABYTES) + return sz; +} + +// 4-step cadence encoder: expands each source byte into 4 DMA "steps" of 8 channels. +// '0' is 0b1000 (1/4 high), '1' is 0b1110 (3/4 high). +// +// Layout: +// Standard - no swap, memory [S0,S1,S2,S3] = [HIGH,data,data,LOW] +// WordSwapped - half-word swap, memory [S2,S3,S0,S1] = [data,0,HIGH,data] +enum class Step4Layout : uint8_t { Standard, WordSwapped }; + +template +inline __attribute__((always_inline)) void encode4Step8(uint8_t* dest, size_t destLen, uint8_t maxChannel, ParallelChannel* channels) { + for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { + MASK alwaysMask = 0; + MASK b0 = 0, b1 = 0, b2 = 0, b3 = 0; + MASK b4 = 0, b5 = 0, b6 = 0, b7 = 0; + + for (uint8_t ch = 0; ch < maxChannel; ch++) { + if (!channels[ch].active) continue; + if (channels[ch].srcPos >= channels[ch].srcLen) continue; + const MASK m = (MASK)(1u << ch); + alwaysMask |= m; + const uint8_t b = channels[ch].srcData[channels[ch].srcPos++]; + b0 |= m & (MASK)(0u - ((b >> 7) & 1u)); + b1 |= m & (MASK)(0u - ((b >> 6) & 1u)); + b2 |= m & (MASK)(0u - ((b >> 5) & 1u)); + b3 |= m & (MASK)(0u - ((b >> 4) & 1u)); + b4 |= m & (MASK)(0u - ((b >> 3) & 1u)); + b5 |= m & (MASK)(0u - ((b >> 2) & 1u)); + b6 |= m & (MASK)(0u - ((b >> 1) & 1u)); + b7 |= m & (MASK)(0u - ((b >> 0) & 1u)); + } + if (!alwaysMask) break; // no active channels produced data + + uint32_t* p = (uint32_t*)(dest + pos); + if constexpr (Layout == Step4Layout::Standard) { + // no swap, memory [S0,S1,S2,S3] = [HIGH,data,data,LOW] + p[0] = (uint32_t)(alwaysMask) | ((uint32_t)(b0) << 8) | ((uint32_t)(b0) << 16); + p[1] = (uint32_t)(alwaysMask) | ((uint32_t)(b1) << 8) | ((uint32_t)(b1) << 16); + p[2] = (uint32_t)(alwaysMask) | ((uint32_t)(b2) << 8) | ((uint32_t)(b2) << 16); + p[3] = (uint32_t)(alwaysMask) | ((uint32_t)(b3) << 8) | ((uint32_t)(b3) << 16); + p[4] = (uint32_t)(alwaysMask) | ((uint32_t)(b4) << 8) | ((uint32_t)(b4) << 16); + p[5] = (uint32_t)(alwaysMask) | ((uint32_t)(b5) << 8) | ((uint32_t)(b5) << 16); + p[6] = (uint32_t)(alwaysMask) | ((uint32_t)(b6) << 8) | ((uint32_t)(b6) << 16); + p[7] = (uint32_t)(alwaysMask) | ((uint32_t)(b7) << 8) | ((uint32_t)(b7) << 16); + } else { + // half-word swap, memory [S2,S3,S0,S1] = [data,0,HIGH,data] (ESP32 classic) + const uint32_t AH = (uint32_t)alwaysMask << 16; + p[0] = AH | (uint32_t)(b0) | ((uint32_t)(b0) << 24); + p[1] = AH | (uint32_t)(b1) | ((uint32_t)(b1) << 24); + p[2] = AH | (uint32_t)(b2) | ((uint32_t)(b2) << 24); + p[3] = AH | (uint32_t)(b3) | ((uint32_t)(b3) << 24); + p[4] = AH | (uint32_t)(b4) | ((uint32_t)(b4) << 24); + p[5] = AH | (uint32_t)(b5) | ((uint32_t)(b5) << 24); + p[6] = AH | (uint32_t)(b6) | ((uint32_t)(b6) << 24); + p[7] = AH | (uint32_t)(b7) | ((uint32_t)(b7) << 24); + } + } +} + +// Encode 16 parallel channels using the target-specific 4-step DMA word layout +template +inline __attribute__((always_inline)) void encode4Step16(uint8_t* dest, size_t destLen, uint8_t maxChannel, ParallelChannel* channels) { + for (size_t pos = 0; pos + 64 <= destLen; pos += 64) { + uint16_t alwaysMask = 0; + uint16_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; + uint16_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; + + for (unsigned ch = 0; ch < maxChannel; ch++) { + ParallelChannel& c = channels[ch]; + if (!c.active || c.srcPos >= c.srcLen) continue; + const uint16_t m = (uint16_t)(1u << ch); + alwaysMask |= m; + const uint8_t b = c.srcData[c.srcPos++]; + 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; + + uint32_t* p = (uint32_t*)(dest + pos); + if constexpr (Layout == Step4Layout::Standard) { + // no swap: word 0 = [S0=HIGH, S1=data], word 1 = [S2=data, S3=LOW] + p[0] = ((uint32_t)b0 << 16) | (uint32_t)alwaysMask; p[1] = (uint32_t)b0; + p[2] = ((uint32_t)b1 << 16) | (uint32_t)alwaysMask; p[3] = (uint32_t)b1; + p[4] = ((uint32_t)b2 << 16) | (uint32_t)alwaysMask; p[5] = (uint32_t)b2; + p[6] = ((uint32_t)b3 << 16) | (uint32_t)alwaysMask; p[7] = (uint32_t)b3; + p[8] = ((uint32_t)b4 << 16) | (uint32_t)alwaysMask; p[9] = (uint32_t)b4; + p[10] = ((uint32_t)b5 << 16) | (uint32_t)alwaysMask; p[11] = (uint32_t)b5; + p[12] = ((uint32_t)b6 << 16) | (uint32_t)alwaysMask; p[13] = (uint32_t)b6; + p[14] = ((uint32_t)b7 << 16) | (uint32_t)alwaysMask; p[15] = (uint32_t)b7; + } else { + // half-word swap (classic ESP32): word 0 = [HIGH in upper, data in lower], word 1 = [data in upper, 0] + const uint32_t ah = (uint32_t)alwaysMask << 16; + p[0] = ah | (uint32_t)b0; p[1] = (uint32_t)b0 << 16; + p[2] = ah | (uint32_t)b1; p[3] = (uint32_t)b1 << 16; + p[4] = ah | (uint32_t)b2; p[5] = (uint32_t)b2 << 16; + p[6] = ah | (uint32_t)b3; p[7] = (uint32_t)b3 << 16; + p[8] = ah | (uint32_t)b4; p[9] = (uint32_t)b4 << 16; + p[10] = ah | (uint32_t)b5; p[11] = (uint32_t)b5 << 16; + p[12] = ah | (uint32_t)b6; p[13] = (uint32_t)b6 << 16; + p[14] = ah | (uint32_t)b7; p[15] = (uint32_t)b7 << 16; + } + } +} + +// Encode source bytes as 4-bit quad-SPI symbols; dest must be zeroed for lane ORing. +template +inline __attribute__((always_inline)) void encode4Step4(uint16_t* dest, size_t srcOffset, size_t srcBytes, uint8_t maxChannel, ParallelChannel* channels) { + for (unsigned lane = 0; lane < maxChannel; lane++) { + const ParallelChannel& c = channels[lane]; + if (!c.active || !c.srcData || srcOffset >= c.srcLen) continue; + + size_t validBytes = c.srcLen - srcOffset; + if (validBytes > srcBytes) validBytes = srcBytes; + + const uint16_t zeroBit = (uint16_t)(ZeroBit << lane); + const uint16_t oneBit = (uint16_t)(OneBit << lane); + const uint8_t* src = c.srcData + srcOffset; + uint16_t* pOut = dest; + + for (size_t i = 0; i < validBytes; i++) { + const uint8_t b = src[i]; + *pOut++ |= (b & 0x80) ? oneBit : zeroBit; + *pOut++ |= (b & 0x40) ? oneBit : zeroBit; + *pOut++ |= (b & 0x20) ? oneBit : zeroBit; + *pOut++ |= (b & 0x10) ? oneBit : zeroBit; + *pOut++ |= (b & 0x08) ? oneBit : zeroBit; + *pOut++ |= (b & 0x04) ? oneBit : zeroBit; + *pOut++ |= (b & 0x02) ? oneBit : zeroBit; + *pOut++ |= (b & 0x01) ? oneBit : zeroBit; + } + } +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp new file mode 100644 index 0000000000..df760e5f1b --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -0,0 +1,511 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel SPI output driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32 C3, other chips may work but are untested +uses 4 parallel outputs and double DMA buffering on SPI2 +Data is output in 4-step cadence meaning each LED bit is encoded into 4 bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +2k per DMA buffer works well, enough for 42 RGB LEDs or roughly 1.2ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" +#ifdef WLEDPB_PARALLELSPI_SUPPORT +#include "WLEDpixelBus_ParallelSpi.h" + +#undef FLAG_ATTR +#define FLAG_ATTR(TYPE) +#include "hal/spi_ll.h" +#include "driver/periph_ctrl.h" +#include "esp_rom_gpio.h" +#include "esp_private/gdma.h" + +namespace WLEDpixelBus { + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) +// low level functions not available in IDF V4 +static inline void IRAM_ATTR spi_ll_apply_config(spi_dev_t *hw) { + hw->cmd.update = 1; + while (hw->cmd.update); //waiting config applied +} + +static inline void IRAM_ATTR spi_ll_user_start(spi_dev_t *hw) { + hw->cmd.usr = 1; +} +#define GPIO_HW &GPIO +#define gpio_ll_set_func_sel(pin, sig) GPIO_HW->func_out_sel_cfg[pin].func_sel = sig +#else +#include "hal/gpio_ll.h" // need low level GPIO register access to disconnect pin inside the ISR +#define GPIO_HW GPIO_LL_GET_HW(0) +#define gpio_ll_set_func_sel(pin, sig) GPIO_HW->func_out_sel_cfg[pin].out_sel = SIG_GPIO_OUT_IDX; +#endif + + + +// Pin assignments for SPI2 quad mode on C3 +// SPI2 signals: FSPID (MOSI/D0), FSPIQ (MISO/D1), FSPIWP (D2), FSPIHD (D3) +static const int WLEDPB_PARALLELSPI_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 WLEDPB_PARALLELSPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high (0000 0000 0000 0001 in binary, output LSB first) +static constexpr uint16_t WLEDPB_PARALLELSPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high (0000 0001 0001 0001 in binary, output LSB first) +static constexpr uint32_t WLEDPB_PARALLELSPI_MIN_CLOCK_HZ = 2000000; +static constexpr uint32_t WLEDPB_PARALLELSPI_MAX_CLOCK_HZ = 5000000; + +// Maximum bits per SPI user transfer: 18-bit length register SPI_MS_DATA_BITLEN=262143, we need 16bits per source bit (4x parallel, 4 steps) or 128bits per byte +static constexpr uint32_t WLEDPB_PARALLELSPI_MAX_BITS = (2047 * 128); // 262016 is the max bits that fit in a single SPI transfer (full encoded bytes), multiple transfers can be chained + +ParallelSpiBusContext* ParallelSpiBusContext::_instance = nullptr; +uint8_t ParallelSpiBusContext::_refCount = 0; + +ParallelSpiBusContext* ParallelSpiBusContext::get(uint8_t /*busNum*/) { + if (_instance == nullptr) { + _instance = new ParallelSpiBusContext(); + } + _refCount++; + return _instance; +} + +void ParallelSpiBusContext::release(uint8_t /*busNum*/) { + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } +} + +ParallelSpiBusContext::ParallelSpiBusContext() + : _peripheralsEnabled(false) + , _activeBuffer(0) + , _gdmaChan(nullptr) + , _dmaChanId(-1) + , _spiIsrHandle(nullptr) + , _hw(&GPSPI2) + , _framePos(0) + , _numBytes(0) + , _bitsLeft(0) + , _resetBits(0) +{ + _isrMux = portMUX_INITIALIZER_UNLOCKED; + for (int i = 0; i < WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = nullptr; + } +} + +ParallelSpiBusContext::~ParallelSpiBusContext() { + deinit(); +} + +bool ParallelSpiBusContext::ensureIdle() { + if (isIdle()) return true; + if (_hw->cmd.usr == 0) { + // A chained segment may briefly clear usr before trans_done restarts it. + const uint32_t waitStart = micros(); + while ((uint32_t)(micros() - waitStart) < 30) { + if (_hw->cmd.usr != 0) return false; + if (isIdle()) return true; + } + abortTransmit(); + return true; + } + if ((uint32_t)(millis() - _txStartMillis) <= WLEDPB_TRANSFER_TIMEOUT_MS) return false; + abortTransmit(); + return true; +} + +// Recovery path for error conditions, cleanly stops DMA, SPI, and disconnects pins to prevent glitches +void ParallelSpiBusContext::abortTransferHw() { + portENTER_CRITICAL(&_isrMux); // make sure no ISR will disturb the sequence + // disconnect pins from SPI and set to idle level + for (int i = 0; i < WLEDPB_PARALLELSPI_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0) { + esp_rom_gpio_connect_out_signal(_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); + gpio_set_level((gpio_num_t)_channels[i].pin, _channels[i].inverted ? 1 : 0); + } + } + if (_peripheralsEnabled) { + _hw->cmd.usr = 0; + _hw->dma_int_ena.val = 0; // disable all SPI interrupts + _hw->dma_int_clr.val = 0xFFFFFFFF; + + // Reset FIFOs after stopping the SPI user transfer. + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + } + + if (_dmaChanId >= 0) { + gdma_dev_t* dma = &GDMA; + dma->intr[_dmaChanId].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, _dmaChanId); + } + + _state = DriverState::Idle; + _stagedMask = 0; + portEXIT_CRITICAL(&_isrMux); +} + +//note: using O2 optimization has little to no effect on FPS +void IRAM_ATTR __attribute__((noinline)) ParallelSpiBusContext::fillBuffer(uint8_t bufIdx) { + uint32_t* buf32 = (uint32_t*)_dmaBuffer[bufIdx]; + const uint32_t* end = buf32 + (_bufferSize >> 2); + while (buf32 < end) *buf32++ = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned. Note: memset is not IRAM safe and may crash + + size_t maxSrcThisChunk = _bufferSize / WLEDPB_PARALLELSPI_DMABYTES; // 16 DMA bytes per source byte + size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; + size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; + + if (srcThisChunk == 0) { + return; // all pixel data has been encoded, nothing to do, just send out zeroed buffer as idle state + } + + encode4Step4( + (uint16_t*)_dmaBuffer[bufIdx], _framePos, srcThisChunk, WLEDPB_PARALLELSPI_MAX_CHANNELS, _channels); + _framePos += srcThisChunk; +} + +// SPI ISR: handles trans_done (normal completion) and outfifo_empty_err +void IRAM_ATTR ParallelSpiBusContext::spiISR(void* arg) { + // Both paths are synchronized with gdmaISR via _isrMux. + ParallelSpiBusContext* ctx = (ParallelSpiBusContext*)arg; + uint32_t status = ctx->_hw->dma_int_st.val; + ctx->_hw->dma_int_clr.val = status; // Clear all flags immediately + + if (status & SPI_DMA_OUTFIFO_EMPTY_ERR_INT_ST) { + if (ctx->_state == DriverState::Idle) return; // state machine finished cleanly, ignore + // SPI FIFO starved (ISR latency too high). The frame is lost, abort and recover immediately + portENTER_CRITICAL_ISR(&ctx->_isrMux); // note: on C3 this is not really needed as GDMA interrupt has the same priority, keep it just in case + // disconnect pins from SPI to prevent garbage output (usr=0 outputs a fast clock) + for (int i = 0; i < WLEDPB_PARALLELSPI_MAX_CHANNELS; i++) { + if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { + gpio_ll_set_func_sel(ctx->_channels[i].pin, SIG_GPIO_OUT_IDX); // disconnect from SPI using direct register write (ISR safe) + // set the pin to static level immediately, note: if implementing this for other ESPs: need to also set the high register for pins >31 + if (ctx->_channels[i].inverted) + GPIO_HW->out_w1ts.out_w1ts = (1 << ctx->_channels[i].pin); // set output high (set) to avoid glitches + else + GPIO_HW->out_w1tc.out_w1tc = (1 << ctx->_channels[i].pin); // set output low (clear) to avoid glitches + } + } + ctx->_hw->cmd.usr = 0; // stop SPI user transfer + ctx->_hw->dma_int_ena.val = 0; // startTransmit() re-arms these + gdma_dev_t* dma = &GDMA; + dma->intr[ctx->_dmaChanId].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, ctx->_dmaChanId); + spi_ll_dma_tx_fifo_reset(ctx->_hw); + spi_ll_outfifo_empty_clr(ctx->_hw); + ctx->_stagedMask = 0; + ctx->_state = DriverState::Idle; // recovered: next show() can send immediately + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return; + } + + if (status & SPI_TRANS_DONE_INT_ST) { + portENTER_CRITICAL_ISR(&ctx->_isrMux); + if (ctx->_state != DriverState::Sending) { + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return; + } + if (ctx->_bitsLeft > 0) { + // Chain the next segment: the circular DMA never stopped, re-arm the SPI transfer + uint32_t bits = (ctx->_bitsLeft > (int32_t)WLEDPB_PARALLELSPI_MAX_BITS) ? WLEDPB_PARALLELSPI_MAX_BITS : (uint32_t)ctx->_bitsLeft; + ctx->_bitsLeft -= (int32_t)bits; + spi_ll_set_mosi_bitlen(ctx->_hw, bits); + spi_ll_apply_config(ctx->_hw); // fast handshake, sub-microsecond + spi_ll_user_start(ctx->_hw); // clock resumes ~1-2us after the last bit + // state stays Sending; encode/DMA are unaffected by segment boundaries + } else { + ctx->_state = DriverState::Idle; // last segment finished (includes the reset tail) + } + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + } +} + +bool IRAM_ATTR ParallelSpiBusContext::gdmaISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data) { + ParallelSpiBusContext* ctx = (ParallelSpiBusContext*)user_data; + gdma_dev_t* dma = &GDMA; + portENTER_CRITICAL_ISR(&ctx->_isrMux); // make sure we are not disturbed filling the buffer to prevent underruns + dma->intr[ctx->_dmaChanId].clr.out_eof = 1; // clear interrupt immediately, harmless if driver already cleared it + + // If we're idle, ignore spurious interrupts + if (ctx->_state == DriverState::Idle) { + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return false; + } + + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer = (completedBuf + 1) % WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT; + ctx->fillBuffer(completedBuf); // fill the completed buffer with next chunk of data (or zeroes for reset) + ctx->_dmaDesc[completedBuf].eof = 1; // Give ownership of the descriptor back to DMA so it can keep feeding SPI TODO: this may be unnecessary + ctx->_dmaDesc[completedBuf].owner = 1; + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return false; // no higher-priority task woken +} + +bool ParallelSpiBusContext::_allocDmaBuffers() { + if (_dmaAllocated) return true; + _bufferSize = calc4StepBufferSize(_maxSrcBytes, WLEDPB_PARALLELSPI_DMABYTES, WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT, MIN_DMA_BUFFER_SIZE, DEFAULT_DMA_BUFFER_SIZE); + if (_bufferSize == 0) return false; + + // Allocate DMA buffers + for (uint8_t i = 0; i < WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT; ++i) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (!_dmaBuffer[i]) { + for (uint8_t j = 0; j < WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT; ++j) { + if (_dmaBuffer[j]) heap_caps_free(_dmaBuffer[j]); + _dmaBuffer[j] = nullptr; + } + _bufferSize = 0; + return false; + } + // Setup DMA descriptors - circular linked list + memset(_dmaBuffer[i], 0, _bufferSize); + _dmaDesc[i] = {}; + _dmaDesc[i].size = _bufferSize; + _dmaDesc[i].length = _bufferSize; + _dmaDesc[i].owner = 1; + _dmaDesc[i].eof = 1; + _dmaDesc[i].buf = _dmaBuffer[i]; + _dmaDesc[i].qe.stqe_next = &_dmaDesc[(i + 1) % WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT]; + } + _dmaAllocated = true; + return true; +} + +bool ParallelSpiBusContext::hwInit() { + // Enable peripheral clocks and force-reset SPI2. + // 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); // GDMA may be shared with other drivers so do not reset. If we call enable, we are also allowed to call disable (calls stack) + _peripheralsEnabled = true; + + // 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: 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 targetFreq = calc4StepClockHz(_timing); + if (targetFreq < WLEDPB_PARALLELSPI_MIN_CLOCK_HZ) targetFreq = WLEDPB_PARALLELSPI_MIN_CLOCK_HZ; + if (targetFreq > WLEDPB_PARALLELSPI_MAX_CLOCK_HZ) targetFreq = WLEDPB_PARALLELSPI_MAX_CLOCK_HZ; + + spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); + + // reset pulse: zeroes appended after the last data bit. 2 DMA bytes per 4-step bit, transfer length counts 8 bits per DMA byte. + _resetBits = (uint32_t)calc4StepResetDmaBytes(_timing, 2) * 8; + + // Route SPI clock to a dummy pin (needed for DMA to work) -> seems to work fine without this (maybe an IDF V5 issue?) + //pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + + // spi_ll_set_mosi_bitlen(_hw, 16384); // dummy init value, not required (set properly when transfer starts) + spi_ll_enable_mosi(_hw, true); + + spi_ll_dma_tx_enable(_hw, true); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + // Configure GDMA + gdma_channel_alloc_config_t allocCfg = {}; + allocCfg.direction = GDMA_CHANNEL_DIRECTION_TX; + esp_err_t err = gdma_new_ahb_channel(&allocCfg, &_gdmaChan); // get a DMA channel + if (err != ESP_OK) { + return false; // no free TX channel -> clean failure instead of silent corruption + } + err = gdma_get_channel_id(_gdmaChan, &_dmaChanId); + if (err != ESP_OK || _dmaChanId < 0 || _dmaChanId >= SOC_GDMA_PAIRS_PER_GROUP_MAX) { + _dmaChanId = -1; + return false; // never use an invalid channel ID to index GDMA registers + } + + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, _dmaChanId); + + err = gdma_connect(_gdmaChan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_SPI, 2)); + if (err != ESP_OK) { return false; } + + gdma_ll_tx_set_desc_addr(dma, _dmaChanId, (uint32_t)&_dmaDesc[0]); + + gdma_tx_event_callbacks_t cbs = {}; + cbs.on_trans_eof = gdmaISR; // signature changes, see below + err = gdma_register_tx_event_callbacks(_gdmaChan, &cbs, this); + if (err != ESP_OK) { + //Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + return false; + } + gdma_ll_tx_reset_channel(dma, _dmaChanId); + gdma_ll_tx_set_desc_addr(dma, _dmaChanId, (uint32_t)&_dmaDesc[0]); + // gdma_ll_tx_start(dma, _dmaChanId); // note: do not start yet, done in startTransmit() + + // Install SPI ISR for trans_done and outfifo_empty_err recovery + _hw->dma_int_clr.val = 0xFFFFFFFF; + _hw->dma_int_ena.trans_done = 1; + _hw->dma_int_ena.outfifo_empty_err = 1; + // _hw->dma_int_ena.val = 0xFFFFFFFF; // REMOVED: Do not enable all interrupts, they trigger false aborts! + err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM, spiISR, this, &_spiIsrHandle); + if (err != ESP_OK) { + //Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); + return false; + } + + return true; +} + +void ParallelSpiBusContext::releaseResources() { + for (int i = 0; i < WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT; i++) { + if (_dmaBuffer[i]) heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } +} + +void ParallelSpiBusContext::hwDeinit() { + if (_gdmaChan) { + gdma_del_channel(_gdmaChan); // also tears down the callback/interrupt it installed + _gdmaChan = nullptr; + } + _dmaChanId = -1; + + if (_spiIsrHandle) { + esp_intr_free(_spiIsrHandle); + _spiIsrHandle = nullptr; + } + + if (_peripheralsEnabled) { + periph_module_disable(PERIPH_SPI2_MODULE); + // do not call disable on the GDMA, we use a legacy driver here, it may interfere with the modern gdma handling + // periph_module_disable(PERIPH_GDMA_MODULE); + _peripheralsEnabled = false; + } +} + +// Route SPI data signal to GPIO +void ParallelSpiBusContext::channelAttached(int8_t idx, int8_t pin, bool inverted) { + pinMode(pin, OUTPUT); + esp_rom_gpio_connect_out_signal(pin, WLEDPB_PARALLELSPI_SIGNAL_INDICES[idx], inverted, false); +} + +void ParallelSpiBusContext::channelDetached(int8_t /*idx*/, int8_t pin) { + if (pin >= 0) { + gpio_reset_pin((gpio_num_t)pin); + } +} + +bool ParallelSpiBusContext::startTransmit() { + if (_state != DriverState::Idle) return false; // must be idle to start a new frame, skip frame + if (_channelCount == 0) return false; + + // Only start transmission once ALL active channels have populated data + if (_stagedMask != _channelMask) return true; // report success while other buses are still being staged + if (!_allocDmaBuffers()) return false; + _stagedMask = 0; + + // Calculate actual data length from staged channels + size_t newBytes = 0; + for (int ch = 0; ch < WLEDPB_PARALLELSPI_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 (in the last segment). + // Note: frames longer than one transfer of WLEDPB_PARALLELSPI_MAX_BITS are chained in the trans_done ISR into multiple SPI user transfers + uint32_t dataBits = _numBytes * WLEDPB_PARALLELSPI_DMABYTES * 8; + uint32_t totalBits = dataBits + _resetBits; + + uint32_t firstBits = (totalBits > WLEDPB_PARALLELSPI_MAX_BITS) ? WLEDPB_PARALLELSPI_MAX_BITS : totalBits; + _bitsLeft = (int32_t)(totalBits - firstBits); // remaining bits are chained by the ISR + + // wait for SPI hardware to finish the last transfer (this is another hardware guard for unexpected race conditions, should never happen) + uint32_t timeout = 10; + while (_hw->cmd.usr && timeout--) delay(1); + if (_hw->cmd.usr) abortTransmit(); // SPI is still busy after timeout. Something went horribly wrong. 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[_dmaChanId].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, _dmaChanId); + + spi_ll_clear_int_stat(_hw); + _hw->dma_int_ena.trans_done = 1; // re-enable interrupts in case they got disabled due to error + _hw->dma_int_ena.outfifo_empty_err = 1; + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_set_mosi_bitlen(_hw, firstBits); + + // Re-initialize DMA descriptors and encode initial buffers + _framePos = 0; + _activeBuffer = 0; + _state = DriverState::Sending; + + for (int i = 0; i < WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT; i++) { + // Restore circular linked list + _dmaDesc[i].qe.stqe_next = &_dmaDesc[(i + 1) % WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT]; + _dmaDesc[i].size = _bufferSize; + _dmaDesc[i].length = _bufferSize; + _dmaDesc[i].owner = 1; + _dmaDesc[i].eof = 1; + fillBuffer(i); + } + + // Phase 4: Brief critical section to start DMA and SPI atomically. + gdma_ll_tx_set_desc_addr(dma, _dmaChanId, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, _dmaChanId); + dma->intr[_dmaChanId].clr.out_eof = 1; + dma->intr[_dmaChanId].ena.out_eof = 1; + + // Re-attach pins to SPI signals + for (int i = 0; i < WLEDPB_PARALLELSPI_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0) { + pinMatrixOutAttach(_channels[i].pin, WLEDPB_PARALLELSPI_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 + _txStartMillis = millis(); + portEXIT_CRITICAL(&_isrMux); + return true; +} + +// ParallelSpiBus implementation +// the bus handling (begin/end/show/canShow) is provided by ParallelBusBase +ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, uint8_t ledType) + : ParallelBusBase(pin, timing, 0) +{ + // encoder is installed by createBus() from BusProperties + _ledType = ledType; +} + +} // namespace WLEDpixelBus +#endif // WLEDPB_PARALLELSPI_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..437860ad15 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -0,0 +1,85 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel SPI output driver implementation + +written by Damian Schneider @dedehai 2026 + +currently only supports ESP32 C3 (other ESPs have I2S, LCD or PARLIO) +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_PARALLELSPI_SUPPORT +#include "WLEDpixelBus_Parallel.h" // shared base classes for the parallel drivers +#include "esp_private/gdma.h" // for gdma_channel_handle_t +namespace WLEDpixelBus { + +#define WLEDPB_PARALLELSPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines +#define WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT 2 // number of DMA buffers, increase to 3 if there are flickering issues +#define WLEDPB_PARALLELSPI_DMABYTES 16 // 16 DMA bytes per source byte in quad-SPI 4-step encoding + + // Parallel-SPI bus context - manages SPI2 quad mode for parallel LED output + // Uses GDMA with circular linked-list and ISR-driven buffer refill +class ParallelSpiBusContext : public ParallelBusContext { +public: + static ParallelSpiBusContext* get(uint8_t busNum = 0); // note: the bus number is ignored + static void release(uint8_t busNum = 0); + + bool startTransmit(); + bool ensureIdle() override; // non-blocking completion check with timeout recovery + +private: + ParallelSpiBusContext(); + ~ParallelSpiBusContext(); + void IRAM_ATTR fillBuffer(uint8_t bufIdx); + bool _allocDmaBuffers(); + static bool IRAM_ATTR gdmaISR(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); + static void IRAM_ATTR spiISR(void* arg); + + bool hwInit() override; + void hwDeinit() override; + void releaseResources() override; + void abortTransferHw() override; // emergency stop, disconnects pins, resets hardware + void channelAttached(int8_t idx, int8_t pin, bool inverted) override; + void channelDetached(int8_t idx, int8_t pin) override; + + bool _peripheralsEnabled; + volatile uint8_t _activeBuffer; // buffer currently being sent by DMA (like I2S _activeBuffer) + // DMA + uint8_t* _dmaBuffer[WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT]; + lldesc_t _dmaDesc[WLEDPB_PARALLELSPI_DMA_BUFFER_COUNT]; + gdma_channel_handle_t _gdmaChan; + int _dmaChanId; + intr_handle_t _spiIsrHandle; + portMUX_TYPE _isrMux; + spi_dev_t* _hw; // SPI device + volatile size_t _framePos; // current source byte position + volatile size_t _numBytes; // total source bytes to send + volatile int32_t _bitsLeft; // bits still to send in chained segments (0 = last segment) + uint32_t _resetBits; // reset pulse length in transfer bits, derived from LED timing + + static ParallelSpiBusContext* _instance; + static uint8_t _refCount; +}; + + + // SPI parallel output bus +class ParallelSpiBus : public ParallelBusBase { +public: + ParallelSpiBus(int8_t pin, const LedTiming& timing, uint8_t ledType = 0); +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "PARALLEL_SPI"; } +#endif +}; + +} // namespace WLEDpixelBus + +#endif // WLEDPB_PARALLELSPI_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp new file mode 100644 index 0000000000..3afaf240e6 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -0,0 +1,237 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - RMT output driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +RMT bus works on ESP32, S3, S2, C3, C5, C6 +Supports auto-distribution of available RMT memory blocks to reduce interrupt frequency - needs to be refined if ever using RMT input. +All supported targets use the glitch-free high priority interrupt driver by @willmmiles (WLEDPB_USE_RMTHI) +Other targets should only use a single RMT output to avoid glitches due to interrupt latency +Note: S3 would support GDMA for RMT but all others variants do not, it is not implemented + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" +#ifdef ARDUINO_ARCH_ESP32 +#include "WLEDpixelBus_RMT.h" + +#ifdef WLEDPB_USE_RMTHI +#include "esp_rom_gpio.h" // for gpio routing to set inverted signal +#include "soc/rmt_struct.h" +#include "soc/gpio_sig_map.h" // RMT_SIG_OUT0_IDX +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) +#include "hal/rmt_ll.h" +#include "esp_private/gpio.h" +#include "esp_private/esp_clk_tree_common.h" +// these macro wrappers require the RCC critical-section, which is not usable from Arduino builds. We call the functions directly (same name) +#undef rmt_ll_enable_bus_clock // macros are defined for some ESP32 variants only, if not defined, this is a no-op +#undef rmt_ll_reset_register +#endif +#endif + +namespace WLEDpixelBus { + +// Static auto-channel counter for RmtBus +uint8_t RmtBus::_expectedChannels = 1; +uint8_t RmtBus::_allocatedCount = 0; +uint8_t RmtBus::_currentChannelIndex = 0; +uint8_t RmtBus::_usedBlocks = 0; + +RmtBus::RmtBus(int8_t pin, const LedTiming& timing, uint8_t ledType) + : _pin(pin) + , _channel(-1) + , _initialized(false) + , _driverInstalled(false) + , _timing(timing) +{ + // encoder is installed by createBus() from BusProperties + _ledType = ledType; +} + +RmtBus::~RmtBus() { + end(); +} + +#ifdef WLEDPB_USE_RMTHI + +bool RmtBus::begin() { + if (_initialized) return true; + uint8_t blocksToUse = 1; + uint8_t maxTxChannels = WLED_MAX_RMT_CHANNELS; + + // 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) + // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks + // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 block, and CH3 with 5 blocks + if (_allocatedCount >= _expectedChannels || _allocatedCount >= maxTxChannels) + return false; + + // total RMT memory blocks per group equals the total channel count (each channel owns one block, + // TX and RX channels share the same memory pool) + const uint8_t totalBlocks = SOC_RMT_CHANNELS_PER_GROUP; + + int left_channels = _expectedChannels - _allocatedCount - 1; + + if (left_channels == 0) { + _channel = _currentChannelIndex; + blocksToUse = totalBlocks - _usedBlocks; + } else { + int k = totalBlocks / _expectedChannels; + int max_k_for_index = maxTxChannels - _currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; + + _channel = _currentChannelIndex; + blocksToUse = k; + } + #ifdef RMT_USE_SINGLE_MEM_BLOCK + blocksToUse = 1; + #endif + + _currentChannelIndex += blocksToUse; + _usedBlocks += blocksToUse; + _allocatedCount++; + + if (_channel >= (int8_t)maxTxChannels) { + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d >= max %u, FAIL\n"), _channel, maxTxChannels); + return false; + } + + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n"), _channel, blocksToUse, _allocatedCount, maxTxChannels); + // RMT clock: 40MHz -> 25ns per tick + const float tickNs = 25.0f; + + auto nsToTicks = [tickNs](uint16_t ns) -> uint16_t { + uint16_t ticks = (uint16_t)((ns + tickNs / 2) / tickNs); + return ticks > 0 ? ticks : 1; + }; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) + rmt_symbol_word_t bit0 = {}, bit1 = {}; +#else + rmt_item32_t bit0 = {}, bit1 = {}; // legacy type, same 32-bit layout +#endif + 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); + + esp_err_t err; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) + // The IDF RMT driver is bypassed entirely; configure the peripheral via rmt_ll register access. + if (_allocatedCount == 1) { + // first bus: enable and reset the peripheral (register reset is global to all channels) + rmt_ll_enable_bus_clock(0, true); + rmt_ll_reset_register(0); + rmt_ll_mem_power_by_pmu(&RMT); + rmt_ll_enable_mem_access_nonfifo(&RMT, true); + #if SOC_RMT_SUPPORT_TX_SYNCHRO + rmt_ll_tx_clear_sync_group(&RMT); // we do not use sync groups, just in case. + #endif + esp_clk_tree_enable_src((soc_module_clk_t)RMT_CLK_SRC_DEFAULT, true); // required on C5, works without this on all other ESPs + } + + // The group clock source is shared by all channels. All buses use + // the same 80 MHz source and 40 MHz channel resolution. + rmt_ll_set_group_clock_src(&RMT, _channel, RMT_CLK_SRC_DEFAULT, 1, 1, 0); + rmt_ll_enable_group_clock(&RMT, true); + rmt_ll_tx_set_channel_clock_div(&RMT, _channel, 2); + rmt_ll_tx_reset_channels_clock_div(&RMT, 1 << _channel); + + #if SOC_RMT_TX_CANDIDATES_PER_GROUP < SOC_RMT_CHANNELS_PER_GROUP + // claim the memory blocks of the RX channels for TX use (layout is TX then RX, we only need to claim the RX ones) + for (int i = SOC_RMT_TX_CANDIDATES_PER_GROUP; i < SOC_RMT_CHANNELS_PER_GROUP; i++) { + rmt_ll_rx_set_mem_owner(&RMT, i, RMT_LL_MEM_OWNER_SW); + } + #endif +#else + // legacy driver performs the initial clock/line setup (IDF V4), RmtHiDriver takes over from there + rmt_config_t config = {}; + + config.rmt_mode = RMT_MODE_TX; + config.channel = (rmt_channel_t)_channel; + 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; + + err = rmt_config(&config); + if (err != ESP_OK) { + return false; + } + + // Register hack for memory blocks normally assigned to RX (S3 / C3): set owner to TX + // TODO: need this for ESP32 as well? +#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 +#endif + + const uint32_t resetUs = (_timing.reset_us > 0) ? _timing.reset_us : 300; + err = RmtHiDriver::Install(_channel, bit0.val, bit1.val, (resetUs * 1000 + 12) / 25, blocksToUse); + if (err != ESP_OK) { + return false; + } + _driverInstalled = true; + + // route the pin, hardware signal inversion via GPIO matrix if _inverted (must be set before begin()) + gpio_func_sel((gpio_num_t)_pin, PIN_FUNC_GPIO); + esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + _channel, _inverted, false); + + _initialized = true; + if (!allocateEncodeBuffer(_numPixels, _encoder.getBytesPerPixel())) { end(); return false; } + return true; +} + +void RmtBus::end() { + if (_driverInstalled) { + RmtHiDriver::Uninstall(_channel); + _driverInstalled = false; + } + if (_initialized && _pin >= 0) gpio_reset_pin((gpio_num_t)_pin); + releaseEncodeBuffer(); + _initialized = false; + _channel = -1; +} + +bool RmtBus::show() { + // Encoding is done per-pixel in setPixelColor(); _encodeBuffer is ready to ship. + // the high priority driver waits for tx done internally and sends the reset/latch gap in-band + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + return RmtHiDriver::Write(_channel, _encodeBuffer, _encodeBufferSize) == ESP_OK; +} + +bool RmtBus::canShow() const { + if (!_initialized) return true; // do not block if init failed! busmanager waits until all buses can show + return (ESP_OK == RmtHiDriver::WaitForTxDone(_channel, 0)); // 0 timeout means "poll and return immediately" +} + +#else // !WLEDPB_USE_RMTHI + +// Stub for targets without RmtHiDriver support (P4 uses a newer RMT IP, C61 has no RMT): +// RMT bus is unavailable, begin() fails so a different output driver can be selected. +bool RmtBus::begin() { return false; } +void RmtBus::end() {} +bool RmtBus::show() { return false; } +bool RmtBus::canShow() const { return true; } + +#endif // WLEDPB_USE_RMTHI + +} // namespace WLEDpixelBus +#endif // ARDUINO_ARCH_ESP32 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h new file mode 100644 index 0000000000..581b06104d --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - RMT output driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#include "RmtHIDriver.h" // high interrupt priority driver (defines WLEDPB_USE_RMTHI on supported targets) + +namespace WLEDpixelBus { + +// RMT Bus: uses ESP32 RMT peripheral for LED timing + +class RmtBus : public PixelBus { +public: + RmtBus(int8_t pin, const LedTiming& timing, uint8_t ledType = 0); + ~RmtBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return "RMT"; } +#endif + + static void setExpectedChannelCount(uint8_t expected) { _expectedChannels = (expected > 0) ? expected : 1; } + static void resetChannelTracking() { + _allocatedCount = 0; + _currentChannelIndex = 0; + _usedBlocks = 0; + } + +private: + int8_t _pin; + int8_t _channel; + bool _initialized; + bool _driverInstalled; + LedTiming _timing; + + // _encodeBuffer and _encodeBufferSize are in PixelBus base + static uint8_t _expectedChannels; // TODO: make none static? would save a few bytes of ram but use more heap + static uint8_t _allocatedCount; + static uint8_t _currentChannelIndex; + static uint8_t _usedBlocks; +}; + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp new file mode 100644 index 0000000000..3675f470d4 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -0,0 +1,283 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32, S3, S2 and C3 both hardware SPI and BitBanged output +supports hardware brightness on APA102 for improved color resolution +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus_SPI.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include "soc/gpio_reg.h" +#endif + +#define SPI_MAX_CLOCK_HZ 20000000UL // maximum SPI clock supported by all target platforms (ESP8266 max is 20 MHz, ESP32 can do 80 MHz but we clamp to 20 MHz for compatibility with ESP8266 and timing accuracy) +static constexpr uint32_t WS2801_LATCH_TIME_US = 500; + +namespace WLEDpixelBus { + +// Derive SPI clock Hz from kHz timing set through busconfig (see busmanager) +static uint32_t kHzToClockHz(uint16_t frequencykHz) { + uint32_t frequencyHz = frequencykHz*1000; + if (frequencyHz < 1000000) frequencyHz = 1000000; // minimum 1 MHz + if (frequencyHz > SPI_MAX_CLOCK_HZ) frequencyHz = SPI_MAX_CLOCK_HZ; + return frequencyHz; +} + +SpiBus::SpiBus(int8_t dataPin, int8_t clockPin, uint16_t frequencykHz, bool useHardware, uint8_t ledType) + : _dataPin(dataPin) + , _clockPin(clockPin) + , _useHardware(useHardware) + , _initialized(false) + , _clockHz(kHzToClockHz(frequencykHz)) +{ + _ledType = ledType; +} + +SpiBus::~SpiBus() { + end(); +} + +bool SpiBus::begin() { + if (_initialized) return true; + + if (_useHardware) { + // On ESP32 SPI.begin(sck, miso, mosi, ss) must be called with the actual pins so the IO matrix routes the SPI peripheral to the right GPIOs. + // On ESP8266 the hardware SPI uses fixed pins (MOSI=GPIO13, SCK=GPIO14) so no pin args are needed. +#if defined(ARDUINO_ARCH_ESP32) + SPI.begin(_clockPin, -1, _dataPin, -1); // note: the Arduino core interprets -1 as "no pin" (in older versions it meant "default pin" this bug was fixed) +#else + SPI.begin(); +#endif + // Frequency and mode are applied per-frame via beginTransaction(); do NOT call the deprecated SPI.setFrequency / SPI.setDataMode here + } else { + // bit banged output + pinMode(_dataPin, OUTPUT); + pinMode(_clockPin, OUTPUT); + // Pre-compute bitmasks so the hot-path bit-bang loop does register writes not the ~10x-slower digitalWrite(). +#if defined(ARDUINO_ARCH_ESP32) + _dataHigh = (_dataPin >= 32); + _clkHigh = (_clockPin >= 32); + _dataMask = 1UL << (_dataHigh ? (_dataPin - 32) : _dataPin); + _clkMask = 1UL << (_clkHigh ? (_clockPin - 32) : _clockPin); + // Drive both lines LOW initially +#ifdef ESP_HAS_HIGH_GPIO_BANK + if (_dataHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _dataMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); + if (_clkHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _clkMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); +#else + REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); + REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); +#endif +#elif defined(ARDUINO_ARCH_ESP8266) + _dataMask = 1UL << _dataPin; + _clkMask = 1UL << _clockPin; + GPOC = _dataMask; + GPOC = _clkMask; +#endif + } + + _initialized = true; + uint8_t allocBytes = _encoder.getBytesPerPixel(); + if (_ledType == TYPE_APA102 || _ledType == TYPE_P9813) allocBytes++; // need more space for per-pixel header byte, see show() + if (!allocateEncodeBuffer(_numPixels, allocBytes)) { end(); return false; } + return true; +} + +void SpiBus::end() { + if (!_initialized) return; + if (_useHardware) { + SPI.end(); + } + #if defined(ARDUINO_ARCH_ESP32) + if (_dataPin >= 0) gpio_reset_pin((gpio_num_t)_dataPin); + if (_clockPin >= 0) gpio_reset_pin((gpio_num_t)_clockPin); + #else + pinMode(_dataPin, INPUT); + pinMode(_clockPin, INPUT); + #endif + releaseEncodeBuffer(); + _initialized = false; +} + +// Fast inline helpers for bit-bang GPIO register access. +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); // set + } else { + if (_dataHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _dataMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); // clear + } +#else + if (high) { + REG_WRITE(GPIO_OUT_W1TS_REG, _dataMask); // set + } else { + REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); // clear + } +#endif +#elif defined(ARDUINO_ARCH_ESP8266) + if (high) GPOS = _dataMask; // set + else GPOC = _dataMask; // clear +#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); // set + } else { + if (_clkHigh) REG_WRITE(GPIO_OUT1_W1TC_REG, _clkMask); else REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); // clear + } +#else + if (high) { + REG_WRITE(GPIO_OUT_W1TS_REG, _clkMask); // set + } else { + REG_WRITE(GPIO_OUT_W1TC_REG, _clkMask); // clear + } +#endif +#elif defined(ARDUINO_ARCH_ESP8266) + if (high) GPOS = _clkMask; // set + else GPOC = _clkMask; // clear +#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) { + uint32_t n = 0; // default: no start frame (WS2801) + if (_ledType == TYPE_LPD8806) { + n = (numPixels + 31) / 32; // LPD8806: start frame is ceil(N/32) zero bytes according to NPB, there is no datasheet info + } else if (_ledType != TYPE_WS2801) { + n = 4; // APA102 / LPD6803 / P9813: fixed 4-byte zero start frame + } + for (uint32_t i = 0; i < n; i++) sendByte(0x00); +} + +void SpiBus::sendEndFrame(uint16_t numPixels) { + uint8_t val = 0; // default end frame byte + uint32_t n = 0; // default: no end frame (WS2801: nothing to send, clk=low for 500us is the latch trigger) + if (_ledType == TYPE_APA102) { + n = 1 + numPixels / 16; // APA102/SK9822: datasheet states 4 0xFF bytes, pololu.com states it is unreliable and better send at least (numPixels-1)/2 zero bits + } else if (_ledType == TYPE_LPD6803) { + n = (numPixels + 7) / 8; // LPD6803: one clock per pixel required, data low + } else if (_ledType == TYPE_LPD8806) { + n = (numPixels + 31) / 32; // LPD8806: stop frame is ceil(N/32) 0xFF bytes according to NPB, there is no datasheet info + val = 0xFF; + } else if (_ledType == TYPE_P9813) { + n = 4; // P9813: fixed 4 zero bytes. + } + for (uint32_t i = 0; i < n; i++) sendByte(val); +} + +bool SpiBus::show() { + if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; + + if (_ledType == TYPE_WS2801) { + while ((uint32_t)(micros() - _lastWs2801FrameEndUs) < WS2801_LATCH_TIME_US) yield(); + } + + const uint8_t pixelBytes = _encoder.getBytesPerPixel(); + + if (_useHardware) { + SPI.beginTransaction(SPISettings(_clockHz, MSBFIRST, SPI_MODE0)); // beginTransaction applies frequency, bit order, and SPI mode + } + + if (_ledType == TYPE_APA102 || _ledType == TYPE_P9813) { + // These chips need a per-pixel header byte (brightness/flag) in front of the RGB data + // Expand the compact encoded buffer in-place, starting from the last pixel so no data is overwritten + // Native wire formats: APA102: [0xE0|bri, B, G, R], P9813: [flag, B, G, R] + // note: this "extension" is intentionally not done when encoding the buffer so we can keep the encoding branch-free and fast for all types. + const uint8_t wireBytes = pixelBytes + 1; + for (uint32_t i = _numPixels; i > 0; i--) { + uint8_t* src = _encodeBuffer + (size_t)(i - 1) * pixelBytes; + uint8_t* dst = _encodeBuffer + (size_t)(i - 1) * wireBytes; + for (int8_t b = pixelBytes - 1; b >= 0; b--) dst[b + 1] = src[b]; + if (_ledType == TYPE_APA102) { + dst[0] = 0xE0 | _apa102HwBri; + } else { // TYPE_P9813 + const uint8_t b = dst[1], g = dst[2], r = dst[3]; + dst[0] = 0xC0 | ((~b & 0xC0) >> 2) | ((~g & 0xC0) >> 4) | ((~r & 0xC0) >> 6); // flag byte is [11 B' G' R'], where B', G', R' are the inverted top 2 bits of each color channel + } + } + sendStartFrame(_numPixels); + uint8_t* src = _encodeBuffer; + if (_useHardware) { + SPI.writeBytes(src, (size_t)_numPixels * wireBytes); + } else { + const size_t total = (size_t)_numPixels * wireBytes; + for (size_t i = 0; i < total; i++) sendByte(src[i]); + } + // Always restore the compact layout: readback, clear, scaling, and the next + // frame must never depend on whether the global pixel buffer is enabled. + src = _encodeBuffer + 1; + for (uint32_t i = 0; i < _numPixels; ++i) { + uint8_t* dst = _encodeBuffer + (size_t)i * pixelBytes; + for (uint8_t b = 0; b < pixelBytes; b++) dst[b] = src[b]; + src += wireBytes; + } + } else { + // WS2801 uses the compact encoded stream directly + // LPD6803 and LPD8806 require protocol-specific payload conversion in addition to their frames + // Note: custom color channels are not supported for clocked LED strips, only color ordering + sendStartFrame(_numPixels); + const uint8_t* src = _encodeBuffer; + if (_ledType != TYPE_LPD6803 && _ledType != TYPE_LPD8806) { + if (_useHardware) { + SPI.writeBytes(src, (size_t)pixelBytes * _numPixels); + } else { + for (size_t i = 0; i < (size_t)pixelBytes * _numPixels; ++i) sendByte(src[i]); + } + } else { + for (uint16_t i = 0; i < _numPixels; ++i, src += pixelBytes) { + uint8_t wire[3]; + size_t wireLen; + if (_ledType == TYPE_LPD6803) { + // 16bit per pixel: RGB555, high byte first, highest bit is always set + const uint16_t color555 = 0x8000u | ((uint16_t)(src[0] & 0xF8) << 7) | ((uint16_t)(src[1] & 0xF8) << 2) | ((uint16_t)(src[2] & 0xF8) >> 3); + wire[0] = color555 >> 8; + wire[1] = color555 & 0xFF; + wireLen = 2; + } else { + // each chip has 6 channels, 7-bit colors, highest bit is always set. We send 3 bytes at a time. TODO: this may need refinement if there are strips with non-RGB encoding + wire[0] = (src[0] >> 1) | 0x80; + wire[1] = (src[1] >> 1) | 0x80; + wire[2] = (src[2] >> 1) | 0x80; + wireLen = 3; + } + if (_useHardware) SPI.writeBytes(wire, wireLen); + else for (size_t b = 0; b < wireLen; ++b) sendByte(wire[b]); + } + } + } + + sendEndFrame(_numPixels); + + if (_useHardware) { + SPI.endTransaction(); + } + + _lastWs2801FrameEndUs = micros(); // record end of frame time + + return true; +} + +bool SpiBus::canShow() const { + return true; // show() is synchronous so we are always ready to call it again +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h new file mode 100644 index 0000000000..edfe023663 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -0,0 +1,60 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + +#pragma once + +#include "WLEDpixelBus.h" +#include + +namespace WLEDpixelBus { + +// SPI Bus: Hardware and Software SPI for 2-wire LEDs like APA102, WS2801 +class SpiBus : public PixelBus { +public: + SpiBus(int8_t dataPin, int8_t clockPin, uint16_t frequencykHz, bool useHardware = true, uint8_t ledType = 0); + ~SpiBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; +#ifdef WLED_DEBUG_BUS + const char* getTypeStr() const override { return _useHardware ? "SPI_HW" : "SPI_SW"; } +#endif + void setApa102HwBri(uint8_t v) override { _apa102HwBri = v & 0x1F; } // Set the APA102 per-pixel 5-bit hardware brightness step (0–31). + +private: + uint32_t _clockHz; // SPI clock in Hz, derived from frequencykHz by the constructor + uint32_t _lastWs2801FrameEndUs = 0; + uint8_t _apa102HwBri = 31; // APA102 5-bit hardware brightness step (0–31); 31 = max (default) + int8_t _dataPin; + int8_t _clockPin; + bool _useHardware; + bool _initialized; + + // Pre-computed GPIO bitmasks for fast bit-bang output (set in begin()) +#if defined(ARDUINO_ARCH_ESP32) + bool _dataHigh; // true when data pin >= 32 (use GPIO1 register) TODO: this is only for ESP32's with a high data bank (ESP32, S3, maybe C6,C5) + bool _clkHigh; // true when clock pin >= 32 (use GPIO1 register) + uint32_t _dataMask; // GPIO set/clear mask for data pin + uint32_t _clkMask; // GPIO set/clear mask for clock pin +#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..7192ca94dd --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -0,0 +1,87 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 +-------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include "../../const.h" + +namespace WLEDpixelBus { + +// Note on timings regarding I2S/LCD/SPI buses (4 step cadence): +// the drivers will always use a 1/4 - 3/4 duty cycle for '0' and '1' bits (e.g. 250ns high + 750ns low for a 1us period) +// since the t0h timing is the most critical (according to my testing) make it accurate and derive the rest + +// LED timing +struct LedTiming { + uint16_t t0h_ns; // '0' bit high time + uint16_t t0l_ns; // '0' bit low time + uint16_t t1h_ns; // '1' bit high time + uint16_t t1l_ns; // '1' bit low time + uint32_t reset_us; // Reset/latch time in microseconds + + constexpr LedTiming(uint16_t t0h, uint16_t t0l, uint16_t t1h, uint16_t t1l, uint32_t reset) + : t0h_ns(t0h), t0l_ns(t0l), t1h_ns(t1h), t1l_ns(t1l), reset_us(reset) {} + + // Calculate bit period in ns: used in hw-parallel buses only. since t0h is most critical and we do 4-step encoding, make the period 4*t0h + // we do not have the flexibility of RMT or BB outputs: t1h is fixed at t0h*3 + constexpr uint32_t bitPeriod() const { + return t0h_ns * 4; + } +}; + +static const PROGMEM LedTiming _ledTimings[] = { +// t0h t0l t1h t1l reset_us + { 300, 900, 700, 500, 300 }, // WS281x + { 800, 1700, 1600, 900, 300 }, // Generic 400Kbps + { 300, 900, 800, 400, 200 }, // TM1829 - output inverted! + { 400, 850, 800, 450, 300 }, // UCS8903 / UCS8904 (16-bit) + { 350, 1350, 1350, 350, 50 }, // APA106 / PL9823 + { 360, 890, 720, 530, 250 }, // TM1814 / TM1914 - output inverted! + { 740, 1780, 1440, 1060, 200 }, // TM1815 - output inverted! + { 300, 900, 700, 500, 200 }, // SK6812 / SK6812 RGBW + { 400, 850, 800, 450, 300 }, // FW1906 GRBCW + { 300, 790, 790, 300, 300 }, // WS2805 RGBCW + { 300, 900, 900, 300, 250 }, // SM16825 (16-bit) +}; + +static inline uint8_t getTimingIndex(uint8_t wledType) { + switch (wledType) { + case TYPE_WS2812_RGB: + case TYPE_WS2812_1CH_X3: + case TYPE_WS2812_1CH: + case TYPE_WS2812_WWA: return 0; // migration: WWA kept for timing selection only + case TYPE_WS2811_400KHZ: return 1; + case TYPE_TM1829: return 2; + case TYPE_UCS8903: + case TYPE_UCS8904: return 3; // identical timing + case TYPE_APA106: return 4; + case TYPE_TM1914: + case TYPE_TM1814: return 5; // identical timing + case TYPE_TM1815: return 6; + case TYPE_SK6812_RGBW: return 7; + case TYPE_FW1906: return 8; + case TYPE_WS2805: return 9; + case TYPE_SM16825: return 10; + default: return 0; // WS281x fallback: SPI buses ignore LED bit timing; use WS281x as metadata stub + } +} + +// Scale LED timings by a factor (1=100%), pulse 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); +} + +LedTiming getLedTiming(uint8_t wledType); + +} // namespace WLEDpixelBus diff --git a/wled00/udp.cpp b/wled00/udp.cpp index ac5554df40..1bb4d68fc9 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -421,7 +421,7 @@ void realtimeLock(uint32_t timeoutMs, byte md) strip.fill(BLACK); } // if strip is off (bri==0) and not already in RTM - if (briT == 0) { + if (briT16 == 0) { strip.setBrightness(briLast, true); } } @@ -430,10 +430,15 @@ 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); - if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show(); + if (briT16 > 0 && md == REALTIME_MODE_GENERIC) strip.show(); } void exitRealtime() { @@ -442,6 +447,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/wled.cpp b/wled00/wled.cpp index a21d3cd036..d08167164b 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -43,7 +43,7 @@ WLED::WLED() // turns all LEDs off and restarts ESP void WLED::reset() { - briT = 0; + briT16 = 0; #ifdef WLED_ENABLE_WEBSOCKETS ws.closeAll(1012); #endif @@ -243,7 +243,7 @@ void WLED::loop() strip.finalizeInit(); // will create buses and also load default ledmap if present if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); - BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824 + BusManager::setBrightness16(scaledBri16(bri<<8)); // fix re-initialised bus' brightness #4005 and #4824 configNeedsWrite = true; } if (loadLedmap >= 0) { diff --git a/wled00/wled.h b/wled00/wled.h index 149143cf5c..2547f0a789 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -617,8 +617,8 @@ WLED_GLOBAL byte colNlT[] _INIT_N(({ 0, 0, 0, 0 })); // current nightligh WLED_GLOBAL bool offMode _INIT(!turnOnAtBoot); WLED_GLOBAL byte briS _INIT(128); // default brightness WLED_GLOBAL byte bri _INIT(briS); // global brightness (set) -WLED_GLOBAL byte briOld _INIT(0); // global brightness while in transition loop (previous iteration) -WLED_GLOBAL byte briT _INIT(0); // global brightness during transition +WLED_GLOBAL uint16_t briOld16 _INIT(0); // Q8.8 global brightness while in transition loop (previous iteration) +WLED_GLOBAL uint16_t briT16 _INIT(0); // Q8.8 global brightness during transition WLED_GLOBAL byte briLast _INIT(128); // brightness before turned off. Used for toggle function // whiteLast is private to ir.cpp - see there. diff --git a/wled00/xml.cpp b/wled00/xml.cpp index e778d96a61..76a133eaca 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -11,7 +11,7 @@ static void appendGPIOinfo(Print& settingsScript); //build XML response to HTTP /win API request void XML_response(Print& dest) { - dest.printf_P(PSTR("%d"), (nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); + dest.printf_P(PSTR("%d"), (nightlightActive && nightlightMode > NL_MODE_SET) ? (briT16 >> 8) : bri); for (int i = 0; i < 3; i++) { dest.printf_P(PSTR("%d"), colPri[i]); @@ -340,7 +340,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str()); // set limits - settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);"), + settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);"), WLED_PLATFORM_ID, // TODO: replace with a info json lookup MAX_LEDS_PER_BUS, MAX_LED_MEMORY, @@ -348,9 +348,11 @@ void getSettingsJS(byte subPage, Print& settingsScript) WLED_MAX_COLOR_ORDER_MAPPINGS, WLED_MAX_DIGITAL_CHANNELS, WLED_MAX_RMT_CHANNELS, - WLED_MAX_I2S_CHANNELS, + WLED_MAX_PARHW_CHANNELS, + WLED_MAX_BB_CHANNELS, WLED_MAX_ANALOG_CHANNELS, - WLED_MAX_BUTTONS + WLED_MAX_BUTTONS, + WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE ); printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments); @@ -378,6 +380,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //swap channels char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed + char sf[4] = "SF"; sf[2] = offset+s; sf[3] = 0; //bus speed factor (percent) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) @@ -419,9 +422,38 @@ void getSettingsJS(byte subPage, Print& settingsScript) } } printSetFormValue(settingsScript,sp,speed); + printSetFormValue(settingsScript,sf,bus->getBusSpeedFactor()); printSetFormValue(settingsScript,la,bus->getLEDCurrent()); printSetFormValue(settingsScript,ma,bus->getMaxCurrent()); printSetFormValue(settingsScript,hs,bus->getCustomText().c_str()); + // Custom bus: send per-channel config fields (active for any digital type when customized) + if (bus->getChannelLayout().custom) { + const BusChannelLayout& cb = bus->getChannelLayout(); + char cben[7] = "CBen"; cben[4] = offset+s; cben[5] = 0; // customize enabled + char cbch[7] = "CBch"; cbch[4] = offset+s; cbch[5] = 0; // channel count + char cbio[7] = "CBio"; cbio[4] = offset+s; cbio[5] = 0; // invert output + char cbb[6] = "CBb"; cbb[3] = offset+s; cbb[4] = 0; // is16bit + char cbt0h[7] = "CBt0h"; cbt0h[5] = offset+s; cbt0h[6] = 0; + char cbt0l[7] = "CBt0l"; cbt0l[5] = offset+s; cbt0l[6] = 0; + char cbt1h[7] = "CBt1h"; cbt1h[5] = offset+s; cbt1h[6] = 0; + char cbt1l[7] = "CBt1l"; cbt1l[5] = offset+s; cbt1l[6] = 0; + char cbrst[7] = "CBrst"; cbrst[5] = offset+s; cbrst[6] = 0; + printSetFormCheckbox(settingsScript, cben, true); + printSetFormValue(settingsScript, cbch, cb.numChannels); + printSetFormCheckbox(settingsScript, cbio, cb.invertOutput); + printSetFormCheckbox(settingsScript, cbb, cb.is16bit); + printSetFormValue(settingsScript, cbt0h, cb.t0h); + printSetFormValue(settingsScript, cbt0l, cb.t0l); + printSetFormValue(settingsScript, cbt1h, cb.t1h); + printSetFormValue(settingsScript, cbt1l, cb.t1l); + printSetFormValue(settingsScript, cbrst, cb.trst); + for (uint8_t ci = 0; ci < 6; ci++) { + char cbc[7] = "CBc"; cbc[3] = '0'+ci; cbc[4] = offset+s; cbc[5] = 0; // channel color + char cbi[7] = "CBi"; cbi[3] = '0'+ci; cbi[4] = offset+s; cbi[5] = 0; // invert bit + printSetFormValue(settingsScript, cbc, cb.channelMap[ci]); + printSetFormCheckbox(settingsScript, cbi, (cb.invertMask >> ci) & 1); + } + } sumMa += bus->getMaxCurrent(); } printSetFormValue(settingsScript,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa);